diff --git a/packages/constants/src/chart.ts b/packages/constants/src/chart.ts new file mode 100644 index 00000000000..f921b8b372b --- /dev/null +++ b/packages/constants/src/chart.ts @@ -0,0 +1,2 @@ +export const LABEL_CLASSNAME = "uppercase text-custom-text-300/60 text-sm tracking-wide"; +export const AXIS_LINE_CLASSNAME = "text-custom-text-400/70"; diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index ece5aad4c34..7fedff05d78 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -1,6 +1,7 @@ export * from "./ai"; export * from "./analytics"; export * from "./auth"; +export * from "./chart"; export * from "./endpoints"; export * from "./file"; export * from "./filter"; diff --git a/packages/propel/.prettierignore b/packages/propel/.prettierignore new file mode 100644 index 00000000000..e841c6b328b --- /dev/null +++ b/packages/propel/.prettierignore @@ -0,0 +1,5 @@ +.next +.turbo +out/ +dist/ +build/ \ No newline at end of file diff --git a/packages/propel/.prettierrc b/packages/propel/.prettierrc new file mode 100644 index 00000000000..87d988f1b26 --- /dev/null +++ b/packages/propel/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/packages/propel/package.json b/packages/propel/package.json index 68a3967201a..1f2ebbeeacf 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -2,26 +2,29 @@ "name": "@plane/propel", "version": "0.24.1", "private": true, - "exports": { - "./globals.css": "./src/globals.css", - "./components/*": "./src/*.tsx" + "scripts": { + "lint": "eslint src --ext .ts,.tsx", + "lint:errors": "eslint src --ext .ts,.tsx --quiet" }, - "devDependencies": { - "@plane/eslint-config": "*", - "@plane/tailwind-config": "*", - "@plane/typescript-config": "*", - "@types/react": "18.3.1", - "@types/react-dom": "18.3.0", - "typescript": "^5.3.3" + "exports": { + "./ui/*": "./src/ui/*.tsx", + "./charts/*": "./src/charts/*/index.ts" }, "dependencies": { "@radix-ui/react-slot": "^1.1.1", "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", "lucide-react": "^0.469.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "tailwind-merge": "^2.6.0", + "recharts": "^2.15.1", "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@plane/eslint-config": "*", + "@plane/tailwind-config": "*", + "@plane/typescript-config": "*", + "@types/react": "18.3.1", + "@types/react-dom": "18.3.0", + "typescript": "^5.3.3" } } diff --git a/web/core/components/core/charts/stacked-bar-chart/index.ts b/packages/propel/src/charts/area-chart/index.ts similarity index 100% rename from web/core/components/core/charts/stacked-bar-chart/index.ts rename to packages/propel/src/charts/area-chart/index.ts diff --git a/packages/propel/src/charts/area-chart/root.tsx b/packages/propel/src/charts/area-chart/root.tsx new file mode 100644 index 00000000000..710c5f70d6a --- /dev/null +++ b/packages/propel/src/charts/area-chart/root.tsx @@ -0,0 +1,124 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +"use client"; + +import React, { useMemo } from "react"; +import { AreaChart as CoreAreaChart, Area, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; +// plane imports +import { AXIS_LINE_CLASSNAME, LABEL_CLASSNAME } from "@plane/constants"; +import { TAreaChartProps } from "@plane/types"; +// local components +import { CustomXAxisTick, CustomYAxisTick } from "../tick"; +import { CustomTooltip } from "../tooltip"; + +export const AreaChart = React.memo((props: TAreaChartProps) => { + const { + data, + areas, + xAxis, + yAxis, + className = "w-full h-96", + tickCount = { + x: undefined, + y: 10, + }, + showTooltip = true, + } = props; + // derived values + const itemKeys = useMemo(() => areas.map((area) => area.key), [areas]); + const itemDotClassNames = useMemo( + () => areas.reduce((acc, area) => ({ ...acc, [area.key]: area.dotClassName }), {}), + [areas] + ); + + const renderAreas = useMemo( + () => + areas.map((area) => ( + + )), + [areas] + ); + + return ( +
+ + + } + tickLine={{ + stroke: "currentColor", + className: AXIS_LINE_CLASSNAME, + }} + axisLine={{ + stroke: "currentColor", + className: AXIS_LINE_CLASSNAME, + }} + label={{ + value: xAxis.label, + dy: 28, + className: LABEL_CLASSNAME, + }} + tickCount={tickCount.x} + /> + } + tickCount={tickCount.y} + allowDecimals={!!yAxis.allowDecimals} + /> + {showTooltip && ( + ( + + )} + /> + )} + {renderAreas} + + +
+ ); +}); +AreaChart.displayName = "AreaChart"; diff --git a/packages/propel/src/charts/bar-chart/bar.tsx b/packages/propel/src/charts/bar-chart/bar.tsx new file mode 100644 index 00000000000..339be704ddb --- /dev/null +++ b/packages/propel/src/charts/bar-chart/bar.tsx @@ -0,0 +1,70 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import React from "react"; +// plane imports +import { TChartData } from "@plane/types"; +import { cn } from "@plane/utils"; + +// Helper to calculate percentage +const calculatePercentage = ( + data: TChartData, + stackKeys: T[], + currentKey: T +): number => { + const total = stackKeys.reduce((sum, key) => sum + data[key], 0); + 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_BORDER_RADIUS = 2; // Border radius for each bar + +export const CustomBar = React.memo((props: any) => { + const { fill, x, y, width, height, dataKey, stackKeys, payload, textClassName, showPercentage } = 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 + const currentBarPercentage = calculatePercentage(payload, stackKeys, dataKey); + 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); + + if (!height) return null; + return ( + + + {showText && ( + + {currentBarPercentage}% + + )} + + ); +}); +CustomBar.displayName = "CustomBar"; diff --git a/web/core/components/core/charts/tree-map/index.ts b/packages/propel/src/charts/bar-chart/index.ts similarity index 100% rename from web/core/components/core/charts/tree-map/index.ts rename to packages/propel/src/charts/bar-chart/index.ts diff --git a/packages/propel/src/charts/bar-chart/root.tsx b/packages/propel/src/charts/bar-chart/root.tsx new file mode 100644 index 00000000000..883a0621cb0 --- /dev/null +++ b/packages/propel/src/charts/bar-chart/root.tsx @@ -0,0 +1,125 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +"use client"; + +import React, { useMemo } from "react"; +import { BarChart as CoreBarChart, Bar, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; +// plane imports +import { AXIS_LINE_CLASSNAME, LABEL_CLASSNAME } from "@plane/constants"; +import { TBarChartProps } from "@plane/types"; +// local components +import { CustomXAxisTick, CustomYAxisTick } from "../tick"; +import { CustomTooltip } from "../tooltip"; +import { CustomBar } from "./bar"; + +export const BarChart = React.memo((props: TBarChartProps) => { + const { + data, + bars, + xAxis, + yAxis, + barSize = 40, + className = "w-full h-96", + tickCount = { + x: undefined, + y: 10, + }, + showTooltip = true, + } = props; + // derived values + const stackKeys = useMemo(() => bars.map((bar) => bar.key), [bars]); + const stackDotClassNames = useMemo( + () => bars.reduce((acc, bar) => ({ ...acc, [bar.key]: bar.dotClassName }), {}), + [bars] + ); + + const renderBars = useMemo( + () => + bars.map((bar) => ( + ( + + )} + /> + )), + [stackKeys, bars] + ); + + return ( +
+ + + } + tickLine={{ + stroke: "currentColor", + className: AXIS_LINE_CLASSNAME, + }} + axisLine={{ + stroke: "currentColor", + className: AXIS_LINE_CLASSNAME, + }} + label={{ + value: xAxis.label, + dy: 28, + className: LABEL_CLASSNAME, + }} + tickCount={tickCount.x} + /> + } + tickCount={tickCount.y} + allowDecimals={!!yAxis.allowDecimals} + /> + {showTooltip && ( + ( + + )} + /> + )} + {renderBars} + + +
+ ); +}); +BarChart.displayName = "BarChart"; diff --git a/packages/propel/src/charts/line-chart/index.ts b/packages/propel/src/charts/line-chart/index.ts new file mode 100644 index 00000000000..1efe34c51ec --- /dev/null +++ b/packages/propel/src/charts/line-chart/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/packages/propel/src/charts/line-chart/root.tsx b/packages/propel/src/charts/line-chart/root.tsx new file mode 100644 index 00000000000..c689fe9ba3c --- /dev/null +++ b/packages/propel/src/charts/line-chart/root.tsx @@ -0,0 +1,115 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +"use client"; + +import React, { useMemo } from "react"; +import { LineChart as CoreLineChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; +// plane imports +import { AXIS_LINE_CLASSNAME, LABEL_CLASSNAME } from "@plane/constants"; +import { TLineChartProps } from "@plane/types"; +// local components +import { CustomXAxisTick, CustomYAxisTick } from "../tick"; +import { CustomTooltip } from "../tooltip"; + +export const LineChart = React.memo((props: TLineChartProps) => { + const { + data, + lines, + xAxis, + yAxis, + className = "w-full h-96", + tickCount = { + x: undefined, + y: 10, + }, + showTooltip = true, + } = props; + // derived values + const itemKeys = useMemo(() => lines.map((line) => line.key), [lines]); + const itemDotClassNames = useMemo( + () => lines.reduce((acc, line) => ({ ...acc, [line.key]: line.dotClassName }), {}), + [lines] + ); + + const renderLines = useMemo( + () => + lines.map((line) => ( + + )), + [lines] + ); + + return ( +
+ + + } + tickLine={{ + stroke: "currentColor", + className: AXIS_LINE_CLASSNAME, + }} + axisLine={{ + stroke: "currentColor", + className: AXIS_LINE_CLASSNAME, + }} + label={{ + value: xAxis.label, + dy: 28, + className: LABEL_CLASSNAME, + }} + tickCount={tickCount.x} + /> + } + tickCount={tickCount.y} + allowDecimals={!!yAxis.allowDecimals} + /> + {showTooltip && ( + ( + + )} + /> + )} + {renderLines} + + +
+ ); +}); +LineChart.displayName = "LineChart"; diff --git a/packages/propel/src/charts/pie-chart/index.ts b/packages/propel/src/charts/pie-chart/index.ts new file mode 100644 index 00000000000..1efe34c51ec --- /dev/null +++ b/packages/propel/src/charts/pie-chart/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/packages/propel/src/charts/pie-chart/root.tsx b/packages/propel/src/charts/pie-chart/root.tsx new file mode 100644 index 00000000000..d9e2558ed0f --- /dev/null +++ b/packages/propel/src/charts/pie-chart/root.tsx @@ -0,0 +1,51 @@ +"use client"; + +import React, { useMemo } from "react"; +import { Cell, PieChart as CorePieChart, Pie, ResponsiveContainer, Tooltip } from "recharts"; +// plane imports +import { TPieChartProps } from "@plane/types"; +// local components +import { CustomPieChartTooltip } from "./tooltip"; + +export const PieChart = React.memo((props: TPieChartProps) => { + const { data, dataKey, cells, className = "w-full h-96", innerRadius, outerRadius, showTooltip = true } = props; + + const renderCells = useMemo( + () => cells.map((cell) => ), + [cells] + ); + + return ( +
+ + + + {renderCells} + + {showTooltip && ( + { + if (!active || !payload || !payload.length) return null; + const cellData = cells.find((c) => c.key === payload[0].name); + if (!cellData) return null; + return ; + }} + /> + )} + + +
+ ); +}); +PieChart.displayName = "PieChart"; diff --git a/packages/propel/src/charts/pie-chart/tooltip.tsx b/packages/propel/src/charts/pie-chart/tooltip.tsx new file mode 100644 index 00000000000..56c7fa34cdc --- /dev/null +++ b/packages/propel/src/charts/pie-chart/tooltip.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { NameType, Payload, ValueType } from "recharts/types/component/DefaultTooltipContent"; +// plane imports +import { Card, ECardSpacing } from "@plane/ui"; +import { cn } from "@plane/utils"; + +type Props = { + dotClassName?: string; + label: string; + payload: Payload[]; +}; + +export const CustomPieChartTooltip = React.memo((props: Props) => { + const { dotClassName, label, payload } = props; + + return ( + +

