From 99043c8d92a3e546b18838bda12f4f309d7461f1 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 16 May 2025 16:47:42 +0530 Subject: [PATCH 001/190] feat(BarChartV2): Implement new BarChartV2 component This change introduces a new BarChartV2 component in the react-ui package. The new component provides an enhanced version of the existing BarChart component with additional features and customization options. The key changes include: - Implement a new BarChartV2 component with support for grouped and stacked bar charts - Add support for custom themes, legends, labels, and grid lines - Implement dynamic axis label formatting based on the chart data length - Provide options to control the animation and visibility of the Y-axis These changes aim to improve the flexibility and usability of the BarChart component, allowing developers to create more visually appealing and customizable bar charts in their applications. --- .../Charts/BarChartV2/BarChartV2.tsx | 226 ++++++++++ .../src/components/Charts/BarChartV2/index.ts | 1 + .../BarChartV2/stories/barChartV2.stories.tsx | 402 ++++++++++++++++++ 3 files changed, 629 insertions(+) create mode 100644 js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx create mode 100644 js/packages/react-ui/src/components/Charts/BarChartV2/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx new file mode 100644 index 000000000..46d64ccfe --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx @@ -0,0 +1,226 @@ +import React from "react"; +import { Bar, LabelList, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; +import { useLayoutContext } from "../../../context/LayoutContext"; +import { + ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, + keyTransform, +} from "../Charts"; +import { cartesianGrid } from "../cartesianGrid"; +import { getDistributedColors, getPalette } from "../utils/PalletUtils"; + +export type BarChartData = Array>; + +export interface BarChartProps { + data: T; + categoryKey: keyof T[number]; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + variant?: "grouped" | "stacked"; + grid?: boolean; + label?: boolean; + legend?: boolean; + radius?: number; + icons?: Partial>; + isAnimationActive?: boolean; + showYAxis?: boolean; + xAxisLabel?: React.ReactNode; + yAxisLabel?: React.ReactNode; +} + +export const BarChartV2 = ({ + data, + categoryKey, + theme = "ocean", + variant = "grouped", + grid = true, + label = true, + legend = true, + icons = {}, + radius = 4, + isAnimationActive = true, + showYAxis = false, + xAxisLabel, + yAxisLabel, +}: BarChartProps) => { + // excluding the categoryKey + const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + + const palette = getPalette(theme); + const colors = getDistributedColors(palette, dataKeys.length); + const { layout } = useLayoutContext(); + + // Create Config + const chartConfig: ChartConfig = dataKeys.reduce( + (config, key, index) => ({ + ...config, + [key]: { + label: key, + icon: icons[key], + color: colors[index], + }, + }), + {}, + ); + + const getTickFormatter = (data: T) => { + const dataLength = data.length; + const maxLengthMap = { + mobile: { + default: 5, + 10: 4, + 11: 4, + }, + tray: { + default: 5, + 8: 4, + 9: 4, + 10: 4, + 11: 4, + }, + copilot: { + default: 5, + 8: 4, + 9: 4, + 10: 4, + 11: 4, + }, + fullscreen: { + default: 5, + 11: 4, + }, + }; + + const layoutConfig = + maxLengthMap[layout as keyof typeof maxLengthMap] || maxLengthMap.fullscreen; + + const maxLength = + dataLength >= 11 + ? 4 + : layoutConfig[dataLength as keyof typeof layoutConfig] || layoutConfig.default; + + return (value: string) => { + if (value.length > maxLength) { + return `${value.slice(0, maxLength)}...`; + } + return value; + }; + }; + const getAxisAngle = (data: T) => { + const angleConfig = { + mobile: { + default: 0, + ranges: [ + { min: 6, max: 9, angle: -45 }, + { min: 10, max: 10, angle: -60 }, + { min: 11, max: Infinity, angle: -75 }, + ], + }, + tray: { + default: 0, + ranges: [{ min: 8, max: Infinity, angle: -45 }], + }, + copilot: { + default: 0, + ranges: [{ min: 8, max: Infinity, angle: -45 }], + }, + fullscreen: { + default: 0, + ranges: [{ min: 12, max: Infinity, angle: -45 }], + }, + }; + + const layoutConfig = angleConfig[layout as keyof typeof angleConfig] || angleConfig.fullscreen; + const dataLength = data.length; + + const matchRange = layoutConfig.ranges.find( + (range) => dataLength >= range.min && dataLength <= range.max, + ); + + return matchRange?.angle ?? layoutConfig.default; + }; + const getTickMargin = (data: T) => { + return data.length <= 6 ? 10 : 15; + }; + + return ( + + + {grid && cartesianGrid()} + + {showYAxis && ( + + )} + } /> + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + if (label) { + return ( + + {label && ( + + )} + + ); + } + return ( + + ); + })} + {legend && } />} + + + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/index.ts b/js/packages/react-ui/src/components/Charts/BarChartV2/index.ts new file mode 100644 index 000000000..06f46423b --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarChartV2/index.ts @@ -0,0 +1 @@ +export * from "./BarChartv2"; diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx new file mode 100644 index 000000000..c1aff5664 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx @@ -0,0 +1,402 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Monitor, TabletSmartphone } from "lucide-react"; +import { Card } from "../../../Card"; +import { BarChartV2, BarChartProps } from "../BarChartv2"; + +const barChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, +// { month: "April", desktop: 180, mobile: 160 }, +// { month: "May", desktop: 250, mobile: 120 }, +// { month: "June", desktop: 300, mobile: 180 }, +]; + +const icons = { + desktop: Monitor, + mobile: TabletSmartphone, +} as const; + +const meta: Meta> = { + title: "Components/Charts/BarChartV2", + component: BarChartV2, + parameters: { + layout: "centered", + docs: { + description: { + component: "```tsx\nimport { BarChartV2 } from '@crayon-ui/react-ui/Charts/BarChartV2';\n```", + }, + }, + }, + tags: ["!dev", "autodocs"], + argTypes: { + data: { + description: + "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", + control: false, + table: { + type: { summary: "Array>" }, + defaultValue: { summary: "[]" }, + category: "Data", + }, + }, + categoryKey: { + description: + "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", + control: false, + table: { + type: { summary: "string" }, + defaultValue: { summary: "string" }, + category: "Data", + }, + }, + theme: { + description: + "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + icons: { + description: + "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", + control: false, + table: { + type: { summary: "Record" }, + defaultValue: { summary: "{}" }, + category: "Appearance", + }, + }, + variant: { + description: + "The style of the bar chart. 'grouped' shows bars side by side, while 'stacked' shows bars stacked on top of each other.", + control: "radio", + options: ["grouped", "stacked"], + table: { + defaultValue: { summary: "grouped" }, + category: "Appearance", + }, + }, + radius: { + description: "The radius of the rounded corners of the bars", + control: "number", + table: { + type: { summary: "number" }, + defaultValue: { summary: "4" }, + category: "Appearance", + }, + }, + grid: { + description: "Whether to display the background grid lines in the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + label: { + description: "Whether to display data point labels above each point on the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + legend: { + description: + "Whether to display the chart legend showing the data series names and their corresponding colors/icons", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + isAnimationActive: { + description: "Whether to animate the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + showYAxis: { + description: "Whether to display the y-axis", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "Display", + }, + }, + xAxisLabel: { + description: "The label for the x-axis", + control: "text", + table: { + type: { summary: "string" }, + defaultValue: { summary: "undefined" }, + category: "Data", + }, + }, + yAxisLabel: { + description: "The label for the y-axis", + control: "text", + table: { + type: { summary: "string" }, + defaultValue: { summary: "undefined" }, + category: "Data", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const BarChartV2Story: Story = { + name: "Bar Chart V2", + args: { + data: barChartData, + categoryKey: "month", + theme: "ocean", + variant: "grouped", + radius: 4, + grid: true, + label: true, + legend: true, + isAnimationActive: true, + showYAxis: false, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` +const barChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, +]; + + + + +`, + }, + }, + }, +}; + +export const BarChartV2WithIcons: Story = { + name: "Bar Chart V2 with Icons", + args: { + ...BarChartV2Story.args, + icons: icons, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` +import { Monitor, TabletSmartphone } from "lucide-react"; + +const barChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, +]; + +const icons = { + desktop: Monitor, + mobile: TabletSmartphone, +}; + + + + +`, + }, + }, + }, +}; + +export const BarChartV2WithYAxis: Story = { + name: "Bar Chart V2 with Y-Axis and Axis Labels", + args: { + ...BarChartV2Story.args, + showYAxis: true, + xAxisLabel: "Time Period", + yAxisLabel: "Number of Users", + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` +const barChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, +]; + + + + +`, + }, + }, + }, +}; + +export const BarChartV2Stacked: Story = { + name: "Stacked Bar Chart V2", + args: { + ...BarChartV2Story.args, + variant: "stacked", + theme: "emerald", + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` +const barChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, +]; + + + + +`, + }, + }, + }, +}; + +export const BarChartV2NoLabels: Story = { + name: "Bar Chart V2 without Labels", + args: { + ...BarChartV2Story.args, + label: false, + theme: "sunset", + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` +const barChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, +]; + + + + +`, + }, + }, + }, +}; \ No newline at end of file From 672baa1723a992adf57b9f64422858505b9a9d95 Mon Sep 17 00:00:00 2001 From: i-subham Date: Fri, 16 May 2025 18:02:31 +0530 Subject: [PATCH 002/190] feat(PieChartV2): Introduce new PieChartV2 component with customizable features This commit adds a new PieChartV2 component to the react-ui package, enhancing chart visualization capabilities. Key features include: - Support for pie and donut chart variants - Dynamic radius calculation based on layout - Customizable themes and data formatting options - Integration with existing chart utilities for color distribution and tooltip rendering - Storybook integration for easy demonstration and testing These enhancements aim to provide developers with a flexible and visually appealing way to represent data in their applications. --- .../Charts/PieChartV2/PieChartV2.tsx | 175 ++++++++++++++++++ .../src/components/Charts/PieChartV2/index.ts | 0 .../PieChartV2/stories/PieChartV2.stories.tsx | 173 +++++++++++++++++ 3 files changed, 348 insertions(+) create mode 100644 js/packages/react-ui/src/components/Charts/PieChartV2/PieChartV2.tsx create mode 100644 js/packages/react-ui/src/components/Charts/PieChartV2/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/PieChartV2/stories/PieChartV2.stories.tsx diff --git a/js/packages/react-ui/src/components/Charts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieChartV2/PieChartV2.tsx new file mode 100644 index 000000000..25794c450 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieChartV2/PieChartV2.tsx @@ -0,0 +1,175 @@ +import clsx from "clsx"; +import { debounce } from "lodash-es"; +import { useEffect, useRef, useState } from "react"; +import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; +import { useLayoutContext } from "../../../context/LayoutContext"; +import { + ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, +} from "../Charts"; +import { getDistributedColors, getPalette } from "../utils/PalletUtils"; + +export type PieChartV2Data = Array>; + +export interface PieChartV2Props { + data: T; + categoryKey: keyof T[number]; + dataKey: keyof T[number]; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + variant?: "pie" | "donut"; + format?: "percentage" | "number"; + legend?: boolean; + label?: boolean; + isAnimationActive?: boolean; +} + +const layoutMap: Record = { + mobile: "crayon-pie-chart-container-mobile", + fullscreen: "crayon-pie-chart-container-fullscreen", + tray: "crayon-pie-chart-container-tray", + copilot: "crayon-pie-chart-container-copilot", +}; + +// Helper function to calculate percentage +const calculatePercentage = (value: number, total: number): number => { + if (total === 0) { + return 0; + } + return Number(((value / total) * 100).toFixed(2)); +}; + +export const PieChartV2 = ({ + data, + categoryKey, + dataKey, + theme = "ocean", + variant = "pie", + format = "number", + legend = true, + label = true, + isAnimationActive = true, +}: PieChartV2Props) => { + const { layout } = useLayoutContext(); + const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(120); + const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(0); + const containerRef = useRef(null); + + // Calculate dynamic radius based on layout + useEffect(() => { + if (!containerRef.current) return; + + const resizeObserver = new ResizeObserver( + debounce((entries: any) => { + const { width } = entries[0].contentRect; + + // Calculate outer radius + let newOuterRadius = 120; // default + if (layout === "mobile") { + newOuterRadius = label ? (width > 300 ? 85 : 75) : width > 300 ? 95 : 80; + } else if (layout === "fullscreen") { + newOuterRadius = 120; + } else if (layout === "tray" || layout === "copilot") { + newOuterRadius = 90; + } + + // Calculate inner radius for donut + let newInnerRadius = 0; + if (variant === "donut") { + if (layout === "mobile") { + newInnerRadius = label ? (width > 300 ? 50 : 30) : width > 300 ? 60 : 50; + } else { + newInnerRadius = 60; + } + } + + setCalculatedOuterRadius(newOuterRadius); + setCalculatedInnerRadius(newInnerRadius); + }, 100), + ); + + resizeObserver.observe(containerRef.current); + return () => resizeObserver.disconnect(); + }, [layout, label, variant]); + + // Calculate total for percentage calculations + const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); + + // Transform data with percentages + const transformedData = data.map((item) => ({ + ...item, + percentage: calculatePercentage(Number(item[dataKey as string]), total), + originalValue: item[dataKey as string], + })); + + // Get color palette and distribute colors + const palette = getPalette(theme); + const colors = getDistributedColors(palette, data.length); + + // Create chart configuration + const chartConfig = data.reduce( + (config, item, index) => ({ + ...config, + [String(item[categoryKey])]: { + label: String(item[categoryKey as string]), + color: colors[index], + }, + }), + {}, + ); + + // Custom label renderer + const renderCustomLabel = ({ payload, cx, cy, x, y, textAnchor, dominantBaseline }: any) => { + if (payload.percentage <= 10) return null; + const displayValue = format === "percentage" ? payload.percentage : payload[dataKey]; + const formattedValue = + String(displayValue).length > 7 ? `${String(displayValue).slice(0, 7)}...` : displayValue; + + return ( + + + {formattedValue} + {format === "percentage" ? "%" : ""} + + + ); + }; + + return ( + + + } /> + {legend && } />} + + {Object.entries(chartConfig).map(([key, config]) => ( + + ))} + + + + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/PieChartV2/index.ts b/js/packages/react-ui/src/components/Charts/PieChartV2/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/js/packages/react-ui/src/components/Charts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieChartV2/stories/PieChartV2.stories.tsx new file mode 100644 index 000000000..a49350df8 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieChartV2/stories/PieChartV2.stories.tsx @@ -0,0 +1,173 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Card } from "../../../Card"; +import { PieChartV2, PieChartV2Props } from "../PieChartV2"; + +const pieChartData = [ + { month: "January", value: 4250 }, + { month: "February", value: 3820 }, + { month: "March", value: 4680 }, + { month: "April", value: 4120 }, + { month: "May", value: 5340 }, + { month: "June", value: 6250 }, + { month: "July", value: 5890 }, +]; + +const meta: Meta> = { + title: "Components/Charts/PieChartV2", + component: PieChartV2, + parameters: { + layout: "centered", + docs: { + description: { + component: "```tsx\nimport { PieChart } from '@crayon-ui/react-ui/Charts/PieChart';\n```", + }, + }, + }, + tags: ["!dev", "autodocs"], + + argTypes: { + data: { + description: + "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", + control: false, + table: { + type: { summary: "Array>" }, + defaultValue: { summary: "[]" }, + category: "Data", + }, + }, + categoryKey: { + description: + "The key from your data object to be used as the segment labels (e.g., 'month', 'category', 'name')", + control: false, + table: { + type: { summary: "string" }, + category: "Data", + }, + }, + dataKey: { + description: + "The key from your data object to be used as the values that determine the slice sizes (e.g., 'value', 'count', 'amount')", + control: false, + table: { + type: { summary: "string" }, + category: "Data", + }, + }, + theme: { + description: + "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + variant: { + description: + "The style of the pie chart. 'pie' shows a pie chart, while 'donut' shows a donut chart.", + control: "radio", + options: ["pie", "donut"], + table: { + defaultValue: { summary: "pie" }, + category: "Appearance", + }, + }, + format: { + description: + "The format of the data. 'percentage' shows the data as a percentage, while 'number' shows the data as a number.", + control: "radio", + options: ["percentage", "number"], + table: { + defaultValue: { summary: "percentage" }, + category: "Display", + }, + }, + legend: { + description: "Whether to display the legend", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + label: { + description: "Whether to display the data point labels above each point on the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + isAnimationActive: { + description: "Whether to animate the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const PieChartStory: Story = { + name: "Pie Chart", + args: { + data: pieChartData, + categoryKey: "month", + dataKey: "value", + theme: "ocean", + variant: "pie", + format: "number", + legend: true, + label: true, + isAnimationActive: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` + const pieChartData = [ + { month: "January", value: 400 }, + { month: "February", value: 300 }, + { month: "March", value: 300 }, + { month: "April", value: 400 }, + { month: "May", value: 300 }, + { month: "June", value: 300 }, + { month: "July", value: 300 }, +]; + + + + + `, + }, + }, + }, +}; From c299395de367c047d81e74f939cf4e953dd06b48 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 19 May 2025 13:57:50 +0530 Subject: [PATCH 003/190] feat(BarChartV2): Rename BarChartProps to BarChartPropsV2 The changes made in this commit are: 1. Renamed the `BarChartProps` interface to `BarChartPropsV2` in the `BarChartV2.tsx` file. 2. Updated the corresponding references to `BarChartProps` in the `barChartV2.stories.tsx` file. 3. Updated the export in the `index.ts` file to export `BarChartV2` instead of `BarChartv2`. These changes were made to avoid naming conflicts and maintain consistency in the codebase. --- .../src/components/Charts/BarChartV2/BarChartV2.tsx | 4 ++-- .../react-ui/src/components/Charts/BarChartV2/index.ts | 2 +- .../Charts/BarChartV2/stories/barChartV2.stories.tsx | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx index 46d64ccfe..0a84e2dce 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx @@ -15,7 +15,7 @@ import { getDistributedColors, getPalette } from "../utils/PalletUtils"; export type BarChartData = Array>; -export interface BarChartProps { +export interface BarChartPropsV2 { data: T; categoryKey: keyof T[number]; theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; @@ -45,7 +45,7 @@ export const BarChartV2 = ({ showYAxis = false, xAxisLabel, yAxisLabel, -}: BarChartProps) => { +}: BarChartPropsV2) => { // excluding the categoryKey const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/index.ts b/js/packages/react-ui/src/components/Charts/BarChartV2/index.ts index 06f46423b..14de9387b 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartV2/index.ts +++ b/js/packages/react-ui/src/components/Charts/BarChartV2/index.ts @@ -1 +1 @@ -export * from "./BarChartv2"; +export * from "./BarChartV2"; diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx index c1aff5664..680c0a68a 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Monitor, TabletSmartphone } from "lucide-react"; import { Card } from "../../../Card"; -import { BarChartV2, BarChartProps } from "../BarChartv2"; +import { BarChartV2, BarChartPropsV2 } from "../BarChartV2"; const barChartData = [ { month: "January", desktop: 150, mobile: 90 }, @@ -17,7 +17,7 @@ const icons = { mobile: TabletSmartphone, } as const; -const meta: Meta> = { +const meta: Meta> = { title: "Components/Charts/BarChartV2", component: BarChartV2, parameters: { @@ -173,7 +173,7 @@ export const BarChartV2Story: Story = { isAnimationActive: true, showYAxis: false, }, - render: (args) => ( + render: (args: BarChartPropsV2) => ( @@ -220,7 +220,7 @@ export const BarChartV2WithIcons: Story = { ...BarChartV2Story.args, icons: icons, }, - render: (args) => ( + render: (args: BarChartPropsV2) => ( @@ -273,7 +273,7 @@ export const BarChartV2WithYAxis: Story = { xAxisLabel: "Time Period", yAxisLabel: "Number of Users", }, - render: (args) => ( + render: (args: BarChartPropsV2) => ( From f1257bc818ce73aeca43254433e3b4796379edec Mon Sep 17 00:00:00 2001 From: i-subham Date: Mon, 19 May 2025 15:08:53 +0530 Subject: [PATCH 004/190] feat(PieChart): Implement dynamic resizing for PieChart component This commit introduces a new function to calculate chart dimensions dynamically, ensuring the PieChart maintains its aspect ratio based on the container width. Key changes include: - Added `calculateChartDimensions` function to determine outer and inner radii based on width, variant, and label presence. - Updated the `useEffect` hook to utilize the new function for calculating radii, simplifying the logic and improving responsiveness. These enhancements aim to provide a more flexible and visually consistent PieChart experience across different layouts. --- .../components/Charts/PieChart/PieChart.tsx | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx b/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx index 44496a4b6..6372256ff 100644 --- a/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx @@ -42,6 +42,32 @@ const calculatePercentage = (value: number, total: number): number => { return Number(((value / total) * 100).toFixed(2)); }; +// Dynamic resize function to maintain aspect ratio +const calculateChartDimensions = ( + width: number, + variant: string, + label: boolean, +): { outerRadius: number; innerRadius: number } => { + const baseRadiusPercentage = 0.4; // 40% of container width + + let outerRadius = Math.round(width * baseRadiusPercentage); + + if (label) { + outerRadius = Math.round(outerRadius * 0.9); + } + + // Set minimum and maximum bounds for radius + outerRadius = Math.max(50, Math.min(outerRadius, width / 2 - 10)); // Ensure radius isn't too small or too large + + // Calculate inner radius for donut variant - consistent ratio regardless of layout + let innerRadius = 0; + if (variant === "donut") { + innerRadius = Math.round(outerRadius * 0.6); + } + + return { outerRadius, innerRadius }; +}; + export const PieChart = ({ data, categoryKey, @@ -58,7 +84,7 @@ export const PieChart = ({ const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(0); const containerRef = useRef(null); - // Calculate dynamic radius based on layout + // Calculate dynamic radius useEffect(() => { if (!containerRef.current) return; @@ -66,34 +92,17 @@ export const PieChart = ({ debounce((entries: any) => { const { width } = entries[0].contentRect; - // Calculate outer radius - let newOuterRadius = 120; // default - if (layout === "mobile") { - newOuterRadius = label ? (width > 300 ? 85 : 75) : width > 300 ? 95 : 80; - } else if (layout === "fullscreen") { - newOuterRadius = 120; - } else if (layout === "tray" || layout === "copilot") { - newOuterRadius = 90; - } - - // Calculate inner radius for donut - let newInnerRadius = 0; - if (variant === "donut") { - if (layout === "mobile") { - newInnerRadius = label ? (width > 300 ? 50 : 30) : width > 300 ? 60 : 50; - } else { - newInnerRadius = 60; - } - } - - setCalculatedOuterRadius(newOuterRadius); - setCalculatedInnerRadius(newInnerRadius); + // Use the dynamic resize function to calculate radii + const { outerRadius, innerRadius } = calculateChartDimensions(width, variant, label); + + setCalculatedOuterRadius(outerRadius); + setCalculatedInnerRadius(innerRadius); }, 100), ); resizeObserver.observe(containerRef.current); return () => resizeObserver.disconnect(); - }, [layout, label, variant]); + }, [variant, label]); // Calculate total for percentage calculations const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); From b72cf20ca0a974bcc1bc78d256f510aa063af568 Mon Sep 17 00:00:00 2001 From: i-subham Date: Mon, 19 May 2025 17:19:02 +0530 Subject: [PATCH 005/190] feat(PieChartCard): Add new PieChartCard component with dynamic resizing and customizable features This commit introduces the PieChartCard component, enhancing the charting capabilities within the react-ui package. Key changes include: - Implementation of the PieChartCard component that supports pie and donut chart variants. - Dynamic resizing functionality based on layout to maintain aspect ratio. - Customizable themes, data formatting options, and legend display. - Integration with existing chart utilities for improved data visualization. - Storybook stories for demonstration and testing. These enhancements aim to provide developers with a flexible and visually appealing way to represent data in their applications. --- .../BarChartV2/stories/barChartV2.stories.tsx | 13 +- .../components/Charts/PieChart/PieChart.tsx | 57 ++--- .../PieChartCard/PieChartCard.stories.tsx | 173 ++++++++++++++ .../Charts/PieChartCard/PieChartCard.tsx | 220 ++++++++++++++++++ .../components/Charts/PieChartCard/index.ts | 1 + .../Charts/PieChartV2/PieChartV2.tsx | 59 +++-- .../src/components/Charts/PieChartV2/index.ts | 1 + 7 files changed, 460 insertions(+), 64 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/PieChartCard/PieChartCard.stories.tsx create mode 100644 js/packages/react-ui/src/components/Charts/PieChartCard/PieChartCard.tsx create mode 100644 js/packages/react-ui/src/components/Charts/PieChartCard/index.ts diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx index 680c0a68a..2a96e2216 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx @@ -1,15 +1,15 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Monitor, TabletSmartphone } from "lucide-react"; import { Card } from "../../../Card"; -import { BarChartV2, BarChartPropsV2 } from "../BarChartV2"; +import { BarChartPropsV2, BarChartV2 } from "../BarChartV2"; const barChartData = [ { month: "January", desktop: 150, mobile: 90 }, { month: "February", desktop: 280, mobile: 180 }, { month: "March", desktop: 220, mobile: 140 }, -// { month: "April", desktop: 180, mobile: 160 }, -// { month: "May", desktop: 250, mobile: 120 }, -// { month: "June", desktop: 300, mobile: 180 }, + // { month: "April", desktop: 180, mobile: 160 }, + // { month: "May", desktop: 250, mobile: 120 }, + // { month: "June", desktop: 300, mobile: 180 }, ]; const icons = { @@ -24,7 +24,8 @@ const meta: Meta> = { layout: "centered", docs: { description: { - component: "```tsx\nimport { BarChartV2 } from '@crayon-ui/react-ui/Charts/BarChartV2';\n```", + component: + "```tsx\nimport { BarChartV2 } from '@crayon-ui/react-ui/Charts/BarChartV2';\n```", }, }, }, @@ -399,4 +400,4 @@ const barChartData = [ }, }, }, -}; \ No newline at end of file +}; diff --git a/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx b/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx index 6372256ff..44496a4b6 100644 --- a/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx @@ -42,32 +42,6 @@ const calculatePercentage = (value: number, total: number): number => { return Number(((value / total) * 100).toFixed(2)); }; -// Dynamic resize function to maintain aspect ratio -const calculateChartDimensions = ( - width: number, - variant: string, - label: boolean, -): { outerRadius: number; innerRadius: number } => { - const baseRadiusPercentage = 0.4; // 40% of container width - - let outerRadius = Math.round(width * baseRadiusPercentage); - - if (label) { - outerRadius = Math.round(outerRadius * 0.9); - } - - // Set minimum and maximum bounds for radius - outerRadius = Math.max(50, Math.min(outerRadius, width / 2 - 10)); // Ensure radius isn't too small or too large - - // Calculate inner radius for donut variant - consistent ratio regardless of layout - let innerRadius = 0; - if (variant === "donut") { - innerRadius = Math.round(outerRadius * 0.6); - } - - return { outerRadius, innerRadius }; -}; - export const PieChart = ({ data, categoryKey, @@ -84,7 +58,7 @@ export const PieChart = ({ const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(0); const containerRef = useRef(null); - // Calculate dynamic radius + // Calculate dynamic radius based on layout useEffect(() => { if (!containerRef.current) return; @@ -92,17 +66,34 @@ export const PieChart = ({ debounce((entries: any) => { const { width } = entries[0].contentRect; - // Use the dynamic resize function to calculate radii - const { outerRadius, innerRadius } = calculateChartDimensions(width, variant, label); - - setCalculatedOuterRadius(outerRadius); - setCalculatedInnerRadius(innerRadius); + // Calculate outer radius + let newOuterRadius = 120; // default + if (layout === "mobile") { + newOuterRadius = label ? (width > 300 ? 85 : 75) : width > 300 ? 95 : 80; + } else if (layout === "fullscreen") { + newOuterRadius = 120; + } else if (layout === "tray" || layout === "copilot") { + newOuterRadius = 90; + } + + // Calculate inner radius for donut + let newInnerRadius = 0; + if (variant === "donut") { + if (layout === "mobile") { + newInnerRadius = label ? (width > 300 ? 50 : 30) : width > 300 ? 60 : 50; + } else { + newInnerRadius = 60; + } + } + + setCalculatedOuterRadius(newOuterRadius); + setCalculatedInnerRadius(newInnerRadius); }, 100), ); resizeObserver.observe(containerRef.current); return () => resizeObserver.disconnect(); - }, [variant, label]); + }, [layout, label, variant]); // Calculate total for percentage calculations const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); diff --git a/js/packages/react-ui/src/components/Charts/PieChartCard/PieChartCard.stories.tsx b/js/packages/react-ui/src/components/Charts/PieChartCard/PieChartCard.stories.tsx new file mode 100644 index 000000000..3811fdf27 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieChartCard/PieChartCard.stories.tsx @@ -0,0 +1,173 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Card } from "../../Card"; +import { PieChartCard, PieChartCardProps } from "./PieChartCard"; + +const pieChartCardData = [ + { month: "January", value: 4250 }, + { month: "February", value: 3820 }, + { month: "March", value: 4680 }, + { month: "April", value: 4120 }, + { month: "May", value: 5340 }, + { month: "June", value: 6250 }, + { month: "July", value: 5890 }, +]; + +const meta: Meta> = { + title: "Components/Charts/PieChartCard", + component: PieChartCard, + parameters: { + layout: "centered", + docs: { + description: { + component: "```tsx\nimport { PieChart } from '@crayon-ui/react-ui/Charts/PieChart';\n```", + }, + }, + }, + tags: ["!dev", "autodocs"], + + argTypes: { + data: { + description: + "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", + control: false, + table: { + type: { summary: "Array>" }, + defaultValue: { summary: "[]" }, + category: "Data", + }, + }, + categoryKey: { + description: + "The key from your data object to be used as the segment labels (e.g., 'month', 'category', 'name')", + control: false, + table: { + type: { summary: "string" }, + category: "Data", + }, + }, + dataKey: { + description: + "The key from your data object to be used as the values that determine the slice sizes (e.g., 'value', 'count', 'amount')", + control: false, + table: { + type: { summary: "string" }, + category: "Data", + }, + }, + theme: { + description: + "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + variant: { + description: + "The style of the pie chart. 'pie' shows a pie chart, while 'donut' shows a donut chart.", + control: "radio", + options: ["pie", "donut"], + table: { + defaultValue: { summary: "pie" }, + category: "Appearance", + }, + }, + format: { + description: + "The format of the data. 'percentage' shows the data as a percentage, while 'number' shows the data as a number.", + control: "radio", + options: ["percentage", "number"], + table: { + defaultValue: { summary: "percentage" }, + category: "Display", + }, + }, + legend: { + description: "Whether to display the legend", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + label: { + description: "Whether to display the data point labels above each point on the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + isAnimationActive: { + description: "Whether to animate the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const PieChartStory: Story = { + name: "Pie Chart", + args: { + data: pieChartCardData, + categoryKey: "month", + dataKey: "value", + theme: "ocean", + variant: "pie", + format: "number", + legend: false, + label: false, + isAnimationActive: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` + const pieChartData = [ + { month: "January", value: 400 }, + { month: "February", value: 300 }, + { month: "March", value: 300 }, + { month: "April", value: 400 }, + { month: "May", value: 300 }, + { month: "June", value: 300 }, + { month: "July", value: 300 }, +]; + + + + + `, + }, + }, + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/PieChartCard/PieChartCard.tsx b/js/packages/react-ui/src/components/Charts/PieChartCard/PieChartCard.tsx new file mode 100644 index 000000000..1edd27923 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieChartCard/PieChartCard.tsx @@ -0,0 +1,220 @@ +import clsx from "clsx"; +import { debounce } from "lodash-es"; +import { useEffect, useRef, useState } from "react"; +import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; +import { useLayoutContext } from "../../../context/LayoutContext"; +import { + ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, +} from "../Charts"; +import { getDistributedColors, getPalette } from "../utils/PalletUtils"; + +export type PieChartCardData = Array>; + +export interface PieChartCardProps { + data: T; + categoryKey: keyof T[number]; + dataKey: keyof T[number]; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + variant?: "pie" | "donut"; + format?: "percentage" | "number"; + legend?: boolean; + label?: boolean; + isAnimationActive?: boolean; +} + +const layoutMap: Record = { + mobile: "crayon-pie-chart-container-mobile", + fullscreen: "crayon-pie-chart-container-fullscreen", + tray: "crayon-pie-chart-container-tray", + copilot: "crayon-pie-chart-container-copilot", +}; + +// Helper function to calculate percentage +const calculatePercentage = (value: number, total: number): number => { + if (total === 0) { + return 0; + } + return Number(((value / total) * 100).toFixed(2)); +}; + +export const PieChartCard = ({ + data, + categoryKey, + dataKey, + theme = "ocean", + variant = "pie", + format = "number", + legend = false, + label = true, + isAnimationActive = true, +}: PieChartCardProps) => { + const { layout } = useLayoutContext(); + const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(120); + const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(0); + const containerRef = useRef(null); + + // Calculate dynamic radius based on layout + useEffect(() => { + if (!containerRef.current) return; + + const resizeObserver = new ResizeObserver( + debounce((entries: any) => { + const { width } = entries[0].contentRect; + + // Calculate outer radius + let newOuterRadius = 120; // default + if (layout === "mobile") { + newOuterRadius = label ? (width > 300 ? 85 : 75) : width > 300 ? 95 : 80; + } else if (layout === "fullscreen") { + newOuterRadius = 120; + } else if (layout === "tray" || layout === "copilot") { + newOuterRadius = 90; + } + + // Calculate inner radius for donut + let newInnerRadius = 0; + if (variant === "donut") { + if (layout === "mobile") { + newInnerRadius = label ? (width > 300 ? 50 : 30) : width > 300 ? 60 : 50; + } else { + newInnerRadius = 60; + } + } + + setCalculatedOuterRadius(newOuterRadius); + setCalculatedInnerRadius(newInnerRadius); + }, 100), + ); + + resizeObserver.observe(containerRef.current); + return () => resizeObserver.disconnect(); + }, [layout, label, variant]); + + // Calculate total for percentage calculations + const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); + + // Transform data with percentages + const transformedData = data.map((item) => ({ + ...item, + percentage: calculatePercentage(Number(item[dataKey as string]), total), + originalValue: item[dataKey as string], + })) as (T[number] & { percentage: number; originalValue: string | number })[]; + + // Get color palette and distribute colors + const palette = getPalette(theme); + const colors = getDistributedColors(palette, data.length); + + // Create chart configuration + const chartConfig = data.reduce( + (config, item, index) => ({ + ...config, + [String(item[categoryKey])]: { + label: String(item[categoryKey as string]), + color: colors[index], + }, + }), + {}, + ); + + // Custom label renderer + const renderCustomLabel = ({ payload, cx, cy, x, y, textAnchor, dominantBaseline }: any) => { + if (payload.percentage <= 10) return null; + const displayValue = format === "percentage" ? payload.percentage : payload[dataKey]; + const formattedValue = + String(displayValue).length > 7 ? `${String(displayValue).slice(0, 7)}...` : displayValue; + + return ( + + + {formattedValue} + {format === "percentage" ? "%" : ""} + + + ); + }; + + return ( +
+ + + } + /> + {legend && ( + } + /> + )} + + {Object.entries(chartConfig).map(([key, config]) => ( + + ))} + + + +
+ {transformedData.map((item, index) => ( +
+
+
+
{item[categoryKey]}
+
+ {format === "percentage" ? `${item.percentage}%` : item.originalValue} +
+
+
+ ))} +
+
+ ); +}; diff --git a/js/packages/react-ui/src/components/Charts/PieChartCard/index.ts b/js/packages/react-ui/src/components/Charts/PieChartCard/index.ts new file mode 100644 index 000000000..28fb2bbc9 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieChartCard/index.ts @@ -0,0 +1 @@ +export * from "./PieChartCard"; diff --git a/js/packages/react-ui/src/components/Charts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieChartV2/PieChartV2.tsx index 25794c450..6e048e085 100644 --- a/js/packages/react-ui/src/components/Charts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieChartV2/PieChartV2.tsx @@ -42,6 +42,32 @@ const calculatePercentage = (value: number, total: number): number => { return Number(((value / total) * 100).toFixed(2)); }; +// Dynamic resize function to maintain aspect ratio +const calculateChartDimensions = ( + width: number, + variant: string, + label: boolean, +): { outerRadius: number; innerRadius: number } => { + const baseRadiusPercentage = 0.4; // 40% of container width + + let outerRadius = Math.round(width * baseRadiusPercentage); + + if (label) { + outerRadius = Math.round(outerRadius * 0.9); + } + + // Set minimum and maximum bounds for radius + outerRadius = Math.max(50, Math.min(outerRadius, width / 2 - 10)); // Ensure radius isn't too small or too large + + // Calculate inner radius for donut variant - consistent ratio regardless of layout + let innerRadius = 0; + if (variant === "donut") { + innerRadius = Math.round(outerRadius * 0.6); + } + + return { outerRadius, innerRadius }; +}; + export const PieChartV2 = ({ data, categoryKey, @@ -58,7 +84,7 @@ export const PieChartV2 = ({ const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(0); const containerRef = useRef(null); - // Calculate dynamic radius based on layout + // Calculate dynamic radius useEffect(() => { if (!containerRef.current) return; @@ -66,34 +92,17 @@ export const PieChartV2 = ({ debounce((entries: any) => { const { width } = entries[0].contentRect; - // Calculate outer radius - let newOuterRadius = 120; // default - if (layout === "mobile") { - newOuterRadius = label ? (width > 300 ? 85 : 75) : width > 300 ? 95 : 80; - } else if (layout === "fullscreen") { - newOuterRadius = 120; - } else if (layout === "tray" || layout === "copilot") { - newOuterRadius = 90; - } - - // Calculate inner radius for donut - let newInnerRadius = 0; - if (variant === "donut") { - if (layout === "mobile") { - newInnerRadius = label ? (width > 300 ? 50 : 30) : width > 300 ? 60 : 50; - } else { - newInnerRadius = 60; - } - } - - setCalculatedOuterRadius(newOuterRadius); - setCalculatedInnerRadius(newInnerRadius); + // Use the dynamic resize function to calculate radii + const { outerRadius, innerRadius } = calculateChartDimensions(width, variant, label); + + setCalculatedOuterRadius(outerRadius); + setCalculatedInnerRadius(innerRadius); }, 100), ); resizeObserver.observe(containerRef.current); return () => resizeObserver.disconnect(); - }, [layout, label, variant]); + }, [variant, label]); // Calculate total for percentage calculations const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); @@ -162,7 +171,7 @@ export const PieChartV2 = ({ labelLine={false} outerRadius={calculatedOuterRadius} innerRadius={calculatedInnerRadius} - label={label ? renderCustomLabel : false} + label={false} isAnimationActive={isAnimationActive} > {Object.entries(chartConfig).map(([key, config]) => ( diff --git a/js/packages/react-ui/src/components/Charts/PieChartV2/index.ts b/js/packages/react-ui/src/components/Charts/PieChartV2/index.ts index e69de29bb..737933f38 100644 --- a/js/packages/react-ui/src/components/Charts/PieChartV2/index.ts +++ b/js/packages/react-ui/src/components/Charts/PieChartV2/index.ts @@ -0,0 +1 @@ +export * from "./PieChartV2"; From 07011cbfc18598a47e592258ead863d809ddf01f Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 19 May 2025 19:47:26 +0530 Subject: [PATCH 006/190] feat(BarChartV2): Enhance responsiveness and add new features This commit introduces several improvements to the BarChartV2 component: 1. Adds support for responsive layout by using a ResizeObserver to detect changes in the container width and update the chart accordingly. 2. Implements a new `getPadding` function to dynamically calculate the left and right padding of the chart based on the data and container width. 3. Adds a new `getWidthOfData` function to calculate the total width of the chart based on the data. 4. Introduces a new `getRadiusArray` function to handle the different radius styles for grouped and stacked bar charts. 5. Updates the `ChartContainer` component to pass the `rechartsProps` object to the `ResponsiveContainer` component, allowing for better control over the chart's responsiveness. These changes improve the overall user experience and flexibility of the BarChartV2 component, making it more adaptable to different data sets and container sizes. --- .../Charts/BarChartV2/BarChartV2.tsx | 160 +++++++++++------- .../Charts/BarChartV2/utils/index.ts | 48 ++++++ .../react-ui/src/components/Charts/Charts.tsx | 10 +- 3 files changed, 154 insertions(+), 64 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/BarChartV2/utils/index.ts diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx index 0a84e2dce..445350cb7 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Bar, LabelList, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; import { useLayoutContext } from "../../../context/LayoutContext"; import { @@ -12,14 +12,17 @@ import { } from "../Charts"; import { cartesianGrid } from "../cartesianGrid"; import { getDistributedColors, getPalette } from "../utils/PalletUtils"; +import { getPadding, getRadiusArray, getWidthOfData } from "./utils"; export type BarChartData = Array>; +export type Variant = "grouped" | "stacked"; + export interface BarChartPropsV2 { data: T; categoryKey: keyof T[number]; theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - variant?: "grouped" | "stacked"; + variant?: Variant; grid?: boolean; label?: boolean; legend?: boolean; @@ -146,47 +149,100 @@ export const BarChartV2 = ({ return data.length <= 6 ? 10 : 15; }; + const chartContainerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + + useEffect(() => { + if (!chartContainerRef.current) { + return () => {}; + } + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerWidth(entry.contentRect.width); + } + }); + + resizeObserver.observe(chartContainerRef.current); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + const padding = getPadding(data, categoryKey as string, containerWidth); + const width = getWidthOfData(data, categoryKey as string); + console.log(width); + return ( - - + - {grid && cartesianGrid()} - - {showYAxis && ( - + {grid && cartesianGrid()} + - )} - } /> - {dataKeys.map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - if (label) { + {showYAxis && ( + + )} + } /> + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + if (label) { + return ( + + {label && ( + + )} + + ); + } return ( ({ radius={radius} stackId={variant === "stacked" ? "a" : undefined} isAnimationActive={isAnimationActive} - maxBarSize={8} - > - {label && ( - - )} - + /> ); - } - return ( - - ); - })} - {legend && } />} - - + })} + {legend && } />} + + +
); }; diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/utils/index.ts b/js/packages/react-ui/src/components/Charts/BarChartV2/utils/index.ts new file mode 100644 index 000000000..3a3872f15 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarChartV2/utils/index.ts @@ -0,0 +1,48 @@ +import { Variant } from "../BarChartV2"; + +const getPadding = ( + data: Array>, + categoryKey: string, + containerWidth: number, +) => { + const numberOfSticks = data.map((ob) => { + const datapoint = Object.keys(ob).filter((key) => key !== categoryKey).length; + return datapoint; + }); + const totalSticks = numberOfSticks.reduce((acc, curr) => acc + curr, 0); + const chartWidth = totalSticks * 8 + totalSticks * 20; + const padding = containerWidth - chartWidth; + + if (padding / 2 < 0) { + return { + left: 0, + right: 0, + }; + } else { + return { + left: padding / 2, + right: padding / 2, + }; + } +}; + +const getWidthOfData = (data: Array>, categoryKey: string) => { + const sticks = data.map((ob) => { + const datapoint = Object.keys(ob).filter((key) => key !== categoryKey).length; + return datapoint; + }); + + const totalSticks = sticks.reduce((acc, curr) => acc + curr, 0); + const width = totalSticks * 8 + totalSticks * 20; + return width; +}; + +const getRadiusArray = (variant: Variant, radius: number): [number, number, number, number] => { + if (variant === "grouped") { + return [radius, radius, 0, 0]; + } else { + return [radius, radius, radius, radius]; + } +}; + +export { getPadding, getRadiusArray, getWidthOfData }; diff --git a/js/packages/react-ui/src/components/Charts/Charts.tsx b/js/packages/react-ui/src/components/Charts/Charts.tsx index 944e250ee..9992bcc57 100644 --- a/js/packages/react-ui/src/components/Charts/Charts.tsx +++ b/js/packages/react-ui/src/components/Charts/Charts.tsx @@ -98,8 +98,12 @@ const ChartContainer = forwardRef< ComponentProps<"div"> & { config: ChartConfig; children: React.ComponentProps["children"]; + rechartsProps?: Omit< + React.ComponentProps, + "children" + >; } ->(({ id, className, children, config, ...props }, ref) => { +>(({ id, className, children, config, rechartsProps, ...props }, ref) => { const uniqueId = useId(); const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; @@ -112,7 +116,9 @@ const ChartContainer = forwardRef< {...props} > - {children} + + {children} + ); From 8166cbeaa5bc41b0935cadf4a60833a40f479651 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 19 May 2025 19:49:21 +0530 Subject: [PATCH 007/190] feat(BarChartV2): remove unnecessary prop from chart container Removes the `style` prop from the chart container component, as the width, min-width, and aspect ratio are already set in the component's styles. This simplifies the component and removes redundant configuration. --- .../react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx index 445350cb7..2e2e8b515 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx @@ -180,7 +180,6 @@ export const BarChartV2 = ({ config={chartConfig} ref={chartContainerRef} style={{ width, minWidth: "100%", aspectRatio: "16/9" }} - > Date: Tue, 20 May 2025 11:56:09 +0530 Subject: [PATCH 008/190] feat(BarChartV3): Introduce new BarChartV3 component with enhanced features This commit adds the BarChartV3 component to the react-ui package, providing an improved version of the bar chart with several new features: - Supports both grouped and stacked bar chart variants. - Implements responsive design using ResizeObserver for dynamic width adjustments. - Allows customizable themes and bar radius. - Includes a total value display next to the chart. - Storybook stories for demonstration and testing. These enhancements aim to offer developers a more flexible and visually appealing way to represent data in their applications. --- .../Charts/BarChartV3/BarChartV3.tsx | 121 ++++++++++++++ .../components/Charts/BarChartV3/index.tsx | 0 .../BarChartV3/stories/BarChartV3.stories.tsx | 152 ++++++++++++++++++ 3 files changed, 273 insertions(+) create mode 100644 js/packages/react-ui/src/components/Charts/BarChartV3/BarChartV3.tsx create mode 100644 js/packages/react-ui/src/components/Charts/BarChartV3/index.tsx create mode 100644 js/packages/react-ui/src/components/Charts/BarChartV3/stories/BarChartV3.stories.tsx diff --git a/js/packages/react-ui/src/components/Charts/BarChartV3/BarChartV3.tsx b/js/packages/react-ui/src/components/Charts/BarChartV3/BarChartV3.tsx new file mode 100644 index 000000000..3ad390bbc --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarChartV3/BarChartV3.tsx @@ -0,0 +1,121 @@ +import { useEffect, useRef, useState } from "react"; +import { Bar, BarChart, XAxis } from "recharts"; +import { getPadding, getWidthOfData } from "../BarChartV2/utils"; +import { ChartConfig, ChartContainer, keyTransform } from "../Charts"; +import { getDistributedColors, getPalette } from "../utils/PalletUtils"; + +export type BarChartV3Data = Array>; + +export type Variant = "grouped" | "stacked"; + +export interface BarChartPropsV3 { + data: T; + categoryKey: keyof T[number]; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + variant?: Variant; + radius?: number; + isAnimationActive?: boolean; + label?: string; +} + +export const BarChartV3 = ({ + data, + categoryKey, + theme = "ocean", + variant = "grouped", + radius = 4, + isAnimationActive = true, + label, +}: BarChartPropsV3) => { + // excluding the categoryKey + const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + + const palette = getPalette(theme); + const colors = getDistributedColors(palette, dataKeys.length); + + // Create Config + const chartConfig: ChartConfig = dataKeys.reduce( + (config, key, index) => ({ + ...config, + [key]: { + label: key, + color: colors[index], + }, + }), + {}, + ); + + const chartContainerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + + useEffect(() => { + if (!chartContainerRef.current) { + return () => {}; + } + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerWidth(entry.contentRect.width); + } + }); + + resizeObserver.observe(chartContainerRef.current); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + const padding = getPadding(data, categoryKey as string, containerWidth); + const width = getWidthOfData(data, categoryKey as string); + + // Helper function to calculate total + const calculateTotal = ( + data: T, + categoryKey: keyof T[number], + ): number => { + const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + return data.reduce((sum, item) => { + return sum + dataKeys.reduce((keySum, key) => keySum + Number(item[key] || 0), 0); + }, 0); + }; + + return ( +
+
+ + + + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + return ( + + ); + })} + + +
+
+ + {calculateTotal(data, categoryKey).toLocaleString()} + + + {label} + +
+
+ ); +}; diff --git a/js/packages/react-ui/src/components/Charts/BarChartV3/index.tsx b/js/packages/react-ui/src/components/Charts/BarChartV3/index.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/js/packages/react-ui/src/components/Charts/BarChartV3/stories/BarChartV3.stories.tsx b/js/packages/react-ui/src/components/Charts/BarChartV3/stories/BarChartV3.stories.tsx new file mode 100644 index 000000000..0c647a203 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarChartV3/stories/BarChartV3.stories.tsx @@ -0,0 +1,152 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Monitor, TabletSmartphone } from "lucide-react"; +import { Card } from "../../../Card"; +import { BarChartPropsV3, BarChartV3 } from "../BarChartV3"; + +const barChartData = [ + { month: "January", desktop: 1500000 }, + { month: "February", desktop: 2800000 }, + { month: "March", desktop: 2200000 }, + { month: "April", desktop: 1800000 }, + { month: "May", desktop: 2500000 }, +]; + +const icons = { + desktop: Monitor, + mobile: TabletSmartphone, +} as const; + +const meta: Meta> = { + title: "Components/Charts/BarChartV3", + component: BarChartV3, + parameters: { + layout: "centered", + docs: { + description: { + component: + "```tsx\nimport { BarChartV3 } from '@crayon-ui/react-ui/Charts/BarChartV3';\n```", + }, + }, + }, + tags: ["!dev", "autodocs"], + argTypes: { + data: { + description: + "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the bars to be plotted.", + control: false, + table: { + type: { summary: "Array>" }, + defaultValue: { summary: "[]" }, + category: "Data", + }, + }, + categoryKey: { + description: + "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", + control: false, + table: { + type: { summary: "keyof T[number]" }, + category: "Data", + }, + }, + theme: { + description: + "The color palette theme for the chart. Each theme provides a different set of colors for the bars.", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + variant: { + description: + "The style of the bar chart. 'grouped' shows bars side by side, while 'stacked' shows bars stacked on top of each other.", + control: "radio", + options: ["grouped", "stacked"], + table: { + defaultValue: { summary: "grouped" }, + category: "Appearance", + }, + }, + radius: { + description: "The radius of the rounded corners of the bars", + control: "number", + table: { + type: { summary: "number" }, + defaultValue: { summary: "4" }, + category: "Appearance", + }, + }, + isAnimationActive: { + description: "Whether to animate the chart when it first renders", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + label: { + description: "Label text that appears next to the total value sum calculation", + control: "text", + table: { + type: { summary: "string" }, + defaultValue: { summary: "undefined" }, + category: "Display", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const BarChartV3Story: Story = { + name: "Bar Chart V3", + args: { + label: "Total sales", + data: barChartData, + categoryKey: "month", + theme: "ocean", + variant: "grouped", + radius: 4, + isAnimationActive: true, + }, + render: (args: BarChartPropsV3) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` +const barChartData = [ + { month: "January", desktop: 1500000 }, + { month: "February", desktop: 2800000 }, + { month: "March", desktop: 2200000 }, + { month: "April", desktop: 1800000 }, + { month: "May", desktop: 2500000 }, +]; + + + + +`, + }, + }, + }, +}; From ffe5d3384613ffd0b627d26316f68ee871d5b2c8 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 20 May 2025 14:25:07 +0530 Subject: [PATCH 009/190] small Fix --- .../src/components/Charts/BarChartV2/BarChartV2.tsx | 3 ++- .../Charts/BarChartV2/stories/barChartV2.stories.tsx | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx index 2e2e8b515..3ec0abc09 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx @@ -247,9 +247,10 @@ export const BarChartV2 = ({ key={key} dataKey={key} fill={color} - radius={radius} + radius={getRadiusArray(variant, radius)} stackId={variant === "stacked" ? "a" : undefined} isAnimationActive={isAnimationActive} + maxBarSize={8} /> ); })} diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx index 2a96e2216..fd6cd8a15 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx @@ -7,9 +7,15 @@ const barChartData = [ { month: "January", desktop: 150, mobile: 90 }, { month: "February", desktop: 280, mobile: 180 }, { month: "March", desktop: 220, mobile: 140 }, - // { month: "April", desktop: 180, mobile: 160 }, - // { month: "May", desktop: 250, mobile: 120 }, - // { month: "June", desktop: 300, mobile: 180 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, + { month: "July", desktop: 350, mobile: 200 }, + { month: "August", desktop: 400, mobile: 220 }, + { month: "September", desktop: 450, mobile: 240 }, + { month: "October", desktop: 500, mobile: 260 }, + { month: "November", desktop: 550, mobile: 280 }, + { month: "December", desktop: 600, mobile: 300 }, ]; const icons = { From e7047386df91129684b088ab70913b1ae6211bdd Mon Sep 17 00:00:00 2001 From: i-subham Date: Tue, 20 May 2025 14:10:03 +0530 Subject: [PATCH 010/190] feat(PieChartV3): Introduce new PieChartV3 component with dynamic resizing and customizable features This commit adds the PieChartV3 component to the react-ui package, enhancing charting capabilities with the following features: - Supports pie and donut chart variants. - Implements dynamic resizing to maintain aspect ratio based on container width. - Allows customizable themes, data formatting options, and legend display. - Integrates with existing chart utilities for improved data visualization. - Includes Storybook stories for demonstration and testing. These enhancements aim to provide developers with a flexible and visually appealing way to represent data in their applications. --- .../Charts/BarChartV2/BarChartV2.tsx | 2 +- .../Charts/BarChartV3/BarChartV3.tsx | 2 +- .../Charts/PieChartV3/PieChartV3.tsx | 153 ++++++++++++++++ .../PieChartV3/stories/PieChartV3.stories.tsx | 173 ++++++++++++++++++ .../src/components/Charts/charts.scss | 1 + .../utils/index.ts => utils/BarChartUtils.ts} | 0 6 files changed, 329 insertions(+), 2 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/PieChartV3/PieChartV3.tsx create mode 100644 js/packages/react-ui/src/components/Charts/PieChartV3/stories/PieChartV3.stories.tsx rename js/packages/react-ui/src/components/Charts/{BarChartV2/utils/index.ts => utils/BarChartUtils.ts} (100%) diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx index 3ec0abc09..a810e22ae 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx @@ -11,8 +11,8 @@ import { keyTransform, } from "../Charts"; import { cartesianGrid } from "../cartesianGrid"; +import { getPadding, getRadiusArray, getWidthOfData } from "../utils/BarChartUtils"; import { getDistributedColors, getPalette } from "../utils/PalletUtils"; -import { getPadding, getRadiusArray, getWidthOfData } from "./utils"; export type BarChartData = Array>; diff --git a/js/packages/react-ui/src/components/Charts/BarChartV3/BarChartV3.tsx b/js/packages/react-ui/src/components/Charts/BarChartV3/BarChartV3.tsx index 3ad390bbc..bf8526a47 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartV3/BarChartV3.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChartV3/BarChartV3.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { Bar, BarChart, XAxis } from "recharts"; -import { getPadding, getWidthOfData } from "../BarChartV2/utils"; import { ChartConfig, ChartContainer, keyTransform } from "../Charts"; +import { getPadding, getWidthOfData } from "../utils/BarChartUtils"; import { getDistributedColors, getPalette } from "../utils/PalletUtils"; export type BarChartV3Data = Array>; diff --git a/js/packages/react-ui/src/components/Charts/PieChartV3/PieChartV3.tsx b/js/packages/react-ui/src/components/Charts/PieChartV3/PieChartV3.tsx new file mode 100644 index 000000000..1ab586154 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieChartV3/PieChartV3.tsx @@ -0,0 +1,153 @@ +import clsx from "clsx"; +import { debounce } from "lodash-es"; +import { useEffect, useRef, useState } from "react"; +import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; +import { useLayoutContext } from "../../../context/LayoutContext"; +import { ChartConfig, ChartContainer } from "../Charts"; +import { getDistributedColors, getPalette } from "../utils/PalletUtils"; + +export type PieChartV3Data = Array>; + +export interface PieChartV3Props { + data: T; + categoryKey: keyof T[number]; + dataKey: keyof T[number]; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + variant?: "pie" | "donut"; + format?: "percentage" | "number"; + legend?: boolean; + label?: boolean; + isAnimationActive?: boolean; +} + +const layoutMap: Record = { + mobile: "crayon-pie-chart-container-mobile", + fullscreen: "crayon-pie-chart-container-fullscreen", + tray: "crayon-pie-chart-container-tray", + copilot: "crayon-pie-chart-container-copilot", +}; + +// Helper function to calculate percentage +const calculatePercentage = (value: number, total: number): number => { + if (total === 0) { + return 0; + } + return Number(((value / total) * 100).toFixed(2)); +}; + +// Dynamic resize function to maintain aspect ratio +const calculateChartDimensions = ( + width: number, + variant: string, + label: boolean, +): { outerRadius: number; innerRadius: number } => { + const baseRadiusPercentage = 0.4; // 40% of container width + + let outerRadius = Math.round(width * baseRadiusPercentage); + + if (label) { + outerRadius = Math.round(outerRadius * 0.9); + } + + // Set minimum and maximum bounds for radius + outerRadius = Math.max(50, Math.min(outerRadius, width / 2 - 10)); // Ensure radius isn't too small or too large + + // Calculate inner radius for donut variant - consistent ratio regardless of layout + let innerRadius = 0; + if (variant === "donut") { + innerRadius = Math.round(outerRadius * 0.6); + } + + return { outerRadius, innerRadius }; +}; + +export const PieChartV3 = ({ + data, + categoryKey, + dataKey, + theme = "ocean", + variant = "pie", + format = "number", + label = true, + isAnimationActive = true, +}: PieChartV3Props) => { + const { layout } = useLayoutContext(); + const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(120); + const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(0); + const containerRef = useRef(null); + + // Calculate dynamic radius + useEffect(() => { + if (!containerRef.current) return; + + const resizeObserver = new ResizeObserver( + debounce((entries: any) => { + const { width } = entries[0].contentRect; + + // Use the dynamic resize function to calculate radii + const { outerRadius, innerRadius } = calculateChartDimensions(width, variant, label); + + setCalculatedOuterRadius(outerRadius); + setCalculatedInnerRadius(innerRadius); + }, 100), + ); + + resizeObserver.observe(containerRef.current); + return () => resizeObserver.disconnect(); + }, [variant, label]); + + // Calculate total for percentage calculations + const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); + + // Transform data with percentages + const transformedData = data.map((item) => ({ + ...item, + percentage: calculatePercentage(Number(item[dataKey as string]), total), + originalValue: item[dataKey as string], + })); + + // Get color palette and distribute colors + const palette = getPalette(theme); + const colors = getDistributedColors(palette, data.length); + + // Create chart configuration + const chartConfig = data.reduce( + (config, item, index) => ({ + ...config, + [String(item[categoryKey])]: { + label: String(item[categoryKey as string]), + color: colors[index], + }, + }), + {}, + ); + + return ( + + + + {Object.entries(chartConfig).map(([key, config]) => ( + + ))} + + + + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/PieChartV3/stories/PieChartV3.stories.tsx b/js/packages/react-ui/src/components/Charts/PieChartV3/stories/PieChartV3.stories.tsx new file mode 100644 index 000000000..06a87999d --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieChartV3/stories/PieChartV3.stories.tsx @@ -0,0 +1,173 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Card } from "../../../Card"; +import { PieChartV3, PieChartV3Props } from "../PieChartV3"; + +const pieChartData = [ + { month: "January", value: 4250 }, + { month: "February", value: 3820 }, + { month: "March", value: 4680 }, + { month: "April", value: 4120 }, + { month: "May", value: 5340 }, + { month: "June", value: 6250 }, + { month: "July", value: 5890 }, +]; + +const meta: Meta> = { + title: "Components/Charts/PieChartV3", + component: PieChartV3, + parameters: { + layout: "centered", + docs: { + description: { + component: "```tsx\nimport { PieChart } from '@crayon-ui/react-ui/Charts/PieChart';\n```", + }, + }, + }, + tags: ["!dev", "autodocs"], + + argTypes: { + data: { + description: + "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", + control: false, + table: { + type: { summary: "Array>" }, + defaultValue: { summary: "[]" }, + category: "Data", + }, + }, + categoryKey: { + description: + "The key from your data object to be used as the segment labels (e.g., 'month', 'category', 'name')", + control: false, + table: { + type: { summary: "string" }, + category: "Data", + }, + }, + dataKey: { + description: + "The key from your data object to be used as the values that determine the slice sizes (e.g., 'value', 'count', 'amount')", + control: false, + table: { + type: { summary: "string" }, + category: "Data", + }, + }, + theme: { + description: + "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + variant: { + description: + "The style of the pie chart. 'pie' shows a pie chart, while 'donut' shows a donut chart.", + control: "radio", + options: ["pie", "donut"], + table: { + defaultValue: { summary: "pie" }, + category: "Appearance", + }, + }, + format: { + description: + "The format of the data. 'percentage' shows the data as a percentage, while 'number' shows the data as a number.", + control: "radio", + options: ["percentage", "number"], + table: { + defaultValue: { summary: "percentage" }, + category: "Display", + }, + }, + legend: { + description: "Whether to display the legend", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + label: { + description: "Whether to display the data point labels above each point on the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + isAnimationActive: { + description: "Whether to animate the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const PieChartStory: Story = { + name: "Pie Chart", + args: { + data: pieChartData, + categoryKey: "month", + dataKey: "value", + theme: "ocean", + variant: "pie", + format: "number", + legend: true, + label: true, + isAnimationActive: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` + const pieChartData = [ + { month: "January", value: 400 }, + { month: "February", value: 300 }, + { month: "March", value: 300 }, + { month: "April", value: 400 }, + { month: "May", value: 300 }, + { month: "June", value: 300 }, + { month: "July", value: 300 }, +]; + + + + + `, + }, + }, + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index e4b13fb0f..dd3eae8ac 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -226,6 +226,7 @@ &-container { margin-left: auto; margin-right: auto; + aspect-ratio: 1/1; &-fullscreen { min-height: 400px; diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/utils/index.ts b/js/packages/react-ui/src/components/Charts/utils/BarChartUtils.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarChartV2/utils/index.ts rename to js/packages/react-ui/src/components/Charts/utils/BarChartUtils.ts From 1a24c3737b2503118682c53996b2d855f3358818 Mon Sep 17 00:00:00 2001 From: i-subham Date: Tue, 20 May 2025 14:23:33 +0530 Subject: [PATCH 011/190] feat(BarChartV2): Introduce BarChartV2 component with enhanced features This commit adds the BarChartV2 component to the react-ui package, providing an improved version of the bar chart with several new features: - Supports both grouped and stacked bar chart variants. - Implements responsive design using ResizeObserver for dynamic width adjustments. - Allows customizable themes, bar radius, and display options for labels and legends. - Includes a total value display next to the chart. - Storybook stories for demonstration and testing. These enhancements aim to offer developers a more flexible and visually appealing way to represent data in their applications. --- .../{ => BarCharts}/BarChartV2/BarChartV2.tsx | 10 +++++----- .../Charts/{ => BarCharts}/BarChartV2/index.ts | 0 .../BarChartV2/stories/barChartV2.stories.tsx | 4 ++-- .../MiniBarChart/MiniBarChart.tsx} | 16 ++++++++-------- .../MiniBarChart}/index.tsx | 0 .../stories/MiniBarChart.stories.tsx} | 18 +++++++++--------- .../MiniPieChart/MiniPieChart.tsx} | 14 +++++++------- .../stories/MiniPieChart.stories.tsx} | 14 +++++++------- .../{ => PieCharts}/PieChartV2/PieChartV2.tsx | 6 +++--- .../Charts/{ => PieCharts}/PieChartV2/index.ts | 0 .../PieChartV2/stories/PieChartV2.stories.tsx | 4 ++-- .../components/Charts/utils/BarChartUtils.ts | 2 +- 12 files changed, 44 insertions(+), 44 deletions(-) rename js/packages/react-ui/src/components/Charts/{ => BarCharts}/BarChartV2/BarChartV2.tsx (95%) rename js/packages/react-ui/src/components/Charts/{ => BarCharts}/BarChartV2/index.ts (100%) rename js/packages/react-ui/src/components/Charts/{ => BarCharts}/BarChartV2/stories/barChartV2.stories.tsx (99%) rename js/packages/react-ui/src/components/Charts/{BarChartV3/BarChartV3.tsx => BarCharts/MiniBarChart/MiniBarChart.tsx} (86%) rename js/packages/react-ui/src/components/Charts/{BarChartV3 => BarCharts/MiniBarChart}/index.tsx (100%) rename js/packages/react-ui/src/components/Charts/{BarChartV3/stories/BarChartV3.stories.tsx => BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx} (88%) rename js/packages/react-ui/src/components/Charts/{PieChartV3/PieChartV3.tsx => PieCharts/MiniPieChart/MiniPieChart.tsx} (90%) rename js/packages/react-ui/src/components/Charts/{PieChartV3/stories/PieChartV3.stories.tsx => PieCharts/MiniPieChart/stories/MiniPieChart.stories.tsx} (93%) rename js/packages/react-ui/src/components/Charts/{ => PieCharts}/PieChartV2/PieChartV2.tsx (97%) rename js/packages/react-ui/src/components/Charts/{ => PieCharts}/PieChartV2/index.ts (100%) rename js/packages/react-ui/src/components/Charts/{ => PieCharts}/PieChartV2/stories/PieChartV2.stories.tsx (97%) diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx similarity index 95% rename from js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx rename to js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index a810e22ae..36359ccd0 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import { Bar, LabelList, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; -import { useLayoutContext } from "../../../context/LayoutContext"; +import { useLayoutContext } from "../../../../context/LayoutContext"; import { ChartConfig, ChartContainer, @@ -9,10 +9,10 @@ import { ChartTooltip, ChartTooltipContent, keyTransform, -} from "../Charts"; -import { cartesianGrid } from "../cartesianGrid"; -import { getPadding, getRadiusArray, getWidthOfData } from "../utils/BarChartUtils"; -import { getDistributedColors, getPalette } from "../utils/PalletUtils"; +} from "../../Charts"; +import { cartesianGrid } from "../../cartesianGrid"; +import { getPadding, getRadiusArray, getWidthOfData } from "../../utils/BarChartUtils"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; export type BarChartData = Array>; diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/index.ts b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarChartV2/index.ts rename to js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/index.ts diff --git a/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx similarity index 99% rename from js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx rename to js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx index fd6cd8a15..14dc82e48 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartV2/stories/barChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Monitor, TabletSmartphone } from "lucide-react"; -import { Card } from "../../../Card"; +import { Card } from "../../../../Card"; import { BarChartPropsV2, BarChartV2 } from "../BarChartV2"; const barChartData = [ @@ -24,7 +24,7 @@ const icons = { } as const; const meta: Meta> = { - title: "Components/Charts/BarChartV2", + title: "Components/Charts/BarCharts/BarChartV2", component: BarChartV2, parameters: { layout: "centered", diff --git a/js/packages/react-ui/src/components/Charts/BarChartV3/BarChartV3.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx similarity index 86% rename from js/packages/react-ui/src/components/Charts/BarChartV3/BarChartV3.tsx rename to js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx index bf8526a47..27fc899b7 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartV3/BarChartV3.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx @@ -1,14 +1,14 @@ import { useEffect, useRef, useState } from "react"; import { Bar, BarChart, XAxis } from "recharts"; -import { ChartConfig, ChartContainer, keyTransform } from "../Charts"; -import { getPadding, getWidthOfData } from "../utils/BarChartUtils"; -import { getDistributedColors, getPalette } from "../utils/PalletUtils"; +import { ChartConfig, ChartContainer, keyTransform } from "../../Charts"; +import { getPadding, getWidthOfData } from "../../utils/BarChartUtils"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; -export type BarChartV3Data = Array>; +export type MiniBarChartData = Array>; export type Variant = "grouped" | "stacked"; -export interface BarChartPropsV3 { +export interface MiniBarChartProps { data: T; categoryKey: keyof T[number]; theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; @@ -18,7 +18,7 @@ export interface BarChartPropsV3 { label?: string; } -export const BarChartV3 = ({ +export const MiniBarChart = ({ data, categoryKey, theme = "ocean", @@ -26,7 +26,7 @@ export const BarChartV3 = ({ radius = 4, isAnimationActive = true, label, -}: BarChartPropsV3) => { +}: MiniBarChartProps) => { // excluding the categoryKey const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); @@ -70,7 +70,7 @@ export const BarChartV3 = ({ const width = getWidthOfData(data, categoryKey as string); // Helper function to calculate total - const calculateTotal = ( + const calculateTotal = ( data: T, categoryKey: keyof T[number], ): number => { diff --git a/js/packages/react-ui/src/components/Charts/BarChartV3/index.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarChartV3/index.tsx rename to js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.tsx diff --git a/js/packages/react-ui/src/components/Charts/BarChartV3/stories/BarChartV3.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx similarity index 88% rename from js/packages/react-ui/src/components/Charts/BarChartV3/stories/BarChartV3.stories.tsx rename to js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx index 0c647a203..a6afd58f4 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartV3/stories/BarChartV3.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Monitor, TabletSmartphone } from "lucide-react"; -import { Card } from "../../../Card"; -import { BarChartPropsV3, BarChartV3 } from "../BarChartV3"; +import { Card } from "../../../../Card"; +import { MiniBarChart, MiniBarChartProps } from "../MiniBarChart"; const barChartData = [ { month: "January", desktop: 1500000 }, @@ -16,15 +16,15 @@ const icons = { mobile: TabletSmartphone, } as const; -const meta: Meta> = { - title: "Components/Charts/BarChartV3", - component: BarChartV3, +const meta: Meta> = { + title: "Components/Charts/BarCharts/MiniBarChart", + component: MiniBarChart, parameters: { layout: "centered", docs: { description: { component: - "```tsx\nimport { BarChartV3 } from '@crayon-ui/react-ui/Charts/BarChartV3';\n```", + "```tsx\nimport { MiniBarChart } from '@crayon-ui/react-ui/Charts/BarCharts/MiniBarChart';\n```", }, }, }, @@ -97,7 +97,7 @@ const meta: Meta> = { }, }, }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; @@ -113,9 +113,9 @@ export const BarChartV3Story: Story = { radius: 4, isAnimationActive: true, }, - render: (args: BarChartPropsV3) => ( + render: (args: MiniBarChartProps) => ( - + ), parameters: { diff --git a/js/packages/react-ui/src/components/Charts/PieChartV3/PieChartV3.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx similarity index 90% rename from js/packages/react-ui/src/components/Charts/PieChartV3/PieChartV3.tsx rename to js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx index 1ab586154..35df2192e 100644 --- a/js/packages/react-ui/src/components/Charts/PieChartV3/PieChartV3.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx @@ -2,13 +2,13 @@ import clsx from "clsx"; import { debounce } from "lodash-es"; import { useEffect, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; -import { useLayoutContext } from "../../../context/LayoutContext"; -import { ChartConfig, ChartContainer } from "../Charts"; -import { getDistributedColors, getPalette } from "../utils/PalletUtils"; +import { useLayoutContext } from "../../../../context/LayoutContext"; +import { ChartConfig, ChartContainer } from "../../Charts"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; -export type PieChartV3Data = Array>; +export type MiniPieChartData = Array>; -export interface PieChartV3Props { +export interface MiniPieChartProps { data: T; categoryKey: keyof T[number]; dataKey: keyof T[number]; @@ -61,7 +61,7 @@ const calculateChartDimensions = ( return { outerRadius, innerRadius }; }; -export const PieChartV3 = ({ +export const MiniPieChart = ({ data, categoryKey, dataKey, @@ -70,7 +70,7 @@ export const PieChartV3 = ({ format = "number", label = true, isAnimationActive = true, -}: PieChartV3Props) => { +}: MiniPieChartProps) => { const { layout } = useLayoutContext(); const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(120); const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(0); diff --git a/js/packages/react-ui/src/components/Charts/PieChartV3/stories/PieChartV3.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/stories/MiniPieChart.stories.tsx similarity index 93% rename from js/packages/react-ui/src/components/Charts/PieChartV3/stories/PieChartV3.stories.tsx rename to js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/stories/MiniPieChart.stories.tsx index 06a87999d..e8a364153 100644 --- a/js/packages/react-ui/src/components/Charts/PieChartV3/stories/PieChartV3.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/stories/MiniPieChart.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Card } from "../../../Card"; -import { PieChartV3, PieChartV3Props } from "../PieChartV3"; +import { Card } from "../../../../Card"; +import { MiniPieChart, MiniPieChartProps } from "../MiniPieChart"; const pieChartData = [ { month: "January", value: 4250 }, @@ -12,9 +12,9 @@ const pieChartData = [ { month: "July", value: 5890 }, ]; -const meta: Meta> = { - title: "Components/Charts/PieChartV3", - component: PieChartV3, +const meta: Meta> = { + title: "Components/Charts/PieCharts/MiniPieChart", + component: MiniPieChart, parameters: { layout: "centered", docs: { @@ -112,7 +112,7 @@ const meta: Meta> = { }, }, }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; @@ -132,7 +132,7 @@ export const PieChartStory: Story = { }, render: (args) => ( - + ), parameters: { diff --git a/js/packages/react-ui/src/components/Charts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx similarity index 97% rename from js/packages/react-ui/src/components/Charts/PieChartV2/PieChartV2.tsx rename to js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 6e048e085..9785f40d7 100644 --- a/js/packages/react-ui/src/components/Charts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -2,7 +2,7 @@ import clsx from "clsx"; import { debounce } from "lodash-es"; import { useEffect, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; -import { useLayoutContext } from "../../../context/LayoutContext"; +import { useLayoutContext } from "../../../../context/LayoutContext"; import { ChartConfig, ChartContainer, @@ -10,8 +10,8 @@ import { ChartLegendContent, ChartTooltip, ChartTooltipContent, -} from "../Charts"; -import { getDistributedColors, getPalette } from "../utils/PalletUtils"; +} from "../../Charts"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; export type PieChartV2Data = Array>; diff --git a/js/packages/react-ui/src/components/Charts/PieChartV2/index.ts b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/PieChartV2/index.ts rename to js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/index.ts diff --git a/js/packages/react-ui/src/components/Charts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx similarity index 97% rename from js/packages/react-ui/src/components/Charts/PieChartV2/stories/PieChartV2.stories.tsx rename to js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index a49350df8..61c593697 100644 --- a/js/packages/react-ui/src/components/Charts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Card } from "../../../Card"; +import { Card } from "../../../../Card"; import { PieChartV2, PieChartV2Props } from "../PieChartV2"; const pieChartData = [ @@ -13,7 +13,7 @@ const pieChartData = [ ]; const meta: Meta> = { - title: "Components/Charts/PieChartV2", + title: "Components/Charts/PieCharts/PieChartV2", component: PieChartV2, parameters: { layout: "centered", diff --git a/js/packages/react-ui/src/components/Charts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/utils/BarChartUtils.ts index 3a3872f15..66c0e1aaa 100644 --- a/js/packages/react-ui/src/components/Charts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/BarChartUtils.ts @@ -1,4 +1,4 @@ -import { Variant } from "../BarChartV2"; +import { Variant } from "../BarCharts/BarChartV2"; const getPadding = ( data: Array>, From f33122aa9cec58eefa6d7034f7f5588577a05cc3 Mon Sep 17 00:00:00 2001 From: i-subham Date: Tue, 20 May 2025 15:03:21 +0530 Subject: [PATCH 012/190] feat(PieChartCard): Introduce PieChartCard component with dynamic features This commit adds the PieChartCard component to the react-ui package, enhancing charting capabilities with the following features: - Supports both pie and donut chart variants. - Implements dynamic resizing based on layout to maintain aspect ratio. - Allows customizable themes, data formatting options, and legend display. - Integrates with existing chart utilities for improved data visualization. - Includes Storybook stories for demonstration and testing. These enhancements aim to provide developers with a flexible and visually appealing way to represent data in their applications. --- .../components/Charts/BarCharts/BarChartV2/BarChartV2.tsx | 2 +- .../Charts/BarCharts/MiniBarChart/MiniBarChart.tsx | 2 +- .../Charts/{ => BarCharts}/utils/BarChartUtils.ts | 2 +- .../{ => PieCharts}/PieChartCard/PieChartCard.stories.tsx | 4 ++-- .../Charts/{ => PieCharts}/PieChartCard/PieChartCard.tsx | 6 +++--- .../components/Charts/{ => PieCharts}/PieChartCard/index.ts | 0 6 files changed, 8 insertions(+), 8 deletions(-) rename js/packages/react-ui/src/components/Charts/{ => BarCharts}/utils/BarChartUtils.ts (96%) rename js/packages/react-ui/src/components/Charts/{ => PieCharts}/PieChartCard/PieChartCard.stories.tsx (98%) rename js/packages/react-ui/src/components/Charts/{ => PieCharts}/PieChartCard/PieChartCard.tsx (97%) rename js/packages/react-ui/src/components/Charts/{ => PieCharts}/PieChartCard/index.ts (100%) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 36359ccd0..4074772cb 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -11,8 +11,8 @@ import { keyTransform, } from "../../Charts"; import { cartesianGrid } from "../../cartesianGrid"; -import { getPadding, getRadiusArray, getWidthOfData } from "../../utils/BarChartUtils"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { getPadding, getRadiusArray, getWidthOfData } from "../utils/BarChartUtils"; export type BarChartData = Array>; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx index 27fc899b7..76faa67c8 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from "react"; import { Bar, BarChart, XAxis } from "recharts"; import { ChartConfig, ChartContainer, keyTransform } from "../../Charts"; -import { getPadding, getWidthOfData } from "../../utils/BarChartUtils"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { getPadding, getWidthOfData } from "../utils/BarChartUtils"; export type MiniBarChartData = Array>; diff --git a/js/packages/react-ui/src/components/Charts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts similarity index 96% rename from js/packages/react-ui/src/components/Charts/utils/BarChartUtils.ts rename to js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts index 66c0e1aaa..3a3872f15 100644 --- a/js/packages/react-ui/src/components/Charts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts @@ -1,4 +1,4 @@ -import { Variant } from "../BarCharts/BarChartV2"; +import { Variant } from "../BarChartV2"; const getPadding = ( data: Array>, diff --git a/js/packages/react-ui/src/components/Charts/PieChartCard/PieChartCard.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.stories.tsx similarity index 98% rename from js/packages/react-ui/src/components/Charts/PieChartCard/PieChartCard.stories.tsx rename to js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.stories.tsx index 3811fdf27..e30a23bd0 100644 --- a/js/packages/react-ui/src/components/Charts/PieChartCard/PieChartCard.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Card } from "../../Card"; +import { Card } from "../../../Card"; import { PieChartCard, PieChartCardProps } from "./PieChartCard"; const pieChartCardData = [ @@ -13,7 +13,7 @@ const pieChartCardData = [ ]; const meta: Meta> = { - title: "Components/Charts/PieChartCard", + title: "Components/Charts/PieCharts/PieChartCard", component: PieChartCard, parameters: { layout: "centered", diff --git a/js/packages/react-ui/src/components/Charts/PieChartCard/PieChartCard.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.tsx similarity index 97% rename from js/packages/react-ui/src/components/Charts/PieChartCard/PieChartCard.tsx rename to js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.tsx index 1edd27923..882a4253a 100644 --- a/js/packages/react-ui/src/components/Charts/PieChartCard/PieChartCard.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.tsx @@ -2,7 +2,7 @@ import clsx from "clsx"; import { debounce } from "lodash-es"; import { useEffect, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; -import { useLayoutContext } from "../../../context/LayoutContext"; +import { useLayoutContext } from "../../../../context/LayoutContext"; import { ChartConfig, ChartContainer, @@ -10,8 +10,8 @@ import { ChartLegendContent, ChartTooltip, ChartTooltipContent, -} from "../Charts"; -import { getDistributedColors, getPalette } from "../utils/PalletUtils"; +} from "../../Charts"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; export type PieChartCardData = Array>; diff --git a/js/packages/react-ui/src/components/Charts/PieChartCard/index.ts b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/PieChartCard/index.ts rename to js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/index.ts From 5eec697e5aa7cb12d71fe227bd7990d4c24819cc Mon Sep 17 00:00:00 2001 From: i-subham Date: Wed, 21 May 2025 15:50:42 +0530 Subject: [PATCH 013/190] refactor(PieCharts): Extract utility functions into PieChartUtils This commit refactors the MiniPieChart and PieChartV2 components by moving the `calculatePercentage`, `calculateChartDimensions`, and `layoutMap` functions into a new utility file, `PieChartUtils.ts`. This change enhances code organization and reusability across pie chart components, promoting cleaner and more maintainable code. --- .../PieCharts/MiniPieChart/MiniPieChart.tsx | 42 +----------------- .../PieCharts/PieChartV2/PieChartV2.tsx | 42 +----------------- .../Charts/PieCharts/utils/PieChartUtils.ts | 44 +++++++++++++++++++ 3 files changed, 46 insertions(+), 82 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx index 35df2192e..698b300d3 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx @@ -5,6 +5,7 @@ import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; import { useLayoutContext } from "../../../../context/LayoutContext"; import { ChartConfig, ChartContainer } from "../../Charts"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { calculateChartDimensions, calculatePercentage, layoutMap } from "../utils/PieChartUtils"; export type MiniPieChartData = Array>; @@ -20,47 +21,6 @@ export interface MiniPieChartProps { isAnimationActive?: boolean; } -const layoutMap: Record = { - mobile: "crayon-pie-chart-container-mobile", - fullscreen: "crayon-pie-chart-container-fullscreen", - tray: "crayon-pie-chart-container-tray", - copilot: "crayon-pie-chart-container-copilot", -}; - -// Helper function to calculate percentage -const calculatePercentage = (value: number, total: number): number => { - if (total === 0) { - return 0; - } - return Number(((value / total) * 100).toFixed(2)); -}; - -// Dynamic resize function to maintain aspect ratio -const calculateChartDimensions = ( - width: number, - variant: string, - label: boolean, -): { outerRadius: number; innerRadius: number } => { - const baseRadiusPercentage = 0.4; // 40% of container width - - let outerRadius = Math.round(width * baseRadiusPercentage); - - if (label) { - outerRadius = Math.round(outerRadius * 0.9); - } - - // Set minimum and maximum bounds for radius - outerRadius = Math.max(50, Math.min(outerRadius, width / 2 - 10)); // Ensure radius isn't too small or too large - - // Calculate inner radius for donut variant - consistent ratio regardless of layout - let innerRadius = 0; - if (variant === "donut") { - innerRadius = Math.round(outerRadius * 0.6); - } - - return { outerRadius, innerRadius }; -}; - export const MiniPieChart = ({ data, categoryKey, diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 9785f40d7..39aead838 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -12,6 +12,7 @@ import { ChartTooltipContent, } from "../../Charts"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { calculateChartDimensions, calculatePercentage, layoutMap } from "../utils/PieChartUtils"; export type PieChartV2Data = Array>; @@ -27,47 +28,6 @@ export interface PieChartV2Props { isAnimationActive?: boolean; } -const layoutMap: Record = { - mobile: "crayon-pie-chart-container-mobile", - fullscreen: "crayon-pie-chart-container-fullscreen", - tray: "crayon-pie-chart-container-tray", - copilot: "crayon-pie-chart-container-copilot", -}; - -// Helper function to calculate percentage -const calculatePercentage = (value: number, total: number): number => { - if (total === 0) { - return 0; - } - return Number(((value / total) * 100).toFixed(2)); -}; - -// Dynamic resize function to maintain aspect ratio -const calculateChartDimensions = ( - width: number, - variant: string, - label: boolean, -): { outerRadius: number; innerRadius: number } => { - const baseRadiusPercentage = 0.4; // 40% of container width - - let outerRadius = Math.round(width * baseRadiusPercentage); - - if (label) { - outerRadius = Math.round(outerRadius * 0.9); - } - - // Set minimum and maximum bounds for radius - outerRadius = Math.max(50, Math.min(outerRadius, width / 2 - 10)); // Ensure radius isn't too small or too large - - // Calculate inner radius for donut variant - consistent ratio regardless of layout - let innerRadius = 0; - if (variant === "donut") { - innerRadius = Math.round(outerRadius * 0.6); - } - - return { outerRadius, innerRadius }; -}; - export const PieChartV2 = ({ data, categoryKey, diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts new file mode 100644 index 000000000..5a902717e --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts @@ -0,0 +1,44 @@ +/** + * Utility functions for pie charts + */ + +// Helper function to calculate percentage +export const calculatePercentage = (value: number, total: number): number => { + if (total === 0) { + return 0; + } + return Number(((value / total) * 100).toFixed(2)); +}; + +// Dynamic resize function to maintain aspect ratio +export const calculateChartDimensions = ( + width: number, + variant: string, + label: boolean, +): { outerRadius: number; innerRadius: number } => { + const baseRadiusPercentage = 0.4; // 40% of container width + + let outerRadius = Math.round(width * baseRadiusPercentage); + + if (label) { + outerRadius = Math.round(outerRadius * 0.9); + } + + // Set minimum and maximum bounds for radius + outerRadius = Math.max(50, Math.min(outerRadius, width / 2 - 10)); // Ensure radius isn't too small or too large + + // Calculate inner radius for donut variant - consistent ratio regardless of layout + let innerRadius = 0; + if (variant === "donut") { + innerRadius = Math.round(outerRadius * 0.6); + } + + return { outerRadius, innerRadius }; +}; + +export const layoutMap: Record = { + mobile: "crayon-pie-chart-container-mobile", + fullscreen: "crayon-pie-chart-container-fullscreen", + tray: "crayon-pie-chart-container-tray", + copilot: "crayon-pie-chart-container-copilot", +}; \ No newline at end of file From 859f850e1a83cb357f339e87e782230a278b382c Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 21 May 2025 17:28:45 +0530 Subject: [PATCH 014/190] feat(bar-chart): Improve bar chart component This commit introduces several improvements to the BarChartV2 component: 1. Calculates the width of the chart content based on the data and the variant (grouped or stacked). 2. Adjusts the padding of the chart container to center the chart content within the available space. 3. Implements custom radius calculations for the stacked bar chart variant to achieve the desired visual effect. 4. Updates the bar chart story with a new data point to showcase the changes. 5. Removes the `legend` prop from the component, as it is currently not being used. These changes aim to enhance the overall appearance and functionality of the BarChartV2 component. --- .../BarCharts/BarChartV2/BarChartV2.tsx | 124 +++++------------- .../BarChartV2/stories/barChartV2.stories.tsx | 43 +++--- .../Charts/BarCharts/utils/BarChartUtils.ts | 80 +++++++---- .../PieCharts/PieChartV2/PieChartV2.tsx | 44 +++---- .../Charts/PieCharts/utils/PieChartUtils.ts | 2 +- 5 files changed, 139 insertions(+), 154 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 4074772cb..0941f5877 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -1,11 +1,8 @@ import React, { useEffect, useRef, useState } from "react"; import { Bar, LabelList, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; -import { useLayoutContext } from "../../../../context/LayoutContext"; import { ChartConfig, ChartContainer, - ChartLegend, - ChartLegendContent, ChartTooltip, ChartTooltipContent, keyTransform, @@ -25,13 +22,13 @@ export interface BarChartPropsV2 { variant?: Variant; grid?: boolean; label?: boolean; - legend?: boolean; radius?: number; icons?: Partial>; isAnimationActive?: boolean; showYAxis?: boolean; xAxisLabel?: React.ReactNode; yAxisLabel?: React.ReactNode; + onBarsClick?: (data: any) => void; } export const BarChartV2 = ({ @@ -41,20 +38,19 @@ export const BarChartV2 = ({ variant = "grouped", grid = true, label = true, - legend = true, icons = {}, - radius = 4, + radius = 2, isAnimationActive = true, showYAxis = false, xAxisLabel, yAxisLabel, + onBarsClick, }: BarChartPropsV2) => { // excluding the categoryKey const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); const palette = getPalette(theme); const colors = getDistributedColors(palette, dataKeys.length); - const { layout } = useLayoutContext(); // Create Config const chartConfig: ChartConfig = dataKeys.reduce( @@ -69,85 +65,16 @@ export const BarChartV2 = ({ {}, ); - const getTickFormatter = (data: T) => { - const dataLength = data.length; - const maxLengthMap = { - mobile: { - default: 5, - 10: 4, - 11: 4, - }, - tray: { - default: 5, - 8: 4, - 9: 4, - 10: 4, - 11: 4, - }, - copilot: { - default: 5, - 8: 4, - 9: 4, - 10: 4, - 11: 4, - }, - fullscreen: { - default: 5, - 11: 4, - }, - }; - - const layoutConfig = - maxLengthMap[layout as keyof typeof maxLengthMap] || maxLengthMap.fullscreen; - - const maxLength = - dataLength >= 11 - ? 4 - : layoutConfig[dataLength as keyof typeof layoutConfig] || layoutConfig.default; + const getTickFormatter = () => { + const maxLength = 3; return (value: string) => { if (value.length > maxLength) { - return `${value.slice(0, maxLength)}...`; + return `${value.slice(0, maxLength)}`; } return value; }; }; - const getAxisAngle = (data: T) => { - const angleConfig = { - mobile: { - default: 0, - ranges: [ - { min: 6, max: 9, angle: -45 }, - { min: 10, max: 10, angle: -60 }, - { min: 11, max: Infinity, angle: -75 }, - ], - }, - tray: { - default: 0, - ranges: [{ min: 8, max: Infinity, angle: -45 }], - }, - copilot: { - default: 0, - ranges: [{ min: 8, max: Infinity, angle: -45 }], - }, - fullscreen: { - default: 0, - ranges: [{ min: 12, max: Infinity, angle: -45 }], - }, - }; - - const layoutConfig = angleConfig[layout as keyof typeof angleConfig] || angleConfig.fullscreen; - const dataLength = data.length; - - const matchRange = layoutConfig.ranges.find( - (range) => dataLength >= range.min && dataLength <= range.max, - ); - - return matchRange?.angle ?? layoutConfig.default; - }; - const getTickMargin = (data: T) => { - return data.length <= 6 ? 10 : 15; - }; const chartContainerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(0); @@ -170,9 +97,8 @@ export const BarChartV2 = ({ }; }, []); - const padding = getPadding(data, categoryKey as string, containerWidth); - const width = getWidthOfData(data, categoryKey as string); - console.log(width); + const padding = getPadding(data, categoryKey as string, containerWidth, variant); + const width = getWidthOfData(data, categoryKey as string, variant); return (
@@ -186,47 +112,59 @@ export const BarChartV2 = ({ data={data} margin={{ top: label ? 30 : 20, + bottom: xAxisLabel ? 20 : 0, }} + onClick={onBarsClick} > {grid && cartesianGrid()} {showYAxis && ( )} } /> - {dataKeys.map((key) => { + {dataKeys.map((key, index) => { const transformedKey = keyTransform(key); const color = `var(--color-${transformedKey})`; + const isFirstInStack = index === 0; + const isLastInStack = index === dataKeys.length - 1; + if (label) { return ( ({ key={key} dataKey={key} fill={color} - radius={getRadiusArray(variant, radius)} + radius={getRadiusArray( + variant, + radius, + variant === "stacked" ? isFirstInStack : undefined, + variant === "stacked" ? isLastInStack : undefined, + )} stackId={variant === "stacked" ? "a" : undefined} isAnimationActive={isAnimationActive} maxBarSize={8} /> ); })} - {legend && } />}
diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx index 14dc82e48..d52821b6c 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx @@ -4,7 +4,7 @@ import { Card } from "../../../../Card"; import { BarChartPropsV2, BarChartV2 } from "../BarChartV2"; const barChartData = [ - { month: "January", desktop: 150, mobile: 90 }, + { month: "January", desktop: 45, mobile: 90 }, { month: "February", desktop: 280, mobile: 180 }, { month: "March", desktop: 220, mobile: 140 }, { month: "April", desktop: 180, mobile: 160 }, @@ -35,7 +35,7 @@ const meta: Meta> = { }, }, }, - tags: ["!dev", "autodocs"], + tags: ["dev", "autodocs"], argTypes: { data: { description: @@ -114,16 +114,16 @@ const meta: Meta> = { category: "Display", }, }, - legend: { - description: - "Whether to display the chart legend showing the data series names and their corresponding colors/icons", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, + // legend: { + // description: + // "Whether to display the chart legend showing the data series names and their corresponding colors/icons", + // control: "boolean", + // table: { + // type: { summary: "boolean" }, + // defaultValue: { summary: "true" }, + // category: "Display", + // }, + // }, isAnimationActive: { description: "Whether to animate the chart", control: "boolean", @@ -173,10 +173,9 @@ export const BarChartV2Story: Story = { categoryKey: "month", theme: "ocean", variant: "grouped", - radius: 4, + radius: 2, grid: true, label: true, - legend: true, isAnimationActive: true, showYAxis: false, }, @@ -273,7 +272,7 @@ const icons = { }; export const BarChartV2WithYAxis: Story = { - name: "Bar Chart V2 with Y-Axis and Axis Labels", + name: "Bar Chart V2 with Y-Axis and X-Axis Labels", args: { ...BarChartV2Story.args, showYAxis: true, @@ -407,3 +406,17 @@ const barChartData = [ }, }, }; + +export const BarChartV2Experimental: Story = { + name: "Bar Chart V2 Experimental", + args: { + ...BarChartV2Story.args, + theme: "emerald", + variant: "grouped", + }, + render: (args) => ( + + console.log(data)} /> + + ), +}; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts index 3a3872f15..2795c7349 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts @@ -1,48 +1,78 @@ import { Variant } from "../BarChartV2"; +const BAR_WIDTH = 8; +const ELEMENT_SPACING = 20; // Spacing per bar in grouped, or per stack in stacked + +const getWidthOfData = ( + data: Array>, + categoryKey: string, + variant: Variant, +) => { + let numberOfElements: number; + + if (variant === "stacked") { + numberOfElements = data.length; // Number of stacks = number of categories + } else { + // Default to "grouped" behavior + const seriesCountsPerCategory = data.map((ob) => { + return Object.keys(ob).filter((key) => key !== categoryKey).length; + }); + numberOfElements = seriesCountsPerCategory.reduce((acc, curr) => acc + curr, 0); + } + + const width = numberOfElements * (BAR_WIDTH + ELEMENT_SPACING); + return width; +}; + const getPadding = ( data: Array>, categoryKey: string, containerWidth: number, + variant: Variant, ) => { - const numberOfSticks = data.map((ob) => { - const datapoint = Object.keys(ob).filter((key) => key !== categoryKey).length; - return datapoint; - }); - const totalSticks = numberOfSticks.reduce((acc, curr) => acc + curr, 0); - const chartWidth = totalSticks * 8 + totalSticks * 20; - const padding = containerWidth - chartWidth; - - if (padding / 2 < 0) { + const chartWidth = getWidthOfData(data, categoryKey, variant); + const paddingValue = containerWidth - chartWidth; + + if (paddingValue < 0) { + // If chart content is wider than container, no padding return { left: 0, right: 0, }; } else { return { - left: padding / 2, - right: padding / 2, + left: paddingValue / 2, + right: paddingValue / 2, }; } }; -const getWidthOfData = (data: Array>, categoryKey: string) => { - const sticks = data.map((ob) => { - const datapoint = Object.keys(ob).filter((key) => key !== categoryKey).length; - return datapoint; - }); - - const totalSticks = sticks.reduce((acc, curr) => acc + curr, 0); - const width = totalSticks * 8 + totalSticks * 20; - return width; -}; - -const getRadiusArray = (variant: Variant, radius: number): [number, number, number, number] => { +const getRadiusArray = ( + variant: Variant, + radius: number, + isFirst?: boolean, + isLast?: boolean, +): [number, number, number, number] => { if (variant === "grouped") { return [radius, radius, 0, 0]; - } else { - return [radius, radius, radius, radius]; + } else if (variant === "stacked") { + if (isFirst && isLast) { + // Single item in stack + return [radius, radius, radius, radius]; + } + if (isFirst) { + // Bottom of the stack + return [0, 0, 0, 0]; + } + if (isLast) { + // Top of the stack + return [radius, radius, 0, 0]; + } + // Middle of the stack + return [0, 0, 0, 0]; } + // Default or other variants + return [radius, radius, radius, radius]; }; export { getPadding, getRadiusArray, getWidthOfData }; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 39aead838..17b65842b 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -91,29 +91,29 @@ export const PieChartV2 = ({ ); // Custom label renderer - const renderCustomLabel = ({ payload, cx, cy, x, y, textAnchor, dominantBaseline }: any) => { - if (payload.percentage <= 10) return null; - const displayValue = format === "percentage" ? payload.percentage : payload[dataKey]; - const formattedValue = - String(displayValue).length > 7 ? `${String(displayValue).slice(0, 7)}...` : displayValue; + // const renderCustomLabel = ({ payload, cx, cy, x, y, textAnchor, dominantBaseline }: any) => { + // if (payload.percentage <= 10) return null; + // const displayValue = format === "percentage" ? payload.percentage : payload[dataKey]; + // const formattedValue = + // String(displayValue).length > 7 ? `${String(displayValue).slice(0, 7)}...` : displayValue; - return ( - - - {formattedValue} - {format === "percentage" ? "%" : ""} - - - ); - }; + // return ( + // + // + // {formattedValue} + // {format === "percentage" ? "%" : ""} + // + // + // ); + // }; return ( = { fullscreen: "crayon-pie-chart-container-fullscreen", tray: "crayon-pie-chart-container-tray", copilot: "crayon-pie-chart-container-copilot", -}; \ No newline at end of file +}; From 5eda8a65e6d37203deffedacd4d09281fa4f9481 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 21 May 2025 17:31:22 +0530 Subject: [PATCH 015/190] feat(MiniBarChart): Enhance padding and width calculations Improve the calculation of padding and width for the MiniBarChart component based on the provided data and the chart variant. This ensures the chart is properly sized and positioned within the container, providing a better visual experience for users. --- .../components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx index 76faa67c8..13b8c738e 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx @@ -66,8 +66,8 @@ export const MiniBarChart = ({ }; }, []); - const padding = getPadding(data, categoryKey as string, containerWidth); - const width = getWidthOfData(data, categoryKey as string); + const padding = getPadding(data, categoryKey as string, containerWidth, variant); + const width = getWidthOfData(data, categoryKey as string, variant); // Helper function to calculate total const calculateTotal = ( From c018c42b6ab583ec39348dab74cfe4a065b5d24a Mon Sep 17 00:00:00 2001 From: i-subham Date: Wed, 21 May 2025 19:07:53 +0530 Subject: [PATCH 016/190] feat(PieCharts): Add TwoLevelPieChart component with dynamic resizing and customizable features This commit introduces the TwoLevelPieChart component to the react-ui package, enhancing charting capabilities with the following features: - Supports two-level pie chart visualization. - Implements dynamic resizing based on container width to maintain aspect ratio. - Allows customizable themes, data formatting options, and legend display. - Includes Storybook stories for demonstration and testing. These enhancements aim to provide developers with a flexible and visually appealing way to represent hierarchical data in their applications. --- .../Charts/PieCharts/MiniPieChart/index.ts | 1 + .../TwoLevelPieChart/TwoLevelPieChart.tsx | 138 ++++++++++++++ .../PieCharts/TwoLevelPieChart/index.ts | 1 + .../stories/TwoLevelPieChart.stories.tsx | 173 ++++++++++++++++++ .../Charts/PieCharts/utils/PieChartUtils.ts | 25 +++ 5 files changed, 338 insertions(+) create mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx create mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/index.ts b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/index.ts new file mode 100644 index 000000000..62e639961 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/index.ts @@ -0,0 +1 @@ +export * from "./MiniPieChart"; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx new file mode 100644 index 000000000..2540a79bf --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx @@ -0,0 +1,138 @@ +import clsx from "clsx"; +import { debounce } from "lodash-es"; +import { useEffect, useRef, useState } from "react"; +import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; +import { useLayoutContext } from "../../../../context/LayoutContext"; +import { ChartConfig, ChartContainer } from "../../Charts"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { + calculatePercentage, + calculateTwoLevelChartDimensions, + layoutMap, +} from "../utils/PieChartUtils"; + +export type TwoLevelPieChartData = Array>; + +export interface TwoLevelPieChartProps { + data: T; + categoryKey: keyof T[number]; + dataKey: keyof T[number]; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + format?: "percentage" | "number"; + appearance?: "circular" | "semiCircular"; + legend?: boolean; + label?: boolean; + isAnimationActive?: boolean; +} + +export const TwoLevelPieChart = ({ + data, + categoryKey, + dataKey, + theme = "ocean", + format = "number", + appearance = "circular", + label = true, + isAnimationActive = true, +}: TwoLevelPieChartProps) => { + const { layout } = useLayoutContext(); + const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(160); + const [calculatedMiddleRadius, setCalculatedMiddleRadius] = useState(140); + const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(40); + const containerRef = useRef(null); + + // Calculate dynamic radius + useEffect(() => { + if (!containerRef.current) return; + + const resizeObserver = new ResizeObserver( + debounce((entries: any) => { + const { width } = entries[0].contentRect; + + // Use the two-level chart dimension calculation function + const { outerRadius, middleRadius, innerRadius } = calculateTwoLevelChartDimensions( + width, + label, + ); + + setCalculatedOuterRadius(outerRadius); + setCalculatedMiddleRadius(middleRadius); + setCalculatedInnerRadius(innerRadius); + }, 100), + ); + + resizeObserver.observe(containerRef.current); + return () => resizeObserver.disconnect(); + }, [label]); + + // Calculate total for percentage calculations + const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); + + // Transform data with percentages + const transformedData = data.map((item) => ({ + ...item, + percentage: calculatePercentage(Number(item[dataKey as string]), total), + originalValue: item[dataKey as string], + })); + + // Get color palette and distribute colors + const palette = getPalette(theme); + const colors = getDistributedColors(palette, data.length); + + // Create chart configuration + const chartConfig = data.reduce( + (config, item, index) => ({ + ...config, + [String(item[categoryKey])]: { + label: String(item[categoryKey as string]), + color: colors[index], + }, + }), + {}, + ); + + return ( + + + + {Object.entries(chartConfig).map(([key, config]) => ( + + ))} + + + {Object.entries(chartConfig).map(([key, config]) => ( + + ))} + + + + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/index.ts b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/index.ts new file mode 100644 index 000000000..e2f5aa607 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/index.ts @@ -0,0 +1 @@ +export * from "./TwoLevelPieChart"; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx new file mode 100644 index 000000000..02826ad0f --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx @@ -0,0 +1,173 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Card } from "../../../../Card"; +import { TwoLevelPieChart, TwoLevelPieChartProps } from "../TwoLevelPieChart"; + +const pieChartData = [ + { month: "January", value: 4250 }, + { month: "February", value: 3820 }, + { month: "March", value: 4680 }, + { month: "April", value: 4120 }, + { month: "May", value: 5340 }, + { month: "June", value: 6250 }, + { month: "July", value: 5890 }, +]; + +const meta: Meta> = { + title: "Components/Charts/PieCharts/TwoLevelPieChart", + component: TwoLevelPieChart, + parameters: { + layout: "centered", + docs: { + description: { + component: "```tsx\nimport { PieChart } from '@crayon-ui/react-ui/Charts/PieChart';\n```", + }, + }, + }, + tags: ["!dev", "autodocs"], + + argTypes: { + data: { + description: + "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", + control: false, + table: { + type: { summary: "Array>" }, + defaultValue: { summary: "[]" }, + category: "Data", + }, + }, + categoryKey: { + description: + "The key from your data object to be used as the segment labels (e.g., 'month', 'category', 'name')", + control: false, + table: { + type: { summary: "string" }, + category: "Data", + }, + }, + dataKey: { + description: + "The key from your data object to be used as the values that determine the slice sizes (e.g., 'value', 'count', 'amount')", + control: false, + table: { + type: { summary: "string" }, + category: "Data", + }, + }, + theme: { + description: + "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + appearance: { + description: + "The appearance of the chart. 'circular' shows a full circle, while 'semiCircular' shows a half circle.", + control: "radio", + options: ["circular", "semiCircular"], + table: { + defaultValue: { summary: "circular" }, + category: "Appearance", + }, + }, + format: { + description: + "The format of the data. 'percentage' shows the data as a percentage, while 'number' shows the data as a number.", + control: "radio", + options: ["percentage", "number"], + table: { + defaultValue: { summary: "percentage" }, + category: "Display", + }, + }, + legend: { + description: "Whether to display the legend", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + label: { + description: "Whether to display the data point labels above each point on the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + isAnimationActive: { + description: "Whether to animate the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const PieChartStory: Story = { + name: "Pie Chart", + args: { + data: pieChartData, + categoryKey: "month", + dataKey: "value", + theme: "ocean", + appearance: "circular", + format: "number", + legend: true, + label: true, + isAnimationActive: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` + const pieChartData = [ + { month: "January", value: 400 }, + { month: "February", value: 300 }, + { month: "March", value: 300 }, + { month: "April", value: 400 }, + { month: "May", value: 300 }, + { month: "June", value: 300 }, + { month: "July", value: 300 }, +]; + + + + + `, + }, + }, + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts index 92ae0d721..257e5c43a 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts @@ -36,6 +36,31 @@ export const calculateChartDimensions = ( return { outerRadius, innerRadius }; }; +// Dynamic resize function for two-level pie chart +export const calculateTwoLevelChartDimensions = ( + width: number, + label: boolean, +): { outerRadius: number; middleRadius: number; innerRadius: number } => { + const baseRadiusPercentage = 0.4; // 40% of container width + + let outerRadius = Math.round(width * baseRadiusPercentage); + + if (label) { + outerRadius = Math.round(outerRadius * 0.9); + } + + // Set minimum and maximum bounds for radius + outerRadius = Math.max(50, Math.min(outerRadius, width / 2 - 10)); + + // Calculate middle radius (inner ring's outer boundary) + const middleRadius = Math.round(outerRadius * 0.85); + + // Calculate inner radius - always has a value in two-level chart + const innerRadius = Math.round(middleRadius * 0.28); + + return { outerRadius, middleRadius, innerRadius }; +}; + export const layoutMap: Record = { mobile: "crayon-pie-chart-container-mobile", fullscreen: "crayon-pie-chart-container-fullscreen", From 1a048348b47f87f7bd470b05e9ee625990ef9557 Mon Sep 17 00:00:00 2001 From: i-subham Date: Wed, 21 May 2025 19:15:36 +0530 Subject: [PATCH 017/190] feat(PieChartV2): Add appearance prop for circular and semi-circular charts This commit introduces an `appearance` prop to the PieChartV2 component, allowing users to choose between a full circular chart and a semi-circular chart. The default value is set to "circular". Additionally, the Storybook stories have been updated to include this new prop with appropriate descriptions and controls for better visualization and testing. --- .../Charts/PieCharts/PieChartV2/PieChartV2.tsx | 6 +++++- .../PieChartV2/stories/PieChartV2.stories.tsx | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 17b65842b..17755b948 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -26,6 +26,7 @@ export interface PieChartV2Props { legend?: boolean; label?: boolean; isAnimationActive?: boolean; + appearance?: "circular" | "semiCircular"; } export const PieChartV2 = ({ @@ -38,6 +39,7 @@ export const PieChartV2 = ({ legend = true, label = true, isAnimationActive = true, + appearance = "circular", }: PieChartV2Props) => { const { layout } = useLayoutContext(); const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(120); @@ -90,7 +92,7 @@ export const PieChartV2 = ({ {}, ); - // Custom label renderer + //Custom label renderer // const renderCustomLabel = ({ payload, cx, cy, x, y, textAnchor, dominantBaseline }: any) => { // if (payload.percentage <= 10) return null; // const displayValue = format === "percentage" ? payload.percentage : payload[dataKey]; @@ -133,6 +135,8 @@ export const PieChartV2 = ({ innerRadius={calculatedInnerRadius} label={false} isAnimationActive={isAnimationActive} + startAngle={appearance === "semiCircular" ? 0 : 0} + endAngle={appearance === "semiCircular" ? 180 : 360} > {Object.entries(chartConfig).map(([key, config]) => ( diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index 61c593697..e04ba1c12 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -64,6 +64,16 @@ const meta: Meta> = { category: "Appearance", }, }, + appearance: { + description: + "The appearance of the chart. 'circular' shows a full circle, while 'semiCircular' shows a half circle.", + control: "radio", + options: ["circular", "semiCircular"], + table: { + defaultValue: { summary: "circular" }, + category: "Appearance", + }, + }, variant: { description: "The style of the pie chart. 'pie' shows a pie chart, while 'donut' shows a donut chart.", From 434003bd2dc9e90282351eef7b8e768775b1396d Mon Sep 17 00:00:00 2001 From: i-subham Date: Wed, 21 May 2025 20:11:43 +0530 Subject: [PATCH 018/190] feat(PieCharts): Enhance PieChartV2 and TwoLevelPieChart with hover effects This commit introduces hover effects to the PieChartV2 and TwoLevelPieChart components by implementing a reusable `useChartHover` hook. The hover effects improve user interaction by highlighting chart segments on mouse events. Additionally, the `getHoverStyles` utility function is added to manage hover styles dynamically, enhancing the visual feedback for users. These changes aim to provide a more engaging and interactive charting experience. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 23 ++++++++++--- .../TwoLevelPieChart/TwoLevelPieChart.tsx | 34 +++++++++++++++---- .../Charts/PieCharts/utils/PieChartUtils.ts | 29 ++++++++++++++++ 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 17755b948..7e3d3a994 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -12,7 +12,13 @@ import { ChartTooltipContent, } from "../../Charts"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; -import { calculateChartDimensions, calculatePercentage, layoutMap } from "../utils/PieChartUtils"; +import { + calculateChartDimensions, + calculatePercentage, + getHoverStyles, + layoutMap, + useChartHover, +} from "../utils/PieChartUtils"; export type PieChartV2Data = Array>; @@ -44,6 +50,7 @@ export const PieChartV2 = ({ const { layout } = useLayoutContext(); const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(120); const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(0); + const { activeIndex, handleMouseEnter, handleMouseLeave } = useChartHover(); const containerRef = useRef(null); // Calculate dynamic radius @@ -137,10 +144,18 @@ export const PieChartV2 = ({ isAnimationActive={isAnimationActive} startAngle={appearance === "semiCircular" ? 0 : 0} endAngle={appearance === "semiCircular" ? 180 : 360} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} > - {Object.entries(chartConfig).map(([key, config]) => ( - - ))} + {transformedData.map((entry, index) => { + const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); + const config = chartConfig[categoryValue]; + const hoverStyles = getHoverStyles(index, activeIndex); + + return ( + + ); + })} diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx index 2540a79bf..4c54cd3d7 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx @@ -8,7 +8,9 @@ import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; import { calculatePercentage, calculateTwoLevelChartDimensions, + getHoverStyles, layoutMap, + useChartHover, } from "../utils/PieChartUtils"; export type TwoLevelPieChartData = Array>; @@ -39,6 +41,7 @@ export const TwoLevelPieChart = ({ const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(160); const [calculatedMiddleRadius, setCalculatedMiddleRadius] = useState(140); const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(40); + const { activeIndex, handleMouseEnter, handleMouseLeave } = useChartHover(); const containerRef = useRef(null); // Calculate dynamic radius @@ -110,10 +113,16 @@ export const TwoLevelPieChart = ({ paddingAngle={0.5} startAngle={appearance === "semiCircular" ? 0 : 0} endAngle={appearance === "semiCircular" ? 180 : 360} + // Add hover event handlers to inner ring + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} > - {Object.entries(chartConfig).map(([key, config]) => ( - - ))} + {transformedData.map((entry, index) => { + const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); + const hoverStyles = getHoverStyles(index, activeIndex); + // Apply neutral color for inner ring + return ; + })} ({ paddingAngle={0.5} startAngle={appearance === "semiCircular" ? 0 : 0} endAngle={appearance === "semiCircular" ? 180 : 360} + // Add hover event handlers to outer ring + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} > - {Object.entries(chartConfig).map(([key, config]) => ( - - ))} + {transformedData.map((entry, index) => { + const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); + const config = chartConfig[categoryValue]; + const hoverStyles = getHoverStyles(index, activeIndex); + + return ( + + ); + })} diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts index 257e5c43a..deaab6695 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts @@ -1,6 +1,7 @@ /** * Utility functions for pie charts */ +import { useState } from "react"; // Helper function to calculate percentage export const calculatePercentage = (value: number, total: number): number => { @@ -67,3 +68,31 @@ export const layoutMap: Record = { tray: "crayon-pie-chart-container-tray", copilot: "crayon-pie-chart-container-copilot", }; + +// Reusable hook for chart hover effects +export const useChartHover = () => { + const [activeIndex, setActiveIndex] = useState(null); + + const handleMouseEnter = (_: any, index: number) => { + setActiveIndex(index); + }; + + const handleMouseLeave = () => { + setActiveIndex(null); + }; + + return { + activeIndex, + handleMouseEnter, + handleMouseLeave, + }; +}; + +// Default hover style properties for cells +export const getHoverStyles = (index: number, activeIndex: number | null) => { + return { + opacity: activeIndex === null || activeIndex === index ? 1 : 0.6, + stroke: activeIndex === index ? "#fff" : "none", + strokeWidth: activeIndex === index ? 2 : 0, + }; +}; From 5d3aef18a5c0f561c9f7c711b462e672f9a83336 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 22 May 2025 17:41:35 +0530 Subject: [PATCH 019/190] feat(BarChartV2): add aspect ratio prop to Recharts component Adds a new `rechartsProps` prop to the `BarChartV2` component, which allows setting the aspect ratio of the Recharts chart. This ensures the chart maintains a consistent 16:9 aspect ratio, regardless of the container size. --- .../src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 0941f5877..cd4881057 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -106,6 +106,9 @@ export const BarChartV2 = ({ config={chartConfig} ref={chartContainerRef} style={{ width, minWidth: "100%", aspectRatio: "16/9" }} + rechartsProps={{ + aspect: 16 / 9, + }} > ({ bottom: xAxisLabel ? 20 : 0, }} onClick={onBarsClick} + > {grid && cartesianGrid()} Date: Mon, 26 May 2025 12:52:08 +0530 Subject: [PATCH 020/190] feat(MiniAreaChart): Add MiniAreaChart component with customizable features This commit introduces the MiniAreaChart component to the react-ui package, providing a compact area chart visualization with the following features: - Supports customizable themes and variants for area interpolation. - Allows for dynamic opacity settings and optional icons for data series. - Integrates with existing chart utilities for improved data handling. - Includes Storybook stories for demonstration and testing. These enhancements aim to offer developers a flexible and visually appealing way to represent data trends in their applications. --- .../MiniAreaChart/MiniAreaChart.tsx | 149 +++++++++++ .../stories/MiniAreaChart.stories.tsx | 252 ++++++++++++++++++ .../BarCharts/BarChartV2/BarChartV2.tsx | 1 - .../BarCharts/MiniBarChart/MiniBarChart.tsx | 76 +++--- .../Charts/WidgetsChart/WidgetsChart.tsx | 26 ++ .../components/Charts/WidgetsChart/index.ts | 0 6 files changed, 466 insertions(+), 38 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx create mode 100644 js/packages/react-ui/src/components/Charts/WidgetsChart/WidgetsChart.tsx create mode 100644 js/packages/react-ui/src/components/Charts/WidgetsChart/index.ts diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx new file mode 100644 index 000000000..26ff76efa --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx @@ -0,0 +1,149 @@ +import React from "react"; +import { Area, AreaChart as RechartsAreaChart, XAxis } from "recharts"; +import { useLayoutContext } from "../../../../context/LayoutContext"; +import { ChartConfig, ChartContainer, keyTransform } from "../../Charts"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; + +export type MiniAreaChartData = Array>; + +export interface MiniAreaChartProps { + data: T; + categoryKey: keyof T[number]; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + variant?: "linear" | "natural" | "step"; + opacity?: number; + icons?: Partial>; + isAnimationActive?: boolean; +} + +export const MiniAreaChart = ({ + data, + categoryKey, + theme = "ocean", + variant = "natural", + opacity = 0.5, + icons = {}, + isAnimationActive = true, +}: MiniAreaChartProps) => { + // excluding the categoryKey + const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + + const palette = getPalette(theme); + const colors = getDistributedColors(palette, dataKeys.length); + const { layout } = useLayoutContext(); + + // Create Config + const chartConfig: ChartConfig = dataKeys.reduce( + (config, key, index) => ({ + ...config, + [key]: { + label: key, + icon: icons[key], + color: colors[index], + }, + }), + {}, + ); + // }, + // tray: { + // default: 5, + // 8: 4, + // 9: 4, + // 10: 4, + // 11: 4, + // }, + // copilot: { + // default: 5, + // 8: 4, + // 9: 4, + // 10: 4, + // 11: 4, + // }, + // fullscreen: { + // default: 5, + // 11: 4, + // }, + // }; + + // const layoutConfig = + // maxLengthMap[layout as keyof typeof maxLengthMap] || maxLengthMap.fullscreen; + + // const maxLength = + // dataLength >= 11 + // ? 4 + // : layoutConfig[dataLength as keyof typeof layoutConfig] || layoutConfig.default; + + // return (value: string) => { + // if (value.length > maxLength) { + // return `${value.slice(0, maxLength)}...`; + // } + // return value; + // }; + // }; + // const getAxisAngle = (data: T) => { + // const angleConfig = { + // mobile: { + // default: 0, + // ranges: [ + // { min: 6, max: 9, angle: -45 }, + // { min: 10, max: 10, angle: -60 }, + // { min: 11, max: Infinity, angle: -75 }, + // ], + // }, + // tray: { + // default: 0, + // ranges: [{ min: 8, max: Infinity, angle: -45 }], + // }, + // copilot: { + // default: 0, + // ranges: [{ min: 8, max: Infinity, angle: -45 }], + // }, + // fullscreen: { + // default: 0, + // ranges: [{ min: 12, max: Infinity, angle: -45 }], + // }, + // }; + + // const layoutConfig = angleConfig[layout as keyof typeof angleConfig] || angleConfig.fullscreen; + // const dataLength = data.length; + + // const matchRange = layoutConfig.ranges.find( + // (range) => dataLength >= range.min && dataLength <= range.max, + // ); + + // return matchRange?.angle ?? layoutConfig.default; + // }; + // const getTickMargin = (data: T) => { + // return data.length <= 6 ? 10 : 15; + // }; + + return ( + + + + + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + return ( + + ); + })} + + + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx new file mode 100644 index 000000000..b87efeac7 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx @@ -0,0 +1,252 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Monitor, TabletSmartphone } from "lucide-react"; +import { Card } from "../../../../Card"; +import { MiniAreaChart, MiniAreaChartProps } from "../MiniAreaChart"; + +const areaChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, +]; + +const icons = { + desktop: Monitor, + mobile: TabletSmartphone, +} as const; + +const meta: Meta> = { + title: "Components/Charts/AreaCharts/MiniAreaChart", + component: MiniAreaChart, + parameters: { + layout: "centered", + docs: { + description: { + component: + "```tsx\nimport { MiniAreaChart } from '@crayon-ui/react-ui/Charts/AreaCharts/MiniAreaChart';\n```", + }, + }, + }, + tags: ["!dev", "autodocs"], + argTypes: { + data: { + description: + "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", + control: false, + table: { + type: { summary: "Array>" }, + defaultValue: { summary: "[]" }, + category: "Data", + }, + }, + categoryKey: { + description: + "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", + control: false, + table: { + type: { summary: "string" }, + defaultValue: { summary: "string" }, + category: "Data", + }, + }, + theme: { + description: + "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + icons: { + description: + "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", + control: false, + table: { + type: { summary: "Record" }, + defaultValue: { summary: "{}" }, + category: "Appearance", + }, + }, + variant: { + description: + "The interpolation method used to create the area curves. 'linear' creates straight lines between points, 'natural' creates smooth curves, and 'step' creates a stepped area.", + control: "radio", + options: ["linear", "natural", "step"], + table: { + defaultValue: { summary: "natural" }, + category: "Appearance", + }, + }, + opacity: { + description: + "The opacity of the filled area beneath each line (0 = fully transparent, 1 = fully opaque)", + control: false, + table: { + type: { summary: "number" }, + defaultValue: { summary: "0.5" }, + category: "Appearance", + }, + }, + // grid: { + // description: "Whether to display the background grid lines in the chart", + // control: "boolean", + // table: { + // type: { summary: "boolean" }, + // defaultValue: { summary: "true" }, + // category: "Display", + // }, + // }, + // label: { + // description: "Whether to display data point labels above each point on the chart", + // control: "boolean", + // table: { + // type: { summary: "boolean" }, + // defaultValue: { summary: "true" }, + // category: "Display", + // }, + // }, + // legend: { + // description: + // "Whether to display the chart legend showing the data series names and their corresponding colors/icons", + // control: "boolean", + // table: { + // type: { summary: "boolean" }, + // defaultValue: { summary: "true" }, + // category: "Display", + // }, + // }, + isAnimationActive: { + description: "Whether to animate the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + // showYAxis: { + // description: "Whether to display the y-axis", + // control: "boolean", + // table: { + // type: { summary: "boolean" }, + // defaultValue: { summary: "false" }, + // category: "Display", + // }, + // }, + // xAxisLabel: { + // description: "The label for the x-axis", + // control: false, + // table: { + // type: { summary: "string" }, + // defaultValue: { summary: "string" }, + // category: "Data", + // }, + // }, + // yAxisLabel: { + // description: "The label for the y-axis", + // control: false, + // table: { + // type: { summary: "string" }, + // defaultValue: { summary: "string" }, + // category: "Data", + // }, + // }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const MiniAreaChartStory: Story = { + name: "Mini Area Chart", + args: { + data: areaChartData, + categoryKey: "month", + theme: "ocean", + variant: "natural", + opacity: 0.5, + isAnimationActive: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` +const areaChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, +]; + + + +`, + }, + }, + }, +}; + +export const MiniAreaChartStoryWithIcons: Story = { + name: "Mini Area Chart with Icons", + args: { + ...MiniAreaChartStory.args, + icons: icons, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` + import { Monitor, TabletSmartphone } from "lucide-react"; + + const areaChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, + ]; + + const icons = { + desktop: Monitor, + mobile: TabletSmartphone, + }; + + + + `, + }, + }, + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index cd4881057..1a4765123 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -118,7 +118,6 @@ export const BarChartV2 = ({ bottom: xAxisLabel ? 20 : 0, }} onClick={onBarsClick} - > {grid && cartesianGrid()} { radius?: number; isAnimationActive?: boolean; label?: string; + onBarsClick?: (data: any) => void; } export const MiniBarChart = ({ @@ -25,7 +26,7 @@ export const MiniBarChart = ({ variant = "grouped", radius = 4, isAnimationActive = true, - label, + onBarsClick, }: MiniBarChartProps) => { // excluding the categoryKey const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); @@ -81,41 +82,42 @@ export const MiniBarChart = ({ }; return ( -
-
- - - - {dataKeys.map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - return ( - - ); - })} - - -
-
- - {calculateTotal(data, categoryKey).toLocaleString()} - - - {label} - -
-
+ //
+ //
+ + + + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + return ( + + ); + })} + + + // {/*
+ //
+ // + // {calculateTotal(data, categoryKey).toLocaleString()} + // + // + // {label} + // + //
+ //
*/} ); }; diff --git a/js/packages/react-ui/src/components/Charts/WidgetsChart/WidgetsChart.tsx b/js/packages/react-ui/src/components/Charts/WidgetsChart/WidgetsChart.tsx new file mode 100644 index 000000000..4ae9e9269 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/WidgetsChart/WidgetsChart.tsx @@ -0,0 +1,26 @@ +const WidgetsChart = () => { + const calculateTotal = ( + data: T, + categoryKey: keyof T[number], + ): number => { + const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + return data.reduce((sum, item) => { + return sum + dataKeys.reduce((keySum, key) => keySum + Number(item[key] || 0), 0); + }, 0); + }; + return ( +
+
+
+ + {calculateTotal(data, categoryKey).toLocaleString()} + + + {label} + +
+
+ ); +}; + +export default WidgetsChart; diff --git a/js/packages/react-ui/src/components/Charts/WidgetsChart/index.ts b/js/packages/react-ui/src/components/Charts/WidgetsChart/index.ts new file mode 100644 index 000000000..e69de29bb From d5bcd752c074f03cfd66404505332a804080c775 Mon Sep 17 00:00:00 2001 From: i-subham Date: Mon, 26 May 2025 13:24:44 +0530 Subject: [PATCH 021/190] feat(MiniLineChart): Introduce MiniLineChart component with customizable features This commit adds the MiniLineChart component to the react-ui package, providing a compact line chart visualization with the following features: - Supports customizable themes and variants for line interpolation. - Allows for dynamic stroke width and optional icons for data series. - Integrates with existing chart utilities for improved data handling. - Includes Storybook stories for demonstration and testing. These enhancements aim to offer developers a flexible and visually appealing way to represent data trends in their applications. --- .../MiniAreaChart/MiniAreaChart.tsx | 72 ------ .../MiniLineChart/MiniLineChart.tsx | 86 +++++++ .../Charts/LineCharts/MiniLineChart/index.ts | 1 + .../stories/MiniLineChart.stories.tsx | 211 ++++++++++++++++++ 4 files changed, 298 insertions(+), 72 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx index 26ff76efa..8e7d2078e 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx @@ -44,78 +44,6 @@ export const MiniAreaChart = ({ }), {}, ); - // }, - // tray: { - // default: 5, - // 8: 4, - // 9: 4, - // 10: 4, - // 11: 4, - // }, - // copilot: { - // default: 5, - // 8: 4, - // 9: 4, - // 10: 4, - // 11: 4, - // }, - // fullscreen: { - // default: 5, - // 11: 4, - // }, - // }; - - // const layoutConfig = - // maxLengthMap[layout as keyof typeof maxLengthMap] || maxLengthMap.fullscreen; - - // const maxLength = - // dataLength >= 11 - // ? 4 - // : layoutConfig[dataLength as keyof typeof layoutConfig] || layoutConfig.default; - - // return (value: string) => { - // if (value.length > maxLength) { - // return `${value.slice(0, maxLength)}...`; - // } - // return value; - // }; - // }; - // const getAxisAngle = (data: T) => { - // const angleConfig = { - // mobile: { - // default: 0, - // ranges: [ - // { min: 6, max: 9, angle: -45 }, - // { min: 10, max: 10, angle: -60 }, - // { min: 11, max: Infinity, angle: -75 }, - // ], - // }, - // tray: { - // default: 0, - // ranges: [{ min: 8, max: Infinity, angle: -45 }], - // }, - // copilot: { - // default: 0, - // ranges: [{ min: 8, max: Infinity, angle: -45 }], - // }, - // fullscreen: { - // default: 0, - // ranges: [{ min: 12, max: Infinity, angle: -45 }], - // }, - // }; - - // const layoutConfig = angleConfig[layout as keyof typeof angleConfig] || angleConfig.fullscreen; - // const dataLength = data.length; - - // const matchRange = layoutConfig.ranges.find( - // (range) => dataLength >= range.min && dataLength <= range.max, - // ); - - // return matchRange?.angle ?? layoutConfig.default; - // }; - // const getTickMargin = (data: T) => { - // return data.length <= 6 ? 10 : 15; - // }; return ( diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx new file mode 100644 index 000000000..c849da7a4 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import { Line, LineChart as RechartsLineChart, XAxis } from "recharts"; +import { ChartConfig, ChartContainer, keyTransform } from "../../Charts"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; + +export type MiniLineChartData = Array>; + +export interface MiniLineChartProps { + data: T; + categoryKey: keyof T[number]; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + variant?: "linear" | "natural" | "step"; + strokeWidth?: number; + icons?: Partial>; + isAnimationActive?: boolean; +} + +export const MiniLineChart = ({ + data, + categoryKey, + theme = "ocean", + variant = "natural", + strokeWidth = 2, + icons = {}, + isAnimationActive = true, +}: MiniLineChartProps) => { + // excluding the categoryKey + const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + + const palette = getPalette(theme); + const colors = getDistributedColors(palette, dataKeys.length); + // const { layout } = useLayoutContext(); + + // Create Config + const chartConfig: ChartConfig = dataKeys.reduce( + (config, key, index) => ({ + ...config, + [key]: { + label: key, + icon: icons[key], + color: colors[index], + }, + }), + {}, + ); + + return ( + + + {/* {grid && cartesianGrid()} */} + + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + return ( + + ); + })} + + + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts new file mode 100644 index 000000000..86eccce92 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts @@ -0,0 +1 @@ +export * from "./MiniLineChart"; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx new file mode 100644 index 000000000..0653aa8a8 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx @@ -0,0 +1,211 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Monitor, TabletSmartphone } from "lucide-react"; +import { Card } from "../../../../Card"; +import { MiniLineChart } from "../MiniLineChart"; + +const lineChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, +]; + +const icons = { + desktop: Monitor, + mobile: TabletSmartphone, +} as const; + +const meta = { + title: "Components/Charts/LineCharts/MiniLineChart", + component: MiniLineChart, + parameters: { + layout: "centered", + docs: { + description: { + component: "```tsx\nimport { LineChart } from '@crayon-ui/react-ui/Charts/LineChart';\n```", + }, + }, + }, + tags: ["!dev", "autodocs"], + + argTypes: { + data: { + description: + "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", + control: { type: "object" }, + table: { + type: { summary: "Array>" }, + defaultValue: { summary: "[]" }, + category: "Data", + }, + }, + categoryKey: { + description: + "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", + control: false, + table: { + type: { summary: "string" }, + defaultValue: { summary: "string" }, + category: "Data", + }, + }, + theme: { + description: + "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + icons: { + description: + "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", + control: false, + table: { + type: { summary: "Record" }, + defaultValue: { summary: "{}" }, + category: "Appearance", + }, + }, + variant: { + description: + "The interpolation method used to create the line curves. 'linear' creates straight lines between points, 'natural' creates smooth curves, and 'step' creates a stepped line.", + control: "radio", + options: ["linear", "natural", "step"], + table: { + defaultValue: { summary: "natural" }, + category: "Appearance", + }, + }, + strokeWidth: { + description: "The width of the line stroke", + control: false, + table: { + type: { summary: "number" }, + defaultValue: { summary: "2" }, + category: "Appearance", + }, + }, + isAnimationActive: { + description: "Whether to animate the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const LineChartStory: Story = { + name: "Line Chart", + args: { + data: lineChartData, + categoryKey: "month", + theme: "ocean", + variant: "natural", + strokeWidth: 2, + isAnimationActive: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` +const lineChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, +]; + + + +`, + }, + }, + }, +}; + +export const LineChartStoryWithIcons: Story = { + name: "Line Chart with Icons", + args: { + ...LineChartStory.args, + icons: icons, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` + import { Monitor, TabletSmartphone } from "lucide-react"; + + const lineChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, +]; + +const icons = { + desktop: Monitor, + mobile: TabletSmartphone, +}; + + + + + `, + }, + }, + }, +}; From 2303d396cb659b9c888e4b5a10f95a13fb6498a5 Mon Sep 17 00:00:00 2001 From: i-subham Date: Mon, 26 May 2025 13:36:34 +0530 Subject: [PATCH 022/190] feat(MiniRadarChart): Introduce MiniRadarChart component with customizable features This commit adds the MiniRadarChart component to the react-ui package, providing a compact radar chart visualization with the following features: - Supports customizable themes and variants for line and area styles. - Allows for dynamic stroke width, area opacity, and optional icons for data series. - Integrates with existing chart utilities for improved data handling. - Includes Storybook stories for demonstration and testing. These enhancements aim to offer developers a flexible and visually appealing way to represent multi-dimensional data in their applications. --- .../MiniRadarChart/MiniRadarChart.tsx | 81 +++++++++ .../RadarCharts/MiniRadarChart/index.ts | 1 + .../stories/MiniRadarChart.stories.tsx | 167 ++++++++++++++++++ 3 files changed, 249 insertions(+) create mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/MiniRadarChart.tsx create mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/stories/MiniRadarChart.stories.tsx diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/MiniRadarChart.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/MiniRadarChart.tsx new file mode 100644 index 000000000..3ff5c22c6 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/MiniRadarChart.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { Radar, RadarChart as RechartsRadarChart } from "recharts"; +import { ChartConfig, ChartContainer, keyTransform } from "../../Charts"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; + +export type MiniRadarChartData = Array>; + +export interface MiniRadarChartProps { + data: T; + categoryKey: keyof T[number]; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + variant?: "line" | "area"; + strokeWidth?: number; + areaOpacity?: number; + icons?: Partial>; + isAnimationActive?: boolean; +} + +export const MiniRadarChart = ({ + data, + categoryKey, + theme = "ocean", + variant = "line", + strokeWidth = 2, + areaOpacity = 0.5, + icons = {}, + isAnimationActive = true, +}: MiniRadarChartProps) => { + // excluding the categoryKey + const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + + const palette = getPalette(theme); + const colors = getDistributedColors(palette, dataKeys.length); + + // Create Config + const chartConfig: ChartConfig = dataKeys.reduce( + (config, key, index) => ({ + ...config, + [key]: { + label: key, + icon: icons[key], + color: colors[index], + }, + }), + {}, + ); + + return ( + + + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + if (variant === "line") { + return ( + + ); + } else { + return ( + + ); + } + })} + + + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/index.ts new file mode 100644 index 000000000..e13066887 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/index.ts @@ -0,0 +1 @@ +export * from "./MiniRadarChart"; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/stories/MiniRadarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/stories/MiniRadarChart.stories.tsx new file mode 100644 index 000000000..6aa49bdf9 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/stories/MiniRadarChart.stories.tsx @@ -0,0 +1,167 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Monitor, TabletSmartphone } from "lucide-react"; +import { Card } from "../../../../Card"; +import { MiniRadarChart, MiniRadarChartProps } from "../MiniRadarChart"; + +const radarChartData = [ + { month: "January", desktop: 250, mobile: 150 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, +]; + +const icons = { + desktop: Monitor, + mobile: TabletSmartphone, +} as const; + +const meta: Meta> = { + title: "Components/Charts/RadarCharts/MiniRadarChart", + component: MiniRadarChart, + parameters: { + layout: "centered", + docs: { + description: { + component: + "```tsx\nimport { MiniRadarChart } from '@crayon-ui/react-ui/Charts/RadarCharts/MiniRadarCharts';\n```", + }, + }, + }, + tags: ["!dev", "autodocs"], + + argTypes: { + data: { + description: + "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", + control: false, + table: { + type: { summary: "Array>" }, + defaultValue: { summary: "[]" }, + category: "Data", + }, + }, + categoryKey: { + description: + "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", + control: false, + table: { + type: { summary: "string" }, + category: "Data", + }, + }, + theme: { + description: + "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + icons: { + description: + "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", + control: false, + table: { + type: { summary: "Record" }, + defaultValue: { summary: "{}" }, + category: "Appearance", + }, + }, + variant: { + description: + "The style of the radar chart. 'line' shows only the connecting lines between data points, while 'area' fills the shape created by the data points.", + control: "radio", + options: ["line", "area"], + table: { + defaultValue: { summary: "area" }, + category: "Appearance", + }, + }, + strokeWidth: { + description: "The width of the line stroke", + control: false, + table: { + type: { summary: "number" }, + defaultValue: { summary: "2" }, + category: "Appearance", + }, + }, + areaOpacity: { + description: "The opacity of the area fill", + control: false, + table: { + type: { summary: "number" }, + defaultValue: { summary: "0.7" }, + category: "Appearance", + }, + }, + isAnimationActive: { + description: "Whether to animate the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const RadarChartStory: Story = { + name: "Radar Chart", + args: { + data: radarChartData, + categoryKey: "month", + theme: "ocean", + variant: "area", + strokeWidth: 2, + areaOpacity: 0.5, + isAnimationActive: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` + const radarChartData = [ + { month: "January", desktop: 250, mobile: 150 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, +]; + + + + + `, + }, + }, + }, +}; From 57ac9c598cce3633b36974768fdf22e9801a35ac Mon Sep 17 00:00:00 2001 From: i-subham Date: Mon, 26 May 2025 14:54:51 +0530 Subject: [PATCH 023/190] fix(BarChartV2, PieChart, PieChartV2): Update Storybook stories and component styles This commit includes the following changes: - Duplicated component import description in BarChartV2 story for clarity. - Adjusted Card width in PieChart story from 500px to 300px for better layout. - Modified PieChartV2 story to set legend and label properties to false, and increased Card width from 500px to 700px. These updates aim to enhance the presentation and usability of the chart components in Storybook. --- .../BarCharts/BarChartV2/stories/barChartV2.stories.tsx | 2 ++ .../components/Charts/PieChart/stories/pieChart.stories.tsx | 2 +- .../PieCharts/PieChartV2/stories/PieChartV2.stories.tsx | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx index d52821b6c..81b3f6588 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx @@ -32,6 +32,8 @@ const meta: Meta> = { description: { component: "```tsx\nimport { BarChartV2 } from '@crayon-ui/react-ui/Charts/BarChartV2';\n```", + component: + "```tsx\nimport { BarChartV2 } from '@crayon-ui/react-ui/Charts/BarChartV2';\n```", }, }, }, diff --git a/js/packages/react-ui/src/components/Charts/PieChart/stories/pieChart.stories.tsx b/js/packages/react-ui/src/components/Charts/PieChart/stories/pieChart.stories.tsx index 910d8c856..0e80089d0 100644 --- a/js/packages/react-ui/src/components/Charts/PieChart/stories/pieChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieChart/stories/pieChart.stories.tsx @@ -131,7 +131,7 @@ export const PieChartStory: Story = { isAnimationActive: true, }, render: (args) => ( - + ), diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index e04ba1c12..0577bb0ae 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -136,12 +136,12 @@ export const PieChartStory: Story = { theme: "ocean", variant: "pie", format: "number", - legend: true, - label: true, + legend: false, + label: false, isAnimationActive: true, }, render: (args) => ( - + ), From a8bd9460b25574afdfed74f8c588789c75077e92 Mon Sep 17 00:00:00 2001 From: i-subham Date: Wed, 28 May 2025 17:50:33 +0530 Subject: [PATCH 024/190] feat(MiniRadialChart): Introduce MiniRadialChart component with utility functions and Storybook stories This commit adds the MiniRadialChart component to the react-ui package, providing a compact radial chart visualization with the following features: - Supports customizable themes and variants (circular and semicircle). - Integrates utility functions for percentage calculations, dynamic resizing, and hover effects. - Includes Storybook stories for demonstration and testing, showcasing various configurations and data formats. These enhancements aim to offer developers a flexible and visually appealing way to represent data in a radial format. --- .../MiniRadialChart/MiniRadialChart.tsx | 148 ++++++++++++++ .../RadialCharts/MiniRadialChart/index.ts | 1 + .../stories/MiniRadialChart.stories.tsx | 189 ++++++++++++++++++ .../RadialCharts/utils/RadialChartUtils.ts | 90 +++++++++ 4 files changed, 428 insertions(+) create mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx create mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/stories/MiniRadialChart.stories.tsx create mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx new file mode 100644 index 000000000..a3c88f257 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx @@ -0,0 +1,148 @@ +import clsx from "clsx"; +import { debounce } from "lodash-es"; +import { useEffect, useRef, useState } from "react"; +import { Cell, RadialBar, RadialBarChart } from "recharts"; +import { useLayoutContext } from "../../../../context/LayoutContext"; +import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { + calculatePercentage, + calculateRadialChartDimensions, + getHoverStyles, + layoutMap, + useChartHover, +} from "../utils/RadialChartUtils"; + +export type MiniRadialChartData = Array>; + +export interface MiniRadialChartProps { + data: T; + categoryKey: keyof T[number]; + dataKey: keyof T[number]; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + variant?: "semicircle" | "circular"; + format?: "percentage" | "number"; + legend?: boolean; + label?: boolean; + grid?: boolean; + isAnimationActive?: boolean; +} + +export const MiniRadialChart = ({ + data, + categoryKey, + dataKey, + theme = "ocean", + variant = "semicircle", + format = "number", + legend = true, + label = true, + grid = true, + isAnimationActive = true, +}: MiniRadialChartProps) => { + const { layout } = useLayoutContext(); + const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(110); + const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(30); + const { activeIndex, handleMouseEnter, handleMouseLeave } = useChartHover(); + const containerRef = useRef(null); + + useEffect(() => { + if (!containerRef.current) return; + + const resizeObserver = new ResizeObserver( + debounce((entries: any) => { + const { width } = entries[0].contentRect; + const { outerRadius, innerRadius } = calculateRadialChartDimensions(width, variant, label); + setCalculatedOuterRadius(outerRadius); + setCalculatedInnerRadius(innerRadius); + }, 100), + ); + + resizeObserver.observe(containerRef.current); + return () => resizeObserver.disconnect(); + }, [layout, label, variant]); + + // Calculate total for percentage calculations + const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); + + // Get color palette and distribute colors + const palette = getPalette(theme); + const colors = getDistributedColors(palette, data.length); + + // Transform data with percentages + const transformedData = data.map((item, index) => ({ + ...item, + percentage: calculatePercentage(Number(item[dataKey as string]), total), + originalValue: item[dataKey as string], + fill: colors[index], + })); + + // Create chart configuration + const chartConfig = data.reduce( + (config, item, index) => ({ + ...config, + [String(item[categoryKey])]: { + label: String(item[categoryKey as string]), + color: colors[index], + }, + }), + {}, + ); + + return ( + + + {/* {grid && } */} + + } + /> + {/* {legend && ( + + } + /> + )} */} + + {Object.entries(chartConfig).map(([key, config], index) => ( + + ))} + {/* {label && ( + formatRadialLabel(value, format)} + /> + )} */} + + + + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/index.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/index.ts new file mode 100644 index 000000000..182e70fa5 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/index.ts @@ -0,0 +1 @@ +export * from "./MiniRadialChart"; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/stories/MiniRadialChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/stories/MiniRadialChart.stories.tsx new file mode 100644 index 000000000..0d1be1da5 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/stories/MiniRadialChart.stories.tsx @@ -0,0 +1,189 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Card } from "../../../../Card"; +import { MiniRadialChart, MiniRadialChartProps } from "../MiniRadialChart"; + +const radialChartData = [ + { month: "January", value: 400 }, + { month: "February", value: 300 }, + { month: "March", value: 300 }, + { month: "April", value: 400 }, + { month: "May", value: 300 }, + { month: "June", value: 300 }, + { month: "July", value: 300 }, +]; + +const meta: Meta> = { + title: "Components/Charts/RadialCharts/MiniRadialChart", + component: MiniRadialChart, + parameters: { + layout: "centered", + docs: { + description: { + component: + "```tsx\nimport { MiniRadialChart } from '@crayon-ui/react-ui/Charts/RadialCharts/MiniRadialChart';\n```", + }, + }, + }, + tags: ["!dev", "autodocs"], + + argTypes: { + data: { + description: + "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", + control: false, + table: { + type: { summary: "Array>" }, + defaultValue: { summary: "[]" }, + category: "Data", + }, + }, + categoryKey: { + description: + "The key from your data object to be used as the category labels for each segment (e.g., 'month', 'year', 'category')", + control: false, + table: { + type: { summary: "string" }, + category: "Data", + }, + }, + dataKey: { + description: + "The key from your data object to be used as the values that determine the segment sizes (e.g., 'value', 'count', 'amount')", + control: false, + table: { + type: { summary: "string" }, + category: "Data", + }, + }, + theme: { + description: + "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + variant: { + description: + "The style of the pie chart. 'circular' shows a circular chart, while 'semicircle' shows a semicircle chart.", + control: "radio", + options: ["circular", "semicircle"], + table: { + defaultValue: { summary: "circular" }, + category: "Appearance", + }, + }, + format: { + description: + "The format of the data. 'percentage' shows the data as a percentage, while 'number' shows the data as a number.", + control: "radio", + options: ["percentage", "number"], + table: { + defaultValue: { summary: "percentage" }, + category: "Display", + }, + }, + legend: { + description: "Whether to display the legend", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + label: { + description: "Whether to display the data point labels above each point on the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + grid: { + description: "Whether to display the grid lines", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + isAnimationActive: { + description: "Whether to animate the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const RadialChartStory: Story = { + name: "Radial Chart", + args: { + data: radialChartData, + categoryKey: "month", + dataKey: "value", + theme: "ocean", + variant: "circular", + format: "number", + legend: true, + label: true, + grid: true, + isAnimationActive: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "A radial chart that displays data in a circular or semicircular format, with customizable themes, labels, and grid options.", + }, + source: { + code: ` +const radialChartData = [ + { month: "January", value: 400 }, + { month: "February", value: 300 }, + { month: "March", value: 300 }, + { month: "April", value: 400 }, + { month: "May", value: 300 }, + { month: "June", value: 300 }, + { month: "July", value: 300 }, +]; + + + + + `, + }, + }, + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts new file mode 100644 index 000000000..3d16a0366 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts @@ -0,0 +1,90 @@ +/** + * Utility functions for radial charts + */ +import { useState } from "react"; + +// Helper function to calculate percentage +export const calculatePercentage = (value: number, total: number): number => { + if (total === 0) { + return 0; + } + return Number(((value / total) * 100).toFixed(2)); +}; + +// Dynamic resize function to maintain aspect ratio for radial charts +export const calculateRadialChartDimensions = ( + width: number, + variant: "semicircle" | "circular", + label: boolean, +): { outerRadius: number; innerRadius: number } => { + const baseRadiusPercentage = 0.4; // 40% of container width + + // Calculate base outer radius + let outerRadius = Math.round(width * baseRadiusPercentage); + + // Adjust for label presence + if (label) { + outerRadius = Math.round(outerRadius * 0.9); + } + + // Set minimum and maximum bounds for radius + outerRadius = Math.max(50, Math.min(outerRadius, width / 2 - 10)); + + // Calculate inner radius - consistent ratio regardless of layout + const innerRadius = Math.round(outerRadius * 0.3); + + return { outerRadius, innerRadius }; +}; + +export const layoutMap: Record = { + mobile: "crayon-pie-chart-container-mobile", + fullscreen: "crayon-pie-chart-container-fullscreen", + tray: "crayon-pie-chart-container-tray", + copilot: "crayon-pie-chart-container-copilot", +}; + +// Reusable hook for chart hover effects +export const useChartHover = () => { + const [activeIndex, setActiveIndex] = useState(null); + + const handleMouseEnter = (_: any, index: number) => { + setActiveIndex(index); + }; + + const handleMouseLeave = () => { + setActiveIndex(null); + }; + + return { + activeIndex, + handleMouseEnter, + handleMouseLeave, + }; +}; + +// Default hover style properties for cells +export const getHoverStyles = (index: number, activeIndex: number | null) => { + return { + opacity: activeIndex === null || activeIndex === index ? 1 : 0.6, + stroke: activeIndex === index ? "#fff" : "none", + strokeWidth: activeIndex === index ? 2 : 0, + }; +}; + +// Helper function to format label values +export const formatRadialLabel = ( + value: string | number, + format: "percentage" | "number", +): string => { + if (format === "percentage") { + return `${value}%`; + } + // For number format, just truncate if too long + const stringValue = String(value); + return stringValue.length > 8 ? `${stringValue.slice(0, 8)}...` : stringValue; +}; + +// Helper function to calculate font size based on data length +export const getRadialFontSize = (dataLength: number): number => { + return dataLength <= 5 ? 12 : 7; +}; From 43813120825c7d73b317e52406b7e8c8a49d1e60 Mon Sep 17 00:00:00 2001 From: i-subham Date: Thu, 29 May 2025 11:45:48 +0530 Subject: [PATCH 025/190] feat(Charts): Refactor chart components to utilize centralized configuration utilities This commit introduces several enhancements across multiple chart components, including MiniAreaChart, MiniLineChart, MiniPieChart, MiniRadarChart, and others. Key changes include: - Creation of utility functions for chart configuration and data transformation, improving code reusability and maintainability. - Refactoring of existing components to leverage these new utilities, streamlining the configuration process for themes, colors, and data handling. - Introduction of new types for chart data, enhancing type safety and clarity. These updates aim to provide a more consistent and efficient approach to chart rendering within the react-ui package, ultimately improving the developer experience and component performance. --- .../MiniAreaChart/MiniAreaChart.tsx | 27 ++-------- .../Charts/AreaCharts/utils/AreaChartUtils.ts | 35 +++++++++++++ .../MiniLineChart/MiniLineChart.tsx | 28 ++--------- .../Charts/LineCharts/utils/LineChartUtils.ts | 35 +++++++++++++ .../PieCharts/MiniPieChart/MiniPieChart.tsx | 43 ++++------------ .../PieCharts/PieChartV2/PieChartV2.tsx | 33 +++--------- .../TwoLevelPieChart/TwoLevelPieChart.tsx | 49 +++--------------- .../Charts/PieCharts/utils/PieChartUtils.ts | 38 ++++++++++++++ .../MiniRadarChart/MiniRadarChart.tsx | 34 ++++--------- .../RadarCharts/utils/RaderChartUtils.ts | 44 ++++++++++++++++ .../MiniRadialChart/MiniRadialChart.tsx | 36 +++---------- .../RadialCharts/utils/RadialChartUtils.ts | 50 +++++++++++++++++++ 12 files changed, 254 insertions(+), 198 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts create mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/utils/RaderChartUtils.ts diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx index 8e7d2078e..bffa50b6e 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx @@ -1,10 +1,8 @@ import React from "react"; import { Area, AreaChart as RechartsAreaChart, XAxis } from "recharts"; import { useLayoutContext } from "../../../../context/LayoutContext"; -import { ChartConfig, ChartContainer, keyTransform } from "../../Charts"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; - -export type MiniAreaChartData = Array>; +import { ChartContainer, keyTransform } from "../../Charts"; +import { MiniAreaChartData, createChartConfig } from "../utils/AreaChartUtils"; export interface MiniAreaChartProps { data: T; @@ -25,25 +23,8 @@ export const MiniAreaChart = ({ icons = {}, isAnimationActive = true, }: MiniAreaChartProps) => { - // excluding the categoryKey - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); - - const palette = getPalette(theme); - const colors = getDistributedColors(palette, dataKeys.length); const { layout } = useLayoutContext(); - - // Create Config - const chartConfig: ChartConfig = dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - icon: icons[key], - color: colors[index], - }, - }), - {}, - ); + const chartConfig = createChartConfig({ data, categoryKey: categoryKey as string, theme, icons }); return ( @@ -55,7 +36,7 @@ export const MiniAreaChart = ({ hide={true} /> - {dataKeys.map((key) => { + {Object.keys(chartConfig).map((key) => { const transformedKey = keyTransform(key); const color = `var(--color-${transformedKey})`; return ( diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts new file mode 100644 index 000000000..738dddc00 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts @@ -0,0 +1,35 @@ +import { ChartConfig } from "../../Charts"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; + +export type MiniAreaChartData = Array>; + +export interface MiniAreaChartConfig { + data: MiniAreaChartData; + categoryKey: string; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + icons?: Partial>; +} + +export const createChartConfig = ({ + data, + categoryKey, + theme = "ocean", + icons = {}, +}: MiniAreaChartConfig): ChartConfig => { + // excluding the categoryKey + const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + const palette = getPalette(theme); + const colors = getDistributedColors(palette, dataKeys.length); + + return dataKeys.reduce( + (config, key, index) => ({ + ...config, + [key]: { + label: key, + icon: icons[key], + color: colors[index], + }, + }), + {}, + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx index c849da7a4..96212c7fe 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx @@ -1,9 +1,7 @@ import React from "react"; import { Line, LineChart as RechartsLineChart, XAxis } from "recharts"; -import { ChartConfig, ChartContainer, keyTransform } from "../../Charts"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; - -export type MiniLineChartData = Array>; +import { ChartContainer, keyTransform } from "../../Charts"; +import { MiniLineChartData, createChartConfig } from "../utils/LineChartUtils"; export interface MiniLineChartProps { data: T; @@ -24,25 +22,7 @@ export const MiniLineChart = ({ icons = {}, isAnimationActive = true, }: MiniLineChartProps) => { - // excluding the categoryKey - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); - - const palette = getPalette(theme); - const colors = getDistributedColors(palette, dataKeys.length); - // const { layout } = useLayoutContext(); - - // Create Config - const chartConfig: ChartConfig = dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - icon: icons[key], - color: colors[index], - }, - }), - {}, - ); + const chartConfig = createChartConfig({ data, categoryKey: categoryKey as string, theme, icons }); return ( @@ -64,7 +44,7 @@ export const MiniLineChart = ({ interval="preserveStartEnd" hide={true} /> - {dataKeys.map((key) => { + {Object.keys(chartConfig).map((key) => { const transformedKey = keyTransform(key); const color = `var(--color-${transformedKey})`; return ( diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts b/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts new file mode 100644 index 000000000..870243fe4 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts @@ -0,0 +1,35 @@ +import { ChartConfig } from "../../Charts"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; + +export type MiniLineChartData = Array>; + +export interface MiniLineChartConfig { + data: MiniLineChartData; + categoryKey: string; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + icons?: Partial>; +} + +export const createChartConfig = ({ + data, + categoryKey, + theme = "ocean", + icons = {}, +}: MiniLineChartConfig): ChartConfig => { + // excluding the categoryKey + const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + const palette = getPalette(theme); + const colors = getDistributedColors(palette, dataKeys.length); + + return dataKeys.reduce( + (config, key, index) => ({ + ...config, + [key]: { + label: key, + icon: icons[key], + color: colors[index], + }, + }), + {}, + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx index 698b300d3..9fd834f1e 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx @@ -3,11 +3,16 @@ import { debounce } from "lodash-es"; import { useEffect, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; import { useLayoutContext } from "../../../../context/LayoutContext"; -import { ChartConfig, ChartContainer } from "../../Charts"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; -import { calculateChartDimensions, calculatePercentage, layoutMap } from "../utils/PieChartUtils"; +import { ChartContainer } from "../../Charts"; +import { + PieChartData, + calculateChartDimensions, + createChartConfig, + layoutMap, + transformDataWithPercentages, +} from "../utils/PieChartUtils"; -export type MiniPieChartData = Array>; +export type MiniPieChartData = PieChartData; export interface MiniPieChartProps { data: T; @@ -43,10 +48,7 @@ export const MiniPieChart = ({ const resizeObserver = new ResizeObserver( debounce((entries: any) => { const { width } = entries[0].contentRect; - - // Use the dynamic resize function to calculate radii const { outerRadius, innerRadius } = calculateChartDimensions(width, variant, label); - setCalculatedOuterRadius(outerRadius); setCalculatedInnerRadius(innerRadius); }, 100), @@ -56,31 +58,8 @@ export const MiniPieChart = ({ return () => resizeObserver.disconnect(); }, [variant, label]); - // Calculate total for percentage calculations - const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); - - // Transform data with percentages - const transformedData = data.map((item) => ({ - ...item, - percentage: calculatePercentage(Number(item[dataKey as string]), total), - originalValue: item[dataKey as string], - })); - - // Get color palette and distribute colors - const palette = getPalette(theme); - const colors = getDistributedColors(palette, data.length); - - // Create chart configuration - const chartConfig = data.reduce( - (config, item, index) => ({ - ...config, - [String(item[categoryKey])]: { - label: String(item[categoryKey as string]), - color: colors[index], - }, - }), - {}, - ); + const transformedData = transformDataWithPercentages(data, dataKey); + const chartConfig = createChartConfig(data, categoryKey, theme); return ( >; +export type PieChartV2Data = PieChartData; export interface PieChartV2Props { data: T; @@ -60,10 +61,7 @@ export const PieChartV2 = ({ const resizeObserver = new ResizeObserver( debounce((entries: any) => { const { width } = entries[0].contentRect; - - // Use the dynamic resize function to calculate radii const { outerRadius, innerRadius } = calculateChartDimensions(width, variant, label); - setCalculatedOuterRadius(outerRadius); setCalculatedInnerRadius(innerRadius); }, 100), @@ -73,32 +71,13 @@ export const PieChartV2 = ({ return () => resizeObserver.disconnect(); }, [variant, label]); - // Calculate total for percentage calculations - const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); - - // Transform data with percentages - const transformedData = data.map((item) => ({ - ...item, - percentage: calculatePercentage(Number(item[dataKey as string]), total), - originalValue: item[dataKey as string], - })); + const transformedData = transformDataWithPercentages(data, dataKey); + const chartConfig = createChartConfig(data, categoryKey, theme); // Get color palette and distribute colors const palette = getPalette(theme); const colors = getDistributedColors(palette, data.length); - // Create chart configuration - const chartConfig = data.reduce( - (config, item, index) => ({ - ...config, - [String(item[categoryKey])]: { - label: String(item[categoryKey as string]), - color: colors[index], - }, - }), - {}, - ); - //Custom label renderer // const renderCustomLabel = ({ payload, cx, cy, x, y, textAnchor, dominantBaseline }: any) => { // if (payload.percentage <= 10) return null; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx index 4c54cd3d7..1eb199f12 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx @@ -3,17 +3,18 @@ import { debounce } from "lodash-es"; import { useEffect, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; import { useLayoutContext } from "../../../../context/LayoutContext"; -import { ChartConfig, ChartContainer } from "../../Charts"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { ChartContainer } from "../../Charts"; import { - calculatePercentage, + PieChartData, calculateTwoLevelChartDimensions, + createChartConfig, getHoverStyles, layoutMap, + transformDataWithPercentages, useChartHover, } from "../utils/PieChartUtils"; -export type TwoLevelPieChartData = Array>; +export type TwoLevelPieChartData = PieChartData; export interface TwoLevelPieChartProps { data: T; @@ -51,8 +52,6 @@ export const TwoLevelPieChart = ({ const resizeObserver = new ResizeObserver( debounce((entries: any) => { const { width } = entries[0].contentRect; - - // Use the two-level chart dimension calculation function const { outerRadius, middleRadius, innerRadius } = calculateTwoLevelChartDimensions( width, label, @@ -68,31 +67,8 @@ export const TwoLevelPieChart = ({ return () => resizeObserver.disconnect(); }, [label]); - // Calculate total for percentage calculations - const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); - - // Transform data with percentages - const transformedData = data.map((item) => ({ - ...item, - percentage: calculatePercentage(Number(item[dataKey as string]), total), - originalValue: item[dataKey as string], - })); - - // Get color palette and distribute colors - const palette = getPalette(theme); - const colors = getDistributedColors(palette, data.length); - - // Create chart configuration - const chartConfig = data.reduce( - (config, item, index) => ({ - ...config, - [String(item[categoryKey])]: { - label: String(item[categoryKey as string]), - color: colors[index], - }, - }), - {}, - ); + const transformedData = transformDataWithPercentages(data, dataKey); + const chartConfig = createChartConfig(data, categoryKey, theme); return ( ({ paddingAngle={0.5} startAngle={appearance === "semiCircular" ? 0 : 0} endAngle={appearance === "semiCircular" ? 180 : 360} - // Add hover event handlers to inner ring onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > {transformedData.map((entry, index) => { const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); const hoverStyles = getHoverStyles(index, activeIndex); - // Apply neutral color for inner ring return ; })} @@ -136,7 +110,6 @@ export const TwoLevelPieChart = ({ paddingAngle={0.5} startAngle={appearance === "semiCircular" ? 0 : 0} endAngle={appearance === "semiCircular" ? 180 : 360} - // Add hover event handlers to outer ring onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > @@ -145,13 +118,7 @@ export const TwoLevelPieChart = ({ const config = chartConfig[categoryValue]; const hoverStyles = getHoverStyles(index, activeIndex); - return ( - - ); + return ; })} diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts index deaab6695..79e70ff44 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts @@ -2,6 +2,10 @@ * Utility functions for pie charts */ import { useState } from "react"; +import { ChartConfig } from "../../Charts"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; + +export type PieChartData = Array>; // Helper function to calculate percentage export const calculatePercentage = (value: number, total: number): number => { @@ -96,3 +100,37 @@ export const getHoverStyles = (index: number, activeIndex: number | null) => { strokeWidth: activeIndex === index ? 2 : 0, }; }; + +// Transform data with percentages +export const transformDataWithPercentages = ( + data: T, + dataKey: keyof T[number], +) => { + const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); + return data.map((item) => ({ + ...item, + percentage: calculatePercentage(Number(item[dataKey as string]), total), + originalValue: item[dataKey as string], + })); +}; + +// Create chart configuration +export const createChartConfig = ( + data: T, + categoryKey: keyof T[number], + theme: string = "ocean", +) => { + const palette = getPalette(theme); + const colors = getDistributedColors(palette, data.length); + + return data.reduce( + (config, item, index) => ({ + ...config, + [String(item[categoryKey])]: { + label: String(item[categoryKey as string]), + color: colors[index], + }, + }), + {}, + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/MiniRadarChart.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/MiniRadarChart.tsx index 3ff5c22c6..580682924 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/MiniRadarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/MiniRadarChart.tsx @@ -1,9 +1,9 @@ import React from "react"; import { Radar, RadarChart as RechartsRadarChart } from "recharts"; -import { ChartConfig, ChartContainer, keyTransform } from "../../Charts"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { ChartContainer, keyTransform } from "../../Charts"; +import { RadarChartData, createChartConfig, getRadarChartMargin } from "../utils/RaderChartUtils"; -export type MiniRadarChartData = Array>; +export type MiniRadarChartData = RadarChartData; export interface MiniRadarChartProps { data: T; @@ -26,29 +26,17 @@ export const MiniRadarChart = ({ icons = {}, isAnimationActive = true, }: MiniRadarChartProps) => { - // excluding the categoryKey - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); - - const palette = getPalette(theme); - const colors = getDistributedColors(palette, dataKeys.length); - - // Create Config - const chartConfig: ChartConfig = dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - icon: icons[key], - color: colors[index], - }, - }), - {}, - ); + const chartConfig = createChartConfig({ + data, + categoryKey: categoryKey as string, + theme, + icons, + }); return ( - - {dataKeys.map((key) => { + + {Object.keys(chartConfig).map((key) => { const transformedKey = keyTransform(key); const color = `var(--color-${transformedKey})`; if (variant === "line") { diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/utils/RaderChartUtils.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/utils/RaderChartUtils.ts new file mode 100644 index 000000000..ec0b82987 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/utils/RaderChartUtils.ts @@ -0,0 +1,44 @@ +import { ChartConfig } from "../../Charts"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; + +export type RadarChartData = Array>; + +export interface RadarChartConfig { + data: RadarChartData; + categoryKey: string; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + icons?: Partial>; +} + +// Create chart configuration +export const createChartConfig = ({ + data, + categoryKey, + theme = "ocean", + icons = {}, +}: RadarChartConfig): ChartConfig => { + // excluding the categoryKey + const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + const palette = getPalette(theme); + const colors = getDistributedColors(palette, dataKeys.length); + + return dataKeys.reduce( + (config, key, index) => ({ + ...config, + [key]: { + label: key, + icon: icons[key], + color: colors[index], + }, + }), + {}, + ); +}; + +// Get radar chart margin +export const getRadarChartMargin = () => ({ + top: 10, + right: 10, + bottom: 10, + left: 10, +}); diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx index a3c88f257..684f02f0a 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx @@ -3,17 +3,18 @@ import { debounce } from "lodash-es"; import { useEffect, useRef, useState } from "react"; import { Cell, RadialBar, RadialBarChart } from "recharts"; import { useLayoutContext } from "../../../../context/LayoutContext"; -import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts"; import { - calculatePercentage, + RadialChartData, calculateRadialChartDimensions, + createRadialChartConfig, getHoverStyles, layoutMap, + transformRadialData, useChartHover, } from "../utils/RadialChartUtils"; -export type MiniRadialChartData = Array>; +export type MiniRadialChartData = RadialChartData; export interface MiniRadialChartProps { data: T; @@ -62,32 +63,11 @@ export const MiniRadialChart = ({ return () => resizeObserver.disconnect(); }, [layout, label, variant]); - // Calculate total for percentage calculations - const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); - - // Get color palette and distribute colors - const palette = getPalette(theme); - const colors = getDistributedColors(palette, data.length); - - // Transform data with percentages - const transformedData = data.map((item, index) => ({ - ...item, - percentage: calculatePercentage(Number(item[dataKey as string]), total), - originalValue: item[dataKey as string], - fill: colors[index], - })); + // Transform data with percentages and colors + const transformedData = transformRadialData(data, dataKey, theme); // Create chart configuration - const chartConfig = data.reduce( - (config, item, index) => ({ - ...config, - [String(item[categoryKey])]: { - label: String(item[categoryKey as string]), - color: colors[index], - }, - }), - {}, - ); + const chartConfig = createRadialChartConfig(data, categoryKey, theme); return ( >; + +export interface RadialChartConfig { + data: RadialChartData; + categoryKey: string; + dataKey: string; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; +} // Helper function to calculate percentage export const calculatePercentage = (value: number, total: number): number => { @@ -88,3 +99,42 @@ export const formatRadialLabel = ( export const getRadialFontSize = (dataLength: number): number => { return dataLength <= 5 ? 12 : 7; }; + +// Transform data with percentages and colors +export const transformRadialData = ( + data: T, + dataKey: keyof T[number], + theme: string = "ocean", +) => { + const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); + const palette = getPalette(theme); + const colors = getDistributedColors(palette, data.length); + + return data.map((item, index) => ({ + ...item, + percentage: calculatePercentage(Number(item[dataKey as string]), total), + originalValue: item[dataKey as string], + fill: colors[index], + })); +}; + +// Create chart configuration +export const createRadialChartConfig = ( + data: T, + categoryKey: keyof T[number], + theme: string = "ocean", +) => { + const palette = getPalette(theme); + const colors = getDistributedColors(palette, data.length); + + return data.reduce( + (config, item, index) => ({ + ...config, + [String(item[categoryKey])]: { + label: String(item[categoryKey as string]), + color: colors[index], + }, + }), + {}, + ); +}; From d6a311da85542d4efe8cbeb43554d7524ab708e0 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 30 May 2025 19:33:23 +0530 Subject: [PATCH 026/190] feat(BarChartV2): bar chart overhall The changes add new data series for tablet and laptop to the BarChartV2 component. This allows for more detailed visualization of the data across different device types. Additionally, the legend has been added to display the data series and their corresponding colors/icons. --- js/packages/react-ui/package.json | 3 +- .../BarCharts/BarChartV2/BarChartV2.tsx | 243 ++++++++++-------- .../Charts/BarCharts/BarChartV2/barChar.scss | 15 ++ .../BarChartV2/components/DefaultLegend.tsx | 61 +++++ .../BarChartV2/components/LineInBarShape.tsx | 82 ++++++ .../BarChartV2/components/YAxisTick.tsx | 48 ++++ .../BarChartV2/components/defaultLegend.scss | 67 +++++ .../BarChartV2/components/yAxisTick.scss | 5 + .../BarChartV2/stories/barChartV2.stories.tsx | 107 ++------ .../Charts/BarCharts/utils/BarChartUtils.ts | 28 +- .../src/components/Charts/charts.scss | 90 +++---- js/pnpm-lock.yaml | 22 +- 12 files changed, 520 insertions(+), 251 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/DefaultLegend.tsx create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/YAxisTick.tsx create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/defaultLegend.scss create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/yAxisTick.scss diff --git a/js/packages/react-ui/package.json b/js/packages/react-ui/package.json index 535c50134..71c39049a 100644 --- a/js/packages/react-ui/package.json +++ b/js/packages/react-ui/package.json @@ -73,7 +73,7 @@ "react-day-picker": "^9.5.1", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^15.6.1", - "recharts": "^2.15.1", + "recharts": "^2.15.3", "rehype-katex": "^7.0.1", "remark-breaks": "^4.0.0", "remark-emoji": "^5.0.1", @@ -85,7 +85,6 @@ "@chromatic-com/storybook": "^3.2.4", "@storybook/addon-essentials": "^8.5.3", "@storybook/addon-interactions": "^8.5.3", - "@storybook/addon-onboarding": "^8.5.3", "@storybook/addon-styling-webpack": "^1.0.1", "@storybook/addon-themes": "^8.5.3", "@storybook/blocks": "^8.5.3", diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 1a4765123..2a35b5110 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -1,5 +1,6 @@ +import clsx from "clsx"; import React, { useEffect, useRef, useState } from "react"; -import { Bar, LabelList, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; +import { Bar, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; import { ChartConfig, ChartContainer, @@ -9,7 +10,16 @@ import { } from "../../Charts"; import { cartesianGrid } from "../../cartesianGrid"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; -import { getPadding, getRadiusArray, getWidthOfData } from "../utils/BarChartUtils"; +import { + BAR_WIDTH, + getPadding, + getRadiusArray, + getWidthOfData, + getYAxisTickFormatter, +} from "../utils/BarChartUtils"; +import { DefaultLegend, LegendItem } from "./components/DefaultLegend"; +import { LineInBarShape } from "./components/LineInBarShape"; +import { YAxisTick } from "./components/YAxisTick"; export type BarChartData = Array>; @@ -21,7 +31,6 @@ export interface BarChartPropsV2 { theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; variant?: Variant; grid?: boolean; - label?: boolean; radius?: number; icons?: Partial>; isAnimationActive?: boolean; @@ -29,6 +38,10 @@ export interface BarChartPropsV2 { xAxisLabel?: React.ReactNode; yAxisLabel?: React.ReactNode; onBarsClick?: (data: any) => void; + barInternalLineColor?: string; + barInternalLineWidth?: number; + legend?: boolean; + className?: string; } export const BarChartV2 = ({ @@ -37,7 +50,6 @@ export const BarChartV2 = ({ theme = "ocean", variant = "grouped", grid = true, - label = true, icons = {}, radius = 2, isAnimationActive = true, @@ -45,6 +57,10 @@ export const BarChartV2 = ({ xAxisLabel, yAxisLabel, onBarsClick, + barInternalLineColor = "rgba(255, 255, 255, 0.5)", // Default internal line color + barInternalLineWidth = 1, // Default internal line width + legend = false, + className, }: BarChartPropsV2) => { // excluding the categoryKey const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); @@ -98,110 +114,131 @@ export const BarChartV2 = ({ }, []); const padding = getPadding(data, categoryKey as string, containerWidth, variant); - const width = getWidthOfData(data, categoryKey as string, variant); + const dataWidth = getWidthOfData(data, categoryKey as string, variant); + + // Create legend items for custom legend + const legendItems: LegendItem[] = dataKeys.map((key, index) => ({ + key, + label: key, + color: colors[index] || "#000000", // Fallback color if undefined + icon: icons[key] as React.ComponentType | undefined, + })); + + // Calculate chart height based on aspect ratio + const chartHeight = containerWidth ? containerWidth * (9 / 16) : 400; return ( -
- - - {grid && cartesianGrid()} - +
+ {showYAxis && ( +
+ {/* Y-axis only chart - synchronized with main chart */} + + } + /> + {/* Invisible bars to maintain scale synchronization */} + {dataKeys.map((key) => { + return ( + + ); + })} + +
+ )} +
+ - {showYAxis && ( - + - )} - } /> - {dataKeys.map((key, index) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - const isFirstInStack = index === 0; - const isLastInStack = index === dataKeys.length - 1; - - if (label) { - return ( - - {label && ( - - )} - - ); - } - return ( - + {grid && cartesianGrid()} + - ); - })} - - + {/* Y-axis is rendered in the separate synchronized chart */} + } /> + {dataKeys.map((key, index) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + const isFirstInStack = index === 0; + const isLastInStack = index === dataKeys.length - 1; + + return ( + + } + /> + ); + })} + + +
+
+ {legend && ( + + )}
); }; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss new file mode 100644 index 000000000..0e4759564 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss @@ -0,0 +1,15 @@ +@use "../../../../cssUtils" as cssUtils; + +.crayon-bar-chart-container-inner { + display: flex; + width: 100%; +} + +.crayon-bar-chart-y-axis-container { + flex-shrink: 0; +} + +.crayon-bar-chart-main-container { + width: 100%; + overflow-x: auto; +} diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/DefaultLegend.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/DefaultLegend.tsx new file mode 100644 index 000000000..4a29ede7b --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/DefaultLegend.tsx @@ -0,0 +1,61 @@ +import clsx from "clsx"; +import React from "react"; + +interface LegendItem { + key: string; + label: string; + color: string; + icon?: React.ComponentType; +} + +interface DefaultLegendProps { + items: LegendItem[]; + className?: string; + yAxisLabel?: React.ReactNode; + xAxisLabel?: React.ReactNode; +} + +const DefaultLegend: React.FC = ({ + items, + className, + yAxisLabel, + xAxisLabel, +}) => { + return ( +
+ {(xAxisLabel || yAxisLabel) && ( +
+ {xAxisLabel && ( + + X-Axis: {xAxisLabel} + + )} + {yAxisLabel && ( + + Y-Axis: {yAxisLabel} + + )} +
+ )} + +
+ {items.map((item) => ( +
+ {item.icon ? ( + + ) : ( +
+ )} + {item.label} +
+ ))} +
+
+ ); +}; + +export { DefaultLegend }; +export type { DefaultLegendProps, LegendItem }; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx new file mode 100644 index 000000000..0b307838b --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx @@ -0,0 +1,82 @@ +import { FunctionComponent } from "react"; + +interface BarWithInternalLineProps { + x?: number; + y?: number; + width?: number; + height?: number; + fill?: string; + stroke?: string; + strokeWidth?: number | string; + radius?: number | number[]; + internalLineColor?: string; + internalLineWidth?: number; + // Recharts also passes other props like payload, value, etc. + // we can add them here if our shape component needs them. + // console.log("props", props); + [key: string]: any; // Allow other props from Recharts +} + +const LineInBarShape: FunctionComponent = (props) => { + const { + x = 0, // Default to 0 to avoid NaN issues if undefined + y = 0, + width = 0, + height = 0, + fill, + radius: r, // Renaming to avoid conflict with BarChartV2's radius prop + stroke, + strokeWidth, + internalLineColor: iLineColor, // Use prop or fallback + internalLineWidth: iLineWidth, + } = props; + + // Ensure rTL and rTR are always numbers, defaulting to 0. + let rTL: number, rTR: number; + if (Array.isArray(r)) { + rTL = r[0] || 0; + rTR = r[1] || 0; + } else if (typeof r === "number") { + rTL = r; + rTR = r; + } else { + rTL = 0; + rTR = 0; + } + + // Path data for a rectangle with potentially rounded top corners + // M = move to, L = line to, A = arc, Z = close path + // Handle cases where rTL or rTR might be 0 (sharp corners) + const path = ` + M ${x},${y + rTL} + ${rTL > 0 ? `A ${rTL},${rTL} 0 0 1 ${x + rTL},${y}` : `L ${x},${y}`} + L ${x + width - rTR},${y} + ${rTR > 0 ? `A ${rTR},${rTR} 0 0 1 ${x + width},${y + rTR}` : `L ${x + width},${y}`} + L ${x + width},${y + height} + L ${x},${y + height} + Z + `; + + return ( + + {/* The main bar shape (using for rounded corners) */} + + + {/* The internal vertical line */} + {width > 0 && + height > 0 && ( // Only render line if bar has dimensions + + )} + + ); +}; + +export { LineInBarShape }; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/YAxisTick.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/YAxisTick.tsx new file mode 100644 index 000000000..5bd42fe95 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/YAxisTick.tsx @@ -0,0 +1,48 @@ +interface YAxisTickProps { + x?: number; + y?: number; + payload?: { + value: any; + coordinate?: number; + tickCoord?: number; + index?: number; + offset?: number; + isShow?: boolean; + }; + textAnchor?: "start" | "middle" | "end"; + verticalAnchor?: "start" | "middle" | "end"; + fill?: string; + stroke?: string; + width?: number; + height?: number; + className?: string; + orientation?: "left" | "right"; + tickFormatter?: (value: any) => string; + index?: number; + visibleTicksCount?: number; +} + +const YAxisTick: React.FC = (props) => { + const { x, y, payload, textAnchor, verticalAnchor, fill, tickFormatter, className } = props; + + // Use the provided tickFormatter or fallback to displaying raw value + const displayValue = tickFormatter ? tickFormatter(payload?.value) : String(payload?.value || ""); + + return ( + + + {displayValue} + + + ); +}; + +export { YAxisTick }; +export type { YAxisTickProps }; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/defaultLegend.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/defaultLegend.scss new file mode 100644 index 000000000..4ddb9e1b8 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/defaultLegend.scss @@ -0,0 +1,67 @@ +@use "../../../../../cssUtils" as cssUtils; + +.crayon-chart-legend-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.crayon-chart-legend-axis-label-container { + display: flex; + align-items: center; + justify-content: center; + gap: cssUtils.$spacing-m; + flex-wrap: wrap; +} + +.crayon-chart-legend-axis-label { + @include cssUtils.typography(label, small); + color: cssUtils.$secondary-text; + + &-text { + color: cssUtils.$primary-text; + } +} + +.crayon-chart-legend { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + text-transform: capitalize; + flex-wrap: wrap; + + &--bottom { + padding-top: cssUtils.$spacing-m; + } + + &-item { + display: flex; + align-items: center; + gap: cssUtils.$spacing-xs; + + svg { + height: 12px; + width: 12px; + color: cssUtils.$primary-text; + } + + &-indicator { + height: 8px; + width: 8px; + flex-shrink: 0; + border-radius: cssUtils.$rounded-3xs; + background-color: var(--color-bg); + } + + &-label { + @include cssUtils.typography(label, small); + max-width: 64px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: cssUtils.$primary-text; + } + } +} diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/yAxisTick.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/yAxisTick.scss new file mode 100644 index 000000000..7f24797ce --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/yAxisTick.scss @@ -0,0 +1,5 @@ +@use "../../../../../cssUtils" as cssUtils; + +.crayon-chart-y-axis-tick { + @include cssUtils.typography(label, extra-small); +} diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx index 81b3f6588..995d537a9 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx @@ -4,18 +4,18 @@ import { Card } from "../../../../Card"; import { BarChartPropsV2, BarChartV2 } from "../BarChartV2"; const barChartData = [ - { month: "January", desktop: 45, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, - { month: "July", desktop: 350, mobile: 200 }, - { month: "August", desktop: 400, mobile: 220 }, - { month: "September", desktop: 450, mobile: 240 }, - { month: "October", desktop: 500, mobile: 260 }, - { month: "November", desktop: 550, mobile: 280 }, - { month: "December", desktop: 600, mobile: 300 }, + { month: "January", desktop: 150, mobile: 90, tablet: 120, laptop: 180 }, + { month: "February", desktop: 280, mobile: 180, tablet: 140, laptop: 160 }, + { month: "March", desktop: 220, mobile: 140, tablet: 160, laptop: 180 }, + { month: "April", desktop: 180, mobile: 160, tablet: 180, laptop: 200 }, + { month: "May", desktop: 250, mobile: 120, tablet: 140, laptop: 160 }, + { month: "June", desktop: 300, mobile: 180, tablet: 160, laptop: 180 }, + { month: "July", desktop: 350, mobile: 220, tablet: 180, laptop: 200 }, + { month: "August", desktop: 400, mobile: 240, tablet: 200, laptop: 220 }, + { month: "September", desktop: 450, mobile: 260, tablet: 220, laptop: 240 }, + { month: "October", desktop: 500, mobile: 280, tablet: 240, laptop: 260 }, + { month: "November", desktop: 550, mobile: 300, tablet: 260, laptop: 280 }, + { month: "December", desktop: 600, mobile: 320, tablet: 280, laptop: 300 }, ]; const icons = { @@ -32,8 +32,6 @@ const meta: Meta> = { description: { component: "```tsx\nimport { BarChartV2 } from '@crayon-ui/react-ui/Charts/BarChartV2';\n```", - component: - "```tsx\nimport { BarChartV2 } from '@crayon-ui/react-ui/Charts/BarChartV2';\n```", }, }, }, @@ -107,25 +105,7 @@ const meta: Meta> = { category: "Display", }, }, - label: { - description: "Whether to display data point labels above each point on the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - // legend: { - // description: - // "Whether to display the chart legend showing the data series names and their corresponding colors/icons", - // control: "boolean", - // table: { - // type: { summary: "boolean" }, - // defaultValue: { summary: "true" }, - // category: "Display", - // }, - // }, + isAnimationActive: { description: "Whether to animate the chart", control: "boolean", @@ -162,6 +142,15 @@ const meta: Meta> = { category: "Data", }, }, + legend: { + description: "Whether to display the chart legend", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "Display", + }, + }, }, } satisfies Meta; @@ -177,9 +166,11 @@ export const BarChartV2Story: Story = { variant: "grouped", radius: 2, grid: true, - label: true, isAnimationActive: true, - showYAxis: false, + showYAxis: true, + xAxisLabel: "Time Period", + yAxisLabel: "Number of Users", + legend: true, }, render: (args: BarChartPropsV2) => ( @@ -278,8 +269,6 @@ export const BarChartV2WithYAxis: Story = { args: { ...BarChartV2Story.args, showYAxis: true, - xAxisLabel: "Time Period", - yAxisLabel: "Number of Users", }, render: (args: BarChartPropsV2) => ( @@ -365,50 +354,6 @@ const barChartData = [ }, }; -export const BarChartV2NoLabels: Story = { - name: "Bar Chart V2 without Labels", - args: { - ...BarChartV2Story.args, - label: false, - theme: "sunset", - }, - render: (args) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` -const barChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - - - - -`, - }, - }, - }, -}; - export const BarChartV2Experimental: Story = { name: "Bar Chart V2 Experimental", args: { diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts index 2795c7349..ffa905178 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts @@ -1,7 +1,7 @@ import { Variant } from "../BarChartV2"; -const BAR_WIDTH = 8; -const ELEMENT_SPACING = 20; // Spacing per bar in grouped, or per stack in stacked +export const BAR_WIDTH = 12; +export const ELEMENT_SPACING = 15; // Spacing per bar in grouped, or per stack in stacked const getWidthOfData = ( data: Array>, @@ -75,4 +75,26 @@ const getRadiusArray = ( return [radius, radius, radius, radius]; }; -export { getPadding, getRadiusArray, getWidthOfData }; +const getYAxisTickFormatter = () => { + return (value: any) => { + // Format the Y-axis tick values with abbreviations + if (typeof value === "number") { + const absValue = Math.abs(value); + + if (absValue >= 1e12) { + return (value / 1e12).toFixed(absValue >= 10e12 ? 0 : 1) + "T"; + } else if (absValue >= 1e9) { + return (value / 1e9).toFixed(absValue >= 10e9 ? 0 : 1) + "B"; + } else if (absValue >= 1e6) { + return (value / 1e6).toFixed(absValue >= 10e6 ? 0 : 1) + "M"; + } else if (absValue >= 1e3) { + return (value / 1e3).toFixed(absValue >= 10e3 ? 0 : 1) + "K"; + } else { + return value.toString(); + } + } + return String(value); + }; +}; + +export { getPadding, getRadiusArray, getWidthOfData, getYAxisTickFormatter }; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index dd3eae8ac..f281cf83c 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -1,4 +1,7 @@ @use "../../cssUtils" as cssUtils; +@forward "./BarCharts/BarChartV2/barChar.scss"; +@forward "./BarCharts/BarChartV2/components/yAxisTick.scss"; +@forward "./BarCharts/BarChartV2/components/defaultLegend.scss"; .crayon-chart { // Container styles @@ -160,52 +163,49 @@ } } + // this is getting used in DefaultLegend.tsx // Legend styles - &-legend { - display: flex; - align-items: center; - justify-content: center; - gap: 16px; - text-transform: capitalize; - flex-wrap: wrap; - - &--top { - padding-bottom: cssUtils.$spacing-s; - } - - &--bottom { - padding-top: cssUtils.$spacing-m; - } - - &-item { - display: flex; - align-items: center; - gap: cssUtils.$spacing-xs; - - svg { - height: 12px; - width: 12px; - color: cssUtils.$primary-text; - } - - &-indicator { - height: 8px; - width: 8px; - flex-shrink: 0; - border-radius: cssUtils.$rounded-3xs; - background-color: var(--color-bg); - } - - &-label { - @include cssUtils.typography(label, heavy); - max-width: 64px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: cssUtils.$primary-text; - } - } - } + // &-legend { + // display: flex; + // align-items: center; + // justify-content: center; + // gap: 16px; + // text-transform: capitalize; + // flex-wrap: wrap; + + // &--bottom { + // padding-top: cssUtils.$spacing-m; + // } + + // &-item { + // display: flex; + // align-items: center; + // gap: cssUtils.$spacing-xs; + + // svg { + // height: 12px; + // width: 12px; + // color: cssUtils.$primary-text; + // } + + // &-indicator { + // height: 8px; + // width: 8px; + // flex-shrink: 0; + // border-radius: cssUtils.$rounded-3xs; + // background-color: var(--color-bg); + // } + + // &-label { + // @include cssUtils.typography(label, heavy); + // max-width: 64px; + // overflow: hidden; + // text-overflow: ellipsis; + // white-space: nowrap; + // color: cssUtils.$primary-text; + // } + // } + // } } .crayon-chart-cartesian-grid { diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index 013ebdfb5..417470ad1 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -166,8 +166,8 @@ importers: specifier: ^15.6.1 version: 15.6.1(react@18.3.1) recharts: - specifier: ^2.15.1 - version: 2.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^2.15.3 + version: 2.15.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rehype-katex: specifier: ^7.0.1 version: 7.0.1 @@ -199,9 +199,6 @@ importers: '@storybook/addon-interactions': specifier: ^8.5.3 version: 8.5.3(storybook@8.5.3(prettier@3.4.2)) - '@storybook/addon-onboarding': - specifier: ^8.5.3 - version: 8.5.3(storybook@8.5.3(prettier@3.4.2)) '@storybook/addon-styling-webpack': specifier: ^1.0.1 version: 1.0.1(storybook@8.5.3(prettier@3.4.2))(webpack@5.97.1(esbuild@0.24.2)) @@ -1942,11 +1939,6 @@ packages: peerDependencies: storybook: ^8.5.3 - '@storybook/addon-onboarding@8.5.3': - resolution: {integrity: sha512-NZhYj3UZK65reO7mXcK7FPPu7QkLCRyIa6TpfQ3mRAocfjqg401mcBsRO37JNywYfHCZrU4w1l7pwpqjvcYceg==} - peerDependencies: - storybook: ^8.5.3 - '@storybook/addon-outline@8.5.3': resolution: {integrity: sha512-e1MkGN6XVdeRh2oUKGdqEDyAo2TD/47ashAAxw8DEiLRWgBMbQ+KBVH4EOG+dn5395jxh7YgRLJn/miqNnfN5g==} peerDependencies: @@ -3875,8 +3867,8 @@ packages: recharts-scale@0.4.5: resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} - recharts@2.15.1: - resolution: {integrity: sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==} + recharts@2.15.3: + resolution: {integrity: sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==} engines: {node: '>=14'} peerDependencies: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -5768,10 +5760,6 @@ snapshots: storybook: 8.5.3(prettier@3.4.2) tiny-invariant: 1.3.3 - '@storybook/addon-onboarding@8.5.3(storybook@8.5.3(prettier@3.4.2))': - dependencies: - storybook: 8.5.3(prettier@3.4.2) - '@storybook/addon-outline@8.5.3(storybook@8.5.3(prettier@3.4.2))': dependencies: '@storybook/global': 5.0.0 @@ -8042,7 +8030,7 @@ snapshots: dependencies: decimal.js-light: 2.5.1 - recharts@2.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + recharts@2.15.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: clsx: 2.1.1 eventemitter3: 4.0.7 From abab871a5b120ecb4d45d798cce862d4f151d23f Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 30 May 2025 20:30:58 +0530 Subject: [PATCH 027/190] feat: Add XAxisTick component and styles This commit introduces the XAxisTick component and its corresponding styles in the BarChartV2 component. The XAxisTick component is responsible for rendering the x-axis ticks, providing customization options such as text alignment, vertical anchor, and tick formatting. The changes include: - Adding the `XAxisTick` component in the `BarChartV2.tsx` file. - Introducing the `xAxisTick.scss` file to handle the styles for the x-axis ticks. - Forwarding the `xAxisTick.scss` file in the `charts.scss` file. These changes enhance the functionality and appearance of the BarChartV2 component, allowing for better control and customization of the x-axis ticks. --- .../BarCharts/BarChartV2/BarChartV2.tsx | 7 +- .../BarChartV2/components/XAxisTick.tsx | 68 +++++++++++++++++++ .../BarChartV2/components/xAxisTick.scss | 5 ++ .../Charts/WidgetsChart/WidgetsChart.tsx | 50 +++++++------- .../src/components/Charts/charts.scss | 2 + 5 files changed, 105 insertions(+), 27 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/xAxisTick.scss diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 2a35b5110..8189806a6 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -19,6 +19,7 @@ import { } from "../utils/BarChartUtils"; import { DefaultLegend, LegendItem } from "./components/DefaultLegend"; import { LineInBarShape } from "./components/LineInBarShape"; +import { XAxisTick } from "./components/XAxisTick"; import { YAxisTick } from "./components/YAxisTick"; export type BarChartData = Array>; @@ -135,7 +136,7 @@ export const BarChartV2 = ({ {/* Y-axis only chart - synchronized with main chart */} ({ syncId="barChartSync" > ({ textAnchor="middle" tickFormatter={getTickFormatter()} interval="preserveStartEnd" + tick={} + orientation="bottom" // gives the padding on the 2 sides see the function for reference padding={padding} /> diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx new file mode 100644 index 000000000..b2d5a5a55 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx @@ -0,0 +1,68 @@ +interface XAxisTickProps { + x?: number; + y?: number; + payload?: { + value: any; + coordinate?: number; + tickCoord?: number; + index?: number; + offset?: number; + isShow?: boolean; + }; + textAnchor?: "start" | "middle" | "end"; + verticalAnchor?: "start" | "middle" | "end"; + fill?: string; + stroke?: string; + width?: number; + height?: number; + className?: string; + orientation?: "top" | "bottom"; + tickFormatter?: (value: any) => string; + index?: number; + visibleTicksCount?: number; +} + +const XAxisTick: React.FC = (props) => { + const { + x, + y, + payload, + textAnchor = "middle", + verticalAnchor = "start", + fill = "#666", + tickFormatter, + className, + orientation = "bottom" + } = props; + + const displayValue = tickFormatter ? tickFormatter(payload?.value) : String(payload?.value || ""); + + // Calculate text offset based on orientation and vertical anchor + const getTextOffset = () => { + const isBottom = orientation === "bottom"; + const offsetMap = { + start: isBottom ? 6 : -4, + middle: isBottom ? 6 : -8, + end: isBottom ? -8 : -16 + }; + return offsetMap[verticalAnchor] || 0; + }; + + return ( + + + {displayValue} + + + ); +}; + +export { XAxisTick }; +export type { XAxisTickProps }; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/xAxisTick.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/xAxisTick.scss new file mode 100644 index 000000000..632f66a01 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/xAxisTick.scss @@ -0,0 +1,5 @@ +@use "../../../../../cssUtils" as cssUtils; + +.crayon-chart-x-axis-tick { + @include cssUtils.typography(label, small); +} diff --git a/js/packages/react-ui/src/components/Charts/WidgetsChart/WidgetsChart.tsx b/js/packages/react-ui/src/components/Charts/WidgetsChart/WidgetsChart.tsx index 4ae9e9269..5815e5ba5 100644 --- a/js/packages/react-ui/src/components/Charts/WidgetsChart/WidgetsChart.tsx +++ b/js/packages/react-ui/src/components/Charts/WidgetsChart/WidgetsChart.tsx @@ -1,26 +1,26 @@ -const WidgetsChart = () => { - const calculateTotal = ( - data: T, - categoryKey: keyof T[number], - ): number => { - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); - return data.reduce((sum, item) => { - return sum + dataKeys.reduce((keySum, key) => keySum + Number(item[key] || 0), 0); - }, 0); - }; - return ( -
-
-
- - {calculateTotal(data, categoryKey).toLocaleString()} - - - {label} - -
-
- ); -}; +// const WidgetsChart = () => { +// const calculateTotal = ( +// data: T, +// categoryKey: keyof T[number], +// ): number => { +// const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); +// return data.reduce((sum, item) => { +// return sum + dataKeys.reduce((keySum, key) => keySum + Number(item[key] || 0), 0); +// }, 0); +// }; +// return ( +//
+//
+//
+// +// {calculateTotal(data, categoryKey).toLocaleString()} +// +// +// {label} +// +//
+//
+// ); +// }; -export default WidgetsChart; +// export default WidgetsChart; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index f281cf83c..40eb9d71b 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -1,6 +1,7 @@ @use "../../cssUtils" as cssUtils; @forward "./BarCharts/BarChartV2/barChar.scss"; @forward "./BarCharts/BarChartV2/components/yAxisTick.scss"; +@forward "./BarCharts/BarChartV2/components/xAxisTick.scss"; @forward "./BarCharts/BarChartV2/components/defaultLegend.scss"; .crayon-chart { @@ -164,6 +165,7 @@ } // this is getting used in DefaultLegend.tsx + // now moved to defaultLegend.scss file once review is done remove it // Legend styles // &-legend { // display: flex; From 6a4724c081b24a1977e9fd82d67a2ca6fc88fcd3 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sat, 31 May 2025 04:24:18 +0530 Subject: [PATCH 028/190] feat(BarChartV2): Increase chart width and add custom cursor - Increase the width of the BarChartV2 component from 500px to 700px to provide more space for the chart. - Add a custom cursor component to the BarChartV2 component to enhance the user experience. - Implement a custom X-axis tick formatter to limit the length of the tick labels to 3 characters. - Add support for a secondary color in the chart configuration, which is used for the secondary bars in the chart. - Implement scroll buttons to allow the user to navigate the chart when the content is wider than the container. --- .../BarCharts/BarChartV2/BarChartV2.tsx | 41 ++-- .../Charts/BarCharts/BarChartV2/barChar.scss | 16 ++ .../BarChartV2/components/CustomCursor.scss | 7 + .../BarChartV2/components/CustomCursor.tsx | 188 ++++++++++++++++++ .../BarChartV2/stories/barChartV2.stories.tsx | 2 +- .../Charts/BarCharts/utils/BarChartUtils.ts | 17 +- .../react-ui/src/components/Charts/Charts.tsx | 22 +- .../src/components/Charts/charts.scss | 1 + 8 files changed, 269 insertions(+), 25 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.scss create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 8189806a6..6815d8eeb 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -1,6 +1,8 @@ import clsx from "clsx"; -import React, { useEffect, useRef, useState } from "react"; +import { ChevronFirst, ChevronLast } from "lucide-react"; +import React, { useEffect, useId, useRef, useState } from "react"; import { Bar, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; +import { IconButton } from "../../../IconButton"; import { ChartConfig, ChartContainer, @@ -15,8 +17,10 @@ import { getPadding, getRadiusArray, getWidthOfData, + getXAxisTickFormatter, getYAxisTickFormatter, } from "../utils/BarChartUtils"; +import { SimpleCursor } from "./components/CustomCursor"; import { DefaultLegend, LegendItem } from "./components/DefaultLegend"; import { LineInBarShape } from "./components/LineInBarShape"; import { XAxisTick } from "./components/XAxisTick"; @@ -77,22 +81,12 @@ export const BarChartV2 = ({ label: key, icon: icons[key], color: colors[index], + secondaryColor: colors[dataKeys.length - index - 1], }, }), {}, ); - const getTickFormatter = () => { - const maxLength = 3; - - return (value: string) => { - if (value.length > maxLength) { - return `${value.slice(0, maxLength)}`; - } - return value; - }; - }; - const chartContainerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(0); @@ -128,6 +122,10 @@ export const BarChartV2 = ({ // Calculate chart height based on aspect ratio const chartHeight = containerWidth ? containerWidth * (9 / 16) : 400; + const id = useId(); + + const chartSyncID = `bar-chart-sync-${id}`; + return (
@@ -145,7 +143,7 @@ export const BarChartV2 = ({ left: 0, right: 0, }} - syncId="barChartSync" + syncId={chartSyncID} > ({ onClick={onBarsClick} // barGap={2} // barCategoryGap={'20%'} - syncId="barChartSync" + syncId={chartSyncID} > {grid && cartesianGrid()} ({ tickLine={false} axisLine={false} textAnchor="middle" - tickFormatter={getTickFormatter()} + tickFormatter={getXAxisTickFormatter()} interval="preserveStartEnd" tick={} orientation="bottom" @@ -205,10 +203,11 @@ export const BarChartV2 = ({ padding={padding} /> {/* Y-axis is rendered in the separate synchronized chart */} - } /> + } content={} /> {dataKeys.map((key, index) => { const transformedKey = keyTransform(key); const color = `var(--color-${transformedKey})`; + // const secondaryColor = `var(--color-${transformedKey}-secondary)`; const isFirstInStack = index === 0; const isLastInStack = index === dataKeys.length - 1; @@ -239,6 +238,16 @@ export const BarChartV2 = ({
+
+ } + /> + } + /> +
{legend && ( )} diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss index 0e4759564..c57d145c9 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss @@ -13,3 +13,19 @@ width: 100%; overflow-x: auto; } + +.crayon-bar-chart-scroll-container { + position: relative; +} + +.crayon-bar-chart-scroll-button { + position: absolute; + + &--left { + left: 20px; + } + + &--right { + right: 0; + } +} diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.scss new file mode 100644 index 000000000..774807c92 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.scss @@ -0,0 +1,7 @@ +@use "../../../../../cssUtils" as cssUtils; + +.crayon-chart-cursor-shape { + fill: cssUtils.$bg-sunk; + stroke: cssUtils.$stroke-default; + stroke-width: 2; +} diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx new file mode 100644 index 000000000..50d66d527 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx @@ -0,0 +1,188 @@ +import React from "react"; + +interface CustomCursorProps { + x?: number; + y?: number; + width?: number; + height?: number; + payload?: any[]; + // Other props that Recharts might pass + [key: string]: any; +} + +export const CustomCursor: React.FC = ({ + x = 0, + y = 0, + width = 0, + height = 0, + payload, + ...props +}) => { + // You can access the data at the cursor position through payload + const hasData = payload && payload.length > 0; + + return ( + + {/* Define a gradient for the background */} + + + + + + + + + + + + + + {/* Main cursor background with rounded corners */} + + + {/* Central vertical line */} + + + {/* Top indicator dot */} + + + {/* Bottom indicator dot */} + + + {/* Side accent lines */} + + + + + {/* Data count indicator (if there's data) */} + {hasData && payload && payload.length > 1 && ( + + + + {payload.length} + + + )} + + ); +}; + +// Alternative simpler cursor component with rounded top, square bottom +export const SimpleCursor: React.FC = (props) => { + const { x = 0, y = 0, width = 0, height = 0 } = props; + + // Define the radius for the top corners + const topRadius = 8; + + // Create a path for rounded top, square bottom + // M = Move to, L = Line to, Q = Quadratic curve, Z = Close path + + const pathData = ` + M ${x + topRadius} ${y} + L ${x + width - topRadius} ${y} + Q ${x + width} ${y} ${x + width} ${y + topRadius} + L ${x + width} ${y + height} + L ${x} ${y + height} + L ${x} ${y + topRadius} + Q ${x} ${y} ${x + topRadius} ${y} + Z + `; + + /* SVG Path Command Documentation: + * + * M ${x + topRadius} ${y} + * - M = "Move to" - Sets the starting point without drawing + * - Moves to a point on the top edge, offset by the radius from the left corner + * + * L ${x + width - topRadius} ${y} + * - L = "Line to" - Draws a straight line from current position + * - Draws the top horizontal line, stopping before the top-right corner radius + * + * Q ${x + width} ${y} ${x + width} ${y + topRadius} + * - Q = "Quadratic curve to" - Draws a curved line using control point + * - Control point: (x + width, y) - the actual top-right corner + * - End point: (x + width, y + topRadius) - down the right edge by radius amount + * - Creates the rounded top-right corner + * + * L ${x + width} ${y + height} + * - L = "Line to" - Draws straight line down the right edge to bottom-right corner + * + * L ${x} ${y + height} + * - L = "Line to" - Draws straight line across the bottom edge (square corner) + * + * L ${x} ${y + topRadius} + * - L = "Line to" - Draws straight line up the left edge to where the curve starts + * + * Q ${x} ${y} ${x + topRadius} ${y} + * - Q = "Quadratic curve to" - Creates the rounded top-left corner + * - Control point: (x, y) - the actual top-left corner + * - End point: (x + topRadius, y) - back to our starting position + * + * Z = "Close path" - Draws a line back to the starting point and closes the shape + */ + + return ( + + + + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx index 995d537a9..b3f718561 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx @@ -173,7 +173,7 @@ export const BarChartV2Story: Story = { legend: true, }, render: (args: BarChartPropsV2) => ( - + ), diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts index ffa905178..088098665 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts @@ -36,8 +36,8 @@ const getPadding = ( if (paddingValue < 0) { // If chart content is wider than container, no padding return { - left: 0, - right: 0, + left: 2, + right: 2, }; } else { return { @@ -97,4 +97,15 @@ const getYAxisTickFormatter = () => { }; }; -export { getPadding, getRadiusArray, getWidthOfData, getYAxisTickFormatter }; +const getXAxisTickFormatter = () => { + const maxLength = 3; + + return (value: string) => { + if (value.length > maxLength) { + return `${value.slice(0, maxLength)}`; + } + return value; + }; +}; + +export { getPadding, getRadiusArray, getWidthOfData, getXAxisTickFormatter, getYAxisTickFormatter }; diff --git a/js/packages/react-ui/src/components/Charts/Charts.tsx b/js/packages/react-ui/src/components/Charts/Charts.tsx index 9992bcc57..c07894aa1 100644 --- a/js/packages/react-ui/src/components/Charts/Charts.tsx +++ b/js/packages/react-ui/src/components/Charts/Charts.tsx @@ -23,8 +23,11 @@ export type ChartConfig = { label?: React.ReactNode; icon?: React.ComponentType; } & ( - | { color?: string; theme?: never } - | { color?: never; theme: Record } + | { color?: string; secondaryColor?: string; theme?: never } + | { color?: never; theme: Record } ); }; @@ -75,9 +78,18 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { ${colorConfig .map(([key, itemConfig]) => { const transformedKey = keyTransform(key); - const color = - itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color; - return color ? ` --color-${transformedKey}: ${color};` : null; + const themeValue = itemConfig.theme?.[theme as keyof typeof itemConfig.theme]; + const color = typeof themeValue === 'string' + ? themeValue + : themeValue?.color || itemConfig.color; + const secondaryColor = typeof themeValue === 'object' + ? themeValue?.secondaryColor + : ('secondaryColor' in itemConfig ? itemConfig.secondaryColor : undefined); + + return [ + color ? ` --color-${transformedKey}: ${color};` : null, + secondaryColor ? ` --color-${transformedKey}-secondary: ${secondaryColor};` : null + ].filter(Boolean).join("\n"); }) .filter(Boolean) .join("\n")} diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index 40eb9d71b..e69e7b522 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -3,6 +3,7 @@ @forward "./BarCharts/BarChartV2/components/yAxisTick.scss"; @forward "./BarCharts/BarChartV2/components/xAxisTick.scss"; @forward "./BarCharts/BarChartV2/components/defaultLegend.scss"; +@forward "./BarCharts/BarChartV2/components/CustomCursor.scss"; .crayon-chart { // Container styles From 1d24f09dad1d6e5b6dc09e18c0ee7ff509382064 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sat, 31 May 2025 04:28:53 +0530 Subject: [PATCH 029/190] feat(MiniRadialChart): remove unused legend prop The changes in this commit remove the unused `legend` prop from the `MiniRadialChart` component. This prop was previously set to `true` by default, but it was not being used in the component's implementation. By removing this prop, we can simplify the component's API and reduce the number of unnecessary configuration options. feat(Charts): improve theme handling The changes in this commit improve the handling of the `theme` prop in the `Charts` component. Previously, the `theme` prop could be either a string or an object, which made the code more complex to handle. The changes introduce a more consistent and type-safe approach, where the `theme` prop is an object that maps theme names to either a string or an object with `color` and `secondaryColor` properties. fix(XAxisTick): improve vertical alignment The changes in this commit fix the vertical alignment of the x-axis tick labels in the `XAxisTick` component. Previously, the vertical alignment was not always correct, depending on the `verticalAnchor` prop. The changes introduce a helper function that calculates the appropriate vertical offset based on the `verticalAnchor` and `orientation` props. refactor(MiniBarChart): remove unused calculateTotal function The changes in this commit remove the unused `calculateTotal` function from the `MiniBarChart` component. This function was previously used to calculate the total of the data values, but it is no longer needed in the current implementation of the component. refactor(MiniAreaChart): remove unused LayoutContext The changes in this commit remove the unused `LayoutContext` import from the `MiniAreaChart` component. This context was previously used in the component, but it is no longer needed in the current implementation. --- .../MiniAreaChart/MiniAreaChart.tsx | 2 - .../BarChartV2/components/CustomCursor.tsx | 1 - .../BarChartV2/components/XAxisTick.tsx | 22 +++++------ .../BarCharts/MiniBarChart/MiniBarChart.tsx | 18 ++++----- .../react-ui/src/components/Charts/Charts.tsx | 37 ++++++++++++------- .../TwoLevelPieChart/TwoLevelPieChart.tsx | 2 +- .../MiniRadialChart/MiniRadialChart.tsx | 4 +- 7 files changed, 47 insertions(+), 39 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx index bffa50b6e..f7e59b532 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx @@ -1,6 +1,5 @@ import React from "react"; import { Area, AreaChart as RechartsAreaChart, XAxis } from "recharts"; -import { useLayoutContext } from "../../../../context/LayoutContext"; import { ChartContainer, keyTransform } from "../../Charts"; import { MiniAreaChartData, createChartConfig } from "../utils/AreaChartUtils"; @@ -23,7 +22,6 @@ export const MiniAreaChart = ({ icons = {}, isAnimationActive = true, }: MiniAreaChartProps) => { - const { layout } = useLayoutContext(); const chartConfig = createChartConfig({ data, categoryKey: categoryKey as string, theme, icons }); return ( diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx index 50d66d527..6655cdcea 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx @@ -16,7 +16,6 @@ export const CustomCursor: React.FC = ({ width = 0, height = 0, payload, - ...props }) => { // You can access the data at the cursor position through payload const hasData = payload && payload.length > 0; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx index b2d5a5a55..28d1613fd 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx @@ -23,16 +23,16 @@ interface XAxisTickProps { } const XAxisTick: React.FC = (props) => { - const { - x, - y, - payload, - textAnchor = "middle", - verticalAnchor = "start", - fill = "#666", - tickFormatter, - className, - orientation = "bottom" + const { + x, + y, + payload, + textAnchor = "middle", + verticalAnchor = "start", + fill = "#666", + tickFormatter, + className, + orientation = "bottom", } = props; const displayValue = tickFormatter ? tickFormatter(payload?.value) : String(payload?.value || ""); @@ -43,7 +43,7 @@ const XAxisTick: React.FC = (props) => { const offsetMap = { start: isBottom ? 6 : -4, middle: isBottom ? 6 : -8, - end: isBottom ? -8 : -16 + end: isBottom ? -8 : -16, }; return offsetMap[verticalAnchor] || 0; }; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx index 1131016bf..3b339c6a5 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx @@ -71,15 +71,15 @@ export const MiniBarChart = ({ const width = getWidthOfData(data, categoryKey as string, variant); // Helper function to calculate total - const calculateTotal = ( - data: T, - categoryKey: keyof T[number], - ): number => { - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); - return data.reduce((sum, item) => { - return sum + dataKeys.reduce((keySum, key) => keySum + Number(item[key] || 0), 0); - }, 0); - }; + // const calculateTotal = ( + // data: T, + // categoryKey: keyof T[number], + // ): number => { + // const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + // return data.reduce((sum, item) => { + // return sum + dataKeys.reduce((keySum, key) => keySum + Number(item[key] || 0), 0); + // }, 0); + // }; return ( //
diff --git a/js/packages/react-ui/src/components/Charts/Charts.tsx b/js/packages/react-ui/src/components/Charts/Charts.tsx index c07894aa1..cdcdd03b1 100644 --- a/js/packages/react-ui/src/components/Charts/Charts.tsx +++ b/js/packages/react-ui/src/components/Charts/Charts.tsx @@ -24,10 +24,17 @@ export type ChartConfig = { icon?: React.ComponentType; } & ( | { color?: string; secondaryColor?: string; theme?: never } - | { color?: never; theme: Record } + | { + color?: never; + theme: Record< + keyof typeof THEMES, + | string + | { + color: string; + secondaryColor?: string; + } + >; + } ); }; @@ -79,17 +86,21 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { .map(([key, itemConfig]) => { const transformedKey = keyTransform(key); const themeValue = itemConfig.theme?.[theme as keyof typeof itemConfig.theme]; - const color = typeof themeValue === 'string' - ? themeValue - : themeValue?.color || itemConfig.color; - const secondaryColor = typeof themeValue === 'object' - ? themeValue?.secondaryColor - : ('secondaryColor' in itemConfig ? itemConfig.secondaryColor : undefined); - + const color = + typeof themeValue === "string" ? themeValue : themeValue?.color || itemConfig.color; + const secondaryColor = + typeof themeValue === "object" + ? themeValue?.secondaryColor + : "secondaryColor" in itemConfig + ? itemConfig.secondaryColor + : undefined; + return [ color ? ` --color-${transformedKey}: ${color};` : null, - secondaryColor ? ` --color-${transformedKey}-secondary: ${secondaryColor};` : null - ].filter(Boolean).join("\n"); + secondaryColor ? ` --color-${transformedKey}-secondary: ${secondaryColor};` : null, + ] + .filter(Boolean) + .join("\n"); }) .filter(Boolean) .join("\n")} diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx index 1eb199f12..5684e3daf 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx @@ -93,7 +93,7 @@ export const TwoLevelPieChart = ({ onMouseLeave={handleMouseLeave} > {transformedData.map((entry, index) => { - const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); + // const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); const hoverStyles = getHoverStyles(index, activeIndex); return ; })} diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx index 684f02f0a..0db811905 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx @@ -23,7 +23,7 @@ export interface MiniRadialChartProps { theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; variant?: "semicircle" | "circular"; format?: "percentage" | "number"; - legend?: boolean; + // legend?: boolean; label?: boolean; grid?: boolean; isAnimationActive?: boolean; @@ -36,7 +36,7 @@ export const MiniRadialChart = ({ theme = "ocean", variant = "semicircle", format = "number", - legend = true, + // legend = true, label = true, grid = true, isAnimationActive = true, From cd75a623efe4e882181dc18ecbdbdd4854dbe0db Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sun, 1 Jun 2025 00:08:58 +0530 Subject: [PATCH 030/190] feat(BarChartUtils): Increase element spacing and handle single data points The changes in this commit include: 1. Increasing the `ELEMENT_SPACING` constant from 15 to 17 to provide more spacing between bars in grouped or stacked bar charts. 2. Adding a new condition to the `getWidthOfData` function to ensure that the minimum width for a single data point is at least 200 pixels. This helps to ensure that single data points are not rendered too narrow. 3. Adding a new function `getScrollAmount` that calculates the appropriate scroll amount for the bar chart based on the data and the chart variant (grouped or stacked). This will be used to set the width of the chart container and ensure that the chart is scrollable when necessary. feat(BarChartV2): Improve positioning of chart labels The changes in this commit include: 1. Adjusting the positioning of the chart labels (left and right) to be vertically centered with the chart. 2. Adjusting the right label to be positioned at the right edge of the chart container, rather than the right edge of the chart itself. fix(LineInBarShape): Only render vertical line if bar has sufficient height The changes in this commit include: 1. Modifying the `LineInBarShape` component to only render the vertical line inside the bar if the bar has a height of at least 5 pixels. This ensures that the line is not rendered for very short bars. --- .../BarCharts/BarChartV2/BarChartV2.tsx | 170 +++++-- .../Charts/BarCharts/BarChartV2/barChar.scss | 6 +- .../BarChartV2/components/LineInBarShape.tsx | 2 +- .../BarChartV2/stories/barChartV2.stories.tsx | 445 +++++++++++------- .../Charts/BarCharts/utils/BarChartUtils.ts | 40 +- 5 files changed, 446 insertions(+), 217 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 6815d8eeb..6155aa696 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -1,6 +1,6 @@ import clsx from "clsx"; import { ChevronFirst, ChevronLast } from "lucide-react"; -import React, { useEffect, useId, useRef, useState } from "react"; +import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; import { Bar, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; import { IconButton } from "../../../IconButton"; import { @@ -16,6 +16,7 @@ import { BAR_WIDTH, getPadding, getRadiusArray, + getScrollAmount, getWidthOfData, getXAxisTickFormatter, getYAxisTickFormatter, @@ -49,7 +50,7 @@ export interface BarChartPropsV2 { className?: string; } -export const BarChartV2 = ({ +const BarChartV2Component = ({ data, categoryKey, theme = "ocean", @@ -67,28 +68,78 @@ export const BarChartV2 = ({ legend = false, className, }: BarChartPropsV2) => { - // excluding the categoryKey - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + const dataKeys = useMemo(() => { + return Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + }, [data, categoryKey]); - const palette = getPalette(theme); - const colors = getDistributedColors(palette, dataKeys.length); + const colors = useMemo(() => { + const palette = getPalette(theme); + return getDistributedColors(palette, dataKeys.length); + }, [theme, dataKeys.length]); - // Create Config - const chartConfig: ChartConfig = dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - icon: icons[key], - color: colors[index], - secondaryColor: colors[dataKeys.length - index - 1], - }, - }), - {}, - ); + const chartConfig: ChartConfig = useMemo(() => { + return dataKeys.reduce( + (config, key, index) => ({ + ...config, + [key]: { + label: key, + icon: icons[key], + color: colors[index], + secondaryColor: colors[dataKeys.length - index - 1], + }, + }), + {}, + ); + }, [dataKeys, icons, colors]); const chartContainerRef = useRef(null); + const mainContainerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(0); + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(false); + + const padding = useMemo(() => { + return getPadding(data, categoryKey as string, containerWidth, variant); + }, [data, categoryKey, containerWidth, variant]); + + const dataWidth = useMemo(() => { + return getWidthOfData(data, categoryKey as string, variant); + }, [data, categoryKey, variant]); + + const scrollAmount = useMemo(() => { + return getScrollAmount(data, categoryKey as string, variant); + }, [data, categoryKey, variant]); + + const chartHeight = useMemo(() => { + return containerWidth ? containerWidth * (9 / 16) : 400; + }, [containerWidth]); + + // Check scroll boundaries + const updateScrollState = useCallback(() => { + if (mainContainerRef.current) { + const { scrollLeft, scrollWidth, clientWidth } = mainContainerRef.current; + setCanScrollLeft(scrollLeft > 0); + setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 1); // -1 for floating point precision + } + }, []); + + const scrollLeft = useCallback(() => { + if (mainContainerRef.current) { + mainContainerRef.current.scrollTo({ + left: mainContainerRef.current.scrollLeft - scrollAmount, + behavior: "smooth", + }); + } + }, [scrollAmount]); + + const scrollRight = useCallback(() => { + if (mainContainerRef.current) { + mainContainerRef.current.scrollTo({ + left: mainContainerRef.current.scrollLeft + scrollAmount, + behavior: "smooth", + }); + } + }, [scrollAmount]); useEffect(() => { if (!chartContainerRef.current) { @@ -108,23 +159,39 @@ export const BarChartV2 = ({ }; }, []); - const padding = getPadding(data, categoryKey as string, containerWidth, variant); - const dataWidth = getWidthOfData(data, categoryKey as string, variant); + // Update scroll state when container width or data width changes + useEffect(() => { + updateScrollState(); + }, [containerWidth, dataWidth, updateScrollState]); + + // Add scroll event listener to update button states + useEffect(() => { + const mainContainer = mainContainerRef.current; + if (!mainContainer) return; - // Create legend items for custom legend - const legendItems: LegendItem[] = dataKeys.map((key, index) => ({ - key, - label: key, - color: colors[index] || "#000000", // Fallback color if undefined - icon: icons[key] as React.ComponentType | undefined, - })); + const handleScroll = () => { + updateScrollState(); + }; - // Calculate chart height based on aspect ratio - const chartHeight = containerWidth ? containerWidth * (9 / 16) : 400; + mainContainer.addEventListener("scroll", handleScroll); + return () => { + mainContainer.removeEventListener("scroll", handleScroll); + }; + }, [updateScrollState]); + + // Memoize legend items creation + const legendItems: LegendItem[] = useMemo(() => { + return dataKeys.map((key, index) => ({ + key, + label: key, + color: colors[index] || "#000000", // Fallback color if undefined + icon: icons[key] as React.ComponentType | undefined, + })); + }, [dataKeys, colors, icons]); const id = useId(); - const chartSyncID = `bar-chart-sync-${id}`; + const chartSyncID = useMemo(() => `bar-chart-sync-${id}`, [id]); return (
@@ -167,7 +234,7 @@ export const BarChartV2 = ({
)} -
+
({ bottom: 0, }} onClick={onBarsClick} - // barGap={2} - // barCategoryGap={'20%'} + barGap={5} + barCategoryGap={"20%"} syncId={chartSyncID} > {grid && cartesianGrid()} @@ -225,6 +292,7 @@ export const BarChartV2 = ({ stackId={variant === "stacked" ? "a" : undefined} isAnimationActive={isAnimationActive} maxBarSize={BAR_WIDTH} + barSize={BAR_WIDTH} shape={ ({
-
- } - /> - } - /> -
+ {/* Scroll buttons */} + {dataWidth > containerWidth && ( +
+ } + variant="secondary" + onClick={scrollLeft} + size="extra-small" + disabled={!canScrollLeft} + /> + } + variant="secondary" + size="extra-small" + onClick={scrollRight} + disabled={!canScrollRight} + /> +
+ )} {legend && ( )}
); }; + +// Add React.memo for performance optimization +export const BarChartV2 = React.memo(BarChartV2Component) as typeof BarChartV2Component; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss index c57d145c9..80703e6b5 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss @@ -22,10 +22,14 @@ position: absolute; &--left { + top: -15px; + transform: translateY(-50%); left: 20px; } &--right { - right: 0; + top: -15px; + transform: translateY(-50%); + right: 0px; } } diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx index 0b307838b..d9439403e 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx @@ -64,7 +64,7 @@ const LineInBarShape: FunctionComponent = (props) => { {/* The internal vertical line */} {width > 0 && - height > 0 && ( // Only render line if bar has dimensions + height > 5 && ( // Only render line if bar has sufficient height ; export const BarChartV2Story: Story = { - name: "Bar Chart V2", + name: "🎛️ Data Switcher - Bar Chart V2", args: { data: barChartData, categoryKey: "month", @@ -172,198 +255,224 @@ export const BarChartV2Story: Story = { yAxisLabel: "Number of Users", legend: true, }, - render: (args: BarChartPropsV2) => ( - - - - ), + render: (args: any) => { + const [selectedDataType, setSelectedDataType] = + useState("default"); + + const currentData = dataVariations[selectedDataType]; + const currentCategoryKey = categoryKeys[selectedDataType]; + + const buttonStyle = { + margin: "2px", + padding: "6px 12px", + fontSize: "12px", + border: "1px solid #ddd", + borderRadius: "4px", + cursor: "pointer", + background: "#fff", + }; + + const activeButtonStyle = { + ...buttonStyle, + background: "#007acc", + color: "white", + border: "1px solid #007acc", + }; + + return ( +
+
+ 💡 Quick Data Switch: +
+ + + + + + + +
+
+ Current: {selectedDataType} | Items:{" "} + {currentData.length} | Category: {currentCategoryKey} +
+
+ + + +
+ ); + }, parameters: { docs: { - source: { - code: ` -const barChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - - - - -`, + description: { + story: + "Use the buttons above the chart to quickly switch between different data variations and test the scrolling functionality. Active button is highlighted in blue.", }, }, }, }; -export const BarChartV2WithIcons: Story = { - name: "Bar Chart V2 with Icons", +export const SmallDataStory: Story = { + name: "📱 Small Data (No Scroll)", args: { - ...BarChartV2Story.args, - icons: icons, + data: dataVariations.small as any, + categoryKey: "month" as any, + theme: "ocean", + variant: "grouped", + radius: 2, + grid: true, + isAnimationActive: true, + showYAxis: true, + legend: true, }, - render: (args: BarChartPropsV2) => ( - + render: (args) => ( + ), - parameters: { - docs: { - source: { - code: ` -import { Monitor, TabletSmartphone } from "lucide-react"; - -const barChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - -const icons = { - desktop: Monitor, - mobile: TabletSmartphone, -}; - - - - -`, - }, - }, - }, }; -export const BarChartV2WithYAxis: Story = { - name: "Bar Chart V2 with Y-Axis and X-Axis Labels", +export const LargeDataStory: Story = { + name: "🌐 Large Data (Scrolling)", args: { - ...BarChartV2Story.args, + data: dataVariations.large as any, + categoryKey: "month" as any, + theme: "emerald", + variant: "grouped", + radius: 2, + grid: true, + isAnimationActive: true, showYAxis: true, + legend: true, }, - render: (args: BarChartPropsV2) => ( - + render: (args) => ( + ), - parameters: { - docs: { - source: { - code: ` -const barChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; +}; - - - -`, - }, - }, +export const WeeklyDataStory: Story = { + name: "📈 Weekly Data (Many Categories)", + args: { + data: dataVariations.weekly as any, + categoryKey: "week" as any, + theme: "sunset", + variant: "grouped", + radius: 2, + grid: true, + isAnimationActive: true, + showYAxis: true, + legend: true, }, + render: (args) => ( + + + + ), }; -export const BarChartV2Stacked: Story = { - name: "Stacked Bar Chart V2", +export const BigNumbersStory: Story = { + name: "💰 Big Numbers (Billions/Trillions)", args: { - ...BarChartV2Story.args, - variant: "stacked", - theme: "emerald", + data: dataVariations.bigNumbers as any, + categoryKey: "company" as any, + theme: "vivid", + variant: "grouped", + radius: 2, + grid: true, + isAnimationActive: true, + showYAxis: true, + legend: true, }, render: (args) => ( ), - parameters: { - docs: { - source: { - code: ` -const barChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; +}; - - - -`, - }, - }, +export const EdgeCaseStory: Story = { + name: "🎯 Edge Case (Single Data Point)", + args: { + data: dataVariations.edge as any, + categoryKey: "period" as any, + theme: "orchid", + variant: "grouped", + radius: 2, + grid: true, + isAnimationActive: true, + showYAxis: true, + legend: true, }, + render: (args) => ( + + + + ), }; -export const BarChartV2Experimental: Story = { - name: "Bar Chart V2 Experimental", +export const NumberRangesStory: Story = { + name: "🔢 Number Ranges (Scale Testing)", args: { - ...BarChartV2Story.args, - theme: "emerald", + data: dataVariations.numbers as any, + categoryKey: "category" as any, + theme: "spectrum", variant: "grouped", + radius: 2, + grid: true, + isAnimationActive: true, + showYAxis: true, + legend: true, }, render: (args) => ( - - console.log(data)} /> + + ), }; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts index 088098665..4886743b1 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts @@ -1,7 +1,7 @@ import { Variant } from "../BarChartV2"; export const BAR_WIDTH = 12; -export const ELEMENT_SPACING = 15; // Spacing per bar in grouped, or per stack in stacked +export const ELEMENT_SPACING = 17; // Spacing per bar in grouped, or per stack in stacked const getWidthOfData = ( data: Array>, @@ -20,7 +20,11 @@ const getWidthOfData = ( numberOfElements = seriesCountsPerCategory.reduce((acc, curr) => acc + curr, 0); } - const width = numberOfElements * (BAR_WIDTH + ELEMENT_SPACING); + let width = numberOfElements * (BAR_WIDTH + ELEMENT_SPACING); + if (data.length === 1) { + const minSingleDataWidth = 200; // Minimum width for single data points + width = Math.max(width, minSingleDataWidth); + } return width; }; @@ -108,4 +112,34 @@ const getXAxisTickFormatter = () => { }; }; -export { getPadding, getRadiusArray, getWidthOfData, getXAxisTickFormatter, getYAxisTickFormatter }; +const getScrollAmount = ( + data: Array>, + categoryKey: string, + variant: Variant, +) => { + if (data.length === 0) return 200; // Fallback + + // Get the number of data keys (excluding categoryKey) + const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + + if (variant === "stacked") { + // For stacked: each category is one stack + // Example: month "January" = 1 stack = BAR_WIDTH + ELEMENT_SPACING + return BAR_WIDTH + ELEMENT_SPACING; + } else { + // For grouped: each category contains multiple bars + // Example: month "January" with desktop+mobile+tablet = 3 bars + // Width = 3 * (BAR_WIDTH + ELEMENT_SPACING) + const seriesPerCategory = dataKeys.length; + return seriesPerCategory * (BAR_WIDTH + ELEMENT_SPACING); + } +}; + +export { + getPadding, + getRadiusArray, + getScrollAmount, + getWidthOfData, + getXAxisTickFormatter, + getYAxisTickFormatter, +}; From bf2e628869395c680b54d4fdb7a7eea9ef6b0c79 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sun, 1 Jun 2025 22:40:32 +0530 Subject: [PATCH 031/190] feat(BarChartV2): Improve chart scrolling and snap behavior This commit introduces several improvements to the BarChartV2 component: 1. Calculates the effective container width by subtracting the Y-axis width when it is shown. 2. Introduces snap positions for the chart to ensure proper group alignment when scrolling. 3. Implements smooth scrolling to the nearest snap position when the user scrolls left or right. 4. Calculates the chart height based on the container width to maintain the 16:9 aspect ratio. 5. Moves the LegendItem type definition to the BarChartUtils module. These changes enhance the overall user experience and visual consistency of the BarChartV2 component. --- .../BarCharts/BarChartV2/BarChartV2.tsx | 83 ++++---- .../BarChartV2/components/DefaultLegend.tsx | 10 +- .../Charts/BarCharts/utils/BarChartUtils.ts | 181 +++++++++++++++++- 3 files changed, 223 insertions(+), 51 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 6155aa696..9ab1ca615 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -14,15 +14,21 @@ import { cartesianGrid } from "../../cartesianGrid"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; import { BAR_WIDTH, + findNearestSnapPosition, + getChartConfig, + getChartHeight, + getDataKeys, + getLegendItems, getPadding, getRadiusArray, - getScrollAmount, + getSnapPositions, getWidthOfData, getXAxisTickFormatter, getYAxisTickFormatter, + type LegendItem, } from "../utils/BarChartUtils"; import { SimpleCursor } from "./components/CustomCursor"; -import { DefaultLegend, LegendItem } from "./components/DefaultLegend"; +import { DefaultLegend } from "./components/DefaultLegend"; import { LineInBarShape } from "./components/LineInBarShape"; import { XAxisTick } from "./components/XAxisTick"; import { YAxisTick } from "./components/YAxisTick"; @@ -50,6 +56,10 @@ export interface BarChartPropsV2 { className?: string; } +const Y_AXIS_WIDTH = 40; // Width of Y-axis chart when shown +const BAR_GAP = 10; // Gap between bars +const BAR_CATEGORY_GAP = "20%"; // Gap between categories + const BarChartV2Component = ({ data, categoryKey, @@ -69,7 +79,7 @@ const BarChartV2Component = ({ className, }: BarChartPropsV2) => { const dataKeys = useMemo(() => { - return Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + return getDataKeys(data, categoryKey as string); }, [data, categoryKey]); const colors = useMemo(() => { @@ -78,18 +88,7 @@ const BarChartV2Component = ({ }, [theme, dataKeys.length]); const chartConfig: ChartConfig = useMemo(() => { - return dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - icon: icons[key], - color: colors[index], - secondaryColor: colors[dataKeys.length - index - 1], - }, - }), - {}, - ); + return getChartConfig(dataKeys, icons, colors); }, [dataKeys, icons, colors]); const chartContainerRef = useRef(null); @@ -98,20 +97,27 @@ const BarChartV2Component = ({ const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(false); + // need this to calculate the padding for the chart container, because the y-axis is rendered in a separate chart + const effectiveContainerWidth = useMemo(() => { + const yAxisWidth = showYAxis ? Y_AXIS_WIDTH : 0; + return Math.max(0, containerWidth - yAxisWidth); + }, [containerWidth, showYAxis]); + const padding = useMemo(() => { - return getPadding(data, categoryKey as string, containerWidth, variant); - }, [data, categoryKey, containerWidth, variant]); + return getPadding(data, categoryKey as string, effectiveContainerWidth, variant); + }, [data, categoryKey, effectiveContainerWidth, variant]); const dataWidth = useMemo(() => { return getWidthOfData(data, categoryKey as string, variant); }, [data, categoryKey, variant]); - const scrollAmount = useMemo(() => { - return getScrollAmount(data, categoryKey as string, variant); + // Calculate snap positions for proper group alignment + const snapPositions = useMemo(() => { + return getSnapPositions(data, categoryKey as string, variant); }, [data, categoryKey, variant]); const chartHeight = useMemo(() => { - return containerWidth ? containerWidth * (9 / 16) : 400; + return getChartHeight(containerWidth); }, [containerWidth]); // Check scroll boundaries @@ -125,21 +131,29 @@ const BarChartV2Component = ({ const scrollLeft = useCallback(() => { if (mainContainerRef.current) { + const currentScroll = mainContainerRef.current.scrollLeft; + const targetIndex = findNearestSnapPosition(snapPositions, currentScroll, "left"); + const targetPosition = snapPositions[targetIndex] ?? 0; + mainContainerRef.current.scrollTo({ - left: mainContainerRef.current.scrollLeft - scrollAmount, + left: targetPosition, behavior: "smooth", }); } - }, [scrollAmount]); + }, [snapPositions]); const scrollRight = useCallback(() => { if (mainContainerRef.current) { + const currentScroll = mainContainerRef.current.scrollLeft; + const targetIndex = findNearestSnapPosition(snapPositions, currentScroll, "right"); + const targetPosition = snapPositions[targetIndex] ?? 0; + mainContainerRef.current.scrollTo({ - left: mainContainerRef.current.scrollLeft + scrollAmount, + left: targetPosition, behavior: "smooth", }); } - }, [scrollAmount]); + }, [snapPositions]); useEffect(() => { if (!chartContainerRef.current) { @@ -181,12 +195,7 @@ const BarChartV2Component = ({ // Memoize legend items creation const legendItems: LegendItem[] = useMemo(() => { - return dataKeys.map((key, index) => ({ - key, - label: key, - color: colors[index] || "#000000", // Fallback color if undefined - icon: icons[key] as React.ComponentType | undefined, - })); + return getLegendItems(dataKeys, colors, icons); }, [dataKeys, colors, icons]); const id = useId(); @@ -194,14 +203,14 @@ const BarChartV2Component = ({ const chartSyncID = useMemo(() => `bar-chart-sync-${id}`, [id]); return ( -
-
+
+
{showYAxis && (
{/* Y-axis only chart - synchronized with main chart */} ({ syncId={chartSyncID} > ({ bottom: 0, }} onClick={onBarsClick} - barGap={5} - barCategoryGap={"20%"} + barGap={BAR_GAP} + barCategoryGap={BAR_CATEGORY_GAP} syncId={chartSyncID} > {grid && cartesianGrid()} @@ -263,7 +272,7 @@ const BarChartV2Component = ({ axisLine={false} textAnchor="middle" tickFormatter={getXAxisTickFormatter()} - interval="preserveStartEnd" + interval={0} tick={} orientation="bottom" // gives the padding on the 2 sides see the function for reference diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/DefaultLegend.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/DefaultLegend.tsx index 4a29ede7b..e1f121cc2 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/DefaultLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/DefaultLegend.tsx @@ -1,12 +1,6 @@ import clsx from "clsx"; import React from "react"; - -interface LegendItem { - key: string; - label: string; - color: string; - icon?: React.ComponentType; -} +import { type LegendItem } from "../../utils/BarChartUtils"; interface DefaultLegendProps { items: LegendItem[]; @@ -58,4 +52,4 @@ const DefaultLegend: React.FC = ({ }; export { DefaultLegend }; -export type { DefaultLegendProps, LegendItem }; +export type { DefaultLegendProps }; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts index 4886743b1..9f03ba838 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts @@ -1,14 +1,39 @@ +import { ChartConfig } from "../../Charts"; import { Variant } from "../BarChartV2"; export const BAR_WIDTH = 12; -export const ELEMENT_SPACING = 17; // Spacing per bar in grouped, or per stack in stacked +export const ELEMENT_SPACING_GROUPED = 16; // Spacing per bar in grouped charts +export const ELEMENT_SPACING_STACKED = 26; // Spacing per stack in stacked charts +/** + * Get the appropriate element spacing based on chart variant + * @param variant - The chart variant + * @returns The spacing value for the given variant + */ +const getElementSpacing = (variant: Variant): number => { + switch (variant) { + case "stacked": + return ELEMENT_SPACING_STACKED; + case "grouped": + default: + return ELEMENT_SPACING_GROUPED; + } +}; + +/** + * This function returns the width of the data in the chart, used for padding calculation, scroll amount calculation, and + * for the width of the chart container. + * @param data - The data to be displayed in the chart. + * @param categoryKey - The key of the category to be displayed in the chart. + * @param variant - The variant of the chart. + */ const getWidthOfData = ( data: Array>, categoryKey: string, variant: Variant, ) => { let numberOfElements: number; + const elementSpacing = getElementSpacing(variant); if (variant === "stacked") { numberOfElements = data.length; // Number of stacks = number of categories @@ -20,7 +45,7 @@ const getWidthOfData = ( numberOfElements = seriesCountsPerCategory.reduce((acc, curr) => acc + curr, 0); } - let width = numberOfElements * (BAR_WIDTH + ELEMENT_SPACING); + let width = numberOfElements * (BAR_WIDTH + elementSpacing); if (data.length === 1) { const minSingleDataWidth = 200; // Minimum width for single data points width = Math.max(width, minSingleDataWidth); @@ -28,6 +53,13 @@ const getWidthOfData = ( return width; }; +/** + * This function returns the padding for the chart, used for the padding of the chart container. + * @param data - The data to be displayed in the chart. + * @param categoryKey - The key of the category to be displayed in the chart. + * @param containerWidth - The width of the container of the chart. + * @param variant - The variant of the chart. + */ const getPadding = ( data: Array>, categoryKey: string, @@ -51,6 +83,13 @@ const getPadding = ( } }; +/** + * This function returns the radius for the chart, used for the radius of the LineInBarShape.tsx. + * @param variant - The variant of the chart. + * @param radius - The radius of the chart. + * @param isFirst - Whether the first item in the stack. + * @param isLast - Whether the last item in the stack. + */ const getRadiusArray = ( variant: Variant, radius: number, @@ -79,6 +118,10 @@ const getRadiusArray = ( return [radius, radius, radius, radius]; }; +/** + * This function returns the formatter for the Y-axis tick values. + * @returns The formatter for the Y-axis tick values. + */ const getYAxisTickFormatter = () => { return (value: any) => { // Format the Y-axis tick values with abbreviations @@ -101,6 +144,7 @@ const getYAxisTickFormatter = () => { }; }; +// return the max length of the x-axis tick values const getXAxisTickFormatter = () => { const maxLength = 3; @@ -112,6 +156,13 @@ const getXAxisTickFormatter = () => { }; }; +/** + * This function returns the scroll amount for the chart, used for the scroll amount of the chart. + * @param data - The data to be displayed in the chart. + * @param categoryKey - The key of the category to be displayed in the chart. + * @param variant - The variant of the chart. + */ + const getScrollAmount = ( data: Array>, categoryKey: string, @@ -121,24 +172,142 @@ const getScrollAmount = ( // Get the number of data keys (excluding categoryKey) const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + const elementSpacing = getElementSpacing(variant); if (variant === "stacked") { // For stacked: each category is one stack - // Example: month "January" = 1 stack = BAR_WIDTH + ELEMENT_SPACING - return BAR_WIDTH + ELEMENT_SPACING; + // Example: month "January" = 1 stack = BAR_WIDTH + ELEMENT_SPACING_STACKED (26) + return BAR_WIDTH + elementSpacing; } else { // For grouped: each category contains multiple bars // Example: month "January" with desktop+mobile+tablet = 3 bars - // Width = 3 * (BAR_WIDTH + ELEMENT_SPACING) + // Width = 3 * (BAR_WIDTH + ELEMENT_SPACING_GROUPED (17)) const seriesPerCategory = dataKeys.length; - return seriesPerCategory * (BAR_WIDTH + ELEMENT_SPACING); + return seriesPerCategory * (BAR_WIDTH + elementSpacing); + } +}; + +/** + * 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. + * @param categoryKey - The key of the category to be displayed in the chart. + * @returns The data keys for the chart. + */ +const getDataKeys = ( + data: Array>, + categoryKey: string, +): string[] => { + return Object.keys(data[0] || {}).filter((key) => key !== categoryKey); +}; + +/** + * This function returns the snap positions for the chart, used for the snap positions of the chart. + * @param data - The data to be displayed in the chart. + * @param categoryKey - The key of the category to be displayed in the chart. + * @param variant - The variant of the chart. + * @returns The snap positions for the chart. + */ +const getSnapPositions = ( + data: Array>, + categoryKey: string, + variant: Variant, +): number[] => { + if (data.length === 0) return [0]; + + const positions = [0]; // Start position + const scrollAmountValue = getScrollAmount(data, categoryKey, variant); + + // Calculate all valid snap positions based on groups + for (let i = 1; i < data.length; i++) { + positions.push(i * scrollAmountValue); } + + return positions; +}; + +// Find the nearest snap position based on current scroll and direction +const findNearestSnapPosition = ( + snapPositions: number[], + currentScroll: number, + direction: "left" | "right", +): number => { + // Find current position index + let currentIndex = 0; + for (let i = 0; i < snapPositions.length; i++) { + const snapPosition = snapPositions[i]; + if (snapPosition !== undefined && currentScroll >= snapPosition) { + currentIndex = i; + } else { + break; + } + } + + if (direction === "left") { + // Go to previous snap position + return Math.max(0, currentIndex - 1); + } else { + // Go to next snap position + return Math.min(snapPositions.length - 1, currentIndex + 1); + } +}; + +// Create chart configuration object +const getChartConfig = ( + dataKeys: string[], + icons: Partial>, + colors: string[], +): ChartConfig => { + return dataKeys.reduce( + (config, key, index) => ({ + ...config, + [key]: { + label: key, + icon: icons[key], + color: colors[index], + secondaryColor: colors[dataKeys.length - index - 1], + }, + }), + {}, + ); +}; + +// Create legend items array +export interface LegendItem { + key: string; + label: string; + color: string; + icon?: React.ComponentType; +} + +const getLegendItems = ( + dataKeys: string[], + colors: string[], + icons: Partial>, +): LegendItem[] => { + return dataKeys.map((key, index) => ({ + key, + label: key, + color: colors[index] || "#000000", // Fallback color if undefined + icon: icons[key] as React.ComponentType | undefined, + })); +}; + +// Calculate chart height based on container width +const getChartHeight = (containerWidth: number): number => { + return containerWidth ? containerWidth * (9 / 16) : 400; }; export { + findNearestSnapPosition, + getChartConfig, + getChartHeight, + getDataKeys, + getElementSpacing, + getLegendItems, getPadding, getRadiusArray, getScrollAmount, + getSnapPositions, getWidthOfData, getXAxisTickFormatter, getYAxisTickFormatter, From 8a7e1bd48bda5fc280e4532477628d1bccc9ec2c Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 2 Jun 2025 04:24:46 +0530 Subject: [PATCH 032/190] feat(BarChartV2): Implement optimized X-axis tick formatter The changes in this commit focus on improving the X-axis tick formatting in the BarChartV2 component. The key changes are: 1. Introduce a new helper function `getOptimalXAxisTickFormatter` that calculates the available width per group and returns an appropriate tick formatter function. 2. The new `getOptimalXAxisTickFormatter` function is used to get the X-axis tick formatter, which is then passed to the `XAxis` component. 3. The `getXAxisTickFormatter` function has been updated to handle intelligent truncation and ellipsis display based on the available space. 4. The internal constants `ELEMENT_SPACING_GROUPED` and `ELEMENT_SPACING_STACKED` have been moved to the top of the file and marked as internal. These changes ensure that the X-axis tick labels are displayed in a more readable and space-efficient manner, improving the overall user experience of the BarChartV2 component. --- .../BarCharts/BarChartV2/BarChartV2.tsx | 9 +- .../Charts/BarCharts/utils/BarChartUtils.ts | 121 +++++++++++++++--- 2 files changed, 108 insertions(+), 22 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 9ab1ca615..85ad836c0 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -19,11 +19,11 @@ import { getChartHeight, getDataKeys, getLegendItems, + getOptimalXAxisTickFormatter, getPadding, getRadiusArray, getSnapPositions, getWidthOfData, - getXAxisTickFormatter, getYAxisTickFormatter, type LegendItem, } from "../utils/BarChartUtils"; @@ -202,6 +202,11 @@ const BarChartV2Component = ({ const chartSyncID = useMemo(() => `bar-chart-sync-${id}`, [id]); + // Get the optimal X-axis tick formatter based on available space + const xAxisTickFormatter = useMemo(() => { + return getOptimalXAxisTickFormatter(data, categoryKey as string, variant); + }, [data, categoryKey, variant]); + return (
@@ -271,7 +276,7 @@ const BarChartV2Component = ({ tickLine={false} axisLine={false} textAnchor="middle" - tickFormatter={getXAxisTickFormatter()} + tickFormatter={xAxisTickFormatter} interval={0} tick={} orientation="bottom" diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts index 9f03ba838..9b2af161c 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts @@ -2,10 +2,13 @@ import { ChartConfig } from "../../Charts"; import { Variant } from "../BarChartV2"; export const BAR_WIDTH = 12; -export const ELEMENT_SPACING_GROUPED = 16; // Spacing per bar in grouped charts -export const ELEMENT_SPACING_STACKED = 26; // Spacing per stack in stacked charts + +// Internal constants - not exported as they're only used within this file +const ELEMENT_SPACING_GROUPED = 16; // Spacing per bar in grouped charts +const ELEMENT_SPACING_STACKED = 26; // Spacing per stack in stacked charts /** + * INTERNAL HELPER FUNCTION * Get the appropriate element spacing based on chart variant * @param variant - The chart variant * @returns The spacing value for the given variant @@ -121,6 +124,7 @@ const getRadiusArray = ( /** * This function returns the formatter for the Y-axis tick values. * @returns The formatter for the Y-axis tick values. + * internally used by the YAxis component reCharts */ const getYAxisTickFormatter = () => { return (value: any) => { @@ -144,26 +148,80 @@ const getYAxisTickFormatter = () => { }; }; -// return the max length of the x-axis tick values -const getXAxisTickFormatter = () => { - const maxLength = 3; - +/** + * INTERNAL HELPER FUNCTION + * This function returns the formatter for the X-axis tick values with intelligent truncation. + * @param groupWidth - The width available for each group/category (optional) + * @param variant - The chart variant (affects truncation logic) + * @returns The formatter for the X-axis tick values. + * internally used by the XAxis component reCharts + */ +const getXAxisTickFormatter = (groupWidth?: number, variant: Variant = "grouped") => { + const CHAR_WIDTH = 7; // Average character width in pixels for most fonts + const ELLIPSIS_WIDTH = CHAR_WIDTH * 3; // "..." takes about 3 character widths + const PADDING = 8; // Safety padding for better visual spacing + // closure is happening here. return (value: string) => { - if (value.length > maxLength) { - return `${value.slice(0, maxLength)}`; + // If no groupWidth provided, fall back to simple logic + if (!groupWidth) { + if (variant === "stacked") { + return value.slice(0, 3); + } else { + return value.length > 3 ? `${value.slice(0, 3)}...` : value; + } + } + + const availableWidth = Math.max(0, groupWidth - PADDING); + const maxCharsWithoutEllipsis = Math.floor(availableWidth / CHAR_WIDTH); + const maxCharsWithEllipsis = Math.floor((availableWidth - ELLIPSIS_WIDTH) / CHAR_WIDTH); + + // For stacked variant: simple truncation, no ellipsis needed + if (variant === "stacked") { + const maxChars = Math.max(1, Math.min(3, maxCharsWithoutEllipsis)); + return value.slice(0, maxChars); + } + + // For grouped variant: intelligent ellipsis handling + if (value.length <= maxCharsWithoutEllipsis) { + // Full text fits comfortably + return value; + } else if (maxCharsWithEllipsis >= 3) { + // We can fit at least 3 characters + ellipsis + return `${value.slice(0, maxCharsWithEllipsis)}...`; + } else { + // Very limited space - just show 3 chars without ellipsis + // (ellipsis would take more space than it's worth) + return value.slice(0, Math.max(1, Math.min(3, maxCharsWithoutEllipsis))); } - return value; }; }; /** + * Helper function to get the optimal X-axis tick formatter with calculated group width + * @param data - The chart data + * @param categoryKey - The category key + * @param variant - The chart variant + * @returns The optimized formatter function + */ +const getOptimalXAxisTickFormatter = ( + data: Array>, + categoryKey: string, + variant: Variant, +) => { + // Calculate the available width per group + const groupWidth = getWidthOfGroup(data, categoryKey, variant); + return getXAxisTickFormatter(groupWidth, variant); +}; + +/** + * INTERNAL HELPER FUNCTION * This function returns the scroll amount for the chart, used for the scroll amount of the chart. + * This can also be used to calculate the width of each group/category. * @param data - The data to be displayed in the chart. * @param categoryKey - The key of the category to be displayed in the chart. * @param variant - The variant of the chart. */ - -const getScrollAmount = ( +const getWidthOfGroup = ( data: Array>, categoryKey: string, variant: Variant, @@ -215,17 +273,23 @@ const getSnapPositions = ( if (data.length === 0) return [0]; const positions = [0]; // Start position - const scrollAmountValue = getScrollAmount(data, categoryKey, variant); + const groupWidthValue = getWidthOfGroup(data, categoryKey, variant); // Calculate all valid snap positions based on groups for (let i = 1; i < data.length; i++) { - positions.push(i * scrollAmountValue); + positions.push(i * groupWidthValue); } return positions; }; -// Find the nearest snap position based on current scroll and direction +/** + * This function returns the nearest snap position for the chart, used for the nearest snap position of the chart. + * @param snapPositions - The snap positions for the chart. + * @param currentScroll - The current scroll of the chart. + * @param direction - The direction of the scroll. + * @returns The nearest snap position for the chart. + */ const findNearestSnapPosition = ( snapPositions: number[], currentScroll: number, @@ -251,7 +315,13 @@ const findNearestSnapPosition = ( } }; -// Create chart configuration object +/** + * This function returns the chart configuration object, used for the chart configuration object of the chart. + * @param dataKeys - The data keys for the chart. + * @param icons - The icons for the chart. + * @param colors - The colors for the chart. + * @returns The chart configuration object for the chart. + */ const getChartConfig = ( dataKeys: string[], icons: Partial>, @@ -271,7 +341,13 @@ const getChartConfig = ( ); }; -// Create legend items array +/** + * This function returns the legend items for the chart, used for the legend items of the chart. + * @param dataKeys - The data keys for the chart. + * @param colors - The colors for the chart. + * @param icons - The icons for the chart. + * @returns The legend items for the chart. + */ export interface LegendItem { key: string; label: string; @@ -292,23 +368,28 @@ const getLegendItems = ( })); }; -// Calculate chart height based on container width +/** + * This function returns the chart height for the chart, used for the chart height of the chart. + * @param containerWidth - The width of the container of the chart. + * @returns The chart height for the chart. + * 16:9 aspect ratio + * to change the aspect ratio, change the 9/16 to the desired aspect ratio + */ const getChartHeight = (containerWidth: number): number => { return containerWidth ? containerWidth * (9 / 16) : 400; }; +// Export only the functions and types that are used externally export { findNearestSnapPosition, getChartConfig, getChartHeight, getDataKeys, - getElementSpacing, getLegendItems, + getOptimalXAxisTickFormatter, getPadding, getRadiusArray, - getScrollAmount, getSnapPositions, getWidthOfData, - getXAxisTickFormatter, getYAxisTickFormatter, }; From 11fd92f63af26c27cc7e6f5f06d6f3cbc72e501a Mon Sep 17 00:00:00 2001 From: i-subham Date: Mon, 2 Jun 2025 11:30:39 +0530 Subject: [PATCH 033/190] feat(PieChartV2): Enhance pie chart functionality and styling This commit introduces several improvements to the PieChartV2 component: 1. Added new props: `cornerRadius`, `paddingAngle`, and event handlers (`onMouseEnter`, `onMouseLeave`, `onClick`) to enhance interactivity and customization. 2. Refactored label rendering logic to utilize custom label and active shape renderers, improving label visibility and interaction. 3. Created a new `PieChartRenderers` module to encapsulate rendering logic for custom labels and active shapes, promoting code reusability. 4. Updated Storybook stories to demonstrate new features, including variations for donut charts, semi-circular charts, and charts with corner radius. These changes aim to provide a more flexible and visually appealing pie chart experience for users. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 65 +++-- .../PieChartV2/stories/PieChartV2.stories.tsx | 121 ++++++--- .../components/PieChartRenderers.tsx | 201 ++++++++++++++ .../Charts/PieCharts/utils/PieChartUtils.ts | 256 ++++++++++++++++-- 4 files changed, 547 insertions(+), 96 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index a09849ce2..88445b971 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -11,10 +11,18 @@ import { ChartTooltipContent, } from "../../Charts"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { + renderActiveShape, + renderCustomLabel, + renderCustomLabelLine, +} from "../components/PieChartRenderers"; import { PieChartData, calculateChartDimensions, + createAnimationConfig, createChartConfig, + createEventHandlers, + createSectorStyle, getHoverStyles, layoutMap, transformDataWithPercentages, @@ -34,6 +42,11 @@ export interface PieChartV2Props { label?: boolean; isAnimationActive?: boolean; appearance?: "circular" | "semiCircular"; + cornerRadius?: number; + paddingAngle?: number; + onMouseEnter?: (data: any, index: number) => void; + onMouseLeave?: () => void; + onClick?: (data: any, index: number) => void; } export const PieChartV2 = ({ @@ -47,6 +60,11 @@ export const PieChartV2 = ({ label = true, isAnimationActive = true, appearance = "circular", + cornerRadius = 0, + paddingAngle = 0, + onMouseEnter, + onMouseLeave, + onClick, }: PieChartV2Props) => { const { layout } = useLayoutContext(); const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(120); @@ -73,36 +91,14 @@ export const PieChartV2 = ({ const transformedData = transformDataWithPercentages(data, dataKey); const chartConfig = createChartConfig(data, categoryKey, theme); + const animationConfig = createAnimationConfig({ isAnimationActive }); + const eventHandlers = createEventHandlers(onMouseEnter, onMouseLeave, onClick); + const sectorStyle = createSectorStyle(cornerRadius, paddingAngle); // Get color palette and distribute colors const palette = getPalette(theme); const colors = getDistributedColors(palette, data.length); - //Custom label renderer - // const renderCustomLabel = ({ payload, cx, cy, x, y, textAnchor, dominantBaseline }: any) => { - // if (payload.percentage <= 10) return null; - // const displayValue = format === "percentage" ? payload.percentage : payload[dataKey]; - // const formattedValue = - // String(displayValue).length > 7 ? `${String(displayValue).slice(0, 7)}...` : displayValue; - - // return ( - // - // - // {formattedValue} - // {format === "percentage" ? "%" : ""} - // - // - // ); - // }; - return ( ({ data={transformedData} dataKey={format === "percentage" ? "percentage" : String(dataKey)} nameKey={String(categoryKey)} - labelLine={false} + labelLine={label && activeIndex === null ? (renderCustomLabelLine as any) : false} outerRadius={calculatedOuterRadius} innerRadius={calculatedInnerRadius} - label={false} - isAnimationActive={isAnimationActive} + label={ + activeIndex === null + ? (props) => + renderCustomLabel( + { ...props, labelDistance: 1 }, + format === "percentage" ? "percentage" : String(dataKey), + format, + ) + : false + } + activeShape={renderActiveShape} + activeIndex={activeIndex ?? undefined} + {...animationConfig} + {...eventHandlers} + {...sectorStyle} startAngle={appearance === "semiCircular" ? 0 : 0} endAngle={appearance === "semiCircular" ? 180 : 360} onMouseEnter={handleMouseEnter} diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index 0577bb0ae..7a56fcc3b 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -19,12 +19,12 @@ const meta: Meta> = { layout: "centered", docs: { description: { - component: "```tsx\nimport { PieChart } from '@crayon-ui/react-ui/Charts/PieChart';\n```", + component: + "```tsx\nimport { PieChartV2 } from '@crayon-ui/react-ui/Charts/PieChartV2';\n```", }, }, }, tags: ["!dev", "autodocs"], - argTypes: { data: { description: @@ -121,14 +121,32 @@ const meta: Meta> = { category: "Display", }, }, + cornerRadius: { + description: "The radius of the corners of each pie slice", + control: { type: "number", min: 0, max: 20 }, + table: { + type: { summary: "number" }, + defaultValue: { summary: "0" }, + category: "Appearance", + }, + }, + paddingAngle: { + description: "The angle between each pie slice", + control: { type: "number", min: 0, max: 10 }, + table: { + type: { summary: "number" }, + defaultValue: { summary: "0" }, + category: "Appearance", + }, + }, }, } satisfies Meta; export default meta; type Story = StoryObj; -export const PieChartStory: Story = { - name: "Pie Chart", +export const Default: Story = { + name: "Default Pie Chart", args: { data: pieChartData, categoryKey: "month", @@ -136,48 +154,73 @@ export const PieChartStory: Story = { theme: "ocean", variant: "pie", format: "number", - legend: false, - label: false, + legend: true, + label: true, isAnimationActive: true, + appearance: "circular", + cornerRadius: 0, + paddingAngle: 0, }, render: (args) => ( ), - parameters: { - docs: { - source: { - code: ` - const pieChartData = [ - { month: "January", value: 400 }, - { month: "February", value: 300 }, - { month: "March", value: 300 }, - { month: "April", value: 400 }, - { month: "May", value: 300 }, - { month: "June", value: 300 }, - { month: "July", value: 300 }, -]; +}; - - - - `, - }, - }, +export const DonutChart: Story = { + name: "Donut Chart", + args: { + ...Default.args, + variant: "donut", + theme: "orchid", + }, + render: (args) => ( + + + + ), +}; + +export const SemiCircular: Story = { + name: "Semi-Circular Chart", + args: { + ...Default.args, + appearance: "semiCircular", + theme: "emerald", + }, + render: (args) => ( + + + + ), +}; + +export const WithCornerRadius: Story = { + name: "Chart with Corner Radius", + args: { + ...Default.args, + cornerRadius: 10, + paddingAngle: 2, + theme: "sunset", + }, + render: (args) => ( + + + + ), +}; + +export const PercentageFormat: Story = { + name: "Percentage Format", + args: { + ...Default.args, + format: "percentage", + theme: "spectrum", }, + render: (args) => ( + + + + ), }; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx new file mode 100644 index 000000000..efb94f72d --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx @@ -0,0 +1,201 @@ +import React from "react"; +import { Sector } from "recharts"; +import { CustomLabelProps, LabelLineProps } from "../utils/PieChartUtils"; + +/** + * Renders a custom label for pie chart segments + * @param props - The label properties including position, text anchor, and payload data + * @param dataKey - The key to use for displaying the value + * @param format - The format to display the value in ('percentage' or 'number') + * @returns A React element containing the label text, or null if percentage is too small + * + * @example + * // Render a percentage label + * renderCustomLabel(props, "value", "percentage") + * // Renders: "75.5%" + * + * @example + * // Render a number label + * renderCustomLabel(props, "value", "number") + * // Renders: "1234" + */ +export const renderCustomLabel = ( + props: CustomLabelProps & { labelDistance?: number }, + dataKey: string, + format: "percentage" | "number" = "number", +): React.ReactElement | null => { + const { payload, cx, cy, x, y, textAnchor, dominantBaseline, labelDistance } = props; + + if (payload.percentage <= 10) return null; + + let displayValue; + if (format === "percentage") { + displayValue = `${payload.percentage}%`; + } else { + displayValue = payload[dataKey]; + } + + // Move label closer to the center (reduce the radius) + const angle = Math.atan2(y - cy, x - cx); + const distance = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2); + const scale = labelDistance !== undefined ? labelDistance : 0.8; + const newDistance = distance * scale; + + // Correction for bottom label + const isBottom = Math.abs(angle - Math.PI / 2) < 0.3; // ~90 degrees + const yCorrection = isBottom ? 8 : 0; // Adjust this value as needed + const newX = cx + Math.cos(angle) * newDistance; + const newY = cy + Math.sin(angle) * newDistance + yCorrection; + + return ( + + + {displayValue} + + + ); +}; + +/** + * Renders a custom label line connecting pie segments to their labels + * @param props - The label line properties including points, value, and text positioning + * @returns A React element containing the line and percentage text + * + * @example + * // Render a label line with percentage + * renderCustomLabelLine({ + * points: [{x: 100, y: 100}, {x: 150, y: 150}], + * value: 75.5, + * textAnchor: "start", + * dominantBaseline: "middle" + * }) + * // Renders: A line with "75.5%" at the end + */ +export const renderCustomLabelLine = (props: LabelLineProps): React.ReactElement => { + const { points, value, textAnchor, dominantBaseline } = props; + if (!points || points.length < 2) { + return ; + } + const [start, end] = points; + return ( + + + {/* + {`${value}%`} + */} + + ); +}; + +/** + * Renders an active (hovered) shape for pie chart segments + * @param props - The active shape properties including position, dimensions, and data + * @returns A React element containing the enhanced active segment with additional visual elements + * + * Features: + * - Displays segment name in the center + * - Shows the main sector with the segment's fill color + * - Adds an outer ring for emphasis + * - Includes a connecting line to the percentage label + * - Shows a small circle at the end of the line + * - Displays the percentage value + * + * @example + * // Render an active shape for a hovered segment + * renderActiveShape({ + * cx: 100, + * cy: 100, + * innerRadius: 50, + * outerRadius: 80, + * startAngle: 0, + * endAngle: 90, + * fill: "#ff0000", + * payload: { name: "Segment A" }, + * percent: 0.25, + * midAngle: 45 + * }) + * // Renders: An enhanced segment with label and percentage + */ +export const renderActiveShape = (props: any): React.ReactElement => { + const { + cx, + cy, + innerRadius, + outerRadius, + startAngle, + endAngle, + fill, + payload, + percent, + midAngle, + } = props; + + const RADIAN = Math.PI / 180; + const sin = Math.sin(-RADIAN * midAngle); + const cos = Math.cos(-RADIAN * midAngle); + const labelLineLength = 16; + const labelOffset = 8; + const labelTextGap = 6; + const mx = cx + (outerRadius + labelLineLength) * cos; + const my = cy + (outerRadius + labelLineLength) * sin; + + return ( + + + {payload.name} + + + + = 0 ? 1 : -1) * labelOffset},${my}`} + stroke={fill} + fill="none" + /> + = 0 ? 1 : -1) * labelOffset} cy={my} r={2} fill={fill} stroke="none" /> + = 0 ? 1 : -1) * (labelOffset + labelTextGap)} + y={my} + textAnchor={cos >= 0 ? "start" : "end"} + dominantBaseline="central" + fill="#333" + > + {`${(percent * 100).toFixed(2)}%`} + + + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts index 79e70ff44..9e32b763b 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts @@ -5,9 +5,90 @@ import { useState } from "react"; import { ChartConfig } from "../../Charts"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +// ========================================== +// Types +// ========================================== + export type PieChartData = Array>; -// Helper function to calculate percentage +export interface CustomLabelProps { + payload: { + percentage: number; + [key: string]: any; + }; + cx: number; + cy: number; + x: number; + y: number; + textAnchor: string; + dominantBaseline: string; +} + +export interface ChartDimensions { + outerRadius: number; + innerRadius: number; +} + +export interface TwoLevelChartDimensions extends ChartDimensions { + middleRadius: number; +} + +export interface HoverStyles { + opacity: number; + stroke: string; + strokeWidth: number; +} + +export interface ChartHoverHook { + activeIndex: number | null; + handleMouseEnter: (event: any, index: number) => void; + handleMouseLeave: () => void; +} + +export interface LabelLineProps { + points: [Point, Point]; // Exactly two points required + payload: any; + value: number; + textAnchor: string; + dominantBaseline: string; +} + +interface Point { + x: number; + y: number; +} + +export interface ActiveShapeProps { + cx: number; + cy: number; + innerRadius: number; + outerRadius: number; + startAngle: number; + endAngle: number; + fill: string; + payload: any; + percent: number; + value: number; + midAngle: number; +} + +export interface AnimationConfig { + isAnimationActive: boolean; + animationBegin: number; + animationDuration: number; + animationEasing: "ease" | "ease-in" | "ease-out" | "ease-in-out" | "linear"; +} + +// ========================================== +// Core Calculation Utilities +// ========================================== + +/** + * Calculates the percentage value of a number relative to a total + * @param value - The value to calculate percentage for + * @param total - The total value to calculate percentage against + * @returns The calculated percentage rounded to 2 decimal places + */ export const calculatePercentage = (value: number, total: number): number => { if (total === 0) { return 0; @@ -15,12 +96,22 @@ export const calculatePercentage = (value: number, total: number): number => { return Number(((value / total) * 100).toFixed(2)); }; -// Dynamic resize function to maintain aspect ratio +// ========================================== +// Chart Dimension Calculations +// ========================================== + +/** + * Calculates dimensions for standard pie/donut charts + * @param width - The container width + * @param variant - The chart variant ('pie' or 'donut') + * @param label - Whether the chart has labels + * @returns Object containing outer and inner radius values + */ export const calculateChartDimensions = ( width: number, variant: string, label: boolean, -): { outerRadius: number; innerRadius: number } => { +): ChartDimensions => { const baseRadiusPercentage = 0.4; // 40% of container width let outerRadius = Math.round(width * baseRadiusPercentage); @@ -41,11 +132,16 @@ export const calculateChartDimensions = ( return { outerRadius, innerRadius }; }; -// Dynamic resize function for two-level pie chart +/** + * Calculates dimensions for two-level pie charts + * @param width - The container width + * @param label - Whether the chart has labels + * @returns Object containing outer, middle, and inner radius values + */ export const calculateTwoLevelChartDimensions = ( width: number, label: boolean, -): { outerRadius: number; middleRadius: number; innerRadius: number } => { +): TwoLevelChartDimensions => { const baseRadiusPercentage = 0.4; // 40% of container width let outerRadius = Math.round(width * baseRadiusPercentage); @@ -66,6 +162,13 @@ export const calculateTwoLevelChartDimensions = ( return { outerRadius, middleRadius, innerRadius }; }; +// ========================================== +// Layout and Styling Utilities +// ========================================== + +/** + * Map of layout types to their corresponding CSS classes + */ export const layoutMap: Record = { mobile: "crayon-pie-chart-container-mobile", fullscreen: "crayon-pie-chart-container-fullscreen", @@ -73,27 +176,13 @@ export const layoutMap: Record = { copilot: "crayon-pie-chart-container-copilot", }; -// Reusable hook for chart hover effects -export const useChartHover = () => { - const [activeIndex, setActiveIndex] = useState(null); - - const handleMouseEnter = (_: any, index: number) => { - setActiveIndex(index); - }; - - const handleMouseLeave = () => { - setActiveIndex(null); - }; - - return { - activeIndex, - handleMouseEnter, - handleMouseLeave, - }; -}; - -// Default hover style properties for cells -export const getHoverStyles = (index: number, activeIndex: number | null) => { +/** + * Generates hover style properties for chart cells + * @param index - The index of the current cell + * @param activeIndex - The index of the currently hovered cell + * @returns Object containing hover style properties + */ +export const getHoverStyles = (index: number, activeIndex: number | null): HoverStyles => { return { opacity: activeIndex === null || activeIndex === index ? 1 : 0.6, stroke: activeIndex === index ? "#fff" : "none", @@ -101,7 +190,16 @@ export const getHoverStyles = (index: number, activeIndex: number | null) => { }; }; -// Transform data with percentages +// ========================================== +// Data Transformation Utilities +// ========================================== + +/** + * Transforms data by adding percentage calculations + * @param data - The input data array + * @param dataKey - The key to use for value calculations + * @returns Transformed data with added percentage and original value + */ export const transformDataWithPercentages = ( data: T, dataKey: keyof T[number], @@ -114,12 +212,18 @@ export const transformDataWithPercentages = ( })); }; -// Create chart configuration +/** + * Creates chart configuration with colors and labels + * @param data - The input data array + * @param categoryKey - The key to use for category labels + * @param theme - The color theme to use + * @returns Chart configuration object + */ export const createChartConfig = ( data: T, categoryKey: keyof T[number], theme: string = "ocean", -) => { +): ChartConfig => { const palette = getPalette(theme); const colors = getDistributedColors(palette, data.length); @@ -129,8 +233,102 @@ export const createChartConfig = ( [String(item[categoryKey])]: { label: String(item[categoryKey as string]), color: colors[index], + secondaryColor: colors[data.length - index - 1], // Add secondary color for gradient effect }, }), {}, ); }; + +// ========================================== +// Hover Effect Utilities +// ========================================== + +/** + * Custom hook for managing chart hover effects + * @returns Object containing hover state and handlers + */ +export const useChartHover = (): ChartHoverHook => { + const [activeIndex, setActiveIndex] = useState(null); + + const handleMouseEnter = (_: any, index: number) => { + setActiveIndex(index); + }; + + const handleMouseLeave = () => { + setActiveIndex(null); + }; + + return { + activeIndex, + handleMouseEnter, + handleMouseLeave, + }; +}; + +// ========================================== +// Animation Utilities +// ========================================== + +/** + * Creates animation configuration for pie chart + * @param config - Animation configuration options + * @returns Animation configuration object + */ +export const createAnimationConfig = (config: Partial = {}): AnimationConfig => { + return { + isAnimationActive: config.isAnimationActive ?? true, + animationBegin: config.animationBegin ?? 0, + animationDuration: config.animationDuration ?? 1500, + animationEasing: config.animationEasing ?? "ease", + }; +}; + +// ========================================== +// Event Handler Utilities +// ========================================== + +/** + * Creates event handlers for pie chart + * @param onMouseEnter - Mouse enter handler + * @param onMouseLeave - Mouse leave handler + * @param onClick - Click handler + * @returns Object containing event handlers + */ +export const createEventHandlers = ( + onMouseEnter?: (data: any, index: number) => void, + onMouseLeave?: () => void, + onClick?: (data: any, index: number) => void, +) => { + return { + onMouseEnter: onMouseEnter + ? (data: any, index: number) => onMouseEnter(data, index) + : undefined, + onMouseLeave: onMouseLeave ? () => onMouseLeave() : undefined, + onClick: onClick ? (data: any, index: number) => onClick(data, index) : undefined, + }; +}; + +// ========================================== +// Sector Style Utilities +// ========================================== + +/** + * Creates sector style configuration + * @param cornerRadius - Corner radius for sectors + * @param paddingAngle - Padding angle between sectors + * @returns Sector style configuration + */ +export const createSectorStyle = (cornerRadius: number = 0, paddingAngle: number = 0) => { + return { + cornerRadius, + paddingAngle, + }; +}; + +// Add export for render functions from components +export { + renderActiveShape, + renderCustomLabel, + renderCustomLabelLine, +} from "../components/PieChartRenderers"; From 4ae68158df9a2166c239cab7f1dde531bc7a8db5 Mon Sep 17 00:00:00 2001 From: i-subham Date: Mon, 2 Jun 2025 13:58:42 +0530 Subject: [PATCH 034/190] feat(PieChartV2): Add gradient support and enhance rendering logic This commit introduces gradient color support to the PieChartV2 component, allowing for more visually appealing charts. Key changes include: 1. Added new props `useGradients` and `gradientColors` to enable gradient rendering for pie chart segments. 2. Implemented the `createGradientDefinitions` function to generate SVG gradient definitions based on provided colors. 3. Updated the rendering logic to apply gradients to pie chart segments when enabled. 4. Enhanced Storybook stories to demonstrate the new gradient features, including variations for single and multi-color gradients. These updates aim to improve the visual customization options for users of the PieChartV2 component. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 24 ++++- .../PieChartV2/stories/PieChartV2.stories.tsx | 64 +++++++++++++ .../components/PieChartRenderers.tsx | 92 ++++++++++++++++--- .../Charts/PieCharts/utils/PieChartUtils.ts | 7 -- 4 files changed, 165 insertions(+), 22 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 88445b971..f8401bb75 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -12,6 +12,7 @@ import { } from "../../Charts"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; import { + createGradientDefinitions, renderActiveShape, renderCustomLabel, renderCustomLabelLine, @@ -31,6 +32,11 @@ import { export type PieChartV2Data = PieChartData; +interface GradientColor { + start?: string; + end?: string; +} + export interface PieChartV2Props { data: T; categoryKey: keyof T[number]; @@ -44,6 +50,8 @@ export interface PieChartV2Props { appearance?: "circular" | "semiCircular"; cornerRadius?: number; paddingAngle?: number; + useGradients?: boolean; + gradientColors?: GradientColor[]; onMouseEnter?: (data: any, index: number) => void; onMouseLeave?: () => void; onClick?: (data: any, index: number) => void; @@ -62,6 +70,8 @@ export const PieChartV2 = ({ appearance = "circular", cornerRadius = 0, paddingAngle = 0, + useGradients = false, + gradientColors, onMouseEnter, onMouseLeave, onClick, @@ -89,6 +99,7 @@ export const PieChartV2 = ({ return () => resizeObserver.disconnect(); }, [variant, label]); + // Transform data and create configurations const transformedData = transformDataWithPercentages(data, dataKey); const chartConfig = createChartConfig(data, categoryKey, theme); const animationConfig = createAnimationConfig({ isAnimationActive }); @@ -99,6 +110,11 @@ export const PieChartV2 = ({ const palette = getPalette(theme); const colors = getDistributedColors(palette, data.length); + // Create gradient definitions if gradients are enabled + const gradientDefinitions = useGradients ? ( + {createGradientDefinitions(transformedData, colors, gradientColors)} + ) : null; + return ( ({ } /> {legend && } />} + {gradientDefinitions} ({ ) : false } - activeShape={renderActiveShape} + activeShape={(props: any) => renderActiveShape(props as any)} activeIndex={activeIndex ?? undefined} {...animationConfig} {...eventHandlers} @@ -139,10 +156,9 @@ export const PieChartV2 = ({ const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); const config = chartConfig[categoryValue]; const hoverStyles = getHoverStyles(index, activeIndex); + const fill = useGradients ? `url(#gradient-${index})` : config?.color || colors[index]; - return ( - - ); + return ; })} diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index 7a56fcc3b..57c009bbc 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -12,6 +12,16 @@ const pieChartData = [ { month: "July", value: 5890 }, ]; +const gradientColors = [ + { start: "#FF6B6B", end: "#FF8E8E" }, + { start: "#4ECDC4", end: "#6ED7D0" }, + { start: "#45B7D1", end: "#6BC5DB" }, + { start: "#96CEB4", end: "#B4DCC9" }, + { start: "#FFEEAD", end: "#FFF4C4" }, + { start: "#D4A5A5", end: "#E5BDBD" }, + { start: "#9B59B6", end: "#B07CC7" }, +]; + const meta: Meta> = { title: "Components/Charts/PieCharts/PieChartV2", component: PieChartV2, @@ -224,3 +234,57 @@ export const PercentageFormat: Story = { ), }; + +export const GradientColors: Story = { + name: "Gradient Colors", + args: { + ...Default.args, + theme: "vivid", + variant: "donut", + cornerRadius: 5, + paddingAngle: 1, + format: "percentage", + useGradients: true, + gradientColors: [ + { start: "#FF6B6B", end: "#FF8E8E" }, + { start: "#4ECDC4", end: "#6ED7D0" }, + { start: "#45B7D1", end: "#6BC5DB" }, + { start: "#96CEB4", end: "#B4DCC9" }, + { start: "#FFEEAD", end: "#FFF4C4" }, + { start: "#D4A5A5", end: "#E5BDBD" }, + { start: "#9B59B6", end: "#B07CC7" }, + ], + }, + render: (args) => ( + + + + ), +}; + +export const SingleColorGradients: Story = { + name: "Single Color Gradients", + args: { + ...Default.args, + theme: "vivid", + variant: "donut", + cornerRadius: 5, + paddingAngle: 1, + format: "percentage", + useGradients: true, + gradientColors: [ + { start: "#FF6B6B" }, // Only start color provided + { end: "#4ECDC4" }, // Only end color provided + { start: "#45B7D1" }, // Only start color provided + { end: "#96CEB4" }, // Only end color provided + { start: "#FFEEAD" }, // Only start color provided + { end: "#D4A5A5" }, // Only end color provided + { start: "#9B59B6" }, // Only start color provided + ], + }, + render: (args) => ( + + + + ), +}; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx index efb94f72d..60765ac8f 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Sector } from "recharts"; -import { CustomLabelProps, LabelLineProps } from "../utils/PieChartUtils"; +import { ActiveShapeProps, CustomLabelProps, LabelLineProps } from "../utils/PieChartUtils"; /** * Renders a custom label for pie chart segments @@ -93,15 +93,6 @@ export const renderCustomLabelLine = (props: LabelLineProps): React.ReactElement strokeWidth={1} fill="none" /> - {/* - {`${value}%`} - */} ); }; @@ -135,7 +126,7 @@ export const renderCustomLabelLine = (props: LabelLineProps): React.ReactElement * }) * // Renders: An enhanced segment with label and percentage */ -export const renderActiveShape = (props: any): React.ReactElement => { +export const renderActiveShape = (props: ActiveShapeProps): React.ReactElement => { const { cx, cy, @@ -199,3 +190,82 @@ export const renderActiveShape = (props: any): React.ReactElement => { ); }; + +/** + * Creates gradient definitions for pie chart segments + * @param data - The chart data array + * @param colors - Array of base colors for segments + * @param gradientColors - Optional array of gradient color configurations + * @returns Array of gradient definition elements + */ +export const createGradientDefinitions = ( + data: any[], + colors: string[], + gradientColors?: Array<{ start?: string; end?: string }>, +) => { + return data.map((_, index) => { + const defaultBaseColor = colors[index] || "#000000"; + + // Get the base color from gradientColors or fallback to default + let baseColor = defaultBaseColor; + let secondaryColor = colors[data.length - index - 1] || "#ffffff"; + + if (gradientColors?.[index]) { + // If both colors are provided, use them + if (gradientColors[index].start && gradientColors[index].end) { + baseColor = gradientColors[index].start; + secondaryColor = gradientColors[index].end; + } + // If only start color is provided, create a darker version for end + else if (gradientColors[index].start) { + baseColor = gradientColors[index].start; + secondaryColor = adjustColorBrightness(baseColor, -20); + } + // If only end color is provided, create a lighter version for start + else if (gradientColors[index].end) { + baseColor = adjustColorBrightness(gradientColors[index].end, 20); + secondaryColor = gradientColors[index].end; + } + } + + return ( + + + + + + + ); + }); +}; + +/** + * Adjusts the brightness of a hex color + * @param hex - The hex color to adjust + * @param percent - The percentage to adjust brightness by (-100 to 100) + * @returns The adjusted hex color + */ +export const adjustColorBrightness = (hex: string, percent: number): string => { + if (!hex || !hex.startsWith("#")) { + return hex; + } + try { + const num = parseInt(hex.replace("#", ""), 16); + const amt = Math.round(2.55 * percent); + const R = Math.min(255, Math.max(0, (num >> 16) + amt)); + const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) + amt)); + const B = Math.min(255, Math.max(0, (num & 0x0000ff) + amt)); + return "#" + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1); + } catch (error) { + console.warn("Invalid color format:", hex); + return hex; + } +}; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts index 9e32b763b..7e4cc68f6 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts @@ -325,10 +325,3 @@ export const createSectorStyle = (cornerRadius: number = 0, paddingAngle: number paddingAngle, }; }; - -// Add export for render functions from components -export { - renderActiveShape, - renderCustomLabel, - renderCustomLabelLine, -} from "../components/PieChartRenderers"; From 104596f5b50ab7e2dbc52a50596a471036ddedb0 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 2 Jun 2025 15:13:50 +0530 Subject: [PATCH 035/190] feat(charts): Improve chart styling and layout This commit includes the following changes: - Reduce the stroke width of the cartesian grid lines from 2 to 1 and remove the stroke dash pattern - Reduce the stroke width of the custom cursor shape from 2 to 1 - Adjust the vertical positioning of the x-axis tick labels to be 10 pixels from the axis - Increase the radius of the bar chart bars from 2 to 4 - Simplify the custom cursor component by using a square-cornered rectangle instead of a rounded top - Increase the default bar chart radius from 2 to 4 These changes aim to improve the overall visual styling and layout of the chart components, making them more consistent and polished. --- .../BarCharts/BarChartV2/BarChartV2.tsx | 2 +- .../BarChartV2/components/CustomCursor.scss | 2 +- .../BarChartV2/components/CustomCursor.tsx | 42 ++++--------------- .../BarChartV2/components/LineInBarShape.tsx | 18 +++++++- .../BarChartV2/components/XAxisTick.tsx | 13 +----- .../BarChartV2/components/yAxisTick.scss | 2 +- .../BarChartV2/stories/barChartV2.stories.tsx | 2 +- .../Charts/BarCharts/utils/BarChartUtils.ts | 1 - .../src/components/Charts/cartesianGrid.tsx | 4 +- 9 files changed, 33 insertions(+), 53 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 85ad836c0..6d4520f07 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -67,7 +67,7 @@ const BarChartV2Component = ({ variant = "grouped", grid = true, icons = {}, - radius = 2, + radius = 4, isAnimationActive = true, showYAxis = false, xAxisLabel, diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.scss index 774807c92..2c1274c38 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.scss +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.scss @@ -3,5 +3,5 @@ .crayon-chart-cursor-shape { fill: cssUtils.$bg-sunk; stroke: cssUtils.$stroke-default; - stroke-width: 2; + stroke-width: 1; } diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx index 6655cdcea..88044f379 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx @@ -125,56 +125,32 @@ export const CustomCursor: React.FC = ({ ); }; -// Alternative simpler cursor component with rounded top, square bottom +// Alternative simpler cursor component with square corners export const SimpleCursor: React.FC = (props) => { const { x = 0, y = 0, width = 0, height = 0 } = props; - // Define the radius for the top corners - const topRadius = 8; - - // Create a path for rounded top, square bottom - // M = Move to, L = Line to, Q = Quadratic curve, Z = Close path - + // Create a simple rectangular path const pathData = ` - M ${x + topRadius} ${y} - L ${x + width - topRadius} ${y} - Q ${x + width} ${y} ${x + width} ${y + topRadius} + M ${x} ${y} + L ${x + width} ${y} L ${x + width} ${y + height} L ${x} ${y + height} - L ${x} ${y + topRadius} - Q ${x} ${y} ${x + topRadius} ${y} Z `; /* SVG Path Command Documentation: * - * M ${x + topRadius} ${y} - * - M = "Move to" - Sets the starting point without drawing - * - Moves to a point on the top edge, offset by the radius from the left corner - * - * L ${x + width - topRadius} ${y} - * - L = "Line to" - Draws a straight line from current position - * - Draws the top horizontal line, stopping before the top-right corner radius + * M ${x} ${y} + * - M = "Move to" - Sets the starting point at top-left corner * - * Q ${x + width} ${y} ${x + width} ${y + topRadius} - * - Q = "Quadratic curve to" - Draws a curved line using control point - * - Control point: (x + width, y) - the actual top-right corner - * - End point: (x + width, y + topRadius) - down the right edge by radius amount - * - Creates the rounded top-right corner + * L ${x + width} ${y} + * - L = "Line to" - Draws the top horizontal line to top-right corner * * L ${x + width} ${y + height} * - L = "Line to" - Draws straight line down the right edge to bottom-right corner * * L ${x} ${y + height} - * - L = "Line to" - Draws straight line across the bottom edge (square corner) - * - * L ${x} ${y + topRadius} - * - L = "Line to" - Draws straight line up the left edge to where the curve starts - * - * Q ${x} ${y} ${x + topRadius} ${y} - * - Q = "Quadratic curve to" - Creates the rounded top-left corner - * - Control point: (x, y) - the actual top-left corner - * - End point: (x + topRadius, y) - back to our starting position + * - L = "Line to" - Draws straight line across the bottom edge to bottom-left corner * * Z = "Close path" - Draws a line back to the starting point and closes the shape */ diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx index d9439403e..23d25c0dc 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx @@ -11,6 +11,10 @@ interface BarWithInternalLineProps { radius?: number | number[]; internalLineColor?: string; internalLineWidth?: number; + isHovered?: boolean; + hoveredCategory?: string | number | null; + categoryKey?: string; + payload?: any; // Recharts also passes other props like payload, value, etc. // we can add them here if our shape component needs them. // console.log("props", props); @@ -29,6 +33,10 @@ const LineInBarShape: FunctionComponent = (props) => { strokeWidth, internalLineColor: iLineColor, // Use prop or fallback internalLineWidth: iLineWidth, + isHovered, + hoveredCategory, + categoryKey, + payload, } = props; // Ensure rTL and rTR are always numbers, defaulting to 0. @@ -44,6 +52,13 @@ const LineInBarShape: FunctionComponent = (props) => { rTR = 0; } + // Calculate opacity based on hover state + let opacity = 1; + if (isHovered && hoveredCategory !== null && payload && categoryKey) { + const currentCategoryValue = payload[categoryKey]; + opacity = currentCategoryValue === hoveredCategory ? 1 : 0.7; + } + // Path data for a rectangle with potentially rounded top corners // M = move to, L = line to, A = arc, Z = close path // Handle cases where rTL or rTR might be 0 (sharp corners) @@ -60,7 +75,7 @@ const LineInBarShape: FunctionComponent = (props) => { return ( {/* The main bar shape (using for rounded corners) */} - + {/* The internal vertical line */} {width > 0 && @@ -73,6 +88,7 @@ const LineInBarShape: FunctionComponent = (props) => { stroke={iLineColor} strokeWidth={iLineWidth} strokeLinecap="round" + opacity={opacity} /> )} diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx index 28d1613fd..8ababb998 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx @@ -37,23 +37,12 @@ const XAxisTick: React.FC = (props) => { const displayValue = tickFormatter ? tickFormatter(payload?.value) : String(payload?.value || ""); - // Calculate text offset based on orientation and vertical anchor - const getTextOffset = () => { - const isBottom = orientation === "bottom"; - const offsetMap = { - start: isBottom ? 6 : -4, - middle: isBottom ? 6 : -8, - end: isBottom ? -8 : -16, - }; - return offsetMap[verticalAnchor] || 0; - }; - return ( { return containerWidth ? containerWidth * (9 / 16) : 400; }; -// Export only the functions and types that are used externally export { findNearestSnapPosition, getChartConfig, diff --git a/js/packages/react-ui/src/components/Charts/cartesianGrid.tsx b/js/packages/react-ui/src/components/Charts/cartesianGrid.tsx index 4f26689b3..e8f60d989 100644 --- a/js/packages/react-ui/src/components/Charts/cartesianGrid.tsx +++ b/js/packages/react-ui/src/components/Charts/cartesianGrid.tsx @@ -5,8 +5,8 @@ export const cartesianGrid = () => ( vertical={false} fillOpacity={1} strokeOpacity={1} - strokeWidth={2} - strokeDasharray="6" + strokeWidth={1} + strokeDasharray="0" strokeLinecap="round" strokeLinejoin="round" stroke="currentColor" From 41de5da6014ab5d5b8092d3d2d51f30471698aa1 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 2 Jun 2025 17:45:26 +0530 Subject: [PATCH 036/190] feat(BarChartV2): Improve legend styles and add stacked bar gap support - Increase spacing between legend items to 2xs - Increase size of legend item indicators to 10px - Adjust border-radius of legend item indicators to 2xs - Add support for stacked bar gaps - Introduce new props: `variant`, `stackGap`, `isFirstInStack` - Adjust bar height to create visual separation between stacked bars - Memoize opacity calculation for hover effects - Memoize adjusted dimensions for stacked bar gaps --- .../BarCharts/BarChartV2/BarChartV2.tsx | 25 ++- .../BarChartV2/components/CustomCursor.tsx | 132 +------------- .../BarChartV2/components/LineInBarShape.tsx | 165 +++++++++++++----- .../BarChartV2/components/YAxisTick.tsx | 3 +- .../BarChartV2/components/defaultLegend.scss | 9 +- .../BarChartV2/components/yAxisTick.scss | 1 + .../Charts/BarCharts/utils/BarChartUtils.ts | 4 +- 7 files changed, 162 insertions(+), 177 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 6d4520f07..013eee863 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -1,5 +1,5 @@ import clsx from "clsx"; -import { ChevronFirst, ChevronLast } from "lucide-react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; import { Bar, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; import { IconButton } from "../../../IconButton"; @@ -96,6 +96,7 @@ const BarChartV2Component = ({ const [containerWidth, setContainerWidth] = useState(0); const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(false); + const [hoveredCategory, setHoveredCategory] = useState(null); // need this to calculate the padding for the chart container, because the y-axis is rendered in a separate chart const effectiveContainerWidth = useMemo(() => { @@ -207,6 +208,17 @@ const BarChartV2Component = ({ return getOptimalXAxisTickFormatter(data, categoryKey as string, variant); }, [data, categoryKey, variant]); + // Handle mouse events for group hovering + const handleChartMouseMove = useCallback((state: any) => { + if (state && state.activeLabel !== undefined) { + setHoveredCategory(state.activeLabel); + } + }, []); + + const handleChartMouseLeave = useCallback(() => { + setHoveredCategory(null); + }, []); + return (
@@ -266,6 +278,8 @@ const BarChartV2Component = ({ bottom: 0, }} onClick={onBarsClick} + onMouseMove={handleChartMouseMove} + onMouseLeave={handleChartMouseLeave} barGap={BAR_GAP} barCategoryGap={BAR_CATEGORY_GAP} syncId={chartSyncID} @@ -311,6 +325,11 @@ const BarChartV2Component = ({ } /> @@ -325,7 +344,7 @@ const BarChartV2Component = ({
} + icon={} variant="secondary" onClick={scrollLeft} size="extra-small" @@ -333,7 +352,7 @@ const BarChartV2Component = ({ /> } + icon={} variant="secondary" size="extra-small" onClick={scrollRight} diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx index 88044f379..7c4d7864c 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; interface CustomCursorProps { x?: number; @@ -10,133 +10,16 @@ interface CustomCursorProps { [key: string]: any; } -export const CustomCursor: React.FC = ({ +const SimpleCursorComponent: React.FC = ({ x = 0, y = 0, width = 0, height = 0, - payload, }) => { - // You can access the data at the cursor position through payload - const hasData = payload && payload.length > 0; - - return ( - - {/* Define a gradient for the background */} - - - - - - - - - - - - - - {/* Main cursor background with rounded corners */} - - - {/* Central vertical line */} - - - {/* Top indicator dot */} - - - {/* Bottom indicator dot */} - - - {/* Side accent lines */} - - - - - {/* Data count indicator (if there's data) */} - {hasData && payload && payload.length > 1 && ( - - - - {payload.length} - - - )} - - ); -}; - -// Alternative simpler cursor component with square corners -export const SimpleCursor: React.FC = (props) => { - const { x = 0, y = 0, width = 0, height = 0 } = props; - - // Create a simple rectangular path - const pathData = ` - M ${x} ${y} - L ${x + width} ${y} - L ${x + width} ${y + height} - L ${x} ${y + height} - Z - `; + // Memoize path calculation to avoid recreating on every render + const pathData = useMemo(() => { + return `M ${x} ${y} L ${x + width} ${y} L ${x + width} ${y + height} L ${x} ${y + height} Z`; + }, [x, y, width, height]); /* SVG Path Command Documentation: * @@ -161,3 +44,6 @@ export const SimpleCursor: React.FC = (props) => { ); }; + +// Memoize component to prevent re-renders when props haven't changed +export const SimpleCursor = React.memo(SimpleCursorComponent); diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx index 23d25c0dc..17a57aa5c 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent } from "react"; +import React, { FunctionComponent, useMemo } from "react"; interface BarWithInternalLineProps { x?: number; @@ -15,13 +15,22 @@ interface BarWithInternalLineProps { hoveredCategory?: string | number | null; categoryKey?: string; payload?: any; + // New props for stacked bar gaps + variant?: "grouped" | "stacked"; + stackGap?: number; + isFirstInStack?: boolean; + // Recharts also passes other props like payload, value, etc. // we can add them here if our shape component needs them. // console.log("props", props); [key: string]: any; // Allow other props from Recharts } -const LineInBarShape: FunctionComponent = (props) => { +const DEFAULT_STACK_GAP = 1; +const MIN_LINE_HEIGHT = 5; +const LINE_PADDING = 6; + +const LineInBarShape: FunctionComponent = React.memo((props) => { const { x = 0, // Default to 0 to avoid NaN issues if undefined y = 0, @@ -37,62 +46,132 @@ const LineInBarShape: FunctionComponent = (props) => { hoveredCategory, categoryKey, payload, + variant = "grouped", + stackGap = DEFAULT_STACK_GAP, // Default 1px gap + isFirstInStack = false, } = props; - // Ensure rTL and rTR are always numbers, defaulting to 0. - let rTL: number, rTR: number; - if (Array.isArray(r)) { - rTL = r[0] || 0; - rTR = r[1] || 0; - } else if (typeof r === "number") { - rTL = r; - rTR = r; - } else { - rTL = 0; - rTR = 0; - } + // Memoized radius calculations - Ensure rTL and rTR are always numbers, defaulting to 0. + // This calculation is memoized to avoid recalculating on every render when radius prop hasn't changed + const { rTL, rTR } = useMemo(() => { + if (Array.isArray(r)) { + return { rTL: r[0] || 0, rTR: r[1] || 0 }; + } else if (typeof r === "number") { + return { rTL: r, rTR: r }; + } + return { rTL: 0, rTR: 0 }; + }, [r]); - // Calculate opacity based on hover state - let opacity = 1; - if (isHovered && hoveredCategory !== null && payload && categoryKey) { + // Memoized opacity calculation for hover effects + // Calculate the opacity value for the bar based on hover state + // When a category is hovered: + // - Bars in the hovered category maintain full opacity (1) + // - All other bars are dimmed to 60% opacity (0.6) + // This creates a visual hierarchy highlighting the hovered category + // + // @default 1 - Full opacity when no hover state is active + // @requires isHovered - Boolean indicating if any category is being hovered + // @requires hoveredCategory - The category value currently being hovered (string|number|null) + // @requires payload - The data payload for this bar containing category information + // @requires categoryKey - The key used to access the category value in the payload + const opacity = useMemo(() => { + if (!isHovered || hoveredCategory === null || !payload || !categoryKey) { + return 1; + } const currentCategoryValue = payload[categoryKey]; - opacity = currentCategoryValue === hoveredCategory ? 1 : 0.7; - } + return currentCategoryValue === hoveredCategory ? 1 : 0.6; + }, [isHovered, hoveredCategory, payload, categoryKey]); + + // Memoized adjusted dimensions for stacked bar gaps + // Adjust dimensions for stacked bar gaps + // This creates visual separation between bars in a stack by reducing height only + // We don't adjust Y position to avoid double gaps and positioning issues + // Only reduce height at the top of each bar except the last one (bottom-most in stack) + // This creates a gap between this bar and the bar above it + const { adjustedY, adjustedHeight } = useMemo(() => { + if (variant === "stacked" && stackGap > 0 && !isFirstInStack) { + return { + adjustedY: y, + adjustedHeight: height - stackGap, + }; + } + return { + adjustedY: y, + adjustedHeight: height, + }; + }, [variant, stackGap, isFirstInStack, y, height]); - // Path data for a rectangle with potentially rounded top corners + // Memoized SVG path calculation for optimized rendering + // Path data for a rectangle with potentially rounded top corners and stack gaps // M = move to, L = line to, A = arc, Z = close path // Handle cases where rTL or rTR might be 0 (sharp corners) - const path = ` - M ${x},${y + rTL} - ${rTL > 0 ? `A ${rTL},${rTL} 0 0 1 ${x + rTL},${y}` : `L ${x},${y}`} - L ${x + width - rTR},${y} - ${rTR > 0 ? `A ${rTR},${rTR} 0 0 1 ${x + width},${y + rTR}` : `L ${x + width},${y}`} - L ${x + width},${y + height} - L ${x},${y + height} + // This SVG path string creates a rectangle with optional rounded top corners + // The path is constructed using SVG path commands: + // M = Move to starting point + // L = Draw line to point + // A = Draw arc (rx,ry rotation large-arc-flag sweep-flag x,y) + // Z = Close path back to start + // + // The path construction: + // 1. Starts at bottom left corner (x, adjustedY+rTL) + // 2. If rTL > 0, draws arc for top left corner, else draws straight line + // 3. Draws line across top to right side + // 4. If rTR > 0, draws arc for top right corner, else draws straight line + // 5. Draws straight line down right side + // 6. Draws straight line across bottom + // 7. Closes path back to start + const path = useMemo(() => { + return ` + M ${x},${adjustedY + rTL} + ${rTL > 0 ? `A ${rTL},${rTL} 0 0 1 ${x + rTL},${adjustedY}` : `L ${x},${adjustedY}`} + L ${x + width - rTR},${adjustedY} + ${rTR > 0 ? `A ${rTR},${rTR} 0 0 1 ${x + width},${adjustedY + rTR}` : `L ${x + width},${adjustedY}`} + L ${x + width},${adjustedY + adjustedHeight} + L ${x},${adjustedY + adjustedHeight} Z `; + }, [x, adjustedY, adjustedHeight, width, rTL, rTR]); + + // Memoized line coordinates calculation for the internal vertical line + // Only calculate coordinates if bar has sufficient width and height + // The internal line is centered horizontally and padded vertically for better visual appearance + const lineCoords = useMemo(() => { + if (width <= 0 || adjustedHeight <= MIN_LINE_HEIGHT) { + return null; + } + + const centerX = x + width / 2; + return { + x1: centerX, + y1: adjustedY + LINE_PADDING, // Starts below the top edge of the adjusted bar + x2: centerX, + y2: adjustedY + adjustedHeight - LINE_PADDING, // Ends above the bottom edge of the adjusted bar + }; + }, [x, width, adjustedY, adjustedHeight]); return ( - {/* The main bar shape (using for rounded corners) */} + {/* The main bar shape (using for rounded corners and stack gaps) */} - {/* The internal vertical line */} - {width > 0 && - height > 5 && ( // Only render line if bar has sufficient height - - )} + {/* The internal vertical line - adjusted for stack gaps */} + {lineCoords && ( + + )} ); -}; +}); + +// Add display name for better debugging +LineInBarShape.displayName = "LineInBarShape"; export { LineInBarShape }; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/YAxisTick.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/YAxisTick.tsx index 5bd42fe95..b3d3b1471 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/YAxisTick.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/YAxisTick.tsx @@ -23,7 +23,7 @@ interface YAxisTickProps { } const YAxisTick: React.FC = (props) => { - const { x, y, payload, textAnchor, verticalAnchor, fill, tickFormatter, className } = props; + const { x, y, payload, textAnchor, verticalAnchor, tickFormatter, className } = props; // Use the provided tickFormatter or fallback to displaying raw value const displayValue = tickFormatter ? tickFormatter(payload?.value) : String(payload?.value || ""); @@ -35,7 +35,6 @@ const YAxisTick: React.FC = (props) => { y={0} dy={verticalAnchor === "middle" ? 4 : 0} // Adjust based on vertical anchor textAnchor={textAnchor || "end"} - fill={fill || "#666"} className="crayon-chart-y-axis-tick" > {displayValue} diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/defaultLegend.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/defaultLegend.scss index 4ddb9e1b8..7dac9480d 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/defaultLegend.scss +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/defaultLegend.scss @@ -5,6 +5,7 @@ flex-direction: column; align-items: center; justify-content: center; + gap: cssUtils.$spacing-2xs; } .crayon-chart-legend-axis-label-container { @@ -39,7 +40,7 @@ &-item { display: flex; align-items: center; - gap: cssUtils.$spacing-xs; + gap: cssUtils.$spacing-2xs; svg { height: 12px; @@ -48,10 +49,10 @@ } &-indicator { - height: 8px; - width: 8px; + height: 10px; + width: 10px; flex-shrink: 0; - border-radius: cssUtils.$rounded-3xs; + border-radius: cssUtils.$rounded-2xs; background-color: var(--color-bg); } diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/yAxisTick.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/yAxisTick.scss index d2fe133f2..7c5435e1c 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/yAxisTick.scss +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/yAxisTick.scss @@ -2,4 +2,5 @@ .crayon-chart-y-axis-tick { @include cssUtils.typography(label, small); + fill: cssUtils.$secondary-text; } diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts index 976e574b0..a8238ed1e 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts @@ -75,8 +75,8 @@ const getPadding = ( if (paddingValue < 0) { // If chart content is wider than container, no padding return { - left: 2, - right: 2, + left: 1, + right: 1, }; } else { return { From 8840ed72990568e708c599d4c93442310e4315ec Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 2 Jun 2025 17:59:23 +0530 Subject: [PATCH 037/190] feat(BarChartV2): Improve XAxisTick and CustomCursor components Refactors the XAxisTick component to remove the `verticalAnchor` prop as it is not being used. Also removes the `orientation` prop as it is not necessary for the component's functionality. Enhances the `CustomCursor` component by breaking the path data string into multiple lines for better readability and maintainability. --- .../Charts/BarCharts/BarChartV2/components/CustomCursor.tsx | 6 +++++- .../Charts/BarCharts/BarChartV2/components/XAxisTick.tsx | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx index 7c4d7864c..38f4bcde1 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx @@ -18,7 +18,11 @@ const SimpleCursorComponent: React.FC = ({ }) => { // Memoize path calculation to avoid recreating on every render const pathData = useMemo(() => { - return `M ${x} ${y} L ${x + width} ${y} L ${x + width} ${y + height} L ${x} ${y + height} Z`; + return `M ${x} ${y} + L ${x + width} ${y} + L ${x + width} ${y + height} + L ${x} ${y + height} + Z`; }, [x, y, width, height]); /* SVG Path Command Documentation: diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx index 8ababb998..cf619ff91 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx @@ -28,11 +28,9 @@ const XAxisTick: React.FC = (props) => { y, payload, textAnchor = "middle", - verticalAnchor = "start", fill = "#666", tickFormatter, className, - orientation = "bottom", } = props; const displayValue = tickFormatter ? tickFormatter(payload?.value) : String(payload?.value || ""); From b413d4258e3bbe615e9d8e634515afca42db883d Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 3 Jun 2025 01:07:55 +0530 Subject: [PATCH 038/190] feat(BarChartV2): Increase minimum line height and add width/height props - Increase the `MIN_LINE_HEIGHT` constant from 5 to 8 to ensure better visibility of the internal line in the bar chart. - Add `width` and `height` props to the `BarChartV2` component to allow users to set a fixed size for the chart. When provided, these props will override the default responsive behavior. - Update the stories to demonstrate the usage of the new `width` and `height` props. - Refactor the calculation of the effective chart width and height to use the new props when available. --- .../BarCharts/BarChartV2/BarChartV2.tsx | 63 ++++++++++++++----- .../BarChartV2/components/LineInBarShape.tsx | 2 +- .../BarChartV2/components/XAxisTick.tsx | 10 +-- .../BarChartV2/stories/barChartV2.stories.tsx | 26 +++++++- .../BarCharts/MiniBarChart/MiniBarChart.tsx | 15 +---- .../stories/MiniBarChart.stories.tsx | 12 +--- 6 files changed, 74 insertions(+), 54 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 013eee863..6587b28bb 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -3,6 +3,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react"; import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; import { Bar, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; import { IconButton } from "../../../IconButton"; +import { useTheme } from "../../../ThemeProvider"; import { ChartConfig, ChartContainer, @@ -50,15 +51,17 @@ export interface BarChartPropsV2 { xAxisLabel?: React.ReactNode; yAxisLabel?: React.ReactNode; onBarsClick?: (data: any) => void; - barInternalLineColor?: string; - barInternalLineWidth?: number; legend?: boolean; className?: string; + height?: number; + width?: number; } const Y_AXIS_WIDTH = 40; // Width of Y-axis chart when shown const BAR_GAP = 10; // Gap between bars const BAR_CATEGORY_GAP = "20%"; // Gap between categories +const BAR_INTERNAL_LINE_WIDTH = 1; +const BAR_RADIUS = 4; const BarChartV2Component = ({ data, @@ -67,16 +70,16 @@ const BarChartV2Component = ({ variant = "grouped", grid = true, icons = {}, - radius = 4, + radius = BAR_RADIUS, isAnimationActive = true, showYAxis = false, xAxisLabel, yAxisLabel, onBarsClick, - barInternalLineColor = "rgba(255, 255, 255, 0.5)", // Default internal line color - barInternalLineWidth = 1, // Default internal line width legend = false, className, + height, + width, }: BarChartPropsV2) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); @@ -98,11 +101,16 @@ const BarChartV2Component = ({ const [canScrollRight, setCanScrollRight] = useState(false); const [hoveredCategory, setHoveredCategory] = useState(null); + // Use provided width or observed width + const effectiveWidth = useMemo(() => { + return width ?? containerWidth; + }, [width, containerWidth]); + // need this to calculate the padding for the chart container, because the y-axis is rendered in a separate chart const effectiveContainerWidth = useMemo(() => { const yAxisWidth = showYAxis ? Y_AXIS_WIDTH : 0; - return Math.max(0, containerWidth - yAxisWidth); - }, [containerWidth, showYAxis]); + return Math.max(0, effectiveWidth - yAxisWidth); + }, [effectiveWidth, showYAxis]); const padding = useMemo(() => { return getPadding(data, categoryKey as string, effectiveContainerWidth, variant); @@ -117,9 +125,13 @@ const BarChartV2Component = ({ return getSnapPositions(data, categoryKey as string, variant); }, [data, categoryKey, variant]); + // Use provided height or calculated height based on container width + // if height is provided, it will be used to set the height of the chart + // if height is not provided, it will be calculated based on the container width (effectiveWidth) + // height will be 16:9 ratio of the width const chartHeight = useMemo(() => { - return getChartHeight(containerWidth); - }, [containerWidth]); + return height ?? getChartHeight(effectiveWidth); + }, [height, effectiveWidth]); // Check scroll boundaries const updateScrollState = useCallback(() => { @@ -157,11 +169,13 @@ const BarChartV2Component = ({ }, [snapPositions]); useEffect(() => { - if (!chartContainerRef.current) { + // Only set up ResizeObserver if width is not provided + if (width || !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) { setContainerWidth(entry.contentRect.width); } @@ -172,12 +186,12 @@ const BarChartV2Component = ({ return () => { resizeObserver.disconnect(); }; - }, []); + }, [width]); // Update scroll state when container width or data width changes useEffect(() => { updateScrollState(); - }, [containerWidth, dataWidth, updateScrollState]); + }, [effectiveWidth, dataWidth, updateScrollState]); // Add scroll event listener to update button states useEffect(() => { @@ -219,8 +233,22 @@ const BarChartV2Component = ({ setHoveredCategory(null); }, []); + const { mode } = useTheme(); + + const barInternalLineColor = useMemo(() => { + if (mode === "light") { + return "rgba(255, 255, 255, 0.3)"; + } + return "rgba(0, 0, 0, 0.3)"; + }, [mode]); + return ( -
+
{showYAxis && (
@@ -252,6 +280,7 @@ const BarChartV2Component = ({ key={`yaxis-${key}`} dataKey={key} fill="transparent" + stackId={variant === "stacked" ? "a" : undefined} isAnimationActive={false} maxBarSize={0} /> @@ -324,7 +353,7 @@ const BarChartV2Component = ({ shape={ ({
- {/* Scroll buttons */} - {dataWidth > containerWidth && ( + {/* if the data width is greater than the effective width, then show the scroll buttons */} + {dataWidth > effectiveWidth && (
({ ); }; -// Add React.memo for performance optimization +// Added React.memo for performance optimization to avoid unnecessary re-renders export const BarChartV2 = React.memo(BarChartV2Component) as typeof BarChartV2Component; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx index 17a57aa5c..b8f9c6ea2 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx @@ -27,7 +27,7 @@ interface BarWithInternalLineProps { } const DEFAULT_STACK_GAP = 1; -const MIN_LINE_HEIGHT = 5; +const MIN_LINE_HEIGHT = 8; const LINE_PADDING = 6; const LineInBarShape: FunctionComponent = React.memo((props) => { diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx index cf619ff91..72b6666af 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx @@ -23,15 +23,7 @@ interface XAxisTickProps { } const XAxisTick: React.FC = (props) => { - const { - x, - y, - payload, - textAnchor = "middle", - fill = "#666", - tickFormatter, - className, - } = props; + const { x, y, payload, textAnchor = "middle", fill = "#666", tickFormatter, className } = props; const displayValue = tickFormatter ? tickFormatter(payload?.value) : String(payload?.value || ""); diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx index 380ba6dd2..522fc7d61 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx @@ -234,6 +234,26 @@ const meta: Meta> = { category: "Display", }, }, + height: { + description: + "Fixed height for the chart in pixels. When provided, overrides the default responsive height calculation.", + control: "number", + table: { + type: { summary: "number" }, + defaultValue: { summary: "undefined" }, + category: "Layout", + }, + }, + width: { + description: + "Fixed width for the chart container in pixels. When provided, disables responsive width behavior.", + control: "number", + table: { + type: { summary: "number" }, + defaultValue: { summary: "undefined" }, + category: "Layout", + }, + }, }, } satisfies Meta; @@ -251,9 +271,11 @@ export const BarChartV2Story: Story = { grid: true, isAnimationActive: true, showYAxis: true, - xAxisLabel: "Time Period", - yAxisLabel: "Number of Users", + // xAxisLabel: "Time Period", + // yAxisLabel: "Number of Users", legend: true, + // width: 600, + // height: 300, }, render: (args: any) => { const [selectedDataType, setSelectedDataType] = diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx index 3b339c6a5..f5465cb4f 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx @@ -15,7 +15,6 @@ export interface MiniBarChartProps { variant?: Variant; radius?: number; isAnimationActive?: boolean; - label?: string; onBarsClick?: (data: any) => void; } @@ -82,12 +81,10 @@ export const MiniBarChart = ({ // }; return ( - //
- //
@@ -109,15 +106,5 @@ export const MiniBarChart = ({ })} - // {/*
- //
- // - // {calculateTotal(data, categoryKey).toLocaleString()} - // - // - // {label} - // - //
- //
*/} ); }; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx index a6afd58f4..2543dae05 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx @@ -28,7 +28,7 @@ const meta: Meta> = { }, }, }, - tags: ["!dev", "autodocs"], + tags: ["dev", "autodocs"], argTypes: { data: { description: @@ -87,15 +87,6 @@ const meta: Meta> = { category: "Display", }, }, - label: { - description: "Label text that appears next to the total value sum calculation", - control: "text", - table: { - type: { summary: "string" }, - defaultValue: { summary: "undefined" }, - category: "Display", - }, - }, }, } satisfies Meta; @@ -105,7 +96,6 @@ type Story = StoryObj; export const BarChartV3Story: Story = { name: "Bar Chart V3", args: { - label: "Total sales", data: barChartData, categoryKey: "month", theme: "ocean", From 8c11ee7b5582cd4294c2aec3e252b918932e3a12 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 3 Jun 2025 16:02:50 +0530 Subject: [PATCH 039/190] feat(BarChartUtils): update Variant import path The changes update the import path for the `Variant` type from `../BarChartV2` to `../types`. This change ensures that the `Variant` type is imported from the correct location, making the code more maintainable and easier to understand. feat(MiniBarChart): optimize MiniBarChart component The changes in the `MiniBarChart` component optimize its performance and simplify the code: - Use `useMemo` hooks to memoize the calculation of `dataKeys`, `colors`, and `chartConfig`, improving performance by avoiding unnecessary re-calculations. - Remove the `useEffect` hook and `ResizeObserver` implementation, as the component now uses a fixed size instead of dynamically adjusting to the container size. - Add a new `size` prop to allow users to control the size of the chart. - Use the `useTheme` hook to determine the appropriate color for the internal bar line based on the current theme (light or dark). --- .../BarCharts/BarChartV2/BarChartV2.tsx | 11 +- .../BarCharts/MiniBarChart/MiniBarChart.tsx | 100 +++++++----------- .../stories/MiniBarChart.stories.tsx | 35 +++--- .../{index.tsx => utils/miniBarChartUtils.ts} | 0 .../Charts/BarCharts/types/index.ts | 5 + .../Charts/BarCharts/utils/BarChartUtils.ts | 8 +- .../components/Charts/utils/PalletUtils.ts | 2 + 7 files changed, 66 insertions(+), 95 deletions(-) rename js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/{index.tsx => utils/miniBarChartUtils.ts} (100%) create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 6587b28bb..b3cc4a7d3 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -12,7 +12,8 @@ import { keyTransform, } from "../../Charts"; import { cartesianGrid } from "../../cartesianGrid"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { getDistributedColors, getPalette, type PaletteName } from "../../utils/PalletUtils"; +import { BarChartData, Variant } from "../types"; import { BAR_WIDTH, findNearestSnapPosition, @@ -34,14 +35,10 @@ import { LineInBarShape } from "./components/LineInBarShape"; import { XAxisTick } from "./components/XAxisTick"; import { YAxisTick } from "./components/YAxisTick"; -export type BarChartData = Array>; - -export type Variant = "grouped" | "stacked"; - export interface BarChartPropsV2 { data: T; categoryKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + theme?: PaletteName; variant?: Variant; grid?: boolean; radius?: number; @@ -91,7 +88,7 @@ const BarChartV2Component = ({ }, [theme, dataKeys.length]); const chartConfig: ChartConfig = useMemo(() => { - return getChartConfig(dataKeys, icons, colors); + return getChartConfig(dataKeys, colors, icons); }, [dataKeys, icons, colors]); const chartContainerRef = useRef(null); diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx index f5465cb4f..8ee24083f 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx @@ -1,94 +1,63 @@ -import { useEffect, useRef, useState } from "react"; +import { useMemo } from "react"; import { Bar, BarChart, XAxis } from "recharts"; +import { useTheme } from "../../../ThemeProvider"; import { ChartConfig, ChartContainer, keyTransform } from "../../Charts"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; -import { getPadding, getWidthOfData } from "../utils/BarChartUtils"; +import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; +import { LineInBarShape } from "../BarChartV2/components/LineInBarShape"; +import { type BarChartData } from "../types"; +import { getChartConfig, getDataKeys } from "../utils/BarChartUtils"; -export type MiniBarChartData = Array>; - -export type Variant = "grouped" | "stacked"; - -export interface MiniBarChartProps { +export interface MiniBarChartProps { data: T; categoryKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - variant?: Variant; + theme?: PaletteName; + radius?: number; isAnimationActive?: boolean; onBarsClick?: (data: any) => void; + size?: number | string; } -export const MiniBarChart = ({ +export const MiniBarChart = ({ data, categoryKey, theme = "ocean", - variant = "grouped", - radius = 4, + radius = 1, isAnimationActive = true, onBarsClick, + size = 160, }: MiniBarChartProps) => { // excluding the categoryKey - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + const dataKeys = useMemo(() => { + return getDataKeys(data, categoryKey as string); + }, [data, categoryKey]); - const palette = getPalette(theme); - const colors = getDistributedColors(palette, dataKeys.length); + const colors = useMemo(() => { + const palette = getPalette(theme); + return getDistributedColors(palette, dataKeys.length); + }, [theme, dataKeys.length]); - // Create Config - const chartConfig: ChartConfig = dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - color: colors[index], - }, - }), - {}, - ); + const chartConfig: ChartConfig = useMemo(() => { + return getChartConfig(dataKeys, colors); + }, [dataKeys, colors]); - const chartContainerRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(0); + const { mode } = useTheme(); - useEffect(() => { - if (!chartContainerRef.current) { - return () => {}; + const barInternalLineColor = useMemo(() => { + if (mode === "light") { + return "rgba(255, 255, 255, 0.3)"; } - - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - setContainerWidth(entry.contentRect.width); - } - }); - - resizeObserver.observe(chartContainerRef.current); - - return () => { - resizeObserver.disconnect(); - }; - }, []); - - const padding = getPadding(data, categoryKey as string, containerWidth, variant); - const width = getWidthOfData(data, categoryKey as string, variant); - - // Helper function to calculate total - // const calculateTotal = ( - // data: T, - // categoryKey: keyof T[number], - // ): number => { - // const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); - // return data.reduce((sum, item) => { - // return sum + dataKeys.reduce((keySum, key) => keySum + Number(item[key] || 0), 0); - // }, 0); - // }; + return "rgba(0, 0, 0, 0.3)"; + }, [mode]); return ( - + {dataKeys.map((key) => { const transformedKey = keyTransform(key); const color = `var(--color-${transformedKey})`; @@ -97,10 +66,13 @@ export const MiniBarChart = ({ key={key} dataKey={key} fill={color} - radius={radius} - stackId={variant === "stacked" ? "a" : undefined} + radius={[radius, radius, 0, 0]} isAnimationActive={isAnimationActive} maxBarSize={8} + barSize={8} + shape={ + + } /> ); })} diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx index 2543dae05..8b9f769d1 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx @@ -1,19 +1,25 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Monitor, TabletSmartphone } from "lucide-react"; +import { Monitor } from "lucide-react"; import { Card } from "../../../../Card"; import { MiniBarChart, MiniBarChartProps } from "../MiniBarChart"; const barChartData = [ - { month: "January", desktop: 1500000 }, - { month: "February", desktop: 2800000 }, - { month: "March", desktop: 2200000 }, - { month: "April", desktop: 1800000 }, - { month: "May", desktop: 2500000 }, + { month: "January", desktop: 2347891 }, + { month: "February", desktop: 1893456 }, + { month: "March", desktop: 3456789 }, + { month: "April", desktop: 2987654 }, + { month: "May", desktop: 1765432 }, + { month: "June", desktop: 4321098 }, + { month: "July", desktop: 3789012 }, + { month: "August", desktop: 2654321 }, + { month: "September", desktop: 4123567 }, + { month: "October", desktop: 3234567 }, + { month: "November", desktop: 2876543 }, + { month: "December", desktop: 3987654 }, ]; const icons = { desktop: Monitor, - mobile: TabletSmartphone, } as const; const meta: Meta> = { @@ -59,22 +65,12 @@ const meta: Meta> = { category: "Appearance", }, }, - variant: { - description: - "The style of the bar chart. 'grouped' shows bars side by side, while 'stacked' shows bars stacked on top of each other.", - control: "radio", - options: ["grouped", "stacked"], - table: { - defaultValue: { summary: "grouped" }, - category: "Appearance", - }, - }, radius: { description: "The radius of the rounded corners of the bars", control: "number", table: { type: { summary: "number" }, - defaultValue: { summary: "4" }, + defaultValue: { summary: "2" }, category: "Appearance", }, }, @@ -99,8 +95,7 @@ export const BarChartV3Story: Story = { data: barChartData, categoryKey: "month", theme: "ocean", - variant: "grouped", - radius: 4, + radius: 2, isAnimationActive: true, }, render: (args: MiniBarChartProps) => ( diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/utils/miniBarChartUtils.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.tsx rename to js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/utils/miniBarChartUtils.ts diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts new file mode 100644 index 000000000..fadabeb41 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts @@ -0,0 +1,5 @@ +export type Variant = "grouped" | "stacked"; + +export type BarChartData = Array>; + +export type MiniBarChartData = Array | Array<{ value: number; label?: string }>; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts index a8238ed1e..28956f18f 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts @@ -1,5 +1,5 @@ import { ChartConfig } from "../../Charts"; -import { Variant } from "../BarChartV2"; +import { Variant } from "../types"; export const BAR_WIDTH = 12; @@ -318,21 +318,21 @@ const findNearestSnapPosition = ( /** * This function returns the chart configuration object, used for the chart configuration object of the chart. * @param dataKeys - The data keys for the chart. - * @param icons - The icons for the chart. * @param colors - The colors for the chart. + * @param icons - The icons for the chart (optional). * @returns The chart configuration object for the chart. */ const getChartConfig = ( dataKeys: string[], - icons: Partial>, colors: string[], + icons?: Partial>, ): ChartConfig => { return dataKeys.reduce( (config, key, index) => ({ ...config, [key]: { label: key, - icon: icons[key], + icon: icons?.[key], color: colors[index], secondaryColor: colors[dataKeys.length - index - 1], }, diff --git a/js/packages/react-ui/src/components/Charts/utils/PalletUtils.ts b/js/packages/react-ui/src/components/Charts/utils/PalletUtils.ts index e8898f962..75e92693a 100644 --- a/js/packages/react-ui/src/components/Charts/utils/PalletUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/PalletUtils.ts @@ -6,6 +6,8 @@ export type ColorPalette = { colors: string[]; }; +export type PaletteName = "ocean" | "orchid" | "emerald" | "spectrum" | "sunset" | "vivid"; + type PaletteMap = Record; const colorPalettes: PaletteMap = { From b97b7f6cb6899ff95130c243d70bafa7f9e37430 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 3 Jun 2025 18:58:04 +0530 Subject: [PATCH 040/190] feat(mini-bar-chart): Enhance MiniBarChart component This commit introduces several improvements to the MiniBarChart component: - Adds a ResizeObserver to dynamically adjust the chart size based on the container's width. - Implements a utility function to get the most recent data that fits within the container's width. - Transforms the data to a consistent format for the Recharts library. - Simplifies the chart configuration by using a single color for the 1D chart. - Adds support for a custom bar color and a className prop. - Ensures the chart maintains a 1:1 aspect ratio. These changes aim to improve the responsiveness, performance, and customization options of the MiniBarChart component. --- .../BarCharts/MiniBarChart/MiniBarChart.tsx | 122 ++++++++---- .../BarCharts/MiniBarChart/miniBarChart.scss | 3 + .../stories/MiniBarChart.stories.tsx | 184 +++++++++++++----- .../MiniBarChart/utils/miniBarChartUtils.ts | 97 +++++++++ .../src/components/Charts/charts.scss | 1 + 5 files changed, 314 insertions(+), 93 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/miniBarChart.scss diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx index 8ee24083f..7e21f0e62 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx @@ -1,45 +1,86 @@ -import { useMemo } from "react"; +import clsx from "clsx"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Bar, BarChart, XAxis } from "recharts"; import { useTheme } from "../../../ThemeProvider"; -import { ChartConfig, ChartContainer, keyTransform } from "../../Charts"; +import { ChartConfig, ChartContainer } from "../../Charts"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; import { LineInBarShape } from "../BarChartV2/components/LineInBarShape"; -import { type BarChartData } from "../types"; -import { getChartConfig, getDataKeys } from "../utils/BarChartUtils"; +import { type MiniBarChartData } from "../types"; +import { + getPadding, + getRecentDataThatFits, + MINI_BAR_WIDTH, + transformDataForChart, +} from "./utils/miniBarChartUtils"; -export interface MiniBarChartProps { - data: T; - categoryKey: keyof T[number]; +export interface MiniBarChartProps { + data: MiniBarChartData; theme?: PaletteName; - radius?: number; isAnimationActive?: boolean; onBarsClick?: (data: any) => void; size?: number | string; + className?: string; + barColor?: string; } -export const MiniBarChart = ({ +const MINI_BAR_CHART_INNER_LINE_WIDTH = 1; + +export const MiniBarChart = ({ data, - categoryKey, theme = "ocean", radius = 1, isAnimationActive = true, onBarsClick, - size = 160, -}: MiniBarChartProps) => { - // excluding the categoryKey - const dataKeys = useMemo(() => { - return getDataKeys(data, categoryKey as string); - }, [data, categoryKey]); + size = "100%", + className, + barColor, +}: MiniBarChartProps) => { + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + + useEffect(() => { + if (!containerRef.current) { + return () => {}; + } + + const resizeObserver = new ResizeObserver((entries) => { + // there is only one entry in the entries array because we are only observing the chart container + for (const entry of entries) { + setContainerWidth(entry.contentRect.width); + } + }); + + resizeObserver.observe(containerRef.current); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + // Get the most recent data that fits in the container + const filteredData = useMemo(() => { + return getRecentDataThatFits(data, containerWidth); + }, [data, containerWidth]); + + // Transform the filtered data to a consistent format for recharts + const chartData = useMemo(() => { + return transformDataForChart(filteredData); + }, [filteredData]); const colors = useMemo(() => { const palette = getPalette(theme); - return getDistributedColors(palette, dataKeys.length); - }, [theme, dataKeys.length]); + return getDistributedColors(palette, 1); // Single color for 1D chart + }, [theme]); const chartConfig: ChartConfig = useMemo(() => { - return getChartConfig(dataKeys, colors); - }, [dataKeys, colors]); + return { + value: { + label: "Value", + color: barColor ? barColor : colors[0], + }, + }; + }, [colors, barColor]); const { mode } = useTheme(); @@ -53,29 +94,30 @@ export const MiniBarChart = ({ return ( - - - {dataKeys.map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - return ( - - } + + + - ); - })} + } + /> ); diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/miniBarChart.scss b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/miniBarChart.scss new file mode 100644 index 000000000..d395a1ecb --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/miniBarChart.scss @@ -0,0 +1,3 @@ +.crayon-charts-mini-bar-chart-container { + padding: 4px; +} diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx index 8b9f769d1..967e6d2a0 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx @@ -3,26 +3,36 @@ import { Monitor } from "lucide-react"; import { Card } from "../../../../Card"; import { MiniBarChart, MiniBarChartProps } from "../MiniBarChart"; -const barChartData = [ - { month: "January", desktop: 2347891 }, - { month: "February", desktop: 1893456 }, - { month: "March", desktop: 3456789 }, - { month: "April", desktop: 2987654 }, - { month: "May", desktop: 1765432 }, - { month: "June", desktop: 4321098 }, - { month: "July", desktop: 3789012 }, - { month: "August", desktop: 2654321 }, - { month: "September", desktop: 4123567 }, - { month: "October", desktop: 3234567 }, - { month: "November", desktop: 2876543 }, - { month: "December", desktop: 3987654 }, +// Simple array of numbers for 1D bar chart +const simpleBarChartData = [ + 1, 1893456, 3456789, 2987654, 1765432, 4321098, 3789012, 2654321, 4123567, 3234567, 2876543, + 3987654, 2347891, 1893456, 3456789, 2987654, 1765432, 4321098, 3789012, 2654321, 4123567, 3234567, + 2876543, 3987654, 2347891, 1893456, 3456789, 2987654, 1765432, 4321098, 3789012, 2654321, 4123567, + 3234567, 2876543, 3987654, 2347891, 1893456, 3456789, 2987654, 1765432, 4321098, 3789012, 2654321, + 4123567, 3234567, 2876543, 100000, +]; + +// Array of objects with value and label +const labeledBarChartData = [ + { value: 2347891, label: "January" }, + { value: 1893456, label: "February" }, + { value: 3456789, label: "March" }, + { value: 2987654, label: "April" }, + { value: 1765432, label: "May" }, + { value: 4321098, label: "June" }, + { value: 3789012, label: "July" }, + { value: 2654321, label: "August" }, + { value: 4123567, label: "September" }, + { value: 3234567, label: "October" }, + { value: 2876543, label: "November" }, + { value: 3987654, label: "December" }, ]; const icons = { desktop: Monitor, } as const; -const meta: Meta> = { +const meta: Meta = { title: "Components/Charts/BarCharts/MiniBarChart", component: MiniBarChart, parameters: { @@ -38,23 +48,14 @@ const meta: Meta> = { argTypes: { data: { description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the bars to be plotted.", + "An array of numbers or an array of objects with value and optional label. Each entry represents a single bar in the chart.", control: false, table: { - type: { summary: "Array>" }, + type: { summary: "Array | Array<{ value: number; label?: string }>" }, defaultValue: { summary: "[]" }, category: "Data", }, }, - categoryKey: { - description: - "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", - control: false, - table: { - type: { summary: "keyof T[number]" }, - category: "Data", - }, - }, theme: { description: "The color palette theme for the chart. Each theme provides a different set of colors for the bars.", @@ -70,7 +71,7 @@ const meta: Meta> = { control: "number", table: { type: { summary: "number" }, - defaultValue: { summary: "2" }, + defaultValue: { summary: "1" }, category: "Appearance", }, }, @@ -83,23 +84,35 @@ const meta: Meta> = { category: "Display", }, }, + size: { + description: "The width and height of the chart", + control: "number", + table: { + type: { summary: "number | string" }, + defaultValue: { summary: "160" }, + category: "Appearance", + }, + }, }, } satisfies Meta; export default meta; type Story = StoryObj; -export const BarChartV3Story: Story = { - name: "Bar Chart V3", +export const SimpleNumberArray: Story = { + name: "Simple Number Array", args: { - data: barChartData, - categoryKey: "month", + data: simpleBarChartData, theme: "ocean", radius: 2, isAnimationActive: true, + size: "100%", }, - render: (args: MiniBarChartProps) => ( - + render: (args: MiniBarChartProps) => ( + +

+ Monthly Sales Data +

), @@ -107,31 +120,96 @@ export const BarChartV3Story: Story = { docs: { source: { code: ` -const barChartData = [ - { month: "January", desktop: 1500000 }, - { month: "February", desktop: 2800000 }, - { month: "March", desktop: 2200000 }, - { month: "April", desktop: 1800000 }, - { month: "May", desktop: 2500000 }, +const salesData = [2347891, 1893456, 3456789, 2987654, 1765432, 4321098]; + + +`, + }, + }, + }, +}; + +export const LabeledData: Story = { + name: "Labeled Data", + args: { + data: labeledBarChartData, + theme: "emerald", + radius: 1, + isAnimationActive: true, + size: 200, + }, + render: (args: MiniBarChartProps) => ( + +

Monthly Revenue

+ +
+ ), + parameters: { + docs: { + source: { + code: ` +const revenueData = [ + { value: 2347891, label: "January" }, + { value: 1893456, label: "February" }, + { value: 3456789, label: "March" }, + // ... ]; - - - + `, }, }, }, }; + +export const SmallSize: Story = { + name: "Small Size", + args: { + data: [10, 20, 15, 30, 25, 35, 18], + theme: "sunset", + radius: 1, + isAnimationActive: true, + size: 120, + }, + render: (args: MiniBarChartProps) => ( + +

Weekly Stats

+ +
+ ), +}; + +export const DifferentThemes: Story = { + name: "Different Themes", + render: () => ( +
+ {(["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"] as const).map((theme) => ( + +

+ {theme} Theme +

+ +
+ ))} +
+ ), +}; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/utils/miniBarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/utils/miniBarChartUtils.ts index e69de29bb..ee839b1c6 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/utils/miniBarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/utils/miniBarChartUtils.ts @@ -0,0 +1,97 @@ +import { type MiniBarChartData } from "../../types"; + +export const MINI_BAR_WIDTH: number = 8; +export const MINI_ELEMENT_SPACING: number = 10; + +const CONTAINER_HORIZONTAL_PADDING: number = 8; // 4px left + 4px right + +/** + * Calculates the total width of the data. + * + * @param data - The mini bar chart data array + * @returns The total width needed in pixels to display all data items + */ +const getWidthOfData = (data: MiniBarChartData) => { + return data.length * (MINI_ELEMENT_SPACING + MINI_BAR_WIDTH); +}; + +/** + * Calculates the left and right padding for the chart container based on available space. + * If the chart data exceeds the container width, no padding is applied. + * + * @param data - The mini bar chart data array + * @param containerWidth - The total width of the container in pixels + * @returns An object with left and right padding values in pixels + */ +const getPadding = (data: MiniBarChartData, containerWidth: number) => { + const availableWidth = containerWidth - CONTAINER_HORIZONTAL_PADDING; + const chartWidth = getWidthOfData(data); + const paddingValue = availableWidth - chartWidth; + + if (paddingValue < 0) { + return { + left: 0, + right: 0, + }; + } + return { + left: paddingValue, + right: 0, + }; +}; + +/** + * Filters the data to include only the most recent items that can fit within the container width. + * This function ensures the chart displays the latest data when space is limited. + * + * @param data - The complete mini bar chart data array + * @param containerWidth - The total width of the container in pixels + * @returns A filtered array containing only the most recent data items that fit in the container + */ +const getRecentDataThatFits = ( + data: MiniBarChartData, + containerWidth: number, +): MiniBarChartData => { + if (containerWidth <= 0 || data.length === 0) { + return data; + } + + // Subtract padding to get actual available width for chart content + const availableWidth = containerWidth - CONTAINER_HORIZONTAL_PADDING; + + // Calculate how many items can fit in the available space + const itemWidth = MINI_BAR_WIDTH + MINI_ELEMENT_SPACING; + const maxItems = Math.floor(availableWidth / itemWidth); + + // If all items fit, return all data + if (maxItems >= data.length) { + return data; + } + + // Return the most recent items that fit + return data.slice(-maxItems); +}; + +type ChartData = Array<{ + value: number; + label: string; +}>; + +/** + * Transforms the mini bar chart data into a standardized format for rendering. + * Handles both numeric values and objects with value/label properties. + * + * @param data - The mini bar chart data array (can contain numbers or objects with value/label) + * @returns An array of chart data objects with value and label properties + */ +const transformDataForChart = (data: MiniBarChartData): ChartData => { + return data.map((item, index) => { + if (typeof item === "number") { + return { value: item, label: `Item ${index + 1}` }; + } else { + return { value: item.value, label: item.label || `Item ${index + 1}` }; + } + }); +}; + +export { getPadding, getRecentDataThatFits, transformDataForChart }; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index e69e7b522..9fb1f821f 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -4,6 +4,7 @@ @forward "./BarCharts/BarChartV2/components/xAxisTick.scss"; @forward "./BarCharts/BarChartV2/components/defaultLegend.scss"; @forward "./BarCharts/BarChartV2/components/CustomCursor.scss"; +@forward "./BarCharts/MiniBarChart/miniBarChart.scss"; .crayon-chart { // Container styles From 2f72f9012a2ea03cfc485c31128c71be105912e7 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 9 Jun 2025 14:34:23 +0530 Subject: [PATCH 041/190] feat(AreaChartV2): add new area chart component The changes introduce a new `AreaChartV2` component in the `react-ui` package. This component provides an enhanced area chart visualization with the following features: - Supports multiple data series with customizable icons and themes - Offers various interpolation methods (linear, natural, step) for the area curves - Includes options to control the display of grid, labels, legend, and animation - Allows for the configuration of x-axis and y-axis labels These changes aim to expand the charting capabilities of the `react-ui` library, providing developers with a more flexible and feature-rich area chart component to use in their applications. --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 241 ++++++++++++++ .../Charts/AreaCharts/AreaChartV2/index.ts | 1 + .../stories/areaChartV2.stories.tsx | 311 ++++++++++++++++++ .../BarCharts/BarChartV2/BarChartV2.tsx | 6 +- .../Charts/BarCharts/utils/BarChartUtils.ts | 7 +- .../PieCharts/PieChartV2/PieChartV2.tsx | 34 +- .../src/components/Charts/charts.scss | 7 +- .../DefaultLegend}/DefaultLegend.tsx | 2 +- .../DefaultLegend}/defaultLegend.scss | 2 +- .../XAxisTick}/XAxisTick.tsx | 0 .../XAxisTick}/xAxisTick.scss | 2 +- .../YAxisTick}/YAxisTick.tsx | 0 .../YAxisTick}/yAxisTick.scss | 2 +- .../src/components/Charts/shared/index.ts | 3 + .../src/components/Charts/types/Legend.ts | 6 + .../src/components/Charts/types/index.ts | 1 + 16 files changed, 589 insertions(+), 36 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx rename js/packages/react-ui/src/components/Charts/{BarCharts/BarChartV2/components => shared/DefaultLegend}/DefaultLegend.tsx (96%) rename js/packages/react-ui/src/components/Charts/{BarCharts/BarChartV2/components => shared/DefaultLegend}/defaultLegend.scss (96%) rename js/packages/react-ui/src/components/Charts/{BarCharts/BarChartV2/components => shared/XAxisTick}/XAxisTick.tsx (100%) rename js/packages/react-ui/src/components/Charts/{BarCharts/BarChartV2/components => shared/XAxisTick}/xAxisTick.scss (63%) rename js/packages/react-ui/src/components/Charts/{BarCharts/BarChartV2/components => shared/YAxisTick}/YAxisTick.tsx (100%) rename js/packages/react-ui/src/components/Charts/{BarCharts/BarChartV2/components => shared/YAxisTick}/yAxisTick.scss (71%) create mode 100644 js/packages/react-ui/src/components/Charts/shared/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/types/Legend.ts create mode 100644 js/packages/react-ui/src/components/Charts/types/index.ts diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx new file mode 100644 index 000000000..d4536e88b --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -0,0 +1,241 @@ +import React from "react"; +import { Area, LabelList, AreaChart as RechartsAreaChart, XAxis, YAxis } from "recharts"; +import { useLayoutContext } from "../../../../context/LayoutContext"; +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, + keyTransform, +} from "../../Charts"; +import { cartesianGrid } from "../../cartesianGrid"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; + +export type AreaChartV2Data = Array>; + +export interface AreaChartV2Props { + data: T; + categoryKey: keyof T[number]; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + variant?: "linear" | "natural" | "step"; + grid?: boolean; + label?: boolean; + legend?: boolean; + opacity?: number; + icons?: Partial>; + isAnimationActive?: boolean; + showYAxis?: boolean; + xAxisLabel?: React.ReactNode; + yAxisLabel?: React.ReactNode; +} + +export const AreaChartV2 = ({ + data, + categoryKey, + theme = "ocean", + variant = "natural", + grid = true, + label = true, + legend = true, + opacity = 0.5, + icons = {}, + isAnimationActive = true, + showYAxis = false, + xAxisLabel, + yAxisLabel, +}: AreaChartV2Props) => { + // excluding the categoryKey + const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + + const palette = getPalette(theme); + const colors = getDistributedColors(palette, dataKeys.length); + const { layout } = useLayoutContext(); + + // Create Config + const chartConfig: ChartConfig = dataKeys.reduce( + (config, key, index) => ({ + ...config, + [key]: { + label: key, + icon: icons[key], + color: colors[index], + }, + }), + {}, + ); + + const getTickFormatter = (data: T) => { + const dataLength = data.length; + const maxLengthMap = { + mobile: { + default: 5, + 10: 4, + 11: 4, + }, + tray: { + default: 5, + 8: 4, + 9: 4, + 10: 4, + 11: 4, + }, + copilot: { + default: 5, + 8: 4, + 9: 4, + 10: 4, + 11: 4, + }, + fullscreen: { + default: 5, + 11: 4, + }, + }; + + const layoutConfig = + maxLengthMap[layout as keyof typeof maxLengthMap] || maxLengthMap.fullscreen; + + const maxLength = + dataLength >= 11 + ? 4 + : layoutConfig[dataLength as keyof typeof layoutConfig] || layoutConfig.default; + + return (value: string) => { + if (value.length > maxLength) { + return `${value.slice(0, maxLength)}...`; + } + return value; + }; + }; + + const getAxisAngle = (data: T) => { + const angleConfig = { + mobile: { + default: 0, + ranges: [ + { min: 6, max: 9, angle: -45 }, + { min: 10, max: 10, angle: -60 }, + { min: 11, max: Infinity, angle: -75 }, + ], + }, + tray: { + default: 0, + ranges: [{ min: 8, max: Infinity, angle: -45 }], + }, + copilot: { + default: 0, + ranges: [{ min: 8, max: Infinity, angle: -45 }], + }, + fullscreen: { + default: 0, + ranges: [{ min: 12, max: Infinity, angle: -45 }], + }, + }; + + const layoutConfig = angleConfig[layout as keyof typeof angleConfig] || angleConfig.fullscreen; + const dataLength = data.length; + + const matchRange = layoutConfig.ranges.find( + (range) => dataLength >= range.min && dataLength <= range.max, + ); + + return matchRange?.angle ?? layoutConfig.default; + }; + + const getTickMargin = (data: T) => { + return data.length <= 6 ? 10 : 15; + }; + + // Add safety check for empty data + if (!data || data.length === 0) { + return ( + +
No data available
+
+ ); + } + + return ( + + + {grid && cartesianGrid()} + + {showYAxis && ( + + )} + + } /> + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + + return ( + + {label && ( + + )} + + ); + })} + + + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/index.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/index.ts new file mode 100644 index 000000000..f60bd5c5c --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/index.ts @@ -0,0 +1 @@ +export * from "./AreaChartV2"; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx new file mode 100644 index 000000000..78de64f9b --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -0,0 +1,311 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Monitor, TabletSmartphone } from "lucide-react"; +import { Card } from "../../../../Card"; +import { AreaChartV2, AreaChartV2Props } from "../AreaChartV2"; + +const areaChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, +]; + +const icons = { + desktop: Monitor, + mobile: TabletSmartphone, +} as const; + +const meta: Meta> = { + title: "Components/Charts/AreaCharts/AreaChartV2", + component: AreaChartV2, + parameters: { + layout: "centered", + docs: { + description: { + component: + "```tsx\nimport { AreaChartV2 } from '@crayon-ui/react-ui/Charts/AreaChartV2';\n```", + }, + }, + }, + tags: ["!dev", "autodocs"], + argTypes: { + data: { + description: + "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", + control: false, + table: { + type: { summary: "Array>" }, + defaultValue: { summary: "[]" }, + category: "Data", + }, + }, + categoryKey: { + description: + "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", + control: false, + table: { + type: { summary: "string" }, + defaultValue: { summary: "string" }, + category: "Data", + }, + }, + theme: { + description: + "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + icons: { + description: + "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", + control: false, + table: { + type: { summary: "Record" }, + defaultValue: { summary: "{}" }, + category: "Appearance", + }, + }, + variant: { + description: + "The interpolation method used to create the area curves. 'linear' creates straight lines between points, 'natural' creates smooth curves, and 'step' creates a stepped area.", + control: "radio", + options: ["linear", "natural", "step"], + table: { + defaultValue: { summary: "natural" }, + category: "Appearance", + }, + }, + opacity: { + description: + "The opacity of the filled area beneath each line (0 = fully transparent, 1 = fully opaque)", + control: false, + table: { + type: { summary: "number" }, + defaultValue: { summary: "0.5" }, + category: "Appearance", + }, + }, + grid: { + description: "Whether to display the background grid lines in the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + label: { + description: "Whether to display data point labels above each point on the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + legend: { + description: + "Whether to display the chart legend showing the data series names and their corresponding colors/icons", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + isAnimationActive: { + description: "Whether to animate the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + showYAxis: { + description: "Whether to display the y-axis", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "Display", + }, + }, + xAxisLabel: { + description: "The label for the x-axis", + control: false, + table: { + type: { summary: "string" }, + defaultValue: { summary: "string" }, + category: "Data", + }, + }, + yAxisLabel: { + description: "The label for the y-axis", + control: false, + table: { + type: { summary: "string" }, + defaultValue: { summary: "string" }, + category: "Data", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const AreaChartV2Story: Story = { + name: "Area Chart V2", + args: { + data: areaChartData, + categoryKey: "month", + theme: "ocean", + variant: "linear", + opacity: 0.5, + grid: true, + legend: true, + label: true, + isAnimationActive: true, + showYAxis: false, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` +const areaChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, +]; + + + +`, + }, + }, + }, +}; + +export const AreaChartV2StoryWithIcons: Story = { + name: "Area Chart V2 with Icons", + args: { + ...AreaChartV2Story.args, + icons: icons, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` + import { Monitor, TabletSmartphone } from "lucide-react"; + + const areaChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, + ]; + + const icons = { + desktop: Monitor, + mobile: TabletSmartphone, + }; + + + + `, + }, + }, + }, +}; + +export const AreaChartV2StoryWithYAxis: Story = { + name: "Area Chart V2 with Y-Axis and Axis Labels", + args: { + ...AreaChartV2Story.args, + showYAxis: true, + xAxisLabel: "Time Period", + yAxisLabel: "Number of Users", + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + source: { + code: ` + import { Monitor, TabletSmartphone } from "lucide-react"; + + const areaChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, + ]; + + + + `, + }, + }, + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index b3cc4a7d3..f72fcbec1 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -12,6 +12,8 @@ import { keyTransform, } from "../../Charts"; import { cartesianGrid } from "../../cartesianGrid"; +import { DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; +import { type LegendItem } from "../../types"; import { getDistributedColors, getPalette, type PaletteName } from "../../utils/PalletUtils"; import { BarChartData, Variant } from "../types"; import { @@ -27,13 +29,9 @@ import { getSnapPositions, getWidthOfData, getYAxisTickFormatter, - type LegendItem, } from "../utils/BarChartUtils"; import { SimpleCursor } from "./components/CustomCursor"; -import { DefaultLegend } from "./components/DefaultLegend"; import { LineInBarShape } from "./components/LineInBarShape"; -import { XAxisTick } from "./components/XAxisTick"; -import { YAxisTick } from "./components/YAxisTick"; export interface BarChartPropsV2 { data: T; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts index 28956f18f..941f0708a 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts @@ -1,4 +1,5 @@ import { ChartConfig } from "../../Charts"; +import { LegendItem } from "../../types"; import { Variant } from "../types"; export const BAR_WIDTH = 12; @@ -348,12 +349,6 @@ const getChartConfig = ( * @param icons - The icons for the chart. * @returns The legend items for the chart. */ -export interface LegendItem { - key: string; - label: string; - color: string; - icon?: React.ComponentType; -} const getLegendItems = ( dataKeys: string[], diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index f8401bb75..5941ccac4 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -3,18 +3,11 @@ import { debounce } from "lodash-es"; import { useEffect, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; import { useLayoutContext } from "../../../../context/LayoutContext"; -import { - ChartContainer, - ChartLegend, - ChartLegendContent, - ChartTooltip, - ChartTooltipContent, -} from "../../Charts"; +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; import { createGradientDefinitions, renderActiveShape, - renderCustomLabel, renderCustomLabelLine, } from "../components/PieChartRenderers"; import { @@ -120,10 +113,13 @@ export const PieChartV2 = ({ ref={containerRef} config={chartConfig} className={clsx("crayon-pie-chart-container", layoutMap[layout])} + rechartsProps={{ + aspect: 1, + }} > } /> - {legend && } />} + {gradientDefinitions} ({ labelLine={label && activeIndex === null ? (renderCustomLabelLine as any) : false} outerRadius={calculatedOuterRadius} innerRadius={calculatedInnerRadius} - label={ - activeIndex === null - ? (props) => - renderCustomLabel( - { ...props, labelDistance: 1 }, - format === "percentage" ? "percentage" : String(dataKey), - format, - ) - : false - } + // label={ + // activeIndex === null + // ? (props) => + // renderCustomLabel( + // { ...props, labelDistance: 1 }, + // format === "percentage" ? "percentage" : String(dataKey), + // format, + // ) + // : false + // } activeShape={(props: any) => renderActiveShape(props as any)} activeIndex={activeIndex ?? undefined} {...animationConfig} diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index 9fb1f821f..242a5b90f 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -1,11 +1,12 @@ @use "../../cssUtils" as cssUtils; @forward "./BarCharts/BarChartV2/barChar.scss"; -@forward "./BarCharts/BarChartV2/components/yAxisTick.scss"; -@forward "./BarCharts/BarChartV2/components/xAxisTick.scss"; -@forward "./BarCharts/BarChartV2/components/defaultLegend.scss"; @forward "./BarCharts/BarChartV2/components/CustomCursor.scss"; @forward "./BarCharts/MiniBarChart/miniBarChart.scss"; +@forward "./shared/XAxisTick/xAxisTick.scss"; +@forward "./shared/DefaultLegend/defaultLegend.scss"; +@forward "./shared/YAxisTick/yAxisTick.scss"; + .crayon-chart { // Container styles &-container { diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/DefaultLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx similarity index 96% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/DefaultLegend.tsx rename to js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx index e1f121cc2..89bcbeeda 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/DefaultLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx @@ -1,6 +1,6 @@ import clsx from "clsx"; import React from "react"; -import { type LegendItem } from "../../utils/BarChartUtils"; +import { type LegendItem } from "../../types"; interface DefaultLegendProps { items: LegendItem[]; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/defaultLegend.scss b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss similarity index 96% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/defaultLegend.scss rename to js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss index 7dac9480d..5c01736b5 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/defaultLegend.scss +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss @@ -1,4 +1,4 @@ -@use "../../../../../cssUtils" as cssUtils; +@use "../../../../cssUtils" as cssUtils; .crayon-chart-legend-container { display: flex; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/XAxisTick.tsx rename to js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/xAxisTick.scss b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/xAxisTick.scss similarity index 63% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/xAxisTick.scss rename to js/packages/react-ui/src/components/Charts/shared/XAxisTick/xAxisTick.scss index 632f66a01..69689a6fd 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/xAxisTick.scss +++ b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/xAxisTick.scss @@ -1,4 +1,4 @@ -@use "../../../../../cssUtils" as cssUtils; +@use "../../../../cssUtils" as cssUtils; .crayon-chart-x-axis-tick { @include cssUtils.typography(label, small); diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/YAxisTick.tsx b/js/packages/react-ui/src/components/Charts/shared/YAxisTick/YAxisTick.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/YAxisTick.tsx rename to js/packages/react-ui/src/components/Charts/shared/YAxisTick/YAxisTick.tsx diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/yAxisTick.scss b/js/packages/react-ui/src/components/Charts/shared/YAxisTick/yAxisTick.scss similarity index 71% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/yAxisTick.scss rename to js/packages/react-ui/src/components/Charts/shared/YAxisTick/yAxisTick.scss index 7c5435e1c..3e6474d05 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/yAxisTick.scss +++ b/js/packages/react-ui/src/components/Charts/shared/YAxisTick/yAxisTick.scss @@ -1,4 +1,4 @@ -@use "../../../../../cssUtils" as cssUtils; +@use "../../../../cssUtils" as cssUtils; .crayon-chart-y-axis-tick { @include cssUtils.typography(label, small); diff --git a/js/packages/react-ui/src/components/Charts/shared/index.ts b/js/packages/react-ui/src/components/Charts/shared/index.ts new file mode 100644 index 000000000..46396a532 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/index.ts @@ -0,0 +1,3 @@ +export * from "./DefaultLegend/DefaultLegend"; +export * from "./XAxisTick/XAxisTick"; +export * from "./YAxisTick/YAxisTick"; diff --git a/js/packages/react-ui/src/components/Charts/types/Legend.ts b/js/packages/react-ui/src/components/Charts/types/Legend.ts new file mode 100644 index 000000000..75fd94384 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/types/Legend.ts @@ -0,0 +1,6 @@ +export interface LegendItem { + key: string; + label: string; + color: string; + icon?: React.ComponentType; +} diff --git a/js/packages/react-ui/src/components/Charts/types/index.ts b/js/packages/react-ui/src/components/Charts/types/index.ts new file mode 100644 index 000000000..a39cb6790 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/types/index.ts @@ -0,0 +1 @@ +export * from "./Legend"; From a5f3bb180eaf5147d719196ffbc60f56fae9c9cf Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 9 Jun 2025 15:54:20 +0530 Subject: [PATCH 042/190] mostly orginazise code --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 95 ++++------------ .../stories/areaChartV2.stories.tsx | 101 +----------------- .../BarCharts/BarChartV2/BarChartV2.tsx | 3 +- .../Charts/BarCharts/utils/BarChartUtils.ts | 42 -------- .../src/components/Charts/utils/dataUtils.ts | 40 +++++++ 5 files changed, 61 insertions(+), 220 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/utils/dataUtils.ts diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index d4536e88b..dc13044fe 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { Area, LabelList, AreaChart as RechartsAreaChart, XAxis, YAxis } from "recharts"; +import React, { useMemo } from "react"; +import { Area, AreaChart as RechartsAreaChart, XAxis, YAxis } from "recharts"; import { useLayoutContext } from "../../../../context/LayoutContext"; import { ChartConfig, @@ -9,14 +9,15 @@ import { keyTransform, } from "../../Charts"; import { cartesianGrid } from "../../cartesianGrid"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; +import { getChartConfig, getDataKeys } from "../../utils/dataUtils"; export type AreaChartV2Data = Array>; export interface AreaChartV2Props { data: T; categoryKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + theme?: PaletteName; variant?: "linear" | "natural" | "step"; grid?: boolean; label?: boolean; @@ -44,69 +45,20 @@ export const AreaChartV2 = ({ xAxisLabel, yAxisLabel, }: AreaChartV2Props) => { - // excluding the categoryKey - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + const dataKeys = useMemo(() => { + return getDataKeys(data, categoryKey as string); + }, [data, categoryKey]); - const palette = getPalette(theme); - const colors = getDistributedColors(palette, dataKeys.length); - const { layout } = useLayoutContext(); + const colors = useMemo(() => { + const palette = getPalette(theme); + return getDistributedColors(palette, dataKeys.length); + }, [theme, dataKeys.length]); - // Create Config - const chartConfig: ChartConfig = dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - icon: icons[key], - color: colors[index], - }, - }), - {}, - ); - - const getTickFormatter = (data: T) => { - const dataLength = data.length; - const maxLengthMap = { - mobile: { - default: 5, - 10: 4, - 11: 4, - }, - tray: { - default: 5, - 8: 4, - 9: 4, - 10: 4, - 11: 4, - }, - copilot: { - default: 5, - 8: 4, - 9: 4, - 10: 4, - 11: 4, - }, - fullscreen: { - default: 5, - 11: 4, - }, - }; - - const layoutConfig = - maxLengthMap[layout as keyof typeof maxLengthMap] || maxLengthMap.fullscreen; - - const maxLength = - dataLength >= 11 - ? 4 - : layoutConfig[dataLength as keyof typeof layoutConfig] || layoutConfig.default; + const { layout } = useLayoutContext(); - return (value: string) => { - if (value.length > maxLength) { - return `${value.slice(0, maxLength)}...`; - } - return value; - }; - }; + const chartConfig: ChartConfig = useMemo(() => { + return getChartConfig(dataKeys, colors, icons); + }, [dataKeys, icons, colors]); const getAxisAngle = (data: T) => { const angleConfig = { @@ -172,9 +124,9 @@ export const AreaChartV2 = ({ tickLine={false} tickMargin={getTickMargin(data)} axisLine={false} - angle={getAxisAngle(data)} + // angle={getAxisAngle(data)} textAnchor="middle" - tickFormatter={getTickFormatter(data)} + // tickFormatter={getTickFormatter(data)} interval="preserveStartEnd" label={ xAxisLabel @@ -223,16 +175,7 @@ export const AreaChartV2 = ({ r: 6, }} isAnimationActive={isAnimationActive} - > - {label && ( - - )} - + /> ); })} diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx index 78de64f9b..a45583cbd 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -29,7 +29,7 @@ const meta: Meta> = { }, }, }, - tags: ["!dev", "autodocs"], + tags: ["dev", "autodocs"], argTypes: { data: { description: @@ -210,102 +210,3 @@ const areaChartData = [ }, }, }; - -export const AreaChartV2StoryWithIcons: Story = { - name: "Area Chart V2 with Icons", - args: { - ...AreaChartV2Story.args, - icons: icons, - }, - render: (args) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` - import { Monitor, TabletSmartphone } from "lucide-react"; - - const areaChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, - ]; - - const icons = { - desktop: Monitor, - mobile: TabletSmartphone, - }; - - - - `, - }, - }, - }, -}; - -export const AreaChartV2StoryWithYAxis: Story = { - name: "Area Chart V2 with Y-Axis and Axis Labels", - args: { - ...AreaChartV2Story.args, - showYAxis: true, - xAxisLabel: "Time Period", - yAxisLabel: "Number of Users", - }, - render: (args) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` - import { Monitor, TabletSmartphone } from "lucide-react"; - - const areaChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, - ]; - - - - `, - }, - }, - }, -}; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index f72fcbec1..7119602f0 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -15,13 +15,12 @@ import { cartesianGrid } from "../../cartesianGrid"; import { DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; import { type LegendItem } from "../../types"; import { getDistributedColors, getPalette, type PaletteName } from "../../utils/PalletUtils"; +import { getChartConfig, getDataKeys } from "../../utils/dataUtils"; import { BarChartData, Variant } from "../types"; import { BAR_WIDTH, findNearestSnapPosition, - getChartConfig, getChartHeight, - getDataKeys, getLegendItems, getOptimalXAxisTickFormatter, getPadding, diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts index 941f0708a..168ea3364 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts @@ -1,4 +1,3 @@ -import { ChartConfig } from "../../Charts"; import { LegendItem } from "../../types"; import { Variant } from "../types"; @@ -246,19 +245,6 @@ const getWidthOfGroup = ( } }; -/** - * 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. - * @param categoryKey - The key of the category to be displayed in the chart. - * @returns The data keys for the chart. - */ -const getDataKeys = ( - data: Array>, - categoryKey: string, -): string[] => { - return Object.keys(data[0] || {}).filter((key) => key !== categoryKey); -}; - /** * This function returns the snap positions for the chart, used for the snap positions of the chart. * @param data - The data to be displayed in the chart. @@ -316,32 +302,6 @@ const findNearestSnapPosition = ( } }; -/** - * This function returns the chart configuration object, used for the chart configuration object of the chart. - * @param dataKeys - The data keys for the chart. - * @param colors - The colors for the chart. - * @param icons - The icons for the chart (optional). - * @returns The chart configuration object for the chart. - */ -const getChartConfig = ( - dataKeys: string[], - colors: string[], - icons?: Partial>, -): ChartConfig => { - return dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - icon: icons?.[key], - color: colors[index], - secondaryColor: colors[dataKeys.length - index - 1], - }, - }), - {}, - ); -}; - /** * This function returns the legend items for the chart, used for the legend items of the chart. * @param dataKeys - The data keys for the chart. @@ -376,9 +336,7 @@ const getChartHeight = (containerWidth: number): number => { export { findNearestSnapPosition, - getChartConfig, getChartHeight, - getDataKeys, getLegendItems, getOptimalXAxisTickFormatter, getPadding, diff --git a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts new file mode 100644 index 000000000..86a910fa2 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts @@ -0,0 +1,40 @@ +import { ChartConfig } from "../Charts"; + +/** + * 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. + * @param categoryKey - The key of the category to be displayed in the chart. + * @returns The data keys for the chart. + */ +export const getDataKeys = ( + data: Array>, + categoryKey: string, +): string[] => { + return Object.keys(data[0] || {}).filter((key) => key !== categoryKey); +}; + +/** + * This function returns the chart configuration object, used for the chart configuration object of the chart. + * @param dataKeys - The data keys for the chart. + * @param colors - The colors for the chart. + * @param icons - The icons for the chart (optional). + * @returns The chart configuration object for the chart. + */ +export const getChartConfig = ( + dataKeys: string[], + colors: string[], + icons?: Partial>, +): ChartConfig => { + return dataKeys.reduce( + (config, key, index) => ({ + ...config, + [key]: { + label: key, + icon: icons?.[key], + color: colors[index], + secondaryColor: colors[dataKeys.length - index - 1], + }, + }), + {}, + ); +}; From 905b39d6bb0dad262d76ddb505901bd332c25951 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 9 Jun 2025 17:44:28 +0530 Subject: [PATCH 043/190] feat(BarChartV2): add smartwatch data to the large dataset The changes add a new data point for "smartwatch" to the large dataset in the `barChartV2.stories.tsx` file. This allows the BarChartV2 component to display data for smartwatches in addition to the existing device types (desktop, mobile, tablet, laptop, TV, and watch). --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 126 +++-------- .../stories/areaChartV2.stories.tsx | 24 +-- .../MiniAreaChart/MiniAreaChart.tsx | 3 +- .../Charts/AreaCharts/types/index.ts | 5 + .../Charts/AreaCharts/utils/AreaChartUtils.ts | 3 +- .../BarCharts/BarChartV2/BarChartV2.tsx | 6 +- .../BarChartV2/stories/barChartV2.stories.tsx | 198 ++++++++++++++++-- .../Charts/BarCharts/types/index.ts | 2 +- .../Charts/BarCharts/utils/BarChartUtils.ts | 18 +- 9 files changed, 240 insertions(+), 145 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index dc13044fe..6df15403a 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -1,6 +1,5 @@ -import React, { useMemo } from "react"; +import React, { useMemo, useRef, useState } from "react"; import { Area, AreaChart as RechartsAreaChart, XAxis, YAxis } from "recharts"; -import { useLayoutContext } from "../../../../context/LayoutContext"; import { ChartConfig, ChartContainer, @@ -11,16 +10,14 @@ import { import { cartesianGrid } from "../../cartesianGrid"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; import { getChartConfig, getDataKeys } from "../../utils/dataUtils"; - -export type AreaChartV2Data = Array>; +import { AreaChartV2Data, AreaChartVariant } from "../types"; export interface AreaChartV2Props { data: T; categoryKey: keyof T[number]; theme?: PaletteName; - variant?: "linear" | "natural" | "step"; + variant?: AreaChartVariant; grid?: boolean; - label?: boolean; legend?: boolean; opacity?: number; icons?: Partial>; @@ -28,22 +25,29 @@ export interface AreaChartV2Props { showYAxis?: boolean; xAxisLabel?: React.ReactNode; yAxisLabel?: React.ReactNode; + className?: string; + height?: number; + width?: number; } +const Y_AXIS_WIDTH = 40; // Width of Y-axis chart when shown + export const AreaChartV2 = ({ data, categoryKey, theme = "ocean", variant = "natural", grid = true, - label = true, - legend = true, - opacity = 0.5, icons = {}, isAnimationActive = true, showYAxis = false, xAxisLabel, yAxisLabel, + legend = true, + opacity = 0.5, + className, + height, + width, }: AreaChartV2Props) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); @@ -54,105 +58,39 @@ export const AreaChartV2 = ({ return getDistributedColors(palette, dataKeys.length); }, [theme, dataKeys.length]); - const { layout } = useLayoutContext(); - const chartConfig: ChartConfig = useMemo(() => { return getChartConfig(dataKeys, colors, icons); }, [dataKeys, icons, colors]); - const getAxisAngle = (data: T) => { - const angleConfig = { - mobile: { - default: 0, - ranges: [ - { min: 6, max: 9, angle: -45 }, - { min: 10, max: 10, angle: -60 }, - { min: 11, max: Infinity, angle: -75 }, - ], - }, - tray: { - default: 0, - ranges: [{ min: 8, max: Infinity, angle: -45 }], - }, - copilot: { - default: 0, - ranges: [{ min: 8, max: Infinity, angle: -45 }], - }, - fullscreen: { - default: 0, - ranges: [{ min: 12, max: Infinity, angle: -45 }], - }, - }; - - const layoutConfig = angleConfig[layout as keyof typeof angleConfig] || angleConfig.fullscreen; - const dataLength = data.length; - - const matchRange = layoutConfig.ranges.find( - (range) => dataLength >= range.min && dataLength <= range.max, - ); + const chartContainerRef = useRef(null); + const mainContainerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(false); + const [hoveredCategory, setHoveredCategory] = useState(null); - return matchRange?.angle ?? layoutConfig.default; - }; + // Use provided width or observed width + const effectiveWidth = useMemo(() => { + return width ?? containerWidth; + }, [width, containerWidth]); - const getTickMargin = (data: T) => { - return data.length <= 6 ? 10 : 15; - }; - - // Add safety check for empty data - if (!data || data.length === 0) { - return ( - -
No data available
-
- ); - } + const effectiveContainerWidth = useMemo(() => { + const yAxisWidth = showYAxis ? Y_AXIS_WIDTH : 0; + return Math.max(0, effectiveWidth - yAxisWidth); + }, [effectiveWidth, showYAxis]); return ( - + {grid && cartesianGrid()} - {showYAxis && ( - - )} + {showYAxis && } } /> {dataKeys.map((key) => { @@ -168,9 +106,9 @@ export const AreaChartV2 = ({ fill={color} fillOpacity={opacity} stackId="a" - dot={{ - fill: color, - }} + // dot={{ + // fill: color, + // }} activeDot={{ r: 6, }} diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx index a45583cbd..9ed71027e 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -4,12 +4,12 @@ import { Card } from "../../../../Card"; import { AreaChartV2, AreaChartV2Props } from "../AreaChartV2"; const areaChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, + { month: "January", desktop: 150, mobile: 90, tablet: 100 }, + { month: "February", desktop: 280, mobile: 180, tablet: 120 }, + { month: "March", desktop: 220, mobile: 140, tablet: 140 }, + { month: "April", desktop: 180, mobile: 160, tablet: 160 }, + { month: "May", desktop: 250, mobile: 120, tablet: 180 }, + { month: "June", desktop: 300, mobile: 180, tablet: 200 }, ]; const icons = { @@ -100,15 +100,6 @@ const meta: Meta> = { category: "Display", }, }, - label: { - description: "Whether to display data point labels above each point on the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, legend: { description: "Whether to display the chart legend showing the data series names and their corresponding colors/icons", @@ -171,9 +162,8 @@ export const AreaChartV2Story: Story = { opacity: 0.5, grid: true, legend: true, - label: true, isAnimationActive: true, - showYAxis: false, + showYAxis: true, }, render: (args) => ( diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx index f7e59b532..f2fdf8855 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx @@ -1,7 +1,8 @@ import React from "react"; import { Area, AreaChart as RechartsAreaChart, XAxis } from "recharts"; import { ChartContainer, keyTransform } from "../../Charts"; -import { MiniAreaChartData, createChartConfig } from "../utils/AreaChartUtils"; +import { MiniAreaChartData } from "../types"; +import { createChartConfig } from "../utils/AreaChartUtils"; export interface MiniAreaChartProps { data: T; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts new file mode 100644 index 000000000..7a9e36730 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts @@ -0,0 +1,5 @@ +export type AreaChartVariant = "linear" | "natural" | "step"; + +export type AreaChartV2Data = Array>; + +export type MiniAreaChartData = Array>; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts index 738dddc00..b3f03f795 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts @@ -1,7 +1,6 @@ import { ChartConfig } from "../../Charts"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; - -export type MiniAreaChartData = Array>; +import { MiniAreaChartData } from "../types"; export interface MiniAreaChartConfig { data: MiniAreaChartData; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 7119602f0..64def223f 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -16,7 +16,7 @@ import { DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; import { type LegendItem } from "../../types"; import { getDistributedColors, getPalette, type PaletteName } from "../../utils/PalletUtils"; import { getChartConfig, getDataKeys } from "../../utils/dataUtils"; -import { BarChartData, Variant } from "../types"; +import { BarChartData, BarChartVariant } from "../types"; import { BAR_WIDTH, findNearestSnapPosition, @@ -36,7 +36,7 @@ export interface BarChartPropsV2 { data: T; categoryKey: keyof T[number]; theme?: PaletteName; - variant?: Variant; + variant?: BarChartVariant; grid?: boolean; radius?: number; icons?: Partial>; @@ -254,7 +254,7 @@ const BarChartV2Component = ({ data={data} margin={{ top: 20, - bottom: 32, + bottom: 32, // this is required for to give space for x-axis left: 0, right: 0, }} diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx index 522fc7d61..91471cc9c 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx @@ -26,24 +26,186 @@ const dataVariations = { { month: "Mar", desktop: 220, mobile: 140 }, ], large: [ - { month: "Jan 2022", desktop: 150, mobile: 90, tablet: 120, laptop: 180, tv: 50, watch: 30 }, - { month: "Feb 2022", desktop: 280, mobile: 180, tablet: 140, laptop: 160, tv: 70, watch: 40 }, - { month: "Mar 2022", desktop: 220, mobile: 140, tablet: 160, laptop: 180, tv: 60, watch: 35 }, - { month: "Apr 2022", desktop: 180, mobile: 160, tablet: 180, laptop: 200, tv: 80, watch: 45 }, - { month: "May 2022", desktop: 250, mobile: 120, tablet: 140, laptop: 160, tv: 55, watch: 25 }, - { month: "Jun 2022", desktop: 300, mobile: 180, tablet: 160, laptop: 180, tv: 75, watch: 50 }, - { month: "Jul 2022", desktop: 350, mobile: 220, tablet: 180, laptop: 200, tv: 85, watch: 55 }, - { month: "Aug 2022", desktop: 400, mobile: 240, tablet: 200, laptop: 220, tv: 90, watch: 60 }, - { month: "Sep 2022", desktop: 450, mobile: 260, tablet: 220, laptop: 240, tv: 95, watch: 65 }, - { month: "Oct 2022", desktop: 500, mobile: 280, tablet: 240, laptop: 260, tv: 100, watch: 70 }, - { month: "Nov 2022", desktop: 550, mobile: 300, tablet: 260, laptop: 280, tv: 105, watch: 75 }, - { month: "Dec 2022", desktop: 600, mobile: 320, tablet: 280, laptop: 300, tv: 110, watch: 80 }, - { month: "Jan 2023", desktop: 650, mobile: 340, tablet: 300, laptop: 320, tv: 115, watch: 85 }, - { month: "Feb 2023", desktop: 700, mobile: 360, tablet: 320, laptop: 340, tv: 120, watch: 90 }, - { month: "Mar 2023", desktop: 750, mobile: 380, tablet: 340, laptop: 360, tv: 125, watch: 95 }, - { month: "Apr 2023", desktop: 800, mobile: 400, tablet: 360, laptop: 380, tv: 130, watch: 100 }, - { month: "May 2023", desktop: 850, mobile: 420, tablet: 380, laptop: 400, tv: 135, watch: 105 }, - { month: "Jun 2023", desktop: 900, mobile: 440, tablet: 400, laptop: 420, tv: 140, watch: 110 }, + { + month: "Jan 2022", + desktop: 150, + mobile: 90, + tablet: 120, + laptop: 180, + tv: 50, + watch: 30, + smartwatch: 20, + }, + { + month: "Feb 2022", + desktop: 280, + mobile: 180, + tablet: 140, + laptop: 160, + tv: 70, + watch: 40, + smartwatch: 30, + }, + { + month: "Mar 2022", + desktop: 220, + mobile: 140, + tablet: 160, + laptop: 180, + tv: 60, + watch: 35, + smartwatch: 25, + }, + { + month: "Apr 2022", + desktop: 180, + mobile: 160, + tablet: 180, + laptop: 200, + tv: 80, + watch: 45, + smartwatch: 35, + }, + { + month: "May 2022", + desktop: 250, + mobile: 120, + tablet: 140, + laptop: 160, + tv: 55, + watch: 25, + smartwatch: 20, + }, + { + month: "Jun 2022", + desktop: 300, + mobile: 180, + tablet: 160, + laptop: 180, + tv: 75, + watch: 50, + smartwatch: 30, + }, + { + month: "Jul 2022", + desktop: 350, + mobile: 220, + tablet: 180, + laptop: 200, + tv: 85, + watch: 55, + smartwatch: 35, + }, + { + month: "Aug 2022", + desktop: 400, + mobile: 240, + tablet: 200, + laptop: 220, + tv: 90, + watch: 60, + smartwatch: 40, + }, + { + month: "Sep 2022", + desktop: 450, + mobile: 260, + tablet: 220, + laptop: 240, + tv: 95, + watch: 65, + smartwatch: 45, + }, + { + month: "Oct 2022", + desktop: 500, + mobile: 280, + tablet: 240, + laptop: 260, + tv: 100, + watch: 70, + smartwatch: 50, + }, + { + month: "Nov 2022", + desktop: 550, + mobile: 300, + tablet: 260, + laptop: 280, + tv: 105, + watch: 75, + smartwatch: 55, + }, + { + month: "Dec 2022", + desktop: 600, + mobile: 320, + tablet: 280, + laptop: 300, + tv: 110, + watch: 80, + smartwatch: 60, + }, + { + month: "Jan 2023", + desktop: 650, + mobile: 340, + tablet: 300, + laptop: 320, + tv: 115, + watch: 85, + smartwatch: 65, + }, + { + month: "Feb 2023", + desktop: 700, + mobile: 360, + tablet: 320, + laptop: 340, + tv: 120, + watch: 90, + smartwatch: 70, + }, + { + month: "Mar 2023", + desktop: 750, + mobile: 380, + tablet: 340, + laptop: 360, + tv: 125, + watch: 95, + smartwatch: 75, + }, + { + month: "Apr 2023", + desktop: 800, + mobile: 400, + tablet: 360, + laptop: 380, + tv: 130, + watch: 100, + smartwatch: 80, + }, + { + month: "May 2023", + desktop: 850, + mobile: 420, + tablet: 380, + laptop: 400, + tv: 135, + watch: 105, + smartwatch: 85, + }, + { + month: "Jun 2023", + desktop: 900, + mobile: 440, + tablet: 400, + laptop: 420, + tv: 140, + watch: 110, + smartwatch: 90, + }, ], simple: [ { quarter: "Q1", revenue: 1200, profit: 800 }, diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts index fadabeb41..d2db3c40c 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts @@ -1,4 +1,4 @@ -export type Variant = "grouped" | "stacked"; +export type BarChartVariant = "grouped" | "stacked"; export type BarChartData = Array>; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts index 168ea3364..688eaaea4 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts @@ -1,5 +1,5 @@ import { LegendItem } from "../../types"; -import { Variant } from "../types"; +import { BarChartVariant } from "../types"; export const BAR_WIDTH = 12; @@ -13,7 +13,7 @@ const ELEMENT_SPACING_STACKED = 26; // Spacing per stack in stacked charts * @param variant - The chart variant * @returns The spacing value for the given variant */ -const getElementSpacing = (variant: Variant): number => { +const getElementSpacing = (variant: BarChartVariant): number => { switch (variant) { case "stacked": return ELEMENT_SPACING_STACKED; @@ -33,7 +33,7 @@ const getElementSpacing = (variant: Variant): number => { const getWidthOfData = ( data: Array>, categoryKey: string, - variant: Variant, + variant: BarChartVariant, ) => { let numberOfElements: number; const elementSpacing = getElementSpacing(variant); @@ -67,7 +67,7 @@ const getPadding = ( data: Array>, categoryKey: string, containerWidth: number, - variant: Variant, + variant: BarChartVariant, ) => { const chartWidth = getWidthOfData(data, categoryKey, variant); const paddingValue = containerWidth - chartWidth; @@ -94,7 +94,7 @@ const getPadding = ( * @param isLast - Whether the last item in the stack. */ const getRadiusArray = ( - variant: Variant, + variant: BarChartVariant, radius: number, isFirst?: boolean, isLast?: boolean, @@ -156,7 +156,7 @@ const getYAxisTickFormatter = () => { * @returns The formatter for the X-axis tick values. * internally used by the XAxis component reCharts */ -const getXAxisTickFormatter = (groupWidth?: number, variant: Variant = "grouped") => { +const getXAxisTickFormatter = (groupWidth?: number, variant: BarChartVariant = "grouped") => { const CHAR_WIDTH = 7; // Average character width in pixels for most fonts const ELLIPSIS_WIDTH = CHAR_WIDTH * 3; // "..." takes about 3 character widths const PADDING = 8; // Safety padding for better visual spacing @@ -206,7 +206,7 @@ const getXAxisTickFormatter = (groupWidth?: number, variant: Variant = "grouped" const getOptimalXAxisTickFormatter = ( data: Array>, categoryKey: string, - variant: Variant, + variant: BarChartVariant, ) => { // Calculate the available width per group const groupWidth = getWidthOfGroup(data, categoryKey, variant); @@ -224,7 +224,7 @@ const getOptimalXAxisTickFormatter = ( const getWidthOfGroup = ( data: Array>, categoryKey: string, - variant: Variant, + variant: BarChartVariant, ) => { if (data.length === 0) return 200; // Fallback @@ -255,7 +255,7 @@ const getWidthOfGroup = ( const getSnapPositions = ( data: Array>, categoryKey: string, - variant: Variant, + variant: BarChartVariant, ): number[] => { if (data.length === 0) return [0]; From 8646d584491b74323dacec7951a2d6a072fd9485 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 10 Jun 2025 01:29:33 +0530 Subject: [PATCH 044/190] feat(area-chart): Implement snap scrolling and improve data width calculation The changes in this commit focus on the following improvements to the area chart component: 1. Implement snap scrolling functionality for the area chart. This allows for smooth scrolling and ensures that the chart data is always aligned with the chart container. 2. Improve the calculation of the width of the chart data. This ensures that the chart container is sized correctly, even for single data points or charts with a large number of data points. The key changes include: - Add functions to calculate the width of the data, the width of each group/category, and the snap positions for the chart. - Implement a function to find the nearest snap position based on the current scroll position and scroll direction. - Add new SCSS files for the area chart component. - Add a new utility function to format the Y-axis tick values with abbreviations. These changes improve the overall user experience and visual quality of the area chart component. --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 276 +++++++++++++++--- .../AreaCharts/AreaChartV2/areaChartV2.scss | 35 +++ .../stories/areaChartV2.stories.tsx | 22 +- .../Charts/AreaCharts/utils/AreaChartUtils.ts | 94 ++++++ .../BarCharts/BarChartV2/BarChartV2.tsx | 20 +- .../Charts/BarCharts/utils/BarChartUtils.ts | 51 ---- .../src/components/Charts/charts.scss | 6 + .../src/components/Charts/utils/dataUtils.ts | 22 ++ .../src/components/Charts/utils/styleUtils.ts | 28 ++ 9 files changed, 446 insertions(+), 108 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss create mode 100644 js/packages/react-ui/src/components/Charts/utils/styleUtils.ts diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index 6df15403a..d9036494f 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -1,5 +1,8 @@ -import React, { useMemo, useRef, useState } from "react"; +import clsx from "clsx"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; import { Area, AreaChart as RechartsAreaChart, XAxis, YAxis } from "recharts"; +import { IconButton } from "../../../IconButton"; import { ChartConfig, ChartContainer, @@ -8,9 +11,13 @@ import { keyTransform, } from "../../Charts"; import { cartesianGrid } from "../../cartesianGrid"; +import { DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; +import { LegendItem } from "../../types"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; -import { getChartConfig, getDataKeys } from "../../utils/dataUtils"; +import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; +import { getYAxisTickFormatter } from "../../utils/styleUtils"; import { AreaChartV2Data, AreaChartVariant } from "../types"; +import { findNearestSnapPosition, getSnapPositions, getWidthOfData } from "../utils/AreaChartUtils"; export interface AreaChartV2Props { data: T; @@ -28,6 +35,7 @@ export interface AreaChartV2Props { className?: string; height?: number; width?: number; + onAreaClick?: (payload: any) => void; } const Y_AXIS_WIDTH = 40; // Width of Y-axis chart when shown @@ -48,6 +56,7 @@ export const AreaChartV2 = ({ className, height, width, + onAreaClick, }: AreaChartV2Props) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); @@ -67,7 +76,6 @@ export const AreaChartV2 = ({ const [containerWidth, setContainerWidth] = useState(0); const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(false); - const [hoveredCategory, setHoveredCategory] = useState(null); // Use provided width or observed width const effectiveWidth = useMemo(() => { @@ -79,44 +87,232 @@ export const AreaChartV2 = ({ return Math.max(0, effectiveWidth - yAxisWidth); }, [effectiveWidth, showYAxis]); + const dataWidth = useMemo(() => { + return getWidthOfData(data); + }, [data]); + + // Calculate snap positions for proper scrolling alignment + const snapPositions = useMemo(() => { + return getSnapPositions(data); + }, [data]); + + const chartHeight = useMemo(() => { + return height ?? 296; + }, [height]); + + // Check scroll boundaries + const updateScrollState = useCallback(() => { + if (mainContainerRef.current) { + const { scrollLeft, scrollWidth, clientWidth } = mainContainerRef.current; + setCanScrollLeft(scrollLeft > 0); + setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 1); // -1 for floating point precision + } + }, []); + + const scrollLeft = useCallback(() => { + if (mainContainerRef.current) { + const currentScroll = mainContainerRef.current.scrollLeft; + const targetIndex = findNearestSnapPosition(snapPositions, currentScroll, "left"); + const targetPosition = snapPositions[targetIndex] ?? 0; + + mainContainerRef.current.scrollTo({ + left: targetPosition, + behavior: "smooth", + }); + } + }, [snapPositions]); + + const scrollRight = useCallback(() => { + if (mainContainerRef.current) { + const currentScroll = mainContainerRef.current.scrollLeft; + const targetIndex = findNearestSnapPosition(snapPositions, currentScroll, "right"); + const targetPosition = snapPositions[targetIndex] ?? 0; + + mainContainerRef.current.scrollTo({ + left: targetPosition, + behavior: "smooth", + }); + } + }, [snapPositions]); + + useEffect(() => { + // Only set up ResizeObserver if width is not provided + if (width || !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) { + setContainerWidth(entry.contentRect.width); + } + }); + + resizeObserver.observe(chartContainerRef.current); + + return () => { + resizeObserver.disconnect(); + }; + }, [width]); + + // Update scroll state when container width or data width changes + useEffect(() => { + updateScrollState(); + }, [effectiveWidth, dataWidth, updateScrollState]); + + // Add scroll event listener to update button states + useEffect(() => { + const mainContainer = mainContainerRef.current; + if (!mainContainer) return; + + const handleScroll = () => { + updateScrollState(); + }; + + mainContainer.addEventListener("scroll", handleScroll); + return () => { + mainContainer.removeEventListener("scroll", handleScroll); + }; + }, [updateScrollState]); + + const legendItems: LegendItem[] = useMemo(() => { + return getLegendItems(dataKeys, colors, icons); + }, [dataKeys, colors, icons]); + + const id = useId(); + + const chartSyncID = useMemo(() => `area-chart-sync-${id}`, [id]); + return ( - - - {grid && cartesianGrid()} - - {showYAxis && } - - } /> - {dataKeys.map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - - return ( - +
+ {showYAxis && ( +
+ {/* Y-axis only chart - synchronized with main chart */} + + } + /> + {/* Invisible area to maintain scale synchronization */} + {dataKeys.map((key) => { + return ( + + ); + })} + +
+ )} +
+ + - ); - })} - - + syncId={chartSyncID} + > + {grid && cartesianGrid()} + } + orientation="bottom" + padding={{ + left: 20, + right: 20, + }} + /> + } /> + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + return ( + + ); + })} + + +
+
+ {/* if the data width is greater than the effective width, then show the scroll buttons */} + {dataWidth > effectiveWidth && ( +
+ } + variant="secondary" + onClick={scrollLeft} + size="extra-small" + disabled={!canScrollLeft} + /> + } + variant="secondary" + size="extra-small" + onClick={scrollRight} + disabled={!canScrollRight} + /> +
+ )} + {legend && ( + + )} +
); }; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss new file mode 100644 index 000000000..02b959540 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss @@ -0,0 +1,35 @@ +@use "../../../../cssUtils" as cssUtils; + +.crayon-area-chart-container-inner { + display: flex; + width: 100%; +} + +.crayon-area-chart-y-axis-container { + flex-shrink: 0; +} + +.crayon-area-chart-main-container { + width: 100%; + overflow-x: auto; +} + +.crayon-area-chart-scroll-container { + position: relative; +} + +.crayon-area-chart-scroll-button { + position: absolute; + + &--left { + top: -15px; + transform: translateY(-50%); + left: 20px; + } + + &--right { + top: -15px; + transform: translateY(-50%); + right: 0px; + } +} diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx index 9ed71027e..d3a97e370 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -4,12 +4,18 @@ import { Card } from "../../../../Card"; import { AreaChartV2, AreaChartV2Props } from "../AreaChartV2"; const areaChartData = [ - { month: "January", desktop: 150, mobile: 90, tablet: 100 }, - { month: "February", desktop: 280, mobile: 180, tablet: 120 }, - { month: "March", desktop: 220, mobile: 140, tablet: 140 }, - { month: "April", desktop: 180, mobile: 160, tablet: 160 }, - { month: "May", desktop: 250, mobile: 120, tablet: 180 }, - { month: "June", desktop: 300, mobile: 180, tablet: 200 }, + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + { month: "April", desktop: 180, mobile: 160 }, + { month: "May", desktop: 250, mobile: 120 }, + { month: "June", desktop: 300, mobile: 180 }, + { month: "July", desktop: 350, mobile: 220 }, + { month: "August", desktop: 400, mobile: 240 }, + { month: "September", desktop: 450, mobile: 260 }, + { month: "October", desktop: 500, mobile: 280 }, + { month: "November", desktop: 550, mobile: 300 }, + { month: "December", desktop: 600, mobile: 320 }, ]; const icons = { @@ -158,7 +164,7 @@ export const AreaChartV2Story: Story = { data: areaChartData, categoryKey: "month", theme: "ocean", - variant: "linear", + variant: "natural", opacity: 0.5, grid: true, legend: true, @@ -166,7 +172,7 @@ export const AreaChartV2Story: Story = { showYAxis: true, }, render: (args) => ( - + ), diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts index b3f03f795..3ea73d89e 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts @@ -2,6 +2,98 @@ import { ChartConfig } from "../../Charts"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; import { MiniAreaChartData } from "../types"; +const ELEMENT_SPACING = 70; + +/** + * This function returns the width of the data in the chart, used for padding calculation, scroll amount calculation, and + * for the width of the chart container. + * @param data - The data to be displayed in the chart. + */ +const getWidthOfData = (data: Array>) => { + // For area charts, we calculate based on the number of data points (always stacked) + const numberOfElements = data.length; // Number of data points + + let width = numberOfElements * ELEMENT_SPACING; // here we are defining the spacing between the data points, + // as the data point has no width, we are just calculating the spacing between the data points + // if 3 data points, then 2 spaces between them, so 2*70 = 140 + + if (data.length === 1) { + const minSingleDataWidth = 200; // Minimum width for single data points + // self note: + // if the data point is only one, then we need to set the width to the minimum width + + width = Math.max(width, minSingleDataWidth); + } + + return width; +}; + +/** + * INTERNAL HELPER FUNCTION + * This function returns the width of each group/category for area charts + * this is basically a checking function to follow DRY principle + * @param data - The data to be displayed in the chart. + * @param categoryKey - The key of the category to be displayed in the chart. + */ +const getWidthOfGroup = (data: Array>) => { + if (data.length === 0) return 200; // Fallback + + // For area charts, each category/data point has the same spacing + return ELEMENT_SPACING; +}; + +/** + * This function returns the snap positions for the area chart, used for smooth scrolling + * @param data - The data to be displayed in the chart. + * @param categoryKey - The key of the category to be displayed in the chart. + * @returns The snap positions for the chart. + */ +const getSnapPositions = (data: Array>): number[] => { + if (data.length === 0) return [0]; + + const positions = [0]; // Start position + const groupWidthValue = getWidthOfGroup(data); + + // Calculate all valid snap positions based on data points + for (let i = 1; i < data.length; i++) { + positions.push(i * groupWidthValue); + } + + return positions; +}; + +/** + * This function returns the nearest snap position for the area chart + * @param snapPositions - The snap positions for the chart. + * @param currentScroll - The current scroll of the chart. + * @param direction - The direction of the scroll. + * @returns The nearest snap position index for the chart. + */ +const findNearestSnapPosition = ( + snapPositions: number[], + currentScroll: number, + direction: "left" | "right", +): number => { + // Find current position index + let currentIndex = 0; + for (let i = 0; i < snapPositions.length; i++) { + const snapPosition = snapPositions[i]; + if (snapPosition !== undefined && currentScroll >= snapPosition) { + currentIndex = i; + } else { + break; + } + } + + if (direction === "left") { + // Go to previous snap position + return Math.max(0, currentIndex - 1); + } else { + // Go to next snap position + return Math.min(snapPositions.length - 1, currentIndex + 1); + } +}; + export interface MiniAreaChartConfig { data: MiniAreaChartData; categoryKey: string; @@ -32,3 +124,5 @@ export const createChartConfig = ({ {}, ); }; + +export { findNearestSnapPosition, getSnapPositions, getWidthOfData }; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 64def223f..2f309df2c 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -15,19 +15,17 @@ import { cartesianGrid } from "../../cartesianGrid"; import { DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; import { type LegendItem } from "../../types"; import { getDistributedColors, getPalette, type PaletteName } from "../../utils/PalletUtils"; -import { getChartConfig, getDataKeys } from "../../utils/dataUtils"; +import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; +import { getYAxisTickFormatter } from "../../utils/styleUtils"; import { BarChartData, BarChartVariant } from "../types"; import { BAR_WIDTH, findNearestSnapPosition, - getChartHeight, - getLegendItems, getOptimalXAxisTickFormatter, getPadding, getRadiusArray, getSnapPositions, getWidthOfData, - getYAxisTickFormatter, } from "../utils/BarChartUtils"; import { SimpleCursor } from "./components/CustomCursor"; import { LineInBarShape } from "./components/LineInBarShape"; @@ -119,13 +117,17 @@ const BarChartV2Component = ({ return getSnapPositions(data, categoryKey as string, variant); }, [data, categoryKey, variant]); + // self note: // Use provided height or calculated height based on container width // if height is provided, it will be used to set the height of the chart // if height is not provided, it will be calculated based on the container width (effectiveWidth) - // height will be 16:9 ratio of the width + // getChartHeight(effectiveWidth) this function is not used here, request of the designer, we will use fix height + // 296 is the height of the chart by default, given by designer + // we want to chart to scale with width but height will be fixed + const chartHeight = useMemo(() => { - return height ?? getChartHeight(effectiveWidth); - }, [height, effectiveWidth]); + return height ?? 296; + }, [height]); // Check scroll boundaries const updateScrollState = useCallback(() => { @@ -248,7 +250,7 @@ const BarChartV2Component = ({
{/* Y-axis only chart - synchronized with main chart */} ({ > { - return (value: any) => { - // Format the Y-axis tick values with abbreviations - if (typeof value === "number") { - const absValue = Math.abs(value); - - if (absValue >= 1e12) { - return (value / 1e12).toFixed(absValue >= 10e12 ? 0 : 1) + "T"; - } else if (absValue >= 1e9) { - return (value / 1e9).toFixed(absValue >= 10e9 ? 0 : 1) + "B"; - } else if (absValue >= 1e6) { - return (value / 1e6).toFixed(absValue >= 10e6 ? 0 : 1) + "M"; - } else if (absValue >= 1e3) { - return (value / 1e3).toFixed(absValue >= 10e3 ? 0 : 1) + "K"; - } else { - return value.toString(); - } - } - return String(value); - }; -}; - /** * INTERNAL HELPER FUNCTION * This function returns the formatter for the X-axis tick values with intelligent truncation. @@ -302,27 +274,6 @@ const findNearestSnapPosition = ( } }; -/** - * This function returns the legend items for the chart, used for the legend items of the chart. - * @param dataKeys - The data keys for the chart. - * @param colors - The colors for the chart. - * @param icons - The icons for the chart. - * @returns The legend items for the chart. - */ - -const getLegendItems = ( - dataKeys: string[], - colors: string[], - icons: Partial>, -): LegendItem[] => { - return dataKeys.map((key, index) => ({ - key, - label: key, - color: colors[index] || "#000000", // Fallback color if undefined - icon: icons[key] as React.ComponentType | undefined, - })); -}; - /** * This function returns the chart height for the chart, used for the chart height of the chart. * @param containerWidth - The width of the container of the chart. @@ -337,11 +288,9 @@ const getChartHeight = (containerWidth: number): number => { export { findNearestSnapPosition, getChartHeight, - getLegendItems, getOptimalXAxisTickFormatter, getPadding, getRadiusArray, getSnapPositions, getWidthOfData, - getYAxisTickFormatter, }; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index 242a5b90f..35f047534 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -1,8 +1,14 @@ @use "../../cssUtils" as cssUtils; + +// bar chart css @forward "./BarCharts/BarChartV2/barChar.scss"; @forward "./BarCharts/BarChartV2/components/CustomCursor.scss"; @forward "./BarCharts/MiniBarChart/miniBarChart.scss"; +// area chart css +@forward "./AreaCharts/AreaChartV2/areaChartV2.scss"; + +// shared components css @forward "./shared/XAxisTick/xAxisTick.scss"; @forward "./shared/DefaultLegend/defaultLegend.scss"; @forward "./shared/YAxisTick/yAxisTick.scss"; diff --git a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts index 86a910fa2..13e1f8a23 100644 --- a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts @@ -1,4 +1,5 @@ import { ChartConfig } from "../Charts"; +import { LegendItem } from "../types"; /** * This function returns the data keys for the chart, used for the data keys of the chart. @@ -38,3 +39,24 @@ export const getChartConfig = ( {}, ); }; + +/** + * This function returns the legend items for the chart, used for the legend items of the chart. + * @param dataKeys - The data keys for the chart. + * @param colors - The colors for the chart. + * @param icons - The icons for the chart. + * @returns The legend items for the chart. + */ + +export const getLegendItems = ( + dataKeys: string[], + colors: string[], + icons: Partial>, +): LegendItem[] => { + return dataKeys.map((key, index) => ({ + key, + label: key, + color: colors[index] || "#000000", // Fallback color if undefined + icon: icons[key] as React.ComponentType | undefined, + })); +}; diff --git a/js/packages/react-ui/src/components/Charts/utils/styleUtils.ts b/js/packages/react-ui/src/components/Charts/utils/styleUtils.ts new file mode 100644 index 000000000..dddbf55c2 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/utils/styleUtils.ts @@ -0,0 +1,28 @@ +/** + * This function returns the formatter for the Y-axis tick values. + * @returns The formatter for the Y-axis tick values. + * internally used by the YAxis component reCharts + */ +const getYAxisTickFormatter = () => { + return (value: any) => { + // Format the Y-axis tick values with abbreviations + if (typeof value === "number") { + const absValue = Math.abs(value); + + if (absValue >= 1e12) { + return (value / 1e12).toFixed(absValue >= 10e12 ? 0 : 1) + "T"; + } else if (absValue >= 1e9) { + return (value / 1e9).toFixed(absValue >= 10e9 ? 0 : 1) + "B"; + } else if (absValue >= 1e6) { + return (value / 1e6).toFixed(absValue >= 10e6 ? 0 : 1) + "M"; + } else if (absValue >= 1e3) { + return (value / 1e3).toFixed(absValue >= 10e3 ? 0 : 1) + "K"; + } else { + return value.toString(); + } + } + return String(value); + }; +}; + +export { getYAxisTickFormatter }; From d4d5e1eca613593634b7f9793e20be5de10d7662 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 10 Jun 2025 03:03:53 +0530 Subject: [PATCH 045/190] story book edge case --- .../stories/areaChartV2.stories.tsx | 567 ++++++++++++++++-- 1 file changed, 521 insertions(+), 46 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx index d3a97e370..10ce7c031 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -1,26 +1,138 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Monitor, TabletSmartphone } from "lucide-react"; +import { Monitor, TabletSmartphone, Calendar, Globe, Smartphone, Laptop, Tv, Watch } from "lucide-react"; +import { useState } from "react"; import { Card } from "../../../../Card"; import { AreaChartV2, AreaChartV2Props } from "../AreaChartV2"; -const areaChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, - { month: "July", desktop: 350, mobile: 220 }, - { month: "August", desktop: 400, mobile: 240 }, - { month: "September", desktop: 450, mobile: 260 }, - { month: "October", desktop: 500, mobile: 280 }, - { month: "November", desktop: 550, mobile: 300 }, - { month: "December", desktop: 600, mobile: 320 }, -]; +// 🔥 COMPREHENSIVE DATA VARIATIONS - Designed to test label collision scenarios +const dataVariations = { + default: [ + { month: "January", desktop: 150, mobile: 90, tablet: 120 }, + { month: "February", desktop: 280, mobile: 180, tablet: 140 }, + { month: "March", desktop: 220, mobile: 140, tablet: 160 }, + { month: "April", desktop: 180, mobile: 160, tablet: 180 }, + { month: "May", desktop: 250, mobile: 120, tablet: 140 }, + { month: "June", desktop: 300, mobile: 180, tablet: 160 }, + { month: "July", desktop: 350, mobile: 220, tablet: 180 }, + { month: "August", desktop: 400, mobile: 240, tablet: 200 }, + { month: "September", desktop: 450, mobile: 260, tablet: 220 }, + { month: "October", desktop: 500, mobile: 280, tablet: 240 }, + { month: "November", desktop: 550, mobile: 300, tablet: 260 }, + { month: "December", desktop: 600, mobile: 320, tablet: 280 }, + ], + // 🏷️ BIG LABELS - Testing collision detection and truncation + bigLabels: [ + { category: "Very Long Category Name That Should Be Truncated", sales: 150, revenue: 90, profit: 120 }, + { category: "Another Extremely Long Label That Causes Collisions", sales: 280, revenue: 180, profit: 140 }, + { category: "Super Duper Long Category Name That Tests Truncation", sales: 220, revenue: 140, profit: 160 }, + { category: "Incredibly Long Text That Should Trigger Collision Detection", sales: 180, revenue: 160, profit: 180 }, + { category: "Maximum Length Category Name That Tests All Edge Cases", sales: 250, revenue: 120, profit: 140 }, + { category: "Extra Long Business Category Name With Many Words", sales: 300, revenue: 180, profit: 160 }, + { category: "Comprehensive Long Label For Testing Horizontal Offset", sales: 350, revenue: 220, profit: 180 }, + { category: "Extended Category Name That Pushes Truncation Limits", sales: 400, revenue: 240, profit: 200 }, + ], + // 📅 DENSE TIMELINE - Many items with medium-length labels + denseTimeline: [ + { period: "Q1 2022 Jan-Mar", visitors: 120, conversions: 15, revenue: 1200 }, + { period: "Q1 2022 Apr-Jun", visitors: 150, conversions: 22, revenue: 1800 }, + { period: "Q2 2022 Jul-Sep", visitors: 180, conversions: 28, revenue: 2100 }, + { period: "Q2 2022 Oct-Dec", visitors: 200, conversions: 35, revenue: 2500 }, + { period: "Q3 2023 Jan-Mar", visitors: 160, conversions: 18, revenue: 1600 }, + { period: "Q3 2023 Apr-Jun", visitors: 190, conversions: 32, revenue: 2300 }, + { period: "Q4 2023 Jul-Sep", visitors: 220, conversions: 40, revenue: 2800 }, + { period: "Q4 2023 Oct-Dec", visitors: 240, conversions: 45, revenue: 3200 }, + { period: "Q1 2024 Jan-Mar", visitors: 210, conversions: 38, revenue: 2700 }, + { period: "Q1 2024 Apr-Jun", visitors: 230, conversions: 42, revenue: 3000 }, + { period: "Q2 2024 Jul-Sep", visitors: 250, conversions: 48, revenue: 3400 }, + { period: "Q2 2024 Oct-Dec", visitors: 270, conversions: 52, revenue: 3800 }, + { period: "Q3 2024 Jan-Mar", visitors: 260, conversions: 50, revenue: 3600 }, + { period: "Q3 2024 Apr-Jun", visitors: 280, conversions: 55, revenue: 4000 }, + { period: "Q4 2024 Jul-Sep", visitors: 300, conversions: 60, revenue: 4300 }, + { period: "Q4 2024 Oct-Dec", visitors: 290, conversions: 58, revenue: 4100 }, + ], + // 🏢 COMPANY NAMES - Real-world long business names + companyNames: [ + { company: "Apple Inc.", revenue: 394328000000, profit: 99803000000, marketCap: 3500000000000 }, + { company: "Microsoft Corporation", revenue: 211915000000, profit: 83383000000, marketCap: 2800000000000 }, + { company: "Alphabet Inc. (Google)", revenue: 307394000000, profit: 76033000000, marketCap: 2100000000000 }, + { company: "Amazon.com Inc.", revenue: 574785000000, profit: 33364000000, marketCap: 1600000000000 }, + { company: "Tesla Motors Inc.", revenue: 96773000000, profit: 15000000000, marketCap: 800000000000 }, + { company: "Meta Platforms Inc.", revenue: 134902000000, profit: 39370000000, marketCap: 900000000000 }, + { company: "NVIDIA Corporation", revenue: 60922000000, profit: 29760000000, marketCap: 1800000000000 }, + { company: "Berkshire Hathaway Inc.", revenue: 364482000000, profit: 96223000000, marketCap: 780000000000 }, + ], + // 🌍 COUNTRY NAMES - Geographic labels with varying lengths + countryData: [ + { country: "United States of America", population: 331900000, gdp: 26900000000000 }, + { country: "People's Republic of China", population: 1412000000, gdp: 17700000000000 }, + { country: "Federal Republic of Germany", population: 83200000, gdp: 4300000000000 }, + { country: "United Kingdom of Great Britain", population: 67500000, gdp: 3100000000000 }, + { country: "French Republic", population: 68000000, gdp: 2900000000000 }, + { country: "Republic of India", population: 1380000000, gdp: 3700000000000 }, + { country: "Federative Republic of Brazil", population: 215000000, gdp: 2100000000000 }, + { country: "Russian Federation", population: 146000000, gdp: 1800000000000 }, + ], + // 📈 FINANCIAL QUARTERS - Testing medium-density scenarios + financialQuarters: [ + { quarter: "Q1 FY2022", revenue: 1200000, expenses: 800000, profit: 400000 }, + { quarter: "Q2 FY2022", revenue: 1500000, expenses: 950000, profit: 550000 }, + { quarter: "Q3 FY2022", revenue: 1800000, expenses: 1100000, profit: 700000 }, + { quarter: "Q4 FY2022", revenue: 2000000, expenses: 1300000, profit: 700000 }, + { quarter: "Q1 FY2023", revenue: 2200000, expenses: 1400000, profit: 800000 }, + { quarter: "Q2 FY2023", revenue: 2500000, expenses: 1600000, profit: 900000 }, + { quarter: "Q3 FY2023", revenue: 2800000, expenses: 1800000, profit: 1000000 }, + { quarter: "Q4 FY2023", revenue: 3000000, expenses: 1900000, profit: 1100000 }, + ], + // 🔤 MIXED LENGTHS - Testing various label length scenarios + mixedLengths: [ + { item: "A", valueA: 100, valueB: 80 }, + { item: "Short", valueA: 150, valueB: 120 }, + { item: "Medium Length Item", valueA: 200, valueB: 160 }, + { item: "Very Long Item Name That Tests Truncation", valueA: 250, valueB: 200 }, + { item: "B", valueA: 180, valueB: 140 }, + { item: "Another Really Long Category Name", valueA: 220, valueB: 180 }, + { item: "XL", valueA: 190, valueB: 150 }, + ], + // 🎯 EDGE CASES - Extreme scenarios + edgeCases: [ + { name: "SinglePointDataSetForTestingEdgeCasesInCollisionDetectionAndLabelTruncationFunctionality", value: 500 }, + { name: "SecondExtremelyLongDataPointNameThatShouldDefinitelyTriggerTruncationMechanisms", value: 600 }, + ], + // 📱 MINIMAL - Small dataset for baseline testing + minimal: [ + { category: "Mobile Devices", users: 150, sessions: 90 }, + { category: "Desktop Computers", users: 280, sessions: 180 }, + { category: "Tablet Devices", users: 220, sessions: 140 }, + ], +}; + +// Category key mappings for different datasets +const categoryKeys = { + default: "month", + bigLabels: "category", + denseTimeline: "period", + companyNames: "company", + countryData: "country", + financialQuarters: "quarter", + mixedLengths: "item", + edgeCases: "name", + minimal: "category", +}; + +// 🔥 ACTIVE DATA - For backward compatibility +const areaChartData = dataVariations.default; const icons = { desktop: Monitor, mobile: TabletSmartphone, + tablet: Calendar, + sales: Globe, + revenue: Smartphone, + profit: Laptop, + visitors: Tv, + conversions: Watch, + users: Monitor, + sessions: TabletSmartphone, } as const; const meta: Meta> = { @@ -31,7 +143,7 @@ const meta: Meta> = { docs: { description: { component: - "```tsx\nimport { AreaChartV2 } from '@crayon-ui/react-ui/Charts/AreaChartV2';\n```", + "```tsx\nimport { AreaChartV2 } from '@crayon-ui/react-ui/Charts/AreaChartV2';\n```\n\nAreaChartV2 features advanced collision detection and label truncation with horizontal offset capabilities. The component automatically handles overlapping X-axis labels by intelligently truncating them with ellipsis while maintaining horizontal positioning (no angle rotation by default).", }, }, }, @@ -90,7 +202,7 @@ const meta: Meta> = { opacity: { description: "The opacity of the filled area beneath each line (0 = fully transparent, 1 = fully opaque)", - control: false, + control: "number", table: { type: { summary: "number" }, defaultValue: { summary: "0.5" }, @@ -136,22 +248,60 @@ const meta: Meta> = { }, xAxisLabel: { description: "The label for the x-axis", - control: false, + control: "text", table: { type: { summary: "string" }, - defaultValue: { summary: "string" }, + defaultValue: { summary: "undefined" }, category: "Data", }, }, yAxisLabel: { description: "The label for the y-axis", - control: false, + control: "text", table: { type: { summary: "string" }, - defaultValue: { summary: "string" }, + defaultValue: { summary: "undefined" }, category: "Data", }, }, + height: { + description: + "Fixed height for the chart in pixels. When provided, overrides the default responsive height calculation.", + control: "number", + table: { + type: { summary: "number" }, + defaultValue: { summary: "undefined" }, + category: "Layout", + }, + }, + width: { + description: + "Fixed width for the chart container in pixels. When provided, disables responsive width behavior.", + control: "number", + table: { + type: { summary: "number" }, + defaultValue: { summary: "undefined" }, + category: "Layout", + }, + }, + className: { + description: "Additional CSS class name for the chart container", + control: "text", + table: { + type: { summary: "string" }, + defaultValue: { summary: "undefined" }, + category: "Layout", + }, + }, + onAreaClick: { + description: "Callback function called when an area is clicked", + control: false, + table: { + type: { summary: "(payload: any) => void" }, + defaultValue: { summary: "undefined" }, + category: "Events", + }, + }, }, } satisfies Meta; @@ -159,7 +309,7 @@ export default meta; type Story = StoryObj; export const AreaChartV2Story: Story = { - name: "Area Chart V2", + name: "🎛️ Data Switcher - Area Chart V2 (Big Labels Focus)", args: { data: areaChartData, categoryKey: "month", @@ -170,6 +320,167 @@ export const AreaChartV2Story: Story = { legend: true, isAnimationActive: true, showYAxis: true, + xAxisLabel: "Time Period", + yAxisLabel: "Values", + // width: 600, + // height: 300, + }, + render: (args: any) => { + const [selectedDataType, setSelectedDataType] = + useState("default"); + + const currentData = dataVariations[selectedDataType]; + const currentCategoryKey = categoryKeys[selectedDataType]; + + const buttonStyle = { + margin: "2px", + padding: "6px 12px", + fontSize: "12px", + border: "1px solid #ddd", + borderRadius: "4px", + cursor: "pointer", + background: "#fff", + fontFamily: "monospace", + }; + + const activeButtonStyle = { + ...buttonStyle, + background: "#007acc", + color: "white", + border: "1px solid #007acc", + }; + + return ( +
+
+ 🏷️ Label Collision Test Suite: +
+ + + + + + + + + +
+
+
Current Dataset: {selectedDataType}
+
Items: {currentData.length} | Category Key: {currentCategoryKey}
+
Features: Auto-truncation, Collision Detection, Horizontal Offset
+
+
+ + + +
+ ); + }, + parameters: { + docs: { + description: { + story: + "**🏷️ Big Labels Focus:** This story specifically tests label collision detection and truncation scenarios. Use the buttons to switch between different data variations that stress-test the chart's ability to handle long category names.\n\n**Key Features:**\n- ✂️ **Auto-truncation** with ellipsis for long labels\n- 🔍 **Collision detection** prevents overlapping text\n- ↔️ **Horizontal offset** for better visual distribution\n- 🚫 **No angle rotation** - labels stay horizontal for readability\n\n**Test Cases:**\n- **Big Labels**: Extremely long category names that trigger truncation\n- **Dense Timeline**: Many items with medium-length labels\n- **Company Names**: Real-world business names with varying lengths\n- **Mixed Lengths**: Combination of short and long labels\n- **Edge Cases**: Extreme scenarios for boundary testing", + }, + }, + }, +}; + +export const BigLabelsStory: Story = { + name: "🏷️ Big Labels (Collision Detection)", + args: { + data: dataVariations.bigLabels as any, + categoryKey: "category" as any, + theme: "emerald", + variant: "natural", + opacity: 0.4, + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "Tests the chart's collision detection system with extremely long category names. Labels should be automatically truncated with ellipsis (...) to prevent overlapping.", + }, + }, + }, +}; + +export const DenseTimelineStory: Story = { + name: "📅 Dense Timeline (Many Periods)", + args: { + data: dataVariations.denseTimeline as any, + categoryKey: "period" as any, + theme: "sunset", + variant: "natural", + opacity: 0.6, + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, }, render: (args) => ( @@ -178,30 +489,194 @@ export const AreaChartV2Story: Story = { ), parameters: { docs: { - source: { - code: ` -const areaChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - - - -`, + description: { + story: + "Tests how the chart handles many data points with medium-length period labels. The chart should enable horizontal scrolling and apply intelligent label truncation.", + }, + }, + }, +}; + +export const CompanyNamesStory: Story = { + name: "🏢 Company Names (Real-world Labels)", + args: { + data: dataVariations.companyNames as any, + categoryKey: "company" as any, + theme: "vivid", + variant: "natural", + opacity: 0.5, + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "Tests with real-world company names that have varying lengths. Demonstrates how the chart handles business data with naturally occurring long labels.", + }, + }, + }, +}; + +export const CountryDataStory: Story = { + name: "🌍 Country Names (Geographic Labels)", + args: { + data: dataVariations.countryData as any, + categoryKey: "country" as any, + theme: "orchid", + variant: "natural", + opacity: 0.4, + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "Tests with official country names that include full formal names. Shows how geographic data with naturally long names is handled by the collision detection system.", + }, + }, + }, +}; + +export const MixedLengthsStory: Story = { + name: "🔤 Mixed Lengths (Varied Label Sizes)", + args: { + data: dataVariations.mixedLengths as any, + categoryKey: "item" as any, + theme: "spectrum", + variant: "linear", + opacity: 0.5, + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "Tests the chart's ability to handle a mix of very short and very long labels within the same dataset. Demonstrates adaptive truncation behavior.", + }, + }, + }, +}; + +export const EdgeCasesStory: Story = { + name: "🎯 Edge Cases (Extreme Scenarios)", + args: { + data: dataVariations.edgeCases as any, + categoryKey: "name" as any, + theme: "ocean", + variant: "step", + opacity: 0.7, + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "Tests extreme edge cases with exceptionally long labels and minimal data points. Pushes the collision detection and truncation system to its limits.", + }, + }, + }, +}; + +export const MinimalDataStory: Story = { + name: "📱 Minimal Data (Baseline)", + args: { + data: dataVariations.minimal as any, + categoryKey: "category" as any, + theme: "emerald", + variant: "natural", + opacity: 0.5, + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "Baseline test with minimal data and standard-length labels. Provides a control case to compare against the big label scenarios.", + }, + }, + }, +}; + +export const ResponsiveWidthStory: Story = { + name: "📐 Responsive Width Test", + args: { + data: dataVariations.bigLabels as any, + categoryKey: "category" as any, + theme: "sunset", + variant: "natural", + opacity: 0.5, + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + }, + render: (args) => ( +
+
+

Small Container (300px)

+ + + +
+
+

Medium Container (500px)

+ + + +
+
+

Large Container (800px)

+ + + +
+
+ ), + parameters: { + docs: { + description: { + story: + "Tests how the collision detection and truncation system adapts to different container widths. Smaller containers should show more aggressive truncation.", }, }, }, From a89dd31d3b9fe2a0a9d4ec832d912bf49a8e9fa9 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 10 Jun 2025 13:59:21 +0530 Subject: [PATCH 046/190] story book update --- .../AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx index 10ce7c031..37fef7d10 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -359,7 +359,7 @@ export const AreaChartV2Story: Story = { background: "#f8f9fa", borderRadius: "8px", border: "1px solid #e9ecef", - width: "800px", + width: "600px", }} > 🏷️ Label Collision Test Suite: @@ -425,7 +425,7 @@ export const AreaChartV2Story: Story = {
Features: Auto-truncation, Collision Detection, Horizontal Offset
- +
From 9496f9efcf6585d1ab1e9e88a89ae634f46ade75 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 10 Jun 2025 14:41:57 +0530 Subject: [PATCH 047/190] feat(dataUtils): add support for secondary colors in chart config The changes in `dataUtils.ts` add support for optional `secondaryColors` in the `getChartConfig` function. This allows for more customization of the chart colors, where the secondary color can be specified separately from the primary color. This is useful for charts that require a secondary color, such as area charts. refactor(AreaChartV2): improve story formatting and readability The changes in `areaChartV2.stories.tsx` focus on improving the formatting and readability of the story data. The long category names and company names are now wrapped in multi-line objects, making the code more readable and maintainable. --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 43 ++++- .../AreaCharts/AreaChartV2/areaChartV2.scss | 6 + .../stories/areaChartV2.stories.tsx | 157 +++++++++++++++--- .../Charts/AreaCharts/utils/AreaChartUtils.ts | 123 +++++++++++++- .../BarCharts/BarChartV2/BarChartV2.tsx | 2 +- .../Charts/shared/XAxisTick/XAxisTick.tsx | 39 ++++- .../src/components/Charts/utils/dataUtils.ts | 3 +- 7 files changed, 335 insertions(+), 38 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index d9036494f..2023bd301 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -17,7 +17,13 @@ import { getDistributedColors, getPalette, PaletteName } from "../../utils/Palle import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; import { getYAxisTickFormatter } from "../../utils/styleUtils"; import { AreaChartV2Data, AreaChartVariant } from "../types"; -import { findNearestSnapPosition, getSnapPositions, getWidthOfData } from "../utils/AreaChartUtils"; +import { + findNearestSnapPosition, + getOptimalXAxisTickFormatter, + getSnapPositions, + getWidthOfData, + getXAxisTickPositionData, +} from "../utils/AreaChartUtils"; export interface AreaChartV2Props { data: T; @@ -68,7 +74,7 @@ export const AreaChartV2 = ({ }, [theme, dataKeys.length]); const chartConfig: ChartConfig = useMemo(() => { - return getChartConfig(dataKeys, colors, icons); + return getChartConfig(dataKeys, colors, undefined, icons); }, [dataKeys, icons, colors]); const chartContainerRef = useRef(null); @@ -100,6 +106,16 @@ export const AreaChartV2 = ({ return height ?? 296; }, [height]); + // Calculate optimal tick formatter for collision detection and truncation + const xAxisTickFormatter = useMemo(() => { + return getOptimalXAxisTickFormatter(data, effectiveContainerWidth); + }, [data, effectiveContainerWidth]); + + // Calculate position data for X-axis tick offset handling + const xAxisPositionData = useMemo(() => { + return getXAxisTickPositionData(data, categoryKey as string); + }, [data, categoryKey]); + // Check scroll boundaries const updateScrollState = useCallback(() => { if (mainContainerRef.current) { @@ -258,7 +274,14 @@ export const AreaChartV2 = ({ axisLine={false} textAnchor="middle" interval={0} - tick={} + tickFormatter={xAxisTickFormatter} + tick={ + + } orientation="bottom" padding={{ left: 20, @@ -293,7 +316,12 @@ export const AreaChartV2 = ({ {dataWidth > effectiveWidth && (
} variant="secondary" onClick={scrollLeft} @@ -301,7 +329,12 @@ export const AreaChartV2 = ({ disabled={!canScrollLeft} /> } variant="secondary" size="extra-small" diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss index 02b959540..5bd4541f6 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss @@ -32,4 +32,10 @@ transform: translateY(-50%); right: 0px; } + + &--disabled { + visibility: hidden; + cursor: not-allowed; + transition: visibility 0.5s ease-in-out; + } } diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx index 37fef7d10..44fa2bdda 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -1,5 +1,14 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Monitor, TabletSmartphone, Calendar, Globe, Smartphone, Laptop, Tv, Watch } from "lucide-react"; +import { + Calendar, + Globe, + Laptop, + Monitor, + Smartphone, + TabletSmartphone, + Tv, + Watch, +} from "lucide-react"; import { useState } from "react"; import { Card } from "../../../../Card"; import { AreaChartV2, AreaChartV2Props } from "../AreaChartV2"; @@ -22,14 +31,54 @@ const dataVariations = { ], // 🏷️ BIG LABELS - Testing collision detection and truncation bigLabels: [ - { category: "Very Long Category Name That Should Be Truncated", sales: 150, revenue: 90, profit: 120 }, - { category: "Another Extremely Long Label That Causes Collisions", sales: 280, revenue: 180, profit: 140 }, - { category: "Super Duper Long Category Name That Tests Truncation", sales: 220, revenue: 140, profit: 160 }, - { category: "Incredibly Long Text That Should Trigger Collision Detection", sales: 180, revenue: 160, profit: 180 }, - { category: "Maximum Length Category Name That Tests All Edge Cases", sales: 250, revenue: 120, profit: 140 }, - { category: "Extra Long Business Category Name With Many Words", sales: 300, revenue: 180, profit: 160 }, - { category: "Comprehensive Long Label For Testing Horizontal Offset", sales: 350, revenue: 220, profit: 180 }, - { category: "Extended Category Name That Pushes Truncation Limits", sales: 400, revenue: 240, profit: 200 }, + { + category: "Very Long Category Name That Should Be Truncated", + sales: 150, + revenue: 90, + profit: 120, + }, + { + category: "Another Extremely Long Label That Causes Collisions", + sales: 280, + revenue: 180, + profit: 140, + }, + { + category: "Super Duper Long Category Name That Tests Truncation", + sales: 220, + revenue: 140, + profit: 160, + }, + { + category: "Incredibly Long Text That Should Trigger Collision Detection", + sales: 180, + revenue: 160, + profit: 180, + }, + { + category: "Maximum Length Category Name That Tests All Edge Cases", + sales: 250, + revenue: 120, + profit: 140, + }, + { + category: "Extra Long Business Category Name With Many Words", + sales: 300, + revenue: 180, + profit: 160, + }, + { + category: "Comprehensive Long Label For Testing Horizontal Offset", + sales: 350, + revenue: 220, + profit: 180, + }, + { + category: "Extended Category Name That Pushes Truncation Limits", + sales: 400, + revenue: 240, + profit: 200, + }, ], // 📅 DENSE TIMELINE - Many items with medium-length labels denseTimeline: [ @@ -53,13 +102,48 @@ const dataVariations = { // 🏢 COMPANY NAMES - Real-world long business names companyNames: [ { company: "Apple Inc.", revenue: 394328000000, profit: 99803000000, marketCap: 3500000000000 }, - { company: "Microsoft Corporation", revenue: 211915000000, profit: 83383000000, marketCap: 2800000000000 }, - { company: "Alphabet Inc. (Google)", revenue: 307394000000, profit: 76033000000, marketCap: 2100000000000 }, - { company: "Amazon.com Inc.", revenue: 574785000000, profit: 33364000000, marketCap: 1600000000000 }, - { company: "Tesla Motors Inc.", revenue: 96773000000, profit: 15000000000, marketCap: 800000000000 }, - { company: "Meta Platforms Inc.", revenue: 134902000000, profit: 39370000000, marketCap: 900000000000 }, - { company: "NVIDIA Corporation", revenue: 60922000000, profit: 29760000000, marketCap: 1800000000000 }, - { company: "Berkshire Hathaway Inc.", revenue: 364482000000, profit: 96223000000, marketCap: 780000000000 }, + { + company: "Microsoft Corporation", + revenue: 211915000000, + profit: 83383000000, + marketCap: 2800000000000, + }, + { + company: "Alphabet Inc. (Google)", + revenue: 307394000000, + profit: 76033000000, + marketCap: 2100000000000, + }, + { + company: "Amazon.com Inc.", + revenue: 574785000000, + profit: 33364000000, + marketCap: 1600000000000, + }, + { + company: "Tesla Motors Inc.", + revenue: 96773000000, + profit: 15000000000, + marketCap: 800000000000, + }, + { + company: "Meta Platforms Inc.", + revenue: 134902000000, + profit: 39370000000, + marketCap: 900000000000, + }, + { + company: "NVIDIA Corporation", + revenue: 60922000000, + profit: 29760000000, + marketCap: 1800000000000, + }, + { + company: "Berkshire Hathaway Inc.", + revenue: 364482000000, + profit: 96223000000, + marketCap: 780000000000, + }, ], // 🌍 COUNTRY NAMES - Geographic labels with varying lengths countryData: [ @@ -95,8 +179,14 @@ const dataVariations = { ], // 🎯 EDGE CASES - Extreme scenarios edgeCases: [ - { name: "SinglePointDataSetForTestingEdgeCasesInCollisionDetectionAndLabelTruncationFunctionality", value: 500 }, - { name: "SecondExtremelyLongDataPointNameThatShouldDefinitelyTriggerTruncationMechanisms", value: 600 }, + { + name: "SinglePointDataSetForTestingEdgeCasesInCollisionDetectionAndLabelTruncationFunctionality", + value: 500, + }, + { + name: "SecondExtremelyLongDataPointNameThatShouldDefinitelyTriggerTruncationMechanisms", + value: 600, + }, ], // 📱 MINIMAL - Small dataset for baseline testing minimal: [ @@ -109,7 +199,7 @@ const dataVariations = { // Category key mappings for different datasets const categoryKeys = { default: "month", - bigLabels: "category", + bigLabels: "category", denseTimeline: "period", companyNames: "company", countryData: "country", @@ -419,10 +509,19 @@ export const AreaChartV2Story: Story = { 📱 Minimal (3 items)
-
-
Current Dataset: {selectedDataType}
-
Items: {currentData.length} | Category Key: {currentCategoryKey}
-
Features: Auto-truncation, Collision Detection, Horizontal Offset
+
+
+ Current Dataset: {selectedDataType} +
+
+ Items: {currentData.length} | Category Key:{" "} + {currentCategoryKey} +
+
+ Features: Auto-truncation, Collision Detection, Horizontal Offset +
@@ -653,19 +752,25 @@ export const ResponsiveWidthStory: Story = { render: (args) => (
-

Small Container (300px)

+

+ Small Container (300px) +

-

Medium Container (500px)

+

+ Medium Container (500px) +

-

Large Container (800px)

+

+ Large Container (800px) +

diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts index 3ea73d89e..1784493e4 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts @@ -42,6 +42,120 @@ const getWidthOfGroup = (data: Array>) => { return ELEMENT_SPACING; }; +/** + * INTERNAL HELPER FUNCTION + * This function returns the formatter for the X-axis tick values with intelligent truncation for AreaChartV2. + * @param groupWidth - The width available for each group/category (optional) + * @param containerWidth - The total container width for responsive calculations (optional) + * @param dataLength - The total length of data for determining first/last positions (optional) + * @returns The formatter for the X-axis tick values. + * Internally used by the XAxis component in Recharts + */ +const getXAxisTickFormatter = ( + groupWidth?: number, + containerWidth?: number, + dataLength?: number, +) => { + const CHAR_WIDTH = 7; // Average character width in pixels for most fonts + const ELLIPSIS_WIDTH = CHAR_WIDTH * 3; // "..." takes about 3 character widths + const PADDING = 2; // Safety padding for better visual spacing + + // closure is happening here. + return (value: string, index?: number) => { + // Convert to string in case we get numbers + const stringValue = String(value); + + // If no groupWidth provided, fall back to simple responsive logic + if (!groupWidth) { + // Use container width for responsive truncation + if (containerWidth) { + // Responsive logic based on container width + if (containerWidth < 400) { + // Small containers: very aggressive truncation + return stringValue.length > 4 ? `${stringValue.slice(0, 4)}...` : stringValue; + } else if (containerWidth < 600) { + // Medium containers: moderate truncation + return stringValue.length > 8 ? `${stringValue.slice(0, 8)}...` : stringValue; + } else { + // Large containers: less aggressive truncation + return stringValue.length > 12 ? `${stringValue.slice(0, 12)}...` : stringValue; + } + } + + // Default fallback when no width info available + return stringValue.length > 6 ? `${stringValue.slice(0, 6)}...` : stringValue; + } + + const availableWidth = Math.max(0, groupWidth - PADDING); + const maxCharsWithoutEllipsis = Math.floor(availableWidth / CHAR_WIDTH); + const maxCharsWithEllipsis = Math.floor((availableWidth - ELLIPSIS_WIDTH) / CHAR_WIDTH); + + // Intelligent ellipsis handling for area charts + if (stringValue.length <= maxCharsWithoutEllipsis) { + // Full text fits comfortably + return stringValue; + } else if (maxCharsWithEllipsis >= 3) { + // We can fit at least 3 characters + ellipsis + return `${stringValue.slice(0, maxCharsWithEllipsis)}...`; + } else { + // Very limited space - just show what we can without ellipsis + // (ellipsis would take more space than it's worth) + return stringValue.slice(0, Math.max(1, Math.min(6, maxCharsWithoutEllipsis))); + } + }; +}; + +/** + * Helper function to get the optimal X-axis tick formatter with calculated group width for AreaChartV2 + * @param data - The chart data + * @param containerWidth - The container width for responsive calculations + * @returns The optimized formatter function + */ +const getOptimalXAxisTickFormatter = ( + data: Array>, + containerWidth?: number, +) => { + // Calculate the available width per group + const groupWidth = getWidthOfGroup(data); + return getXAxisTickFormatter(groupWidth, containerWidth, data.length); +}; + +/** + * Helper function to get position information for X-axis ticks with offset handling + * @param data - The chart data + * @param categoryKey - The category key for the chart + * @returns Object containing position data for the tick renderer + */ +const getXAxisTickPositionData = ( + data: Array>, + categoryKey: string, +) => { + return { + dataLength: data.length, + categoryValues: data.map((item) => String(item[categoryKey])), + getPositionOffset: (value: string): number => { + const index = data.findIndex((item) => String(item[categoryKey]) === value); + if (index === 0) { + // First label: offset to the right by 5px + return 5; + } else if (index === data.length - 1) { + // Last label: offset to the left by 5px + return -5; + } + // Middle labels: no offset + return 0; + }, + isFirstTick: (value: string): boolean => { + const index = data.findIndex((item) => String(item[categoryKey]) === value); + return index === 0; + }, + isLastTick: (value: string): boolean => { + const index = data.findIndex((item) => String(item[categoryKey]) === value); + return index === data.length - 1; + }, + }; +}; + /** * This function returns the snap positions for the area chart, used for smooth scrolling * @param data - The data to be displayed in the chart. @@ -125,4 +239,11 @@ export const createChartConfig = ({ ); }; -export { findNearestSnapPosition, getSnapPositions, getWidthOfData }; +export { + findNearestSnapPosition, + getOptimalXAxisTickFormatter, + getSnapPositions, + getWidthOfData, + getXAxisTickFormatter, + getXAxisTickPositionData, +}; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 2f309df2c..12ae6a950 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -83,7 +83,7 @@ const BarChartV2Component = ({ }, [theme, dataKeys.length]); const chartConfig: ChartConfig = useMemo(() => { - return getChartConfig(dataKeys, colors, icons); + return getChartConfig(dataKeys, colors, undefined, icons); }, [dataKeys, icons, colors]); const chartContainerRef = useRef(null); diff --git a/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx index 72b6666af..97d8c27bd 100644 --- a/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx @@ -20,20 +20,51 @@ interface XAxisTickProps { tickFormatter?: (value: any) => string; index?: number; visibleTicksCount?: number; + // Extended props for position-based offset handling + getPositionOffset?: (value: string) => number; + isFirstTick?: (value: string) => boolean; + isLastTick?: (value: string) => boolean; } const XAxisTick: React.FC = (props) => { - const { x, y, payload, textAnchor = "middle", fill = "#666", tickFormatter, className } = props; + const { + x, + y, + payload, + textAnchor = "middle", + fill = "#666", + tickFormatter, + className, + getPositionOffset, + isFirstTick, + isLastTick, + } = props; - const displayValue = tickFormatter ? tickFormatter(payload?.value) : String(payload?.value || ""); + const value = String(payload?.value || ""); + const displayValue = tickFormatter ? tickFormatter(payload?.value) : value; + + // Calculate position offset for first and last labels (optional extension) + let xOffset = 0; + if (getPositionOffset) { + xOffset = getPositionOffset(value); + } + + // Optional text anchor adjustment for first and last labels + // if the text need to get adjusted then we can do so from here + let adjustedTextAnchor = textAnchor; + if (isFirstTick && isFirstTick(value)) { + adjustedTextAnchor = "middle"; + } else if (isLastTick && isLastTick(value)) { + adjustedTextAnchor = "middle"; + } return ( diff --git a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts index 13e1f8a23..daab455da 100644 --- a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts @@ -24,6 +24,7 @@ export const getDataKeys = ( export const getChartConfig = ( dataKeys: string[], colors: string[], + secondaryColors?: string[], icons?: Partial>, ): ChartConfig => { return dataKeys.reduce( @@ -33,7 +34,7 @@ export const getChartConfig = ( label: key, icon: icons?.[key], color: colors[index], - secondaryColor: colors[dataKeys.length - index - 1], + secondaryColor: secondaryColors?.[index] || colors[dataKeys.length - index - 1], }, }), {}, From 099e4d0b3cd5febf33d498f4730cecb35ecc8462 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 10 Jun 2025 15:30:43 +0530 Subject: [PATCH 048/190] minor commenting --- .../components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index 2023bd301..517a78db3 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -90,7 +90,7 @@ export const AreaChartV2 = ({ const effectiveContainerWidth = useMemo(() => { const yAxisWidth = showYAxis ? Y_AXIS_WIDTH : 0; - return Math.max(0, effectiveWidth - yAxisWidth); + return Math.max(0, effectiveWidth - yAxisWidth - 40); // -40 because we are giving 20px padding in xAxis on each side }, [effectiveWidth, showYAxis]); const dataWidth = useMemo(() => { From 6b80156bd25aac190486450e29f3b05b0b78311e Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 10 Jun 2025 16:00:07 +0530 Subject: [PATCH 049/190] feat(mini-area-chart): Enhance MiniAreaChart component This change enhances the MiniAreaChart component in the following ways: - Adds support for responsive sizing by observing the container's width and rendering the chart accordingly. - Transforms the input data to a consistent format for Recharts. - Calculates the optimal padding for the chart based on the data. - Allows customizing the area color and adding a click handler. - Improves the overall code structure and readability. The main focus of this change is to make the MiniAreaChart more flexible and adaptable to different use cases. --- .../AreaChartV2/components/ActiveDot.tsx | 0 .../MiniAreaChart/MiniAreaChart.tsx | 135 +++++-- .../stories/MiniAreaChart.stories.tsx | 375 +++++++++++------- .../MiniAreaChart/utils/miniAreaChartUtils.ts | 95 +++++ .../Charts/AreaCharts/types/index.ts | 2 +- 5 files changed, 416 insertions(+), 191 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/components/ActiveDot.tsx create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/components/ActiveDot.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/components/ActiveDot.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx index f2fdf8855..3ea6d6109 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx @@ -1,56 +1,117 @@ -import React from "react"; +import clsx from "clsx"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Area, AreaChart as RechartsAreaChart, XAxis } from "recharts"; -import { ChartContainer, keyTransform } from "../../Charts"; +import { ChartConfig, ChartContainer } from "../../Charts"; +import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; import { MiniAreaChartData } from "../types"; -import { createChartConfig } from "../utils/AreaChartUtils"; +import { + getPadding, + getRecentDataThatFits, + transformDataForChart, +} from "./utils/miniAreaChartUtils"; -export interface MiniAreaChartProps { - data: T; - categoryKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; +export interface MiniAreaChartProps { + data: MiniAreaChartData; + theme?: PaletteName; variant?: "linear" | "natural" | "step"; opacity?: number; - icons?: Partial>; isAnimationActive?: boolean; + onAreaClick?: (data: any) => void; + size?: number | string; + className?: string; + areaColor?: string; } -export const MiniAreaChart = ({ +export const MiniAreaChart = ({ data, - categoryKey, theme = "ocean", variant = "natural", opacity = 0.5, - icons = {}, isAnimationActive = true, -}: MiniAreaChartProps) => { - const chartConfig = createChartConfig({ data, categoryKey: categoryKey as string, theme, icons }); + onAreaClick, + size = "100%", + className, + areaColor, +}: MiniAreaChartProps) => { + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + + useEffect(() => { + if (!containerRef.current) { + return () => {}; + } + + const resizeObserver = new ResizeObserver((entries) => { + // there is only one entry in the entries array because we are only observing the chart container + for (const entry of entries) { + setContainerWidth(entry.contentRect.width); + } + }); + + resizeObserver.observe(containerRef.current); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + // Get the most recent data that fits in the container + const filteredData = useMemo(() => { + return getRecentDataThatFits(data, containerWidth); + }, [data, containerWidth]); + + // Transform the filtered data to a consistent format for recharts + const chartData = useMemo(() => { + return transformDataForChart(filteredData); + }, [filteredData]); + + const colors = useMemo(() => { + const palette = getPalette(theme); + return getDistributedColors(palette, 1); // Single color for 1D chart + }, [theme]); + + const chartConfig: ChartConfig = useMemo(() => { + return { + value: { + label: "Value", + color: areaColor ? areaColor : colors[0], + }, + }; + }, [colors, areaColor]); + + const padding = useMemo(() => { + return getPadding(filteredData, containerWidth); + }, [filteredData, containerWidth]); return ( - - - + + + - {Object.keys(chartConfig).map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - return ( - - ); - })} + ); diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx index b87efeac7..ac23d8b45 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx @@ -1,23 +1,32 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Monitor, TabletSmartphone } from "lucide-react"; import { Card } from "../../../../Card"; import { MiniAreaChart, MiniAreaChartProps } from "../MiniAreaChart"; -const areaChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, +// Simple array of numbers for 1D area chart +const simpleAreaChartData = [ + 12, 45, 78, 32, 67, 89, 23, 56, 91, 34, 76, 28, 85, 42, 19, 63, 87, 31, 74, 58, 92, 26, 49, 83, + 37, 50, ]; -const icons = { - desktop: Monitor, - mobile: TabletSmartphone, -} as const; +// Array of objects with value and label +const labeledAreaChartData = [ + { value: 150, label: "January" }, + { value: 280, label: "February" }, + { value: 220, label: "March" }, + { value: 180, label: "April" }, + { value: 250, label: "May" }, + { value: 300, label: "June" }, + { value: 320, label: "July" }, + { value: 280, label: "August" }, + { value: 310, label: "September" }, + { value: 290, label: "October" }, + { value: 340, label: "November" }, + { value: 360, label: "December" }, + // { value: 360, label: "December" }, + // { value: 360, label: "December" }, +]; -const meta: Meta> = { +const meta: Meta = { title: "Components/Charts/AreaCharts/MiniAreaChart", component: MiniAreaChart, parameters: { @@ -25,35 +34,25 @@ const meta: Meta> = { docs: { description: { component: - "```tsx\nimport { MiniAreaChart } from '@crayon-ui/react-ui/Charts/AreaCharts/MiniAreaChart';\n```", + "```tsx\nimport { MiniAreaChart } from '@crayon-ui/react-ui/Charts/AreaCharts/MiniAreaChart';\n```\n\nA responsive mini area chart component that accepts 1D data (numbers or objects with value/label) with automatic data filtering for space-constrained containers.", }, }, }, - tags: ["!dev", "autodocs"], + tags: ["dev", "autodocs"], argTypes: { data: { description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", + "An array of numbers or an array of objects with value and optional label. Each entry represents a single point in the area chart.", control: false, table: { - type: { summary: "Array>" }, + type: { summary: "Array | Array<{ value: number; label?: string }>" }, defaultValue: { summary: "[]" }, category: "Data", }, }, - categoryKey: { - description: - "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", - control: false, - table: { - type: { summary: "string" }, - defaultValue: { summary: "string" }, - category: "Data", - }, - }, theme: { description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + "The color palette theme for the chart. Each theme provides a different color for the area.", control: "select", options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], table: { @@ -61,16 +60,6 @@ const meta: Meta> = { category: "Appearance", }, }, - icons: { - description: - "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", - control: false, - table: { - type: { summary: "Record" }, - defaultValue: { summary: "{}" }, - category: "Appearance", - }, - }, variant: { description: "The interpolation method used to create the area curves. 'linear' creates straight lines between points, 'natural' creates smooth curves, and 'step' creates a stepped area.", @@ -83,44 +72,16 @@ const meta: Meta> = { }, opacity: { description: - "The opacity of the filled area beneath each line (0 = fully transparent, 1 = fully opaque)", - control: false, + "The opacity of the filled area beneath the line (0 = fully transparent, 1 = fully opaque)", + control: "number", table: { type: { summary: "number" }, defaultValue: { summary: "0.5" }, category: "Appearance", }, }, - // grid: { - // description: "Whether to display the background grid lines in the chart", - // control: "boolean", - // table: { - // type: { summary: "boolean" }, - // defaultValue: { summary: "true" }, - // category: "Display", - // }, - // }, - // label: { - // description: "Whether to display data point labels above each point on the chart", - // control: "boolean", - // table: { - // type: { summary: "boolean" }, - // defaultValue: { summary: "true" }, - // category: "Display", - // }, - // }, - // legend: { - // description: - // "Whether to display the chart legend showing the data series names and their corresponding colors/icons", - // control: "boolean", - // table: { - // type: { summary: "boolean" }, - // defaultValue: { summary: "true" }, - // category: "Display", - // }, - // }, isAnimationActive: { - description: "Whether to animate the chart", + description: "Whether to animate the chart when it first renders", control: "boolean", table: { type: { summary: "boolean" }, @@ -128,51 +89,42 @@ const meta: Meta> = { category: "Display", }, }, - // showYAxis: { - // description: "Whether to display the y-axis", - // control: "boolean", - // table: { - // type: { summary: "boolean" }, - // defaultValue: { summary: "false" }, - // category: "Display", - // }, - // }, - // xAxisLabel: { - // description: "The label for the x-axis", - // control: false, - // table: { - // type: { summary: "string" }, - // defaultValue: { summary: "string" }, - // category: "Data", - // }, - // }, - // yAxisLabel: { - // description: "The label for the y-axis", - // control: false, - // table: { - // type: { summary: "string" }, - // defaultValue: { summary: "string" }, - // category: "Data", - // }, - // }, + size: { + description: "The width and height of the chart", + control: "text", + table: { + type: { summary: "number | string" }, + defaultValue: { summary: "100%" }, + category: "Appearance", + }, + }, + areaColor: { + description: "Custom color for the area fill and stroke", + control: "color", + table: { + type: { summary: "string" }, + category: "Appearance", + }, + }, }, } satisfies Meta; export default meta; type Story = StoryObj; -export const MiniAreaChartStory: Story = { - name: "Mini Area Chart", +export const SimpleNumberArray: Story = { + name: "Simple Number Array", args: { - data: areaChartData, - categoryKey: "month", + data: simpleAreaChartData, theme: "ocean", variant: "natural", opacity: 0.5, isAnimationActive: true, + size: "100%", }, - render: (args) => ( - + render: (args: MiniAreaChartProps) => ( + +

Daily Activity

), @@ -180,38 +132,35 @@ export const MiniAreaChartStory: Story = { docs: { source: { code: ` -const areaChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; +const activityData = [12, 45, 78, 32, 67, 89, 23, 56, 91, 34]; - - -`, + +`, }, }, }, }; -export const MiniAreaChartStoryWithIcons: Story = { - name: "Mini Area Chart with Icons", +export const LabeledData: Story = { + name: "Labeled Data", args: { - ...MiniAreaChartStory.args, - icons: icons, + data: labeledAreaChartData, + theme: "emerald", + variant: "natural", + opacity: 0.6, + isAnimationActive: true, + size: "100%", }, - render: (args) => ( - + render: (args: MiniAreaChartProps) => ( + +

Monthly Revenue

), @@ -219,34 +168,154 @@ export const MiniAreaChartStoryWithIcons: Story = { docs: { source: { code: ` - import { Monitor, TabletSmartphone } from "lucide-react"; - - const areaChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, - ]; - - const icons = { - desktop: Monitor, - mobile: TabletSmartphone, - }; - - - - `, +const revenueData = [ + { value: 150, label: "January" }, + { value: 280, label: "February" }, + { value: 220, label: "March" }, + // ... +]; + + +`, }, }, }, }; + +export const ResponsiveData: Story = { + name: "Responsive Data Filtering", + args: { + data: simpleAreaChartData, + theme: "orchid", + variant: "natural", + opacity: 0.4, + isAnimationActive: true, + size: "100%", + }, + render: (args) => ( +
+ +

Small (200px)

+ +
+ +

+ Medium (400px) +

+ +
+ +

Large (600px)

+ +
+
+ ), +}; + +export const DifferentSizes: Story = { + name: "Different Sizes", + render: () => ( +
+ +

Small

+ +
+ +

Medium

+ +
+ +

Large

+ +
+
+ ), +}; + +export const DifferentThemes: Story = { + name: "Different Themes", + render: () => ( +
+ {(["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"] as const).map((theme) => ( + +

+ {theme} Theme +

+ +
+ ))} +
+ ), +}; + +export const DifferentVariants: Story = { + name: "Different Variants", + render: () => ( +
+ {(["linear", "natural", "step"] as const).map((variant) => ( + +

+ {variant} Variant +

+ +
+ ))} +
+ ), +}; + +export const CustomColor: Story = { + name: "Custom Color", + args: { + data: [20, 45, 28, 80, 99, 43, 67, 23, 89, 56], + theme: "ocean", + variant: "natural", + opacity: 0.7, + areaColor: "#ff6b6b", + size: 200, + }, + render: (args) => ( + +

Custom Red Area

+ +
+ ), +}; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts new file mode 100644 index 000000000..1cf32fb8f --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts @@ -0,0 +1,95 @@ +import { type MiniAreaChartData } from "../../types"; + +export const MINI_ELEMENT_SPACING: number = 20; + +const CONTAINER_HORIZONTAL_PADDING: number = 0; // 0px left + 0px right as this is a area char where data starts from 0th position + +type ChartData = Array<{ + value: number; + label: string; +}>; + +/** + * Transforms the mini area chart data into a standardized format for rendering. + * Handles both numeric values and objects with value/label properties. + * + * @param data - The mini area chart data array (can contain numbers or objects with value/label) + * @returns An array of chart data objects with value and label properties + */ +const transformDataForChart = (data: MiniAreaChartData): ChartData => { + return data.map((item, index) => { + if (typeof item === "number") { + return { value: item, label: `Item ${index + 1}` }; + } else { + return { value: item.value, label: item.label || `Item ${index + 1}` }; + } + }); +}; + +/** + * Calculates the total width of the data. + * + * @param data - The mini area chart data array + * @returns The total width needed in pixels to display all data items + */ +const getWidthOfData = (data: MiniAreaChartData) => { + return data.length * MINI_ELEMENT_SPACING; +}; + +/** + * Calculates the left and right padding for the chart container based on available space. + * If the chart data exceeds the container width, no padding is applied. + * + * @param data - The mini area chart data array + * @param containerWidth - The total width of the container in pixels + * @returns An object with left and right padding values in pixels + */ +const getPadding = (data: MiniAreaChartData, containerWidth: number) => { + const availableWidth = containerWidth - CONTAINER_HORIZONTAL_PADDING; + const chartWidth = getWidthOfData(data); + const paddingValue = availableWidth - chartWidth; + + if (paddingValue < 0) { + return { + left: 0, + right: 0, + }; + } + return { + left: paddingValue, + right: 0, + }; +}; + +/** + * Filters the data to include only the most recent items that can fit within the container width. + * This function ensures the chart displays the latest data when space is limited. + * + * @param data - The complete mini area chart data array + * @param containerWidth - The total width of the container in pixels + * @returns A filtered array containing only the most recent data items that fit in the container + */ +const getRecentDataThatFits = ( + data: MiniAreaChartData, + containerWidth: number, +): MiniAreaChartData => { + if (containerWidth <= 0 || data.length === 0) { + return data; + } + + // Subtract padding to get actual available width for chart content + const availableWidth = containerWidth - CONTAINER_HORIZONTAL_PADDING; + + // Calculate how many items can fit in the available space + const maxItems = Math.floor(availableWidth / MINI_ELEMENT_SPACING); + + // If all items fit, return all data + if (maxItems >= data.length) { + return data; + } + + // Return the most recent items that fit + return data.slice(-maxItems); +}; + +export { getPadding, getRecentDataThatFits, transformDataForChart }; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts index 7a9e36730..a9712758b 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts @@ -2,4 +2,4 @@ export type AreaChartVariant = "linear" | "natural" | "step"; export type AreaChartV2Data = Array>; -export type MiniAreaChartData = Array>; +export type MiniAreaChartData = Array | Array<{ value: number; label?: string }>; From a1a684f5a4e0b62166c7c0270c32fa6ba36d2059 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 10 Jun 2025 17:38:18 +0530 Subject: [PATCH 050/190] hiding scroll bar --- .../Charts/AreaCharts/AreaChartV2/areaChartV2.scss | 10 ++++++++++ .../Charts/BarCharts/BarChartV2/barChar.scss | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss index 5bd4541f6..5e55eabb6 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss @@ -12,6 +12,16 @@ .crayon-area-chart-main-container { width: 100%; overflow-x: auto; + /* Hide scrollbar for Chrome, Safari and Opera */ + &::-webkit-scrollbar { + display: none; + } + + /* Hide scrollbar for Firefox */ + scrollbar-width: none; + + /* Hide scrollbar for IE and Edge */ + -ms-overflow-style: none; } .crayon-area-chart-scroll-container { diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss index 80703e6b5..645640d35 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss @@ -12,6 +12,16 @@ .crayon-bar-chart-main-container { width: 100%; overflow-x: auto; + /* Hide scrollbar for Chrome, Safari and Opera */ + &::-webkit-scrollbar { + display: none; + } + + /* Hide scrollbar for Firefox */ + scrollbar-width: none; + + /* Hide scrollbar for IE and Edge */ + -ms-overflow-style: none; } .crayon-bar-chart-scroll-container { From deeeaf007a748cef21ae84743155fef2331136d5 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 10 Jun 2025 18:12:30 +0530 Subject: [PATCH 051/190] feat(mini-area-chart): remove unused utility functions The changes remove the `getPadding` and `getWidthOfData` utility functions from the `miniAreaChartUtils.ts` file, as they are no longer needed. The `getRecentDataThatFits` and `transformDataForChart` functions remain, as they are still required for the mini area chart functionality. Additionally, the changes in `AreaChartUtils.ts` update the type definitions for the `getWidthOfData`, `getWidthOfGroup`, `getOptimalXAxisTickFormatter`, and `getXAxisTickPositionData` functions to use the `AreaChartData` type instead of the generic `Array>`. This ensures that the functions work with the expected data structure for area charts. --- .../MiniAreaChart/MiniAreaChart.tsx | 32 +++--- .../stories/MiniAreaChart.stories.tsx | 98 +++++++++++++++++-- .../MiniAreaChart/utils/miniAreaChartUtils.ts | 66 ++++++------- .../Charts/AreaCharts/utils/AreaChartUtils.ts | 17 ++-- 4 files changed, 149 insertions(+), 64 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx index 3ea6d6109..17995cb4e 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx @@ -1,14 +1,10 @@ import clsx from "clsx"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useId, useMemo, useRef, useState } from "react"; import { Area, AreaChart as RechartsAreaChart, XAxis } from "recharts"; import { ChartConfig, ChartContainer } from "../../Charts"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; import { MiniAreaChartData } from "../types"; -import { - getPadding, - getRecentDataThatFits, - transformDataForChart, -} from "./utils/miniAreaChartUtils"; +import { getRecentDataThatFits, transformDataForChart } from "./utils/miniAreaChartUtils"; export interface MiniAreaChartProps { data: MiniAreaChartData; @@ -20,6 +16,7 @@ export interface MiniAreaChartProps { size?: number | string; className?: string; areaColor?: string; + useGradient?: boolean; } export const MiniAreaChart = ({ @@ -32,6 +29,7 @@ export const MiniAreaChart = ({ size = "100%", className, areaColor, + useGradient = true, }: MiniAreaChartProps) => { const containerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(0); @@ -79,9 +77,10 @@ export const MiniAreaChart = ({ }; }, [colors, areaColor]); - const padding = useMemo(() => { - return getPadding(filteredData, containerWidth); - }, [filteredData, containerWidth]); + const id = useId(); + + // Generate unique gradient ID to avoid conflicts when multiple charts are on the same page + const gradientId = useMemo(() => `miniAreaGradient-${id}`, [id]); return ( - + {useGradient && ( + + + + + + + )} + + diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx index ac23d8b45..be5dbb0f0 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx @@ -34,7 +34,7 @@ const meta: Meta = { docs: { description: { component: - "```tsx\nimport { MiniAreaChart } from '@crayon-ui/react-ui/Charts/AreaCharts/MiniAreaChart';\n```\n\nA responsive mini area chart component that accepts 1D data (numbers or objects with value/label) with automatic data filtering for space-constrained containers.", + "```tsx\nimport { MiniAreaChart } from '@crayon-ui/react-ui/Charts/AreaCharts/MiniAreaChart';\n```\n\nA responsive mini area chart component that accepts 1D data (numbers or objects with value/label) with automatic data filtering for space-constrained containers. Features linear gradient fills from color to transparent.", }, }, }, @@ -72,7 +72,7 @@ const meta: Meta = { }, opacity: { description: - "The opacity of the filled area beneath the line (0 = fully transparent, 1 = fully opaque)", + "The opacity of the filled area beneath the line (0 = fully transparent, 1 = fully opaque). Only used when useGradient is false.", control: "number", table: { type: { summary: "number" }, @@ -80,6 +80,16 @@ const meta: Meta = { category: "Appearance", }, }, + useGradient: { + description: + "Whether to use a linear gradient fill that goes from the area color at the top to transparent at the bottom.", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Appearance", + }, + }, isAnimationActive: { description: "Whether to animate the chart when it first renders", control: "boolean", @@ -113,12 +123,13 @@ export default meta; type Story = StoryObj; export const SimpleNumberArray: Story = { - name: "Simple Number Array", + name: "Simple Number Array (With Gradient)", args: { data: simpleAreaChartData, theme: "ocean", variant: "natural", opacity: 0.5, + useGradient: true, isAnimationActive: true, size: "100%", }, @@ -138,7 +149,7 @@ const activityData = [12, 45, 78, 32, 67, 89, 23, 56, 91, 34]; data={activityData} theme="ocean" variant="natural" - opacity={0.5} + useGradient={true} isAnimationActive={true} size="100%" /> @@ -149,12 +160,13 @@ const activityData = [12, 45, 78, 32, 67, 89, 23, 56, 91, 34]; }; export const LabeledData: Story = { - name: "Labeled Data", + name: "Labeled Data (With Gradient)", args: { data: labeledAreaChartData, theme: "emerald", variant: "natural", opacity: 0.6, + useGradient: true, isAnimationActive: true, size: "100%", }, @@ -179,7 +191,7 @@ const revenueData = [ data={revenueData} theme="emerald" variant="natural" - opacity={0.6} + useGradient={true} size="100%" /> `, @@ -188,6 +200,72 @@ const revenueData = [ }, }; +export const WithoutGradient: Story = { + name: "Solid Fill (Without Gradient)", + args: { + data: simpleAreaChartData.slice(0, 15), + theme: "sunset", + variant: "natural", + opacity: 0.4, + useGradient: false, + isAnimationActive: true, + size: "100%", + }, + render: (args: MiniAreaChartProps) => ( + +

Solid Fill Area

+ +
+ ), + parameters: { + docs: { + source: { + code: ` + +`, + }, + }, + }, +}; + +export const GradientComparison: Story = { + name: "Gradient vs Solid Comparison", + render: () => ( +
+ +

+ With Gradient (Default) +

+ +
+ +

Solid Fill

+ +
+
+ ), +}; + export const ResponsiveData: Story = { name: "Responsive Data Filtering", args: { @@ -195,6 +273,7 @@ export const ResponsiveData: Story = { theme: "orchid", variant: "natural", opacity: 0.4, + useGradient: true, isAnimationActive: true, size: "100%", }, @@ -239,7 +318,7 @@ export const DifferentSizes: Story = { }; export const DifferentThemes: Story = { - name: "Different Themes", + name: "Different Themes (All With Gradients)", render: () => (
{(["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"] as const).map((theme) => ( @@ -258,6 +337,7 @@ export const DifferentThemes: Story = { data={[15, 25, 20, 35, 30, 18, 22, 28, 33, 19]} theme={theme} variant="natural" + useGradient={true} size={160} /> @@ -286,6 +366,7 @@ export const DifferentVariants: Story = { data={[15, 35, 25, 45, 30, 50, 28, 42, 38, 29]} theme="spectrum" variant={variant} + useGradient={true} size={180} /> @@ -295,13 +376,14 @@ export const DifferentVariants: Story = { }; export const CustomColor: Story = { - name: "Custom Color", + name: "Custom Color with Gradient", args: { data: [20, 45, 28, 80, 99, 43, 67, 23, 89, 56], theme: "ocean", variant: "natural", opacity: 0.7, areaColor: "#ff6b6b", + useGradient: true, size: 200, }, render: (args) => ( diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts index 1cf32fb8f..9c78169f2 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts @@ -26,40 +26,40 @@ const transformDataForChart = (data: MiniAreaChartData): ChartData => { }); }; -/** - * Calculates the total width of the data. - * - * @param data - The mini area chart data array - * @returns The total width needed in pixels to display all data items - */ -const getWidthOfData = (data: MiniAreaChartData) => { - return data.length * MINI_ELEMENT_SPACING; -}; +// /** +// * Calculates the total width of the data. +// * +// * @param data - The mini area chart data array +// * @returns The total width needed in pixels to display all data items +// */ +// const getWidthOfData = (data: MiniAreaChartData) => { +// return data.length * MINI_ELEMENT_SPACING; +// }; -/** - * Calculates the left and right padding for the chart container based on available space. - * If the chart data exceeds the container width, no padding is applied. - * - * @param data - The mini area chart data array - * @param containerWidth - The total width of the container in pixels - * @returns An object with left and right padding values in pixels - */ -const getPadding = (data: MiniAreaChartData, containerWidth: number) => { - const availableWidth = containerWidth - CONTAINER_HORIZONTAL_PADDING; - const chartWidth = getWidthOfData(data); - const paddingValue = availableWidth - chartWidth; +// /** +// * Calculates the left and right padding for the chart container based on available space. +// * If the chart data exceeds the container width, no padding is applied. +// * +// * @param data - The mini area chart data array +// * @param containerWidth - The total width of the container in pixels +// * @returns An object with left and right padding values in pixels +// */ +// const getPadding = (data: MiniAreaChartData, containerWidth: number) => { +// const availableWidth = containerWidth - CONTAINER_HORIZONTAL_PADDING; +// const chartWidth = getWidthOfData(data); +// const paddingValue = availableWidth - chartWidth; - if (paddingValue < 0) { - return { - left: 0, - right: 0, - }; - } - return { - left: paddingValue, - right: 0, - }; -}; +// if (paddingValue < 0) { +// return { +// left: 0, +// right: 0, +// }; +// } +// return { +// left: paddingValue, +// right: 0, +// }; +// }; /** * Filters the data to include only the most recent items that can fit within the container width. @@ -92,4 +92,4 @@ const getRecentDataThatFits = ( return data.slice(-maxItems); }; -export { getPadding, getRecentDataThatFits, transformDataForChart }; +export { getRecentDataThatFits, transformDataForChart }; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts index 1784493e4..adcd48784 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts @@ -1,3 +1,4 @@ +import { AreaChartData } from "../../AreaChart/AreaChart"; import { ChartConfig } from "../../Charts"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; import { MiniAreaChartData } from "../types"; @@ -9,7 +10,7 @@ const ELEMENT_SPACING = 70; * for the width of the chart container. * @param data - The data to be displayed in the chart. */ -const getWidthOfData = (data: Array>) => { +const getWidthOfData = (data: AreaChartData) => { // For area charts, we calculate based on the number of data points (always stacked) const numberOfElements = data.length; // Number of data points @@ -35,7 +36,7 @@ const getWidthOfData = (data: Array>) => { * @param data - The data to be displayed in the chart. * @param categoryKey - The key of the category to be displayed in the chart. */ -const getWidthOfGroup = (data: Array>) => { +const getWidthOfGroup = (data: AreaChartData) => { if (data.length === 0) return 200; // Fallback // For area charts, each category/data point has the same spacing @@ -111,10 +112,7 @@ const getXAxisTickFormatter = ( * @param containerWidth - The container width for responsive calculations * @returns The optimized formatter function */ -const getOptimalXAxisTickFormatter = ( - data: Array>, - containerWidth?: number, -) => { +const getOptimalXAxisTickFormatter = (data: AreaChartData, containerWidth?: number) => { // Calculate the available width per group const groupWidth = getWidthOfGroup(data); return getXAxisTickFormatter(groupWidth, containerWidth, data.length); @@ -126,10 +124,7 @@ const getOptimalXAxisTickFormatter = ( * @param categoryKey - The category key for the chart * @returns Object containing position data for the tick renderer */ -const getXAxisTickPositionData = ( - data: Array>, - categoryKey: string, -) => { +const getXAxisTickPositionData = (data: AreaChartData, categoryKey: string) => { return { dataLength: data.length, categoryValues: data.map((item) => String(item[categoryKey])), @@ -162,7 +157,7 @@ const getXAxisTickPositionData = ( * @param categoryKey - The key of the category to be displayed in the chart. * @returns The snap positions for the chart. */ -const getSnapPositions = (data: Array>): number[] => { +const getSnapPositions = (data: AreaChartData): number[] => { if (data.length === 0) return [0]; const positions = [0]; // Start position From 63a74c02e01bb88b8658b53b18229a51c5323dbf Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 10 Jun 2025 18:16:36 +0530 Subject: [PATCH 052/190] feat(AreaChartUtils): optimize x-axis tick formatter Optimize the x-axis tick formatter function by removing the `dataLength` parameter, as it is not used in the function. This simplifies the function and makes it more efficient. --- .../Charts/AreaCharts/utils/AreaChartUtils.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts index adcd48784..7b257925d 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts @@ -52,17 +52,13 @@ const getWidthOfGroup = (data: AreaChartData) => { * @returns The formatter for the X-axis tick values. * Internally used by the XAxis component in Recharts */ -const getXAxisTickFormatter = ( - groupWidth?: number, - containerWidth?: number, - dataLength?: number, -) => { +const getXAxisTickFormatter = (groupWidth?: number, containerWidth?: number) => { const CHAR_WIDTH = 7; // Average character width in pixels for most fonts const ELLIPSIS_WIDTH = CHAR_WIDTH * 3; // "..." takes about 3 character widths const PADDING = 2; // Safety padding for better visual spacing // closure is happening here. - return (value: string, index?: number) => { + return (value: string) => { // Convert to string in case we get numbers const stringValue = String(value); @@ -115,7 +111,7 @@ const getXAxisTickFormatter = ( const getOptimalXAxisTickFormatter = (data: AreaChartData, containerWidth?: number) => { // Calculate the available width per group const groupWidth = getWidthOfGroup(data); - return getXAxisTickFormatter(groupWidth, containerWidth, data.length); + return getXAxisTickFormatter(groupWidth, containerWidth); }; /** From 855657c82eb5f4cac77bb7a040b9d0ec012cb445 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 10 Jun 2025 18:28:16 +0530 Subject: [PATCH 053/190] feat(mini-area-chart): optimize data rendering for limited space Optimize the rendering of mini area chart data to ensure the latest data is displayed when the container width is limited. Remove the unnecessary `CONTAINER_HORIZONTAL_PADDING` constant and update the `getFilteredData` function to calculate the maximum number of items that can fit in the available container width. --- .../MiniAreaChart/utils/miniAreaChartUtils.ts | 44 ++----------------- 1 file changed, 3 insertions(+), 41 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts index 9c78169f2..981de20a2 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts @@ -2,8 +2,6 @@ import { type MiniAreaChartData } from "../../types"; export const MINI_ELEMENT_SPACING: number = 20; -const CONTAINER_HORIZONTAL_PADDING: number = 0; // 0px left + 0px right as this is a area char where data starts from 0th position - type ChartData = Array<{ value: number; label: string; @@ -26,41 +24,6 @@ const transformDataForChart = (data: MiniAreaChartData): ChartData => { }); }; -// /** -// * Calculates the total width of the data. -// * -// * @param data - The mini area chart data array -// * @returns The total width needed in pixels to display all data items -// */ -// const getWidthOfData = (data: MiniAreaChartData) => { -// return data.length * MINI_ELEMENT_SPACING; -// }; - -// /** -// * Calculates the left and right padding for the chart container based on available space. -// * If the chart data exceeds the container width, no padding is applied. -// * -// * @param data - The mini area chart data array -// * @param containerWidth - The total width of the container in pixels -// * @returns An object with left and right padding values in pixels -// */ -// const getPadding = (data: MiniAreaChartData, containerWidth: number) => { -// const availableWidth = containerWidth - CONTAINER_HORIZONTAL_PADDING; -// const chartWidth = getWidthOfData(data); -// const paddingValue = availableWidth - chartWidth; - -// if (paddingValue < 0) { -// return { -// left: 0, -// right: 0, -// }; -// } -// return { -// left: paddingValue, -// right: 0, -// }; -// }; - /** * Filters the data to include only the most recent items that can fit within the container width. * This function ensures the chart displays the latest data when space is limited. @@ -77,11 +40,10 @@ const getRecentDataThatFits = ( return data; } - // Subtract padding to get actual available width for chart content - const availableWidth = containerWidth - CONTAINER_HORIZONTAL_PADDING; - // Calculate how many items can fit in the available space - const maxItems = Math.floor(availableWidth / MINI_ELEMENT_SPACING); + const maxItems = Math.floor((containerWidth + 20) / MINI_ELEMENT_SPACING); + // +20 because the element spacing is between so if we have 2 element then its data 20px data + // so we need to add 20px to the container width to get the actual width of the data // If all items fit, return all data if (maxItems >= data.length) { From 190b5bea3799febfce1364ecfc5b43028afc66c7 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 10 Jun 2025 18:33:00 +0530 Subject: [PATCH 054/190] feat(MiniAreaChart): Adjust gradient opacity Reduces the gradient opacity from 0.8 to 0.6 for the start of the gradient in the MiniAreaChart component. This change improves the visual appearance of the chart by making the gradient less pronounced. --- .../Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx index 17995cb4e..8caf1038e 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx @@ -103,7 +103,7 @@ export const MiniAreaChart = ({ {useGradient && ( - + From 921b3693c48f0fb0aa74b7b7a1d977ffd17f2d6e Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 11 Jun 2025 15:23:16 +0530 Subject: [PATCH 055/190] feat(AreaChartV2): add gradient fill and active dot The changes made in this commit include: 1. Added a new component `ActiveDot` to display a dot when the area chart is hovered over. 2. Implemented a gradient fill for the area chart instead of a solid color fill. 3. Adjusted the styles for the scroll buttons to have a consistent --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 36 ++++++++++++------- .../AreaCharts/AreaChartV2/areaChartV2.scss | 7 +++- .../AreaChartV2/components/ActiveDot.tsx | 33 +++++++++++++++++ 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index 517a78db3..89d510fdc 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -24,6 +24,7 @@ import { getWidthOfData, getXAxisTickPositionData, } from "../utils/AreaChartUtils"; +import { ActiveDot } from "./components/ActiveDot"; export interface AreaChartV2Props { data: T; @@ -199,6 +200,8 @@ export const AreaChartV2 = ({ const chartSyncID = useMemo(() => `area-chart-sync-${id}`, [id]); + const gradientId = useMemo(() => `area-chart-gradient-${id}`, [id]); + return (
({ const transformedKey = keyTransform(key); const color = `var(--color-${transformedKey})`; return ( - + <> + + + + + + + } + dot={false} + isAnimationActive={isAnimationActive} + /> + ); })} diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss index 5e55eabb6..721e8d123 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss @@ -28,8 +28,13 @@ position: relative; } -.crayon-area-chart-scroll-button { +button.crayon-area-chart-scroll-button { position: absolute; + background-color: cssUtils.$bg-container; + + &:hover { + background-color: cssUtils.$bg-container; + } &--left { top: -15px; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/components/ActiveDot.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/components/ActiveDot.tsx index e69de29bb..d74706087 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/components/ActiveDot.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/components/ActiveDot.tsx @@ -0,0 +1,33 @@ +import React from "react"; + +export interface ActiveDotProps { + cx?: number; + cy?: number; + payload?: any; + value?: any; +} + +export const ActiveDot: React.FC = (props) => { + const { cx, cy } = props; + + return ( + + + + + ); +}; From 4b43227604539ec0044d700d544da22e89ab7c56 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 11 Jun 2025 16:17:24 +0530 Subject: [PATCH 056/190] feat(LineCharts): Add support for mini line charts This commit introduces a new `MiniLineChart` component in the `react-ui` package. The changes include: 1. Added a new `types/index.ts` file to define the `MiniLineChartData` type, which can be an array of numbers or an array of objects with `value` and optional `label` properties. 2. Moved the `MiniLineChartData` type definition from `LineChartUtils.ts` to the new `types/index.ts` file. 3. Implemented the `MiniLineChart` component in `MiniLineChart.tsx`, which: - Uses a `ResizeObserver` to dynamically adjust the chart size based on the container's width. - Filters the `data` prop to only include the most recent data that fits within the container's width. - Transforms the filtered data into a format compatible with the Recharts library. - Applies the specified `theme`, `variant`, `strokeWidth`, and `isAnimationActive` props. - Allows for optional `onLineClick` and `lineColor` props. - Provides a consistent chart configuration using the `ChartConfig` and `ChartContainer` components. These changes enable the creation of compact, responsive line charts that can be used in various parts of the application. --- .../MiniLineChart/MiniLineChart.tsx | 126 +++++++---- .../Charts/LineCharts/MiniLineChart/index.ts | 1 - .../stories/MiniLineChart.stories.tsx | 201 ++++++++---------- .../MiniLineChart/utils/miniLineChartUtils.ts | 57 +++++ .../Charts/LineCharts/types/index.ts | 1 + .../Charts/LineCharts/utils/LineChartUtils.ts | 3 +- 6 files changed, 231 insertions(+), 158 deletions(-) delete mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/utils/miniLineChartUtils.ts create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx index 96212c7fe..9113bd1e7 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx @@ -1,65 +1,107 @@ -import React from "react"; +import clsx from "clsx"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Line, LineChart as RechartsLineChart, XAxis } from "recharts"; -import { ChartContainer, keyTransform } from "../../Charts"; -import { MiniLineChartData, createChartConfig } from "../utils/LineChartUtils"; +import { ChartConfig, ChartContainer } from "../../Charts"; +import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; +import { MiniLineChartData } from "../types"; +import { getRecentLineDataThatFits, transformDataForLineChart } from "./utils/miniLineChartUtils"; -export interface MiniLineChartProps { - data: T; - categoryKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; +export interface MiniLineChartProps { + data: MiniLineChartData; + theme?: PaletteName; variant?: "linear" | "natural" | "step"; strokeWidth?: number; - icons?: Partial>; isAnimationActive?: boolean; + onLineClick?: (data: any) => void; + size?: number | string; + className?: string; + lineColor?: string; } -export const MiniLineChart = ({ +export const MiniLineChart = ({ data, - categoryKey, theme = "ocean", variant = "natural", strokeWidth = 2, - icons = {}, isAnimationActive = true, -}: MiniLineChartProps) => { - const chartConfig = createChartConfig({ data, categoryKey: categoryKey as string, theme, icons }); + onLineClick, + size = "100%", + className, + lineColor, +}: MiniLineChartProps) => { + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + + useEffect(() => { + if (!containerRef.current) { + return () => {}; + } + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerWidth(entry.contentRect.width); + } + }); + + resizeObserver.observe(containerRef.current); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + // Get the most recent data that fits in the container + const filteredData = useMemo(() => { + return getRecentLineDataThatFits(data, containerWidth); + }, [data, containerWidth]); + + // Transform the filtered data to a consistent format for recharts + const chartData = useMemo(() => { + return transformDataForLineChart(filteredData); + }, [filteredData]); + + const colors = useMemo(() => { + const palette = getPalette(theme); + return getDistributedColors(palette, 1); // Single color for 1D chart + }, [theme]); + + const chartConfig: ChartConfig = useMemo(() => { + return { + value: { + label: "Value", + color: lineColor ? lineColor : colors[0], + }, + }; + }, [colors, lineColor]); return ( - + - {/* {grid && cartesianGrid()} */} - + + - {Object.keys(chartConfig).map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - return ( - - ); - })} ); diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts deleted file mode 100644 index 86eccce92..000000000 --- a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./MiniLineChart"; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx index 0653aa8a8..8e64c71db 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx @@ -1,59 +1,56 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Monitor, TabletSmartphone } from "lucide-react"; import { Card } from "../../../../Card"; -import { MiniLineChart } from "../MiniLineChart"; +import { MiniLineChart, MiniLineChartProps } from "../MiniLineChart"; -const lineChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, +// Simple array of numbers for 1D line chart +const simpleLineChartData = [ + 12, 45, 78, 32, 67, 89, 23, 56, 91, 34, 76, 28, 85, 42, 19, 63, 87, 31, 74, 58, 92, 26, 49, 83, + 37, 50, 1, ]; -const icons = { - desktop: Monitor, - mobile: TabletSmartphone, -} as const; +// Array of objects with value and label +const labeledLineChartData = [ + { value: 150, label: "January" }, + { value: 280, label: "February" }, + { value: 220, label: "March" }, + { value: 180, label: "April" }, + { value: 250, label: "May" }, + { value: 300, label: "June" }, + { value: 320, label: "July" }, + { value: 280, label: "August" }, + { value: 310, label: "September" }, + { value: 290, label: "October" }, + { value: 340, label: "November" }, + { value: 360, label: "December" }, +]; -const meta = { +const meta: Meta = { title: "Components/Charts/LineCharts/MiniLineChart", component: MiniLineChart, parameters: { layout: "centered", docs: { description: { - component: "```tsx\nimport { LineChart } from '@crayon-ui/react-ui/Charts/LineChart';\n```", + component: + "```tsx\nimport { MiniLineChart } from '@crayon-ui/react-ui/Charts/LineCharts/MiniLineChart';\n```\n\nA responsive mini line chart component that accepts 1D data (numbers or objects with value/label) with automatic data filtering for space-constrained containers. Features smooth line interpolation and customizable styling.", }, }, }, - tags: ["!dev", "autodocs"], - + tags: ["dev", "autodocs"], argTypes: { data: { description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", - control: { type: "object" }, - table: { - type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", - }, - }, - categoryKey: { - description: - "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", + "An array of numbers or an array of objects with value and optional label. Each entry represents a single point in the line chart.", control: false, table: { - type: { summary: "string" }, - defaultValue: { summary: "string" }, + type: { summary: "Array | Array<{ value: number; label?: string }>" }, + defaultValue: { summary: "[]" }, category: "Data", }, }, theme: { description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + "The color palette theme for the chart. Each theme provides a different color for the line.", control: "select", options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], table: { @@ -61,16 +58,6 @@ const meta = { category: "Appearance", }, }, - icons: { - description: - "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", - control: false, - table: { - type: { summary: "Record" }, - defaultValue: { summary: "{}" }, - category: "Appearance", - }, - }, variant: { description: "The interpolation method used to create the line curves. 'linear' creates straight lines between points, 'natural' creates smooth curves, and 'step' creates a stepped line.", @@ -83,7 +70,7 @@ const meta = { }, strokeWidth: { description: "The width of the line stroke", - control: false, + control: "number", table: { type: { summary: "number" }, defaultValue: { summary: "2" }, @@ -91,7 +78,7 @@ const meta = { }, }, isAnimationActive: { - description: "Whether to animate the chart", + description: "Whether to animate the chart when it first renders", control: "boolean", table: { type: { summary: "boolean" }, @@ -99,24 +86,42 @@ const meta = { category: "Display", }, }, + size: { + description: "The width and height of the chart", + control: "text", + table: { + type: { summary: "number | string" }, + defaultValue: { summary: "100%" }, + category: "Appearance", + }, + }, + lineColor: { + description: "Custom color for the line stroke", + control: "color", + table: { + type: { summary: "string" }, + category: "Appearance", + }, + }, }, } satisfies Meta; export default meta; type Story = StoryObj; -export const LineChartStory: Story = { - name: "Line Chart", +export const SimpleNumberArray: Story = { + name: "Simple Number Array", args: { - data: lineChartData, - categoryKey: "month", + data: simpleLineChartData, theme: "ocean", variant: "natural", strokeWidth: 2, isAnimationActive: true, + size: "100%", }, - render: (args) => ( - + render: (args: MiniLineChartProps) => ( + +

Daily Activity

), @@ -124,45 +129,35 @@ export const LineChartStory: Story = { docs: { source: { code: ` -const lineChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - - - +const activityData = [12, 45, 78, 32, 67, 89, 23, 56, 91, 34]; + + `, }, }, }, }; -export const LineChartStoryWithIcons: Story = { - name: "Line Chart with Icons", +export const LabeledData: Story = { + name: "Labeled Data", args: { - ...LineChartStory.args, - icons: icons, + data: labeledLineChartData, + theme: "emerald", + variant: "natural", + strokeWidth: 2, + isAnimationActive: true, + size: "100%", }, - render: (args) => ( - + render: (args: MiniLineChartProps) => ( + +

Monthly Revenue

), @@ -170,41 +165,21 @@ export const LineChartStoryWithIcons: Story = { docs: { source: { code: ` - import { Monitor, TabletSmartphone } from "lucide-react"; - - const lineChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, +const revenueData = [ + { value: 150, label: "January" }, + { value: 280, label: "February" }, + { value: 220, label: "March" }, + // ... ]; -const icons = { - desktop: Monitor, - mobile: TabletSmartphone, -}; - - - - - `, + +`, }, }, }, diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/utils/miniLineChartUtils.ts b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/utils/miniLineChartUtils.ts new file mode 100644 index 000000000..86efdff02 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/utils/miniLineChartUtils.ts @@ -0,0 +1,57 @@ +import { type MiniLineChartData } from "../../types"; + +export const MINI_LINE_ELEMENT_SPACING: number = 20; + +type ChartData = Array<{ + value: number; + label: string; +}>; + +/** + * Transforms the mini line chart data into a standardized format for rendering. + * Handles both numeric values and objects with value/label properties. + * + * @param data - The mini line chart data array (can contain numbers or objects with value/label) + * @returns An array of chart data objects with value and label properties + */ +const transformDataForLineChart = (data: MiniLineChartData): ChartData => { + return data.map((item, index) => { + if (typeof item === "number") { + return { value: item, label: `Item ${index + 1}` }; + } else { + return { value: item.value, label: item.label || `Item ${index + 1}` }; + } + }); +}; + +/** + * Filters the data to include only the most recent items that can fit within the container width. + * This function ensures the chart displays the latest data when space is limited. + * + * @param data - The complete mini line chart data array + * @param containerWidth - The total width of the container in pixels + * @returns A filtered array containing only the most recent data items that fit in the container + */ +const getRecentLineDataThatFits = ( + data: MiniLineChartData, + containerWidth: number, +): MiniLineChartData => { + if (containerWidth <= 0 || data.length === 0) { + return data; + } + + // Calculate how many items can fit in the available space + const maxItems = Math.floor((containerWidth + 20) / MINI_LINE_ELEMENT_SPACING); + // +20 because the element spacing is between so if we have 2 element then its data 20px data + // so we need to add 20px to the container width to get the actual width of the data + + // If all items fit, return all data + if (maxItems >= data.length) { + return data; + } + + // Return the most recent items that fit + return data.slice(-maxItems); +}; + +export { getRecentLineDataThatFits, transformDataForLineChart }; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts new file mode 100644 index 000000000..59487ff9f --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts @@ -0,0 +1 @@ +export type MiniLineChartData = Array | Array<{ value: number; label?: string }>; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts b/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts index 870243fe4..738405bcb 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts @@ -1,7 +1,6 @@ import { ChartConfig } from "../../Charts"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; - -export type MiniLineChartData = Array>; +import { MiniLineChartData } from "../types"; export interface MiniLineChartConfig { data: MiniLineChartData; From 665d657e63ffb64914d93b939839a4d3336abe9c Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 11 Jun 2025 17:08:04 +0530 Subject: [PATCH 057/190] feat(AreaChartV2): add active dot component and improve animation The changes made in this commit include: 1. Importing the `ActiveDot` component from the shared directory and using it in the `AreaChartV2` component. 2. Removing the `opacity` prop from the `AreaChartV2` component, as it is no longer needed. 3. Updating the `areaChartV2.scss` file to reduce the transition duration for the disabled state of the chart. 4. Adding new types for the `LineChartVariant` and `LineChartV2Data` in the `types/index.ts` file. These changes aim to improve the visual appearance and animation of the `AreaChartV2` component, as well as provide more flexibility in the types used for line charts. --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 5 +-- .../AreaCharts/AreaChartV2/areaChartV2.scss | 2 +- .../stories/areaChartV2.stories.tsx | 20 +---------- .../Charts/AreaCharts/utils/AreaChartUtils.ts | 34 ------------------- .../LineCharts/LineChartV2/LineChartV2.tsx | 0 .../Charts/LineCharts/LineChartV2/index.ts | 0 .../LineCharts/LineChartV2/lineChartV2.scss | 0 .../Charts/LineCharts/types/index.ts | 4 +++ .../Charts/LineCharts/utils/LineChartUtils.ts | 34 ------------------- .../ActiveDot}/ActiveDot.tsx | 0 .../src/components/Charts/shared/index.ts | 1 + 11 files changed, 8 insertions(+), 92 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/lineChartV2.scss rename js/packages/react-ui/src/components/Charts/{AreaCharts/AreaChartV2/components => shared/ActiveDot}/ActiveDot.tsx (100%) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index 89d510fdc..4704fdf49 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -11,7 +11,7 @@ import { keyTransform, } from "../../Charts"; import { cartesianGrid } from "../../cartesianGrid"; -import { DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; +import { ActiveDot, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; import { LegendItem } from "../../types"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; @@ -24,7 +24,6 @@ import { getWidthOfData, getXAxisTickPositionData, } from "../utils/AreaChartUtils"; -import { ActiveDot } from "./components/ActiveDot"; export interface AreaChartV2Props { data: T; @@ -33,7 +32,6 @@ export interface AreaChartV2Props { variant?: AreaChartVariant; grid?: boolean; legend?: boolean; - opacity?: number; icons?: Partial>; isAnimationActive?: boolean; showYAxis?: boolean; @@ -59,7 +57,6 @@ export const AreaChartV2 = ({ xAxisLabel, yAxisLabel, legend = true, - opacity = 0.5, className, height, width, diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss index 721e8d123..07d7db0a1 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss @@ -51,6 +51,6 @@ button.crayon-area-chart-scroll-button { &--disabled { visibility: hidden; cursor: not-allowed; - transition: visibility 0.5s ease-in-out; + transition: visibility 0.1s linear; } } diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx index 44fa2bdda..b6a28b3aa 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -289,16 +289,7 @@ const meta: Meta> = { category: "Appearance", }, }, - opacity: { - description: - "The opacity of the filled area beneath each line (0 = fully transparent, 1 = fully opaque)", - control: "number", - table: { - type: { summary: "number" }, - defaultValue: { summary: "0.5" }, - category: "Appearance", - }, - }, + grid: { description: "Whether to display the background grid lines in the chart", control: "boolean", @@ -405,7 +396,6 @@ export const AreaChartV2Story: Story = { categoryKey: "month", theme: "ocean", variant: "natural", - opacity: 0.5, grid: true, legend: true, isAnimationActive: true, @@ -547,7 +537,6 @@ export const BigLabelsStory: Story = { categoryKey: "category" as any, theme: "emerald", variant: "natural", - opacity: 0.4, grid: true, legend: true, isAnimationActive: true, @@ -575,7 +564,6 @@ export const DenseTimelineStory: Story = { categoryKey: "period" as any, theme: "sunset", variant: "natural", - opacity: 0.6, grid: true, legend: true, isAnimationActive: true, @@ -603,7 +591,6 @@ export const CompanyNamesStory: Story = { categoryKey: "company" as any, theme: "vivid", variant: "natural", - opacity: 0.5, grid: true, legend: true, isAnimationActive: true, @@ -631,7 +618,6 @@ export const CountryDataStory: Story = { categoryKey: "country" as any, theme: "orchid", variant: "natural", - opacity: 0.4, grid: true, legend: true, isAnimationActive: true, @@ -659,7 +645,6 @@ export const MixedLengthsStory: Story = { categoryKey: "item" as any, theme: "spectrum", variant: "linear", - opacity: 0.5, grid: true, legend: true, isAnimationActive: true, @@ -687,7 +672,6 @@ export const EdgeCasesStory: Story = { categoryKey: "name" as any, theme: "ocean", variant: "step", - opacity: 0.7, grid: true, legend: true, isAnimationActive: true, @@ -715,7 +699,6 @@ export const MinimalDataStory: Story = { categoryKey: "category" as any, theme: "emerald", variant: "natural", - opacity: 0.5, grid: true, legend: true, isAnimationActive: true, @@ -743,7 +726,6 @@ export const ResponsiveWidthStory: Story = { categoryKey: "category" as any, theme: "sunset", variant: "natural", - opacity: 0.5, grid: true, legend: true, isAnimationActive: true, diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts index 7b257925d..d13ed6459 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts @@ -1,7 +1,4 @@ import { AreaChartData } from "../../AreaChart/AreaChart"; -import { ChartConfig } from "../../Charts"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; -import { MiniAreaChartData } from "../types"; const ELEMENT_SPACING = 70; @@ -199,37 +196,6 @@ const findNearestSnapPosition = ( } }; -export interface MiniAreaChartConfig { - data: MiniAreaChartData; - categoryKey: string; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - icons?: Partial>; -} - -export const createChartConfig = ({ - data, - categoryKey, - theme = "ocean", - icons = {}, -}: MiniAreaChartConfig): ChartConfig => { - // excluding the categoryKey - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); - const palette = getPalette(theme); - const colors = getDistributedColors(palette, dataKeys.length); - - return dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - icon: icons[key], - color: colors[index], - }, - }), - {}, - ); -}; - export { findNearestSnapPosition, getOptimalXAxisTickFormatter, diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/index.ts b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/lineChartV2.scss b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/lineChartV2.scss new file mode 100644 index 000000000..e69de29bb diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts index 59487ff9f..b4ec6dc6b 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts +++ b/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts @@ -1 +1,5 @@ +export type LineChartVariant = "linear" | "natural" | "step"; + +export type LineChartV2Data = Array>; + export type MiniLineChartData = Array | Array<{ value: number; label?: string }>; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts b/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts index 738405bcb..e69de29bb 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts @@ -1,34 +0,0 @@ -import { ChartConfig } from "../../Charts"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; -import { MiniLineChartData } from "../types"; - -export interface MiniLineChartConfig { - data: MiniLineChartData; - categoryKey: string; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - icons?: Partial>; -} - -export const createChartConfig = ({ - data, - categoryKey, - theme = "ocean", - icons = {}, -}: MiniLineChartConfig): ChartConfig => { - // excluding the categoryKey - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); - const palette = getPalette(theme); - const colors = getDistributedColors(palette, dataKeys.length); - - return dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - icon: icons[key], - color: colors[index], - }, - }), - {}, - ); -}; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/components/ActiveDot.tsx b/js/packages/react-ui/src/components/Charts/shared/ActiveDot/ActiveDot.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/components/ActiveDot.tsx rename to js/packages/react-ui/src/components/Charts/shared/ActiveDot/ActiveDot.tsx diff --git a/js/packages/react-ui/src/components/Charts/shared/index.ts b/js/packages/react-ui/src/components/Charts/shared/index.ts index 46396a532..cef54e7db 100644 --- a/js/packages/react-ui/src/components/Charts/shared/index.ts +++ b/js/packages/react-ui/src/components/Charts/shared/index.ts @@ -1,3 +1,4 @@ +export * from "./ActiveDot/ActiveDot"; export * from "./DefaultLegend/DefaultLegend"; export * from "./XAxisTick/XAxisTick"; export * from "./YAxisTick/YAxisTick"; From fa16bfa6fee0bd062e3012c1d56e91d224c4dfe9 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 12 Jun 2025 04:43:52 +0530 Subject: [PATCH 058/190] feat(LineChartV2): Add new LineChartV2 component and styles This commit introduces the new LineChartV2 component and its corresponding styles. The key changes are: - Added the `LineChartV2` export in the `index.ts` file. - Implemented the `lineChartV2.scss` file, which includes styles for the line chart container, y-axis container, main container, scroll container, and scroll buttons. - Implemented the `LineChartUtils.ts` file, which provides utility functions for calculating the width of the data, the width of each group/category, and the X-axis tick formatter with intelligent truncation. These changes aim to provide a more robust and customizable line chart component for the application. --- .../LineCharts/LineChartV2/LineChartV2.tsx | 348 ++++++++++ .../Charts/LineCharts/LineChartV2/index.ts | 1 + .../LineCharts/LineChartV2/lineChartV2.scss | 56 ++ .../stories/lineChartV2.stories.tsx | 633 ++++++++++++++++++ .../Charts/LineCharts/types/index.ts | 4 +- .../Charts/LineCharts/utils/LineChartUtils.ts | 210 ++++++ .../src/components/Charts/charts.scss | 3 + 7 files changed, 1253 insertions(+), 2 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx index e69de29bb..c73fe1968 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx @@ -0,0 +1,348 @@ +import clsx from "clsx"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; +import { Line, LineChart as RechartsLineChart, XAxis, YAxis } from "recharts"; +import { IconButton } from "../../../IconButton"; +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, + keyTransform, +} from "../../Charts"; +import { cartesianGrid } from "../../cartesianGrid"; +import { ActiveDot, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; +import { LegendItem } from "../../types"; +import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; +import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; +import { getYAxisTickFormatter } from "../../utils/styleUtils"; +import { LineChartV2Data, LineChartVariant } from "../types"; +import { + findNearestSnapPosition, + getOptimalXAxisTickFormatter, + getSnapPositions, + getWidthOfData, + getXAxisTickPositionData, +} from "../utils/LineChartUtils"; + +export interface LineChartV2Props { + data: T; + categoryKey: keyof T[number]; + theme?: PaletteName; + variant?: LineChartVariant; + grid?: boolean; + legend?: boolean; + icons?: Partial>; + isAnimationActive?: boolean; + showYAxis?: boolean; + xAxisLabel?: React.ReactNode; + yAxisLabel?: React.ReactNode; + className?: string; + height?: number; + width?: number; + strokeWidth?: number; + onLineClick?: (payload: any) => void; +} + +const Y_AXIS_WIDTH = 40; // Width of Y-axis chart when shown + +export const LineChartV2 = ({ + data, + categoryKey, + theme = "ocean", + variant = "natural", + grid = true, + icons = {}, + isAnimationActive = true, + showYAxis = false, + xAxisLabel, + yAxisLabel, + legend = true, + className, + height, + width, + strokeWidth = 2, + onLineClick, +}: LineChartV2Props) => { + const dataKeys = useMemo(() => { + return getDataKeys(data, categoryKey as string); + }, [data, categoryKey]); + + const colors = useMemo(() => { + const palette = getPalette(theme); + return getDistributedColors(palette, dataKeys.length); + }, [theme, dataKeys.length]); + + const chartConfig: ChartConfig = useMemo(() => { + return getChartConfig(dataKeys, colors, undefined, icons); + }, [dataKeys, icons, colors]); + + const chartContainerRef = useRef(null); + const mainContainerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(false); + + // Use provided width or observed width + const effectiveWidth = useMemo(() => { + return width ?? containerWidth; + }, [width, containerWidth]); + + const effectiveContainerWidth = useMemo(() => { + const yAxisWidth = showYAxis ? Y_AXIS_WIDTH : 0; + return Math.max(0, effectiveWidth - yAxisWidth - 40); // -40 because we are giving 20px padding in xAxis on each side + }, [effectiveWidth, showYAxis]); + + const dataWidth = useMemo(() => { + return getWidthOfData(data, effectiveContainerWidth); + }, [data, effectiveContainerWidth]); + + // Calculate snap positions for proper scrolling alignment + const snapPositions = useMemo(() => { + return getSnapPositions(data); + }, [data]); + + const chartHeight = useMemo(() => { + return height ?? 296; + }, [height]); + + // Calculate optimal tick formatter for collision detection and truncation + const xAxisTickFormatter = useMemo(() => { + return getOptimalXAxisTickFormatter(data, effectiveContainerWidth); + }, [data, effectiveContainerWidth]); + + // Calculate position data for X-axis tick offset handling + const xAxisPositionData = useMemo(() => { + return getXAxisTickPositionData(data, categoryKey as string); + }, [data, categoryKey]); + + // Check scroll boundaries + const updateScrollState = useCallback(() => { + if (mainContainerRef.current) { + const { scrollLeft, scrollWidth, clientWidth } = mainContainerRef.current; + setCanScrollLeft(scrollLeft > 0); + setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 1); // -1 for floating point precision + } + }, []); + + const scrollLeft = useCallback(() => { + if (mainContainerRef.current) { + const currentScroll = mainContainerRef.current.scrollLeft; + const targetIndex = findNearestSnapPosition(snapPositions, currentScroll, "left"); + const targetPosition = snapPositions[targetIndex] ?? 0; + + mainContainerRef.current.scrollTo({ + left: targetPosition, + behavior: "smooth", + }); + } + }, [snapPositions]); + + const scrollRight = useCallback(() => { + if (mainContainerRef.current) { + const currentScroll = mainContainerRef.current.scrollLeft; + const targetIndex = findNearestSnapPosition(snapPositions, currentScroll, "right"); + const targetPosition = snapPositions[targetIndex] ?? 0; + + mainContainerRef.current.scrollTo({ + left: targetPosition, + behavior: "smooth", + }); + } + }, [snapPositions]); + + useEffect(() => { + // Only set up ResizeObserver if width is not provided + if (width || !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) { + setContainerWidth(entry.contentRect.width); + } + }); + + resizeObserver.observe(chartContainerRef.current); + + return () => { + resizeObserver.disconnect(); + }; + }, [width]); + + // Update scroll state when container width or data width changes + useEffect(() => { + updateScrollState(); + }, [effectiveWidth, dataWidth, updateScrollState]); + + // Add scroll event listener to update button states + useEffect(() => { + const mainContainer = mainContainerRef.current; + if (!mainContainer) return; + + const handleScroll = () => { + updateScrollState(); + }; + + mainContainer.addEventListener("scroll", handleScroll); + return () => { + mainContainer.removeEventListener("scroll", handleScroll); + }; + }, [updateScrollState]); + + const legendItems: LegendItem[] = useMemo(() => { + return getLegendItems(dataKeys, colors, icons); + }, [dataKeys, colors, icons]); + + const id = useId(); + + const chartSyncID = useMemo(() => `line-chart-sync-${id}`, [id]); + + return ( +
+
+ {showYAxis && ( +
+ {/* Y-axis only chart - synchronized with main chart */} + + } + /> + {/* Invisible lines to maintain scale synchronization */} + {dataKeys.map((key) => { + return ( + + ); + })} + +
+ )} +
+ + + {grid && cartesianGrid()} + + } + orientation="bottom" + padding={{ + left: 20, + right: 20, + }} + /> + } /> + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + return ( + } + isAnimationActive={isAnimationActive} + /> + ); + })} + + +
+
+ {/* if the data width is greater than the effective width, then show the scroll buttons */} + {dataWidth > effectiveWidth && ( +
+ } + variant="secondary" + onClick={scrollLeft} + size="extra-small" + disabled={!canScrollLeft} + /> + } + variant="secondary" + size="extra-small" + onClick={scrollRight} + disabled={!canScrollRight} + /> +
+ )} + {legend && ( + + )} +
+ ); +}; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/index.ts b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/index.ts index e69de29bb..6addf7f62 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/index.ts +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/index.ts @@ -0,0 +1 @@ +export * from "./LineChartV2"; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/lineChartV2.scss b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/lineChartV2.scss index e69de29bb..8c6b1a48a 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/lineChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/lineChartV2.scss @@ -0,0 +1,56 @@ +@use "../../../../cssUtils" as cssUtils; + +.crayon-line-chart-container-inner { + display: flex; + width: 100%; +} + +.crayon-line-chart-y-axis-container { + flex-shrink: 0; +} + +.crayon-line-chart-main-container { + width: 100%; + overflow-x: auto; + /* Hide scrollbar for Chrome, Safari and Opera */ + &::-webkit-scrollbar { + display: none; + } + + /* Hide scrollbar for Firefox */ + scrollbar-width: none; + + /* Hide scrollbar for IE and Edge */ + -ms-overflow-style: none; +} + +.crayon-line-chart-scroll-container { + position: relative; +} + +button.crayon-line-chart-scroll-button { + position: absolute; + background-color: cssUtils.$bg-container; + + &:hover { + background-color: cssUtils.$bg-container; + } + + &--left { + top: -15px; + transform: translateY(-50%); + left: 20px; + } + + &--right { + top: -15px; + transform: translateY(-50%); + right: 0px; + } + + &--disabled { + visibility: hidden; + cursor: not-allowed; + transition: visibility 0.1s linear; + } +} diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx new file mode 100644 index 000000000..02092a3f0 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx @@ -0,0 +1,633 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { + Calendar, + Globe, + Laptop, + Monitor, + Smartphone, + TabletSmartphone, + Tv, + Watch, +} from "lucide-react"; +import { useState } from "react"; +import { Card } from "../../../../Card"; +import { LineChartV2, LineChartV2Props } from "../LineChartV2"; + +// 🔥 COMPREHENSIVE DATA VARIATIONS - Designed to test label collision scenarios +const dataVariations = { + default: [ + { month: "January", desktop: 150, mobile: 90, tablet: 120 }, + { month: "February", desktop: 280, mobile: 180, tablet: 140 }, + { month: "March", desktop: 220, mobile: 140, tablet: 160 }, + { month: "April", desktop: 180, mobile: 160, tablet: 180 }, + { month: "May", desktop: 250, mobile: 120, tablet: 140 }, + { month: "June", desktop: 300, mobile: 180, tablet: 160 }, + { month: "July", desktop: 350, mobile: 220, tablet: 180 }, + { month: "August", desktop: 400, mobile: 240, tablet: 200 }, + { month: "September", desktop: 450, mobile: 260, tablet: 220 }, + { month: "October", desktop: 500, mobile: 280, tablet: 240 }, + { month: "November", desktop: 550, mobile: 300, tablet: 260 }, + { month: "December", desktop: 600, mobile: 320, tablet: 280 }, + ], + // 🏷️ BIG LABELS - Testing collision detection and truncation + bigLabels: [ + { + category: "Very Long Category Name That Should Be Truncated", + sales: 150, + revenue: 90, + profit: 120, + }, + { + category: "Another Extremely Long Label That Causes Collisions", + sales: 280, + revenue: 180, + profit: 140, + }, + { + category: "Super Duper Long Category Name That Tests Truncation", + sales: 220, + revenue: 140, + profit: 160, + }, + { + category: "Incredibly Long Text That Should Trigger Collision Detection", + sales: 180, + revenue: 160, + profit: 180, + }, + { + category: "Maximum Length Category Name That Tests All Edge Cases", + sales: 250, + revenue: 120, + profit: 140, + }, + { + category: "Extra Long Business Category Name With Many Words", + sales: 300, + revenue: 180, + profit: 160, + }, + { + category: "Comprehensive Long Label For Testing Horizontal Offset", + sales: 350, + revenue: 220, + profit: 180, + }, + { + category: "Extended Category Name That Pushes Truncation Limits", + sales: 400, + revenue: 240, + profit: 200, + }, + ], + // 📅 DENSE TIMELINE - Many items with medium-length labels + denseTimeline: [ + { period: "Q1 2022 Jan-Mar", visitors: 120, conversions: 15, revenue: 1200 }, + { period: "Q1 2022 Apr-Jun", visitors: 150, conversions: 22, revenue: 1800 }, + { period: "Q2 2022 Jul-Sep", visitors: 180, conversions: 28, revenue: 2100 }, + { period: "Q2 2022 Oct-Dec", visitors: 200, conversions: 35, revenue: 2500 }, + { period: "Q3 2023 Jan-Mar", visitors: 160, conversions: 18, revenue: 1600 }, + { period: "Q3 2023 Apr-Jun", visitors: 190, conversions: 32, revenue: 2300 }, + { period: "Q4 2023 Jul-Sep", visitors: 220, conversions: 40, revenue: 2800 }, + { period: "Q4 2023 Oct-Dec", visitors: 240, conversions: 45, revenue: 3200 }, + { period: "Q1 2024 Jan-Mar", visitors: 210, conversions: 38, revenue: 2700 }, + { period: "Q1 2024 Apr-Jun", visitors: 230, conversions: 42, revenue: 3000 }, + { period: "Q2 2024 Jul-Sep", visitors: 250, conversions: 48, revenue: 3400 }, + { period: "Q2 2024 Oct-Dec", visitors: 270, conversions: 52, revenue: 3800 }, + { period: "Q3 2024 Jan-Mar", visitors: 260, conversions: 50, revenue: 3600 }, + { period: "Q3 2024 Apr-Jun", visitors: 280, conversions: 55, revenue: 4000 }, + { period: "Q4 2024 Jul-Sep", visitors: 300, conversions: 60, revenue: 4300 }, + { period: "Q4 2024 Oct-Dec", visitors: 290, conversions: 58, revenue: 4100 }, + ], + // 🏢 COMPANY NAMES - Real-world long business names + companyNames: [ + { company: "Apple Inc.", revenue: 394328000000, profit: 99803000000, marketCap: 3500000000000 }, + { + company: "Microsoft Corporation", + revenue: 211915000000, + profit: 83383000000, + marketCap: 2800000000000, + }, + { + company: "Alphabet Inc. (Google)", + revenue: 307394000000, + profit: 76033000000, + marketCap: 2100000000000, + }, + { + company: "Amazon.com Inc.", + revenue: 574785000000, + profit: 33364000000, + marketCap: 1600000000000, + }, + { + company: "Tesla Motors Inc.", + revenue: 96773000000, + profit: 15000000000, + marketCap: 800000000000, + }, + { + company: "Meta Platforms Inc.", + revenue: 134902000000, + profit: 39370000000, + marketCap: 900000000000, + }, + { + company: "NVIDIA Corporation", + revenue: 60922000000, + profit: 29760000000, + marketCap: 1800000000000, + }, + { + company: "Berkshire Hathaway Inc.", + revenue: 364482000000, + profit: 96223000000, + marketCap: 780000000000, + }, + ], + // 🌍 COUNTRY NAMES - Geographic labels with varying lengths + countryData: [ + { country: "United States of America", population: 331900000, gdp: 17700000000000 }, + { country: "People's Republic of China", population: 1412000000, gdp: 17700000000000 }, + { country: "Federal Republic of Germany", population: 83200000, gdp: 4300000000000 }, + { country: "United Kingdom of Great Britain", population: 67500000, gdp: 3100000000000 }, + { country: "French Republic", population: 68000000, gdp: 2900000000000 }, + { country: "Republic of India", population: 1380000000, gdp: 3700000000000 }, + { country: "Federative Republic of Brazil", population: 215000000, gdp: 2100000000000 }, + { country: "Russian Federation", population: 146000000, gdp: 1800000000000 }, + ], + // 📈 FINANCIAL QUARTERS - Testing medium-density scenarios + financialQuarters: [ + { quarter: "Q1 FY2022", revenue: 1200000, expenses: 800000, profit: 400000 }, + { quarter: "Q2 FY2022", revenue: 1500000, expenses: 950000, profit: 550000 }, + { quarter: "Q3 FY2022", revenue: 1800000, expenses: 1100000, profit: 700000 }, + { quarter: "Q4 FY2022", revenue: 2000000, expenses: 1300000, profit: 700000 }, + { quarter: "Q1 FY2023", revenue: 2200000, expenses: 1400000, profit: 800000 }, + { quarter: "Q2 FY2023", revenue: 2500000, expenses: 1600000, profit: 900000 }, + { quarter: "Q3 FY2023", revenue: 2800000, expenses: 1800000, profit: 1000000 }, + { quarter: "Q4 FY2023", revenue: 3000000, expenses: 1900000, profit: 1100000 }, + ], + // 🔤 MIXED LENGTHS - Testing various label length scenarios + mixedLengths: [ + { item: "A", valueA: 100, valueB: 80 }, + { item: "Short", valueA: 150, valueB: 120 }, + { item: "Medium Length Item", valueA: 200, valueB: 160 }, + { item: "Very Long Item Name That Tests Truncation", valueA: 250, valueB: 200 }, + { item: "B", valueA: 180, valueB: 140 }, + { item: "Another Really Long Category Name", valueA: 220, valueB: 180 }, + { item: "XL", valueA: 190, valueB: 150 }, + ], + // 🎯 EDGE CASES - Extreme scenarios + edgeCases: [ + { + name: "SinglePointDataSetForTestingEdgeCasesInCollisionDetectionAndLabelTruncationFunctionality", + value: 500, + }, + { + name: "SecondExtremelyLongDataPointNameThatShouldDefinitelyTriggerTruncationMechanisms", + value: 600, + }, + ], + // 📱 MINIMAL - Small dataset for baseline testing + minimal: [ + { category: "Mobile Devices", users: 150, sessions: 90 }, + { category: "Desktop Computers", users: 280, sessions: 180 }, + { category: "Tablet Devices", users: 220, sessions: 140 }, + ], +}; + +// Category key mappings for different datasets +const categoryKeys = { + default: "month", + bigLabels: "category", + denseTimeline: "period", + companyNames: "company", + countryData: "country", + financialQuarters: "quarter", + mixedLengths: "item", + edgeCases: "name", + minimal: "category", +}; + +// 🔥 ACTIVE DATA - For backward compatibility +const lineChartData = dataVariations.default; + +const icons = { + desktop: Monitor, + mobile: TabletSmartphone, + tablet: Calendar, + sales: Globe, + revenue: Smartphone, + profit: Laptop, + visitors: Tv, + conversions: Watch, + users: Monitor, + sessions: TabletSmartphone, +} as const; + +const meta: Meta> = { + title: "Components/Charts/LineCharts/LineChartV2", + component: LineChartV2, + parameters: { + layout: "centered", + docs: { + description: { + component: + "LineChartV2 features advanced collision detection and label truncation with horizontal offset capabilities. The component automatically handles overlapping X-axis labels by intelligently truncating them with ellipsis while maintaining horizontal positioning.", + }, + }, + }, + tags: ["dev", "autodocs"], + argTypes: { + data: { + description: "An array of data objects for the line chart", + control: false, + table: { + type: { summary: "Array>" }, + category: "Data", + }, + }, + categoryKey: { + description: "The key for x-axis categories", + control: false, + table: { + type: { summary: "string" }, + category: "Data", + }, + }, + theme: { + description: "Color theme for the chart", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + variant: { + description: "Line interpolation method", + control: "radio", + options: ["linear", "natural", "step"], + table: { + defaultValue: { summary: "natural" }, + category: "Appearance", + }, + }, + strokeWidth: { + description: "Width of the line strokes", + control: "number", + table: { + defaultValue: { summary: "2" }, + category: "Appearance", + }, + }, + strokeDasharray: { + description: "Dash pattern for lines", + control: "text", + table: { + defaultValue: { summary: "undefined" }, + category: "Appearance", + }, + }, + dot: { + description: "Show dots on data points", + control: "boolean", + table: { + defaultValue: { summary: "false" }, + category: "Appearance", + }, + }, + activeDot: { + description: "Show active dots on hover", + control: "boolean", + table: { + defaultValue: { summary: "true" }, + category: "Appearance", + }, + }, + grid: { + description: "Display background grid", + control: "boolean", + table: { + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + legend: { + description: "Display chart legend", + control: "boolean", + table: { + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + showYAxis: { + description: "Display y-axis", + control: "boolean", + table: { + defaultValue: { summary: "false" }, + category: "Display", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const LineChartV2Story: Story = { + name: "🎛️ Data Switcher - Line Chart V2", + args: { + data: lineChartData, + categoryKey: "month", + theme: "ocean", + variant: "natural", + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + strokeWidth: 2, + dot: false, + activeDot: true, + }, + render: (args: any) => { + const [selectedDataType, setSelectedDataType] = + useState("default"); + + const currentData = dataVariations[selectedDataType]; + const currentCategoryKey = categoryKeys[selectedDataType]; + + const buttonStyle = { + margin: "2px", + padding: "6px 12px", + fontSize: "12px", + border: "1px solid #ddd", + borderRadius: "4px", + cursor: "pointer", + background: "#fff", + fontFamily: "monospace", + }; + + const activeButtonStyle = { + ...buttonStyle, + background: "#007acc", + color: "white", + border: "1px solid #007acc", + }; + + return ( +
+
+ 📈 Line Chart Test Suite: +
+ + + + + + + + + +
+
+ + + +
+ ); + }, +}; + +export const BigLabelsStory: Story = { + name: "🏷️ Big Labels", + args: { + data: dataVariations.bigLabels as any, + categoryKey: "category" as any, + theme: "emerald", + variant: "natural", + grid: true, + legend: true, + showYAxis: true, + strokeWidth: 3, + }, + render: (args) => ( + + + + ), +}; + +export const DenseTimelineStory: Story = { + name: "📅 Dense Timeline (Many Periods)", + args: { + data: dataVariations.denseTimeline as any, + categoryKey: "period" as any, + theme: "sunset", + variant: "natural", + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + strokeWidth: 2, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "Tests how the chart handles many data points with medium-length period labels. The chart should enable horizontal scrolling and apply intelligent label truncation.", + }, + }, + }, +}; + +export const StrokeCustomizationStory: Story = { + name: "🎨 Stroke Customization", + args: { + data: dataVariations.default as any, + categoryKey: "month" as any, + theme: "vivid", + variant: "natural", + grid: true, + legend: true, + showYAxis: true, + }, + render: (args) => ( +
+
+

Thick Lines (strokeWidth: 4)

+ + + +
+
+

Dashed Lines

+ + + +
+
+

With Dots

+ + + +
+
+ ), +}; + +export const VariantComparisonStory: Story = { + name: "📐 Line Variants", + args: { + data: dataVariations.minimal as any, + categoryKey: "category" as any, + theme: "orchid", + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + strokeWidth: 3, + }, + render: (args) => ( +
+
+

Linear Variant

+ + + +
+
+

+ Natural Variant (Smooth Curves) +

+ + + +
+
+

Step Variant

+ + + +
+
+ ), + parameters: { + docs: { + description: { + story: + "Compares the three available line variants: linear (straight lines), natural (smooth curves), and step (stepped lines).", + }, + }, + }, +}; + +export const ResponsiveWidthStory: Story = { + name: "📐 Responsive Width Test", + args: { + data: dataVariations.bigLabels as any, + categoryKey: "category" as any, + theme: "sunset", + variant: "natural", + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + strokeWidth: 2, + }, + render: (args) => ( +
+
+

+ Small Container (300px) +

+ + + +
+
+

+ Medium Container (500px) +

+ + + +
+
+

+ Large Container (800px) +

+ + + +
+
+ ), + parameters: { + docs: { + description: { + story: + "Tests how the collision detection and truncation system adapts to different container widths. Smaller containers should show more aggressive truncation.", + }, + }, + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts index b4ec6dc6b..c46c0eb49 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts +++ b/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts @@ -1,5 +1,5 @@ -export type LineChartVariant = "linear" | "natural" | "step"; - export type LineChartV2Data = Array>; +export type LineChartVariant = "linear" | "natural" | "step"; + export type MiniLineChartData = Array | Array<{ value: number; label?: string }>; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts b/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts index e69de29bb..d0e64d3cd 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts @@ -0,0 +1,210 @@ +import { LineChartData } from "../../LineChart/LineChart"; + +const ELEMENT_SPACING = 70; + +/** + * This function returns the width of the data in the chart, used for padding calculation, scroll amount calculation, and + * for the width of the chart container. + * @param data - The data to be displayed in the chart. + */ +const getWidthOfData = (data: LineChartData, containerWidth: number) => { + // For line charts, we calculate based on the number of data points + const numberOfElements = data.length; // Number of data points + + let width = numberOfElements * ELEMENT_SPACING; // here we are defining the spacing between the data points, + // as the data point has no width, we are just calculating the spacing between the data points + // if 3 data points, then 2 spaces between them, so 2*70 = 140 + + if (containerWidth >= width) { + return containerWidth; + } + + if (data.length === 1) { + const minSingleDataWidth = 200; // Minimum width for single data points + // self note: + // if the data point is only one, then we need to set the width to the minimum width + + width = Math.max(width, minSingleDataWidth); + } + + return width; +}; + +/** + * INTERNAL HELPER FUNCTION + * This function returns the width of each group/category for line charts + * this is basically a checking function to follow DRY principle + * @param data - The data to be displayed in the chart. + * @param categoryKey - The key of the category to be displayed in the chart. + */ +const getWidthOfGroup = (data: LineChartData) => { + if (data.length === 0) return 200; // Fallback + + // For line charts, each category/data point has the same spacing + return ELEMENT_SPACING; +}; + +/** + * INTERNAL HELPER FUNCTION + * This function returns the formatter for the X-axis tick values with intelligent truncation for LineChartV2. + * @param groupWidth - The width available for each group/category (optional) + * @param containerWidth - The total container width for responsive calculations (optional) + * @param dataLength - The total length of data for determining first/last positions (optional) + * @returns The formatter for the X-axis tick values. + * Internally used by the XAxis component in Recharts + */ +const getXAxisTickFormatter = (groupWidth?: number, containerWidth?: number) => { + const CHAR_WIDTH = 7; // Average character width in pixels for most fonts + const ELLIPSIS_WIDTH = CHAR_WIDTH * 3; // "..." takes about 3 character widths + const PADDING = 2; // Safety padding for better visual spacing + + // closure is happening here. + return (value: string) => { + // Convert to string in case we get numbers + const stringValue = String(value); + + // If no groupWidth provided, fall back to simple responsive logic + if (!groupWidth) { + // Use container width for responsive truncation + if (containerWidth) { + // Responsive logic based on container width + if (containerWidth < 400) { + // Small containers: very aggressive truncation + return stringValue.length > 4 ? `${stringValue.slice(0, 4)}...` : stringValue; + } else if (containerWidth < 600) { + // Medium containers: moderate truncation + return stringValue.length > 8 ? `${stringValue.slice(0, 8)}...` : stringValue; + } else { + // Large containers: less aggressive truncation + return stringValue.length > 12 ? `${stringValue.slice(0, 12)}...` : stringValue; + } + } + + // Default fallback when no width info available + return stringValue.length > 6 ? `${stringValue.slice(0, 6)}...` : stringValue; + } + + const availableWidth = Math.max(0, groupWidth - PADDING); + const maxCharsWithoutEllipsis = Math.floor(availableWidth / CHAR_WIDTH); + const maxCharsWithEllipsis = Math.floor((availableWidth - ELLIPSIS_WIDTH) / CHAR_WIDTH); + + // Intelligent ellipsis handling for line charts + if (stringValue.length <= maxCharsWithoutEllipsis) { + // Full text fits comfortably + return stringValue; + } else if (maxCharsWithEllipsis >= 3) { + // We can fit at least 3 characters + ellipsis + return `${stringValue.slice(0, maxCharsWithEllipsis)}...`; + } else { + // Very limited space - just show what we can without ellipsis + // (ellipsis would take more space than it's worth) + return stringValue.slice(0, Math.max(1, Math.min(6, maxCharsWithoutEllipsis))); + } + }; +}; + +/** + * Helper function to get the optimal X-axis tick formatter with calculated group width for LineChartV2 + * @param data - The chart data + * @param containerWidth - The container width for responsive calculations + * @returns The optimized formatter function + */ +const getOptimalXAxisTickFormatter = (data: LineChartData, containerWidth?: number) => { + // Calculate the available width per group + const groupWidth = getWidthOfGroup(data); + return getXAxisTickFormatter(groupWidth, containerWidth); +}; + +/** + * Helper function to get position information for X-axis ticks with offset handling + * @param data - The chart data + * @param categoryKey - The category key for the chart + * @returns Object containing position data for the tick renderer + */ +const getXAxisTickPositionData = (data: LineChartData, categoryKey: string) => { + return { + dataLength: data.length, + categoryValues: data.map((item) => String(item[categoryKey])), + getPositionOffset: (value: string): number => { + const index = data.findIndex((item) => String(item[categoryKey]) === value); + if (index === 0) { + // First label: offset to the right by 5px + return 5; + } else if (index === data.length - 1) { + // Last label: offset to the left by 5px + return -5; + } + // Middle labels: no offset + return 0; + }, + isFirstTick: (value: string): boolean => { + const index = data.findIndex((item) => String(item[categoryKey]) === value); + return index === 0; + }, + isLastTick: (value: string): boolean => { + const index = data.findIndex((item) => String(item[categoryKey]) === value); + return index === data.length - 1; + }, + }; +}; + +/** + * This function returns the snap positions for the line chart, used for smooth scrolling + * @param data - The data to be displayed in the chart. + * @param categoryKey - The key of the category to be displayed in the chart. + * @returns The snap positions for the chart. + */ +const getSnapPositions = (data: LineChartData): number[] => { + if (data.length === 0) return [0]; + + const positions = [0]; // Start position + const groupWidthValue = getWidthOfGroup(data); + + // Calculate all valid snap positions based on data points + for (let i = 1; i < data.length; i++) { + positions.push(i * groupWidthValue); + } + + return positions; +}; + +/** + * This function returns the nearest snap position for the line chart + * @param snapPositions - The snap positions for the chart. + * @param currentScroll - The current scroll of the chart. + * @param direction - The direction of the scroll. + * @returns The nearest snap position index for the chart. + */ +const findNearestSnapPosition = ( + snapPositions: number[], + currentScroll: number, + direction: "left" | "right", +): number => { + // Find current position index + let currentIndex = 0; + for (let i = 0; i < snapPositions.length; i++) { + const snapPosition = snapPositions[i]; + if (snapPosition !== undefined && currentScroll >= snapPosition) { + currentIndex = i; + } else { + break; + } + } + + if (direction === "left") { + // Go to previous snap position + return Math.max(0, currentIndex - 1); + } else { + // Go to next snap position + return Math.min(snapPositions.length - 1, currentIndex + 1); + } +}; + +export { + findNearestSnapPosition, + getOptimalXAxisTickFormatter, + getSnapPositions, + getWidthOfData, + getXAxisTickFormatter, + getXAxisTickPositionData, +}; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index 35f047534..fa6f16479 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -8,6 +8,9 @@ // area chart css @forward "./AreaCharts/AreaChartV2/areaChartV2.scss"; +// line chart css +@forward "./LineCharts/LineChartV2/lineChartV2.scss"; + // shared components css @forward "./shared/XAxisTick/xAxisTick.scss"; @forward "./shared/DefaultLegend/defaultLegend.scss"; From f403e474444bd2584496176ff6f207ef05ab4748 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 12 Jun 2025 05:27:42 +0530 Subject: [PATCH 059/190] feat(mini-charts): optimize data rendering for mini charts The changes made in this commit optimize the data rendering for mini area and line charts. The key changes are: 1. Moved the `getRecentDataThatFits` and `transformDataForChart` functions from the `miniAreaChartUtils` module to a shared `BarAndLineUtils` module. This allows these utility functions to be used by both the `MiniAreaChart` and `MiniLineChart` components. 2. Removed the `getRecentLineDataThatFits` and `transformDataForLineChart` functions from the `miniLineChartUtils` module, as they are no longer needed. Instead, the shared `getRecentDataThatFits` and `transformDataForChart` functions are used. 3. Updated the `MiniLineChart` component to use the shared `getRecentDataThatFits` and `transformDataForChart` functions. These changes ensure that the data rendering logic is consistent across the mini area and line chart components, and that the most recent data is displayed when the container width is limited. --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 14 +- .../MiniAreaChart/MiniAreaChart.tsx | 5 +- .../Charts/AreaCharts/utils/AreaChartUtils.ts | 206 ------------------ .../LineCharts/LineChartV2/LineChartV2.tsx | 14 +- .../MiniLineChart/MiniLineChart.tsx | 9 +- .../MiniLineChart/utils/miniLineChartUtils.ts | 57 ----- .../BarAndLineUtils/AreaAndLineUtils.ts} | 169 ++++++++------ .../BarAndLineUtils/MiniAreaAndLineUtils.ts} | 30 ++- 8 files changed, 143 insertions(+), 361 deletions(-) delete mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts delete mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/utils/miniLineChartUtils.ts rename js/packages/react-ui/src/components/Charts/{LineCharts/utils/LineChartUtils.ts => utils/BarAndLineUtils/AreaAndLineUtils.ts} (66%) rename js/packages/react-ui/src/components/Charts/{AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts => utils/BarAndLineUtils/MiniAreaAndLineUtils.ts} (56%) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index 4704fdf49..f5a74801c 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -13,17 +13,17 @@ import { import { cartesianGrid } from "../../cartesianGrid"; import { ActiveDot, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; import { LegendItem } from "../../types"; -import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; -import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; -import { getYAxisTickFormatter } from "../../utils/styleUtils"; -import { AreaChartV2Data, AreaChartVariant } from "../types"; import { findNearestSnapPosition, + getAreaChartWidthOfData, getOptimalXAxisTickFormatter, getSnapPositions, - getWidthOfData, getXAxisTickPositionData, -} from "../utils/AreaChartUtils"; +} from "../../utils/BarAndLineUtils/AreaAndLineUtils"; +import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; +import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; +import { getYAxisTickFormatter } from "../../utils/styleUtils"; +import { AreaChartV2Data, AreaChartVariant } from "../types"; export interface AreaChartV2Props { data: T; @@ -92,7 +92,7 @@ export const AreaChartV2 = ({ }, [effectiveWidth, showYAxis]); const dataWidth = useMemo(() => { - return getWidthOfData(data); + return getAreaChartWidthOfData(data); }, [data]); // Calculate snap positions for proper scrolling alignment diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx index 8caf1038e..e3183698b 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx @@ -2,9 +2,12 @@ import clsx from "clsx"; import { useEffect, useId, useMemo, useRef, useState } from "react"; import { Area, AreaChart as RechartsAreaChart, XAxis } from "recharts"; import { ChartConfig, ChartContainer } from "../../Charts"; +import { + getRecentDataThatFits, + transformDataForChart, +} from "../../utils/BarAndLineUtils/MiniAreaAndLineUtils"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; import { MiniAreaChartData } from "../types"; -import { getRecentDataThatFits, transformDataForChart } from "./utils/miniAreaChartUtils"; export interface MiniAreaChartProps { data: MiniAreaChartData; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts deleted file mode 100644 index d13ed6459..000000000 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/utils/AreaChartUtils.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { AreaChartData } from "../../AreaChart/AreaChart"; - -const ELEMENT_SPACING = 70; - -/** - * This function returns the width of the data in the chart, used for padding calculation, scroll amount calculation, and - * for the width of the chart container. - * @param data - The data to be displayed in the chart. - */ -const getWidthOfData = (data: AreaChartData) => { - // For area charts, we calculate based on the number of data points (always stacked) - const numberOfElements = data.length; // Number of data points - - let width = numberOfElements * ELEMENT_SPACING; // here we are defining the spacing between the data points, - // as the data point has no width, we are just calculating the spacing between the data points - // if 3 data points, then 2 spaces between them, so 2*70 = 140 - - if (data.length === 1) { - const minSingleDataWidth = 200; // Minimum width for single data points - // self note: - // if the data point is only one, then we need to set the width to the minimum width - - width = Math.max(width, minSingleDataWidth); - } - - return width; -}; - -/** - * INTERNAL HELPER FUNCTION - * This function returns the width of each group/category for area charts - * this is basically a checking function to follow DRY principle - * @param data - The data to be displayed in the chart. - * @param categoryKey - The key of the category to be displayed in the chart. - */ -const getWidthOfGroup = (data: AreaChartData) => { - if (data.length === 0) return 200; // Fallback - - // For area charts, each category/data point has the same spacing - return ELEMENT_SPACING; -}; - -/** - * INTERNAL HELPER FUNCTION - * This function returns the formatter for the X-axis tick values with intelligent truncation for AreaChartV2. - * @param groupWidth - The width available for each group/category (optional) - * @param containerWidth - The total container width for responsive calculations (optional) - * @param dataLength - The total length of data for determining first/last positions (optional) - * @returns The formatter for the X-axis tick values. - * Internally used by the XAxis component in Recharts - */ -const getXAxisTickFormatter = (groupWidth?: number, containerWidth?: number) => { - const CHAR_WIDTH = 7; // Average character width in pixels for most fonts - const ELLIPSIS_WIDTH = CHAR_WIDTH * 3; // "..." takes about 3 character widths - const PADDING = 2; // Safety padding for better visual spacing - - // closure is happening here. - return (value: string) => { - // Convert to string in case we get numbers - const stringValue = String(value); - - // If no groupWidth provided, fall back to simple responsive logic - if (!groupWidth) { - // Use container width for responsive truncation - if (containerWidth) { - // Responsive logic based on container width - if (containerWidth < 400) { - // Small containers: very aggressive truncation - return stringValue.length > 4 ? `${stringValue.slice(0, 4)}...` : stringValue; - } else if (containerWidth < 600) { - // Medium containers: moderate truncation - return stringValue.length > 8 ? `${stringValue.slice(0, 8)}...` : stringValue; - } else { - // Large containers: less aggressive truncation - return stringValue.length > 12 ? `${stringValue.slice(0, 12)}...` : stringValue; - } - } - - // Default fallback when no width info available - return stringValue.length > 6 ? `${stringValue.slice(0, 6)}...` : stringValue; - } - - const availableWidth = Math.max(0, groupWidth - PADDING); - const maxCharsWithoutEllipsis = Math.floor(availableWidth / CHAR_WIDTH); - const maxCharsWithEllipsis = Math.floor((availableWidth - ELLIPSIS_WIDTH) / CHAR_WIDTH); - - // Intelligent ellipsis handling for area charts - if (stringValue.length <= maxCharsWithoutEllipsis) { - // Full text fits comfortably - return stringValue; - } else if (maxCharsWithEllipsis >= 3) { - // We can fit at least 3 characters + ellipsis - return `${stringValue.slice(0, maxCharsWithEllipsis)}...`; - } else { - // Very limited space - just show what we can without ellipsis - // (ellipsis would take more space than it's worth) - return stringValue.slice(0, Math.max(1, Math.min(6, maxCharsWithoutEllipsis))); - } - }; -}; - -/** - * Helper function to get the optimal X-axis tick formatter with calculated group width for AreaChartV2 - * @param data - The chart data - * @param containerWidth - The container width for responsive calculations - * @returns The optimized formatter function - */ -const getOptimalXAxisTickFormatter = (data: AreaChartData, containerWidth?: number) => { - // Calculate the available width per group - const groupWidth = getWidthOfGroup(data); - return getXAxisTickFormatter(groupWidth, containerWidth); -}; - -/** - * Helper function to get position information for X-axis ticks with offset handling - * @param data - The chart data - * @param categoryKey - The category key for the chart - * @returns Object containing position data for the tick renderer - */ -const getXAxisTickPositionData = (data: AreaChartData, categoryKey: string) => { - return { - dataLength: data.length, - categoryValues: data.map((item) => String(item[categoryKey])), - getPositionOffset: (value: string): number => { - const index = data.findIndex((item) => String(item[categoryKey]) === value); - if (index === 0) { - // First label: offset to the right by 5px - return 5; - } else if (index === data.length - 1) { - // Last label: offset to the left by 5px - return -5; - } - // Middle labels: no offset - return 0; - }, - isFirstTick: (value: string): boolean => { - const index = data.findIndex((item) => String(item[categoryKey]) === value); - return index === 0; - }, - isLastTick: (value: string): boolean => { - const index = data.findIndex((item) => String(item[categoryKey]) === value); - return index === data.length - 1; - }, - }; -}; - -/** - * This function returns the snap positions for the area chart, used for smooth scrolling - * @param data - The data to be displayed in the chart. - * @param categoryKey - The key of the category to be displayed in the chart. - * @returns The snap positions for the chart. - */ -const getSnapPositions = (data: AreaChartData): number[] => { - if (data.length === 0) return [0]; - - const positions = [0]; // Start position - const groupWidthValue = getWidthOfGroup(data); - - // Calculate all valid snap positions based on data points - for (let i = 1; i < data.length; i++) { - positions.push(i * groupWidthValue); - } - - return positions; -}; - -/** - * This function returns the nearest snap position for the area chart - * @param snapPositions - The snap positions for the chart. - * @param currentScroll - The current scroll of the chart. - * @param direction - The direction of the scroll. - * @returns The nearest snap position index for the chart. - */ -const findNearestSnapPosition = ( - snapPositions: number[], - currentScroll: number, - direction: "left" | "right", -): number => { - // Find current position index - let currentIndex = 0; - for (let i = 0; i < snapPositions.length; i++) { - const snapPosition = snapPositions[i]; - if (snapPosition !== undefined && currentScroll >= snapPosition) { - currentIndex = i; - } else { - break; - } - } - - if (direction === "left") { - // Go to previous snap position - return Math.max(0, currentIndex - 1); - } else { - // Go to next snap position - return Math.min(snapPositions.length - 1, currentIndex + 1); - } -}; - -export { - findNearestSnapPosition, - getOptimalXAxisTickFormatter, - getSnapPositions, - getWidthOfData, - getXAxisTickFormatter, - getXAxisTickPositionData, -}; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx index c73fe1968..9f53befd9 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx @@ -13,17 +13,17 @@ import { import { cartesianGrid } from "../../cartesianGrid"; import { ActiveDot, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; import { LegendItem } from "../../types"; -import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; -import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; -import { getYAxisTickFormatter } from "../../utils/styleUtils"; -import { LineChartV2Data, LineChartVariant } from "../types"; import { findNearestSnapPosition, + getLineChartWidthOfData, getOptimalXAxisTickFormatter, getSnapPositions, - getWidthOfData, getXAxisTickPositionData, -} from "../utils/LineChartUtils"; +} from "../../utils/BarAndLineUtils/AreaAndLineUtils"; +import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; +import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; +import { getYAxisTickFormatter } from "../../utils/styleUtils"; +import { LineChartV2Data, LineChartVariant } from "../types"; export interface LineChartV2Props { data: T; @@ -94,7 +94,7 @@ export const LineChartV2 = ({ }, [effectiveWidth, showYAxis]); const dataWidth = useMemo(() => { - return getWidthOfData(data, effectiveContainerWidth); + return getLineChartWidthOfData(data, effectiveContainerWidth); }, [data, effectiveContainerWidth]); // Calculate snap positions for proper scrolling alignment diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx index 9113bd1e7..a3644e6f2 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx @@ -2,9 +2,12 @@ import clsx from "clsx"; import { useEffect, useMemo, useRef, useState } from "react"; import { Line, LineChart as RechartsLineChart, XAxis } from "recharts"; import { ChartConfig, ChartContainer } from "../../Charts"; +import { + getRecentDataThatFits, + transformDataForChart, +} from "../../utils/BarAndLineUtils/MiniAreaAndLineUtils"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; import { MiniLineChartData } from "../types"; -import { getRecentLineDataThatFits, transformDataForLineChart } from "./utils/miniLineChartUtils"; export interface MiniLineChartProps { data: MiniLineChartData; @@ -52,12 +55,12 @@ export const MiniLineChart = ({ // Get the most recent data that fits in the container const filteredData = useMemo(() => { - return getRecentLineDataThatFits(data, containerWidth); + return getRecentDataThatFits(data, containerWidth); }, [data, containerWidth]); // Transform the filtered data to a consistent format for recharts const chartData = useMemo(() => { - return transformDataForLineChart(filteredData); + return transformDataForChart(filteredData); }, [filteredData]); const colors = useMemo(() => { diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/utils/miniLineChartUtils.ts b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/utils/miniLineChartUtils.ts deleted file mode 100644 index 86efdff02..000000000 --- a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/utils/miniLineChartUtils.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { type MiniLineChartData } from "../../types"; - -export const MINI_LINE_ELEMENT_SPACING: number = 20; - -type ChartData = Array<{ - value: number; - label: string; -}>; - -/** - * Transforms the mini line chart data into a standardized format for rendering. - * Handles both numeric values and objects with value/label properties. - * - * @param data - The mini line chart data array (can contain numbers or objects with value/label) - * @returns An array of chart data objects with value and label properties - */ -const transformDataForLineChart = (data: MiniLineChartData): ChartData => { - return data.map((item, index) => { - if (typeof item === "number") { - return { value: item, label: `Item ${index + 1}` }; - } else { - return { value: item.value, label: item.label || `Item ${index + 1}` }; - } - }); -}; - -/** - * Filters the data to include only the most recent items that can fit within the container width. - * This function ensures the chart displays the latest data when space is limited. - * - * @param data - The complete mini line chart data array - * @param containerWidth - The total width of the container in pixels - * @returns A filtered array containing only the most recent data items that fit in the container - */ -const getRecentLineDataThatFits = ( - data: MiniLineChartData, - containerWidth: number, -): MiniLineChartData => { - if (containerWidth <= 0 || data.length === 0) { - return data; - } - - // Calculate how many items can fit in the available space - const maxItems = Math.floor((containerWidth + 20) / MINI_LINE_ELEMENT_SPACING); - // +20 because the element spacing is between so if we have 2 element then its data 20px data - // so we need to add 20px to the container width to get the actual width of the data - - // If all items fit, return all data - if (maxItems >= data.length) { - return data; - } - - // Return the most recent items that fit - return data.slice(-maxItems); -}; - -export { getRecentLineDataThatFits, transformDataForLineChart }; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts b/js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/AreaAndLineUtils.ts similarity index 66% rename from js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts rename to js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/AreaAndLineUtils.ts index d0e64d3cd..71545b1cb 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/utils/LineChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/AreaAndLineUtils.ts @@ -1,24 +1,28 @@ -import { LineChartData } from "../../LineChart/LineChart"; +// Common utility functions for Area and Line charts +// These functions are chart-type agnostic and can be shared between AreaChartV2 and LineChartV2 + +import { AreaChartV2Data } from "../../AreaCharts/types"; +import { LineChartV2Data } from "../../LineCharts/types"; const ELEMENT_SPACING = 70; +// Common type for chart data - both AreaChart and LineChart data structures +export type ChartData = AreaChartV2Data | LineChartV2Data; + /** - * This function returns the width of the data in the chart, used for padding calculation, scroll amount calculation, and + * AREA CHART SPECIFIC FUNCTION + * This function returns the width of the data in the area chart, used for padding calculation, scroll amount calculation, and * for the width of the chart container. * @param data - The data to be displayed in the chart. */ -const getWidthOfData = (data: LineChartData, containerWidth: number) => { - // For line charts, we calculate based on the number of data points +export const getAreaChartWidthOfData = (data: AreaChartV2Data) => { + // For area charts, we calculate based on the number of data points (always stacked) const numberOfElements = data.length; // Number of data points let width = numberOfElements * ELEMENT_SPACING; // here we are defining the spacing between the data points, // as the data point has no width, we are just calculating the spacing between the data points // if 3 data points, then 2 spaces between them, so 2*70 = 140 - if (containerWidth >= width) { - return containerWidth; - } - if (data.length === 1) { const minSingleDataWidth = 200; // Minimum width for single data points // self note: @@ -31,29 +35,45 @@ const getWidthOfData = (data: LineChartData, containerWidth: number) => { }; /** - * INTERNAL HELPER FUNCTION - * This function returns the width of each group/category for line charts - * this is basically a checking function to follow DRY principle + * LINE CHART SPECIFIC FUNCTION + * This function returns the width of the data in the line chart, used for padding calculation, scroll amount calculation, and + * for the width of the chart container. * @param data - The data to be displayed in the chart. - * @param categoryKey - The key of the category to be displayed in the chart. + * @param containerWidth - The container width to determine if scrolling is needed. */ -const getWidthOfGroup = (data: LineChartData) => { - if (data.length === 0) return 200; // Fallback +export const getLineChartWidthOfData = (data: LineChartV2Data, containerWidth: number) => { + // For line charts, we calculate based on the number of data points + const numberOfElements = data.length; // Number of data points - // For line charts, each category/data point has the same spacing - return ELEMENT_SPACING; + let width = numberOfElements * ELEMENT_SPACING; // here we are defining the spacing between the data points, + // as the data point has no width, we are just calculating the spacing between the data points + // if 3 data points, then 2 spaces between them, so 2*70 = 140 + + if (containerWidth >= width) { + return containerWidth; + } + + if (data.length === 1) { + const minSingleDataWidth = 200; // Minimum width for single data points + // self note: + // if the data point is only one, then we need to set the width to the minimum width + + width = Math.max(width, minSingleDataWidth); + } + + return width; }; /** - * INTERNAL HELPER FUNCTION - * This function returns the formatter for the X-axis tick values with intelligent truncation for LineChartV2. + * SHARED UTILITY FUNCTION + * This function returns the formatter for the X-axis tick values with intelligent truncation. + * This is identical for both AreaChart and LineChart components. * @param groupWidth - The width available for each group/category (optional) * @param containerWidth - The total container width for responsive calculations (optional) - * @param dataLength - The total length of data for determining first/last positions (optional) * @returns The formatter for the X-axis tick values. * Internally used by the XAxis component in Recharts */ -const getXAxisTickFormatter = (groupWidth?: number, containerWidth?: number) => { +export const getXAxisTickFormatter = (groupWidth?: number, containerWidth?: number) => { const CHAR_WIDTH = 7; // Average character width in pixels for most fonts const ELLIPSIS_WIDTH = CHAR_WIDTH * 3; // "..." takes about 3 character widths const PADDING = 2; // Safety padding for better visual spacing @@ -88,7 +108,7 @@ const getXAxisTickFormatter = (groupWidth?: number, containerWidth?: number) => const maxCharsWithoutEllipsis = Math.floor(availableWidth / CHAR_WIDTH); const maxCharsWithEllipsis = Math.floor((availableWidth - ELLIPSIS_WIDTH) / CHAR_WIDTH); - // Intelligent ellipsis handling for line charts + // Intelligent ellipsis handling if (stringValue.length <= maxCharsWithoutEllipsis) { // Full text fits comfortably return stringValue; @@ -104,24 +124,76 @@ const getXAxisTickFormatter = (groupWidth?: number, containerWidth?: number) => }; /** - * Helper function to get the optimal X-axis tick formatter with calculated group width for LineChartV2 + * SHARED UTILITY FUNCTION + * This function returns the nearest snap position index for both chart types. + * The implementation is identical for both AreaChart and LineChart. + * @param snapPositions - The snap positions for the chart. + * @param currentScroll - The current scroll of the chart. + * @param direction - The direction of the scroll. + * @returns The nearest snap position index for the chart. + */ +export const findNearestSnapPosition = ( + snapPositions: number[], + currentScroll: number, + direction: "left" | "right", +): number => { + // Find current position index + let currentIndex = 0; + for (let i = 0; i < snapPositions.length; i++) { + const snapPosition = snapPositions[i]; + if (snapPosition !== undefined && currentScroll >= snapPosition) { + currentIndex = i; + } else { + break; + } + } + + if (direction === "left") { + // Go to previous snap position + return Math.max(0, currentIndex - 1); + } else { + // Go to next snap position + return Math.min(snapPositions.length - 1, currentIndex + 1); + } +}; + +/** + * SHARED UTILITY FUNCTION + * This function returns the width of each group/category for both chart types. + * Both AreaChart and LineChart use the same ELEMENT_SPACING. + * @param data - The data to be displayed in the chart. + * @returns The width of each group/category. + */ +export const getWidthOfGroup = (data: ChartData) => { + if (data.length === 0) return 200; // Fallback + + // Both chart types use the same spacing + return ELEMENT_SPACING; +}; + +/** + * SHARED UTILITY FUNCTION + * Helper function to get the optimal X-axis tick formatter with calculated group width. + * This is generic and works for both AreaChart and LineChart data. * @param data - The chart data * @param containerWidth - The container width for responsive calculations * @returns The optimized formatter function */ -const getOptimalXAxisTickFormatter = (data: LineChartData, containerWidth?: number) => { +export const getOptimalXAxisTickFormatter = (data: ChartData, containerWidth?: number) => { // Calculate the available width per group const groupWidth = getWidthOfGroup(data); return getXAxisTickFormatter(groupWidth, containerWidth); }; /** - * Helper function to get position information for X-axis ticks with offset handling + * SHARED UTILITY FUNCTION + * Helper function to get position information for X-axis ticks with offset handling. + * This is generic and works for both AreaChart and LineChart data. * @param data - The chart data * @param categoryKey - The category key for the chart * @returns Object containing position data for the tick renderer */ -const getXAxisTickPositionData = (data: LineChartData, categoryKey: string) => { +export const getXAxisTickPositionData = (data: ChartData, categoryKey: string) => { return { dataLength: data.length, categoryValues: data.map((item) => String(item[categoryKey])), @@ -149,12 +221,12 @@ const getXAxisTickPositionData = (data: LineChartData, categoryKey: string) => { }; /** - * This function returns the snap positions for the line chart, used for smooth scrolling + * SHARED UTILITY FUNCTION + * This function returns the snap positions for both chart types, used for smooth scrolling. * @param data - The data to be displayed in the chart. - * @param categoryKey - The key of the category to be displayed in the chart. * @returns The snap positions for the chart. */ -const getSnapPositions = (data: LineChartData): number[] => { +export const getSnapPositions = (data: ChartData): number[] => { if (data.length === 0) return [0]; const positions = [0]; // Start position @@ -167,44 +239,3 @@ const getSnapPositions = (data: LineChartData): number[] => { return positions; }; - -/** - * This function returns the nearest snap position for the line chart - * @param snapPositions - The snap positions for the chart. - * @param currentScroll - The current scroll of the chart. - * @param direction - The direction of the scroll. - * @returns The nearest snap position index for the chart. - */ -const findNearestSnapPosition = ( - snapPositions: number[], - currentScroll: number, - direction: "left" | "right", -): number => { - // Find current position index - let currentIndex = 0; - for (let i = 0; i < snapPositions.length; i++) { - const snapPosition = snapPositions[i]; - if (snapPosition !== undefined && currentScroll >= snapPosition) { - currentIndex = i; - } else { - break; - } - } - - if (direction === "left") { - // Go to previous snap position - return Math.max(0, currentIndex - 1); - } else { - // Go to next snap position - return Math.min(snapPositions.length - 1, currentIndex + 1); - } -}; - -export { - findNearestSnapPosition, - getOptimalXAxisTickFormatter, - getSnapPositions, - getWidthOfData, - getXAxisTickFormatter, - getXAxisTickPositionData, -}; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts b/js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/MiniAreaAndLineUtils.ts similarity index 56% rename from js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts rename to js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/MiniAreaAndLineUtils.ts index 981de20a2..f50b7c0af 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/utils/miniAreaChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/MiniAreaAndLineUtils.ts @@ -1,5 +1,10 @@ -import { type MiniAreaChartData } from "../../types"; +// Common utility functions for Mini Area and Line charts +// These functions are shared between MiniAreaChart and MiniLineChart components +import { MiniAreaChartData } from "../../AreaCharts/types"; +import { MiniLineChartData } from "../../LineCharts/types"; + +// Element spacing constant for both chart types export const MINI_ELEMENT_SPACING: number = 20; type ChartData = Array<{ @@ -7,14 +12,18 @@ type ChartData = Array<{ label: string; }>; +// Common type for mini chart data - both area and line use the same structure +export type MiniChartData = MiniAreaChartData | MiniLineChartData; + /** - * Transforms the mini area chart data into a standardized format for rendering. + * Transforms mini chart data into a standardized format for rendering. * Handles both numeric values and objects with value/label properties. + * Works for both MiniAreaChart and MiniLineChart components. * - * @param data - The mini area chart data array (can contain numbers or objects with value/label) + * @param data - The mini chart data array (can contain numbers or objects with value/label) * @returns An array of chart data objects with value and label properties */ -const transformDataForChart = (data: MiniAreaChartData): ChartData => { +export const transformDataForChart = (data: MiniChartData): ChartData => { return data.map((item, index) => { if (typeof item === "number") { return { value: item, label: `Item ${index + 1}` }; @@ -25,17 +34,18 @@ const transformDataForChart = (data: MiniAreaChartData): ChartData => { }; /** - * Filters the data to include only the most recent items that can fit within the container width. + * Filters data to include only the most recent items that can fit within the container width. * This function ensures the chart displays the latest data when space is limited. + * Works for both MiniAreaChart and MiniLineChart components. * - * @param data - The complete mini area chart data array + * @param data - The complete mini chart data array * @param containerWidth - The total width of the container in pixels * @returns A filtered array containing only the most recent data items that fit in the container */ -const getRecentDataThatFits = ( - data: MiniAreaChartData, +export const getRecentDataThatFits = ( + data: MiniChartData, containerWidth: number, -): MiniAreaChartData => { +): MiniChartData => { if (containerWidth <= 0 || data.length === 0) { return data; } @@ -53,5 +63,3 @@ const getRecentDataThatFits = ( // Return the most recent items that fit return data.slice(-maxItems); }; - -export { getRecentDataThatFits, transformDataForChart }; From 41c6d41bab60901a3142a21600aa8dcd8136771f Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 12 Jun 2025 13:48:59 +0530 Subject: [PATCH 060/190] feat(AreaChartV2): Improve data width calculation and X-axis tick formatting The changes made in this commit improve the data width calculation and X-axis tick formatting for the AreaChartV2 and LineChartV2 components. 1. Refactor the `getWidthOfData` function to handle both AreaChartV2 and LineChartV2 data. This function now calculates the width of the data based on the number of data points and the container width, ensuring the chart width is at least the container width. 2. Modify the `getXAxisTickFormatter` function to increase the padding between the tick labels and the chart area, improving the visual spacing. 3. Remove the `getAreaChartWidthOfData` and `getLineChartWidthOfData` functions, as the functionality has been consolidated into the new `getWidthOfData` function. These changes improve the overall responsiveness and visual appearance of the AreaChartV2 and LineChartV2 components. --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 6 +-- .../LineCharts/LineChartV2/LineChartV2.tsx | 4 +- .../utils/BarAndLineUtils/AreaAndLineUtils.ts | 40 +++++-------------- 3 files changed, 15 insertions(+), 35 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index f5a74801c..ff7ce2419 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -15,9 +15,9 @@ import { ActiveDot, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; import { LegendItem } from "../../types"; import { findNearestSnapPosition, - getAreaChartWidthOfData, getOptimalXAxisTickFormatter, getSnapPositions, + getWidthOfData, getXAxisTickPositionData, } from "../../utils/BarAndLineUtils/AreaAndLineUtils"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; @@ -92,8 +92,8 @@ export const AreaChartV2 = ({ }, [effectiveWidth, showYAxis]); const dataWidth = useMemo(() => { - return getAreaChartWidthOfData(data); - }, [data]); + return getWidthOfData(data, effectiveContainerWidth); + }, [data, effectiveContainerWidth]); // Calculate snap positions for proper scrolling alignment const snapPositions = useMemo(() => { diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx index 9f53befd9..01a221dac 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx @@ -15,9 +15,9 @@ import { ActiveDot, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; import { LegendItem } from "../../types"; import { findNearestSnapPosition, - getLineChartWidthOfData, getOptimalXAxisTickFormatter, getSnapPositions, + getWidthOfData, getXAxisTickPositionData, } from "../../utils/BarAndLineUtils/AreaAndLineUtils"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; @@ -94,7 +94,7 @@ export const LineChartV2 = ({ }, [effectiveWidth, showYAxis]); const dataWidth = useMemo(() => { - return getLineChartWidthOfData(data, effectiveContainerWidth); + return getWidthOfData(data, effectiveContainerWidth); }, [data, effectiveContainerWidth]); // Calculate snap positions for proper scrolling alignment diff --git a/js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/AreaAndLineUtils.ts b/js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/AreaAndLineUtils.ts index 71545b1cb..25cce8f1e 100644 --- a/js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/AreaAndLineUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/AreaAndLineUtils.ts @@ -7,47 +7,26 @@ import { LineChartV2Data } from "../../LineCharts/types"; const ELEMENT_SPACING = 70; // Common type for chart data - both AreaChart and LineChart data structures -export type ChartData = AreaChartV2Data | LineChartV2Data; +type ChartData = AreaChartV2Data | LineChartV2Data; /** - * AREA CHART SPECIFIC FUNCTION + * AREA CHART AND LINE CHART SPECIFIC FUNCTION * This function returns the width of the data in the area chart, used for padding calculation, scroll amount calculation, and * for the width of the chart container. * @param data - The data to be displayed in the chart. */ -export const getAreaChartWidthOfData = (data: AreaChartV2Data) => { +export const getWidthOfData = (data: ChartData, containerWidth: number) => { // For area charts, we calculate based on the number of data points (always stacked) const numberOfElements = data.length; // Number of data points - let width = numberOfElements * ELEMENT_SPACING; // here we are defining the spacing between the data points, + let width = numberOfElements * ELEMENT_SPACING - ELEMENT_SPACING; // here we are defining the spacing between the data points, // as the data point has no width, we are just calculating the spacing between the data points // if 3 data points, then 2 spaces between them, so 2*70 = 140 + // so the subtraction is to remove the last spacing as number of data points is 1 more than the number of spaces - if (data.length === 1) { - const minSingleDataWidth = 200; // Minimum width for single data points - // self note: - // if the data point is only one, then we need to set the width to the minimum width - - width = Math.max(width, minSingleDataWidth); - } - - return width; -}; - -/** - * LINE CHART SPECIFIC FUNCTION - * This function returns the width of the data in the line chart, used for padding calculation, scroll amount calculation, and - * for the width of the chart container. - * @param data - The data to be displayed in the chart. - * @param containerWidth - The container width to determine if scrolling is needed. - */ -export const getLineChartWidthOfData = (data: LineChartV2Data, containerWidth: number) => { - // For line charts, we calculate based on the number of data points - const numberOfElements = data.length; // Number of data points - - let width = numberOfElements * ELEMENT_SPACING; // here we are defining the spacing between the data points, - // as the data point has no width, we are just calculating the spacing between the data points - // if 3 data points, then 2 spaces between them, so 2*70 = 140 + // if the container width is greater than the width of the data, then we return the container width + // because the we need the chart minimum width to be the container width + // this decision is made because area chart an bar chart are span from the left to the right of the container if (containerWidth >= width) { return containerWidth; @@ -72,11 +51,12 @@ export const getLineChartWidthOfData = (data: LineChartV2Data, containerWidth: n * @param containerWidth - The total container width for responsive calculations (optional) * @returns The formatter for the X-axis tick values. * Internally used by the XAxis component in Recharts + * this function can be improved for coalition detection and better truncation */ export const getXAxisTickFormatter = (groupWidth?: number, containerWidth?: number) => { const CHAR_WIDTH = 7; // Average character width in pixels for most fonts const ELLIPSIS_WIDTH = CHAR_WIDTH * 3; // "..." takes about 3 character widths - const PADDING = 2; // Safety padding for better visual spacing + const PADDING = 5; // Safety padding for better visual spacing // closure is happening here. return (value: string) => { From aac8a29a46fc3e685e48cfa90cdd990b2e14568c Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 12 Jun 2025 17:41:43 +0530 Subject: [PATCH 061/190] feat(DefaultLegend): add container width and collapse/expand functionality This change adds the `containerWidth` prop to the `DefaultLegend` component and implements a collapsible/expandable functionality for the legend items. The main changes are: - Add `containerWidth` prop to the `DefaultLegend` component to determine the available width for the legen function `calculateVisibleItems` to determine which legend items can fit within the available width, and whether there are more items that can be shown. - Add a toggle button to the legend that allows the user to expand or collapse the legend items. - Adjust the styles of the legend items to accommodate the new functionality, including reducing the size of the indicator icons and adjusting the label width. - Add a new SCSS class `DefaultLegend--collapsed` to handle the collapsed state of the legend. These changes improve the usability of the `DefaultLegend` component by ensuring that the legend items are displayed in a compact and responsive manner, and providing the user with the ability to expand the legend to see all the items. --- .../src/components/Button/button.scss | 4 +- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 7 +- .../BarCharts/BarChartV2/BarChartV2.tsx | 7 +- .../LineCharts/LineChartV2/LineChartV2.tsx | 7 +- .../stories/lineChartV2.stories.tsx | 30 +--- .../shared/DefaultLegend/DefaultLegend.tsx | 63 ++++++- .../shared/DefaultLegend/defaultLegend.scss | 30 +++- .../stories/DefaultLegend.stories.tsx | 166 ++++++++++++++++++ .../DefaultLegend/utils/defaultLegendUtils.ts | 81 +++++++++ 9 files changed, 351 insertions(+), 44 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx create mode 100644 js/packages/react-ui/src/components/Charts/shared/DefaultLegend/utils/defaultLegendUtils.ts diff --git a/js/packages/react-ui/src/components/Button/button.scss b/js/packages/react-ui/src/components/Button/button.scss index d305ffc97..86fd4c9b3 100644 --- a/js/packages/react-ui/src/components/Button/button.scss +++ b/js/packages/react-ui/src/components/Button/button.scss @@ -12,8 +12,8 @@ gap: cssUtils.$spacing-2xs; align-items: center; & svg { - height: 1rem; - width: 1rem; + height: 1em; + width: 1em; } // Primary variant diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index ff7ce2419..59be0fd50 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -351,7 +351,12 @@ export const AreaChartV2 = ({
)} {legend && ( - + )}
); diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 12ae6a950..7b6db16da 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -386,7 +386,12 @@ const BarChartV2Component = ({
)} {legend && ( - + )}
); diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx index 01a221dac..1e29af1fe 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx @@ -341,7 +341,12 @@ export const LineChartV2 = ({
)} {legend && ( - + )}
); diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx index 02092a3f0..52b27e660 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx @@ -281,30 +281,6 @@ const meta: Meta> = { category: "Appearance", }, }, - strokeDasharray: { - description: "Dash pattern for lines", - control: "text", - table: { - defaultValue: { summary: "undefined" }, - category: "Appearance", - }, - }, - dot: { - description: "Show dots on data points", - control: "boolean", - table: { - defaultValue: { summary: "false" }, - category: "Appearance", - }, - }, - activeDot: { - description: "Show active dots on hover", - control: "boolean", - table: { - defaultValue: { summary: "true" }, - category: "Appearance", - }, - }, grid: { description: "Display background grid", control: "boolean", @@ -347,8 +323,6 @@ export const LineChartV2Story: Story = { isAnimationActive: true, showYAxis: true, strokeWidth: 2, - dot: false, - activeDot: true, }, render: (args: any) => { const [selectedDataType, setSelectedDataType] = @@ -522,13 +496,13 @@ export const StrokeCustomizationStory: Story = {

Dashed Lines

- +

With Dots

- +
diff --git a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx index 89bcbeeda..741350c52 100644 --- a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx @@ -1,12 +1,16 @@ import clsx from "clsx"; -import React from "react"; +import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; +import React, { useEffect, useMemo, useState } from "react"; +import { Button } from "../../../Button/Button"; import { type LegendItem } from "../../types"; +import { calculateVisibleItems, getToggleButtonText } from "./utils/defaultLegendUtils"; interface DefaultLegendProps { items: LegendItem[]; className?: string; yAxisLabel?: React.ReactNode; xAxisLabel?: React.ReactNode; + containerWidth?: number; } const DefaultLegend: React.FC = ({ @@ -14,9 +18,37 @@ const DefaultLegend: React.FC = ({ className, yAxisLabel, xAxisLabel, + containerWidth, }) => { + const [isExpanded, setIsExpanded] = useState(false); + + // Only memoize expensive calculations + const { visibleItems, hasMoreItems } = useMemo(() => { + return calculateVisibleItems(items, containerWidth); + }, [items, containerWidth]); + + const displayItems = useMemo(() => { + return isExpanded ? items : visibleItems; + }, [isExpanded, items, visibleItems]); + + // Reset expanded state when items change + useEffect(() => { + setIsExpanded(false); + }, [items]); + + const handleToggleExpanded = () => { + setIsExpanded(!isExpanded); + }; + + const showToggleButton = hasMoreItems; + + const toggleButtonText = useMemo(() => { + return getToggleButtonText(isExpanded, items.length, visibleItems.length); + }, [isExpanded, items.length, visibleItems.length]); + return (
+ {/* this is x and y axis labels container*/} {(xAxisLabel || yAxisLabel) && (
{xAxisLabel && ( @@ -31,9 +63,14 @@ const DefaultLegend: React.FC = ({ )}
)} - -
- {items.map((item) => ( + {/* this is the legend items container*/} +
+ {displayItems.map((item) => (
{item.icon ? ( @@ -46,6 +83,24 @@ const DefaultLegend: React.FC = ({ {item.label}
))} + + {showToggleButton && ( + + )}
); diff --git a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss index 5c01736b5..5580e7cd5 100644 --- a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss @@ -29,7 +29,7 @@ display: flex; align-items: center; justify-content: center; - gap: 16px; + gap: cssUtils.$spacing-m; text-transform: capitalize; flex-wrap: wrap; @@ -37,14 +37,23 @@ padding-top: cssUtils.$spacing-m; } + &--collapsed { + flex-wrap: nowrap; + overflow: hidden; + } + + &--expanded { + flex-wrap: wrap; + } + &-item { display: flex; align-items: center; gap: cssUtils.$spacing-2xs; svg { - height: 12px; - width: 12px; + height: 10px; + width: 10px; color: cssUtils.$primary-text; } @@ -58,10 +67,17 @@ &-label { @include cssUtils.typography(label, small); - max-width: 64px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + color: cssUtils.$primary-text; + } + } + + &-toggle-button { + @include cssUtils.typography(label, small); + color: cssUtils.$primary-text; + + &-icon { + width: 12px; + height: 12px; color: cssUtils.$primary-text; } } diff --git a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx new file mode 100644 index 000000000..f60813b70 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx @@ -0,0 +1,166 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useEffect, useRef, useState } from "react"; +import { LegendItem } from "../../../types"; +import { DefaultLegend } from "../DefaultLegend"; + +const meta: Meta = { + title: "Components/Charts/Shared/DefaultLegend", + component: DefaultLegend, + parameters: { + layout: "centered", + }, + tags: ["!dev", "autodocs"], + argTypes: { + containerWidth: { + control: { type: "range", min: 200, max: 800, step: 50 }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +// Sample legend items with varying label lengths +const shortItems: LegendItem[] = [ + { key: "sales", label: "Sales", color: "#3b82f6" }, + { key: "users", label: "Users", color: "#ef4444" }, + { key: "revenue", label: "Revenue", color: "#10b981" }, +]; + +const mediumItems: LegendItem[] = [ + { key: "sales", label: "Sales Data", color: "#3b82f6" }, + { key: "marketing", label: "Marketing Leads", color: "#ef4444" }, + { key: "revenue", label: "Total Revenue", color: "#10b981" }, + { key: "conversion", label: "Conversion Rate", color: "#f59e0b" }, + { key: "retention", label: "User Retention", color: "#8b5cf6" }, +]; + +const longItems: LegendItem[] = [ + { key: "sales", label: "Monthly Sales Performance", color: "#3b82f6" }, + { key: "marketing", label: "Digital Marketing Campaigns", color: "#ef4444" }, + { key: "revenue", label: "Quarterly Revenue Growth", color: "#10b981" }, + { key: "conversion", label: "Customer Conversion Metrics", color: "#f59e0b" }, + { key: "retention", label: "Long-term User Retention", color: "#8b5cf6" }, + { key: "engagement", label: "User Engagement Analytics", color: "#ec4899" }, + { key: "support", label: "Customer Support Tickets", color: "#14b8a6" }, + { key: "satisfaction", label: "Customer Satisfaction Score", color: "#f97316" }, +]; + +export const Basic: Story = { + args: { + items: shortItems, + containerWidth: 400, + }, +}; + +export const WithAxisLabels: Story = { + args: { + items: mediumItems, + containerWidth: 400, + xAxisLabel: "Time Period", + yAxisLabel: "Value", + }, +}; + +export const SmallContainer: Story = { + args: { + items: mediumItems, + containerWidth: 300, + }, +}; + +export const ManyItemsSmallContainer: Story = { + args: { + items: longItems, + containerWidth: 400, + }, +}; + +export const ManyItemsLargeContainer: Story = { + args: { + items: longItems, + containerWidth: 800, + }, +}; + +export const VerySmallContainer: Story = { + args: { + items: longItems, + containerWidth: 200, + }, +}; + +export const Interactive: Story = { + args: { + items: longItems, + xAxisLabel: "Monthly Data", + yAxisLabel: "Performance Metrics", + }, + render: (args) => { + const DynamicLegendExample = () => { + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(500); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + // Subtract padding from the observed width + const paddingX = 40; // 20px padding on each side + const observedWidth = Math.max(200, entry.contentRect.width - paddingX); + setContainerWidth(observedWidth); + } + }); + + resizeObserver.observe(container); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + return ( +
+

+ This legend demonstrates dynamic behavior with ResizeObserver. + Drag the bottom-right corner to resize the container and see how the + legend adapts in real-time. Current width: {containerWidth}px +

+ +
+ ); + }; + + return ; + }, +}; + +export const DefaultBehavior: Story = { + args: { + items: longItems, + // No containerWidth provided - should show all items + xAxisLabel: "Monthly Data", + yAxisLabel: "Performance Metrics", + }, + render: (args) => ( +
+

+ This demonstrates the default behavior when no containerWidth is provided. All legend items + are shown without truncation or show more/less functionality. +

+ +
+ ), +}; diff --git a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/utils/defaultLegendUtils.ts b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/utils/defaultLegendUtils.ts new file mode 100644 index 000000000..0134f7f27 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/utils/defaultLegendUtils.ts @@ -0,0 +1,81 @@ +import { LegendItem } from "../../../types"; + +// Constants +export const SHOW_MORE_BUTTON_WIDTH = 65; +export const CHARACTER_WIDTH = 7; +export const INDICATOR_WIDTH = 10; +export const GAP_WIDTH = 12; + +/** + * Calculate the estimated width of a legend item + */ +export const calculateItemWidth = (label: string): number => { + return label.length * CHARACTER_WIDTH + INDICATOR_WIDTH + GAP_WIDTH; +}; + +/** + * Calculate which items can fit in the available width + */ +export const calculateVisibleItems = ( + items: LegendItem[], + containerWidth?: number, +): { + visibleItems: LegendItem[]; + hasMoreItems: boolean; +} => { + // If no containerWidth provided, show all items (default behavior) + if (!containerWidth || items.length === 0) { + return { visibleItems: items, hasMoreItems: false }; + } + + // Reserve space for "show more" button + const availableWidth = containerWidth - SHOW_MORE_BUTTON_WIDTH; + + let currentWidth = 0; + let visibleCount = 0; + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (!item) continue; + + const itemWidth = calculateItemWidth(item.label); + + if (currentWidth + itemWidth <= availableWidth) { + currentWidth += itemWidth; + visibleCount++; + } else { + break; + } + } + + // If all items fit, don't need show more button + if (visibleCount === items.length) { + return { visibleItems: items, hasMoreItems: false }; + } + + // If no items fit even with show more button, show at least one + if (visibleCount === 0 && items[0]) { + return { visibleItems: [items[0]], hasMoreItems: items.length > 1 }; + } + + return { + visibleItems: items.slice(0, visibleCount), + hasMoreItems: items.length > visibleCount, + }; +}; + +/** + * Generate toggle button text based on the number of hidden items + */ +export const getToggleButtonText = ( + isExpanded: boolean, + totalItems: number, + visibleItemsCount: number, +): string => { + if (isExpanded) { + return "Show Less"; + } + + const hiddenCount = totalItems - visibleItemsCount; + return hiddenCount === 1 ? "1 more" : `${hiddenCount} more`; +}; From 5f20f0d44e623b443f19ccc219d4a1318d73a31b Mon Sep 17 00:00:00 2001 From: i-subham Date: Thu, 12 Jun 2025 17:56:01 +0530 Subject: [PATCH 062/190] feat(PieChartV2): Introduce new PieChartV2 component with responsive design This commit adds the new PieChartV2 component, which features a responsive design that adjusts based on container dimensions. Key changes include: - Implementation of a new SCSS file for styling the PieChartV2 component. - Refactoring of the component to utilize hooks for dynamic width and height calculations. - Removal of the label prop, simplifying the API. - Integration of a ResizeObserver to handle container size changes. - Updates to the storybook to reflect the new component structure and props. These enhancements improve the usability and visual appeal of the PieChartV2 component, making it more adaptable to various layouts. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 191 ++++++++++------- .../PieCharts/PieChartV2/pieChartV2.scss | 20 ++ .../PieChartV2/stories/PieChartV2.stories.tsx | 12 +- .../components/PieChartRenderers.tsx | 193 ------------------ .../Charts/PieCharts/utils/PieChartUtils.ts | 73 +------ 5 files changed, 137 insertions(+), 352 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 5941ccac4..1baeb2f59 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -1,24 +1,16 @@ import clsx from "clsx"; -import { debounce } from "lodash-es"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; -import { useLayoutContext } from "../../../../context/LayoutContext"; import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; -import { - createGradientDefinitions, - renderActiveShape, - renderCustomLabelLine, -} from "../components/PieChartRenderers"; +import { createGradientDefinitions } from "../components/PieChartRenderers"; import { PieChartData, - calculateChartDimensions, createAnimationConfig, createChartConfig, createEventHandlers, createSectorStyle, getHoverStyles, - layoutMap, transformDataWithPercentages, useChartHover, } from "../utils/PieChartUtils"; @@ -38,7 +30,6 @@ export interface PieChartV2Props { variant?: "pie" | "donut"; format?: "percentage" | "number"; legend?: boolean; - label?: boolean; isAnimationActive?: boolean; appearance?: "circular" | "semiCircular"; cornerRadius?: number; @@ -48,6 +39,9 @@ export interface PieChartV2Props { onMouseEnter?: (data: any, index: number) => void; onMouseLeave?: () => void; onClick?: (data: any, index: number) => void; + className?: string; + height?: number; + width?: number; } export const PieChartV2 = ({ @@ -58,7 +52,6 @@ export const PieChartV2 = ({ variant = "pie", format = "number", legend = true, - label = true, isAnimationActive = true, appearance = "circular", cornerRadius = 0, @@ -68,29 +61,29 @@ export const PieChartV2 = ({ onMouseEnter, onMouseLeave, onClick, + className, + height, + width, }: PieChartV2Props) => { - const { layout } = useLayoutContext(); - const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(120); - const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(0); + const chartContainerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + const [containerHeight, setContainerHeight] = useState(0); const { activeIndex, handleMouseEnter, handleMouseLeave } = useChartHover(); - const containerRef = useRef(null); - // Calculate dynamic radius - useEffect(() => { - if (!containerRef.current) return; + // Use provided dimensions or observed dimensions + const effectiveWidth = useMemo(() => { + return width ?? containerWidth; + }, [width, containerWidth]); - const resizeObserver = new ResizeObserver( - debounce((entries: any) => { - const { width } = entries[0].contentRect; - const { outerRadius, innerRadius } = calculateChartDimensions(width, variant, label); - setCalculatedOuterRadius(outerRadius); - setCalculatedInnerRadius(innerRadius); - }, 100), - ); + const effectiveHeight = useMemo(() => { + return height ?? containerHeight; + }, [height, containerHeight]); - resizeObserver.observe(containerRef.current); - return () => resizeObserver.disconnect(); - }, [variant, label]); + // Calculate chart dimensions based on the smaller dimension + const chartSize = useMemo(() => { + const size = Math.min(effectiveWidth, effectiveHeight); + return Math.max(200, Math.min(size, 800)); // Min 200px, max 800px + }, [effectiveWidth, effectiveHeight]); // Transform data and create configurations const transformedData = transformDataWithPercentages(data, dataKey); @@ -108,56 +101,100 @@ export const PieChartV2 = ({ {createGradientDefinitions(transformedData, colors, gradientColors)} ) : null; + useEffect(() => { + // Only set up ResizeObserver if dimensions are not provided + if ((width && height) || !chartContainerRef.current) { + return () => {}; + } + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const newWidth = entry.contentRect.width; + const newHeight = entry.contentRect.height; + setContainerWidth(newWidth); + setContainerHeight(newHeight); + } + }); + + resizeObserver.observe(chartContainerRef.current); + + return () => { + resizeObserver.disconnect(); + }; + }, [width, height]); + return ( - - - } /> - - {gradientDefinitions} - - // renderCustomLabel( - // { ...props, labelDistance: 1 }, - // format === "percentage" ? "percentage" : String(dataKey), - // format, - // ) - // : false - // } - activeShape={(props: any) => renderActiveShape(props as any)} - activeIndex={activeIndex ?? undefined} - {...animationConfig} - {...eventHandlers} - {...sectorStyle} - startAngle={appearance === "semiCircular" ? 0 : 0} - endAngle={appearance === "semiCircular" ? 180 : 360} - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} +
+
- {transformedData.map((entry, index) => { - const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); - const config = chartConfig[categoryValue]; - const hoverStyles = getHoverStyles(index, activeIndex); - const fill = useGradients ? `url(#gradient-${index})` : config?.color || colors[index]; - - return ; - })} - - - + + + } + /> + + {gradientDefinitions} + + {transformedData.map((entry, index) => { + const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); + const config = chartConfig[categoryValue]; + const hoverStyles = getHoverStyles(index, activeIndex); + const fill = useGradients + ? `url(#gradient-${index})` + : config?.color || colors[index]; + + return ; + })} + + + +
+
+
); }; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss new file mode 100644 index 000000000..8cc54cdaf --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss @@ -0,0 +1,20 @@ +@use "../../../../cssUtils" as cssUtils; + +.crayon-pie-chart-container { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; +} + +.crayon-pie-chart-container-inner { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.crayon-pie-chart { + position: relative; +} diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index 57c009bbc..e99c97ef1 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -113,15 +113,6 @@ const meta: Meta> = { category: "Display", }, }, - label: { - description: "Whether to display the data point labels above each point on the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, isAnimationActive: { description: "Whether to animate the chart", control: "boolean", @@ -165,14 +156,13 @@ export const Default: Story = { variant: "pie", format: "number", legend: true, - label: true, isAnimationActive: true, appearance: "circular", cornerRadius: 0, paddingAngle: 0, }, render: (args) => ( - + ), diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx index 60765ac8f..2ed24f566 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx @@ -1,196 +1,3 @@ -import React from "react"; -import { Sector } from "recharts"; -import { ActiveShapeProps, CustomLabelProps, LabelLineProps } from "../utils/PieChartUtils"; - -/** - * Renders a custom label for pie chart segments - * @param props - The label properties including position, text anchor, and payload data - * @param dataKey - The key to use for displaying the value - * @param format - The format to display the value in ('percentage' or 'number') - * @returns A React element containing the label text, or null if percentage is too small - * - * @example - * // Render a percentage label - * renderCustomLabel(props, "value", "percentage") - * // Renders: "75.5%" - * - * @example - * // Render a number label - * renderCustomLabel(props, "value", "number") - * // Renders: "1234" - */ -export const renderCustomLabel = ( - props: CustomLabelProps & { labelDistance?: number }, - dataKey: string, - format: "percentage" | "number" = "number", -): React.ReactElement | null => { - const { payload, cx, cy, x, y, textAnchor, dominantBaseline, labelDistance } = props; - - if (payload.percentage <= 10) return null; - - let displayValue; - if (format === "percentage") { - displayValue = `${payload.percentage}%`; - } else { - displayValue = payload[dataKey]; - } - - // Move label closer to the center (reduce the radius) - const angle = Math.atan2(y - cy, x - cx); - const distance = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2); - const scale = labelDistance !== undefined ? labelDistance : 0.8; - const newDistance = distance * scale; - - // Correction for bottom label - const isBottom = Math.abs(angle - Math.PI / 2) < 0.3; // ~90 degrees - const yCorrection = isBottom ? 8 : 0; // Adjust this value as needed - const newX = cx + Math.cos(angle) * newDistance; - const newY = cy + Math.sin(angle) * newDistance + yCorrection; - - return ( - - - {displayValue} - - - ); -}; - -/** - * Renders a custom label line connecting pie segments to their labels - * @param props - The label line properties including points, value, and text positioning - * @returns A React element containing the line and percentage text - * - * @example - * // Render a label line with percentage - * renderCustomLabelLine({ - * points: [{x: 100, y: 100}, {x: 150, y: 150}], - * value: 75.5, - * textAnchor: "start", - * dominantBaseline: "middle" - * }) - * // Renders: A line with "75.5%" at the end - */ -export const renderCustomLabelLine = (props: LabelLineProps): React.ReactElement => { - const { points, value, textAnchor, dominantBaseline } = props; - if (!points || points.length < 2) { - return ; - } - const [start, end] = points; - return ( - - - - ); -}; - -/** - * Renders an active (hovered) shape for pie chart segments - * @param props - The active shape properties including position, dimensions, and data - * @returns A React element containing the enhanced active segment with additional visual elements - * - * Features: - * - Displays segment name in the center - * - Shows the main sector with the segment's fill color - * - Adds an outer ring for emphasis - * - Includes a connecting line to the percentage label - * - Shows a small circle at the end of the line - * - Displays the percentage value - * - * @example - * // Render an active shape for a hovered segment - * renderActiveShape({ - * cx: 100, - * cy: 100, - * innerRadius: 50, - * outerRadius: 80, - * startAngle: 0, - * endAngle: 90, - * fill: "#ff0000", - * payload: { name: "Segment A" }, - * percent: 0.25, - * midAngle: 45 - * }) - * // Renders: An enhanced segment with label and percentage - */ -export const renderActiveShape = (props: ActiveShapeProps): React.ReactElement => { - const { - cx, - cy, - innerRadius, - outerRadius, - startAngle, - endAngle, - fill, - payload, - percent, - midAngle, - } = props; - - const RADIAN = Math.PI / 180; - const sin = Math.sin(-RADIAN * midAngle); - const cos = Math.cos(-RADIAN * midAngle); - const labelLineLength = 16; - const labelOffset = 8; - const labelTextGap = 6; - const mx = cx + (outerRadius + labelLineLength) * cos; - const my = cy + (outerRadius + labelLineLength) * sin; - - return ( - - - {payload.name} - - - - = 0 ? 1 : -1) * labelOffset},${my}`} - stroke={fill} - fill="none" - /> - = 0 ? 1 : -1) * labelOffset} cy={my} r={2} fill={fill} stroke="none" /> - = 0 ? 1 : -1) * (labelOffset + labelTextGap)} - y={my} - textAnchor={cos >= 0 ? "start" : "end"} - dominantBaseline="central" - fill="#333" - > - {`${(percent * 100).toFixed(2)}%`} - - - ); -}; - /** * Creates gradient definitions for pie chart segments * @param data - The chart data array diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts index 7e4cc68f6..5436ca267 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts @@ -11,19 +11,6 @@ import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; export type PieChartData = Array>; -export interface CustomLabelProps { - payload: { - percentage: number; - [key: string]: any; - }; - cx: number; - cy: number; - x: number; - y: number; - textAnchor: string; - dominantBaseline: string; -} - export interface ChartDimensions { outerRadius: number; innerRadius: number; @@ -45,33 +32,6 @@ export interface ChartHoverHook { handleMouseLeave: () => void; } -export interface LabelLineProps { - points: [Point, Point]; // Exactly two points required - payload: any; - value: number; - textAnchor: string; - dominantBaseline: string; -} - -interface Point { - x: number; - y: number; -} - -export interface ActiveShapeProps { - cx: number; - cy: number; - innerRadius: number; - outerRadius: number; - startAngle: number; - endAngle: number; - fill: string; - payload: any; - percent: number; - value: number; - midAngle: number; -} - export interface AnimationConfig { isAnimationActive: boolean; animationBegin: number; @@ -104,22 +64,12 @@ export const calculatePercentage = (value: number, total: number): number => { * Calculates dimensions for standard pie/donut charts * @param width - The container width * @param variant - The chart variant ('pie' or 'donut') - * @param label - Whether the chart has labels * @returns Object containing outer and inner radius values */ -export const calculateChartDimensions = ( - width: number, - variant: string, - label: boolean, -): ChartDimensions => { +export const calculateChartDimensions = (width: number, variant: string): ChartDimensions => { const baseRadiusPercentage = 0.4; // 40% of container width - let outerRadius = Math.round(width * baseRadiusPercentage); - if (label) { - outerRadius = Math.round(outerRadius * 0.9); - } - // Set minimum and maximum bounds for radius outerRadius = Math.max(50, Math.min(outerRadius, width / 2 - 10)); // Ensure radius isn't too small or too large @@ -135,21 +85,12 @@ export const calculateChartDimensions = ( /** * Calculates dimensions for two-level pie charts * @param width - The container width - * @param label - Whether the chart has labels * @returns Object containing outer, middle, and inner radius values */ -export const calculateTwoLevelChartDimensions = ( - width: number, - label: boolean, -): TwoLevelChartDimensions => { +export const calculateTwoLevelChartDimensions = (width: number): TwoLevelChartDimensions => { const baseRadiusPercentage = 0.4; // 40% of container width - let outerRadius = Math.round(width * baseRadiusPercentage); - if (label) { - outerRadius = Math.round(outerRadius * 0.9); - } - // Set minimum and maximum bounds for radius outerRadius = Math.max(50, Math.min(outerRadius, width / 2 - 10)); @@ -166,16 +107,6 @@ export const calculateTwoLevelChartDimensions = ( // Layout and Styling Utilities // ========================================== -/** - * Map of layout types to their corresponding CSS classes - */ -export const layoutMap: Record = { - mobile: "crayon-pie-chart-container-mobile", - fullscreen: "crayon-pie-chart-container-fullscreen", - tray: "crayon-pie-chart-container-tray", - copilot: "crayon-pie-chart-container-copilot", -}; - /** * Generates hover style properties for chart cells * @param index - The index of the current cell From 94b6279fe88cde5c3a98dab7ba4fefaa5b70c88f Mon Sep 17 00:00:00 2001 From: i-subham Date: Fri, 13 Jun 2025 12:38:43 +0530 Subject: [PATCH 063/190] feat(PieChartV2): Enhance PieChartV2 with Resizable and Stacked Legend Features This commit introduces significant updates to the PieChartV2 component, including: - Implementation of a resizable container for the PieChartV2, allowing users to adjust the chart's dimensions dynamically. - Addition of a new StackedLegend component that displays legend items alongside the chart, enhancing data visualization. - Refactoring of storybook examples to demonstrate the new interactive features, including a responsive layout that adapts to container size changes. - Introduction of SCSS styles for the StackedLegend to improve its appearance and usability. These enhancements improve the overall user experience by providing a more interactive and visually appealing charting solution. --- .../PieChartV2/stories/PieChartV2.stories.tsx | 236 +++++++++++++++++- .../shared/StackedLegend/StackedLegend.tsx | 52 ++++ .../shared/StackedLegend/stackedLegend.scss | 65 +++++ 3 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx create mode 100644 js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index e99c97ef1..656100f57 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -1,5 +1,10 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Card } from "../../../../Card"; +import { DefaultLegend } from "../../../shared/DefaultLegend/DefaultLegend"; +import StackedLegend from "../../../shared/StackedLegend/StackedLegend"; +import { LegendItem } from "../../../types"; +import { getDistributedColors, getPalette } from "../../../utils/PalletUtils"; import { PieChartV2, PieChartV2Props } from "../PieChartV2"; const pieChartData = [ @@ -161,11 +166,128 @@ export const Default: Story = { cornerRadius: 0, paddingAngle: 0, }, - render: (args) => ( - - - - ), + render: (args) => { + const ResizableExample = () => { + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(500); + + // Create legend items from the chart data + const legendItems: LegendItem[] = useMemo(() => { + const palette = getPalette(args.theme || "ocean"); + const colors = getDistributedColors(palette, args.data.length); + + return args.data.map((item, index) => ({ + key: String(item[args.categoryKey]), + label: String(item[args.categoryKey]), + color: colors[index] || "#000000", // Fallback color if undefined + })); + }, [args.data, args.categoryKey, args.theme]); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + // Subtract padding from the observed width + const paddingX = 40; // 20px padding on each side + const observedWidth = Math.max(200, entry.contentRect.width - paddingX); + setContainerWidth(observedWidth); + } + }); + + resizeObserver.observe(container); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + return ( + + + + + + + ); + }; + + return ; + }, +}; + +export const Interactive: Story = { + name: "Interactive with Resize", + args: { + ...Default.args, + theme: "vivid", + variant: "donut", + cornerRadius: 5, + paddingAngle: 1, + format: "percentage", + }, + render: (args) => { + const ResizableExample = () => { + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(500); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + // Subtract padding from the observed width + const paddingX = 40; // 20px padding on each side + const observedWidth = Math.max(200, entry.contentRect.width - paddingX); + setContainerWidth(observedWidth); + } + }); + + resizeObserver.observe(container); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + return ( +
+

+ This example demonstrates how the chart and legend adapt to container width. + Drag the bottom-right corner to resize the container and see how the + components adapt in real-time. Current width: {containerWidth}px +

+ + + +
+ ); + }; + + return ; + }, }; export const DonutChart: Story = { @@ -278,3 +400,107 @@ export const SingleColorGradients: Story = {
), }; + +export const WithStackedLegend: Story = { + name: "With Stacked Legend", + args: { + ...Default.args, + theme: "vivid", + variant: "pie", + cornerRadius: 5, + paddingAngle: 1, + format: "percentage", + }, + render: (args) => { + const ResizableExample = () => { + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(500); + const [containerHeight, setContainerHeight] = useState(400); + + // Create legend items from the chart data + const legendItems = useMemo(() => { + const palette = getPalette(args.theme || "vivid"); + const colors = getDistributedColors(palette, args.data.length); + + return args.data.map((item, index) => ({ + key: String(item[args.categoryKey]), + label: String(item[args.categoryKey]), + value: Number(item[args.dataKey]), + color: colors[index] || "#000000", + })); + }, [args.data, args.categoryKey, args.dataKey, args.theme]); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const paddingX = 40; + const paddingY = 40; + const observedWidth = Math.max(200, entry.contentRect.width - paddingX); + const observedHeight = Math.max(200, entry.contentRect.height - paddingY); + setContainerWidth(observedWidth); + setContainerHeight(observedHeight); + } + }); + + resizeObserver.observe(container); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + return ( + +
+ + + +
+ +
+
+
+ ); + }; + + return ; + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx new file mode 100644 index 000000000..31288bbbc --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx @@ -0,0 +1,52 @@ +import "./stackedLegend.scss"; + +interface LegendItem { + key: string; + label: string; + value: number; + color: string; +} + +interface StackedLegendProps { + items: LegendItem[]; + onItemHover?: (key: string | null) => void; + activeKey?: string | null; +} + +const StackedLegend = ({ items, onItemHover, activeKey }: StackedLegendProps) => { + const handleMouseEnter = (key: string) => { + onItemHover?.(key); + }; + + const handleMouseLeave = () => { + onItemHover?.(null); + }; + + return ( +
+ {items.map((item) => ( +
handleMouseEnter(item.key)} + onMouseLeave={handleMouseLeave} + > +
+
+
+
+
{item.label}
+
+
{item.value}
+
+ ))} +
+ ); +}; + +export default StackedLegend; diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss new file mode 100644 index 000000000..2cbfe86e8 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss @@ -0,0 +1,65 @@ +@use "../../../../cssUtils" as cssUtils; +.crayon-stacked-legend { + width: 100%; + display: flex; + flex-direction: column; + gap: 10px; + min-width: 300px; + + &__item { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 10px; + padding: 4px 8px; + border-radius: 4px; + transition: background-color 0.2s ease; + + &:hover { + background-color: rgba(0, 0, 0, 0.05); + cursor: pointer; + } + + &--active { + background-color: rgba(0, 0, 0, 0.1); + } + + &-label { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + &-text { + color: cssUtils.$primary-text; + } + } + + &-color-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 10px; + height: 36px; + width: 32px; + } + + &-color { + width: 10px; + height: 10px; + border-radius: 2px; + transition: transform 0.2s ease; + + .crayon-stacked-legend__item:hover & { + transform: scale(1.2); + } + } + + &-label-text { + font-size: 14px; + color: cssUtils.$primary-text; + } + } +} From 764c06c28e238dbd012d859cd8a70aa95c2885b5 Mon Sep 17 00:00:00 2001 From: i-subham Date: Fri, 13 Jun 2025 18:08:58 +0530 Subject: [PATCH 064/190] feat(PieChartV2): Update PieChartV2 story data and enhance StackedLegend functionality This commit modifies the PieChartV2 story data to reflect a more simplified dataset with monthly values ranging from 10 to 120. Additionally, it enhances the StackedLegend component by implementing percentage formatting for item values and limiting the number of visible items to improve clarity and usability. These changes aim to provide a more intuitive and informative visualization experience. --- .../PieChartV2/stories/PieChartV2.stories.tsx | 19 ++++++---- .../shared/StackedLegend/StackedLegend.tsx | 38 ++++++++++++++++++- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index 656100f57..0c69e68d8 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -8,13 +8,18 @@ import { getDistributedColors, getPalette } from "../../../utils/PalletUtils"; import { PieChartV2, PieChartV2Props } from "../PieChartV2"; const pieChartData = [ - { month: "January", value: 4250 }, - { month: "February", value: 3820 }, - { month: "March", value: 4680 }, - { month: "April", value: 4120 }, - { month: "May", value: 5340 }, - { month: "June", value: 6250 }, - { month: "July", value: 5890 }, + { month: "January", value: 10 }, + { month: "February", value: 20 }, + { month: "March", value: 30 }, + { month: "April", value: 40 }, + { month: "May", value: 50 }, + { month: "June", value: 60 }, + { month: "July", value: 70 }, + { month: "August", value: 80 }, + { month: "September", value: 90 }, + { month: "October", value: 100 }, + { month: "November", value: 110 }, + { month: "December", value: 120 }, ]; const gradientColors = [ diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx index 31288bbbc..6bf4d6b8b 100644 --- a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx @@ -13,6 +13,13 @@ interface StackedLegendProps { activeKey?: string | null; } +const formatPercentage = (value: number, total: number): string => { + const percentage = (value / total) * 100; + return `${percentage.toFixed(1)}%`; +}; + +const MAX_VISIBLE_ITEMS = 10; + const StackedLegend = ({ items, onItemHover, activeKey }: StackedLegendProps) => { const handleMouseEnter = (key: string) => { onItemHover?.(key); @@ -22,9 +29,36 @@ const StackedLegend = ({ items, onItemHover, activeKey }: StackedLegendProps) => onItemHover?.(null); }; + // Calculate total for percentage + const total = items.reduce((sum, item) => sum + item.value, 0); + + // Sort items by value in descending order (higher to lower) + const sortedItems = [...items].sort((a, b) => b.value - a.value); + + // Process items to handle more than MAX_VISIBLE_ITEMS + const processedItems = (() => { + if (sortedItems.length <= MAX_VISIBLE_ITEMS) { + return sortedItems; + } + + const visibleItems = sortedItems.slice(0, MAX_VISIBLE_ITEMS - 1); + const remainingItems = sortedItems.slice(MAX_VISIBLE_ITEMS - 1); + const remainingTotal = remainingItems.reduce((sum, item) => sum + item.value, 0); + + return [ + ...visibleItems, + { + key: "others", + label: "Others", + value: remainingTotal, + color: "#808080", // Gray color for Others + }, + ]; + })(); + return (
- {items.map((item) => ( + {processedItems.map((item) => (
{item.label}
-
{item.value}
+
{formatPercentage(item.value, total)}
))}
From 131ea0bf8e84673ef5951629fc58b4a094aa1d32 Mon Sep 17 00:00:00 2001 From: i-subham Date: Fri, 13 Jun 2025 18:17:34 +0530 Subject: [PATCH 065/190] feat(PieChartV2): Update PieChartV2 story data and enhance StackedLegend styles This commit updates the PieChartV2 story data with new monthly values for improved visualization. Additionally, it enhances the StackedLegend component by applying consistent typography styles to both the label and value elements, ensuring better readability and a cohesive design. These changes aim to provide a more informative and visually appealing charting experience. --- .../PieChartV2/stories/PieChartV2.stories.tsx | 25 +++++++++---------- .../shared/StackedLegend/StackedLegend.tsx | 4 ++- .../shared/StackedLegend/stackedLegend.scss | 7 +++++- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index 0c69e68d8..8ef333970 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -8,20 +8,19 @@ import { getDistributedColors, getPalette } from "../../../utils/PalletUtils"; import { PieChartV2, PieChartV2Props } from "../PieChartV2"; const pieChartData = [ - { month: "January", value: 10 }, - { month: "February", value: 20 }, - { month: "March", value: 30 }, - { month: "April", value: 40 }, - { month: "May", value: 50 }, - { month: "June", value: 60 }, - { month: "July", value: 70 }, - { month: "August", value: 80 }, - { month: "September", value: 90 }, - { month: "October", value: 100 }, - { month: "November", value: 110 }, - { month: "December", value: 120 }, + { month: "January", value: 1250 }, + { month: "February", value: 980 }, + { month: "March", value: 1450 }, + { month: "April", value: 1320 }, + { month: "May", value: 1680 }, + { month: "June", value: 2100 }, + { month: "July", value: 1950 }, + { month: "August", value: 1820 }, + { month: "September", value: 1650 }, + { month: "October", value: 1480 }, + { month: "November", value: 1350 }, + { month: "December", value: 1200 }, ]; - const gradientColors = [ { start: "#FF6B6B", end: "#FF8E8E" }, { start: "#4ECDC4", end: "#6ED7D0" }, diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx index 6bf4d6b8b..9efc7e1ad 100644 --- a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx @@ -76,7 +76,9 @@ const StackedLegend = ({ items, onItemHover, activeKey }: StackedLegendProps) =>
{item.label}
-
{formatPercentage(item.value, total)}
+
+ {formatPercentage(item.value, total)} +
))}
diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss index 2cbfe86e8..7ba652730 100644 --- a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss @@ -58,7 +58,12 @@ } &-label-text { - font-size: 14px; + @include cssUtils.typography(label, large); + color: cssUtils.$primary-text; + } + + &-value { + @include cssUtils.typography(label, large); color: cssUtils.$primary-text; } } From 801c7eeded993fb49d46f0a45e3da0e573df0d95 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 13 Jun 2025 22:39:01 +0530 Subject: [PATCH 066/190] feat(DefaultLegend): add expand/collapse functionality Adds an expanded version of the DefaultLegend component with support for expanding and collapsing legend items. This allows displaying a larger number of items in a limited container width by showing only the items that fit initially and providing a toggle button to expand and see all items. The changes include: - Added a new set of sample legend items with longer labels - Implemented the expand/collapse logic, including dynamic width calculation and responsive behavior - Added a new story to demonstrate the expand/collapse functionality - Added a second story to showcase the responsive behavior of the expand/collapse feature across different container widths --- .cursor/rules/use-pnpm.mdc | 6 + .cursorignore | 2 + .../stories/areaChartV2.stories.tsx | 255 +++++++++++++++++ .../BarChartV2/stories/barChartV2.stories.tsx | 256 ++++++++++++++++++ .../stories/lineChartV2.stories.tsx | 256 ++++++++++++++++++ .../stories/DefaultLegend.stories.tsx | 172 ++++++++++++ 6 files changed, 947 insertions(+) create mode 100644 .cursor/rules/use-pnpm.mdc create mode 100644 .cursorignore diff --git a/.cursor/rules/use-pnpm.mdc b/.cursor/rules/use-pnpm.mdc new file mode 100644 index 000000000..77f0e6ad7 --- /dev/null +++ b/.cursor/rules/use-pnpm.mdc @@ -0,0 +1,6 @@ +--- +description: any task that uses terminal +globs: +alwaysApply: false +--- +Always use pnpm insteed of npm for this project diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 000000000..d07acd33b --- /dev/null +++ b/.cursorignore @@ -0,0 +1,2 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) +.env \ No newline at end of file diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx index b6a28b3aa..a66655413 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -194,6 +194,225 @@ const dataVariations = { { category: "Desktop Computers", users: 280, sessions: 180 }, { category: "Tablet Devices", users: 220, sessions: 140 }, ], + // 🔄 EXPAND/COLLAPSE - Marketing channels dataset for legend overflow testing + expandCollapseMarketing: [ + { + channel: "Website Traffic and Organic Search Results", + impressions: 120000, + clicks: 15000, + conversions: 1200, + cost: 8500, + revenue: 24000, + roi: 182, + ctr: 12.5, + cpc: 0.57, + cpa: 7.08, + reach: 95000, + engagement: 8200, + shares: 420, + saves: 180, + comments: 650, + videoViews: 0, + }, + { + channel: "Social Media Engagement and Brand Awareness", + impressions: 85000, + clicks: 12000, + conversions: 950, + cost: 6200, + revenue: 19000, + roi: 206, + ctr: 14.1, + cpc: 0.52, + cpa: 6.53, + reach: 72000, + engagement: 15400, + shares: 890, + saves: 340, + comments: 1200, + videoViews: 28000, + }, + { + channel: "Email Marketing Campaign Performance", + impressions: 45000, + clicks: 8500, + conversions: 800, + cost: 2100, + revenue: 16000, + roi: 562, + ctr: 18.9, + cpc: 0.25, + cpa: 2.63, + reach: 42000, + engagement: 6800, + shares: 120, + saves: 85, + comments: 240, + videoViews: 0, + }, + { + channel: "Paid Advertising and PPC Campaign ROI", + impressions: 95000, + clicks: 18000, + conversions: 1500, + cost: 12500, + revenue: 30000, + roi: 140, + ctr: 18.9, + cpc: 0.69, + cpa: 8.33, + reach: 88000, + engagement: 12600, + shares: 320, + saves: 150, + comments: 480, + videoViews: 5200, + }, + { + channel: "Content Marketing and Blog Performance", + impressions: 60000, + clicks: 9500, + conversions: 750, + cost: 4800, + revenue: 15000, + roi: 213, + ctr: 15.8, + cpc: 0.51, + cpa: 6.4, + reach: 55000, + engagement: 7800, + shares: 680, + saves: 420, + comments: 950, + videoViews: 12000, + }, + { + channel: "Mobile Application Downloads and Usage", + impressions: 70000, + clicks: 11000, + conversions: 1100, + cost: 5500, + revenue: 22000, + roi: 300, + ctr: 15.7, + cpc: 0.5, + cpa: 5.0, + reach: 65000, + engagement: 9200, + shares: 180, + saves: 95, + comments: 320, + videoViews: 8500, + }, + { + channel: "Customer Support Response Time and Quality", + impressions: 35000, + clicks: 5500, + conversions: 450, + cost: 2800, + revenue: 9000, + roi: 221, + ctr: 15.7, + cpc: 0.51, + cpa: 6.22, + reach: 32000, + engagement: 4200, + shares: 45, + saves: 25, + comments: 180, + videoViews: 0, + }, + { + channel: "Sales Funnel Conversion and Lead Generation", + impressions: 110000, + clicks: 22000, + conversions: 1800, + cost: 15000, + revenue: 36000, + roi: 140, + ctr: 20.0, + cpc: 0.68, + cpa: 8.33, + reach: 98000, + engagement: 18500, + shares: 420, + saves: 280, + comments: 750, + videoViews: 3200, + }, + { + channel: "User Retention and Churn Rate Analysis", + impressions: 80000, + clicks: 14000, + conversions: 1200, + cost: 7200, + revenue: 24000, + roi: 233, + ctr: 17.5, + cpc: 0.51, + cpa: 6.0, + reach: 75000, + engagement: 11200, + shares: 280, + saves: 150, + comments: 420, + videoViews: 6800, + }, + { + channel: "Product Feature Usage and Performance Metrics", + impressions: 65000, + clicks: 10500, + conversions: 900, + cost: 5200, + revenue: 18000, + roi: 246, + ctr: 16.2, + cpc: 0.5, + cpa: 5.78, + reach: 58000, + engagement: 8500, + shares: 320, + saves: 180, + comments: 650, + videoViews: 4200, + }, + { + channel: "Market Research and Competitive Analysis", + impressions: 40000, + clicks: 6500, + conversions: 500, + cost: 3200, + revenue: 10000, + roi: 213, + ctr: 16.3, + cpc: 0.49, + cpa: 6.4, + reach: 36000, + engagement: 5200, + shares: 95, + saves: 65, + comments: 220, + videoViews: 1800, + }, + { + channel: "Brand Sentiment and Public Relations Impact", + impressions: 55000, + clicks: 8000, + conversions: 650, + cost: 4100, + revenue: 13000, + roi: 217, + ctr: 14.5, + cpc: 0.51, + cpa: 6.31, + reach: 48000, + engagement: 6800, + shares: 520, + saves: 280, + comments: 950, + videoViews: 15000, + }, + ], }; // Category key mappings for different datasets @@ -207,6 +426,7 @@ const categoryKeys = { mixedLengths: "item", edgeCases: "name", minimal: "category", + expandCollapseMarketing: "channel", }; // 🔥 ACTIVE DATA - For backward compatibility @@ -498,6 +718,14 @@ export const AreaChartV2Story: Story = { > 📱 Minimal (3 items) +
( + + + + ), + parameters: { + docs: { + description: { + story: + "Tests the legend expand/collapse functionality with 12 marketing channels that have long descriptive names. The legend should automatically show a 'Show More' button when items overflow the container width, allowing users to toggle between collapsed and expanded states.", + }, + }, + }, +}; + export const ResponsiveWidthStory: Story = { name: "📐 Responsive Width Test", args: { diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx index 91471cc9c..6fb630ae2 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx @@ -246,6 +246,225 @@ const dataVariations = { { company: "Tesla", revenue: 96773000000, profit: 15000000000, marketCap: 800000000000 }, // 96B, 15B, 800B { company: "Meta", revenue: 134902000000, profit: 39370000000, marketCap: 900000000000 }, // 134B, 39B, 900B ], + // 🔄 EXPAND/COLLAPSE - Marketing channels dataset for legend overflow testing + expandCollapseMarketing: [ + { + channel: "Website Traffic and Organic Search Results", + impressions: 120000, + clicks: 15000, + conversions: 1200, + cost: 8500, + revenue: 24000, + roi: 182, + ctr: 12.5, + cpc: 0.57, + cpa: 7.08, + reach: 95000, + engagement: 8200, + shares: 420, + saves: 180, + comments: 650, + videoViews: 0, + }, + { + channel: "Social Media Engagement and Brand Awareness", + impressions: 85000, + clicks: 12000, + conversions: 950, + cost: 6200, + revenue: 19000, + roi: 206, + ctr: 14.1, + cpc: 0.52, + cpa: 6.53, + reach: 72000, + engagement: 15400, + shares: 890, + saves: 340, + comments: 1200, + videoViews: 28000, + }, + { + channel: "Email Marketing Campaign Performance", + impressions: 45000, + clicks: 8500, + conversions: 800, + cost: 2100, + revenue: 16000, + roi: 562, + ctr: 18.9, + cpc: 0.25, + cpa: 2.63, + reach: 42000, + engagement: 6800, + shares: 120, + saves: 85, + comments: 240, + videoViews: 0, + }, + { + channel: "Paid Advertising and PPC Campaign ROI", + impressions: 95000, + clicks: 18000, + conversions: 1500, + cost: 12500, + revenue: 30000, + roi: 140, + ctr: 18.9, + cpc: 0.69, + cpa: 8.33, + reach: 88000, + engagement: 12600, + shares: 320, + saves: 150, + comments: 480, + videoViews: 5200, + }, + { + channel: "Content Marketing and Blog Performance", + impressions: 60000, + clicks: 9500, + conversions: 750, + cost: 4800, + revenue: 15000, + roi: 213, + ctr: 15.8, + cpc: 0.51, + cpa: 6.4, + reach: 55000, + engagement: 7800, + shares: 680, + saves: 420, + comments: 950, + videoViews: 12000, + }, + { + channel: "Mobile Application Downloads and Usage", + impressions: 70000, + clicks: 11000, + conversions: 1100, + cost: 5500, + revenue: 22000, + roi: 300, + ctr: 15.7, + cpc: 0.5, + cpa: 5.0, + reach: 65000, + engagement: 9200, + shares: 180, + saves: 95, + comments: 320, + videoViews: 8500, + }, + { + channel: "Customer Support Response Time and Quality", + impressions: 35000, + clicks: 5500, + conversions: 450, + cost: 2800, + revenue: 9000, + roi: 221, + ctr: 15.7, + cpc: 0.51, + cpa: 6.22, + reach: 32000, + engagement: 4200, + shares: 45, + saves: 25, + comments: 180, + videoViews: 0, + }, + { + channel: "Sales Funnel Conversion and Lead Generation", + impressions: 110000, + clicks: 22000, + conversions: 1800, + cost: 15000, + revenue: 36000, + roi: 140, + ctr: 20.0, + cpc: 0.68, + cpa: 8.33, + reach: 98000, + engagement: 18500, + shares: 420, + saves: 280, + comments: 750, + videoViews: 3200, + }, + { + channel: "User Retention and Churn Rate Analysis", + impressions: 80000, + clicks: 14000, + conversions: 1200, + cost: 7200, + revenue: 24000, + roi: 233, + ctr: 17.5, + cpc: 0.51, + cpa: 6.0, + reach: 75000, + engagement: 11200, + shares: 280, + saves: 150, + comments: 420, + videoViews: 6800, + }, + { + channel: "Product Feature Usage and Performance Metrics", + impressions: 65000, + clicks: 10500, + conversions: 900, + cost: 5200, + revenue: 18000, + roi: 246, + ctr: 16.2, + cpc: 0.5, + cpa: 5.78, + reach: 58000, + engagement: 8500, + shares: 320, + saves: 180, + comments: 650, + videoViews: 4200, + }, + { + channel: "Market Research and Competitive Analysis", + impressions: 40000, + clicks: 6500, + conversions: 500, + cost: 3200, + revenue: 10000, + roi: 213, + ctr: 16.3, + cpc: 0.49, + cpa: 6.4, + reach: 36000, + engagement: 5200, + shares: 95, + saves: 65, + comments: 220, + videoViews: 1800, + }, + { + channel: "Brand Sentiment and Public Relations Impact", + impressions: 55000, + clicks: 8000, + conversions: 650, + cost: 4100, + revenue: 13000, + roi: 217, + ctr: 14.5, + cpc: 0.51, + cpa: 6.31, + reach: 48000, + engagement: 6800, + shares: 520, + saves: 280, + comments: 950, + videoViews: 15000, + }, + ], }; // Category key mappings for different datasets @@ -258,6 +477,7 @@ const categoryKeys = { numbers: "category", weekly: "week", bigNumbers: "company", + expandCollapseMarketing: "channel", }; // 🔥 ACTIVE DATA - For backward compatibility @@ -519,6 +739,14 @@ export const BarChartV2Story: Story = { > 🔢 Number Ranges +
Current: {selectedDataType} | Items:{" "} @@ -660,3 +888,31 @@ export const NumberRangesStory: Story = { ), }; + +export const ExpandCollapseMarketingStory: Story = { + name: "🔄 Marketing Channels (Legend Expand/Collapse)", + args: { + data: dataVariations.expandCollapseMarketing as any, + categoryKey: "channel" as any, + theme: "emerald", + variant: "grouped", + radius: 4, + grid: true, + isAnimationActive: true, + showYAxis: true, + legend: true, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "Tests the legend expand/collapse functionality with 12 marketing channels that have long descriptive names. The legend should automatically show a 'Show More' button when items overflow the container width, allowing users to toggle between collapsed and expanded states.", + }, + }, + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx index 52b27e660..7d99807fc 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx @@ -194,6 +194,225 @@ const dataVariations = { { category: "Desktop Computers", users: 280, sessions: 180 }, { category: "Tablet Devices", users: 220, sessions: 140 }, ], + // 🔄 EXPAND/COLLAPSE - Marketing channels dataset for legend overflow testing + expandCollapseMarketing: [ + { + channel: "Website Traffic and Organic Search Results", + impressions: 120000, + clicks: 15000, + conversions: 1200, + cost: 8500, + revenue: 24000, + roi: 182, + ctr: 12.5, + cpc: 0.57, + cpa: 7.08, + reach: 95000, + engagement: 8200, + shares: 420, + saves: 180, + comments: 650, + videoViews: 0, + }, + { + channel: "Social Media Engagement and Brand Awareness", + impressions: 85000, + clicks: 12000, + conversions: 950, + cost: 6200, + revenue: 19000, + roi: 206, + ctr: 14.1, + cpc: 0.52, + cpa: 6.53, + reach: 72000, + engagement: 15400, + shares: 890, + saves: 340, + comments: 1200, + videoViews: 28000, + }, + { + channel: "Email Marketing Campaign Performance", + impressions: 45000, + clicks: 8500, + conversions: 800, + cost: 2100, + revenue: 16000, + roi: 562, + ctr: 18.9, + cpc: 0.25, + cpa: 2.63, + reach: 42000, + engagement: 6800, + shares: 120, + saves: 85, + comments: 240, + videoViews: 0, + }, + { + channel: "Paid Advertising and PPC Campaign ROI", + impressions: 95000, + clicks: 18000, + conversions: 1500, + cost: 12500, + revenue: 30000, + roi: 140, + ctr: 18.9, + cpc: 0.69, + cpa: 8.33, + reach: 88000, + engagement: 12600, + shares: 320, + saves: 150, + comments: 480, + videoViews: 5200, + }, + { + channel: "Content Marketing and Blog Performance", + impressions: 60000, + clicks: 9500, + conversions: 750, + cost: 4800, + revenue: 15000, + roi: 213, + ctr: 15.8, + cpc: 0.51, + cpa: 6.4, + reach: 55000, + engagement: 7800, + shares: 680, + saves: 420, + comments: 950, + videoViews: 12000, + }, + { + channel: "Mobile Application Downloads and Usage", + impressions: 70000, + clicks: 11000, + conversions: 1100, + cost: 5500, + revenue: 22000, + roi: 300, + ctr: 15.7, + cpc: 0.5, + cpa: 5.0, + reach: 65000, + engagement: 9200, + shares: 180, + saves: 95, + comments: 320, + videoViews: 8500, + }, + { + channel: "Customer Support Response Time and Quality", + impressions: 35000, + clicks: 5500, + conversions: 450, + cost: 2800, + revenue: 9000, + roi: 221, + ctr: 15.7, + cpc: 0.51, + cpa: 6.22, + reach: 32000, + engagement: 4200, + shares: 45, + saves: 25, + comments: 180, + videoViews: 0, + }, + { + channel: "Sales Funnel Conversion and Lead Generation", + impressions: 110000, + clicks: 22000, + conversions: 1800, + cost: 15000, + revenue: 36000, + roi: 140, + ctr: 20.0, + cpc: 0.68, + cpa: 8.33, + reach: 98000, + engagement: 18500, + shares: 420, + saves: 280, + comments: 750, + videoViews: 3200, + }, + { + channel: "User Retention and Churn Rate Analysis", + impressions: 80000, + clicks: 14000, + conversions: 1200, + cost: 7200, + revenue: 24000, + roi: 233, + ctr: 17.5, + cpc: 0.51, + cpa: 6.0, + reach: 75000, + engagement: 11200, + shares: 280, + saves: 150, + comments: 420, + videoViews: 6800, + }, + { + channel: "Product Feature Usage and Performance Metrics", + impressions: 65000, + clicks: 10500, + conversions: 900, + cost: 5200, + revenue: 18000, + roi: 246, + ctr: 16.2, + cpc: 0.5, + cpa: 5.78, + reach: 58000, + engagement: 8500, + shares: 320, + saves: 180, + comments: 650, + videoViews: 4200, + }, + { + channel: "Market Research and Competitive Analysis", + impressions: 40000, + clicks: 6500, + conversions: 500, + cost: 3200, + revenue: 10000, + roi: 213, + ctr: 16.3, + cpc: 0.49, + cpa: 6.4, + reach: 36000, + engagement: 5200, + shares: 95, + saves: 65, + comments: 220, + videoViews: 1800, + }, + { + channel: "Brand Sentiment and Public Relations Impact", + impressions: 55000, + clicks: 8000, + conversions: 650, + cost: 4100, + revenue: 13000, + roi: 217, + ctr: 14.5, + cpc: 0.51, + cpa: 6.31, + reach: 48000, + engagement: 6800, + shares: 520, + saves: 280, + comments: 950, + videoViews: 15000, + }, + ], }; // Category key mappings for different datasets @@ -207,6 +426,7 @@ const categoryKeys = { mixedLengths: "item", edgeCases: "name", minimal: "category", + expandCollapseMarketing: "channel", }; // 🔥 ACTIVE DATA - For backward compatibility @@ -417,6 +637,14 @@ export const LineChartV2Story: Story = { > 📱 Minimal +
@@ -555,6 +783,34 @@ export const VariantComparisonStory: Story = { }, }; +export const ExpandCollapseMarketingStory: Story = { + name: "🔄 Marketing Channels (Legend Expand/Collapse)", + args: { + data: dataVariations.expandCollapseMarketing as any, + categoryKey: "channel" as any, + theme: "spectrum", + variant: "natural", + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + strokeWidth: 2, + }, + render: (args) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "Tests the legend expand/collapse functionality with 12 marketing channels that have long descriptive names. The legend should automatically show a 'Show More' button when items overflow the container width, allowing users to toggle between collapsed and expanded states.", + }, + }, + }, +}; + export const ResponsiveWidthStory: Story = { name: "📐 Responsive Width Test", args: { diff --git a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx index f60813b70..990846259 100644 --- a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx @@ -46,6 +46,37 @@ const longItems: LegendItem[] = [ { key: "satisfaction", label: "Customer Satisfaction Score", color: "#f97316" }, ]; +const expandCollapseItems: LegendItem[] = [ + { key: "web_traffic", label: "Website Traffic and Organic Search Results", color: "#3b82f6" }, + { key: "social_media", label: "Social Media Engagement and Brand Awareness", color: "#ef4444" }, + { key: "email_campaigns", label: "Email Marketing Campaign Performance", color: "#10b981" }, + { key: "paid_advertising", label: "Paid Advertising and PPC Campaign ROI", color: "#f59e0b" }, + { key: "content_marketing", label: "Content Marketing and Blog Performance", color: "#8b5cf6" }, + { key: "mobile_app", label: "Mobile Application Downloads and Usage", color: "#ec4899" }, + { + key: "customer_support", + label: "Customer Support Response Time and Quality", + color: "#14b8a6", + }, + { + key: "sales_conversion", + label: "Sales Funnel Conversion and Lead Generation", + color: "#f97316", + }, + { key: "user_retention", label: "User Retention and Churn Rate Analysis", color: "#6366f1" }, + { + key: "product_analytics", + label: "Product Feature Usage and Performance Metrics", + color: "#06b6d4", + }, + { key: "market_research", label: "Market Research and Competitive Analysis", color: "#84cc16" }, + { + key: "brand_sentiment", + label: "Brand Sentiment and Public Relations Impact", + color: "#f59e0b", + }, +]; + export const Basic: Story = { args: { items: shortItems, @@ -147,6 +178,147 @@ export const Interactive: Story = { }, }; +export const ExpandCollapseDemo: Story = { + args: { + items: expandCollapseItems, + containerWidth: 500, + xAxisLabel: "Marketing Channels", + yAxisLabel: "Performance Metrics", + }, + render: (args) => ( +
+

+ 🔄 Expand/Collapse Functionality Demo +

+

+ This legend has 12 items with long labels in a{" "} + 500px container. Only the items that fit in one line are shown initially, + with a "Show More" button to expand and see all items. Click the button to + toggle between collapsed and expanded states. +

+
+ +
+
+ 💡 Features: Dynamic width calculation, intelligent truncation, responsive + behavior +
+
+ ), + parameters: { + docs: { + description: { + story: + "Demonstrates the expand/collapse functionality with 12 marketing-themed legend items. The legend automatically calculates how many items fit in the container width and shows a toggle button when items overflow.", + }, + }, + }, +}; + +export const ResponsiveExpandCollapse: Story = { + args: { + items: expandCollapseItems, + xAxisLabel: "Marketing Channels", + yAxisLabel: "Performance Metrics", + }, + render: (args) => ( +
+

+ 📐 Responsive Expand/Collapse Behavior +

+

+ The same legend data shown in different container widths. Notice how the number of visible + items and the toggle button behavior adapts to the available space. +

+ +
+
+

+ Small Container (350px) - Shows fewer items +

+
+ +
+
+ +
+

+ Medium Container (500px) - Shows more items +

+
+ +
+
+ +
+

+ Large Container (700px) - Shows most items +

+
+ +
+
+ +
+

+ Extra Large Container (900px) - Shows all items (no toggle button) +

+
+ +
+
+
+
+ ), + parameters: { + docs: { + description: { + story: + "Shows how the expand/collapse behavior adapts to different container widths. Smaller containers show fewer items initially, while larger containers may show all items without needing a toggle button.", + }, + }, + }, +}; + export const DefaultBehavior: Story = { args: { items: longItems, From 1b40699922f9933fca255968860ae2a8658f6030 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 18 Jun 2025 17:55:27 +0530 Subject: [PATCH 067/190] feat(portal-tooltip): add floating tooltip with Floating UI This change adds a new floating tooltip component that uses Floating UI for intelligent positioning and collision detection. The new `FloatingUIPortal` and `CustomTooltipContent` components are exported from the `PortalTooltip` module. The `AreaChartV2` component now supports a new `useFloatingTooltip` prop that enables the floating tooltip functionality. When set to `true`, the tooltip will follow the mouse cursor and automatically adjust its position to avoid collisions with the chart boundaries. Additionally, the `PortalTooltip` module and its associated styles have been added to the main `charts.scss` file, making the floating tooltip functionality available across the entire charts library. --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 11 +- .../AreaCharts/AreaChartV2/areaChartV2.scss | 1 + .../stories/areaChartV2.stories.tsx | 84 ++++++++ .../react-ui/src/components/Charts/Charts.tsx | 3 + .../src/components/Charts/charts.scss | 3 + .../PortalTooltip/CustomTooltipContent.tsx | 186 ++++++++++++++++++ .../shared/PortalTooltip/FloatingUIPortal.tsx | 102 ++++++++++ .../Charts/shared/PortalTooltip/README.md | 160 +++++++++++++++ .../Charts/shared/PortalTooltip/example.tsx | 59 ++++++ .../Charts/shared/PortalTooltip/index.ts | 2 + .../shared/PortalTooltip/portalTooltip.scss | 4 + .../src/components/Charts/shared/index.ts | 1 + 12 files changed, 615 insertions(+), 1 deletion(-) create mode 100644 js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx create mode 100644 js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx create mode 100644 js/packages/react-ui/src/components/Charts/shared/PortalTooltip/README.md create mode 100644 js/packages/react-ui/src/components/Charts/shared/PortalTooltip/example.tsx create mode 100644 js/packages/react-ui/src/components/Charts/shared/PortalTooltip/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index 59be0fd50..4446ae4dd 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -10,6 +10,7 @@ import { ChartTooltipContent, keyTransform, } from "../../Charts"; +import { FloatingUIPortal, CustomTooltipContent } from "../../shared/PortalTooltip"; import { cartesianGrid } from "../../cartesianGrid"; import { ActiveDot, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; import { LegendItem } from "../../types"; @@ -41,6 +42,7 @@ export interface AreaChartV2Props { height?: number; width?: number; onAreaClick?: (payload: any) => void; + useFloatingTooltip?: boolean; } const Y_AXIS_WIDTH = 40; // Width of Y-axis chart when shown @@ -61,6 +63,7 @@ export const AreaChartV2 = ({ height, width, onAreaClick, + useFloatingTooltip = true, }: AreaChartV2Props) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); @@ -288,7 +291,13 @@ export const AreaChartV2 = ({ right: 20, }} /> - } /> + + {useFloatingTooltip ? ( + } /> + ) : ( + } /> + )} + {dataKeys.map((key) => { const transformedKey = keyTransform(key); const color = `var(--color-${transformedKey})`; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss index 07d7db0a1..fa52293ac 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss @@ -1,5 +1,6 @@ @use "../../../../cssUtils" as cssUtils; + .crayon-area-chart-container-inner { display: flex; width: 100%; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx index a66655413..4ad5cf966 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -603,6 +603,15 @@ const meta: Meta> = { category: "Events", }, }, + useFloatingTooltip: { + description: "Whether to use the floating tooltip that follows the mouse cursor. Uses Floating UI for intelligent positioning and collision detection.", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "Tooltip", + }, + }, }, } satisfies Meta; @@ -622,6 +631,7 @@ export const AreaChartV2Story: Story = { showYAxis: true, xAxisLabel: "Time Period", yAxisLabel: "Values", + useFloatingTooltip: true, // width: 600, // height: 300, }, @@ -1023,3 +1033,77 @@ export const ResponsiveWidthStory: Story = { }, }, }; + +export const FloatingTooltipStory: Story = { + name: "🎯 Floating Tooltip (Mouse Following)", + args: { + data: areaChartData, + categoryKey: "month", + theme: "ocean", + variant: "natural", + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + useFloatingTooltip: true, + xAxisLabel: "Time Period", + yAxisLabel: "Values", + }, + render: (args) => ( +
+
+ 🎯 Floating Tooltip Demo: +

+ Move your mouse over the chart to see the floating tooltip that follows your cursor. + This tooltip uses Floating UI for intelligent positioning and collision detection. +

+
+ + + +
+ +

Default Tooltip

+ +
+ +

Floating Tooltip

+ +
+
+
+ ), + parameters: { + docs: { + description: { + story: + "**🎯 Floating Tooltip Demo:** This story demonstrates the new floating tooltip functionality that follows your mouse cursor. The tooltip uses Floating UI for intelligent positioning, collision detection, and smooth animations.\n\n**Key Features:**\n- ✨ **Mouse Following** - Tooltip follows your cursor in real-time\n- 🚀 **Floating UI** - Uses `@floating-ui/react-dom` for superior positioning\n- 🎯 **Smart Positioning** - Automatically adjusts position to stay on screen\n- 🔄 **Collision Detection** - Prevents tooltip from going off-screen\n- 🎨 **Custom Styling** - Fully customizable appearance\n\n**Comparison:**\n- **Default Tooltip**: Standard Recharts tooltip with fixed positioning\n- **Floating Tooltip**: Advanced tooltip that follows mouse with Floating UI positioning", + }, + source: { + code: ` +const areaChartData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, + // ... more data +]; + + + +`, + }, + }, + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/Charts.tsx b/js/packages/react-ui/src/components/Charts/Charts.tsx index cdcdd03b1..cadf93518 100644 --- a/js/packages/react-ui/src/components/Charts/Charts.tsx +++ b/js/packages/react-ui/src/components/Charts/Charts.tsx @@ -184,6 +184,7 @@ const ChartTooltipContent = forwardRef< nameKey, labelKey, showPercentage = false, + coordinate, }, ref, ) => { @@ -390,4 +391,6 @@ export { ChartStyle, ChartTooltip, ChartTooltipContent, + getPayloadConfigFromPayload, + useChart, }; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index fa6f16479..2b18e390d 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -16,6 +16,9 @@ @forward "./shared/DefaultLegend/defaultLegend.scss"; @forward "./shared/YAxisTick/yAxisTick.scss"; +// portal tooltip css +@forward "./shared/PortalTooltip/portalTooltip.scss"; + .crayon-chart { // Container styles &-container { diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx new file mode 100644 index 000000000..9a6134100 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx @@ -0,0 +1,186 @@ +import clsx from "clsx"; +import { forwardRef, useMemo } from "react"; +import * as RechartsPrimitive from "recharts"; +import { useChart, ChartConfig } from "../../../Charts/Charts"; +import { FloatingUIPortal } from "./FloatingUIPortal"; + +/** + * Helper function to extract configuration for a chart element from a payload + * (Local copy since getPayloadConfigFromPayload is not exported from Charts) + */ +function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) { + if (typeof payload !== "object" || payload === null) { + return undefined; + } + + const payloadPayload = + "payload" in payload && typeof payload.payload === "object" && payload.payload !== null + ? payload.payload + : undefined; + + let configLabelKey: string = key; + + if (key in payload && typeof payload[key as keyof typeof payload] === "string") { + configLabelKey = payload[key as keyof typeof payload] as string; + } else if ( + payloadPayload && + key in payloadPayload && + typeof payloadPayload[key as keyof typeof payloadPayload] === "string" + ) { + configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string; + } + + return configLabelKey in config ? config[configLabelKey] : config[key]; +} + +/** + * Custom tooltip content component for floating tooltips + * Mirrors the functionality of ChartTooltipContent but works with FloatingUIPortal + */ +export const CustomTooltipContent = forwardRef< + HTMLDivElement, + React.ComponentProps & + React.ComponentProps<"div"> & { + hideLabel?: boolean; + hideIndicator?: boolean; + indicator?: "line" | "dot" | "dashed"; + nameKey?: string; + labelKey?: string; + showPercentage?: boolean; + } +>( + ( + { + active, + payload, + className, + indicator = "dot", + hideLabel = false, + hideIndicator = false, + label, + labelFormatter, + labelClassName, + formatter, + color, + nameKey, + labelKey, + showPercentage = false, + coordinate, + }, + ref, + ) => { + const { config } = useChart(); + + const tooltipLabel = useMemo(() => { + if (hideLabel || !payload?.length) { + return null; + } + + const [item] = payload; + const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); + const value = + !labelKey && typeof label === "string" ? config[label]?.label || label : itemConfig?.label; + + if (labelFormatter) { + return ( +
+ {labelFormatter(value, payload)} +
+ ); + } + + if (!value) { + return null; + } + + return
{value}
; + }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]); + + if (!active || !payload?.length) { + return null; + } + + const nestLabel = payload.length === 1 && indicator !== "dot"; + + const tooltipContent = ( +
+ {!nestLabel && tooltipLabel} +
+ {payload.map((item, index) => { + const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); + const indicatorColor = (color ?? item.payload.fill) || item.color; + + return ( +
+ {formatter && item?.value !== undefined && item.name ? ( + formatter(item.value, item.name, item, index, item.payload) + ) : ( + <> + {itemConfig?.icon ? ( + + ) : ( + !hideIndicator && ( +
+ ) + )} +
+
+ {nestLabel && tooltipLabel} + {itemConfig?.label || item.name} +
+ {item.value !== undefined && ( + + {item.value.toLocaleString()} + {showPercentage ? "%" : ""} + + )} +
+ + )} +
+ ); + })} +
+
+ ); + + return ( + + {tooltipContent} + + ); + }, +); + +CustomTooltipContent.displayName = "CustomTooltipContent"; \ No newline at end of file diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx new file mode 100644 index 000000000..18eb06328 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx @@ -0,0 +1,102 @@ +import React, { useRef, useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { computePosition, flip, offset, shift } from "@floating-ui/react-dom"; +import type { Placement } from "@floating-ui/react-dom"; + +interface VirtualElement { + getBoundingClientRect(): DOMRect; +} + +interface FloatingUIPortalProps { + active: boolean; + children: React.ReactNode; + placement?: Placement; + offsetDistance?: number; + className?: string; +} + +export const FloatingUIPortal: React.FC = ({ + active, + children, + placement = "right-start", + offsetDistance = 8, + className = "", +}) => { + const mousePositionRef = useRef({ x: 0, y: 0 }); + const virtualElementRef = useRef(null); + const tooltipRef = useRef(null); + const [position, setPosition] = useState({ x: 0, y: 0 }); + + useEffect(() => { + // Create virtual element that tracks mouse position + const virtualElement: VirtualElement = { + getBoundingClientRect(): DOMRect { + return { + width: 0, + height: 0, + x: mousePositionRef.current.x, + y: mousePositionRef.current.y, + top: mousePositionRef.current.y, + left: mousePositionRef.current.x, + right: mousePositionRef.current.x, + bottom: mousePositionRef.current.y, + } as DOMRect; + }, + }; + + virtualElementRef.current = virtualElement; + }, []); + + useEffect(() => { + if (!active || !virtualElementRef.current || !tooltipRef.current) return; + + const updatePosition = async () => { + const { x, y } = await computePosition(virtualElementRef.current!, tooltipRef.current!, { + placement, + middleware: [ + offset(offsetDistance), + flip(), + shift({ padding: 8 }), + ], + }); + + setPosition({ x, y }); + }; + + const handleMouseMove = (event: MouseEvent) => { + mousePositionRef.current = { + x: event.clientX, + y: event.clientY, + }; + updatePosition(); + }; + + if (active) { + document.addEventListener("mousemove", handleMouseMove); + updatePosition(); + } + + return () => { + document.removeEventListener("mousemove", handleMouseMove); + }; + }, [active, placement, offsetDistance]); + + if (!active) return null; + + return createPortal( +
+ {children} +
, + document.body + ); +}; \ No newline at end of file diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/README.md b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/README.md new file mode 100644 index 000000000..a0d90e0b5 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/README.md @@ -0,0 +1,160 @@ +# Floating Tooltip Components + +A collection of components that provide mouse-following tooltips for charts using Floating UI for superior positioning and collision detection. + +## Components + +### FloatingUIPortal + +A portal-based tooltip component that follows the mouse cursor and uses Floating UI for intelligent positioning. + +```tsx +import { FloatingUIPortal } from "./FloatingUIPortal"; + + +
Tooltip content
+
; +``` + +#### Props + +- `active` (boolean): Whether the tooltip should be visible and active +- `children` (ReactNode): The content to display in the tooltip +- `placement` (Placement, optional): Floating UI placement option (default: "right-start") +- `offsetDistance` (number, optional): Distance from the mouse cursor (default: 8) +- `className` (string, optional): Additional CSS classes + +#### Features + +- **Mouse Following**: Tooltip follows mouse cursor in real-time +- **Smart Positioning**: Uses Floating UI's `flip`, `offset`, and `shift` middleware +- **Collision Detection**: Automatically adjusts position to stay on screen +- **Portal Rendering**: Renders outside the chart container to avoid clipping +- **Virtual Element**: Creates a virtual element that tracks mouse position + +### CustomTooltipContent + +A specialized tooltip content component that mirrors ChartTooltipContent but works with the floating tooltip system. + +```tsx +import { CustomTooltipContent } from "./CustomTooltipContent"; + +} />; +``` + +#### Props + +Supports all the same props as `ChartTooltipContent`: + +- `hideLabel` (boolean, optional): Hide the tooltip label +- `hideIndicator` (boolean, optional): Hide the color indicators +- `indicator` ("line" | "dot" | "dashed", optional): Indicator style +- `nameKey` (string, optional): Key for extracting item names +- `labelKey` (string, optional): Key for extracting labels +- `showPercentage` (boolean, optional): Show values as percentages +- `formatter` (function, optional): Custom value formatter +- `labelFormatter` (function, optional): Custom label formatter + +#### Features + +- **Consistent Styling**: Uses the same CSS classes as ChartTooltipContent +- **Full Feature Parity**: Supports all formatting and styling options +- **Floating UI Integration**: Automatically wraps content with FloatingUIPortal +- **Chart Context**: Access to chart configuration and styling + +## Usage with AreaChartV2 + +The floating tooltip is integrated into AreaChartV2 via the `useFloatingTooltip` prop: + +```tsx +import { AreaChartV2 } from "../AreaCharts/AreaChartV2"; + +; +``` + +## Implementation Details + +### Virtual Element + +The FloatingUIPortal creates a virtual element that implements Floating UI's positioning interface: + +```tsx +const virtualElement: VirtualElement = { + getBoundingClientRect(): DOMRect { + return { + width: 0, + height: 0, + x: mousePositionRef.current.x, + y: mousePositionRef.current.y, + top: mousePositionRef.current.y, + left: mousePositionRef.current.x, + right: mousePositionRef.current.x, + bottom: mousePositionRef.current.y, + } as DOMRect; + }, +}; +``` + +### Mouse Tracking + +Mouse position is tracked via global `mousemove` events and stored in a ref for performance: + +```tsx +const handleMouseMove = (event: MouseEvent) => { + mousePositionRef.current = { + x: event.clientX, + y: event.clientY, + }; + updatePosition(); +}; +``` + +### Positioning + +Uses Floating UI's middleware for intelligent positioning: + +```tsx +const { x, y } = await computePosition(virtualElement, tooltipElement, { + placement: "right-start", + middleware: [ + offset(8), // Distance from cursor + flip(), // Flip when hitting boundaries + shift({ padding: 8 }), // Shift to stay in viewport + ], +}); +``` + +## Dependencies + +- `@floating-ui/react-dom`: For positioning calculations +- `react-dom`: For portal functionality +- Existing chart components and styling + +## Browser Support + +Works in all modern browsers that support: + +- React Portals +- CSS Custom Properties +- Mouse Events +- Async/Await + +## Performance + +- Uses `useRef` for mouse position to avoid re-renders +- Debounced position updates via `requestAnimationFrame` +- Minimal DOM manipulation +- Portal rendering prevents layout thrashing diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/example.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/example.tsx new file mode 100644 index 000000000..9291475a9 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/example.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { AreaChartV2 } from '../../AreaCharts/AreaChartV2'; + +// Example data for the chart +const sampleData = [ + { month: "January", desktop: 150, mobile: 90, tablet: 120 }, + { month: "February", desktop: 280, mobile: 180, tablet: 140 }, + { month: "March", desktop: 220, mobile: 140, tablet: 160 }, + { month: "April", desktop: 180, mobile: 160, tablet: 180 }, + { month: "May", desktop: 250, mobile: 120, tablet: 140 }, + { month: "June", desktop: 300, mobile: 180, tablet: 160 }, +]; + +/** + * Example usage of floating tooltip with AreaChartV2 + */ +export const FloatingTooltipExample: React.FC = () => { + return ( +
+

Floating Tooltip Example

+ +
+

With Floating Tooltip (follows mouse)

+
+ +
+
+ +
+

Standard Tooltip (fixed position)

+
+ +
+
+
+ ); +}; + +export default FloatingTooltipExample; \ No newline at end of file diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/index.ts b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/index.ts new file mode 100644 index 000000000..22f66a172 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/index.ts @@ -0,0 +1,2 @@ +export { FloatingUIPortal } from "./FloatingUIPortal"; +export { CustomTooltipContent } from "./CustomTooltipContent"; \ No newline at end of file diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss new file mode 100644 index 000000000..1facea437 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss @@ -0,0 +1,4 @@ +.crayon-portal-tooltip { + pointer-events: none; + z-index: 1000; +} \ No newline at end of file diff --git a/js/packages/react-ui/src/components/Charts/shared/index.ts b/js/packages/react-ui/src/components/Charts/shared/index.ts index cef54e7db..b3901290d 100644 --- a/js/packages/react-ui/src/components/Charts/shared/index.ts +++ b/js/packages/react-ui/src/components/Charts/shared/index.ts @@ -1,4 +1,5 @@ export * from "./ActiveDot/ActiveDot"; export * from "./DefaultLegend/DefaultLegend"; +export * from "./PortalTooltip"; export * from "./XAxisTick/XAxisTick"; export * from "./YAxisTick/YAxisTick"; From c0ec80b3760d44014001b502b8745f967358c14d Mon Sep 17 00:00:00 2001 From: i-subham Date: Wed, 18 Jun 2025 16:24:32 +0530 Subject: [PATCH 068/190] feat(TwoLevelPieChart): Introduce TwoLevelPieChart component with enhanced features This commit adds the TwoLevelPieChart component, which includes a new SCSS file for styling and various enhancements to improve functionality and usability. Key changes include: - Implementation of dynamic dimensions using hooks and ResizeObserver for responsive behavior. - Addition of new props such as cornerRadius, paddingAngle, useGradients, and gradientColors for customization. - Refactoring of the chart rendering logic to support inner and outer pie segments with gradient fills. - Updates to the storybook to demonstrate the new features, including interactive examples and various chart configurations. These enhancements aim to provide a more flexible and visually appealing charting solution. --- .../TwoLevelPieChart/TwoLevelPieChart.scss | 20 + .../TwoLevelPieChart/TwoLevelPieChart.tsx | 266 +++++++++---- .../stories/TwoLevelPieChart.stories.tsx | 369 +++++++++++++++--- 3 files changed, 533 insertions(+), 122 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.scss diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.scss b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.scss new file mode 100644 index 000000000..8cc54cdaf --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.scss @@ -0,0 +1,20 @@ +@use "../../../../cssUtils" as cssUtils; + +.crayon-pie-chart-container { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; +} + +.crayon-pie-chart-container-inner { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.crayon-pie-chart { + position: relative; +} diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx index 5684e3daf..e6e770d9e 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx @@ -1,21 +1,28 @@ import clsx from "clsx"; import { debounce } from "lodash-es"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; -import { useLayoutContext } from "../../../../context/LayoutContext"; -import { ChartContainer } from "../../Charts"; +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts"; +import { createGradientDefinitions } from "../components/PieChartRenderers"; import { PieChartData, calculateTwoLevelChartDimensions, + createAnimationConfig, createChartConfig, + createEventHandlers, + createSectorStyle, getHoverStyles, - layoutMap, transformDataWithPercentages, useChartHover, } from "../utils/PieChartUtils"; export type TwoLevelPieChartData = PieChartData; +interface GradientColor { + start?: string; + end?: string; +} + export interface TwoLevelPieChartProps { data: T; categoryKey: keyof T[number]; @@ -24,8 +31,17 @@ export interface TwoLevelPieChartProps { format?: "percentage" | "number"; appearance?: "circular" | "semiCircular"; legend?: boolean; - label?: boolean; isAnimationActive?: boolean; + cornerRadius?: number; + paddingAngle?: number; + useGradients?: boolean; + gradientColors?: GradientColor[]; + onMouseEnter?: (data: any, index: number) => void; + onMouseLeave?: () => void; + onClick?: (data: any, index: number) => void; + height?: number; + width?: number; + className?: string; } export const TwoLevelPieChart = ({ @@ -35,93 +51,189 @@ export const TwoLevelPieChart = ({ theme = "ocean", format = "number", appearance = "circular", - label = true, + legend = true, isAnimationActive = true, + cornerRadius = 0, + paddingAngle = 0.5, + useGradients = false, + gradientColors, + onMouseEnter, + onMouseLeave, + onClick, + className, + height, + width, }: TwoLevelPieChartProps) => { - const { layout } = useLayoutContext(); - const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(160); - const [calculatedMiddleRadius, setCalculatedMiddleRadius] = useState(140); - const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(40); + const chartContainerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + const [containerHeight, setContainerHeight] = useState(0); const { activeIndex, handleMouseEnter, handleMouseLeave } = useChartHover(); - const containerRef = useRef(null); + + // Use provided dimensions or observed dimensions + const effectiveWidth = useMemo(() => { + return width ?? containerWidth; + }, [width, containerWidth]); + + const effectiveHeight = useMemo(() => { + return height ?? containerHeight; + }, [height, containerHeight]); + + // Calculate chart dimensions based on the smaller dimension + const chartSize = useMemo(() => { + const size = Math.min(effectiveWidth, effectiveHeight); + return Math.max(200, Math.min(size, 800)); // Min 200px, max 800px + }, [effectiveWidth, effectiveHeight]); + + // Transform data and create configurations + const transformedData = transformDataWithPercentages(data, dataKey); + const chartConfig = createChartConfig(data, categoryKey, theme); + const animationConfig = createAnimationConfig({ isAnimationActive }); + const eventHandlers = createEventHandlers(onMouseEnter, onMouseLeave, onClick); + const sectorStyle = createSectorStyle(cornerRadius, paddingAngle); // Calculate dynamic radius + const { outerRadius, middleRadius, innerRadius } = useMemo(() => { + return calculateTwoLevelChartDimensions(chartSize); + }, [chartSize]); + useEffect(() => { - if (!containerRef.current) return; + // Only set up ResizeObserver if dimensions are not provided + if ((width && height) || !chartContainerRef.current) { + return () => {}; + } const resizeObserver = new ResizeObserver( - debounce((entries: any) => { - const { width } = entries[0].contentRect; - const { outerRadius, middleRadius, innerRadius } = calculateTwoLevelChartDimensions( - width, - label, - ); - - setCalculatedOuterRadius(outerRadius); - setCalculatedMiddleRadius(middleRadius); - setCalculatedInnerRadius(innerRadius); + debounce((entries) => { + for (const entry of entries) { + const newWidth = entry.contentRect.width; + const newHeight = entry.contentRect.height; + setContainerWidth(newWidth); + setContainerHeight(newHeight); + } }, 100), ); - resizeObserver.observe(containerRef.current); - return () => resizeObserver.disconnect(); - }, [label]); + resizeObserver.observe(chartContainerRef.current); - const transformedData = transformDataWithPercentages(data, dataKey); - const chartConfig = createChartConfig(data, categoryKey, theme); + return () => { + resizeObserver.disconnect(); + }; + }, [width, height]); return ( - - - +
- {transformedData.map((entry, index) => { - // const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); - const hoverStyles = getHoverStyles(index, activeIndex); - return ; - })} - - - {transformedData.map((entry, index) => { - const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); - const config = chartConfig[categoryValue]; - const hoverStyles = getHoverStyles(index, activeIndex); - - return ; - })} - - - + + + } + /> + + {useGradients && ( + + {createGradientDefinitions( + transformedData, + Object.values(chartConfig) + .map((config) => config.color) + .filter((color): color is string => color !== undefined), + gradientColors, + )} + + )} + + {/* Inner Pie */} + + {transformedData.map((entry, index) => { + const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); + const config = chartConfig[categoryValue]; + const hoverStyles = getHoverStyles(index, activeIndex); + const fill = useGradients ? `url(#gradient-${index})` : "lightgray"; + + return ( + + ); + })} + + + {/* Outer Pie */} + + {transformedData.map((entry, index) => { + const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); + const config = chartConfig[categoryValue]; + const hoverStyles = getHoverStyles(index, activeIndex); + const fill = useGradients ? `url(#gradient-${index})` : config?.color; + + return ( + + ); + })} + + + +
+
+
); }; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx index 02826ad0f..afa1322e9 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx @@ -1,5 +1,10 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Card } from "../../../../Card"; +import { DefaultLegend } from "../../../shared/DefaultLegend/DefaultLegend"; +import StackedLegend from "../../../shared/StackedLegend/StackedLegend"; +import { LegendItem } from "../../../types"; +import { getDistributedColors, getPalette } from "../../../utils/PalletUtils"; import { TwoLevelPieChart, TwoLevelPieChartProps } from "../TwoLevelPieChart"; const pieChartData = [ @@ -12,6 +17,16 @@ const pieChartData = [ { month: "July", value: 5890 }, ]; +const gradientColors = [ + { start: "#FF6B6B", end: "#FF8E8E" }, + { start: "#4ECDC4", end: "#6ED7D0" }, + { start: "#45B7D1", end: "#6BC5DB" }, + { start: "#96CEB4", end: "#B4DCC9" }, + { start: "#FFEEAD", end: "#FFF4C4" }, + { start: "#D4A5A5", end: "#E5BDBD" }, + { start: "#9B59B6", end: "#B07CC7" }, +]; + const meta: Meta> = { title: "Components/Charts/PieCharts/TwoLevelPieChart", component: TwoLevelPieChart, @@ -19,7 +34,8 @@ const meta: Meta> = { layout: "centered", docs: { description: { - component: "```tsx\nimport { PieChart } from '@crayon-ui/react-ui/Charts/PieChart';\n```", + component: + "```tsx\nimport { TwoLevelPieChart } from '@crayon-ui/react-ui/Charts/PieCharts/TwoLevelPieChart';\n```", }, }, }, @@ -80,7 +96,7 @@ const meta: Meta> = { control: "radio", options: ["percentage", "number"], table: { - defaultValue: { summary: "percentage" }, + defaultValue: { summary: "number" }, category: "Display", }, }, @@ -93,8 +109,8 @@ const meta: Meta> = { category: "Display", }, }, - label: { - description: "Whether to display the data point labels above each point on the chart", + isAnimationActive: { + description: "Whether to animate the chart", control: "boolean", table: { type: { summary: "boolean" }, @@ -102,13 +118,31 @@ const meta: Meta> = { category: "Display", }, }, - isAnimationActive: { - description: "Whether to animate the chart", + cornerRadius: { + description: "The radius of the corners of each segment", + control: { type: "number", min: 0, max: 20, step: 1 }, + table: { + type: { summary: "number" }, + defaultValue: { summary: "0" }, + category: "Appearance", + }, + }, + paddingAngle: { + description: "The angle of padding between segments", + control: { type: "number", min: 0, max: 10, step: 0.5 }, + table: { + type: { summary: "number" }, + defaultValue: { summary: "0.5" }, + category: "Appearance", + }, + }, + useGradients: { + description: "Whether to use gradient fills for segments", control: "boolean", table: { type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", + defaultValue: { summary: "false" }, + category: "Appearance", }, }, }, @@ -117,8 +151,8 @@ const meta: Meta> = { export default meta; type Story = StoryObj; -export const PieChartStory: Story = { - name: "Pie Chart", +export const Default: Story = { + name: "Default Two Level Pie Chart", args: { data: pieChartData, categoryKey: "month", @@ -127,47 +161,292 @@ export const PieChartStory: Story = { appearance: "circular", format: "number", legend: true, - label: true, isAnimationActive: true, + cornerRadius: 0, + paddingAngle: 0.5, + useGradients: false, + }, + render: (args) => { + const ResizableExample = () => { + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(500); + + // Create legend items from the chart data + const legendItems: LegendItem[] = useMemo(() => { + const palette = getPalette(args.theme || "ocean"); + const colors = getDistributedColors(palette, args.data.length); + + return args.data.map((item, index) => ({ + key: String(item[args.categoryKey]), + label: String(item[args.categoryKey]), + color: colors[index] || "#000000", + })); + }, [args.data, args.categoryKey, args.theme]); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const paddingX = 40; + const observedWidth = Math.max(200, entry.contentRect.width - paddingX); + setContainerWidth(observedWidth); + } + }); + + resizeObserver.observe(container); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + return ( + + + + + + + ); + }; + + return ; + }, +}; + +export const Interactive: Story = { + name: "Interactive with Resize", + args: { + ...Default.args, + theme: "vivid", + cornerRadius: 5, + paddingAngle: 1, + format: "percentage", + }, + render: (args) => { + const ResizableExample = () => { + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(500); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const paddingX = 40; + const observedWidth = Math.max(200, entry.contentRect.width - paddingX); + setContainerWidth(observedWidth); + } + }); + + resizeObserver.observe(container); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + return ( +
+

+ This example demonstrates how the chart adapts to container width. + Drag the bottom-right corner to resize the container and see how the + components adapt in real-time. Current width: {containerWidth}px +

+ + + +
+ ); + }; + + return ; + }, +}; + +export const SemiCircular: Story = { + name: "Semi-Circular Chart", + args: { + ...Default.args, + appearance: "semiCircular", + theme: "emerald", }, render: (args) => ( - + ), - parameters: { - docs: { - source: { - code: ` - const pieChartData = [ - { month: "January", value: 400 }, - { month: "February", value: 300 }, - { month: "March", value: 300 }, - { month: "April", value: 400 }, - { month: "May", value: 300 }, - { month: "June", value: 300 }, - { month: "July", value: 300 }, -]; +}; - - - - `, - }, - }, +export const WithCornerRadius: Story = { + name: "Chart with Corner Radius", + args: { + ...Default.args, + cornerRadius: 10, + paddingAngle: 2, + theme: "sunset", + }, + render: (args) => ( + + + + ), +}; + +export const PercentageFormat: Story = { + name: "Percentage Format", + args: { + ...Default.args, + format: "percentage", + theme: "spectrum", + }, + render: (args) => ( + + + + ), +}; + +export const GradientColors: Story = { + name: "Gradient Colors", + args: { + ...Default.args, + theme: "vivid", + cornerRadius: 5, + paddingAngle: 1, + format: "percentage", + useGradients: true, + gradientColors, + }, + render: (args) => ( + + + + ), +}; + +export const WithStackedLegend: Story = { + name: "With Stacked Legend", + args: { + ...Default.args, + theme: "vivid", + cornerRadius: 5, + paddingAngle: 1, + format: "percentage", + }, + render: (args) => { + const ResizableExample = () => { + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(500); + const [containerHeight, setContainerHeight] = useState(400); + + // Create legend items from the chart data + const legendItems = useMemo(() => { + const palette = getPalette(args.theme || "vivid"); + const colors = getDistributedColors(palette, args.data.length); + + return args.data.map((item, index) => ({ + key: String(item[args.categoryKey]), + label: String(item[args.categoryKey]), + value: Number(item[args.dataKey]), + color: colors[index] || "#000000", + })); + }, [args.data, args.categoryKey, args.dataKey, args.theme]); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const paddingX = 40; + const paddingY = 40; + const observedWidth = Math.max(200, entry.contentRect.width - paddingX); + const observedHeight = Math.max(200, entry.contentRect.height - paddingY); + setContainerWidth(observedWidth); + setContainerHeight(observedHeight); + } + }); + + resizeObserver.observe(container); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + return ( + +
+ + + +
+ +
+
+
+ ); + }; + + return ; }, }; From 026122c249484f307b22e3acc79ffcde852f8f5d Mon Sep 17 00:00:00 2001 From: i-subham Date: Wed, 18 Jun 2025 17:22:05 +0530 Subject: [PATCH 069/190] feat(PieChartV2): Enhance PieChartV2 with two-level chart support and improved rendering logic This commit introduces support for a two-level pie chart variant in the PieChartV2 component. Key changes include: - Implementation of dynamic dimensions for the donut variant using a new utility function. - Refactoring of the rendering logic to handle inner and outer pie segments, allowing for gradient fills and improved visual distinction. - Updates to the storybook to reflect the new two-level variant, including enhanced descriptions and example configurations. These enhancements aim to provide a more versatile and visually appealing charting experience. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 149 ++++++++++++++---- .../PieChartV2/stories/PieChartV2.stories.tsx | 9 +- 2 files changed, 125 insertions(+), 33 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 1baeb2f59..15b761fd0 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -6,6 +6,7 @@ import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; import { createGradientDefinitions } from "../components/PieChartRenderers"; import { PieChartData, + calculateTwoLevelChartDimensions, createAnimationConfig, createChartConfig, createEventHandlers, @@ -96,10 +97,17 @@ export const PieChartV2 = ({ const palette = getPalette(theme); const colors = getDistributedColors(palette, data.length); - // Create gradient definitions if gradients are enabled - const gradientDefinitions = useGradients ? ( - {createGradientDefinitions(transformedData, colors, gradientColors)} - ) : null; + // Calculate dimensions based on variant + const dimensions = useMemo(() => { + if (variant === "donut") { + return calculateTwoLevelChartDimensions(chartSize); + } + return { + outerRadius: "90%", + innerRadius: 0, + middleRadius: 0, + }; + }, [variant, chartSize]); useEffect(() => { // Only set up ResizeObserver if dimensions are not provided @@ -123,6 +131,99 @@ export const PieChartV2 = ({ }; }, [width, height]); + const renderPieCharts = () => { + if (variant === "donut") { + return ( + <> + {/* Inner Pie */} + + {transformedData.map((entry, index) => { + const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); + const config = chartConfig[categoryValue]; + const hoverStyles = getHoverStyles(index, activeIndex); + const fill = useGradients ? `url(#gradient-${index})` : "lightgray"; + + return ( + + ); + })} + + + {/* Outer Pie */} + + {transformedData.map((entry, index) => { + const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); + const config = chartConfig[categoryValue]; + const hoverStyles = getHoverStyles(index, activeIndex); + const fill = useGradients ? `url(#gradient-${index})` : config?.color; + + return ( + + ); + })} + + + ); + } + + return ( + + {transformedData.map((entry, index) => { + const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); + const config = chartConfig[categoryValue]; + const hoverStyles = getHoverStyles(index, activeIndex); + const fill = useGradients ? `url(#gradient-${index})` : config?.color || colors[index]; + + return ; + })} + + ); + }; + return (
({ content={} /> - {gradientDefinitions} - - {transformedData.map((entry, index) => { - const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); - const config = chartConfig[categoryValue]; - const hoverStyles = getHoverStyles(index, activeIndex); - const fill = useGradients - ? `url(#gradient-${index})` - : config?.color || colors[index]; - - return ; - })} - + {useGradients && ( + + {createGradientDefinitions( + transformedData, + Object.values(chartConfig) + .map((config) => config.color) + .filter((color): color is string => color !== undefined), + gradientColors, + )} + + )} + + {renderPieCharts()}
diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index 8ef333970..f536676cb 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -95,7 +95,7 @@ const meta: Meta> = { }, variant: { description: - "The style of the pie chart. 'pie' shows a pie chart, while 'donut' shows a donut chart.", + "The style of the pie chart. 'pie' shows a pie chart, 'donut' shows a donut chart, and 'twoLevel' shows a two-level pie chart.", control: "radio", options: ["pie", "donut"], table: { @@ -295,11 +295,16 @@ export const Interactive: Story = { }; export const DonutChart: Story = { - name: "Donut Chart", + name: "Donut Chart (Two Level)", args: { ...Default.args, variant: "donut", theme: "orchid", + useGradients: true, + gradientColors, + cornerRadius: 5, + paddingAngle: 1, + format: "percentage", }, render: (args) => ( From a912769a0f482b9c60b038cc68b25711587f9342 Mon Sep 17 00:00:00 2001 From: i-subham Date: Wed, 18 Jun 2025 17:40:56 +0530 Subject: [PATCH 070/190] fix(PieChartV2): Adjust sector style for donut variant This commit modifies the sector style calculation in the PieChartV2 component to accommodate a specific case for the donut variant. The padding angle is now conditionally set based on the variant type, enhancing the visual representation of the chart. This change aims to improve the overall appearance and functionality of the PieChartV2 component. --- .../src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 15b761fd0..713e2d525 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -91,7 +91,7 @@ export const PieChartV2 = ({ const chartConfig = createChartConfig(data, categoryKey, theme); const animationConfig = createAnimationConfig({ isAnimationActive }); const eventHandlers = createEventHandlers(onMouseEnter, onMouseLeave, onClick); - const sectorStyle = createSectorStyle(cornerRadius, paddingAngle); + const sectorStyle = createSectorStyle(cornerRadius, variant === "donut" ? 0.5 : paddingAngle); // Get color palette and distribute colors const palette = getPalette(theme); From 1f9349fa479607b532e0546cf64febd4bda14612 Mon Sep 17 00:00:00 2001 From: i-subham Date: Wed, 18 Jun 2025 19:14:44 +0530 Subject: [PATCH 071/190] refactor(Charts): Remove MiniRadarChart and MiniRadialChart components This commit deletes the MiniRadarChart and MiniRadialChart components along with their associated stories and index files. The removal aims to streamline the charting library by eliminating unused components, thereby improving maintainability and reducing clutter in the codebase. --- .../MiniRadarChart/MiniRadarChart.tsx | 69 ------- .../RadarCharts/MiniRadarChart/index.ts | 1 - .../stories/MiniRadarChart.stories.tsx | 167 ---------------- .../MiniRadialChart/MiniRadialChart.tsx | 128 ------------ .../RadialCharts/MiniRadialChart/index.ts | 1 - .../stories/MiniRadialChart.stories.tsx | 189 ------------------ 6 files changed, 555 deletions(-) delete mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/MiniRadarChart.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/stories/MiniRadarChart.stories.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/stories/MiniRadialChart.stories.tsx diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/MiniRadarChart.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/MiniRadarChart.tsx deleted file mode 100644 index 580682924..000000000 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/MiniRadarChart.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from "react"; -import { Radar, RadarChart as RechartsRadarChart } from "recharts"; -import { ChartContainer, keyTransform } from "../../Charts"; -import { RadarChartData, createChartConfig, getRadarChartMargin } from "../utils/RaderChartUtils"; - -export type MiniRadarChartData = RadarChartData; - -export interface MiniRadarChartProps { - data: T; - categoryKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - variant?: "line" | "area"; - strokeWidth?: number; - areaOpacity?: number; - icons?: Partial>; - isAnimationActive?: boolean; -} - -export const MiniRadarChart = ({ - data, - categoryKey, - theme = "ocean", - variant = "line", - strokeWidth = 2, - areaOpacity = 0.5, - icons = {}, - isAnimationActive = true, -}: MiniRadarChartProps) => { - const chartConfig = createChartConfig({ - data, - categoryKey: categoryKey as string, - theme, - icons, - }); - - return ( - - - {Object.keys(chartConfig).map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - if (variant === "line") { - return ( - - ); - } else { - return ( - - ); - } - })} - - - ); -}; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/index.ts deleted file mode 100644 index e13066887..000000000 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./MiniRadarChart"; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/stories/MiniRadarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/stories/MiniRadarChart.stories.tsx deleted file mode 100644 index 6aa49bdf9..000000000 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/MiniRadarChart/stories/MiniRadarChart.stories.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { Monitor, TabletSmartphone } from "lucide-react"; -import { Card } from "../../../../Card"; -import { MiniRadarChart, MiniRadarChartProps } from "../MiniRadarChart"; - -const radarChartData = [ - { month: "January", desktop: 250, mobile: 150 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - -const icons = { - desktop: Monitor, - mobile: TabletSmartphone, -} as const; - -const meta: Meta> = { - title: "Components/Charts/RadarCharts/MiniRadarChart", - component: MiniRadarChart, - parameters: { - layout: "centered", - docs: { - description: { - component: - "```tsx\nimport { MiniRadarChart } from '@crayon-ui/react-ui/Charts/RadarCharts/MiniRadarCharts';\n```", - }, - }, - }, - tags: ["!dev", "autodocs"], - - argTypes: { - data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", - control: false, - table: { - type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", - }, - }, - categoryKey: { - description: - "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", - control: "select", - options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], - table: { - defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - icons: { - description: - "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", - control: false, - table: { - type: { summary: "Record" }, - defaultValue: { summary: "{}" }, - category: "Appearance", - }, - }, - variant: { - description: - "The style of the radar chart. 'line' shows only the connecting lines between data points, while 'area' fills the shape created by the data points.", - control: "radio", - options: ["line", "area"], - table: { - defaultValue: { summary: "area" }, - category: "Appearance", - }, - }, - strokeWidth: { - description: "The width of the line stroke", - control: false, - table: { - type: { summary: "number" }, - defaultValue: { summary: "2" }, - category: "Appearance", - }, - }, - areaOpacity: { - description: "The opacity of the area fill", - control: false, - table: { - type: { summary: "number" }, - defaultValue: { summary: "0.7" }, - category: "Appearance", - }, - }, - isAnimationActive: { - description: "Whether to animate the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const RadarChartStory: Story = { - name: "Radar Chart", - args: { - data: radarChartData, - categoryKey: "month", - theme: "ocean", - variant: "area", - strokeWidth: 2, - areaOpacity: 0.5, - isAnimationActive: true, - }, - render: (args) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` - const radarChartData = [ - { month: "January", desktop: 250, mobile: 150 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - - - - - `, - }, - }, - }, -}; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx deleted file mode 100644 index 0db811905..000000000 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/MiniRadialChart.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import clsx from "clsx"; -import { debounce } from "lodash-es"; -import { useEffect, useRef, useState } from "react"; -import { Cell, RadialBar, RadialBarChart } from "recharts"; -import { useLayoutContext } from "../../../../context/LayoutContext"; -import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts"; -import { - RadialChartData, - calculateRadialChartDimensions, - createRadialChartConfig, - getHoverStyles, - layoutMap, - transformRadialData, - useChartHover, -} from "../utils/RadialChartUtils"; - -export type MiniRadialChartData = RadialChartData; - -export interface MiniRadialChartProps { - data: T; - categoryKey: keyof T[number]; - dataKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - variant?: "semicircle" | "circular"; - format?: "percentage" | "number"; - // legend?: boolean; - label?: boolean; - grid?: boolean; - isAnimationActive?: boolean; -} - -export const MiniRadialChart = ({ - data, - categoryKey, - dataKey, - theme = "ocean", - variant = "semicircle", - format = "number", - // legend = true, - label = true, - grid = true, - isAnimationActive = true, -}: MiniRadialChartProps) => { - const { layout } = useLayoutContext(); - const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(110); - const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(30); - const { activeIndex, handleMouseEnter, handleMouseLeave } = useChartHover(); - const containerRef = useRef(null); - - useEffect(() => { - if (!containerRef.current) return; - - const resizeObserver = new ResizeObserver( - debounce((entries: any) => { - const { width } = entries[0].contentRect; - const { outerRadius, innerRadius } = calculateRadialChartDimensions(width, variant, label); - setCalculatedOuterRadius(outerRadius); - setCalculatedInnerRadius(innerRadius); - }, 100), - ); - - resizeObserver.observe(containerRef.current); - return () => resizeObserver.disconnect(); - }, [layout, label, variant]); - - // Transform data with percentages and colors - const transformedData = transformRadialData(data, dataKey, theme); - - // Create chart configuration - const chartConfig = createRadialChartConfig(data, categoryKey, theme); - - return ( - - - {/* {grid && } */} - - } - /> - {/* {legend && ( - - } - /> - )} */} - - {Object.entries(chartConfig).map(([key, config], index) => ( - - ))} - {/* {label && ( - formatRadialLabel(value, format)} - /> - )} */} - - - - ); -}; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/index.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/index.ts deleted file mode 100644 index 182e70fa5..000000000 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./MiniRadialChart"; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/stories/MiniRadialChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/stories/MiniRadialChart.stories.tsx deleted file mode 100644 index 0d1be1da5..000000000 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/MiniRadialChart/stories/MiniRadialChart.stories.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { Card } from "../../../../Card"; -import { MiniRadialChart, MiniRadialChartProps } from "../MiniRadialChart"; - -const radialChartData = [ - { month: "January", value: 400 }, - { month: "February", value: 300 }, - { month: "March", value: 300 }, - { month: "April", value: 400 }, - { month: "May", value: 300 }, - { month: "June", value: 300 }, - { month: "July", value: 300 }, -]; - -const meta: Meta> = { - title: "Components/Charts/RadialCharts/MiniRadialChart", - component: MiniRadialChart, - parameters: { - layout: "centered", - docs: { - description: { - component: - "```tsx\nimport { MiniRadialChart } from '@crayon-ui/react-ui/Charts/RadialCharts/MiniRadialChart';\n```", - }, - }, - }, - tags: ["!dev", "autodocs"], - - argTypes: { - data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", - control: false, - table: { - type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", - }, - }, - categoryKey: { - description: - "The key from your data object to be used as the category labels for each segment (e.g., 'month', 'year', 'category')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - dataKey: { - description: - "The key from your data object to be used as the values that determine the segment sizes (e.g., 'value', 'count', 'amount')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", - control: "select", - options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], - table: { - defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - variant: { - description: - "The style of the pie chart. 'circular' shows a circular chart, while 'semicircle' shows a semicircle chart.", - control: "radio", - options: ["circular", "semicircle"], - table: { - defaultValue: { summary: "circular" }, - category: "Appearance", - }, - }, - format: { - description: - "The format of the data. 'percentage' shows the data as a percentage, while 'number' shows the data as a number.", - control: "radio", - options: ["percentage", "number"], - table: { - defaultValue: { summary: "percentage" }, - category: "Display", - }, - }, - legend: { - description: "Whether to display the legend", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - label: { - description: "Whether to display the data point labels above each point on the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - grid: { - description: "Whether to display the grid lines", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - isAnimationActive: { - description: "Whether to animate the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const RadialChartStory: Story = { - name: "Radial Chart", - args: { - data: radialChartData, - categoryKey: "month", - dataKey: "value", - theme: "ocean", - variant: "circular", - format: "number", - legend: true, - label: true, - grid: true, - isAnimationActive: true, - }, - render: (args) => ( - - - - ), - parameters: { - docs: { - description: { - story: - "A radial chart that displays data in a circular or semicircular format, with customizable themes, labels, and grid options.", - }, - source: { - code: ` -const radialChartData = [ - { month: "January", value: 400 }, - { month: "February", value: 300 }, - { month: "March", value: 300 }, - { month: "April", value: 400 }, - { month: "May", value: 300 }, - { month: "June", value: 300 }, - { month: "July", value: 300 }, -]; - - - - - `, - }, - }, - }, -}; From 42144b85423211dd9dd7587d53be5b1558279cba Mon Sep 17 00:00:00 2001 From: i-subham Date: Wed, 18 Jun 2025 19:17:48 +0530 Subject: [PATCH 072/190] refactor(MiniPieChart): Simplify component by removing unused layoutMap and label dependency This commit refactors the MiniPieChart component by removing the unused layoutMap import and eliminating the label dependency from the resize observer effect. These changes streamline the code and enhance maintainability without altering the component's functionality. --- .../Charts/PieCharts/MiniPieChart/MiniPieChart.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx index 9fd834f1e..556ce7431 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx @@ -8,7 +8,6 @@ import { PieChartData, calculateChartDimensions, createChartConfig, - layoutMap, transformDataWithPercentages, } from "../utils/PieChartUtils"; @@ -48,7 +47,7 @@ export const MiniPieChart = ({ const resizeObserver = new ResizeObserver( debounce((entries: any) => { const { width } = entries[0].contentRect; - const { outerRadius, innerRadius } = calculateChartDimensions(width, variant, label); + const { outerRadius, innerRadius } = calculateChartDimensions(width, variant); setCalculatedOuterRadius(outerRadius); setCalculatedInnerRadius(innerRadius); }, 100), @@ -56,7 +55,7 @@ export const MiniPieChart = ({ resizeObserver.observe(containerRef.current); return () => resizeObserver.disconnect(); - }, [variant, label]); + }, [variant]); const transformedData = transformDataWithPercentages(data, dataKey); const chartConfig = createChartConfig(data, categoryKey, theme); @@ -65,7 +64,7 @@ export const MiniPieChart = ({ Date: Wed, 18 Jun 2025 19:21:33 +0530 Subject: [PATCH 073/190] refactor(AreaChartV2): Clean up code and improve tooltip descriptions This commit refactors the AreaChartV2 component by removing unnecessary whitespace in the SCSS file and optimizing the import statements in the TypeScript file. Additionally, it enhances the tooltip descriptions in the storybook for better clarity and readability. These changes aim to streamline the codebase and improve the user experience when interacting with tooltips. --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 2 +- .../AreaCharts/AreaChartV2/areaChartV2.scss | 1 - .../stories/areaChartV2.stories.tsx | 24 ++++++++++++++----- .../PieCharts/MiniPieChart/MiniPieChart.tsx | 7 +----- .../PortalTooltip/CustomTooltipContent.tsx | 10 +++----- .../shared/PortalTooltip/FloatingUIPortal.tsx | 16 +++++-------- .../Charts/shared/PortalTooltip/example.tsx | 18 +++++++------- .../Charts/shared/PortalTooltip/index.ts | 2 +- .../shared/PortalTooltip/portalTooltip.scss | 2 +- 9 files changed, 40 insertions(+), 42 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index 4446ae4dd..e09303a32 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -10,9 +10,9 @@ import { ChartTooltipContent, keyTransform, } from "../../Charts"; -import { FloatingUIPortal, CustomTooltipContent } from "../../shared/PortalTooltip"; import { cartesianGrid } from "../../cartesianGrid"; import { ActiveDot, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; +import { CustomTooltipContent } from "../../shared/PortalTooltip"; import { LegendItem } from "../../types"; import { findNearestSnapPosition, diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss index fa52293ac..07d7db0a1 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss @@ -1,6 +1,5 @@ @use "../../../../cssUtils" as cssUtils; - .crayon-area-chart-container-inner { display: flex; width: 100%; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx index 4ad5cf966..ae18ff2ae 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -604,7 +604,8 @@ const meta: Meta> = { }, }, useFloatingTooltip: { - description: "Whether to use the floating tooltip that follows the mouse cursor. Uses Floating UI for intelligent positioning and collision detection.", + description: + "Whether to use the floating tooltip that follows the mouse cursor. Uses Floating UI for intelligent positioning and collision detection.", control: "boolean", table: { type: { summary: "boolean" }, @@ -1051,11 +1052,18 @@ export const FloatingTooltipStory: Story = { }, render: (args) => (
-
+
🎯 Floating Tooltip Demo:

- Move your mouse over the chart to see the floating tooltip that follows your cursor. - This tooltip uses Floating UI for intelligent positioning and collision detection. + Move your mouse over the chart to see the floating tooltip that follows your cursor. This + tooltip uses Floating UI for intelligent positioning and collision detection.

@@ -1063,11 +1071,15 @@ export const FloatingTooltipStory: Story = {
-

Default Tooltip

+

+ Default Tooltip +

-

Floating Tooltip

+

+ Floating Tooltip +

diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx index 556ce7431..16dfbbd70 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx @@ -1,4 +1,3 @@ -import clsx from "clsx"; import { debounce } from "lodash-es"; import { useEffect, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; @@ -61,11 +60,7 @@ export const MiniPieChart = ({ const chartConfig = createChartConfig(data, categoryKey, theme); return ( - + ); - return ( - - {tooltipContent} - - ); + return {tooltipContent}; }, ); -CustomTooltipContent.displayName = "CustomTooltipContent"; \ No newline at end of file +CustomTooltipContent.displayName = "CustomTooltipContent"; diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx index 18eb06328..4b747b4aa 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx @@ -1,7 +1,7 @@ -import React, { useRef, useEffect, useState } from "react"; -import { createPortal } from "react-dom"; -import { computePosition, flip, offset, shift } from "@floating-ui/react-dom"; import type { Placement } from "@floating-ui/react-dom"; +import { computePosition, flip, offset, shift } from "@floating-ui/react-dom"; +import React, { useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; interface VirtualElement { getBoundingClientRect(): DOMRect; @@ -53,11 +53,7 @@ export const FloatingUIPortal: React.FC = ({ const updatePosition = async () => { const { x, y } = await computePosition(virtualElementRef.current!, tooltipRef.current!, { placement, - middleware: [ - offset(offsetDistance), - flip(), - shift({ padding: 8 }), - ], + middleware: [offset(offsetDistance), flip(), shift({ padding: 8 })], }); setPosition({ x, y }); @@ -97,6 +93,6 @@ export const FloatingUIPortal: React.FC = ({ > {children}
, - document.body + document.body, ); -}; \ No newline at end of file +}; diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/example.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/example.tsx index 9291475a9..04150636c 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/example.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/example.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { AreaChartV2 } from '../../AreaCharts/AreaChartV2'; +import React from "react"; +import { AreaChartV2 } from "../../AreaCharts/AreaChartV2"; // Example data for the chart const sampleData = [ @@ -16,12 +16,12 @@ const sampleData = [ */ export const FloatingTooltipExample: React.FC = () => { return ( -
+

Floating Tooltip Example

- -
+ +

With Floating Tooltip (follows mouse)

-
+
{
-
+

Standard Tooltip (fixed position)

-
+
{ ); }; -export default FloatingTooltipExample; \ No newline at end of file +export default FloatingTooltipExample; diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/index.ts b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/index.ts index 22f66a172..23a6bfcaa 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/index.ts +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/index.ts @@ -1,2 +1,2 @@ +export { CustomTooltipContent } from "./CustomTooltipContent"; export { FloatingUIPortal } from "./FloatingUIPortal"; -export { CustomTooltipContent } from "./CustomTooltipContent"; \ No newline at end of file diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss index 1facea437..4ce5ee6e0 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss @@ -1,4 +1,4 @@ .crayon-portal-tooltip { pointer-events: none; z-index: 1000; -} \ No newline at end of file +} From 9997943562b5a2e199e801261097f3b6af421609 Mon Sep 17 00:00:00 2001 From: i-subham Date: Wed, 18 Jun 2025 21:04:22 +0530 Subject: [PATCH 074/190] refactor(StackedLegend): Export StackedLegend component and update import statement This commit refactors the StackedLegend component by exporting it for use in other modules. Additionally, the import statement in the PieChartV2 story file has been updated to reflect the new export style. These changes enhance the modularity and usability of the StackedLegend component within the charting library. --- .../PieCharts/PieChartV2/stories/PieChartV2.stories.tsx | 2 +- .../components/Charts/shared/StackedLegend/StackedLegend.tsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index f536676cb..6789ddf6b 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { useEffect, useMemo, useRef, useState } from "react"; import { Card } from "../../../../Card"; import { DefaultLegend } from "../../../shared/DefaultLegend/DefaultLegend"; -import StackedLegend from "../../../shared/StackedLegend/StackedLegend"; +import { StackedLegend } from "../../../shared/StackedLegend/StackedLegend"; import { LegendItem } from "../../../types"; import { getDistributedColors, getPalette } from "../../../utils/PalletUtils"; import { PieChartV2, PieChartV2Props } from "../PieChartV2"; diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx index 9efc7e1ad..44f157297 100644 --- a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx @@ -20,7 +20,7 @@ const formatPercentage = (value: number, total: number): string => { const MAX_VISIBLE_ITEMS = 10; -const StackedLegend = ({ items, onItemHover, activeKey }: StackedLegendProps) => { +export const StackedLegend = ({ items, onItemHover, activeKey }: StackedLegendProps) => { const handleMouseEnter = (key: string) => { onItemHover?.(key); }; @@ -84,5 +84,3 @@ const StackedLegend = ({ items, onItemHover, activeKey }: StackedLegendProps) =>
); }; - -export default StackedLegend; From 62fd278da31bfa2b34b282cc10d4850b7f1d6ad8 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 19 Jun 2025 14:18:25 +0530 Subject: [PATCH 075/190] feat(AreaChartV2): add data-chart attribute to chart container The changes made in this commit include: 1. Adding a `data-chart` attribute to the `ChartContainer` component in the `AreaChartV2` component. This attribute is set to a unique ID generated for the chart, which helps with identifying the chart element in the DOM. 2. Updating the `chartId` generation logic to use the `crayon-chart-` prefix for the generated ID, making it more specific to the Crayon charting library. 3. Updating the `ChartContext.Provider` to include the `id` property, which is the generated chart ID, in the context value. This allows other components that use the `ChartContext` to access the chart ID. 4. Removing the `FloatingTooltipExample` component, as it was not directly related to the changes made in this commit. The main purpose of these changes is to improve the identification and accessibility of the chart elements in the DOM, which can be useful for various purposes, such as targeting specific charts for styling or event handling. --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 1 + .../Charts/shared/PortalTooltip/example.tsx | 59 ------------------- 2 files changed, 1 insertion(+), 59 deletions(-) delete mode 100644 js/packages/react-ui/src/components/Charts/shared/PortalTooltip/example.tsx diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index e09303a32..82da65c1a 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -254,6 +254,7 @@ export const AreaChartV2 = ({
{ - return ( -
-

Floating Tooltip Example

- -
-

With Floating Tooltip (follows mouse)

-
- -
-
- -
-

Standard Tooltip (fixed position)

-
- -
-
-
- ); -}; - -export default FloatingTooltipExample; From 11a8cb8b54ea5674a250bba1c28da6e784b284cc Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 19 Jun 2025 14:21:14 +0530 Subject: [PATCH 076/190] feat(charts): add unique chart id to chart context The changes add a unique `id` property to the `ChartContext` to ensure each chart instance has a unique identifier. This is important for cases where multiple charts are rendered on the same page, as it allows for better identification and targeting of individual charts. The key changes are: 1. Add `id` property to the `ChartContextProps` type. 2. Update the `chartId` generation to use the provided `id` or a generated unique ID. 3. Update the `ChartContext.Provider` to include the `id` in the context value. --- .../PortalTooltip/CustomTooltipContent.tsx | 263 ++++++++---------- 1 file changed, 115 insertions(+), 148 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx index bc3cdd105..1af449d82 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx @@ -1,38 +1,9 @@ import clsx from "clsx"; import { forwardRef, useMemo } from "react"; import * as RechartsPrimitive from "recharts"; -import { ChartConfig, useChart } from "../../../Charts/Charts"; +import { getPayloadConfigFromPayload, useChart } from "../../../Charts/Charts"; import { FloatingUIPortal } from "./FloatingUIPortal"; -/** - * Helper function to extract configuration for a chart element from a payload - * (Local copy since getPayloadConfigFromPayload is not exported from Charts) - */ -function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) { - if (typeof payload !== "object" || payload === null) { - return undefined; - } - - const payloadPayload = - "payload" in payload && typeof payload.payload === "object" && payload.payload !== null - ? payload.payload - : undefined; - - let configLabelKey: string = key; - - if (key in payload && typeof payload[key as keyof typeof payload] === "string") { - configLabelKey = payload[key as keyof typeof payload] as string; - } else if ( - payloadPayload && - key in payloadPayload && - typeof payloadPayload[key as keyof typeof payloadPayload] === "string" - ) { - configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string; - } - - return configLabelKey in config ? config[configLabelKey] : config[key]; -} - /** * Custom tooltip content component for floating tooltips * Mirrors the functionality of ChartTooltipContent but works with FloatingUIPortal @@ -48,135 +19,131 @@ export const CustomTooltipContent = forwardRef< labelKey?: string; showPercentage?: boolean; } ->( - ( - { - active, - payload, - className, - indicator = "dot", - hideLabel = false, - hideIndicator = false, - label, - labelFormatter, - labelClassName, - formatter, - color, - nameKey, - labelKey, - showPercentage = false, - coordinate, - }, - ref, - ) => { - const { config } = useChart(); - - const tooltipLabel = useMemo(() => { - if (hideLabel || !payload?.length) { - return null; - } - - const [item] = payload; - const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`; - const itemConfig = getPayloadConfigFromPayload(config, item, key); - const value = - !labelKey && typeof label === "string" ? config[label]?.label || label : itemConfig?.label; - - if (labelFormatter) { - return ( -
- {labelFormatter(value, payload)} -
- ); - } +>((props, ref) => { + console.log("CustomTooltipContent", props); + const { + active, + payload, + className, + indicator = "dot", + hideLabel = false, + hideIndicator = false, + label, + labelFormatter, + labelClassName, + formatter, + color, + nameKey, + labelKey, + showPercentage = false, + } = props; + const { config } = useChart(); + + const tooltipLabel = useMemo(() => { + if (hideLabel || !payload?.length) { + return null; + } - if (!value) { - return null; - } + const [item] = payload; + const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); + const value = + !labelKey && typeof label === "string" ? config[label]?.label || label : itemConfig?.label; - return
{value}
; - }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]); + if (labelFormatter) { + return ( +
+ {labelFormatter(value, payload)} +
+ ); + } - if (!active || !payload?.length) { + if (!value) { return null; } - const nestLabel = payload.length === 1 && indicator !== "dot"; + return
{value}
; + }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]); - const tooltipContent = ( -
- {!nestLabel && tooltipLabel} -
- {payload.map((item, index) => { - const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`; - const itemConfig = getPayloadConfigFromPayload(config, item, key); - const indicatorColor = (color ?? item.payload.fill) || item.color; + if (!active || !payload?.length) { + return null; + } - return ( -
- {formatter && item?.value !== undefined && item.name ? ( - formatter(item.value, item.name, item, index, item.payload) - ) : ( - <> - {itemConfig?.icon ? ( - - ) : ( - !hideIndicator && ( -
- ) + const nestLabel = payload.length === 1 && indicator !== "dot"; + + const tooltipContent = ( +
+ {!nestLabel && tooltipLabel} +
+ {payload.map((item, index) => { + const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); + const indicatorColor = (color ?? item.payload.fill) || item.color; + + return ( +
+ {formatter && item?.value !== undefined && item.name ? ( + formatter(item.value, item.name, item, index, item.payload) + ) : ( + <> + {itemConfig?.icon ? ( + + ) : ( + !hideIndicator && ( +
+ ) + )} +
-
- {nestLabel && tooltipLabel} - {itemConfig?.label || item.name} -
- {item.value !== undefined && ( - - {item.value.toLocaleString()} - {showPercentage ? "%" : ""} - - )} + > +
+ {nestLabel && tooltipLabel} + {itemConfig?.label || item.name}
- - )} -
- ); - })} -
+ {item.value !== undefined && ( + + {item.value.toLocaleString()} + {showPercentage ? "%" : ""} + + )} +
+ + )} +
+ ); + })}
- ); +
+ ); - return {tooltipContent}; - }, -); + return {tooltipContent}; +}); CustomTooltipContent.displayName = "CustomTooltipContent"; From 5416741f203251cc818870e21f9a0fa75f5f574f Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 19 Jun 2025 15:42:32 +0530 Subject: [PATCH 077/190] feat(charts): add chart id to chart context and portal tooltip The changes made in this commit are: 1. Added the `id` property to the `ChartContextProps` type, which is used to provide a unique identifier for each chart. 2. Updated the `chartId` variable in the `Charts` component to use the `id` property from the context, or a generated unique ID if not provided. 3. Updated the `FloatingUIPortal` component to accept a `chartId` prop, which is used to add a `data-chart` attribute to the portal element. 4. Added a comment to the `charts.scss` file, indicating that the tooltip styles might need to be moved to a dedicated file, and that there are two types of tooltips (default and portal). 5. Removed an unnecessary console log statement from the `CustomTooltipContent` component. 6. Added the `ChartStyle` component to the `FloatingUIPortal` component, which applies the chart's styles to the tooltip content. These changes are focused on improving the chart component's functionality and maintainability, by providing a unique identifier for each chart and ensuring that the tooltip styles are applied correctly. --- .../react-ui/src/components/Charts/Charts.tsx | 8 ++++++-- .../react-ui/src/components/Charts/charts.scss | 6 ++++-- .../shared/PortalTooltip/CustomTooltipContent.tsx | 13 +++++++++---- .../shared/PortalTooltip/FloatingUIPortal.tsx | 6 +++++- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/Charts.tsx b/js/packages/react-ui/src/components/Charts/Charts.tsx index cadf93518..a73960bcf 100644 --- a/js/packages/react-ui/src/components/Charts/Charts.tsx +++ b/js/packages/react-ui/src/components/Charts/Charts.tsx @@ -43,6 +43,7 @@ export type ChartConfig = { */ type ChartContextProps = { config: ChartConfig; + id: string; }; const ChartContext = createContext(null); @@ -128,10 +129,10 @@ const ChartContainer = forwardRef< } >(({ id, className, children, config, rechartsProps, ...props }, ref) => { const uniqueId = useId(); - const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; + const chartId = `crayon-chart-${id || uniqueId.replace(/:/g, "")}`; return ( - +
((props, ref) => { - console.log("CustomTooltipContent", props); + const { active, payload, @@ -37,7 +37,7 @@ export const CustomTooltipContent = forwardRef< labelKey, showPercentage = false, } = props; - const { config } = useChart(); + const { config, id } = useChart(); const tooltipLabel = useMemo(() => { if (hideLabel || !payload?.length) { @@ -143,7 +143,12 @@ export const CustomTooltipContent = forwardRef<
); - return {tooltipContent}; + return ( + + + {tooltipContent} + + ); }); CustomTooltipContent.displayName = "CustomTooltipContent"; diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx index 4b747b4aa..4bdd64776 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx @@ -1,5 +1,6 @@ import type { Placement } from "@floating-ui/react-dom"; import { computePosition, flip, offset, shift } from "@floating-ui/react-dom"; +import clsx from "clsx"; import React, { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; @@ -13,6 +14,7 @@ interface FloatingUIPortalProps { placement?: Placement; offsetDistance?: number; className?: string; + chartId?: string; } export const FloatingUIPortal: React.FC = ({ @@ -21,6 +23,7 @@ export const FloatingUIPortal: React.FC = ({ placement = "right-start", offsetDistance = 8, className = "", + chartId, }) => { const mousePositionRef = useRef({ x: 0, y: 0 }); const virtualElementRef = useRef(null); @@ -82,7 +85,8 @@ export const FloatingUIPortal: React.FC = ({ return createPortal(
Date: Thu, 19 Jun 2025 16:39:45 +0530 Subject: [PATCH 078/190] feat(area-chart): Improve area chart component and tooltip The changes made in this commit improve the functionality and styling of the area chart component and the custom tooltip component. The main changes are: 1. Removed an unnecessary empty line in the `AreaChartV2` component. 2. Moved the `position: absolute` style from the `FloatingUIPortal` component to the `portalTooltip.scss` file, making the tooltip positioning more consistent. 3. Removed the `zIndex` and `pointerEvents` styles from the `FloatingUIPortal` component, as they are now handled in the `portalTooltip.scss` file. 4. Reordered the imports in the `CustomTooltipContent` component to maintain alphabetical order. These changes enhance the overall quality and maintainability of the chart and tooltip components. --- .../components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx | 1 - .../Charts/shared/PortalTooltip/CustomTooltipContent.tsx | 3 +-- .../Charts/shared/PortalTooltip/FloatingUIPortal.tsx | 3 --- .../components/Charts/shared/PortalTooltip/portalTooltip.scss | 1 + 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index 82da65c1a..e09303a32 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -254,7 +254,6 @@ export const AreaChartV2 = ({
((props, ref) => { - const { active, payload, diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx index 4bdd64776..57a61d11d 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx @@ -88,11 +88,8 @@ export const FloatingUIPortal: React.FC = ({ className={clsx("crayon-portal-tooltip", className)} data-chart={chartId} style={{ - position: "absolute", left: position.x, top: position.y, - zIndex: 1000, - pointerEvents: "none", }} > {children} diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss index 4ce5ee6e0..8b25a86da 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss @@ -1,4 +1,5 @@ .crayon-portal-tooltip { pointer-events: none; z-index: 1000; + position: absolute; } From 4a9ecda2070981e3d6a05a3f39cae899bbda1972 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 19 Jun 2025 17:07:01 +0530 Subject: [PATCH 079/190] feat(charts): improve tooltip styles and add comments The changes made in this commit focus on improving the styles of the tooltip in the charts component. The main changes are: - Added comments to explain the purpose of the tooltip content and the indicator styles. - Removed the border-radius and border styles from the tooltip content, as they were not necessary. - Adjusted the border-radius of the indicator element to use a smaller value. - Removed the height property from the indicator line style, as it was not needed. - Added a comment suggesting to either remove or move the Pie Chart styles to a dedicated file. These changes aim to enhance the overall appearance and clarity of the charts component, making it more maintainable and easier to understand. --- .../react-ui/src/components/Charts/charts.scss | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index 23a22ec6f..14b297d83 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -88,6 +88,9 @@ @include cssUtils.typography(label, heavy); } + // this is the content of the tooltip, where the items of the of each data point is rendered, + // todo: remove commented style once the review is done. + &-content { display: grid; align-items: start; @@ -96,8 +99,8 @@ padding: cssUtils.$spacing-xs; color: cssUtils.$primary-text; @include cssUtils.typography(label, default); - border-radius: cssUtils.$rounded-s; - border: 1px solid cssUtils.$stroke-default; + // border-radius: cssUtils.$rounded-s; + // border: 1px solid cssUtils.$stroke-default; background-color: cssUtils.$bg-container; box-shadow: cssUtils.$shadow-s; text-transform: capitalize; @@ -118,9 +121,10 @@ color: cssUtils.$primary-text; } } + // this portion is responsible for styling the colored box or other indicators like line, dashed, etc. &-indicator { flex-shrink: 0; - border-radius: cssUtils.$rounded-3xs; + border-radius: cssUtils.$rounded-2xs; &--dot { height: 10px; @@ -131,7 +135,6 @@ &--line { width: 4px; - height: 100%; background-color: var(--color-bg); border-color: var(--color-border); } @@ -240,6 +243,7 @@ } //Pie Chart styles +// @subham either remove this or move this to it dedicated file. .crayon-pie-chart { &-container { From 42f3525f9217a0fb4a22d1d7452c465aa84811a6 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 19 Jun 2025 17:38:47 +0530 Subject: [PATCH 080/190] feat(portal-tooltip): add portal container prop to FloatingUIPortal Adds a new `portalContainer` prop to the `FloatingUIPortal` component to allow specifying a custom container for the portal. This is useful when the tooltip needs to be rendered within a specific container, rather than the default `document.body`. The `CustomTooltipContent` component is also updated to pass the `portalContainer` prop to the `FloatingUIPortal`. Additionally, the `AreaChartV2` component is updated to pass the `rootContainerRef` as the `portalContainer` prop to the `ChartTooltip` component, ensuring the tooltip is rendered within the chart's root container. --- .../Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx | 6 +++++- .../shared/PortalTooltip/CustomTooltipContent.tsx | 4 +++- .../shared/PortalTooltip/FloatingUIPortal.tsx | 13 ++++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index e09303a32..389590266 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -80,6 +80,7 @@ export const AreaChartV2 = ({ const chartContainerRef = useRef(null); const mainContainerRef = useRef(null); + const rootContainerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(0); const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(false); @@ -208,6 +209,7 @@ export const AreaChartV2 = ({ style={{ width: width ? `${width}px` : undefined, }} + ref={rootContainerRef} >
{showYAxis && ( @@ -293,7 +295,9 @@ export const AreaChartV2 = ({ /> {useFloatingTooltip ? ( - } /> + } + /> ) : ( } /> )} diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx index 8eb6a2fa4..757809683 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx @@ -18,6 +18,7 @@ export const CustomTooltipContent = forwardRef< nameKey?: string; labelKey?: string; showPercentage?: boolean; + portalContainer?: React.RefObject; } >((props, ref) => { const { @@ -35,6 +36,7 @@ export const CustomTooltipContent = forwardRef< nameKey, labelKey, showPercentage = false, + portalContainer, } = props; const { config, id } = useChart(); @@ -143,7 +145,7 @@ export const CustomTooltipContent = forwardRef< ); return ( - + {tooltipContent} diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx index 57a61d11d..3ddf41dec 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx @@ -15,6 +15,7 @@ interface FloatingUIPortalProps { offsetDistance?: number; className?: string; chartId?: string; + portalContainer?: React.RefObject; } export const FloatingUIPortal: React.FC = ({ @@ -24,12 +25,22 @@ export const FloatingUIPortal: React.FC = ({ offsetDistance = 8, className = "", chartId, + portalContainer, }) => { const mousePositionRef = useRef({ x: 0, y: 0 }); const virtualElementRef = useRef(null); const tooltipRef = useRef(null); const [position, setPosition] = useState({ x: 0, y: 0 }); + // Function to get the portal target element + const getPortalTarget = (): HTMLElement => { + if (!portalContainer || !portalContainer.current) { + return document.body; + } + + return portalContainer.current; + }; + useEffect(() => { // Create virtual element that tracks mouse position const virtualElement: VirtualElement = { @@ -94,6 +105,6 @@ export const FloatingUIPortal: React.FC = ({ > {children}
, - document.body, + getPortalTarget(), ); }; From 7887678db4a1a37a9e2d44d0810bd82856906902 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 19 Jun 2025 18:28:50 +0530 Subject: [PATCH 081/190] feat(AreaChartV2): Remove unused rootContainerRef and optimize tooltip positioning The changes made in this commit include: 1. Removing the unused `rootContainerRef` from the `AreaChartV2` component. 2. Optimizing the tooltip positioning in the `FloatingUIPortal` component by: - Introducing a new state variable `isPositioned` to avoid the tooltip from flickering when the mouse is moving fast and the tooltip is not positioned yet initially. - Updating the tooltip's `opacity` based on the `isPositioned` state to provide a smooth transition. These changes improve the overall performance and user experience of the `AreaChartV2` component. --- .../Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx | 6 +----- .../Charts/shared/PortalTooltip/FloatingUIPortal.tsx | 8 ++++++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index 389590266..e09303a32 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -80,7 +80,6 @@ export const AreaChartV2 = ({ const chartContainerRef = useRef(null); const mainContainerRef = useRef(null); - const rootContainerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(0); const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(false); @@ -209,7 +208,6 @@ export const AreaChartV2 = ({ style={{ width: width ? `${width}px` : undefined, }} - ref={rootContainerRef} >
{showYAxis && ( @@ -295,9 +293,7 @@ export const AreaChartV2 = ({ /> {useFloatingTooltip ? ( - } - /> + } /> ) : ( } /> )} diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx index 3ddf41dec..f55859455 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx @@ -31,6 +31,7 @@ export const FloatingUIPortal: React.FC = ({ const virtualElementRef = useRef(null); const tooltipRef = useRef(null); const [position, setPosition] = useState({ x: 0, y: 0 }); + const [isPositioned, setIsPositioned] = useState(false); // Function to get the portal target element const getPortalTarget = (): HTMLElement => { @@ -65,12 +66,17 @@ export const FloatingUIPortal: React.FC = ({ if (!active || !virtualElementRef.current || !tooltipRef.current) return; const updatePosition = async () => { + // https://floating-ui.com/docs/computePosition const { x, y } = await computePosition(virtualElementRef.current!, tooltipRef.current!, { placement, middleware: [offset(offsetDistance), flip(), shift({ padding: 8 })], }); setPosition({ x, y }); + // this is to avoid the tooltip from flickering when the mouse is moving fast and the tooltip is not positioned yet initially + setTimeout(() => { + setIsPositioned(true); + }, 20); }; const handleMouseMove = (event: MouseEvent) => { @@ -88,6 +94,7 @@ export const FloatingUIPortal: React.FC = ({ return () => { document.removeEventListener("mousemove", handleMouseMove); + setIsPositioned(false); }; }, [active, placement, offsetDistance]); @@ -101,6 +108,7 @@ export const FloatingUIPortal: React.FC = ({ style={{ left: position.x, top: position.y, + opacity: isPositioned ? 1 : 0, }} > {children} From db52bdfc863a6746b16a39e46a00dbda49cb46ba Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 19 Jun 2025 23:15:14 +0530 Subject: [PATCH 082/190] feat(AreaChartV2): Increase chart width and add tooltip offset - Increase the width of the AreaChartV2 component from 600px to 700px to provide more space for the chart. - Add an `offset` prop to the `ChartTooltip` component in the AreaChartV2 component, setting it to 15 pixels. This will adjust the position of the tooltip to be slightly offset from the data point. feat(CustomTooltipContent): Add offset prop to FloatingUIPortal - Add an `offset` prop to the `FloatingUIPortal` component in the `CustomTooltipContent` component. This will adjust the position of the tooltip to be slightly offset from the data point. refactor(TwoLevelPieChart.stories): Import StackedLegend directly - Import the `StackedLegend` component directly from the `StackedLegend` module instead of using a default import. feat(FloatingUIPortal): Use autoPlacement middleware - Update the `computePosition` function in the `FloatingUIPortal` component to use the `autoPlacement` middleware. This will automatically choose the best placement for the tooltip based on the available space. --- .../Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx | 6 +++--- .../AreaChartV2/stories/areaChartV2.stories.tsx | 2 +- .../stories/TwoLevelPieChart.stories.tsx | 2 +- .../Charts/shared/PortalTooltip/CustomTooltipContent.tsx | 8 +++++++- .../Charts/shared/PortalTooltip/FloatingUIPortal.tsx | 9 ++++++--- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index e09303a32..86dd40576 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -293,7 +293,7 @@ export const AreaChartV2 = ({ /> {useFloatingTooltip ? ( - } /> + } offset={15} /> ) : ( } /> )} @@ -302,7 +302,7 @@ export const AreaChartV2 = ({ const transformedKey = keyTransform(key); const color = `var(--color-${transformedKey})`; return ( - <> + @@ -321,7 +321,7 @@ export const AreaChartV2 = ({ dot={false} isAnimationActive={isAnimationActive} /> - + ); })} diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx index ae18ff2ae..662701e4c 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -782,7 +782,7 @@ export const BigLabelsStory: Story = { showYAxis: true, }, render: (args) => ( - + ), diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx index afa1322e9..47a53f0b2 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { useEffect, useMemo, useRef, useState } from "react"; import { Card } from "../../../../Card"; import { DefaultLegend } from "../../../shared/DefaultLegend/DefaultLegend"; -import StackedLegend from "../../../shared/StackedLegend/StackedLegend"; +import { StackedLegend } from "../../../shared/StackedLegend/StackedLegend"; import { LegendItem } from "../../../types"; import { getDistributedColors, getPalette } from "../../../utils/PalletUtils"; import { TwoLevelPieChart, TwoLevelPieChartProps } from "../TwoLevelPieChart"; diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx index 757809683..c4f575904 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx @@ -37,6 +37,7 @@ export const CustomTooltipContent = forwardRef< labelKey, showPercentage = false, portalContainer, + offset, } = props; const { config, id } = useChart(); @@ -145,7 +146,12 @@ export const CustomTooltipContent = forwardRef< ); return ( - + {tooltipContent} diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx index f55859455..ab4b3342a 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx @@ -1,5 +1,5 @@ import type { Placement } from "@floating-ui/react-dom"; -import { computePosition, flip, offset, shift } from "@floating-ui/react-dom"; +import { autoPlacement, computePosition, flip, offset, shift } from "@floating-ui/react-dom"; import clsx from "clsx"; import React, { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; @@ -22,7 +22,7 @@ export const FloatingUIPortal: React.FC = ({ active, children, placement = "right-start", - offsetDistance = 8, + offsetDistance = 10, className = "", chartId, portalContainer, @@ -44,6 +44,7 @@ export const FloatingUIPortal: React.FC = ({ useEffect(() => { // Create virtual element that tracks mouse position + // this element element is basically tracks the mouse position always shadows the mouse cursor. const virtualElement: VirtualElement = { getBoundingClientRect(): DOMRect { return { @@ -58,6 +59,7 @@ export const FloatingUIPortal: React.FC = ({ } as DOMRect; }, }; + // it as 0 width and height because it is not a real element, it is just a virtual element that tracks the mouse position. virtualElementRef.current = virtualElement; }, []); @@ -67,9 +69,10 @@ export const FloatingUIPortal: React.FC = ({ const updatePosition = async () => { // https://floating-ui.com/docs/computePosition + // not a synchronous function, it returns a promise. so we need to await it. const { x, y } = await computePosition(virtualElementRef.current!, tooltipRef.current!, { placement, - middleware: [offset(offsetDistance), flip(), shift({ padding: 8 })], + middleware: [offset(offsetDistance), flip(), shift({ padding: 8 }), autoPlacement()], }); setPosition({ x, y }); From c431344c41fc716bbbede094ab53e9bb73b16712 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 19 Jun 2025 23:29:32 +0530 Subject: [PATCH 083/190] feat(PortalTooltip): remove autoPlacement middleware Removes the autoPlacement middleware from the computePosition options in the FloatingUIPortal component. This change ensures that the tooltip is always positioned relative to the virtual element, regardless of the available space on the screen. --- .../components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx index ab4b3342a..acbb1ade4 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx @@ -72,7 +72,7 @@ export const FloatingUIPortal: React.FC = ({ // not a synchronous function, it returns a promise. so we need to await it. const { x, y } = await computePosition(virtualElementRef.current!, tooltipRef.current!, { placement, - middleware: [offset(offsetDistance), flip(), shift({ padding: 8 }), autoPlacement()], + middleware: [offset(offsetDistance), flip(), shift({ padding: 8 })], }); setPosition({ x, y }); From bcdedb399dc0159aa5d3f797639765b2ca42db1b Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 20 Jun 2025 00:33:19 +0530 Subject: [PATCH 084/190] feat(tooltip): improve custom tooltip content component This change improves the `CustomTooltipContent` component in the `react-ui` package. The main improvements are: - Memoize the component to prevent unnecessary re-renders - Add a `DEFAULT_INDICATOR` constant to simplify the code - Optimize the `tooltipLabel` rendering by early returning when the payload is empty or the label is hidden - Memoize the `nestLabel` calculation to improve performance These changes aim to enhance the performance and readability of the `CustomTooltipContent` component. --- .../react-ui/src/components/Charts/Charts.tsx | 1 - .../PieCharts/MiniPieChart/MiniPieChart.tsx | 2 - .../PortalTooltip/CustomTooltipContent.tsx | 308 ++++++++++-------- .../shared/PortalTooltip/FloatingUIPortal.tsx | 9 +- 4 files changed, 171 insertions(+), 149 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/Charts.tsx b/js/packages/react-ui/src/components/Charts/Charts.tsx index a73960bcf..e1ebf21db 100644 --- a/js/packages/react-ui/src/components/Charts/Charts.tsx +++ b/js/packages/react-ui/src/components/Charts/Charts.tsx @@ -185,7 +185,6 @@ const ChartTooltipContent = forwardRef< nameKey, labelKey, showPercentage = false, - coordinate, }, ref, ) => { diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx index 16dfbbd70..40a01685c 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx @@ -20,7 +20,6 @@ export interface MiniPieChartProps { variant?: "pie" | "donut"; format?: "percentage" | "number"; legend?: boolean; - label?: boolean; isAnimationActive?: boolean; } @@ -31,7 +30,6 @@ export const MiniPieChart = ({ theme = "ocean", variant = "pie", format = "number", - label = true, isAnimationActive = true, }: MiniPieChartProps) => { const { layout } = useLayoutContext(); diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx index c4f575904..59cbdd24c 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx @@ -1,161 +1,185 @@ import clsx from "clsx"; -import { forwardRef, useMemo } from "react"; +import { forwardRef, memo, useMemo } from "react"; import * as RechartsPrimitive from "recharts"; import { ChartStyle, getPayloadConfigFromPayload, useChart } from "../../../Charts/Charts"; import { FloatingUIPortal } from "./FloatingUIPortal"; + +const DEFAULT_INDICATOR = "dot" as const; + /** * Custom tooltip content component for floating tooltips * Mirrors the functionality of ChartTooltipContent but works with FloatingUIPortal */ -export const CustomTooltipContent = forwardRef< - HTMLDivElement, - React.ComponentProps & - React.ComponentProps<"div"> & { - hideLabel?: boolean; - hideIndicator?: boolean; - indicator?: "line" | "dot" | "dashed"; - nameKey?: string; - labelKey?: string; - showPercentage?: boolean; - portalContainer?: React.RefObject; - } ->((props, ref) => { - const { - active, - payload, - className, - indicator = "dot", - hideLabel = false, - hideIndicator = false, - label, - labelFormatter, - labelClassName, - formatter, - color, - nameKey, - labelKey, - showPercentage = false, - portalContainer, - offset, - } = props; - const { config, id } = useChart(); - - const tooltipLabel = useMemo(() => { - if (hideLabel || !payload?.length) { - return null; - } +export const CustomTooltipContent = memo( + forwardRef< + HTMLDivElement, + React.ComponentProps & + React.ComponentProps<"div"> & { + hideLabel?: boolean; + hideIndicator?: boolean; + indicator?: "line" | "dot" | "dashed"; + nameKey?: string; + labelKey?: string; + showPercentage?: boolean; + portalContainer?: React.RefObject; + } + >((props, ref) => { + const { + active, + payload, + className, + indicator = DEFAULT_INDICATOR, + hideLabel = false, + hideIndicator = false, + label, + labelFormatter, + labelClassName, + formatter, + color, + nameKey, + labelKey, + showPercentage = false, + portalContainer, + offset, + } = props; - const [item] = payload; - const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`; - const itemConfig = getPayloadConfigFromPayload(config, item, key); - const value = - !labelKey && typeof label === "string" ? config[label]?.label || label : itemConfig?.label; - - if (labelFormatter) { - return ( -
- {labelFormatter(value, payload)} -
- ); - } + const { config, id } = useChart(); - if (!value) { + // Early return for inactive or empty payload + if (!active || !payload?.length) { return null; } - return
{value}
; - }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]); - - if (!active || !payload?.length) { - return null; - } - - const nestLabel = payload.length === 1 && indicator !== "dot"; - - const tooltipContent = ( -
- {!nestLabel && tooltipLabel} -
- {payload.map((item, index) => { - const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`; - const itemConfig = getPayloadConfigFromPayload(config, item, key); - const indicatorColor = (color ?? item.payload.fill) || item.color; - - return ( -
- {formatter && item?.value !== undefined && item.name ? ( - formatter(item.value, item.name, item, index, item.payload) - ) : ( - <> - {itemConfig?.icon ? ( - - ) : ( - !hideIndicator && ( -
- ) + const tooltipLabel = useMemo(() => { + if (hideLabel || !payload?.length) { + return null; + } + + const [item] = payload; + const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); + const value = + !labelKey && typeof label === "string" ? config[label]?.label || label : itemConfig?.label; + + if (labelFormatter) { + return ( +
+ {labelFormatter(value, payload)} +
+ ); + } + + if (!value) { + return null; + } + + return
{value}
; + }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]); + + const nestLabel = useMemo( + () => payload.length === 1 && indicator !== DEFAULT_INDICATOR, + [payload.length, indicator], + ); + + // Memoize payload items rendering for better performance + const payloadItems = useMemo(() => { + return payload.map((item, index) => { + const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); + const indicatorColor = (color ?? item.payload?.fill) || item.color; + + return ( +
+ {formatter && item?.value !== undefined && item.name ? ( + formatter(item.value, item.name, item, index, item.payload) + ) : ( + <> + {itemConfig?.icon ? ( + + ) : ( + !hideIndicator && ( +
+ ) + )} +
-
- {nestLabel && tooltipLabel} - {itemConfig?.label || item.name} -
- {item.value !== undefined && ( - - {item.value.toLocaleString()} - {showPercentage ? "%" : ""} - - )} + > +
+ {nestLabel && tooltipLabel} + {itemConfig?.label || item.name}
- - )} -
- ); - })} + {item.value !== undefined && ( + + {typeof item.value === "number" ? item.value.toLocaleString() : item.value} + {showPercentage ? "%" : ""} + + )} +
+ + )} +
+ ); + }); + }, [ + payload, + nameKey, + config, + color, + indicator, + formatter, + hideIndicator, + nestLabel, + tooltipLabel, + showPercentage, + ]); + + const tooltipContent = ( +
+ {!nestLabel && tooltipLabel} +
{payloadItems}
-
- ); - - return ( - - - {tooltipContent} - - ); -}); + ); + + return ( + + + {tooltipContent} + + ); + }), +); CustomTooltipContent.displayName = "CustomTooltipContent"; diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx index acbb1ade4..1ffaf4bf3 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx @@ -1,7 +1,7 @@ import type { Placement } from "@floating-ui/react-dom"; -import { autoPlacement, computePosition, flip, offset, shift } from "@floating-ui/react-dom"; +import { computePosition, flip, offset, shift } from "@floating-ui/react-dom"; import clsx from "clsx"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; interface VirtualElement { @@ -34,13 +34,14 @@ export const FloatingUIPortal: React.FC = ({ const [isPositioned, setIsPositioned] = useState(false); // Function to get the portal target element - const getPortalTarget = (): HTMLElement => { + + const getPortalTarget = useCallback((): HTMLElement => { if (!portalContainer || !portalContainer.current) { return document.body; } return portalContainer.current; - }; + }, [portalContainer]); useEffect(() => { // Create virtual element that tracks mouse position From 2ae1e4a1a015ec24c6b13e1028d95b5de292004f Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 20 Jun 2025 01:05:37 +0530 Subject: [PATCH 085/190] feat(PortalTooltip): Optimize FloatingUIPortal component The changes in this commit optimize the `FloatingUIPortal` component by: 1. Memoizing the virtual element that tracks the mouse position to avoid recreating it on every render. 2. Memoizing the `getPortalTarget` function to avoid recreating it on every render. 3. Memoizing the `updatePosition` function to avoid recreating it on every render. 4. Memoizing the `handleMouseMove` function to avoid recreating it on every render. 5. Simplifying the `useEffect` hook that handles the mouse move event and the tooltip positioning. These changes improve the performance of the `FloatingUIPortal` component by reducing unnecessary re-renders and function creations. --- .../PortalTooltip/CustomTooltipContent.tsx | 1 - .../shared/PortalTooltip/FloatingUIPortal.tsx | 93 ++++++++++--------- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx index 59cbdd24c..b6b4884a0 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx @@ -4,7 +4,6 @@ import * as RechartsPrimitive from "recharts"; import { ChartStyle, getPayloadConfigFromPayload, useChart } from "../../../Charts/Charts"; import { FloatingUIPortal } from "./FloatingUIPortal"; - const DEFAULT_INDICATOR = "dot" as const; /** diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx index 1ffaf4bf3..b3a808abd 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx @@ -1,7 +1,7 @@ import type { Placement } from "@floating-ui/react-dom"; import { computePosition, flip, offset, shift } from "@floating-ui/react-dom"; import clsx from "clsx"; -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; interface VirtualElement { @@ -28,25 +28,15 @@ export const FloatingUIPortal: React.FC = ({ portalContainer, }) => { const mousePositionRef = useRef({ x: 0, y: 0 }); - const virtualElementRef = useRef(null); const tooltipRef = useRef(null); const [position, setPosition] = useState({ x: 0, y: 0 }); const [isPositioned, setIsPositioned] = useState(false); - // Function to get the portal target element - - const getPortalTarget = useCallback((): HTMLElement => { - if (!portalContainer || !portalContainer.current) { - return document.body; - } - - return portalContainer.current; - }, [portalContainer]); - - useEffect(() => { - // Create virtual element that tracks mouse position - // this element element is basically tracks the mouse position always shadows the mouse cursor. - const virtualElement: VirtualElement = { + // Memoize the virtual element to avoid recreating it on every render + // this virtual element basically shares the same position as the mouse position + // and it is used to position the tooltip + const virtualElement = useMemo( + () => ({ getBoundingClientRect(): DOMRect { return { width: 0, @@ -59,48 +49,63 @@ export const FloatingUIPortal: React.FC = ({ bottom: mousePositionRef.current.y, } as DOMRect; }, - }; - // it as 0 width and height because it is not a real element, it is just a virtual element that tracks the mouse position. - - virtualElementRef.current = virtualElement; - }, []); - - useEffect(() => { - if (!active || !virtualElementRef.current || !tooltipRef.current) return; - - const updatePosition = async () => { - // https://floating-ui.com/docs/computePosition - // not a synchronous function, it returns a promise. so we need to await it. - const { x, y } = await computePosition(virtualElementRef.current!, tooltipRef.current!, { - placement, - middleware: [offset(offsetDistance), flip(), shift({ padding: 8 })], - }); + }), + [], + ); - setPosition({ x, y }); - // this is to avoid the tooltip from flickering when the mouse is moving fast and the tooltip is not positioned yet initially - setTimeout(() => { - setIsPositioned(true); - }, 20); - }; + // Function to get the portal target element + // also memoize it to avoid recreating it on every render + const getPortalTarget = useCallback((): HTMLElement => { + if (!portalContainer || !portalContainer.current) { + return document.body; + } + return portalContainer.current; + }, [portalContainer]); - const handleMouseMove = (event: MouseEvent) => { + // Memoize the updatePosition function to avoid recreating it + const updatePosition = useCallback(async () => { + if (!virtualElement || !tooltipRef.current) return; + + // https://floating-ui.com/docs/computePosition + // not a synchronous function, it returns a promise. so we need to await it. + const { x, y } = await computePosition(virtualElement, tooltipRef.current, { + placement, + middleware: [offset(offsetDistance), flip(), shift({ padding: 8 })], + }); + + setPosition({ x, y }); + // this is to avoid the tooltip from flickering when the mouse is moving fast and the tooltip is not positioned yet initially + setTimeout(() => { + setIsPositioned(true); + }, 20); + }, [virtualElement, placement, offsetDistance]); + + // Memoize the mouse move handler + const handleMouseMove = useCallback( + (event: MouseEvent) => { mousePositionRef.current = { x: event.clientX, y: event.clientY, }; updatePosition(); - }; + }, + [updatePosition], + ); - if (active) { - document.addEventListener("mousemove", handleMouseMove); - updatePosition(); + useEffect(() => { + if (!active) { + setIsPositioned(false); + return; } + document.addEventListener("mousemove", handleMouseMove); + updatePosition(); + return () => { document.removeEventListener("mousemove", handleMouseMove); setIsPositioned(false); }; - }, [active, placement, offsetDistance]); + }, [active, handleMouseMove, updatePosition]); if (!active) return null; From f36e1ee3e583976de8d273b4be1bccc50f9e8dd4 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 20 Jun 2025 02:15:38 +0530 Subject: [PATCH 086/190] feat: remove unused WidgetsChart component The WidgetsChart component was not being used in the application, so it has been removed to keep the codebase clean and maintainable. --- .../Charts/WidgetsChart/WidgetsChart.tsx | 26 ------------------- .../components/Charts/WidgetsChart/index.ts | 0 2 files changed, 26 deletions(-) delete mode 100644 js/packages/react-ui/src/components/Charts/WidgetsChart/WidgetsChart.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/WidgetsChart/index.ts diff --git a/js/packages/react-ui/src/components/Charts/WidgetsChart/WidgetsChart.tsx b/js/packages/react-ui/src/components/Charts/WidgetsChart/WidgetsChart.tsx deleted file mode 100644 index 5815e5ba5..000000000 --- a/js/packages/react-ui/src/components/Charts/WidgetsChart/WidgetsChart.tsx +++ /dev/null @@ -1,26 +0,0 @@ -// const WidgetsChart = () => { -// const calculateTotal = ( -// data: T, -// categoryKey: keyof T[number], -// ): number => { -// const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); -// return data.reduce((sum, item) => { -// return sum + dataKeys.reduce((keySum, key) => keySum + Number(item[key] || 0), 0); -// }, 0); -// }; -// return ( -//
-//
-//
-// -// {calculateTotal(data, categoryKey).toLocaleString()} -// -// -// {label} -// -//
-//
-// ); -// }; - -// export default WidgetsChart; diff --git a/js/packages/react-ui/src/components/Charts/WidgetsChart/index.ts b/js/packages/react-ui/src/components/Charts/WidgetsChart/index.ts deleted file mode 100644 index e69de29bb..000000000 From 63249ee6445e5f7ef74a397cd7946332a32d602b Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 20 Jun 2025 02:23:09 +0530 Subject: [PATCH 087/190] exporting everthis from areachar v2 that is needed --- .../Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx | 6 +++--- .../AreaChartV2/stories/areaChartV2.stories.tsx | 12 ++++++------ .../Charts/AreaCharts/MiniAreaChart/index.ts | 1 + .../src/components/Charts/AreaCharts/index.ts | 3 +++ 4 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/index.ts diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index 86dd40576..d6d0f48cb 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -42,7 +42,7 @@ export interface AreaChartV2Props { height?: number; width?: number; onAreaClick?: (payload: any) => void; - useFloatingTooltip?: boolean; + floatingTooltip?: boolean; } const Y_AXIS_WIDTH = 40; // Width of Y-axis chart when shown @@ -63,7 +63,7 @@ export const AreaChartV2 = ({ height, width, onAreaClick, - useFloatingTooltip = true, + floatingTooltip = true, }: AreaChartV2Props) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); @@ -292,7 +292,7 @@ export const AreaChartV2 = ({ }} /> - {useFloatingTooltip ? ( + {floatingTooltip ? ( } offset={15} /> ) : ( } /> diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx index 662701e4c..8b115b497 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -603,13 +603,13 @@ const meta: Meta> = { category: "Events", }, }, - useFloatingTooltip: { + floatingTooltip: { description: "Whether to use the floating tooltip that follows the mouse cursor. Uses Floating UI for intelligent positioning and collision detection.", control: "boolean", table: { type: { summary: "boolean" }, - defaultValue: { summary: "false" }, + defaultValue: { summary: "true" }, category: "Tooltip", }, }, @@ -632,7 +632,7 @@ export const AreaChartV2Story: Story = { showYAxis: true, xAxisLabel: "Time Period", yAxisLabel: "Values", - useFloatingTooltip: true, + floatingTooltip: true, // width: 600, // height: 300, }, @@ -1046,7 +1046,7 @@ export const FloatingTooltipStory: Story = { legend: true, isAnimationActive: true, showYAxis: true, - useFloatingTooltip: true, + floatingTooltip: true, xAxisLabel: "Time Period", yAxisLabel: "Values", }, @@ -1074,13 +1074,13 @@ export const FloatingTooltipStory: Story = {

Default Tooltip

- +

Floating Tooltip

- +
diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/index.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/index.ts new file mode 100644 index 000000000..4f962ecee --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/index.ts @@ -0,0 +1 @@ +export * from "./MiniAreaChart"; \ No newline at end of file diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/index.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/index.ts new file mode 100644 index 000000000..20b21af87 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./MiniAreaChart"; +export * from "./AreaChartV2"; From cda6007daaff95774ceec253a740c6c18ba17dcc Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 20 Jun 2025 02:32:19 +0530 Subject: [PATCH 088/190] feat(area-charts): Add dependencies and forward area chart styles This commit introduces the following changes: 1. Adds dependencies for the `MiniAreaChart` and `AreaChartV2` components in their respective `dependencies.ts` files. 2. Forwards the `AreaChartV2` styles in the `index.scss` file for the `AreaCharts` module. 3. Reorganizes the exports in the `index.ts` file for the `AreaCharts` module to ensure consistent ordering. These changes are necessary to support the standards --- .../components/Charts/AreaCharts/AreaChartV2/dependencies.ts | 2 ++ .../Charts/AreaCharts/MiniAreaChart/dependencies.ts | 2 ++ .../src/components/Charts/AreaCharts/MiniAreaChart/index.ts | 2 +- .../react-ui/src/components/Charts/AreaCharts/index.scss | 1 + .../react-ui/src/components/Charts/AreaCharts/index.ts | 4 ++-- js/packages/react-ui/src/components/Charts/charts.scss | 2 +- 6 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/dependencies.ts create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/dependencies.ts create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/index.scss diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/dependencies.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/dependencies.ts new file mode 100644 index 000000000..af878f3f2 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/dependencies.ts @@ -0,0 +1,2 @@ +const dependencies = ["AreaChartV2", "IconButton"]; +export default dependencies; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/dependencies.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/dependencies.ts new file mode 100644 index 000000000..3b1962326 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/dependencies.ts @@ -0,0 +1,2 @@ +const dependencies = ["MiniAreaChart"]; +export default dependencies; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/index.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/index.ts index 4f962ecee..8d4661adc 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/index.ts +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/index.ts @@ -1 +1 @@ -export * from "./MiniAreaChart"; \ No newline at end of file +export * from "./MiniAreaChart"; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/index.scss b/js/packages/react-ui/src/components/Charts/AreaCharts/index.scss new file mode 100644 index 000000000..1481a4630 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/index.scss @@ -0,0 +1 @@ +@forward "./AreaChartV2/areaChartV2.scss"; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/index.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/index.ts index 20b21af87..b8d927d2d 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/index.ts +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/index.ts @@ -1,3 +1,3 @@ -export * from "./types"; -export * from "./MiniAreaChart"; export * from "./AreaChartV2"; +export * from "./MiniAreaChart"; +export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index 14b297d83..feda2104c 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -6,7 +6,7 @@ @forward "./BarCharts/MiniBarChart/miniBarChart.scss"; // area chart css -@forward "./AreaCharts/AreaChartV2/areaChartV2.scss"; +@forward "./AreaCharts/index.scss"; // line chart css @forward "./LineCharts/LineChartV2/lineChartV2.scss"; From f1ce144dcc48d1db66c3d11740e45fa33863db1e Mon Sep 17 00:00:00 2001 From: i-subham Date: Thu, 19 Jun 2025 18:44:52 +0530 Subject: [PATCH 089/190] feat(PieChartV2): Introduce stacked legend variant and enhance responsiveness This commit enhances the PieChartV2 component by adding a new 'stacked' legend variant, which supports a responsive layout for better usability on different screen sizes. Key changes include: - Implementation of a new StackedLegend component for displaying legend items in a vertical format. - Updates to the PieChartV2 to handle legend interactions and hover effects specifically for the stacked variant. - Enhancements to the SCSS styles for both PieChartV2 and StackedLegend to improve layout and visual transitions. These improvements aim to provide a more flexible and user-friendly charting experience. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 291 ++++++++++--- .../PieCharts/PieChartV2/pieChartV2.scss | 97 +++++ .../PieChartV2/stories/PieChartV2.stories.tsx | 393 ++---------------- .../shared/StackedLegend/StackedLegend.tsx | 25 +- .../shared/StackedLegend/stackedLegend.scss | 35 +- 5 files changed, 429 insertions(+), 412 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 713e2d525..091db6d74 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -1,7 +1,10 @@ import clsx from "clsx"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts"; +import { DefaultLegend } from "../../shared/DefaultLegend/DefaultLegend"; +import { StackedLegend } from "../../shared/StackedLegend/StackedLegend"; +import { LegendItem } from "../../types/Legend"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; import { createGradientDefinitions } from "../components/PieChartRenderers"; import { @@ -15,6 +18,7 @@ import { transformDataWithPercentages, useChartHover, } from "../utils/PieChartUtils"; +import "./pieChartV2.scss"; export type PieChartV2Data = PieChartData; @@ -31,6 +35,7 @@ export interface PieChartV2Props { variant?: "pie" | "donut"; format?: "percentage" | "number"; legend?: boolean; + legendVariant?: "default" | "stacked"; isAnimationActive?: boolean; appearance?: "circular" | "semiCircular"; cornerRadius?: number; @@ -53,6 +58,7 @@ export const PieChartV2 = ({ variant = "pie", format = "number", legend = true, + legendVariant = "stacked", isAnimationActive = true, appearance = "circular", cornerRadius = 0, @@ -67,8 +73,12 @@ export const PieChartV2 = ({ width, }: PieChartV2Props) => { const chartContainerRef = useRef(null); + const wrapperRef = useRef(null); const [containerWidth, setContainerWidth] = useState(0); const [containerHeight, setContainerHeight] = useState(0); + const [wrapperWidth, setWrapperWidth] = useState(0); + const [isMobile, setIsMobile] = useState(false); + const [hoveredLegendKey, setHoveredLegendKey] = useState(null); const { activeIndex, handleMouseEnter, handleMouseLeave } = useChartHover(); // Use provided dimensions or observed dimensions @@ -97,6 +107,83 @@ export const PieChartV2 = ({ const palette = getPalette(theme); const colors = getDistributedColors(palette, data.length); + // Create legend items for both variants + const legendItems = useMemo(() => { + return data.map((item, index) => ({ + key: String(item[categoryKey]), + label: String(item[categoryKey]), + value: Number(item[dataKey]), + color: colors[index] || "#000000", + })); + }, [data, categoryKey, dataKey, colors]); + + // Create DefaultLegend items + const defaultLegendItems = useMemo((): LegendItem[] => { + return data.map((item, index) => ({ + key: String(item[categoryKey]), + label: String(item[categoryKey]), + color: colors[index] || "#000000", + })); + }, [data, categoryKey, colors]); + + // Handle legend hover + const handleLegendHover = useCallback( + (key: string | null) => { + // Only handle legend hover for stacked legend variant + if (legendVariant !== "stacked") return; + setHoveredLegendKey(key); + }, + [legendVariant], + ); + + // Handle legend item hover to highlight pie slice + const handleLegendItemHover = useCallback( + (index: number | null) => { + // Only handle legend hover for stacked legend variant + if (legendVariant !== "stacked") return; + + if (index !== null) { + // Find the corresponding data item and set it as active + const sortedItems = [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])); + const item = sortedItems[index]; + if (item) { + const categoryValue = String(item[categoryKey]); + setHoveredLegendKey(categoryValue); + // Find the index in the original data array + const originalIndex = data.findIndex((d) => String(d[categoryKey]) === categoryValue); + if (originalIndex !== -1) { + handleMouseEnter(data[originalIndex], originalIndex); + } + } + } else { + setHoveredLegendKey(null); + handleMouseLeave(); + } + }, + [data, categoryKey, dataKey, handleMouseEnter, handleMouseLeave, legendVariant], + ); + + // Enhanced chart hover handlers + const handleChartMouseEnter = useCallback( + (data: any, index: number) => { + handleMouseEnter(data, index); + // Only set legend hover for stacked legend variant + if (legend && legendVariant === "stacked") { + const categoryValue = String(data[categoryKey]); + setHoveredLegendKey(categoryValue); + } + }, + [handleMouseEnter, categoryKey, legend, legendVariant], + ); + + const handleChartMouseLeave = useCallback(() => { + handleMouseLeave(); + // Only clear legend hover for stacked legend variant + if (legend && legendVariant === "stacked") { + setHoveredLegendKey(null); + } + }, [handleMouseLeave, legend, legendVariant]); + // Calculate dimensions based on variant const dimensions = useMemo(() => { if (variant === "donut") { @@ -109,8 +196,57 @@ export const PieChartV2 = ({ }; }, [variant, chartSize]); + // Determine layout based on wrapper width and legend variant + const layoutConfig = useMemo(() => { + if (!legend || legendVariant !== "stacked") { + return { isRow: false, isMobile: false }; + } + + // Use wrapper width for responsive decisions + const availableWidth = wrapperWidth || containerWidth; + const isMobileLayout = availableWidth <= 400; + + return { + isRow: !isMobileLayout && availableWidth > 400, + isMobile: isMobileLayout, + }; + }, [legend, legendVariant, wrapperWidth, containerWidth]); + useEffect(() => { - // Only set up ResizeObserver if dimensions are not provided + // Check screen size for responsive behavior + const checkScreenSize = () => { + setIsMobile(window.innerWidth < 600); + }; + + checkScreenSize(); + window.addEventListener("resize", checkScreenSize); + + return () => { + window.removeEventListener("resize", checkScreenSize); + }; + }, []); + + useEffect(() => { + // Set up ResizeObserver for wrapper to get overall container width + if (!wrapperRef.current) { + return () => {}; + } + + const wrapperResizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + setWrapperWidth(entry.contentRect.width); + } + }); + + wrapperResizeObserver.observe(wrapperRef.current); + + return () => { + wrapperResizeObserver.disconnect(); + }; + }, []); + + useEffect(() => { + // Only set up ResizeObserver for chart container if dimensions are not provided if ((width && height) || !chartContainerRef.current) { return () => {}; } @@ -145,18 +281,20 @@ export const PieChartV2 = ({ outerRadius={dimensions.middleRadius} label={false} {...animationConfig} - {...eventHandlers} + {...(legendVariant === "stacked" ? eventHandlers : {})} {...sectorStyle} startAngle={appearance === "semiCircular" ? 0 : 0} endAngle={appearance === "semiCircular" ? 180 : 360} - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} + onMouseEnter={legendVariant === "stacked" ? handleChartMouseEnter : undefined} + onMouseLeave={legendVariant === "stacked" ? handleChartMouseLeave : undefined} > {transformedData.map((entry, index) => { const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); const config = chartConfig[categoryValue]; const hoverStyles = getHoverStyles(index, activeIndex); - const fill = useGradients ? `url(#gradient-${index})` : "lightgray"; + const fill = useGradients + ? `url(#gradient-${index})` + : `var(--crayon-container-hover-fills)`; return ( @@ -174,12 +312,12 @@ export const PieChartV2 = ({ outerRadius={dimensions.outerRadius} label={false} {...animationConfig} - {...eventHandlers} + {...(legendVariant === "stacked" ? eventHandlers : {})} {...sectorStyle} startAngle={appearance === "semiCircular" ? 0 : 0} endAngle={appearance === "semiCircular" ? 180 : 360} - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} + onMouseEnter={legendVariant === "stacked" ? handleChartMouseEnter : undefined} + onMouseLeave={legendVariant === "stacked" ? handleChartMouseLeave : undefined} > {transformedData.map((entry, index) => { const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); @@ -203,19 +341,19 @@ export const PieChartV2 = ({ nameKey={String(categoryKey)} outerRadius={dimensions.outerRadius} innerRadius={dimensions.innerRadius} - activeIndex={activeIndex ?? undefined} + activeIndex={legendVariant === "stacked" ? (activeIndex ?? undefined) : undefined} {...animationConfig} - {...eventHandlers} + {...(legendVariant === "stacked" ? eventHandlers : {})} {...sectorStyle} startAngle={appearance === "semiCircular" ? 0 : 0} endAngle={appearance === "semiCircular" ? 180 : 360} - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} + onMouseEnter={legendVariant === "stacked" ? handleChartMouseEnter : undefined} + onMouseLeave={legendVariant === "stacked" ? handleChartMouseLeave : undefined} > {transformedData.map((entry, index) => { const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); const config = chartConfig[categoryValue]; - const hoverStyles = getHoverStyles(index, activeIndex); + const hoverStyles = legendVariant === "stacked" ? getHoverStyles(index, activeIndex) : {}; const fill = useGradients ? `url(#gradient-${index})` : config?.color || colors[index]; return ; @@ -224,64 +362,107 @@ export const PieChartV2 = ({ ); }; + const renderLegend = () => { + if (!legend) return null; + + if (legendVariant === "stacked") { + return ( + + ); + } + + // Default legend variant - render directly like AreaChartV2 + return ; + }; + return (
- - - } - /> - - {useGradients && ( - - {createGradientDefinitions( - transformedData, - Object.values(chartConfig) - .map((config) => config.color) - .filter((color): color is string => color !== undefined), - gradientColors, - )} - - )} - - {renderPieCharts()} - - + + + } + /> + + {useGradients && ( + + {createGradientDefinitions( + transformedData, + Object.values(chartConfig) + .map((config) => config.color) + .filter((color): color is string => color !== undefined), + gradientColors, + )} + + )} + + {renderPieCharts()} + + +
+ {legend && legendVariant === "stacked" && ( +
+ {renderLegend()} +
+ )} + {legend && legendVariant === "default" && renderLegend()}
); }; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss index 8cc54cdaf..e91169994 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss @@ -1,10 +1,52 @@ @use "../../../../cssUtils" as cssUtils; +.crayon-pie-chart-container-wrapper { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; + display: flex; + gap: 20px; + + // Default legend variant - always column layout (chart on top, legend at bottom) + &--default-legend { + flex-direction: column; + align-items: center; + gap: 20px; + } + + // Stacked legend variant - responsive layout (side-by-side on desktop, stacked on mobile) + &--stacked-legend { + display: flex; + flex-direction: row; + align-items: center; + gap: 20px; + min-height: 0; + } + + &--stacked-legend-column { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + } +} + .crayon-pie-chart-container { width: 100%; height: 100%; position: relative; overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + min-height: 0; + + // When in row layout with stacked legend, ensure proper sizing + &--with-stacked-legend { + flex: 1; + min-width: 0; + } } .crayon-pie-chart-container-inner { @@ -17,4 +59,59 @@ .crayon-pie-chart { position: relative; + + // Add smooth transitions for hover effects + .recharts-pie-sector { + transition: all 0.2s ease-in-out; + } +} + +.crayon-pie-chart-legend-container { + display: flex; + align-items: flex-start; + justify-content: flex-start; + min-width: 0; + overflow: hidden; + + // Ensure the legend takes available space in row layout + .crayon-stacked-legend { + width: 100%; + min-width: 0; + } +} + +.crayon-pie-chart-legend { + display: flex; + align-items: center; + justify-content: center; + + // Stacked legend variant - responsive positioning + &:not(.crayon-pie-chart-legend--bottom) { + // Mobile layout + @media (max-width: 599px) { + width: 100%; + margin-top: 20px; + } + + // Desktop layout + @media (min-width: 600px) { + width: 100%; + } + } + + // Stacked legend variant styling + .crayon-stacked-legend { + width: 100%; + + // Mobile layout for stacked legend + @media (max-width: 599px) { + width: 100%; + margin-top: 20px; + } + + // Desktop layout for stacked legend + @media (min-width: 600px) { + width: 100%; + } + } } diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index 6789ddf6b..05cf06cf1 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -1,10 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { useEffect, useMemo, useRef, useState } from "react"; import { Card } from "../../../../Card"; -import { DefaultLegend } from "../../../shared/DefaultLegend/DefaultLegend"; -import { StackedLegend } from "../../../shared/StackedLegend/StackedLegend"; -import { LegendItem } from "../../../types"; -import { getDistributedColors, getPalette } from "../../../utils/PalletUtils"; import { PieChartV2, PieChartV2Props } from "../PieChartV2"; const pieChartData = [ @@ -15,12 +10,13 @@ const pieChartData = [ { month: "May", value: 1680 }, { month: "June", value: 2100 }, { month: "July", value: 1950 }, - { month: "August", value: 1820 }, + { month: "Augustajdfoabldlskdbiwbdfjkbkbfjkadbfkadofisodhoisdjg", value: 1820 }, { month: "September", value: 1650 }, { month: "October", value: 1480 }, { month: "November", value: 1350 }, { month: "December", value: 1200 }, ]; + const gradientColors = [ { start: "#FF6B6B", end: "#FF8E8E" }, { start: "#4ECDC4", end: "#6ED7D0" }, @@ -95,7 +91,7 @@ const meta: Meta> = { }, variant: { description: - "The style of the pie chart. 'pie' shows a pie chart, 'donut' shows a donut chart, and 'twoLevel' shows a two-level pie chart.", + "The style of the pie chart. 'pie' shows a pie chart, 'donut' shows a donut chart.", control: "radio", options: ["pie", "donut"], table: { @@ -109,7 +105,7 @@ const meta: Meta> = { control: "radio", options: ["percentage", "number"], table: { - defaultValue: { summary: "percentage" }, + defaultValue: { summary: "number" }, category: "Display", }, }, @@ -122,6 +118,16 @@ const meta: Meta> = { category: "Display", }, }, + legendVariant: { + description: + "The type of legend to display. 'default' shows a horizontal legend at bottom, 'stacked' shows a vertical stacked legend with responsive layout.", + control: "radio", + options: ["default", "stacked"], + table: { + defaultValue: { summary: "default" }, + category: "Display", + }, + }, isAnimationActive: { description: "Whether to animate the chart", control: "boolean", @@ -149,14 +155,39 @@ const meta: Meta> = { category: "Appearance", }, }, + useGradients: { + description: "Whether to use gradient colors for the pie slices", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "Appearance", + }, + }, + height: { + description: "Fixed height of the chart container", + control: { type: "number", min: 200, max: 800 }, + table: { + type: { summary: "number" }, + category: "Layout", + }, + }, + width: { + description: "Fixed width of the chart container", + control: { type: "number", min: 200, max: 800 }, + table: { + type: { summary: "number" }, + category: "Layout", + }, + }, }, } satisfies Meta; export default meta; type Story = StoryObj; -export const Default: Story = { - name: "Default Pie Chart", +export const PieChartV2Demo: Story = { + name: "PieChartV2", args: { data: pieChartData, categoryKey: "month", @@ -165,351 +196,19 @@ export const Default: Story = { variant: "pie", format: "number", legend: true, + legendVariant: "stacked", isAnimationActive: true, appearance: "circular", cornerRadius: 0, paddingAngle: 0, - }, - render: (args) => { - const ResizableExample = () => { - const containerRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(500); - - // Create legend items from the chart data - const legendItems: LegendItem[] = useMemo(() => { - const palette = getPalette(args.theme || "ocean"); - const colors = getDistributedColors(palette, args.data.length); - - return args.data.map((item, index) => ({ - key: String(item[args.categoryKey]), - label: String(item[args.categoryKey]), - color: colors[index] || "#000000", // Fallback color if undefined - })); - }, [args.data, args.categoryKey, args.theme]); - - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - // Subtract padding from the observed width - const paddingX = 40; // 20px padding on each side - const observedWidth = Math.max(200, entry.contentRect.width - paddingX); - setContainerWidth(observedWidth); - } - }); - - resizeObserver.observe(container); - - return () => { - resizeObserver.disconnect(); - }; - }, []); - - return ( - - - - - - - ); - }; - - return ; - }, -}; - -export const Interactive: Story = { - name: "Interactive with Resize", - args: { - ...Default.args, - theme: "vivid", - variant: "donut", - cornerRadius: 5, - paddingAngle: 1, - format: "percentage", - }, - render: (args) => { - const ResizableExample = () => { - const containerRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(500); - - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - // Subtract padding from the observed width - const paddingX = 40; // 20px padding on each side - const observedWidth = Math.max(200, entry.contentRect.width - paddingX); - setContainerWidth(observedWidth); - } - }); - - resizeObserver.observe(container); - - return () => { - resizeObserver.disconnect(); - }; - }, []); - - return ( -
-

- This example demonstrates how the chart and legend adapt to container width. - Drag the bottom-right corner to resize the container and see how the - components adapt in real-time. Current width: {containerWidth}px -

- - - -
- ); - }; - - return ; - }, -}; - -export const DonutChart: Story = { - name: "Donut Chart (Two Level)", - args: { - ...Default.args, - variant: "donut", - theme: "orchid", - useGradients: true, + useGradients: false, gradientColors, - cornerRadius: 5, - paddingAngle: 1, - format: "percentage", + height: undefined, + width: undefined, }, render: (args) => ( - + ), }; - -export const SemiCircular: Story = { - name: "Semi-Circular Chart", - args: { - ...Default.args, - appearance: "semiCircular", - theme: "emerald", - }, - render: (args) => ( - - - - ), -}; - -export const WithCornerRadius: Story = { - name: "Chart with Corner Radius", - args: { - ...Default.args, - cornerRadius: 10, - paddingAngle: 2, - theme: "sunset", - }, - render: (args) => ( - - - - ), -}; - -export const PercentageFormat: Story = { - name: "Percentage Format", - args: { - ...Default.args, - format: "percentage", - theme: "spectrum", - }, - render: (args) => ( - - - - ), -}; - -export const GradientColors: Story = { - name: "Gradient Colors", - args: { - ...Default.args, - theme: "vivid", - variant: "donut", - cornerRadius: 5, - paddingAngle: 1, - format: "percentage", - useGradients: true, - gradientColors: [ - { start: "#FF6B6B", end: "#FF8E8E" }, - { start: "#4ECDC4", end: "#6ED7D0" }, - { start: "#45B7D1", end: "#6BC5DB" }, - { start: "#96CEB4", end: "#B4DCC9" }, - { start: "#FFEEAD", end: "#FFF4C4" }, - { start: "#D4A5A5", end: "#E5BDBD" }, - { start: "#9B59B6", end: "#B07CC7" }, - ], - }, - render: (args) => ( - - - - ), -}; - -export const SingleColorGradients: Story = { - name: "Single Color Gradients", - args: { - ...Default.args, - theme: "vivid", - variant: "donut", - cornerRadius: 5, - paddingAngle: 1, - format: "percentage", - useGradients: true, - gradientColors: [ - { start: "#FF6B6B" }, // Only start color provided - { end: "#4ECDC4" }, // Only end color provided - { start: "#45B7D1" }, // Only start color provided - { end: "#96CEB4" }, // Only end color provided - { start: "#FFEEAD" }, // Only start color provided - { end: "#D4A5A5" }, // Only end color provided - { start: "#9B59B6" }, // Only start color provided - ], - }, - render: (args) => ( - - - - ), -}; - -export const WithStackedLegend: Story = { - name: "With Stacked Legend", - args: { - ...Default.args, - theme: "vivid", - variant: "pie", - cornerRadius: 5, - paddingAngle: 1, - format: "percentage", - }, - render: (args) => { - const ResizableExample = () => { - const containerRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(500); - const [containerHeight, setContainerHeight] = useState(400); - - // Create legend items from the chart data - const legendItems = useMemo(() => { - const palette = getPalette(args.theme || "vivid"); - const colors = getDistributedColors(palette, args.data.length); - - return args.data.map((item, index) => ({ - key: String(item[args.categoryKey]), - label: String(item[args.categoryKey]), - value: Number(item[args.dataKey]), - color: colors[index] || "#000000", - })); - }, [args.data, args.categoryKey, args.dataKey, args.theme]); - - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const paddingX = 40; - const paddingY = 40; - const observedWidth = Math.max(200, entry.contentRect.width - paddingX); - const observedHeight = Math.max(200, entry.contentRect.height - paddingY); - setContainerWidth(observedWidth); - setContainerHeight(observedHeight); - } - }); - - resizeObserver.observe(container); - - return () => { - resizeObserver.disconnect(); - }; - }, []); - - return ( - -
- - - -
- -
-
-
- ); - }; - - return ; - }, -}; diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx index 44f157297..4b8b890bc 100644 --- a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx @@ -11,6 +11,8 @@ interface StackedLegendProps { items: LegendItem[]; onItemHover?: (key: string | null) => void; activeKey?: string | null; + onLegendItemHover?: (index: number | null) => void; + containerWidth?: number; } const formatPercentage = (value: number, total: number): string => { @@ -20,13 +22,21 @@ const formatPercentage = (value: number, total: number): string => { const MAX_VISIBLE_ITEMS = 10; -export const StackedLegend = ({ items, onItemHover, activeKey }: StackedLegendProps) => { - const handleMouseEnter = (key: string) => { +export const StackedLegend = ({ + items, + onItemHover, + activeKey, + onLegendItemHover, + containerWidth, +}: StackedLegendProps) => { + const handleMouseEnter = (key: string, index: number) => { onItemHover?.(key); + onLegendItemHover?.(index); }; const handleMouseLeave = () => { onItemHover?.(null); + onLegendItemHover?.(null); }; // Calculate total for percentage @@ -57,14 +67,19 @@ export const StackedLegend = ({ items, onItemHover, activeKey }: StackedLegendPr })(); return ( -
- {processedItems.map((item) => ( +
+ {processedItems.map((item, index) => (
handleMouseEnter(item.key)} + onMouseEnter={() => handleMouseEnter(item.key, index)} onMouseLeave={handleMouseLeave} >
diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss index 7ba652730..275985cab 100644 --- a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss @@ -1,10 +1,12 @@ @use "../../../../cssUtils" as cssUtils; + .crayon-stacked-legend { width: 100%; display: flex; flex-direction: column; - gap: 10px; - min-width: 300px; + gap: 2px; + min-width: 200px; + overflow: hidden; &__item { width: 100%; @@ -15,15 +17,17 @@ gap: 10px; padding: 4px 8px; border-radius: 4px; - transition: background-color 0.2s ease; + transition: all 0.2s ease-in-out; + cursor: pointer; + height: 36px; &:hover { background-color: rgba(0, 0, 0, 0.05); - cursor: pointer; } &--active { background-color: rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } &-label { @@ -31,8 +35,15 @@ flex-direction: row; align-items: center; gap: 8px; + min-width: 0; + flex: 1; + &-text { color: cssUtils.$primary-text; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; } } @@ -44,27 +55,41 @@ gap: 10px; height: 36px; width: 32px; + flex-shrink: 0; } &-color { width: 10px; height: 10px; border-radius: 2px; - transition: transform 0.2s ease; + transition: all 0.2s ease-in-out; .crayon-stacked-legend__item:hover & { transform: scale(1.2); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + } + + .crayon-stacked-legend__item--active & { + transform: scale(1.3); + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3); } } &-label-text { @include cssUtils.typography(label, large); color: cssUtils.$primary-text; + transition: color 0.2s ease-in-out; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; } &-value { @include cssUtils.typography(label, large); color: cssUtils.$primary-text; + transition: all 0.2s ease-in-out; + flex-shrink: 0; } } } From e28f13254a29fa0e9ec6c3e17f519b0514b085b2 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 20 Jun 2025 16:17:07 +0530 Subject: [PATCH 090/190] feat(charts): Add BarChartV2 and types exports Adds the BarChartV2 and types exports to the Charts/BarCharts index file to make them available for use in the application. feat(charts): Wrap AreaChartV2 with React.memo Wraps the AreaChartV2 component with React.memo to optimize performance and avoid unnecessary re-renders. --- .../components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx | 5 ++++- .../react-ui/src/components/Charts/BarCharts/index.ts | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/index.ts diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index d6d0f48cb..babde719d 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -47,7 +47,7 @@ export interface AreaChartV2Props { const Y_AXIS_WIDTH = 40; // Width of Y-axis chart when shown -export const AreaChartV2 = ({ +const AreaChartV2Component = ({ data, categoryKey, theme = "ocean", @@ -370,3 +370,6 @@ export const AreaChartV2 = ({
); }; + +// Added React.memo for performance optimization to avoid unnecessary re-renders +export const AreaChartV2 = React.memo(AreaChartV2Component) as typeof AreaChartV2Component; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/index.ts b/js/packages/react-ui/src/components/Charts/BarCharts/index.ts new file mode 100644 index 000000000..487277f65 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/index.ts @@ -0,0 +1,2 @@ +export * from "./BarChartV2"; +export * from "./types"; From efb4e1bec31b4660318eede1d3f14d4076052d3f Mon Sep 17 00:00:00 2001 From: i-subham Date: Fri, 20 Jun 2025 18:03:29 +0530 Subject: [PATCH 091/190] feat(PieChartV2): Enhance PieChartV2 and StackedLegend with carousel functionality This commit introduces several improvements to the PieChartV2 and StackedLegend components: - Added a new carousel feature to the StackedLegend, allowing users to scroll through legend items when they exceed the visible area. - Updated the PieChartV2 stories to include an extended dataset for better demonstration of the carousel functionality. - Enhanced SCSS styles for both components to support the new layout and scrolling behavior. These changes aim to improve usability and provide a more interactive experience for users navigating through chart legends. --- .../PieCharts/PieChartV2/pieChartV2.scss | 1 + .../PieChartV2/stories/PieChartV2.stories.tsx | 63 ++++++++- .../shared/StackedLegend/StackedLegend.tsx | 126 ++++++++++++++---- .../shared/StackedLegend/stackedLegend.scss | 36 ++++- 4 files changed, 200 insertions(+), 26 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss index e91169994..782387997 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss @@ -67,6 +67,7 @@ } .crayon-pie-chart-legend-container { + height: 100%; display: flex; align-items: flex-start; justify-content: flex-start; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index 05cf06cf1..644b7a8e5 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -10,13 +10,37 @@ const pieChartData = [ { month: "May", value: 1680 }, { month: "June", value: 2100 }, { month: "July", value: 1950 }, - { month: "Augustajdfoabldlskdbiwbdfjkbkbfjkadbfkadofisodhoisdjg", value: 1820 }, + { month: "August", value: 1820 }, { month: "September", value: 1650 }, { month: "October", value: 1480 }, { month: "November", value: 1350 }, { month: "December", value: 1200 }, ]; +// Extended data for carousel demo +const extendedPieChartData = [ + { month: "January", value: 1250 }, + { month: "February", value: 980 }, + { month: "March", value: 1450 }, + { month: "April", value: 1320 }, + { month: "May", value: 1680 }, + { month: "June", value: 2100 }, + { month: "July", value: 1950 }, + { month: "August", value: 1820 }, + { month: "September", value: 1650 }, + { month: "October", value: 1480 }, + { month: "November", value: 1350 }, + { month: "December", value: 1200 }, + { month: "Q1 Bonus", value: 850 }, + { month: "Q2 Bonus", value: 920 }, + { month: "Q3 Bonus", value: 780 }, + { month: "Q4 Bonus", value: 1100 }, + { month: "Holiday Pay", value: 650 }, + { month: "Overtime", value: 420 }, + { month: "Commission", value: 890 }, + { month: "Incentives", value: 720 }, +]; + const gradientColors = [ { start: "#FF6B6B", end: "#FF8E8E" }, { start: "#4ECDC4", end: "#6ED7D0" }, @@ -207,8 +231,43 @@ export const PieChartV2Demo: Story = { width: undefined, }, render: (args) => ( - + + + + ), +}; + +export const PieChartV2WithCarousel: Story = { + name: "PieChartV2 with Up/Down Carousel", + args: { + data: extendedPieChartData, + categoryKey: "month", + dataKey: "value", + theme: "ocean", + variant: "pie", + format: "number", + legend: true, + legendVariant: "stacked", + isAnimationActive: true, + appearance: "circular", + cornerRadius: 0, + paddingAngle: 0, + useGradients: false, + gradientColors, + height: undefined, + width: undefined, + }, + render: (args) => ( + ), + parameters: { + docs: { + description: { + story: + "This example demonstrates the up/down carousel functionality when there are many legend items. The legend has navigation buttons that appear when content overflows, allowing users to scroll through all items.", + }, + }, + }, }; diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx index 4b8b890bc..e41f17014 100644 --- a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx @@ -1,3 +1,6 @@ +import { ChevronDown, ChevronUp } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { IconButton } from "../../../IconButton"; import "./stackedLegend.scss"; interface LegendItem { @@ -21,6 +24,8 @@ const formatPercentage = (value: number, total: number): string => { }; const MAX_VISIBLE_ITEMS = 10; +const ITEM_HEIGHT = 36; // Height of each legend item +const ITEM_GAP = 2; // Gap between items export const StackedLegend = ({ items, @@ -29,6 +34,11 @@ export const StackedLegend = ({ onLegendItemHover, containerWidth, }: StackedLegendProps) => { + const containerRef = useRef(null); + const listRef = useRef(null); + const [showUpButton, setShowUpButton] = useState(false); + const [showDownButton, setShowDownButton] = useState(false); + const handleMouseEnter = (key: string, index: number) => { onItemHover?.(key); onLegendItemHover?.(index); @@ -39,6 +49,51 @@ export const StackedLegend = ({ onLegendItemHover?.(null); }; + // Check if scrolling is needed + useEffect(() => { + const checkScroll = () => { + if (listRef.current && containerRef.current) { + const { scrollTop, scrollHeight, clientHeight } = listRef.current; + setShowUpButton(scrollTop > 0); + setShowDownButton(scrollTop < scrollHeight - clientHeight - 1); // -1 for rounding errors + } + }; + + // Initial check + checkScroll(); + + // Add event listener for scroll + const currentRef = listRef.current; + if (currentRef) { + currentRef.addEventListener("scroll", checkScroll); + + // Also add resize observer to handle responsive changes + const resizeObserver = new ResizeObserver(checkScroll); + resizeObserver.observe(currentRef); + + return () => { + currentRef.removeEventListener("scroll", checkScroll); + resizeObserver.disconnect(); + }; + } + return () => {}; + }, []); + + // Scroll functions + const scrollUp = () => { + if (listRef.current) { + // Scroll one item up + listRef.current.scrollBy({ top: -(ITEM_HEIGHT + ITEM_GAP), behavior: "smooth" }); + } + }; + + const scrollDown = () => { + if (listRef.current) { + // Scroll one item down + listRef.current.scrollBy({ top: ITEM_HEIGHT + ITEM_GAP, behavior: "smooth" }); + } + }; + // Calculate total for percentage const total = items.reduce((sum, item) => sum + item.value, 0); @@ -61,41 +116,66 @@ export const StackedLegend = ({ key: "others", label: "Others", value: remainingTotal, - color: "#808080", // Gray color for Others + color: "var(--crayon-container-hover-fills)", }, ]; })(); return (
- {processedItems.map((item, index) => ( -
handleMouseEnter(item.key, index)} - onMouseLeave={handleMouseLeave} - > -
-
-
+ {showUpButton && ( + } + variant="secondary" + size="small" + /> + )} + +
+ {processedItems.map((item, index) => ( +
handleMouseEnter(item.key, index)} + onMouseLeave={handleMouseLeave} + > +
+
+
+
+
{item.label}
+
+
+ {formatPercentage(item.value, total)}
-
{item.label}
-
-
- {formatPercentage(item.value, total)}
-
- ))} + ))} +
+ + {showDownButton && ( + } + variant="secondary" + size="small" + /> + )}
); }; diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss index 275985cab..37c5fd41e 100644 --- a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss @@ -1,12 +1,46 @@ @use "../../../../cssUtils" as cssUtils; +.crayon-stacked-legend-container { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + height: 100%; +} + +.crayon-stacked-legend-scroll-button { + position: absolute; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 100; + + &.crayon-stacked-legend-scroll-up { + top: 2px; + } + + &.crayon-stacked-legend-scroll-down { + bottom: 2px; + } +} + .crayon-stacked-legend { width: 100%; display: flex; flex-direction: column; gap: 2px; min-width: 200px; - overflow: hidden; + overflow-y: auto; + height: 100%; + scrollbar-width: none; + -ms-overflow-style: none; + scroll-behavior: smooth; + + &::-webkit-scrollbar { + display: none; + } &__item { width: 100%; From c704b1470623e6bd70d8eabe5e56070bc7014b31 Mon Sep 17 00:00:00 2001 From: i-subham Date: Fri, 20 Jun 2025 18:19:30 +0530 Subject: [PATCH 092/190] feat(PieChartV2): Optimize performance with memoization and enhance layout This commit improves the PieChartV2 component by implementing memoization for various calculations and configurations, reducing unnecessary re-renders. Key changes include: - Memoized string conversions, data transformations, and chart configurations to enhance performance. - Updated event handlers and styles to prevent redundant calculations. - Adjusted the PieChartV2 story layout for better visual representation. These enhancements aim to provide a more efficient and responsive user experience when interacting with the PieChartV2 component. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 300 ++++++++++-------- .../PieChartV2/stories/PieChartV2.stories.tsx | 2 +- 2 files changed, 177 insertions(+), 125 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 091db6d74..26508bd43 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -77,10 +77,14 @@ export const PieChartV2 = ({ const [containerWidth, setContainerWidth] = useState(0); const [containerHeight, setContainerHeight] = useState(0); const [wrapperWidth, setWrapperWidth] = useState(0); - const [isMobile, setIsMobile] = useState(false); const [hoveredLegendKey, setHoveredLegendKey] = useState(null); const { activeIndex, handleMouseEnter, handleMouseLeave } = useChartHover(); + // Memoize string conversions to avoid repeated calls + const categoryKeyString = useMemo(() => String(categoryKey), [categoryKey]); + const dataKeyString = useMemo(() => String(dataKey), [dataKey]); + const formatKey = useMemo(() => format === "percentage" ? "percentage" : dataKeyString, [format, dataKeyString]); + // Use provided dimensions or observed dimensions const effectiveWidth = useMemo(() => { return width ?? containerWidth; @@ -96,16 +100,49 @@ export const PieChartV2 = ({ return Math.max(200, Math.min(size, 800)); // Min 200px, max 800px }, [effectiveWidth, effectiveHeight]); - // Transform data and create configurations - const transformedData = transformDataWithPercentages(data, dataKey); - const chartConfig = createChartConfig(data, categoryKey, theme); - const animationConfig = createAnimationConfig({ isAnimationActive }); - const eventHandlers = createEventHandlers(onMouseEnter, onMouseLeave, onClick); - const sectorStyle = createSectorStyle(cornerRadius, variant === "donut" ? 0.5 : paddingAngle); + // Memoize expensive data transformations and configurations + const transformedData = useMemo(() => + transformDataWithPercentages(data, dataKey), + [data, dataKey] + ); + + const chartConfig = useMemo(() => + createChartConfig(data, categoryKey, theme), + [data, categoryKey, theme] + ); + + const animationConfig = useMemo(() => + createAnimationConfig({ isAnimationActive }), + [isAnimationActive] + ); + + const eventHandlers = useMemo(() => + createEventHandlers(onMouseEnter, onMouseLeave, onClick), + [onMouseEnter, onMouseLeave, onClick] + ); + + const sectorStyle = useMemo(() => + createSectorStyle(cornerRadius, variant === "donut" ? 0.5 : paddingAngle), + [cornerRadius, variant, paddingAngle] + ); // Get color palette and distribute colors - const palette = getPalette(theme); - const colors = getDistributedColors(palette, data.length); + const palette = useMemo(() => getPalette(theme), [theme]); + const colors = useMemo(() => + getDistributedColors(palette, data.length), + [palette, data.length] + ); + + // Memoize gradient definitions + const gradientDefinitions = useMemo(() => { + if (!useGradients) return null; + + const chartColors = Object.values(chartConfig) + .map((config) => config.color) + .filter((color): color is string => color !== undefined); + + return createGradientDefinitions(transformedData, chartColors, gradientColors); + }, [useGradients, chartConfig, transformedData, gradientColors]); // Create legend items for both variants const legendItems = useMemo(() => { @@ -126,6 +163,12 @@ export const PieChartV2 = ({ })); }, [data, categoryKey, colors]); + // Memoize sorted data for legend hover handling + const sortedData = useMemo(() => + [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), + [data, dataKey] + ); + // Handle legend hover const handleLegendHover = useCallback( (key: string | null) => { @@ -144,8 +187,7 @@ export const PieChartV2 = ({ if (index !== null) { // Find the corresponding data item and set it as active - const sortedItems = [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])); - const item = sortedItems[index]; + const item = sortedData[index]; if (item) { const categoryValue = String(item[categoryKey]); setHoveredLegendKey(categoryValue); @@ -160,7 +202,7 @@ export const PieChartV2 = ({ handleMouseLeave(); } }, - [data, categoryKey, dataKey, handleMouseEnter, handleMouseLeave, legendVariant], + [sortedData, categoryKey, data, handleMouseEnter, handleMouseLeave, legendVariant], ); // Enhanced chart hover handlers @@ -212,19 +254,67 @@ export const PieChartV2 = ({ }; }, [legend, legendVariant, wrapperWidth, containerWidth]); - useEffect(() => { - // Check screen size for responsive behavior - const checkScreenSize = () => { - setIsMobile(window.innerWidth < 600); - }; - - checkScreenSize(); - window.addEventListener("resize", checkScreenSize); - - return () => { - window.removeEventListener("resize", checkScreenSize); - }; - }, []); + // Memoize style objects to prevent unnecessary re-renders + const containerStyle = useMemo(() => ({ + width: width ? `${width}px` : "100%", + height: height ? `${height}px` : "100%", + flex: legend && legendVariant === "stacked" && layoutConfig.isRow ? "1" : "none", + }), [width, height, legend, legendVariant, layoutConfig.isRow]); + + const innerContainerStyle = useMemo(() => ({ + width: "100%", + height: "100%", + display: "flex", + alignItems: "center", + justifyContent: "center", + }), []); + + const chartSizeStyle = useMemo(() => ({ + width: chartSize, + height: chartSize, + }), [chartSize]); + + const legendContainerStyle = useMemo(() => ({ + flex: layoutConfig.isRow ? "1" : "none", + width: layoutConfig.isRow ? "auto" : "100%", + }), [layoutConfig.isRow]); + + const rechartsProps = useMemo(() => ({ + width: chartSize, + height: chartSize, + }), [chartSize]); + + // Memoize angle calculations + const startAngle = useMemo(() => appearance === "semiCircular" ? 0 : 0, [appearance]); + const endAngle = useMemo(() => appearance === "semiCircular" ? 180 : 360, [appearance]); + + // Memoize common pie props + const commonPieProps = useMemo(() => ({ + data: transformedData, + dataKey: formatKey, + nameKey: categoryKeyString, + labelLine: false, + label: false, + ...animationConfig, + ...(legendVariant === "stacked" ? eventHandlers : {}), + ...sectorStyle, + startAngle, + endAngle, + onMouseEnter: legendVariant === "stacked" ? handleChartMouseEnter : undefined, + onMouseLeave: legendVariant === "stacked" ? handleChartMouseLeave : undefined, + }), [ + transformedData, + formatKey, + categoryKeyString, + animationConfig, + legendVariant, + eventHandlers, + sectorStyle, + startAngle, + endAngle, + handleChartMouseEnter, + handleChartMouseLeave, + ]); useEffect(() => { // Set up ResizeObserver for wrapper to get overall container width @@ -267,30 +357,18 @@ export const PieChartV2 = ({ }; }, [width, height]); - const renderPieCharts = () => { + // Memoize the renderPieCharts function + const renderPieCharts = useCallback(() => { if (variant === "donut") { return ( <> {/* Inner Pie */} - {transformedData.map((entry, index) => { - const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); - const config = chartConfig[categoryValue]; + {transformedData.map((_, index) => { const hoverStyles = getHoverStyles(index, activeIndex); const fill = useGradients ? `url(#gradient-${index})` @@ -304,20 +382,9 @@ export const PieChartV2 = ({ {/* Outer Pie */} {transformedData.map((entry, index) => { const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); @@ -336,19 +403,10 @@ export const PieChartV2 = ({ return ( {transformedData.map((entry, index) => { const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); @@ -360,9 +418,21 @@ export const PieChartV2 = ({ })} ); - }; - - const renderLegend = () => { + }, [ + variant, + commonPieProps, + dimensions, + transformedData, + categoryKey, + chartConfig, + activeIndex, + useGradients, + legendVariant, + colors, + ]); + + // Memoize the renderLegend function + const renderLegend = useCallback(() => { if (!legend) return null; if (legendVariant === "stacked") { @@ -379,69 +449,57 @@ export const PieChartV2 = ({ // Default legend variant - render directly like AreaChartV2 return ; - }; + }, [ + legend, + legendVariant, + legendItems, + handleLegendHover, + hoveredLegendKey, + handleLegendItemHover, + layoutConfig.isRow, + wrapperWidth, + defaultLegendItems, + containerWidth, + ]); + + // Memoize className calculations + const wrapperClassName = useMemo(() => + clsx("crayon-pie-chart-container-wrapper", className, { + "crayon-pie-chart-container-wrapper--stacked-legend": + legend && legendVariant === "stacked" && layoutConfig.isRow, + "crayon-pie-chart-container-wrapper--stacked-legend-column": + legend && legendVariant === "stacked" && !layoutConfig.isRow, + "crayon-pie-chart-container-wrapper--default-legend": legend && legendVariant === "default", + }), + [className, legend, legendVariant, layoutConfig.isRow] + ); + + const containerClassName = useMemo(() => + clsx("crayon-pie-chart-container", { + "crayon-pie-chart-container--with-stacked-legend": + legend && legendVariant === "stacked" && layoutConfig.isRow, + }), + [legend, legendVariant, layoutConfig.isRow] + ); return ( -
-
-
-
+
+
+
+
} /> - {useGradients && ( + {gradientDefinitions && ( - {createGradientDefinitions( - transformedData, - Object.values(chartConfig) - .map((config) => config.color) - .filter((color): color is string => color !== undefined), - gradientColors, - )} + {gradientDefinitions} )} @@ -452,13 +510,7 @@ export const PieChartV2 = ({
{legend && legendVariant === "stacked" && ( -
+
{renderLegend()}
)} diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index 644b7a8e5..252843b61 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -231,7 +231,7 @@ export const PieChartV2Demo: Story = { width: undefined, }, render: (args) => ( - + ), From 76a220c71cdd157d7195d0822bc50bccb0f38ca1 Mon Sep 17 00:00:00 2001 From: i-subham Date: Fri, 20 Jun 2025 18:23:16 +0530 Subject: [PATCH 093/190] refactor(PieChartV2): Simplify event handling and improve hover logic This commit refines the PieChartV2 component by streamlining event handling for mouse interactions. Key changes include: - Updated comments for clarity regarding legend hover behavior. - Simplified the logic for setting and clearing the hovered legend key. - Enhanced the mapping of transformed data to improve readability and maintainability. These adjustments aim to enhance the overall code quality and maintainability of the PieChartV2 component. --- .../Charts/PieCharts/PieChartV2/PieChartV2.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 26508bd43..b35662eba 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -209,7 +209,7 @@ export const PieChartV2 = ({ const handleChartMouseEnter = useCallback( (data: any, index: number) => { handleMouseEnter(data, index); - // Only set legend hover for stacked legend variant + // Set legend hover for stacked legend variant if (legend && legendVariant === "stacked") { const categoryValue = String(data[categoryKey]); setHoveredLegendKey(categoryValue); @@ -220,7 +220,7 @@ export const PieChartV2 = ({ const handleChartMouseLeave = useCallback(() => { handleMouseLeave(); - // Only clear legend hover for stacked legend variant + // Clear legend hover for stacked legend variant if (legend && legendVariant === "stacked") { setHoveredLegendKey(null); } @@ -296,18 +296,17 @@ export const PieChartV2 = ({ labelLine: false, label: false, ...animationConfig, - ...(legendVariant === "stacked" ? eventHandlers : {}), + ...eventHandlers, ...sectorStyle, startAngle, endAngle, - onMouseEnter: legendVariant === "stacked" ? handleChartMouseEnter : undefined, - onMouseLeave: legendVariant === "stacked" ? handleChartMouseLeave : undefined, + onMouseEnter: handleChartMouseEnter, + onMouseLeave: handleChartMouseLeave, }), [ transformedData, formatKey, categoryKeyString, animationConfig, - legendVariant, eventHandlers, sectorStyle, startAngle, @@ -368,7 +367,7 @@ export const PieChartV2 = ({ innerRadius={dimensions.innerRadius} outerRadius={dimensions.middleRadius} > - {transformedData.map((_, index) => { + {transformedData.map((entry, index) => { const hoverStyles = getHoverStyles(index, activeIndex); const fill = useGradients ? `url(#gradient-${index})` @@ -406,12 +405,12 @@ export const PieChartV2 = ({ {...commonPieProps} outerRadius={dimensions.outerRadius} innerRadius={dimensions.innerRadius} - activeIndex={legendVariant === "stacked" ? (activeIndex ?? undefined) : undefined} + activeIndex={activeIndex ?? undefined} > {transformedData.map((entry, index) => { const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); const config = chartConfig[categoryValue]; - const hoverStyles = legendVariant === "stacked" ? getHoverStyles(index, activeIndex) : {}; + const hoverStyles = getHoverStyles(index, activeIndex); const fill = useGradients ? `url(#gradient-${index})` : config?.color || colors[index]; return ; @@ -427,7 +426,6 @@ export const PieChartV2 = ({ chartConfig, activeIndex, useGradients, - legendVariant, colors, ]); From a2997b9bd32f46025a3ff18c90d1d4e5da98a415 Mon Sep 17 00:00:00 2001 From: i-subham Date: Fri, 20 Jun 2025 19:35:48 +0530 Subject: [PATCH 094/190] refactor(PieChartV2): Wrap StackedLegend in a container for improved layout This commit modifies the PieChartV2 component by wrapping the StackedLegend in a new container div. Key changes include: - Added a div with a class name for styling around the StackedLegend when the legend variant is "stacked". - Simplified the rendering logic for the legend to enhance readability. These adjustments aim to improve the layout and maintainability of the PieChartV2 component. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index b35662eba..eae907e25 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -435,17 +435,17 @@ export const PieChartV2 = ({ if (legendVariant === "stacked") { return ( - +
+ +
); } - - // Default legend variant - render directly like AreaChartV2 return ; }, [ legend, @@ -458,6 +458,7 @@ export const PieChartV2 = ({ wrapperWidth, defaultLegendItems, containerWidth, + legendContainerStyle, ]); // Memoize className calculations @@ -507,12 +508,7 @@ export const PieChartV2 = ({
- {legend && legendVariant === "stacked" && ( -
- {renderLegend()} -
- )} - {legend && legendVariant === "default" && renderLegend()} + {renderLegend()}
); }; From 641c6ad5c4ca5d2627145385f88fd46e3c01c391 Mon Sep 17 00:00:00 2001 From: i-subham Date: Fri, 20 Jun 2025 19:39:11 +0530 Subject: [PATCH 095/190] refactor(PieChartV2): Improve code readability and consistency with memoization This commit enhances the PieChartV2 component by refining the use of memoization for various calculations and configurations. Key changes include: - Standardized formatting for memoized functions to improve readability. - Consolidated multiple lines of code into single lines where appropriate. - Removed unnecessary whitespace to streamline the code. These adjustments aim to enhance the maintainability and clarity of the PieChartV2 component. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 222 ++++++++++-------- 1 file changed, 121 insertions(+), 101 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index eae907e25..dda8bb81d 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -83,7 +83,10 @@ export const PieChartV2 = ({ // Memoize string conversions to avoid repeated calls const categoryKeyString = useMemo(() => String(categoryKey), [categoryKey]); const dataKeyString = useMemo(() => String(dataKey), [dataKey]); - const formatKey = useMemo(() => format === "percentage" ? "percentage" : dataKeyString, [format, dataKeyString]); + const formatKey = useMemo( + () => (format === "percentage" ? "percentage" : dataKeyString), + [format, dataKeyString], + ); // Use provided dimensions or observed dimensions const effectiveWidth = useMemo(() => { @@ -101,46 +104,43 @@ export const PieChartV2 = ({ }, [effectiveWidth, effectiveHeight]); // Memoize expensive data transformations and configurations - const transformedData = useMemo(() => - transformDataWithPercentages(data, dataKey), - [data, dataKey] + const transformedData = useMemo( + () => transformDataWithPercentages(data, dataKey), + [data, dataKey], ); - const chartConfig = useMemo(() => - createChartConfig(data, categoryKey, theme), - [data, categoryKey, theme] + const chartConfig = useMemo( + () => createChartConfig(data, categoryKey, theme), + [data, categoryKey, theme], ); - const animationConfig = useMemo(() => - createAnimationConfig({ isAnimationActive }), - [isAnimationActive] + const animationConfig = useMemo( + () => createAnimationConfig({ isAnimationActive }), + [isAnimationActive], ); - const eventHandlers = useMemo(() => - createEventHandlers(onMouseEnter, onMouseLeave, onClick), - [onMouseEnter, onMouseLeave, onClick] + const eventHandlers = useMemo( + () => createEventHandlers(onMouseEnter, onMouseLeave, onClick), + [onMouseEnter, onMouseLeave, onClick], ); - const sectorStyle = useMemo(() => - createSectorStyle(cornerRadius, variant === "donut" ? 0.5 : paddingAngle), - [cornerRadius, variant, paddingAngle] + const sectorStyle = useMemo( + () => createSectorStyle(cornerRadius, variant === "donut" ? 0.5 : paddingAngle), + [cornerRadius, variant, paddingAngle], ); // Get color palette and distribute colors const palette = useMemo(() => getPalette(theme), [theme]); - const colors = useMemo(() => - getDistributedColors(palette, data.length), - [palette, data.length] - ); + const colors = useMemo(() => getDistributedColors(palette, data.length), [palette, data.length]); // Memoize gradient definitions const gradientDefinitions = useMemo(() => { if (!useGradients) return null; - + const chartColors = Object.values(chartConfig) .map((config) => config.color) .filter((color): color is string => color !== undefined); - + return createGradientDefinitions(transformedData, chartColors, gradientColors); }, [useGradients, chartConfig, transformedData, gradientColors]); @@ -164,9 +164,9 @@ export const PieChartV2 = ({ }, [data, categoryKey, colors]); // Memoize sorted data for legend hover handling - const sortedData = useMemo(() => - [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), - [data, dataKey] + const sortedData = useMemo( + () => [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), + [data, dataKey], ); // Handle legend hover @@ -255,65 +255,83 @@ export const PieChartV2 = ({ }, [legend, legendVariant, wrapperWidth, containerWidth]); // Memoize style objects to prevent unnecessary re-renders - const containerStyle = useMemo(() => ({ - width: width ? `${width}px` : "100%", - height: height ? `${height}px` : "100%", - flex: legend && legendVariant === "stacked" && layoutConfig.isRow ? "1" : "none", - }), [width, height, legend, legendVariant, layoutConfig.isRow]); - - const innerContainerStyle = useMemo(() => ({ - width: "100%", - height: "100%", - display: "flex", - alignItems: "center", - justifyContent: "center", - }), []); - - const chartSizeStyle = useMemo(() => ({ - width: chartSize, - height: chartSize, - }), [chartSize]); - - const legendContainerStyle = useMemo(() => ({ - flex: layoutConfig.isRow ? "1" : "none", - width: layoutConfig.isRow ? "auto" : "100%", - }), [layoutConfig.isRow]); - - const rechartsProps = useMemo(() => ({ - width: chartSize, - height: chartSize, - }), [chartSize]); + const containerStyle = useMemo( + () => ({ + width: width ? `${width}px` : "100%", + height: height ? `${height}px` : "100%", + flex: legend && legendVariant === "stacked" && layoutConfig.isRow ? "1" : "none", + }), + [width, height, legend, legendVariant, layoutConfig.isRow], + ); + + const innerContainerStyle = useMemo( + () => ({ + width: "100%", + height: "100%", + display: "flex", + alignItems: "center", + justifyContent: "center", + }), + [], + ); + + const chartSizeStyle = useMemo( + () => ({ + width: chartSize, + height: chartSize, + }), + [chartSize], + ); + + const legendContainerStyle = useMemo( + () => ({ + flex: layoutConfig.isRow ? "1" : "none", + width: layoutConfig.isRow ? "auto" : "100%", + }), + [layoutConfig.isRow], + ); + + const rechartsProps = useMemo( + () => ({ + width: chartSize, + height: chartSize, + }), + [chartSize], + ); // Memoize angle calculations - const startAngle = useMemo(() => appearance === "semiCircular" ? 0 : 0, [appearance]); - const endAngle = useMemo(() => appearance === "semiCircular" ? 180 : 360, [appearance]); + const startAngle = useMemo(() => (appearance === "semiCircular" ? 0 : 0), [appearance]); + const endAngle = useMemo(() => (appearance === "semiCircular" ? 180 : 360), [appearance]); // Memoize common pie props - const commonPieProps = useMemo(() => ({ - data: transformedData, - dataKey: formatKey, - nameKey: categoryKeyString, - labelLine: false, - label: false, - ...animationConfig, - ...eventHandlers, - ...sectorStyle, - startAngle, - endAngle, - onMouseEnter: handleChartMouseEnter, - onMouseLeave: handleChartMouseLeave, - }), [ - transformedData, - formatKey, - categoryKeyString, - animationConfig, - eventHandlers, - sectorStyle, - startAngle, - endAngle, - handleChartMouseEnter, - handleChartMouseLeave, - ]); + const commonPieProps = useMemo( + () => ({ + data: transformedData, + dataKey: formatKey, + nameKey: categoryKeyString, + labelLine: false, + label: false, + ...animationConfig, + ...eventHandlers, + ...sectorStyle, + startAngle, + endAngle, + onMouseEnter: handleChartMouseEnter, + onMouseLeave: handleChartMouseLeave, + }), + [ + transformedData, + formatKey, + categoryKeyString, + animationConfig, + eventHandlers, + sectorStyle, + startAngle, + endAngle, + handleChartMouseEnter, + handleChartMouseLeave, + ], + ); useEffect(() => { // Set up ResizeObserver for wrapper to get overall container width @@ -462,29 +480,35 @@ export const PieChartV2 = ({ ]); // Memoize className calculations - const wrapperClassName = useMemo(() => - clsx("crayon-pie-chart-container-wrapper", className, { - "crayon-pie-chart-container-wrapper--stacked-legend": - legend && legendVariant === "stacked" && layoutConfig.isRow, - "crayon-pie-chart-container-wrapper--stacked-legend-column": - legend && legendVariant === "stacked" && !layoutConfig.isRow, - "crayon-pie-chart-container-wrapper--default-legend": legend && legendVariant === "default", - }), - [className, legend, legendVariant, layoutConfig.isRow] + const wrapperClassName = useMemo( + () => + clsx("crayon-pie-chart-container-wrapper", className, { + "crayon-pie-chart-container-wrapper--stacked-legend": + legend && legendVariant === "stacked" && layoutConfig.isRow, + "crayon-pie-chart-container-wrapper--stacked-legend-column": + legend && legendVariant === "stacked" && !layoutConfig.isRow, + "crayon-pie-chart-container-wrapper--default-legend": legend && legendVariant === "default", + }), + [className, legend, legendVariant, layoutConfig.isRow], ); - const containerClassName = useMemo(() => - clsx("crayon-pie-chart-container", { - "crayon-pie-chart-container--with-stacked-legend": - legend && legendVariant === "stacked" && layoutConfig.isRow, - }), - [legend, legendVariant, layoutConfig.isRow] + const containerClassName = useMemo( + () => + clsx("crayon-pie-chart-container", { + "crayon-pie-chart-container--with-stacked-legend": + legend && legendVariant === "stacked" && layoutConfig.isRow, + }), + [legend, legendVariant, layoutConfig.isRow], ); return (
-
+
({ content={} /> - {gradientDefinitions && ( - - {gradientDefinitions} - - )} + {gradientDefinitions && {gradientDefinitions}} {renderPieCharts()} From 811e6b39e4818f4e5eea0f0417930a0b5e3d9045 Mon Sep 17 00:00:00 2001 From: i-subham Date: Fri, 20 Jun 2025 20:03:12 +0530 Subject: [PATCH 096/190] refactor(PieChartUtils): Change exported functions to local scope and streamline exports This commit refactors the PieChartUtils module by changing the exported utility functions to local scope. Key changes include: - Updated function declarations from `export const` to `const` for better encapsulation. - Consolidated all utility function exports at the end of the file for improved organization. These adjustments aim to enhance code clarity and maintainability within the PieChartUtils module. --- .../Charts/PieCharts/utils/PieChartUtils.ts | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts index 5436ca267..c1b0fc0a1 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts @@ -49,7 +49,7 @@ export interface AnimationConfig { * @param total - The total value to calculate percentage against * @returns The calculated percentage rounded to 2 decimal places */ -export const calculatePercentage = (value: number, total: number): number => { +const calculatePercentage = (value: number, total: number): number => { if (total === 0) { return 0; } @@ -66,7 +66,7 @@ export const calculatePercentage = (value: number, total: number): number => { * @param variant - The chart variant ('pie' or 'donut') * @returns Object containing outer and inner radius values */ -export const calculateChartDimensions = (width: number, variant: string): ChartDimensions => { +const calculateChartDimensions = (width: number, variant: string): ChartDimensions => { const baseRadiusPercentage = 0.4; // 40% of container width let outerRadius = Math.round(width * baseRadiusPercentage); @@ -87,7 +87,7 @@ export const calculateChartDimensions = (width: number, variant: string): ChartD * @param width - The container width * @returns Object containing outer, middle, and inner radius values */ -export const calculateTwoLevelChartDimensions = (width: number): TwoLevelChartDimensions => { +const calculateTwoLevelChartDimensions = (width: number): TwoLevelChartDimensions => { const baseRadiusPercentage = 0.4; // 40% of container width let outerRadius = Math.round(width * baseRadiusPercentage); @@ -113,7 +113,7 @@ export const calculateTwoLevelChartDimensions = (width: number): TwoLevelChartDi * @param activeIndex - The index of the currently hovered cell * @returns Object containing hover style properties */ -export const getHoverStyles = (index: number, activeIndex: number | null): HoverStyles => { +const getHoverStyles = (index: number, activeIndex: number | null): HoverStyles => { return { opacity: activeIndex === null || activeIndex === index ? 1 : 0.6, stroke: activeIndex === index ? "#fff" : "none", @@ -131,7 +131,7 @@ export const getHoverStyles = (index: number, activeIndex: number | null): Hover * @param dataKey - The key to use for value calculations * @returns Transformed data with added percentage and original value */ -export const transformDataWithPercentages = ( +const transformDataWithPercentages = ( data: T, dataKey: keyof T[number], ) => { @@ -150,7 +150,7 @@ export const transformDataWithPercentages = ( * @param theme - The color theme to use * @returns Chart configuration object */ -export const createChartConfig = ( +const createChartConfig = ( data: T, categoryKey: keyof T[number], theme: string = "ocean", @@ -179,7 +179,7 @@ export const createChartConfig = ( * Custom hook for managing chart hover effects * @returns Object containing hover state and handlers */ -export const useChartHover = (): ChartHoverHook => { +const useChartHover = (): ChartHoverHook => { const [activeIndex, setActiveIndex] = useState(null); const handleMouseEnter = (_: any, index: number) => { @@ -206,7 +206,7 @@ export const useChartHover = (): ChartHoverHook => { * @param config - Animation configuration options * @returns Animation configuration object */ -export const createAnimationConfig = (config: Partial = {}): AnimationConfig => { +const createAnimationConfig = (config: Partial = {}): AnimationConfig => { return { isAnimationActive: config.isAnimationActive ?? true, animationBegin: config.animationBegin ?? 0, @@ -226,7 +226,7 @@ export const createAnimationConfig = (config: Partial = {}): An * @param onClick - Click handler * @returns Object containing event handlers */ -export const createEventHandlers = ( +const createEventHandlers = ( onMouseEnter?: (data: any, index: number) => void, onMouseLeave?: () => void, onClick?: (data: any, index: number) => void, @@ -250,9 +250,26 @@ export const createEventHandlers = ( * @param paddingAngle - Padding angle between sectors * @returns Sector style configuration */ -export const createSectorStyle = (cornerRadius: number = 0, paddingAngle: number = 0) => { +const createSectorStyle = (cornerRadius: number = 0, paddingAngle: number = 0) => { return { cornerRadius, paddingAngle, }; }; + +// ========================================== +// Export all utility functions +// ========================================== + +export { + calculatePercentage, + calculateChartDimensions, + calculateTwoLevelChartDimensions, + getHoverStyles, + transformDataWithPercentages, + createChartConfig, + useChartHover, + createAnimationConfig, + createEventHandlers, + createSectorStyle, +}; From 81b2038715f659d5e55e9bea09c87466d3cabdda Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 20 Jun 2025 21:21:11 +0530 Subject: [PATCH 097/190] feat(BarCharts): Add MiniBarChart and floating tooltip support This commit introduces the following changes: 1. Adds the `MiniBarChart` component to the `BarCharts` module. 2. Adds support for a floating tooltip in the `BarChartV2` component, which can be enabled by setting the `floatingTooltip` prop to `true`. 3. Consolidates the SCSS imports for the `BarCharts` module into a single `index.scss` file. These changes enhance the functionality and flexibility of the `BarCharts` module, allowing for the creation of mini bar charts and providing a more customizable tooltip experience. --- .../Charts/BarCharts/BarChartV2/BarChartV2.tsx | 14 ++++++++++++-- .../Charts/BarCharts/MiniBarChart/index.ts | 1 + .../src/components/Charts/BarCharts/index.scss | 3 +++ .../src/components/Charts/BarCharts/index.ts | 1 + .../react-ui/src/components/Charts/charts.scss | 4 +--- 5 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/index.scss diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 7b6db16da..f1dfcd92f 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -12,7 +12,7 @@ import { keyTransform, } from "../../Charts"; import { cartesianGrid } from "../../cartesianGrid"; -import { DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; +import { CustomTooltipContent, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; import { type LegendItem } from "../../types"; import { getDistributedColors, getPalette, type PaletteName } from "../../utils/PalletUtils"; import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; @@ -44,6 +44,7 @@ export interface BarChartPropsV2 { yAxisLabel?: React.ReactNode; onBarsClick?: (data: any) => void; legend?: boolean; + floatingTooltip?: boolean; className?: string; height?: number; width?: number; @@ -69,6 +70,7 @@ const BarChartV2Component = ({ yAxisLabel, onBarsClick, legend = false, + floatingTooltip = true, className, height, width, @@ -323,7 +325,15 @@ const BarChartV2Component = ({ padding={padding} /> {/* Y-axis is rendered in the separate synchronized chart */} - } content={} /> + {floatingTooltip ? ( + } + content={} + offset={15} + /> + ) : ( + } content={} /> + )} {dataKeys.map((key, index) => { const transformedKey = keyTransform(key); const color = `var(--color-${transformedKey})`; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.ts b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.ts new file mode 100644 index 000000000..bc3e8e9ab --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.ts @@ -0,0 +1 @@ +export * from "./MiniBarChart"; \ No newline at end of file diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/index.scss b/js/packages/react-ui/src/components/Charts/BarCharts/index.scss new file mode 100644 index 000000000..70022ddf4 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/index.scss @@ -0,0 +1,3 @@ +@forward "./BarChartV2/barChar.scss"; +@forward "./MiniBarChart/miniBarChart.scss"; +@forward "./BarChartV2/components/CustomCursor.scss"; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/index.ts b/js/packages/react-ui/src/components/Charts/BarCharts/index.ts index 487277f65..a5de18bca 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/index.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/index.ts @@ -1,2 +1,3 @@ export * from "./BarChartV2"; export * from "./types"; +export * from "./MiniBarChart"; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index feda2104c..2d8fe9037 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -1,9 +1,7 @@ @use "../../cssUtils" as cssUtils; // bar chart css -@forward "./BarCharts/BarChartV2/barChar.scss"; -@forward "./BarCharts/BarChartV2/components/CustomCursor.scss"; -@forward "./BarCharts/MiniBarChart/miniBarChart.scss"; +@forward "./BarCharts/index.scss"; // area chart css @forward "./AreaCharts/index.scss"; From a688a758e36ce9aea2d4e0e9c29387e5998c7df5 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sat, 21 Jun 2025 04:31:29 +0530 Subject: [PATCH 098/190] feat(CustomTooltipContent): improve tooltip handling This change improves the handling of the custom tooltip content in the `CustomTooltipContent` component. The main changes are: - Moved the early return for inactive or empty payload to the end of the component, after all the necessary hooks have been executed. - Simplified the `nestLabel` memoization logic by using the optional chaining operator. - Memoized the rendering of the payload items for better performance. - Added a new `floatingTooltip` prop to the `LineChartV2` component to allow for a floating tooltip. These changes ensure that the tooltip is rendered correctly, even when the payload is empty or the chart is inactive, and improve the overall performance of the tooltip rendering. --- .../Charts/BarCharts/MiniBarChart/index.ts | 2 +- .../src/components/Charts/BarCharts/index.ts | 2 +- .../LineCharts/LineChartV2/LineChartV2.tsx | 10 ++++++++-- .../Charts/PieCharts/utils/PieChartUtils.ts | 10 +++++----- .../PortalTooltip/CustomTooltipContent.tsx | 19 +++++++++++-------- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.ts b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.ts index bc3e8e9ab..171dd71d0 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.ts @@ -1 +1 @@ -export * from "./MiniBarChart"; \ No newline at end of file +export * from "./MiniBarChart"; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/index.ts b/js/packages/react-ui/src/components/Charts/BarCharts/index.ts index a5de18bca..d996fc0ef 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/index.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/index.ts @@ -1,3 +1,3 @@ export * from "./BarChartV2"; -export * from "./types"; export * from "./MiniBarChart"; +export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx index 1e29af1fe..2578bc92d 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx @@ -11,7 +11,7 @@ import { keyTransform, } from "../../Charts"; import { cartesianGrid } from "../../cartesianGrid"; -import { ActiveDot, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; +import { ActiveDot, CustomTooltipContent, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; import { LegendItem } from "../../types"; import { findNearestSnapPosition, @@ -37,6 +37,7 @@ export interface LineChartV2Props { showYAxis?: boolean; xAxisLabel?: React.ReactNode; yAxisLabel?: React.ReactNode; + floatingTooltip?: boolean; className?: string; height?: number; width?: number; @@ -59,6 +60,7 @@ export const LineChartV2 = ({ yAxisLabel, legend = true, className, + floatingTooltip = true, height, width, strokeWidth = 2, @@ -288,7 +290,11 @@ export const LineChartV2 = ({ right: 20, }} /> - } /> + {floatingTooltip ? ( + } offset={15} /> + ) : ( + } /> + )} {dataKeys.map((key) => { const transformedKey = keyTransform(key); const color = `var(--color-${transformedKey})`; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts index c1b0fc0a1..65ae95464 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts @@ -262,14 +262,14 @@ const createSectorStyle = (cornerRadius: number = 0, paddingAngle: number = 0) = // ========================================== export { - calculatePercentage, calculateChartDimensions, + calculatePercentage, calculateTwoLevelChartDimensions, - getHoverStyles, - transformDataWithPercentages, - createChartConfig, - useChartHover, createAnimationConfig, + createChartConfig, createEventHandlers, createSectorStyle, + getHoverStyles, + transformDataWithPercentages, + useChartHover, }; diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx index b6b4884a0..66fd4e08a 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx @@ -45,11 +45,6 @@ export const CustomTooltipContent = memo( const { config, id } = useChart(); - // Early return for inactive or empty payload - if (!active || !payload?.length) { - return null; - } - const tooltipLabel = useMemo(() => { if (hideLabel || !payload?.length) { return null; @@ -77,12 +72,15 @@ export const CustomTooltipContent = memo( }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]); const nestLabel = useMemo( - () => payload.length === 1 && indicator !== DEFAULT_INDICATOR, - [payload.length, indicator], + () => payload?.length === 1 && indicator !== DEFAULT_INDICATOR, + [payload?.length, indicator], ); - // Memoize payload items rendering for better performance const payloadItems = useMemo(() => { + if (!payload?.length) { + return []; + } + return payload.map((item, index) => { const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); @@ -160,6 +158,11 @@ export const CustomTooltipContent = memo( showPercentage, ]); + // Early return for inactive or empty payload - moved after all hooks + if (!active || !payload?.length) { + return null; + } + const tooltipContent = (
{!nestLabel && tooltipLabel} From 0f5f2d6f8981b83cd7c235868b34c49526b79fb6 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sat, 21 Jun 2025 04:43:06 +0530 Subject: [PATCH 099/190] feat(LineCharts): add new line chart components This commit introduces new line chart components to the `react-ui` package: - `LineChartV2`: A more advanced line chart component with additional features - `MiniLineChart`: A compact line chart component for smaller visualizations - `types`: Defines the data types for the new line chart components The changes were made to expand the charting capabilities of the `react-ui` package and provide more flexible and feature-rich line chart options for our users. feat(BarCharts): update BarChartV2 component This commit updates the `BarChartV2` component in the `react-ui` package: - Renames the `BarChartData` type to `BarChartV2Data` to differentiate it from the previous `BarChartData` type - Updates the `BarChartPropsV2` interface to use the new `BarChartV2Data` type These changes were made to improve the clarity and consistency of the API for the `BarChartV2` component. feat(Charts): add new chart types to the index This commit adds new chart types to the `Charts` index in the `react-ui` package: - `AreaCharts` - `BarCharts` - `LineCharts` This change was made to make it easier for users to access and use the new chart types introduced in this commit. --- .../components/Charts/BarCharts/BarChartV2/BarChartV2.tsx | 6 +++--- .../react-ui/src/components/Charts/BarCharts/types/index.ts | 2 +- .../src/components/Charts/LineCharts/MiniLineChart/index.ts | 1 + .../react-ui/src/components/Charts/LineCharts/index.scss | 1 + .../react-ui/src/components/Charts/LineCharts/index.ts | 3 +++ js/packages/react-ui/src/components/Charts/charts.scss | 2 +- js/packages/react-ui/src/components/Charts/index.ts | 5 +++++ 7 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/index.scss create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/index.ts diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index f1dfcd92f..99453675b 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -17,7 +17,7 @@ import { type LegendItem } from "../../types"; import { getDistributedColors, getPalette, type PaletteName } from "../../utils/PalletUtils"; import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; import { getYAxisTickFormatter } from "../../utils/styleUtils"; -import { BarChartData, BarChartVariant } from "../types"; +import { BarChartV2Data, BarChartVariant } from "../types"; import { BAR_WIDTH, findNearestSnapPosition, @@ -30,7 +30,7 @@ import { import { SimpleCursor } from "./components/CustomCursor"; import { LineInBarShape } from "./components/LineInBarShape"; -export interface BarChartPropsV2 { +export interface BarChartPropsV2 { data: T; categoryKey: keyof T[number]; theme?: PaletteName; @@ -56,7 +56,7 @@ const BAR_CATEGORY_GAP = "20%"; // Gap between categories const BAR_INTERNAL_LINE_WIDTH = 1; const BAR_RADIUS = 4; -const BarChartV2Component = ({ +const BarChartV2Component = ({ data, categoryKey, theme = "ocean", diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts index d2db3c40c..f67c7c85d 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts @@ -1,5 +1,5 @@ export type BarChartVariant = "grouped" | "stacked"; -export type BarChartData = Array>; +export type BarChartV2Data = Array>; export type MiniBarChartData = Array | Array<{ value: number; label?: string }>; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts new file mode 100644 index 000000000..86eccce92 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts @@ -0,0 +1 @@ +export * from "./MiniLineChart"; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/index.scss b/js/packages/react-ui/src/components/Charts/LineCharts/index.scss new file mode 100644 index 000000000..2b9416604 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/LineCharts/index.scss @@ -0,0 +1 @@ +@forward "./LineChartV2/lineChartV2.scss"; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/index.ts b/js/packages/react-ui/src/components/Charts/LineCharts/index.ts new file mode 100644 index 000000000..41237b841 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/LineCharts/index.ts @@ -0,0 +1,3 @@ +export * from "./LineChartV2"; +export * from "./MiniLineChart"; +export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index 2d8fe9037..911b45806 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -7,7 +7,7 @@ @forward "./AreaCharts/index.scss"; // line chart css -@forward "./LineCharts/LineChartV2/lineChartV2.scss"; +@forward "./LineCharts/index.scss"; // shared components css @forward "./shared/XAxisTick/xAxisTick.scss"; diff --git a/js/packages/react-ui/src/components/Charts/index.ts b/js/packages/react-ui/src/components/Charts/index.ts index 5dd6c7966..1b1e3d785 100644 --- a/js/packages/react-ui/src/components/Charts/index.ts +++ b/js/packages/react-ui/src/components/Charts/index.ts @@ -4,3 +4,8 @@ export * from "./LineChart"; export * from "./PieChart"; export * from "./RadarChart"; export * from "./RadialChart"; + +// New charts +export * from "./AreaCharts"; +export * from "./BarCharts"; +export * from "./LineCharts"; From 69b3df4cc1e3535bb77d093d26c2252c0bbbd26a Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sat, 21 Jun 2025 05:20:57 +0530 Subject: [PATCH 100/190] chore: update pnpm-lock.yaml to reflect dependency version changes This commit updates the pnpm-lock.yaml file to modify the versions of several dependencies, including upgrading recharts to version 2.15.3 and updating Storybook addons to version 8.6.14. These changes ensure that the project uses the latest compatible versions of the specified packages. --- js/pnpm-lock.yaml | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index 05f236eb2..e382cd0b2 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -172,10 +172,8 @@ importers: specifier: ^15.6.1 version: 15.6.1(react@19.1.0) recharts: - - specifier: ^2.15.1 + specifier: ^2.15.3 version: 2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - rehype-katex: specifier: ^7.0.1 version: 7.0.1 @@ -206,10 +204,7 @@ importers: version: 8.6.14(@types/react@19.1.8)(storybook@8.6.14(prettier@3.5.3)) '@storybook/addon-interactions': specifier: ^8.5.3 - - version: 8.5.3(storybook@8.5.3(prettier@3.4.2)) - - + version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@storybook/addon-styling-webpack': specifier: ^1.0.1 version: 1.0.1(storybook@8.6.14(prettier@3.5.3))(webpack@5.99.9(esbuild@0.25.5)) @@ -1555,15 +1550,8 @@ packages: peerDependencies: storybook: ^8.6.14 - - '@storybook/addon-onboarding@8.6.14': - resolution: {integrity: sha512-bHdHiGJFigVcSzMIsNLHY5IODZHr+nKwyz5/QOZLMkLcGH2IaUbOJfm4RyGOaTTPsUtAKbdsVXNEG3Otf+qO9A==} - peerDependencies: - storybook: ^8.6.14 - '@storybook/addon-outline@8.6.14': resolution: {integrity: sha512-CW857JvN6OxGWElqjlzJO2S69DHf+xO3WsEfT5mT3ZtIjmsvRDukdWfDU9bIYUFyA2lFvYjncBGjbK+I91XR7w==} - peerDependencies: storybook: ^8.6.14 @@ -5117,6 +5105,7 @@ snapshots: storybook: 8.6.14(prettier@3.5.3) tiny-invariant: 1.3.3 + '@storybook/addon-outline@8.6.14(storybook@8.6.14(prettier@3.5.3))': dependencies: '@storybook/global': 5.0.0 storybook: 8.6.14(prettier@3.5.3) @@ -7395,9 +7384,7 @@ snapshots: dependencies: decimal.js-light: 2.5.1 - recharts@2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: clsx: 2.1.1 eventemitter3: 4.0.7 From bd9d6db6ba609b298dc6be9035d8f5ebef9b7e54 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sat, 21 Jun 2025 05:24:06 +0530 Subject: [PATCH 101/190] feat(pieChart.stories): remove unnecessary div Removes an unnecessary `
` element from the `PieChart` component in the stories file. This simplifies the component structure and improves the overall readability of the code. --- .../src/components/Charts/PieChart/stories/pieChart.stories.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/PieChart/stories/pieChart.stories.tsx b/js/packages/react-ui/src/components/Charts/PieChart/stories/pieChart.stories.tsx index 9893cde8e..1ada8fffa 100644 --- a/js/packages/react-ui/src/components/Charts/PieChart/stories/pieChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieChart/stories/pieChart.stories.tsx @@ -133,7 +133,6 @@ export const PieChartStory: Story = { render: (args: any) => ( - ), From 4e835fe50f5c6191eb0fa4096ebbf929e318e07d Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sat, 21 Jun 2025 18:34:27 +0530 Subject: [PATCH 102/190] feat(charts): improve MiniPieChart component The changes made in this commit include: - Removed the `label` prop from the `MiniPieChart` component as it was not being used. - Updated the `render` function to use the `args` parameter as `any` type instead of a specific type. These changes improve the overall quality and maintainability of the `MiniPieChart` component. --- js/packages/react-ui/package.json | 2 +- .../stories/areaChartV2.stories.tsx | 20 +++++++++---------- .../stories/MiniAreaChart.stories.tsx | 10 +++++----- .../BarChartV2/stories/barChartV2.stories.tsx | 14 ++++++------- .../stories/MiniBarChart.stories.tsx | 6 +++--- .../stories/lineChartV2.stories.tsx | 18 ++++++++--------- .../stories/MiniLineChart.stories.tsx | 4 ++-- .../stories/MiniPieChart.stories.tsx | 11 +--------- .../PieChartCard/PieChartCard.stories.tsx | 2 +- .../PieChartV2/stories/PieChartV2.stories.tsx | 4 ++-- .../stories/TwoLevelPieChart.stories.tsx | 18 ++++++++--------- .../stories/DefaultLegend.stories.tsx | 8 ++++---- js/pnpm-lock.yaml | 10 +++++----- 13 files changed, 59 insertions(+), 68 deletions(-) diff --git a/js/packages/react-ui/package.json b/js/packages/react-ui/package.json index 6d90b4259..251eb5e0e 100644 --- a/js/packages/react-ui/package.json +++ b/js/packages/react-ui/package.json @@ -73,7 +73,7 @@ "react-day-picker": "^9.5.1", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^15.6.1", - "recharts": "^2.15.3", + "recharts": "^2.15.4", "rehype-katex": "^7.0.1", "remark-breaks": "^4.0.0", "remark-emoji": "^5.0.1", diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx index 8b115b497..3427f67a6 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -781,7 +781,7 @@ export const BigLabelsStory: Story = { isAnimationActive: true, showYAxis: true, }, - render: (args) => ( + render: (args: any) => ( @@ -808,7 +808,7 @@ export const DenseTimelineStory: Story = { isAnimationActive: true, showYAxis: true, }, - render: (args) => ( + render: (args: any) => ( @@ -835,7 +835,7 @@ export const CompanyNamesStory: Story = { isAnimationActive: true, showYAxis: true, }, - render: (args) => ( + render: (args: any) => ( @@ -862,7 +862,7 @@ export const CountryDataStory: Story = { isAnimationActive: true, showYAxis: true, }, - render: (args) => ( + render: (args: any) => ( @@ -889,7 +889,7 @@ export const MixedLengthsStory: Story = { isAnimationActive: true, showYAxis: true, }, - render: (args) => ( + render: (args: any) => ( @@ -916,7 +916,7 @@ export const EdgeCasesStory: Story = { isAnimationActive: true, showYAxis: true, }, - render: (args) => ( + render: (args: any) => ( @@ -943,7 +943,7 @@ export const MinimalDataStory: Story = { isAnimationActive: true, showYAxis: true, }, - render: (args) => ( + render: (args: any) => ( @@ -970,7 +970,7 @@ export const ExpandCollapseMarketingStory: Story = { isAnimationActive: true, showYAxis: true, }, - render: (args) => ( + render: (args: any) => ( @@ -997,7 +997,7 @@ export const ResponsiveWidthStory: Story = { isAnimationActive: true, showYAxis: true, }, - render: (args) => ( + render: (args: any) => (

@@ -1050,7 +1050,7 @@ export const FloatingTooltipStory: Story = { xAxisLabel: "Time Period", yAxisLabel: "Values", }, - render: (args) => ( + render: (args: any) => (
( + render: (args: any) => (

Daily Activity

@@ -170,7 +170,7 @@ export const LabeledData: Story = { isAnimationActive: true, size: "100%", }, - render: (args: MiniAreaChartProps) => ( + render: (args: any) => (

Monthly Revenue

@@ -211,7 +211,7 @@ export const WithoutGradient: Story = { isAnimationActive: true, size: "100%", }, - render: (args: MiniAreaChartProps) => ( + render: (args: any) => (

Solid Fill Area

@@ -277,7 +277,7 @@ export const ResponsiveData: Story = { isAnimationActive: true, size: "100%", }, - render: (args) => ( + render: (args: any) => (

Small (200px)

@@ -386,7 +386,7 @@ export const CustomColor: Story = { useGradient: true, size: 200, }, - render: (args) => ( + render: (args: any) => ( ( + render: (args: any) => ( @@ -802,7 +802,7 @@ export const LargeDataStory: Story = { showYAxis: true, legend: true, }, - render: (args) => ( + render: (args: any) => ( @@ -822,7 +822,7 @@ export const WeeklyDataStory: Story = { showYAxis: true, legend: true, }, - render: (args) => ( + render: (args: any) => ( @@ -842,7 +842,7 @@ export const BigNumbersStory: Story = { showYAxis: true, legend: true, }, - render: (args) => ( + render: (args: any) => ( @@ -862,7 +862,7 @@ export const EdgeCaseStory: Story = { showYAxis: true, legend: true, }, - render: (args) => ( + render: (args: any) => ( @@ -882,7 +882,7 @@ export const NumberRangesStory: Story = { showYAxis: true, legend: true, }, - render: (args) => ( + render: (args: any) => ( @@ -902,7 +902,7 @@ export const ExpandCollapseMarketingStory: Story = { showYAxis: true, legend: true, }, - render: (args) => ( + render: (args: any) => ( diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx index 967e6d2a0..3a1cb46a5 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx @@ -108,7 +108,7 @@ export const SimpleNumberArray: Story = { isAnimationActive: true, size: "100%", }, - render: (args: MiniBarChartProps) => ( + render: (args: any) => (

Monthly Sales Data @@ -144,7 +144,7 @@ export const LabeledData: Story = { isAnimationActive: true, size: 200, }, - render: (args: MiniBarChartProps) => ( + render: (args: any) => (

Monthly Revenue

@@ -183,7 +183,7 @@ export const SmallSize: Story = { isAnimationActive: true, size: 120, }, - render: (args: MiniBarChartProps) => ( + render: (args: any) => (

Weekly Stats

diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx index 7d99807fc..e59a912c8 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx @@ -430,7 +430,7 @@ const categoryKeys = { }; // 🔥 ACTIVE DATA - For backward compatibility -const lineChartData = dataVariations.default; +const lineChartV2Data = dataVariations.default; const icons = { desktop: Monitor, @@ -445,7 +445,7 @@ const icons = { sessions: TabletSmartphone, } as const; -const meta: Meta> = { +const meta: Meta> = { title: "Components/Charts/LineCharts/LineChartV2", component: LineChartV2, parameters: { @@ -534,7 +534,7 @@ type Story = StoryObj; export const LineChartV2Story: Story = { name: "🎛️ Data Switcher - Line Chart V2", args: { - data: lineChartData, + data: lineChartV2Data, categoryKey: "month", theme: "ocean", variant: "natural", @@ -667,7 +667,7 @@ export const BigLabelsStory: Story = { showYAxis: true, strokeWidth: 3, }, - render: (args) => ( + render: (args: any) => ( @@ -687,7 +687,7 @@ export const DenseTimelineStory: Story = { showYAxis: true, strokeWidth: 2, }, - render: (args) => ( + render: (args: any) => ( @@ -713,7 +713,7 @@ export const StrokeCustomizationStory: Story = { legend: true, showYAxis: true, }, - render: (args) => ( + render: (args: any) => (

Thick Lines (strokeWidth: 4)

@@ -749,7 +749,7 @@ export const VariantComparisonStory: Story = { showYAxis: true, strokeWidth: 3, }, - render: (args) => ( + render: (args: any) => (

Linear Variant

@@ -796,7 +796,7 @@ export const ExpandCollapseMarketingStory: Story = { showYAxis: true, strokeWidth: 2, }, - render: (args) => ( + render: (args: any) => ( @@ -824,7 +824,7 @@ export const ResponsiveWidthStory: Story = { showYAxis: true, strokeWidth: 2, }, - render: (args) => ( + render: (args: any) => (

diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx index 8e64c71db..0349a435c 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx @@ -119,7 +119,7 @@ export const SimpleNumberArray: Story = { isAnimationActive: true, size: "100%", }, - render: (args: MiniLineChartProps) => ( + render: (args: any) => (

Daily Activity

@@ -155,7 +155,7 @@ export const LabeledData: Story = { isAnimationActive: true, size: "100%", }, - render: (args: MiniLineChartProps) => ( + render: (args: any) => (

Monthly Revenue

diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/stories/MiniPieChart.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/stories/MiniPieChart.stories.tsx index e8a364153..05bb32e34 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/stories/MiniPieChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/stories/MiniPieChart.stories.tsx @@ -93,15 +93,6 @@ const meta: Meta> = { category: "Display", }, }, - label: { - description: "Whether to display the data point labels above each point on the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, isAnimationActive: { description: "Whether to animate the chart", control: "boolean", @@ -130,7 +121,7 @@ export const PieChartStory: Story = { label: true, isAnimationActive: true, }, - render: (args) => ( + render: (args: any) => ( diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.stories.tsx index e30a23bd0..b2c7db215 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.stories.tsx @@ -130,7 +130,7 @@ export const PieChartStory: Story = { label: false, isAnimationActive: true, }, - render: (args) => ( + render: (args: any) => ( diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index 252843b61..6414f4fa7 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -230,7 +230,7 @@ export const PieChartV2Demo: Story = { height: undefined, width: undefined, }, - render: (args) => ( + render: (args: any) => ( @@ -257,7 +257,7 @@ export const PieChartV2WithCarousel: Story = { height: undefined, width: undefined, }, - render: (args) => ( + render: (args: any) => ( diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx index 47a53f0b2..12884f5de 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx @@ -166,7 +166,7 @@ export const Default: Story = { paddingAngle: 0.5, useGradients: false, }, - render: (args) => { + render: (args: any) => { const ResizableExample = () => { const containerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(500); @@ -176,7 +176,7 @@ export const Default: Story = { const palette = getPalette(args.theme || "ocean"); const colors = getDistributedColors(palette, args.data.length); - return args.data.map((item, index) => ({ + return args.data.map((item: any, index: any) => ({ key: String(item[args.categoryKey]), label: String(item[args.categoryKey]), color: colors[index] || "#000000", @@ -235,7 +235,7 @@ export const Interactive: Story = { paddingAngle: 1, format: "percentage", }, - render: (args) => { + render: (args: any) => { const ResizableExample = () => { const containerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(500); @@ -294,7 +294,7 @@ export const SemiCircular: Story = { appearance: "semiCircular", theme: "emerald", }, - render: (args) => ( + render: (args: any) => ( @@ -309,7 +309,7 @@ export const WithCornerRadius: Story = { paddingAngle: 2, theme: "sunset", }, - render: (args) => ( + render: (args: any) => ( @@ -323,7 +323,7 @@ export const PercentageFormat: Story = { format: "percentage", theme: "spectrum", }, - render: (args) => ( + render: (args: any) => ( @@ -341,7 +341,7 @@ export const GradientColors: Story = { useGradients: true, gradientColors, }, - render: (args) => ( + render: (args: any) => ( @@ -357,7 +357,7 @@ export const WithStackedLegend: Story = { paddingAngle: 1, format: "percentage", }, - render: (args) => { + render: (args: any) => { const ResizableExample = () => { const containerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(500); @@ -368,7 +368,7 @@ export const WithStackedLegend: Story = { const palette = getPalette(args.theme || "vivid"); const colors = getDistributedColors(palette, args.data.length); - return args.data.map((item, index) => ({ + return args.data.map((item: any, index: any) => ({ key: String(item[args.categoryKey]), label: String(item[args.categoryKey]), value: Number(item[args.dataKey]), diff --git a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx index 990846259..bbf62ac9c 100644 --- a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx @@ -127,7 +127,7 @@ export const Interactive: Story = { xAxisLabel: "Monthly Data", yAxisLabel: "Performance Metrics", }, - render: (args) => { + render: (args: any) => { const DynamicLegendExample = () => { const containerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(500); @@ -185,7 +185,7 @@ export const ExpandCollapseDemo: Story = { xAxisLabel: "Marketing Channels", yAxisLabel: "Performance Metrics", }, - render: (args) => ( + render: (args: any) => (

🔄 Expand/Collapse Functionality Demo @@ -228,7 +228,7 @@ export const ResponsiveExpandCollapse: Story = { xAxisLabel: "Marketing Channels", yAxisLabel: "Performance Metrics", }, - render: (args) => ( + render: (args: any) => (

📐 Responsive Expand/Collapse Behavior @@ -326,7 +326,7 @@ export const DefaultBehavior: Story = { xAxisLabel: "Monthly Data", yAxisLabel: "Performance Metrics", }, - render: (args) => ( + render: (args: any) => (

This demonstrates the default behavior when no containerWidth is provided. All legend items diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index e382cd0b2..34977c713 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -172,8 +172,8 @@ importers: specifier: ^15.6.1 version: 15.6.1(react@19.1.0) recharts: - specifier: ^2.15.3 - version: 2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^2.15.4 + version: 2.15.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) rehype-katex: specifier: ^7.0.1 version: 7.0.1 @@ -3506,8 +3506,8 @@ packages: recharts-scale@0.4.5: resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} - recharts@2.15.3: - resolution: {integrity: sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==} + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} engines: {node: '>=14'} peerDependencies: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -7384,7 +7384,7 @@ snapshots: dependencies: decimal.js-light: 2.5.1 - recharts@2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + recharts@2.15.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: clsx: 2.1.1 eventemitter3: 4.0.7 From 0ce1353b7c7dbd14ce70cbf7a0b3697ea654a14c Mon Sep 17 00:00:00 2001 From: i-subham Date: Sat, 21 Jun 2025 18:38:43 +0530 Subject: [PATCH 103/190] refactor(charts): clean up PieChart components and stories This commit includes several changes to improve the structure and readability of the PieChart components: - Removed unused imports and props from `MiniAreaChart`, `MiniBarChart`, and `MiniLineChart` story files. - Deleted the `PieChartCard` component and its associated stories, as they were no longer needed. - Cleaned up the formatting in the `MiniBarChart` story for better consistency. These changes enhance the maintainability of the chart components and streamline the codebase. --- .../stories/MiniAreaChart.stories.tsx | 2 +- .../stories/MiniBarChart.stories.tsx | 2 +- .../stories/MiniLineChart.stories.tsx | 2 +- .../PieChartCard/PieChartCard.stories.tsx | 173 ------- .../PieCharts/PieChartCard/PieChartCard.tsx | 220 --------- .../Charts/PieCharts/PieChartCard/index.ts | 1 - .../TwoLevelPieChart/TwoLevelPieChart.scss | 20 - .../TwoLevelPieChart/TwoLevelPieChart.tsx | 239 --------- .../PieCharts/TwoLevelPieChart/index.ts | 1 - .../stories/TwoLevelPieChart.stories.tsx | 452 ------------------ 10 files changed, 3 insertions(+), 1109 deletions(-) delete mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.stories.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.scss delete mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx index 819e1511b..af06c6b2f 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Card } from "../../../../Card"; -import { MiniAreaChart, MiniAreaChartProps } from "../MiniAreaChart"; +import { MiniAreaChart } from "../MiniAreaChart"; // Simple array of numbers for 1D area chart const simpleAreaChartData = [ diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx index 3a1cb46a5..ffbe54030 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx @@ -183,7 +183,7 @@ export const SmallSize: Story = { isAnimationActive: true, size: 120, }, - render: (args: any) => ( + render: (args: any) => (

Weekly Stats

diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx index 0349a435c..59a188bd4 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Card } from "../../../../Card"; -import { MiniLineChart, MiniLineChartProps } from "../MiniLineChart"; +import { MiniLineChart } from "../MiniLineChart"; // Simple array of numbers for 1D line chart const simpleLineChartData = [ diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.stories.tsx deleted file mode 100644 index b2c7db215..000000000 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.stories.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { Card } from "../../../Card"; -import { PieChartCard, PieChartCardProps } from "./PieChartCard"; - -const pieChartCardData = [ - { month: "January", value: 4250 }, - { month: "February", value: 3820 }, - { month: "March", value: 4680 }, - { month: "April", value: 4120 }, - { month: "May", value: 5340 }, - { month: "June", value: 6250 }, - { month: "July", value: 5890 }, -]; - -const meta: Meta> = { - title: "Components/Charts/PieCharts/PieChartCard", - component: PieChartCard, - parameters: { - layout: "centered", - docs: { - description: { - component: "```tsx\nimport { PieChart } from '@crayon-ui/react-ui/Charts/PieChart';\n```", - }, - }, - }, - tags: ["!dev", "autodocs"], - - argTypes: { - data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", - control: false, - table: { - type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", - }, - }, - categoryKey: { - description: - "The key from your data object to be used as the segment labels (e.g., 'month', 'category', 'name')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - dataKey: { - description: - "The key from your data object to be used as the values that determine the slice sizes (e.g., 'value', 'count', 'amount')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", - control: "select", - options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], - table: { - defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - variant: { - description: - "The style of the pie chart. 'pie' shows a pie chart, while 'donut' shows a donut chart.", - control: "radio", - options: ["pie", "donut"], - table: { - defaultValue: { summary: "pie" }, - category: "Appearance", - }, - }, - format: { - description: - "The format of the data. 'percentage' shows the data as a percentage, while 'number' shows the data as a number.", - control: "radio", - options: ["percentage", "number"], - table: { - defaultValue: { summary: "percentage" }, - category: "Display", - }, - }, - legend: { - description: "Whether to display the legend", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - label: { - description: "Whether to display the data point labels above each point on the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - isAnimationActive: { - description: "Whether to animate the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const PieChartStory: Story = { - name: "Pie Chart", - args: { - data: pieChartCardData, - categoryKey: "month", - dataKey: "value", - theme: "ocean", - variant: "pie", - format: "number", - legend: false, - label: false, - isAnimationActive: true, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` - const pieChartData = [ - { month: "January", value: 400 }, - { month: "February", value: 300 }, - { month: "March", value: 300 }, - { month: "April", value: 400 }, - { month: "May", value: 300 }, - { month: "June", value: 300 }, - { month: "July", value: 300 }, -]; - - - - - `, - }, - }, - }, -}; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.tsx deleted file mode 100644 index 882a4253a..000000000 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/PieChartCard.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import clsx from "clsx"; -import { debounce } from "lodash-es"; -import { useEffect, useRef, useState } from "react"; -import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; -import { useLayoutContext } from "../../../../context/LayoutContext"; -import { - ChartConfig, - ChartContainer, - ChartLegend, - ChartLegendContent, - ChartTooltip, - ChartTooltipContent, -} from "../../Charts"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; - -export type PieChartCardData = Array>; - -export interface PieChartCardProps { - data: T; - categoryKey: keyof T[number]; - dataKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - variant?: "pie" | "donut"; - format?: "percentage" | "number"; - legend?: boolean; - label?: boolean; - isAnimationActive?: boolean; -} - -const layoutMap: Record = { - mobile: "crayon-pie-chart-container-mobile", - fullscreen: "crayon-pie-chart-container-fullscreen", - tray: "crayon-pie-chart-container-tray", - copilot: "crayon-pie-chart-container-copilot", -}; - -// Helper function to calculate percentage -const calculatePercentage = (value: number, total: number): number => { - if (total === 0) { - return 0; - } - return Number(((value / total) * 100).toFixed(2)); -}; - -export const PieChartCard = ({ - data, - categoryKey, - dataKey, - theme = "ocean", - variant = "pie", - format = "number", - legend = false, - label = true, - isAnimationActive = true, -}: PieChartCardProps) => { - const { layout } = useLayoutContext(); - const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(120); - const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(0); - const containerRef = useRef(null); - - // Calculate dynamic radius based on layout - useEffect(() => { - if (!containerRef.current) return; - - const resizeObserver = new ResizeObserver( - debounce((entries: any) => { - const { width } = entries[0].contentRect; - - // Calculate outer radius - let newOuterRadius = 120; // default - if (layout === "mobile") { - newOuterRadius = label ? (width > 300 ? 85 : 75) : width > 300 ? 95 : 80; - } else if (layout === "fullscreen") { - newOuterRadius = 120; - } else if (layout === "tray" || layout === "copilot") { - newOuterRadius = 90; - } - - // Calculate inner radius for donut - let newInnerRadius = 0; - if (variant === "donut") { - if (layout === "mobile") { - newInnerRadius = label ? (width > 300 ? 50 : 30) : width > 300 ? 60 : 50; - } else { - newInnerRadius = 60; - } - } - - setCalculatedOuterRadius(newOuterRadius); - setCalculatedInnerRadius(newInnerRadius); - }, 100), - ); - - resizeObserver.observe(containerRef.current); - return () => resizeObserver.disconnect(); - }, [layout, label, variant]); - - // Calculate total for percentage calculations - const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); - - // Transform data with percentages - const transformedData = data.map((item) => ({ - ...item, - percentage: calculatePercentage(Number(item[dataKey as string]), total), - originalValue: item[dataKey as string], - })) as (T[number] & { percentage: number; originalValue: string | number })[]; - - // Get color palette and distribute colors - const palette = getPalette(theme); - const colors = getDistributedColors(palette, data.length); - - // Create chart configuration - const chartConfig = data.reduce( - (config, item, index) => ({ - ...config, - [String(item[categoryKey])]: { - label: String(item[categoryKey as string]), - color: colors[index], - }, - }), - {}, - ); - - // Custom label renderer - const renderCustomLabel = ({ payload, cx, cy, x, y, textAnchor, dominantBaseline }: any) => { - if (payload.percentage <= 10) return null; - const displayValue = format === "percentage" ? payload.percentage : payload[dataKey]; - const formattedValue = - String(displayValue).length > 7 ? `${String(displayValue).slice(0, 7)}...` : displayValue; - - return ( - - - {formattedValue} - {format === "percentage" ? "%" : ""} - - - ); - }; - - return ( -
- - - } - /> - {legend && ( - } - /> - )} - - {Object.entries(chartConfig).map(([key, config]) => ( - - ))} - - - -
- {transformedData.map((item, index) => ( -
-
-
-
{item[categoryKey]}
-
- {format === "percentage" ? `${item.percentage}%` : item.originalValue} -
-
-
- ))} -
-
- ); -}; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/index.ts b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/index.ts deleted file mode 100644 index 28fb2bbc9..000000000 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartCard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./PieChartCard"; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.scss b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.scss deleted file mode 100644 index 8cc54cdaf..000000000 --- a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.scss +++ /dev/null @@ -1,20 +0,0 @@ -@use "../../../../cssUtils" as cssUtils; - -.crayon-pie-chart-container { - width: 100%; - height: 100%; - position: relative; - overflow: hidden; -} - -.crayon-pie-chart-container-inner { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -.crayon-pie-chart { - position: relative; -} diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx deleted file mode 100644 index e6e770d9e..000000000 --- a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/TwoLevelPieChart.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import clsx from "clsx"; -import { debounce } from "lodash-es"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; -import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts"; -import { createGradientDefinitions } from "../components/PieChartRenderers"; -import { - PieChartData, - calculateTwoLevelChartDimensions, - createAnimationConfig, - createChartConfig, - createEventHandlers, - createSectorStyle, - getHoverStyles, - transformDataWithPercentages, - useChartHover, -} from "../utils/PieChartUtils"; - -export type TwoLevelPieChartData = PieChartData; - -interface GradientColor { - start?: string; - end?: string; -} - -export interface TwoLevelPieChartProps { - data: T; - categoryKey: keyof T[number]; - dataKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - format?: "percentage" | "number"; - appearance?: "circular" | "semiCircular"; - legend?: boolean; - isAnimationActive?: boolean; - cornerRadius?: number; - paddingAngle?: number; - useGradients?: boolean; - gradientColors?: GradientColor[]; - onMouseEnter?: (data: any, index: number) => void; - onMouseLeave?: () => void; - onClick?: (data: any, index: number) => void; - height?: number; - width?: number; - className?: string; -} - -export const TwoLevelPieChart = ({ - data, - categoryKey, - dataKey, - theme = "ocean", - format = "number", - appearance = "circular", - legend = true, - isAnimationActive = true, - cornerRadius = 0, - paddingAngle = 0.5, - useGradients = false, - gradientColors, - onMouseEnter, - onMouseLeave, - onClick, - className, - height, - width, -}: TwoLevelPieChartProps) => { - const chartContainerRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(0); - const [containerHeight, setContainerHeight] = useState(0); - const { activeIndex, handleMouseEnter, handleMouseLeave } = useChartHover(); - - // Use provided dimensions or observed dimensions - const effectiveWidth = useMemo(() => { - return width ?? containerWidth; - }, [width, containerWidth]); - - const effectiveHeight = useMemo(() => { - return height ?? containerHeight; - }, [height, containerHeight]); - - // Calculate chart dimensions based on the smaller dimension - const chartSize = useMemo(() => { - const size = Math.min(effectiveWidth, effectiveHeight); - return Math.max(200, Math.min(size, 800)); // Min 200px, max 800px - }, [effectiveWidth, effectiveHeight]); - - // Transform data and create configurations - const transformedData = transformDataWithPercentages(data, dataKey); - const chartConfig = createChartConfig(data, categoryKey, theme); - const animationConfig = createAnimationConfig({ isAnimationActive }); - const eventHandlers = createEventHandlers(onMouseEnter, onMouseLeave, onClick); - const sectorStyle = createSectorStyle(cornerRadius, paddingAngle); - - // Calculate dynamic radius - const { outerRadius, middleRadius, innerRadius } = useMemo(() => { - return calculateTwoLevelChartDimensions(chartSize); - }, [chartSize]); - - useEffect(() => { - // Only set up ResizeObserver if dimensions are not provided - if ((width && height) || !chartContainerRef.current) { - return () => {}; - } - - const resizeObserver = new ResizeObserver( - debounce((entries) => { - for (const entry of entries) { - const newWidth = entry.contentRect.width; - const newHeight = entry.contentRect.height; - setContainerWidth(newWidth); - setContainerHeight(newHeight); - } - }, 100), - ); - - resizeObserver.observe(chartContainerRef.current); - - return () => { - resizeObserver.disconnect(); - }; - }, [width, height]); - - return ( -
-
-
- - - } - /> - - {useGradients && ( - - {createGradientDefinitions( - transformedData, - Object.values(chartConfig) - .map((config) => config.color) - .filter((color): color is string => color !== undefined), - gradientColors, - )} - - )} - - {/* Inner Pie */} - - {transformedData.map((entry, index) => { - const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); - const config = chartConfig[categoryValue]; - const hoverStyles = getHoverStyles(index, activeIndex); - const fill = useGradients ? `url(#gradient-${index})` : "lightgray"; - - return ( - - ); - })} - - - {/* Outer Pie */} - - {transformedData.map((entry, index) => { - const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); - const config = chartConfig[categoryValue]; - const hoverStyles = getHoverStyles(index, activeIndex); - const fill = useGradients ? `url(#gradient-${index})` : config?.color; - - return ( - - ); - })} - - - -
-
-
- ); -}; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/index.ts b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/index.ts deleted file mode 100644 index e2f5aa607..000000000 --- a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./TwoLevelPieChart"; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx deleted file mode 100644 index 12884f5de..000000000 --- a/js/packages/react-ui/src/components/Charts/PieCharts/TwoLevelPieChart/stories/TwoLevelPieChart.stories.tsx +++ /dev/null @@ -1,452 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { Card } from "../../../../Card"; -import { DefaultLegend } from "../../../shared/DefaultLegend/DefaultLegend"; -import { StackedLegend } from "../../../shared/StackedLegend/StackedLegend"; -import { LegendItem } from "../../../types"; -import { getDistributedColors, getPalette } from "../../../utils/PalletUtils"; -import { TwoLevelPieChart, TwoLevelPieChartProps } from "../TwoLevelPieChart"; - -const pieChartData = [ - { month: "January", value: 4250 }, - { month: "February", value: 3820 }, - { month: "March", value: 4680 }, - { month: "April", value: 4120 }, - { month: "May", value: 5340 }, - { month: "June", value: 6250 }, - { month: "July", value: 5890 }, -]; - -const gradientColors = [ - { start: "#FF6B6B", end: "#FF8E8E" }, - { start: "#4ECDC4", end: "#6ED7D0" }, - { start: "#45B7D1", end: "#6BC5DB" }, - { start: "#96CEB4", end: "#B4DCC9" }, - { start: "#FFEEAD", end: "#FFF4C4" }, - { start: "#D4A5A5", end: "#E5BDBD" }, - { start: "#9B59B6", end: "#B07CC7" }, -]; - -const meta: Meta> = { - title: "Components/Charts/PieCharts/TwoLevelPieChart", - component: TwoLevelPieChart, - parameters: { - layout: "centered", - docs: { - description: { - component: - "```tsx\nimport { TwoLevelPieChart } from '@crayon-ui/react-ui/Charts/PieCharts/TwoLevelPieChart';\n```", - }, - }, - }, - tags: ["!dev", "autodocs"], - - argTypes: { - data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", - control: false, - table: { - type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", - }, - }, - categoryKey: { - description: - "The key from your data object to be used as the segment labels (e.g., 'month', 'category', 'name')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - dataKey: { - description: - "The key from your data object to be used as the values that determine the slice sizes (e.g., 'value', 'count', 'amount')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", - control: "select", - options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], - table: { - defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - appearance: { - description: - "The appearance of the chart. 'circular' shows a full circle, while 'semiCircular' shows a half circle.", - control: "radio", - options: ["circular", "semiCircular"], - table: { - defaultValue: { summary: "circular" }, - category: "Appearance", - }, - }, - format: { - description: - "The format of the data. 'percentage' shows the data as a percentage, while 'number' shows the data as a number.", - control: "radio", - options: ["percentage", "number"], - table: { - defaultValue: { summary: "number" }, - category: "Display", - }, - }, - legend: { - description: "Whether to display the legend", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - isAnimationActive: { - description: "Whether to animate the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - cornerRadius: { - description: "The radius of the corners of each segment", - control: { type: "number", min: 0, max: 20, step: 1 }, - table: { - type: { summary: "number" }, - defaultValue: { summary: "0" }, - category: "Appearance", - }, - }, - paddingAngle: { - description: "The angle of padding between segments", - control: { type: "number", min: 0, max: 10, step: 0.5 }, - table: { - type: { summary: "number" }, - defaultValue: { summary: "0.5" }, - category: "Appearance", - }, - }, - useGradients: { - description: "Whether to use gradient fills for segments", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "false" }, - category: "Appearance", - }, - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - name: "Default Two Level Pie Chart", - args: { - data: pieChartData, - categoryKey: "month", - dataKey: "value", - theme: "ocean", - appearance: "circular", - format: "number", - legend: true, - isAnimationActive: true, - cornerRadius: 0, - paddingAngle: 0.5, - useGradients: false, - }, - render: (args: any) => { - const ResizableExample = () => { - const containerRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(500); - - // Create legend items from the chart data - const legendItems: LegendItem[] = useMemo(() => { - const palette = getPalette(args.theme || "ocean"); - const colors = getDistributedColors(palette, args.data.length); - - return args.data.map((item: any, index: any) => ({ - key: String(item[args.categoryKey]), - label: String(item[args.categoryKey]), - color: colors[index] || "#000000", - })); - }, [args.data, args.categoryKey, args.theme]); - - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const paddingX = 40; - const observedWidth = Math.max(200, entry.contentRect.width - paddingX); - setContainerWidth(observedWidth); - } - }); - - resizeObserver.observe(container); - - return () => { - resizeObserver.disconnect(); - }; - }, []); - - return ( - - - - - - - ); - }; - - return ; - }, -}; - -export const Interactive: Story = { - name: "Interactive with Resize", - args: { - ...Default.args, - theme: "vivid", - cornerRadius: 5, - paddingAngle: 1, - format: "percentage", - }, - render: (args: any) => { - const ResizableExample = () => { - const containerRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(500); - - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const paddingX = 40; - const observedWidth = Math.max(200, entry.contentRect.width - paddingX); - setContainerWidth(observedWidth); - } - }); - - resizeObserver.observe(container); - - return () => { - resizeObserver.disconnect(); - }; - }, []); - - return ( -
-

- This example demonstrates how the chart adapts to container width. - Drag the bottom-right corner to resize the container and see how the - components adapt in real-time. Current width: {containerWidth}px -

- - - -
- ); - }; - - return ; - }, -}; - -export const SemiCircular: Story = { - name: "Semi-Circular Chart", - args: { - ...Default.args, - appearance: "semiCircular", - theme: "emerald", - }, - render: (args: any) => ( - - - - ), -}; - -export const WithCornerRadius: Story = { - name: "Chart with Corner Radius", - args: { - ...Default.args, - cornerRadius: 10, - paddingAngle: 2, - theme: "sunset", - }, - render: (args: any) => ( - - - - ), -}; - -export const PercentageFormat: Story = { - name: "Percentage Format", - args: { - ...Default.args, - format: "percentage", - theme: "spectrum", - }, - render: (args: any) => ( - - - - ), -}; - -export const GradientColors: Story = { - name: "Gradient Colors", - args: { - ...Default.args, - theme: "vivid", - cornerRadius: 5, - paddingAngle: 1, - format: "percentage", - useGradients: true, - gradientColors, - }, - render: (args: any) => ( - - - - ), -}; - -export const WithStackedLegend: Story = { - name: "With Stacked Legend", - args: { - ...Default.args, - theme: "vivid", - cornerRadius: 5, - paddingAngle: 1, - format: "percentage", - }, - render: (args: any) => { - const ResizableExample = () => { - const containerRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(500); - const [containerHeight, setContainerHeight] = useState(400); - - // Create legend items from the chart data - const legendItems = useMemo(() => { - const palette = getPalette(args.theme || "vivid"); - const colors = getDistributedColors(palette, args.data.length); - - return args.data.map((item: any, index: any) => ({ - key: String(item[args.categoryKey]), - label: String(item[args.categoryKey]), - value: Number(item[args.dataKey]), - color: colors[index] || "#000000", - })); - }, [args.data, args.categoryKey, args.dataKey, args.theme]); - - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const paddingX = 40; - const paddingY = 40; - const observedWidth = Math.max(200, entry.contentRect.width - paddingX); - const observedHeight = Math.max(200, entry.contentRect.height - paddingY); - setContainerWidth(observedWidth); - setContainerHeight(observedHeight); - } - }); - - resizeObserver.observe(container); - - return () => { - resizeObserver.disconnect(); - }; - }, []); - - return ( - -
- - - -
- -
-
-
- ); - }; - - return ; - }, -}; From c5027a72252023bb93bb0c691ad44b2210c9e0c7 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sat, 21 Jun 2025 19:06:26 +0530 Subject: [PATCH 104/190] feat(charts): Add dependencies for chart components Adds dependencies for the following chart components: - MiniBarChart - LineChartV2 - BarChartV2 - MiniLineChart This ensures that the chart components can be properly imported and used in the application. --- .../src/components/Charts/BarCharts/BarChartV2/dependencies.ts | 2 ++ .../components/Charts/BarCharts/MiniBarChart/dependencies.ts | 2 ++ .../components/Charts/LineCharts/LineChartV2/dependencies.ts | 2 ++ .../components/Charts/LineCharts/MiniLineChart/dependencies.ts | 2 ++ 4 files changed, 8 insertions(+) create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/dependencies.ts create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/dependencies.ts create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/dependencies.ts create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/dependencies.ts diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/dependencies.ts b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/dependencies.ts new file mode 100644 index 000000000..2d4a6054b --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/dependencies.ts @@ -0,0 +1,2 @@ +const dependencies = ["BarChartV2", "IconButton"]; +export default dependencies; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/dependencies.ts b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/dependencies.ts new file mode 100644 index 000000000..ea021d98a --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/dependencies.ts @@ -0,0 +1,2 @@ +const dependencies = ["MiniBarChart"]; +export default dependencies; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/dependencies.ts b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/dependencies.ts new file mode 100644 index 000000000..62eb75321 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/dependencies.ts @@ -0,0 +1,2 @@ +const dependencies = ["LineChartV2", "IconButton"]; +export default dependencies; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/dependencies.ts b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/dependencies.ts new file mode 100644 index 000000000..573358a14 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/dependencies.ts @@ -0,0 +1,2 @@ +const dependencies = ["MiniLineChart"]; +export default dependencies; From 96b32d6c5eccbc65c3a2a0bd64b1eaa60b6f0d0c Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sat, 21 Jun 2025 19:56:03 +0530 Subject: [PATCH 105/190] feat(PieChartRenderers): handle invalid color format Adds a console warning to handle cases where the provided color format is invalid. This ensures that the component can gracefully handle unexpected color inputs and provide more informative error messages. fix(MiniPieChart): remove unused layout context Removes the unused `useLayoutContext` hook from the `MiniPieChart` component, as it is no longer needed. --- .../components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx | 2 -- .../Charts/PieCharts/components/PieChartRenderers.tsx | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx index 40a01685c..1428aefb7 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx @@ -1,7 +1,6 @@ import { debounce } from "lodash-es"; import { useEffect, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; -import { useLayoutContext } from "../../../../context/LayoutContext"; import { ChartContainer } from "../../Charts"; import { PieChartData, @@ -32,7 +31,6 @@ export const MiniPieChart = ({ format = "number", isAnimationActive = true, }: MiniPieChartProps) => { - const { layout } = useLayoutContext(); const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(120); const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(0); const containerRef = useRef(null); diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx index 2ed24f566..6e0ce2cd5 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx @@ -73,6 +73,7 @@ export const adjustColorBrightness = (hex: string, percent: number): string => { return "#" + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1); } catch (error) { console.warn("Invalid color format:", hex); + console.warn(error); return hex; } }; From 2101a6dad9ec156e550e43eff189ab6b1a6e309f Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sun, 22 Jun 2025 06:40:23 +0530 Subject: [PATCH 106/190] feat(ProgressBar): Implement progress bar component This commit introduces a new `ProgressBar` component in the `react-ui` package. The progress bar is a visual representation of data, where each segment represents a portion of the total. The key changes include: - Implement the `ProgressBar` component in `ProgressBar.tsx` with support for: - Displaying multiple segments with customizable colors - Animating the progress bar on initial render - Applying a theme to the progress bar - Providing a `className` prop for custom styling - Add the necessary styles for the progress bar in `progressBar.scss` - Update the `charts.scss` file to include the progress bar styles - Add a new styling rule in `.cursor/rules/styling-rule.mdc` to use the `cssUtils.scss` file - Export the `ProgressBar` component in the `index.ts` file - Add Storybook stories for the `ProgressBar` component in `ProgressBar.stories.tsx` to showcase different usage scenarios These changes provide a reusable progress bar component that can be used across the application, allowing for better data visualisation and user experience. --- .cursor/rules/styling-rule.mdc | 6 + .../Charts/ProgressBar/ProgressBar.tsx | 77 +++++++ .../components/Charts/ProgressBar/index.ts | 2 + .../Charts/ProgressBar/progressBar.scss | 55 +++++ .../stories/ProgressBar.stories.tsx | 194 ++++++++++++++++++ .../src/components/Charts/charts.scss | 5 +- .../react-ui/src/components/Charts/index.ts | 1 + 7 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 .cursor/rules/styling-rule.mdc create mode 100644 js/packages/react-ui/src/components/Charts/ProgressBar/ProgressBar.tsx create mode 100644 js/packages/react-ui/src/components/Charts/ProgressBar/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/ProgressBar/progressBar.scss create mode 100644 js/packages/react-ui/src/components/Charts/ProgressBar/stories/ProgressBar.stories.tsx diff --git a/.cursor/rules/styling-rule.mdc b/.cursor/rules/styling-rule.mdc new file mode 100644 index 000000000..151554f1c --- /dev/null +++ b/.cursor/rules/styling-rule.mdc @@ -0,0 +1,6 @@ +--- +description: +globs: .scss +alwaysApply: false +--- +for styling component use cssUtils.scss \ No newline at end of file diff --git a/js/packages/react-ui/src/components/Charts/ProgressBar/ProgressBar.tsx b/js/packages/react-ui/src/components/Charts/ProgressBar/ProgressBar.tsx new file mode 100644 index 000000000..4b44cf715 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/ProgressBar/ProgressBar.tsx @@ -0,0 +1,77 @@ +import clsx from "clsx"; +import { useMemo } from "react"; +import { getDistributedColors, getPalette, PaletteName } from "../utils/PalletUtils"; + +export type ProgressBarData = Array; + +export interface ProgressBarProps { + data: ProgressBarData; + theme?: PaletteName; + className?: string; + animated?: boolean; +} + +export const ProgressBar = ({ + data, + theme = "ocean", + className, + animated = true, +}: ProgressBarProps) => { + // Calculate percentages + const segments = useMemo(() => { + if (!data || data.length === 0) { + return []; + } + + const total = data.reduce((acc, value) => acc + value, 0); + + return data.map((value, index) => ({ + value, + index, + percentage: total > 0 ? (value / total) * 100 : 0, + })); + }, [data]); + + // Get theme colors for each segment + const colors = useMemo(() => { + const palette = getPalette(theme); + return getDistributedColors(palette, Math.max(segments.length, 1)); + }, [theme, segments.length]); + + // Custom styles for dynamic values + const customStyles = useMemo(() => { + const styles: any = {}; + + return styles; + }, []); + + // Segmented progress bar + return ( +
+
+ {segments.map((segment, index) => { + const isFirst = index === 0; + const isLast = index === segments.length - 1; + const isSingle = segments.length === 1; + + return ( +
+ ); + })} +
+
+ ); +}; diff --git a/js/packages/react-ui/src/components/Charts/ProgressBar/index.ts b/js/packages/react-ui/src/components/Charts/ProgressBar/index.ts new file mode 100644 index 000000000..19abec72a --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/ProgressBar/index.ts @@ -0,0 +1,2 @@ +export { ProgressBar } from "./ProgressBar"; +export type { ProgressBarData, ProgressBarProps } from "./ProgressBar"; diff --git a/js/packages/react-ui/src/components/Charts/ProgressBar/progressBar.scss b/js/packages/react-ui/src/components/Charts/ProgressBar/progressBar.scss new file mode 100644 index 000000000..09a661f11 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/ProgressBar/progressBar.scss @@ -0,0 +1,55 @@ +@use "../../../cssUtils.scss" as cssUtils; + +.crayon-progress-bar-container { + width: 100%; + font-family: inherit; +} + +.crayon-progress-bar-track { + width: 100%; + height: 24px; + background-color: cssUtils.$bg-fill; + border-radius: cssUtils.$rounded-s; + overflow: hidden; + position: relative; + box-shadow: cssUtils.$shadow-s; + display: flex; +} + +.crayon-progress-bar-segment { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + position: relative; + min-width: 2px; + box-shadow: cssUtils.$shadow-s; + + /* Add subtle border between segments */ + &:not(:last-child) { + border-right: 1px solid rgba(255, 255, 255, 0.2); + } + + &--first { + border-radius: cssUtils.$rounded-s 0 0 cssUtils.$rounded-s; + } + + &--last { + border-radius: 0 cssUtils.$rounded-s cssUtils.$rounded-s 0; + } + + &--single { + border-radius: cssUtils.$rounded-s; + } +} + +.crayon-progress-bar-animated { + transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Dark mode adjustments */ +[data-theme="dark"] { + .crayon-progress-bar-segment:not(:last-child) { + border-right-color: rgba(0, 0, 0, 0.2); + } +} diff --git a/js/packages/react-ui/src/components/Charts/ProgressBar/stories/ProgressBar.stories.tsx b/js/packages/react-ui/src/components/Charts/ProgressBar/stories/ProgressBar.stories.tsx new file mode 100644 index 000000000..6b99711a6 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/ProgressBar/stories/ProgressBar.stories.tsx @@ -0,0 +1,194 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Card } from "../../../Card"; +import { ProgressBar, ProgressBarProps } from "../ProgressBar"; + +// Sample data variations - simplified to numbers only +const progressData = { + simple: [75], + multiple: [25, 30, 20], // Individual segments: 25, 30, 20 + segmented: [40, 25, 20, 15], // Four segments with different sizes + manySegments: [15, 12, 18, 10, 8, 14, 11, 12], // Eight segments + lowProgress: [15], + highProgress: [95], + completed: [100], +}; + +const meta: Meta = { + title: "Components/Charts/ProgressBar", + component: ProgressBar, + parameters: { + layout: "centered", + docs: { + description: { + component: + "```tsx\nimport { ProgressBar } from '@crayon-ui/react-ui/Charts/ProgressBar';\n```", + }, + }, + }, + tags: ["!dev", "autodocs"], + argTypes: { + data: { + description: + "An array of numbers where each number represents an individual segment with its own color.", + control: false, + table: { + type: { summary: "Array" }, + defaultValue: { summary: "[]" }, + category: "Data", + }, + }, + theme: { + description: + "The color palette theme for the progress bar. Each theme provides different colors.", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + animated: { + description: "Whether to animate the progress bar fill", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + name: "Default Progress Bar", + args: { + data: progressData.multiple, + theme: "ocean", + animated: true, + }, + render: (args: any) => ( + +

+ Task Completion (3 Segments) +

+ +
+ ), +}; + +export const FourSegments: Story = { + name: "Four Segments", + args: { + data: progressData.segmented, + theme: "spectrum", + animated: true, + }, + render: (args: any) => ( + +

+ Task Distribution (4 Segments) +

+ +
+ ), +}; + +export const ThreeSegments: Story = { + name: "Three Segments", + args: { + data: progressData.multiple, + theme: "emerald", + animated: true, + }, + render: (args: any) => ( + +

+ Simple Progress Distribution +

+ +
+ ), +}; + +export const ManySegments: Story = { + name: "Many Segments (8 Colors)", + args: { + data: progressData.manySegments, + theme: "vivid", + animated: true, + }, + render: (args: any) => ( + +

+ Multi-Segment Progress +

+ +
+ Hover over segments to see values +
+
+ ), +}; + +export const SmallProgress: Story = { + name: "Small Progress", + args: { + data: progressData.lowProgress, + theme: "sunset", + animated: true, + }, + render: (args: any) => ( + +

Getting Started

+ +
+ ), +}; + +export const HighProgress: Story = { + name: "High Progress", + args: { + data: progressData.highProgress, + theme: "vivid", + animated: true, + }, + render: (args: any) => ( + +

Nearly Complete

+ +
+ ), +}; + +export const Completed: Story = { + name: "Completed", + args: { + data: progressData.completed, + theme: "emerald", + animated: true, + }, + render: (args: any) => ( + +

Task Complete!

+ +
+ ), +}; + +export const NoAnimation: Story = { + name: "No Animation", + args: { + data: [45], + theme: "spectrum", + animated: false, + }, + render: (args: any) => ( + +

Static Progress

+ +
+ ), +}; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index 911b45806..f875eaa8e 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -1,4 +1,4 @@ -@use "../../cssUtils" as cssUtils; +@use "../../cssUtils.scss" as cssUtils; // bar chart css @forward "./BarCharts/index.scss"; @@ -9,6 +9,9 @@ // line chart css @forward "./LineCharts/index.scss"; +// progress bar css +@forward "./ProgressBar/progressBar.scss"; + // shared components css @forward "./shared/XAxisTick/xAxisTick.scss"; @forward "./shared/DefaultLegend/defaultLegend.scss"; diff --git a/js/packages/react-ui/src/components/Charts/index.ts b/js/packages/react-ui/src/components/Charts/index.ts index 1b1e3d785..e3355a6a7 100644 --- a/js/packages/react-ui/src/components/Charts/index.ts +++ b/js/packages/react-ui/src/components/Charts/index.ts @@ -9,3 +9,4 @@ export * from "./RadialChart"; export * from "./AreaCharts"; export * from "./BarCharts"; export * from "./LineCharts"; +export * from "./ProgressBar"; From 92834dd8685787f975ae9a5b4f3957eb3afe0351 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 23 Jun 2025 02:22:04 +0530 Subject: [PATCH 107/190] feat(AreaChartV2): Improve gradient rendering performance Refactor the gradient rendering logic to use a separate `defs` block instead of rendering it inline with each `Area` component. This improves the performance of the chart by reducing the number of DOM elements that need to be rendered and updated. --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index babde719d..1331cf3ee 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -302,26 +302,31 @@ const AreaChartV2Component = ({ const transformedKey = keyTransform(key); const color = `var(--color-${transformedKey})`; return ( - - - - - - - - } - dot={false} - isAnimationActive={isAnimationActive} - /> - + + + + + + + ); + })} + + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + return ( + } + dot={false} + isAnimationActive={isAnimationActive} + /> ); })} From 3fd855510530506c0fd44bef7d784a92c91383a0 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 23 Jun 2025 02:40:38 +0530 Subject: [PATCH 108/190] feat(ProgressBarChart): Rename ProgressBar to ProgressBarChart This change renames the `ProgressBar` component to `ProgressBarChart` to better reflect its purpose as a chart component. The corresponding files and imports have been updated accordingly. --- .../ProgressBar.tsx => ProgressBarChart/ProgressBarChart.tsx} | 0 .../Charts/{ProgressBar => ProgressBarChart}/index.ts | 4 ++-- .../progressBarChart.scss} | 0 .../stories/ProgressBarChart.stories.tsx} | 4 ++-- js/packages/react-ui/src/components/Charts/charts.scss | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename js/packages/react-ui/src/components/Charts/{ProgressBar/ProgressBar.tsx => ProgressBarChart/ProgressBarChart.tsx} (100%) rename js/packages/react-ui/src/components/Charts/{ProgressBar => ProgressBarChart}/index.ts (50%) rename js/packages/react-ui/src/components/Charts/{ProgressBar/progressBar.scss => ProgressBarChart/progressBarChart.scss} (100%) rename js/packages/react-ui/src/components/Charts/{ProgressBar/stories/ProgressBar.stories.tsx => ProgressBarChart/stories/ProgressBarChart.stories.tsx} (97%) diff --git a/js/packages/react-ui/src/components/Charts/ProgressBar/ProgressBar.tsx b/js/packages/react-ui/src/components/Charts/ProgressBarChart/ProgressBarChart.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/ProgressBar/ProgressBar.tsx rename to js/packages/react-ui/src/components/Charts/ProgressBarChart/ProgressBarChart.tsx diff --git a/js/packages/react-ui/src/components/Charts/ProgressBar/index.ts b/js/packages/react-ui/src/components/Charts/ProgressBarChart/index.ts similarity index 50% rename from js/packages/react-ui/src/components/Charts/ProgressBar/index.ts rename to js/packages/react-ui/src/components/Charts/ProgressBarChart/index.ts index 19abec72a..0117bd9b5 100644 --- a/js/packages/react-ui/src/components/Charts/ProgressBar/index.ts +++ b/js/packages/react-ui/src/components/Charts/ProgressBarChart/index.ts @@ -1,2 +1,2 @@ -export { ProgressBar } from "./ProgressBar"; -export type { ProgressBarData, ProgressBarProps } from "./ProgressBar"; +export { ProgressBar } from "./ProgressBarChart"; +export type { ProgressBarData, ProgressBarProps } from "./ProgressBarChart"; diff --git a/js/packages/react-ui/src/components/Charts/ProgressBar/progressBar.scss b/js/packages/react-ui/src/components/Charts/ProgressBarChart/progressBarChart.scss similarity index 100% rename from js/packages/react-ui/src/components/Charts/ProgressBar/progressBar.scss rename to js/packages/react-ui/src/components/Charts/ProgressBarChart/progressBarChart.scss diff --git a/js/packages/react-ui/src/components/Charts/ProgressBar/stories/ProgressBar.stories.tsx b/js/packages/react-ui/src/components/Charts/ProgressBarChart/stories/ProgressBarChart.stories.tsx similarity index 97% rename from js/packages/react-ui/src/components/Charts/ProgressBar/stories/ProgressBar.stories.tsx rename to js/packages/react-ui/src/components/Charts/ProgressBarChart/stories/ProgressBarChart.stories.tsx index 6b99711a6..dec82faa8 100644 --- a/js/packages/react-ui/src/components/Charts/ProgressBar/stories/ProgressBar.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/ProgressBarChart/stories/ProgressBarChart.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Card } from "../../../Card"; -import { ProgressBar, ProgressBarProps } from "../ProgressBar"; +import { ProgressBar, ProgressBarProps } from "../ProgressBarChart"; // Sample data variations - simplified to numbers only const progressData = { @@ -14,7 +14,7 @@ const progressData = { }; const meta: Meta = { - title: "Components/Charts/ProgressBar", + title: "Components/Charts/ProgressBarChart", component: ProgressBar, parameters: { layout: "centered", diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index f875eaa8e..09c2d2413 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -10,7 +10,7 @@ @forward "./LineCharts/index.scss"; // progress bar css -@forward "./ProgressBar/progressBar.scss"; +@forward "./ProgressBarChart/progressBarChart.scss"; // shared components css @forward "./shared/XAxisTick/xAxisTick.scss"; From 3362653f6948cd45422fee7e2207466b55699e2c Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 23 Jun 2025 03:52:33 +0530 Subject: [PATCH 109/190] feat(progress-bar-chart): Improve progress bar chart component The changes made to the progress bar chart component include: 1. Simplifying the component structure by removing unnecessary elements and styles. 2. Introducing a more compact and consistent design with a fixed height and padding. 3. Removing the segmented border and rounded corners, creating a cleaner look. 4. Updating the stories to showcase the improved progress bar chart component. These changes aim to provide a more streamlined and visually appealing progress bar chart component, making it easier to use and integrate into various parts of the application. --- .../components/Charts/AreaChart/AreaChart.tsx | 2 +- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 3 +- .../components/Charts/BarChart/BarChart.tsx | 2 +- .../BarCharts/BarChartV2/BarChartV2.tsx | 9 +- .../components/Charts/LineChart/LineChart.tsx | 2 +- .../LineCharts/LineChartV2/LineChartV2.tsx | 10 +- .../ProgressBarChart/ProgressBarChart.tsx | 53 ++++------- .../Charts/ProgressBarChart/index.ts | 3 +- .../ProgressBarChart/progressBarChart.scss | 41 ++------ .../stories/ProgressBarChart.stories.tsx | 94 ++----------------- .../Charts/ProgressBarChart/types/index.ts | 1 + .../src/components/Charts/dependencies.ts | 2 +- .../react-ui/src/components/Charts/index.ts | 2 +- .../CartesianGrid}/cartesianGrid.tsx | 0 .../src/components/Charts/shared/index.ts | 1 + 15 files changed, 61 insertions(+), 164 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/ProgressBarChart/types/index.ts rename js/packages/react-ui/src/components/Charts/{ => shared/CartesianGrid}/cartesianGrid.tsx (100%) diff --git a/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx index 1b08719fe..8991f8578 100644 --- a/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx @@ -10,7 +10,7 @@ import { ChartTooltipContent, keyTransform, } from "../Charts"; -import { cartesianGrid } from "../cartesianGrid"; +import { cartesianGrid } from "../shared"; import { getDistributedColors, getPalette } from "../utils/PalletUtils"; export type AreaChartData = Array>; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index 1331cf3ee..4f115badd 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -10,8 +10,7 @@ import { ChartTooltipContent, keyTransform, } from "../../Charts"; -import { cartesianGrid } from "../../cartesianGrid"; -import { ActiveDot, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; +import { ActiveDot, cartesianGrid, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; import { CustomTooltipContent } from "../../shared/PortalTooltip"; import { LegendItem } from "../../types"; import { diff --git a/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx b/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx index bb55f7815..4d3dfa7f0 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx @@ -10,7 +10,7 @@ import { ChartTooltipContent, keyTransform, } from "../Charts"; -import { cartesianGrid } from "../cartesianGrid"; +import { cartesianGrid } from "../shared/CartesianGrid/cartesianGrid"; import { getDistributedColors, getPalette } from "../utils/PalletUtils"; export type BarChartData = Array>; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 99453675b..1fd5569ce 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -11,8 +11,13 @@ import { ChartTooltipContent, keyTransform, } from "../../Charts"; -import { cartesianGrid } from "../../cartesianGrid"; -import { CustomTooltipContent, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; +import { + cartesianGrid, + CustomTooltipContent, + DefaultLegend, + XAxisTick, + YAxisTick, +} from "../../shared"; import { type LegendItem } from "../../types"; import { getDistributedColors, getPalette, type PaletteName } from "../../utils/PalletUtils"; import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; diff --git a/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx b/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx index 6463d3858..c0af1449b 100644 --- a/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx +++ b/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx @@ -10,7 +10,7 @@ import { ChartTooltipContent, keyTransform, } from "../Charts"; -import { cartesianGrid } from "../cartesianGrid"; +import { cartesianGrid } from "../shared/CartesianGrid/cartesianGrid"; import { getDistributedColors, getPalette } from "../utils/PalletUtils"; export type LineChartData = Array>; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx index 2578bc92d..d332b81f8 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx @@ -10,8 +10,14 @@ import { ChartTooltipContent, keyTransform, } from "../../Charts"; -import { cartesianGrid } from "../../cartesianGrid"; -import { ActiveDot, CustomTooltipContent, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; +import { + ActiveDot, + cartesianGrid, + CustomTooltipContent, + DefaultLegend, + XAxisTick, + YAxisTick, +} from "../../shared"; import { LegendItem } from "../../types"; import { findNearestSnapPosition, diff --git a/js/packages/react-ui/src/components/Charts/ProgressBarChart/ProgressBarChart.tsx b/js/packages/react-ui/src/components/Charts/ProgressBarChart/ProgressBarChart.tsx index 4b44cf715..d9412992b 100644 --- a/js/packages/react-ui/src/components/Charts/ProgressBarChart/ProgressBarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/ProgressBarChart/ProgressBarChart.tsx @@ -1,13 +1,13 @@ import clsx from "clsx"; import { useMemo } from "react"; +import { ProgressBarData } from "."; import { getDistributedColors, getPalette, PaletteName } from "../utils/PalletUtils"; -export type ProgressBarData = Array; - export interface ProgressBarProps { data: ProgressBarData; theme?: PaletteName; className?: string; + style?: React.CSSProperties; animated?: boolean; } @@ -15,6 +15,7 @@ export const ProgressBar = ({ data, theme = "ocean", className, + style, animated = true, }: ProgressBarProps) => { // Calculate percentages @@ -38,40 +39,24 @@ export const ProgressBar = ({ return getDistributedColors(palette, Math.max(segments.length, 1)); }, [theme, segments.length]); - // Custom styles for dynamic values - const customStyles = useMemo(() => { - const styles: any = {}; - - return styles; - }, []); - // Segmented progress bar return ( -
-
- {segments.map((segment, index) => { - const isFirst = index === 0; - const isLast = index === segments.length - 1; - const isSingle = segments.length === 1; - - return ( -
- ); - })} -
+
+ {segments.map((segment, index) => { + return ( +
+ ); + })}
); }; diff --git a/js/packages/react-ui/src/components/Charts/ProgressBarChart/index.ts b/js/packages/react-ui/src/components/Charts/ProgressBarChart/index.ts index 0117bd9b5..44b296c22 100644 --- a/js/packages/react-ui/src/components/Charts/ProgressBarChart/index.ts +++ b/js/packages/react-ui/src/components/Charts/ProgressBarChart/index.ts @@ -1,2 +1,3 @@ export { ProgressBar } from "./ProgressBarChart"; -export type { ProgressBarData, ProgressBarProps } from "./ProgressBarChart"; +export type { ProgressBarProps } from "./ProgressBarChart"; +export type { ProgressBarData } from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/ProgressBarChart/progressBarChart.scss b/js/packages/react-ui/src/components/Charts/ProgressBarChart/progressBarChart.scss index 09a661f11..7693f5ea0 100644 --- a/js/packages/react-ui/src/components/Charts/ProgressBarChart/progressBarChart.scss +++ b/js/packages/react-ui/src/components/Charts/ProgressBarChart/progressBarChart.scss @@ -1,55 +1,28 @@ @use "../../../cssUtils.scss" as cssUtils; -.crayon-progress-bar-container { +.crayon-progress-bar-chart { width: 100%; + height: 20px; font-family: inherit; -} - -.crayon-progress-bar-track { - width: 100%; - height: 24px; background-color: cssUtils.$bg-fill; border-radius: cssUtils.$rounded-s; overflow: hidden; - position: relative; box-shadow: cssUtils.$shadow-s; display: flex; + padding: cssUtils.$spacing-2xs; + gap: cssUtils.$spacing-3xs; } -.crayon-progress-bar-segment { +.crayon-progress-bar-chart-segment { height: 100%; display: flex; align-items: center; justify-content: center; - position: relative; min-width: 2px; box-shadow: cssUtils.$shadow-s; - - /* Add subtle border between segments */ - &:not(:last-child) { - border-right: 1px solid rgba(255, 255, 255, 0.2); - } - - &--first { - border-radius: cssUtils.$rounded-s 0 0 cssUtils.$rounded-s; - } - - &--last { - border-radius: 0 cssUtils.$rounded-s cssUtils.$rounded-s 0; - } - - &--single { - border-radius: cssUtils.$rounded-s; - } + border-radius: cssUtils.$rounded-xs; } -.crayon-progress-bar-animated { +.crayon-progress-bar-chart-animated { transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); } - -/* Dark mode adjustments */ -[data-theme="dark"] { - .crayon-progress-bar-segment:not(:last-child) { - border-right-color: rgba(0, 0, 0, 0.2); - } -} diff --git a/js/packages/react-ui/src/components/Charts/ProgressBarChart/stories/ProgressBarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/ProgressBarChart/stories/ProgressBarChart.stories.tsx index dec82faa8..a3fe7a1d7 100644 --- a/js/packages/react-ui/src/components/Charts/ProgressBarChart/stories/ProgressBarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/ProgressBarChart/stories/ProgressBarChart.stories.tsx @@ -8,9 +8,6 @@ const progressData = { multiple: [25, 30, 20], // Individual segments: 25, 30, 20 segmented: [40, 25, 20, 15], // Four segments with different sizes manySegments: [15, 12, 18, 10, 8, 14, 11, 12], // Eight segments - lowProgress: [15], - highProgress: [95], - completed: [100], }; const meta: Meta = { @@ -70,7 +67,16 @@ export const Default: Story = { animated: true, }, render: (args: any) => ( - +

Task Completion (3 Segments)

@@ -96,23 +102,6 @@ export const FourSegments: Story = { ), }; -export const ThreeSegments: Story = { - name: "Three Segments", - args: { - data: progressData.multiple, - theme: "emerald", - animated: true, - }, - render: (args: any) => ( - -

- Simple Progress Distribution -

- -
- ), -}; - export const ManySegments: Story = { name: "Many Segments (8 Colors)", args: { @@ -126,69 +115,6 @@ export const ManySegments: Story = { Multi-Segment Progress

-
- Hover over segments to see values -
- - ), -}; - -export const SmallProgress: Story = { - name: "Small Progress", - args: { - data: progressData.lowProgress, - theme: "sunset", - animated: true, - }, - render: (args: any) => ( - -

Getting Started

- -
- ), -}; - -export const HighProgress: Story = { - name: "High Progress", - args: { - data: progressData.highProgress, - theme: "vivid", - animated: true, - }, - render: (args: any) => ( - -

Nearly Complete

- -
- ), -}; - -export const Completed: Story = { - name: "Completed", - args: { - data: progressData.completed, - theme: "emerald", - animated: true, - }, - render: (args: any) => ( - -

Task Complete!

- -
- ), -}; - -export const NoAnimation: Story = { - name: "No Animation", - args: { - data: [45], - theme: "spectrum", - animated: false, - }, - render: (args: any) => ( - -

Static Progress

-
), }; diff --git a/js/packages/react-ui/src/components/Charts/ProgressBarChart/types/index.ts b/js/packages/react-ui/src/components/Charts/ProgressBarChart/types/index.ts new file mode 100644 index 000000000..a29610435 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/ProgressBarChart/types/index.ts @@ -0,0 +1 @@ +export type ProgressBarData = Array; diff --git a/js/packages/react-ui/src/components/Charts/dependencies.ts b/js/packages/react-ui/src/components/Charts/dependencies.ts index f6ded9582..39a26fb45 100644 --- a/js/packages/react-ui/src/components/Charts/dependencies.ts +++ b/js/packages/react-ui/src/components/Charts/dependencies.ts @@ -1,2 +1,2 @@ -const dependencies = ["Charts"]; +const dependencies = ["Charts", "IconButton"]; export default dependencies; diff --git a/js/packages/react-ui/src/components/Charts/index.ts b/js/packages/react-ui/src/components/Charts/index.ts index e3355a6a7..93336dddc 100644 --- a/js/packages/react-ui/src/components/Charts/index.ts +++ b/js/packages/react-ui/src/components/Charts/index.ts @@ -9,4 +9,4 @@ export * from "./RadialChart"; export * from "./AreaCharts"; export * from "./BarCharts"; export * from "./LineCharts"; -export * from "./ProgressBar"; +export * from "./ProgressBarChart"; diff --git a/js/packages/react-ui/src/components/Charts/cartesianGrid.tsx b/js/packages/react-ui/src/components/Charts/shared/CartesianGrid/cartesianGrid.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/cartesianGrid.tsx rename to js/packages/react-ui/src/components/Charts/shared/CartesianGrid/cartesianGrid.tsx diff --git a/js/packages/react-ui/src/components/Charts/shared/index.ts b/js/packages/react-ui/src/components/Charts/shared/index.ts index b3901290d..fb2a1b3ba 100644 --- a/js/packages/react-ui/src/components/Charts/shared/index.ts +++ b/js/packages/react-ui/src/components/Charts/shared/index.ts @@ -1,4 +1,5 @@ export * from "./ActiveDot/ActiveDot"; +export * from "./CartesianGrid/cartesianGrid"; export * from "./DefaultLegend/DefaultLegend"; export * from "./PortalTooltip"; export * from "./XAxisTick/XAxisTick"; From 487f32c64c8253d1fd3675970dea438c85093e0f Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 23 Jun 2025 03:55:40 +0530 Subject: [PATCH 110/190] feat(stories): disable autodocs for DefaultLegend story The changes disable the "autodocs" tag for the DefaultLegend story, which prevents the story from being automatically documented. This is done to improve the overall documentation experience by focusing on the most important stories. --- .../shared/DefaultLegend/stories/DefaultLegend.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx index bbf62ac9c..8b617b284 100644 --- a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/stories/DefaultLegend.stories.tsx @@ -9,7 +9,7 @@ const meta: Meta = { parameters: { layout: "centered", }, - tags: ["!dev", "autodocs"], + tags: ["!dev", "!autodocs"], argTypes: { containerWidth: { control: { type: "range", min: 200, max: 800, step: 50 }, From 576c5e607ed12085ac9de2d11e857fe15c1f22cd Mon Sep 17 00:00:00 2001 From: i-subham Date: Mon, 23 Jun 2025 15:43:47 +0530 Subject: [PATCH 111/190] refactor(PieChartV2): simplify rendering of inner and outer pie components Refactor the rendering logic of the PieChartV2 component to return an array of Pie components instead of using fragments. This change enhances readability and maintains the existing functionality of the inner and outer pie charts. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 71 +++++++++---------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index dda8bb81d..cebfc9829 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -377,45 +377,38 @@ export const PieChartV2 = ({ // Memoize the renderPieCharts function const renderPieCharts = useCallback(() => { if (variant === "donut") { - return ( - <> - {/* Inner Pie */} - - {transformedData.map((entry, index) => { - const hoverStyles = getHoverStyles(index, activeIndex); - const fill = useGradients - ? `url(#gradient-${index})` - : `var(--crayon-container-hover-fills)`; - - return ( - - ); - })} - - - {/* Outer Pie */} - - {transformedData.map((entry, index) => { - const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); - const config = chartConfig[categoryValue]; - const hoverStyles = getHoverStyles(index, activeIndex); - const fill = useGradients ? `url(#gradient-${index})` : config?.color; - - return ( - - ); - })} - - - ); + return [ + // Inner Pie + + {transformedData.map((entry, index) => { + const hoverStyles = getHoverStyles(index, activeIndex); + const fill = useGradients + ? `url(#gradient-${index})` + : `var(--crayon-container-hover-fills)`; + + return ; + })} + , + // Outer Pie + + {transformedData.map((entry, index) => { + const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); + const config = chartConfig[categoryValue]; + const hoverStyles = getHoverStyles(index, activeIndex); + const fill = useGradients ? `url(#gradient-${index})` : config?.color; + + return ; + })} + , + ]; } return ( From 50190b54a299c671c6b7ed4b7fe553ffcb1c8b72 Mon Sep 17 00:00:00 2001 From: i-subham Date: Mon, 23 Jun 2025 15:56:31 +0530 Subject: [PATCH 112/190] feat(RadialChartV2): introduce new RadialChartV2 component with enhanced features This commit adds the RadialChartV2 component, which includes: - A new implementation of a radial chart with customizable themes, variants, and data formats. - Support for legends in both default and stacked layouts. - Enhanced hover effects and animations for better user interaction. - A dedicated SCSS file for styling the component. - Storybook stories to demonstrate various use cases and configurations. These changes aim to provide a flexible and visually appealing radial chart solution for data visualization. --- .../RadialChartV2/RadialChartV2.tsx | 457 ++++++++++++++++++ .../RadialCharts/RadialChartV2/index.ts | 0 .../RadialChartV2/radialChartV2.scss | 129 +++++ .../stories/RadialChartV2.stories.tsx | 377 +++++++++++++++ .../components/RadialChartRenderers.tsx | 79 +++ .../RadialCharts/utils/RadialChartUtils.ts | 254 +++++++--- 6 files changed, 1236 insertions(+), 60 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx create mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/radialChartV2.scss create mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx create mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/components/RadialChartRenderers.tsx diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx new file mode 100644 index 000000000..c2ec2c5f2 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx @@ -0,0 +1,457 @@ +import clsx from "clsx"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Cell, LabelList, PolarGrid, RadialBar, RadialBarChart } from "recharts"; +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts"; +import { DefaultLegend } from "../../shared/DefaultLegend/DefaultLegend"; +import { StackedLegend } from "../../shared/StackedLegend/StackedLegend"; +import { LegendItem } from "../../types/Legend"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { createRadialGradientDefinitions } from "../components/RadialChartRenderers"; +import { + RadialChartData, + calculateRadialChartDimensions, + createRadialAnimationConfig, + createRadialChartConfig, + createRadialEventHandlers, + formatRadialLabel, + getRadialFontSize, + getRadialHoverStyles, + transformRadialDataWithPercentages, + useRadialChartHover, +} from "../utils/RadialChartUtils"; +import "./radialChartV2.scss"; + +export type RadialChartV2Data = RadialChartData; + +interface GradientColor { + start?: string; + end?: string; +} + +export interface RadialChartV2Props { + data: T; + categoryKey: keyof T[number]; + dataKey: keyof T[number]; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + variant?: "semicircle" | "circular"; + format?: "percentage" | "number"; + legend?: boolean; + legendVariant?: "default" | "stacked"; + label?: boolean; + grid?: boolean; + isAnimationActive?: boolean; + cornerRadius?: number; + useGradients?: boolean; + gradientColors?: GradientColor[]; + onMouseEnter?: (data: any, index: number) => void; + onMouseLeave?: () => void; + onClick?: (data: any, index: number) => void; + className?: string; + height?: number; + width?: number; +} + +export const RadialChartV2 = ({ + data, + categoryKey, + dataKey, + theme = "ocean", + variant = "semicircle", + format = "number", + legend = true, + legendVariant = "stacked", + label = true, + grid = true, + isAnimationActive = true, + cornerRadius = 10, + useGradients = false, + gradientColors, + onMouseEnter, + onMouseLeave, + onClick, + className, + height, + width, +}: RadialChartV2Props) => { + const chartContainerRef = useRef(null); + const wrapperRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + const [containerHeight, setContainerHeight] = useState(0); + const [wrapperWidth, setWrapperWidth] = useState(0); + const [hoveredLegendKey, setHoveredLegendKey] = useState(null); + const { activeIndex, handleMouseEnter, handleMouseLeave } = useRadialChartHover(); + + // Memoize string conversions to avoid repeated calls + const categoryKeyString = useMemo(() => String(categoryKey), [categoryKey]); + const dataKeyString = useMemo(() => String(dataKey), [dataKey]); + const formatKey = useMemo( + () => (format === "percentage" ? "percentage" : dataKeyString), + [format, dataKeyString], + ); + + // Use provided dimensions or observed dimensions + const effectiveWidth = useMemo(() => { + return width ?? containerWidth; + }, [width, containerWidth]); + + const effectiveHeight = useMemo(() => { + return height ?? containerHeight; + }, [height, containerHeight]); + + // Calculate chart dimensions based on the smaller dimension + const chartSize = useMemo(() => { + const size = Math.min(effectiveWidth, effectiveHeight); + return Math.max(200, Math.min(size, 800)); // Min 200px, max 800px + }, [effectiveWidth, effectiveHeight]); + + // Calculate chart dimensions + const dimensions = useMemo(() => { + return calculateRadialChartDimensions(chartSize, variant, label); + }, [chartSize, variant, label]); + + // Memoize expensive data transformations and configurations + const transformedData = useMemo( + () => transformRadialDataWithPercentages(data, dataKey, theme), + [data, dataKey, theme], + ); + + const chartConfig = useMemo( + () => createRadialChartConfig(data, categoryKey, theme), + [data, categoryKey, theme], + ); + + const animationConfig = useMemo( + () => createRadialAnimationConfig({ isAnimationActive }), + [isAnimationActive], + ); + + const eventHandlers = useMemo( + () => createRadialEventHandlers(onMouseEnter, onMouseLeave, onClick), + [onMouseEnter, onMouseLeave, onClick], + ); + + // Get color palette and distribute colors + const palette = useMemo(() => getPalette(theme), [theme]); + const colors = useMemo(() => getDistributedColors(palette, data.length), [palette, data.length]); + + // Memoize gradient definitions + const gradientDefinitions = useMemo(() => { + if (!useGradients) return null; + + const chartColors = Object.values(chartConfig) + .map((config) => config.color) + .filter((color): color is string => color !== undefined); + + return createRadialGradientDefinitions(transformedData, chartColors, gradientColors); + }, [useGradients, chartConfig, transformedData, gradientColors]); + + // Create legend items for both variants + const legendItems = useMemo(() => { + return data.map((item, index) => ({ + key: String(item[categoryKey]), + label: String(item[categoryKey]), + value: Number(item[dataKey]), + color: colors[index] || "#000000", + })); + }, [data, categoryKey, dataKey, colors]); + + // Create DefaultLegend items + const defaultLegendItems = useMemo((): LegendItem[] => { + return data.map((item, index) => ({ + key: String(item[categoryKey]), + label: String(item[categoryKey]), + color: colors[index] || "#000000", + })); + }, [data, categoryKey, colors]); + + // Memoize sorted data for legend hover handling + const sortedData = useMemo( + () => [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), + [data, dataKey], + ); + + // Handle legend hover + const handleLegendHover = useCallback( + (key: string | null) => { + if (legendVariant !== "stacked") return; + setHoveredLegendKey(key); + }, + [legendVariant], + ); + + // Handle legend item hover to highlight radial bar + const handleLegendItemHover = useCallback( + (index: number | null) => { + if (legendVariant !== "stacked") return; + + if (index !== null) { + const item = sortedData[index]; + if (item) { + const categoryValue = String(item[categoryKey]); + setHoveredLegendKey(categoryValue); + const originalIndex = data.findIndex((d) => String(d[categoryKey]) === categoryValue); + if (originalIndex !== -1) { + handleMouseEnter(data[originalIndex], originalIndex); + } + } + } else { + setHoveredLegendKey(null); + handleMouseLeave(); + } + }, + [sortedData, categoryKey, data, handleMouseEnter, handleMouseLeave, legendVariant], + ); + + // Enhanced chart hover handlers + const handleChartMouseEnter = useCallback( + (data: any, index: number) => { + handleMouseEnter(data, index); + if (legend && legendVariant === "stacked") { + const categoryValue = String(data[categoryKey]); + setHoveredLegendKey(categoryValue); + } + }, + [handleMouseEnter, categoryKey, legend, legendVariant], + ); + + const handleChartMouseLeave = useCallback(() => { + handleMouseLeave(); + if (legend && legendVariant === "stacked") { + setHoveredLegendKey(null); + } + }, [handleMouseLeave, legend, legendVariant]); + + // Determine layout based on wrapper width and legend variant + const layoutConfig = useMemo(() => { + if (!legend || legendVariant !== "stacked") { + return { isRow: false, isMobile: false }; + } + + const availableWidth = wrapperWidth || containerWidth; + const isMobileLayout = availableWidth <= 400; + + return { + isRow: !isMobileLayout && availableWidth > 400, + isMobile: isMobileLayout, + }; + }, [legend, legendVariant, wrapperWidth, containerWidth]); + + // Memoize style objects to prevent unnecessary re-renders + const containerStyle = useMemo( + () => ({ + width: width ? `${width}px` : "100%", + height: height ? `${height}px` : "100%", + flex: legend && legendVariant === "stacked" && layoutConfig.isRow ? "1" : "none", + }), + [width, height, legend, legendVariant, layoutConfig.isRow], + ); + + const innerContainerStyle = useMemo( + () => ({ + width: "100%", + height: "100%", + display: "flex", + alignItems: "center", + justifyContent: "center", + }), + [], + ); + + const chartSizeStyle = useMemo( + () => ({ + width: chartSize, + height: chartSize, + }), + [chartSize], + ); + + const legendContainerStyle = useMemo( + () => ({ + flex: layoutConfig.isRow ? "1" : "none", + width: layoutConfig.isRow ? "auto" : "100%", + }), + [layoutConfig.isRow], + ); + + const rechartsProps = useMemo( + () => ({ + width: chartSize, + height: chartSize, + }), + [chartSize], + ); + + // Memoize angle calculations + const startAngle = useMemo(() => (variant === "circular" ? -90 : 0), [variant]); + const endAngle = useMemo(() => (variant === "circular" ? 270 : 180), [variant]); + + useEffect(() => { + if (!wrapperRef.current) { + return () => {}; + } + + const wrapperResizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + setWrapperWidth(entry.contentRect.width); + } + }); + + wrapperResizeObserver.observe(wrapperRef.current); + + return () => { + wrapperResizeObserver.disconnect(); + }; + }, []); + + useEffect(() => { + if ((width && height) || !chartContainerRef.current) { + return () => {}; + } + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const newWidth = entry.contentRect.width; + const newHeight = entry.contentRect.height; + setContainerWidth(newWidth); + setContainerHeight(newHeight); + } + }); + + resizeObserver.observe(chartContainerRef.current); + + return () => { + resizeObserver.disconnect(); + }; + }, [width, height]); + + // Memoize the renderLegend function + const renderLegend = useCallback(() => { + if (!legend) return null; + + if (legendVariant === "stacked") { + return ( +
+ +
+ ); + } + return ; + }, [ + legend, + legendVariant, + legendItems, + handleLegendHover, + hoveredLegendKey, + handleLegendItemHover, + layoutConfig.isRow, + wrapperWidth, + defaultLegendItems, + containerWidth, + legendContainerStyle, + ]); + + // Memoize className calculations + const wrapperClassName = useMemo( + () => + clsx("crayon-radial-chart-container-wrapper", className, { + "crayon-radial-chart-container-wrapper--stacked-legend": + legend && legendVariant === "stacked" && layoutConfig.isRow, + "crayon-radial-chart-container-wrapper--stacked-legend-column": + legend && legendVariant === "stacked" && !layoutConfig.isRow, + "crayon-radial-chart-container-wrapper--default-legend": + legend && legendVariant === "default", + }), + [className, legend, legendVariant, layoutConfig.isRow], + ); + + const containerClassName = useMemo( + () => + clsx("crayon-radial-chart-container", { + "crayon-radial-chart-container--with-stacked-legend": + legend && legendVariant === "stacked" && layoutConfig.isRow, + }), + [legend, legendVariant, layoutConfig.isRow], + ); + + return ( +
+
+
+
+ + + {grid && } + + } + /> + + {gradientDefinitions && {gradientDefinitions}} + + + {transformedData.map((entry, index) => { + const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); + const config = chartConfig[categoryValue]; + const hoverStyles = getRadialHoverStyles(index, activeIndex); + const fill = useGradients + ? `url(#radial-gradient-${index})` + : config?.color || colors[index]; + + return ( + + ); + })} + {label && ( + formatRadialLabel(value, format)} + /> + )} + + + +
+
+
+ {renderLegend()} +
+ ); +}; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/index.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/radialChartV2.scss b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/radialChartV2.scss new file mode 100644 index 000000000..7e8d9407a --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/radialChartV2.scss @@ -0,0 +1,129 @@ +@use "../../../../cssUtils" as cssUtils; + +.crayon-radial-chart-container-wrapper { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; + display: flex; + gap: 20px; + + // Default legend variant - always column layout (chart on top, legend at bottom) + &--default-legend { + flex-direction: column; + align-items: center; + gap: 20px; + } + + // Stacked legend variant - responsive layout (side-by-side on desktop, stacked on mobile) + &--stacked-legend { + display: flex; + flex-direction: row; + align-items: center; + gap: 20px; + min-height: 0; + } + + &--stacked-legend-column { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + } +} + +.crayon-radial-chart-container { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + min-height: 0; + + // When in row layout with stacked legend, ensure proper sizing + &--with-stacked-legend { + flex: 1; + min-width: 0; + } +} + +.crayon-radial-chart-container-inner { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.crayon-radial-chart { + position: relative; + + // Add smooth transitions for hover effects + .recharts-radial-bar-sector { + transition: all 0.2s ease-in-out; + } + + // Improve grid appearance + .recharts-polar-grid { + opacity: 0.3; + } + + // Label positioning and styling + .recharts-label { + font-weight: 500; + fill: var(--crayon-text-primary); + } +} + +.crayon-radial-chart-legend-container { + height: 100%; + display: flex; + align-items: flex-start; + justify-content: flex-start; + min-width: 0; + overflow: hidden; + + // Ensure the legend takes available space in row layout + .crayon-stacked-legend { + width: 100%; + min-width: 0; + } +} + +.crayon-radial-chart-legend { + display: flex; + align-items: center; + justify-content: center; + + // Stacked legend variant - responsive positioning + &:not(.crayon-radial-chart-legend--bottom) { + // Mobile layout + @media (max-width: 599px) { + width: 100%; + margin-top: 20px; + } + + // Desktop layout + @media (min-width: 600px) { + width: 100%; + } + } + + // Stacked legend variant styling + .crayon-stacked-legend { + width: 100%; + + // Mobile layout for stacked legend + @media (max-width: 599px) { + width: 100%; + margin-top: 20px; + } + + // Desktop layout for stacked legend + @media (min-width: 600px) { + width: 100%; + } + } +} diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx new file mode 100644 index 000000000..6354d7afa --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx @@ -0,0 +1,377 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Card } from "../../../../Card"; +import { RadialChartV2, RadialChartV2Props } from "../RadialChartV2"; + +const radialChartData = [ + { month: "January", value: 1250 }, + { month: "February", value: 980 }, + { month: "March", value: 1450 }, + { month: "April", value: 1320 }, + { month: "May", value: 1680 }, + { month: "June", value: 2100 }, + { month: "July", value: 1950 }, + { month: "August", value: 1820 }, + { month: "September", value: 1650 }, + { month: "October", value: 1480 }, + { month: "November", value: 1350 }, + { month: "December", value: 1200 }, +]; + +// Extended data for carousel demo +const extendedRadialChartData = [ + { month: "January", value: 1250 }, + { month: "February", value: 980 }, + { month: "March", value: 1450 }, + { month: "April", value: 1320 }, + { month: "May", value: 1680 }, + { month: "June", value: 2100 }, + { month: "July", value: 1950 }, + { month: "August", value: 1820 }, + { month: "September", value: 1650 }, + { month: "October", value: 1480 }, + { month: "November", value: 1350 }, + { month: "December", value: 1200 }, + { month: "Q1 Bonus", value: 850 }, + { month: "Q2 Bonus", value: 920 }, + { month: "Q3 Bonus", value: 780 }, + { month: "Q4 Bonus", value: 1100 }, + { month: "Holiday Pay", value: 650 }, + { month: "Overtime", value: 420 }, + { month: "Commission", value: 890 }, + { month: "Incentives", value: 720 }, +]; + +const gradientColors = [ + { start: "#FF6B6B", end: "#FF8E8E" }, + { start: "#4ECDC4", end: "#6ED7D0" }, + { start: "#45B7D1", end: "#6BC5DB" }, + { start: "#96CEB4", end: "#B4DCC9" }, + { start: "#FFEEAD", end: "#FFF4C4" }, + { start: "#D4A5A5", end: "#E5BDBD" }, + { start: "#9B59B6", end: "#B07CC7" }, +]; + +const meta: Meta> = { + title: "Components/Charts/RadialCharts/RadialChartV2", + component: RadialChartV2, + parameters: { + layout: "centered", + docs: { + description: { + component: + "```tsx\nimport { RadialChartV2 } from '@crayon-ui/react-ui/Charts/RadialChartV2';\n```", + }, + }, + }, + tags: ["!dev", "autodocs"], + argTypes: { + data: { + description: + "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the radial bars to be plotted.", + control: false, + table: { + type: { summary: "Array>" }, + defaultValue: { summary: "[]" }, + category: "Data", + }, + }, + categoryKey: { + description: + "The key from your data object to be used as the bar labels (e.g., 'month', 'category', 'name')", + control: false, + table: { + type: { summary: "string" }, + category: "Data", + }, + }, + dataKey: { + description: + "The key from your data object to be used as the values that determine the bar sizes (e.g., 'value', 'count', 'amount')", + control: false, + table: { + type: { summary: "string" }, + category: "Data", + }, + }, + theme: { + description: + "The color palette theme for the chart. Each theme provides a different set of colors for the radial bars.", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + variant: { + description: + "The style of the radial chart. 'semicircle' shows a half circle, 'circular' shows a full circle.", + control: "radio", + options: ["semicircle", "circular"], + table: { + defaultValue: { summary: "semicircle" }, + category: "Appearance", + }, + }, + format: { + description: + "The format of the data. 'percentage' shows the data as a percentage, while 'number' shows the data as a number.", + control: "radio", + options: ["percentage", "number"], + table: { + defaultValue: { summary: "number" }, + category: "Display", + }, + }, + legend: { + description: "Whether to display the legend", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + legendVariant: { + description: + "The type of legend to display. 'default' shows a horizontal legend at bottom, 'stacked' shows a vertical stacked legend with responsive layout.", + control: "radio", + options: ["default", "stacked"], + table: { + defaultValue: { summary: "stacked" }, + category: "Display", + }, + }, + label: { + description: "Whether to display labels on the radial bars", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + grid: { + description: "Whether to display the polar grid", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + isAnimationActive: { + description: "Whether to animate the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + cornerRadius: { + description: "The radius of the corners of each radial bar", + control: { type: "number", min: 0, max: 20 }, + table: { + type: { summary: "number" }, + defaultValue: { summary: "10" }, + category: "Appearance", + }, + }, + useGradients: { + description: "Whether to use gradient colors for the radial bars", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "Appearance", + }, + }, + height: { + description: "Fixed height of the chart container", + control: { type: "number", min: 200, max: 800 }, + table: { + type: { summary: "number" }, + category: "Layout", + }, + }, + width: { + description: "Fixed width of the chart container", + control: { type: "number", min: 200, max: 800 }, + table: { + type: { summary: "number" }, + category: "Layout", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const RadialChartV2Demo: Story = { + name: "RadialChartV2", + args: { + data: radialChartData, + categoryKey: "month", + dataKey: "value", + theme: "ocean", + variant: "semicircle", + format: "number", + legend: true, + legendVariant: "stacked", + label: true, + grid: true, + isAnimationActive: true, + cornerRadius: 10, + useGradients: false, + gradientColors, + height: undefined, + width: undefined, + }, + render: (args: any) => ( + + + + ), +}; + +export const RadialChartV2Circular: Story = { + name: "RadialChartV2 Circular", + args: { + data: radialChartData, + categoryKey: "month", + dataKey: "value", + theme: "emerald", + variant: "circular", + format: "number", + legend: true, + legendVariant: "stacked", + label: true, + grid: true, + isAnimationActive: true, + cornerRadius: 10, + useGradients: false, + gradientColors, + height: undefined, + width: undefined, + }, + render: (args: any) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "This example shows the radial chart in circular variant with full 360-degree display.", + }, + }, + }, +}; + +export const RadialChartV2WithGradients: Story = { + name: "RadialChartV2 with Gradients", + args: { + data: radialChartData.slice(0, 6), // Use fewer data points for cleaner gradient display + categoryKey: "month", + dataKey: "value", + theme: "sunset", + variant: "semicircle", + format: "percentage", + legend: true, + legendVariant: "stacked", + label: true, + grid: false, + isAnimationActive: true, + cornerRadius: 15, + useGradients: true, + gradientColors, + height: undefined, + width: undefined, + }, + render: (args: any) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "This example demonstrates the gradient functionality with custom gradient colors and percentage format.", + }, + }, + }, +}; + +export const RadialChartV2WithCarousel: Story = { + name: "RadialChartV2 with Up/Down Carousel", + args: { + data: extendedRadialChartData, + categoryKey: "month", + dataKey: "value", + theme: "spectrum", + variant: "circular", + format: "number", + legend: true, + legendVariant: "stacked", + label: false, // Disable labels for cleaner look with many data points + grid: true, + isAnimationActive: true, + cornerRadius: 8, + useGradients: false, + gradientColors, + height: undefined, + width: undefined, + }, + render: (args: any) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "This example demonstrates the up/down carousel functionality when there are many legend items. The legend has navigation buttons that appear when content overflows, allowing users to scroll through all items.", + }, + }, + }, +}; + +export const RadialChartV2Minimal: Story = { + name: "RadialChartV2 Minimal", + args: { + data: radialChartData.slice(0, 4), + categoryKey: "month", + dataKey: "value", + theme: "orchid", + variant: "semicircle", + format: "number", + legend: false, + legendVariant: "default", + label: false, + grid: false, + isAnimationActive: true, + cornerRadius: 5, + useGradients: false, + gradientColors, + height: undefined, + width: undefined, + }, + render: (args: any) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "This example shows a minimal radial chart without legend, labels, or grid for clean presentation.", + }, + }, + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/components/RadialChartRenderers.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/components/RadialChartRenderers.tsx new file mode 100644 index 000000000..d71f578ac --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/components/RadialChartRenderers.tsx @@ -0,0 +1,79 @@ +/** + * Creates gradient definitions for radial chart segments + * @param data - The chart data array + * @param colors - Array of base colors for segments + * @param gradientColors - Optional array of gradient color configurations + * @returns Array of gradient definition elements + */ +export const createRadialGradientDefinitions = ( + data: any[], + colors: string[], + gradientColors?: Array<{ start?: string; end?: string }>, +) => { + return data.map((_, index) => { + const defaultBaseColor = colors[index] || "#000000"; + + // Get the base color from gradientColors or fallback to default + let baseColor = defaultBaseColor; + let secondaryColor = colors[data.length - index - 1] || "#ffffff"; + + if (gradientColors?.[index]) { + // If both colors are provided, use them + if (gradientColors[index].start && gradientColors[index].end) { + baseColor = gradientColors[index].start; + secondaryColor = gradientColors[index].end; + } + // If only start color is provided, create a darker version for end + else if (gradientColors[index].start) { + baseColor = gradientColors[index].start; + secondaryColor = adjustColorBrightness(baseColor, -20); + } + // If only end color is provided, create a lighter version for start + else if (gradientColors[index].end) { + baseColor = adjustColorBrightness(gradientColors[index].end, 20); + secondaryColor = gradientColors[index].end; + } + } + + return ( + + + + + + + ); + }); +}; + +/** + * Adjusts the brightness of a hex color + * @param hex - The hex color to adjust + * @param percent - The percentage to adjust brightness by (-100 to 100) + * @returns The adjusted hex color + */ +export const adjustColorBrightness = (hex: string, percent: number): string => { + if (!hex || !hex.startsWith("#")) { + return hex; + } + try { + const num = parseInt(hex.replace("#", ""), 16); + const amt = Math.round(2.55 * percent); + const R = Math.min(255, Math.max(0, (num >> 16) + amt)); + const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) + amt)); + const B = Math.min(255, Math.max(0, (num & 0x0000ff) + amt)); + return "#" + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1); + } catch (error) { + console.warn("Invalid color format:", hex); + console.warn(error); + return hex; + } +}; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts index 0dd7bf8c4..b226a4cd1 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts @@ -5,16 +5,46 @@ import { useState } from "react"; import { ChartConfig } from "../../Charts"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +// ========================================== +// Types +// ========================================== + export type RadialChartData = Array>; -export interface RadialChartConfig { - data: RadialChartData; - categoryKey: string; - dataKey: string; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; +export interface RadialChartDimensions { + outerRadius: number; + innerRadius: number; +} + +export interface RadialHoverStyles { + opacity: number; + stroke: string; + strokeWidth: number; +} + +export interface RadialChartHoverHook { + activeIndex: number | null; + handleMouseEnter: (event: any, index: number) => void; + handleMouseLeave: () => void; +} + +export interface RadialAnimationConfig { + isAnimationActive: boolean; + animationBegin: number; + animationDuration: number; + animationEasing: "ease" | "ease-in" | "ease-out" | "ease-in-out" | "linear"; } -// Helper function to calculate percentage +// ========================================== +// Core Calculation Utilities +// ========================================== + +/** + * Calculates the percentage value of a number relative to a total + * @param value - The value to calculate percentage for + * @param total - The total value to calculate percentage against + * @returns The calculated percentage rounded to 2 decimal places + */ export const calculatePercentage = (value: number, total: number): number => { if (total === 0) { return 0; @@ -22,19 +52,27 @@ export const calculatePercentage = (value: number, total: number): number => { return Number(((value / total) * 100).toFixed(2)); }; -// Dynamic resize function to maintain aspect ratio for radial charts +// ========================================== +// Chart Dimension Calculations +// ========================================== + +/** + * Calculates dimensions for radial charts based on container size + * @param width - The container width + * @param variant - The chart variant ('semicircle' or 'circular') + * @param hasLabel - Whether the chart has labels + * @returns Object containing outer and inner radius values + */ export const calculateRadialChartDimensions = ( width: number, variant: "semicircle" | "circular", - label: boolean, -): { outerRadius: number; innerRadius: number } => { + hasLabel: boolean = false, +): RadialChartDimensions => { const baseRadiusPercentage = 0.4; // 40% of container width - - // Calculate base outer radius let outerRadius = Math.round(width * baseRadiusPercentage); // Adjust for label presence - if (label) { + if (hasLabel) { outerRadius = Math.round(outerRadius * 0.9); } @@ -47,34 +85,20 @@ export const calculateRadialChartDimensions = ( return { outerRadius, innerRadius }; }; -export const layoutMap: Record = { - mobile: "crayon-pie-chart-container-mobile", - fullscreen: "crayon-pie-chart-container-fullscreen", - tray: "crayon-pie-chart-container-tray", - copilot: "crayon-pie-chart-container-copilot", -}; - -// Reusable hook for chart hover effects -export const useChartHover = () => { - const [activeIndex, setActiveIndex] = useState(null); - - const handleMouseEnter = (_: any, index: number) => { - setActiveIndex(index); - }; - - const handleMouseLeave = () => { - setActiveIndex(null); - }; - - return { - activeIndex, - handleMouseEnter, - handleMouseLeave, - }; -}; +// ========================================== +// Layout and Styling Utilities +// ========================================== -// Default hover style properties for cells -export const getHoverStyles = (index: number, activeIndex: number | null) => { +/** + * Generates hover style properties for radial chart cells + * @param index - The index of the current cell + * @param activeIndex - The index of the currently hovered cell + * @returns Object containing hover style properties + */ +export const getRadialHoverStyles = ( + index: number, + activeIndex: number | null, +): RadialHoverStyles => { return { opacity: activeIndex === null || activeIndex === index ? 1 : 0.6, stroke: activeIndex === index ? "#fff" : "none", @@ -82,26 +106,18 @@ export const getHoverStyles = (index: number, activeIndex: number | null) => { }; }; -// Helper function to format label values -export const formatRadialLabel = ( - value: string | number, - format: "percentage" | "number", -): string => { - if (format === "percentage") { - return `${value}%`; - } - // For number format, just truncate if too long - const stringValue = String(value); - return stringValue.length > 8 ? `${stringValue.slice(0, 8)}...` : stringValue; -}; - -// Helper function to calculate font size based on data length -export const getRadialFontSize = (dataLength: number): number => { - return dataLength <= 5 ? 12 : 7; -}; +// ========================================== +// Data Transformation Utilities +// ========================================== -// Transform data with percentages and colors -export const transformRadialData = ( +/** + * Transforms data by adding percentage calculations and colors + * @param data - The input data array + * @param dataKey - The key to use for value calculations + * @param theme - The color theme to use + * @returns Transformed data with added percentage, original value, and fill color + */ +export const transformRadialDataWithPercentages = ( data: T, dataKey: keyof T[number], theme: string = "ocean", @@ -118,12 +134,18 @@ export const transformRadialData = ( })); }; -// Create chart configuration +/** + * Creates chart configuration with colors and labels + * @param data - The input data array + * @param categoryKey - The key to use for category labels + * @param theme - The color theme to use + * @returns Chart configuration object + */ export const createRadialChartConfig = ( data: T, categoryKey: keyof T[number], theme: string = "ocean", -) => { +): ChartConfig => { const palette = getPalette(theme); const colors = getDistributedColors(palette, data.length); @@ -133,8 +155,120 @@ export const createRadialChartConfig = ( [String(item[categoryKey])]: { label: String(item[categoryKey as string]), color: colors[index], + secondaryColor: colors[data.length - index - 1], // Add secondary color for gradient effect }, }), {}, ); }; + +// ========================================== +// Hover Effect Utilities +// ========================================== + +/** + * Custom hook for managing radial chart hover effects + * @returns Object containing hover state and handlers + */ +export const useRadialChartHover = (): RadialChartHoverHook => { + const [activeIndex, setActiveIndex] = useState(null); + + const handleMouseEnter = (_: any, index: number) => { + setActiveIndex(index); + }; + + const handleMouseLeave = () => { + setActiveIndex(null); + }; + + return { + activeIndex, + handleMouseEnter, + handleMouseLeave, + }; +}; + +// ========================================== +// Animation Utilities +// ========================================== + +/** + * Creates animation configuration for radial chart + * @param config - Animation configuration options + * @returns Animation configuration object + */ +export const createRadialAnimationConfig = ( + config: Partial = {}, +): RadialAnimationConfig => { + return { + isAnimationActive: config.isAnimationActive ?? true, + animationBegin: config.animationBegin ?? 0, + animationDuration: config.animationDuration ?? 1500, + animationEasing: config.animationEasing ?? "ease", + }; +}; + +// ========================================== +// Event Handler Utilities +// ========================================== + +/** + * Creates event handlers for radial chart + * @param onMouseEnter - Mouse enter handler + * @param onMouseLeave - Mouse leave handler + * @param onClick - Click handler + * @returns Object containing event handlers + */ +export const createRadialEventHandlers = ( + onMouseEnter?: (data: any, index: number) => void, + onMouseLeave?: () => void, + onClick?: (data: any, index: number) => void, +) => { + return { + onMouseEnter: onMouseEnter + ? (data: any, index: number) => onMouseEnter(data, index) + : undefined, + onMouseLeave: onMouseLeave ? () => onMouseLeave() : undefined, + onClick: onClick ? (data: any, index: number) => onClick(data, index) : undefined, + }; +}; + +// ========================================== +// Label Formatting Utilities +// ========================================== + +/** + * Helper function to format label values + * @param value - The value to format + * @param format - The format type ('percentage' or 'number') + * @returns Formatted string + */ +export const formatRadialLabel = ( + value: string | number, + format: "percentage" | "number", +): string => { + if (format === "percentage") { + return `${value}%`; + } + // For number format, just truncate if too long + const stringValue = String(value); + return stringValue.length > 8 ? `${stringValue.slice(0, 8)}...` : stringValue; +}; + +/** + * Helper function to calculate font size based on data length + * @param dataLength - The number of data items + * @returns Font size number + */ +export const getRadialFontSize = (dataLength: number): number => { + return dataLength <= 5 ? 12 : 7; +}; + +// ========================================== +// Backward compatibility - keeping old function names +// ========================================== + +// Keep old function names for backward compatibility +export const transformRadialData = transformRadialDataWithPercentages; +export const useChartHover = useRadialChartHover; +export const getHoverStyles = getRadialHoverStyles; From 918ea80cbc5b23e89789b9833cc9da66214f55d8 Mon Sep 17 00:00:00 2001 From: i-subham Date: Mon, 23 Jun 2025 17:49:01 +0530 Subject: [PATCH 113/190] feat(RadialChartV2): enhance radial chart with improved hover effects and styling This commit introduces several updates to the RadialChartV2 component, including: - Added hover transition effects for radial bars to improve user interaction. - Updated the chart variant from 'semicircle' to 'circular' for a more consistent appearance. - Removed label display options to simplify the component's configuration. - Adjusted the grid display setting to default to false for cleaner visuals. - Refactored utility functions to remove unused label formatting logic. These changes aim to enhance the visual appeal and usability of the RadialChartV2 component. --- .../RadialChartV2/RadialChartV2.tsx | 32 +++++----------- .../RadialChartV2/radialChartV2.scss | 10 +++++ .../stories/RadialChartV2.stories.tsx | 32 +++++----------- .../RadialCharts/utils/RadialChartUtils.ts | 38 ------------------- 4 files changed, 28 insertions(+), 84 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx index c2ec2c5f2..4fe005039 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx @@ -1,6 +1,6 @@ import clsx from "clsx"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Cell, LabelList, PolarGrid, RadialBar, RadialBarChart } from "recharts"; +import { Cell, PolarGrid, RadialBar, RadialBarChart } from "recharts"; import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts"; import { DefaultLegend } from "../../shared/DefaultLegend/DefaultLegend"; import { StackedLegend } from "../../shared/StackedLegend/StackedLegend"; @@ -13,8 +13,6 @@ import { createRadialAnimationConfig, createRadialChartConfig, createRadialEventHandlers, - formatRadialLabel, - getRadialFontSize, getRadialHoverStyles, transformRadialDataWithPercentages, useRadialChartHover, @@ -37,7 +35,6 @@ export interface RadialChartV2Props { format?: "percentage" | "number"; legend?: boolean; legendVariant?: "default" | "stacked"; - label?: boolean; grid?: boolean; isAnimationActive?: boolean; cornerRadius?: number; @@ -56,12 +53,11 @@ export const RadialChartV2 = ({ categoryKey, dataKey, theme = "ocean", - variant = "semicircle", + variant = "circular", format = "number", legend = true, legendVariant = "stacked", - label = true, - grid = true, + grid = false, isAnimationActive = true, cornerRadius = 10, useGradients = false, @@ -106,8 +102,8 @@ export const RadialChartV2 = ({ // Calculate chart dimensions const dimensions = useMemo(() => { - return calculateRadialChartDimensions(chartSize, variant, label); - }, [chartSize, variant, label]); + return calculateRadialChartDimensions(chartSize, variant); + }, [chartSize, variant]); // Memoize expensive data transformations and configurations const transformedData = useMemo( @@ -399,8 +395,6 @@ export const RadialChartV2 = ({ endAngle={endAngle} innerRadius={dimensions.innerRadius} outerRadius={dimensions.outerRadius} - onMouseEnter={handleChartMouseEnter} - onMouseLeave={handleChartMouseLeave} > {grid && } ({ background={!grid} cornerRadius={cornerRadius} {...animationConfig} - {...eventHandlers} + activeIndex={activeIndex ?? undefined} + onMouseEnter={handleChartMouseEnter} + onMouseLeave={handleChartMouseLeave} + onClick={eventHandlers.onClick} > {transformedData.map((entry, index) => { const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); @@ -434,17 +431,6 @@ export const RadialChartV2 = ({ ); })} - {label && ( - formatRadialLabel(value, format)} - /> - )} diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/radialChartV2.scss b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/radialChartV2.scss index 7e8d9407a..4da5419c3 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/radialChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/radialChartV2.scss @@ -70,6 +70,16 @@ opacity: 0.3; } + // Ensure proper hover states for radial bars + .recharts-radial-bar { + .recharts-radial-bar-sector { + transition: + opacity 0.2s ease-in-out, + stroke 0.2s ease-in-out, + stroke-width 0.2s ease-in-out; + } + } + // Label positioning and styling .recharts-label { font-weight: 500; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx index 6354d7afa..08af0f4da 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx @@ -109,7 +109,7 @@ const meta: Meta> = { control: "radio", options: ["semicircle", "circular"], table: { - defaultValue: { summary: "semicircle" }, + defaultValue: { summary: "circular" }, category: "Appearance", }, }, @@ -142,21 +142,12 @@ const meta: Meta> = { category: "Display", }, }, - label: { - description: "Whether to display labels on the radial bars", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, grid: { description: "Whether to display the polar grid", control: "boolean", table: { type: { summary: "boolean" }, - defaultValue: { summary: "true" }, + defaultValue: { summary: "false" }, category: "Display", }, }, @@ -216,12 +207,11 @@ export const RadialChartV2Demo: Story = { categoryKey: "month", dataKey: "value", theme: "ocean", - variant: "semicircle", + variant: "circular", format: "number", legend: true, legendVariant: "stacked", - label: true, - grid: true, + grid: false, isAnimationActive: true, cornerRadius: 10, useGradients: false, @@ -247,8 +237,7 @@ export const RadialChartV2Circular: Story = { format: "number", legend: true, legendVariant: "stacked", - label: true, - grid: true, + grid: false, isAnimationActive: true, cornerRadius: 10, useGradients: false, @@ -278,11 +267,10 @@ export const RadialChartV2WithGradients: Story = { categoryKey: "month", dataKey: "value", theme: "sunset", - variant: "semicircle", + variant: "circular", format: "percentage", legend: true, legendVariant: "stacked", - label: true, grid: false, isAnimationActive: true, cornerRadius: 15, @@ -317,8 +305,7 @@ export const RadialChartV2WithCarousel: Story = { format: "number", legend: true, legendVariant: "stacked", - label: false, // Disable labels for cleaner look with many data points - grid: true, + grid: false, isAnimationActive: true, cornerRadius: 8, useGradients: false, @@ -348,11 +335,10 @@ export const RadialChartV2Minimal: Story = { categoryKey: "month", dataKey: "value", theme: "orchid", - variant: "semicircle", + variant: "circular", format: "number", legend: false, legendVariant: "default", - label: false, grid: false, isAnimationActive: true, cornerRadius: 5, @@ -370,7 +356,7 @@ export const RadialChartV2Minimal: Story = { docs: { description: { story: - "This example shows a minimal radial chart without legend, labels, or grid for clean presentation.", + "This example shows a minimal radial chart without legend or grid for clean presentation.", }, }, }, diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts index b226a4cd1..288eccafe 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts @@ -60,22 +60,15 @@ export const calculatePercentage = (value: number, total: number): number => { * Calculates dimensions for radial charts based on container size * @param width - The container width * @param variant - The chart variant ('semicircle' or 'circular') - * @param hasLabel - Whether the chart has labels * @returns Object containing outer and inner radius values */ export const calculateRadialChartDimensions = ( width: number, variant: "semicircle" | "circular", - hasLabel: boolean = false, ): RadialChartDimensions => { const baseRadiusPercentage = 0.4; // 40% of container width let outerRadius = Math.round(width * baseRadiusPercentage); - // Adjust for label presence - if (hasLabel) { - outerRadius = Math.round(outerRadius * 0.9); - } - // Set minimum and maximum bounds for radius outerRadius = Math.max(50, Math.min(outerRadius, width / 2 - 10)); @@ -233,37 +226,6 @@ export const createRadialEventHandlers = ( }; }; -// ========================================== -// Label Formatting Utilities -// ========================================== - -/** - * Helper function to format label values - * @param value - The value to format - * @param format - The format type ('percentage' or 'number') - * @returns Formatted string - */ -export const formatRadialLabel = ( - value: string | number, - format: "percentage" | "number", -): string => { - if (format === "percentage") { - return `${value}%`; - } - // For number format, just truncate if too long - const stringValue = String(value); - return stringValue.length > 8 ? `${stringValue.slice(0, 8)}...` : stringValue; -}; - -/** - * Helper function to calculate font size based on data length - * @param dataLength - The number of data items - * @returns Font size number - */ -export const getRadialFontSize = (dataLength: number): number => { - return dataLength <= 5 ? 12 : 7; -}; - // ========================================== // Backward compatibility - keeping old function names // ========================================== From 8a9a1ad5c710c8de016db7815ea177b10f5a0965 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 23 Jun 2025 18:00:02 +0530 Subject: [PATCH 114/190] fix(BarChartUtils): ensure snap position handling is safe Updated the snap position handling in the findNearestSnapPosition function to use non-null assertion for snapPosition. This change improves type safety by ensuring that snapPosition is defined before comparison, enhancing the robustness of the function. --- .../src/components/Charts/BarCharts/utils/BarChartUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts index 9109c62d8..4e2a23edd 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts @@ -257,8 +257,8 @@ const findNearestSnapPosition = ( // Find current position index let currentIndex = 0; for (let i = 0; i < snapPositions.length; i++) { - const snapPosition = snapPositions[i]; - if (snapPosition !== undefined && currentScroll >= snapPosition) { + const snapPosition = snapPositions[i]!; + if (currentScroll >= snapPosition) { currentIndex = i; } else { break; From cc8aef469a2550c2b966abc3c52669327d92fa83 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 23 Jun 2025 18:01:41 +0530 Subject: [PATCH 115/190] refactor(Charts): move tooltip styles to PortalTooltip component This commit refactors the tooltip styles by moving them from charts.scss to a new portalTooltip.scss file. The changes include the complete transfer of tooltip-related styles, ensuring better organization and maintainability of the code. The original tooltip styles are commented out in charts.scss for future reference and potential removal after review. --- .../src/components/Charts/charts.scss | 231 +++++++++--------- .../shared/PortalTooltip/portalTooltip.scss | 121 +++++++++ 2 files changed, 237 insertions(+), 115 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index 09c2d2413..a8aaafdd4 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -67,123 +67,124 @@ // we have 2 types of tooltips, one is the default tooltip and the other is the portal tooltip. // both are using this style. // Tooltip styles - &-tooltip { - display: grid; - align-items: start; - min-width: 128px; - gap: cssUtils.$spacing-xs; - padding: cssUtils.$spacing-xs; - color: cssUtils.$primary-text; - @include cssUtils.typography(label, default); - border-radius: cssUtils.$rounded-s; - border: 1px solid cssUtils.$stroke-default; - background-color: cssUtils.$bg-container; - box-shadow: cssUtils.$shadow-s; - text-transform: capitalize; - - &-label { - @include cssUtils.typography(label, default); - } + // this is moved to portalTooltip.scss file once review is done remove it + // &-tooltip { + // display: grid; + // align-items: start; + // min-width: 128px; + // gap: cssUtils.$spacing-xs; + // padding: cssUtils.$spacing-xs; + // color: cssUtils.$primary-text; + // @include cssUtils.typography(label, default); + // border-radius: cssUtils.$rounded-s; + // border: 1px solid cssUtils.$stroke-default; + // background-color: cssUtils.$bg-container; + // box-shadow: cssUtils.$shadow-s; + // text-transform: capitalize; - &-label-heavy { - @include cssUtils.typography(label, heavy); - } + // &-label { + // @include cssUtils.typography(label, default); + // } - // this is the content of the tooltip, where the items of the of each data point is rendered, - // todo: remove commented style once the review is done. - - &-content { - display: grid; - align-items: start; - min-width: 128px; - gap: cssUtils.$spacing-xs; - padding: cssUtils.$spacing-xs; - color: cssUtils.$primary-text; - @include cssUtils.typography(label, default); - // border-radius: cssUtils.$rounded-s; - // border: 1px solid cssUtils.$stroke-default; - background-color: cssUtils.$bg-container; - box-shadow: cssUtils.$shadow-s; - text-transform: capitalize; - - &-item { - display: flex; - width: 100%; - flex-wrap: wrap; - gap: cssUtils.$spacing-xs; - align-items: stretch; - &--dot { - align-items: center; - } - - svg { - height: 10px; - width: 10px; - color: cssUtils.$primary-text; - } - } - // this portion is responsible for styling the colored box or other indicators like line, dashed, etc. - &-indicator { - flex-shrink: 0; - border-radius: cssUtils.$rounded-2xs; - - &--dot { - height: 10px; - width: 10px; - background-color: var(--color-bg); - border-color: var(--color-border); - } - - &--line { - width: 4px; - background-color: var(--color-bg); - border-color: var(--color-border); - } - - &--dashed { - width: 0; - border: 1.5px dashed var(--color-border); - background-color: transparent; - } - - &--nested-dashed { - margin: cssUtils.$spacing-3xs 0; - } - } - - &-value-wrapper { - display: flex; - flex: 1; - justify-content: space-between; - line-height: 1; - - &--nested { - align-items: flex-end; - } - - &--standard { - align-items: center; - } - } - - &-label { - display: grid; - gap: cssUtils.$spacing-xs; - color: cssUtils.$primary-text; - @include cssUtils.typography(label, default); - } - - &-value { - font-variant-numeric: tabular-nums; - color: cssUtils.$primary-text; - @include cssUtils.typography(label, default); - - &--percentage { - padding-left: cssUtils.$spacing-s; - } - } - } - } + // &-label-heavy { + // @include cssUtils.typography(label, heavy); + // } + + // // this is the content of the tooltip, where the items of the of each data point is rendered, + // // todo: remove commented style once the review is done. + + // &-content { + // display: grid; + // align-items: start; + // min-width: 128px; + // gap: cssUtils.$spacing-xs; + // padding: cssUtils.$spacing-xs; + // color: cssUtils.$primary-text; + // @include cssUtils.typography(label, default); + // // border-radius: cssUtils.$rounded-s; + // // border: 1px solid cssUtils.$stroke-default; + // background-color: cssUtils.$bg-container; + // box-shadow: cssUtils.$shadow-s; + // text-transform: capitalize; + + // &-item { + // display: flex; + // width: 100%; + // flex-wrap: wrap; + // gap: cssUtils.$spacing-xs; + // align-items: stretch; + // &--dot { + // align-items: center; + // } + + // svg { + // height: 10px; + // width: 10px; + // color: cssUtils.$primary-text; + // } + // } + // // this portion is responsible for styling the colored box or other indicators like line, dashed, etc. + // &-indicator { + // flex-shrink: 0; + // border-radius: cssUtils.$rounded-2xs; + + // &--dot { + // height: 10px; + // width: 10px; + // background-color: var(--color-bg); + // border-color: var(--color-border); + // } + + // &--line { + // width: 4px; + // background-color: var(--color-bg); + // border-color: var(--color-border); + // } + + // &--dashed { + // width: 0; + // border: 1.5px dashed var(--color-border); + // background-color: transparent; + // } + + // &--nested-dashed { + // margin: cssUtils.$spacing-3xs 0; + // } + // } + + // &-value-wrapper { + // display: flex; + // flex: 1; + // justify-content: space-between; + // line-height: 1; + + // &--nested { + // align-items: flex-end; + // } + + // &--standard { + // align-items: center; + // } + // } + + // &-label { + // display: grid; + // gap: cssUtils.$spacing-xs; + // color: cssUtils.$primary-text; + // @include cssUtils.typography(label, default); + // } + + // &-value { + // font-variant-numeric: tabular-nums; + // color: cssUtils.$primary-text; + // @include cssUtils.typography(label, default); + + // &--percentage { + // padding-left: cssUtils.$spacing-s; + // } + // } + // } + // } // this is getting used in DefaultLegend.tsx // now moved to defaultLegend.scss file once review is done remove it diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss index 8b25a86da..c1945400d 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss @@ -1,5 +1,126 @@ +@use "../../../../cssUtils.scss" as cssUtils; + .crayon-portal-tooltip { pointer-events: none; z-index: 1000; position: absolute; } + +.crayon-chart-tooltip { + display: grid; + align-items: start; + min-width: 128px; + max-width: 240px; + gap: cssUtils.$spacing-xs; + padding: cssUtils.$spacing-xs; + color: cssUtils.$primary-text; + @include cssUtils.typography(label, default); + border-radius: cssUtils.$rounded-s; + border: 1px solid cssUtils.$stroke-default; + background-color: cssUtils.$bg-container; + box-shadow: cssUtils.$shadow-s; + text-transform: capitalize; + + &-label { + @include cssUtils.typography(label, default); + } + + &-label-heavy { + @include cssUtils.typography(label, heavy); + } + + // this is the content of the tooltip, where the items of the of each data point is rendered, + // todo: remove commented style once the review is done. + + &-content { + display: grid; + align-items: start; + min-width: 128px; + gap: cssUtils.$spacing-xs; + padding: cssUtils.$spacing-xs; + color: cssUtils.$primary-text; + @include cssUtils.typography(label, default); + // border-radius: cssUtils.$rounded-s; + // border: 1px solid cssUtils.$stroke-default; + background-color: cssUtils.$bg-container; + box-shadow: cssUtils.$shadow-s; + text-transform: capitalize; + + &-item { + display: flex; + width: 100%; + flex-wrap: wrap; + gap: cssUtils.$spacing-xs; + align-items: stretch; + &--dot { + align-items: center; + } + + svg { + height: 10px; + width: 10px; + color: cssUtils.$primary-text; + } + } + // this portion is responsible for styling the colored box or other indicators like line, dashed, etc. + &-indicator { + flex-shrink: 0; + border-radius: cssUtils.$rounded-2xs; + + &--dot { + height: 10px; + width: 10px; + background-color: var(--color-bg); + border-color: var(--color-border); + } + + &--line { + width: 4px; + background-color: var(--color-bg); + border-color: var(--color-border); + } + + &--dashed { + width: 0; + border: 1.5px dashed var(--color-border); + background-color: transparent; + } + + &--nested-dashed { + margin: cssUtils.$spacing-3xs 0; + } + } + + &-value-wrapper { + display: flex; + flex: 1; + justify-content: space-between; + line-height: 1; + + &--nested { + align-items: flex-end; + } + + &--standard { + align-items: center; + } + } + + &-label { + display: grid; + gap: cssUtils.$spacing-xs; + color: cssUtils.$primary-text; + @include cssUtils.typography(label, default); + } + + &-value { + font-variant-numeric: tabular-nums; + color: cssUtils.$primary-text; + @include cssUtils.typography(label, default); + + &--percentage { + padding-left: cssUtils.$spacing-s; + } + } + } +} From cfe16460a897d89e4177a2517df773a95526aefb Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 23 Jun 2025 20:59:21 +0530 Subject: [PATCH 116/190] feat(CustomTooltip): enhance tooltip content rendering and styling This commit improves the CustomTooltipContent component by adding support for custom label formatting and enhancing the rendering of tooltip items. Key changes include: - Introduced a new utility function, tooltipNumberFormatter, for consistent number formatting. - Updated the rendering logic to handle cases with fewer payload items, improving the display of tooltip content. - Enhanced SCSS styles for better layout and visual separation of tooltip items, including new classes for vertical alignment and item separators. These changes aim to provide a more flexible and visually appealing tooltip experience in charts. --- .../PortalTooltip/CustomTooltipContent.tsx | 84 ++++++++++++++++++- .../shared/PortalTooltip/portalTooltip.scss | 28 ++++++- .../shared/PortalTooltip/utils/index.ts | 21 +++++ 3 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/shared/PortalTooltip/utils/index.ts diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx index 66fd4e08a..60d22b00a 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx @@ -3,6 +3,7 @@ import { forwardRef, memo, useMemo } from "react"; import * as RechartsPrimitive from "recharts"; import { ChartStyle, getPayloadConfigFromPayload, useChart } from "../../../Charts/Charts"; import { FloatingUIPortal } from "./FloatingUIPortal"; +import { tooltipNumberFormatter } from "./utils"; const DEFAULT_INDICATOR = "dot" as const; @@ -56,6 +57,8 @@ export const CustomTooltipContent = memo( const value = !labelKey && typeof label === "string" ? config[label]?.label || label : itemConfig?.label; + // this is active when we pass a labelFormatter prop to the recharts tooltip + // normally we would not need this, but it's useful for customizing the label if (labelFormatter) { return (
@@ -81,6 +84,82 @@ export const CustomTooltipContent = memo( return []; } + if (payload.length <= 2) { + return payload.map((item, index) => { + const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); + const indicatorColor = (color ?? item.payload?.fill) || item.color; + + return ( +
+ {formatter && item?.value !== undefined && item.name ? ( + formatter(item.value, item.name, item, index, item.payload) + ) : ( + <> + {itemConfig?.icon ? ( + + ) : ( + // this is the color indicator + !hideIndicator && ( +
+ ) + )} + +
+
+ {nestLabel && tooltipLabel} + {itemConfig?.label || item.name} +
+ + {/* this is the value the number or the string */} + {item.value !== undefined && ( + + {typeof item.value === "number" + ? tooltipNumberFormatter(item.value) + : item.value} + {showPercentage ? "%" : ""} + + )} +
+ + )} +
+
+ ); + }); + } + return payload.map((item, index) => { const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); @@ -135,7 +214,9 @@ export const CustomTooltipContent = memo( showPercentage && "percentage", )} > - {typeof item.value === "number" ? item.value.toLocaleString() : item.value} + {typeof item.value === "number" + ? tooltipNumberFormatter(item.value) + : item.value} {showPercentage ? "%" : ""} )} @@ -166,6 +247,7 @@ export const CustomTooltipContent = memo( const tooltipContent = (
{!nestLabel && tooltipLabel} +
{payloadItems}
); diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss index c1945400d..8936b6df3 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss @@ -23,10 +23,12 @@ &-label { @include cssUtils.typography(label, default); + color: cssUtils.$secondary-text; } &-label-heavy { @include cssUtils.typography(label, heavy); + color: cssUtils.$secondary-text; } // this is the content of the tooltip, where the items of the of each data point is rendered, @@ -37,11 +39,9 @@ align-items: start; min-width: 128px; gap: cssUtils.$spacing-xs; - padding: cssUtils.$spacing-xs; + // padding: cssUtils.$spacing-xs; color: cssUtils.$primary-text; @include cssUtils.typography(label, default); - // border-radius: cssUtils.$rounded-s; - // border: 1px solid cssUtils.$stroke-default; background-color: cssUtils.$bg-container; box-shadow: cssUtils.$shadow-s; text-transform: capitalize; @@ -89,11 +89,15 @@ &--nested-dashed { margin: cssUtils.$spacing-3xs 0; } + &--two-items { + margin-top: 2px; + } } &-value-wrapper { display: flex; flex: 1; + gap: cssUtils.$spacing-s; justify-content: space-between; line-height: 1; @@ -104,12 +108,18 @@ &--standard { align-items: center; } + &--vertical { + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + gap: cssUtils.$spacing-2xs; + } } &-label { display: grid; gap: cssUtils.$spacing-xs; - color: cssUtils.$primary-text; + color: cssUtils.$secondary-text; @include cssUtils.typography(label, default); } @@ -122,5 +132,15 @@ padding-left: cssUtils.$spacing-s; } } + + &-item-separator { + width: 100%; + height: 1px; + background-color: cssUtils.$stroke-default; + margin: 0; + } + &-item:last-child &-item-separator { + display: none; + } } } diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/utils/index.ts b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/utils/index.ts new file mode 100644 index 000000000..6dc61a404 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/utils/index.ts @@ -0,0 +1,21 @@ +const tooltipNumberFormatter = (value: number) => { + if (value < 100000) { + return value.toLocaleString(); + } + + const units = ["", "K", "M", "B", "T"]; + let unitIndex = 0; + let scaledValue = value; + + while (scaledValue >= 1000 && unitIndex < units.length - 1) { + scaledValue /= 1000; + unitIndex++; + } + + // Format with at most 1 decimal place + const formattedValue = Math.floor(scaledValue * 10) / 10; + + return `${formattedValue}${units[unitIndex]}`; +}; + +export { tooltipNumberFormatter }; From 8a060fa2ff824c93d5a33f5232a328253d84985c Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 24 Jun 2025 19:40:27 +0530 Subject: [PATCH 117/190] fix(FloatingUIPortal): improve tooltip positioning logic This commit enhances the FloatingUIPortal component by updating the mouse position reference to use maximum safe integer values. Additionally, it refines the updatePosition function to prevent tooltip rendering when the virtual element's bounding rectangle is invalid or when the position is at the origin. These changes aim to improve the reliability and accuracy of tooltip positioning in charts. --- .../shared/PortalTooltip/FloatingUIPortal.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx index b3a808abd..2cddeef78 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx @@ -27,7 +27,7 @@ export const FloatingUIPortal: React.FC = ({ chartId, portalContainer, }) => { - const mousePositionRef = useRef({ x: 0, y: 0 }); + const mousePositionRef = useRef({ x: Number.MAX_SAFE_INTEGER, y: Number.MAX_SAFE_INTEGER }); const tooltipRef = useRef(null); const [position, setPosition] = useState({ x: 0, y: 0 }); const [isPositioned, setIsPositioned] = useState(false); @@ -64,7 +64,14 @@ export const FloatingUIPortal: React.FC = ({ // Memoize the updatePosition function to avoid recreating it const updatePosition = useCallback(async () => { - if (!virtualElement || !tooltipRef.current) return; + if ( + !virtualElement || + !tooltipRef.current || + virtualElement.getBoundingClientRect().x === Number.MAX_SAFE_INTEGER || + virtualElement.getBoundingClientRect().y === Number.MAX_SAFE_INTEGER + ) { + return; + } // https://floating-ui.com/docs/computePosition // not a synchronous function, it returns a promise. so we need to await it. @@ -73,6 +80,10 @@ export const FloatingUIPortal: React.FC = ({ middleware: [offset(offsetDistance), flip(), shift({ padding: 8 })], }); + if (x === 0 && y === 0) { + return; + } + setPosition({ x, y }); // this is to avoid the tooltip from flickering when the mouse is moving fast and the tooltip is not positioned yet initially setTimeout(() => { From ea7efc7429b86ad43bb7af780a04fb3746ce0e62 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 24 Jun 2025 19:41:20 +0530 Subject: [PATCH 118/190] feat(SideBarTooltip): add new SideBarTooltip component with styling This commit introduces the SideBarTooltip component, which includes a header with a close button and a content area for displaying tooltip information. Additionally, it adds corresponding SCSS styles to ensure proper layout and visual appeal. The new component aims to enhance the user experience in chart interactions. --- .../shared/SideBarTooltip/SideBarTooltip.tsx | 25 +++++++++++++++++++ .../Charts/shared/SideBarTooltip/index.ts | 1 + .../shared/SideBarTooltip/sideBarTooltip.scss | 12 +++++++++ 3 files changed, 38 insertions(+) create mode 100644 js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx create mode 100644 js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss diff --git a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx new file mode 100644 index 000000000..f35dc8a4d --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx @@ -0,0 +1,25 @@ +import { X } from "lucide-react"; +import { IconButton } from "../../../IconButton"; +import { useSideBarTooltip } from "../../context/SideBarTooltipContext"; + +const SideBarTooltip = () => { + const { setIsSideBarTooltipOpen } = useSideBarTooltip(); + + return ( +
+
+
+ } + size="extra-small" + onClick={() => setIsSideBarTooltipOpen(false)} + /> +
+
+
+
+
+ ); +}; + +export { SideBarTooltip }; diff --git a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/index.ts b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/index.ts new file mode 100644 index 000000000..3c3aa2cec --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/index.ts @@ -0,0 +1 @@ +export * from "./SideBarTooltip"; diff --git a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss new file mode 100644 index 000000000..1b2e7b4c5 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss @@ -0,0 +1,12 @@ +@use "../../../../cssUtils.scss" as cssUtils; + +.crayon-chart-side-bar-tooltip { + display: flex; + flex-direction: column; + min-width: 180px; + max-width: 180px; + padding: cssUtils.$spacing-xs; + background-color: cssUtils.$bg-container; + border-radius: cssUtils.$rounded-s; + border: 1px solid cssUtils.$stroke-default; +} From 87f7322fa7b6de1cb9a23801c414ba8904c698b8 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 24 Jun 2025 19:43:12 +0530 Subject: [PATCH 119/190] feat(PortalTooltip): add 'view more' styling to tooltip items This commit introduces a new SCSS style for the 'view more' section within the PortalTooltip component. The added styles include typography settings and color adjustments to enhance the visual presentation and alignment of the tooltip content. These changes aim to improve user interaction and readability in chart tooltips. --- .../Charts/shared/PortalTooltip/portalTooltip.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss index 8936b6df3..18b0a92e6 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss @@ -142,5 +142,11 @@ &-item:last-child &-item-separator { display: none; } + + &-view-more { + @include cssUtils.typography(label, default); + color: cssUtils.$secondary-text; + text-align: left; + } } } From b31d0b5e903d6cea11f2b23f325938eaba13bbf8 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 24 Jun 2025 19:44:25 +0530 Subject: [PATCH 120/190] feat(index): export SideBarTooltip from shared index file This commit updates the shared index file to include the newly created SideBarTooltip component, allowing it to be easily imported alongside other chart components. This change enhances the modularity and accessibility of the tooltip features within the chart library. --- js/packages/react-ui/src/components/Charts/shared/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/js/packages/react-ui/src/components/Charts/shared/index.ts b/js/packages/react-ui/src/components/Charts/shared/index.ts index fb2a1b3ba..7bc3ff5d3 100644 --- a/js/packages/react-ui/src/components/Charts/shared/index.ts +++ b/js/packages/react-ui/src/components/Charts/shared/index.ts @@ -2,5 +2,6 @@ export * from "./ActiveDot/ActiveDot"; export * from "./CartesianGrid/cartesianGrid"; export * from "./DefaultLegend/DefaultLegend"; export * from "./PortalTooltip"; +export * from "./SideBarTooltip"; export * from "./XAxisTick/XAxisTick"; export * from "./YAxisTick/YAxisTick"; From 1cce95751ed2a4a6c75ee2c293931ac150277736 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 24 Jun 2025 19:45:02 +0530 Subject: [PATCH 121/190] feat(SideBarTooltipContext): create context for SideBarTooltip management This commit introduces the SideBarTooltipContext, providing a context API for managing the state of the SideBarTooltip component. It includes a provider for encapsulating tooltip data and visibility state, enhancing the modularity and reusability of the tooltip functionality within the chart library. --- .../Charts/context/SideBarTooltipContext.tsx | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 js/packages/react-ui/src/components/Charts/context/SideBarTooltipContext.tsx diff --git a/js/packages/react-ui/src/components/Charts/context/SideBarTooltipContext.tsx b/js/packages/react-ui/src/components/Charts/context/SideBarTooltipContext.tsx new file mode 100644 index 000000000..5333f60f6 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/context/SideBarTooltipContext.tsx @@ -0,0 +1,52 @@ +import React, { createContext, ReactNode, useContext, useState } from "react"; + +interface ChartData { + title: string; + values: { + value: number; + label: string; + color: string; + }[]; +} + +interface SideBarTooltipContextType { + data: ChartData[]; + isSideBarTooltipOpen: boolean; + setData: (data: ChartData[]) => void; + setIsSideBarTooltipOpen: (isOpen: boolean) => void; +} + +const SideBarTooltipContext = createContext(undefined); + +interface SideBarTooltipProviderProps { + children: ReactNode; + isSideBarTooltipOpen: boolean; + setIsSideBarTooltipOpen: (isOpen: boolean) => void; +} + +export const SideBarTooltipProvider: React.FC = ({ + children, + isSideBarTooltipOpen, + setIsSideBarTooltipOpen, +}) => { + const [data, setData] = useState([]); + + const value: SideBarTooltipContextType = { + data, + isSideBarTooltipOpen, + setData, + setIsSideBarTooltipOpen, + }; + + return {children}; +}; + +export const useSideBarTooltip = (): SideBarTooltipContextType => { + const context = useContext(SideBarTooltipContext); + if (context === undefined) { + throw new Error("useSideBarTooltip must be used within a SideBarTooltipProvider"); + } + return context; +}; + +export default SideBarTooltipContext; From d2cd31d2508b2c101610634b31618c79f103e2ef Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 25 Jun 2025 12:43:33 +0530 Subject: [PATCH 122/190] feat(charts.scss): add SideBarTooltip styles to charts component This commit updates the charts.scss file to include a forward declaration for the SideBarTooltip styles, enhancing the organization of tooltip-related styles within the chart library. This addition aims to improve modularity and maintainability of the tooltip components. --- js/packages/react-ui/src/components/Charts/charts.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index a8aaafdd4..bc2a8ca37 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -20,6 +20,9 @@ // portal tooltip css @forward "./shared/PortalTooltip/portalTooltip.scss"; +// side bar tooltip css +@forward "./shared/SideBarTooltip/sideBarTooltip.scss"; + .crayon-chart { // Container styles &-container { From 9734f40eea9768d8a5a33c507273aa67b8820397 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 25 Jun 2025 19:32:32 +0530 Subject: [PATCH 123/190] feat(Charts): enhance keyTransform function for improved key formatting This commit updates the keyTransform function to enhance key formatting by replacing whitespace with hyphens, sanitizing non-alphanumeric characters, and ensuring keys do not start with a number. Additionally, it provides a fallback with a unique ID for empty keys. These changes aim to improve the consistency and usability of keys within the chart components. --- .../react-ui/src/components/Charts/Charts.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/Charts.tsx b/js/packages/react-ui/src/components/Charts/Charts.tsx index 760f83430..35b167efa 100644 --- a/js/packages/react-ui/src/components/Charts/Charts.tsx +++ b/js/packages/react-ui/src/components/Charts/Charts.tsx @@ -64,7 +64,19 @@ function useChart() { } export function keyTransform(key: string) { - return key.replaceAll(/\s/g, "-"); + return key + // Replace whitespace with hyphens + .replaceAll(/\s+/g, "-") + // Replace any character that's not alphanumeric, hyphen, or underscore with hyphen + .replaceAll(/[^a-zA-Z0-9_-]/g, "-") + // Remove multiple consecutive hyphens + .replaceAll(/-+/g, "-") + // Remove leading/trailing hyphens + .replace(/^-+|-+$/g, "") + // Ensure it doesn't start with a number (prepend 'key-' if it does) + .replace(/^(\d)/, "key-$1") + // Fallback with unique ID if the key is empty + || `key-${uniqueId()}`; } /** From fa8b008df3ef541a457b7faeade64ac35e05dac7 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 25 Jun 2025 19:33:02 +0530 Subject: [PATCH 124/190] feat(Charts): add getColorForDataKey utility function for consistent color mapping This commit introduces the getColorForDataKey function, which retrieves the color value for a specific data key based on its position in the dataKeys array. This utility ensures consistent color mapping across chart components and provides a fallback color if the data key is not found or the color is undefined. Additionally, it updates the existing getLegendItems function to use the nullish coalescing operator for fallback color handling. --- .../src/components/Charts/utils/dataUtils.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts index daab455da..004adfa3b 100644 --- a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts @@ -57,7 +57,24 @@ export const getLegendItems = ( return dataKeys.map((key, index) => ({ key, label: key, - color: colors[index] || "#000000", // Fallback color if undefined + color: colors[index] ?? "#000000", // Fallback color if undefined icon: icons[key] as React.ComponentType | undefined, })); }; + +/** + * This function returns the color value for a specific data key based on its position in the dataKeys array. + * Use this instead of payload.fill to ensure consistent color mapping. + * @param dataKey - The data key to get the color for. + * @param dataKeys - The array of all data keys in the chart. + * @param colors - The array of colors corresponding to the data keys. + * @returns The color value for the specified data key. + */ +export const getColorForDataKey = ( + dataKey: string, + dataKeys: string[], + colors: string[], +): string => { + const index = dataKeys.indexOf(dataKey); + return colors[index] ?? "#000000"; // Fallback color if dataKey not found or color undefined +}; From 56a8fc395bbcf41c9ebb3874d3c25a4b970e265b Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 25 Jun 2025 22:37:10 +0530 Subject: [PATCH 125/190] feat(Charts): refactor tooltip components and enhance cursor functionality This commit refactors the CustomCursor component by removing unnecessary memoization and updating the DefaultLegend component to use memo for performance optimization. Additionally, it enhances the CustomTooltipContent component by introducing a new layout for tooltip items, improving the rendering logic for payload items, and adding a "view more" feature for better user interaction. The FloatingUIPortal component's tooltip positioning logic is also simplified to enhance reliability. --- .../BarChartV2/components/CustomCursor.tsx | 3 +- .../shared/DefaultLegend/DefaultLegend.tsx | 17 +-- .../PortalTooltip/CustomTooltipContent.tsx | 117 ++++++------------ .../shared/PortalTooltip/FloatingUIPortal.tsx | 5 +- 4 files changed, 48 insertions(+), 94 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx index 38f4bcde1..08d288919 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx @@ -1,5 +1,6 @@ import React, { useMemo } from "react"; +// discard this is this is not used, talk with the designer first interface CustomCursorProps { x?: number; y?: number; @@ -50,4 +51,4 @@ const SimpleCursorComponent: React.FC = ({ }; // Memoize component to prevent re-renders when props haven't changed -export const SimpleCursor = React.memo(SimpleCursorComponent); +export const SimpleCursor = SimpleCursorComponent; diff --git a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx index 741350c52..873231c42 100644 --- a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx @@ -1,6 +1,6 @@ import clsx from "clsx"; import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState, memo } from "react"; import { Button } from "../../../Button/Button"; import { type LegendItem } from "../../types"; import { calculateVisibleItems, getToggleButtonText } from "./utils/defaultLegendUtils"; @@ -11,16 +11,20 @@ interface DefaultLegendProps { yAxisLabel?: React.ReactNode; xAxisLabel?: React.ReactNode; containerWidth?: number; + isExpanded: boolean; + setIsExpanded: (isExpanded: boolean) => void; } -const DefaultLegend: React.FC = ({ +const DefaultLegend: React.FC = memo(({ items, className, yAxisLabel, xAxisLabel, containerWidth, + isExpanded, + setIsExpanded, }) => { - const [isExpanded, setIsExpanded] = useState(false); + // Only memoize expensive calculations const { visibleItems, hasMoreItems } = useMemo(() => { @@ -31,10 +35,7 @@ const DefaultLegend: React.FC = ({ return isExpanded ? items : visibleItems; }, [isExpanded, items, visibleItems]); - // Reset expanded state when items change - useEffect(() => { - setIsExpanded(false); - }, [items]); + const handleToggleExpanded = () => { setIsExpanded(!isExpanded); @@ -104,7 +105,7 @@ const DefaultLegend: React.FC = ({
); -}; +}); export { DefaultLegend }; export type { DefaultLegendProps }; diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx index 60d22b00a..b23a3600c 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx @@ -1,7 +1,8 @@ import clsx from "clsx"; -import { forwardRef, memo, useMemo } from "react"; +import { forwardRef, memo, useLayoutEffect, useMemo, useState } from "react"; import * as RechartsPrimitive from "recharts"; import { ChartStyle, getPayloadConfigFromPayload, useChart } from "../../../Charts/Charts"; +import { useSideBarTooltip } from "../../context/SideBarTooltipContext"; import { FloatingUIPortal } from "./FloatingUIPortal"; import { tooltipNumberFormatter } from "./utils"; @@ -45,6 +46,17 @@ export const CustomTooltipContent = memo( } = props; const { config, id } = useChart(); + const { isSideBarTooltipOpen } = useSideBarTooltip(); + const [isGreaterThanTen, setIsGreaterThanTen] = useState( + !!(payload?.length && payload.length > 10), + ); + useLayoutEffect(() => { + if (payload?.length && payload.length > 10) { + setIsGreaterThanTen(true); + } else { + setIsGreaterThanTen(false); + } + }, [payload]); const tooltipLabel = useMemo(() => { if (hideLabel || !payload?.length) { @@ -84,83 +96,7 @@ export const CustomTooltipContent = memo( return []; } - if (payload.length <= 2) { - return payload.map((item, index) => { - const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`; - const itemConfig = getPayloadConfigFromPayload(config, item, key); - const indicatorColor = (color ?? item.payload?.fill) || item.color; - - return ( -
- {formatter && item?.value !== undefined && item.name ? ( - formatter(item.value, item.name, item, index, item.payload) - ) : ( - <> - {itemConfig?.icon ? ( - - ) : ( - // this is the color indicator - !hideIndicator && ( -
- ) - )} - -
-
- {nestLabel && tooltipLabel} - {itemConfig?.label || item.name} -
- - {/* this is the value the number or the string */} - {item.value !== undefined && ( - - {typeof item.value === "number" - ? tooltipNumberFormatter(item.value) - : item.value} - {showPercentage ? "%" : ""} - - )} -
- - )} -
-
- ); - }); - } - - return payload.map((item, index) => { + const renderPayloadItem = (item: any, index: number, isTwoItemsLayout: boolean) => { const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); const indicatorColor = (color ?? item.payload?.fill) || item.color; @@ -170,7 +106,7 @@ export const CustomTooltipContent = memo( key={`${item.dataKey}-${index}`} className={clsx( "crayon-chart-tooltip-content-item", - indicator === DEFAULT_INDICATOR && "crayon-chart-tooltip-content-item--dot", + !isTwoItemsLayout && indicator === DEFAULT_INDICATOR && "crayon-chart-tooltip-content-item--dot", )} > {formatter && item?.value !== undefined && item.name ? ( @@ -185,6 +121,7 @@ export const CustomTooltipContent = memo( className={clsx( "crayon-chart-tooltip-content-indicator", `crayon-chart-tooltip-content-indicator--${indicator}`, + isTwoItemsLayout && "crayon-chart-tooltip-content-indicator--two-items", )} style={ { @@ -195,9 +132,11 @@ export const CustomTooltipContent = memo( /> ) )} +
{itemConfig?.label || item.name}
+ {item.value !== undefined && ( )} +
); - }); + }; + + // Handle two items layout + if (payload.length <= 2) { + return payload.map((item, index) => renderPayloadItem(item, index, true)); + } + + // Handle regular layout with potential truncation + const morphPayload = isGreaterThanTen ? payload.slice(0, 10) : payload; + return morphPayload.map((item, index) => renderPayloadItem(item, index, false)); }, [ payload, nameKey, @@ -237,10 +187,11 @@ export const CustomTooltipContent = memo( nestLabel, tooltipLabel, showPercentage, + isGreaterThanTen, ]); // Early return for inactive or empty payload - moved after all hooks - if (!active || !payload?.length) { + if (!active || !payload?.length || isSideBarTooltipOpen) { return null; } @@ -249,6 +200,10 @@ export const CustomTooltipContent = memo( {!nestLabel && tooltipLabel}
{payloadItems}
+ {isGreaterThanTen &&
} + {isGreaterThanTen && ( +
Click to view all
+ )}
); diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx index 2cddeef78..c63c469ce 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/FloatingUIPortal.tsx @@ -85,10 +85,7 @@ export const FloatingUIPortal: React.FC = ({ } setPosition({ x, y }); - // this is to avoid the tooltip from flickering when the mouse is moving fast and the tooltip is not positioned yet initially - setTimeout(() => { - setIsPositioned(true); - }, 20); + setIsPositioned(true); }, [virtualElement, placement, offsetDistance]); // Memoize the mouse move handler From 45602e2a9675099ca800f4d3065b3f65742852ee Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 26 Jun 2025 01:30:02 +0530 Subject: [PATCH 126/190] feat(Charts): implement SideBarTooltip functionality in Area and Bar charts This commit introduces the SideBarTooltip component to both AreaChartV2 and BarChartV2, enhancing user interaction by displaying detailed data on click events. It includes state management for tooltip visibility and data, along with styling adjustments for better presentation. The tooltip now features a header with a close button and a structured content layout, improving the overall user experience in chart interactions. Additionally, the context for managing tooltip state is refined for better modularity. --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 392 ++++++++++-------- .../AreaCharts/AreaChartV2/areaChartV2.scss | 6 + .../BarCharts/BarChartV2/BarChartV2.tsx | 387 +++++++++-------- .../Charts/BarCharts/BarChartV2/barChar.scss | 12 + .../Charts/context/SideBarTooltipContext.tsx | 14 +- .../shared/SideBarTooltip/SideBarTooltip.tsx | 63 ++- .../shared/SideBarTooltip/sideBarTooltip.scss | 91 ++++ 7 files changed, 610 insertions(+), 355 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index 4f115badd..e873d37af 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -1,17 +1,20 @@ import clsx from "clsx"; import { ChevronLeft, ChevronRight } from "lucide-react"; -import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Area, AreaChart as RechartsAreaChart, XAxis, YAxis } from "recharts"; +import { useId } from "../../../../polyfills"; import { IconButton } from "../../../IconButton"; +import { ChartConfig, ChartContainer, ChartTooltip, keyTransform } from "../../Charts"; +import { SideBarChartData, SideBarTooltipProvider } from "../../context/SideBarTooltipContext"; import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, - keyTransform, -} from "../../Charts"; -import { ActiveDot, cartesianGrid, DefaultLegend, XAxisTick, YAxisTick } from "../../shared"; -import { CustomTooltipContent } from "../../shared/PortalTooltip"; + ActiveDot, + cartesianGrid, + CustomTooltipContent, + DefaultLegend, + SideBarTooltip, + XAxisTick, + YAxisTick, +} from "../../shared"; import { LegendItem } from "../../types"; import { findNearestSnapPosition, @@ -21,10 +24,20 @@ import { getXAxisTickPositionData, } from "../../utils/BarAndLineUtils/AreaAndLineUtils"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; -import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; +import { + getChartConfig, + getColorForDataKey, + getDataKeys, + getLegendItems, +} from "../../utils/dataUtils"; import { getYAxisTickFormatter } from "../../utils/styleUtils"; import { AreaChartV2Data, AreaChartVariant } from "../types"; +// this a technic to get the type of the onClick event of the bar chart +// we need to do this because the onClick event type is not exported by recharts +type AreaChartOnClick = React.ComponentProps["onClick"]; +type AreaClickData = Parameters>[0]; + export interface AreaChartV2Props { data: T; categoryKey: keyof T[number]; @@ -40,8 +53,6 @@ export interface AreaChartV2Props { className?: string; height?: number; width?: number; - onAreaClick?: (payload: any) => void; - floatingTooltip?: boolean; } const Y_AXIS_WIDTH = 40; // Width of Y-axis chart when shown @@ -61,8 +72,6 @@ const AreaChartV2Component = ({ className, height, width, - onAreaClick, - floatingTooltip = true, }: AreaChartV2Props) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); @@ -82,6 +91,12 @@ const AreaChartV2Component = ({ const [containerWidth, setContainerWidth] = useState(0); const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(false); + const [isSideBarTooltipOpen, setIsSideBarTooltipOpen] = useState(false); + const [isLegendExpanded, setIsLegendExpanded] = useState(false); + const [sideBarTooltipData, setSideBarTooltipData] = useState({ + title: "", + values: [], + }); // Use provided width or observed width const effectiveWidth = useMemo(() => { @@ -176,6 +191,11 @@ const AreaChartV2Component = ({ updateScrollState(); }, [effectiveWidth, dataWidth, updateScrollState]); + useEffect(() => { + setIsSideBarTooltipOpen(false); + setIsLegendExpanded(false); + }, [dataKeys]); + // Add scroll event listener to update button states useEffect(() => { const mainContainer = mainContainerRef.current; @@ -201,177 +221,201 @@ const AreaChartV2Component = ({ const gradientId = useMemo(() => `area-chart-gradient-${id}`, [id]); + const onAreaClick = useCallback( + (data: AreaClickData) => { + if (data?.activePayload?.length && data.activePayload.length > 10) { + setIsSideBarTooltipOpen(true); + setSideBarTooltipData({ + title: data.activeLabel as string, + values: data.activePayload.map((payload) => ({ + value: payload.value as number, + label: payload.name || payload.dataKey, + color: getColorForDataKey(payload.dataKey, dataKeys, colors), + })), + }); + } + }, + [dataKeys, colors], + ); + return ( -
-
- {showYAxis && ( -
- {/* Y-axis only chart - synchronized with main chart */} - - +
+ {showYAxis && ( +
+ {/* Y-axis only chart - synchronized with main chart */} + } - /> - {/* Invisible area to maintain scale synchronization */} - {dataKeys.map((key) => { - return ( - - ); - })} - -
- )} -
- - + } + /> + {/* Invisible area to maintain scale synchronization */} + {dataKeys.map((key) => { + return ( + + ); + })} + +
+ )} +
+ - {grid && cartesianGrid()} - - } - orientation="bottom" - padding={{ - left: 20, - right: 20, + + syncId={chartSyncID} + onClick={onAreaClick} + > + {grid && cartesianGrid()} + + } + orientation="bottom" + padding={{ + left: 20, + right: 20, + }} + /> - {floatingTooltip ? ( } offset={15} /> - ) : ( - } /> - )} - {dataKeys.map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - return ( - - - - - - - ); - })} - - {dataKeys.map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - return ( - } - dot={false} - isAnimationActive={isAnimationActive} - /> - ); - })} - - + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + return ( + + + + + + + ); + })} + + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + return ( + } + dot={false} + isAnimationActive={isAnimationActive} + /> + ); + })} + + +
+ {isSideBarTooltipOpen && }
-
- {/* if the data width is greater than the effective width, then show the scroll buttons */} - {dataWidth > effectiveWidth && ( -
- } - variant="secondary" - onClick={scrollLeft} - size="extra-small" - disabled={!canScrollLeft} - /> - } - variant="secondary" - size="extra-small" - onClick={scrollRight} - disabled={!canScrollRight} + {/* if the data width is greater than the effective width, then show the scroll buttons */} + {dataWidth > effectiveWidth && ( +
+ } + variant="secondary" + onClick={scrollLeft} + size="extra-small" + disabled={!canScrollLeft} + /> + } + variant="secondary" + size="extra-small" + onClick={scrollRight} + disabled={!canScrollRight} + /> +
+ )} + {legend && ( + -
- )} - {legend && ( - - )} -
+ )} +
+ ); }; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss index 07d7db0a1..5656c78ad 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss @@ -53,4 +53,10 @@ button.crayon-area-chart-scroll-button { cursor: not-allowed; transition: visibility 0.1s linear; } + + &--SideBarTooltip { + top: -15px; + transform: translateY(-50%); + right: 185px; + } } diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx index 1fd5569ce..990eb29ed 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx @@ -1,26 +1,28 @@ import clsx from "clsx"; import { ChevronLeft, ChevronRight } from "lucide-react"; -import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Bar, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; +import { useId } from "../../../../polyfills"; import { IconButton } from "../../../IconButton"; import { useTheme } from "../../../ThemeProvider"; -import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, - keyTransform, -} from "../../Charts"; +import { ChartConfig, ChartContainer, ChartTooltip, keyTransform } from "../../Charts"; +import { SideBarChartData, SideBarTooltipProvider } from "../../context/SideBarTooltipContext"; import { cartesianGrid, CustomTooltipContent, DefaultLegend, + SideBarTooltip, XAxisTick, YAxisTick, } from "../../shared"; import { type LegendItem } from "../../types"; import { getDistributedColors, getPalette, type PaletteName } from "../../utils/PalletUtils"; -import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; +import { + getChartConfig, + getColorForDataKey, + getDataKeys, + getLegendItems, +} from "../../utils/dataUtils"; import { getYAxisTickFormatter } from "../../utils/styleUtils"; import { BarChartV2Data, BarChartVariant } from "../types"; import { @@ -32,9 +34,12 @@ import { getSnapPositions, getWidthOfData, } from "../utils/BarChartUtils"; -import { SimpleCursor } from "./components/CustomCursor"; import { LineInBarShape } from "./components/LineInBarShape"; +// this a technic to get the type of the onClick event of the bar chart +// we need to do this because the onClick event type is not exported by recharts +type BarChartOnClick = React.ComponentProps["onClick"]; +type BarClickData = Parameters>[0]; export interface BarChartPropsV2 { data: T; categoryKey: keyof T[number]; @@ -47,9 +52,7 @@ export interface BarChartPropsV2 { showYAxis?: boolean; xAxisLabel?: React.ReactNode; yAxisLabel?: React.ReactNode; - onBarsClick?: (data: any) => void; legend?: boolean; - floatingTooltip?: boolean; className?: string; height?: number; width?: number; @@ -73,9 +76,7 @@ const BarChartV2Component = ({ showYAxis = false, xAxisLabel, yAxisLabel, - onBarsClick, legend = false, - floatingTooltip = true, className, height, width, @@ -99,6 +100,12 @@ const BarChartV2Component = ({ const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(false); const [hoveredCategory, setHoveredCategory] = useState(null); + const [isLegendExpanded, setIsLegendExpanded] = useState(false); + const [isSideBarTooltipOpen, setIsSideBarTooltipOpen] = useState(false); + const [sideBarTooltipData, setSideBarTooltipData] = useState({ + title: "", + values: [], + }); // Use provided width or observed width const effectiveWidth = useMemo(() => { @@ -196,6 +203,11 @@ const BarChartV2Component = ({ updateScrollState(); }, [effectiveWidth, dataWidth, updateScrollState]); + useEffect(() => { + setIsSideBarTooltipOpen(false); + setIsLegendExpanded(false); + }, [dataKeys]); + // Add scroll event listener to update button states useEffect(() => { const mainContainer = mainContainerRef.current; @@ -245,170 +257,211 @@ const BarChartV2Component = ({ return "rgba(0, 0, 0, 0.3)"; }, [mode]); + const onBarsClick = useCallback( + (data: BarClickData) => { + if (data?.activePayload?.length && data.activePayload.length > 10) { + setIsSideBarTooltipOpen(true); + setSideBarTooltipData({ + title: data.activeLabel as string, + values: data.activePayload.map((payload) => ({ + value: payload.value as number, + label: payload.name || payload.dataKey, + color: getColorForDataKey(payload.dataKey, dataKeys, colors), + })), + }); + } + }, + [dataKeys, colors], + ); + return ( -
-
- {showYAxis && ( -
- {/* Y-axis only chart - synchronized with main chart */} - - +
+ {showYAxis && ( +
+ {/* Y-axis only chart - synchronized with main chart */} + } - /> - {/* Invisible bars to maintain scale synchronization */} - {dataKeys.map((key) => { - return ( - - ); - })} - -
- )} -
- - + } + /> + {/* Invisible bars to maintain scale synchronization */} + {dataKeys.map((key) => { + return ( + + ); + })} + +
+ )} +
+ - {grid && cartesianGrid()} - } - orientation="bottom" - // gives the padding on the 2 sides see the function for reference - padding={padding} - /> - {/* Y-axis is rendered in the separate synchronized chart */} - {floatingTooltip ? ( + + {grid && cartesianGrid()} + } + orientation="bottom" + // gives the padding on the 2 sides see the function for reference + padding={padding} + /> + {/* Y-axis is rendered in the separate synchronized chart */} + } + // cursor={} + cursor={{ + fill: "var(--crayon-sunk-fills)", + stroke: "var(--crayon-stroke-default)", + opacity: 0.4, + strokeWidth: 1, + }} content={} offset={15} /> - ) : ( - } content={} /> - )} - {dataKeys.map((key, index) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - // const secondaryColor = `var(--color-${transformedKey}-secondary)`; - const isFirstInStack = index === 0; - const isLastInStack = index === dataKeys.length - 1; - - return ( - - } - /> - ); - })} - - + + {dataKeys.map((key, index) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + const isFirstInStack = index === 0; + const isLastInStack = index === dataKeys.length - 1; + + return ( + + } + /> + ); + })} + + +
+ {isSideBarTooltipOpen && }
-
- {/* if the data width is greater than the effective width, then show the scroll buttons */} - {dataWidth > effectiveWidth && ( -
- } - variant="secondary" - onClick={scrollLeft} - size="extra-small" - disabled={!canScrollLeft} - /> - } - variant="secondary" - size="extra-small" - onClick={scrollRight} - disabled={!canScrollRight} + {/* if the data width is greater than the effective width, then show the scroll buttons */} + {dataWidth > effectiveWidth && ( +
+ } + variant="secondary" + onClick={scrollLeft} + size="extra-small" + disabled={!canScrollLeft} + /> + } + variant="secondary" + size="extra-small" + onClick={scrollRight} + disabled={!canScrollRight} + /> +
+ )} + {legend && ( + -
- )} - {legend && ( - - )} -
+ )} +
+ ); }; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss index 645640d35..feef9cce6 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss @@ -42,4 +42,16 @@ transform: translateY(-50%); right: 0px; } + + &--disabled { + visibility: hidden; + cursor: not-allowed; + transition: visibility 0.1s linear; + } + + &--SideBarTooltip { + top: -15px; + transform: translateY(-50%); + right: 185px; + } } diff --git a/js/packages/react-ui/src/components/Charts/context/SideBarTooltipContext.tsx b/js/packages/react-ui/src/components/Charts/context/SideBarTooltipContext.tsx index 5333f60f6..273db1c71 100644 --- a/js/packages/react-ui/src/components/Charts/context/SideBarTooltipContext.tsx +++ b/js/packages/react-ui/src/components/Charts/context/SideBarTooltipContext.tsx @@ -1,6 +1,6 @@ -import React, { createContext, ReactNode, useContext, useState } from "react"; +import React, { createContext, ReactNode, useContext } from "react"; -interface ChartData { +export interface SideBarChartData { title: string; values: { value: number; @@ -10,9 +10,9 @@ interface ChartData { } interface SideBarTooltipContextType { - data: ChartData[]; + data: SideBarChartData; isSideBarTooltipOpen: boolean; - setData: (data: ChartData[]) => void; + setData: (data: SideBarChartData) => void; setIsSideBarTooltipOpen: (isOpen: boolean) => void; } @@ -22,15 +22,17 @@ interface SideBarTooltipProviderProps { children: ReactNode; isSideBarTooltipOpen: boolean; setIsSideBarTooltipOpen: (isOpen: boolean) => void; + data: SideBarChartData; + setData: (data: SideBarChartData) => void; } export const SideBarTooltipProvider: React.FC = ({ children, isSideBarTooltipOpen, setIsSideBarTooltipOpen, + data, + setData, }) => { - const [data, setData] = useState([]); - const value: SideBarTooltipContextType = { data, isSideBarTooltipOpen, diff --git a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx index f35dc8a4d..b450d3997 100644 --- a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx @@ -1,25 +1,72 @@ +import React, { useCallback, useMemo } from "react"; import { X } from "lucide-react"; import { IconButton } from "../../../IconButton"; import { useSideBarTooltip } from "../../context/SideBarTooltipContext"; +import { tooltipNumberFormatter } from "../PortalTooltip/utils"; -const SideBarTooltip = () => { - const { setIsSideBarTooltipOpen } = useSideBarTooltip(); +interface SideBarTooltipProps { + height: number; +} + +// Utility function for capitalizing strings +const capitalizeString = (str: string): string => { + return str.charAt(0).toUpperCase() + str.slice(1); +}; + +const SideBarTooltip = React.memo(({ height }: SideBarTooltipProps) => { + const { setIsSideBarTooltipOpen, data } = useSideBarTooltip(); + + const handleClose = useCallback(() => { + setIsSideBarTooltipOpen(false); + }, [setIsSideBarTooltipOpen]); + + const processedValues = useMemo(() => { + return data.values?.map((value, index) => ({ + ...value, + capitalizedLabel: capitalizeString(value.label), + isLast: index === data.values.length - 1, + })); + }, [data.values]); + + const title = data.title; return ( -
+
-
+
{title}
} size="extra-small" - onClick={() => setIsSideBarTooltipOpen(false)} + onClick={handleClose} + className="crayon-chart-side-bar-tooltip-close-button" />
-
-
+
+
+ {processedValues?.map((value, index) => ( +
+
+
+
+ {value.capitalizedLabel} +
+
+ {tooltipNumberFormatter(value.value)} +
+
+ {!value.isLast && ( +
+ )} +
+ ))}
); -}; +}); + +SideBarTooltip.displayName = "SideBarTooltip"; export { SideBarTooltip }; diff --git a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss index 1b2e7b4c5..b196c4bae 100644 --- a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss +++ b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss @@ -5,8 +5,99 @@ flex-direction: column; min-width: 180px; max-width: 180px; + overflow: hidden; padding: cssUtils.$spacing-xs; background-color: cssUtils.$bg-container; border-radius: cssUtils.$rounded-s; border: 1px solid cssUtils.$stroke-default; + + + &-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-shrink: 0; + } + + &-close-button { + flex-shrink: 0; + } + + &-title { + @include cssUtils.typography(label, default); + } + + &-content { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: cssUtils.$spacing-xs; + min-height: 0; + &:focus { + outline: none; + } + &:focus-visible { + outline: none; + } + + // Custom scrollbar styling + scrollbar-width: thin; // Firefox - closest option (can't set exact px) + scrollbar-color: cssUtils.$stroke-default transparent; // Firefox - thumb and track colors + + // Custom scrollbar for Webkit browsers (Chrome, Safari, newer Edge) + &::-webkit-scrollbar { + width: 2px; // Track width + margin-right: 2px; // Offset to right + } + + &::-webkit-scrollbar-track { + background: transparent; + border-radius: 0; + margin-right: 2px; // Additional offset for track + } + + &::-webkit-scrollbar-thumb { + background-color: cssUtils.$stroke-default; + border-radius: 1px; + width: 1.5px; // Thumb width (though width is constrained by scrollbar width) + border: 0.25px solid transparent; // Creates the 1.5px effect within 2px track + background-clip: content-box; + } + + &::-webkit-scrollbar-thumb:hover { + background-color: cssUtils.$stroke-default; + } + + &-item { + display: flex; + justify-content: space-between; + align-items: center; + gap: cssUtils.$spacing-xs; + + &-label { + @include cssUtils.typography(label, default); + flex: 1; + min-width: 0; + } + + &-value { + @include cssUtils.typography(label, default); + flex-shrink: 0; + text-align: right; + } + + &-color { + width: 10px; + height: 10px; + border-radius: cssUtils.$rounded-2xs + } + &-separator { + width: 100%; + margin: cssUtils.$spacing-xs 0; + height: 1px; + background-color: cssUtils.$stroke-default; + } + } + } } From 81f321f51e6b96fa69d7649462a5a216edd7415f Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 26 Jun 2025 01:48:27 +0530 Subject: [PATCH 127/190] feat(Charts): enhance LineChartV2 and AreaChartV2 with SideBarTooltip integration This commit integrates the SideBarTooltip functionality into the LineChartV2 and AreaChartV2 components, allowing users to view detailed data upon clicking chart elements. It includes state management for tooltip visibility and data, as well as styling adjustments for improved presentation. The keyTransform function is also updated for better key formatting, ensuring consistent key usage across charts. Additionally, minor refactoring is done in the DefaultLegend and CustomTooltipContent components for performance optimization. --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 6 +- .../react-ui/src/components/Charts/Charts.tsx | 26 +- .../LineCharts/LineChartV2/LineChartV2.tsx | 355 ++++++++++-------- .../LineCharts/LineChartV2/lineChartV2.scss | 6 + .../shared/DefaultLegend/DefaultLegend.tsx | 156 ++++---- .../PortalTooltip/CustomTooltipContent.tsx | 4 +- .../shared/SideBarTooltip/SideBarTooltip.tsx | 2 +- .../shared/SideBarTooltip/sideBarTooltip.scss | 15 +- 8 files changed, 304 insertions(+), 266 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index e873d37af..d21bf62bc 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -219,7 +219,7 @@ const AreaChartV2Component = ({ const chartSyncID = useMemo(() => `area-chart-sync-${id}`, [id]); - const gradientId = useMemo(() => `area-chart-gradient-${id}`, [id]); + const gradientID = useMemo(() => `area-chart-gradient-${id}`, [id]); const onAreaClick = useCallback( (data: AreaClickData) => { @@ -341,7 +341,7 @@ const AreaChartV2Component = ({ const color = `var(--color-${transformedKey})`; return ( - + @@ -358,7 +358,7 @@ const AreaChartV2Component = ({ dataKey={key} type={variant} stroke={color} - fill={`url(#${gradientId}-${key})`} + fill={`url(#${gradientID}-${key})`} fillOpacity={1} stackId="a" activeDot={} diff --git a/js/packages/react-ui/src/components/Charts/Charts.tsx b/js/packages/react-ui/src/components/Charts/Charts.tsx index 35b167efa..3816482db 100644 --- a/js/packages/react-ui/src/components/Charts/Charts.tsx +++ b/js/packages/react-ui/src/components/Charts/Charts.tsx @@ -64,19 +64,21 @@ function useChart() { } export function keyTransform(key: string) { - return key - // Replace whitespace with hyphens - .replaceAll(/\s+/g, "-") - // Replace any character that's not alphanumeric, hyphen, or underscore with hyphen - .replaceAll(/[^a-zA-Z0-9_-]/g, "-") - // Remove multiple consecutive hyphens - .replaceAll(/-+/g, "-") - // Remove leading/trailing hyphens - .replace(/^-+|-+$/g, "") - // Ensure it doesn't start with a number (prepend 'key-' if it does) - .replace(/^(\d)/, "key-$1") + return ( + key + // Replace whitespace with hyphens + .replaceAll(/\s+/g, "-") + // Replace any character that's not alphanumeric, hyphen, or underscore with hyphen + .replaceAll(/[^a-zA-Z0-9_-]/g, "-") + // Remove multiple consecutive hyphens + .replaceAll(/-+/g, "-") + // Remove leading/trailing hyphens + .replace(/^-+|-+$/g, "") + // Ensure it doesn't start with a number (prepend 'key-' if it does) + .replace(/^(\d)/, "key-$1") ?? // Fallback with unique ID if the key is empty - || `key-${uniqueId()}`; + `key-${uniqueId()}` + ); } /** diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx index d332b81f8..2544ff03f 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx @@ -1,20 +1,17 @@ import clsx from "clsx"; import { ChevronLeft, ChevronRight } from "lucide-react"; -import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Line, LineChart as RechartsLineChart, XAxis, YAxis } from "recharts"; +import { useId } from "../../../../polyfills"; import { IconButton } from "../../../IconButton"; -import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, - keyTransform, -} from "../../Charts"; +import { ChartConfig, ChartContainer, ChartTooltip, keyTransform } from "../../Charts"; +import { SideBarChartData, SideBarTooltipProvider } from "../../context/SideBarTooltipContext"; import { ActiveDot, cartesianGrid, CustomTooltipContent, DefaultLegend, + SideBarTooltip, XAxisTick, YAxisTick, } from "../../shared"; @@ -27,10 +24,18 @@ import { getXAxisTickPositionData, } from "../../utils/BarAndLineUtils/AreaAndLineUtils"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; -import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; +import { + getChartConfig, + getColorForDataKey, + getDataKeys, + getLegendItems, +} from "../../utils/dataUtils"; import { getYAxisTickFormatter } from "../../utils/styleUtils"; import { LineChartV2Data, LineChartVariant } from "../types"; +type LineChartOnClick = React.ComponentProps["onClick"]; +type LineClickData = Parameters>[0]; + export interface LineChartV2Props { data: T; categoryKey: keyof T[number]; @@ -43,12 +48,10 @@ export interface LineChartV2Props { showYAxis?: boolean; xAxisLabel?: React.ReactNode; yAxisLabel?: React.ReactNode; - floatingTooltip?: boolean; className?: string; height?: number; width?: number; strokeWidth?: number; - onLineClick?: (payload: any) => void; } const Y_AXIS_WIDTH = 40; // Width of Y-axis chart when shown @@ -66,11 +69,9 @@ export const LineChartV2 = ({ yAxisLabel, legend = true, className, - floatingTooltip = true, height, width, strokeWidth = 2, - onLineClick, }: LineChartV2Props) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); @@ -90,6 +91,12 @@ export const LineChartV2 = ({ const [containerWidth, setContainerWidth] = useState(0); const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(false); + const [isSideBarTooltipOpen, setIsSideBarTooltipOpen] = useState(false); + const [isLegendExpanded, setIsLegendExpanded] = useState(false); + const [sideBarTooltipData, setSideBarTooltipData] = useState({ + title: "", + values: [], + }); // Use provided width or observed width const effectiveWidth = useMemo(() => { @@ -184,6 +191,11 @@ export const LineChartV2 = ({ updateScrollState(); }, [effectiveWidth, dataWidth, updateScrollState]); + useEffect(() => { + setIsSideBarTooltipOpen(false); + setIsLegendExpanded(false); + }, [dataKeys]); + // Add scroll event listener to update button states useEffect(() => { const mainContainer = mainContainerRef.current; @@ -207,159 +219,186 @@ export const LineChartV2 = ({ const chartSyncID = useMemo(() => `line-chart-sync-${id}`, [id]); + const onLineClick = useCallback( + (data: LineClickData) => { + if (data?.activePayload?.length && data.activePayload.length > 10) { + setIsSideBarTooltipOpen(true); + setSideBarTooltipData({ + title: data.activeLabel as string, + values: data.activePayload.map((payload) => ({ + value: payload.value as number, + label: payload.name || payload.dataKey, + color: getColorForDataKey(payload.dataKey, dataKeys, colors), + })), + }); + } + }, + [dataKeys, colors], + ); + return ( -
-
- {showYAxis && ( -
- {/* Y-axis only chart - synchronized with main chart */} - - +
+ {showYAxis && ( +
+ {/* Y-axis only chart - synchronized with main chart */} + } - /> - {/* Invisible lines to maintain scale synchronization */} - {dataKeys.map((key) => { - return ( - - ); - })} - -
- )} -
- - + } + /> + {/* Invisible lines to maintain scale synchronization */} + {dataKeys.map((key) => { + return ( + + ); + })} + +
+ )} +
+ - {grid && cartesianGrid()} - - } - orientation="bottom" - padding={{ - left: 20, - right: 20, + - {floatingTooltip ? ( + syncId={chartSyncID} + onClick={onLineClick} + > + {grid && cartesianGrid()} + + } + orientation="bottom" + padding={{ + left: 20, + right: 20, + }} + /> + } offset={15} /> - ) : ( - } /> - )} - {dataKeys.map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - return ( - } - isAnimationActive={isAnimationActive} - /> - ); - })} - - + + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + return ( + } + isAnimationActive={isAnimationActive} + /> + ); + })} + + +
+ {isSideBarTooltipOpen && }
-
- {/* if the data width is greater than the effective width, then show the scroll buttons */} - {dataWidth > effectiveWidth && ( -
- } - variant="secondary" - onClick={scrollLeft} - size="extra-small" - disabled={!canScrollLeft} - /> - } - variant="secondary" - size="extra-small" - onClick={scrollRight} - disabled={!canScrollRight} + {/* if the data width is greater than the effective width, then show the scroll buttons */} + {dataWidth > effectiveWidth && ( +
+ } + variant="secondary" + onClick={scrollLeft} + size="extra-small" + disabled={!canScrollLeft} + /> + } + variant="secondary" + size="extra-small" + onClick={scrollRight} + disabled={!canScrollRight} + /> +
+ )} + {legend && ( + -
- )} - {legend && ( - - )} -
+ )} +
+ ); }; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/lineChartV2.scss b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/lineChartV2.scss index 8c6b1a48a..26e324374 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/lineChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/lineChartV2.scss @@ -53,4 +53,10 @@ button.crayon-line-chart-scroll-button { cursor: not-allowed; transition: visibility 0.1s linear; } + + &--SideBarTooltip { + top: -15px; + transform: translateY(-50%); + right: 185px; + } } diff --git a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx index 873231c42..1830b4c8b 100644 --- a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx @@ -1,6 +1,6 @@ import clsx from "clsx"; import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; -import React, { useEffect, useMemo, useState, memo } from "react"; +import React, { memo, useMemo } from "react"; import { Button } from "../../../Button/Button"; import { type LegendItem } from "../../types"; import { calculateVisibleItems, getToggleButtonText } from "./utils/defaultLegendUtils"; @@ -15,97 +15,87 @@ interface DefaultLegendProps { setIsExpanded: (isExpanded: boolean) => void; } -const DefaultLegend: React.FC = memo(({ - items, - className, - yAxisLabel, - xAxisLabel, - containerWidth, - isExpanded, - setIsExpanded, -}) => { +const DefaultLegend: React.FC = memo( + ({ items, className, yAxisLabel, xAxisLabel, containerWidth, isExpanded, setIsExpanded }) => { + // Only memoize expensive calculations + const { visibleItems, hasMoreItems } = useMemo(() => { + return calculateVisibleItems(items, containerWidth); + }, [items, containerWidth]); + const displayItems = useMemo(() => { + return isExpanded ? items : visibleItems; + }, [isExpanded, items, visibleItems]); - // Only memoize expensive calculations - const { visibleItems, hasMoreItems } = useMemo(() => { - return calculateVisibleItems(items, containerWidth); - }, [items, containerWidth]); + const handleToggleExpanded = () => { + setIsExpanded(!isExpanded); + }; - const displayItems = useMemo(() => { - return isExpanded ? items : visibleItems; - }, [isExpanded, items, visibleItems]); + const showToggleButton = hasMoreItems; - + const toggleButtonText = useMemo(() => { + return getToggleButtonText(isExpanded, items.length, visibleItems.length); + }, [isExpanded, items.length, visibleItems.length]); - const handleToggleExpanded = () => { - setIsExpanded(!isExpanded); - }; - - const showToggleButton = hasMoreItems; - - const toggleButtonText = useMemo(() => { - return getToggleButtonText(isExpanded, items.length, visibleItems.length); - }, [isExpanded, items.length, visibleItems.length]); - - return ( -
- {/* this is x and y axis labels container*/} - {(xAxisLabel || yAxisLabel) && ( -
- {xAxisLabel && ( - - X-Axis: {xAxisLabel} - - )} - {yAxisLabel && ( - - Y-Axis: {yAxisLabel} - - )} -
- )} - {/* this is the legend items container*/} -
- {displayItems.map((item) => ( -
- {item.icon ? ( - - ) : ( -
+ return ( +
+ {/* this is x and y axis labels container*/} + {(xAxisLabel || yAxisLabel) && ( +
+ {xAxisLabel && ( + + X-Axis: {xAxisLabel} + + )} + {yAxisLabel && ( + + Y-Axis: {yAxisLabel} + )} - {item.label}
- ))} - - {showToggleButton && ( - )} + {/* this is the legend items container*/} +
+ {displayItems.map((item) => ( +
+ {item.icon ? ( + + ) : ( +
+ )} + {item.label} +
+ ))} + + {showToggleButton && ( + + )} +
-
- ); -}); + ); + }, +); export { DefaultLegend }; export type { DefaultLegendProps }; diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx index b23a3600c..19c282304 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx @@ -106,7 +106,9 @@ export const CustomTooltipContent = memo( key={`${item.dataKey}-${index}`} className={clsx( "crayon-chart-tooltip-content-item", - !isTwoItemsLayout && indicator === DEFAULT_INDICATOR && "crayon-chart-tooltip-content-item--dot", + !isTwoItemsLayout && + indicator === DEFAULT_INDICATOR && + "crayon-chart-tooltip-content-item--dot", )} > {formatter && item?.value !== undefined && item.name ? ( diff --git a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx index b450d3997..dfd52f482 100644 --- a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx @@ -1,5 +1,5 @@ -import React, { useCallback, useMemo } from "react"; import { X } from "lucide-react"; +import React, { useCallback, useMemo } from "react"; import { IconButton } from "../../../IconButton"; import { useSideBarTooltip } from "../../context/SideBarTooltipContext"; import { tooltipNumberFormatter } from "../PortalTooltip/utils"; diff --git a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss index b196c4bae..3da897534 100644 --- a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss +++ b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss @@ -10,7 +10,6 @@ background-color: cssUtils.$bg-container; border-radius: cssUtils.$rounded-s; border: 1px solid cssUtils.$stroke-default; - &-header { display: flex; @@ -18,7 +17,7 @@ align-items: flex-start; flex-shrink: 0; } - + &-close-button { flex-shrink: 0; } @@ -40,23 +39,23 @@ &:focus-visible { outline: none; } - + // Custom scrollbar styling scrollbar-width: thin; // Firefox - closest option (can't set exact px) scrollbar-color: cssUtils.$stroke-default transparent; // Firefox - thumb and track colors - + // Custom scrollbar for Webkit browsers (Chrome, Safari, newer Edge) &::-webkit-scrollbar { width: 2px; // Track width margin-right: 2px; // Offset to right } - + &::-webkit-scrollbar-track { background: transparent; border-radius: 0; margin-right: 2px; // Additional offset for track } - + &::-webkit-scrollbar-thumb { background-color: cssUtils.$stroke-default; border-radius: 1px; @@ -64,7 +63,7 @@ border: 0.25px solid transparent; // Creates the 1.5px effect within 2px track background-clip: content-box; } - + &::-webkit-scrollbar-thumb:hover { background-color: cssUtils.$stroke-default; } @@ -90,7 +89,7 @@ &-color { width: 10px; height: 10px; - border-radius: cssUtils.$rounded-2xs + border-radius: cssUtils.$rounded-2xs; } &-separator { width: 100%; From 78beaebf63ccf2818ec68a21e80198c7146b399d Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 26 Jun 2025 01:58:54 +0530 Subject: [PATCH 128/190] feat(Charts): add legend expansion state to PieChartV2 and RadialChartV2 This commit introduces a new state management feature for expanding and collapsing the legend in both PieChartV2 and RadialChartV2 components. The DefaultLegend component is updated to accept props for controlling its expanded state, enhancing user interaction and chart usability. Additionally, minor cleanup is performed in the Charts.tsx file to remove unnecessary whitespace. --- .../stories/areaChartV2.stories.tsx | 19 ------------------- .../react-ui/src/components/Charts/Charts.tsx | 2 -- .../PieCharts/PieChartV2/PieChartV2.tsx | 10 +++++++++- .../RadialChartV2/RadialChartV2.tsx | 10 +++++++++- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx index 3427f67a6..f8ab18da3 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx @@ -594,25 +594,6 @@ const meta: Meta> = { category: "Layout", }, }, - onAreaClick: { - description: "Callback function called when an area is clicked", - control: false, - table: { - type: { summary: "(payload: any) => void" }, - defaultValue: { summary: "undefined" }, - category: "Events", - }, - }, - floatingTooltip: { - description: - "Whether to use the floating tooltip that follows the mouse cursor. Uses Floating UI for intelligent positioning and collision detection.", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Tooltip", - }, - }, }, } satisfies Meta; diff --git a/js/packages/react-ui/src/components/Charts/Charts.tsx b/js/packages/react-ui/src/components/Charts/Charts.tsx index ba9d040b8..3816482db 100644 --- a/js/packages/react-ui/src/components/Charts/Charts.tsx +++ b/js/packages/react-ui/src/components/Charts/Charts.tsx @@ -64,7 +64,6 @@ function useChart() { } export function keyTransform(key: string) { - return ( key // Replace whitespace with hyphens @@ -80,7 +79,6 @@ export function keyTransform(key: string) { // Fallback with unique ID if the key is empty `key-${uniqueId()}` ); - } /** diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index cebfc9829..19dd0c6fc 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -78,6 +78,7 @@ export const PieChartV2 = ({ const [containerHeight, setContainerHeight] = useState(0); const [wrapperWidth, setWrapperWidth] = useState(0); const [hoveredLegendKey, setHoveredLegendKey] = useState(null); + const [isLegendExpanded, setIsLegendExpanded] = useState(false); const { activeIndex, handleMouseEnter, handleMouseLeave } = useChartHover(); // Memoize string conversions to avoid repeated calls @@ -457,7 +458,14 @@ export const PieChartV2 = ({
); } - return ; + return ( + + ); }, [ legend, legendVariant, diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx index 4fe005039..05ae38857 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx @@ -75,6 +75,7 @@ export const RadialChartV2 = ({ const [containerHeight, setContainerHeight] = useState(0); const [wrapperWidth, setWrapperWidth] = useState(0); const [hoveredLegendKey, setHoveredLegendKey] = useState(null); + const [isLegendExpanded, setIsLegendExpanded] = useState(false); const { activeIndex, handleMouseEnter, handleMouseLeave } = useRadialChartHover(); // Memoize string conversions to avoid repeated calls @@ -337,7 +338,14 @@ export const RadialChartV2 = ({
); } - return ; + return ( + + ); }, [ legend, legendVariant, From 9ce04608ec65335f69ab04e3796ffe761e0cbb1c Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 26 Jun 2025 13:51:17 +0530 Subject: [PATCH 129/190] feat(Charts): refactor ActiveDot component to use useLayoutEffect for rendering This commit refactors the ActiveDot component to utilize the useLayoutEffect hook for rendering SVG circles, improving performance and ensuring proper DOM updates. The component now creates and appends the circles dynamically, enhancing flexibility in rendering while maintaining existing functionality. Additionally, it includes a null check for cx and cy props to prevent rendering issues. --- .../Charts/shared/ActiveDot/ActiveDot.tsx | 62 ++++++++++++------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/shared/ActiveDot/ActiveDot.tsx b/js/packages/react-ui/src/components/Charts/shared/ActiveDot/ActiveDot.tsx index d74706087..63a2bff59 100644 --- a/js/packages/react-ui/src/components/Charts/shared/ActiveDot/ActiveDot.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/ActiveDot/ActiveDot.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useLayoutEffect, useRef } from "react"; export interface ActiveDotProps { cx?: number; @@ -9,25 +9,45 @@ export interface ActiveDotProps { export const ActiveDot: React.FC = (props) => { const { cx, cy } = props; + const ref = useRef(null); - return ( - - - - - ); + useLayoutEffect(() => { + if (ref.current) { + const parent = ref.current.parentElement?.parentElement; + const dotGroup = document.createElementNS("http://www.w3.org/2000/svg", "g"); + + const circle1 = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + circle1.setAttribute("cx", String(cx)); + circle1.setAttribute("cy", String(cy)); + circle1.setAttribute("r", "4"); + circle1.setAttribute("fill", "var(--crayon-container-fills)"); + circle1.setAttribute("stroke", "var(--crayon-container-fills)"); + circle1.setAttribute("stroke-width", "1"); + + const circle2 = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + circle2.setAttribute("cx", String(cx)); + circle2.setAttribute("cy", String(cy)); + circle2.setAttribute("r", "2"); + circle2.setAttribute("fill", "var(--crayon-inverted-fills)"); + circle2.setAttribute("stroke", "var(--crayon-inverted-fills)"); + circle2.setAttribute("stroke-width", "0.5"); + + dotGroup.appendChild(circle1); + dotGroup.appendChild(circle2); + if (parent) { + parent.appendChild(dotGroup); + } + + return () => { + dotGroup.remove(); + }; + } + return undefined; + }); + + if (cx === undefined || cy === undefined || cx === null || cy === null) { + return null; + } + + return ; }; From 793e0da3b63415b3147d4072d785e2fa2c305bf1 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Thu, 26 Jun 2025 23:26:38 +0530 Subject: [PATCH 130/190] feat(Charts): add RadarChartV2 component and update chart styles This commit introduces the RadarChartV2 component, complete with a new SCSS file for styling and a corresponding index file for exports. Additionally, it includes updates to the charts.scss file to forward the new RadarChartV2 styles. The MiniPieChart component and its related files have been removed to streamline the chart offerings. A new environment configuration file is also added to manage project dependencies and startup commands. --- .cursor/environment.json | 5 + .../PieCharts/MiniPieChart/MiniPieChart.tsx | 82 --- .../Charts/PieCharts/MiniPieChart/index.ts | 1 - .../stories/MiniPieChart.stories.tsx | 164 ----- .../RadarCharts/RadarChartV2/RadarChartV2.tsx | 160 +++++ .../Charts/RadarCharts/RadarChartV2/index.ts | 1 + .../RadarChartV2/radarChartV2.scss | 10 + .../stories/RadarChartV2.stories.tsx | 594 ++++++++++++++++++ .../components/Charts/RadarCharts/index.ts | 0 .../RadarCharts/utils/RaderChartUtils.ts | 44 -- .../src/components/Charts/charts.scss | 3 + .../shared/DefaultLegend/DefaultLegend.tsx | 146 +++-- .../PortalTooltip/CustomTooltipContent.tsx | 9 +- 13 files changed, 857 insertions(+), 362 deletions(-) create mode 100644 .cursor/environment.json delete mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/stories/MiniPieChart.stories.tsx create mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx create mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss create mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/stories/RadarChartV2.stories.tsx create mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/utils/RaderChartUtils.ts diff --git a/.cursor/environment.json b/.cursor/environment.json new file mode 100644 index 000000000..06198657e --- /dev/null +++ b/.cursor/environment.json @@ -0,0 +1,5 @@ +{ + "agentCanUpdateSnapshot": true, + "install": "pnpm install", + "start": "pnpm dev:start" +} \ No newline at end of file diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx deleted file mode 100644 index 1428aefb7..000000000 --- a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/MiniPieChart.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { debounce } from "lodash-es"; -import { useEffect, useRef, useState } from "react"; -import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; -import { ChartContainer } from "../../Charts"; -import { - PieChartData, - calculateChartDimensions, - createChartConfig, - transformDataWithPercentages, -} from "../utils/PieChartUtils"; - -export type MiniPieChartData = PieChartData; - -export interface MiniPieChartProps { - data: T; - categoryKey: keyof T[number]; - dataKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - variant?: "pie" | "donut"; - format?: "percentage" | "number"; - legend?: boolean; - isAnimationActive?: boolean; -} - -export const MiniPieChart = ({ - data, - categoryKey, - dataKey, - theme = "ocean", - variant = "pie", - format = "number", - isAnimationActive = true, -}: MiniPieChartProps) => { - const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(120); - const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(0); - const containerRef = useRef(null); - - // Calculate dynamic radius - useEffect(() => { - if (!containerRef.current) return; - - const resizeObserver = new ResizeObserver( - debounce((entries: any) => { - const { width } = entries[0].contentRect; - const { outerRadius, innerRadius } = calculateChartDimensions(width, variant); - setCalculatedOuterRadius(outerRadius); - setCalculatedInnerRadius(innerRadius); - }, 100), - ); - - resizeObserver.observe(containerRef.current); - return () => resizeObserver.disconnect(); - }, [variant]); - - const transformedData = transformDataWithPercentages(data, dataKey); - const chartConfig = createChartConfig(data, categoryKey, theme); - - return ( - - - - {Object.entries(chartConfig).map(([key, config]) => ( - - ))} - - - - ); -}; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/index.ts b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/index.ts deleted file mode 100644 index 62e639961..000000000 --- a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./MiniPieChart"; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/stories/MiniPieChart.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/stories/MiniPieChart.stories.tsx deleted file mode 100644 index 05bb32e34..000000000 --- a/js/packages/react-ui/src/components/Charts/PieCharts/MiniPieChart/stories/MiniPieChart.stories.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { Card } from "../../../../Card"; -import { MiniPieChart, MiniPieChartProps } from "../MiniPieChart"; - -const pieChartData = [ - { month: "January", value: 4250 }, - { month: "February", value: 3820 }, - { month: "March", value: 4680 }, - { month: "April", value: 4120 }, - { month: "May", value: 5340 }, - { month: "June", value: 6250 }, - { month: "July", value: 5890 }, -]; - -const meta: Meta> = { - title: "Components/Charts/PieCharts/MiniPieChart", - component: MiniPieChart, - parameters: { - layout: "centered", - docs: { - description: { - component: "```tsx\nimport { PieChart } from '@crayon-ui/react-ui/Charts/PieChart';\n```", - }, - }, - }, - tags: ["!dev", "autodocs"], - - argTypes: { - data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", - control: false, - table: { - type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", - }, - }, - categoryKey: { - description: - "The key from your data object to be used as the segment labels (e.g., 'month', 'category', 'name')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - dataKey: { - description: - "The key from your data object to be used as the values that determine the slice sizes (e.g., 'value', 'count', 'amount')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", - control: "select", - options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], - table: { - defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - variant: { - description: - "The style of the pie chart. 'pie' shows a pie chart, while 'donut' shows a donut chart.", - control: "radio", - options: ["pie", "donut"], - table: { - defaultValue: { summary: "pie" }, - category: "Appearance", - }, - }, - format: { - description: - "The format of the data. 'percentage' shows the data as a percentage, while 'number' shows the data as a number.", - control: "radio", - options: ["percentage", "number"], - table: { - defaultValue: { summary: "percentage" }, - category: "Display", - }, - }, - legend: { - description: "Whether to display the legend", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - isAnimationActive: { - description: "Whether to animate the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const PieChartStory: Story = { - name: "Pie Chart", - args: { - data: pieChartData, - categoryKey: "month", - dataKey: "value", - theme: "ocean", - variant: "pie", - format: "number", - legend: true, - label: true, - isAnimationActive: true, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` - const pieChartData = [ - { month: "January", value: 400 }, - { month: "February", value: 300 }, - { month: "March", value: 300 }, - { month: "April", value: 400 }, - { month: "May", value: 300 }, - { month: "June", value: 300 }, - { month: "July", value: 300 }, -]; - - - - - `, - }, - }, - }, -}; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx new file mode 100644 index 000000000..970030227 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx @@ -0,0 +1,160 @@ +import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { PolarAngleAxis, PolarGrid, Radar, RadarChart as RechartsRadarChart } from "recharts"; +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, + keyTransform, +} from "../../Charts"; +import { DefaultLegend } from "../../shared"; +import { LegendItem } from "../../types"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; + +export type RadarChartV2Data = Array>; + +export interface RadarChartV2Props { + data: T; + categoryKey: keyof T[number]; + theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + variant?: "line" | "area"; + grid?: boolean; + legend?: boolean; + strokeWidth?: number; + areaOpacity?: number; + icons?: Partial>; + isAnimationActive?: boolean; + size?: number; +} + +export const RadarChartV2 = ({ + data, + categoryKey, + theme = "ocean", + variant = "line", + grid = true, + legend = true, + strokeWidth = 2, + areaOpacity = 0.5, + icons = {}, + isAnimationActive = true, + size, +}: RadarChartV2Props) => { + const dataKeys = useMemo(() => { + return getDataKeys(data, categoryKey as string); + }, [data, categoryKey]); + + const colors = useMemo(() => { + const palette = getPalette(theme); + return getDistributedColors(palette, dataKeys.length); + }, [theme, dataKeys.length]); + + // Create Config + const chartConfig: ChartConfig = useMemo(() => { + return getChartConfig(dataKeys, colors, undefined, icons); + }, [dataKeys, icons, colors]); + + const legendItems: LegendItem[] = useMemo(() => { + return getLegendItems(dataKeys, colors, icons); + }, [dataKeys, colors, icons]); + + const [isLegendExpanded, setIsLegendExpanded] = useState(false); + const [containerDimensions, setContainerDimensions] = useState({ width: 0, height: 0 }); + const rootRef = useRef(null); + const chartContainerRef = useRef(null); + + useLayoutEffect(() => { + // Only set up ResizeObserver if width is not provided + if (size || !rootRef.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) { + setContainerDimensions({ + width: entry.contentRect.width, + height: entry.contentRect.height, + }); + } + }); + + resizeObserver.observe(rootRef.current); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + useEffect(() => { + console.log("containerDimensions", containerDimensions); + }, [containerDimensions]); + + return ( +
+ + + {grid && } + + + } /> + {dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + if (variant === "line") { + return ( + + ); + } else { + return ( + + ); + } + })} + + + {legend && ( + + )} +
+ ); +}; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts new file mode 100644 index 000000000..4468d36d8 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts @@ -0,0 +1 @@ +export * from "./RadarChartV2"; \ No newline at end of file diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss new file mode 100644 index 000000000..a052c7b5e --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss @@ -0,0 +1,10 @@ +.crayon-radar-chart-v2 { + &-container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } +} diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/stories/RadarChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/stories/RadarChartV2.stories.tsx new file mode 100644 index 000000000..628e0c48d --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/stories/RadarChartV2.stories.tsx @@ -0,0 +1,594 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Shield, Target, TrendingUp, Users, Zap, Star } from "lucide-react"; +import { useState } from "react"; +import { Card } from "../../../../Card"; +import { RadarChartV2, RadarChartV2Props } from "../RadarChartV2"; + +// 📊 COMPREHENSIVE DATA VARIATIONS - Designed to test various radar chart scenarios +const dataVariations = { + default: [ + { skill: "JavaScript", beginner: 3, intermediate: 7, advanced: 9 }, + { skill: "React", beginner: 4, intermediate: 8, advanced: 8 }, + { skill: "TypeScript", beginner: 2, intermediate: 6, advanced: 9 }, + { skill: "Node.js", beginner: 3, intermediate: 7, advanced: 7 }, + { skill: "CSS", beginner: 5, intermediate: 8, advanced: 6 }, + { skill: "Git", beginner: 4, intermediate: 6, advanced: 8 }, + ], + performance: [ + { metric: "Speed", team_a: 85, team_b: 92, team_c: 78 }, + { metric: "Quality", team_a: 90, team_b: 88, team_c: 95 }, + { metric: "Efficiency", team_a: 78, team_b: 85, team_c: 82 }, + { metric: "Innovation", team_a: 92, team_b: 75, team_c: 88 }, + { metric: "Collaboration", team_a: 88, team_b: 94, team_c: 85 }, + { metric: "Reliability", team_a: 95, team_b: 87, team_c: 90 }, + ], + businessMetrics: [ + { department: "Sales", q1: 85, q2: 92, q3: 78, q4: 95 }, + { department: "Marketing", q1: 78, q2: 85, q3: 90, q4: 88 }, + { department: "Support", q1: 92, q2: 88, q3: 85, q4: 90 }, + { department: "Product", q1: 88, q2: 90, q3: 92, q4: 85 }, + { department: "Engineering", q1: 90, q2: 85, q3: 88, q4: 92 }, + ], + gameStats: [ + { attribute: "Strength", player1: 95, player2: 78, player3: 85 }, + { attribute: "Speed", player1: 82, player2: 95, player3: 88 }, + { attribute: "Intelligence", player1: 88, player2: 85, player3: 92 }, + { attribute: "Defense", player1: 90, player2: 82, player3: 78 }, + { attribute: "Magic", player1: 75, player2: 92, player3: 88 }, + { attribute: "Luck", player1: 85, player2: 88, player3: 90 }, + ], + productFeatures: [ + { feature: "Usability", current: 8, competitor_a: 7, competitor_b: 9 }, + { feature: "Performance", current: 9, competitor_a: 8, competitor_b: 7 }, + { feature: "Design", current: 8, competitor_a: 9, competitor_b: 8 }, + { feature: "Features", current: 7, competitor_a: 8, competitor_b: 9 }, + { feature: "Price", current: 9, competitor_a: 6, competitor_b: 8 }, + { feature: "Support", current: 8, competitor_a: 7, competitor_b: 6 }, + ], + minimal: [ + { category: "Category A", value1: 8, value2: 6 }, + { category: "Category B", value1: 7, value2: 9 }, + { category: "Category C", value1: 9, value2: 7 }, + ], + singleMetric: [ + { dimension: "North", score: 85 }, + { dimension: "South", score: 72 }, + { dimension: "East", score: 90 }, + { dimension: "West", score: 78 }, + { dimension: "Center", score: 88 }, + ], + manyDimensions: [ + { aspect: "UX Design", mobile: 8, web: 9, desktop: 7, tablet: 8 }, + { aspect: "Performance", mobile: 9, web: 8, desktop: 9, tablet: 8 }, + { aspect: "Accessibility", mobile: 7, web: 8, desktop: 9, tablet: 7 }, + { aspect: "Security", mobile: 8, web: 9, desktop: 8, tablet: 8 }, + { aspect: "Scalability", mobile: 7, web: 9, desktop: 8, tablet: 7 }, + { aspect: "Maintenance", mobile: 8, web: 7, desktop: 8, tablet: 8 }, + { aspect: "Testing", mobile: 9, web: 8, desktop: 7, tablet: 8 }, + { aspect: "Documentation", mobile: 6, web: 8, desktop: 9, tablet: 7 }, + ], +}; + +// Map data variations to their category keys +const categoryKeys = { + default: "skill", + performance: "metric", + businessMetrics: "department", + gameStats: "attribute", + productFeatures: "feature", + minimal: "category", + singleMetric: "dimension", + manyDimensions: "aspect", +} as const; + +// Default data for the main story +const radarChartData = dataVariations.default; + +const meta: Meta> = { + title: "Components/Charts/RadarCharts/RadarChartV2", + component: RadarChartV2, + parameters: { + layout: "centered", + docs: { + description: { + component: + "```tsx\nimport { RadarChartV2 } from '@crayon-ui/react-ui/Charts/RadarChartV2';\n```", + }, + }, + }, + tags: ["dev", "autodocs"], + argTypes: { + data: { + description: + "An array of data objects where each object represents a data point. Each object should have a category field and one or more numeric values for the radar dimensions.", + control: false, + table: { + type: { summary: "Array>" }, + defaultValue: { summary: "[]" }, + category: "Data", + }, + }, + categoryKey: { + description: + "The key from your data object to be used as the radar axis labels (e.g., 'skill', 'metric', 'dimension')", + control: false, + table: { + type: { summary: "string" }, + defaultValue: { summary: "string" }, + category: "Data", + }, + }, + theme: { + description: + "The color palette theme for the chart. Each theme provides a different set of colors for the radar areas/lines.", + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "Appearance", + }, + }, + variant: { + description: + "The style of the radar chart. 'line' shows only outlines, while 'area' shows filled areas.", + control: "radio", + options: ["line", "area"], + table: { + defaultValue: { summary: "line" }, + category: "Appearance", + }, + }, + grid: { + description: "Whether to display the background grid lines in the radar chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + legend: { + description: "Whether to display the chart legend", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + strokeWidth: { + description: "The width of the radar lines", + control: "number", + table: { + type: { summary: "number" }, + defaultValue: { summary: "2" }, + category: "Appearance", + }, + }, + areaOpacity: { + description: "The opacity of the filled areas when variant is 'area'", + control: { type: "range", min: 0, max: 1, step: 0.1 }, + table: { + type: { summary: "number" }, + defaultValue: { summary: "0.5" }, + category: "Appearance", + }, + }, + icons: { + description: + "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", + control: false, + table: { + type: { summary: "Record" }, + defaultValue: { summary: "{}" }, + category: "Appearance", + }, + }, + isAnimationActive: { + description: "Whether to animate the chart", + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "Display", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const RadarChartV2Story: Story = { + name: "🎛️ Data Switcher - Radar Chart V2", + args: { + data: radarChartData, + categoryKey: "skill", + theme: "ocean", + variant: "line", + grid: true, + legend: true, + strokeWidth: 2, + areaOpacity: 0.5, + isAnimationActive: true, + }, + render: (args: any) => { + const [selectedDataType, setSelectedDataType] = + useState("default"); + + const currentData = dataVariations[selectedDataType]; + const currentCategoryKey = categoryKeys[selectedDataType]; + + const buttonStyle = { + margin: "2px", + padding: "6px 12px", + fontSize: "12px", + border: "1px solid #ddd", + borderRadius: "4px", + cursor: "pointer", + background: "#fff", + fontFamily: "monospace", + }; + + const activeButtonStyle = { + ...buttonStyle, + background: "#007acc", + color: "white", + border: "1px solid #007acc", + }; + + return ( +
+
+ 🕸️ Radar Chart Test Suite: +
+ + + + + + + + +
+
+ Current: {selectedDataType} | Items:{" "} + {currentData.length} | Category: {currentCategoryKey} +
+
+ + + +
+ ); + }, + parameters: { + docs: { + description: { + story: + "Use the buttons above the chart to quickly switch between different data variations. Active button is highlighted in blue. Each dataset tests different aspects of the radar chart functionality.", + }, + }, + }, +}; + +export const SkillsStory: Story = { + name: "📚 Skills Assessment", + args: { + data: dataVariations.default as any, + categoryKey: "skill" as any, + theme: "ocean", + variant: "area", + grid: true, + legend: true, + strokeWidth: 2, + areaOpacity: 0.3, + isAnimationActive: true, + }, + render: (args: any) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "A skills assessment radar chart showing different proficiency levels across various programming skills. Uses area variant with transparency to show overlapping competencies.", + }, + }, + }, +}; + +export const TeamPerformanceStory: Story = { + name: "🏆 Team Performance Comparison", + args: { + data: dataVariations.performance as any, + categoryKey: "metric" as any, + theme: "emerald", + variant: "line", + grid: true, + legend: true, + strokeWidth: 3, + areaOpacity: 0.5, + isAnimationActive: true, + icons: { + team_a: Shield, + team_b: Target, + team_c: Star, + }, + }, + render: (args: any) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "Compare team performance across multiple metrics. Features custom icons in the legend and thicker stroke width for better visibility.", + }, + }, + }, +}; + +export const BusinessMetricsStory: Story = { + name: "📈 Quarterly Business Metrics", + args: { + data: dataVariations.businessMetrics as any, + categoryKey: "department" as any, + theme: "sunset", + variant: "area", + grid: true, + legend: true, + strokeWidth: 2, + areaOpacity: 0.4, + isAnimationActive: true, + }, + render: (args: any) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "Track quarterly performance across different departments. Shows how area charts can effectively display multiple overlapping datasets.", + }, + }, + }, +}; + +export const VariantComparisonStory: Story = { + name: "🎨 Line vs Area Variants", + args: { + data: dataVariations.gameStats as any, + categoryKey: "attribute" as any, + theme: "vivid", + variant: "line", + grid: true, + legend: true, + strokeWidth: 2, + areaOpacity: 0.5, + isAnimationActive: true, + }, + render: (args: any) => ( +
+
+

Line Variant

+ + + +
+
+

Area Variant

+ + + +
+
+ ), + parameters: { + docs: { + description: { + story: + "Compare the two available variants: line (outline only) and area (filled). The area variant is useful for showing magnitude, while line variant is better for precise value comparison.", + }, + }, + }, +}; + +export const ThemeShowcaseStory: Story = { + name: "🌈 Theme Showcase", + args: { + data: dataVariations.minimal as any, + categoryKey: "category" as any, + theme: "ocean", + variant: "area", + grid: true, + legend: true, + strokeWidth: 2, + areaOpacity: 0.6, + isAnimationActive: true, + }, + render: (args: any) => ( +
+ {(["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"] as const).map((theme) => ( +
+

+ {theme} +

+ + + +
+ ))} +
+ ), + parameters: { + docs: { + description: { + story: + "Showcase all available color themes. Each theme provides a carefully curated color palette optimized for data visualization.", + }, + }, + }, +}; + +export const SingleMetricStory: Story = { + name: "🎯 Single Metric", + args: { + data: dataVariations.singleMetric as any, + categoryKey: "dimension" as any, + theme: "orchid", + variant: "area", + grid: true, + legend: false, + strokeWidth: 3, + areaOpacity: 0.7, + isAnimationActive: true, + }, + render: (args: any) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "Single metric radar chart without legend. Useful for displaying one dataset across multiple dimensions with emphasis on the overall shape.", + }, + }, + }, +}; + +export const ManyDimensionsStory: Story = { + name: "🌐 Many Dimensions", + args: { + data: dataVariations.manyDimensions as any, + categoryKey: "aspect" as any, + theme: "spectrum", + variant: "line", + grid: true, + legend: true, + strokeWidth: 2, + areaOpacity: 0.3, + isAnimationActive: true, + }, + render: (args: any) => ( + + + + ), + parameters: { + docs: { + description: { + story: + "Radar chart with many dimensions (8 axes) to test how the component handles complex datasets. Line variant works best for many overlapping series.", + }, + }, + }, +}; + +export const CustomizationStory: Story = { + name: "🎛️ Customization Options", + args: { + data: dataVariations.productFeatures as any, + categoryKey: "feature" as any, + theme: "emerald", + variant: "area", + grid: true, + legend: true, + strokeWidth: 2, + areaOpacity: 0.5, + isAnimationActive: true, + icons: { + current: TrendingUp, + competitor_a: Users, + competitor_b: Zap, + }, + }, + render: (args: any) => ( +
+
+

With Icons & Animation

+ + + +
+
+

No Grid, No Animation, Thick Lines

+ + + +
+
+ ), + parameters: { + docs: { + description: { + story: + "Demonstrate various customization options including icons, grid toggle, animation control, and stroke width adjustment.", + }, + }, + }, +}; \ No newline at end of file diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/utils/RaderChartUtils.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/utils/RaderChartUtils.ts deleted file mode 100644 index ec0b82987..000000000 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/utils/RaderChartUtils.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ChartConfig } from "../../Charts"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; - -export type RadarChartData = Array>; - -export interface RadarChartConfig { - data: RadarChartData; - categoryKey: string; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - icons?: Partial>; -} - -// Create chart configuration -export const createChartConfig = ({ - data, - categoryKey, - theme = "ocean", - icons = {}, -}: RadarChartConfig): ChartConfig => { - // excluding the categoryKey - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); - const palette = getPalette(theme); - const colors = getDistributedColors(palette, dataKeys.length); - - return dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - icon: icons[key], - color: colors[index], - }, - }), - {}, - ); -}; - -// Get radar chart margin -export const getRadarChartMargin = () => ({ - top: 10, - right: 10, - bottom: 10, - left: 10, -}); diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index bc2a8ca37..8690009df 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -9,6 +9,9 @@ // line chart css @forward "./LineCharts/index.scss"; +// radar chart v2 css +@forward "./RadarCharts/RadarChartV2/radarChartV2.scss"; + // progress bar css @forward "./ProgressBarChart/progressBarChart.scss"; diff --git a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx index 1830b4c8b..a5848fc6e 100644 --- a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx @@ -13,89 +13,97 @@ interface DefaultLegendProps { containerWidth?: number; isExpanded: boolean; setIsExpanded: (isExpanded: boolean) => void; + style?: React.CSSProperties; } -const DefaultLegend: React.FC = memo( - ({ items, className, yAxisLabel, xAxisLabel, containerWidth, isExpanded, setIsExpanded }) => { - // Only memoize expensive calculations - const { visibleItems, hasMoreItems } = useMemo(() => { - return calculateVisibleItems(items, containerWidth); - }, [items, containerWidth]); +const DefaultLegend = memo( + React.forwardRef( + ({ items, className, yAxisLabel, xAxisLabel, containerWidth, isExpanded, setIsExpanded, style }, ref) => { + // Only memoize expensive calculations + const { visibleItems, hasMoreItems } = useMemo(() => { + return calculateVisibleItems(items, containerWidth); + }, [items, containerWidth]); - const displayItems = useMemo(() => { - return isExpanded ? items : visibleItems; - }, [isExpanded, items, visibleItems]); + const displayItems = useMemo(() => { + return isExpanded ? items : visibleItems; + }, [isExpanded, items, visibleItems]); - const handleToggleExpanded = () => { - setIsExpanded(!isExpanded); - }; + const handleToggleExpanded = () => { + setIsExpanded(!isExpanded); + }; - const showToggleButton = hasMoreItems; + const showToggleButton = hasMoreItems; - const toggleButtonText = useMemo(() => { - return getToggleButtonText(isExpanded, items.length, visibleItems.length); - }, [isExpanded, items.length, visibleItems.length]); + const toggleButtonText = useMemo(() => { + return getToggleButtonText(isExpanded, items.length, visibleItems.length); + }, [isExpanded, items.length, visibleItems.length]); - return ( -
- {/* this is x and y axis labels container*/} - {(xAxisLabel || yAxisLabel) && ( -
- {xAxisLabel && ( - - X-Axis: {xAxisLabel} - - )} - {yAxisLabel && ( - - Y-Axis: {yAxisLabel} - - )} -
- )} - {/* this is the legend items container*/} + return (
- {displayItems.map((item) => ( -
- {item.icon ? ( - - ) : ( -
+ {/* this is x and y axis labels container*/} + {(xAxisLabel || yAxisLabel) && ( +
+ {xAxisLabel && ( + + X-Axis: {xAxisLabel} + + )} + {yAxisLabel && ( + + Y-Axis: {yAxisLabel} + )} - {item.label}
- ))} - - {showToggleButton && ( - )} + {/* this is the legend items container*/} +
+ {displayItems.map((item) => ( +
+ {item.icon ? ( + + ) : ( +
+ )} + {item.label} +
+ ))} + + {showToggleButton && ( + + )} +
-
- ); - }, + ); + } + ) ); +DefaultLegend.displayName = "DefaultLegend"; export { DefaultLegend }; export type { DefaultLegendProps }; diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx index 19c282304..7cc00a75d 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/CustomTooltipContent.tsx @@ -50,6 +50,8 @@ export const CustomTooltipContent = memo( const [isGreaterThanTen, setIsGreaterThanTen] = useState( !!(payload?.length && payload.length > 10), ); + const [remainingItems, setRemainingItems] = useState(0); + useLayoutEffect(() => { if (payload?.length && payload.length > 10) { setIsGreaterThanTen(true); @@ -176,7 +178,8 @@ export const CustomTooltipContent = memo( } // Handle regular layout with potential truncation - const morphPayload = isGreaterThanTen ? payload.slice(0, 10) : payload; + const morphPayload = isGreaterThanTen ? payload.slice(0, 5) : payload; + setRemainingItems(payload.length - morphPayload.length); return morphPayload.map((item, index) => renderPayloadItem(item, index, false)); }, [ payload, @@ -204,7 +207,9 @@ export const CustomTooltipContent = memo(
{payloadItems}
{isGreaterThanTen &&
} {isGreaterThanTen && ( -
Click to view all
+
+ Click to view all {remainingItems} +
)}
); From f837ef449b78632f3c15390ebaf66652ca1919b9 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 27 Jun 2025 05:30:28 +0530 Subject: [PATCH 131/190] feat(Charts): enhance RadarChartV2 with improved layout and rendering This commit refines the RadarChartV2 component by introducing a new inner container for better layout management and updating the rendering logic to utilize useCallback for radar elements. It also adds a new AxisLabel component for improved axis labeling. Additionally, the SCSS file is updated to include styles for the new inner container, enhancing the overall visual structure of the chart. Minor adjustments are made to the DefaultLegend component for better prop handling. --- .../RadarCharts/RadarChartV2/RadarChartV2.tsx | 139 ++++++++++-------- .../RadarChartV2/components/AxisLabel.tsx | 33 +++++ .../Charts/RadarCharts/RadarChartV2/index.ts | 2 +- .../RadarChartV2/radarChartV2.scss | 8 + .../stories/RadarChartV2.stories.tsx | 22 +-- .../shared/DefaultLegend/DefaultLegend.tsx | 18 ++- 6 files changed, 148 insertions(+), 74 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx index 970030227..a0541c160 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react"; import { PolarAngleAxis, PolarGrid, Radar, RadarChart as RechartsRadarChart } from "recharts"; import { ChartConfig, @@ -7,10 +7,11 @@ import { ChartTooltipContent, keyTransform, } from "../../Charts"; -import { DefaultLegend } from "../../shared"; +import { ActiveDot, DefaultLegend } from "../../shared"; import { LegendItem } from "../../types"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; +import { AxisLabel } from "./components/AxisLabel"; export type RadarChartV2Data = Array>; @@ -61,12 +62,11 @@ export const RadarChartV2 = ({ const [isLegendExpanded, setIsLegendExpanded] = useState(false); const [containerDimensions, setContainerDimensions] = useState({ width: 0, height: 0 }); - const rootRef = useRef(null); const chartContainerRef = useRef(null); useLayoutEffect(() => { // Only set up ResizeObserver if width is not provided - if (size || !rootRef.current) { + if (size || !chartContainerRef.current) { return () => {}; } @@ -80,72 +80,91 @@ export const RadarChartV2 = ({ } }); - resizeObserver.observe(rootRef.current); + resizeObserver.observe(chartContainerRef.current); return () => { resizeObserver.disconnect(); }; }, []); - useEffect(() => { - console.log("containerDimensions", containerDimensions); - }, [containerDimensions]); + const chartSize = useMemo( + () => Math.min(containerDimensions.width, containerDimensions.height), + [containerDimensions], + ); + + const renderRadars = useCallback( + ( + dataKeys: string[], + variant: "line" | "area", + strokeWidth: number, + areaOpacity: number, + isAnimationActive: boolean, + ) => { + return dataKeys.map((key) => { + const transformedKey = keyTransform(key); + const color = `var(--color-${transformedKey})`; + if (variant === "line") { + return ( + } + /> + ); + } else { + return ( + } + /> + ); + } + }); + }, + [], + ); return ( -
- - +
+ - {grid && } - + + {grid && } + } /> - } /> - {dataKeys.map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - if (variant === "line") { - return ( - - ); - } else { - return ( - - ); - } - })} - - + } /> + {renderRadars(dataKeys, variant, strokeWidth, areaOpacity, isAnimationActive)} + + +
{legend && ( = (props) => { + const { x, y, payload, textAnchor } = props; + // The props can be incomplete sometimes on first render. + if (!payload || x === undefined || y === undefined) { + return null; + } + return ( + + {payload.value} + + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts index 4468d36d8..59a239fc8 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts @@ -1 +1 @@ -export * from "./RadarChartV2"; \ No newline at end of file +export * from "./RadarChartV2"; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss index a052c7b5e..89085587e 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss @@ -7,4 +7,12 @@ align-items: center; justify-content: center; } + &-container-inner { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } } diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/stories/RadarChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/stories/RadarChartV2.stories.tsx index 628e0c48d..045255d33 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/stories/RadarChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/stories/RadarChartV2.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Shield, Target, TrendingUp, Users, Zap, Star } from "lucide-react"; +import { Shield, Star, Target, TrendingUp, Users, Zap } from "lucide-react"; import { useState } from "react"; import { Card } from "../../../../Card"; import { RadarChartV2, RadarChartV2Props } from "../RadarChartV2"; @@ -305,14 +305,16 @@ export const RadarChartV2Story: Story = { {currentData.length} | Category: {currentCategoryKey}
- +
@@ -591,4 +593,4 @@ export const CustomizationStory: Story = { }, }, }, -}; \ No newline at end of file +}; diff --git a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx index a5848fc6e..37f9ac691 100644 --- a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/DefaultLegend.tsx @@ -18,7 +18,19 @@ interface DefaultLegendProps { const DefaultLegend = memo( React.forwardRef( - ({ items, className, yAxisLabel, xAxisLabel, containerWidth, isExpanded, setIsExpanded, style }, ref) => { + ( + { + items, + className, + yAxisLabel, + xAxisLabel, + containerWidth, + isExpanded, + setIsExpanded, + style, + }, + ref, + ) => { // Only memoize expensive calculations const { visibleItems, hasMoreItems } = useMemo(() => { return calculateVisibleItems(items, containerWidth); @@ -100,8 +112,8 @@ const DefaultLegend = memo(
); - } - ) + }, + ), ); DefaultLegend.displayName = "DefaultLegend"; From 19d039ab0c8f9fd22f101d500cb7706338436bbe Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 27 Jun 2025 07:37:10 +0530 Subject: [PATCH 132/190] feat(Charts): integrate SideBarTooltip and enhance RadarChartV2 functionality This commit introduces the SideBarTooltipProvider to the RadarChartV2 component, improving tooltip management and interaction. It updates the AxisLabel component to support dynamic text truncation and positioning, enhancing axis labeling. Additionally, the SCSS file is modified to include new styles for the polar angle axis label. Minor adjustments are made to the DefaultLegend component for better styling consistency. --- .../RadarCharts/RadarChartV2/RadarChartV2.tsx | 100 ++++++++------- .../RadarChartV2/components/AxisLabel.tsx | 117 +++++++++++++++--- .../RadarChartV2/radarChartV2.scss | 8 ++ .../RadarCharts/RadarChartV2/utils/index.ts | 72 +++++++++++ .../Charts/context/SideBarTooltipContext.tsx | 4 +- .../shared/DefaultLegend/defaultLegend.scss | 1 + .../shared/SideBarTooltip/SideBarTooltip.tsx | 2 + 7 files changed, 238 insertions(+), 66 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/utils/index.ts diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx index a0541c160..e53b7db4b 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx @@ -1,13 +1,8 @@ import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react"; import { PolarAngleAxis, PolarGrid, Radar, RadarChart as RechartsRadarChart } from "recharts"; -import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, - keyTransform, -} from "../../Charts"; -import { ActiveDot, DefaultLegend } from "../../shared"; +import { ChartConfig, ChartContainer, ChartTooltip, keyTransform } from "../../Charts"; +import { SideBarTooltipProvider } from "../../context/SideBarTooltipContext"; +import { ActiveDot, CustomTooltipContent, DefaultLegend } from "../../shared"; import { LegendItem } from "../../types"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; @@ -63,6 +58,7 @@ export const RadarChartV2 = ({ const [isLegendExpanded, setIsLegendExpanded] = useState(false); const [containerDimensions, setContainerDimensions] = useState({ width: 0, height: 0 }); const chartContainerRef = useRef(null); + const portalContainerRef = useRef(null); useLayoutEffect(() => { // Only set up ResizeObserver if width is not provided @@ -134,46 +130,60 @@ export const RadarChartV2 = ({ ); return ( -
-
- - {}} + data={undefined} + setData={() => {}} + > +
+
+ - {grid && } - } /> + + {grid && } + } + /> - } /> - {renderRadars(dataKeys, variant, strokeWidth, areaOpacity, isAnimationActive)} - - + } /> + {renderRadars(dataKeys, variant, strokeWidth, areaOpacity, isAnimationActive)} + + +
+ {legend && ( + + )}
- {legend && ( - - )} -
+ ); }; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx index f9ba9f5dc..2302a9d67 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx @@ -1,4 +1,6 @@ -import React from "react"; +import clsx from "clsx"; +import React, { useLayoutEffect, useMemo, useRef } from "react"; +import { calculateAvailableWidth, truncateText } from "../utils"; // This is the props that are passed by recharts to the custom tick component interface AxisLabelProps { @@ -8,26 +10,103 @@ interface AxisLabelProps { payload?: { value: string; }; + className?: string; + portalContainerRef?: React.RefObject; [key: string]: any; // To allow other props from recharts } export const AxisLabel: React.FC = (props) => { - const { x, y, payload, textAnchor } = props; - // The props can be incomplete sometimes on first render. - if (!payload || x === undefined || y === undefined) { - return null; - } - return ( - - {payload.value} - - ); + const { x, y, payload, textAnchor, portalContainerRef, className } = props; + const anchorRef = useRef(null); + + // Memoize the truncated text calculation + const truncatedText = useMemo(() => { + if (!payload?.value || !portalContainerRef?.current) { + return payload?.value || ""; + } + + const container = portalContainerRef.current; + const containerWidth = container.getBoundingClientRect().width; + const padding = 0; + const fontSize = 10; + + // Calculate available width based on text anchor and position + const availableWidth = calculateAvailableWidth( + x || 0, + containerWidth, + textAnchor || "middle", + padding, + ); + + return truncateText(payload.value, availableWidth, fontSize); + }, [payload?.value, x, textAnchor, portalContainerRef]); + + useLayoutEffect(() => { + const container = portalContainerRef?.current; + const anchor = anchorRef.current; + + if (!container || !anchor || !truncatedText) { + return; + } + + // Create the label element + const labelEl = document.createElement("div"); + labelEl.textContent = truncatedText; + container.appendChild(labelEl); + + // Function to calculate and apply styles + const updatePosition = () => { + const anchorRect = anchor.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + const left = anchorRect.left - containerRect.left; + const top = anchorRect.top - containerRect.top; + + const padding = 0; + let transform = ""; + if (textAnchor === "end") { + transform = `translate(calc(-100% - ${padding}px), -50%)`; + } else if (textAnchor === "start") { + transform = `translate(${padding}px, -50%)`; + } else { + transform = "translate(-50%, -50%)"; + } + + labelEl.style.position = "absolute"; + labelEl.style.left = `${left}px`; + labelEl.style.top = `${top}px`; + labelEl.style.transform = transform; + labelEl.style.pointerEvents = "none"; + labelEl.style.zIndex = "0"; + labelEl.className = clsx("crayon-chart-polar-angle-axis-label", className); + + // Update text content if it has changed due to container resize + const containerWidth = containerRect.width; + const availableWidth = calculateAvailableWidth( + left, + containerWidth, + textAnchor || "middle", + 10, + ); + const newTruncatedText = truncateText(payload?.value || "", availableWidth, 12); + if (labelEl.textContent !== newTruncatedText) { + labelEl.textContent = newTruncatedText; + } + }; + + updatePosition(); + + // Observe the container for resizes + const resizeObserver = new ResizeObserver(updatePosition); + resizeObserver.observe(container); + + // Cleanup function to run when the component unmounts or deps change + return () => { + resizeObserver.disconnect(); + if (container.contains(labelEl)) { + container.removeChild(labelEl); + } + }; + }, [x, y, textAnchor, truncatedText, portalContainerRef, className, payload?.value]); + + return ; }; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss index 89085587e..faf90025f 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss @@ -1,3 +1,5 @@ +@use "../../../../cssUtils" as cssUtils; + .crayon-radar-chart-v2 { &-container { width: 100%; @@ -16,3 +18,9 @@ justify-content: center; } } + +.crayon-chart-polar-angle-axis-label { + pointer-events: none; + @include cssUtils.typography(label, small); + color: cssUtils.$secondary-text; +} diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/utils/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/utils/index.ts new file mode 100644 index 000000000..8c5b45cf2 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/utils/index.ts @@ -0,0 +1,72 @@ +/** + * Truncates text to fit within specified width bounds + * @param text - The original text + * @param maxWidth - Maximum width in pixels + * @param fontSize - Font size for measurement + * @returns Truncated text with ellipsis if needed + */ +export const truncateText = (text: string, maxWidth: number, fontSize = 12): string => { + if (maxWidth <= 0) return text; + + // Create a temporary canvas to measure text width + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + if (!context) return text; + + context.font = `${fontSize}px sans-serif`; + + // If text fits, return as is + if (context.measureText(text).width <= maxWidth) { + return text; + } + + // Binary search for the longest text that fits + let low = 0; + let high = text.length; + let result = text; + + while (low <= high) { + const mid = Math.floor((low + high) / 2); + const truncated = text.substring(0, mid) + "..."; + const width = context.measureText(truncated).width; + + if (width <= maxWidth) { + result = truncated; + low = mid + 1; + } else { + high = mid - 1; + } + } + + return result; +}; + +/** + * Detects collision with container bounds and calculates available width + * @param labelX - Label's x position + * @param containerWidth - Container's width + * @param textAnchor - Text anchor position + * @param padding - Padding around the text + * @returns Available width for the label + */ +export const calculateAvailableWidth = ( + labelX: number, + containerWidth: number, + textAnchor: string, + padding = 10, +): number => { + switch (textAnchor) { + case "start": + // Text starts at labelX, extends to the right + return Math.max(0, containerWidth - labelX - padding); + case "end": + // Text ends at labelX, extends to the left + return Math.max(0, labelX - padding); + case "middle": + default: + // Text is centered at labelX + const leftSpace = labelX - padding; + const rightSpace = containerWidth - labelX - padding; + return Math.max(0, Math.min(leftSpace, rightSpace) * 2); + } +}; diff --git a/js/packages/react-ui/src/components/Charts/context/SideBarTooltipContext.tsx b/js/packages/react-ui/src/components/Charts/context/SideBarTooltipContext.tsx index 273db1c71..4fad8db27 100644 --- a/js/packages/react-ui/src/components/Charts/context/SideBarTooltipContext.tsx +++ b/js/packages/react-ui/src/components/Charts/context/SideBarTooltipContext.tsx @@ -10,7 +10,7 @@ export interface SideBarChartData { } interface SideBarTooltipContextType { - data: SideBarChartData; + data: SideBarChartData | undefined; isSideBarTooltipOpen: boolean; setData: (data: SideBarChartData) => void; setIsSideBarTooltipOpen: (isOpen: boolean) => void; @@ -22,7 +22,7 @@ interface SideBarTooltipProviderProps { children: ReactNode; isSideBarTooltipOpen: boolean; setIsSideBarTooltipOpen: (isOpen: boolean) => void; - data: SideBarChartData; + data: SideBarChartData | undefined; setData: (data: SideBarChartData) => void; } diff --git a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss index 5580e7cd5..1e2c9af57 100644 --- a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss @@ -74,6 +74,7 @@ &-toggle-button { @include cssUtils.typography(label, small); color: cssUtils.$primary-text; + height: 16px; &-icon { width: 12px; diff --git a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx index dfd52f482..a7e04ad9e 100644 --- a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx @@ -16,6 +16,8 @@ const capitalizeString = (str: string): string => { const SideBarTooltip = React.memo(({ height }: SideBarTooltipProps) => { const { setIsSideBarTooltipOpen, data } = useSideBarTooltip(); + if (!data) return null; + const handleClose = useCallback(() => { setIsSideBarTooltipOpen(false); }, [setIsSideBarTooltipOpen]); From 40b4080233060c3631623b24568563bd7fb4ed85 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 27 Jun 2025 08:00:41 +0530 Subject: [PATCH 133/190] feat(Charts): enhance AxisLabel and SideBarTooltip functionality This commit updates the AxisLabel component to accept an isLegendExpanded prop, improving its dynamic behavior based on legend state. Additionally, it modifies the SideBarTooltip to close when no data is available, enhancing user experience by preventing unnecessary tooltip display. These changes contribute to a more interactive and responsive charting experience. --- .../RadarCharts/RadarChartV2/RadarChartV2.tsx | 7 ++++++- .../RadarChartV2/components/AxisLabel.tsx | 15 ++++++++++++--- .../shared/SideBarTooltip/SideBarTooltip.tsx | 6 +++++- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx index e53b7db4b..756354550 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx @@ -166,7 +166,12 @@ export const RadarChartV2 = ({ {grid && } } + tick={ + + } /> } /> diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx index 2302a9d67..136f25a09 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx @@ -12,11 +12,12 @@ interface AxisLabelProps { }; className?: string; portalContainerRef?: React.RefObject; + isLegendExpanded?: boolean; [key: string]: any; // To allow other props from recharts } export const AxisLabel: React.FC = (props) => { - const { x, y, payload, textAnchor, portalContainerRef, className } = props; + const { x, y, payload, textAnchor, portalContainerRef, className, isLegendExpanded } = props; const anchorRef = useRef(null); // Memoize the truncated text calculation @@ -95,7 +96,6 @@ export const AxisLabel: React.FC = (props) => { updatePosition(); - // Observe the container for resizes const resizeObserver = new ResizeObserver(updatePosition); resizeObserver.observe(container); @@ -106,7 +106,16 @@ export const AxisLabel: React.FC = (props) => { container.removeChild(labelEl); } }; - }, [x, y, textAnchor, truncatedText, portalContainerRef, className, payload?.value]); + }, [ + x, + y, + textAnchor, + truncatedText, + portalContainerRef, + className, + payload?.value, + isLegendExpanded, + ]); return ; }; diff --git a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx index a7e04ad9e..21568fcd3 100644 --- a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/SideBarTooltip.tsx @@ -16,7 +16,11 @@ const capitalizeString = (str: string): string => { const SideBarTooltip = React.memo(({ height }: SideBarTooltipProps) => { const { setIsSideBarTooltipOpen, data } = useSideBarTooltip(); - if (!data) return null; + // if no data, close the tooltip + if (!data) { + setIsSideBarTooltipOpen(false); + return null; + } const handleClose = useCallback(() => { setIsSideBarTooltipOpen(false); From a71ed60ac643f77a5c5af2209e08c0e24eb5f094 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 27 Jun 2025 09:22:21 +0530 Subject: [PATCH 134/190] fix(Charts): update RadarChartV2 styles and properties This commit modifies the SCSS for RadarChartV2 to set overflow to visible, enhancing layout behavior. Additionally, it updates the RadarChartV2 component to enforce minimum width and height, ensuring better rendering and responsiveness across different chart sizes. --- .../Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx | 3 ++- .../Charts/RadarCharts/RadarChartV2/radarChartV2.scss | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx index 756354550..619df61ca 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx @@ -148,7 +148,8 @@ export const RadarChartV2 = ({ width: chartSize, height: chartSize, aspectRatio: 1, - overflow: "visible", + minWidth: 100, + minHeight: 100, }} rechartsProps={{ aspect: 1, diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss index faf90025f..b683774a9 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss @@ -13,9 +13,9 @@ width: 100%; height: 100%; display: flex; - flex-direction: column; align-items: center; justify-content: center; + overflow: visible; } } From 102b7b688fff12b8b09e2bace2a296b52fe0c51c Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 27 Jun 2025 09:43:19 +0530 Subject: [PATCH 135/190] feat(Charts): refactor RadarChartV2 for improved performance and structure This commit refactors the RadarChartV2 component by introducing memoization for the main component, enhancing rendering performance. It also updates the exports to include types, ensuring better type management and clarity in the component's API. The overall structure is improved for better maintainability. --- .../Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx | 11 +++++++---- .../Charts/RadarCharts/RadarChartV2/index.ts | 1 + .../Charts/RadarCharts/RadarChartV2/types/index.ts | 1 + .../src/components/Charts/RadarCharts/index.ts | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/types/index.ts diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx index 619df61ca..54d1be050 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react"; +import React, { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from "react"; import { PolarAngleAxis, PolarGrid, Radar, RadarChart as RechartsRadarChart } from "recharts"; import { ChartConfig, ChartContainer, ChartTooltip, keyTransform } from "../../Charts"; import { SideBarTooltipProvider } from "../../context/SideBarTooltipContext"; @@ -7,8 +7,7 @@ import { LegendItem } from "../../types"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; import { AxisLabel } from "./components/AxisLabel"; - -export type RadarChartV2Data = Array>; +import { RadarChartV2Data } from "./types"; export interface RadarChartV2Props { data: T; @@ -24,7 +23,7 @@ export interface RadarChartV2Props { size?: number; } -export const RadarChartV2 = ({ +const RadarChartV2Component = ({ data, categoryKey, theme = "ocean", @@ -193,3 +192,7 @@ export const RadarChartV2 = ({ ); }; + +export const RadarChartV2 = memo(RadarChartV2Component); + +RadarChartV2.displayName = "RadarChartV2"; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts index 59a239fc8..89a7893bf 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts @@ -1 +1,2 @@ export * from "./RadarChartV2"; +export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/types/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/types/index.ts new file mode 100644 index 000000000..7d4919a26 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/types/index.ts @@ -0,0 +1 @@ +export type RadarChartV2Data = Array>; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/index.ts index e69de29bb..59a239fc8 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/index.ts +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/index.ts @@ -0,0 +1 @@ +export * from "./RadarChartV2"; From ad088413b423200626ecea284ac50ab2543b2ced Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 27 Jun 2025 21:39:00 +0530 Subject: [PATCH 136/190] refactor(Charts): enhance AxisLabel text truncation logic and update utility functions This commit improves the AxisLabel component by refining the text truncation logic to better handle available space and font size adjustments. The truncateText utility function is updated to use a default font size of 10 and the calculation of available width is modified to account for padding. Additionally, detailed comments are added to clarify the functionality of the truncation and width calculation processes, enhancing code maintainability and readability. --- .../RadarChartV2/components/AxisLabel.tsx | 28 ++++++++++++++--- .../RadarCharts/RadarChartV2/utils/index.ts | 31 +++++++++++++------ 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx index 136f25a09..0dfad45a4 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx @@ -20,18 +20,36 @@ export const AxisLabel: React.FC = (props) => { const { x, y, payload, textAnchor, portalContainerRef, className, isLegendExpanded } = props; const anchorRef = useRef(null); - // Memoize the truncated text calculation + /** + * Memoizes the calculation of truncated text for axis labels + * + * This hook handles text truncation based on available space in the chart container: + * 1. Returns empty string or original value if payload/container is missing + * 2. Calculates container width and available space based on text anchor position + * 3. Truncates text to fit within available width using specified font size + * + * @returns {string} Truncated text that fits within available space + * + * Dependencies: + * - payload?.value: The text content to truncate + * - x: X-coordinate of the label + * - textAnchor: Text anchor position ('start', 'middle', 'end') + * - portalContainerRef: Reference to container element + */ const truncatedText = useMemo(() => { if (!payload?.value || !portalContainerRef?.current) { return payload?.value || ""; } const container = portalContainerRef.current; + // Get the width of the container const containerWidth = container.getBoundingClientRect().width; + // Get the padding of the container const padding = 0; + // Get the font size of the text const fontSize = 10; - // Calculate available width based on text anchor and position + // Calculate available width based on text anchor and position, and padding const availableWidth = calculateAvailableWidth( x || 0, containerWidth, @@ -85,10 +103,10 @@ export const AxisLabel: React.FC = (props) => { const availableWidth = calculateAvailableWidth( left, containerWidth, - textAnchor || "middle", - 10, + textAnchor ?? "middle", + 0, ); - const newTruncatedText = truncateText(payload?.value || "", availableWidth, 12); + const newTruncatedText = truncateText(payload?.value ?? "", availableWidth, 10); if (labelEl.textContent !== newTruncatedText) { labelEl.textContent = newTruncatedText; } diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/utils/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/utils/index.ts index 8c5b45cf2..2e29e5816 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/utils/index.ts +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/utils/index.ts @@ -5,7 +5,7 @@ * @param fontSize - Font size for measurement * @returns Truncated text with ellipsis if needed */ -export const truncateText = (text: string, maxWidth: number, fontSize = 12): string => { +const truncateText = (text: string, maxWidth: number, fontSize = 10): string => { if (maxWidth <= 0) return text; // Create a temporary canvas to measure text width @@ -13,7 +13,7 @@ export const truncateText = (text: string, maxWidth: number, fontSize = 12): str const context = canvas.getContext("2d"); if (!context) return text; - context.font = `${fontSize}px sans-serif`; + context.font = `${fontSize}px Inter`; // If text fits, return as is if (context.measureText(text).width <= maxWidth) { @@ -42,18 +42,27 @@ export const truncateText = (text: string, maxWidth: number, fontSize = 12): str }; /** - * Detects collision with container bounds and calculates available width - * @param labelX - Label's x position - * @param containerWidth - Container's width - * @param textAnchor - Text anchor position - * @param padding - Padding around the text - * @returns Available width for the label + * Calculates the available width for a text label based on its position and anchor point + * + * This function determines how much horizontal space is available for a text label + * by checking its position relative to the container bounds. The calculation varies + * based on the text anchor position: + * + * - For "start" anchored text: Available space is from labelX to container right edge + * - For "end" anchored text: Available space is from container left edge to labelX + * - For "middle" anchored text: Takes minimum of left/right space and doubles it + * + * @param labelX - X coordinate of the label position + * @param containerWidth - Total width of the container + * @param textAnchor - How text is anchored ("start", "end", or "middle") + * @param padding - Optional padding to maintain from container edges (default 10px) + * @returns The maximum width available for the label in pixels */ -export const calculateAvailableWidth = ( +const calculateAvailableWidth = ( labelX: number, containerWidth: number, textAnchor: string, - padding = 10, + padding = 0, ): number => { switch (textAnchor) { case "start": @@ -70,3 +79,5 @@ export const calculateAvailableWidth = ( return Math.max(0, Math.min(leftSpace, rightSpace) * 2); } }; + +export { truncateText, calculateAvailableWidth }; \ No newline at end of file From 42eed5565a05a0b2b54b3b7203ff9d408d58c43f Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Fri, 27 Jun 2025 22:30:23 +0530 Subject: [PATCH 137/190] refactor(Charts): update XAxisTick to use forwardRef and improve text rendering logic This commit refactors the XAxisTick component to utilize React's forwardRef for better integration with parent components. Additionally, it enhances the text rendering logic by ensuring the reference is correctly applied to the SVG group element. This change improves the overall flexibility and performance of the XAxisTick component. --- .../Charts/shared/XAxisTick/XAxisTick.tsx | 9 +- .../utils/BarAndLineUtils/AreaAndLineUtils.ts | 98 ++++++++++++------- 2 files changed, 67 insertions(+), 40 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx index 97d8c27bd..e80e578c1 100644 --- a/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx @@ -1,3 +1,4 @@ +import React from "react"; interface XAxisTickProps { x?: number; y?: number; @@ -26,7 +27,7 @@ interface XAxisTickProps { isLastTick?: (value: string) => boolean; } -const XAxisTick: React.FC = (props) => { +const XAxisTick = React.forwardRef((props, ref) => { const { x, y, @@ -59,7 +60,7 @@ const XAxisTick: React.FC = (props) => { } return ( - + = (props) => { ); -}; +}); + +XAxisTick.displayName = 'XAxisTick'; export { XAxisTick }; export type { XAxisTickProps }; diff --git a/js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/AreaAndLineUtils.ts b/js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/AreaAndLineUtils.ts index 25cce8f1e..649bd6eff 100644 --- a/js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/AreaAndLineUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/AreaAndLineUtils.ts @@ -54,52 +54,76 @@ export const getWidthOfData = (data: ChartData, containerWidth: number) => { * this function can be improved for coalition detection and better truncation */ export const getXAxisTickFormatter = (groupWidth?: number, containerWidth?: number) => { - const CHAR_WIDTH = 7; // Average character width in pixels for most fonts - const ELLIPSIS_WIDTH = CHAR_WIDTH * 3; // "..." takes about 3 character widths - const PADDING = 5; // Safety padding for better visual spacing + const PADDING = 10; // More generous padding for visual clarity + + // Setup canvas context once per formatter creation for efficiency. + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + if (context) { + // Should match the chart's actual font for accuracy + context.font = "12px Inter"; + } - // closure is happening here. return (value: string) => { - // Convert to string in case we get numbers + // If canvas isn't supported, or for some reason context is null, return original value. + if (!context) return String(value); + const stringValue = String(value); - // If no groupWidth provided, fall back to simple responsive logic - if (!groupWidth) { - // Use container width for responsive truncation - if (containerWidth) { - // Responsive logic based on container width - if (containerWidth < 400) { - // Small containers: very aggressive truncation - return stringValue.length > 4 ? `${stringValue.slice(0, 4)}...` : stringValue; - } else if (containerWidth < 600) { - // Medium containers: moderate truncation - return stringValue.length > 8 ? `${stringValue.slice(0, 8)}...` : stringValue; - } else { - // Large containers: less aggressive truncation - return stringValue.length > 12 ? `${stringValue.slice(0, 12)}...` : stringValue; - } - } + // Determine the maximum available width for the tick. Prioritize groupWidth. + let availableWidth = 0; + if (groupWidth) { + availableWidth = Math.max(0, groupWidth - PADDING); + } else if (containerWidth) { + // Fallback responsive logic if no groupWidth is available. + // We assume a certain number of ticks could be visible. + // This is less accurate but better than nothing. + const assumedMaxTicks = containerWidth / 100; // e.g., assume ticks are ~100px apart + availableWidth = Math.max(0, containerWidth / assumedMaxTicks - PADDING); + } else { + // No width info at all, perform a simple character slice as a last resort. + return stringValue.length > 10 ? `${stringValue.slice(0, 10)}...` : stringValue; + } - // Default fallback when no width info available - return stringValue.length > 6 ? `${stringValue.slice(0, 6)}...` : stringValue; + // If the original text already fits, return it. + if (context.measureText(stringValue).width <= availableWidth) { + return stringValue; } - const availableWidth = Math.max(0, groupWidth - PADDING); - const maxCharsWithoutEllipsis = Math.floor(availableWidth / CHAR_WIDTH); - const maxCharsWithEllipsis = Math.floor((availableWidth - ELLIPSIS_WIDTH) / CHAR_WIDTH); + // If it doesn't fit, perform a binary search to find the best truncation point. + let low = 0; + let high = stringValue.length; + let result = ""; + + // binary search to find the best truncation point. + while (low <= high) { + const mid = Math.floor((low + high) / 2); + // Don't append "..." if mid is 0, just return an empty string or first char. + if (mid === 0) { + low = mid + 1; + continue; + } + const truncated = stringValue.substring(0, mid) + "..."; + const measuredWidth = context.measureText(truncated).width; + + if (measuredWidth <= availableWidth) { + result = truncated; + low = mid + 1; + } else { + high = mid - 1; + } + } - // Intelligent ellipsis handling - if (stringValue.length <= maxCharsWithoutEllipsis) { - // Full text fits comfortably - return stringValue; - } else if (maxCharsWithEllipsis >= 3) { - // We can fit at least 3 characters + ellipsis - return `${stringValue.slice(0, maxCharsWithEllipsis)}...`; - } else { - // Very limited space - just show what we can without ellipsis - // (ellipsis would take more space than it's worth) - return stringValue.slice(0, Math.max(1, Math.min(6, maxCharsWithoutEllipsis))); + // A final check: if result is empty (very tight space), it might be better + // to show the first character instead of nothing. + if (result === "") { + const firstChar = stringValue.substring(0, 1); + if (context.measureText(firstChar).width <= availableWidth) { + return firstChar; + } } + + return result; }; }; From d7f7410cfa24d4d10fca197c0b00df79dd3f0a26 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sat, 28 Jun 2025 00:11:26 +0530 Subject: [PATCH 138/190] refactor(Charts): adjust RadarChartV2 area opacity and enhance AxisLabel documentation This commit updates the RadarChartV2 component to reduce the areaOpacity from 0.5 to 0.2 for improved visual clarity. Additionally, it enhances the documentation in the AxisLabel component by refining comments related to text truncation and available width calculations, ensuring better understanding and maintainability of the code. Minor formatting adjustments are also made in the XAxisTick component for consistency. --- .../Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx | 4 +++- .../RadarCharts/RadarChartV2/components/AxisLabel.tsx | 6 +++--- .../Charts/RadarCharts/RadarChartV2/utils/index.ts | 8 ++++---- .../src/components/Charts/shared/XAxisTick/XAxisTick.tsx | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx index 54d1be050..5f916d2c7 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx @@ -31,7 +31,7 @@ const RadarChartV2Component = ({ grid = true, legend = true, strokeWidth = 2, - areaOpacity = 0.5, + areaOpacity = 0.2, icons = {}, isAnimationActive = true, size, @@ -117,6 +117,8 @@ const RadarChartV2Component = ({ key={key} dataKey={key} fill={color} + stroke={color} + strokeWidth={strokeWidth} fillOpacity={areaOpacity} isAnimationActive={isAnimationActive} activeDot={} diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx index 0dfad45a4..0a08cbbd5 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx @@ -22,14 +22,14 @@ export const AxisLabel: React.FC = (props) => { /** * Memoizes the calculation of truncated text for axis labels - * + * * This hook handles text truncation based on available space in the chart container: * 1. Returns empty string or original value if payload/container is missing * 2. Calculates container width and available space based on text anchor position * 3. Truncates text to fit within available width using specified font size - * + * * @returns {string} Truncated text that fits within available space - * + * * Dependencies: * - payload?.value: The text content to truncate * - x: X-coordinate of the label diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/utils/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/utils/index.ts index 2e29e5816..a2780afb6 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/utils/index.ts +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/utils/index.ts @@ -43,15 +43,15 @@ const truncateText = (text: string, maxWidth: number, fontSize = 10): string => /** * Calculates the available width for a text label based on its position and anchor point - * + * * This function determines how much horizontal space is available for a text label * by checking its position relative to the container bounds. The calculation varies * based on the text anchor position: - * + * * - For "start" anchored text: Available space is from labelX to container right edge * - For "end" anchored text: Available space is from container left edge to labelX * - For "middle" anchored text: Takes minimum of left/right space and doubles it - * + * * @param labelX - X coordinate of the label position * @param containerWidth - Total width of the container * @param textAnchor - How text is anchored ("start", "end", or "middle") @@ -80,4 +80,4 @@ const calculateAvailableWidth = ( } }; -export { truncateText, calculateAvailableWidth }; \ No newline at end of file +export { calculateAvailableWidth, truncateText }; diff --git a/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx index e80e578c1..856df0d4d 100644 --- a/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx @@ -75,7 +75,7 @@ const XAxisTick = React.forwardRef((props, ref) => ); }); -XAxisTick.displayName = 'XAxisTick'; +XAxisTick.displayName = "XAxisTick"; export { XAxisTick }; export type { XAxisTickProps }; From 2038215400102efc7f2381219614d69c3565b918 Mon Sep 17 00:00:00 2001 From: i-subham Date: Sat, 28 Jun 2025 17:14:04 +0530 Subject: [PATCH 139/190] refactor(Charts): enhance RadialChartV2 layout and responsiveness This commit refines the RadialChartV2 component by improving the layout handling for stacked legends, introducing responsive design features, and optimizing the chart's size calculations based on container dimensions. The SCSS file is updated to streamline legend styles and ensure proper alignment in both column and row layouts. Additionally, the component now utilizes a ResizeObserver for dynamic resizing, enhancing the overall user experience and adaptability of the chart. --- .../RadialChartV2/RadialChartV2.tsx | 293 ++++++------------ .../RadialChartV2/radialChartV2.scss | 115 ++----- .../stories/RadialChartV2.stories.tsx | 166 +++++++++- 3 files changed, 299 insertions(+), 275 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx index 05ae38857..4a820b6f8 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx @@ -48,6 +48,8 @@ export interface RadialChartV2Props { width?: number; } +const STACKED_LEGEND_BREAKPOINT = 600; // px + export const RadialChartV2 = ({ data, categoryKey, @@ -69,15 +71,16 @@ export const RadialChartV2 = ({ height, width, }: RadialChartV2Props) => { - const chartContainerRef = useRef(null); const wrapperRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(0); - const [containerHeight, setContainerHeight] = useState(0); - const [wrapperWidth, setWrapperWidth] = useState(0); + const [wrapperRect, setWrapperRect] = useState({ width: 0, height: 0 }); const [hoveredLegendKey, setHoveredLegendKey] = useState(null); const [isLegendExpanded, setIsLegendExpanded] = useState(false); const { activeIndex, handleMouseEnter, handleMouseLeave } = useRadialChartHover(); + // Determine layout mode based on container width + const isRowLayout = + legend && legendVariant === "stacked" && wrapperRect.width >= STACKED_LEGEND_BREAKPOINT; + // Memoize string conversions to avoid repeated calls const categoryKeyString = useMemo(() => String(categoryKey), [categoryKey]); const dataKeyString = useMemo(() => String(dataKey), [dataKey]); @@ -86,25 +89,46 @@ export const RadialChartV2 = ({ [format, dataKeyString], ); - // Use provided dimensions or observed dimensions - const effectiveWidth = useMemo(() => { - return width ?? containerWidth; - }, [width, containerWidth]); - - const effectiveHeight = useMemo(() => { - return height ?? containerHeight; - }, [height, containerHeight]); + // Use provided dimensions or observed dimensions from the wrapper + const effectiveWidth = width ?? wrapperRect.width; + const effectiveHeight = height ?? wrapperRect.height; - // Calculate chart dimensions based on the smaller dimension + // Calculate chart dimensions based on the smaller dimension of the container const chartSize = useMemo(() => { + // When in a row layout, the chart and legend are side-by-side. + if (isRowLayout) { + // The chart container takes up roughly half the width. We subtract the gap between items. + const chartContainerWidth = (effectiveWidth - 20) / 2; + // The size of the chart is the smaller of its container's width or the total available height. + const size = Math.min(chartContainerWidth, effectiveHeight); + return Math.max(150, size); + } + // In a column layout, the chart's size is constrained by the smaller of the total container's width or height. const size = Math.min(effectiveWidth, effectiveHeight); - return Math.max(200, Math.min(size, 800)); // Min 200px, max 800px - }, [effectiveWidth, effectiveHeight]); + return Math.max(150, size); + }, [effectiveWidth, effectiveHeight, isRowLayout]); - // Calculate chart dimensions - const dimensions = useMemo(() => { - return calculateRadialChartDimensions(chartSize, variant); - }, [chartSize, variant]); + const chartSizeStyle = useMemo( + () => ({ + width: chartSize, + height: chartSize, + }), + [chartSize], + ); + + const rechartsProps = useMemo( + () => ({ + width: chartSize, + height: chartSize, + }), + [chartSize], + ); + + // Calculate chart radii + const dimensions = useMemo( + () => calculateRadialChartDimensions(chartSize, variant), + [chartSize, variant], + ); // Memoize expensive data transformations and configurations const transformedData = useMemo( @@ -134,32 +158,27 @@ export const RadialChartV2 = ({ // Memoize gradient definitions const gradientDefinitions = useMemo(() => { if (!useGradients) return null; - const chartColors = Object.values(chartConfig) .map((config) => config.color) .filter((color): color is string => color !== undefined); - return createRadialGradientDefinitions(transformedData, chartColors, gradientColors); }, [useGradients, chartConfig, transformedData, gradientColors]); // Create legend items for both variants - const legendItems = useMemo(() => { - return data.map((item, index) => ({ - key: String(item[categoryKey]), - label: String(item[categoryKey]), - value: Number(item[dataKey]), - color: colors[index] || "#000000", - })); - }, [data, categoryKey, dataKey, colors]); + const legendItems = useMemo( + () => + data.map((item, index) => ({ + key: String(item[categoryKey]), + label: String(item[categoryKey]), + value: Number(item[dataKey]), + color: colors[index] || "#000000", + })), + [data, categoryKey, dataKey, colors], + ); - // Create DefaultLegend items const defaultLegendItems = useMemo((): LegendItem[] => { - return data.map((item, index) => ({ - key: String(item[categoryKey]), - label: String(item[categoryKey]), - color: colors[index] || "#000000", - })); - }, [data, categoryKey, colors]); + return legendItems.map(({ key, label, color }) => ({ key, label, color })); + }, [legendItems]); // Memoize sorted data for legend hover handling const sortedData = useMemo( @@ -167,20 +186,10 @@ export const RadialChartV2 = ({ [data, dataKey], ); - // Handle legend hover - const handleLegendHover = useCallback( - (key: string | null) => { - if (legendVariant !== "stacked") return; - setHoveredLegendKey(key); - }, - [legendVariant], - ); - // Handle legend item hover to highlight radial bar const handleLegendItemHover = useCallback( (index: number | null) => { if (legendVariant !== "stacked") return; - if (index !== null) { const item = sortedData[index]; if (item) { @@ -201,14 +210,14 @@ export const RadialChartV2 = ({ // Enhanced chart hover handlers const handleChartMouseEnter = useCallback( - (data: any, index: number) => { - handleMouseEnter(data, index); + (entry: any, index: number) => { + handleMouseEnter(entry, index); if (legend && legendVariant === "stacked") { - const categoryValue = String(data[categoryKey]); - setHoveredLegendKey(categoryValue); + setHoveredLegendKey(String(entry[categoryKey])); } + eventHandlers.onMouseEnter?.(entry, index); }, - [handleMouseEnter, categoryKey, legend, legendVariant], + [handleMouseEnter, categoryKey, legend, legendVariant, eventHandlers.onMouseEnter], ); const handleChartMouseLeave = useCallback(() => { @@ -216,124 +225,45 @@ export const RadialChartV2 = ({ if (legend && legendVariant === "stacked") { setHoveredLegendKey(null); } - }, [handleMouseLeave, legend, legendVariant]); - - // Determine layout based on wrapper width and legend variant - const layoutConfig = useMemo(() => { - if (!legend || legendVariant !== "stacked") { - return { isRow: false, isMobile: false }; - } - - const availableWidth = wrapperWidth || containerWidth; - const isMobileLayout = availableWidth <= 400; - - return { - isRow: !isMobileLayout && availableWidth > 400, - isMobile: isMobileLayout, - }; - }, [legend, legendVariant, wrapperWidth, containerWidth]); - - // Memoize style objects to prevent unnecessary re-renders - const containerStyle = useMemo( - () => ({ - width: width ? `${width}px` : "100%", - height: height ? `${height}px` : "100%", - flex: legend && legendVariant === "stacked" && layoutConfig.isRow ? "1" : "none", - }), - [width, height, legend, legendVariant, layoutConfig.isRow], - ); - - const innerContainerStyle = useMemo( - () => ({ - width: "100%", - height: "100%", - display: "flex", - alignItems: "center", - justifyContent: "center", - }), - [], - ); - - const chartSizeStyle = useMemo( - () => ({ - width: chartSize, - height: chartSize, - }), - [chartSize], - ); - - const legendContainerStyle = useMemo( - () => ({ - flex: layoutConfig.isRow ? "1" : "none", - width: layoutConfig.isRow ? "auto" : "100%", - }), - [layoutConfig.isRow], - ); - - const rechartsProps = useMemo( - () => ({ - width: chartSize, - height: chartSize, - }), - [chartSize], - ); - - // Memoize angle calculations - const startAngle = useMemo(() => (variant === "circular" ? -90 : 0), [variant]); - const endAngle = useMemo(() => (variant === "circular" ? 270 : 180), [variant]); + eventHandlers.onMouseLeave?.(); + }, [handleMouseLeave, legend, legendVariant, eventHandlers.onMouseLeave]); + // Setup ResizeObserver to watch the wrapper element useEffect(() => { - if (!wrapperRef.current) { - return () => {}; + const wrapper = wrapperRef.current; + if (!wrapper) return; + + // Use ResizeObserver if component is in responsive mode (no fixed width/height) + if (!width || !height) { + const observer = new ResizeObserver((entries) => { + const entry = entries[0]; + if (entry) { + setWrapperRect({ + width: entry.contentRect.width, + height: entry.contentRect.height, + }); + } + }); + observer.observe(wrapper); + return () => observer.disconnect(); } - - const wrapperResizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - setWrapperWidth(entry.contentRect.width); - } - }); - - wrapperResizeObserver.observe(wrapperRef.current); - - return () => { - wrapperResizeObserver.disconnect(); - }; - }, []); - - useEffect(() => { - if ((width && height) || !chartContainerRef.current) { - return () => {}; + // If fixed dimensions are provided, just set them once. + else { + setWrapperRect({ width, height }); } - - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const newWidth = entry.contentRect.width; - const newHeight = entry.contentRect.height; - setContainerWidth(newWidth); - setContainerHeight(newHeight); - } - }); - - resizeObserver.observe(chartContainerRef.current); - - return () => { - resizeObserver.disconnect(); - }; }, [width, height]); - // Memoize the renderLegend function const renderLegend = useCallback(() => { if (!legend) return null; - if (legendVariant === "stacked") { return ( -
+
); @@ -341,7 +271,7 @@ export const RadialChartV2 = ({ return ( @@ -350,47 +280,29 @@ export const RadialChartV2 = ({ legend, legendVariant, legendItems, - handleLegendHover, hoveredLegendKey, handleLegendItemHover, - layoutConfig.isRow, - wrapperWidth, + wrapperRect.width, + isRowLayout, defaultLegendItems, - containerWidth, - legendContainerStyle, + isLegendExpanded, ]); - // Memoize className calculations - const wrapperClassName = useMemo( - () => - clsx("crayon-radial-chart-container-wrapper", className, { - "crayon-radial-chart-container-wrapper--stacked-legend": - legend && legendVariant === "stacked" && layoutConfig.isRow, - "crayon-radial-chart-container-wrapper--stacked-legend-column": - legend && legendVariant === "stacked" && !layoutConfig.isRow, - "crayon-radial-chart-container-wrapper--default-legend": - legend && legendVariant === "default", - }), - [className, legend, legendVariant, layoutConfig.isRow], - ); + const wrapperClassName = clsx("crayon-radial-chart-container-wrapper", className, { + "layout-row": isRowLayout, + "layout-column": !isRowLayout, + "legend-default": legend && legendVariant === "default", + "legend-stacked": legend && legendVariant === "stacked", + }); - const containerClassName = useMemo( - () => - clsx("crayon-radial-chart-container", { - "crayon-radial-chart-container--with-stacked-legend": - legend && legendVariant === "stacked" && layoutConfig.isRow, - }), - [legend, legendVariant, layoutConfig.isRow], - ); + // Correct angles for semicircle (top half) + const startAngle = variant === "semicircle" ? 180 : 0; + const endAngle = variant === "semicircle" ? 0 : 360; return ( -
-
-
+
+
+
({ /> } /> - {gradientDefinitions && {gradientDefinitions}} - ({ const fill = useGradients ? `url(#radial-gradient-${index})` : config?.color || colors[index]; - return ( ); diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/radialChartV2.scss b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/radialChartV2.scss index 4da5419c3..5b530c25b 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/radialChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/radialChartV2.scss @@ -1,139 +1,90 @@ @use "../../../../cssUtils" as cssUtils; +// Main wrapper for the entire chart component .crayon-radial-chart-container-wrapper { + display: flex; width: 100%; height: 100%; position: relative; - overflow: hidden; - display: flex; gap: 20px; - // Default legend variant - always column layout (chart on top, legend at bottom) - &--default-legend { + // Default legend is always a column layout + &.legend-default { flex-direction: column; align-items: center; - gap: 20px; } - // Stacked legend variant - responsive layout (side-by-side on desktop, stacked on mobile) - &--stacked-legend { - display: flex; - flex-direction: row; + // Stacked legend uses a column layout by default (mobile-first) + &.legend-stacked.layout-column { + flex-direction: column; align-items: center; - gap: 20px; - min-height: 0; } - &--stacked-legend-column { - display: flex; - flex-direction: column; - align-items: center; - gap: 20px; + // Switches to a row layout on wider containers + &.legend-stacked.layout-row { + flex-direction: row; + align-items: center; // Vertically center chart and legend } } +// Container for the chart itself .crayon-radial-chart-container { width: 100%; height: 100%; - position: relative; - overflow: hidden; display: flex; align-items: center; justify-content: center; - min-height: 0; + position: relative; - // When in row layout with stacked legend, ensure proper sizing - &--with-stacked-legend { + // When in a row layout, the chart container should be flexible + .layout-row & { flex: 1; - min-width: 0; + min-width: 0; // Prevent flexbox overflow issues } } -.crayon-radial-chart-container-inner { - width: 100%; - height: 100%; +// Recharts chart styles +.crayon-radial-chart { display: flex; align-items: center; justify-content: center; -} - -.crayon-radial-chart { - position: relative; - // Add smooth transitions for hover effects .recharts-radial-bar-sector { transition: all 0.2s ease-in-out; } - // Improve grid appearance .recharts-polar-grid { opacity: 0.3; } - // Ensure proper hover states for radial bars - .recharts-radial-bar { - .recharts-radial-bar-sector { - transition: - opacity 0.2s ease-in-out, - stroke 0.2s ease-in-out, - stroke-width 0.2s ease-in-out; - } - } - - // Label positioning and styling .recharts-label { font-weight: 500; fill: var(--crayon-text-primary); } } +// Container for the legend component .crayon-radial-chart-legend-container { - height: 100%; - display: flex; - align-items: flex-start; - justify-content: flex-start; - min-width: 0; - overflow: hidden; - - // Ensure the legend takes available space in row layout - .crayon-stacked-legend { - width: 100%; - min-width: 0; - } -} - -.crayon-radial-chart-legend { display: flex; - align-items: center; justify-content: center; - // Stacked legend variant - responsive positioning - &:not(.crayon-radial-chart-legend--bottom) { - // Mobile layout - @media (max-width: 599px) { - width: 100%; - margin-top: 20px; - } - - // Desktop layout - @media (min-width: 600px) { - width: 100%; - } + // Default legend is centered at the bottom + .legend-default & { + width: 100%; + align-items: center; } - // Stacked legend variant styling - .crayon-stacked-legend { + // Stacked legend in column layout + .layout-column.legend-stacked & { width: 100%; + align-items: flex-start; + } - // Mobile layout for stacked legend - @media (max-width: 599px) { - width: 100%; - margin-top: 20px; - } - - // Desktop layout for stacked legend - @media (min-width: 600px) { - width: 100%; - } + // Stacked legend in row layout + .layout-row.legend-stacked & { + flex: 1; + min-width: 0; // Prevent flexbox overflow + height: 100%; + align-items: center; // Vertically center legend content } } diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx index 08af0f4da..914b70a8e 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; import { Card } from "../../../../Card"; import { RadialChartV2, RadialChartV2Props } from "../RadialChartV2"; @@ -220,7 +221,7 @@ export const RadialChartV2Demo: Story = { width: undefined, }, render: (args: any) => ( - + ), @@ -361,3 +362,166 @@ export const RadialChartV2Minimal: Story = { }, }, }; + +export const ResizableAndResponsive: Story = { + name: "Resizable and Responsive", + args: { + data: radialChartData, + categoryKey: "month", + dataKey: "value", + theme: "spectrum", + variant: "circular", + format: "number", + legend: true, + legendVariant: "stacked", + grid: false, + isAnimationActive: false, + cornerRadius: 8, + useGradients: false, + }, + render: (args: any) => { + const [dimensions, setDimensions] = useState({ width: 700, height: 400 }); + + const handleMouseDown = (e: React.MouseEvent, handle: string) => { + e.preventDefault(); + e.stopPropagation(); + const startX = e.clientX; + const startY = e.clientY; + const startWidth = dimensions.width; + const startHeight = dimensions.height; + + const doDrag = (e: MouseEvent) => { + const dx = e.clientX - startX; + const dy = e.clientY - startY; + let newWidth = startWidth; + let newHeight = startHeight; + + if (handle.includes("e")) newWidth = startWidth + dx; + if (handle.includes("w")) newWidth = startWidth - dx; + if (handle.includes("s")) newHeight = startHeight + dy; + if (handle.includes("n")) newHeight = startHeight - dy; + + setDimensions({ + width: Math.max(300, newWidth), + height: Math.max(300, newHeight), + }); + }; + + const stopDrag = () => { + document.removeEventListener("mousemove", doDrag); + document.removeEventListener("mouseup", stopDrag); + }; + + document.addEventListener("mousemove", doDrag); + document.addEventListener("mouseup", stopDrag); + }; + + const handleStyle: React.CSSProperties = { + position: "absolute", + background: "#3b82f6", + opacity: 0.5, + zIndex: 10, + }; + + return ( + + + + {/* Resizer handles */} +
handleMouseDown(e, "n")} + /> +
handleMouseDown(e, "s")} + /> +
handleMouseDown(e, "w")} + /> +
handleMouseDown(e, "e")} + /> +
handleMouseDown(e, "nw")} + /> +
handleMouseDown(e, "ne")} + /> +
handleMouseDown(e, "sw")} + /> +
handleMouseDown(e, "se")} + /> +
+ {dimensions.width}px × {dimensions.height}px +
+ + ); + }, + parameters: { + docs: { + description: { + story: + "This story demonstrates the responsive and resizable behavior of the RadialChartV2. Drag the edges or corners of the dashed container to resize it and see how the chart and its legend adapt perfectly to the new dimensions.", + }, + }, + }, +}; From d4145e00913f64f9258f4502c93c8dbb0b40a310 Mon Sep 17 00:00:00 2001 From: i-subham Date: Sat, 28 Jun 2025 18:12:03 +0530 Subject: [PATCH 140/190] refactor(Charts): enhance PieChartV2 layout and responsiveness This commit improves the PieChartV2 component by refining the layout handling for stacked legends and introducing responsive design features. The SCSS file is updated to streamline legend styles and ensure proper alignment in both column and row layouts. Additionally, the component now utilizes a ResizeObserver for dynamic resizing, enhancing the overall user experience and adaptability of the chart. New stories are added to demonstrate the responsive and resizable behavior of the PieChartV2. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 289 +++++------------- .../PieCharts/PieChartV2/pieChartV2.scss | 105 ++----- .../PieChartV2/stories/PieChartV2.stories.tsx | 167 +++++++++- 3 files changed, 276 insertions(+), 285 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 19dd0c6fc..106e1eb87 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -50,6 +50,8 @@ export interface PieChartV2Props { width?: number; } +const STACKED_LEGEND_BREAKPOINT = 600; // px + export const PieChartV2 = ({ data, categoryKey, @@ -72,15 +74,16 @@ export const PieChartV2 = ({ height, width, }: PieChartV2Props) => { - const chartContainerRef = useRef(null); const wrapperRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(0); - const [containerHeight, setContainerHeight] = useState(0); - const [wrapperWidth, setWrapperWidth] = useState(0); + const [wrapperRect, setWrapperRect] = useState({ width: 0, height: 0 }); const [hoveredLegendKey, setHoveredLegendKey] = useState(null); const [isLegendExpanded, setIsLegendExpanded] = useState(false); const { activeIndex, handleMouseEnter, handleMouseLeave } = useChartHover(); + // Determine layout mode based on container width + const isRowLayout = + legend && legendVariant === "stacked" && wrapperRect.width >= STACKED_LEGEND_BREAKPOINT; + // Memoize string conversions to avoid repeated calls const categoryKeyString = useMemo(() => String(categoryKey), [categoryKey]); const dataKeyString = useMemo(() => String(dataKey), [dataKey]); @@ -89,20 +92,23 @@ export const PieChartV2 = ({ [format, dataKeyString], ); - // Use provided dimensions or observed dimensions - const effectiveWidth = useMemo(() => { - return width ?? containerWidth; - }, [width, containerWidth]); + // Use provided dimensions or observed dimensions from the wrapper + const effectiveWidth = width ?? wrapperRect.width; + const effectiveHeight = height ?? wrapperRect.height; - const effectiveHeight = useMemo(() => { - return height ?? containerHeight; - }, [height, containerHeight]); - - // Calculate chart dimensions based on the smaller dimension + // Calculate chart dimensions based on the smaller dimension of the container const chartSize = useMemo(() => { + if (isRowLayout) { + const chartContainerWidth = (effectiveWidth - 20) / 2; // Subtract gap + const size = Math.min(chartContainerWidth, effectiveHeight); + return Math.max(150, size); + } const size = Math.min(effectiveWidth, effectiveHeight); - return Math.max(200, Math.min(size, 800)); // Min 200px, max 800px - }, [effectiveWidth, effectiveHeight]); + return Math.max(150, size); + }, [effectiveWidth, effectiveHeight, isRowLayout]); + + const chartSizeStyle = useMemo(() => ({ width: chartSize, height: chartSize }), [chartSize]); + const rechartsProps = useMemo(() => ({ width: chartSize, height: chartSize }), [chartSize]); // Memoize expensive data transformations and configurations const transformedData = useMemo( @@ -130,69 +136,45 @@ export const PieChartV2 = ({ [cornerRadius, variant, paddingAngle], ); - // Get color palette and distribute colors const palette = useMemo(() => getPalette(theme), [theme]); const colors = useMemo(() => getDistributedColors(palette, data.length), [palette, data.length]); - // Memoize gradient definitions const gradientDefinitions = useMemo(() => { if (!useGradients) return null; - const chartColors = Object.values(chartConfig) .map((config) => config.color) .filter((color): color is string => color !== undefined); - return createGradientDefinitions(transformedData, chartColors, gradientColors); }, [useGradients, chartConfig, transformedData, gradientColors]); - // Create legend items for both variants - const legendItems = useMemo(() => { - return data.map((item, index) => ({ - key: String(item[categoryKey]), - label: String(item[categoryKey]), - value: Number(item[dataKey]), - color: colors[index] || "#000000", - })); - }, [data, categoryKey, dataKey, colors]); + const legendItems = useMemo( + () => + data.map((item, index) => ({ + key: String(item[categoryKey]), + label: String(item[categoryKey]), + value: Number(item[dataKey]), + color: colors[index] || "#000000", + })), + [data, categoryKey, dataKey, colors], + ); - // Create DefaultLegend items const defaultLegendItems = useMemo((): LegendItem[] => { - return data.map((item, index) => ({ - key: String(item[categoryKey]), - label: String(item[categoryKey]), - color: colors[index] || "#000000", - })); - }, [data, categoryKey, colors]); + return legendItems.map(({ key, label, color }) => ({ key, label, color })); + }, [legendItems]); - // Memoize sorted data for legend hover handling const sortedData = useMemo( () => [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), [data, dataKey], ); - // Handle legend hover - const handleLegendHover = useCallback( - (key: string | null) => { - // Only handle legend hover for stacked legend variant - if (legendVariant !== "stacked") return; - setHoveredLegendKey(key); - }, - [legendVariant], - ); - - // Handle legend item hover to highlight pie slice const handleLegendItemHover = useCallback( (index: number | null) => { - // Only handle legend hover for stacked legend variant if (legendVariant !== "stacked") return; - if (index !== null) { - // Find the corresponding data item and set it as active const item = sortedData[index]; if (item) { const categoryValue = String(item[categoryKey]); setHoveredLegendKey(categoryValue); - // Find the index in the original data array const originalIndex = data.findIndex((d) => String(d[categoryKey]) === categoryValue); if (originalIndex !== -1) { handleMouseEnter(data[originalIndex], originalIndex); @@ -206,105 +188,35 @@ export const PieChartV2 = ({ [sortedData, categoryKey, data, handleMouseEnter, handleMouseLeave, legendVariant], ); - // Enhanced chart hover handlers const handleChartMouseEnter = useCallback( - (data: any, index: number) => { - handleMouseEnter(data, index); - // Set legend hover for stacked legend variant + (entry: any, index: number) => { + handleMouseEnter(entry, index); if (legend && legendVariant === "stacked") { - const categoryValue = String(data[categoryKey]); - setHoveredLegendKey(categoryValue); + setHoveredLegendKey(String(entry[categoryKey])); } + eventHandlers.onMouseEnter?.(entry, index); }, - [handleMouseEnter, categoryKey, legend, legendVariant], + [handleMouseEnter, categoryKey, legend, legendVariant, eventHandlers.onMouseEnter], ); const handleChartMouseLeave = useCallback(() => { handleMouseLeave(); - // Clear legend hover for stacked legend variant if (legend && legendVariant === "stacked") { setHoveredLegendKey(null); } - }, [handleMouseLeave, legend, legendVariant]); + eventHandlers.onMouseLeave?.(); + }, [handleMouseLeave, legend, legendVariant, eventHandlers.onMouseLeave]); - // Calculate dimensions based on variant const dimensions = useMemo(() => { if (variant === "donut") { return calculateTwoLevelChartDimensions(chartSize); } - return { - outerRadius: "90%", - innerRadius: 0, - middleRadius: 0, - }; + return { outerRadius: "90%", innerRadius: 0, middleRadius: 0 }; }, [variant, chartSize]); - // Determine layout based on wrapper width and legend variant - const layoutConfig = useMemo(() => { - if (!legend || legendVariant !== "stacked") { - return { isRow: false, isMobile: false }; - } - - // Use wrapper width for responsive decisions - const availableWidth = wrapperWidth || containerWidth; - const isMobileLayout = availableWidth <= 400; + const startAngle = useMemo(() => (appearance === "semiCircular" ? 180 : 0), [appearance]); + const endAngle = useMemo(() => (appearance === "semiCircular" ? 0 : 360), [appearance]); - return { - isRow: !isMobileLayout && availableWidth > 400, - isMobile: isMobileLayout, - }; - }, [legend, legendVariant, wrapperWidth, containerWidth]); - - // Memoize style objects to prevent unnecessary re-renders - const containerStyle = useMemo( - () => ({ - width: width ? `${width}px` : "100%", - height: height ? `${height}px` : "100%", - flex: legend && legendVariant === "stacked" && layoutConfig.isRow ? "1" : "none", - }), - [width, height, legend, legendVariant, layoutConfig.isRow], - ); - - const innerContainerStyle = useMemo( - () => ({ - width: "100%", - height: "100%", - display: "flex", - alignItems: "center", - justifyContent: "center", - }), - [], - ); - - const chartSizeStyle = useMemo( - () => ({ - width: chartSize, - height: chartSize, - }), - [chartSize], - ); - - const legendContainerStyle = useMemo( - () => ({ - flex: layoutConfig.isRow ? "1" : "none", - width: layoutConfig.isRow ? "auto" : "100%", - }), - [layoutConfig.isRow], - ); - - const rechartsProps = useMemo( - () => ({ - width: chartSize, - height: chartSize, - }), - [chartSize], - ); - - // Memoize angle calculations - const startAngle = useMemo(() => (appearance === "semiCircular" ? 0 : 0), [appearance]); - const endAngle = useMemo(() => (appearance === "semiCircular" ? 180 : 360), [appearance]); - - // Memoize common pie props const commonPieProps = useMemo( () => ({ data: transformedData, @@ -335,67 +247,44 @@ export const PieChartV2 = ({ ); useEffect(() => { - // Set up ResizeObserver for wrapper to get overall container width - if (!wrapperRef.current) { - return () => {}; - } - - const wrapperResizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - setWrapperWidth(entry.contentRect.width); - } - }); - - wrapperResizeObserver.observe(wrapperRef.current); - - return () => { - wrapperResizeObserver.disconnect(); - }; - }, []); - - useEffect(() => { - // Only set up ResizeObserver for chart container if dimensions are not provided - if ((width && height) || !chartContainerRef.current) { - return () => {}; + const wrapper = wrapperRef.current; + if (!wrapper) return; + if (!width || !height) { + const observer = new ResizeObserver((entries) => { + const entry = entries[0]; + if (entry) { + setWrapperRect({ + width: entry.contentRect.width, + height: entry.contentRect.height, + }); + } + }); + observer.observe(wrapper); + return () => observer.disconnect(); + } else { + setWrapperRect({ width, height }); } - - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const newWidth = entry.contentRect.width; - const newHeight = entry.contentRect.height; - setContainerWidth(newWidth); - setContainerHeight(newHeight); - } - }); - - resizeObserver.observe(chartContainerRef.current); - - return () => { - resizeObserver.disconnect(); - }; }, [width, height]); - // Memoize the renderPieCharts function const renderPieCharts = useCallback(() => { if (variant === "donut") { return [ - // Inner Pie - {transformedData.map((entry, index) => { + {transformedData.map((_entry, index) => { const hoverStyles = getHoverStyles(index, activeIndex); const fill = useGradients ? `url(#gradient-${index})` : `var(--crayon-container-hover-fills)`; - return ; })} , - // Outer Pie ({ const config = chartConfig[categoryValue]; const hoverStyles = getHoverStyles(index, activeIndex); const fill = useGradients ? `url(#gradient-${index})` : config?.color; - return ; })} , ]; } - return ( ({ const config = chartConfig[categoryValue]; const hoverStyles = getHoverStyles(index, activeIndex); const fill = useGradients ? `url(#gradient-${index})` : config?.color || colors[index]; - return ; })} @@ -441,19 +327,17 @@ export const PieChartV2 = ({ colors, ]); - // Memoize the renderLegend function const renderLegend = useCallback(() => { if (!legend) return null; - if (legendVariant === "stacked") { return ( -
+
); @@ -461,7 +345,7 @@ export const PieChartV2 = ({ return ( @@ -470,46 +354,29 @@ export const PieChartV2 = ({ legend, legendVariant, legendItems, - handleLegendHover, hoveredLegendKey, handleLegendItemHover, - layoutConfig.isRow, - wrapperWidth, + wrapperRect.width, + isRowLayout, defaultLegendItems, - containerWidth, - legendContainerStyle, + isLegendExpanded, ]); - // Memoize className calculations const wrapperClassName = useMemo( () => clsx("crayon-pie-chart-container-wrapper", className, { - "crayon-pie-chart-container-wrapper--stacked-legend": - legend && legendVariant === "stacked" && layoutConfig.isRow, - "crayon-pie-chart-container-wrapper--stacked-legend-column": - legend && legendVariant === "stacked" && !layoutConfig.isRow, - "crayon-pie-chart-container-wrapper--default-legend": legend && legendVariant === "default", + "layout-row": isRowLayout, + "layout-column": !isRowLayout, + "legend-default": legend && legendVariant === "default", + "legend-stacked": legend && legendVariant === "stacked", }), - [className, legend, legendVariant, layoutConfig.isRow], - ); - - const containerClassName = useMemo( - () => - clsx("crayon-pie-chart-container", { - "crayon-pie-chart-container--with-stacked-legend": - legend && legendVariant === "stacked" && layoutConfig.isRow, - }), - [legend, legendVariant, layoutConfig.isRow], + [className, legend, legendVariant, isRowLayout], ); return ( -
-
-
+
+
+
({ } /> - {gradientDefinitions && {gradientDefinitions}} - {renderPieCharts()} diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss index 782387997..ef64008b6 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss @@ -1,118 +1,79 @@ @use "../../../../cssUtils" as cssUtils; +// Main wrapper for the entire chart component .crayon-pie-chart-container-wrapper { + display: flex; width: 100%; height: 100%; position: relative; - overflow: hidden; - display: flex; gap: 20px; - // Default legend variant - always column layout (chart on top, legend at bottom) - &--default-legend { + // Default legend is always a column layout + &.legend-default { flex-direction: column; align-items: center; - gap: 20px; } - // Stacked legend variant - responsive layout (side-by-side on desktop, stacked on mobile) - &--stacked-legend { - display: flex; - flex-direction: row; + // Stacked legend uses a column layout by default (mobile-first) + &.legend-stacked.layout-column { + flex-direction: column; align-items: center; - gap: 20px; - min-height: 0; } - &--stacked-legend-column { - display: flex; - flex-direction: column; - align-items: center; - gap: 20px; + // Switches to a row layout on wider containers + &.legend-stacked.layout-row { + flex-direction: row; + align-items: center; // Vertically center chart and legend } } +// Container for the chart itself .crayon-pie-chart-container { - width: 100%; - height: 100%; - position: relative; - overflow: hidden; display: flex; align-items: center; justify-content: center; - min-height: 0; + position: relative; - // When in row layout with stacked legend, ensure proper sizing - &--with-stacked-legend { + // When in a row layout, the chart container should be flexible + .layout-row & { flex: 1; - min-width: 0; + min-width: 0; // Prevent flexbox overflow issues } } -.crayon-pie-chart-container-inner { - width: 100%; - height: 100%; +// Recharts chart styles +.crayon-pie-chart { display: flex; align-items: center; justify-content: center; -} - -.crayon-pie-chart { - position: relative; - // Add smooth transitions for hover effects .recharts-pie-sector { transition: all 0.2s ease-in-out; } } +// Container for the legend component .crayon-pie-chart-legend-container { - height: 100%; - display: flex; - align-items: flex-start; - justify-content: flex-start; - min-width: 0; - overflow: hidden; - - // Ensure the legend takes available space in row layout - .crayon-stacked-legend { - width: 100%; - min-width: 0; - } -} - -.crayon-pie-chart-legend { display: flex; - align-items: center; justify-content: center; - // Stacked legend variant - responsive positioning - &:not(.crayon-pie-chart-legend--bottom) { - // Mobile layout - @media (max-width: 599px) { - width: 100%; - margin-top: 20px; - } - - // Desktop layout - @media (min-width: 600px) { - width: 100%; - } + // Default legend is centered at the bottom + .legend-default & { + width: 100%; + align-items: center; } - // Stacked legend variant styling - .crayon-stacked-legend { + // Stacked legend in column layout + .layout-column.legend-stacked & { width: 100%; + align-items: flex-start; + } - // Mobile layout for stacked legend - @media (max-width: 599px) { - width: 100%; - margin-top: 20px; - } - - // Desktop layout for stacked legend - @media (min-width: 600px) { - width: 100%; - } + // Stacked legend in row layout + .layout-row.legend-stacked & { + flex: 1; + min-width: 0; // Prevent flexbox overflow + height: 100%; + align-items: center; // Vertically center legend content } } diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index 6414f4fa7..e4f6b8bd1 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; import { Card } from "../../../../Card"; import { PieChartV2, PieChartV2Props } from "../PieChartV2"; @@ -258,7 +259,7 @@ export const PieChartV2WithCarousel: Story = { width: undefined, }, render: (args: any) => ( - + ), @@ -271,3 +272,167 @@ export const PieChartV2WithCarousel: Story = { }, }, }; + +export const ResizableAndResponsive: Story = { + name: "Resizable and Responsive", + args: { + data: pieChartData, + categoryKey: "month", + dataKey: "value", + theme: "spectrum", + variant: "donut", + format: "number", + legend: true, + legendVariant: "stacked", + isAnimationActive: false, + appearance: "circular", + cornerRadius: 8, + paddingAngle: 2, + useGradients: false, + }, + render: (args: any) => { + const [dimensions, setDimensions] = useState({ width: 700, height: 400 }); + + const handleMouseDown = (e: React.MouseEvent, handle: string) => { + e.preventDefault(); + e.stopPropagation(); + const startX = e.clientX; + const startY = e.clientY; + const startWidth = dimensions.width; + const startHeight = dimensions.height; + + const doDrag = (e: MouseEvent) => { + const dx = e.clientX - startX; + const dy = e.clientY - startY; + let newWidth = startWidth; + let newHeight = startHeight; + + if (handle.includes("e")) newWidth = startWidth + dx; + if (handle.includes("w")) newWidth = startWidth - dx; + if (handle.includes("s")) newHeight = startHeight + dy; + if (handle.includes("n")) newHeight = startHeight - dy; + + setDimensions({ + width: Math.max(300, newWidth), + height: Math.max(300, newHeight), + }); + }; + + const stopDrag = () => { + document.removeEventListener("mousemove", doDrag); + document.removeEventListener("mouseup", stopDrag); + }; + + document.addEventListener("mousemove", doDrag); + document.addEventListener("mouseup", stopDrag); + }; + + const handleStyle: React.CSSProperties = { + position: "absolute", + background: "#3b82f6", + opacity: 0.5, + zIndex: 10, + }; + + return ( + + + + {/* Resizer handles */} +
handleMouseDown(e, "n")} + /> +
handleMouseDown(e, "s")} + /> +
handleMouseDown(e, "w")} + /> +
handleMouseDown(e, "e")} + /> +
handleMouseDown(e, "nw")} + /> +
handleMouseDown(e, "ne")} + /> +
handleMouseDown(e, "sw")} + /> +
handleMouseDown(e, "se")} + /> +
+ {dimensions.width}px × {dimensions.height}px +
+ + ); + }, + parameters: { + docs: { + description: { + story: + "This story demonstrates the responsive and resizable behavior of the PieChartV2. Drag the edges or corners of the dashed container to resize it and see how the chart and its legend adapt perfectly to the new dimensions.", + }, + }, + }, +}; From 0684ab91cba2f6a683d690c5a8317b8dd172da01 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sun, 29 Jun 2025 04:43:06 +0530 Subject: [PATCH 141/190] fix(Charts): prevent unnecessary state updates in RadialChartV2 This commit modifies the RadialChartV2 component to include an early return in the useEffect hook when fixed dimensions are provided. This change prevents unnecessary state updates, improving performance and ensuring more efficient rendering of the chart. --- .../Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx index 4a820b6f8..a30588d25 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx @@ -250,7 +250,9 @@ export const RadialChartV2 = ({ // If fixed dimensions are provided, just set them once. else { setWrapperRect({ width, height }); + return; } + }, [width, height]); const renderLegend = useCallback(() => { From 23bfaba7d76e42a30dbdd61d30b1e1186ebefba2 Mon Sep 17 00:00:00 2001 From: i-subham Date: Sun, 29 Jun 2025 10:28:31 +0530 Subject: [PATCH 142/190] feat(Charts): implement grouping of small slices in PieChartV2 This commit introduces a new utility function, groupSmallSlices, to the PieChartV2 component, allowing for the aggregation of smaller slices into an "Others" category when the number of slices exceeds a specified threshold. The PieChartV2 is updated to utilize this function, enhancing data visualization by simplifying the chart when there are too many small segments. Additionally, the legend items and data transformations are adjusted to reflect the processed data, improving overall chart clarity and usability. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 38 ++++++++++------ .../Charts/PieCharts/utils/PieChartUtils.ts | 43 +++++++++++++++++++ .../shared/StackedLegend/StackedLegend.tsx | 24 +---------- 3 files changed, 69 insertions(+), 36 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 106e1eb87..fa3b2896d 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -15,6 +15,7 @@ import { createEventHandlers, createSectorStyle, getHoverStyles, + groupSmallSlices, transformDataWithPercentages, useChartHover, } from "../utils/PieChartUtils"; @@ -84,6 +85,12 @@ export const PieChartV2 = ({ const isRowLayout = legend && legendVariant === "stacked" && wrapperRect.width >= STACKED_LEGEND_BREAKPOINT; + // Group small slices into "Others" if necessary + const processedData = useMemo( + () => groupSmallSlices(data, categoryKey, dataKey), + [data, categoryKey, dataKey], + ); + // Memoize string conversions to avoid repeated calls const categoryKeyString = useMemo(() => String(categoryKey), [categoryKey]); const dataKeyString = useMemo(() => String(dataKey), [dataKey]); @@ -112,13 +119,13 @@ export const PieChartV2 = ({ // Memoize expensive data transformations and configurations const transformedData = useMemo( - () => transformDataWithPercentages(data, dataKey), - [data, dataKey], + () => transformDataWithPercentages(processedData, dataKey), + [processedData, dataKey], ); const chartConfig = useMemo( - () => createChartConfig(data, categoryKey, theme), - [data, categoryKey, theme], + () => createChartConfig(processedData, categoryKey, theme), + [processedData, categoryKey, theme], ); const animationConfig = useMemo( @@ -137,7 +144,10 @@ export const PieChartV2 = ({ ); const palette = useMemo(() => getPalette(theme), [theme]); - const colors = useMemo(() => getDistributedColors(palette, data.length), [palette, data.length]); + const colors = useMemo( + () => getDistributedColors(palette, processedData.length), + [palette, processedData.length], + ); const gradientDefinitions = useMemo(() => { if (!useGradients) return null; @@ -149,13 +159,13 @@ export const PieChartV2 = ({ const legendItems = useMemo( () => - data.map((item, index) => ({ + processedData.map((item, index) => ({ key: String(item[categoryKey]), label: String(item[categoryKey]), value: Number(item[dataKey]), color: colors[index] || "#000000", })), - [data, categoryKey, dataKey, colors], + [processedData, categoryKey, dataKey, colors], ); const defaultLegendItems = useMemo((): LegendItem[] => { @@ -163,8 +173,8 @@ export const PieChartV2 = ({ }, [legendItems]); const sortedData = useMemo( - () => [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), - [data, dataKey], + () => [...processedData].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), + [processedData, dataKey], ); const handleLegendItemHover = useCallback( @@ -175,9 +185,11 @@ export const PieChartV2 = ({ if (item) { const categoryValue = String(item[categoryKey]); setHoveredLegendKey(categoryValue); - const originalIndex = data.findIndex((d) => String(d[categoryKey]) === categoryValue); - if (originalIndex !== -1) { - handleMouseEnter(data[originalIndex], originalIndex); + const processedIndex = processedData.findIndex( + (d) => String(d[categoryKey]) === categoryValue, + ); + if (processedIndex !== -1) { + handleMouseEnter(processedData[processedIndex], processedIndex); } } } else { @@ -185,7 +197,7 @@ export const PieChartV2 = ({ handleMouseLeave(); } }, - [sortedData, categoryKey, data, handleMouseEnter, handleMouseLeave, legendVariant], + [sortedData, categoryKey, processedData, handleMouseEnter, handleMouseLeave, legendVariant], ); const handleChartMouseEnter = useCallback( diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts index 65ae95464..80a0618aa 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/PieCharts/utils/PieChartUtils.ts @@ -125,6 +125,48 @@ const getHoverStyles = (index: number, activeIndex: number | null): HoverStyles // Data Transformation Utilities // ========================================== +const MAX_PIE_SLICES = 10; + +const groupSmallSlices = ( + data: T, + categoryKey: keyof T[number], + dataKey: keyof T[number], + threshold: number = MAX_PIE_SLICES, +): T => { + if (data.length <= threshold) { + return data; + } + + const sortedData = [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])); + + const topItems = sortedData.slice(0, threshold - 1); + const otherItems = sortedData.slice(threshold - 1); + + const othersValue = otherItems.reduce((sum, item) => sum + Number(item[dataKey]), 0); + + const othersItem: T[number] = { + ...data[0], // Copy structure from first item + [categoryKey]: "Others", + [dataKey]: othersValue, + }; + + // Ensure other properties are initialized to avoid undefined issues. + for (const key in othersItem) { + if (key !== categoryKey && key !== dataKey) { + // @ts-expect-error - we are trying to clear other properties + othersItem[key] = undefined; + } + } + + // Then restore the main keys + // @ts-expect-error - we are trying to build the object + othersItem[categoryKey] = "Others"; + // @ts-expect-error - we are trying to build the object + othersItem[dataKey] = othersValue; + + return [...topItems, othersItem] as T; +}; + /** * Transforms data by adding percentage calculations * @param data - The input data array @@ -270,6 +312,7 @@ export { createEventHandlers, createSectorStyle, getHoverStyles, + groupSmallSlices, transformDataWithPercentages, useChartHover, }; diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx index e41f17014..af452771d 100644 --- a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx @@ -23,7 +23,6 @@ const formatPercentage = (value: number, total: number): string => { return `${percentage.toFixed(1)}%`; }; -const MAX_VISIBLE_ITEMS = 10; const ITEM_HEIGHT = 36; // Height of each legend item const ITEM_GAP = 2; // Gap between items @@ -100,27 +99,6 @@ export const StackedLegend = ({ // Sort items by value in descending order (higher to lower) const sortedItems = [...items].sort((a, b) => b.value - a.value); - // Process items to handle more than MAX_VISIBLE_ITEMS - const processedItems = (() => { - if (sortedItems.length <= MAX_VISIBLE_ITEMS) { - return sortedItems; - } - - const visibleItems = sortedItems.slice(0, MAX_VISIBLE_ITEMS - 1); - const remainingItems = sortedItems.slice(MAX_VISIBLE_ITEMS - 1); - const remainingTotal = remainingItems.reduce((sum, item) => sum + item.value, 0); - - return [ - ...visibleItems, - { - key: "others", - label: "Others", - value: remainingTotal, - color: "var(--crayon-container-hover-fills)", - }, - ]; - })(); - return (
- {processedItems.map((item, index) => ( + {sortedItems.map((item, index) => (
Date: Sun, 29 Jun 2025 12:20:05 +0530 Subject: [PATCH 143/190] feat(Charts): enhance PieChartV2 inner cell styling and fill logic This commit introduces a new CSS class for the inner cells of the PieChartV2, applying a hover fill color defined in the SCSS. Additionally, the fill logic for both inner and outer cells is updated to utilize a color configuration based on the category value, improving visual consistency and customization of the chart's appearance. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 20 +++++++++++++------ .../PieCharts/PieChartV2/pieChartV2.scss | 4 ++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index fa3b2896d..8ec265998 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -287,12 +287,20 @@ export const PieChartV2 = ({ innerRadius={dimensions.innerRadius} outerRadius={dimensions.middleRadius} > - {transformedData.map((_entry, index) => { + {transformedData.map((entry, index) => { + const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); + const config = chartConfig[categoryValue]; const hoverStyles = getHoverStyles(index, activeIndex); - const fill = useGradients - ? `url(#gradient-${index})` - : `var(--crayon-container-hover-fills)`; - return ; + const fill = config?.color || colors[index]; + return ( + + ); })} , ({ const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); const config = chartConfig[categoryValue]; const hoverStyles = getHoverStyles(index, activeIndex); - const fill = useGradients ? `url(#gradient-${index})` : config?.color; + const fill = useGradients ? `url(#gradient-${index})` : config?.color || colors[index]; return ; })} , diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss index ef64008b6..2eb3a3359 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss @@ -50,6 +50,10 @@ .recharts-pie-sector { transition: all 0.2s ease-in-out; } + + .crayon-pie-chart__inner-cell { + fill: var(--crayon-container-hover-fills); + } } // Container for the legend component From 384985999fd55141e583ffb1a40c4b521125373c Mon Sep 17 00:00:00 2001 From: i-subham Date: Sun, 29 Jun 2025 12:28:53 +0530 Subject: [PATCH 144/190] feat(Charts): implement grouping of small slices in RadialChartV2 This commit introduces the groupSmallSlices utility function to the RadialChartV2 component, allowing for the aggregation of smaller slices into an "Others" category when the number of slices exceeds a specified threshold. The component is updated to utilize this function, enhancing data visualization by simplifying the chart when there are too many small segments. Additionally, various data transformations and legend items are adjusted to reflect the processed data, improving overall chart clarity and usability. --- .../RadialChartV2/RadialChartV2.tsx | 37 +++++++++----- .../RadialCharts/utils/RadialChartUtils.ts | 50 +++++++++++++++++++ 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx index a30588d25..7b4de451c 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx @@ -14,6 +14,7 @@ import { createRadialChartConfig, createRadialEventHandlers, getRadialHoverStyles, + groupSmallSlices, transformRadialDataWithPercentages, useRadialChartHover, } from "../utils/RadialChartUtils"; @@ -81,6 +82,12 @@ export const RadialChartV2 = ({ const isRowLayout = legend && legendVariant === "stacked" && wrapperRect.width >= STACKED_LEGEND_BREAKPOINT; + // Group small slices into "Others" if necessary + const processedData = useMemo( + () => groupSmallSlices(data, categoryKey, dataKey), + [data, categoryKey, dataKey], + ); + // Memoize string conversions to avoid repeated calls const categoryKeyString = useMemo(() => String(categoryKey), [categoryKey]); const dataKeyString = useMemo(() => String(dataKey), [dataKey]); @@ -132,13 +139,13 @@ export const RadialChartV2 = ({ // Memoize expensive data transformations and configurations const transformedData = useMemo( - () => transformRadialDataWithPercentages(data, dataKey, theme), - [data, dataKey, theme], + () => transformRadialDataWithPercentages(processedData, dataKey, theme), + [processedData, dataKey, theme], ); const chartConfig = useMemo( - () => createRadialChartConfig(data, categoryKey, theme), - [data, categoryKey, theme], + () => createRadialChartConfig(processedData, categoryKey, theme), + [processedData, categoryKey, theme], ); const animationConfig = useMemo( @@ -153,7 +160,10 @@ export const RadialChartV2 = ({ // Get color palette and distribute colors const palette = useMemo(() => getPalette(theme), [theme]); - const colors = useMemo(() => getDistributedColors(palette, data.length), [palette, data.length]); + const colors = useMemo( + () => getDistributedColors(palette, processedData.length), + [palette, processedData.length], + ); // Memoize gradient definitions const gradientDefinitions = useMemo(() => { @@ -167,13 +177,13 @@ export const RadialChartV2 = ({ // Create legend items for both variants const legendItems = useMemo( () => - data.map((item, index) => ({ + processedData.map((item, index) => ({ key: String(item[categoryKey]), label: String(item[categoryKey]), value: Number(item[dataKey]), color: colors[index] || "#000000", })), - [data, categoryKey, dataKey, colors], + [processedData, categoryKey, dataKey, colors], ); const defaultLegendItems = useMemo((): LegendItem[] => { @@ -182,8 +192,8 @@ export const RadialChartV2 = ({ // Memoize sorted data for legend hover handling const sortedData = useMemo( - () => [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), - [data, dataKey], + () => [...processedData].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), + [processedData, dataKey], ); // Handle legend item hover to highlight radial bar @@ -195,9 +205,11 @@ export const RadialChartV2 = ({ if (item) { const categoryValue = String(item[categoryKey]); setHoveredLegendKey(categoryValue); - const originalIndex = data.findIndex((d) => String(d[categoryKey]) === categoryValue); + const originalIndex = processedData.findIndex( + (d) => String(d[categoryKey]) === categoryValue, + ); if (originalIndex !== -1) { - handleMouseEnter(data[originalIndex], originalIndex); + handleMouseEnter(processedData[originalIndex], originalIndex); } } } else { @@ -205,7 +217,7 @@ export const RadialChartV2 = ({ handleMouseLeave(); } }, - [sortedData, categoryKey, data, handleMouseEnter, handleMouseLeave, legendVariant], + [sortedData, categoryKey, processedData, handleMouseEnter, handleMouseLeave, legendVariant], ); // Enhanced chart hover handlers @@ -252,7 +264,6 @@ export const RadialChartV2 = ({ setWrapperRect({ width, height }); return; } - }, [width, height]); const renderLegend = useCallback(() => { diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts index 288eccafe..6370e1bcb 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts @@ -103,6 +103,56 @@ export const getRadialHoverStyles = ( // Data Transformation Utilities // ========================================== +export const MAX_RADIAL_SLICES = 10; + +/** + * Groups small slices into an "Others" category if the number of data points exceeds a threshold. + * @param data The input data array. + * @param categoryKey The key for the category labels. + * @param dataKey The key for the data values. + * @param threshold The maximum number of slices before grouping. + * @returns A new data array with smaller slices grouped into "Others". + */ +export const groupSmallSlices = ( + data: T, + categoryKey: keyof T[number], + dataKey: keyof T[number], + threshold: number = MAX_RADIAL_SLICES, +): T => { + if (data.length <= threshold) { + return data; + } + + const sortedData = [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])); + + const topItems = sortedData.slice(0, threshold - 1); + const otherItems = sortedData.slice(threshold - 1); + + const othersValue = otherItems.reduce((sum, item) => sum + Number(item[dataKey]), 0); + + const othersItem: T[number] = { + ...data[0], // Copy structure from first item + [categoryKey]: "Others", + [dataKey]: othersValue, + }; + + // Ensure other properties are initialized to avoid undefined issues. + for (const key in othersItem) { + if (key !== categoryKey && key !== dataKey) { + // @ts-expect-error - we are trying to clear other properties + othersItem[key] = undefined; + } + } + + // Then restore the main keys + // @ts-expect-error - we are trying to build the object + othersItem[categoryKey] = "Others"; + // @ts-expect-error - we are trying to build the object + othersItem[dataKey] = othersValue; + + return [...topItems, othersItem] as T; +}; + /** * Transforms data by adding percentage calculations and colors * @param data - The input data array From 8536ac91d76e51c51d22e1842355dcdeb4823883 Mon Sep 17 00:00:00 2001 From: i-subham Date: Sun, 29 Jun 2025 13:19:10 +0530 Subject: [PATCH 145/190] feat(Charts): enhance PieChartV2 layout flexibility and responsiveness This commit updates the PieChartV2 component to improve layout handling in column mode with a default legend, ensuring it remains flexible and responsive. The SCSS file is modified to include styles that allow for better adaptability in this layout. Additionally, a ResizeObserver is implemented to dynamically adjust the chart's size based on its container dimensions, enhancing the overall user experience. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 35 +++++++++++++++++-- .../PieCharts/PieChartV2/pieChartV2.scss | 6 ++++ .../PieChartV2/stories/PieChartV2.stories.tsx | 2 +- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 8ec265998..676b29d1b 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -76,11 +76,15 @@ export const PieChartV2 = ({ width, }: PieChartV2Props) => { const wrapperRef = useRef(null); + const chartContainerRef = useRef(null); const [wrapperRect, setWrapperRect] = useState({ width: 0, height: 0 }); + const [chartContainerRect, setChartContainerRect] = useState({ width: 0, height: 0 }); const [hoveredLegendKey, setHoveredLegendKey] = useState(null); const [isLegendExpanded, setIsLegendExpanded] = useState(false); const { activeIndex, handleMouseEnter, handleMouseLeave } = useChartHover(); + const isDefaultLegendLayout = legend && legendVariant === "default"; + // Determine layout mode based on container width const isRowLayout = legend && legendVariant === "stacked" && wrapperRect.width >= STACKED_LEGEND_BREAKPOINT; @@ -110,9 +114,20 @@ export const PieChartV2 = ({ const size = Math.min(chartContainerWidth, effectiveHeight); return Math.max(150, size); } + if (isDefaultLegendLayout) { + const size = Math.min(chartContainerRect.width, chartContainerRect.height); + return Math.max(150, size); + } const size = Math.min(effectiveWidth, effectiveHeight); return Math.max(150, size); - }, [effectiveWidth, effectiveHeight, isRowLayout]); + }, [ + effectiveWidth, + effectiveHeight, + isRowLayout, + isDefaultLegendLayout, + chartContainerRect.width, + chartContainerRect.height, + ]); const chartSizeStyle = useMemo(() => ({ width: chartSize, height: chartSize }), [chartSize]); const rechartsProps = useMemo(() => ({ width: chartSize, height: chartSize }), [chartSize]); @@ -278,6 +293,22 @@ export const PieChartV2 = ({ } }, [width, height]); + useEffect(() => { + const container = chartContainerRef.current; + if (!container) return; + const observer = new ResizeObserver((entries) => { + const entry = entries[0]; + if (entry) { + setChartContainerRect({ + width: entry.contentRect.width, + height: entry.contentRect.height, + }); + } + }); + observer.observe(container); + return () => observer.disconnect(); + }, []); + const renderPieCharts = useCallback(() => { if (variant === "donut") { return [ @@ -395,7 +426,7 @@ export const PieChartV2 = ({ return (
-
+
( - + ), From efc66e063da1b809cfb6f619e8102f253ef3074b Mon Sep 17 00:00:00 2001 From: i-subham Date: Sun, 29 Jun 2025 13:54:37 +0530 Subject: [PATCH 146/190] fix(Charts): remove unnecessary comment in PieChartV2 --- .../src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 676b29d1b..40a0e1b6c 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -51,7 +51,7 @@ export interface PieChartV2Props { width?: number; } -const STACKED_LEGEND_BREAKPOINT = 600; // px +const STACKED_LEGEND_BREAKPOINT = 600; export const PieChartV2 = ({ data, From efbe0c843cf07bb157f5a2dfc29b97c9e3aa2451 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sun, 29 Jun 2025 13:58:32 +0530 Subject: [PATCH 147/190] refactor(Charts): standardize typography usage across chart components This commit updates various chart components to standardize typography by replacing instances of the 'small' label with '2-extra-small' for better visual consistency. Additionally, it modifies the typography settings in the PortalTooltip and SideBarTooltip components to use 'extra-small' where appropriate, enhancing the overall readability and design coherence across the charts. --- .../RadarCharts/RadarChartV2/radarChartV2.scss | 2 +- .../react-ui/src/components/Charts/charts.scss | 6 ------ .../Charts/shared/DefaultLegend/defaultLegend.scss | 6 +++--- .../Charts/shared/PortalTooltip/portalTooltip.scss | 14 +++++++------- .../shared/SideBarTooltip/sideBarTooltip.scss | 6 +++--- .../Charts/shared/StackedLegend/stackedLegend.scss | 4 ++-- .../Charts/shared/XAxisTick/xAxisTick.scss | 2 +- .../Charts/shared/YAxisTick/yAxisTick.scss | 2 +- js/packages/react-ui/src/cssUtils.scss | 8 ++++---- 9 files changed, 22 insertions(+), 28 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss index b683774a9..b7a304d11 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss @@ -21,6 +21,6 @@ .crayon-chart-polar-angle-axis-label { pointer-events: none; - @include cssUtils.typography(label, small); + @include cssUtils.typography(label, 2-extra-small); color: cssUtils.$secondary-text; } diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index 8690009df..b033d036b 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -285,9 +285,3 @@ fill: cssUtils.$secondary-text; padding: 0; } - -.crayon-chart-axis-label { - fill: cssUtils.$primary-text; - @include cssUtils.typography(label, default); - text-anchor: middle; -} diff --git a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss index 1e2c9af57..cb935fe69 100644 --- a/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss +++ b/js/packages/react-ui/src/components/Charts/shared/DefaultLegend/defaultLegend.scss @@ -17,7 +17,7 @@ } .crayon-chart-legend-axis-label { - @include cssUtils.typography(label, small); + @include cssUtils.typography(label, 2-extra-small); color: cssUtils.$secondary-text; &-text { @@ -66,13 +66,13 @@ } &-label { - @include cssUtils.typography(label, small); + @include cssUtils.typography(label, 2-extra-small); color: cssUtils.$primary-text; } } &-toggle-button { - @include cssUtils.typography(label, small); + @include cssUtils.typography(label, 2-extra-small); color: cssUtils.$primary-text; height: 16px; diff --git a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss index 18b0a92e6..fd1124dfd 100644 --- a/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss +++ b/js/packages/react-ui/src/components/Charts/shared/PortalTooltip/portalTooltip.scss @@ -14,7 +14,7 @@ gap: cssUtils.$spacing-xs; padding: cssUtils.$spacing-xs; color: cssUtils.$primary-text; - @include cssUtils.typography(label, default); + @include cssUtils.typography(label, extra-small); border-radius: cssUtils.$rounded-s; border: 1px solid cssUtils.$stroke-default; background-color: cssUtils.$bg-container; @@ -22,12 +22,12 @@ text-transform: capitalize; &-label { - @include cssUtils.typography(label, default); + @include cssUtils.typography(label, extra-small); color: cssUtils.$secondary-text; } &-label-heavy { - @include cssUtils.typography(label, heavy); + @include cssUtils.typography(label, extra-small-heavy); color: cssUtils.$secondary-text; } @@ -41,7 +41,7 @@ gap: cssUtils.$spacing-xs; // padding: cssUtils.$spacing-xs; color: cssUtils.$primary-text; - @include cssUtils.typography(label, default); + @include cssUtils.typography(label, extra-small); background-color: cssUtils.$bg-container; box-shadow: cssUtils.$shadow-s; text-transform: capitalize; @@ -120,13 +120,13 @@ display: grid; gap: cssUtils.$spacing-xs; color: cssUtils.$secondary-text; - @include cssUtils.typography(label, default); + @include cssUtils.typography(label, extra-small); } &-value { font-variant-numeric: tabular-nums; color: cssUtils.$primary-text; - @include cssUtils.typography(label, default); + @include cssUtils.typography(label, extra-small); &--percentage { padding-left: cssUtils.$spacing-s; @@ -144,7 +144,7 @@ } &-view-more { - @include cssUtils.typography(label, default); + @include cssUtils.typography(label, extra-small); color: cssUtils.$secondary-text; text-align: left; } diff --git a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss index 3da897534..3ae12a1a2 100644 --- a/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss +++ b/js/packages/react-ui/src/components/Charts/shared/SideBarTooltip/sideBarTooltip.scss @@ -23,7 +23,7 @@ } &-title { - @include cssUtils.typography(label, default); + @include cssUtils.typography(label, 2-extra-small); } &-content { @@ -75,13 +75,13 @@ gap: cssUtils.$spacing-xs; &-label { - @include cssUtils.typography(label, default); + @include cssUtils.typography(label, 2-extra-small); flex: 1; min-width: 0; } &-value { - @include cssUtils.typography(label, default); + @include cssUtils.typography(label, 2-extra-small); flex-shrink: 0; text-align: right; } diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss index 37c5fd41e..ae4295eb4 100644 --- a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss @@ -110,7 +110,7 @@ } &-label-text { - @include cssUtils.typography(label, large); + @include cssUtils.typography(label, medium); color: cssUtils.$primary-text; transition: color 0.2s ease-in-out; overflow: hidden; @@ -120,7 +120,7 @@ } &-value { - @include cssUtils.typography(label, large); + @include cssUtils.typography(label, medium); color: cssUtils.$primary-text; transition: all 0.2s ease-in-out; flex-shrink: 0; diff --git a/js/packages/react-ui/src/components/Charts/shared/XAxisTick/xAxisTick.scss b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/xAxisTick.scss index 69689a6fd..a2036cec7 100644 --- a/js/packages/react-ui/src/components/Charts/shared/XAxisTick/xAxisTick.scss +++ b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/xAxisTick.scss @@ -1,5 +1,5 @@ @use "../../../../cssUtils" as cssUtils; .crayon-chart-x-axis-tick { - @include cssUtils.typography(label, small); + @include cssUtils.typography(label, 2-extra-small); } diff --git a/js/packages/react-ui/src/components/Charts/shared/YAxisTick/yAxisTick.scss b/js/packages/react-ui/src/components/Charts/shared/YAxisTick/yAxisTick.scss index 3e6474d05..95f7b2aaf 100644 --- a/js/packages/react-ui/src/components/Charts/shared/YAxisTick/yAxisTick.scss +++ b/js/packages/react-ui/src/components/Charts/shared/YAxisTick/yAxisTick.scss @@ -1,6 +1,6 @@ @use "../../../../cssUtils" as cssUtils; .crayon-chart-y-axis-tick { - @include cssUtils.typography(label, small); + @include cssUtils.typography(label, 2-extra-small); fill: cssUtils.$secondary-text; } diff --git a/js/packages/react-ui/src/cssUtils.scss b/js/packages/react-ui/src/cssUtils.scss index d309f0d8b..a327ad9a6 100644 --- a/js/packages/react-ui/src/cssUtils.scss +++ b/js/packages/react-ui/src/cssUtils.scss @@ -143,14 +143,14 @@ $typography: ( label: ( default: typography(crayon-font-label), heavy: typography(crayon-font-label-heavy), - small: typography(crayon-font-label-small), - small-heavy: typography(crayon-font-label-small-heavy), - extra-small: typography(crayon-font-label-extra-small), - extra-small-heavy: typography(crayon-font-label-extra-small-heavy), large: typography(crayon-font-label-large), large-heavy: typography(crayon-font-label-large-heavy), medium: typography(crayon-font-label-medium), medium-heavy: typography(crayon-font-label-medium-heavy), + small: typography(crayon-font-label-small), + small-heavy: typography(crayon-font-label-small-heavy), + extra-small: typography(crayon-font-label-extra-small), + extra-small-heavy: typography(crayon-font-label-extra-small-heavy), 2-extra-small: typography(crayon-font-label-2-extra-small), 2-extra-small-heavy: typography(crayon-font-label-2-extra-small-heavy), ), From 5d2841bf4940788284d34b5acb3cd575690e8ab2 Mon Sep 17 00:00:00 2001 From: i-subham Date: Sun, 29 Jun 2025 18:29:31 +0530 Subject: [PATCH 148/190] fix(Charts): update PieChartV2 stories for improved layout and responsiveness This commit modifies the PieChartV2 stories to enhance layout flexibility by adjusting card dimensions and ensuring height is set to "auto". The state management for dimensions is also updated to accommodate dynamic height adjustments, improving the overall responsiveness of the component. --- .../PieChartV2/stories/PieChartV2.stories.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index c5970783f..43ee4ad28 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -232,7 +232,7 @@ export const PieChartV2Demo: Story = { width: undefined, }, render: (args: any) => ( - + ), @@ -259,7 +259,7 @@ export const PieChartV2WithCarousel: Story = { width: undefined, }, render: (args: any) => ( - + ), @@ -291,7 +291,10 @@ export const ResizableAndResponsive: Story = { useGradients: false, }, render: (args: any) => { - const [dimensions, setDimensions] = useState({ width: 700, height: 400 }); + const [dimensions, setDimensions] = useState<{ width: number; height: number | string }>({ + width: 700, + height: "auto", + }); const handleMouseDown = (e: React.MouseEvent, handle: string) => { e.preventDefault(); @@ -299,7 +302,7 @@ export const ResizableAndResponsive: Story = { const startX = e.clientX; const startY = e.clientY; const startWidth = dimensions.width; - const startHeight = dimensions.height; + const startHeight = (e.currentTarget.parentElement as HTMLElement).offsetHeight; const doDrag = (e: MouseEvent) => { const dx = e.clientX - startX; @@ -314,7 +317,7 @@ export const ResizableAndResponsive: Story = { setDimensions({ width: Math.max(300, newWidth), - height: Math.max(300, newHeight), + height: "auto", }); }; @@ -339,7 +342,7 @@ export const ResizableAndResponsive: Story = { style={{ position: "relative", width: `${dimensions.width}px`, - height: `${dimensions.height}px`, + height: typeof dimensions.height === "number" ? `${dimensions.height}px` : dimensions.height, border: "1px dashed #9ca3af", padding: "20px", boxSizing: "border-box", @@ -422,7 +425,7 @@ export const ResizableAndResponsive: Story = { fontFamily: "monospace", }} > - {dimensions.width}px × {dimensions.height}px + {dimensions.width}px × {typeof dimensions.height === "number" ? `${dimensions.height}px` : dimensions.height}
); From f3acc94cc77faf741fc95456f6acec8ca95d73b7 Mon Sep 17 00:00:00 2001 From: i-subham Date: Sun, 29 Jun 2025 19:02:20 +0530 Subject: [PATCH 149/190] refactor(Charts): simplify PieChartV2 layout and remove unused state This commit refactors the PieChartV2 component by removing unnecessary state management related to the chart container dimensions and simplifying the layout logic. Additionally, it updates the SCSS to enhance flexibility in the component's styling. The PieChartV2 stories are also adjusted to improve layout responsiveness by modifying card dimensions. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 37 ++----------------- .../PieCharts/PieChartV2/pieChartV2.scss | 6 --- .../PieChartV2/stories/PieChartV2.stories.tsx | 2 +- .../stories/RadialChartV2.stories.tsx | 26 +++++++------ 4 files changed, 19 insertions(+), 52 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 40a0e1b6c..8ec265998 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -51,7 +51,7 @@ export interface PieChartV2Props { width?: number; } -const STACKED_LEGEND_BREAKPOINT = 600; +const STACKED_LEGEND_BREAKPOINT = 600; // px export const PieChartV2 = ({ data, @@ -76,15 +76,11 @@ export const PieChartV2 = ({ width, }: PieChartV2Props) => { const wrapperRef = useRef(null); - const chartContainerRef = useRef(null); const [wrapperRect, setWrapperRect] = useState({ width: 0, height: 0 }); - const [chartContainerRect, setChartContainerRect] = useState({ width: 0, height: 0 }); const [hoveredLegendKey, setHoveredLegendKey] = useState(null); const [isLegendExpanded, setIsLegendExpanded] = useState(false); const { activeIndex, handleMouseEnter, handleMouseLeave } = useChartHover(); - const isDefaultLegendLayout = legend && legendVariant === "default"; - // Determine layout mode based on container width const isRowLayout = legend && legendVariant === "stacked" && wrapperRect.width >= STACKED_LEGEND_BREAKPOINT; @@ -114,20 +110,9 @@ export const PieChartV2 = ({ const size = Math.min(chartContainerWidth, effectiveHeight); return Math.max(150, size); } - if (isDefaultLegendLayout) { - const size = Math.min(chartContainerRect.width, chartContainerRect.height); - return Math.max(150, size); - } const size = Math.min(effectiveWidth, effectiveHeight); return Math.max(150, size); - }, [ - effectiveWidth, - effectiveHeight, - isRowLayout, - isDefaultLegendLayout, - chartContainerRect.width, - chartContainerRect.height, - ]); + }, [effectiveWidth, effectiveHeight, isRowLayout]); const chartSizeStyle = useMemo(() => ({ width: chartSize, height: chartSize }), [chartSize]); const rechartsProps = useMemo(() => ({ width: chartSize, height: chartSize }), [chartSize]); @@ -293,22 +278,6 @@ export const PieChartV2 = ({ } }, [width, height]); - useEffect(() => { - const container = chartContainerRef.current; - if (!container) return; - const observer = new ResizeObserver((entries) => { - const entry = entries[0]; - if (entry) { - setChartContainerRect({ - width: entry.contentRect.width, - height: entry.contentRect.height, - }); - } - }); - observer.observe(container); - return () => observer.disconnect(); - }, []); - const renderPieCharts = useCallback(() => { if (variant === "donut") { return [ @@ -426,7 +395,7 @@ export const PieChartV2 = ({ return (
-
+
( - + ), diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx index 914b70a8e..800f3250c 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx @@ -315,7 +315,7 @@ export const RadialChartV2WithCarousel: Story = { width: undefined, }, render: (args: any) => ( - + ), @@ -349,7 +349,7 @@ export const RadialChartV2Minimal: Story = { width: undefined, }, render: (args: any) => ( - + ), @@ -370,17 +370,21 @@ export const ResizableAndResponsive: Story = { categoryKey: "month", dataKey: "value", theme: "spectrum", - variant: "circular", + variant: "donut", format: "number", legend: true, legendVariant: "stacked", - grid: false, isAnimationActive: false, + appearance: "circular", cornerRadius: 8, + paddingAngle: 2, useGradients: false, }, render: (args: any) => { - const [dimensions, setDimensions] = useState({ width: 700, height: 400 }); + const [dimensions, setDimensions] = useState<{ width: number; height: number | string }>({ + width: 700, + height: "auto", + }); const handleMouseDown = (e: React.MouseEvent, handle: string) => { e.preventDefault(); @@ -388,7 +392,7 @@ export const ResizableAndResponsive: Story = { const startX = e.clientX; const startY = e.clientY; const startWidth = dimensions.width; - const startHeight = dimensions.height; + const startHeight = (e.currentTarget.parentElement as HTMLElement).offsetHeight; const doDrag = (e: MouseEvent) => { const dx = e.clientX - startX; @@ -403,7 +407,7 @@ export const ResizableAndResponsive: Story = { setDimensions({ width: Math.max(300, newWidth), - height: Math.max(300, newHeight), + height: "auto", }); }; @@ -428,11 +432,11 @@ export const ResizableAndResponsive: Story = { style={{ position: "relative", width: `${dimensions.width}px`, - height: `${dimensions.height}px`, + height: typeof dimensions.height === "number" ? `${dimensions.height}px` : dimensions.height, border: "1px dashed #9ca3af", padding: "20px", boxSizing: "border-box", - overflow: "hidden", // Important for containing the chart properly + overflow: "hidden", }} > @@ -511,7 +515,7 @@ export const ResizableAndResponsive: Story = { fontFamily: "monospace", }} > - {dimensions.width}px × {dimensions.height}px + {dimensions.width}px × {typeof dimensions.height === "number" ? `${dimensions.height}px` : dimensions.height}
); @@ -520,7 +524,7 @@ export const ResizableAndResponsive: Story = { docs: { description: { story: - "This story demonstrates the responsive and resizable behavior of the RadialChartV2. Drag the edges or corners of the dashed container to resize it and see how the chart and its legend adapt perfectly to the new dimensions.", + "This story demonstrates the responsive and resizable behavior of the PieChartV2. Drag the edges or corners of the dashed container to resize it and see how the chart and its legend adapt perfectly to the new dimensions.", }, }, }, From 0b240c228998cf85c8cba006f7bbde8add85649f Mon Sep 17 00:00:00 2001 From: i-subham Date: Sun, 29 Jun 2025 19:52:28 +0530 Subject: [PATCH 150/190] fix(Charts): optimize PieChartV2 ResizeObserver logic for better performance This commit refines the ResizeObserver implementation in the PieChartV2 component by ensuring that the observer is only created when width and height are not provided. This change improves performance by reducing unnecessary observer instances and enhances the overall responsiveness of the chart layout. --- .../PieCharts/PieChartV2/PieChartV2.tsx | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index 8ec265998..d19959b0d 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -261,21 +261,23 @@ export const PieChartV2 = ({ useEffect(() => { const wrapper = wrapperRef.current; if (!wrapper) return; - if (!width || !height) { - const observer = new ResizeObserver((entries) => { - const entry = entries[0]; - if (entry) { - setWrapperRect({ - width: entry.contentRect.width, - height: entry.contentRect.height, - }); - } - }); - observer.observe(wrapper); - return () => observer.disconnect(); - } else { + + if (width && height) { setWrapperRect({ width, height }); + return; } + + const observer = new ResizeObserver((entries) => { + const entry = entries[0]; + if (entry) { + setWrapperRect({ + width: entry.contentRect.width, + height: entry.contentRect.height, + }); + } + }); + observer.observe(wrapper); + return () => observer.disconnect(); }, [width, height]); const renderPieCharts = useCallback(() => { From 350bea1266a9e393275e5d2f0b44107fb320ec87 Mon Sep 17 00:00:00 2001 From: i-subham Date: Sun, 29 Jun 2025 20:10:22 +0530 Subject: [PATCH 151/190] feat(Charts): add PieChartV2 and RadialChartV2 components with SCSS support This commit introduces the PieChartV2 and RadialChartV2 components, along with their respective SCSS files for styling. The charts are now included in the main charts index for easier access. Additionally, the PieChartV2 and RadialChartV2 components have been updated to remove unused SCSS imports, enhancing performance and maintainability. --- .../components/Charts/PieCharts/PieChartV2/PieChartV2.tsx | 1 - .../PieCharts/PieChartV2/stories/PieChartV2.stories.tsx | 6 ++++-- .../react-ui/src/components/Charts/PieCharts/index.ts | 1 + .../Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx | 1 - .../components/Charts/RadialCharts/RadialChartV2/index.ts | 1 + .../RadialChartV2/stories/RadialChartV2.stories.tsx | 6 ++++-- .../react-ui/src/components/Charts/RadialCharts/index.ts | 1 + js/packages/react-ui/src/components/Charts/charts.scss | 6 ++++++ js/packages/react-ui/src/components/Charts/index.ts | 2 ++ 9 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/index.ts diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx index d19959b0d..36e649c59 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx @@ -19,7 +19,6 @@ import { transformDataWithPercentages, useChartHover, } from "../utils/PieChartUtils"; -import "./pieChartV2.scss"; export type PieChartV2Data = PieChartData; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx index 442140f70..c67ed17ec 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx @@ -342,7 +342,8 @@ export const ResizableAndResponsive: Story = { style={{ position: "relative", width: `${dimensions.width}px`, - height: typeof dimensions.height === "number" ? `${dimensions.height}px` : dimensions.height, + height: + typeof dimensions.height === "number" ? `${dimensions.height}px` : dimensions.height, border: "1px dashed #9ca3af", padding: "20px", boxSizing: "border-box", @@ -425,7 +426,8 @@ export const ResizableAndResponsive: Story = { fontFamily: "monospace", }} > - {dimensions.width}px × {typeof dimensions.height === "number" ? `${dimensions.height}px` : dimensions.height} + {dimensions.width}px ×{" "} + {typeof dimensions.height === "number" ? `${dimensions.height}px` : dimensions.height}
); diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/index.ts b/js/packages/react-ui/src/components/Charts/PieCharts/index.ts new file mode 100644 index 000000000..737933f38 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieCharts/index.ts @@ -0,0 +1 @@ +export * from "./PieChartV2"; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx index 7b4de451c..fbe571f3d 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx @@ -18,7 +18,6 @@ import { transformRadialDataWithPercentages, useRadialChartHover, } from "../utils/RadialChartUtils"; -import "./radialChartV2.scss"; export type RadialChartV2Data = RadialChartData; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/index.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/index.ts index e69de29bb..2571e2cb6 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/index.ts +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/index.ts @@ -0,0 +1 @@ +export * from "./RadialChartV2"; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx index 800f3250c..8746aa6b4 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx @@ -432,7 +432,8 @@ export const ResizableAndResponsive: Story = { style={{ position: "relative", width: `${dimensions.width}px`, - height: typeof dimensions.height === "number" ? `${dimensions.height}px` : dimensions.height, + height: + typeof dimensions.height === "number" ? `${dimensions.height}px` : dimensions.height, border: "1px dashed #9ca3af", padding: "20px", boxSizing: "border-box", @@ -515,7 +516,8 @@ export const ResizableAndResponsive: Story = { fontFamily: "monospace", }} > - {dimensions.width}px × {typeof dimensions.height === "number" ? `${dimensions.height}px` : dimensions.height} + {dimensions.width}px ×{" "} + {typeof dimensions.height === "number" ? `${dimensions.height}px` : dimensions.height}
); diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/index.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/index.ts new file mode 100644 index 000000000..2571e2cb6 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/index.ts @@ -0,0 +1 @@ +export * from "./RadialChartV2"; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index b033d036b..0d6053ebe 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -15,6 +15,12 @@ // progress bar css @forward "./ProgressBarChart/progressBarChart.scss"; +// pie chart css +@forward "./PieCharts/PieChartV2/pieChartV2.scss"; + +// radial chart css +@forward "./RadialCharts/RadialChartV2/radialChartV2.scss"; + // shared components css @forward "./shared/XAxisTick/xAxisTick.scss"; @forward "./shared/DefaultLegend/defaultLegend.scss"; diff --git a/js/packages/react-ui/src/components/Charts/index.ts b/js/packages/react-ui/src/components/Charts/index.ts index 93336dddc..c0276991f 100644 --- a/js/packages/react-ui/src/components/Charts/index.ts +++ b/js/packages/react-ui/src/components/Charts/index.ts @@ -9,4 +9,6 @@ export * from "./RadialChart"; export * from "./AreaCharts"; export * from "./BarCharts"; export * from "./LineCharts"; +export * from "./PieCharts"; export * from "./ProgressBarChart"; +export * from "./RadialCharts"; From 9e7ee36f7d59727b2d6c5df3cc5b9e5f05351b36 Mon Sep 17 00:00:00 2001 From: i-subham Date: Sun, 29 Jun 2025 20:43:38 +0530 Subject: [PATCH 152/190] feat(Charts): add StackedLegend and RadarCharts to chart components This commit introduces the StackedLegend component and includes it in the charts SCSS for styling. Additionally, the RadarCharts component is now exported in the main charts index, enhancing the accessibility of these components within the library. --- js/packages/react-ui/src/components/Charts/charts.scss | 1 + js/packages/react-ui/src/components/Charts/index.ts | 1 + js/packages/react-ui/src/components/Charts/shared/index.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index 0d6053ebe..d88d3fe97 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -24,6 +24,7 @@ // shared components css @forward "./shared/XAxisTick/xAxisTick.scss"; @forward "./shared/DefaultLegend/defaultLegend.scss"; +@forward "./shared/StackedLegend/stackedLegend.scss"; @forward "./shared/YAxisTick/yAxisTick.scss"; // portal tooltip css diff --git a/js/packages/react-ui/src/components/Charts/index.ts b/js/packages/react-ui/src/components/Charts/index.ts index c0276991f..eba0b7b69 100644 --- a/js/packages/react-ui/src/components/Charts/index.ts +++ b/js/packages/react-ui/src/components/Charts/index.ts @@ -11,4 +11,5 @@ export * from "./BarCharts"; export * from "./LineCharts"; export * from "./PieCharts"; export * from "./ProgressBarChart"; +export * from "./RadarCharts"; export * from "./RadialCharts"; diff --git a/js/packages/react-ui/src/components/Charts/shared/index.ts b/js/packages/react-ui/src/components/Charts/shared/index.ts index 7bc3ff5d3..8d85cffde 100644 --- a/js/packages/react-ui/src/components/Charts/shared/index.ts +++ b/js/packages/react-ui/src/components/Charts/shared/index.ts @@ -3,5 +3,6 @@ export * from "./CartesianGrid/cartesianGrid"; export * from "./DefaultLegend/DefaultLegend"; export * from "./PortalTooltip"; export * from "./SideBarTooltip"; +export * from "./StackedLegend/StackedLegend"; export * from "./XAxisTick/XAxisTick"; export * from "./YAxisTick/YAxisTick"; From f1d862c60b9a77174ae58a553f23ed637617ce98 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sun, 29 Jun 2025 21:19:11 +0530 Subject: [PATCH 153/190] refactor(Charts): reorganize utility functions for Area and Line charts This commit refactors the utility functions for Area and Line charts by moving them to a new shared directory, enhancing code organization and reusability. The imports in AreaChartV2, MiniAreaChart, LineChartV2, and MiniLineChart components are updated accordingly. Additionally, a new SCSS rule is added to the RadarChartV2 to ensure a minimum height for better layout consistency. --- .../components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx | 3 ++- .../Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx | 2 +- .../components/Charts/LineCharts/LineChartV2/LineChartV2.tsx | 2 +- .../Charts/LineCharts/MiniLineChart/MiniLineChart.tsx | 2 +- .../Charts/RadarCharts/RadarChartV2/radarChartV2.scss | 1 + .../utils/{BarAndLineUtils => AreaAndLine}/AreaAndLineUtils.ts | 0 .../{BarAndLineUtils => AreaAndLine}/MiniAreaAndLineUtils.ts | 0 7 files changed, 6 insertions(+), 4 deletions(-) rename js/packages/react-ui/src/components/Charts/utils/{BarAndLineUtils => AreaAndLine}/AreaAndLineUtils.ts (100%) rename js/packages/react-ui/src/components/Charts/utils/{BarAndLineUtils => AreaAndLine}/MiniAreaAndLineUtils.ts (100%) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index d21bf62bc..a84707168 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -22,7 +22,7 @@ import { getSnapPositions, getWidthOfData, getXAxisTickPositionData, -} from "../../utils/BarAndLineUtils/AreaAndLineUtils"; +} from "../../utils/AreaAndLine/AreaAndLineUtils"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; import { getChartConfig, @@ -180,6 +180,7 @@ const AreaChartV2Component = ({ }); resizeObserver.observe(chartContainerRef.current); + setContainerWidth(chartContainerRef.current.getBoundingClientRect().width); return () => { resizeObserver.disconnect(); diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx index e3183698b..4adbb7a24 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx @@ -5,7 +5,7 @@ import { ChartConfig, ChartContainer } from "../../Charts"; import { getRecentDataThatFits, transformDataForChart, -} from "../../utils/BarAndLineUtils/MiniAreaAndLineUtils"; +} from "../../utils/AreaAndLine/MiniAreaAndLineUtils"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; import { MiniAreaChartData } from "../types"; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx index 2544ff03f..331b7b8f0 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx @@ -22,7 +22,7 @@ import { getSnapPositions, getWidthOfData, getXAxisTickPositionData, -} from "../../utils/BarAndLineUtils/AreaAndLineUtils"; +} from "../../utils/AreaAndLine/AreaAndLineUtils"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; import { getChartConfig, diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx index a3644e6f2..e289dd0cf 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx @@ -5,7 +5,7 @@ import { ChartConfig, ChartContainer } from "../../Charts"; import { getRecentDataThatFits, transformDataForChart, -} from "../../utils/BarAndLineUtils/MiniAreaAndLineUtils"; +} from "../../utils/AreaAndLine/MiniAreaAndLineUtils"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; import { MiniLineChartData } from "../types"; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss index b7a304d11..3493270df 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss @@ -8,6 +8,7 @@ flex-direction: column; align-items: center; justify-content: center; + min-height: 300px; } &-container-inner { width: 100%; diff --git a/js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/AreaAndLineUtils.ts b/js/packages/react-ui/src/components/Charts/utils/AreaAndLine/AreaAndLineUtils.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/AreaAndLineUtils.ts rename to js/packages/react-ui/src/components/Charts/utils/AreaAndLine/AreaAndLineUtils.ts diff --git a/js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/MiniAreaAndLineUtils.ts b/js/packages/react-ui/src/components/Charts/utils/AreaAndLine/MiniAreaAndLineUtils.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/utils/BarAndLineUtils/MiniAreaAndLineUtils.ts rename to js/packages/react-ui/src/components/Charts/utils/AreaAndLine/MiniAreaAndLineUtils.ts From c19e85c55e69256a0e5950d0394b2b815025471f Mon Sep 17 00:00:00 2001 From: i-subham Date: Sun, 29 Jun 2025 21:20:32 +0530 Subject: [PATCH 154/190] refactor(Charts): remove unused SCSS import from StackedLegend component --- .../src/components/Charts/shared/StackedLegend/StackedLegend.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx index af452771d..bd033160b 100644 --- a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx @@ -1,7 +1,6 @@ import { ChevronDown, ChevronUp } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { IconButton } from "../../../IconButton"; -import "./stackedLegend.scss"; interface LegendItem { key: string; From b40def58a0160f2bc97fc30216e421a8791eb10a Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sun, 29 Jun 2025 21:31:31 +0530 Subject: [PATCH 155/190] style(Charts): adjust min-height for RadarChartV2 layout consistency This commit removes the min-height from the outer container of RadarChartV2 and adds it to the inner container, ensuring better layout consistency across different screen sizes. --- .../Charts/RadarCharts/RadarChartV2/radarChartV2.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss index 3493270df..0f9856d15 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss @@ -8,7 +8,6 @@ flex-direction: column; align-items: center; justify-content: center; - min-height: 300px; } &-container-inner { width: 100%; @@ -17,6 +16,7 @@ align-items: center; justify-content: center; overflow: visible; + min-height: 300px; } } From c9ddaa378aa71880f086693950007bbd389f4e62 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sun, 29 Jun 2025 22:46:00 +0530 Subject: [PATCH 156/190] feat(Charts): integrate transformed keys into chart components This commit enhances the chart components (AreaChartV2, BarChartV2, LineChartV2, RadarChartV2) by incorporating transformed keys for improved data handling. The keyTransform function is replaced with a new useTransformedKeys hook, streamlining the process of obtaining transformed keys. Additionally, the getChartConfig function is updated to accept transformed keys, ensuring consistent usage across all chart types. --- .../AreaCharts/AreaChartV2/AreaChartV2.tsx | 13 +-- .../BarCharts/BarChartV2/BarChartV2.tsx | 11 ++- .../react-ui/src/components/Charts/Charts.tsx | 5 +- .../LineCharts/LineChartV2/LineChartV2.tsx | 11 ++- .../RadarCharts/RadarChartV2/RadarChartV2.tsx | 88 +++++++++---------- .../src/components/Charts/hooks/index.ts | 1 + .../Charts/hooks/useTransformKey.tsx | 20 +++++ .../src/components/Charts/utils/dataUtils.ts | 4 + 8 files changed, 92 insertions(+), 61 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/hooks/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/hooks/useTransformKey.tsx diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx index a84707168..8a2c116ae 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx @@ -4,8 +4,9 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import { Area, AreaChart as RechartsAreaChart, XAxis, YAxis } from "recharts"; import { useId } from "../../../../polyfills"; import { IconButton } from "../../../IconButton"; -import { ChartConfig, ChartContainer, ChartTooltip, keyTransform } from "../../Charts"; +import { ChartConfig, ChartContainer, ChartTooltip } from "../../Charts"; import { SideBarChartData, SideBarTooltipProvider } from "../../context/SideBarTooltipContext"; +import { useTransformedKeys } from "../../hooks"; import { ActiveDot, cartesianGrid, @@ -77,14 +78,16 @@ const AreaChartV2Component = ({ return getDataKeys(data, categoryKey as string); }, [data, categoryKey]); + const transformedKeys = useTransformedKeys(dataKeys); + const colors = useMemo(() => { const palette = getPalette(theme); return getDistributedColors(palette, dataKeys.length); }, [theme, dataKeys.length]); const chartConfig: ChartConfig = useMemo(() => { - return getChartConfig(dataKeys, colors, undefined, icons); - }, [dataKeys, icons, colors]); + return getChartConfig(dataKeys, colors, transformedKeys, undefined, icons); + }, [dataKeys, icons, colors, transformedKeys]); const chartContainerRef = useRef(null); const mainContainerRef = useRef(null); @@ -338,7 +341,7 @@ const AreaChartV2Component = ({ } offset={15} /> {dataKeys.map((key) => { - const transformedKey = keyTransform(key); + const transformedKey = transformedKeys[key]; const color = `var(--color-${transformedKey})`; return ( @@ -351,7 +354,7 @@ const AreaChartV2Component = ({ })} {dataKeys.map((key) => { - const transformedKey = keyTransform(key); + const transformedKey = transformedKeys[key]; const color = `var(--color-${transformedKey})`; return ( ({ return getDataKeys(data, categoryKey as string); }, [data, categoryKey]); + const transformedKeys = useTransformedKeys(dataKeys); + const colors = useMemo(() => { const palette = getPalette(theme); return getDistributedColors(palette, dataKeys.length); }, [theme, dataKeys.length]); const chartConfig: ChartConfig = useMemo(() => { - return getChartConfig(dataKeys, colors, undefined, icons); - }, [dataKeys, icons, colors]); + return getChartConfig(dataKeys, colors, transformedKeys, undefined, icons); + }, [dataKeys, icons, colors, transformedKeys]); const chartContainerRef = useRef(null); const mainContainerRef = useRef(null); @@ -379,7 +382,7 @@ const BarChartV2Component = ({ /> {dataKeys.map((key, index) => { - const transformedKey = keyTransform(key); + const transformedKey = transformedKeys[key]; const color = `var(--color-${transformedKey})`; const isFirstInStack = index === 0; const isLastInStack = index === dataKeys.length - 1; diff --git a/js/packages/react-ui/src/components/Charts/Charts.tsx b/js/packages/react-ui/src/components/Charts/Charts.tsx index 3816482db..ae719e2b8 100644 --- a/js/packages/react-ui/src/components/Charts/Charts.tsx +++ b/js/packages/react-ui/src/components/Charts/Charts.tsx @@ -23,6 +23,7 @@ export type ChartConfig = { [k in string]: { label?: React.ReactNode; icon?: React.ComponentType; + transformed?: string; } & ( | { color?: string; secondaryColor?: string; theme?: never } | { @@ -100,7 +101,9 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { ${prefix} [data-chart=${id}] { ${colorConfig .map(([key, itemConfig]) => { - const transformedKey = keyTransform(key); + // TODO: remove this after successful migration + // keyTransform fn can be removed after successful migration + const transformedKey = itemConfig.transformed || keyTransform(key); const themeValue = itemConfig.theme?.[theme as keyof typeof itemConfig.theme]; const color = typeof themeValue === "string" ? themeValue : themeValue?.color || itemConfig.color; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx index 331b7b8f0..abbdc48d4 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx @@ -4,8 +4,9 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import { Line, LineChart as RechartsLineChart, XAxis, YAxis } from "recharts"; import { useId } from "../../../../polyfills"; import { IconButton } from "../../../IconButton"; -import { ChartConfig, ChartContainer, ChartTooltip, keyTransform } from "../../Charts"; +import { ChartConfig, ChartContainer, ChartTooltip } from "../../Charts"; import { SideBarChartData, SideBarTooltipProvider } from "../../context/SideBarTooltipContext"; +import { useTransformedKeys } from "../../hooks"; import { ActiveDot, cartesianGrid, @@ -77,14 +78,16 @@ export const LineChartV2 = ({ return getDataKeys(data, categoryKey as string); }, [data, categoryKey]); + const transformedKeys = useTransformedKeys(dataKeys); + const colors = useMemo(() => { const palette = getPalette(theme); return getDistributedColors(palette, dataKeys.length); }, [theme, dataKeys.length]); const chartConfig: ChartConfig = useMemo(() => { - return getChartConfig(dataKeys, colors, undefined, icons); - }, [dataKeys, icons, colors]); + return getChartConfig(dataKeys, colors, transformedKeys, undefined, icons); + }, [dataKeys, icons, colors, transformedKeys]); const chartContainerRef = useRef(null); const mainContainerRef = useRef(null); @@ -336,7 +339,7 @@ export const LineChartV2 = ({ } offset={15} /> {dataKeys.map((key) => { - const transformedKey = keyTransform(key); + const transformedKey = transformedKeys[key]; const color = `var(--color-${transformedKey})`; return ( ({ return getDataKeys(data, categoryKey as string); }, [data, categoryKey]); + const transformedKeys = useTransformedKeys(dataKeys); + const colors = useMemo(() => { const palette = getPalette(theme); return getDistributedColors(palette, dataKeys.length); @@ -47,8 +50,8 @@ const RadarChartV2Component = ({ // Create Config const chartConfig: ChartConfig = useMemo(() => { - return getChartConfig(dataKeys, colors, undefined, icons); - }, [dataKeys, icons, colors]); + return getChartConfig(dataKeys, colors, transformedKeys, undefined, icons); + }, [dataKeys, icons, colors, transformedKeys]); const legendItems: LegendItem[] = useMemo(() => { return getLegendItems(dataKeys, colors, icons); @@ -87,48 +90,39 @@ const RadarChartV2Component = ({ [containerDimensions], ); - const renderRadars = useCallback( - ( - dataKeys: string[], - variant: "line" | "area", - strokeWidth: number, - areaOpacity: number, - isAnimationActive: boolean, - ) => { - return dataKeys.map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - if (variant === "line") { - return ( - } - /> - ); - } else { - return ( - } - /> - ); - } - }); - }, - [], - ); + const radars = useMemo(() => { + return dataKeys.map((key) => { + const transformedKey = transformedKeys[key]; + const color = `var(--color-${transformedKey})`; + if (variant === "line") { + return ( + } + /> + ); + } else { + return ( + } + /> + ); + } + }); + }, [dataKeys, transformedKeys, variant, strokeWidth, areaOpacity, isAnimationActive]); return ( ({ /> } /> - {renderRadars(dataKeys, variant, strokeWidth, areaOpacity, isAnimationActive)} + {radars}
diff --git a/js/packages/react-ui/src/components/Charts/hooks/index.ts b/js/packages/react-ui/src/components/Charts/hooks/index.ts new file mode 100644 index 000000000..82638018f --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useTransformKey"; diff --git a/js/packages/react-ui/src/components/Charts/hooks/useTransformKey.tsx b/js/packages/react-ui/src/components/Charts/hooks/useTransformKey.tsx new file mode 100644 index 000000000..fc563b0a4 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/hooks/useTransformKey.tsx @@ -0,0 +1,20 @@ +import { useMemo } from "react"; + +export const useTransformedKeys = (keys: string[]) => { + return useMemo(() => { + return keys.reduce( + (acc, key) => { + const transformedKey = crypto.randomUUID(); + acc[key] = transformedKey; + return acc; + }, + {} as Record, + ); + }, [keys]); +}; + +// const transformedKeys = useTransformedKeys(["a", "b", "c"]) + +// dataKeys.map((key) => { +// return `var(${transformedKeys[key]})` +// }) diff --git a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts index 004adfa3b..42904a426 100644 --- a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts @@ -18,12 +18,15 @@ export const getDataKeys = ( * This function returns the chart configuration object, used for the chart configuration object of the chart. * @param dataKeys - The data keys for the chart. * @param colors - The colors for the chart. + * @param transformedKeys - The transformed keys for the chart. + * @param secondaryColors - The secondary colors for the chart (optional). * @param icons - The icons for the chart (optional). * @returns The chart configuration object for the chart. */ export const getChartConfig = ( dataKeys: string[], colors: string[], + transformedKeys: Record, secondaryColors?: string[], icons?: Partial>, ): ChartConfig => { @@ -35,6 +38,7 @@ export const getChartConfig = ( icon: icons?.[key], color: colors[index], secondaryColor: secondaryColors?.[index] || colors[dataKeys.length - index - 1], + transformed: transformedKeys[key], }, }), {}, From 54d5ce2a4fbb21d7183248cc1277dc0c59e3a4ad Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sun, 29 Jun 2025 23:00:04 +0530 Subject: [PATCH 157/190] fix(Charts): update key transformation logic and enhance LineChartV2 props This commit modifies the key transformation logic in ChartStyle to use nullish coalescing for better handling of undefined values. Additionally, it adds the isAnimationActive prop to the LineChartV2 component, improving its configurability. A comment is also added in RadarChartV2 to clarify the rendering of radars. --- js/packages/react-ui/src/components/Charts/Charts.tsx | 2 +- .../components/Charts/LineCharts/LineChartV2/LineChartV2.tsx | 1 + .../components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/Charts.tsx b/js/packages/react-ui/src/components/Charts/Charts.tsx index ae719e2b8..cc2399465 100644 --- a/js/packages/react-ui/src/components/Charts/Charts.tsx +++ b/js/packages/react-ui/src/components/Charts/Charts.tsx @@ -103,7 +103,7 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { .map(([key, itemConfig]) => { // TODO: remove this after successful migration // keyTransform fn can be removed after successful migration - const transformedKey = itemConfig.transformed || keyTransform(key); + const transformedKey = itemConfig.transformed ?? keyTransform(key); const themeValue = itemConfig.theme?.[theme as keyof typeof itemConfig.theme]; const color = typeof themeValue === "string" ? themeValue : themeValue?.color || itemConfig.color; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx index abbdc48d4..b1cca3806 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx @@ -288,6 +288,7 @@ export const LineChartV2 = ({ strokeWidth={0} dot={false} activeDot={false} + isAnimationActive={isAnimationActive} /> ); })} diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx index 6ac73e527..2147271bb 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx @@ -171,6 +171,7 @@ const RadarChartV2Component = ({ /> } /> + {/* rendering the radars here */} {radars} From ca4812dcfa1fca9a58526f520d415848765200e2 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 02:15:31 +0530 Subject: [PATCH 158/190] refactor(Charts): consolidate chart components and remove deprecated versions This commit refactors the chart components by consolidating AreaChart, BarChart, LineChart, and RadarChart into their respective new versions (AreaChartV2, BarChartV2, LineChartV2, RadarChartV2). It removes deprecated files and updates imports to reflect the new structure. Additionally, the SCSS files are streamlined to enhance maintainability and performance across the chart components. --- .../components/Charts/AreaChart/AreaChart.tsx | 243 ------------- .../AreaChart/stories/areaChart.stories.tsx | 312 ----------------- .../AreaChart.tsx} | 10 +- .../areaChart.scss} | 0 .../AreaCharts/AreaChart/dependencies.ts | 2 + .../{ => AreaCharts}/AreaChart/index.ts | 0 .../stories/areaChart.stories.tsx} | 38 +- .../AreaCharts/AreaChartV2/dependencies.ts | 2 - .../Charts/AreaCharts/AreaChartV2/index.ts | 1 - .../components/Charts/AreaCharts/index.scss | 2 +- .../src/components/Charts/AreaCharts/index.ts | 2 +- .../Charts/AreaCharts/types/index.ts | 2 +- .../components/Charts/BarChart/BarChart.tsx | 225 ------------ .../BarChart/stories/barChart.stories.tsx | 316 ----------------- .../BarChartV2.tsx => BarChart/BarChart.tsx} | 10 +- .../barChar.scss => BarChart/barChart.scss} | 0 .../components/CustomCursor.scss | 0 .../components/CustomCursor.tsx | 0 .../components/LineInBarShape.tsx | 0 .../Charts/BarCharts/BarChart/dependencies.ts | 2 + .../Charts/{ => BarCharts}/BarChart/index.ts | 0 .../stories/barChart.stories.tsx} | 24 +- .../BarCharts/BarChartV2/dependencies.ts | 2 - .../Charts/BarCharts/BarChartV2/index.ts | 1 - .../BarCharts/MiniBarChart/MiniBarChart.tsx | 2 +- .../components/Charts/BarCharts/index.scss | 4 +- .../src/components/Charts/BarCharts/index.ts | 2 +- .../Charts/BarCharts/types/index.ts | 2 +- .../components/Charts/LineChart/LineChart.tsx | 239 ------------- .../LineChart/stories/lineChart.stories.tsx | 324 ------------------ .../LineChart.tsx} | 8 +- .../LineCharts/LineChart/dependencies.ts | 2 + .../{ => LineCharts}/LineChart/index.ts | 0 .../lineChart.scss} | 0 .../stories/lineChart.stories.tsx} | 34 +- .../LineCharts/LineChartV2/dependencies.ts | 2 - .../Charts/LineCharts/LineChartV2/index.ts | 1 - .../components/Charts/LineCharts/index.scss | 2 +- .../src/components/Charts/LineCharts/index.ts | 2 +- .../Charts/LineCharts/types/index.ts | 2 +- .../components/Charts/PieChart/PieChart.tsx | 175 ---------- .../PieChart/stories/pieChart.stories.tsx | 174 ---------- .../PieChartV2.tsx => PieChart/PieChart.tsx} | 32 +- .../components/PieChartRenderers.tsx | 0 .../Charts/{ => PieCharts}/PieChart/index.ts | 0 .../pieChart.scss} | 0 .../stories/PieChart.stories.tsx} | 14 +- .../{ => PieChart}/utils/PieChartUtils.ts | 11 +- .../Charts/PieCharts/PieChartV2/index.ts | 1 - .../components/Charts/PieCharts/index.scss | 1 + .../src/components/Charts/PieCharts/index.ts | 3 +- .../Charts/PieCharts/types/index.ts | 1 + .../Charts/RadarChart/RadarChart.tsx | 105 ------ .../RadarChart/stories/raderChart.stories.tsx | 243 ------------- .../RadarChart.tsx} | 12 +- .../components/AxisLabel.tsx | 0 .../{ => RadarCharts}/RadarChart/index.ts | 1 + .../radarChart.scss} | 0 .../stories/RadarChart.stories.tsx} | 30 +- .../RadarCharts/RadarChart/types/index.ts | 1 + .../utils/index.ts | 0 .../Charts/RadarCharts/RadarChartV2/index.ts | 2 - .../RadarCharts/RadarChartV2/types/index.ts | 1 - .../components/Charts/RadarCharts/index.scss | 1 + .../components/Charts/RadarCharts/index.ts | 2 +- .../Charts/RadialChart/RadialChart.tsx | 194 ----------- .../stories/radialChart.stories.tsx | 189 ---------- .../RadialChart.tsx} | 18 +- .../components/RadialChartRenderers.tsx | 0 .../{ => RadialCharts}/RadialChart/index.ts | 0 .../radialChart.scss} | 0 .../stories/RadialChartV2.stories.tsx | 4 +- .../utils/RadialChartUtils.ts | 9 +- .../RadialCharts/RadialChartV2/index.ts | 1 - .../components/Charts/RadialCharts/index.scss | 1 + .../components/Charts/RadialCharts/index.ts | 3 +- .../Charts/RadialCharts/types/index.ts | 1 + .../src/components/Charts/charts.scss | 175 +--------- .../react-ui/src/components/Charts/index.ts | 8 - .../utils/AreaAndLine/AreaAndLineUtils.ts | 6 +- 80 files changed, 157 insertions(+), 3082 deletions(-) delete mode 100644 js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/AreaChart/stories/areaChart.stories.tsx rename js/packages/react-ui/src/components/Charts/AreaCharts/{AreaChartV2/AreaChartV2.tsx => AreaChart/AreaChart.tsx} (97%) rename js/packages/react-ui/src/components/Charts/AreaCharts/{AreaChartV2/areaChartV2.scss => AreaChart/areaChart.scss} (100%) create mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/dependencies.ts rename js/packages/react-ui/src/components/Charts/{ => AreaCharts}/AreaChart/index.ts (100%) rename js/packages/react-ui/src/components/Charts/AreaCharts/{AreaChartV2/stories/areaChartV2.stories.tsx => AreaChart/stories/areaChart.stories.tsx} (97%) delete mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/dependencies.ts delete mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/BarChart/stories/barChart.stories.tsx rename js/packages/react-ui/src/components/Charts/BarCharts/{BarChartV2/BarChartV2.tsx => BarChart/BarChart.tsx} (98%) rename js/packages/react-ui/src/components/Charts/BarCharts/{BarChartV2/barChar.scss => BarChart/barChart.scss} (100%) rename js/packages/react-ui/src/components/Charts/BarCharts/{BarChartV2 => BarChart}/components/CustomCursor.scss (100%) rename js/packages/react-ui/src/components/Charts/BarCharts/{BarChartV2 => BarChart}/components/CustomCursor.tsx (100%) rename js/packages/react-ui/src/components/Charts/BarCharts/{BarChartV2 => BarChart}/components/LineInBarShape.tsx (100%) create mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChart/dependencies.ts rename js/packages/react-ui/src/components/Charts/{ => BarCharts}/BarChart/index.ts (100%) rename js/packages/react-ui/src/components/Charts/BarCharts/{BarChartV2/stories/barChartV2.stories.tsx => BarChart/stories/barChart.stories.tsx} (97%) delete mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/dependencies.ts delete mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/LineChart/stories/lineChart.stories.tsx rename js/packages/react-ui/src/components/Charts/LineCharts/{LineChartV2/LineChartV2.tsx => LineChart/LineChart.tsx} (98%) create mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/LineChart/dependencies.ts rename js/packages/react-ui/src/components/Charts/{ => LineCharts}/LineChart/index.ts (100%) rename js/packages/react-ui/src/components/Charts/LineCharts/{LineChartV2/lineChartV2.scss => LineChart/lineChart.scss} (100%) rename js/packages/react-ui/src/components/Charts/LineCharts/{LineChartV2/stories/lineChartV2.stories.tsx => LineChart/stories/lineChart.stories.tsx} (96%) delete mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/dependencies.ts delete mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/PieChart/stories/pieChart.stories.tsx rename js/packages/react-ui/src/components/Charts/PieCharts/{PieChartV2/PieChartV2.tsx => PieChart/PieChart.tsx} (94%) rename js/packages/react-ui/src/components/Charts/PieCharts/{ => PieChart}/components/PieChartRenderers.tsx (100%) rename js/packages/react-ui/src/components/Charts/{ => PieCharts}/PieChart/index.ts (100%) rename js/packages/react-ui/src/components/Charts/PieCharts/{PieChartV2/pieChartV2.scss => PieChart/pieChart.scss} (100%) rename js/packages/react-ui/src/components/Charts/PieCharts/{PieChartV2/stories/PieChartV2.stories.tsx => PieChart/stories/PieChart.stories.tsx} (97%) rename js/packages/react-ui/src/components/Charts/PieCharts/{ => PieChart}/utils/PieChartUtils.ts (96%) delete mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/index.scss create mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/types/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/RadarChart/stories/raderChart.stories.tsx rename js/packages/react-ui/src/components/Charts/RadarCharts/{RadarChartV2/RadarChartV2.tsx => RadarChart/RadarChart.tsx} (95%) rename js/packages/react-ui/src/components/Charts/RadarCharts/{RadarChartV2 => RadarChart}/components/AxisLabel.tsx (100%) rename js/packages/react-ui/src/components/Charts/{ => RadarCharts}/RadarChart/index.ts (54%) rename js/packages/react-ui/src/components/Charts/RadarCharts/{RadarChartV2/radarChartV2.scss => RadarChart/radarChart.scss} (100%) rename js/packages/react-ui/src/components/Charts/RadarCharts/{RadarChartV2/stories/RadarChartV2.stories.tsx => RadarChart/stories/RadarChart.stories.tsx} (96%) create mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/types/index.ts rename js/packages/react-ui/src/components/Charts/RadarCharts/{RadarChartV2 => RadarChart}/utils/index.ts (100%) delete mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/types/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/index.scss delete mode 100644 js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/RadialChart/stories/radialChart.stories.tsx rename js/packages/react-ui/src/components/Charts/RadialCharts/{RadialChartV2/RadialChartV2.tsx => RadialChart/RadialChart.tsx} (96%) rename js/packages/react-ui/src/components/Charts/RadialCharts/{ => RadialChart}/components/RadialChartRenderers.tsx (100%) rename js/packages/react-ui/src/components/Charts/{ => RadialCharts}/RadialChart/index.ts (100%) rename js/packages/react-ui/src/components/Charts/RadialCharts/{RadialChartV2/radialChartV2.scss => RadialChart/radialChart.scss} (100%) rename js/packages/react-ui/src/components/Charts/RadialCharts/{RadialChartV2 => RadialChart}/stories/RadialChartV2.stories.tsx (99%) rename js/packages/react-ui/src/components/Charts/RadialCharts/{ => RadialChart}/utils/RadialChartUtils.ts (97%) delete mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/index.ts create mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/index.scss create mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/types/index.ts diff --git a/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx deleted file mode 100644 index 8991f8578..000000000 --- a/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import React from "react"; -import { Area, LabelList, AreaChart as RechartsAreaChart, XAxis, YAxis } from "recharts"; -import { useLayoutContext } from "../../../context/LayoutContext"; -import { - ChartConfig, - ChartContainer, - ChartLegend, - ChartLegendContent, - ChartTooltip, - ChartTooltipContent, - keyTransform, -} from "../Charts"; -import { cartesianGrid } from "../shared"; -import { getDistributedColors, getPalette } from "../utils/PalletUtils"; - -export type AreaChartData = Array>; - -export interface AreaChartProps { - data: T; - categoryKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - variant?: "linear" | "natural" | "step"; - grid?: boolean; - label?: boolean; - legend?: boolean; - opacity?: number; - icons?: Partial>; - isAnimationActive?: boolean; - showYAxis?: boolean; - xAxisLabel?: React.ReactNode; - yAxisLabel?: React.ReactNode; -} - -export const AreaChart = ({ - data, - categoryKey, - theme = "ocean", - variant = "natural", - grid = true, - label = true, - legend = true, - opacity = 0.5, - icons = {}, - isAnimationActive = true, - showYAxis = false, - xAxisLabel, - yAxisLabel, -}: AreaChartProps) => { - // excluding the categoryKey - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); - - const palette = getPalette(theme); - const colors = getDistributedColors(palette, dataKeys.length); - const { layout } = useLayoutContext(); - - // Create Config - const chartConfig: ChartConfig = dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - icon: icons[key], - color: colors[index], - }, - }), - {}, - ); - - const getTickFormatter = (data: T) => { - const dataLength = data.length; - const maxLengthMap = { - mobile: { - default: 5, - 10: 4, - 11: 4, - }, - tray: { - default: 5, - 8: 4, - 9: 4, - 10: 4, - 11: 4, - }, - copilot: { - default: 5, - 8: 4, - 9: 4, - 10: 4, - 11: 4, - }, - fullscreen: { - default: 5, - 11: 4, - }, - }; - - const layoutConfig = - maxLengthMap[layout as keyof typeof maxLengthMap] || maxLengthMap.fullscreen; - - const maxLength = - dataLength >= 11 - ? 4 - : layoutConfig[dataLength as keyof typeof layoutConfig] || layoutConfig.default; - - return (value: string) => { - if (value.length > maxLength) { - return `${value.slice(0, maxLength)}...`; - } - return value; - }; - }; - const getAxisAngle = (data: T) => { - const angleConfig = { - mobile: { - default: 0, - ranges: [ - { min: 6, max: 9, angle: -45 }, - { min: 10, max: 10, angle: -60 }, - { min: 11, max: Infinity, angle: -75 }, - ], - }, - tray: { - default: 0, - ranges: [{ min: 8, max: Infinity, angle: -45 }], - }, - copilot: { - default: 0, - ranges: [{ min: 8, max: Infinity, angle: -45 }], - }, - fullscreen: { - default: 0, - ranges: [{ min: 12, max: Infinity, angle: -45 }], - }, - }; - - const layoutConfig = angleConfig[layout as keyof typeof angleConfig] || angleConfig.fullscreen; - const dataLength = data.length; - - const matchRange = layoutConfig.ranges.find( - (range) => dataLength >= range.min && dataLength <= range.max, - ); - - return matchRange?.angle ?? layoutConfig.default; - }; - const getTickMargin = (data: T) => { - return data.length <= 6 ? 10 : 15; - }; - - return ( - - - {grid && cartesianGrid()} - - {showYAxis && ( - - )} - - } /> - {dataKeys.map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - if (label) { - return ( - - {label && ( - - )} - - ); - } - return ( - - ); - })} - {legend && } />} - - - ); -}; diff --git a/js/packages/react-ui/src/components/Charts/AreaChart/stories/areaChart.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaChart/stories/areaChart.stories.tsx deleted file mode 100644 index d4d60a065..000000000 --- a/js/packages/react-ui/src/components/Charts/AreaChart/stories/areaChart.stories.tsx +++ /dev/null @@ -1,312 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { Monitor, TabletSmartphone } from "lucide-react"; -import { Card } from "../../../Card"; -import { AreaChart, AreaChartProps } from "../AreaChart"; - -const areaChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - -const icons = { - desktop: Monitor, - mobile: TabletSmartphone, -} as const; - -const meta: Meta> = { - title: "Components/Charts/AreaChart", - component: AreaChart, - parameters: { - layout: "centered", - docs: { - description: { - component: "```tsx\nimport { AreaChart } from '@crayon-ui/react-ui/Charts/AreaChart';\n```", - }, - }, - }, - tags: ["!dev", "autodocs"], - argTypes: { - data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", - control: false, - table: { - type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", - }, - }, - categoryKey: { - description: - "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", - control: false, - table: { - type: { summary: "string" }, - defaultValue: { summary: "string" }, - category: "Data", - }, - }, - theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", - control: "select", - options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], - table: { - defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - icons: { - description: - "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", - control: false, - table: { - type: { summary: "Record" }, - defaultValue: { summary: "{}" }, - category: "Appearance", - }, - }, - variant: { - description: - "The interpolation method used to create the area curves. 'linear' creates straight lines between points, 'natural' creates smooth curves, and 'step' creates a stepped area.", - control: "radio", - options: ["linear", "natural", "step"], - table: { - defaultValue: { summary: "natural" }, - category: "Appearance", - }, - }, - opacity: { - description: - "The opacity of the filled area beneath each line (0 = fully transparent, 1 = fully opaque)", - control: false, - table: { - type: { summary: "number" }, - defaultValue: { summary: "0.5" }, - category: "Appearance", - }, - }, - grid: { - description: "Whether to display the background grid lines in the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - label: { - description: "Whether to display data point labels above each point on the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - legend: { - description: - "Whether to display the chart legend showing the data series names and their corresponding colors/icons", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - isAnimationActive: { - description: "Whether to animate the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - showYAxis: { - description: "Whether to display the y-axis", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "false" }, - category: "Display", - }, - }, - xAxisLabel: { - description: "The label for the x-axis", - control: false, - table: { - type: { summary: "string" }, - defaultValue: { summary: "string" }, - category: "Data", - }, - }, - yAxisLabel: { - description: "The label for the y-axis", - control: false, - table: { - type: { summary: "string" }, - defaultValue: { summary: "string" }, - category: "Data", - }, - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const AreaChartStory: Story = { - name: "Area Chart", - args: { - data: areaChartData, - categoryKey: "month", - theme: "ocean", - variant: "linear", - opacity: 0.5, - grid: true, - legend: true, - label: true, - isAnimationActive: true, - showYAxis: false, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` -const areaChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - - - -`, - }, - }, - }, -}; - -export const AreaChartStoryWithIcons: Story = { - name: "Area Chart with Icons", - args: { - ...AreaChartStory.args, - icons: icons, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` - import { Monitor, TabletSmartphone } from "lucide-react"; - - const areaChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, - ]; - - const icons = { - desktop: Monitor, - mobile: TabletSmartphone, - }; - - - - `, - }, - }, - }, -}; - -export const AreaChartStoryWithYAxis: Story = { - name: "Area Chart with Y-Axis and Axis Labels", - args: { - ...AreaChartStory.args, - showYAxis: true, - xAxisLabel: "Time Period", - yAxisLabel: "Number of Users", - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` - import { Monitor, TabletSmartphone } from "lucide-react"; - - const areaChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, - ]; - - - - `, - }, - }, - }, -}; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/AreaChart.tsx similarity index 97% rename from js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx rename to js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/AreaChart.tsx index 8a2c116ae..e912cae35 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/AreaChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/AreaChart.tsx @@ -32,14 +32,14 @@ import { getLegendItems, } from "../../utils/dataUtils"; import { getYAxisTickFormatter } from "../../utils/styleUtils"; -import { AreaChartV2Data, AreaChartVariant } from "../types"; +import { AreaChartData, AreaChartVariant } from "../types"; // this a technic to get the type of the onClick event of the bar chart // we need to do this because the onClick event type is not exported by recharts type AreaChartOnClick = React.ComponentProps["onClick"]; type AreaClickData = Parameters>[0]; -export interface AreaChartV2Props { +export interface AreaChartProps { data: T; categoryKey: keyof T[number]; theme?: PaletteName; @@ -58,7 +58,7 @@ export interface AreaChartV2Props { const Y_AXIS_WIDTH = 40; // Width of Y-axis chart when shown -const AreaChartV2Component = ({ +const AreaChartComponent = ({ data, categoryKey, theme = "ocean", @@ -73,7 +73,7 @@ const AreaChartV2Component = ({ className, height, width, -}: AreaChartV2Props) => { +}: AreaChartProps) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); }, [data, categoryKey]); @@ -424,4 +424,4 @@ const AreaChartV2Component = ({ }; // Added React.memo for performance optimization to avoid unnecessary re-renders -export const AreaChartV2 = React.memo(AreaChartV2Component) as typeof AreaChartV2Component; +export const AreaChart = React.memo(AreaChartComponent) as typeof AreaChartComponent; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/areaChart.scss similarity index 100% rename from js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/areaChartV2.scss rename to js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/areaChart.scss diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/dependencies.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/dependencies.ts new file mode 100644 index 000000000..5819b0194 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/dependencies.ts @@ -0,0 +1,2 @@ +const dependencies = ["AreaChart", "IconButton"]; +export default dependencies; diff --git a/js/packages/react-ui/src/components/Charts/AreaChart/index.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/AreaChart/index.ts rename to js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/index.ts diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/stories/areaChart.stories.tsx similarity index 97% rename from js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx rename to js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/stories/areaChart.stories.tsx index f8ab18da3..60dfd91f3 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/stories/areaChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/stories/areaChart.stories.tsx @@ -11,7 +11,7 @@ import { } from "lucide-react"; import { useState } from "react"; import { Card } from "../../../../Card"; -import { AreaChartV2, AreaChartV2Props } from "../AreaChartV2"; +import { AreaChart, AreaChartProps } from "../AreaChart"; // 🔥 COMPREHENSIVE DATA VARIATIONS - Designed to test label collision scenarios const dataVariations = { @@ -445,9 +445,9 @@ const icons = { sessions: TabletSmartphone, } as const; -const meta: Meta> = { +const meta: Meta> = { title: "Components/Charts/AreaCharts/AreaChartV2", - component: AreaChartV2, + component: AreaChart, parameters: { layout: "centered", docs: { @@ -595,7 +595,7 @@ const meta: Meta> = { }, }, }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; @@ -735,7 +735,7 @@ export const AreaChartV2Story: Story = {
- +
); @@ -764,7 +764,7 @@ export const BigLabelsStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -791,7 +791,7 @@ export const DenseTimelineStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -818,7 +818,7 @@ export const CompanyNamesStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -845,7 +845,7 @@ export const CountryDataStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -872,7 +872,7 @@ export const MixedLengthsStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -899,7 +899,7 @@ export const EdgeCasesStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -926,7 +926,7 @@ export const MinimalDataStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -953,7 +953,7 @@ export const ExpandCollapseMarketingStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -985,7 +985,7 @@ export const ResponsiveWidthStory: Story = { Small Container (300px)

- +
@@ -993,7 +993,7 @@ export const ResponsiveWidthStory: Story = { Medium Container (500px)

- +
@@ -1001,7 +1001,7 @@ export const ResponsiveWidthStory: Story = { Large Container (800px)

- +
@@ -1048,20 +1048,20 @@ export const FloatingTooltipStory: Story = {

- +

Default Tooltip

- +

Floating Tooltip

- +

diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/dependencies.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/dependencies.ts deleted file mode 100644 index af878f3f2..000000000 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/dependencies.ts +++ /dev/null @@ -1,2 +0,0 @@ -const dependencies = ["AreaChartV2", "IconButton"]; -export default dependencies; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/index.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/index.ts deleted file mode 100644 index f60bd5c5c..000000000 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChartV2/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./AreaChartV2"; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/index.scss b/js/packages/react-ui/src/components/Charts/AreaCharts/index.scss index 1481a4630..a0c0b3c73 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/index.scss +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/index.scss @@ -1 +1 @@ -@forward "./AreaChartV2/areaChartV2.scss"; +@forward "./AreaChart/areaChart.scss"; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/index.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/index.ts index b8d927d2d..f93b92d20 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/index.ts +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/index.ts @@ -1,3 +1,3 @@ -export * from "./AreaChartV2"; +export * from "./AreaChart"; export * from "./MiniAreaChart"; export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts index a9712758b..7db3648e2 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts @@ -1,5 +1,5 @@ export type AreaChartVariant = "linear" | "natural" | "step"; -export type AreaChartV2Data = Array>; +export type AreaChartData = Array>; export type MiniAreaChartData = Array | Array<{ value: number; label?: string }>; diff --git a/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx b/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx deleted file mode 100644 index 4d3dfa7f0..000000000 --- a/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import React from "react"; -import { Bar, LabelList, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; -import { useLayoutContext } from "../../../context/LayoutContext"; -import { - ChartConfig, - ChartContainer, - ChartLegend, - ChartLegendContent, - ChartTooltip, - ChartTooltipContent, - keyTransform, -} from "../Charts"; -import { cartesianGrid } from "../shared/CartesianGrid/cartesianGrid"; -import { getDistributedColors, getPalette } from "../utils/PalletUtils"; - -export type BarChartData = Array>; - -export interface BarChartProps { - data: T; - categoryKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - variant?: "grouped" | "stacked"; - grid?: boolean; - label?: boolean; - legend?: boolean; - radius?: number; - icons?: Partial>; - isAnimationActive?: boolean; - showYAxis?: boolean; - xAxisLabel?: React.ReactNode; - yAxisLabel?: React.ReactNode; -} - -export const BarChart = ({ - data, - categoryKey, - theme = "ocean", - variant = "grouped", - grid = true, - label = true, - legend = true, - icons = {}, - radius = 4, - isAnimationActive = true, - showYAxis = false, - xAxisLabel, - yAxisLabel, -}: BarChartProps) => { - // excluding the categoryKey - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); - - const palette = getPalette(theme); - const colors = getDistributedColors(palette, dataKeys.length); - const { layout } = useLayoutContext(); - - // Create Config - const chartConfig: ChartConfig = dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - icon: icons[key], - color: colors[index], - }, - }), - {}, - ); - - const getTickFormatter = (data: T) => { - const dataLength = data.length; - const maxLengthMap = { - mobile: { - default: 5, - 10: 4, - 11: 4, - }, - tray: { - default: 5, - 8: 4, - 9: 4, - 10: 4, - 11: 4, - }, - copilot: { - default: 5, - 8: 4, - 9: 4, - 10: 4, - 11: 4, - }, - fullscreen: { - default: 5, - 11: 4, - }, - }; - - const layoutConfig = - maxLengthMap[layout as keyof typeof maxLengthMap] || maxLengthMap.fullscreen; - - const maxLength = - dataLength >= 11 - ? 4 - : layoutConfig[dataLength as keyof typeof layoutConfig] || layoutConfig.default; - - return (value: string) => { - if (value.length > maxLength) { - return `${value.slice(0, maxLength)}...`; - } - return value; - }; - }; - const getAxisAngle = (data: T) => { - const angleConfig = { - mobile: { - default: 0, - ranges: [ - { min: 6, max: 9, angle: -45 }, - { min: 10, max: 10, angle: -60 }, - { min: 11, max: Infinity, angle: -75 }, - ], - }, - tray: { - default: 0, - ranges: [{ min: 8, max: Infinity, angle: -45 }], - }, - copilot: { - default: 0, - ranges: [{ min: 8, max: Infinity, angle: -45 }], - }, - fullscreen: { - default: 0, - ranges: [{ min: 12, max: Infinity, angle: -45 }], - }, - }; - - const layoutConfig = angleConfig[layout as keyof typeof angleConfig] || angleConfig.fullscreen; - const dataLength = data.length; - - const matchRange = layoutConfig.ranges.find( - (range) => dataLength >= range.min && dataLength <= range.max, - ); - - return matchRange?.angle ?? layoutConfig.default; - }; - const getTickMargin = (data: T) => { - return data.length <= 6 ? 10 : 15; - }; - - return ( - - - {grid && cartesianGrid()} - - {showYAxis && ( - - )} - } /> - {dataKeys.map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - if (label) { - return ( - - {label && ( - - )} - - ); - } - return ( - - ); - })} - {legend && } />} - - - ); -}; diff --git a/js/packages/react-ui/src/components/Charts/BarChart/stories/barChart.stories.tsx b/js/packages/react-ui/src/components/Charts/BarChart/stories/barChart.stories.tsx deleted file mode 100644 index 89b5e8a24..000000000 --- a/js/packages/react-ui/src/components/Charts/BarChart/stories/barChart.stories.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { Monitor, TabletSmartphone } from "lucide-react"; -import { Card } from "../../../Card"; -import { BarChart, BarChartProps } from "../BarChart"; - -const barChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - -const icons = { - desktop: Monitor, - mobile: TabletSmartphone, -} as const; - -const meta: Meta> = { - title: "Components/Charts/BarChart", - component: BarChart, - parameters: { - layout: "centered", - docs: { - description: { - component: "```tsx\nimport { BarChart } from '@crayon-ui/react-ui/Charts/BarChart';\n```", - }, - }, - }, - tags: ["!dev", "autodocs"], - argTypes: { - data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", - control: false, - table: { - type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", - }, - }, - categoryKey: { - description: - "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", - control: false, - table: { - type: { summary: "string" }, - defaultValue: { summary: "string" }, - category: "Data", - }, - }, - theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", - control: "select", - options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], - table: { - defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - icons: { - description: - "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", - control: false, - table: { - type: { summary: "Record" }, - defaultValue: { summary: "{}" }, - category: "Appearance", - }, - }, - variant: { - description: - "The style of the bar chart. 'grouped' shows bars side by side, while 'stacked' shows bars stacked on top of each other.", - control: "radio", - options: ["grouped", "stacked"], - table: { - defaultValue: { summary: "grouped" }, - category: "Appearance", - }, - }, - radius: { - description: "The radius of the rounded corners of the bars", - control: false, - table: { - type: { summary: "number" }, - defaultValue: { summary: "4" }, - category: "Appearance", - }, - }, - grid: { - description: "Whether to display the background grid lines in the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - label: { - description: "Whether to display data point labels above each point on the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - legend: { - description: - "Whether to display the chart legend showing the data series names and their corresponding colors/icons", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - isAnimationActive: { - description: "Whether to animate the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - showYAxis: { - description: "Whether to display the y-axis", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "false" }, - category: "Display", - }, - }, - xAxisLabel: { - description: "The label for the x-axis", - control: false, - table: { - type: { summary: "string" }, - defaultValue: { summary: "string" }, - category: "Data", - }, - }, - yAxisLabel: { - description: "The label for the y-axis", - control: false, - table: { - type: { summary: "string" }, - defaultValue: { summary: "string" }, - category: "Data", - }, - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const BarChartStory: Story = { - name: "Bar Chart", - args: { - data: barChartData, - categoryKey: "month", - theme: "ocean", - variant: "grouped", - radius: 4, - grid: true, - label: true, - legend: true, - isAnimationActive: true, - showYAxis: false, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` -const barChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - - - - -`, - }, - }, - }, -}; - -export const BarChartStoryWithIcons: Story = { - name: "Bar Chart with Icons", - args: { - ...BarChartStory.args, - icons: icons, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` - import { Monitor, TabletSmartphone } from "lucide-react"; - - const barChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, - ]; - - const icons = { - desktop: Monitor, - mobile: TabletSmartphone, - }; - - - - - `, - }, - }, - }, -}; - -export const BarChartStoryWithYAxis: Story = { - name: "Bar Chart with Y-Axis and Axis Labels", - args: { - ...BarChartStory.args, - showYAxis: true, - xAxisLabel: "Time Period", - yAxisLabel: "Number of Users", - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` - import { Monitor, TabletSmartphone } from "lucide-react"; - - const barChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, - ]; - - - - - `, - }, - }, - }, -}; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/BarChart.tsx similarity index 98% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx rename to js/packages/react-ui/src/components/Charts/BarCharts/BarChart/BarChart.tsx index 037fa4ca1..bfb5a1929 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/BarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/BarChart.tsx @@ -25,7 +25,7 @@ import { getLegendItems, } from "../../utils/dataUtils"; import { getYAxisTickFormatter } from "../../utils/styleUtils"; -import { BarChartV2Data, BarChartVariant } from "../types"; +import { BarChartData, BarChartVariant } from "../types"; import { BAR_WIDTH, findNearestSnapPosition, @@ -41,7 +41,7 @@ import { LineInBarShape } from "./components/LineInBarShape"; // we need to do this because the onClick event type is not exported by recharts type BarChartOnClick = React.ComponentProps["onClick"]; type BarClickData = Parameters>[0]; -export interface BarChartPropsV2 { +export interface BarChartProps { data: T; categoryKey: keyof T[number]; theme?: PaletteName; @@ -65,7 +65,7 @@ const BAR_CATEGORY_GAP = "20%"; // Gap between categories const BAR_INTERNAL_LINE_WIDTH = 1; const BAR_RADIUS = 4; -const BarChartV2Component = ({ +const BarChartComponent = ({ data, categoryKey, theme = "ocean", @@ -81,7 +81,7 @@ const BarChartV2Component = ({ className, height, width, -}: BarChartPropsV2) => { +}: BarChartProps) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); }, [data, categoryKey]); @@ -469,4 +469,4 @@ const BarChartV2Component = ({ }; // Added React.memo for performance optimization to avoid unnecessary re-renders -export const BarChartV2 = React.memo(BarChartV2Component) as typeof BarChartV2Component; +export const BarChart = React.memo(BarChartComponent) as typeof BarChartComponent; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/barChart.scss similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/barChar.scss rename to js/packages/react-ui/src/components/Charts/BarCharts/BarChart/barChart.scss diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/CustomCursor.scss similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.scss rename to js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/CustomCursor.scss diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/CustomCursor.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/CustomCursor.tsx rename to js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/CustomCursor.tsx diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/LineInBarShape.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/components/LineInBarShape.tsx rename to js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/LineInBarShape.tsx diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/dependencies.ts b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/dependencies.ts new file mode 100644 index 000000000..4cfac500e --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/dependencies.ts @@ -0,0 +1,2 @@ +const dependencies = ["BarChart", "IconButton"]; +export default dependencies; diff --git a/js/packages/react-ui/src/components/Charts/BarChart/index.ts b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarChart/index.ts rename to js/packages/react-ui/src/components/Charts/BarCharts/BarChart/index.ts diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/stories/barChart.stories.tsx similarity index 97% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx rename to js/packages/react-ui/src/components/Charts/BarCharts/BarChart/stories/barChart.stories.tsx index 193b6afa6..9c5cc6105 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/stories/barChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/stories/barChart.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Monitor, TabletSmartphone } from "lucide-react"; import { useState } from "react"; import { Card } from "../../../../Card"; -import { BarChartPropsV2, BarChartV2 } from "../BarChartV2"; +import { BarChart, BarChartProps } from "../BarChart"; // 📊 ALL DATA VARIATIONS - For easy switching in stories const dataVariations = { @@ -488,9 +488,9 @@ const icons = { mobile: TabletSmartphone, } as const; -const meta: Meta> = { +const meta: Meta> = { title: "Components/Charts/BarCharts/BarChartV2", - component: BarChartV2, + component: BarChart, parameters: { layout: "centered", docs: { @@ -637,7 +637,7 @@ const meta: Meta> = { }, }, }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; @@ -754,7 +754,7 @@ export const BarChartV2Story: Story = {
- +
); @@ -784,7 +784,7 @@ export const SmallDataStory: Story = { }, render: (args: any) => ( - + ), }; @@ -804,7 +804,7 @@ export const LargeDataStory: Story = { }, render: (args: any) => ( - + ), }; @@ -824,7 +824,7 @@ export const WeeklyDataStory: Story = { }, render: (args: any) => ( - + ), }; @@ -844,7 +844,7 @@ export const BigNumbersStory: Story = { }, render: (args: any) => ( - + ), }; @@ -864,7 +864,7 @@ export const EdgeCaseStory: Story = { }, render: (args: any) => ( - + ), }; @@ -884,7 +884,7 @@ export const NumberRangesStory: Story = { }, render: (args: any) => ( - + ), }; @@ -904,7 +904,7 @@ export const ExpandCollapseMarketingStory: Story = { }, render: (args: any) => ( - + ), parameters: { diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/dependencies.ts b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/dependencies.ts deleted file mode 100644 index 2d4a6054b..000000000 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/dependencies.ts +++ /dev/null @@ -1,2 +0,0 @@ -const dependencies = ["BarChartV2", "IconButton"]; -export default dependencies; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/index.ts b/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/index.ts deleted file mode 100644 index 14de9387b..000000000 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChartV2/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./BarChartV2"; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx index 7e21f0e62..ca92b6fe9 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx @@ -4,7 +4,7 @@ import { Bar, BarChart, XAxis } from "recharts"; import { useTheme } from "../../../ThemeProvider"; import { ChartConfig, ChartContainer } from "../../Charts"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; -import { LineInBarShape } from "../BarChartV2/components/LineInBarShape"; +import { LineInBarShape } from "../BarChart/components/LineInBarShape"; import { type MiniBarChartData } from "../types"; import { getPadding, diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/index.scss b/js/packages/react-ui/src/components/Charts/BarCharts/index.scss index 70022ddf4..348018b6a 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/index.scss +++ b/js/packages/react-ui/src/components/Charts/BarCharts/index.scss @@ -1,3 +1,3 @@ -@forward "./BarChartV2/barChar.scss"; +@forward "./BarChart/barChart.scss"; @forward "./MiniBarChart/miniBarChart.scss"; -@forward "./BarChartV2/components/CustomCursor.scss"; +@forward "./BarChart/components/CustomCursor.scss"; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/index.ts b/js/packages/react-ui/src/components/Charts/BarCharts/index.ts index d996fc0ef..16f85d4dc 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/index.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/index.ts @@ -1,3 +1,3 @@ -export * from "./BarChartV2"; +export * from "./BarChart"; export * from "./MiniBarChart"; export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts index f67c7c85d..d2db3c40c 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts @@ -1,5 +1,5 @@ export type BarChartVariant = "grouped" | "stacked"; -export type BarChartV2Data = Array>; +export type BarChartData = Array>; export type MiniBarChartData = Array | Array<{ value: number; label?: string }>; diff --git a/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx b/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx deleted file mode 100644 index c0af1449b..000000000 --- a/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import React from "react"; -import { LabelList, Line, LineChart as RechartsLineChart, XAxis, YAxis } from "recharts"; -import { useLayoutContext } from "../../../context/LayoutContext"; -import { - ChartConfig, - ChartContainer, - ChartLegend, - ChartLegendContent, - ChartTooltip, - ChartTooltipContent, - keyTransform, -} from "../Charts"; -import { cartesianGrid } from "../shared/CartesianGrid/cartesianGrid"; -import { getDistributedColors, getPalette } from "../utils/PalletUtils"; - -export type LineChartData = Array>; - -export interface LineChartProps { - data: T; - categoryKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - variant?: "linear" | "natural" | "step"; - grid?: boolean; - label?: boolean; - legend?: boolean; - strokeWidth?: number; - icons?: Partial>; - isAnimationActive?: boolean; - showYAxis?: boolean; - xAxisLabel?: React.ReactNode; - yAxisLabel?: React.ReactNode; -} - -export const LineChart = ({ - data, - categoryKey, - theme = "ocean", - variant = "natural", - grid = true, - label = true, - legend = true, - strokeWidth = 2, - icons = {}, - isAnimationActive = true, - showYAxis = false, - xAxisLabel, - yAxisLabel, -}: LineChartProps) => { - // excluding the categoryKey - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); - - const palette = getPalette(theme); - const colors = getDistributedColors(palette, dataKeys.length); - const { layout } = useLayoutContext(); - - // Create Config - const chartConfig: ChartConfig = dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - icon: icons[key], - color: colors[index], - }, - }), - {}, - ); - - const getTickFormatter = (data: T) => { - const dataLength = data.length; - const maxLengthMap = { - mobile: { - default: 5, - 10: 4, - 11: 4, - }, - tray: { - default: 5, - 8: 4, - 9: 4, - 10: 4, - 11: 4, - }, - copilot: { - default: 5, - 8: 4, - 9: 4, - 10: 4, - 11: 4, - }, - fullscreen: { - default: 5, - 11: 4, - }, - }; - - const layoutConfig = - maxLengthMap[layout as keyof typeof maxLengthMap] || maxLengthMap.fullscreen; - - const maxLength = - dataLength >= 11 - ? 4 - : layoutConfig[dataLength as keyof typeof layoutConfig] || layoutConfig.default; - - return (value: string) => { - if (value.length > maxLength) { - return `${value.slice(0, maxLength)}...`; - } - return value; - }; - }; - const getAxisAngle = (data: T) => { - const angleConfig = { - mobile: { - default: 0, - ranges: [ - { min: 6, max: 9, angle: -45 }, - { min: 10, max: 10, angle: -60 }, - { min: 11, max: Infinity, angle: -75 }, - ], - }, - tray: { - default: 0, - ranges: [{ min: 8, max: Infinity, angle: -45 }], - }, - copilot: { - default: 0, - ranges: [{ min: 8, max: Infinity, angle: -45 }], - }, - fullscreen: { - default: 0, - ranges: [{ min: 12, max: Infinity, angle: -45 }], - }, - }; - - const layoutConfig = angleConfig[layout as keyof typeof angleConfig] || angleConfig.fullscreen; - const dataLength = data.length; - - const matchRange = layoutConfig.ranges.find( - (range) => dataLength >= range.min && dataLength <= range.max, - ); - - return matchRange?.angle ?? layoutConfig.default; - }; - const getTickMargin = (data: T) => { - return data.length <= 6 ? 10 : 15; - }; - - return ( - - - {grid && cartesianGrid()} - - {showYAxis && ( - - )} - } /> - {dataKeys.map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - if (label) { - return ( - - {label && ( - - )} - - ); - } - return ( - - ); - })} - {legend && } />} - - - ); -}; diff --git a/js/packages/react-ui/src/components/Charts/LineChart/stories/lineChart.stories.tsx b/js/packages/react-ui/src/components/Charts/LineChart/stories/lineChart.stories.tsx deleted file mode 100644 index d3b1fb7f8..000000000 --- a/js/packages/react-ui/src/components/Charts/LineChart/stories/lineChart.stories.tsx +++ /dev/null @@ -1,324 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { Monitor, TabletSmartphone } from "lucide-react"; -import { Card } from "../../../Card"; -import { LineChart, LineChartProps } from "../LineChart"; - -const lineChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - -const icons = { - desktop: Monitor, - mobile: TabletSmartphone, -} as const; - -const meta: Meta> = { - title: "Components/Charts/LineChart", - component: LineChart, - parameters: { - layout: "centered", - docs: { - description: { - component: "```tsx\nimport { LineChart } from '@crayon-ui/react-ui/Charts/LineChart';\n```", - }, - }, - }, - tags: ["!dev", "autodocs"], - - argTypes: { - data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", - control: false, - table: { - type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", - }, - }, - categoryKey: { - description: - "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", - control: false, - table: { - type: { summary: "string" }, - defaultValue: { summary: "string" }, - category: "Data", - }, - }, - theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", - control: "select", - options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], - table: { - defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - icons: { - description: - "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", - control: false, - table: { - type: { summary: "Record" }, - defaultValue: { summary: "{}" }, - category: "Appearance", - }, - }, - variant: { - description: - "The interpolation method used to create the line curves. 'linear' creates straight lines between points, 'natural' creates smooth curves, and 'step' creates a stepped line.", - control: "radio", - options: ["linear", "natural", "step"], - table: { - defaultValue: { summary: "natural" }, - category: "Appearance", - }, - }, - strokeWidth: { - description: "The width of the line stroke", - control: false, - table: { - type: { summary: "number" }, - defaultValue: { summary: "2" }, - category: "Appearance", - }, - }, - grid: { - description: "Whether to display the background grid lines in the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - label: { - description: "Whether to display data point labels above each point on the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - legend: { - description: - "Whether to display the chart legend showing the data series names and their corresponding colors/icons", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - isAnimationActive: { - description: "Whether to animate the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - showYAxis: { - description: "Whether to display the y-axis", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "false" }, - category: "Display", - }, - }, - xAxisLabel: { - description: "The label for the x-axis", - control: false, - table: { - type: { summary: "string" }, - defaultValue: { summary: "string" }, - category: "Data", - }, - }, - yAxisLabel: { - description: "The label for the y-axis", - control: false, - table: { - type: { summary: "string" }, - defaultValue: { summary: "string" }, - category: "Data", - }, - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const LineChartStory: Story = { - name: "Line Chart", - args: { - data: lineChartData, - categoryKey: "month", - theme: "ocean", - variant: "natural", - strokeWidth: 2, - grid: true, - label: true, - legend: true, - isAnimationActive: true, - showYAxis: false, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` -const lineChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - - - -`, - }, - }, - }, -}; - -export const LineChartStoryWithIcons: Story = { - name: "Line Chart with Icons", - args: { - ...LineChartStory.args, - icons: icons, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` - import { Monitor, TabletSmartphone } from "lucide-react"; - - const lineChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - -const icons = { - desktop: Monitor, - mobile: TabletSmartphone, -}; - - - - - `, - }, - }, - }, -}; - -export const LineChartStoryWithYAxis: Story = { - name: "Line Chart with Y-Axis and Axis Labels", - args: { - ...LineChartStory.args, - showYAxis: true, - xAxisLabel: "Time Period", - yAxisLabel: "Number of Users", - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` - import { Monitor, TabletSmartphone } from "lucide-react"; - - const lineChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - - - - - `, - }, - }, - }, -}; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/LineChart.tsx similarity index 98% rename from js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx rename to js/packages/react-ui/src/components/Charts/LineCharts/LineChart/LineChart.tsx index b1cca3806..7516817d4 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/LineChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/LineChart.tsx @@ -32,12 +32,12 @@ import { getLegendItems, } from "../../utils/dataUtils"; import { getYAxisTickFormatter } from "../../utils/styleUtils"; -import { LineChartV2Data, LineChartVariant } from "../types"; +import { LineChartData, LineChartVariant } from "../types"; type LineChartOnClick = React.ComponentProps["onClick"]; type LineClickData = Parameters>[0]; -export interface LineChartV2Props { +export interface LineChartProps { data: T; categoryKey: keyof T[number]; theme?: PaletteName; @@ -57,7 +57,7 @@ export interface LineChartV2Props { const Y_AXIS_WIDTH = 40; // Width of Y-axis chart when shown -export const LineChartV2 = ({ +export const LineChart = ({ data, categoryKey, theme = "ocean", @@ -73,7 +73,7 @@ export const LineChartV2 = ({ height, width, strokeWidth = 2, -}: LineChartV2Props) => { +}: LineChartProps) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); }, [data, categoryKey]); diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/dependencies.ts b/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/dependencies.ts new file mode 100644 index 000000000..e9ef0ced9 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/dependencies.ts @@ -0,0 +1,2 @@ +const dependencies = ["LineChart", "IconButton"]; +export default dependencies; diff --git a/js/packages/react-ui/src/components/Charts/LineChart/index.ts b/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/LineChart/index.ts rename to js/packages/react-ui/src/components/Charts/LineCharts/LineChart/index.ts diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/lineChartV2.scss b/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/lineChart.scss similarity index 100% rename from js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/lineChartV2.scss rename to js/packages/react-ui/src/components/Charts/LineCharts/LineChart/lineChart.scss diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/stories/lineChart.stories.tsx similarity index 96% rename from js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx rename to js/packages/react-ui/src/components/Charts/LineCharts/LineChart/stories/lineChart.stories.tsx index e59a912c8..cdeaa46bb 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/stories/lineChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/stories/lineChart.stories.tsx @@ -11,7 +11,7 @@ import { } from "lucide-react"; import { useState } from "react"; import { Card } from "../../../../Card"; -import { LineChartV2, LineChartV2Props } from "../LineChartV2"; +import { LineChart, LineChartProps } from "../LineChart"; // 🔥 COMPREHENSIVE DATA VARIATIONS - Designed to test label collision scenarios const dataVariations = { @@ -445,9 +445,9 @@ const icons = { sessions: TabletSmartphone, } as const; -const meta: Meta> = { +const meta: Meta> = { title: "Components/Charts/LineCharts/LineChartV2", - component: LineChartV2, + component: LineChart, parameters: { layout: "centered", docs: { @@ -526,7 +526,7 @@ const meta: Meta> = { }, }, }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; @@ -648,7 +648,7 @@ export const LineChartV2Story: Story = {
- +
); @@ -669,7 +669,7 @@ export const BigLabelsStory: Story = { }, render: (args: any) => ( - + ), }; @@ -689,7 +689,7 @@ export const DenseTimelineStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -718,19 +718,19 @@ export const StrokeCustomizationStory: Story = {

Thick Lines (strokeWidth: 4)

- +

Dashed Lines

- +

With Dots

- +
@@ -754,7 +754,7 @@ export const VariantComparisonStory: Story = {

Linear Variant

- +
@@ -762,13 +762,13 @@ export const VariantComparisonStory: Story = { Natural Variant (Smooth Curves) - +

Step Variant

- +
@@ -798,7 +798,7 @@ export const ExpandCollapseMarketingStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -831,7 +831,7 @@ export const ResponsiveWidthStory: Story = { Small Container (300px) - +
@@ -839,7 +839,7 @@ export const ResponsiveWidthStory: Story = { Medium Container (500px) - +
@@ -847,7 +847,7 @@ export const ResponsiveWidthStory: Story = { Large Container (800px) - +
diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/dependencies.ts b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/dependencies.ts deleted file mode 100644 index 62eb75321..000000000 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/dependencies.ts +++ /dev/null @@ -1,2 +0,0 @@ -const dependencies = ["LineChartV2", "IconButton"]; -export default dependencies; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/index.ts b/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/index.ts deleted file mode 100644 index 6addf7f62..000000000 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChartV2/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./LineChartV2"; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/index.scss b/js/packages/react-ui/src/components/Charts/LineCharts/index.scss index 2b9416604..a14d89e40 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/index.scss +++ b/js/packages/react-ui/src/components/Charts/LineCharts/index.scss @@ -1 +1 @@ -@forward "./LineChartV2/lineChartV2.scss"; +@forward "./LineChart/lineChart.scss"; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/index.ts b/js/packages/react-ui/src/components/Charts/LineCharts/index.ts index 41237b841..3f81940f9 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/index.ts +++ b/js/packages/react-ui/src/components/Charts/LineCharts/index.ts @@ -1,3 +1,3 @@ -export * from "./LineChartV2"; +export * from "./LineChart"; export * from "./MiniLineChart"; export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts index c46c0eb49..4333668ed 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts +++ b/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts @@ -1,4 +1,4 @@ -export type LineChartV2Data = Array>; +export type LineChartData = Array>; export type LineChartVariant = "linear" | "natural" | "step"; diff --git a/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx b/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx deleted file mode 100644 index 44496a4b6..000000000 --- a/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import clsx from "clsx"; -import { debounce } from "lodash-es"; -import { useEffect, useRef, useState } from "react"; -import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; -import { useLayoutContext } from "../../../context/LayoutContext"; -import { - ChartConfig, - ChartContainer, - ChartLegend, - ChartLegendContent, - ChartTooltip, - ChartTooltipContent, -} from "../Charts"; -import { getDistributedColors, getPalette } from "../utils/PalletUtils"; - -export type PieChartData = Array>; - -export interface PieChartProps { - data: T; - categoryKey: keyof T[number]; - dataKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - variant?: "pie" | "donut"; - format?: "percentage" | "number"; - legend?: boolean; - label?: boolean; - isAnimationActive?: boolean; -} - -const layoutMap: Record = { - mobile: "crayon-pie-chart-container-mobile", - fullscreen: "crayon-pie-chart-container-fullscreen", - tray: "crayon-pie-chart-container-tray", - copilot: "crayon-pie-chart-container-copilot", -}; - -// Helper function to calculate percentage -const calculatePercentage = (value: number, total: number): number => { - if (total === 0) { - return 0; - } - return Number(((value / total) * 100).toFixed(2)); -}; - -export const PieChart = ({ - data, - categoryKey, - dataKey, - theme = "ocean", - variant = "pie", - format = "number", - legend = true, - label = true, - isAnimationActive = true, -}: PieChartProps) => { - const { layout } = useLayoutContext(); - const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(120); - const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(0); - const containerRef = useRef(null); - - // Calculate dynamic radius based on layout - useEffect(() => { - if (!containerRef.current) return; - - const resizeObserver = new ResizeObserver( - debounce((entries: any) => { - const { width } = entries[0].contentRect; - - // Calculate outer radius - let newOuterRadius = 120; // default - if (layout === "mobile") { - newOuterRadius = label ? (width > 300 ? 85 : 75) : width > 300 ? 95 : 80; - } else if (layout === "fullscreen") { - newOuterRadius = 120; - } else if (layout === "tray" || layout === "copilot") { - newOuterRadius = 90; - } - - // Calculate inner radius for donut - let newInnerRadius = 0; - if (variant === "donut") { - if (layout === "mobile") { - newInnerRadius = label ? (width > 300 ? 50 : 30) : width > 300 ? 60 : 50; - } else { - newInnerRadius = 60; - } - } - - setCalculatedOuterRadius(newOuterRadius); - setCalculatedInnerRadius(newInnerRadius); - }, 100), - ); - - resizeObserver.observe(containerRef.current); - return () => resizeObserver.disconnect(); - }, [layout, label, variant]); - - // Calculate total for percentage calculations - const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); - - // Transform data with percentages - const transformedData = data.map((item) => ({ - ...item, - percentage: calculatePercentage(Number(item[dataKey as string]), total), - originalValue: item[dataKey as string], - })); - - // Get color palette and distribute colors - const palette = getPalette(theme); - const colors = getDistributedColors(palette, data.length); - - // Create chart configuration - const chartConfig = data.reduce( - (config, item, index) => ({ - ...config, - [String(item[categoryKey])]: { - label: String(item[categoryKey as string]), - color: colors[index], - }, - }), - {}, - ); - - // Custom label renderer - const renderCustomLabel = ({ payload, cx, cy, x, y, textAnchor, dominantBaseline }: any) => { - if (payload.percentage <= 10) return null; - const displayValue = format === "percentage" ? payload.percentage : payload[dataKey]; - const formattedValue = - String(displayValue).length > 7 ? `${String(displayValue).slice(0, 7)}...` : displayValue; - - return ( - - - {formattedValue} - {format === "percentage" ? "%" : ""} - - - ); - }; - - return ( - - - } /> - {legend && } />} - - {Object.entries(chartConfig).map(([key, config]) => ( - - ))} - - - - ); -}; diff --git a/js/packages/react-ui/src/components/Charts/PieChart/stories/pieChart.stories.tsx b/js/packages/react-ui/src/components/Charts/PieChart/stories/pieChart.stories.tsx deleted file mode 100644 index 1ada8fffa..000000000 --- a/js/packages/react-ui/src/components/Charts/PieChart/stories/pieChart.stories.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { Card } from "../../../Card"; -import { PieChart, PieChartProps } from "../PieChart"; - -const pieChartData = [ - { month: "January", value: 4250 }, - { month: "February", value: 3820 }, - { month: "March", value: 4680 }, - { month: "April", value: 4120 }, - { month: "May", value: 5340 }, - { month: "June", value: 6250 }, - { month: "July", value: 5890 }, -]; - -const meta: Meta> = { - title: "Components/Charts/PieChart", - component: PieChart, - parameters: { - layout: "centered", - docs: { - description: { - component: "```tsx\nimport { PieChart } from '@crayon-ui/react-ui/Charts/PieChart';\n```", - }, - }, - }, - tags: ["!dev", "autodocs"], - - argTypes: { - data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", - control: false, - table: { - type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", - }, - }, - categoryKey: { - description: - "The key from your data object to be used as the segment labels (e.g., 'month', 'category', 'name')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - dataKey: { - description: - "The key from your data object to be used as the values that determine the slice sizes (e.g., 'value', 'count', 'amount')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", - control: "select", - options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], - table: { - defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - variant: { - description: - "The style of the pie chart. 'pie' shows a pie chart, while 'donut' shows a donut chart.", - control: "radio", - options: ["pie", "donut"], - table: { - defaultValue: { summary: "pie" }, - category: "Appearance", - }, - }, - format: { - description: - "The format of the data. 'percentage' shows the data as a percentage, while 'number' shows the data as a number.", - control: "radio", - options: ["percentage", "number"], - table: { - defaultValue: { summary: "percentage" }, - category: "Display", - }, - }, - legend: { - description: "Whether to display the legend", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - label: { - description: "Whether to display the data point labels above each point on the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - isAnimationActive: { - description: "Whether to animate the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const PieChartStory: Story = { - name: "Pie Chart", - args: { - data: pieChartData, - categoryKey: "month", - dataKey: "value", - theme: "ocean", - variant: "pie", - format: "number", - legend: true, - label: true, - isAnimationActive: true, - }, - - render: (args: any) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` - const pieChartData = [ - { month: "January", value: 400 }, - { month: "February", value: 300 }, - { month: "March", value: 300 }, - { month: "April", value: 400 }, - { month: "May", value: 300 }, - { month: "June", value: 300 }, - { month: "July", value: 300 }, -]; - - - - - `, - }, - }, - }, -}; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/PieChart.tsx similarity index 94% rename from js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx rename to js/packages/react-ui/src/components/Charts/PieCharts/PieChart/PieChart.tsx index 36e649c59..84617d30b 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/PieChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/PieChart.tsx @@ -1,14 +1,14 @@ import clsx from "clsx"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; -import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts"; -import { DefaultLegend } from "../../shared/DefaultLegend/DefaultLegend"; -import { StackedLegend } from "../../shared/StackedLegend/StackedLegend"; -import { LegendItem } from "../../types/Legend"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; -import { createGradientDefinitions } from "../components/PieChartRenderers"; +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts.js"; +import { DefaultLegend } from "../../shared/DefaultLegend/DefaultLegend.js"; +import { StackedLegend } from "../../shared/StackedLegend/StackedLegend.js"; +import { LegendItem } from "../../types/Legend.js"; +import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils.js"; +import { createGradientDefinitions } from "./components/PieChartRenderers.js"; +import { PieChartData } from "../types/index.js"; import { - PieChartData, calculateTwoLevelChartDimensions, createAnimationConfig, createChartConfig, @@ -18,20 +18,18 @@ import { groupSmallSlices, transformDataWithPercentages, useChartHover, -} from "../utils/PieChartUtils"; - -export type PieChartV2Data = PieChartData; +} from "./utils/PieChartUtils.js"; interface GradientColor { start?: string; end?: string; } -export interface PieChartV2Props { +export interface PieChartProps { data: T; categoryKey: keyof T[number]; dataKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + theme?: PaletteName; variant?: "pie" | "donut"; format?: "percentage" | "number"; legend?: boolean; @@ -52,7 +50,7 @@ export interface PieChartV2Props { const STACKED_LEGEND_BREAKPOINT = 600; // px -export const PieChartV2 = ({ +export const PieChart = ({ data, categoryKey, dataKey, @@ -73,7 +71,7 @@ export const PieChartV2 = ({ className, height, width, -}: PieChartV2Props) => { +}: PieChartProps) => { const wrapperRef = useRef(null); const [wrapperRect, setWrapperRect] = useState({ width: 0, height: 0 }); const [hoveredLegendKey, setHoveredLegendKey] = useState(null); @@ -288,7 +286,7 @@ export const PieChartV2 = ({ innerRadius={dimensions.innerRadius} outerRadius={dimensions.middleRadius} > - {transformedData.map((entry, index) => { + {transformedData.map((entry, index: number) => { const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); const config = chartConfig[categoryValue]; const hoverStyles = getHoverStyles(index, activeIndex); @@ -310,7 +308,7 @@ export const PieChartV2 = ({ innerRadius={dimensions.middleRadius} outerRadius={dimensions.outerRadius} > - {transformedData.map((entry, index) => { + {transformedData.map((entry, index: number) => { const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); const config = chartConfig[categoryValue]; const hoverStyles = getHoverStyles(index, activeIndex); @@ -327,7 +325,7 @@ export const PieChartV2 = ({ innerRadius={dimensions.innerRadius} activeIndex={activeIndex ?? undefined} > - {transformedData.map((entry, index) => { + {transformedData.map((entry, index: number) => { const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); const config = chartConfig[categoryValue]; const hoverStyles = getHoverStyles(index, activeIndex); diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/components/PieChartRenderers.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/PieCharts/components/PieChartRenderers.tsx rename to js/packages/react-ui/src/components/Charts/PieCharts/PieChart/components/PieChartRenderers.tsx diff --git a/js/packages/react-ui/src/components/Charts/PieChart/index.ts b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/PieChart/index.ts rename to js/packages/react-ui/src/components/Charts/PieCharts/PieChart/index.ts diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/pieChart.scss similarity index 100% rename from js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/pieChartV2.scss rename to js/packages/react-ui/src/components/Charts/PieCharts/PieChart/pieChart.scss diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx similarity index 97% rename from js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx rename to js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx index c67ed17ec..06895546b 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/stories/PieChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { useState } from "react"; import { Card } from "../../../../Card"; -import { PieChartV2, PieChartV2Props } from "../PieChartV2"; +import { PieChart, PieChartProps } from "../PieChart"; const pieChartData = [ { month: "January", value: 1250 }, @@ -52,9 +52,9 @@ const gradientColors = [ { start: "#9B59B6", end: "#B07CC7" }, ]; -const meta: Meta> = { +const meta: Meta> = { title: "Components/Charts/PieCharts/PieChartV2", - component: PieChartV2, + component: PieChart, parameters: { layout: "centered", docs: { @@ -206,7 +206,7 @@ const meta: Meta> = { }, }, }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; @@ -233,7 +233,7 @@ export const PieChartV2Demo: Story = { }, render: (args: any) => ( - + ), }; @@ -260,7 +260,7 @@ export const PieChartV2WithCarousel: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -350,7 +350,7 @@ export const ResizableAndResponsive: Story = { overflow: "hidden", }} > - + {/* Resizer handles */}
>; +import { getDistributedColors, getPalette } from "../../../utils/PalletUtils"; +import { PieChartData } from "../../types"; +import { ChartConfig } from "../../../Charts"; export interface ChartDimensions { outerRadius: number; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/index.ts b/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/index.ts deleted file mode 100644 index 737933f38..000000000 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChartV2/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./PieChartV2"; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/index.scss b/js/packages/react-ui/src/components/Charts/PieCharts/index.scss new file mode 100644 index 000000000..080e04c13 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieCharts/index.scss @@ -0,0 +1 @@ +@forward "./PieChart/pieChart.scss"; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/index.ts b/js/packages/react-ui/src/components/Charts/PieCharts/index.ts index 737933f38..d4a4262d5 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/index.ts +++ b/js/packages/react-ui/src/components/Charts/PieCharts/index.ts @@ -1 +1,2 @@ -export * from "./PieChartV2"; +export * from "./PieChart"; +export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/PieCharts/types/index.ts new file mode 100644 index 000000000..44e9afb30 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/PieCharts/types/index.ts @@ -0,0 +1 @@ +export type PieChartData = Array>; diff --git a/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx b/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx deleted file mode 100644 index af863997c..000000000 --- a/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from "react"; -import { PolarAngleAxis, PolarGrid, Radar, RadarChart as RechartsRadarChart } from "recharts"; -import { - ChartConfig, - ChartContainer, - ChartLegend, - ChartLegendContent, - ChartTooltip, - ChartTooltipContent, - keyTransform, -} from "../Charts"; -import { getDistributedColors, getPalette } from "../utils/PalletUtils"; - -export type RadarChartData = Array>; - -export interface RadarChartProps { - data: T; - categoryKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - variant?: "line" | "area"; - grid?: boolean; - legend?: boolean; - strokeWidth?: number; - areaOpacity?: number; - icons?: Partial>; - isAnimationActive?: boolean; -} - -export const RadarChart = ({ - data, - categoryKey, - theme = "ocean", - variant = "line", - grid = true, - legend = true, - strokeWidth = 2, - areaOpacity = 0.5, - icons = {}, - isAnimationActive = true, -}: RadarChartProps) => { - // excluding the categoryKey - const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== categoryKey); - - const palette = getPalette(theme); - const colors = getDistributedColors(palette, dataKeys.length); - - // Create Config - const chartConfig: ChartConfig = dataKeys.reduce( - (config, key, index) => ({ - ...config, - [key]: { - label: key, - icon: icons[key], - color: colors[index], - }, - }), - {}, - ); - - return ( - - - {grid && } - - - } /> - {dataKeys.map((key) => { - const transformedKey = keyTransform(key); - const color = `var(--color-${transformedKey})`; - if (variant === "line") { - return ( - - ); - } else { - return ( - - ); - } - })} - - {legend && ( - - } - /> - )} - - - ); -}; diff --git a/js/packages/react-ui/src/components/Charts/RadarChart/stories/raderChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadarChart/stories/raderChart.stories.tsx deleted file mode 100644 index d230d1275..000000000 --- a/js/packages/react-ui/src/components/Charts/RadarChart/stories/raderChart.stories.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { Monitor, TabletSmartphone } from "lucide-react"; -import { Card } from "../../../Card"; -import { RadarChart, RadarChartProps } from "../RadarChart"; - -const radarChartData = [ - { month: "January", desktop: 250, mobile: 150 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - -const icons = { - desktop: Monitor, - mobile: TabletSmartphone, -} as const; - -const meta: Meta> = { - title: "Components/Charts/RadarChart", - component: RadarChart, - parameters: { - layout: "centered", - docs: { - description: { - component: - "```tsx\nimport { RadarChart } from '@crayon-ui/react-ui/Charts/RadarChart';\n```", - }, - }, - }, - tags: ["!dev", "autodocs"], - - argTypes: { - data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", - control: false, - table: { - type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", - }, - }, - categoryKey: { - description: - "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", - control: "select", - options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], - table: { - defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - icons: { - description: - "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", - control: false, - table: { - type: { summary: "Record" }, - defaultValue: { summary: "{}" }, - category: "Appearance", - }, - }, - variant: { - description: - "The style of the radar chart. 'line' shows only the connecting lines between data points, while 'area' fills the shape created by the data points.", - control: "radio", - options: ["line", "area"], - table: { - defaultValue: { summary: "area" }, - category: "Appearance", - }, - }, - strokeWidth: { - description: "The width of the line stroke", - control: false, - table: { - type: { summary: "number" }, - defaultValue: { summary: "2" }, - category: "Appearance", - }, - }, - grid: { - description: "Whether to display the background grid lines in the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - areaOpacity: { - description: "The opacity of the area fill", - control: false, - table: { - type: { summary: "number" }, - defaultValue: { summary: "0.7" }, - category: "Appearance", - }, - }, - legend: { - description: - "Whether to display the chart legend showing the data series names and their corresponding colors/icons", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - isAnimationActive: { - description: "Whether to animate the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const RadarChartStory: Story = { - name: "Radar Chart", - args: { - data: radarChartData, - categoryKey: "month", - theme: "ocean", - variant: "area", - strokeWidth: 2, - areaOpacity: 0.5, - legend: true, - grid: true, - isAnimationActive: true, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` - const radarChartData = [ - { month: "January", desktop: 250, mobile: 150 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - - - - - `, - }, - }, - }, -}; - -export const RadarChartStoryWithIcons: Story = { - name: "Radar Chart with Icons", - args: { - ...RadarChartStory.args, - icons: icons, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - source: { - code: ` -import { Monitor, TabletSmartphone } from "lucide-react"; - -const radarChartData = [ - { month: "January", desktop: 250, mobile: 150 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - { month: "April", desktop: 180, mobile: 160 }, - { month: "May", desktop: 250, mobile: 120 }, - { month: "June", desktop: 300, mobile: 180 }, -]; - -const icons = { - desktop: Monitor, - mobile: TabletSmartphone, -} as const; - - - - - `, - }, - }, - }, -}; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/RadarChart.tsx similarity index 95% rename from js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx rename to js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/RadarChart.tsx index 2147271bb..438362d1e 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/RadarChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/RadarChart.tsx @@ -8,9 +8,9 @@ import { LegendItem } from "../../types"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; import { AxisLabel } from "./components/AxisLabel"; -import { RadarChartV2Data } from "./types"; +import { RadarChartData } from "./types"; -export interface RadarChartV2Props { +export interface RadarChartProps { data: T; categoryKey: keyof T[number]; theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; @@ -24,7 +24,7 @@ export interface RadarChartV2Props { size?: number; } -const RadarChartV2Component = ({ +const RadarChartComponent = ({ data, categoryKey, theme = "ocean", @@ -36,7 +36,7 @@ const RadarChartV2Component = ({ icons = {}, isAnimationActive = true, size, -}: RadarChartV2Props) => { +}: RadarChartProps) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); }, [data, categoryKey]); @@ -190,6 +190,6 @@ const RadarChartV2Component = ({ ); }; -export const RadarChartV2 = memo(RadarChartV2Component); +export const RadarChart = memo(RadarChartComponent); -RadarChartV2.displayName = "RadarChartV2"; +RadarChart.displayName = "RadarChart"; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/components/AxisLabel.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/components/AxisLabel.tsx rename to js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/components/AxisLabel.tsx diff --git a/js/packages/react-ui/src/components/Charts/RadarChart/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/index.ts similarity index 54% rename from js/packages/react-ui/src/components/Charts/RadarChart/index.ts rename to js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/index.ts index 536521034..ba0fe04a9 100644 --- a/js/packages/react-ui/src/components/Charts/RadarChart/index.ts +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/index.ts @@ -1 +1,2 @@ export * from "./RadarChart"; +export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/radarChart.scss similarity index 100% rename from js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/radarChartV2.scss rename to js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/radarChart.scss diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/stories/RadarChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx similarity index 96% rename from js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/stories/RadarChartV2.stories.tsx rename to js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx index 045255d33..8ce1af362 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/stories/RadarChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Shield, Star, Target, TrendingUp, Users, Zap } from "lucide-react"; import { useState } from "react"; import { Card } from "../../../../Card"; -import { RadarChartV2, RadarChartV2Props } from "../RadarChartV2"; +import { RadarChart, RadarChartProps } from "../RadarChart"; // 📊 COMPREHENSIVE DATA VARIATIONS - Designed to test various radar chart scenarios const dataVariations = { @@ -84,9 +84,9 @@ const categoryKeys = { // Default data for the main story const radarChartData = dataVariations.default; -const meta: Meta> = { +const meta: Meta> = { title: "Components/Charts/RadarCharts/RadarChartV2", - component: RadarChartV2, + component: RadarChart, parameters: { layout: "centered", docs: { @@ -194,7 +194,7 @@ const meta: Meta> = { }, }, }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; @@ -315,7 +315,7 @@ export const RadarChartV2Story: Story = { minHeight: "300px", }} > - +
); @@ -345,7 +345,7 @@ export const SkillsStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -378,7 +378,7 @@ export const TeamPerformanceStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -406,7 +406,7 @@ export const BusinessMetricsStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -437,13 +437,13 @@ export const VariantComparisonStory: Story = {

Line Variant

- +

Area Variant

- +
@@ -479,7 +479,7 @@ export const ThemeShowcaseStory: Story = { {theme} - +
))} @@ -510,7 +510,7 @@ export const SingleMetricStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -538,7 +538,7 @@ export const ManyDimensionsStory: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -574,13 +574,13 @@ export const CustomizationStory: Story = {

With Icons & Animation

- +

No Grid, No Animation, Thick Lines

- +
diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/types/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/types/index.ts new file mode 100644 index 000000000..1639bd74b --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/types/index.ts @@ -0,0 +1 @@ +export type RadarChartData = Array>; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/utils/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/utils/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/utils/index.ts rename to js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/utils/index.ts diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts deleted file mode 100644 index 89a7893bf..000000000 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./RadarChartV2"; -export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/types/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/types/index.ts deleted file mode 100644 index 7d4919a26..000000000 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChartV2/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type RadarChartV2Data = Array>; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/index.scss b/js/packages/react-ui/src/components/Charts/RadarCharts/index.scss new file mode 100644 index 000000000..e6535cba6 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/index.scss @@ -0,0 +1 @@ +@forward "./RadarChart/radarChart.scss"; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/index.ts index 59a239fc8..536521034 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/index.ts +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/index.ts @@ -1 +1 @@ -export * from "./RadarChartV2"; +export * from "./RadarChart"; diff --git a/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx b/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx deleted file mode 100644 index bd7bc5d33..000000000 --- a/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import clsx from "clsx"; -import { debounce } from "lodash-es"; -import { useEffect, useRef, useState } from "react"; -import { Cell, LabelList, PolarGrid, RadialBar, RadialBarChart } from "recharts"; -import { useLayoutContext } from "../../../context/LayoutContext"; -import { - ChartConfig, - ChartContainer, - ChartLegend, - ChartLegendContent, - ChartTooltip, - ChartTooltipContent, -} from "../Charts"; -import { getDistributedColors, getPalette } from "../utils/PalletUtils"; - -export type RadialChartData = Array>; - -export interface RadialChartProps { - data: T; - categoryKey: keyof T[number]; - dataKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; - variant?: "semicircle" | "circular"; - format?: "percentage" | "number"; - legend?: boolean; - label?: boolean; - grid?: boolean; - isAnimationActive?: boolean; -} - -const layoutMap: Record = { - mobile: "crayon-pie-chart-container-mobile", - fullscreen: "crayon-pie-chart-container-fullscreen", - tray: "crayon-pie-chart-container-tray", - copilot: "crayon-pie-chart-container-copilot", -}; - -// Helper function to calculate percentage -const calculatePercentage = (value: number, total: number): number => { - if (total === 0) return 0; - return Number(((value / total) * 100).toFixed(2)); -}; - -export const RadialChart = ({ - data, - categoryKey, - dataKey, - theme = "ocean", - variant = "semicircle", - format = "number", - legend = true, - label = true, - grid = true, - isAnimationActive = true, -}: RadialChartProps) => { - const { layout } = useLayoutContext(); - const [calculatedOuterRadius, setCalculatedOuterRadius] = useState(110); - const [calculatedInnerRadius, setCalculatedInnerRadius] = useState(30); - const containerRef = useRef(null); - - useEffect(() => { - if (!containerRef.current) return; - - const resizeObserver = new ResizeObserver( - debounce((entries: any) => { - const { width } = entries[0].contentRect; - - // Calculate outer radius - let newOuterRadius = 110; // default - if (layout === "mobile") { - newOuterRadius = label ? (width > 300 ? 110 : 90) : width > 300 ? 95 : 80; - } else if (layout === "fullscreen") { - newOuterRadius = 160; - } else if (layout === "tray" || layout === "copilot") { - newOuterRadius = 130; - } else { - newOuterRadius = 100; - } - - // Calculate inner radius - let newInnerRadius = 30; // default - if (layout === "mobile") { - newInnerRadius = 30; - } else if (layout === "fullscreen") { - newInnerRadius = 50; - } else { - newInnerRadius = 30; - } - - setCalculatedOuterRadius(newOuterRadius); - setCalculatedInnerRadius(newInnerRadius); - }, 100), - ); - - resizeObserver.observe(containerRef.current); - return () => resizeObserver.disconnect(); - }, [layout, label]); - - // Calculate total for percentage calculations - const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); - - // Get color palette and distribute colors - const palette = getPalette(theme); - const colors = getDistributedColors(palette, data.length); - - // Transform data with percentages - const transformedData = data.map((item, index) => ({ - ...item, - percentage: calculatePercentage(Number(item[dataKey as string]), total), - originalValue: item[dataKey as string], - fill: colors[index], - })); - - // Create chart configuration - const chartConfig = data.reduce( - (config, item, index) => ({ - ...config, - [String(item[categoryKey])]: { - label: String(item[categoryKey as string]), - color: colors[index], - }, - }), - {}, - ); - - const getFontSize = (dataLength: number) => { - return dataLength <= 5 ? 12 : 7; - }; - - const formatLabel = (value: string | number) => { - if (format === "percentage") { - const item = transformedData.find((d) => String(d.originalValue) === String(value)); - return item ? `${item.percentage}%` : value; - } - // For number format, just truncate if too long - const stringValue = String(value); - return stringValue.length > 8 ? `${stringValue.slice(0, 8)}...` : stringValue; - }; - - return ( - - - {grid && } - - } - /> - {legend && ( - - } - /> - )} - - {Object.entries(chartConfig).map(([key, config]) => ( - - ))} - {label && ( - - )} - - - - ); -}; diff --git a/js/packages/react-ui/src/components/Charts/RadialChart/stories/radialChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialChart/stories/radialChart.stories.tsx deleted file mode 100644 index c672d75f8..000000000 --- a/js/packages/react-ui/src/components/Charts/RadialChart/stories/radialChart.stories.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { Card } from "../../../Card"; -import { RadialChart, RadialChartProps } from "../RadialChart"; - -const radialChartData = [ - { month: "January", value: 400 }, - { month: "February", value: 300 }, - { month: "March", value: 300 }, - { month: "April", value: 400 }, - { month: "May", value: 300 }, - { month: "June", value: 300 }, - { month: "July", value: 300 }, -]; - -const meta: Meta> = { - title: "Components/Charts/RadialChart", - component: RadialChart, - parameters: { - layout: "centered", - docs: { - description: { - component: - "```tsx\nimport { RadialChart } from '@crayon-ui/react-ui/Charts/RadialChart';\n```", - }, - }, - }, - tags: ["!dev", "autodocs"], - - argTypes: { - data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", - control: false, - table: { - type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", - }, - }, - categoryKey: { - description: - "The key from your data object to be used as the category labels for each segment (e.g., 'month', 'year', 'category')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - dataKey: { - description: - "The key from your data object to be used as the values that determine the segment sizes (e.g., 'value', 'count', 'amount')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", - control: "select", - options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], - table: { - defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - variant: { - description: - "The style of the pie chart. 'circular' shows a circular chart, while 'semicircle' shows a semicircle chart.", - control: "radio", - options: ["circular", "semicircle"], - table: { - defaultValue: { summary: "circular" }, - category: "Appearance", - }, - }, - format: { - description: - "The format of the data. 'percentage' shows the data as a percentage, while 'number' shows the data as a number.", - control: "radio", - options: ["percentage", "number"], - table: { - defaultValue: { summary: "percentage" }, - category: "Display", - }, - }, - legend: { - description: "Whether to display the legend", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - label: { - description: "Whether to display the data point labels above each point on the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - grid: { - description: "Whether to display the grid lines", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - isAnimationActive: { - description: "Whether to animate the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const RadialChartStory: Story = { - name: "Radial Chart", - args: { - data: radialChartData, - categoryKey: "month", - dataKey: "value", - theme: "ocean", - variant: "circular", - format: "number", - legend: true, - label: true, - grid: true, - isAnimationActive: true, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - description: { - story: - "A radial chart that displays data in a circular or semicircular format, with customizable themes, labels, and grid options.", - }, - source: { - code: ` -const radialChartData = [ - { month: "January", value: 400 }, - { month: "February", value: 300 }, - { month: "March", value: 300 }, - { month: "April", value: 400 }, - { month: "May", value: 300 }, - { month: "June", value: 300 }, - { month: "July", value: 300 }, -]; - - - - - `, - }, - }, - }, -}; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/RadialChart.tsx similarity index 96% rename from js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx rename to js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/RadialChart.tsx index fbe571f3d..69d1d5583 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/RadialChartV2.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/RadialChart.tsx @@ -5,10 +5,10 @@ import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts" import { DefaultLegend } from "../../shared/DefaultLegend/DefaultLegend"; import { StackedLegend } from "../../shared/StackedLegend/StackedLegend"; import { LegendItem } from "../../types/Legend"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; -import { createRadialGradientDefinitions } from "../components/RadialChartRenderers"; +import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; +import { RadialChartData } from "../types"; +import { createRadialGradientDefinitions } from "./components/RadialChartRenderers"; import { - RadialChartData, calculateRadialChartDimensions, createRadialAnimationConfig, createRadialChartConfig, @@ -17,20 +17,18 @@ import { groupSmallSlices, transformRadialDataWithPercentages, useRadialChartHover, -} from "../utils/RadialChartUtils"; - -export type RadialChartV2Data = RadialChartData; +} from "./utils/RadialChartUtils"; interface GradientColor { start?: string; end?: string; } -export interface RadialChartV2Props { +export interface RadialChartProps { data: T; categoryKey: keyof T[number]; dataKey: keyof T[number]; - theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + theme?: PaletteName; variant?: "semicircle" | "circular"; format?: "percentage" | "number"; legend?: boolean; @@ -50,7 +48,7 @@ export interface RadialChartV2Props { const STACKED_LEGEND_BREAKPOINT = 600; // px -export const RadialChartV2 = ({ +export const RadialChartV2 = ({ data, categoryKey, dataKey, @@ -70,7 +68,7 @@ export const RadialChartV2 = ({ className, height, width, -}: RadialChartV2Props) => { +}: RadialChartProps) => { const wrapperRef = useRef(null); const [wrapperRect, setWrapperRect] = useState({ width: 0, height: 0 }); const [hoveredLegendKey, setHoveredLegendKey] = useState(null); diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/components/RadialChartRenderers.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/components/RadialChartRenderers.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/RadialCharts/components/RadialChartRenderers.tsx rename to js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/components/RadialChartRenderers.tsx diff --git a/js/packages/react-ui/src/components/Charts/RadialChart/index.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/RadialChart/index.ts rename to js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/index.ts diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/radialChartV2.scss b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/radialChart.scss similarity index 100% rename from js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/radialChartV2.scss rename to js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/radialChart.scss diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChartV2.stories.tsx similarity index 99% rename from js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx rename to js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChartV2.stories.tsx index 8746aa6b4..384b2a052 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/stories/RadialChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChartV2.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { useState } from "react"; import { Card } from "../../../../Card"; -import { RadialChartV2, RadialChartV2Props } from "../RadialChartV2"; +import { RadialChartProps, RadialChartV2 } from "../RadialChart"; const radialChartData = [ { month: "January", value: 1250 }, @@ -52,7 +52,7 @@ const gradientColors = [ { start: "#9B59B6", end: "#B07CC7" }, ]; -const meta: Meta> = { +const meta: Meta> = { title: "Components/Charts/RadialCharts/RadialChartV2", component: RadialChartV2, parameters: { diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/utils/RadialChartUtils.ts similarity index 97% rename from js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts rename to js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/utils/RadialChartUtils.ts index 6370e1bcb..0f06d2067 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/utils/RadialChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/utils/RadialChartUtils.ts @@ -2,14 +2,11 @@ * Utility functions for radial charts */ import { useState } from "react"; -import { ChartConfig } from "../../Charts"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { ChartConfig } from "../../../Charts"; +import { getDistributedColors, getPalette } from "../../../utils/PalletUtils"; +import { RadialChartData } from "../../types"; // ========================================== -// Types -// ========================================== - -export type RadialChartData = Array>; export interface RadialChartDimensions { outerRadius: number; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/index.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/index.ts deleted file mode 100644 index 2571e2cb6..000000000 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChartV2/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./RadialChartV2"; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/index.scss b/js/packages/react-ui/src/components/Charts/RadialCharts/index.scss new file mode 100644 index 000000000..7a3b98d81 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/index.scss @@ -0,0 +1 @@ +@forward "./RadialChart/radialChart.scss"; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/index.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/index.ts index 2571e2cb6..2f12d30de 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/index.ts +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/index.ts @@ -1 +1,2 @@ -export * from "./RadialChartV2"; +export * from "./RadialChart"; +export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/types/index.ts new file mode 100644 index 000000000..498dd6e66 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/types/index.ts @@ -0,0 +1 @@ +export type RadialChartData = Array>; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index d88d3fe97..baa8f37e0 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -9,17 +9,17 @@ // line chart css @forward "./LineCharts/index.scss"; -// radar chart v2 css -@forward "./RadarCharts/RadarChartV2/radarChartV2.scss"; +// radar chart css +@forward "./RadarCharts/index.scss"; // progress bar css @forward "./ProgressBarChart/progressBarChart.scss"; // pie chart css -@forward "./PieCharts/PieChartV2/pieChartV2.scss"; +@forward "./PieCharts/index.scss"; // radial chart css -@forward "./RadialCharts/RadialChartV2/radialChartV2.scss"; +@forward "./RadialCharts/index.scss"; // shared components css @forward "./shared/XAxisTick/xAxisTick.scss"; @@ -76,173 +76,6 @@ outline: none; } } - // the tooltip style will might need to be moved to it dedicated file. - // we have 2 types of tooltips, one is the default tooltip and the other is the portal tooltip. - // both are using this style. - // Tooltip styles - // this is moved to portalTooltip.scss file once review is done remove it - // &-tooltip { - // display: grid; - // align-items: start; - // min-width: 128px; - // gap: cssUtils.$spacing-xs; - // padding: cssUtils.$spacing-xs; - // color: cssUtils.$primary-text; - // @include cssUtils.typography(label, default); - // border-radius: cssUtils.$rounded-s; - // border: 1px solid cssUtils.$stroke-default; - // background-color: cssUtils.$bg-container; - // box-shadow: cssUtils.$shadow-s; - // text-transform: capitalize; - - // &-label { - // @include cssUtils.typography(label, default); - // } - - // &-label-heavy { - // @include cssUtils.typography(label, heavy); - // } - - // // this is the content of the tooltip, where the items of the of each data point is rendered, - // // todo: remove commented style once the review is done. - - // &-content { - // display: grid; - // align-items: start; - // min-width: 128px; - // gap: cssUtils.$spacing-xs; - // padding: cssUtils.$spacing-xs; - // color: cssUtils.$primary-text; - // @include cssUtils.typography(label, default); - // // border-radius: cssUtils.$rounded-s; - // // border: 1px solid cssUtils.$stroke-default; - // background-color: cssUtils.$bg-container; - // box-shadow: cssUtils.$shadow-s; - // text-transform: capitalize; - - // &-item { - // display: flex; - // width: 100%; - // flex-wrap: wrap; - // gap: cssUtils.$spacing-xs; - // align-items: stretch; - // &--dot { - // align-items: center; - // } - - // svg { - // height: 10px; - // width: 10px; - // color: cssUtils.$primary-text; - // } - // } - // // this portion is responsible for styling the colored box or other indicators like line, dashed, etc. - // &-indicator { - // flex-shrink: 0; - // border-radius: cssUtils.$rounded-2xs; - - // &--dot { - // height: 10px; - // width: 10px; - // background-color: var(--color-bg); - // border-color: var(--color-border); - // } - - // &--line { - // width: 4px; - // background-color: var(--color-bg); - // border-color: var(--color-border); - // } - - // &--dashed { - // width: 0; - // border: 1.5px dashed var(--color-border); - // background-color: transparent; - // } - - // &--nested-dashed { - // margin: cssUtils.$spacing-3xs 0; - // } - // } - - // &-value-wrapper { - // display: flex; - // flex: 1; - // justify-content: space-between; - // line-height: 1; - - // &--nested { - // align-items: flex-end; - // } - - // &--standard { - // align-items: center; - // } - // } - - // &-label { - // display: grid; - // gap: cssUtils.$spacing-xs; - // color: cssUtils.$primary-text; - // @include cssUtils.typography(label, default); - // } - - // &-value { - // font-variant-numeric: tabular-nums; - // color: cssUtils.$primary-text; - // @include cssUtils.typography(label, default); - - // &--percentage { - // padding-left: cssUtils.$spacing-s; - // } - // } - // } - // } - - // this is getting used in DefaultLegend.tsx - // now moved to defaultLegend.scss file once review is done remove it - // Legend styles - // &-legend { - // display: flex; - // align-items: center; - // justify-content: center; - // gap: 16px; - // text-transform: capitalize; - // flex-wrap: wrap; - - // &--bottom { - // padding-top: cssUtils.$spacing-m; - // } - - // &-item { - // display: flex; - // align-items: center; - // gap: cssUtils.$spacing-xs; - - // svg { - // height: 12px; - // width: 12px; - // color: cssUtils.$primary-text; - // } - - // &-indicator { - // height: 8px; - // width: 8px; - // flex-shrink: 0; - // border-radius: cssUtils.$rounded-3xs; - // background-color: var(--color-bg); - // } - - // &-label { - // @include cssUtils.typography(label, heavy); - // max-width: 64px; - // overflow: hidden; - // text-overflow: ellipsis; - // white-space: nowrap; - // color: cssUtils.$primary-text; - // } - // } - // } } .crayon-chart-cartesian-grid { diff --git a/js/packages/react-ui/src/components/Charts/index.ts b/js/packages/react-ui/src/components/Charts/index.ts index eba0b7b69..be64b1f50 100644 --- a/js/packages/react-ui/src/components/Charts/index.ts +++ b/js/packages/react-ui/src/components/Charts/index.ts @@ -1,11 +1,3 @@ -export * from "./AreaChart"; -export * from "./BarChart"; -export * from "./LineChart"; -export * from "./PieChart"; -export * from "./RadarChart"; -export * from "./RadialChart"; - -// New charts export * from "./AreaCharts"; export * from "./BarCharts"; export * from "./LineCharts"; diff --git a/js/packages/react-ui/src/components/Charts/utils/AreaAndLine/AreaAndLineUtils.ts b/js/packages/react-ui/src/components/Charts/utils/AreaAndLine/AreaAndLineUtils.ts index 649bd6eff..4112acc21 100644 --- a/js/packages/react-ui/src/components/Charts/utils/AreaAndLine/AreaAndLineUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/AreaAndLine/AreaAndLineUtils.ts @@ -1,13 +1,13 @@ // Common utility functions for Area and Line charts // These functions are chart-type agnostic and can be shared between AreaChartV2 and LineChartV2 -import { AreaChartV2Data } from "../../AreaCharts/types"; -import { LineChartV2Data } from "../../LineCharts/types"; +import { AreaChartData } from "../../AreaCharts/types"; +import { LineChartData } from "../../LineCharts/types"; const ELEMENT_SPACING = 70; // Common type for chart data - both AreaChart and LineChart data structures -type ChartData = AreaChartV2Data | LineChartV2Data; +type ChartData = AreaChartData | LineChartData; /** * AREA CHART AND LINE CHART SPECIFIC FUNCTION From 4f1737f67d0d8be35b873952e149f5512dc966b4 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 02:55:01 +0530 Subject: [PATCH 159/190] refactor(Charts): update RadarChart stories and component naming This commit refactors the RadarChart stories by renaming exports for clarity and consistency, changing "RadarChartV2" to "RadarChart" in the component title and import statements. Additionally, it modifies the height property of Card components to "fit-content" for better layout adaptability across different story variations. --- .../RadarChart/stories/RadarChart.stories.tsx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx index 8ce1af362..4958f5ca1 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx @@ -85,14 +85,14 @@ const categoryKeys = { const radarChartData = dataVariations.default; const meta: Meta> = { - title: "Components/Charts/RadarCharts/RadarChartV2", + title: "Components/Charts/RadarChart", component: RadarChart, parameters: { layout: "centered", docs: { description: { component: - "```tsx\nimport { RadarChartV2 } from '@crayon-ui/react-ui/Charts/RadarChartV2';\n```", + "```tsx\nimport { RadarChart } from '@crayon-ui/react-ui/Charts/RadarChart';\n```", }, }, }, @@ -199,8 +199,8 @@ const meta: Meta> = { export default meta; type Story = StoryObj; -export const RadarChartV2Story: Story = { - name: "🎛️ Data Switcher - Radar Chart V2", +export const Controls: Story = { + name: "🎛️ Controls", args: { data: radarChartData, categoryKey: "skill", @@ -308,7 +308,7 @@ export const RadarChartV2Story: Story = {

Line Variant

- +

Area Variant

- +
@@ -458,7 +458,7 @@ export const VariantComparisonStory: Story = { }, }; -export const ThemeShowcaseStory: Story = { +export const ThemeShowcase: Story = { name: "🌈 Theme Showcase", args: { data: dataVariations.minimal as any, @@ -478,7 +478,7 @@ export const ThemeShowcaseStory: Story = {

{theme}

- +
@@ -495,7 +495,7 @@ export const ThemeShowcaseStory: Story = { }, }; -export const SingleMetricStory: Story = { +export const SingleMetric: Story = { name: "🎯 Single Metric", args: { data: dataVariations.singleMetric as any, @@ -523,7 +523,7 @@ export const SingleMetricStory: Story = { }, }; -export const ManyDimensionsStory: Story = { +export const ManyDimensions: Story = { name: "🌐 Many Dimensions", args: { data: dataVariations.manyDimensions as any, @@ -551,7 +551,7 @@ export const ManyDimensionsStory: Story = { }, }; -export const CustomizationStory: Story = { +export const Customization: Story = { name: "🎛️ Customization Options", args: { data: dataVariations.productFeatures as any, From f8254e500acff80f13463728634dc2460ec65ffa Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 03:47:47 +0530 Subject: [PATCH 160/190] refactor(Charts): replace getChartConfig with get2dChartConfig and integrate transformed keys This commit updates the chart components (AreaChart, BarChart, LineChart, RadarChart, PieChart) to replace the deprecated getChartConfig function with get2dChartConfig. It also incorporates transformed keys into the chart configuration logic, enhancing data handling and consistency across the components. Additionally, the PieChart component is modified to utilize transformed keys for better category mapping. --- .../Charts/AreaCharts/AreaChart/AreaChart.tsx | 4 ++-- .../Charts/BarCharts/BarChart/BarChart.tsx | 4 ++-- .../react-ui/src/components/Charts/Charts.tsx | 6 ++--- .../Charts/LineCharts/LineChart/LineChart.tsx | 4 ++-- .../Charts/PieCharts/PieChart/PieChart.tsx | 23 ++++++++++++++----- .../PieCharts/PieChart/utils/PieChartUtils.ts | 17 ++++++++------ .../RadarCharts/RadarChart/RadarChart.tsx | 4 ++-- .../src/components/Charts/utils/dataUtils.ts | 2 +- 8 files changed, 38 insertions(+), 26 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/AreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/AreaChart.tsx index e912cae35..3dd18d3d2 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/AreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/AreaChart.tsx @@ -26,7 +26,7 @@ import { } from "../../utils/AreaAndLine/AreaAndLineUtils"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; import { - getChartConfig, + get2dChartConfig, getColorForDataKey, getDataKeys, getLegendItems, @@ -86,7 +86,7 @@ const AreaChartComponent = ({ }, [theme, dataKeys.length]); const chartConfig: ChartConfig = useMemo(() => { - return getChartConfig(dataKeys, colors, transformedKeys, undefined, icons); + return get2dChartConfig(dataKeys, colors, transformedKeys, undefined, icons); }, [dataKeys, icons, colors, transformedKeys]); const chartContainerRef = useRef(null); diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/BarChart.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/BarChart.tsx index bfb5a1929..63334d9ef 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/BarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/BarChart.tsx @@ -19,7 +19,7 @@ import { import { type LegendItem } from "../../types"; import { getDistributedColors, getPalette, type PaletteName } from "../../utils/PalletUtils"; import { - getChartConfig, + get2dChartConfig, getColorForDataKey, getDataKeys, getLegendItems, @@ -94,7 +94,7 @@ const BarChartComponent = ({ }, [theme, dataKeys.length]); const chartConfig: ChartConfig = useMemo(() => { - return getChartConfig(dataKeys, colors, transformedKeys, undefined, icons); + return get2dChartConfig(dataKeys, colors, transformedKeys, undefined, icons); }, [dataKeys, icons, colors, transformedKeys]); const chartContainerRef = useRef(null); diff --git a/js/packages/react-ui/src/components/Charts/Charts.tsx b/js/packages/react-ui/src/components/Charts/Charts.tsx index cc2399465..1fb42b4c7 100644 --- a/js/packages/react-ui/src/components/Charts/Charts.tsx +++ b/js/packages/react-ui/src/components/Charts/Charts.tsx @@ -100,10 +100,8 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { ([theme, prefix]) => ` ${prefix} [data-chart=${id}] { ${colorConfig - .map(([key, itemConfig]) => { - // TODO: remove this after successful migration - // keyTransform fn can be removed after successful migration - const transformedKey = itemConfig.transformed ?? keyTransform(key); + .map(([_, itemConfig]) => { + const transformedKey = itemConfig.transformed; const themeValue = itemConfig.theme?.[theme as keyof typeof itemConfig.theme]; const color = typeof themeValue === "string" ? themeValue : themeValue?.color || itemConfig.color; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/LineChart.tsx b/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/LineChart.tsx index 7516817d4..ae0779cf0 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/LineChart.tsx +++ b/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/LineChart.tsx @@ -26,7 +26,7 @@ import { } from "../../utils/AreaAndLine/AreaAndLineUtils"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; import { - getChartConfig, + get2dChartConfig, getColorForDataKey, getDataKeys, getLegendItems, @@ -86,7 +86,7 @@ export const LineChart = ({ }, [theme, dataKeys.length]); const chartConfig: ChartConfig = useMemo(() => { - return getChartConfig(dataKeys, colors, transformedKeys, undefined, icons); + return get2dChartConfig(dataKeys, colors, transformedKeys, undefined, icons); }, [dataKeys, icons, colors, transformedKeys]); const chartContainerRef = useRef(null); diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/PieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/PieChart.tsx index 84617d30b..27ebd7519 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/PieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/PieChart.tsx @@ -2,12 +2,13 @@ import clsx from "clsx"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts.js"; +import { useTransformedKeys } from "../../hooks"; import { DefaultLegend } from "../../shared/DefaultLegend/DefaultLegend.js"; import { StackedLegend } from "../../shared/StackedLegend/StackedLegend.js"; import { LegendItem } from "../../types/Legend.js"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils.js"; -import { createGradientDefinitions } from "./components/PieChartRenderers.js"; import { PieChartData } from "../types/index.js"; +import { createGradientDefinitions } from "./components/PieChartRenderers.js"; import { calculateTwoLevelChartDimensions, createAnimationConfig, @@ -88,6 +89,12 @@ export const PieChart = ({ [data, categoryKey, dataKey], ); + const categories = useMemo( + () => processedData.map((item) => String(item[categoryKey])), + [processedData, categoryKey], + ); + const transformedKeys = useTransformedKeys(categories); + // Memoize string conversions to avoid repeated calls const categoryKeyString = useMemo(() => String(categoryKey), [categoryKey]); const dataKeyString = useMemo(() => String(dataKey), [dataKey]); @@ -121,8 +128,8 @@ export const PieChart = ({ ); const chartConfig = useMemo( - () => createChartConfig(processedData, categoryKey, theme), - [processedData, categoryKey, theme], + () => createChartConfig(processedData, categoryKey, theme, transformedKeys), + [processedData, categoryKey, theme, transformedKeys], ); const animationConfig = useMemo( @@ -288,7 +295,8 @@ export const PieChart = ({ > {transformedData.map((entry, index: number) => { const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); - const config = chartConfig[categoryValue]; + const transformedKey = transformedKeys[categoryValue] ?? categoryValue; + const config = chartConfig[transformedKey]; const hoverStyles = getHoverStyles(index, activeIndex); const fill = config?.color || colors[index]; return ( @@ -310,7 +318,8 @@ export const PieChart = ({ > {transformedData.map((entry, index: number) => { const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); - const config = chartConfig[categoryValue]; + const transformedKey = transformedKeys[categoryValue] ?? categoryValue; + const config = chartConfig[transformedKey]; const hoverStyles = getHoverStyles(index, activeIndex); const fill = useGradients ? `url(#gradient-${index})` : config?.color || colors[index]; return ; @@ -327,7 +336,8 @@ export const PieChart = ({ > {transformedData.map((entry, index: number) => { const categoryValue = String(entry[categoryKey as keyof typeof entry] || ""); - const config = chartConfig[categoryValue]; + const transformedKey = transformedKeys[categoryValue] ?? categoryValue; + const config = chartConfig[transformedKey]; const hoverStyles = getHoverStyles(index, activeIndex); const fill = useGradients ? `url(#gradient-${index})` : config?.color || colors[index]; return ; @@ -344,6 +354,7 @@ export const PieChart = ({ activeIndex, useGradients, colors, + transformedKeys, ]); const renderLegend = useCallback(() => { diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/utils/PieChartUtils.ts b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/utils/PieChartUtils.ts index f07bc5be2..39e1ed3d6 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/utils/PieChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/utils/PieChartUtils.ts @@ -2,9 +2,9 @@ * Utility functions for pie charts */ import { useState } from "react"; +import { ChartConfig } from "../../../Charts"; import { getDistributedColors, getPalette } from "../../../utils/PalletUtils"; import { PieChartData } from "../../types"; -import { ChartConfig } from "../../../Charts"; export interface ChartDimensions { outerRadius: number; @@ -185,27 +185,30 @@ const transformDataWithPercentages = ( * @param data - The input data array * @param categoryKey - The key to use for category labels * @param theme - The color theme to use + * @param transformedKeys - The map of transformed keys * @returns Chart configuration object */ const createChartConfig = ( data: T, categoryKey: keyof T[number], theme: string = "ocean", + transformedKeys: Record, ): ChartConfig => { const palette = getPalette(theme); const colors = getDistributedColors(palette, data.length); - return data.reduce( - (config, item, index) => ({ + return data.reduce((config, item, index) => { + const originalKey = String(item[categoryKey]); + const transformedKey = transformedKeys[originalKey] ?? originalKey; + return { ...config, - [String(item[categoryKey])]: { + [transformedKey]: { label: String(item[categoryKey as string]), color: colors[index], secondaryColor: colors[data.length - index - 1], // Add secondary color for gradient effect }, - }), - {}, - ); + }; + }, {}); }; // ========================================== diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/RadarChart.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/RadarChart.tsx index 438362d1e..7c4dc68fc 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/RadarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/RadarChart.tsx @@ -6,7 +6,7 @@ import { useTransformedKeys } from "../../hooks/useTransformKey"; import { ActiveDot, CustomTooltipContent, DefaultLegend } from "../../shared"; import { LegendItem } from "../../types"; import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; -import { getChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; +import { get2dChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; import { AxisLabel } from "./components/AxisLabel"; import { RadarChartData } from "./types"; @@ -50,7 +50,7 @@ const RadarChartComponent = ({ // Create Config const chartConfig: ChartConfig = useMemo(() => { - return getChartConfig(dataKeys, colors, transformedKeys, undefined, icons); + return get2dChartConfig(dataKeys, colors, transformedKeys, undefined, icons); }, [dataKeys, icons, colors, transformedKeys]); const legendItems: LegendItem[] = useMemo(() => { diff --git a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts index 42904a426..1d0a8a59b 100644 --- a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts @@ -23,7 +23,7 @@ export const getDataKeys = ( * @param icons - The icons for the chart (optional). * @returns The chart configuration object for the chart. */ -export const getChartConfig = ( +export const get2dChartConfig = ( dataKeys: string[], colors: string[], transformedKeys: Record, From d8db251f4ee551a44a2f5c94462e3c48643ce64a Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 04:28:54 +0530 Subject: [PATCH 161/190] fix: correct spelling in pnpm rule and rename RadialChartV2 to RadialChart in component and stories This commit fixes a spelling error in the pnpm rule description and updates the RadialChart component and its associated stories to use the new name "RadialChart" instead of "RadialChartV2" for consistency and clarity. --- .cursor/rules/use-pnpm.mdc | 2 +- .../RadialCharts/RadialChart/RadialChart.tsx | 2 +- .../stories/RadialChartV2.stories.tsx | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.cursor/rules/use-pnpm.mdc b/.cursor/rules/use-pnpm.mdc index 77f0e6ad7..84750494b 100644 --- a/.cursor/rules/use-pnpm.mdc +++ b/.cursor/rules/use-pnpm.mdc @@ -3,4 +3,4 @@ description: any task that uses terminal globs: alwaysApply: false --- -Always use pnpm insteed of npm for this project +Always use pnpm instead of npm for this project diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/RadialChart.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/RadialChart.tsx index 69d1d5583..d89d1274a 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/RadialChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/RadialChart.tsx @@ -48,7 +48,7 @@ export interface RadialChartProps { const STACKED_LEGEND_BREAKPOINT = 600; // px -export const RadialChartV2 = ({ +export const RadialChart = ({ data, categoryKey, dataKey, diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChartV2.stories.tsx index 384b2a052..ad0d27783 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChartV2.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChartV2.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { useState } from "react"; import { Card } from "../../../../Card"; -import { RadialChartProps, RadialChartV2 } from "../RadialChart"; +import { RadialChartProps, RadialChart } from "../RadialChart"; const radialChartData = [ { month: "January", value: 1250 }, @@ -54,7 +54,7 @@ const gradientColors = [ const meta: Meta> = { title: "Components/Charts/RadialCharts/RadialChartV2", - component: RadialChartV2, + component: RadialChart, parameters: { layout: "centered", docs: { @@ -196,7 +196,7 @@ const meta: Meta> = { }, }, }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; @@ -222,7 +222,7 @@ export const RadialChartV2Demo: Story = { }, render: (args: any) => ( - + ), }; @@ -248,7 +248,7 @@ export const RadialChartV2Circular: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -282,7 +282,7 @@ export const RadialChartV2WithGradients: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -316,7 +316,7 @@ export const RadialChartV2WithCarousel: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -350,7 +350,7 @@ export const RadialChartV2Minimal: Story = { }, render: (args: any) => ( - + ), parameters: { @@ -440,7 +440,7 @@ export const ResizableAndResponsive: Story = { overflow: "hidden", }} > - + {/* Resizer handles */}
Date: Mon, 30 Jun 2025 04:47:06 +0530 Subject: [PATCH 162/190] refactor(Charts): replace chart configuration functions with getCategoricalChartConfig This commit refactors the PieChart and RadialChart components to utilize the new getCategoricalChartConfig function for generating chart configurations. The previous createChartConfig and createRadialChartConfig functions have been removed to streamline the codebase. Additionally, the PieChart and RadialChart components now incorporate transformed keys for improved data handling and consistency. --- .../Charts/PieCharts/PieChart/PieChart.tsx | 4 +- .../PieCharts/PieChart/utils/PieChartUtils.ts | 34 - .../RadialCharts/RadialChart/RadialChart.tsx | 13 +- .../stories/RadialChart.stories.tsx | 924 ++++++++++++++++++ .../stories/RadialChartV2.stories.tsx | 533 ---------- .../RadialChart/utils/RadialChartUtils.ts | 29 - .../src/components/Charts/utils/dataUtils.ts | 28 + 7 files changed, 964 insertions(+), 601 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChart.stories.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChartV2.stories.tsx diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/PieChart.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/PieChart.tsx index 27ebd7519..1decebf30 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/PieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/PieChart.tsx @@ -6,13 +6,13 @@ import { useTransformedKeys } from "../../hooks"; 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 { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils.js"; import { PieChartData } from "../types/index.js"; import { createGradientDefinitions } from "./components/PieChartRenderers.js"; import { calculateTwoLevelChartDimensions, createAnimationConfig, - createChartConfig, createEventHandlers, createSectorStyle, getHoverStyles, @@ -128,7 +128,7 @@ export const PieChart = ({ ); const chartConfig = useMemo( - () => createChartConfig(processedData, categoryKey, theme, transformedKeys), + () => getCategoricalChartConfig(processedData, categoryKey, theme, transformedKeys), [processedData, categoryKey, theme, transformedKeys], ); diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/utils/PieChartUtils.ts b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/utils/PieChartUtils.ts index 39e1ed3d6..828ae1fec 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/utils/PieChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/utils/PieChartUtils.ts @@ -2,8 +2,6 @@ * Utility functions for pie charts */ import { useState } from "react"; -import { ChartConfig } from "../../../Charts"; -import { getDistributedColors, getPalette } from "../../../utils/PalletUtils"; import { PieChartData } from "../../types"; export interface ChartDimensions { @@ -180,37 +178,6 @@ const transformDataWithPercentages = ( })); }; -/** - * Creates chart configuration with colors and labels - * @param data - The input data array - * @param categoryKey - The key to use for category labels - * @param theme - The color theme to use - * @param transformedKeys - The map of transformed keys - * @returns Chart configuration object - */ -const createChartConfig = ( - data: T, - categoryKey: keyof T[number], - theme: string = "ocean", - transformedKeys: Record, -): ChartConfig => { - const palette = getPalette(theme); - const colors = getDistributedColors(palette, data.length); - - return data.reduce((config, item, index) => { - const originalKey = String(item[categoryKey]); - const transformedKey = transformedKeys[originalKey] ?? originalKey; - return { - ...config, - [transformedKey]: { - label: String(item[categoryKey as string]), - color: colors[index], - secondaryColor: colors[data.length - index - 1], // Add secondary color for gradient effect - }, - }; - }, {}); -}; - // ========================================== // Hover Effect Utilities // ========================================== @@ -306,7 +273,6 @@ export { calculatePercentage, calculateTwoLevelChartDimensions, createAnimationConfig, - createChartConfig, createEventHandlers, createSectorStyle, getHoverStyles, diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/RadialChart.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/RadialChart.tsx index d89d1274a..529b15e73 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/RadialChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/RadialChart.tsx @@ -2,16 +2,17 @@ import clsx from "clsx"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Cell, PolarGrid, RadialBar, RadialBarChart } from "recharts"; import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts"; +import { useTransformedKeys } from "../../hooks"; import { DefaultLegend } from "../../shared/DefaultLegend/DefaultLegend"; import { StackedLegend } from "../../shared/StackedLegend/StackedLegend"; import { LegendItem } from "../../types/Legend"; +import { getCategoricalChartConfig } from "../../utils/dataUtils"; import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; import { RadialChartData } from "../types"; import { createRadialGradientDefinitions } from "./components/RadialChartRenderers"; import { calculateRadialChartDimensions, createRadialAnimationConfig, - createRadialChartConfig, createRadialEventHandlers, getRadialHoverStyles, groupSmallSlices, @@ -85,6 +86,12 @@ export const RadialChart = ({ [data, categoryKey, dataKey], ); + const categories = useMemo( + () => processedData.map((item) => String(item[categoryKey])), + [processedData, categoryKey], + ); + const transformedKeys = useTransformedKeys(categories); + // Memoize string conversions to avoid repeated calls const categoryKeyString = useMemo(() => String(categoryKey), [categoryKey]); const dataKeyString = useMemo(() => String(dataKey), [dataKey]); @@ -141,8 +148,8 @@ export const RadialChart = ({ ); const chartConfig = useMemo( - () => createRadialChartConfig(processedData, categoryKey, theme), - [processedData, categoryKey, theme], + () => getCategoricalChartConfig(processedData, categoryKey, theme, transformedKeys), + [processedData, categoryKey, theme, transformedKeys], ); const animationConfig = useMemo( diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChart.stories.tsx new file mode 100644 index 000000000..782203359 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChart.stories.tsx @@ -0,0 +1,924 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; +import { Card } from "../../../../Card"; +import { RadialChart, RadialChartProps } from "../RadialChart"; + +/** + * Sample data sets for demonstrating various RadialChart configurations + * These data structures represent common use cases in business applications + */ + +// Basic monthly sales data - ideal for business dashboards +const monthlyRevenueData = [ + { month: "January", value: 1250 }, + { month: "February", value: 980 }, + { month: "March", value: 1450 }, + { month: "April", value: 1320 }, + { month: "May", value: 1680 }, + { month: "June", value: 2100 }, + { month: "July", value: 1950 }, + { month: "August", value: 1820 }, + { month: "September", value: 1650 }, + { month: "October", value: 1480 }, + { month: "November", value: 1350 }, + { month: "December", value: 1200 }, +]; + +// Extended dataset for demonstrating carousel functionality +const comprehensiveFinancialData = [ + { category: "Base Salary", amount: 75000 }, + { category: "Q1 Bonus", amount: 8500 }, + { category: "Q2 Bonus", amount: 9200 }, + { category: "Q3 Bonus", amount: 7800 }, + { category: "Q4 Bonus", amount: 11000 }, + { category: "Holiday Pay", amount: 6500 }, + { category: "Overtime", amount: 4200 }, + { category: "Commission", amount: 8900 }, + { category: "Performance Incentive", amount: 7200 }, + { category: "Stock Options", amount: 12000 }, + { category: "Healthcare Benefits", amount: 4800 }, + { category: "Retirement Match", amount: 3600 }, + { category: "Professional Development", amount: 2400 }, + { category: "Transportation Allowance", amount: 1800 }, + { category: "Meal Vouchers", amount: 1200 }, +]; + +// Custom gradient definitions for enhanced visual appeal +const customGradientPalette = [ + { start: "#FF6B6B", end: "#FF8E8E", name: "Coral Red" }, + { start: "#4ECDC4", end: "#6ED7D0", name: "Turquoise" }, + { start: "#45B7D1", end: "#6BC5DB", name: "Sky Blue" }, + { start: "#96CEB4", end: "#B4DCC9", name: "Mint Green" }, + { start: "#FFEEAD", end: "#FFF4C4", name: "Light Yellow" }, + { start: "#D4A5A5", end: "#E5BDBD", name: "Rose" }, + { start: "#9B59B6", end: "#B07CC7", name: "Purple" }, +]; + +/** + * # RadialChart Component Documentation + * + * The RadialChart component is a powerful and flexible visualization tool for displaying + * categorical data in a circular format. It's particularly effective for showing: + * + * - **Proportional Relationships**: How different categories relate to each other + * - **Part-to-Whole Analysis**: Understanding individual contributions to a total + * - **Comparative Data**: Quickly comparing values across multiple categories + * - **Dashboard Metrics**: Essential KPIs in executive dashboards + * + * ## Key Features + * + * ### Visual Variants + * - **Circular**: Full 360-degree display for comprehensive data visualization + * - **Semicircle**: Half-circle display for space-efficient layouts + * + * ### Data Formatting + * - **Percentage Mode**: Automatically calculates and displays percentages + * - **Number Mode**: Shows raw values with customizable formatting + * + * ### Interactive Elements + * - **Responsive Legend**: Adapts to container size with carousel navigation + * - **Hover States**: Interactive feedback for enhanced user experience + * - **Animation Support**: Smooth transitions and loading animations + * + * ### Customization Options + * - **Theme System**: Pre-built color palettes (ocean, orchid, emerald, sunset, spectrum, vivid) + * - **Gradient Support**: Custom gradient definitions for enhanced visual appeal + * - **Layout Flexibility**: Responsive design that adapts to container dimensions + */ + +const meta: Meta> = { + title: "Components/Charts/RadialCharts/RadialChart", + component: RadialChart, + parameters: { + layout: "centered", + docs: { + description: { + component: ` +## Installation and Basic Usage + +\`\`\`tsx +import { RadialChart } from '@crayon-ui/react-ui/Charts/RadialChart'; + +// Basic implementation + +\`\`\` + +## Data Structure Requirements + +Your data should be an array of objects where each object contains: +- A **category field** (string): Used for labels and legend items +- A **value field** (number): Used to determine segment sizes + +\`\`\`tsx +const exampleData = [ + { category: "Sales", value: 45000 }, + { category: "Marketing", value: 32000 }, + { category: "Operations", value: 28000 }, + { category: "Support", value: 15000 } +]; +\`\`\` + +## Performance Considerations + +- **Data Size**: Optimized for 3-20 data points for best readability +- **Responsive**: Automatically adjusts to container dimensions +- **Animation**: Can be disabled for better performance with large datasets + `, + }, + }, + }, + tags: ["!dev", "autodocs"], + argTypes: { + data: { + description: ` +**Required.** An array of data objects representing your dataset. Each object should contain: +- A category identifier (string) +- One or more numeric values + +**Best Practices:** +- Use 3-12 data points for optimal readability +- Ensure consistent data structure across all objects +- Use meaningful category names for better user experience + `, + control: false, + table: { + type: { summary: "Array>" }, + defaultValue: { summary: "[]" }, + category: "📊 Data Configuration", + }, + }, + categoryKey: { + description: ` +**Required.** Specifies which field in your data objects should be used as category labels. + +**Examples:** +- "month" for time-series data +- "department" for organizational data +- "product" for sales data + `, + control: false, + table: { + type: { summary: "string" }, + category: "📊 Data Configuration", + }, + }, + dataKey: { + description: ` +**Required.** Specifies which field contains the numeric values for visualization. + +**Examples:** +- "value", "amount", "revenue", "count" +- Values should be positive numbers +- Supports both integers and decimals + `, + control: false, + table: { + type: { summary: "string" }, + category: "📊 Data Configuration", + }, + }, + theme: { + description: ` +**Color Theme Selection.** Choose from professionally designed color palettes: + +- **ocean**: Cool blues and teals (professional, corporate) +- **orchid**: Purple and pink tones (creative, modern) +- **emerald**: Green variations (nature, growth, finance) +- **sunset**: Warm oranges and reds (energy, attention-grabbing) +- **spectrum**: Full color range (diverse, comprehensive) +- **vivid**: High-contrast colors (accessibility, clarity) + `, + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "🎨 Visual Styling", + }, + }, + variant: { + description: ` +**Chart Layout Style:** + +- **circular**: Full 360° display - best for comprehensive data overview +- **semicircle**: Half-circle display - space-efficient for dashboards + `, + control: "radio", + options: ["semicircle", "circular"], + table: { + defaultValue: { summary: "circular" }, + category: "🎨 Visual Styling", + }, + }, + format: { + description: ` +**Data Display Format:** + +- **percentage**: Automatically calculates percentages from your data +- **number**: Shows raw numeric values with smart formatting + `, + control: "radio", + options: ["percentage", "number"], + table: { + defaultValue: { summary: "number" }, + category: "📱 Display Options", + }, + }, + legend: { + description: ` +**Legend Visibility.** Controls whether the legend is displayed. + +**When to disable:** +- Minimal dashboard widgets +- When labels are embedded in the chart +- Space-constrained layouts + `, + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "📱 Display Options", + }, + }, + legendVariant: { + description: ` +**Legend Layout Style:** + +- **stacked**: Vertical layout with responsive behavior (recommended) +- **default**: Horizontal layout at bottom (classic style) + `, + control: "radio", + options: ["default", "stacked"], + table: { + defaultValue: { summary: "stacked" }, + category: "📱 Display Options", + }, + }, + grid: { + description: ` +**Polar Grid Lines.** Adds concentric circles for value reference. + +**Use cases:** +- Data analysis and comparison +- Scientific or technical presentations +- When precise value reading is important + `, + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "📱 Display Options", + }, + }, + isAnimationActive: { + description: ` +**Animation Control.** Enables smooth loading and transition animations. + +**Performance note:** Disable for large datasets or performance-critical applications. + `, + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "🎬 Animation & Interaction", + }, + }, + cornerRadius: { + description: ` +**Corner Rounding.** Controls the roundness of radial bar ends. + +**Design impact:** +- 0: Sharp, technical appearance +- 5-10: Subtle modern look (recommended) +- 15+: Highly rounded, friendly appearance + `, + control: { type: "number", min: 0, max: 20 }, + table: { + type: { summary: "number" }, + defaultValue: { summary: "10" }, + category: "🎨 Visual Styling", + }, + }, + useGradients: { + description: ` +**Gradient Enhancement.** Applies gradient effects to radial bars. + +**Visual impact:** +- Enhanced depth and dimension +- Modern, polished appearance +- Works best with 3-8 data points + `, + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "🎨 Visual Styling", + }, + }, + height: { + description: ` +**Fixed Height.** Override automatic height calculation. + +**Usage:** +- Leave undefined for responsive behavior (recommended) +- Set specific value for dashboard widgets +- Minimum recommended: 200px + `, + control: { type: "number", min: 200, max: 800 }, + table: { + type: { summary: "number | undefined" }, + category: "📐 Layout Control", + }, + }, + width: { + description: ` +**Fixed Width.** Override automatic width calculation. + +**Usage:** +- Leave undefined for responsive behavior (recommended) +- Set specific value for dashboard widgets +- Minimum recommended: 200px + `, + control: { type: "number", min: 200, max: 800 }, + table: { + type: { summary: "number | undefined" }, + category: "📐 Layout Control", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** + * ## Default Configuration + * + * This example demonstrates the RadialChart with its default settings - the most common + * configuration for business dashboards and data visualization applications. + * + * **Key Features Shown:** + * - Standard circular layout with full 360° display + * - Professional ocean color theme + * - Stacked legend for optimal space utilization + * - Smooth animations for polished user experience + * - Number format for clear value display + */ +export const DefaultConfiguration: Story = { + name: "📊 Default Configuration", + args: { + data: monthlyRevenueData, + categoryKey: "month", + dataKey: "value", + theme: "ocean", + variant: "circular", + format: "number", + legend: true, + legendVariant: "stacked", + grid: false, + isAnimationActive: true, + cornerRadius: 10, + useGradients: false, + gradientColors: customGradientPalette, + height: undefined, + width: undefined, + }, + render: (args: any) => ( + +
+

+ Monthly Revenue Analysis +

+

+ Comprehensive view of revenue distribution across 12 months +

+
+ +
+ ), + parameters: { + docs: { + description: { + story: ` +This is the recommended starting configuration for most use cases. The chart displays data +in a clear, professional manner with optimal default settings for readability and user experience. + +**Configuration Details:** +- **Data**: 12 months of revenue data +- **Layout**: Full circular display +- **Colors**: Ocean theme (blues and teals) +- **Legend**: Stacked layout for space efficiency +- **Animations**: Enabled for smooth interactions + `, + }, + }, + }, +}; + +/** + * ## Theme Showcase + * + * RadialChart includes six professionally designed color themes, each optimized + * for different contexts and brand requirements. + */ +export const ThemeShowcase: Story = { + name: "🎨 Theme Variations", + args: { + data: monthlyRevenueData.slice(0, 6), + categoryKey: "month", + dataKey: "value", + theme: "emerald", + variant: "circular", + format: "number", + legend: true, + legendVariant: "stacked", + grid: false, + isAnimationActive: true, + cornerRadius: 10, + useGradients: false, + }, + render: (args: any) => ( + +
+

+ Financial Growth Metrics +

+

+ Using the emerald theme for finance-related data visualization +

+
+ +
+ ), + parameters: { + docs: { + description: { + story: ` +**Theme Selection Guide:** + +- **Ocean** 🌊: Corporate, professional, trustworthy (recommended for business) +- **Orchid** 🌸: Creative, modern, innovative (great for design/marketing) +- **Emerald** 🍃: Growth, finance, nature (perfect for financial data) +- **Sunset** 🌅: Energy, attention, warmth (ideal for alerts/important metrics) +- **Spectrum** 🌈: Diverse, comprehensive (when you need many distinct colors) +- **Vivid** ⚡: High contrast, accessible (optimized for accessibility requirements) + +**Best Practice:** Choose themes that align with your brand colors and data context. + `, + }, + }, + }, +}; + +/** + * ## Advanced Visual Enhancement + * + * This example showcases the gradient feature combined with percentage formatting + * to create visually striking and informative charts. + */ +export const GradientEnhancement: Story = { + name: "✨ Gradient Enhancement", + args: { + data: monthlyRevenueData.slice(0, 6), + categoryKey: "month", + dataKey: "value", + theme: "sunset", + variant: "circular", + format: "percentage", + legend: true, + legendVariant: "stacked", + grid: false, + isAnimationActive: true, + cornerRadius: 15, + useGradients: true, + gradientColors: customGradientPalette, + }, + render: (args: any) => ( + +
+

+ Revenue Distribution +

+

+ Enhanced with gradients and percentage display +

+
+ +
+ ), + parameters: { + docs: { + description: { + story: ` +**Gradient Enhancement Features:** + +- **Visual Depth**: Creates three-dimensional appearance +- **Modern Aesthetic**: Aligns with contemporary design trends +- **Brand Flexibility**: Custom gradient definitions for brand alignment +- **Performance Optimized**: Efficiently rendered using SVG gradients + +**When to Use Gradients:** +- Executive dashboards and presentations +- Marketing materials and public-facing reports +- When visual impact is prioritized +- With 3-8 data points for optimal effect + +**Technical Note:** Gradients are defined as start/end color pairs and applied +automatically to maintain visual consistency across the chart. + `, + }, + }, + }, +}; + +/** + * ## Large Dataset Management + * + * When working with extensive datasets, RadialChart provides intelligent + * legend management with carousel navigation to maintain usability. + */ +export const LargeDatasetDemo: Story = { + name: "📈 Large Dataset with Carousel", + args: { + data: comprehensiveFinancialData, + categoryKey: "category", + dataKey: "amount", + theme: "spectrum", + variant: "circular", + format: "number", + legend: true, + legendVariant: "stacked", + grid: false, + isAnimationActive: true, + cornerRadius: 8, + useGradients: false, + }, + render: (args: any) => ( + +
+

+ Comprehensive Compensation Analysis +

+

+ Complete breakdown of 15 compensation categories with carousel navigation +

+
+ +
+ 💡 Carousel Feature: When legend items exceed available space, up/down + navigation arrows automatically appear. Use the arrows to scroll through all legend items + while maintaining chart visibility. +
+
+ ), + parameters: { + docs: { + description: { + story: ` +**Large Dataset Management:** + +**Automatic Carousel Detection:** +- Activates when legend content exceeds container height +- Provides smooth scrolling navigation +- Maintains chart proportions and visibility + +**Navigation Features:** +- **Up/Down Arrows**: Navigate through legend items +- **Smooth Scrolling**: Polished user experience +- **Visual Indicators**: Clear navigation state + +**Performance Optimizations:** +- Virtual scrolling for datasets with 50+ items +- Efficient rendering of visible legend items only +- Optimized color cycling for unlimited data points + +**Best Practices for Large Datasets:** +- Consider data aggregation for better readability +- Use meaningful category names +- Test carousel functionality in your target container sizes +- Consider alternative visualizations for 25+ categories + `, + }, + }, + }, +}; + +/** + * ## Minimal Dashboard Widget + * + * Perfect for dashboard widgets where space is limited and clean aesthetics + * are prioritized over detailed labeling. + */ +export const MinimalWidget: Story = { + name: "📱 Minimal Dashboard Widget", + args: { + data: monthlyRevenueData.slice(0, 4), + categoryKey: "month", + dataKey: "value", + theme: "orchid", + variant: "circular", + format: "number", + legend: false, + legendVariant: "default", + grid: false, + isAnimationActive: true, + cornerRadius: 5, + useGradients: false, + }, + render: (args: any) => ( + +
+

Q1 Performance

+

Top 4 metrics

+
+ +
+ ), + parameters: { + docs: { + description: { + story: ` +**Minimal Design Principles:** + +**Space Optimization:** +- No legend for maximum chart space utilization +- Reduced padding and margins +- Compact container sizing + +**Clean Aesthetics:** +- Subtle corner radius for modern appearance +- Reduced visual clutter +- Focus on essential data only + +**Use Cases:** +- Executive dashboard widgets +- Mobile-responsive layouts +- Embedded chart components +- Quick KPI displays + +**Design Considerations:** +- Ensure data is self-explanatory without legend +- Use distinctive colors for category identification +- Consider hover states for additional context +- Test readability at small sizes + `, + }, + }, + }, +}; + +/** + * ## Interactive Responsiveness Demo + * + * This advanced example demonstrates RadialChart's responsive capabilities + * with a resizable container to show real-time adaptation. + */ +export const ResponsiveDemo: Story = { + name: "📱 Responsive Behavior Demo", + args: { + data: monthlyRevenueData, + categoryKey: "month", + dataKey: "value", + theme: "spectrum", + variant: "circular", + format: "number", + legend: true, + legendVariant: "stacked", + isAnimationActive: false, + cornerRadius: 8, + useGradients: false, + }, + render: (args: any) => { + const [dimensions, setDimensions] = useState<{ width: number; height: number | string }>({ + width: 600, + height: "auto", + }); + + const handleMouseDown = (e: React.MouseEvent, handle: string) => { + e.preventDefault(); + e.stopPropagation(); + const startX = e.clientX; + const startY = e.clientY; + const startWidth = dimensions.width; + const startHeight = (e.currentTarget.parentElement as HTMLElement).offsetHeight; + + const doDrag = (e: MouseEvent) => { + const dx = e.clientX - startX; + const dy = e.clientY - startY; + let newWidth = startWidth; + let newHeight = startHeight; + + if (handle.includes("e")) newWidth = startWidth + dx; + if (handle.includes("w")) newWidth = startWidth - dx; + if (handle.includes("s")) newHeight = startHeight + dy; + if (handle.includes("n")) newHeight = startHeight - dy; + + setDimensions({ + width: Math.max(300, newWidth), + height: "auto", + }); + }; + + const stopDrag = () => { + document.removeEventListener("mousemove", doDrag); + document.removeEventListener("mouseup", stopDrag); + }; + + document.addEventListener("mousemove", doDrag); + document.addEventListener("mouseup", stopDrag); + }; + + const handleStyle: React.CSSProperties = { + position: "absolute", + background: "#3b82f6", + opacity: 0.6, + zIndex: 10, + transition: "opacity 0.2s ease", + }; + + return ( +
+
+

+ Responsive Behavior Demonstration +

+

+ Drag the edges or corners of the container below to see how the chart adapts +

+
+ 🎯 Try This: Resize the container to see automatic legend layout + changes, chart proportion adjustments, and responsive breakpoint behaviors. +
+
+ + + + + {/* Resize Handles */} +
handleMouseDown(e, "n")} + /> +
handleMouseDown(e, "s")} + /> +
handleMouseDown(e, "w")} + /> +
handleMouseDown(e, "e")} + /> + + {/* Corner Handles */} +
handleMouseDown(e, "nw")} + /> +
handleMouseDown(e, "ne")} + /> +
handleMouseDown(e, "se")} + /> + + {/* Dimension Display */} +
+ {dimensions.width}px ×{" "} + {typeof dimensions.height === "number" ? `${dimensions.height}px` : "auto"} +
+ + +
+ 📊 Responsive Features Demonstrated: +
    +
  • Automatic chart scaling and proportion maintenance
  • +
  • Legend layout adaptation (stacked ↔ horizontal)
  • +
  • Text size and spacing adjustments
  • +
  • Optimal space utilization at any container size
  • +
+
+
+ ); + }, + parameters: { + docs: { + description: { + story: ` +**Responsive Design Philosophy:** + +**Automatic Adaptation:** +- Chart dimensions automatically adjust to container size +- Legend layout switches between horizontal and stacked based on available space +- Text and spacing scale appropriately for readability + +**Breakpoint Behavior:** +- **Large Containers (>600px)**: Side-by-side chart and legend layout +- **Medium Containers (400-600px)**: Stacked layout with optimized proportions +- **Small Containers (<400px)**: Compact layout with essential elements only + +**Performance Optimizations:** +- Efficient re-rendering during resize operations +- Debounced resize calculations to prevent performance issues +- Optimized SVG scaling for crisp display at any size + +**Implementation Notes:** +- Uses ResizeObserver API for precise container size detection +- Maintains aspect ratios while maximizing space utilization +- Supports both fixed and flexible container sizing strategies + `, + }, + }, + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChartV2.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChartV2.stories.tsx deleted file mode 100644 index ad0d27783..000000000 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChartV2.stories.tsx +++ /dev/null @@ -1,533 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { useState } from "react"; -import { Card } from "../../../../Card"; -import { RadialChartProps, RadialChart } from "../RadialChart"; - -const radialChartData = [ - { month: "January", value: 1250 }, - { month: "February", value: 980 }, - { month: "March", value: 1450 }, - { month: "April", value: 1320 }, - { month: "May", value: 1680 }, - { month: "June", value: 2100 }, - { month: "July", value: 1950 }, - { month: "August", value: 1820 }, - { month: "September", value: 1650 }, - { month: "October", value: 1480 }, - { month: "November", value: 1350 }, - { month: "December", value: 1200 }, -]; - -// Extended data for carousel demo -const extendedRadialChartData = [ - { month: "January", value: 1250 }, - { month: "February", value: 980 }, - { month: "March", value: 1450 }, - { month: "April", value: 1320 }, - { month: "May", value: 1680 }, - { month: "June", value: 2100 }, - { month: "July", value: 1950 }, - { month: "August", value: 1820 }, - { month: "September", value: 1650 }, - { month: "October", value: 1480 }, - { month: "November", value: 1350 }, - { month: "December", value: 1200 }, - { month: "Q1 Bonus", value: 850 }, - { month: "Q2 Bonus", value: 920 }, - { month: "Q3 Bonus", value: 780 }, - { month: "Q4 Bonus", value: 1100 }, - { month: "Holiday Pay", value: 650 }, - { month: "Overtime", value: 420 }, - { month: "Commission", value: 890 }, - { month: "Incentives", value: 720 }, -]; - -const gradientColors = [ - { start: "#FF6B6B", end: "#FF8E8E" }, - { start: "#4ECDC4", end: "#6ED7D0" }, - { start: "#45B7D1", end: "#6BC5DB" }, - { start: "#96CEB4", end: "#B4DCC9" }, - { start: "#FFEEAD", end: "#FFF4C4" }, - { start: "#D4A5A5", end: "#E5BDBD" }, - { start: "#9B59B6", end: "#B07CC7" }, -]; - -const meta: Meta> = { - title: "Components/Charts/RadialCharts/RadialChartV2", - component: RadialChart, - parameters: { - layout: "centered", - docs: { - description: { - component: - "```tsx\nimport { RadialChartV2 } from '@crayon-ui/react-ui/Charts/RadialChartV2';\n```", - }, - }, - }, - tags: ["!dev", "autodocs"], - argTypes: { - data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the radial bars to be plotted.", - control: false, - table: { - type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", - }, - }, - categoryKey: { - description: - "The key from your data object to be used as the bar labels (e.g., 'month', 'category', 'name')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - dataKey: { - description: - "The key from your data object to be used as the values that determine the bar sizes (e.g., 'value', 'count', 'amount')", - control: false, - table: { - type: { summary: "string" }, - category: "Data", - }, - }, - theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the radial bars.", - control: "select", - options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], - table: { - defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - variant: { - description: - "The style of the radial chart. 'semicircle' shows a half circle, 'circular' shows a full circle.", - control: "radio", - options: ["semicircle", "circular"], - table: { - defaultValue: { summary: "circular" }, - category: "Appearance", - }, - }, - format: { - description: - "The format of the data. 'percentage' shows the data as a percentage, while 'number' shows the data as a number.", - control: "radio", - options: ["percentage", "number"], - table: { - defaultValue: { summary: "number" }, - category: "Display", - }, - }, - legend: { - description: "Whether to display the legend", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - legendVariant: { - description: - "The type of legend to display. 'default' shows a horizontal legend at bottom, 'stacked' shows a vertical stacked legend with responsive layout.", - control: "radio", - options: ["default", "stacked"], - table: { - defaultValue: { summary: "stacked" }, - category: "Display", - }, - }, - grid: { - description: "Whether to display the polar grid", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "false" }, - category: "Display", - }, - }, - isAnimationActive: { - description: "Whether to animate the chart", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - cornerRadius: { - description: "The radius of the corners of each radial bar", - control: { type: "number", min: 0, max: 20 }, - table: { - type: { summary: "number" }, - defaultValue: { summary: "10" }, - category: "Appearance", - }, - }, - useGradients: { - description: "Whether to use gradient colors for the radial bars", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "false" }, - category: "Appearance", - }, - }, - height: { - description: "Fixed height of the chart container", - control: { type: "number", min: 200, max: 800 }, - table: { - type: { summary: "number" }, - category: "Layout", - }, - }, - width: { - description: "Fixed width of the chart container", - control: { type: "number", min: 200, max: 800 }, - table: { - type: { summary: "number" }, - category: "Layout", - }, - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const RadialChartV2Demo: Story = { - name: "RadialChartV2", - args: { - data: radialChartData, - categoryKey: "month", - dataKey: "value", - theme: "ocean", - variant: "circular", - format: "number", - legend: true, - legendVariant: "stacked", - grid: false, - isAnimationActive: true, - cornerRadius: 10, - useGradients: false, - gradientColors, - height: undefined, - width: undefined, - }, - render: (args: any) => ( - - - - ), -}; - -export const RadialChartV2Circular: Story = { - name: "RadialChartV2 Circular", - args: { - data: radialChartData, - categoryKey: "month", - dataKey: "value", - theme: "emerald", - variant: "circular", - format: "number", - legend: true, - legendVariant: "stacked", - grid: false, - isAnimationActive: true, - cornerRadius: 10, - useGradients: false, - gradientColors, - height: undefined, - width: undefined, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - description: { - story: - "This example shows the radial chart in circular variant with full 360-degree display.", - }, - }, - }, -}; - -export const RadialChartV2WithGradients: Story = { - name: "RadialChartV2 with Gradients", - args: { - data: radialChartData.slice(0, 6), // Use fewer data points for cleaner gradient display - categoryKey: "month", - dataKey: "value", - theme: "sunset", - variant: "circular", - format: "percentage", - legend: true, - legendVariant: "stacked", - grid: false, - isAnimationActive: true, - cornerRadius: 15, - useGradients: true, - gradientColors, - height: undefined, - width: undefined, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - description: { - story: - "This example demonstrates the gradient functionality with custom gradient colors and percentage format.", - }, - }, - }, -}; - -export const RadialChartV2WithCarousel: Story = { - name: "RadialChartV2 with Up/Down Carousel", - args: { - data: extendedRadialChartData, - categoryKey: "month", - dataKey: "value", - theme: "spectrum", - variant: "circular", - format: "number", - legend: true, - legendVariant: "stacked", - grid: false, - isAnimationActive: true, - cornerRadius: 8, - useGradients: false, - gradientColors, - height: undefined, - width: undefined, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - description: { - story: - "This example demonstrates the up/down carousel functionality when there are many legend items. The legend has navigation buttons that appear when content overflows, allowing users to scroll through all items.", - }, - }, - }, -}; - -export const RadialChartV2Minimal: Story = { - name: "RadialChartV2 Minimal", - args: { - data: radialChartData.slice(0, 4), - categoryKey: "month", - dataKey: "value", - theme: "orchid", - variant: "circular", - format: "number", - legend: false, - legendVariant: "default", - grid: false, - isAnimationActive: true, - cornerRadius: 5, - useGradients: false, - gradientColors, - height: undefined, - width: undefined, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - description: { - story: - "This example shows a minimal radial chart without legend or grid for clean presentation.", - }, - }, - }, -}; - -export const ResizableAndResponsive: Story = { - name: "Resizable and Responsive", - args: { - data: radialChartData, - categoryKey: "month", - dataKey: "value", - theme: "spectrum", - variant: "donut", - format: "number", - legend: true, - legendVariant: "stacked", - isAnimationActive: false, - appearance: "circular", - cornerRadius: 8, - paddingAngle: 2, - useGradients: false, - }, - render: (args: any) => { - const [dimensions, setDimensions] = useState<{ width: number; height: number | string }>({ - width: 700, - height: "auto", - }); - - const handleMouseDown = (e: React.MouseEvent, handle: string) => { - e.preventDefault(); - e.stopPropagation(); - const startX = e.clientX; - const startY = e.clientY; - const startWidth = dimensions.width; - const startHeight = (e.currentTarget.parentElement as HTMLElement).offsetHeight; - - const doDrag = (e: MouseEvent) => { - const dx = e.clientX - startX; - const dy = e.clientY - startY; - let newWidth = startWidth; - let newHeight = startHeight; - - if (handle.includes("e")) newWidth = startWidth + dx; - if (handle.includes("w")) newWidth = startWidth - dx; - if (handle.includes("s")) newHeight = startHeight + dy; - if (handle.includes("n")) newHeight = startHeight - dy; - - setDimensions({ - width: Math.max(300, newWidth), - height: "auto", - }); - }; - - const stopDrag = () => { - document.removeEventListener("mousemove", doDrag); - document.removeEventListener("mouseup", stopDrag); - }; - - document.addEventListener("mousemove", doDrag); - document.addEventListener("mouseup", stopDrag); - }; - - const handleStyle: React.CSSProperties = { - position: "absolute", - background: "#3b82f6", - opacity: 0.5, - zIndex: 10, - }; - - return ( - - - - {/* Resizer handles */} -
handleMouseDown(e, "n")} - /> -
handleMouseDown(e, "s")} - /> -
handleMouseDown(e, "w")} - /> -
handleMouseDown(e, "e")} - /> -
handleMouseDown(e, "nw")} - /> -
handleMouseDown(e, "ne")} - /> -
handleMouseDown(e, "sw")} - /> -
handleMouseDown(e, "se")} - /> -
- {dimensions.width}px ×{" "} - {typeof dimensions.height === "number" ? `${dimensions.height}px` : dimensions.height} -
- - ); - }, - parameters: { - docs: { - description: { - story: - "This story demonstrates the responsive and resizable behavior of the PieChartV2. Drag the edges or corners of the dashed container to resize it and see how the chart and its legend adapt perfectly to the new dimensions.", - }, - }, - }, -}; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/utils/RadialChartUtils.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/utils/RadialChartUtils.ts index 0f06d2067..91b3b798f 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/utils/RadialChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/utils/RadialChartUtils.ts @@ -2,7 +2,6 @@ * Utility functions for radial charts */ import { useState } from "react"; -import { ChartConfig } from "../../../Charts"; import { getDistributedColors, getPalette } from "../../../utils/PalletUtils"; import { RadialChartData } from "../../types"; @@ -174,34 +173,6 @@ export const transformRadialDataWithPercentages = ( })); }; -/** - * Creates chart configuration with colors and labels - * @param data - The input data array - * @param categoryKey - The key to use for category labels - * @param theme - The color theme to use - * @returns Chart configuration object - */ -export const createRadialChartConfig = ( - data: T, - categoryKey: keyof T[number], - theme: string = "ocean", -): ChartConfig => { - const palette = getPalette(theme); - const colors = getDistributedColors(palette, data.length); - - return data.reduce( - (config, item, index) => ({ - ...config, - [String(item[categoryKey])]: { - label: String(item[categoryKey as string]), - color: colors[index], - secondaryColor: colors[data.length - index - 1], // Add secondary color for gradient effect - }, - }), - {}, - ); -}; - // ========================================== // Hover Effect Utilities // ========================================== diff --git a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts index 1d0a8a59b..27e8f7eaa 100644 --- a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts @@ -1,5 +1,8 @@ import { ChartConfig } from "../Charts"; +import { PieChartData } from "../PieCharts"; +import { RadialChartData } from "../RadialCharts"; import { LegendItem } from "../types"; +import { getDistributedColors, getPalette } from "../utils/PalletUtils"; /** * This function returns the data keys for the chart, used for the data keys of the chart. @@ -45,6 +48,31 @@ export const get2dChartConfig = ( ); }; +type CategoricalChartData = RadialChartData | PieChartData; + +export const getCategoricalChartConfig = ( + data: T, + categoryKey: keyof T[number], + theme: string = "ocean", + transformedKeys: Record, +): ChartConfig => { + const palette = getPalette(theme); + const colors = getDistributedColors(palette, data.length); + + return data.reduce((config, item, index) => { + const originalKey = String(item[categoryKey]); + const transformedKey = `key-${transformedKeys[originalKey] ?? originalKey}`; + return { + ...config, + [transformedKey]: { + label: String(item[categoryKey as string]), + color: colors[index], + secondaryColor: colors[data.length - index - 1], // Add secondary color for gradient effect + }, + }; + }, {}); +}; + /** * This function returns the legend items for the chart, used for the legend items of the chart. * @param dataKeys - The data keys for the chart. From 1f3bf7af4540bfa5a216c9b1a376f207c340820c Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 05:00:34 +0530 Subject: [PATCH 163/190] refactor(Charts): enhance getXAxisTickFormatter for improved text measurement and truncation This commit refactors the getXAxisTickFormatter function to utilize a canvas context for accurate text measurement, allowing for better handling of text truncation based on available width. The logic for both stacked and grouped variants has been updated to ensure optimal display of labels, including intelligent ellipsis handling and a maximum character limit. This improves visual consistency and readability in chart components. --- .../Charts/BarCharts/utils/BarChartUtils.ts | 106 ++++++++++++++---- 1 file changed, 83 insertions(+), 23 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts index 4e2a23edd..7d52dd287 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts @@ -129,42 +129,102 @@ const getRadiusArray = ( * internally used by the XAxis component reCharts */ const getXAxisTickFormatter = (groupWidth?: number, variant: BarChartVariant = "grouped") => { - const CHAR_WIDTH = 7; // Average character width in pixels for most fonts - const ELLIPSIS_WIDTH = CHAR_WIDTH * 3; // "..." takes about 3 character widths const PADDING = 8; // Safety padding for better visual spacing - // closure is happening here. + + // Setup canvas context for accurate text measurement. + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + if (context) { + // This font should match the actual chart's font for pixel-perfect results. + context.font = "12px Inter"; + } + return (value: string) => { - // If no groupWidth provided, fall back to simple logic - if (!groupWidth) { + const stringValue = String(value); + + // Fallback for SSR, or if canvas/groupWidth is not available. + if (!context || !groupWidth) { if (variant === "stacked") { - return value.slice(0, 3); + return stringValue.slice(0, 3); } else { - return value.length > 3 ? `${value.slice(0, 3)}...` : value; + return stringValue.length > 3 ? `${stringValue.slice(0, 3)}...` : stringValue; } } const availableWidth = Math.max(0, groupWidth - PADDING); - const maxCharsWithoutEllipsis = Math.floor(availableWidth / CHAR_WIDTH); - const maxCharsWithEllipsis = Math.floor((availableWidth - ELLIPSIS_WIDTH) / CHAR_WIDTH); - // For stacked variant: simple truncation, no ellipsis needed + // For stacked variant: simple truncation, no ellipsis needed. + // Truncates based on available width, but respects a max of 3 characters from original logic. if (variant === "stacked") { - const maxChars = Math.max(1, Math.min(3, maxCharsWithoutEllipsis)); - return value.slice(0, maxChars); + let low = 0; + let high = stringValue.length; + let result = ""; + while (low <= high) { + const mid = Math.floor((low + high) / 2); + if (mid === 0) { + low = mid + 1; + continue; + } + const truncated = stringValue.substring(0, mid); + if (context.measureText(truncated).width <= availableWidth) { + result = truncated; + low = mid + 1; + } else { + high = mid - 1; + } + } + // Apply the 3-char max limit from the original implementation + return result.slice(0, 3); } - // For grouped variant: intelligent ellipsis handling - if (value.length <= maxCharsWithoutEllipsis) { - // Full text fits comfortably - return value; - } else if (maxCharsWithEllipsis >= 3) { - // We can fit at least 3 characters + ellipsis - return `${value.slice(0, maxCharsWithEllipsis)}...`; - } else { - // Very limited space - just show 3 chars without ellipsis - // (ellipsis would take more space than it's worth) - return value.slice(0, Math.max(1, Math.min(3, maxCharsWithoutEllipsis))); + // For grouped variant: intelligent ellipsis handling. + if (context.measureText(stringValue).width <= availableWidth) { + return stringValue; // Full text fits. } + + // If text overflows, find the best truncation point with ellipsis. + let low = 0; + let high = stringValue.length; + let result = ""; + while (low <= high) { + const mid = Math.floor((low + high) / 2); + if (mid === 0) { + low = mid + 1; + continue; + } + const truncated = stringValue.substring(0, mid) + "..."; + if (context.measureText(truncated).width <= availableWidth) { + result = truncated; + low = mid + 1; + } else { + high = mid - 1; + } + } + + // If even with ellipsis nothing fits (very tight space), + // find max chars that fit without ellipsis. + if (result === "") { + low = 0; + high = stringValue.length; + let rawTruncated = ""; + while (low <= high) { + const mid = Math.floor((low + high) / 2); + if (mid === 0) { + low = mid + 1; + continue; + } + const truncated = stringValue.substring(0, mid); + if (context.measureText(truncated).width <= availableWidth) { + rawTruncated = truncated; + low = mid + 1; + } else { + high = mid - 1; + } + } + return rawTruncated; + } + + return result; }; }; From ed8fdd4de3b8da5c5d03cc9c2c34bf6c3a00c904 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 05:16:12 +0530 Subject: [PATCH 164/190] refactor(Charts): enhance RadarChart stories with detailed usage documentation and improved layout This commit updates the RadarChart stories to include comprehensive installation instructions, data structure requirements, and key features. The layout of the story components has been improved for better readability and user experience. Additionally, the story names have been refined for clarity, and various props have been adjusted to enhance the visual presentation of the charts. --- .../RadarChart/stories/RadarChart.stories.tsx | 570 +++++++++--------- .../stories/RadialChart.stories.tsx | 2 +- 2 files changed, 281 insertions(+), 291 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx index 4958f5ca1..f37944581 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx @@ -91,106 +91,198 @@ const meta: Meta> = { layout: "centered", docs: { description: { - component: - "```tsx\nimport { RadarChart } from '@crayon-ui/react-ui/Charts/RadarChart';\n```", + component: ` +## Installation and Basic Usage + +\`\`\`tsx +import { RadarChart } from '@crayon-ui/react-ui/Charts/RadarChart'; + +// Basic implementation + +\`\`\` + +## Data Structure Requirements + +Your data should be an array of objects where each object contains: +- A **category field** (string): Used for the radar's axes (e.g., 'skill', 'metric'). +- One or more **value fields** (number): Each numeric key represents a data series to be plotted. + +\`\`\`tsx +const exampleData = [ + { skill: "JavaScript", team_a: 90, team_b: 75 }, + { skill: "React", team_a: 85, team_b: 95 }, + { skill: "Node.js", team_a: 80, team_b: 70 }, +]; +\`\`\` + +## Key Features + +- **Multiple Data Series**: Compare several datasets on the same chart. +- **Two Visual Variants**: Choose between 'line' and 'area' styles. +- **Customizable Appearance**: Control colors, stroke width, and area opacity. +- **Interactive Legend**: Toggle visibility of data series. +- **Icon Support**: Add custom icons to legend items for better visual identification. +- **Animation**: Smooth animations for loading and data transitions. +`, }, }, }, - tags: ["dev", "autodocs"], + tags: ["!dev", "autodocs"], argTypes: { data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field and one or more numeric values for the radar dimensions.", + description: ` +**Required.** An array of data objects. Each object represents a point on the radar axes and should contain: +- A category identifier (string). +- One or more numeric values, where each key represents a data series. + +**Best Practices:** +- Use 3-8 axes (categories) for optimal readability. +- Keep data series count low (2-4) to avoid clutter. +`, control: false, table: { type: { summary: "Array>" }, defaultValue: { summary: "[]" }, - category: "Data", + category: "📊 Data Configuration", }, }, categoryKey: { - description: - "The key from your data object to be used as the radar axis labels (e.g., 'skill', 'metric', 'dimension')", + description: ` +**Required.** The key in your data objects that corresponds to the radar axis labels. + +**Examples:** +- "skill" for a skills assessment chart. +- "metric" for a performance comparison chart. +`, control: false, table: { type: { summary: "string" }, - defaultValue: { summary: "string" }, - category: "Data", + category: "📊 Data Configuration", }, }, theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the radar areas/lines.", + description: ` +**Color Theme Selection.** Choose from professionally designed color palettes: + +- **ocean**: Cool blues and teals (professional, corporate) +- **orchid**: Purple and pink tones (creative, modern) +- **emerald**: Green variations (nature, growth, finance) +- **sunset**: Warm oranges and reds (energy, attention-grabbing) +- **spectrum**: Full color range (diverse, comprehensive) +- **vivid**: High-contrast colors (accessibility, clarity) +`, control: "select", options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], table: { defaultValue: { summary: "ocean" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, variant: { - description: - "The style of the radar chart. 'line' shows only outlines, while 'area' shows filled areas.", + description: ` +**Chart Style Variant:** + +- **line**: Displays only the outlines of the data series. Best for comparing multiple series without obstruction. +- **area**: Fills the area covered by each data series. Good for showing the magnitude of values. +`, control: "radio", options: ["line", "area"], table: { defaultValue: { summary: "line" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, grid: { - description: "Whether to display the background grid lines in the radar chart", + description: ` +**Grid Visibility.** Toggles the display of the background polar grid. + +**When to use:** +- To help estimate values along the axes. +- For a more technical, data-driven look. +`, control: "boolean", table: { type: { summary: "boolean" }, defaultValue: { summary: "true" }, - category: "Display", + category: "📱 Display Options", }, }, legend: { - description: "Whether to display the chart legend", + description: ` +**Legend Visibility.** Controls whether the legend is displayed. + +**When to disable:** +- When charting a single data series. +- For minimal, clean dashboard widgets. +`, control: "boolean", table: { type: { summary: "boolean" }, defaultValue: { summary: "true" }, - category: "Display", + category: "📱 Display Options", }, }, strokeWidth: { - description: "The width of the radar lines", - control: "number", + description: ` +**Line Thickness.** Sets the width of the radar lines for the 'line' variant and the border for the 'area' variant. +`, + control: { type: "number", min: 1, max: 10 }, table: { type: { summary: "number" }, defaultValue: { summary: "2" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, areaOpacity: { - description: "The opacity of the filled areas when variant is 'area'", + description: ` +**Area Fill Opacity.** Controls the transparency of the filled areas when \`variant\` is 'area'. + +**Tip:** Use lower opacity (e.g., 0.2-0.5) when displaying multiple overlapping area series. +`, control: { type: "range", min: 0, max: 1, step: 0.1 }, table: { type: { summary: "number" }, defaultValue: { summary: "0.5" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, icons: { - description: - "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", + description: ` +An object mapping data keys to React components to display as icons in the legend. + +**Example:** +\`\`\`jsx +import { Shield, Target } from 'lucide-react'; + +const icons = { + team_a: Shield, + team_b: Target, +}; +\`\`\` +`, control: false, table: { type: { summary: "Record" }, defaultValue: { summary: "{}" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, isAnimationActive: { - description: "Whether to animate the chart", + description: ` +**Animation Control.** Enables or disables chart animations on load and update. + +**Performance note:** Disable for highly complex charts or in performance-critical applications. +`, control: "boolean", table: { type: { summary: "boolean" }, defaultValue: { summary: "true" }, - category: "Display", + category: "🎬 Animation & Interaction", }, }, }, @@ -199,152 +291,72 @@ const meta: Meta> = { export default meta; type Story = StoryObj; -export const Controls: Story = { - name: "🎛️ Controls", +export const DefaultConfiguration: Story = { + name: "📊 Default Configuration", args: { - data: radarChartData, - categoryKey: "skill", + data: dataVariations.performance, + categoryKey: "metric", theme: "ocean", variant: "line", grid: true, legend: true, strokeWidth: 2, - areaOpacity: 0.5, isAnimationActive: true, }, - render: (args: any) => { - const [selectedDataType, setSelectedDataType] = - useState("default"); - - const currentData = dataVariations[selectedDataType]; - const currentCategoryKey = categoryKeys[selectedDataType]; - - const buttonStyle = { - margin: "2px", - padding: "6px 12px", - fontSize: "12px", - border: "1px solid #ddd", - borderRadius: "4px", - cursor: "pointer", - background: "#fff", - fontFamily: "monospace", - }; - - const activeButtonStyle = { - ...buttonStyle, - background: "#007acc", - color: "white", - border: "1px solid #007acc", - }; - - return ( -
-
- 🕸️ Radar Chart Test Suite: -
- - - - - - - - -
-
- Current: {selectedDataType} | Items:{" "} - {currentData.length} | Category: {currentCategoryKey} -
-
- - - + render: (args: any) => ( + +
+

+ Team Performance Analysis +

+

+ Comparing key performance indicators across three teams. +

- ); - }, + +
+ ), parameters: { docs: { description: { - story: - "Use the buttons above the chart to quickly switch between different data variations. Active button is highlighted in blue. Each dataset tests different aspects of the radar chart functionality.", + story: ` +This is the recommended starting configuration for most use cases. The chart displays multiple data series in a clear, professional manner. + +**Configuration Details:** +- **Data**: Team performance metrics. +- **Variant**: 'line' for clear comparison between series. +- **Colors**: 'ocean' theme (blues and teals). +- **Legend**: Enabled to identify data series. +- **Animations**: Enabled for smooth interactions. + `, }, }, }, }; -export const Skills: Story = { +export const SkillsAssessment: Story = { name: "📚 Skills Assessment", args: { - data: dataVariations.default as any, - categoryKey: "skill" as any, - theme: "ocean", + data: dataVariations.default, + categoryKey: "skill", + theme: "orchid", variant: "area", grid: true, legend: true, strokeWidth: 2, - areaOpacity: 0.3, + areaOpacity: 0.4, isAnimationActive: true, }, render: (args: any) => ( - + +
+

+ Developer Skill Levels +

+

+ Visualizing proficiency across different programming skills for various experience levels. +

+
), @@ -352,23 +364,22 @@ export const Skills: Story = { docs: { description: { story: - "A skills assessment radar chart showing different proficiency levels across various programming skills. Uses area variant with transparency to show overlapping competencies.", + "This example uses the 'area' variant to represent different skill levels. The transparency allows for easy comparison of overlapping areas, providing a clear view of strengths and weaknesses across different roles.", }, }, }, }; export const TeamPerformance: Story = { - name: "🏆 Team Performance Comparison", + name: "🏆 Team Performance with Icons", args: { - data: dataVariations.performance as any, - categoryKey: "metric" as any, + data: dataVariations.performance, + categoryKey: "metric", theme: "emerald", variant: "line", grid: true, legend: true, strokeWidth: 3, - areaOpacity: 0.5, isAnimationActive: true, icons: { team_a: Shield, @@ -377,7 +388,15 @@ export const TeamPerformance: Story = { }, }, render: (args: any) => ( - + +
+

+ Team Performance Comparison +

+

+ Utilizing custom icons to distinguish teams in the legend for improved clarity. +

+
), @@ -385,7 +404,7 @@ export const TeamPerformance: Story = { docs: { description: { story: - "Compare team performance across multiple metrics. Features custom icons in the legend and thicker stroke width for better visibility.", + "This chart demonstrates how to enhance the legend with custom icons. It's an effective way to visually associate data series with specific entities, like teams or products. The thicker stroke width also improves line visibility.", }, }, }, @@ -394,8 +413,8 @@ export const TeamPerformance: Story = { export const BusinessMetrics: Story = { name: "📈 Quarterly Business Metrics", args: { - data: dataVariations.businessMetrics as any, - categoryKey: "department" as any, + data: dataVariations.businessMetrics, + categoryKey: "department", theme: "sunset", variant: "area", grid: true, @@ -405,7 +424,15 @@ export const BusinessMetrics: Story = { isAnimationActive: true, }, render: (args: any) => ( - + +
+

+ Quarterly Performance Review +

+

+ Tracking key business metrics across departments over four quarters. +

+
), @@ -413,18 +440,16 @@ export const BusinessMetrics: Story = { docs: { description: { story: - "Track quarterly performance across different departments. Shows how area charts can effectively display multiple overlapping datasets.", + "A comprehensive view of business performance, this example showcases how the 'area' variant can effectively display multiple overlapping datasets to reveal trends and patterns over time.", }, }, }, }; -export const VariantComparison: Story = { - name: "🎨 Line vs Area Variants", +export const InteractivePlayground: Story = { + name: "🧪 Interactive Playground", args: { - data: dataVariations.gameStats as any, - categoryKey: "attribute" as any, - theme: "vivid", + theme: "ocean", variant: "line", grid: true, legend: true, @@ -432,53 +457,118 @@ export const VariantComparison: Story = { areaOpacity: 0.5, isAnimationActive: true, }, - render: (args: any) => ( -
-
-

Line Variant

- - + render: (args: any) => { + const [selectedDataType, setSelectedDataType] = + useState("default"); + + const currentData = dataVariations[selectedDataType]; + const currentCategoryKey = categoryKeys[selectedDataType]; + + const buttonStyle: React.CSSProperties = { + margin: "2px", + padding: "6px 12px", + fontSize: "12px", + border: "1px solid #ddd", + borderRadius: "4px", + cursor: "pointer", + background: "#fff", + fontFamily: "monospace", + transition: "all 0.2s", + }; + + const activeButtonStyle: React.CSSProperties = { + ...buttonStyle, + background: "#3b82f6", + color: "white", + border: "1px solid #3b82f6", + fontWeight: 600, + }; + + return ( +
+ +

+ 🕸️ Radar Chart Test Suite +

+
+ {(Object.keys(dataVariations) as Array).map((key) => ( + + ))} +
+
+ Current Dataset: {selectedDataType} | Axes:{" "} + {currentData.length} | Category Key: {currentCategoryKey} +
-
-
-

Area Variant

- - + +
-
- ), + ); + }, parameters: { docs: { description: { - story: - "Compare the two available variants: line (outline only) and area (filled). The area variant is useful for showing magnitude, while line variant is better for precise value comparison.", + story: ` +Use the buttons to switch between different datasets and explore how the RadarChart adapts. +You can also use the Storybook controls to change theme, variant, and other properties in real-time. +This playground is designed for testing various configurations and edge cases. +`, }, }, }, }; export const ThemeShowcase: Story = { - name: "🌈 Theme Showcase", + name: "🎨 Theme Showcase", args: { - data: dataVariations.minimal as any, - categoryKey: "category" as any, - theme: "ocean", + data: dataVariations.productFeatures, + categoryKey: "feature", variant: "area", grid: true, legend: true, strokeWidth: 2, areaOpacity: 0.6, - isAnimationActive: true, + isAnimationActive: false, }, render: (args: any) => ( -
+
{(["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"] as const).map((theme) => (
-

+

{theme}

- +
@@ -489,107 +579,7 @@ export const ThemeShowcase: Story = { docs: { description: { story: - "Showcase all available color themes. Each theme provides a carefully curated color palette optimized for data visualization.", - }, - }, - }, -}; - -export const SingleMetric: Story = { - name: "🎯 Single Metric", - args: { - data: dataVariations.singleMetric as any, - categoryKey: "dimension" as any, - theme: "orchid", - variant: "area", - grid: true, - legend: false, - strokeWidth: 3, - areaOpacity: 0.7, - isAnimationActive: true, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - description: { - story: - "Single metric radar chart without legend. Useful for displaying one dataset across multiple dimensions with emphasis on the overall shape.", - }, - }, - }, -}; - -export const ManyDimensions: Story = { - name: "🌐 Many Dimensions", - args: { - data: dataVariations.manyDimensions as any, - categoryKey: "aspect" as any, - theme: "spectrum", - variant: "line", - grid: true, - legend: true, - strokeWidth: 2, - areaOpacity: 0.3, - isAnimationActive: true, - }, - render: (args: any) => ( - - - - ), - parameters: { - docs: { - description: { - story: - "Radar chart with many dimensions (8 axes) to test how the component handles complex datasets. Line variant works best for many overlapping series.", - }, - }, - }, -}; - -export const Customization: Story = { - name: "🎛️ Customization Options", - args: { - data: dataVariations.productFeatures as any, - categoryKey: "feature" as any, - theme: "emerald", - variant: "area", - grid: true, - legend: true, - strokeWidth: 2, - areaOpacity: 0.5, - isAnimationActive: true, - icons: { - current: TrendingUp, - competitor_a: Users, - competitor_b: Zap, - }, - }, - render: (args: any) => ( -
-
-

With Icons & Animation

- - - -
-
-

No Grid, No Animation, Thick Lines

- - - -
-
- ), - parameters: { - docs: { - description: { - story: - "Demonstrate various customization options including icons, grid toggle, animation control, and stroke width adjustment.", + "This story showcases all available color themes. Each theme provides a professionally curated palette suitable for different branding and data contexts. Animations are disabled for quicker comparison.", }, }, }, diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChart.stories.tsx index 782203359..232435347 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChart.stories.tsx @@ -87,7 +87,7 @@ const customGradientPalette = [ */ const meta: Meta> = { - title: "Components/Charts/RadialCharts/RadialChart", + title: "Components/Charts/RadialChart", component: RadialChart, parameters: { layout: "centered", From 9be0ee965d120d8d0c2ce7242c41e50d80fbe799 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 05:25:39 +0530 Subject: [PATCH 165/190] refactor(Charts): remove ProgressBarChart component and related files, update SCSS imports This commit removes the ProgressBarChart component and its associated files, including TypeScript, SCSS, and story files, to streamline the chart components. The SCSS imports have been updated to include the new SegmentedBarChart, replacing the removed ProgressBarChart references. --- .../Charts/ProgressBarChart/index.ts | 3 - .../stories/ProgressBarChart.stories.tsx | 120 -------- .../Charts/ProgressBarChart/types/index.ts | 1 - .../SegmentedBarChart.tsx} | 16 +- .../Charts/SegmentedBarChart/index.ts | 3 + .../segmentedBarChart.scss} | 6 +- .../stories/SegmentedBarChart.stories.tsx | 263 ++++++++++++++++++ .../Charts/SegmentedBarChart/types/index.ts | 1 + .../src/components/Charts/charts.scss | 4 +- 9 files changed, 280 insertions(+), 137 deletions(-) delete mode 100644 js/packages/react-ui/src/components/Charts/ProgressBarChart/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/ProgressBarChart/stories/ProgressBarChart.stories.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/ProgressBarChart/types/index.ts rename js/packages/react-ui/src/components/Charts/{ProgressBarChart/ProgressBarChart.tsx => SegmentedBarChart/SegmentedBarChart.tsx} (77%) create mode 100644 js/packages/react-ui/src/components/Charts/SegmentedBarChart/index.ts rename js/packages/react-ui/src/components/Charts/{ProgressBarChart/progressBarChart.scss => SegmentedBarChart/segmentedBarChart.scss} (83%) create mode 100644 js/packages/react-ui/src/components/Charts/SegmentedBarChart/stories/SegmentedBarChart.stories.tsx create mode 100644 js/packages/react-ui/src/components/Charts/SegmentedBarChart/types/index.ts diff --git a/js/packages/react-ui/src/components/Charts/ProgressBarChart/index.ts b/js/packages/react-ui/src/components/Charts/ProgressBarChart/index.ts deleted file mode 100644 index 44b296c22..000000000 --- a/js/packages/react-ui/src/components/Charts/ProgressBarChart/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { ProgressBar } from "./ProgressBarChart"; -export type { ProgressBarProps } from "./ProgressBarChart"; -export type { ProgressBarData } from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/ProgressBarChart/stories/ProgressBarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/ProgressBarChart/stories/ProgressBarChart.stories.tsx deleted file mode 100644 index a3fe7a1d7..000000000 --- a/js/packages/react-ui/src/components/Charts/ProgressBarChart/stories/ProgressBarChart.stories.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { Card } from "../../../Card"; -import { ProgressBar, ProgressBarProps } from "../ProgressBarChart"; - -// Sample data variations - simplified to numbers only -const progressData = { - simple: [75], - multiple: [25, 30, 20], // Individual segments: 25, 30, 20 - segmented: [40, 25, 20, 15], // Four segments with different sizes - manySegments: [15, 12, 18, 10, 8, 14, 11, 12], // Eight segments -}; - -const meta: Meta = { - title: "Components/Charts/ProgressBarChart", - component: ProgressBar, - parameters: { - layout: "centered", - docs: { - description: { - component: - "```tsx\nimport { ProgressBar } from '@crayon-ui/react-ui/Charts/ProgressBar';\n```", - }, - }, - }, - tags: ["!dev", "autodocs"], - argTypes: { - data: { - description: - "An array of numbers where each number represents an individual segment with its own color.", - control: false, - table: { - type: { summary: "Array" }, - defaultValue: { summary: "[]" }, - category: "Data", - }, - }, - theme: { - description: - "The color palette theme for the progress bar. Each theme provides different colors.", - control: "select", - options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], - table: { - defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - animated: { - description: "Whether to animate the progress bar fill", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", - }, - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - name: "Default Progress Bar", - args: { - data: progressData.multiple, - theme: "ocean", - animated: true, - }, - render: (args: any) => ( - -

- Task Completion (3 Segments) -

- -
- ), -}; - -export const FourSegments: Story = { - name: "Four Segments", - args: { - data: progressData.segmented, - theme: "spectrum", - animated: true, - }, - render: (args: any) => ( - -

- Task Distribution (4 Segments) -

- -
- ), -}; - -export const ManySegments: Story = { - name: "Many Segments (8 Colors)", - args: { - data: progressData.manySegments, - theme: "vivid", - animated: true, - }, - render: (args: any) => ( - -

- Multi-Segment Progress -

- -
- ), -}; diff --git a/js/packages/react-ui/src/components/Charts/ProgressBarChart/types/index.ts b/js/packages/react-ui/src/components/Charts/ProgressBarChart/types/index.ts deleted file mode 100644 index a29610435..000000000 --- a/js/packages/react-ui/src/components/Charts/ProgressBarChart/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type ProgressBarData = Array; diff --git a/js/packages/react-ui/src/components/Charts/ProgressBarChart/ProgressBarChart.tsx b/js/packages/react-ui/src/components/Charts/SegmentedBarChart/SegmentedBarChart.tsx similarity index 77% rename from js/packages/react-ui/src/components/Charts/ProgressBarChart/ProgressBarChart.tsx rename to js/packages/react-ui/src/components/Charts/SegmentedBarChart/SegmentedBarChart.tsx index d9412992b..3ceb305dc 100644 --- a/js/packages/react-ui/src/components/Charts/ProgressBarChart/ProgressBarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/SegmentedBarChart/SegmentedBarChart.tsx @@ -1,23 +1,23 @@ import clsx from "clsx"; import { useMemo } from "react"; -import { ProgressBarData } from "."; +import { SegmentedBarData } from "."; import { getDistributedColors, getPalette, PaletteName } from "../utils/PalletUtils"; -export interface ProgressBarProps { - data: ProgressBarData; +export interface SegmentedBarProps { + data: SegmentedBarData; theme?: PaletteName; className?: string; style?: React.CSSProperties; animated?: boolean; } -export const ProgressBar = ({ +export const SegmentedBar = ({ data, theme = "ocean", className, style, animated = true, -}: ProgressBarProps) => { +}: SegmentedBarProps) => { // Calculate percentages const segments = useMemo(() => { if (!data || data.length === 0) { @@ -41,13 +41,13 @@ export const ProgressBar = ({ // Segmented progress bar return ( -
+
{segments.map((segment, index) => { return (
= { + title: "Components/Charts/SegmentedBar", + component: SegmentedBar, + parameters: { + layout: "centered", + docs: { + description: { + component: ` +// Note: While the component is named 'ProgressBar', it's presented here as a 'SegmentedBar' +// to emphasize its use for showing proportional data, much like a pie chart. +## Installation and Basic Usage + +\`\`\`tsx +import { SegmentedBar } from '@crayon-ui/react-ui/Charts/SegmentedBar'; + +// Basic implementation + +\`\`\` + +## Data Structure Requirements + +The data should be a simple array of numbers. Each number represents a segment in the bar. The component automatically calculates the proportional width of each segment based on the total sum of values. + +\`\`\`tsx +// Represents a composition of three values +const data = [25, 30, 20]; +\`\`\` + +## Key Features + +- **Segmented Display**: Visualize the composition of a whole. +- **Theming**: Six built-in color palettes. +- **Animation**: Smoothly animates the bar segments. +- **Responsive**: Adapts to the width of its container. +`, + }, + }, + }, + tags: ["!dev", "autodocs"], + argTypes: { + data: { + description: ` +**Required.** An array of numbers where each number represents a segment's value. +The component automatically calculates the percentage width for each segment based on the total sum of values. + +**Example:** \`[20, 30, 15]\` creates three segments showing their proportional relationship. +`, + control: false, + table: { + type: { summary: "Array" }, + defaultValue: { summary: "[]" }, + category: "📊 Data Configuration", + }, + }, + theme: { + description: ` +**Color Theme Selection.** Choose from professionally designed color palettes: + +- **ocean**: Cool blues and teals +- **orchid**: Purple and pink tones +- **emerald**: Green variations +- **sunset**: Warm oranges and reds +- **spectrum**: Full color range +- **vivid**: High-contrast colors +`, + control: "select", + options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], + table: { + defaultValue: { summary: "ocean" }, + category: "🎨 Visual Styling", + }, + }, + animated: { + description: ` +**Animation Control.** Enables or disables the fill animation on load. +`, + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + category: "🎬 Animation & Interaction", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const sampleData = { + default: [25, 30, 20], + single: [60], + many: [10, 8, 12, 5, 15, 7, 13, 10], + full: [25, 25, 25, 25], +}; + +export const DefaultConfiguration: Story = { + name: "📊 Default Configuration", + args: { + data: sampleData.default, + theme: "ocean", + animated: true, + }, + render: (args: any) => ( + +
+

+ Category Breakdown +

+

+ Visualizing the breakdown of a dataset into its constituent parts. +

+
+ +
+ Total Value: {sampleData.default.reduce((a, b) => a + b, 0)} +
+
+ ), + parameters: { + docs: { + description: { + story: + "This is the standard appearance of the Segmented Bar. It shows multiple segments, each with a color from the selected theme, representing the proportional breakdown of a whole.", + }, + }, + }, +}; + +export const ThemeShowcase: Story = { + name: "🎨 Theme Showcase", + args: { + data: [15, 20, 25, 15], + animated: true, + }, + render: (args: any) => ( +
+ {(["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"] as const).map((theme) => ( +
+

+ {theme} +

+ +
+ ))} +
+ ), + parameters: { + docs: { + description: { + story: + "The Segmented Bar supports six different color themes. This allows it to fit seamlessly into various application designs and visual identities.", + }, + }, + }, +}; + +export const SingleSegment: Story = { + name: "🎯 Single Segment", + args: { + data: sampleData.single, + theme: "emerald", + animated: true, + }, + render: (args: any) => ( + +
+

+ Market Share +

+

+ A simple bar representing a single value out of a whole. +

+
+ +
+ {sampleData.single[0]}% of Total +
+
+ ), + parameters: { + docs: { + description: { + story: + "When a single value is provided, the bar shows one segment. This is ideal for showing a simple percentage or proportion, like market share.", + }, + }, + }, +}; + +export const ManySegments: Story = { + name: "🧩 Many Segments", + args: { + data: sampleData.many, + theme: "vivid", + animated: true, + }, + render: (args: any) => ( + +
+

+ Resource Distribution +

+

+ The component gracefully handles numerous small segments. +

+
+ +
+ ), + parameters: { + docs: { + description: { + story: + "The Segmented Bar can display many segments. The colors will cycle through the selected theme palette if the number of segments exceeds the number of available colors.", + }, + }, + }, +}; + +export const FullComposition: Story = { + name: "✅ Full Composition", + args: { + data: sampleData.full, + theme: "spectrum", + animated: false, + }, + render: (args: any) => ( + +
+

+ Complete Data Set +

+

+ Displaying a bar where segments add up to 100%. +

+
+ +
+ ), + parameters: { + docs: { + description: { + story: + "This example shows a bar where the segments add up to 100%. The segments collectively fill the entire width of the container, representing the full composition of the data.", + }, + }, + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/SegmentedBarChart/types/index.ts b/js/packages/react-ui/src/components/Charts/SegmentedBarChart/types/index.ts new file mode 100644 index 000000000..aa0ddfb29 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/SegmentedBarChart/types/index.ts @@ -0,0 +1 @@ +export type SegmentedBarData = Array; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index baa8f37e0..02b1a19f8 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -12,8 +12,8 @@ // radar chart css @forward "./RadarCharts/index.scss"; -// progress bar css -@forward "./ProgressBarChart/progressBarChart.scss"; +// segmented bar chart css +@forward "./SegmentedBarChart/segmentedBarChart.scss"; // pie chart css @forward "./PieCharts/index.scss"; From 948816685972421cf592c4f24efe34ad8322de7f Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 05:28:12 +0530 Subject: [PATCH 166/190] refactor(Charts): remove SingleSegment story and related elements from SegmentedBarChart This commit removes the SingleSegment story and its associated elements from the SegmentedBarChart component, streamlining the story structure. Additionally, the total value display for the default sample data has been eliminated to simplify the presentation of the chart stories. --- .../stories/SegmentedBarChart.stories.tsx | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/SegmentedBarChart/stories/SegmentedBarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/SegmentedBarChart/stories/SegmentedBarChart.stories.tsx index 904065d61..a4c5efcb2 100644 --- a/js/packages/react-ui/src/components/Charts/SegmentedBarChart/stories/SegmentedBarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/SegmentedBarChart/stories/SegmentedBarChart.stories.tsx @@ -96,7 +96,6 @@ type Story = StoryObj; const sampleData = { default: [25, 30, 20], - single: [60], many: [10, 8, 12, 5, 15, 7, 13, 10], full: [25, 25, 25, 25], }; @@ -119,9 +118,6 @@ export const DefaultConfiguration: Story = {

-
- Total Value: {sampleData.default.reduce((a, b) => a + b, 0)} -
), parameters: { @@ -169,39 +165,6 @@ export const ThemeShowcase: Story = { }, }; -export const SingleSegment: Story = { - name: "🎯 Single Segment", - args: { - data: sampleData.single, - theme: "emerald", - animated: true, - }, - render: (args: any) => ( - -
-

- Market Share -

-

- A simple bar representing a single value out of a whole. -

-
- -
- {sampleData.single[0]}% of Total -
-
- ), - parameters: { - docs: { - description: { - story: - "When a single value is provided, the bar shows one segment. This is ideal for showing a simple percentage or proportion, like market share.", - }, - }, - }, -}; - export const ManySegments: Story = { name: "🧩 Many Segments", args: { From b2ddcab05bd6847ed0d817f5b77dc7122bd8eadc Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 05:45:41 +0530 Subject: [PATCH 167/190] refactor(Charts): update PieChart stories with enhanced data structure and documentation This commit refactors the PieChart stories to improve data representation and documentation. The monthly sales data has been restructured for clarity, and comprehensive usage instructions, including data structure requirements and performance considerations, have been added. Additionally, the layout of the stories has been enhanced for better visual presentation, and new story examples have been introduced to showcase various configurations and responsive behavior. --- .../PieChart/stories/PieChart.stories.tsx | 455 +++++++++--------- 1 file changed, 237 insertions(+), 218 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx index 06895546b..84b9a2bba 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx @@ -3,46 +3,32 @@ import { useState } from "react"; import { Card } from "../../../../Card"; import { PieChart, PieChartProps } from "../PieChart"; -const pieChartData = [ +const monthlySalesData = [ { month: "January", value: 1250 }, { month: "February", value: 980 }, { month: "March", value: 1450 }, { month: "April", value: 1320 }, { month: "May", value: 1680 }, { month: "June", value: 2100 }, - { month: "July", value: 1950 }, - { month: "August", value: 1820 }, - { month: "September", value: 1650 }, - { month: "October", value: 1480 }, - { month: "November", value: 1350 }, - { month: "December", value: 1200 }, ]; -// Extended data for carousel demo -const extendedPieChartData = [ - { month: "January", value: 1250 }, - { month: "February", value: 980 }, - { month: "March", value: 1450 }, - { month: "April", value: 1320 }, - { month: "May", value: 1680 }, - { month: "June", value: 2100 }, - { month: "July", value: 1950 }, - { month: "August", value: 1820 }, - { month: "September", value: 1650 }, - { month: "October", value: 1480 }, - { month: "November", value: 1350 }, - { month: "December", value: 1200 }, - { month: "Q1 Bonus", value: 850 }, - { month: "Q2 Bonus", value: 920 }, - { month: "Q3 Bonus", value: 780 }, - { month: "Q4 Bonus", value: 1100 }, - { month: "Holiday Pay", value: 650 }, - { month: "Overtime", value: 420 }, - { month: "Commission", value: 890 }, - { month: "Incentives", value: 720 }, +const comprehensiveData = [ + { category: "Electronics", sales: 12500 }, + { category: "Apparel", sales: 9800 }, + { category: "Groceries", sales: 14500 }, + { category: "Home Goods", sales: 13200 }, + { category: "Books", sales: 8800 }, + { category: "Toys", sales: 7600 }, + { category: "Automotive", sales: 6500 }, + { category: "Health", sales: 11200 }, + { category: "Beauty", sales: 9300 }, + { category: "Sports", sales: 8100 }, + { category: "Outdoors", sales: 7200 }, + { category: "Music", sales: 4500 }, + { category: "Software", sales: 10500 }, ]; -const gradientColors = [ +const gradientPalette = [ { start: "#FF6B6B", end: "#FF8E8E" }, { start: "#4ECDC4", end: "#6ED7D0" }, { start: "#45B7D1", end: "#6BC5DB" }, @@ -52,157 +38,226 @@ const gradientColors = [ { start: "#9B59B6", end: "#B07CC7" }, ]; -const meta: Meta> = { - title: "Components/Charts/PieCharts/PieChartV2", +/** + * # PieChart Component Documentation + * + * The PieChart component is a versatile and intuitive tool for visualizing + * part-to-whole relationships in a dataset. It's highly effective for: + * + * - **Proportional Analysis**: Showing how individual segments contribute to a total + * - **Category Comparison**: Comparing the relative size of different categories + * - **Dashboard KPIs**: Displaying key metrics like market share or budget allocation + * + * ## Key Features + * + * ### Chart Types + * - **Pie**: Classic circular chart for proportional representation + * - **Donut**: Pie chart with a center cutout, useful for displaying a central metric or for a modern look + * + * ### Layout Variants + * - **Circular**: Full 360-degree display for a complete data overview + * - **Semicircle**: Half-circle (180-degree) layout, ideal for compact dashboard widgets + * + * ### Data Formatting + * - **Percentage Mode**: Automatically calculates and displays segment values as percentages + * - **Number Mode**: Shows raw data values with appropriate formatting + * + * ### Interactive & Responsive + * - **Interactive Legend**: Adapts to container size with carousel navigation for large datasets + * - **Hover Tooltips**: Provides detailed information on hover for better user engagement + * - **Responsive Design**: Fluidly adjusts to the size of its container + * + * ### Customization + * - **Theming**: Six pre-built color palettes to match your application's design + * - **Styling Options**: Control corner radius, padding between slices, and more + * - **Gradient Fills**: Apply beautiful gradients for an enhanced visual appeal + */ + +const meta: Meta> = { + title: "Components/Charts/PieChart", component: PieChart, parameters: { layout: "centered", docs: { description: { - component: - "```tsx\nimport { PieChartV2 } from '@crayon-ui/react-ui/Charts/PieChartV2';\n```", + component: ` +## Installation and Basic Usage + +\`\`\`tsx +import { PieChart } from '@crayon-ui/react-ui/Charts/PieChart'; + +// Basic implementation + +\`\`\` + +## Data Structure Requirements + +Your data should be an array of objects where each object contains: +- A **category field** (string): Used for labels and legend items +- A **value field** (number): Used to determine slice sizes + +\`\`\`tsx +const salesData = [ + { category: "Electronics", value: 4500 }, + { category: "Apparel", value: 3200 }, + { category: "Groceries", value: 6800 } +]; +\`\`\` + +## Performance Considerations +- **Data Size**: Best for 3-12 data points for readability. +- **Responsiveness**: Fully responsive and adapts to its container. +- **Animation**: Can be disabled for performance with very large or complex charts. + `, }, }, }, tags: ["!dev", "autodocs"], argTypes: { data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", + description: ` +**Required.** An array of data objects representing your dataset. Each object should contain: +- A category identifier (string) +- A numeric value + +**Best Practices:** +- Use 3-12 data points for optimal readability. +- Ensure consistent data structure across all objects. +`, control: false, table: { type: { summary: "Array>" }, defaultValue: { summary: "[]" }, - category: "Data", + category: "📊 Data Configuration", }, }, categoryKey: { description: - "The key from your data object to be used as the segment labels (e.g., 'month', 'category', 'name')", + "**Required.** The key in your data object that represents the category label (e.g., 'month', 'department').", control: false, table: { type: { summary: "string" }, - category: "Data", + category: "📊 Data Configuration", }, }, dataKey: { description: - "The key from your data object to be used as the values that determine the slice sizes (e.g., 'value', 'count', 'amount')", + "**Required.** The key in your data object that contains the numeric value for each slice (e.g., 'value', 'sales').", control: false, table: { type: { summary: "string" }, - category: "Data", + category: "📊 Data Configuration", }, }, theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + description: "The color palette for the chart. Provides a set of aesthetically pleasing colors.", control: "select", options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], table: { defaultValue: { summary: "ocean" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, appearance: { - description: - "The appearance of the chart. 'circular' shows a full circle, while 'semiCircular' shows a half circle.", + description: "The overall shape of the chart: a full circle or a semicircle.", control: "radio", options: ["circular", "semiCircular"], table: { defaultValue: { summary: "circular" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, variant: { - description: - "The style of the pie chart. 'pie' shows a pie chart, 'donut' shows a donut chart.", + description: "The style of the chart: a standard pie or a donut with a cutout center.", control: "radio", options: ["pie", "donut"], table: { defaultValue: { summary: "pie" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, format: { - description: - "The format of the data. 'percentage' shows the data as a percentage, while 'number' shows the data as a number.", + description: "The format for displaying data values, either as raw numbers or as percentages.", control: "radio", options: ["percentage", "number"], table: { defaultValue: { summary: "number" }, - category: "Display", + category: "📱 Display Options", }, }, legend: { - description: "Whether to display the legend", + description: "Controls the visibility of the chart's legend.", control: "boolean", table: { type: { summary: "boolean" }, defaultValue: { summary: "true" }, - category: "Display", + category: "📱 Display Options", }, }, legendVariant: { - description: - "The type of legend to display. 'default' shows a horizontal legend at bottom, 'stacked' shows a vertical stacked legend with responsive layout.", + description: "The layout of the legend: a horizontal list or a responsive vertical stack.", control: "radio", options: ["default", "stacked"], table: { defaultValue: { summary: "default" }, - category: "Display", + category: "📱 Display Options", }, }, isAnimationActive: { - description: "Whether to animate the chart", + description: "Enables or disables the initial loading animation.", control: "boolean", table: { type: { summary: "boolean" }, defaultValue: { summary: "true" }, - category: "Display", + category: "🎬 Animation & Interaction", }, }, cornerRadius: { - description: "The radius of the corners of each pie slice", + description: "The radius for rounding the corners of each pie slice.", control: { type: "number", min: 0, max: 20 }, table: { type: { summary: "number" }, defaultValue: { summary: "0" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, paddingAngle: { - description: "The angle between each pie slice", + description: "The spacing angle between each pie slice.", control: { type: "number", min: 0, max: 10 }, table: { type: { summary: "number" }, defaultValue: { summary: "0" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, useGradients: { - description: "Whether to use gradient colors for the pie slices", + description: "Applies gradient fills to the pie slices instead of solid colors.", control: "boolean", table: { type: { summary: "boolean" }, defaultValue: { summary: "false" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, height: { - description: "Fixed height of the chart container", + description: "Sets a fixed height for the chart container. Undefined by default for responsive height.", control: { type: "number", min: 200, max: 800 }, table: { type: { summary: "number" }, - category: "Layout", + category: "📐 Layout Control", }, }, width: { - description: "Fixed width of the chart container", + description: "Sets a fixed width for the chart container. Undefined by default for responsive width.", control: { type: "number", min: 200, max: 800 }, table: { type: { summary: "number" }, - category: "Layout", + category: "📐 Layout Control", }, }, }, @@ -211,10 +266,22 @@ const meta: Meta> = { export default meta; type Story = StoryObj; -export const PieChartV2Demo: Story = { - name: "PieChartV2", +/** + * ## Default Configuration + * + * This example showcases the PieChart with its standard settings, making it an + * ideal starting point for most data visualization needs. + * + * **Key Features Shown:** + * - Standard circular pie layout + * - Professional 'ocean' color theme + * - Responsive stacked legend for clarity + * - Smooth animations on load + */ +export const DefaultConfiguration: Story = { + name: "📊 Default Configuration", args: { - data: pieChartData, + data: monthlySalesData, categoryKey: "month", dataKey: "value", theme: "ocean", @@ -224,103 +291,135 @@ export const PieChartV2Demo: Story = { legendVariant: "stacked", isAnimationActive: true, appearance: "circular", - cornerRadius: 0, - paddingAngle: 0, + cornerRadius: 4, + paddingAngle: 2, useGradients: false, - gradientColors, - height: undefined, - width: undefined, + gradientColors: gradientPalette, + height: 400, + width: 600, }, render: (args: any) => ( - + +

Monthly Sales Performance

), }; -export const PieChartV2WithCarousel: Story = { - name: "PieChartV2 with Up/Down Carousel", +/** + * ## Layout and Variant Options + * + * This story demonstrates the different layout (`appearance`) and style (`variant`) + * options available in the PieChart component. + * + * **Options Shown:** + * - **Pie vs. Donut**: See the difference between a full pie and one with a center cutout. + * - **Circular vs. Semicircle**: Compare the full 360° layout with the space-saving 180° version. + */ +export const LayoutAndVariantOptions: Story = { + name: "🎨 Layout & Variant Options", args: { - data: extendedPieChartData, + data: monthlySalesData.slice(0, 4), categoryKey: "month", dataKey: "value", - theme: "ocean", - variant: "pie", - format: "number", + theme: "emerald", legend: true, legendVariant: "stacked", - isAnimationActive: true, - appearance: "circular", - cornerRadius: 0, - paddingAngle: 0, - useGradients: false, - gradientColors, - height: undefined, - width: undefined, + isAnimationActive: false, + cornerRadius: 4, + paddingAngle: 2, }, render: (args: any) => ( - +
+
+

Pie - Circular

+ +
+
+

Donut - Circular

+ +
+
+

Pie - Semicircle

+ +
+
+

Donut - Semicircle

+ +
+
+ ), +}; + +/** + * ## Large Dataset with Carousel + * + * The PieChart's legend intelligently handles a large number of items by + * enabling a scrollable carousel with up/down navigation. + * + * **Feature Highlight:** + * - The legend's carousel appears automatically when items overflow the available space, ensuring all data points remain accessible without cluttering the UI. + */ +export const LargeDatasetWithCarousel: Story = { + name: "📈 Large Dataset with Carousel", + args: { + data: comprehensiveData, + categoryKey: "category", + dataKey: "sales", + theme: "spectrum", + variant: "donut", + legend: true, + legendVariant: "stacked", + cornerRadius: 4, + paddingAngle: 1, + height: 500, + width: 700, + }, + render: (args: any) => ( + +

Comprehensive Sales Breakdown

), - parameters: { - docs: { - description: { - story: - "This example demonstrates the up/down carousel functionality when there are many legend items. The legend has navigation buttons that appear when content overflows, allowing users to scroll through all items.", - }, - }, - }, }; -export const ResizableAndResponsive: Story = { - name: "Resizable and Responsive", +/** + * ## Responsive Behavior Demo + * + * This story demonstrates the responsive nature of the PieChart. Drag the + * handles on the container to resize it and observe how the chart and its + * legend adapt to the new dimensions. + */ +export const ResponsiveBehaviorDemo: Story = { + name: "📱 Responsive Behavior Demo", args: { - data: pieChartData, + data: monthlySalesData, categoryKey: "month", dataKey: "value", - theme: "spectrum", + theme: "sunset", variant: "donut", - format: "number", legend: true, legendVariant: "stacked", isAnimationActive: false, - appearance: "circular", cornerRadius: 8, paddingAngle: 2, - useGradients: false, }, render: (args: any) => { - const [dimensions, setDimensions] = useState<{ width: number; height: number | string }>({ - width: 700, - height: "auto", - }); + const [dimensions, setDimensions] = useState({ width: 600, height: 400 }); - const handleMouseDown = (e: React.MouseEvent, handle: string) => { + const handleMouseDown = (e: React.MouseEvent) => { e.preventDefault(); - e.stopPropagation(); const startX = e.clientX; const startY = e.clientY; const startWidth = dimensions.width; - const startHeight = (e.currentTarget.parentElement as HTMLElement).offsetHeight; + const startHeight = dimensions.height; const doDrag = (e: MouseEvent) => { - const dx = e.clientX - startX; - const dy = e.clientY - startY; - let newWidth = startWidth; - let newHeight = startHeight; - - if (handle.includes("e")) newWidth = startWidth + dx; - if (handle.includes("w")) newWidth = startWidth - dx; - if (handle.includes("s")) newHeight = startHeight + dy; - if (handle.includes("n")) newHeight = startHeight - dy; - setDimensions({ - width: Math.max(300, newWidth), - height: "auto", + width: Math.max(300, startWidth + e.clientX - startX), + height: Math.max(250, startHeight + e.clientY - startY), }); }; - const stopDrag = () => { document.removeEventListener("mousemove", doDrag); document.removeEventListener("mouseup", stopDrag); @@ -330,114 +429,34 @@ export const ResizableAndResponsive: Story = { document.addEventListener("mouseup", stopDrag); }; - const handleStyle: React.CSSProperties = { - position: "absolute", - background: "#3b82f6", - opacity: 0.5, - zIndex: 10, - }; - return ( - - {/* Resizer handles */} -
handleMouseDown(e, "n")} - /> -
handleMouseDown(e, "s")} - /> -
handleMouseDown(e, "w")} - /> -
handleMouseDown(e, "e")} - /> -
handleMouseDown(e, "nw")} - />
handleMouseDown(e, "ne")} - /> -
handleMouseDown(e, "sw")} - /> -
handleMouseDown(e, "se")} + onMouseDown={handleMouseDown} /> -
- {dimensions.width}px ×{" "} - {typeof dimensions.height === "number" ? `${dimensions.height}px` : dimensions.height} +
+ {dimensions.width}px × {dimensions.height}px
); }, - parameters: { - docs: { - description: { - story: - "This story demonstrates the responsive and resizable behavior of the PieChartV2. Drag the edges or corners of the dashed container to resize it and see how the chart and its legend adapt perfectly to the new dimensions.", - }, - }, - }, }; From c5ca8fe3b296def093f3ad7c1305e77e30624749 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 05:45:55 +0530 Subject: [PATCH 168/190] refactor(Charts): improve PieChart stories formatting and enhance RadarChart imports This commit refines the formatting of the PieChart stories for better readability by adjusting the description strings and layout. Additionally, it updates the RadarChart stories to remove unused imports, streamlining the codebase and improving overall clarity. --- .../PieChart/stories/PieChart.stories.tsx | 50 ++++++++++++------- .../RadarChart/stories/RadarChart.stories.tsx | 2 +- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx index 84b9a2bba..60eccad5a 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx @@ -155,7 +155,8 @@ const salesData = [ }, }, theme: { - description: "The color palette for the chart. Provides a set of aesthetically pleasing colors.", + description: + "The color palette for the chart. Provides a set of aesthetically pleasing colors.", control: "select", options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], table: { @@ -182,7 +183,8 @@ const salesData = [ }, }, format: { - description: "The format for displaying data values, either as raw numbers or as percentages.", + description: + "The format for displaying data values, either as raw numbers or as percentages.", control: "radio", options: ["percentage", "number"], table: { @@ -245,7 +247,8 @@ const salesData = [ }, }, height: { - description: "Sets a fixed height for the chart container. Undefined by default for responsive height.", + description: + "Sets a fixed height for the chart container. Undefined by default for responsive height.", control: { type: "number", min: 200, max: 800 }, table: { type: { summary: "number" }, @@ -253,7 +256,8 @@ const salesData = [ }, }, width: { - description: "Sets a fixed width for the chart container. Undefined by default for responsive width.", + description: + "Sets a fixed width for the chart container. Undefined by default for responsive width.", control: { type: "number", min: 200, max: 800 }, table: { type: { summary: "number" }, @@ -300,7 +304,9 @@ export const DefaultConfiguration: Story = { }, render: (args: any) => ( -

Monthly Sales Performance

+

+ Monthly Sales Performance +

), @@ -330,22 +336,30 @@ export const LayoutAndVariantOptions: Story = { paddingAngle: 2, }, render: (args: any) => ( -
+
-

Pie - Circular

- +

Pie - Circular

+ + +
-

Donut - Circular

- +

Donut - Circular

+ + +
-

Pie - Semicircle

- +

Pie - Semicircle

+ + +
-

Donut - Semicircle

- +

Donut - Semicircle

+ + +
), @@ -377,7 +391,9 @@ export const LargeDatasetWithCarousel: Story = { }, render: (args: any) => ( -

Comprehensive Sales Breakdown

+

+ Comprehensive Sales Breakdown +

), @@ -449,11 +465,11 @@ export const ResponsiveBehaviorDemo: Story = { width: 16, height: 16, cursor: "nwse-resize", - background: 'rgba(0,0,0,0.1)' + background: "rgba(0,0,0,0.1)", }} onMouseDown={handleMouseDown} /> -
+
{dimensions.width}px × {dimensions.height}px
diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx index f37944581..5df8aac8b 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Shield, Star, Target, TrendingUp, Users, Zap } from "lucide-react"; +import { Shield, Star, Target } from "lucide-react"; import { useState } from "react"; import { Card } from "../../../../Card"; import { RadarChart, RadarChartProps } from "../RadarChart"; From 3f998a82ba1ed3af9ad3d757807b1e562cc08889 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 05:49:29 +0530 Subject: [PATCH 169/190] refactor(Charts): update MiniAreaChart story tags for clarity This commit modifies the tags for the MiniAreaChart stories, changing them from ["dev", "autodocs"] to ["!dev", "!autodocs"]. This adjustment aims to enhance the clarity and categorization of the story documentation. --- .../AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx index af06c6b2f..cbf38855c 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx @@ -38,7 +38,7 @@ const meta: Meta = { }, }, }, - tags: ["dev", "autodocs"], + tags: ["!dev", "!autodocs"], argTypes: { data: { description: From dfc5c993c8e362c9e11a651aacdc6cd7ff8c4643 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 15:56:24 +0530 Subject: [PATCH 170/190] refactor(Charts): remove CustomCursor component and update BarChart styles This commit removes the CustomCursor component and its associated SCSS file from the BarChart implementation, streamlining the codebase. Additionally, it updates the BarChart styles to improve spacing and element dimensions, enhancing the overall visual presentation of the charts. A new data structure for single group sales has also been added to the BarChart stories for better representation. --- .../BarChart/components/CustomCursor.scss | 7 -- .../BarChart/components/CustomCursor.tsx | 54 ------------ .../BarChart/stories/barChart.stories.tsx | 17 ++++ .../components/Charts/BarCharts/index.scss | 1 - .../Charts/BarCharts/utils/BarChartUtils.ts | 85 ++++--------------- 5 files changed, 32 insertions(+), 132 deletions(-) delete mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/CustomCursor.scss delete mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/CustomCursor.tsx diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/CustomCursor.scss b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/CustomCursor.scss deleted file mode 100644 index 2c1274c38..000000000 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/CustomCursor.scss +++ /dev/null @@ -1,7 +0,0 @@ -@use "../../../../../cssUtils" as cssUtils; - -.crayon-chart-cursor-shape { - fill: cssUtils.$bg-sunk; - stroke: cssUtils.$stroke-default; - stroke-width: 1; -} diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/CustomCursor.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/CustomCursor.tsx deleted file mode 100644 index 08d288919..000000000 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/CustomCursor.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useMemo } from "react"; - -// discard this is this is not used, talk with the designer first -interface CustomCursorProps { - x?: number; - y?: number; - width?: number; - height?: number; - payload?: any[]; - // Other props that Recharts might pass - [key: string]: any; -} - -const SimpleCursorComponent: React.FC = ({ - x = 0, - y = 0, - width = 0, - height = 0, -}) => { - // Memoize path calculation to avoid recreating on every render - const pathData = useMemo(() => { - return `M ${x} ${y} - L ${x + width} ${y} - L ${x + width} ${y + height} - L ${x} ${y + height} - Z`; - }, [x, y, width, height]); - - /* SVG Path Command Documentation: - * - * M ${x} ${y} - * - M = "Move to" - Sets the starting point at top-left corner - * - * L ${x + width} ${y} - * - L = "Line to" - Draws the top horizontal line to top-right corner - * - * L ${x + width} ${y + height} - * - L = "Line to" - Draws straight line down the right edge to bottom-right corner - * - * L ${x} ${y + height} - * - L = "Line to" - Draws straight line across the bottom edge to bottom-left corner - * - * Z = "Close path" - Draws a line back to the starting point and closes the shape - */ - - return ( - - - - ); -}; - -// Memoize component to prevent re-renders when props haven't changed -export const SimpleCursor = SimpleCursorComponent; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/stories/barChart.stories.tsx b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/stories/barChart.stories.tsx index 9c5cc6105..62ae898fb 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/stories/barChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/stories/barChart.stories.tsx @@ -465,6 +465,16 @@ const dataVariations = { videoViews: 15000, }, ], + singleGroup: [ + { month: "January", sales: 150 }, + { month: "February", sales: 280 }, + { month: "March", sales: 220 }, + { month: "April", sales: 180 }, + { month: "May", sales: 250 }, + { month: "June", sales: 300 }, + { month: "July", sales: 350 }, + { month: "August", sales: 400 }, + ], }; // Category key mappings for different datasets @@ -478,6 +488,7 @@ const categoryKeys = { weekly: "week", bigNumbers: "company", expandCollapseMarketing: "channel", + singleGroup: "month", }; // 🔥 ACTIVE DATA - For backward compatibility @@ -739,6 +750,12 @@ export const BarChartV2Story: Story = { > 🔢 Number Ranges + + )} + {isShowMoreLayout && showAll && ( + + )}
); }; diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss index 7e96f9b20..2957c4085 100644 --- a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss @@ -15,7 +15,7 @@ align-items: center; justify-content: space-between; gap: 10px; - padding: 0 16px; + padding: cssUtils.$spacing-xs 0 cssUtils.$spacing-xs cssUtils.$spacing-m; color: cssUtils.$secondary-text; &-buttons { @@ -138,4 +138,14 @@ flex-shrink: 0; } } + + &-show-more-button { + width: 100%; + justify-content: center; + } + + &-show-less-button { + width: 100%; + justify-content: center; + } } From b10b4d50441f6a93f5431fba1c5dae2aa232c067 Mon Sep 17 00:00:00 2001 From: i-subham Date: Mon, 30 Jun 2025 19:20:11 +0530 Subject: [PATCH 181/190] refactor(Charts): remove duplicate entries in PieChart story data This commit eliminates redundant entries for April and May in the monthly sales data of the PieChart stories, streamlining the data representation and improving clarity in the component's examples. --- .../Charts/PieCharts/PieChart/stories/PieChart.stories.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx index ddcca2f2f..05fbe99ce 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx @@ -10,8 +10,6 @@ const monthlySalesData = [ { month: "April", value: 1320 }, { month: "May", value: 1680 }, { month: "June", value: 2100 }, - { month: "April", value: 1320 }, - { month: "May", value: 1680 }, ]; const comprehensiveData = [ From 1c057093500b027ea92fab13b3eddfede9fc114f Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 19:46:04 +0530 Subject: [PATCH 182/190] refactor(Charts): update RadarChart styles and improve chart size calculation This commit refines the styling of the RadarChart component by replacing the stroke color with a variable from cssUtils for consistency. Additionally, it enhances the chart size calculation logic for better clarity and readability. Unused PolarGrid properties have been removed to streamline the component's code. --- .../Charts/RadarCharts/RadarChart/RadarChart.tsx | 14 ++++---------- .../Charts/RadarCharts/RadarChart/radarChart.scss | 6 +++++- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/RadarChart.tsx b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/RadarChart.tsx index d97012eda..6d25842b6 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/RadarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/RadarChart.tsx @@ -83,9 +83,9 @@ const RadarChartComponent = ({ const chartSize = useMemo(() => { const effectiveWidth = wrapperRect.width; const effectiveHeight = wrapperRect.height; - let chartsz = Math.min(effectiveWidth, effectiveHeight); - chartsz = Math.min(chartsz, MAX_CHART_SIZE); - return Math.max(MIN_CHART_SIZE, chartsz); + let charts = Math.min(effectiveWidth, effectiveHeight); + charts = Math.min(charts, MAX_CHART_SIZE); + return Math.max(MIN_CHART_SIZE, charts); }, [wrapperRect]); const chartSizeStyle = useMemo(() => ({ width: chartSize, height: chartSize }), [chartSize]); @@ -158,13 +158,7 @@ const RadarChartComponent = ({ bottom: 10, }} > - {grid && ( - - )} + {grid && } Date: Mon, 30 Jun 2025 20:02:48 +0530 Subject: [PATCH 183/190] refactor(Charts): update StackedLegend and RadarChart styles for consistency This commit modifies the styling of the StackedLegend and RadarChart components by updating padding values and stroke colors to use variables from cssUtils. The changes aim to enhance visual consistency and improve the overall layout of the chart elements, including adjustments to typography settings for better readability. --- .../Charts/RadarCharts/RadarChart/radarChart.scss | 4 ++-- .../Charts/shared/StackedLegend/StackedLegend.tsx | 4 ++-- .../Charts/shared/StackedLegend/stackedLegend.scss | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/radarChart.scss b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/radarChart.scss index cf7336f89..8fd1084bc 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/radarChart.scss +++ b/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/radarChart.scss @@ -33,11 +33,11 @@ .crayon-radar-chart { .recharts-polar-grid-concentric-polygon { stroke-width: 1; - stroke: cssUtils.$stroke-default; + stroke: cssUtils.$stroke-interactive-el; } .recharts-polar-grid-angle line { - stroke: cssUtils.$stroke-default; + stroke: cssUtils.$stroke-interactive-el; } } diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx index eb704aab5..9605934d5 100644 --- a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/StackedLegend.tsx @@ -134,7 +134,7 @@ export const StackedLegend = ({ aria-label="Scroll legend up" icon={} variant="secondary" - size="small" + size="extra-small" disabled={!showUpButton} /> } variant="secondary" - size="small" + size="extra-small" disabled={!showDownButton} /> diff --git a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss index 2957c4085..11ec1894f 100644 --- a/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss +++ b/js/packages/react-ui/src/components/Charts/shared/StackedLegend/stackedLegend.scss @@ -15,14 +15,14 @@ align-items: center; justify-content: space-between; gap: 10px; - padding: cssUtils.$spacing-xs 0 cssUtils.$spacing-xs cssUtils.$spacing-m; + padding: 0 cssUtils.$spacing-xs cssUtils.$spacing-xs cssUtils.$spacing-l; color: cssUtils.$secondary-text; &-buttons { display: flex; flex-direction: row; align-items: center; - gap: 10px; + gap: cssUtils.$spacing-xs; } } .crayon-stacked-legend-scroll-button { @@ -122,7 +122,7 @@ } &-label-text { - @include cssUtils.typography(label, medium); + @include cssUtils.typography(label, default); color: cssUtils.$primary-text; transition: color 0.2s ease-in-out; overflow: hidden; @@ -132,7 +132,7 @@ } &-value { - @include cssUtils.typography(label, medium); + @include cssUtils.typography(label, default); color: cssUtils.$primary-text; transition: all 0.2s ease-in-out; flex-shrink: 0; From dcec127c5132391ec7c51d22cee8811e0ca23c6d Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 21:01:16 +0530 Subject: [PATCH 184/190] refactor(Charts): reorganize chart component structure and update imports This commit refactors the chart components by reorganizing the directory structure, consolidating SCSS files, and updating import paths for consistency. It removes obsolete files related to AreaCharts, BarCharts, LineCharts, PieCharts, RadarCharts, and RadialCharts, streamlining the codebase. The changes enhance maintainability and improve the overall organization of the chart components. --- .../{AreaCharts => }/AreaChart/AreaChart.tsx | 26 ++++++++--------- .../{AreaCharts => }/AreaChart/areaChart.scss | 2 +- .../AreaChart/dependencies.ts | 0 .../{AreaCharts => }/AreaChart/index.ts | 1 + .../AreaChart/stories/areaChart.stories.tsx | 2 +- .../{AreaCharts => AreaChart}/types/index.ts | 2 -- .../Charts/AreaCharts/MiniAreaChart/index.ts | 1 - .../components/Charts/AreaCharts/index.scss | 1 - .../{BarCharts => }/BarChart/BarChart.tsx | 28 +++++++++---------- .../{BarCharts => }/BarChart/barChart.scss | 2 +- .../BarChart/components/LineInBarShape.tsx | 0 .../{BarCharts => }/BarChart/dependencies.ts | 0 .../Charts/{BarCharts => }/BarChart/index.ts | 1 + .../BarChart/stories/barChart.stories.tsx | 2 +- .../{BarCharts => BarChart}/types/index.ts | 2 -- .../utils/BarChartUtils.ts | 0 .../Charts/BarCharts/MiniBarChart/index.ts | 1 - .../components/Charts/BarCharts/index.scss | 2 -- .../{LineCharts => }/LineChart/LineChart.tsx | 26 ++++++++--------- .../LineChart/dependencies.ts | 0 .../{LineCharts => }/LineChart/index.ts | 1 + .../{LineCharts => }/LineChart/lineChart.scss | 2 +- .../LineChart/stories/lineChart.stories.tsx | 2 +- .../{LineCharts => LineChart}/types/index.ts | 2 -- .../Charts/LineCharts/MiniLineChart/index.ts | 1 - .../components/Charts/LineCharts/index.scss | 1 - .../MiniAreaChart/MiniAreaChart.tsx | 8 +++--- .../MiniAreaChart/dependencies.ts | 0 .../{AreaCharts => MiniAreaChart}/index.ts | 1 - .../stories/MiniAreaChart.stories.tsx | 2 +- .../Charts/MiniAreaChart/types/index.ts | 1 + .../MiniBarChart/MiniBarChart.tsx | 8 +++--- .../MiniBarChart/dependencies.ts | 0 .../{BarCharts => MiniBarChart}/index.ts | 1 - .../MiniBarChart/miniBarChart.scss | 0 .../stories/MiniBarChart.stories.tsx | 2 +- .../Charts/MiniBarChart/types/index.ts | 1 + .../MiniBarChart/utils/miniBarChartUtils.ts | 2 +- .../MiniLineChart/MiniLineChart.tsx | 8 +++--- .../MiniLineChart/dependencies.ts | 0 .../{LineCharts => MiniLineChart}/index.ts | 1 - .../stories/MiniLineChart.stories.tsx | 2 +- .../Charts/MiniLineChart/types/index.ts | 1 + .../{PieCharts => }/PieChart/PieChart.tsx | 16 +++++------ .../PieChart/components/PieChartRenderers.tsx | 0 .../Charts/{PieCharts => PieChart}/index.ts | 0 .../{PieCharts => }/PieChart/pieChart.scss | 2 +- .../PieChart/stories/PieChart.stories.tsx | 2 +- .../{PieCharts => PieChart}/types/index.ts | 0 .../PieChart/utils/PieChartUtils.ts | 2 +- .../Charts/PieCharts/PieChart/index.ts | 1 - .../components/Charts/PieCharts/index.scss | 1 - .../RadarChart/RadarChart.tsx | 14 +++++----- .../RadarChart/components/AxisLabel.tsx | 0 .../{RadarCharts => }/RadarChart/index.ts | 0 .../RadarChart/radarChart.scss | 2 +- .../RadarChart/stories/RadarChart.stories.tsx | 2 +- .../RadarChart/types/index.ts | 0 .../RadarChart/utils/index.ts | 0 .../components/Charts/RadarCharts/index.scss | 1 - .../components/Charts/RadarCharts/index.ts | 1 - .../RadialChart/RadialChart.tsx | 16 +++++------ .../components/RadialChartRenderers.tsx | 0 .../{RadialCharts => RadialChart}/index.ts | 0 .../RadialChart/radialChart.scss | 2 +- .../stories/RadialChart.stories.tsx | 2 +- .../types/index.ts | 0 .../RadialChart/utils/RadialChartUtils.ts | 4 +-- .../Charts/RadialCharts/RadialChart/index.ts | 1 - .../components/Charts/RadialCharts/index.scss | 1 - .../src/components/Charts/charts.scss | 13 +++++---- .../react-ui/src/components/Charts/index.ts | 12 ++++---- .../utils/AreaAndLine/AreaAndLineUtils.ts | 4 +-- .../utils/AreaAndLine/MiniAreaAndLineUtils.ts | 4 +-- .../src/components/Charts/utils/dataUtils.ts | 4 +-- 75 files changed, 119 insertions(+), 134 deletions(-) rename js/packages/react-ui/src/components/Charts/{AreaCharts => }/AreaChart/AreaChart.tsx (95%) rename js/packages/react-ui/src/components/Charts/{AreaCharts => }/AreaChart/areaChart.scss (96%) rename js/packages/react-ui/src/components/Charts/{AreaCharts => }/AreaChart/dependencies.ts (100%) rename js/packages/react-ui/src/components/Charts/{AreaCharts => }/AreaChart/index.ts (53%) rename js/packages/react-ui/src/components/Charts/{AreaCharts => }/AreaChart/stories/areaChart.stories.tsx (99%) rename js/packages/react-ui/src/components/Charts/{AreaCharts => AreaChart}/types/index.ts (59%) delete mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/AreaCharts/index.scss rename js/packages/react-ui/src/components/Charts/{BarCharts => }/BarChart/BarChart.tsx (96%) rename js/packages/react-ui/src/components/Charts/{BarCharts => }/BarChart/barChart.scss (95%) rename js/packages/react-ui/src/components/Charts/{BarCharts => }/BarChart/components/LineInBarShape.tsx (100%) rename js/packages/react-ui/src/components/Charts/{BarCharts => }/BarChart/dependencies.ts (100%) rename js/packages/react-ui/src/components/Charts/{BarCharts => }/BarChart/index.ts (52%) rename js/packages/react-ui/src/components/Charts/{BarCharts => }/BarChart/stories/barChart.stories.tsx (99%) rename js/packages/react-ui/src/components/Charts/{BarCharts => BarChart}/types/index.ts (57%) rename js/packages/react-ui/src/components/Charts/{BarCharts => BarChart}/utils/BarChartUtils.ts (100%) delete mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/BarCharts/index.scss rename js/packages/react-ui/src/components/Charts/{LineCharts => }/LineChart/LineChart.tsx (95%) rename js/packages/react-ui/src/components/Charts/{LineCharts => }/LineChart/dependencies.ts (100%) rename js/packages/react-ui/src/components/Charts/{LineCharts => }/LineChart/index.ts (53%) rename js/packages/react-ui/src/components/Charts/{LineCharts => }/LineChart/lineChart.scss (96%) rename js/packages/react-ui/src/components/Charts/{LineCharts => }/LineChart/stories/lineChart.stories.tsx (99%) rename js/packages/react-ui/src/components/Charts/{LineCharts => LineChart}/types/index.ts (59%) delete mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/LineCharts/index.scss rename js/packages/react-ui/src/components/Charts/{AreaCharts => }/MiniAreaChart/MiniAreaChart.tsx (95%) rename js/packages/react-ui/src/components/Charts/{AreaCharts => }/MiniAreaChart/dependencies.ts (100%) rename js/packages/react-ui/src/components/Charts/{AreaCharts => MiniAreaChart}/index.ts (66%) rename js/packages/react-ui/src/components/Charts/{AreaCharts => }/MiniAreaChart/stories/MiniAreaChart.stories.tsx (99%) create mode 100644 js/packages/react-ui/src/components/Charts/MiniAreaChart/types/index.ts rename js/packages/react-ui/src/components/Charts/{BarCharts => }/MiniBarChart/MiniBarChart.tsx (94%) rename js/packages/react-ui/src/components/Charts/{BarCharts => }/MiniBarChart/dependencies.ts (100%) rename js/packages/react-ui/src/components/Charts/{BarCharts => MiniBarChart}/index.ts (67%) rename js/packages/react-ui/src/components/Charts/{BarCharts => }/MiniBarChart/miniBarChart.scss (100%) rename js/packages/react-ui/src/components/Charts/{BarCharts => }/MiniBarChart/stories/MiniBarChart.stories.tsx (99%) create mode 100644 js/packages/react-ui/src/components/Charts/MiniBarChart/types/index.ts rename js/packages/react-ui/src/components/Charts/{BarCharts => }/MiniBarChart/utils/miniBarChartUtils.ts (98%) rename js/packages/react-ui/src/components/Charts/{LineCharts => }/MiniLineChart/MiniLineChart.tsx (93%) rename js/packages/react-ui/src/components/Charts/{LineCharts => }/MiniLineChart/dependencies.ts (100%) rename js/packages/react-ui/src/components/Charts/{LineCharts => MiniLineChart}/index.ts (66%) rename js/packages/react-ui/src/components/Charts/{LineCharts => }/MiniLineChart/stories/MiniLineChart.stories.tsx (99%) create mode 100644 js/packages/react-ui/src/components/Charts/MiniLineChart/types/index.ts rename js/packages/react-ui/src/components/Charts/{PieCharts => }/PieChart/PieChart.tsx (96%) rename js/packages/react-ui/src/components/Charts/{PieCharts => }/PieChart/components/PieChartRenderers.tsx (100%) rename js/packages/react-ui/src/components/Charts/{PieCharts => PieChart}/index.ts (100%) rename js/packages/react-ui/src/components/Charts/{PieCharts => }/PieChart/pieChart.scss (97%) rename js/packages/react-ui/src/components/Charts/{PieCharts => }/PieChart/stories/PieChart.stories.tsx (99%) rename js/packages/react-ui/src/components/Charts/{PieCharts => PieChart}/types/index.ts (100%) rename js/packages/react-ui/src/components/Charts/{PieCharts => }/PieChart/utils/PieChartUtils.ts (99%) delete mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/PieChart/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/PieCharts/index.scss rename js/packages/react-ui/src/components/Charts/{RadarCharts => }/RadarChart/RadarChart.tsx (94%) rename js/packages/react-ui/src/components/Charts/{RadarCharts => }/RadarChart/components/AxisLabel.tsx (100%) rename js/packages/react-ui/src/components/Charts/{RadarCharts => }/RadarChart/index.ts (100%) rename js/packages/react-ui/src/components/Charts/{RadarCharts => }/RadarChart/radarChart.scss (95%) rename js/packages/react-ui/src/components/Charts/{RadarCharts => }/RadarChart/stories/RadarChart.stories.tsx (99%) rename js/packages/react-ui/src/components/Charts/{RadarCharts => }/RadarChart/types/index.ts (100%) rename js/packages/react-ui/src/components/Charts/{RadarCharts => }/RadarChart/utils/index.ts (100%) delete mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/index.scss delete mode 100644 js/packages/react-ui/src/components/Charts/RadarCharts/index.ts rename js/packages/react-ui/src/components/Charts/{RadialCharts => }/RadialChart/RadialChart.tsx (96%) rename js/packages/react-ui/src/components/Charts/{RadialCharts => }/RadialChart/components/RadialChartRenderers.tsx (100%) rename js/packages/react-ui/src/components/Charts/{RadialCharts => RadialChart}/index.ts (100%) rename js/packages/react-ui/src/components/Charts/{RadialCharts => }/RadialChart/radialChart.scss (97%) rename js/packages/react-ui/src/components/Charts/{RadialCharts => }/RadialChart/stories/RadialChart.stories.tsx (99%) rename js/packages/react-ui/src/components/Charts/{RadialCharts => RadialChart}/types/index.ts (100%) rename js/packages/react-ui/src/components/Charts/{RadialCharts => }/RadialChart/utils/RadialChartUtils.ts (98%) delete mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/index.ts delete mode 100644 js/packages/react-ui/src/components/Charts/RadialCharts/index.scss diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/AreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx similarity index 95% rename from js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/AreaChart.tsx rename to js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx index eab2e52c4..302ce6edf 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/AreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx @@ -2,11 +2,11 @@ import clsx from "clsx"; import { ChevronLeft, ChevronRight } from "lucide-react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Area, AreaChart as RechartsAreaChart, XAxis, YAxis } from "recharts"; -import { useId } from "../../../../polyfills"; -import { IconButton } from "../../../IconButton"; -import { ChartConfig, ChartContainer, ChartTooltip } from "../../Charts"; -import { SideBarChartData, SideBarTooltipProvider } from "../../context/SideBarTooltipContext"; -import { useTransformedKeys } from "../../hooks"; +import { useId } from "../../../polyfills"; +import { IconButton } from "../../IconButton"; +import { ChartConfig, ChartContainer, ChartTooltip } from "../Charts"; +import { SideBarChartData, SideBarTooltipProvider } from "../context/SideBarTooltipContext"; +import { useTransformedKeys } from "../hooks"; import { ActiveDot, cartesianGrid, @@ -15,24 +15,24 @@ import { SideBarTooltip, XAxisTick, YAxisTick, -} from "../../shared"; -import { LegendItem } from "../../types"; +} from "../shared"; +import { LegendItem } from "../types"; import { findNearestSnapPosition, getOptimalXAxisTickFormatter, getSnapPositions, getWidthOfData, getXAxisTickPositionData, -} from "../../utils/AreaAndLine/AreaAndLineUtils"; -import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; +} from "../utils/AreaAndLine/AreaAndLineUtils"; +import { getDistributedColors, getPalette, PaletteName } from "../utils/PalletUtils"; import { get2dChartConfig, getColorForDataKey, getDataKeys, getLegendItems, -} from "../../utils/dataUtils"; -import { getYAxisTickFormatter } from "../../utils/styleUtils"; -import { AreaChartData, AreaChartVariant } from "../types"; +} from "../utils/dataUtils"; +import { getYAxisTickFormatter } from "../utils/styleUtils"; +import { AreaChartData, AreaChartVariant } from "./types"; // this a technic to get the type of the onClick event of the bar chart // we need to do this because the onClick event type is not exported by recharts @@ -66,7 +66,7 @@ const AreaChartComponent = ({ grid = true, icons = {}, isAnimationActive = false, - showYAxis = false, + showYAxis = true, xAxisLabel, yAxisLabel, legend = true, diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/areaChart.scss b/js/packages/react-ui/src/components/Charts/AreaChart/areaChart.scss similarity index 96% rename from js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/areaChart.scss rename to js/packages/react-ui/src/components/Charts/AreaChart/areaChart.scss index 5656c78ad..f447954f4 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/areaChart.scss +++ b/js/packages/react-ui/src/components/Charts/AreaChart/areaChart.scss @@ -1,4 +1,4 @@ -@use "../../../../cssUtils" as cssUtils; +@use "../../../cssUtils" as cssUtils; .crayon-area-chart-container-inner { display: flex; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/dependencies.ts b/js/packages/react-ui/src/components/Charts/AreaChart/dependencies.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/dependencies.ts rename to js/packages/react-ui/src/components/Charts/AreaChart/dependencies.ts diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/index.ts b/js/packages/react-ui/src/components/Charts/AreaChart/index.ts similarity index 53% rename from js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/index.ts rename to js/packages/react-ui/src/components/Charts/AreaChart/index.ts index da5fa4e47..72dd774a1 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/index.ts +++ b/js/packages/react-ui/src/components/Charts/AreaChart/index.ts @@ -1 +1,2 @@ export * from "./AreaChart"; +export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/stories/areaChart.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaChart/stories/areaChart.stories.tsx similarity index 99% rename from js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/stories/areaChart.stories.tsx rename to js/packages/react-ui/src/components/Charts/AreaChart/stories/areaChart.stories.tsx index 60dfd91f3..9d5a1ec73 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/AreaChart/stories/areaChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaChart/stories/areaChart.stories.tsx @@ -10,7 +10,7 @@ import { Watch, } from "lucide-react"; import { useState } from "react"; -import { Card } from "../../../../Card"; +import { Card } from "../../../Card"; import { AreaChart, AreaChartProps } from "../AreaChart"; // 🔥 COMPREHENSIVE DATA VARIATIONS - Designed to test label collision scenarios diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/AreaChart/types/index.ts similarity index 59% rename from js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts rename to js/packages/react-ui/src/components/Charts/AreaChart/types/index.ts index 7db3648e2..b01afe131 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/types/index.ts +++ b/js/packages/react-ui/src/components/Charts/AreaChart/types/index.ts @@ -1,5 +1,3 @@ export type AreaChartVariant = "linear" | "natural" | "step"; export type AreaChartData = Array>; - -export type MiniAreaChartData = Array | Array<{ value: number; label?: string }>; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/index.ts b/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/index.ts deleted file mode 100644 index 8d4661adc..000000000 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./MiniAreaChart"; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/index.scss b/js/packages/react-ui/src/components/Charts/AreaCharts/index.scss deleted file mode 100644 index a0c0b3c73..000000000 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/index.scss +++ /dev/null @@ -1 +0,0 @@ -@forward "./AreaChart/areaChart.scss"; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/BarChart.tsx b/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx similarity index 96% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChart/BarChart.tsx rename to js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx index fb72a9e6d..3a07e5b9b 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/BarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx @@ -2,12 +2,12 @@ import clsx from "clsx"; import { ChevronLeft, ChevronRight } from "lucide-react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Bar, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; -import { useId } from "../../../../polyfills"; -import { IconButton } from "../../../IconButton"; -import { useTheme } from "../../../ThemeProvider"; -import { ChartConfig, ChartContainer, ChartTooltip } from "../../Charts"; -import { SideBarChartData, SideBarTooltipProvider } from "../../context/SideBarTooltipContext"; -import { useTransformedKeys } from "../../hooks"; +import { useId } from "../../../polyfills"; +import { IconButton } from "../../IconButton"; +import { useTheme } from "../../ThemeProvider"; +import { ChartConfig, ChartContainer, ChartTooltip } from "../Charts"; +import { SideBarChartData, SideBarTooltipProvider } from "../context/SideBarTooltipContext"; +import { useTransformedKeys } from "../hooks"; import { cartesianGrid, CustomTooltipContent, @@ -15,17 +15,18 @@ import { SideBarTooltip, XAxisTick, YAxisTick, -} from "../../shared"; -import { type LegendItem } from "../../types"; -import { getDistributedColors, getPalette, type PaletteName } from "../../utils/PalletUtils"; +} from "../shared"; +import { type LegendItem } from "../types"; +import { getDistributedColors, getPalette, type PaletteName } from "../utils/PalletUtils"; import { get2dChartConfig, getColorForDataKey, getDataKeys, getLegendItems, -} from "../../utils/dataUtils"; -import { getYAxisTickFormatter } from "../../utils/styleUtils"; -import { BarChartData, BarChartVariant } from "../types"; +} from "../utils/dataUtils"; +import { getYAxisTickFormatter } from "../utils/styleUtils"; +import { LineInBarShape } from "./components/LineInBarShape"; +import { BarChartData, BarChartVariant } from "./types"; import { BAR_WIDTH, findNearestSnapPosition, @@ -34,8 +35,7 @@ import { getRadiusArray, getSnapPositions, getWidthOfData, -} from "../utils/BarChartUtils"; -import { LineInBarShape } from "./components/LineInBarShape"; +} from "./utils/BarChartUtils"; // this a technic to get the type of the onClick event of the bar chart // we need to do this because the onClick event type is not exported by recharts diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/barChart.scss b/js/packages/react-ui/src/components/Charts/BarChart/barChart.scss similarity index 95% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChart/barChart.scss rename to js/packages/react-ui/src/components/Charts/BarChart/barChart.scss index feef9cce6..5e8ab0099 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/barChart.scss +++ b/js/packages/react-ui/src/components/Charts/BarChart/barChart.scss @@ -1,4 +1,4 @@ -@use "../../../../cssUtils" as cssUtils; +@use "../../../cssUtils" as cssUtils; .crayon-bar-chart-container-inner { display: flex; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/LineInBarShape.tsx b/js/packages/react-ui/src/components/Charts/BarChart/components/LineInBarShape.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChart/components/LineInBarShape.tsx rename to js/packages/react-ui/src/components/Charts/BarChart/components/LineInBarShape.tsx diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/dependencies.ts b/js/packages/react-ui/src/components/Charts/BarChart/dependencies.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChart/dependencies.ts rename to js/packages/react-ui/src/components/Charts/BarChart/dependencies.ts diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/index.ts b/js/packages/react-ui/src/components/Charts/BarChart/index.ts similarity index 52% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChart/index.ts rename to js/packages/react-ui/src/components/Charts/BarChart/index.ts index ea65af237..ddbf3231c 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/index.ts +++ b/js/packages/react-ui/src/components/Charts/BarChart/index.ts @@ -1 +1,2 @@ export * from "./BarChart"; +export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/stories/barChart.stories.tsx b/js/packages/react-ui/src/components/Charts/BarChart/stories/barChart.stories.tsx similarity index 99% rename from js/packages/react-ui/src/components/Charts/BarCharts/BarChart/stories/barChart.stories.tsx rename to js/packages/react-ui/src/components/Charts/BarChart/stories/barChart.stories.tsx index 23fcf8137..1f125653e 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/BarChart/stories/barChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChart/stories/barChart.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Monitor, TabletSmartphone } from "lucide-react"; import { useState } from "react"; -import { Card } from "../../../../Card"; +import { Card } from "../../../Card"; import { BarChart, BarChartProps } from "../BarChart"; // 📊 ALL DATA VARIATIONS - For easy switching in stories diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/BarChart/types/index.ts similarity index 57% rename from js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts rename to js/packages/react-ui/src/components/Charts/BarChart/types/index.ts index d2db3c40c..6f3195c54 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/types/index.ts +++ b/js/packages/react-ui/src/components/Charts/BarChart/types/index.ts @@ -1,5 +1,3 @@ export type BarChartVariant = "grouped" | "stacked"; export type BarChartData = Array>; - -export type MiniBarChartData = Array | Array<{ value: number; label?: string }>; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts b/js/packages/react-ui/src/components/Charts/BarChart/utils/BarChartUtils.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarCharts/utils/BarChartUtils.ts rename to js/packages/react-ui/src/components/Charts/BarChart/utils/BarChartUtils.ts diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.ts b/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.ts deleted file mode 100644 index 171dd71d0..000000000 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./MiniBarChart"; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/index.scss b/js/packages/react-ui/src/components/Charts/BarCharts/index.scss deleted file mode 100644 index c7ad2a77d..000000000 --- a/js/packages/react-ui/src/components/Charts/BarCharts/index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@forward "./BarChart/barChart.scss"; -@forward "./MiniBarChart/miniBarChart.scss"; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/LineChart.tsx b/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx similarity index 95% rename from js/packages/react-ui/src/components/Charts/LineCharts/LineChart/LineChart.tsx rename to js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx index b43b33ba8..1affc555e 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/LineChart.tsx +++ b/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx @@ -2,11 +2,11 @@ import clsx from "clsx"; import { ChevronLeft, ChevronRight } from "lucide-react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Line, LineChart as RechartsLineChart, XAxis, YAxis } from "recharts"; -import { useId } from "../../../../polyfills"; -import { IconButton } from "../../../IconButton"; -import { ChartConfig, ChartContainer, ChartTooltip } from "../../Charts"; -import { SideBarChartData, SideBarTooltipProvider } from "../../context/SideBarTooltipContext"; -import { useTransformedKeys } from "../../hooks"; +import { useId } from "../../../polyfills"; +import { IconButton } from "../../IconButton"; +import { ChartConfig, ChartContainer, ChartTooltip } from "../Charts"; +import { SideBarChartData, SideBarTooltipProvider } from "../context/SideBarTooltipContext"; +import { useTransformedKeys } from "../hooks"; import { ActiveDot, cartesianGrid, @@ -15,24 +15,24 @@ import { SideBarTooltip, XAxisTick, YAxisTick, -} from "../../shared"; -import { LegendItem } from "../../types"; +} from "../shared"; +import { LegendItem } from "../types"; import { findNearestSnapPosition, getOptimalXAxisTickFormatter, getSnapPositions, getWidthOfData, getXAxisTickPositionData, -} from "../../utils/AreaAndLine/AreaAndLineUtils"; -import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; +} from "../utils/AreaAndLine/AreaAndLineUtils"; +import { getDistributedColors, getPalette, PaletteName } from "../utils/PalletUtils"; import { get2dChartConfig, getColorForDataKey, getDataKeys, getLegendItems, -} from "../../utils/dataUtils"; -import { getYAxisTickFormatter } from "../../utils/styleUtils"; -import { LineChartData, LineChartVariant } from "../types"; +} from "../utils/dataUtils"; +import { getYAxisTickFormatter } from "../utils/styleUtils"; +import { LineChartData, LineChartVariant } from "./types"; type LineChartOnClick = React.ComponentProps["onClick"]; type LineClickData = Parameters>[0]; @@ -65,7 +65,7 @@ export const LineChart = ({ grid = true, icons = {}, isAnimationActive = false, - showYAxis = false, + showYAxis = true, xAxisLabel, yAxisLabel, legend = true, diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/dependencies.ts b/js/packages/react-ui/src/components/Charts/LineChart/dependencies.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/LineCharts/LineChart/dependencies.ts rename to js/packages/react-ui/src/components/Charts/LineChart/dependencies.ts diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/index.ts b/js/packages/react-ui/src/components/Charts/LineChart/index.ts similarity index 53% rename from js/packages/react-ui/src/components/Charts/LineCharts/LineChart/index.ts rename to js/packages/react-ui/src/components/Charts/LineChart/index.ts index e873ee101..3f293679e 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/index.ts +++ b/js/packages/react-ui/src/components/Charts/LineChart/index.ts @@ -1 +1,2 @@ export * from "./LineChart"; +export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/lineChart.scss b/js/packages/react-ui/src/components/Charts/LineChart/lineChart.scss similarity index 96% rename from js/packages/react-ui/src/components/Charts/LineCharts/LineChart/lineChart.scss rename to js/packages/react-ui/src/components/Charts/LineChart/lineChart.scss index 26e324374..2a2be6ba9 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/lineChart.scss +++ b/js/packages/react-ui/src/components/Charts/LineChart/lineChart.scss @@ -1,4 +1,4 @@ -@use "../../../../cssUtils" as cssUtils; +@use "../../../cssUtils" as cssUtils; .crayon-line-chart-container-inner { display: flex; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/stories/lineChart.stories.tsx b/js/packages/react-ui/src/components/Charts/LineChart/stories/lineChart.stories.tsx similarity index 99% rename from js/packages/react-ui/src/components/Charts/LineCharts/LineChart/stories/lineChart.stories.tsx rename to js/packages/react-ui/src/components/Charts/LineChart/stories/lineChart.stories.tsx index cdeaa46bb..f45098965 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/LineChart/stories/lineChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/LineChart/stories/lineChart.stories.tsx @@ -10,7 +10,7 @@ import { Watch, } from "lucide-react"; import { useState } from "react"; -import { Card } from "../../../../Card"; +import { Card } from "../../../Card"; import { LineChart, LineChartProps } from "../LineChart"; // 🔥 COMPREHENSIVE DATA VARIATIONS - Designed to test label collision scenarios diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/LineChart/types/index.ts similarity index 59% rename from js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts rename to js/packages/react-ui/src/components/Charts/LineChart/types/index.ts index 4333668ed..083280dd2 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/types/index.ts +++ b/js/packages/react-ui/src/components/Charts/LineChart/types/index.ts @@ -1,5 +1,3 @@ export type LineChartData = Array>; export type LineChartVariant = "linear" | "natural" | "step"; - -export type MiniLineChartData = Array | Array<{ value: number; label?: string }>; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts b/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts deleted file mode 100644 index 86eccce92..000000000 --- a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./MiniLineChart"; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/index.scss b/js/packages/react-ui/src/components/Charts/LineCharts/index.scss deleted file mode 100644 index a14d89e40..000000000 --- a/js/packages/react-ui/src/components/Charts/LineCharts/index.scss +++ /dev/null @@ -1 +0,0 @@ -@forward "./LineChart/lineChart.scss"; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx b/js/packages/react-ui/src/components/Charts/MiniAreaChart/MiniAreaChart.tsx similarity index 95% rename from js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx rename to js/packages/react-ui/src/components/Charts/MiniAreaChart/MiniAreaChart.tsx index 605e01713..3df3dc36e 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/MiniAreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/MiniAreaChart/MiniAreaChart.tsx @@ -1,13 +1,13 @@ import clsx from "clsx"; import { useEffect, useId, useMemo, useRef, useState } from "react"; import { Area, AreaChart as RechartsAreaChart, XAxis } from "recharts"; -import { ChartConfig, ChartContainer } from "../../Charts"; +import { ChartConfig, ChartContainer } from "../Charts"; import { getRecentDataThatFits, transformDataForChart, -} from "../../utils/AreaAndLine/MiniAreaAndLineUtils"; -import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; -import { MiniAreaChartData } from "../types"; +} from "../utils/AreaAndLine/MiniAreaAndLineUtils"; +import { getDistributedColors, getPalette, PaletteName } from "../utils/PalletUtils"; +import { MiniAreaChartData } from "./types"; export interface MiniAreaChartProps { data: MiniAreaChartData; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/dependencies.ts b/js/packages/react-ui/src/components/Charts/MiniAreaChart/dependencies.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/dependencies.ts rename to js/packages/react-ui/src/components/Charts/MiniAreaChart/dependencies.ts diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/index.ts b/js/packages/react-ui/src/components/Charts/MiniAreaChart/index.ts similarity index 66% rename from js/packages/react-ui/src/components/Charts/AreaCharts/index.ts rename to js/packages/react-ui/src/components/Charts/MiniAreaChart/index.ts index f93b92d20..02e709aa0 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/index.ts +++ b/js/packages/react-ui/src/components/Charts/MiniAreaChart/index.ts @@ -1,3 +1,2 @@ -export * from "./AreaChart"; export * from "./MiniAreaChart"; export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx b/js/packages/react-ui/src/components/Charts/MiniAreaChart/stories/MiniAreaChart.stories.tsx similarity index 99% rename from js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx rename to js/packages/react-ui/src/components/Charts/MiniAreaChart/stories/MiniAreaChart.stories.tsx index cbf38855c..73b8711e6 100644 --- a/js/packages/react-ui/src/components/Charts/AreaCharts/MiniAreaChart/stories/MiniAreaChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/MiniAreaChart/stories/MiniAreaChart.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Card } from "../../../../Card"; +import { Card } from "../../../Card"; import { MiniAreaChart } from "../MiniAreaChart"; // Simple array of numbers for 1D area chart diff --git a/js/packages/react-ui/src/components/Charts/MiniAreaChart/types/index.ts b/js/packages/react-ui/src/components/Charts/MiniAreaChart/types/index.ts new file mode 100644 index 000000000..9a51c7451 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/MiniAreaChart/types/index.ts @@ -0,0 +1 @@ +export type MiniAreaChartData = Array | Array<{ value: number; label?: string }>; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx b/js/packages/react-ui/src/components/Charts/MiniBarChart/MiniBarChart.tsx similarity index 94% rename from js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx rename to js/packages/react-ui/src/components/Charts/MiniBarChart/MiniBarChart.tsx index e6bc61874..47aeb26e6 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/MiniBarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/MiniBarChart/MiniBarChart.tsx @@ -1,11 +1,11 @@ import clsx from "clsx"; import { useEffect, useMemo, useRef, useState } from "react"; import { Bar, BarChart, XAxis } from "recharts"; -import { useTheme } from "../../../ThemeProvider"; -import { ChartConfig, ChartContainer } from "../../Charts"; -import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; +import { useTheme } from "../../ThemeProvider"; import { LineInBarShape } from "../BarChart/components/LineInBarShape"; -import { type MiniBarChartData } from "../types"; +import { ChartConfig, ChartContainer } from "../Charts"; +import { getDistributedColors, getPalette, PaletteName } from "../utils/PalletUtils"; +import { type MiniBarChartData } from "./types"; import { getPadding, getRecentDataThatFits, diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/dependencies.ts b/js/packages/react-ui/src/components/Charts/MiniBarChart/dependencies.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/dependencies.ts rename to js/packages/react-ui/src/components/Charts/MiniBarChart/dependencies.ts diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/index.ts b/js/packages/react-ui/src/components/Charts/MiniBarChart/index.ts similarity index 67% rename from js/packages/react-ui/src/components/Charts/BarCharts/index.ts rename to js/packages/react-ui/src/components/Charts/MiniBarChart/index.ts index 16f85d4dc..02620bc68 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/index.ts +++ b/js/packages/react-ui/src/components/Charts/MiniBarChart/index.ts @@ -1,3 +1,2 @@ -export * from "./BarChart"; export * from "./MiniBarChart"; export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/miniBarChart.scss b/js/packages/react-ui/src/components/Charts/MiniBarChart/miniBarChart.scss similarity index 100% rename from js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/miniBarChart.scss rename to js/packages/react-ui/src/components/Charts/MiniBarChart/miniBarChart.scss diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/MiniBarChart/stories/MiniBarChart.stories.tsx similarity index 99% rename from js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx rename to js/packages/react-ui/src/components/Charts/MiniBarChart/stories/MiniBarChart.stories.tsx index ffbe54030..c8aa94efb 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/stories/MiniBarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/MiniBarChart/stories/MiniBarChart.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Monitor } from "lucide-react"; -import { Card } from "../../../../Card"; +import { Card } from "../../../Card"; import { MiniBarChart, MiniBarChartProps } from "../MiniBarChart"; // Simple array of numbers for 1D bar chart diff --git a/js/packages/react-ui/src/components/Charts/MiniBarChart/types/index.ts b/js/packages/react-ui/src/components/Charts/MiniBarChart/types/index.ts new file mode 100644 index 000000000..51f9a0492 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/MiniBarChart/types/index.ts @@ -0,0 +1 @@ +export type MiniBarChartData = Array | Array<{ value: number; label?: string }>; diff --git a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/utils/miniBarChartUtils.ts b/js/packages/react-ui/src/components/Charts/MiniBarChart/utils/miniBarChartUtils.ts similarity index 98% rename from js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/utils/miniBarChartUtils.ts rename to js/packages/react-ui/src/components/Charts/MiniBarChart/utils/miniBarChartUtils.ts index ee839b1c6..28b01f92b 100644 --- a/js/packages/react-ui/src/components/Charts/BarCharts/MiniBarChart/utils/miniBarChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/MiniBarChart/utils/miniBarChartUtils.ts @@ -1,4 +1,4 @@ -import { type MiniBarChartData } from "../../types"; +import { type MiniBarChartData } from "../types"; export const MINI_BAR_WIDTH: number = 8; export const MINI_ELEMENT_SPACING: number = 10; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx b/js/packages/react-ui/src/components/Charts/MiniLineChart/MiniLineChart.tsx similarity index 93% rename from js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx rename to js/packages/react-ui/src/components/Charts/MiniLineChart/MiniLineChart.tsx index e289dd0cf..9b6b1e827 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/MiniLineChart.tsx +++ b/js/packages/react-ui/src/components/Charts/MiniLineChart/MiniLineChart.tsx @@ -1,13 +1,13 @@ import clsx from "clsx"; import { useEffect, useMemo, useRef, useState } from "react"; import { Line, LineChart as RechartsLineChart, XAxis } from "recharts"; -import { ChartConfig, ChartContainer } from "../../Charts"; +import { ChartConfig, ChartContainer } from "../Charts"; import { getRecentDataThatFits, transformDataForChart, -} from "../../utils/AreaAndLine/MiniAreaAndLineUtils"; -import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; -import { MiniLineChartData } from "../types"; +} from "../utils/AreaAndLine/MiniAreaAndLineUtils"; +import { getDistributedColors, getPalette, PaletteName } from "../utils/PalletUtils"; +import { MiniLineChartData } from "./types"; export interface MiniLineChartProps { data: MiniLineChartData; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/dependencies.ts b/js/packages/react-ui/src/components/Charts/MiniLineChart/dependencies.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/dependencies.ts rename to js/packages/react-ui/src/components/Charts/MiniLineChart/dependencies.ts diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/index.ts b/js/packages/react-ui/src/components/Charts/MiniLineChart/index.ts similarity index 66% rename from js/packages/react-ui/src/components/Charts/LineCharts/index.ts rename to js/packages/react-ui/src/components/Charts/MiniLineChart/index.ts index 3f81940f9..c489b3a1b 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/index.ts +++ b/js/packages/react-ui/src/components/Charts/MiniLineChart/index.ts @@ -1,3 +1,2 @@ -export * from "./LineChart"; export * from "./MiniLineChart"; export * from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx b/js/packages/react-ui/src/components/Charts/MiniLineChart/stories/MiniLineChart.stories.tsx similarity index 99% rename from js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx rename to js/packages/react-ui/src/components/Charts/MiniLineChart/stories/MiniLineChart.stories.tsx index 59a188bd4..ba0ed9e4f 100644 --- a/js/packages/react-ui/src/components/Charts/LineCharts/MiniLineChart/stories/MiniLineChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/MiniLineChart/stories/MiniLineChart.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Card } from "../../../../Card"; +import { Card } from "../../../Card"; import { MiniLineChart } from "../MiniLineChart"; // Simple array of numbers for 1D line chart diff --git a/js/packages/react-ui/src/components/Charts/MiniLineChart/types/index.ts b/js/packages/react-ui/src/components/Charts/MiniLineChart/types/index.ts new file mode 100644 index 000000000..59487ff9f --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/MiniLineChart/types/index.ts @@ -0,0 +1 @@ +export type MiniLineChartData = Array | Array<{ value: number; label?: string }>; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/PieChart.tsx b/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx similarity index 96% rename from js/packages/react-ui/src/components/Charts/PieCharts/PieChart/PieChart.tsx rename to js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx index 982a308f4..b7a55b9e2 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/PieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx @@ -1,19 +1,19 @@ import clsx from "clsx"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; -import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts.js"; -import { useTransformedKeys } from "../../hooks"; -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 { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils.js"; -import { PieChartData } from "../types/index.js"; +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../Charts.js"; +import { 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 { getDistributedColors, getPalette, PaletteName } from "../utils/PalletUtils.js"; import { createGradientDefinitions, MAX_CHART_SIZE, MIN_CHART_SIZE, } from "./components/PieChartRenderers.js"; +import { PieChartData } from "./types/index.js"; import { calculateTwoLevelChartDimensions, createAnimationConfig, diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/components/PieChartRenderers.tsx b/js/packages/react-ui/src/components/Charts/PieChart/components/PieChartRenderers.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/PieCharts/PieChart/components/PieChartRenderers.tsx rename to js/packages/react-ui/src/components/Charts/PieChart/components/PieChartRenderers.tsx diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/index.ts b/js/packages/react-ui/src/components/Charts/PieChart/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/PieCharts/index.ts rename to js/packages/react-ui/src/components/Charts/PieChart/index.ts diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/pieChart.scss b/js/packages/react-ui/src/components/Charts/PieChart/pieChart.scss similarity index 97% rename from js/packages/react-ui/src/components/Charts/PieCharts/PieChart/pieChart.scss rename to js/packages/react-ui/src/components/Charts/PieChart/pieChart.scss index a1cb2f287..9e9a51c67 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/pieChart.scss +++ b/js/packages/react-ui/src/components/Charts/PieChart/pieChart.scss @@ -1,4 +1,4 @@ -@use "../../../../cssUtils" as cssUtils; +@use "../../../cssUtils" as cssUtils; // Main wrapper for the entire chart component .crayon-pie-chart-container-wrapper { diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx b/js/packages/react-ui/src/components/Charts/PieChart/stories/PieChart.stories.tsx similarity index 99% rename from js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx rename to js/packages/react-ui/src/components/Charts/PieChart/stories/PieChart.stories.tsx index 05fbe99ce..fcd180e9a 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/stories/PieChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/PieChart/stories/PieChart.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { useState } from "react"; -import { Card } from "../../../../Card"; +import { Card } from "../../../Card"; import { PieChart, PieChartProps } from "../PieChart"; const monthlySalesData = [ diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/PieChart/types/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/PieCharts/types/index.ts rename to js/packages/react-ui/src/components/Charts/PieChart/types/index.ts diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/utils/PieChartUtils.ts b/js/packages/react-ui/src/components/Charts/PieChart/utils/PieChartUtils.ts similarity index 99% rename from js/packages/react-ui/src/components/Charts/PieCharts/PieChart/utils/PieChartUtils.ts rename to js/packages/react-ui/src/components/Charts/PieChart/utils/PieChartUtils.ts index a5852fba8..48c80dcbc 100644 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/utils/PieChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/PieChart/utils/PieChartUtils.ts @@ -2,7 +2,7 @@ * Utility functions for pie charts */ import { useState } from "react"; -import { PieChartData } from "../../types"; +import { PieChartData } from "../types"; export interface ChartDimensions { outerRadius: number; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/index.ts b/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/index.ts deleted file mode 100644 index 223a80128..000000000 --- a/js/packages/react-ui/src/components/Charts/PieCharts/PieChart/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./PieChart"; diff --git a/js/packages/react-ui/src/components/Charts/PieCharts/index.scss b/js/packages/react-ui/src/components/Charts/PieCharts/index.scss deleted file mode 100644 index 080e04c13..000000000 --- a/js/packages/react-ui/src/components/Charts/PieCharts/index.scss +++ /dev/null @@ -1 +0,0 @@ -@forward "./PieChart/pieChart.scss"; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/RadarChart.tsx b/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx similarity index 94% rename from js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/RadarChart.tsx rename to js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx index 6d25842b6..8c596bdb0 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/RadarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx @@ -1,13 +1,13 @@ import clsx from "clsx"; import React, { memo, useEffect, useMemo, useRef, useState } from "react"; import { PolarAngleAxis, PolarGrid, Radar, RadarChart as RechartsRadarChart } from "recharts"; -import { ChartConfig, ChartContainer, ChartTooltip } from "../../Charts"; -import { SideBarTooltipProvider } from "../../context/SideBarTooltipContext"; -import { useTransformedKeys } from "../../hooks/useTransformKey"; -import { ActiveDot, CustomTooltipContent, DefaultLegend } from "../../shared"; -import { LegendItem } from "../../types"; -import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; -import { get2dChartConfig, getDataKeys, getLegendItems } from "../../utils/dataUtils"; +import { ChartConfig, ChartContainer, ChartTooltip } from "../Charts"; +import { SideBarTooltipProvider } from "../context/SideBarTooltipContext"; +import { useTransformedKeys } from "../hooks/useTransformKey"; +import { ActiveDot, CustomTooltipContent, DefaultLegend } from "../shared"; +import { LegendItem } from "../types"; +import { getDistributedColors, getPalette } from "../utils/PalletUtils"; +import { get2dChartConfig, getDataKeys, getLegendItems } from "../utils/dataUtils"; import { AxisLabel } from "./components/AxisLabel"; import { RadarChartData } from "./types"; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/components/AxisLabel.tsx b/js/packages/react-ui/src/components/Charts/RadarChart/components/AxisLabel.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/components/AxisLabel.tsx rename to js/packages/react-ui/src/components/Charts/RadarChart/components/AxisLabel.tsx diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/index.ts b/js/packages/react-ui/src/components/Charts/RadarChart/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/index.ts rename to js/packages/react-ui/src/components/Charts/RadarChart/index.ts diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/radarChart.scss b/js/packages/react-ui/src/components/Charts/RadarChart/radarChart.scss similarity index 95% rename from js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/radarChart.scss rename to js/packages/react-ui/src/components/Charts/RadarChart/radarChart.scss index 8fd1084bc..a1a5bfe7b 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/radarChart.scss +++ b/js/packages/react-ui/src/components/Charts/RadarChart/radarChart.scss @@ -1,4 +1,4 @@ -@use "../../../../cssUtils" as cssUtils; +@use "../../../cssUtils" as cssUtils; .crayon-radar-chart-container-wrapper { display: flex; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadarChart/stories/RadarChart.stories.tsx similarity index 99% rename from js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx rename to js/packages/react-ui/src/components/Charts/RadarChart/stories/RadarChart.stories.tsx index c1f22930c..18759c702 100644 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/stories/RadarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarChart/stories/RadarChart.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Shield, Star, Target } from "lucide-react"; import { useState } from "react"; -import { Card } from "../../../../Card"; +import { Card } from "../../../Card"; import { RadarChart, RadarChartProps } from "../RadarChart"; // 📊 COMPREHENSIVE DATA VARIATIONS - Designed to test various radar chart scenarios diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/types/index.ts b/js/packages/react-ui/src/components/Charts/RadarChart/types/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/types/index.ts rename to js/packages/react-ui/src/components/Charts/RadarChart/types/index.ts diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/utils/index.ts b/js/packages/react-ui/src/components/Charts/RadarChart/utils/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/RadarCharts/RadarChart/utils/index.ts rename to js/packages/react-ui/src/components/Charts/RadarChart/utils/index.ts diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/index.scss b/js/packages/react-ui/src/components/Charts/RadarCharts/index.scss deleted file mode 100644 index e6535cba6..000000000 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/index.scss +++ /dev/null @@ -1 +0,0 @@ -@forward "./RadarChart/radarChart.scss"; diff --git a/js/packages/react-ui/src/components/Charts/RadarCharts/index.ts b/js/packages/react-ui/src/components/Charts/RadarCharts/index.ts deleted file mode 100644 index 536521034..000000000 --- a/js/packages/react-ui/src/components/Charts/RadarCharts/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./RadarChart"; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/RadialChart.tsx b/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx similarity index 96% rename from js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/RadialChart.tsx rename to js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx index ea16e832a..43ad69644 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/RadialChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx @@ -1,19 +1,19 @@ import clsx from "clsx"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Cell, PolarGrid, RadialBar, RadialBarChart } from "recharts"; -import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../../Charts"; -import { useTransformedKeys } from "../../hooks"; -import { DefaultLegend } from "../../shared/DefaultLegend/DefaultLegend"; -import { StackedLegend } from "../../shared/StackedLegend/StackedLegend"; -import { LegendItem } from "../../types/Legend"; -import { getCategoricalChartConfig } from "../../utils/dataUtils"; -import { getDistributedColors, getPalette, PaletteName } from "../../utils/PalletUtils"; -import { RadialChartData } from "../types"; +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../Charts"; +import { useTransformedKeys } from "../hooks"; +import { DefaultLegend } from "../shared/DefaultLegend/DefaultLegend"; +import { StackedLegend } from "../shared/StackedLegend/StackedLegend"; +import { LegendItem } from "../types/Legend"; +import { getCategoricalChartConfig } from "../utils/dataUtils"; +import { getDistributedColors, getPalette, PaletteName } from "../utils/PalletUtils"; import { createRadialGradientDefinitions, MAX_CHART_SIZE, MIN_CHART_SIZE, } from "./components/RadialChartRenderers"; +import { RadialChartData } from "./types"; import { calculateRadialChartDimensions, createRadialAnimationConfig, diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/components/RadialChartRenderers.tsx b/js/packages/react-ui/src/components/Charts/RadialChart/components/RadialChartRenderers.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/components/RadialChartRenderers.tsx rename to js/packages/react-ui/src/components/Charts/RadialChart/components/RadialChartRenderers.tsx diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/index.ts b/js/packages/react-ui/src/components/Charts/RadialChart/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/RadialCharts/index.ts rename to js/packages/react-ui/src/components/Charts/RadialChart/index.ts diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/radialChart.scss b/js/packages/react-ui/src/components/Charts/RadialChart/radialChart.scss similarity index 97% rename from js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/radialChart.scss rename to js/packages/react-ui/src/components/Charts/RadialChart/radialChart.scss index e23189489..a89a478a3 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/radialChart.scss +++ b/js/packages/react-ui/src/components/Charts/RadialChart/radialChart.scss @@ -1,4 +1,4 @@ -@use "../../../../cssUtils" as cssUtils; +@use "../../../cssUtils" as cssUtils; // Main wrapper for the entire chart component .crayon-radial-chart-container-wrapper { diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialChart/stories/RadialChart.stories.tsx similarity index 99% rename from js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChart.stories.tsx rename to js/packages/react-ui/src/components/Charts/RadialChart/stories/RadialChart.stories.tsx index 866271135..c0fc1994b 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/stories/RadialChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialChart/stories/RadialChart.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { useState } from "react"; -import { Card } from "../../../../Card"; +import { Card } from "../../../Card"; import { RadialChart, RadialChartProps } from "../RadialChart"; /** diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/types/index.ts b/js/packages/react-ui/src/components/Charts/RadialChart/types/index.ts similarity index 100% rename from js/packages/react-ui/src/components/Charts/RadialCharts/types/index.ts rename to js/packages/react-ui/src/components/Charts/RadialChart/types/index.ts diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/utils/RadialChartUtils.ts b/js/packages/react-ui/src/components/Charts/RadialChart/utils/RadialChartUtils.ts similarity index 98% rename from js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/utils/RadialChartUtils.ts rename to js/packages/react-ui/src/components/Charts/RadialChart/utils/RadialChartUtils.ts index f1afdbe6d..3b1cb7f75 100644 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/utils/RadialChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/RadialChart/utils/RadialChartUtils.ts @@ -2,8 +2,8 @@ * Utility functions for radial charts */ import { useState } from "react"; -import { getDistributedColors, getPalette } from "../../../utils/PalletUtils"; -import { RadialChartData } from "../../types"; +import { getDistributedColors, getPalette } from "../../utils/PalletUtils"; +import { RadialChartData } from "../types"; // ========================================== diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/index.ts b/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/index.ts deleted file mode 100644 index d5578ec7c..000000000 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/RadialChart/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./RadialChart"; diff --git a/js/packages/react-ui/src/components/Charts/RadialCharts/index.scss b/js/packages/react-ui/src/components/Charts/RadialCharts/index.scss deleted file mode 100644 index 7a3b98d81..000000000 --- a/js/packages/react-ui/src/components/Charts/RadialCharts/index.scss +++ /dev/null @@ -1 +0,0 @@ -@forward "./RadialChart/radialChart.scss"; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index eef5173a0..759377bed 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -1,25 +1,26 @@ @use "../../cssUtils.scss" as cssUtils; // bar chart css -@forward "./BarCharts/index.scss"; +@forward "./BarChart/barChart.scss"; +@forward "./MiniBarChart/miniBarChart.scss"; // area chart css -@forward "./AreaCharts/index.scss"; +@forward "./AreaChart/areaChart.scss"; // line chart css -@forward "./LineCharts/index.scss"; +@forward "./LineChart/lineChart.scss"; // radar chart css -@forward "./RadarCharts/index.scss"; +@forward "./RadarChart/radarChart.scss"; // segmented bar chart css @forward "./SegmentedBarChart/segmentedBarChart.scss"; // pie chart css -@forward "./PieCharts/index.scss"; +@forward "./PieChart/pieChart.scss"; // radial chart css -@forward "./RadialCharts/index.scss"; +@forward "./RadialChart/radialChart.scss"; // shared components css @forward "./shared/XAxisTick/xAxisTick.scss"; diff --git a/js/packages/react-ui/src/components/Charts/index.ts b/js/packages/react-ui/src/components/Charts/index.ts index 45e63de26..92b81eaf9 100644 --- a/js/packages/react-ui/src/components/Charts/index.ts +++ b/js/packages/react-ui/src/components/Charts/index.ts @@ -1,7 +1,7 @@ -export * from "./AreaCharts"; -export * from "./BarCharts"; -export * from "./LineCharts"; -export * from "./PieCharts"; -export * from "./RadarCharts"; -export * from "./RadialCharts"; +export * from "./AreaChart"; +export * from "./BarChart"; +export * from "./LineChart"; +export * from "./PieChart"; +export * from "./RadarChart"; +export * from "./RadialChart"; export * from "./SegmentedBarChart"; diff --git a/js/packages/react-ui/src/components/Charts/utils/AreaAndLine/AreaAndLineUtils.ts b/js/packages/react-ui/src/components/Charts/utils/AreaAndLine/AreaAndLineUtils.ts index 4112acc21..fafef7c0d 100644 --- a/js/packages/react-ui/src/components/Charts/utils/AreaAndLine/AreaAndLineUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/AreaAndLine/AreaAndLineUtils.ts @@ -1,8 +1,8 @@ // Common utility functions for Area and Line charts // These functions are chart-type agnostic and can be shared between AreaChartV2 and LineChartV2 -import { AreaChartData } from "../../AreaCharts/types"; -import { LineChartData } from "../../LineCharts/types"; +import { AreaChartData } from "../../AreaChart/types"; +import { LineChartData } from "../../LineChart/types"; const ELEMENT_SPACING = 70; diff --git a/js/packages/react-ui/src/components/Charts/utils/AreaAndLine/MiniAreaAndLineUtils.ts b/js/packages/react-ui/src/components/Charts/utils/AreaAndLine/MiniAreaAndLineUtils.ts index f50b7c0af..f4c107ef5 100644 --- a/js/packages/react-ui/src/components/Charts/utils/AreaAndLine/MiniAreaAndLineUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/AreaAndLine/MiniAreaAndLineUtils.ts @@ -1,8 +1,8 @@ // Common utility functions for Mini Area and Line charts // These functions are shared between MiniAreaChart and MiniLineChart components -import { MiniAreaChartData } from "../../AreaCharts/types"; -import { MiniLineChartData } from "../../LineCharts/types"; +import { MiniAreaChartData } from "../../MiniAreaChart/types"; +import { MiniLineChartData } from "../../MiniLineChart/types"; // Element spacing constant for both chart types export const MINI_ELEMENT_SPACING: number = 20; diff --git a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts index 27e8f7eaa..c852a68bd 100644 --- a/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/dataUtils.ts @@ -1,6 +1,6 @@ import { ChartConfig } from "../Charts"; -import { PieChartData } from "../PieCharts"; -import { RadialChartData } from "../RadialCharts"; +import { PieChartData } from "../PieChart"; +import { RadialChartData } from "../RadialChart"; import { LegendItem } from "../types"; import { getDistributedColors, getPalette } from "../utils/PalletUtils"; From 4fd4854532a97423b59d0206afab188a943159f5 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 21:25:09 +0530 Subject: [PATCH 185/190] fix(Charts): update corner radius in RadialChart and remove blur effect in Tabs styles This commit modifies the corner radius of the RadialChart component from 0 to 10 for improved aesthetics. Additionally, it removes the blur effect from the Tabs component styles to enhance clarity and visual consistency. --- .../react-ui/src/components/Charts/RadialChart/RadialChart.tsx | 2 +- js/packages/react-ui/src/components/Tabs/tabs.scss | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx b/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx index 43ad69644..6e126b758 100644 --- a/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx @@ -61,7 +61,7 @@ export const RadialChart = ({ legendVariant = "stacked", grid = false, isAnimationActive = false, - cornerRadius = 0, + cornerRadius = 10, useGradients = false, gradientColors, onMouseEnter, diff --git a/js/packages/react-ui/src/components/Tabs/tabs.scss b/js/packages/react-ui/src/components/Tabs/tabs.scss index 67d0b4063..e2c5777ad 100644 --- a/js/packages/react-ui/src/components/Tabs/tabs.scss +++ b/js/packages/react-ui/src/components/Tabs/tabs.scss @@ -109,7 +109,6 @@ align-items: center; justify-content: center; padding: 0 10px; - filter: blur(0.9); padding: 0 4px 0 0; background-color: cssUtils.$bg-container; } @@ -123,7 +122,6 @@ align-items: center; justify-content: center; padding: 0 10px; - filter: blur(0.9); padding: 0 0 0 4px; background-color: cssUtils.$bg-container; } From 60f731774f804269430b9916f74a2995d0c2e14d Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 21:44:46 +0530 Subject: [PATCH 186/190] refactor(Charts): update gradient definitions in AreaChart for consistency This commit modifies the gradient definitions in the AreaChart component by using transformed keys for gradient IDs. This change enhances consistency in the gradient rendering and improves the overall clarity of the chart's visual elements. --- .../src/components/Charts/AreaChart/AreaChart.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx b/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx index 302ce6edf..ff3738c9f 100644 --- a/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx @@ -344,8 +344,14 @@ const AreaChartComponent = ({ const transformedKey = transformedKeys[key]; const color = `var(--color-${transformedKey})`; return ( - - + + @@ -362,7 +368,7 @@ const AreaChartComponent = ({ dataKey={key} type={variant} stroke={color} - fill={`url(#${gradientID}-${key})`} + fill={`url(#${gradientID}-${transformedKey})`} fillOpacity={1} stackId="a" activeDot={} From eb7f27e6790452d4f78834762f268253195dbe6d Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Jun 2025 23:46:22 +0530 Subject: [PATCH 187/190] style(Charts): update BarChart button styles for improved hover effect This commit adds a background color to the BarChart scroll button and enhances the hover effect by applying the same background color. These changes aim to improve the visual consistency and user experience of the BarChart component. --- .../react-ui/src/components/Charts/BarChart/barChart.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/js/packages/react-ui/src/components/Charts/BarChart/barChart.scss b/js/packages/react-ui/src/components/Charts/BarChart/barChart.scss index 5e8ab0099..77438cbd9 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/barChart.scss +++ b/js/packages/react-ui/src/components/Charts/BarChart/barChart.scss @@ -30,6 +30,11 @@ .crayon-bar-chart-scroll-button { position: absolute; + background-color: cssUtils.$bg-container; + + &:hover { + background-color: cssUtils.$bg-container; + } &--left { top: -15px; From 0b152c5e75b14d514ab70d836983f0f5c523d1d1 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 1 Jul 2025 00:10:35 +0530 Subject: [PATCH 188/190] docs(Charts): enhance documentation and stories for AreaChart, BarChart, and LineChart components This commit updates the documentation for the AreaChart, BarChart, and LineChart components, providing detailed descriptions of their features, usage, and data structure requirements. Additionally, it improves the stories for these components, showcasing various scenarios such as label collision, data density, and responsive behavior. The changes aim to enhance clarity and usability for developers working with these chart components. --- .../AreaChart/stories/areaChart.stories.tsx | 551 ++++++++++++------ .../BarChart/stories/barChart.stories.tsx | 438 +++++++++++--- .../LineChart/stories/lineChart.stories.tsx | 455 ++++++++++++--- .../stories/MiniBarChart.stories.tsx | 2 +- .../stories/MiniLineChart.stories.tsx | 2 +- .../stories/SegmentedBarChart.stories.tsx | 2 +- 6 files changed, 1119 insertions(+), 331 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/AreaChart/stories/areaChart.stories.tsx b/js/packages/react-ui/src/components/Charts/AreaChart/stories/areaChart.stories.tsx index 9d5a1ec73..283cd5651 100644 --- a/js/packages/react-ui/src/components/Charts/AreaChart/stories/areaChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaChart/stories/areaChart.stories.tsx @@ -445,153 +445,202 @@ const icons = { sessions: TabletSmartphone, } as const; +/** + * # AreaChart Component Documentation + * + * The AreaChart is excellent for visualizing quantitative data and showing the volume of a metric over a progression. + * It's ideal for: + * + * - **Time-Series Analysis**: Showing how a value or volume changes over time. + * - **Showing Volume**: Emphasizing the magnitude of a value. + * - **Comparing Trends**: Stacking areas to show how different series contribute to a total. + * + * ## Key Features + * + * ### Advanced Label Handling + * - **Collision Detection**: Prevents X-axis labels from overlapping. + * - **Intelligent Truncation**: Automatically shortens long labels with an ellipsis (...). + * - **Horizontal Offset**: Ensures labels are correctly positioned for maximum readability. + * + * ### Interactive & Responsive + * - **Interactive Legend**: Toggle data series visibility. It gracefully handles overflow with a "Show More" feature. + * - **Hover Tooltips**: Choose between a standard tooltip or a floating one that follows the cursor. + * - **Responsive Design**: Adapts fluidly to its container's size. + * + * ### Customization + * - **Theming**: Comes with six pre-built color palettes. + * - **Area Styles**: Supports `linear`, `natural` (smooth), and `step` interpolation. + * - **Styling Options**: Control grid visibility, axes, and more. + */ const meta: Meta> = { - title: "Components/Charts/AreaCharts/AreaChartV2", + title: "Components/Charts/AreaChart", component: AreaChart, parameters: { layout: "centered", docs: { description: { - component: - "```tsx\nimport { AreaChartV2 } from '@crayon-ui/react-ui/Charts/AreaChartV2';\n```\n\nAreaChartV2 features advanced collision detection and label truncation with horizontal offset capabilities. The component automatically handles overlapping X-axis labels by intelligently truncating them with ellipsis while maintaining horizontal positioning (no angle rotation by default).", + component: ` +## Installation and Basic Usage + +\`\`\`tsx +import { AreaChart } from '@crayon-ui/react-ui/Charts/AreaChart'; + +const trafficData = [ + { date: "2023-01-01", visits: 2200 }, + { date: "2023-01-02", visits: 2500 }, + { date: "2023-01-03", visits: 2300 }, +]; + +// Basic implementation + +\`\`\` + +## Data Structure Requirements + +Your data should be an array of objects. Each object must contain: +- A **category field** (string or number): This will be used for the X-axis labels (e.g., dates, names, etc.). +- One or more **data series fields** (number): These are the values that will be plotted on the Y-axis. The key of the field will be used as the series name in the legend and tooltip. + +\`\`\`tsx +const salesData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, +]; +\`\`\` + +## Performance Considerations +- **Data Density**: For very large datasets, consider disabling animations to maintain smooth performance. +- **Responsiveness**: The chart is fully responsive, but test label visibility on very small screens. +- **Animation**: Controlled via \`isAnimationActive={false}\`. +`, }, }, }, - tags: ["dev", "autodocs"], + tags: ["!dev", "autodocs"], argTypes: { data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", + description: ` +**Required.** An array of data objects for the area chart. Each object should contain: +- A category identifier (string/number) for the X-axis. +- One or more numeric values for the Y-axis. +`, control: false, table: { type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", + category: "📊 Data Configuration", }, }, categoryKey: { description: - "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", + "**Required.** The key in your data object that represents the category for the X-axis.", control: false, table: { type: { summary: "string" }, - defaultValue: { summary: "string" }, - category: "Data", + category: "📊 Data Configuration", }, }, theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + description: "Specifies the color palette for the chart's areas, tooltips, and legend.", control: "select", options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], table: { defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - icons: { - description: - "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", - control: false, - table: { - type: { summary: "Record" }, - defaultValue: { summary: "{}" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, variant: { - description: - "The interpolation method used to create the area curves. 'linear' creates straight lines between points, 'natural' creates smooth curves, and 'step' creates a stepped area.", + description: "Determines the area interpolation method, affecting the shape of the curves.", control: "radio", options: ["linear", "natural", "step"], table: { defaultValue: { summary: "natural" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, - - grid: { - description: "Whether to display the background grid lines in the chart", - control: "boolean", + icons: { + description: + "An object mapping data series keys to React components to be used as icons in the legend.", + control: false, table: { - type: { summary: "boolean" }, - defaultValue: { summary: "true" }, - category: "Display", + type: { summary: "Record" }, + category: "🎨 Visual Styling", }, }, - legend: { - description: - "Whether to display the chart legend showing the data series names and their corresponding colors/icons", + grid: { + description: "Toggles the visibility of the background grid lines.", control: "boolean", table: { - type: { summary: "boolean" }, defaultValue: { summary: "true" }, - category: "Display", + category: "📱 Display Options", }, }, - isAnimationActive: { - description: "Whether to animate the chart", + legend: { + description: "Toggles the visibility of the chart legend.", control: "boolean", table: { - type: { summary: "boolean" }, defaultValue: { summary: "true" }, - category: "Display", + category: "📱 Display Options", }, }, showYAxis: { - description: "Whether to display the y-axis", + description: "Toggles the visibility of the Y-axis line and labels.", control: "boolean", table: { - type: { summary: "boolean" }, - defaultValue: { summary: "false" }, - category: "Display", + defaultValue: { summary: "true" }, + category: "📱 Display Options", }, }, xAxisLabel: { - description: "The label for the x-axis", + description: "A label to display below the X-axis.", control: "text", table: { - type: { summary: "string" }, - defaultValue: { summary: "undefined" }, - category: "Data", + type: { summary: "React.ReactNode" }, + category: "📱 Display Options", }, }, yAxisLabel: { - description: "The label for the y-axis", + description: "A label to display beside the Y-axis.", control: "text", table: { - type: { summary: "string" }, - defaultValue: { summary: "undefined" }, - category: "Data", + type: { summary: "React.ReactNode" }, + category: "📱 Display Options", + }, + }, + isAnimationActive: { + description: "Enables or disables the initial loading animation for the areas.", + control: "boolean", + table: { + defaultValue: { summary: "true" }, + category: "🎬 Animation & Interaction", }, }, height: { - description: - "Fixed height for the chart in pixels. When provided, overrides the default responsive height calculation.", + description: "Sets a fixed height for the chart container in pixels.", control: "number", table: { type: { summary: "number" }, - defaultValue: { summary: "undefined" }, - category: "Layout", + category: "Layout & Sizing", }, }, width: { - description: - "Fixed width for the chart container in pixels. When provided, disables responsive width behavior.", + description: "Sets a fixed width for the chart container in pixels.", control: "number", table: { type: { summary: "number" }, - defaultValue: { summary: "undefined" }, - category: "Layout", + category: "Layout & Sizing", }, }, className: { - description: "Additional CSS class name for the chart container", + description: "Custom CSS class to apply to the chart's container.", control: "text", table: { type: { summary: "string" }, - defaultValue: { summary: "undefined" }, - category: "Layout", + category: "Layout & Sizing", }, }, }, @@ -600,8 +649,20 @@ const meta: Meta> = { export default meta; type Story = StoryObj; -export const AreaChartV2Story: Story = { - name: "🎛️ Data Switcher - Area Chart V2 (Big Labels Focus)", +/** + * ## Comprehensive Data Explorer + * + * This story serves as a comprehensive test suite for the AreaChart component. + * Use the buttons to switch between various datasets designed to test edge cases in: + * + * - **Label Collision**: Datasets with long, dense, or overlapping labels. + * - **Data Density**: Scenarios with few or many data points. + * - **Legend Overflow**: A dataset with many series to test legend expand/collapse functionality. + * + * It's an excellent tool for developers to see how the chart behaves under different conditions. + */ +export const DataExplorer: Story = { + name: "🎛️ Comprehensive Data Explorer", args: { data: areaChartData, categoryKey: "month", @@ -613,8 +674,6 @@ export const AreaChartV2Story: Story = { showYAxis: true, xAxisLabel: "Time Period", yAxisLabel: "Values", - floatingTooltip: true, - // width: 600, // height: 300, }, render: (args: any) => { @@ -750,6 +809,13 @@ export const AreaChartV2Story: Story = { }, }; +/** + * ## Big Labels + * + * This story demonstrates the chart's ability to handle very long category labels. + * The chart's intelligent truncation and collision detection should be visible here. + * X-axis labels that are too long to fit will be gracefully truncated with an ellipsis. + */ export const BigLabelsStory: Story = { name: "🏷️ Big Labels (Collision Detection)", args: { @@ -777,6 +843,13 @@ export const BigLabelsStory: Story = { }, }; +/** + * ## Dense Timeline + * + * Tests how the chart handles many data points with medium-length period labels. + * The chart should enable horizontal scrolling and apply intelligent label truncation. + * This scenario is common in financial or analytical dashboards where many time periods are displayed. + */ export const DenseTimelineStory: Story = { name: "📅 Dense Timeline (Many Periods)", args: { @@ -804,6 +877,12 @@ export const DenseTimelineStory: Story = { }, }; +/** + * ## Company Names + * + * This story tests the chart with real-world company names that have varying lengths. + * It demonstrates how the chart handles business data with naturally occurring long labels. + */ export const CompanyNamesStory: Story = { name: "🏢 Company Names (Real-world Labels)", args: { @@ -831,6 +910,12 @@ export const CompanyNamesStory: Story = { }, }; +/** + * ## Country Names + * + * This story tests the chart with official country names, which are often long. + * It shows how geographic data is handled by the label collision detection system. + */ export const CountryDataStory: Story = { name: "🌍 Country Names (Geographic Labels)", args: { @@ -858,6 +943,12 @@ export const CountryDataStory: Story = { }, }; +/** + * ## Mixed Lengths + * + * This story tests the chart's ability to handle a mix of very short and very long labels + * within the same dataset, demonstrating its adaptive truncation behavior. + */ export const MixedLengthsStory: Story = { name: "🔤 Mixed Lengths (Varied Label Sizes)", args: { @@ -885,6 +976,12 @@ export const MixedLengthsStory: Story = { }, }; +/** + * ## Edge Cases + * + * This story pushes the collision detection and truncation system to its limits + * with exceptionally long labels and minimal data points. + */ export const EdgeCasesStory: Story = { name: "🎯 Edge Cases (Extreme Scenarios)", args: { @@ -912,6 +1009,12 @@ export const EdgeCasesStory: Story = { }, }; +/** + * ## Minimal Data + * + * A baseline test with a small dataset and standard-length labels. This serves as a control + * case to compare against the more complex label scenarios. + */ export const MinimalDataStory: Story = { name: "📱 Minimal Data (Baseline)", args: { @@ -939,8 +1042,15 @@ export const MinimalDataStory: Story = { }, }; +/** + * ## Legend Expand/Collapse + * + * Tests the legend's expand/collapse functionality with a large number of data series. + * The legend should automatically show a "Show More" button when items overflow, + * allowing users to toggle between a collapsed and expanded view. + */ export const ExpandCollapseMarketingStory: Story = { - name: "🔄 Marketing Channels (Legend Expand/Collapse)", + name: "🔄 Legend Expand/Collapse", args: { data: dataVariations.expandCollapseMarketing as any, categoryKey: "channel" as any, @@ -966,8 +1076,19 @@ export const ExpandCollapseMarketingStory: Story = { }, }; -export const ResponsiveWidthStory: Story = { - name: "📐 Responsive Width Test", +/** + * ## Responsive Behavior Demo + * + * This story demonstrates the responsive capabilities of the AreaChart. Drag the handles + * on the container to resize it and observe how the chart adapts. + * + * **Responsive Features:** + * - **Label Truncation**: More aggressive truncation on smaller container widths. + * - **Layout Adaptation**: Chart elements adjust to fit the available space. + * - **Legend Behavior**: The legend may wrap or show expand/collapse buttons as needed. + */ +export const ResponsiveBehaviorDemo: Story = { + name: "📱 Responsive Behavior Demo", args: { data: dataVariations.bigLabels as any, categoryKey: "category" as any, @@ -975,128 +1096,178 @@ export const ResponsiveWidthStory: Story = { variant: "natural", grid: true, legend: true, - isAnimationActive: true, + isAnimationActive: false, showYAxis: true, }, - render: (args: any) => ( -
-
-

- Small Container (300px) -

- - - -
-
-

- Medium Container (500px) -

- - - -
-
-

- Large Container (800px) -

- + render: (args: any) => { + const [dimensions, setDimensions] = useState<{ width: number; height: number | string }>({ + width: 700, + height: "auto", + }); + + const handleMouseDown = (e: React.MouseEvent, handle: string) => { + e.preventDefault(); + e.stopPropagation(); + const startX = e.clientX; + const startY = e.clientY; + const startWidth = dimensions.width; + const startHeight = (e.currentTarget.parentElement as HTMLElement).offsetHeight; + + const doDrag = (e: MouseEvent) => { + const dx = e.clientX - startX; + const dy = e.clientY - startY; + let newWidth = startWidth; + let newHeight = startHeight; + + if (handle.includes("e")) newWidth = startWidth + dx; + if (handle.includes("w")) newWidth = startWidth - dx; + if (handle.includes("s")) newHeight = startHeight + dy; + if (handle.includes("n")) newHeight = startHeight - dy; + + setDimensions({ + width: Math.max(300, newWidth), + height: "auto", + }); + }; + + const stopDrag = () => { + document.removeEventListener("mousemove", doDrag); + document.removeEventListener("mouseup", stopDrag); + }; + + document.addEventListener("mousemove", doDrag); + document.addEventListener("mouseup", stopDrag); + }; + + const handleStyle: React.CSSProperties = { + position: "absolute", + background: "#3b82f6", + opacity: 0.6, + zIndex: 10, + transition: "opacity 0.2s ease", + }; + + return ( +
+
+

+ Responsive Behavior Demonstration +

+

+ Drag the edges or corners of the container below to see how the chart adapts +

+
+ 🎯 Try This: Resize the container to see automatic label truncation and + layout adjustments. +
+
+ + - -
-
- ), - parameters: { - docs: { - description: { - story: - "Tests how the collision detection and truncation system adapts to different container widths. Smaller containers should show more aggressive truncation.", - }, - }, - }, -}; -export const FloatingTooltipStory: Story = { - name: "🎯 Floating Tooltip (Mouse Following)", - args: { - data: areaChartData, - categoryKey: "month", - theme: "ocean", - variant: "natural", - grid: true, - legend: true, - isAnimationActive: true, - showYAxis: true, - floatingTooltip: true, - xAxisLabel: "Time Period", - yAxisLabel: "Values", - }, - render: (args: any) => ( -
-
- 🎯 Floating Tooltip Demo: -

- Move your mouse over the chart to see the floating tooltip that follows your cursor. This - tooltip uses Floating UI for intelligent positioning and collision detection. -

-
- - - -
- -

- Default Tooltip -

- -
- -

- Floating Tooltip -

- + {/* Resize Handles */} +
handleMouseDown(e, "n")} + /> +
handleMouseDown(e, "s")} + /> +
handleMouseDown(e, "w")} + /> +
handleMouseDown(e, "e")} + /> + + {/* Corner Handles */} +
handleMouseDown(e, "nw")} + /> +
handleMouseDown(e, "ne")} + /> +
handleMouseDown(e, "se")} + /> + + {/* Dimension Display */} +
+ {dimensions.width}px ×{" "} + {typeof dimensions.height === "number" ? `${dimensions.height}px` : "auto"} +
-
- ), - parameters: { - docs: { - description: { - story: - "**🎯 Floating Tooltip Demo:** This story demonstrates the new floating tooltip functionality that follows your mouse cursor. The tooltip uses Floating UI for intelligent positioning, collision detection, and smooth animations.\n\n**Key Features:**\n- ✨ **Mouse Following** - Tooltip follows your cursor in real-time\n- 🚀 **Floating UI** - Uses `@floating-ui/react-dom` for superior positioning\n- 🎯 **Smart Positioning** - Automatically adjusts position to stay on screen\n- 🔄 **Collision Detection** - Prevents tooltip from going off-screen\n- 🎨 **Custom Styling** - Fully customizable appearance\n\n**Comparison:**\n- **Default Tooltip**: Standard Recharts tooltip with fixed positioning\n- **Floating Tooltip**: Advanced tooltip that follows mouse with Floating UI positioning", - }, - source: { - code: ` -const areaChartData = [ - { month: "January", desktop: 150, mobile: 90 }, - { month: "February", desktop: 280, mobile: 180 }, - { month: "March", desktop: 220, mobile: 140 }, - // ... more data -]; - - - -`, - }, - }, + ); }, }; diff --git a/js/packages/react-ui/src/components/Charts/BarChart/stories/barChart.stories.tsx b/js/packages/react-ui/src/components/Charts/BarChart/stories/barChart.stories.tsx index 1f125653e..9a5939059 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/stories/barChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChart/stories/barChart.stories.tsx @@ -499,152 +499,204 @@ const icons = { mobile: TabletSmartphone, } as const; +/** + * # BarChart Component Documentation + * + * The BarChart is a versatile component for comparing values across different categories. + * It's highly effective for: + * + * - **Category Comparison**: Easily compare metrics like sales, users, or revenue across different groups. + * - **Ranking**: Show the highest and lowest performing items in a dataset. + * - **Data Over Time**: Visualize changes across discrete time intervals (e.g., monthly sales). + * + * ## Key Features + * + * ### Layout Variants + * - **Grouped**: Compare sub-categories side-by-side within a primary category. + * - **Stacked**: Show how sub-categories contribute to a total value for each primary category. + * + * ### Interactive & Responsive + * - **Horizontal Scrolling**: Automatically enables scrolling when the number of bars exceeds the container width. + * - **Interactive Legend**: Toggle data series on and off. Handles overflow with a "Show More" button. + * - **Hover Tooltips**: Provides detailed data on hover for better user engagement. + * - **Responsive Design**: Adjusts gracefully to the size of its container. + * + * ### Customization + * - **Theming**: Six built-in color palettes. + * - **Bar Styling**: Customize the corner radius of the bars. + * - **Axis and Grid Control**: Toggle visibility of axes and grid lines. + */ const meta: Meta> = { - title: "Components/Charts/BarCharts/BarChartV2", + title: "Components/Charts/BarChart", component: BarChart, parameters: { layout: "centered", docs: { description: { - component: - "```tsx\nimport { BarChartV2 } from '@crayon-ui/react-ui/Charts/BarChartV2';\n```", + component: ` +## Installation and Basic Usage + +\`\`\`tsx +import { BarChart } from '@crayon-ui/react-ui/Charts/BarChart'; + +const monthlyData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, +]; + +// Basic implementation + +\`\`\` + +## Data Structure Requirements + +Your data should be an array of objects. Each object must contain: +- A **category field** (string or number): Used for the X-axis labels (e.g., months, quarters). +- One or more **data series fields** (number): These are the values that will be plotted as bars. The key of the field is used as the series name in the legend and tooltip. + +\`\`\`tsx +const salesData = [ + { region: "North", sales: 5400, marketing: 1200 }, + { region: "South", sales: 8200, marketing: 1500 }, + { region: "East", sales: 7100, marketing: 1300 }, +]; +\`\`\` + +## Performance Considerations +- **Data Volume**: The chart uses virtualization for the X-axis, but performance can still be impacted by an extremely large number of data points (e.g., 1000+). +- **Responsiveness**: The chart adapts to its container, but ensure that bar widths and gaps are readable on very small screens. +- **Animation**: Can be disabled via \`isAnimationActive={false}\` for better performance with large datasets. +`, }, }, }, - tags: ["dev", "autodocs"], + tags: ["!dev", "autodocs"], argTypes: { data: { - description: - "An array of data objects where each object represents a data point. Each object should have a category field (e.g., month) and one or more numeric values for the areas to be plotted.", + description: ` +**Required.** An array of data objects representing your dataset. Each object should contain: +- A category identifier (string/number) for the X-axis. +- One or more numeric values for the Y-axis bars. +`, control: false, table: { type: { summary: "Array>" }, - defaultValue: { summary: "[]" }, - category: "Data", + category: "📊 Data Configuration", }, }, categoryKey: { description: - "The key from your data object to be used as the x-axis categories (e.g., 'month', 'year', 'date')", + "**Required.** The key in your data object that represents the category for the X-axis (e.g., 'month', 'department').", control: false, table: { type: { summary: "string" }, - defaultValue: { summary: "string" }, - category: "Data", + category: "📊 Data Configuration", }, }, theme: { - description: - "The color palette theme for the chart. Each theme provides a different set of colors for the areas.", + description: "Specifies the color palette for the chart's bars, tooltips, and legend.", control: "select", options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], table: { defaultValue: { summary: "ocean" }, - category: "Appearance", - }, - }, - icons: { - description: - "An object that maps data keys to icon components. These icons will appear in the legend next to their corresponding data series.", - control: false, - table: { - type: { summary: "Record" }, - defaultValue: { summary: "{}" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, variant: { description: - "The style of the bar chart. 'grouped' shows bars side by side, while 'stacked' shows bars stacked on top of each other.", + "Defines how multiple data series are displayed: `grouped` (side-by-side) or `stacked` (on top of each other).", control: "radio", options: ["grouped", "stacked"], table: { defaultValue: { summary: "grouped" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, radius: { - description: "The radius of the rounded corners of the bars", - control: "number", + description: "Sets the corner radius for the top of each bar, creating a rounded look.", + control: { type: "number", min: 0, max: 20 }, table: { type: { summary: "number" }, defaultValue: { summary: "4" }, - category: "Appearance", + category: "🎨 Visual Styling", + }, + }, + icons: { + description: + "An object mapping data series keys to React components to be used as icons in the legend.", + control: false, + table: { + type: { summary: "Record" }, + category: "🎨 Visual Styling", }, }, grid: { - description: "Whether to display the background grid lines in the chart", + description: "Toggles the visibility of the background grid lines.", control: "boolean", table: { - type: { summary: "boolean" }, defaultValue: { summary: "true" }, - category: "Display", + category: "📱 Display Options", }, }, - - isAnimationActive: { - description: "Whether to animate the chart", + legend: { + description: "Toggles the visibility of the chart legend.", control: "boolean", table: { - type: { summary: "boolean" }, defaultValue: { summary: "true" }, - category: "Display", + category: "📱 Display Options", }, }, showYAxis: { - description: "Whether to display the y-axis", + description: "Toggles the visibility of the Y-axis line and labels.", control: "boolean", table: { - type: { summary: "boolean" }, - defaultValue: { summary: "false" }, - category: "Display", + defaultValue: { summary: "true" }, + category: "📱 Display Options", }, }, xAxisLabel: { - description: "The label for the x-axis", + description: "A label to display below the X-axis.", control: "text", table: { - type: { summary: "string" }, - defaultValue: { summary: "undefined" }, - category: "Data", + type: { summary: "React.ReactNode" }, + category: "📱 Display Options", }, }, yAxisLabel: { - description: "The label for the y-axis", + description: "A label to display beside the Y-axis.", control: "text", table: { - type: { summary: "string" }, - defaultValue: { summary: "undefined" }, - category: "Data", + type: { summary: "React.ReactNode" }, + category: "📱 Display Options", }, }, - legend: { - description: "Whether to display the chart legend", + isAnimationActive: { + description: "Enables or disables the initial loading animation for the bars.", control: "boolean", table: { - type: { summary: "boolean" }, - defaultValue: { summary: "false" }, - category: "Display", + defaultValue: { summary: "true" }, + category: "🎬 Animation & Interaction", }, }, height: { - description: - "Fixed height for the chart in pixels. When provided, overrides the default responsive height calculation.", + description: "Sets a fixed height for the chart container in pixels.", control: "number", table: { type: { summary: "number" }, - defaultValue: { summary: "undefined" }, - category: "Layout", + category: "Layout & Sizing", }, }, width: { - description: - "Fixed width for the chart container in pixels. When provided, disables responsive width behavior.", + description: "Sets a fixed width for the chart container in pixels.", control: "number", table: { type: { summary: "number" }, - defaultValue: { summary: "undefined" }, - category: "Layout", + category: "Layout & Sizing", }, }, }, @@ -653,8 +705,20 @@ const meta: Meta> = { export default meta; type Story = StoryObj; -export const BarChartV2Story: Story = { - name: "🎛️ Data Switcher - Bar Chart V2", +/** + * ## Comprehensive Data Explorer + * + * This story serves as a comprehensive test suite for the BarChart component. + * Use the buttons to switch between various datasets designed to test edge cases in: + * + * - **Data Volume**: Scenarios with few, many, or a single data point. + * - **Data Formatting**: Handling of large numbers and different value ranges. + * - **Legend Overflow**: A dataset with many series to test legend expand/collapse functionality. + * + * It's an excellent tool for developers to see how the chart behaves under different conditions. + */ +export const DataExplorer: Story = { + name: "🎛️ Comprehensive Data Explorer", args: { data: barChartData, categoryKey: "month", @@ -664,10 +728,6 @@ export const BarChartV2Story: Story = { grid: true, isAnimationActive: true, showYAxis: true, - // xAxisLabel: "Time Period", - // yAxisLabel: "Number of Users", - legend: true, - // width: 600, // height: 500, }, render: (args: any) => { @@ -786,6 +846,12 @@ export const BarChartV2Story: Story = { }, }; +/** + * ## Small Dataset (No Scroll) + * + * This story demonstrates the BarChart's appearance with a small dataset. + * When the number of bars fits within the container width, horizontal scrolling is disabled. + */ export const SmallDataStory: Story = { name: "📱 Small Data (No Scroll)", args: { @@ -806,6 +872,12 @@ export const SmallDataStory: Story = { ), }; +/** + * ## Large Dataset (With Scrolling) + * + * This story showcases the chart's horizontal scrolling capability. + * When the total width of the bars exceeds the container's width, users can scroll to view all data points. + */ export const LargeDataStory: Story = { name: "🌐 Large Data (Scrolling)", args: { @@ -826,6 +898,12 @@ export const LargeDataStory: Story = { ), }; +/** + * ## Weekly Data (Many Categories) + * + * Similar to the large dataset, this story tests the chart with a high number of categories, + * which should trigger horizontal scrolling. + */ export const WeeklyDataStory: Story = { name: "📈 Weekly Data (Many Categories)", args: { @@ -846,6 +924,13 @@ export const WeeklyDataStory: Story = { ), }; +/** + * ## Big Numbers + * + * This story tests the chart's ability to handle and format very large numbers, + * such as billions and trillions, on the Y-axis. The axis labels should be formatted + * with appropriate suffixes (e.g., "B" for billion, "T" for trillion). + */ export const BigNumbersStory: Story = { name: "💰 Big Numbers (Billions/Trillions)", args: { @@ -866,6 +951,12 @@ export const BigNumbersStory: Story = { ), }; +/** + * ## Edge Case (Single Data Point) + * + * This story demonstrates how the chart renders with only a single data point. + * It's a useful test to ensure that the layout and axes behave correctly in minimal-data scenarios. + */ export const EdgeCaseStory: Story = { name: "🎯 Edge Case (Single Data Point)", args: { @@ -886,6 +977,12 @@ export const EdgeCaseStory: Story = { ), }; +/** + * ## Number Ranges (Scale Testing) + * + * This story tests the Y-axis scaling with data points of vastly different magnitudes. + * The chart should automatically adjust its scale to accommodate all values appropriately. + */ export const NumberRangesStory: Story = { name: "🔢 Number Ranges (Scale Testing)", args: { @@ -906,8 +1003,15 @@ export const NumberRangesStory: Story = { ), }; +/** + * ## Legend Expand/Collapse + * + * Tests the legend's expand/collapse functionality with a large number of data series. + * The legend should automatically show a "Show More" button when items overflow, + * allowing users to toggle between a collapsed and expanded view. + */ export const ExpandCollapseMarketingStory: Story = { - name: "🔄 Marketing Channels (Legend Expand/Collapse)", + name: "🔄 Legend Expand/Collapse", args: { data: dataVariations.expandCollapseMarketing as any, categoryKey: "channel" as any, @@ -933,3 +1037,199 @@ export const ExpandCollapseMarketingStory: Story = { }, }, }; + +/** + * ## Responsive Behavior Demo + * + * This story demonstrates the responsive capabilities of the BarChart. Drag the handles + * on the container to resize it and observe how the chart adapts. + * + * **Responsive Features:** + * - **Bar Sizing**: Bar widths and gaps adjust to the container size. + * - **Layout Adaptation**: Chart elements are repositioned to fit the available space. + * - **Legend Behavior**: The legend may wrap or show expand/collapse buttons as needed. + */ +export const ResponsiveBehaviorDemo: Story = { + name: "📱 Responsive Behavior Demo", + args: { + data: dataVariations.default as any, + categoryKey: "month" as any, + theme: "sunset", + variant: "grouped", + grid: true, + legend: true, + isAnimationActive: false, + showYAxis: true, + }, + render: (args: any) => { + const [dimensions, setDimensions] = useState<{ width: number; height: number | string }>({ + width: 700, + height: "auto", + }); + + const handleMouseDown = (e: React.MouseEvent, handle: string) => { + e.preventDefault(); + e.stopPropagation(); + const startX = e.clientX; + const startY = e.clientY; + const startWidth = dimensions.width; + const startHeight = (e.currentTarget.parentElement as HTMLElement).offsetHeight; + + const doDrag = (e: MouseEvent) => { + const dx = e.clientX - startX; + const dy = e.clientY - startY; + let newWidth = startWidth; + let newHeight = startHeight; + + if (handle.includes("e")) newWidth = startWidth + dx; + if (handle.includes("w")) newWidth = startWidth - dx; + if (handle.includes("s")) newHeight = startHeight + dy; + if (handle.includes("n")) newHeight = startHeight - dy; + + setDimensions({ + width: Math.max(300, newWidth), + height: "auto", + }); + }; + + const stopDrag = () => { + document.removeEventListener("mousemove", doDrag); + document.removeEventListener("mouseup", stopDrag); + }; + + document.addEventListener("mousemove", doDrag); + document.addEventListener("mouseup", stopDrag); + }; + + const handleStyle: React.CSSProperties = { + position: "absolute", + background: "#3b82f6", + opacity: 0.6, + zIndex: 10, + transition: "opacity 0.2s ease", + }; + + return ( +
+
+

+ Responsive Behavior Demonstration +

+

+ Drag the edges or corners of the container below to see how the chart adapts +

+
+ 🎯 Try This: Resize the container to see how the bar widths and layout + adjustments. +
+
+ + + + + {/* Resize Handles */} +
handleMouseDown(e, "n")} + /> +
handleMouseDown(e, "s")} + /> +
handleMouseDown(e, "w")} + /> +
handleMouseDown(e, "e")} + /> + + {/* Corner Handles */} +
handleMouseDown(e, "nw")} + /> +
handleMouseDown(e, "ne")} + /> +
handleMouseDown(e, "se")} + /> + + {/* Dimension Display */} +
+ {dimensions.width}px ×{" "} + {typeof dimensions.height === "number" ? `${dimensions.height}px` : "auto"} +
+ +
+ ); + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/LineChart/stories/lineChart.stories.tsx b/js/packages/react-ui/src/components/Charts/LineChart/stories/lineChart.stories.tsx index f45098965..ec03c1a4c 100644 --- a/js/packages/react-ui/src/components/Charts/LineChart/stories/lineChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/LineChart/stories/lineChart.stories.tsx @@ -445,84 +445,214 @@ const icons = { sessions: TabletSmartphone, } as const; +/** + * # LineChart Component Documentation + * + * The LineChart component is a powerful and flexible tool for visualizing trends over time or comparing continuous data. + * It's ideal for: + * + * - **Time-Series Analysis**: Tracking metrics over days, months, or years. + * - **Comparative Analysis**: Comparing the performance of multiple data series. + * - **Trend Identification**: Spotting upward or downward trends, and patterns in datasets. + * + * ## Key Features + * + * ### Advanced Label Handling + * - **Collision Detection**: Automatically detects and prevents overlapping X-axis labels. + * - **Intelligent Truncation**: Truncates long labels with an ellipsis (...) to keep the chart clean. + * - **Horizontal Offset**: Ensures truncated labels remain horizontally centered for readability. + * + * ### Interactive & Responsive + * - **Interactive Legend**: Allows toggling data series visibility and adapts with a "Show More" feature for many items. + * - **Hover Tooltips**: Provides detailed data points on hover for enhanced user interaction. + * - **Responsive Design**: Fluidly adjusts to any container size, from small widgets to large dashboards. + * + * ### Customization + * - **Theming**: Six pre-built color palettes to fit your application's design. + * - **Line Styles**: Supports `linear`, `natural` (smooth), and `step` variants. + * - **Styling Options**: Control stroke width, grid visibility, and more. + */ const meta: Meta> = { - title: "Components/Charts/LineCharts/LineChartV2", + title: "Components/Charts/LineChart", component: LineChart, parameters: { layout: "centered", docs: { description: { - component: - "LineChartV2 features advanced collision detection and label truncation with horizontal offset capabilities. The component automatically handles overlapping X-axis labels by intelligently truncating them with ellipsis while maintaining horizontal positioning.", + component: ` +## Installation and Basic Usage + +\`\`\`tsx +import { LineChart } from '@crayon-ui/react-ui/Charts/LineChart'; + +const timeSeriesData = [ + { month: "January", desktop: 150, mobile: 90 }, + { month: "February", desktop: 280, mobile: 180 }, + { month: "March", desktop: 220, mobile: 140 }, +]; + +// Basic implementation + +\`\`\` + +## Data Structure Requirements + +Your data should be an array of objects. Each object must contain: +- A **category field** (string or number): This will be used for the X-axis labels (e.g., dates, names, etc.). +- One or more **data series fields** (number): These are the values that will be plotted on the Y-axis. The key of the field will be used as the series name in the legend and tooltip. + +\`\`\`tsx +const salesData = [ + { date: "2023-01-01", productA: 4000, productB: 2400 }, + { date: "2023-02-01", productA: 3000, productB: 1398 }, + { date: "2023-03-01", productA: 2000, productB: 9800 }, +]; +\`\`\` + +## Performance Considerations +- **Data Density**: While the chart can handle many data points, performance may degrade with thousands of points, especially with animations enabled. +- **Responsiveness**: The chart is fully responsive, but consider the visual clarity of labels on very small screens. +- **Animation**: Can be disabled via \`isAnimationActive={false}\` for performance-critical applications. +`, }, }, }, - tags: ["dev", "autodocs"], + tags: ["!dev", "autodocs"], argTypes: { data: { - description: "An array of data objects for the line chart", + description: ` +**Required.** An array of data objects for the line chart. Each object should contain: +- A category identifier (string/number) for the X-axis. +- One or more numeric values for the Y-axis. + +**Best Practices:** +- Ensure consistent data structure across all objects. +- For time-series data, ensure items are sorted chronologically. +`, control: false, table: { type: { summary: "Array>" }, - category: "Data", + category: "📊 Data Configuration", }, }, categoryKey: { - description: "The key for x-axis categories", + description: + "**Required.** The key in your data object that represents the category for the X-axis (e.g., 'date', 'month').", control: false, table: { type: { summary: "string" }, - category: "Data", + category: "📊 Data Configuration", }, }, theme: { - description: "Color theme for the chart", + description: "Specifies the color palette for the chart's lines, tooltips, and legend.", control: "select", options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], table: { defaultValue: { summary: "ocean" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, variant: { - description: "Line interpolation method", + description: "Determines the line interpolation method, affecting the shape of the lines.", control: "radio", options: ["linear", "natural", "step"], table: { defaultValue: { summary: "natural" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, strokeWidth: { - description: "Width of the line strokes", - control: "number", + description: "Controls the thickness of the lines in pixels.", + control: { type: "number", min: 1, max: 10, step: 1 }, table: { defaultValue: { summary: "2" }, - category: "Appearance", + category: "🎨 Visual Styling", }, }, grid: { - description: "Display background grid", + description: "Toggles the visibility of the background grid lines.", control: "boolean", table: { defaultValue: { summary: "true" }, - category: "Display", + category: "📱 Display Options", }, }, legend: { - description: "Display chart legend", + description: "Toggles the visibility of the chart legend.", control: "boolean", table: { defaultValue: { summary: "true" }, - category: "Display", + category: "📱 Display Options", }, }, showYAxis: { - description: "Display y-axis", + description: "Toggles the visibility of the Y-axis line and labels.", + control: "boolean", + table: { + defaultValue: { summary: "true" }, + category: "📱 Display Options", + }, + }, + isAnimationActive: { + description: "Enables or disables the initial loading animation for the lines.", control: "boolean", table: { - defaultValue: { summary: "false" }, - category: "Display", + defaultValue: { summary: "true" }, + category: "🎬 Animation & Interaction", + }, + }, + icons: { + description: + "An object mapping data series keys to React components to be used as icons in the legend.", + control: false, + table: { + type: { summary: "Record" }, + category: "🎨 Visual Styling", + }, + }, + xAxisLabel: { + description: "A label to display below the X-axis.", + control: "text", + table: { + type: { summary: "React.ReactNode" }, + category: "📱 Display Options", + }, + }, + yAxisLabel: { + description: "A label to display beside the Y-axis.", + control: "text", + table: { + type: { summary: "React.ReactNode" }, + category: "📱 Display Options", + }, + }, + className: { + description: "Custom CSS class to apply to the chart's container.", + control: "text", + table: { + type: { summary: "string" }, + category: "Layout & Sizing", + }, + }, + height: { + description: "Sets the height of the chart container.", + control: "number", + table: { + type: { summary: "number" }, + category: "Layout & Sizing", + }, + }, + width: { + description: "Sets the width of the chart container.", + control: "number", + table: { + type: { summary: "number" }, + category: "Layout & Sizing", }, }, }, @@ -531,8 +661,20 @@ const meta: Meta> = { export default meta; type Story = StoryObj; -export const LineChartV2Story: Story = { - name: "🎛️ Data Switcher - Line Chart V2", +/** + * ## Comprehensive Data Explorer + * + * This story serves as a comprehensive test suite for the LineChart component. + * Use the buttons to switch between various datasets designed to test edge cases in: + * + * - **Label Collision**: Datasets with long, dense, or overlapping labels. + * - **Data Density**: Scenarios with few or many data points. + * - **Legend Overflow**: A dataset with many series to test legend expand/collapse functionality. + * + * It's an excellent tool for developers to see how the chart behaves under different conditions. + */ +export const DataExplorer: Story = { + name: "🎛️ Comprehensive Data Explorer", args: { data: lineChartV2Data, categoryKey: "month", @@ -655,6 +797,13 @@ export const LineChartV2Story: Story = { }, }; +/** + * ## Big Labels + * + * This story demonstrates the chart's ability to handle very long category labels. + * The chart's intelligent truncation and collision detection should be visible here. + * X-axis labels that are too long to fit will be gracefully truncated with an ellipsis. + */ export const BigLabelsStory: Story = { name: "🏷️ Big Labels", args: { @@ -674,6 +823,13 @@ export const BigLabelsStory: Story = { ), }; +/** + * ## Dense Timeline + * + * Tests how the chart handles many data points with medium-length period labels. + * The chart should enable horizontal scrolling and apply intelligent label truncation. + * This scenario is common in financial or analytical dashboards where many time periods are displayed. + */ export const DenseTimelineStory: Story = { name: "📅 Dense Timeline (Many Periods)", args: { @@ -702,6 +858,12 @@ export const DenseTimelineStory: Story = { }, }; +/** + * ## Stroke and Style Customization + * + * This story showcases how to customize the appearance of the lines. + * You can adjust properties like `strokeWidth` to create different visual effects. + */ export const StrokeCustomizationStory: Story = { name: "🎨 Stroke Customization", args: { @@ -716,27 +878,29 @@ export const StrokeCustomizationStory: Story = { render: (args: any) => (
-

Thick Lines (strokeWidth: 4)

- - - -
-
-

Dashed Lines

+

Default Lines (strokeWidth: 2)

-

With Dots

+

Thick Lines (strokeWidth: 4)

- +
), }; +/** + * ## Line Variants + * + * Compares the three available line variants: + * - **Linear**: Straight lines connecting data points. + * - **Natural**: Smooth curves for a softer, organic look. + * - **Step**: Stepped lines, useful for showing discrete changes. + */ export const VariantComparisonStory: Story = { name: "📐 Line Variants", args: { @@ -783,8 +947,15 @@ export const VariantComparisonStory: Story = { }, }; +/** + * ## Legend Expand/Collapse + * + * Tests the legend's expand/collapse functionality with a large number of data series. + * The legend should automatically show a "Show More" button when items overflow, + * allowing users to toggle between a collapsed and expanded view. + */ export const ExpandCollapseMarketingStory: Story = { - name: "🔄 Marketing Channels (Legend Expand/Collapse)", + name: "🔄 Legend Expand/Collapse", args: { data: dataVariations.expandCollapseMarketing as any, categoryKey: "channel" as any, @@ -811,8 +982,19 @@ export const ExpandCollapseMarketingStory: Story = { }, }; -export const ResponsiveWidthStory: Story = { - name: "📐 Responsive Width Test", +/** + * ## Responsive Behavior Demo + * + * This story demonstrates the responsive capabilities of the LineChart. Drag the handles + * on the container to resize it and observe how the chart adapts. + * + * **Responsive Features:** + * - **Label Truncation**: More aggressive truncation on smaller container widths. + * - **Layout Adaptation**: Chart elements adjust to fit the available space. + * - **Legend Behavior**: The legend may wrap or show expand/collapse buttons as needed. + */ +export const ResponsiveBehaviorDemo: Story = { + name: "📱 Responsive Behavior Demo", args: { data: dataVariations.bigLabels as any, categoryKey: "category" as any, @@ -820,44 +1002,179 @@ export const ResponsiveWidthStory: Story = { variant: "natural", grid: true, legend: true, - isAnimationActive: true, + isAnimationActive: false, showYAxis: true, strokeWidth: 2, }, - render: (args: any) => ( -
-
-

- Small Container (300px) -

- - - -
-
-

- Medium Container (500px) -

- - - -
-
-

- Large Container (800px) -

- + render: (args: any) => { + const [dimensions, setDimensions] = useState<{ width: number; height: number | string }>({ + width: 700, + height: "auto", + }); + + const handleMouseDown = (e: React.MouseEvent, handle: string) => { + e.preventDefault(); + e.stopPropagation(); + const startX = e.clientX; + const startY = e.clientY; + const startWidth = dimensions.width; + const startHeight = (e.currentTarget.parentElement as HTMLElement).offsetHeight; + + const doDrag = (e: MouseEvent) => { + const dx = e.clientX - startX; + const dy = e.clientY - startY; + let newWidth = startWidth; + let newHeight = startHeight; + + if (handle.includes("e")) newWidth = startWidth + dx; + if (handle.includes("w")) newWidth = startWidth - dx; + if (handle.includes("s")) newHeight = startHeight + dy; + if (handle.includes("n")) newHeight = startHeight - dy; + + setDimensions({ + width: Math.max(300, newWidth), + height: "auto", + }); + }; + + const stopDrag = () => { + document.removeEventListener("mousemove", doDrag); + document.removeEventListener("mouseup", stopDrag); + }; + + document.addEventListener("mousemove", doDrag); + document.addEventListener("mouseup", stopDrag); + }; + + const handleStyle: React.CSSProperties = { + position: "absolute", + background: "#3b82f6", + opacity: 0.6, + zIndex: 10, + transition: "opacity 0.2s ease", + }; + + return ( +
+
+

+ Responsive Behavior Demonstration +

+

+ Drag the edges or corners of the container below to see how the chart adapts +

+
+ 🎯 Try This: Resize the container to see automatic label truncation and + layout adjustments. +
+
+ + + + {/* Resize Handles */} +
handleMouseDown(e, "n")} + /> +
handleMouseDown(e, "s")} + /> +
handleMouseDown(e, "w")} + /> +
handleMouseDown(e, "e")} + /> + + {/* Corner Handles */} +
handleMouseDown(e, "nw")} + /> +
handleMouseDown(e, "ne")} + /> +
handleMouseDown(e, "se")} + /> + + {/* Dimension Display */} +
+ {dimensions.width}px ×{" "} + {typeof dimensions.height === "number" ? `${dimensions.height}px` : "auto"} +
-
- ), - parameters: { - docs: { - description: { - story: - "Tests how the collision detection and truncation system adapts to different container widths. Smaller containers should show more aggressive truncation.", - }, - }, + ); }, }; diff --git a/js/packages/react-ui/src/components/Charts/MiniBarChart/stories/MiniBarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/MiniBarChart/stories/MiniBarChart.stories.tsx index c8aa94efb..ecd296f68 100644 --- a/js/packages/react-ui/src/components/Charts/MiniBarChart/stories/MiniBarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/MiniBarChart/stories/MiniBarChart.stories.tsx @@ -44,7 +44,7 @@ const meta: Meta = { }, }, }, - tags: ["dev", "autodocs"], + tags: ["!dev", "!autodocs"], argTypes: { data: { description: diff --git a/js/packages/react-ui/src/components/Charts/MiniLineChart/stories/MiniLineChart.stories.tsx b/js/packages/react-ui/src/components/Charts/MiniLineChart/stories/MiniLineChart.stories.tsx index ba0ed9e4f..85230d7dd 100644 --- a/js/packages/react-ui/src/components/Charts/MiniLineChart/stories/MiniLineChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/MiniLineChart/stories/MiniLineChart.stories.tsx @@ -36,7 +36,7 @@ const meta: Meta = { }, }, }, - tags: ["dev", "autodocs"], + tags: ["!dev", "!autodocs"], argTypes: { data: { description: diff --git a/js/packages/react-ui/src/components/Charts/SegmentedBarChart/stories/SegmentedBarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/SegmentedBarChart/stories/SegmentedBarChart.stories.tsx index a4c5efcb2..5540fb670 100644 --- a/js/packages/react-ui/src/components/Charts/SegmentedBarChart/stories/SegmentedBarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/SegmentedBarChart/stories/SegmentedBarChart.stories.tsx @@ -43,7 +43,7 @@ const data = [25, 30, 20]; }, }, }, - tags: ["!dev", "autodocs"], + tags: ["!dev", "!autodocs"], argTypes: { data: { description: ` From 243b75f2437db83ac1bfdaf4db27b974546fb023 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 1 Jul 2025 00:19:43 +0530 Subject: [PATCH 189/190] refactor(Charts): simplify RadarChart and RadialChart components by removing unused props and optimizing calculations This commit refactors the RadarChart and RadialChart components by removing the unused `isLegendExpanded` prop from the AxisLabel component and optimizing the `calculateRadialChartDimensions` function to only require the width parameter. These changes enhance code clarity and maintainability. --- .../src/components/Charts/RadarChart/RadarChart.tsx | 7 +------ .../components/Charts/RadarChart/components/AxisLabel.tsx | 4 ++-- .../src/components/Charts/RadialChart/RadialChart.tsx | 5 +---- .../Charts/RadialChart/utils/RadialChartUtils.ts | 5 +---- 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx b/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx index 8c596bdb0..c94b7c1ed 100644 --- a/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx @@ -161,12 +161,7 @@ const RadarChartComponent = ({ {grid && } - } + tick={} /> } /> diff --git a/js/packages/react-ui/src/components/Charts/RadarChart/components/AxisLabel.tsx b/js/packages/react-ui/src/components/Charts/RadarChart/components/AxisLabel.tsx index 0aac92854..d03e3fa3d 100644 --- a/js/packages/react-ui/src/components/Charts/RadarChart/components/AxisLabel.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarChart/components/AxisLabel.tsx @@ -12,12 +12,12 @@ interface AxisLabelProps { }; className?: string; portalContainerRef?: React.RefObject; - isLegendExpanded?: boolean; + [key: string]: any; // To allow other props from recharts } export const AxisLabel: React.FC = (props) => { - const { x, y, payload, textAnchor, portalContainerRef, className, isLegendExpanded } = props; + const { x, y, payload, textAnchor, portalContainerRef, className } = props; const anchorRef = useRef(null); /** diff --git a/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx b/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx index 6e126b758..91ea4a737 100644 --- a/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx @@ -134,10 +134,7 @@ export const RadialChart = ({ ); // Calculate chart radii - const dimensions = useMemo( - () => calculateRadialChartDimensions(chartSize, variant), - [chartSize, variant], - ); + const dimensions = useMemo(() => calculateRadialChartDimensions(chartSize), [chartSize]); // Memoize expensive data transformations and configurations const transformedData = useMemo( diff --git a/js/packages/react-ui/src/components/Charts/RadialChart/utils/RadialChartUtils.ts b/js/packages/react-ui/src/components/Charts/RadialChart/utils/RadialChartUtils.ts index 3b1cb7f75..e299ff127 100644 --- a/js/packages/react-ui/src/components/Charts/RadialChart/utils/RadialChartUtils.ts +++ b/js/packages/react-ui/src/components/Charts/RadialChart/utils/RadialChartUtils.ts @@ -58,10 +58,7 @@ export const calculatePercentage = (value: number, total: number): number => { * @param variant - The chart variant ('semicircle' or 'circular') * @returns Object containing outer and inner radius values */ -export const calculateRadialChartDimensions = ( - width: number, - variant: "semicircle" | "circular", -): RadialChartDimensions => { +export const calculateRadialChartDimensions = (width: number): RadialChartDimensions => { const baseRadiusPercentage = 0.4; // 40% of container width let outerRadius = Math.round(width * baseRadiusPercentage); From ee3677f4eaa60db73e30875c5c82cc5a6f110461 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 1 Jul 2025 00:23:32 +0530 Subject: [PATCH 190/190] chore(react-ui): bump version to 0.8.1 --- js/packages/react-ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/packages/react-ui/package.json b/js/packages/react-ui/package.json index ba4ba9fba..5b0c29fe5 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.8.0", + "version": "0.8.1", "description": "Component library for Generative UI SDK", "main": "dist/index.js", "types": "dist/index.d.ts",