diff --git a/packages/propel/src/charts/bar-chart/bar.tsx b/packages/propel/src/charts/bar-chart/bar.tsx index 5cc9dac2fd..a13e154b2d 100644 --- a/packages/propel/src/charts/bar-chart/bar.tsx +++ b/packages/propel/src/charts/bar-chart/bar.tsx @@ -1,10 +1,38 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import React from "react"; // plane imports -import { TChartData } from "@plane/types"; +import { TBarChartShapeVariant, TBarItem, TChartData } from "@plane/types"; import { cn } from "@plane/utils"; -// Helper to calculate percentage +// Constants +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 { + 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; + dotted?: boolean; +} + +// Helper Functions const calculatePercentage = ( data: TChartData, stackKeys: T[], @@ -14,11 +42,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 +`; -export const CustomBar = React.memo((props: any) => { +const PercentageText = ({ + x, + y, + percentage, + className, +}: { + x: number; + y: number; + percentage: number; + className?: string; +}) => ( + + {percentage}% + +); + +// Base Components +const CustomBar = React.memo((props: TBarProps) => { const { opacity, fill, @@ -34,56 +87,104 @@ 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 && ( - - {currentBarPercentage}% - + )} ); }); + +const CustomBarLollipop = React.memo((props: TBarProps) => { + const { fill, x, y, width, height, dataKey, stackKeys, payload, textClassName, showPercentage, dotted } = props; + + const currentBarPercentage = calculatePercentage(payload, stackKeys, dataKey); + + return ( + + + + {showPercentage && ( + + )} + + ); +}); + +// 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 => { + const showTopBorderRadius = bar.showTopBorderRadius?.(shapeProps.dataKey, shapeProps.payload); + const showBottomBorderRadius = bar.showBottomBorderRadius?.(shapeProps.dataKey, shapeProps.payload); + + return ( + + ); + }; + +export const barShapeVariants: Record< + TBarChartShapeVariant, + (props: TShapeProps, bar: TBarItem, stackKeys: string[]) => JSX.Element +> = { + 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 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 8826a55cf7..e662425247 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 316cfd6b89..685aed2145 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" | "lollipop-dotted"; + 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 & {