+ {label} +

+ {payload?.map((item) => ( +
+
+ {item?.name}: + {item?.value} +
+ ))} + + ); +}); +CustomPieChartTooltip.displayName = "CustomPieChartTooltip"; diff --git a/web/core/components/core/charts/stacked-bar-chart/tick.tsx b/packages/propel/src/charts/tick.tsx similarity index 100% rename from web/core/components/core/charts/stacked-bar-chart/tick.tsx rename to packages/propel/src/charts/tick.tsx diff --git a/packages/propel/src/charts/tooltip.tsx b/packages/propel/src/charts/tooltip.tsx new file mode 100644 index 00000000000..e7f92a9cb1f --- /dev/null +++ b/packages/propel/src/charts/tooltip.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { NameType, Payload, ValueType } from "recharts/types/component/DefaultTooltipContent"; +// plane imports +import { Card, ECardSpacing } from "@plane/ui"; +import { cn } from "@plane/utils"; + +type Props = { + active: boolean | undefined; + label: string | undefined; + payload: Payload[] | undefined; + itemKeys: string[]; + itemDotClassNames: Record; +}; + +export const CustomTooltip = React.memo((props: Props) => { + const { active, label, payload, itemKeys, itemDotClassNames } = props; + // derived values + const filteredPayload = payload?.filter((item) => item.dataKey && itemKeys.includes(`${item.dataKey}`)); + + if (!active || !filteredPayload || !filteredPayload.length) return null; + return ( + +

