From 77e80966b778bc83687b90697e9cbe61adddac03 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Wed, 25 Jun 2025 17:50:50 +0530 Subject: [PATCH 1/6] feat: enhance bar chart component with shape variants and custom tooltip --- packages/propel/src/charts/bar-chart/bar.tsx | 161 ++++++++++++++---- packages/propel/src/charts/bar-chart/root.tsx | 44 ++--- packages/types/src/charts/index.d.ts | 3 + 3 files changed, 151 insertions(+), 57 deletions(-) diff --git a/packages/propel/src/charts/bar-chart/bar.tsx b/packages/propel/src/charts/bar-chart/bar.tsx index 5cc9dac2fde..79c146a6391 100644 --- a/packages/propel/src/charts/bar-chart/bar.tsx +++ b/packages/propel/src/charts/bar-chart/bar.tsx @@ -1,10 +1,37 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import React from "react"; // plane imports -import { TChartData } from "@plane/types"; +import { TBarItem, TChartData } from "@plane/types"; import { cn } from "@plane/utils"; -// Helper to calculate percentage +// Constants +const MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT = 14; +const BAR_TOP_BORDER_RADIUS = 4; +const BAR_BOTTOM_BORDER_RADIUS = 4; +const DEFAULT_LOLLIPOP_LINE_WIDTH = 2; +const DEFAULT_LOLLIPOP_CIRCLE_RADIUS = 8; + +// Types +interface TShapeProps { + x: number; + y: number; + width: number; + height: number; + dataKey: string; + payload: any; + opacity?: number; +} + +interface TBarProps extends TShapeProps { + fill: string | ((payload: any) => string); + stackKeys: string[]; + textClassName?: string; + showPercentage?: boolean; + showTopBorderRadius?: boolean; + showBottomBorderRadius?: boolean; +} + +// Utilities const calculatePercentage = ( data: TChartData, stackKeys: T[], @@ -14,11 +41,36 @@ const calculatePercentage = ( return total === 0 ? 0 : Math.round((data[currentKey] / total) * 100); }; -const MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT = 14; // Minimum height needed to show text inside -const BAR_TOP_BORDER_RADIUS = 4; // Border radius for each bar -const BAR_BOTTOM_BORDER_RADIUS = 4; // Border radius for each bar +const getBarPath = (x: number, y: number, width: number, height: number, topRadius: number, bottomRadius: number) => ` + M${x},${y + topRadius} + Q${x},${y} ${x + topRadius},${y} + L${x + width - topRadius},${y} + Q${x + width},${y} ${x + width},${y + topRadius} + L${x + width},${y + height - bottomRadius} + Q${x + width},${y + height} ${x + width - bottomRadius},${y + height} + L${x + bottomRadius},${y + height} + Q${x},${y + height} ${x},${y + height - bottomRadius} + Z +`; + +const PercentageText = ({ + x, + y, + percentage, + className, +}: { + x: number; + y: number; + percentage: number; + className?: string; +}) => ( + + {percentage}% + +); -export const CustomBar = React.memo((props: any) => { +// Base Components +const CustomBar = React.memo((props: TBarProps) => { const { opacity, fill, @@ -34,51 +86,69 @@ export const CustomBar = React.memo((props: any) => { showTopBorderRadius, showBottomBorderRadius, } = props; - // Calculate text position - const TEXT_PADDING_Y = Math.min(6, Math.abs(MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT - height / 2)); - const textY = y + height - TEXT_PADDING_Y; // Position inside bar if tall enough - // derived values + + if (!height) return null; + const currentBarPercentage = calculatePercentage(payload, stackKeys, dataKey); + const TEXT_PADDING_Y = Math.min(6, Math.abs(MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT - height / 2)); + const textY = y + height - TEXT_PADDING_Y; + const showText = - // from props showPercentage && - // height of the bar is greater than or equal to the minimum height required to show the text height >= MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT && - // bar percentage text has some value currentBarPercentage !== undefined && - // bar percentage is a number !Number.isNaN(currentBarPercentage); const topBorderRadius = showTopBorderRadius ? BAR_TOP_BORDER_RADIUS : 0; const bottomBorderRadius = showBottomBorderRadius ? BAR_BOTTOM_BORDER_RADIUS : 0; - if (!height) return null; - return ( {showText && ( + + )} + + ); +}); + +const CustomBarLollipop = React.memo((props: TBarProps) => { + const { fill, x, y, width, height, dataKey, stackKeys, payload, textClassName, showPercentage } = props; + + const currentBarPercentage = calculatePercentage(payload, stackKeys, dataKey); + + console.log(y, "y", x, "x", width, "width", height, "height"); + return ( + + + + {showPercentage && ( {currentBarPercentage}% @@ -86,4 +156,33 @@ export const CustomBar = React.memo((props: any) => { ); }); + +// Shape Variants Factory +const createShapeVariant = + (Component: React.ComponentType) => + (shapeProps: TShapeProps, bar: TBarItem, stackKeys: string[]) => { + const showTopBorderRadius = bar.showTopBorderRadius?.(shapeProps.dataKey, shapeProps.payload); + const showBottomBorderRadius = bar.showBottomBorderRadius?.(shapeProps.dataKey, shapeProps.payload); + + return ( + + ); + }; + +// Shape Variants +export const barShapeVariants = { + bar: createShapeVariant(CustomBar), + lollipop: createShapeVariant(CustomBarLollipop), +}; + +// Display names CustomBar.displayName = "CustomBar"; +CustomBarLollipop.displayName = "CustomBarLollipop"; diff --git a/packages/propel/src/charts/bar-chart/root.tsx b/packages/propel/src/charts/bar-chart/root.tsx index 8826a55cf77..e6624252476 100644 --- a/packages/propel/src/charts/bar-chart/root.tsx +++ b/packages/propel/src/charts/bar-chart/root.tsx @@ -19,7 +19,7 @@ import { TBarChartProps } from "@plane/types"; import { getLegendProps } from "../components/legend"; import { CustomXAxisTick, CustomYAxisTick } from "../components/tick"; import { CustomTooltip } from "../components/tooltip"; -import { CustomBar } from "./bar"; +import { barShapeVariants } from "./bar"; export const BarChart = React.memo((props: TBarChartProps) => { const { @@ -36,6 +36,7 @@ export const BarChart = React.memo((props: T y: 10, }, showTooltip = true, + customTooltipContent, } = props; // states const [activeBar, setActiveBar] = useState(null); @@ -66,20 +67,8 @@ export const BarChart = React.memo((props: T stackId={bar.stackId} opacity={!!activeLegend && activeLegend !== bar.key ? 0.1 : 1} shape={(shapeProps: any) => { - const showTopBorderRadius = bar.showTopBorderRadius?.(shapeProps.dataKey, shapeProps.payload); - const showBottomBorderRadius = bar.showBottomBorderRadius?.(shapeProps.dataKey, shapeProps.payload); - - return ( - - ); + const shapeVariant = barShapeVariants[bar.shapeVariant ?? "bar"]; + return shapeVariant(shapeProps, bar, stackKeys); }} className="[&_path]:transition-opacity [&_path]:duration-200" onMouseEnter={() => setActiveBar(bar.key)} @@ -150,17 +139,20 @@ export const BarChart = React.memo((props: T wrapperStyle={{ pointerEvents: "auto", }} - content={({ active, label, payload }) => ( - - )} + content={({ active, label, payload }) => { + if (customTooltipContent) return customTooltipContent({ active, label, payload }); + return ( + + ); + }} /> )} {renderBars} diff --git a/packages/types/src/charts/index.d.ts b/packages/types/src/charts/index.d.ts index 316cfd6b89f..4ef4a5eb141 100644 --- a/packages/types/src/charts/index.d.ts +++ b/packages/types/src/charts/index.d.ts @@ -53,6 +53,8 @@ type TChartProps = { // Bar Chart // ============================================================ +export type TBarChartShapeVariant = "bar" | "lollipop"; + export type TBarItem = { key: T; label: string; @@ -62,6 +64,7 @@ export type TBarItem = { stackId: string; showTopBorderRadius?: (barKey: string, payload: any) => boolean; showBottomBorderRadius?: (barKey: string, payload: any) => boolean; + shapeVariant?: TBarChartShapeVariant; }; export type TBarChartProps = TChartProps & { From 30941d13b2bb35fa6aa2c03ff1edeb9cd3c1ee61 Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Wed, 25 Jun 2025 18:18:19 +0530 Subject: [PATCH 2/6] Update packages/propel/src/charts/bar-chart/bar.tsx removed the unknown props Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/propel/src/charts/bar-chart/bar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/propel/src/charts/bar-chart/bar.tsx b/packages/propel/src/charts/bar-chart/bar.tsx index 79c146a6391..95b2d66b2bd 100644 --- a/packages/propel/src/charts/bar-chart/bar.tsx +++ b/packages/propel/src/charts/bar-chart/bar.tsx @@ -140,7 +140,6 @@ const CustomBarLollipop = React.memo((props: TBarProps) => { r={DEFAULT_LOLLIPOP_CIRCLE_RADIUS} fill={typeof fill === "function" ? fill(payload) : fill} stroke="none" - alignmentBaseline="middle" /> {showPercentage && ( Date: Wed, 25 Jun 2025 18:18:44 +0530 Subject: [PATCH 3/6] Update packages/propel/src/charts/bar-chart/bar.tsx removed console log Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/propel/src/charts/bar-chart/bar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/propel/src/charts/bar-chart/bar.tsx b/packages/propel/src/charts/bar-chart/bar.tsx index 95b2d66b2bd..985e0c4deb6 100644 --- a/packages/propel/src/charts/bar-chart/bar.tsx +++ b/packages/propel/src/charts/bar-chart/bar.tsx @@ -122,7 +122,6 @@ const CustomBarLollipop = React.memo((props: TBarProps) => { const currentBarPercentage = calculatePercentage(payload, stackKeys, dataKey); - console.log(y, "y", x, "x", width, "width", height, "height"); return ( Date: Wed, 25 Jun 2025 18:27:05 +0530 Subject: [PATCH 4/6] refactor: replace inline percentage text with PercentageText component in bar chart --- packages/propel/src/charts/bar-chart/bar.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/propel/src/charts/bar-chart/bar.tsx b/packages/propel/src/charts/bar-chart/bar.tsx index 985e0c4deb6..46ef2760533 100644 --- a/packages/propel/src/charts/bar-chart/bar.tsx +++ b/packages/propel/src/charts/bar-chart/bar.tsx @@ -141,15 +141,7 @@ const CustomBarLollipop = React.memo((props: TBarProps) => { stroke="none" /> {showPercentage && ( - - {currentBarPercentage}% - + )} ); From a5d761bc1ff09044b0472da0a10c93011b6b9102 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Wed, 25 Jun 2025 19:32:05 +0530 Subject: [PATCH 5/6] Added new variant - lollipop-dotted --- packages/propel/src/charts/bar-chart/bar.tsx | 17 ++++++++++++----- packages/types/src/charts/index.d.ts | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/propel/src/charts/bar-chart/bar.tsx b/packages/propel/src/charts/bar-chart/bar.tsx index 46ef2760533..30cbeb60c49 100644 --- a/packages/propel/src/charts/bar-chart/bar.tsx +++ b/packages/propel/src/charts/bar-chart/bar.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import React from "react"; // plane imports -import { TBarItem, TChartData } from "@plane/types"; +import { TBarChartShapeVariant, TBarItem, TChartData } from "@plane/types"; import { cn } from "@plane/utils"; // Constants @@ -29,6 +29,7 @@ interface TBarProps extends TShapeProps { showPercentage?: boolean; showTopBorderRadius?: boolean; showBottomBorderRadius?: boolean; + dotted?: boolean; } // Utilities @@ -118,7 +119,7 @@ const CustomBar = React.memo((props: TBarProps) => { }); const CustomBarLollipop = React.memo((props: TBarProps) => { - const { fill, x, y, width, height, dataKey, stackKeys, payload, textClassName, showPercentage } = props; + const { fill, x, y, width, height, dataKey, stackKeys, payload, textClassName, showPercentage, dotted } = props; const currentBarPercentage = calculatePercentage(payload, stackKeys, dataKey); @@ -132,6 +133,7 @@ const CustomBarLollipop = React.memo((props: TBarProps) => { stroke={typeof fill === "function" ? fill(payload) : fill} strokeWidth={DEFAULT_LOLLIPOP_LINE_WIDTH} strokeLinecap="round" + strokeDasharray={dotted ? "4 4" : "0"} /> { // Shape Variants Factory const createShapeVariant = - (Component: React.ComponentType) => - (shapeProps: TShapeProps, bar: TBarItem, stackKeys: string[]) => { + (Component: React.ComponentType, factoryProps?: Partial) => + (shapeProps: TShapeProps, bar: TBarItem, stackKeys: string[]): JSX.Element => { const showTopBorderRadius = bar.showTopBorderRadius?.(shapeProps.dataKey, shapeProps.payload); const showBottomBorderRadius = bar.showBottomBorderRadius?.(shapeProps.dataKey, shapeProps.payload); @@ -163,14 +165,19 @@ const createShapeVariant = showPercentage={bar.showPercentage} showTopBorderRadius={!!showTopBorderRadius} showBottomBorderRadius={!!showBottomBorderRadius} + {...factoryProps} /> ); }; // Shape Variants -export const barShapeVariants = { +export const barShapeVariants: Record< + TBarChartShapeVariant, + (props: TShapeProps, bar: TBarItem, stackKeys: string[]) => JSX.Element +> = { bar: createShapeVariant(CustomBar), lollipop: createShapeVariant(CustomBarLollipop), + "lollipop-dotted": createShapeVariant(CustomBarLollipop, { dotted: true }), }; // Display names diff --git a/packages/types/src/charts/index.d.ts b/packages/types/src/charts/index.d.ts index 4ef4a5eb141..685aed2145c 100644 --- a/packages/types/src/charts/index.d.ts +++ b/packages/types/src/charts/index.d.ts @@ -53,7 +53,7 @@ type TChartProps = { // Bar Chart // ============================================================ -export type TBarChartShapeVariant = "bar" | "lollipop"; +export type TBarChartShapeVariant = "bar" | "lollipop" | "lollipop-dotted"; export type TBarItem = { key: T; From 0aed001840f9d0bdd463dbc3fbdac788cd69e809 Mon Sep 17 00:00:00 2001 From: JayashTripathy Date: Wed, 25 Jun 2025 19:43:22 +0530 Subject: [PATCH 6/6] added some comments --- packages/propel/src/charts/bar-chart/bar.tsx | 27 ++++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/propel/src/charts/bar-chart/bar.tsx b/packages/propel/src/charts/bar-chart/bar.tsx index 30cbeb60c49..a13e154b2d3 100644 --- a/packages/propel/src/charts/bar-chart/bar.tsx +++ b/packages/propel/src/charts/bar-chart/bar.tsx @@ -5,11 +5,11 @@ import { TBarChartShapeVariant, TBarItem, TChartData } from "@plane/types"; import { cn } from "@plane/utils"; // Constants -const MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT = 14; -const BAR_TOP_BORDER_RADIUS = 4; -const BAR_BOTTOM_BORDER_RADIUS = 4; -const DEFAULT_LOLLIPOP_LINE_WIDTH = 2; -const DEFAULT_LOLLIPOP_CIRCLE_RADIUS = 8; +const MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT = 14; // Minimum height required to show text inside bar +const BAR_TOP_BORDER_RADIUS = 4; // Border radius for the top of bars +const BAR_BOTTOM_BORDER_RADIUS = 4; // Border radius for the bottom of bars +const DEFAULT_LOLLIPOP_LINE_WIDTH = 2; // Width of lollipop stick +const DEFAULT_LOLLIPOP_CIRCLE_RADIUS = 8; // Radius of lollipop circle // Types interface TShapeProps { @@ -32,7 +32,7 @@ interface TBarProps extends TShapeProps { dotted?: boolean; } -// Utilities +// Helper Functions const calculatePercentage = ( data: TChartData, stackKeys: T[], @@ -149,7 +149,13 @@ const CustomBarLollipop = React.memo((props: TBarProps) => { ); }); -// Shape Variants Factory +// Shape Variants +/** + * Factory function to create shape variants with consistent props + * @param Component - The base component to render + * @param factoryProps - Additional props to pass to the component + * @returns A function that creates the shape with proper props + */ const createShapeVariant = (Component: React.ComponentType, factoryProps?: Partial) => (shapeProps: TShapeProps, bar: TBarItem, stackKeys: string[]): JSX.Element => { @@ -170,14 +176,13 @@ const createShapeVariant = ); }; -// Shape Variants export const barShapeVariants: Record< TBarChartShapeVariant, (props: TShapeProps, bar: TBarItem, stackKeys: string[]) => JSX.Element > = { - bar: createShapeVariant(CustomBar), - lollipop: createShapeVariant(CustomBarLollipop), - "lollipop-dotted": createShapeVariant(CustomBarLollipop, { dotted: true }), + bar: createShapeVariant(CustomBar), // Standard bar with rounded corners + lollipop: createShapeVariant(CustomBarLollipop), // Line with circle at top + "lollipop-dotted": createShapeVariant(CustomBarLollipop, { dotted: true }), // Dotted line lollipop variant }; // Display names