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 ff3738c9f..6861723ae 100644 --- a/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx @@ -24,7 +24,7 @@ import { getWidthOfData, getXAxisTickPositionData, } from "../utils/AreaAndLine/AreaAndLineUtils"; -import { getDistributedColors, getPalette, PaletteName } from "../utils/PalletUtils"; +import { PaletteName, useChartPalette } from "../utils/PalletUtils"; import { get2dChartConfig, getColorForDataKey, @@ -43,6 +43,7 @@ export interface AreaChartProps { data: T; categoryKey: keyof T[number]; theme?: PaletteName; + customPalette?: string[]; variant?: AreaChartVariant; grid?: boolean; legend?: boolean; @@ -62,6 +63,7 @@ const AreaChartComponent = ({ data, categoryKey, theme = "ocean", + customPalette, variant = "natural", grid = true, icons = {}, @@ -80,10 +82,12 @@ const AreaChartComponent = ({ const transformedKeys = useTransformedKeys(dataKeys); - const colors = useMemo(() => { - const palette = getPalette(theme); - return getDistributedColors(palette, dataKeys.length); - }, [theme, dataKeys.length]); + const colors = useChartPalette({ + chartThemeName: theme, + customPalette, + themePaletteName: "areaChartPalette", + dataLength: dataKeys.length, + }); const chartConfig: ChartConfig = useMemo(() => { return get2dChartConfig(dataKeys, colors, transformedKeys, undefined, icons); 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 283cd5651..f6d7ee0ef 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 @@ -545,7 +545,8 @@ const salesData = [ }, }, theme: { - description: "Specifies the color palette for the chart's areas, tooltips, and legend.", + description: + "Specifies the color palette for the chart's areas, tooltips, and legend. Ignored when customPalette is provided.", control: "select", options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], table: { @@ -553,6 +554,15 @@ const salesData = [ category: "🎨 Visual Styling", }, }, + customPalette: { + description: + "Custom array of colors to use instead of the theme palette. Overrides the theme prop when provided.", + control: "object", + table: { + type: { summary: "string[]" }, + category: "🎨 Visual Styling", + }, + }, variant: { description: "Determines the area interpolation method, affecting the shape of the curves.", control: "radio", @@ -1076,6 +1086,87 @@ export const ExpandCollapseMarketingStory: Story = { }, }; +/** + * ## Custom Palette + * + * This story demonstrates how to use the customPalette prop to provide your own color scheme + * for the chart. This is useful when you need to match your brand colors or create + * specific visual themes. + */ + +export const CustomPaletteStory: Story = { + name: "🎨 Custom Palette", + args: { + data: dataVariations.default as any, + categoryKey: "month" as any, + theme: "ocean", // This will be overridden by customPalette + variant: "natural", + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + xAxisLabel: "Month", + yAxisLabel: "Traffic", + }, + render: (args: any) => ( +
+
+

🎨 Custom Color Palette

+

+ This chart uses a custom color palette instead of the default theme colors. +

+
+ {args.customPalette?.map((color: string, index: number) => ( +
+
+ {color} +
+ ))} +
+
+ + + +
+ ), + parameters: { + docs: { + description: { + story: + 'Demonstrates how to use the `customPalette` prop to provide your own color scheme for the chart. When `customPalette` is provided, it overrides the `theme` prop and uses your specified colors instead of the predefined theme palettes.\n\n**Key Features:**\n- 🎨 **Custom Colors**: Override default theme colors with your own palette\n- 🔄 **Theme Override**: The `theme` prop is ignored when `customPalette` is provided\n- 📊 **Consistent Distribution**: Colors are distributed evenly across data series\n- 🎯 **Brand Matching**: Perfect for matching your application\'s brand colors\n\n**Usage:**\n```tsx\n\n```', + }, + }, + }, +}; + /** * ## Responsive Behavior Demo * 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 3a07e5b9b..c9e81dc6e 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx @@ -17,7 +17,7 @@ import { YAxisTick, } from "../shared"; import { type LegendItem } from "../types"; -import { getDistributedColors, getPalette, type PaletteName } from "../utils/PalletUtils"; +import { useChartPalette, type PaletteName } from "../utils/PalletUtils"; import { get2dChartConfig, getColorForDataKey, @@ -45,6 +45,7 @@ export interface BarChartProps { data: T; categoryKey: keyof T[number]; theme?: PaletteName; + customPalette?: string[]; variant?: BarChartVariant; grid?: boolean; radius?: number; @@ -69,6 +70,7 @@ const BarChartComponent = ({ data, categoryKey, theme = "ocean", + customPalette, variant = "grouped", grid = true, icons = {}, @@ -88,10 +90,12 @@ const BarChartComponent = ({ const transformedKeys = useTransformedKeys(dataKeys); - const colors = useMemo(() => { - const palette = getPalette(theme); - return getDistributedColors(palette, dataKeys.length); - }, [theme, dataKeys.length]); + const colors = useChartPalette({ + chartThemeName: theme, + customPalette, + themePaletteName: "barChartPalette", + dataLength: dataKeys.length, + }); const chartConfig: ChartConfig = useMemo(() => { return get2dChartConfig(dataKeys, colors, transformedKeys, undefined, icons); 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 9a5939059..65ed580a6 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 @@ -522,7 +522,7 @@ const icons = { * - **Responsive Design**: Adjusts gracefully to the size of its container. * * ### Customization - * - **Theming**: Six built-in color palettes. + * - **Theming**: Six built-in color palettes, or use custom colors with `customPalette`. * - **Bar Styling**: Customize the corner radius of the bars. * - **Axis and Grid Control**: Toggle visibility of axes and grid lines. */ @@ -551,6 +551,13 @@ const monthlyData = [ categoryKey="month" theme="ocean" /> + +// With custom colors + \`\`\` ## Data Structure Requirements @@ -599,7 +606,8 @@ const salesData = [ }, }, theme: { - description: "Specifies the color palette for the chart's bars, tooltips, and legend.", + description: + "Specifies the color palette for the chart's bars, tooltips, and legend. Ignored when customPalette is provided.", control: "select", options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], table: { @@ -607,6 +615,15 @@ const salesData = [ category: "🎨 Visual Styling", }, }, + customPalette: { + description: + "Custom array of colors to use instead of the theme palette. Overrides the theme prop when provided.", + control: "object", + table: { + type: { summary: "string[]" }, + category: "🎨 Visual Styling", + }, + }, variant: { description: "Defines how multiple data series are displayed: `grouped` (side-by-side) or `stacked` (on top of each other).", @@ -1038,6 +1055,99 @@ export const ExpandCollapseMarketingStory: Story = { }, }; +/** + * ## Custom Palette + * + * This story demonstrates how to use the customPalette prop to provide your own color scheme + * for the chart. This is useful when you need to match your brand colors or create + * specific visual themes. + */ +export const CustomPaletteStory: Story = { + name: "🎨 Custom Palette", + args: { + data: dataVariations.default as any, + categoryKey: "month" as any, + customPalette: [ + "#0A0E60", + "#14197B", + "#272DA6", + "#383FC9", + "#444CE7", + "#5F67F4", + "#7884FF", + "#97A9FF", + "#B4C6FF", + "#CBD7FF", + ], + theme: "ocean", // This will be overridden by customPalette + variant: "grouped", + radius: 4, + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + xAxisLabel: "Month", + yAxisLabel: "Traffic", + }, + render: (args: any) => ( +
+
+

🎨 Custom Color Palette

+

+ This chart uses a custom color palette instead of the default theme colors. +

+
+ {args.customPalette?.map((color: string, index: number) => ( +
+
+ {color} +
+ ))} +
+
+ + + +
+ ), + parameters: { + docs: { + description: { + story: + 'Demonstrates how to use the `customPalette` prop to provide your own color scheme for the chart. When `customPalette` is provided, it overrides the `theme` prop and uses your specified colors instead of the predefined theme palettes.\n\n**Key Features:**\n- 🎨 **Custom Colors**: Override default theme colors with your own palette\n- 🔄 **Theme Override**: The `theme` prop is ignored when `customPalette` is provided\n- 📊 **Consistent Distribution**: Colors are distributed evenly across data series\n- 🎯 **Brand Matching**: Perfect for matching your application\'s brand colors\n\n**Usage:**\n```tsx\n\n```', + }, + }, + }, +}; + /** * ## Responsive Behavior Demo * 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 1affc555e..7d79a5bd6 100644 --- a/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx +++ b/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx @@ -24,7 +24,7 @@ import { getWidthOfData, getXAxisTickPositionData, } from "../utils/AreaAndLine/AreaAndLineUtils"; -import { getDistributedColors, getPalette, PaletteName } from "../utils/PalletUtils"; +import { PaletteName, useChartPalette } from "../utils/PalletUtils"; import { get2dChartConfig, getColorForDataKey, @@ -41,6 +41,7 @@ export interface LineChartProps { data: T; categoryKey: keyof T[number]; theme?: PaletteName; + customPalette?: string[]; variant?: LineChartVariant; grid?: boolean; legend?: boolean; @@ -61,6 +62,7 @@ export const LineChart = ({ data, categoryKey, theme = "ocean", + customPalette, variant = "natural", grid = true, icons = {}, @@ -80,10 +82,12 @@ export const LineChart = ({ const transformedKeys = useTransformedKeys(dataKeys); - const colors = useMemo(() => { - const palette = getPalette(theme); - return getDistributedColors(palette, dataKeys.length); - }, [theme, dataKeys.length]); + const colors = useChartPalette({ + chartThemeName: theme, + customPalette, + themePaletteName: "lineChartPalette", + dataLength: dataKeys.length, + }); const chartConfig: ChartConfig = useMemo(() => { return get2dChartConfig(dataKeys, colors, transformedKeys, undefined, icons); 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 ec03c1a4c..29f0aaa23 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 @@ -13,6 +13,19 @@ import { useState } from "react"; import { Card } from "../../../Card"; import { LineChart, LineChartProps } from "../LineChart"; +const customColorPalette = [ + "#0A0E60", + "#14197B", + "#272DA6", + "#383FC9", + "#444CE7", + "#5F67F4", + "#7884FF", + "#97A9FF", + "#B4C6FF", + "#CBD7FF", +]; + // 🔥 COMPREHENSIVE DATA VARIATIONS - Designed to test label collision scenarios const dataVariations = { default: [ @@ -468,7 +481,7 @@ const icons = { * - **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. + * - **Theming**: Six pre-built color palettes to fit your application's design, or use custom colors with `customPalette`. * - **Line Styles**: Supports `linear`, `natural` (smooth), and `step` variants. * - **Styling Options**: Control stroke width, grid visibility, and more. */ @@ -497,6 +510,13 @@ const timeSeriesData = [ categoryKey="month" theme="ocean" /> + +// With custom colors + \`\`\` ## Data Structure Requirements @@ -549,7 +569,8 @@ const salesData = [ }, }, theme: { - description: "Specifies the color palette for the chart's lines, tooltips, and legend.", + description: + "Specifies the color palette for the chart's lines, tooltips, and legend. Ignored when customPalette is provided.", control: "select", options: ["ocean", "orchid", "emerald", "sunset", "spectrum", "vivid"], table: { @@ -557,6 +578,15 @@ const salesData = [ category: "🎨 Visual Styling", }, }, + customPalette: { + description: + "Custom array of colors to use instead of the theme palette. Overrides the theme prop when provided.", + control: "object", + table: { + type: { summary: "string[]" }, + category: "🎨 Visual Styling", + }, + }, variant: { description: "Determines the line interpolation method, affecting the shape of the lines.", control: "radio", @@ -947,6 +977,88 @@ export const VariantComparisonStory: Story = { }, }; +/** + * ## Custom Palette + * + * This story demonstrates how to use the customPalette prop to provide your own color scheme + * for the chart. This is useful when you need to match your brand colors or create + * specific visual themes. + */ +export const CustomPaletteStory: Story = { + name: "🎨 Custom Palette", + args: { + data: dataVariations.default as any, + categoryKey: "month" as any, + customPalette: customColorPalette, + theme: "ocean", // This will be overridden by customPalette + variant: "natural", + grid: true, + legend: true, + isAnimationActive: true, + showYAxis: true, + strokeWidth: 2, + xAxisLabel: "Month", + yAxisLabel: "Traffic", + }, + render: (args: any) => ( +
+
+

🎨 Custom Color Palette

+

+ This chart uses a custom color palette instead of the default theme colors. +

+
+ {args.customPalette?.map((color: string, index: number) => ( +
+
+ {color} +
+ ))} +
+
+ + + +
+ ), + parameters: { + docs: { + description: { + story: + 'Demonstrates how to use the `customPalette` prop to provide your own color scheme for the chart. When `customPalette` is provided, it overrides the `theme` prop and uses your specified colors instead of the predefined theme palettes.\n\n**Key Features:**\n- 🎨 **Custom Colors**: Override default theme colors with your own palette\n- 🔄 **Theme Override**: The `theme` prop is ignored when `customPalette` is provided\n- 📊 **Consistent Distribution**: Colors are distributed evenly across data series\n- 🎯 **Brand Matching**: Perfect for matching your application\'s brand colors\n\n**Usage:**\n```tsx\n\n```', + }, + }, + }, +}; + /** * ## Legend Expand/Collapse * diff --git a/js/packages/react-ui/src/components/Charts/MiniAreaChart/MiniAreaChart.tsx b/js/packages/react-ui/src/components/Charts/MiniAreaChart/MiniAreaChart.tsx index b401ac68b..a99407d86 100644 --- a/js/packages/react-ui/src/components/Charts/MiniAreaChart/MiniAreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/MiniAreaChart/MiniAreaChart.tsx @@ -69,7 +69,7 @@ export const MiniAreaChart = ({ const colors = useMemo(() => { const palette = getPalette(theme); - return getDistributedColors(palette, 1); // Single color for 1D chart + return getDistributedColors(palette.colors, 1); // Single color for 1D chart }, [theme]); const chartConfig: ChartConfig = useMemo(() => { diff --git a/js/packages/react-ui/src/components/Charts/MiniBarChart/MiniBarChart.tsx b/js/packages/react-ui/src/components/Charts/MiniBarChart/MiniBarChart.tsx index 47aeb26e6..63e54f625 100644 --- a/js/packages/react-ui/src/components/Charts/MiniBarChart/MiniBarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/MiniBarChart/MiniBarChart.tsx @@ -70,7 +70,7 @@ export const MiniBarChart = ({ const colors = useMemo(() => { const palette = getPalette(theme); - return getDistributedColors(palette, 1); // Single color for 1D chart + return getDistributedColors(palette.colors, 1); // Single color for 1D chart }, [theme]); const chartConfig: ChartConfig = useMemo(() => { diff --git a/js/packages/react-ui/src/components/Charts/MiniLineChart/MiniLineChart.tsx b/js/packages/react-ui/src/components/Charts/MiniLineChart/MiniLineChart.tsx index 9b6b1e827..36747a53c 100644 --- a/js/packages/react-ui/src/components/Charts/MiniLineChart/MiniLineChart.tsx +++ b/js/packages/react-ui/src/components/Charts/MiniLineChart/MiniLineChart.tsx @@ -65,7 +65,7 @@ export const MiniLineChart = ({ const colors = useMemo(() => { const palette = getPalette(theme); - return getDistributedColors(palette, 1); // Single color for 1D chart + return getDistributedColors(palette.colors, 1); // Single color for 1D chart }, [theme]); const chartConfig: ChartConfig = useMemo(() => { 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 b7a55b9e2..659679e66 100644 --- a/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx @@ -7,12 +7,7 @@ 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 { PaletteName, useChartPalette } from "../utils/PalletUtils.js"; import { PieChartData } from "./types/index.js"; import { calculateTwoLevelChartDimensions, @@ -24,16 +19,12 @@ import { useChartHover, } from "./utils/PieChartUtils.js"; -interface GradientColor { - start?: string; - end?: string; -} - export interface PieChartProps { data: T; categoryKey: keyof T[number]; dataKey: keyof T[number]; theme?: PaletteName; + customPalette?: string[]; variant?: "pie" | "donut"; format?: "percentage" | "number"; legend?: boolean; @@ -42,8 +33,6 @@ export interface PieChartProps { 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; @@ -51,12 +40,15 @@ export interface PieChartProps { } const STACKED_LEGEND_BREAKPOINT = 400; +const MIN_CHART_SIZE = 150; +const MAX_CHART_SIZE = 500; const PieChartComponent = ({ data, categoryKey, dataKey, theme = "ocean", + customPalette, variant = "pie", format = "number", legend = true, @@ -65,8 +57,6 @@ const PieChartComponent = ({ appearance = "circular", cornerRadius = 0, paddingAngle = 0, - useGradients = false, - gradientColors, onMouseEnter, onMouseLeave, onClick, @@ -82,12 +72,15 @@ const PieChartComponent = ({ const isRowLayout = legend && legendVariant === "stacked" && wrapperRect.width >= STACKED_LEGEND_BREAKPOINT; - // The data that is processed and rendered in the chart - const processedData = useMemo(() => data, [data]); + // Sort data by value (highest to lowest) for pie chart rendering + const sortedProcessedData = useMemo( + () => [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), + [data, dataKey], + ); const categories = useMemo( - () => processedData.map((item) => String(item[categoryKey])), - [processedData, categoryKey], + () => sortedProcessedData.map((item) => String(item[categoryKey])), + [sortedProcessedData, categoryKey], ); const transformedKeys = useTransformedKeys(categories); @@ -121,13 +114,13 @@ const PieChartComponent = ({ // Memoize expensive data transformations and configurations const transformedData = useMemo( - () => transformDataWithPercentages(processedData, dataKey), - [processedData, dataKey], + () => transformDataWithPercentages(sortedProcessedData as T, dataKey), + [sortedProcessedData, dataKey], ); const chartConfig = useMemo( - () => getCategoricalChartConfig(processedData, categoryKey, theme, transformedKeys), - [processedData, categoryKey, theme, transformedKeys], + () => getCategoricalChartConfig(sortedProcessedData as T, categoryKey, theme, transformedKeys), + [sortedProcessedData, categoryKey, theme, transformedKeys], ); const animationConfig = useMemo( @@ -145,53 +138,41 @@ const PieChartComponent = ({ [cornerRadius, variant, paddingAngle], ); - const palette = useMemo(() => getPalette(theme), [theme]); - const colors = useMemo( - () => getDistributedColors(palette, processedData.length), - [palette, processedData.length], - ); - - 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]); + const colors = useChartPalette({ + chartThemeName: theme, + customPalette, + themePaletteName: "pieChartPalette", + dataLength: sortedProcessedData.length, + }); const legendItems = useMemo( () => - processedData.map((item, index) => ({ + sortedProcessedData.map((item, index) => ({ key: String(item[categoryKey]), label: String(item[categoryKey]), value: Number(item[dataKey]), color: colors[index] || "#000000", })), - [processedData, categoryKey, dataKey, colors], + [sortedProcessedData, categoryKey, dataKey, colors], ); const defaultLegendItems = useMemo((): LegendItem[] => { return legendItems.map(({ key, label, color }) => ({ key, label, color })); }, [legendItems]); - const sortedData = useMemo( - () => [...processedData].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), - [processedData, dataKey], - ); - const handleLegendItemHover = useCallback( (index: number | null) => { if (legendVariant !== "stacked") return; if (index !== null) { - const item = sortedData[index]; + const item = sortedProcessedData[index]; if (item) { const categoryValue = String(item[categoryKey]); setHoveredLegendKey(categoryValue); - const processedIndex = processedData.findIndex( - (d) => String(d[categoryKey]) === categoryValue, + const transformedIndex = transformedData.findIndex( + (d) => String((d as any)[categoryKey]) === categoryValue, ); - if (processedIndex !== -1) { - handleMouseEnter(processedData[processedIndex], processedIndex); + if (transformedIndex !== -1) { + handleMouseEnter(transformedData[transformedIndex], transformedIndex); } } } else { @@ -199,7 +180,14 @@ const PieChartComponent = ({ handleMouseLeave(); } }, - [sortedData, categoryKey, processedData, handleMouseEnter, handleMouseLeave, legendVariant], + [ + sortedProcessedData, + categoryKey, + transformedData, + handleMouseEnter, + handleMouseLeave, + legendVariant, + ], ); const handleChartMouseEnter = useCallback( @@ -314,7 +302,7 @@ const PieChartComponent = ({ const transformedKey = transformedKeys[categoryValue] ?? categoryValue; const config = chartConfig[transformedKey]; const hoverStyles = getHoverStyles(index, activeIndex); - const fill = useGradients ? `url(#gradient-${index})` : config?.color || colors[index]; + const fill = config?.color || colors[index]; return ; })} , @@ -332,7 +320,7 @@ const PieChartComponent = ({ const transformedKey = transformedKeys[categoryValue] ?? categoryValue; const config = chartConfig[transformedKey]; const hoverStyles = getHoverStyles(index, activeIndex); - const fill = useGradients ? `url(#gradient-${index})` : config?.color || colors[index]; + const fill = config?.color || colors[index]; return ; })} @@ -345,7 +333,6 @@ const PieChartComponent = ({ categoryKey, chartConfig, activeIndex, - useGradients, colors, transformedKeys, ]); @@ -410,7 +397,6 @@ const PieChartComponent = ({ } /> - {gradientDefinitions && {gradientDefinitions}} {renderPieCharts()} diff --git a/js/packages/react-ui/src/components/Charts/PieChart/components/PieChartRenderers.tsx b/js/packages/react-ui/src/components/Charts/PieChart/components/PieChartRenderers.tsx deleted file mode 100644 index be1ced441..000000000 --- a/js/packages/react-ui/src/components/Charts/PieChart/components/PieChartRenderers.tsx +++ /dev/null @@ -1,82 +0,0 @@ -export const MIN_CHART_SIZE = 150; -export const MAX_CHART_SIZE = 500; - -/** - * 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); - console.warn(error); - return hex; - } -}; 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 fcd180e9a..beb115e3e 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 @@ -28,16 +28,6 @@ const comprehensiveData = [ { category: "Software", sales: 10500 }, ]; -const gradientPalette = [ - { 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" }, -]; - /** * # PieChart Component Documentation * @@ -70,7 +60,7 @@ const gradientPalette = [ * ### 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> = { @@ -164,6 +154,15 @@ const salesData = [ category: "🎨 Visual Styling", }, }, + customPalette: { + description: + "An array of color strings to use as a custom palette for the chart. This overrides the `theme` prop.", + control: "object", + table: { + type: { summary: "string[]" }, + category: "🎨 Visual Styling", + }, + }, appearance: { description: "The overall shape of the chart: a full circle or a semicircle.", control: "radio", @@ -237,15 +236,6 @@ const salesData = [ category: "🎨 Visual Styling", }, }, - useGradients: { - description: "Applies gradient fills to the pie slices instead of solid colors.", - control: "boolean", - table: { - type: { summary: "boolean" }, - defaultValue: { summary: "false" }, - category: "🎨 Visual Styling", - }, - }, }, } satisfies Meta; @@ -279,8 +269,6 @@ export const DefaultConfiguration: Story = { appearance: "circular", cornerRadius: 0, paddingAngle: 0, - useGradients: false, - gradientColors: gradientPalette, }, render: (args: any) => ( @@ -345,6 +333,36 @@ export const LayoutAndVariantOptions: Story = { ), }; +/** + * ## Custom Palette + * + * This example demonstrates how to use a custom color palette. + * + * **Feature Highlight:** + * - The `customPalette` prop allows you to define your own array of colors. + */ +export const CustomPalette: Story = { + name: "🎨 Custom Palette", + args: { + data: monthlySalesData, + categoryKey: "month", + dataKey: "value", + variant: "donut", + legend: true, + legendVariant: "stacked", + cornerRadius: 4, + paddingAngle: 2, + }, + render: (args: any) => ( + +

+ Chart with Custom Palette +

+ +
+ ), +}; + /** * ## Large Dataset with Carousel * @@ -387,7 +405,6 @@ export const ResponsiveDemo: Story = { legendVariant: "stacked", isAnimationActive: false, cornerRadius: 8, - useGradients: false, }, render: (args: any) => { const [dimensions, setDimensions] = useState<{ width: number; height: number | string }>({ 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 c94b7c1ed..108fdf099 100644 --- a/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx @@ -6,7 +6,7 @@ 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 { useChartPalette } from "../utils/PalletUtils"; import { get2dChartConfig, getDataKeys, getLegendItems } from "../utils/dataUtils"; import { AxisLabel } from "./components/AxisLabel"; import { RadarChartData } from "./types"; @@ -18,6 +18,7 @@ export interface RadarChartProps { data: T; categoryKey: keyof T[number]; theme?: "ocean" | "orchid" | "emerald" | "sunset" | "spectrum" | "vivid"; + customPalette?: string[]; variant?: "line" | "area"; grid?: boolean; legend?: boolean; @@ -31,6 +32,7 @@ const RadarChartComponent = ({ data, categoryKey, theme = "ocean", + customPalette, variant = "line", grid = true, legend = true, @@ -45,10 +47,12 @@ const RadarChartComponent = ({ const transformedKeys = useTransformedKeys(dataKeys); - const colors = useMemo(() => { - const palette = getPalette(theme); - return getDistributedColors(palette, dataKeys.length); - }, [theme, dataKeys.length]); + const colors = useChartPalette({ + chartThemeName: theme, + customPalette, + themePaletteName: "radarChartPalette", + dataLength: dataKeys.length, + }); // Create Config const chartConfig: ChartConfig = useMemo(() => { diff --git a/js/packages/react-ui/src/components/Charts/RadarChart/stories/RadarChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadarChart/stories/RadarChart.stories.tsx index 18759c702..446b1977c 100644 --- a/js/packages/react-ui/src/components/Charts/RadarChart/stories/RadarChart.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarChart/stories/RadarChart.stories.tsx @@ -104,6 +104,14 @@ import { RadarChart } from '@crayon-ui/react-ui/Charts/RadarChart'; theme="ocean" variant="line" /> + +// With custom colors + \`\`\` ## Data Structure Requirements @@ -125,6 +133,7 @@ const exampleData = [ - **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. +- **Custom Color Palettes**: Use predefined themes or provide your own custom colors. - **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. @@ -167,7 +176,7 @@ const exampleData = [ }, theme: { description: ` -**Color Theme Selection.** Choose from professionally designed color palettes: +**Color Theme Selection.** Choose from professionally designed color palettes. Ignored when customPalette is provided. - **ocean**: Cool blues and teals (professional, corporate) - **orchid**: Purple and pink tones (creative, modern) @@ -183,6 +192,15 @@ const exampleData = [ category: "🎨 Visual Styling", }, }, + customPalette: { + description: + "Custom array of colors to use instead of the theme palette. Overrides the theme prop when provided.", + control: "object", + table: { + type: { summary: "string[]" }, + category: "🎨 Visual Styling", + }, + }, variant: { description: ` **Chart Style Variant:** @@ -584,3 +602,101 @@ export const ThemeShowcase: Story = { }, }, }; + +/** + * ## Custom Palette + * + * This story demonstrates how to use the customPalette prop to provide your own color scheme + * for the chart. This is useful when you need to match your brand colors or create + * specific visual themes. + */ +export const CustomPaletteStory: Story = { + name: "🎨 Custom Palette", + args: { + data: dataVariations.productFeatures, + categoryKey: "metric", + customPalette: [ + "#0A0E60", + "#14197B", + "#272DA6", + "#383FC9", + "#444CE7", + "#5F67F4", + "#7884FF", + "#97A9FF", + "#B4C6FF", + "#CBD7FF", + ], + theme: "ocean", // This will be overridden by customPalette + variant: "area", + grid: true, + legend: true, + strokeWidth: 3, + isAnimationActive: true, + }, + render: (args: any) => ( +
+
+

🎨 Custom Color Palette

+

+ This radar chart uses a custom color palette instead of the default theme colors. +

+
+ {args.customPalette?.map((color: string, index: number) => ( +
+
+ {color} +
+ ))} +
+
+ +
+

+ Team Performance with Custom Colors +

+

+ Using custom brand colors to match your application's design system. +

+
+ +
+
+ ), + parameters: { + docs: { + description: { + story: + 'Demonstrates how to use the `customPalette` prop to provide your own color scheme for the radar chart. When `customPalette` is provided, it overrides the `theme` prop and uses your specified colors instead of the predefined theme palettes.\n\n**Key Features:**\n- 🎨 **Custom Colors**: Override default theme colors with your own palette\n- 🔄 **Theme Override**: The `theme` prop is ignored when `customPalette` is provided\n- 📊 **Consistent Distribution**: Colors are distributed evenly across data series\n- 🎯 **Brand Matching**: Perfect for matching your application\'s brand colors\n\n**Usage:**\n```tsx\n\n```', + }, + }, + }, +}; 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 91ea4a737..e94afd937 100644 --- a/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx @@ -7,12 +7,7 @@ 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 { PaletteName, useChartPalette } from "../utils/PalletUtils"; import { RadialChartData } from "./types"; import { calculateRadialChartDimensions, @@ -23,16 +18,12 @@ import { useRadialChartHover, } from "./utils/RadialChartUtils"; -interface GradientColor { - start?: string; - end?: string; -} - export interface RadialChartProps { data: T; categoryKey: keyof T[number]; dataKey: keyof T[number]; theme?: PaletteName; + customPalette?: string[]; variant?: "semicircle" | "circular"; format?: "percentage" | "number"; legend?: boolean; @@ -40,8 +31,6 @@ export interface RadialChartProps { grid?: boolean; isAnimationActive?: boolean; cornerRadius?: number; - useGradients?: boolean; - gradientColors?: GradientColor[]; onMouseEnter?: (data: any, index: number) => void; onMouseLeave?: () => void; onClick?: (data: any, index: number) => void; @@ -49,12 +38,15 @@ export interface RadialChartProps { } const STACKED_LEGEND_BREAKPOINT = 400; +const MIN_CHART_SIZE = 150; +const MAX_CHART_SIZE = 500; export const RadialChart = ({ data, categoryKey, dataKey, theme = "ocean", + customPalette, variant = "circular", format = "number", legend = true, @@ -62,8 +54,6 @@ export const RadialChart = ({ grid = false, isAnimationActive = false, cornerRadius = 10, - useGradients = false, - gradientColors, onMouseEnter, onMouseLeave, onClick, @@ -79,12 +69,15 @@ export const RadialChart = ({ const isRowLayout = legend && legendVariant === "stacked" && wrapperRect.width >= STACKED_LEGEND_BREAKPOINT; - // The data that is processed and rendered in the chart - const processedData = useMemo(() => data, [data]); + // Sort data by value (highest to lowest) for radial chart rendering + const sortedProcessedData = useMemo( + () => [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), + [data, dataKey], + ); const categories = useMemo( - () => processedData.map((item) => String(item[categoryKey])), - [processedData, categoryKey], + () => sortedProcessedData.map((item) => String(item[categoryKey])), + [sortedProcessedData, categoryKey], ); const transformedKeys = useTransformedKeys(categories); @@ -138,13 +131,13 @@ export const RadialChart = ({ // Memoize expensive data transformations and configurations const transformedData = useMemo( - () => transformRadialDataWithPercentages(processedData, dataKey, theme), - [processedData, dataKey, theme], + () => transformRadialDataWithPercentages(sortedProcessedData as T, dataKey, theme), + [sortedProcessedData, dataKey, theme], ); const chartConfig = useMemo( - () => getCategoricalChartConfig(processedData, categoryKey, theme, transformedKeys), - [processedData, categoryKey, theme, transformedKeys], + () => getCategoricalChartConfig(sortedProcessedData as T, categoryKey, theme, transformedKeys), + [sortedProcessedData, categoryKey, theme, transformedKeys], ); const animationConfig = useMemo( @@ -158,57 +151,44 @@ export const RadialChart = ({ ); // Get color palette and distribute colors - const palette = useMemo(() => getPalette(theme), [theme]); - const colors = useMemo( - () => getDistributedColors(palette, processedData.length), - [palette, processedData.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]); + const colors = useChartPalette({ + chartThemeName: theme, + customPalette, + themePaletteName: "radialChartPalette", + dataLength: sortedProcessedData.length, + }); // Create legend items for both variants const legendItems = useMemo( () => - processedData.map((item, index) => ({ + sortedProcessedData.map((item, index) => ({ key: String(item[categoryKey]), label: String(item[categoryKey]), value: Number(item[dataKey]), color: colors[index] || "#000000", })), - [processedData, categoryKey, dataKey, colors], + [sortedProcessedData, categoryKey, dataKey, colors], ); const defaultLegendItems = useMemo((): LegendItem[] => { return legendItems.map(({ key, label, color }) => ({ key, label, color })); }, [legendItems]); - // Memoize sorted data for legend hover handling - const sortedData = useMemo( - () => [...processedData].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), - [processedData, dataKey], - ); - // 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]; + const item = sortedProcessedData[index]; if (item) { const categoryValue = String(item[categoryKey]); setHoveredLegendKey(categoryValue); - const originalIndex = processedData.findIndex( - (d) => String(d[categoryKey]) === categoryValue, + // Find the index in the transformed data (which is also sorted) + const transformedIndex = transformedData.findIndex( + (d) => String((d as any)[categoryKey]) === categoryValue, ); - if (originalIndex !== -1) { - handleMouseEnter(processedData[originalIndex], originalIndex); + if (transformedIndex !== -1) { + handleMouseEnter(transformedData[transformedIndex], transformedIndex); } } } else { @@ -216,7 +196,14 @@ export const RadialChart = ({ handleMouseLeave(); } }, - [sortedData, categoryKey, processedData, handleMouseEnter, handleMouseLeave, legendVariant], + [ + sortedProcessedData, + categoryKey, + transformedData, + handleMouseEnter, + handleMouseLeave, + legendVariant, + ], ); // Enhanced chart hover handlers @@ -331,7 +318,6 @@ export const RadialChart = ({ /> } /> - {gradientDefinitions && {gradientDefinitions}} ({ 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]; + const fill = config?.color || colors[index]; return ( ); diff --git a/js/packages/react-ui/src/components/Charts/RadialChart/components/RadialChartRenderers.tsx b/js/packages/react-ui/src/components/Charts/RadialChart/components/RadialChartRenderers.tsx deleted file mode 100644 index 8f79cd5b3..000000000 --- a/js/packages/react-ui/src/components/Charts/RadialChart/components/RadialChartRenderers.tsx +++ /dev/null @@ -1,82 +0,0 @@ -export const MIN_CHART_SIZE = 150; -export const MAX_CHART_SIZE = 500; - -/** - * 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/RadialChart/stories/RadialChart.stories.tsx b/js/packages/react-ui/src/components/Charts/RadialChart/stories/RadialChart.stories.tsx index 4dbf258c8..32b58354e 100644 --- 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 @@ -43,15 +43,19 @@ const comprehensiveFinancialData = [ { 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" }, +// Custom color palette for demonstration + +const customColorPalette = [ + "#0A0E60", + "#14197B", + "#272DA6", + "#383FC9", + "#444CE7", + "#5F67F4", + "#7884FF", + "#97A9FF", + "#B4C6FF", + "#CBD7FF", ]; /** @@ -82,7 +86,7 @@ const customGradientPalette = [ * * ### 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 */ @@ -201,6 +205,26 @@ const exampleData = [ category: "🎨 Visual Styling", }, }, + customPalette: { + description: ` +**Custom Color Palette.** Override the theme colors with your own color array. + +**Usage:** +- Provide an array of hex color strings +- Colors will be applied in order to chart segments +- Takes precedence over theme colors when provided +- Useful for brand-specific color requirements + +**Example:** +\`["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4"]\` + `, + control: false, + table: { + type: { summary: "string[]" }, + defaultValue: { summary: "undefined" }, + category: "🎨 Visual Styling", + }, + }, variant: { description: ` **Chart Layout Style:** @@ -304,22 +328,6 @@ const exampleData = [ 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", - }, - }, }, } satisfies Meta; @@ -353,8 +361,6 @@ export const DefaultConfiguration: Story = { grid: false, isAnimationActive: true, cornerRadius: 10, - useGradients: false, - gradientColors: customGradientPalette, }, render: (args: any) => ( @@ -408,7 +414,6 @@ export const ThemeShowcase: Story = { grid: false, isAnimationActive: true, cornerRadius: 10, - useGradients: false, }, render: (args: any) => ( @@ -446,11 +451,10 @@ export const ThemeShowcase: Story = { /** * ## Advanced Visual Enhancement * - * This example showcases the gradient feature combined with percentage formatting - * to create visually striking and informative charts. + * This example showcases percentage formatting to create visually striking and informative charts. */ -export const GradientEnhancement: Story = { - name: "✨ Gradient Enhancement", +export const VisualEnhancement: Story = { + name: "✨ Visual Enhancement", args: { data: monthlyRevenueData.slice(0, 6), categoryKey: "month", @@ -463,8 +467,6 @@ export const GradientEnhancement: Story = { grid: false, isAnimationActive: true, cornerRadius: 15, - useGradients: true, - gradientColors: customGradientPalette, }, render: (args: any) => ( @@ -473,7 +475,7 @@ export const GradientEnhancement: Story = { Revenue Distribution

- Enhanced with gradients and percentage display + Enhanced with percentage display

@@ -483,21 +485,127 @@ export const GradientEnhancement: Story = { docs: { description: { story: ` -**Gradient Enhancement Features:** +**Visual Enhancement Features:** -- **Visual Depth**: Creates three-dimensional appearance +- **Percentage Display**: Shows data as percentages for better comparison - **Modern Aesthetic**: Aligns with contemporary design trends -- **Brand Flexibility**: Custom gradient definitions for brand alignment -- **Performance Optimized**: Efficiently rendered using SVG gradients +- **Theme Integration**: Uses theme colors for consistent visual design +- **Performance Optimized**: Efficiently rendered for smooth interactions -**When to Use Gradients:** +**When to Use Percentage Format:** - Executive dashboards and presentations - Marketing materials and public-facing reports -- When visual impact is prioritized -- With 3-8 data points for optimal effect +- When relative comparisons are important +- With 3-8 data points for optimal readability -**Technical Note:** Gradients are defined as start/end color pairs and applied -automatically to maintain visual consistency across the chart. +**Technical Note:** Percentages are automatically calculated from the data values, +providing clear relative comparisons across all chart segments. + `, + }, + }, + }, +}; + +/** + * ## Custom Color Palette + * + * This example demonstrates how to use a custom color palette to override + * the default theme colors with brand-specific or custom color schemes. + */ +export const CustomPalette: Story = { + name: "🎨 Custom Color Palette", + args: { + data: monthlyRevenueData.slice(0, 8), + categoryKey: "month", + dataKey: "value", + customPalette: customColorPalette, + variant: "circular", + format: "number", + legend: true, + legendVariant: "stacked", + grid: false, + isAnimationActive: true, + cornerRadius: 12, + }, + render: (args: any) => ( + +
+

+ Brand-Specific Color Scheme +

+

+ Using custom colors that align with your brand identity +

+
+ +
+ 🎨 Custom Palette:{" "} + {customColorPalette.slice(0, 8).map((color, index) => ( + + ))} +
+
+ ), + parameters: { + docs: { + description: { + story: ` +**Custom Palette Features:** + +- **Brand Alignment**: Use your exact brand colors for consistent visual identity +- **Override Themes**: Takes precedence over theme-based color selection +- **Flexible Array**: Provide any number of colors - they'll cycle automatically for larger datasets +- **Hex Color Support**: Standard hex color format (#RRGGBB) + +**Implementation Example:** +\`\`\`tsx +const brandColors = [ + "#FF6B6B", // Primary brand color + "#4ECDC4", // Secondary brand color + "#45B7D1", // Accent color 1 + "#96CEB4", // Accent color 2 + // ... more colors as needed +]; + + +\`\`\` + +**Best Practices:** +- Ensure sufficient contrast between adjacent colors +- Test color accessibility for users with color vision differences +- Provide at least as many colors as data points for optimal display +- Consider color psychology and brand associations when selecting colors + +**Technical Notes:** +- Colors are applied in array order to data segments +- If fewer colors than data points, colors will cycle automatically `, }, }, @@ -524,7 +632,6 @@ export const LargeDatasetDemo: Story = { grid: false, isAnimationActive: true, cornerRadius: 8, - useGradients: false, }, render: (args: any) => ( @@ -604,7 +711,6 @@ export const ResponsiveDemo: Story = { legendVariant: "stacked", isAnimationActive: false, cornerRadius: 8, - useGradients: false, }, render: (args: any) => { const [dimensions, setDimensions] = useState<{ width: number; height: number | string }>({ 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 e299ff127..126be9488 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 @@ -110,7 +110,7 @@ export const transformRadialDataWithPercentages = ( ) => { const total = data.reduce((sum, item) => sum + Number(item[dataKey]), 0); const palette = getPalette(theme); - const colors = getDistributedColors(palette, data.length); + const colors = getDistributedColors(palette.colors, data.length); return data.map((item, index) => ({ ...item, diff --git a/js/packages/react-ui/src/components/Charts/SegmentedBarChart/SegmentedBarChart.tsx b/js/packages/react-ui/src/components/Charts/SegmentedBarChart/SegmentedBarChart.tsx index 3ceb305dc..3555bb24a 100644 --- a/js/packages/react-ui/src/components/Charts/SegmentedBarChart/SegmentedBarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/SegmentedBarChart/SegmentedBarChart.tsx @@ -36,7 +36,7 @@ export const SegmentedBar = ({ // Get theme colors for each segment const colors = useMemo(() => { const palette = getPalette(theme); - return getDistributedColors(palette, Math.max(segments.length, 1)); + return getDistributedColors(palette.colors, Math.max(segments.length, 1)); }, [theme, segments.length]); // Segmented progress bar 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 6939da931..381766684 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 @@ -113,10 +113,8 @@ export const StackedLegend = ({ // 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); - - const itemsToDisplay = isShowMoreLayout && !showAll ? sortedItems.slice(0, 6) : sortedItems; + // Items are already sorted by the parent component, so we use them as-is + const itemsToDisplay = isShowMoreLayout && !showAll ? items.slice(0, 6) : items; return (
{ return colorPalettes; }; -export const getDistributedColors = (palette: ColorPalette, dataLength: number): string[] => { - const colors = palette.colors; +export const getDistributedColors = (colors: string[], dataLength: number): string[] => { const midIndex = Math.floor(colors.length / 2); if (dataLength === 1) { - // For single item, return the middle color return [colors[midIndex]!]; } if (dataLength === 2) { - // For two items, return colors equally spaced around the middle return [colors[midIndex - 1]!, colors[midIndex + 1]!]; } - // For 3 or more items, distribute colors evenly around the middle const result: string[] = []; const offset = Math.floor((dataLength - 1) / 2); for (let i = 0; i < dataLength; i++) { const index = midIndex + (i - offset); - result.push(colors[index] ?? colors[midIndex]!); // Fallback to middle color if index out of bounds + + // Handle out of bounds by cycling through colors + let actualIndex: number; + if (index < 0) { + // Wrap around from the end + actualIndex = colors.length + (index % colors.length); + } else if (index >= colors.length) { + // Wrap around from the beginning + actualIndex = index % colors.length; + } else { + actualIndex = index; + } + + result.push(colors[actualIndex]!); } return result; }; + +export const useChartPalette = ({ + chartThemeName, + customPalette, + themePaletteName, + dataLength, +}: { + chartThemeName: PaletteName; + customPalette?: string[]; + themePaletteName: keyof ChartColorPalette; + dataLength: number; +}) => { + const { theme } = useTheme(); + const paletteFromTheme = theme[themePaletteName] || theme.defaultChartPalette; + const paletteFromChartTheme = getPalette(chartThemeName); + + const palette = customPalette || paletteFromTheme || paletteFromChartTheme.colors; + + return useMemo(() => { + return getDistributedColors(palette, dataLength); + }, [palette, dataLength]); +}; 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 c852a68bd..b9d9b8d24 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,7 @@ export const getCategoricalChartConfig = ( transformedKeys: Record, ): ChartConfig => { const palette = getPalette(theme); - const colors = getDistributedColors(palette, data.length); + const colors = getDistributedColors(palette.colors, data.length); return data.reduce((config, item, index) => { const originalKey = String(item[categoryKey]); diff --git a/js/packages/react-ui/src/components/ThemeProvider/ThemeProvider.tsx b/js/packages/react-ui/src/components/ThemeProvider/ThemeProvider.tsx index fdf85bfc5..1ffffb485 100644 --- a/js/packages/react-ui/src/components/ThemeProvider/ThemeProvider.tsx +++ b/js/packages/react-ui/src/components/ThemeProvider/ThemeProvider.tsx @@ -1,4 +1,5 @@ -import React, { createContext, useContext } from "react"; +import React, { createContext, useContext, useMemo } from "react"; +import { useShallow } from "zustand/react/shallow"; import { ColorTheme, EffectTheme, LayoutTheme, Theme, ThemeMode, TypographyTheme } from "./types"; export type ThemeProps = { @@ -322,12 +323,16 @@ export const ThemeProvider = ({ darkTheme: userDarkTheme, }: ThemeProps) => { const baseTheme = themes[mode]; - const lightTheme = { ...baseTheme, ...userTheme }; - const darkTheme = { ...baseTheme, ...(userDarkTheme || userTheme) }; + const lightTheme = useShallow(() => ({ ...baseTheme, ...userTheme }))(undefined); + const darkTheme = useShallow(() => ({ ...baseTheme, ...(userDarkTheme || userTheme) }))( + undefined, + ); + const theme = mode === "light" ? lightTheme : darkTheme; + const contextValue = useMemo(() => ({ theme, mode }), [theme, mode]); return ( - +