+ {label} +

+ {filteredPayload.map((item) => { + if (!item.dataKey) return null; + return ( +
+ {itemDotClassNames[item?.dataKey] && ( +
+ )} + {item?.name}: + {item?.value} +
+ ); + })} + + ); +}); +CustomTooltip.displayName = "CustomTooltip"; diff --git a/packages/propel/src/charts/tree-map/index.ts b/packages/propel/src/charts/tree-map/index.ts new file mode 100644 index 00000000000..1efe34c51ec --- /dev/null +++ b/packages/propel/src/charts/tree-map/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/web/core/components/core/charts/tree-map/map-content.tsx b/packages/propel/src/charts/tree-map/map-content.tsx similarity index 100% rename from web/core/components/core/charts/tree-map/map-content.tsx rename to packages/propel/src/charts/tree-map/map-content.tsx diff --git a/web/core/components/core/charts/tree-map/root.tsx b/packages/propel/src/charts/tree-map/root.tsx similarity index 100% rename from web/core/components/core/charts/tree-map/root.tsx rename to packages/propel/src/charts/tree-map/root.tsx diff --git a/web/core/components/core/charts/tree-map/tooltip.tsx b/packages/propel/src/charts/tree-map/tooltip.tsx similarity index 100% rename from web/core/components/core/charts/tree-map/tooltip.tsx rename to packages/propel/src/charts/tree-map/tooltip.tsx diff --git a/packages/propel/src/globals.css b/packages/propel/src/globals.css index a1be0c34358..ee28968088a 100644 --- a/packages/propel/src/globals.css +++ b/packages/propel/src/globals.css @@ -2,53 +2,6 @@ @tailwind components; @tailwind utilities; -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 47.4% 11.2%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --card: 0 0% 100%; - --card-foreground: 222.2 47.4% 11.2%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 100% 50%; - --destructive-foreground: 210 40% 98%; - --ring: 215 20.2% 65.1%; - --radius: 0.5rem; - } - - .dark { - --background: 224 71% 4%; - --foreground: 213 31% 91%; - --muted: 223 47% 11%; - --muted-foreground: 215.4 16.3% 56.9%; - --accent: 216 34% 17%; - --accent-foreground: 210 40% 98%; - --popover: 224 71% 4%; - --popover-foreground: 215 20.2% 65.1%; - --border: 216 34% 17%; - --input: 216 34% 17%; - --card: 224 71% 4%; - --card-foreground: 213 31% 91%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 1.2%; - --secondary: 222.2 47.4% 11.2%; - --secondary-foreground: 210 40% 98%; - --destructive: 0 63% 31%; - --destructive-foreground: 210 40% 98%; - --ring: 216 34% 17%; - } -} - @layer base { * { @apply border-border; diff --git a/packages/propel/src/index.ts b/packages/propel/src/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/propel/src/ui/button.tsx b/packages/propel/src/ui/button.tsx deleted file mode 100644 index 7f40dba66c3..00000000000 --- a/packages/propel/src/ui/button.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; -import * as React from "react"; - -import { cn } from "@plane/utils"; - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", - outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean; -} - -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - return ( - - ); - } -); -Button.displayName = "Button"; - -export { Button, buttonVariants }; diff --git a/packages/tailwind-config/tailwind.config.js b/packages/tailwind-config/tailwind.config.js index 4c8563f5f4d..0731894f06f 100644 --- a/packages/tailwind-config/tailwind.config.js +++ b/packages/tailwind-config/tailwind.config.js @@ -19,6 +19,7 @@ module.exports = { "./app/**/*.tsx", "./ui/**/*.tsx", "../packages/ui/src/**/*.{js,ts,jsx,tsx}", + "../packages/propel/src/**/*.{js,ts,jsx,tsx}", "../packages/editor/src/**/*.{js,ts,jsx,tsx}", "!../packages/ui/**/*.stories{js,ts,jsx,tsx}", ], diff --git a/packages/types/src/charts.d.ts b/packages/types/src/charts.d.ts index 04234d6e725..473c1077e89 100644 --- a/packages/types/src/charts.d.ts +++ b/packages/types/src/charts.d.ts @@ -1,29 +1,20 @@ -export type TStackItem = { - key: T; - fillClassName: string; - textClassName: string; - dotClassName?: string; - showPercentage?: boolean; -}; - -export type TStackChartData = { +export type TChartData = { + // required key [key in K]: string | number; } & Record; -export type TStackedBarChartProps = { - data: TStackChartData[]; - stacks: TStackItem[]; +type TChartProps = { + data: TChartData[]; xAxis: { - key: keyof TStackChartData; + key: keyof TChartData; label: string; }; yAxis: { - key: keyof TStackChartData; + key: keyof TChartData; label: string; domain?: [number, number]; allowDecimals?: boolean; }; - barSize?: number; className?: string; tickCount?: { x?: number; @@ -32,6 +23,60 @@ export type TStackedBarChartProps = { showTooltip?: boolean; }; +export type TBarItem = { + key: T; + fillClassName: string; + textClassName: string; + dotClassName?: string; + showPercentage?: boolean; + stackId: string; +}; + +export type TBarChartProps = TChartProps & { + bars: TBarItem[]; + barSize?: number; +}; + +export type TLineItem = { + key: T; + className?: string; + style?: Record; + dotClassName?: string; +}; + +export type TLineChartProps = TChartProps & { + lines: TLineItem[]; +}; + +export type TAreaItem = { + key: T; + stackId: string; + className?: string; + style?: Record; + dotClassName?: string; +}; + +export type TAreaChartProps = TChartProps & { + areas: TAreaItem[]; +}; + +export type TCellItem = { + key: T; + className?: string; + style?: Record; + dotClassName?: string; +}; + +export type TPieChartProps = Pick< + TChartProps, + "className" | "data" | "showTooltip" +> & { + dataKey: T; + cells: TCellItem[]; + innerRadius?: number; + outerRadius?: number; +}; + export type TreeMapItem = { name: string; value: number; @@ -45,7 +90,7 @@ export type TreeMapItem = { | { fillClassName: string; } - ); +); export type TreeMapChartProps = { data: TreeMapItem[]; diff --git a/web/core/components/core/charts/stacked-bar-chart/bar.tsx b/web/core/components/core/charts/stacked-bar-chart/bar.tsx deleted file mode 100644 index 96fd9b3ceaf..00000000000 --- a/web/core/components/core/charts/stacked-bar-chart/bar.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import React from "react"; -// plane imports -import { TStackChartData } from "@plane/types"; -import { cn } from "@plane/utils"; - -// Helper to calculate percentage -const calculatePercentage = ( - data: TStackChartData, - stackKeys: T[], - currentKey: T -): number => { - const total = stackKeys.reduce((sum, key) => sum + data[key], 0); - return total === 0 ? 0 : Math.round((data[currentKey] / total) * 100); -}; - -export const CustomStackBar = React.memo((props: any) => { - const { fill, x, y, width, height, dataKey, stackKeys, payload, textClassName, showPercentage } = props; - // Calculate text position - const MIN_BAR_HEIGHT_FOR_INTERNAL = 14; // Minimum height needed to show text inside - const TEXT_PADDING = Math.min(6, Math.abs(MIN_BAR_HEIGHT_FOR_INTERNAL - height / 2)); - const textY = y + height - TEXT_PADDING; // Position inside bar if tall enough - // derived values - const RADIUS = 2; - const currentBarPercentage = calculatePercentage(payload, stackKeys, dataKey); - - if (!height) return null; - return ( - - - {showPercentage && - height >= MIN_BAR_HEIGHT_FOR_INTERNAL && - currentBarPercentage !== undefined && - !Number.isNaN(currentBarPercentage) && ( - - {currentBarPercentage}% - - )} - - ); -}); -CustomStackBar.displayName = "CustomStackBar"; diff --git a/web/core/components/core/charts/stacked-bar-chart/root.tsx b/web/core/components/core/charts/stacked-bar-chart/root.tsx deleted file mode 100644 index 2fd8ccfc6cd..00000000000 --- a/web/core/components/core/charts/stacked-bar-chart/root.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -"use client"; - -import React from "react"; -import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Tooltip } from "recharts"; -// plane imports -import { TStackedBarChartProps } from "@plane/types"; -import { cn } from "@plane/utils"; -// local components -import { CustomStackBar } from "./bar"; -import { CustomXAxisTick, CustomYAxisTick } from "./tick"; -import { CustomTooltip } from "./tooltip"; - -// Common classnames -const LABEL_CLASSNAME = "uppercase text-custom-text-300/60 text-sm tracking-wide"; -const AXIS_LINE_CLASSNAME = "text-custom-text-400/70"; - -export const StackedBarChart = React.memo( - ({ - data, - stacks, - xAxis, - yAxis, - barSize = 40, - className = "w-full h-96", - tickCount = { - x: undefined, - y: 10, - }, - showTooltip = true, - }: TStackedBarChartProps) => { - // derived values - const stackKeys = React.useMemo(() => stacks.map((stack) => stack.key), [stacks]); - const stackDotClassNames = React.useMemo( - () => stacks.reduce((acc, stack) => ({ ...acc, [stack.key]: stack.dotClassName }), {}), - [stacks] - ); - - const renderBars = React.useMemo( - () => - stacks.map((stack) => ( - ( - - )} - /> - )), - [stackKeys, stacks] - ); - - return ( -
- - - } - tickLine={{ - stroke: "currentColor", - className: AXIS_LINE_CLASSNAME, - }} - axisLine={{ - stroke: "currentColor", - className: AXIS_LINE_CLASSNAME, - }} - label={{ - value: xAxis.label, - dy: 28, - className: LABEL_CLASSNAME, - }} - tickCount={tickCount.x} - /> - } - tickCount={tickCount.y} - allowDecimals={yAxis.allowDecimals ?? false} - /> - {showTooltip && ( - ( - - )} - /> - )} - {renderBars} - - -
- ); - } -); -StackedBarChart.displayName = "StackedBarChart"; diff --git a/web/core/components/core/charts/stacked-bar-chart/tooltip.tsx b/web/core/components/core/charts/stacked-bar-chart/tooltip.tsx deleted file mode 100644 index 32226db3f12..00000000000 --- a/web/core/components/core/charts/stacked-bar-chart/tooltip.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import React from "react"; -// plane imports -import { Card, ECardSpacing } from "@plane/ui"; -import { cn } from "@plane/utils"; - -type TStackedBarChartProps = { - active: boolean | undefined; - label: string | undefined; - payload: any[] | undefined; - stackKeys: string[]; - stackDotClassNames: Record; -}; - -export const CustomTooltip = React.memo( - ({ active, label, payload, stackKeys, stackDotClassNames }: TStackedBarChartProps) => { - // derived values - const filteredPayload = payload?.filter((item: any) => item.dataKey && stackKeys.includes(item.dataKey)); - - if (!active || !filteredPayload || !filteredPayload.length) return null; - return ( - -

- {label} -

- {filteredPayload.map((item: any) => ( -
- {stackDotClassNames[item?.dataKey] && ( -
- )} - {item?.name}: - {item?.value} -
- ))} - - ); - } -); -CustomTooltip.displayName = "CustomTooltip"; diff --git a/web/next.config.js b/web/next.config.js index e6e918e0734..672d9151b5f 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -18,7 +18,7 @@ const nextConfig = { images: { unoptimized: true, }, - transpilePackages: ["@plane/i18n"], + transpilePackages: ["@plane/i18n", "@plane/propel"], async redirects() { return [ { diff --git a/web/package.json b/web/package.json index 84aeea4b0fc..0dfe18078e0 100644 --- a/web/package.json +++ b/web/package.json @@ -32,6 +32,7 @@ "@plane/editor": "*", "@plane/hooks": "*", "@plane/i18n": "*", + "@plane/propel": "*", "@plane/types": "*", "@plane/ui": "*", "@plane/utils": "*", diff --git a/yarn.lock b/yarn.lock index 0306370784d..81d6a200e9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9165,21 +9165,6 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -lucide-react@^0.356.0: - version "0.356.0" - resolved "https://registry.npmjs.org/lucide-react/-/lucide-react-0.356.0.tgz" - integrity sha512-MDInjLrmZToccH2UxEshntujBlFwtOofGB22FN/eg39FfGVYV1TT1eMIv2j4rdaTJBpYjUuX7fEo9pwYkNFgwA== - -lucide-react@^0.378.0: - version "0.378.0" - resolved "https://registry.npmjs.org/lucide-react/-/lucide-react-0.378.0.tgz" - integrity sha512-u6EPU8juLUk9ytRcyapkWI18epAv3RU+6+TC23ivjR0e+glWKBobFeSgRwOIJihzktILQuy6E0E80P2jVTDR5g== - -lucide-react@^0.379.0: - version "0.379.0" - resolved "https://registry.npmjs.org/lucide-react/-/lucide-react-0.379.0.tgz" - integrity sha512-KcdeVPqmhRldldAAgptb8FjIunM2x2Zy26ZBh1RsEUcdLIvsEmbcw7KpzFYUy5BbpGeWhPu9Z9J5YXfStiXwhg== - lucide-react@^0.469.0: version "0.469.0" resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.469.0.tgz#f16936ca6521482fef754a7eabb310e6c68e1482" @@ -11170,6 +11155,15 @@ react-smooth@^4.0.0: prop-types "^15.8.1" react-transition-group "^4.4.5" +react-smooth@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.4.tgz#a5875f8bb61963ca61b819cedc569dc2453894b4" + integrity sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q== + dependencies: + fast-equals "^5.0.1" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + react-style-singleton@^2.2.1: version "2.2.1" resolved "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz" @@ -11274,6 +11268,20 @@ recharts@^2.12.7: tiny-invariant "^1.3.1" victory-vendor "^36.6.8" +recharts@^2.15.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.1.tgz#0941adf0402528d54f6d81997eb15840c893aa3c" + integrity sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q== + dependencies: + clsx "^2.0.0" + eventemitter3 "^4.0.1" + lodash "^4.17.21" + react-is "^18.3.1" + react-smooth "^4.0.4" + recharts-scale "^0.4.4" + tiny-invariant "^1.3.1" + victory-vendor "^36.6.8" + redent@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz" @@ -12028,16 +12036,7 @@ streamx@^2.15.0, streamx@^2.20.0: optionalDependencies: bare-events "^2.2.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12149,14 +12148,7 @@ string_decoder@^1.1.1, string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -12297,7 +12289,7 @@ tabbable@^6.0.0: resolved "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz" integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== -tailwind-merge@^2.0.0, tailwind-merge@^2.5.5, tailwind-merge@^2.6.0: +tailwind-merge@^2.0.0, tailwind-merge@^2.5.5: version "2.6.0" resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5" integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA== @@ -13449,16 +13441,7 @@ word-wrap@^1.2.5: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==