From 313cac46188674e06a0860fdf03bfd018be621e1 Mon Sep 17 00:00:00 2001 From: Hrijul Bhatnagar Date: Tue, 19 Aug 2025 11:46:31 +0530 Subject: [PATCH 1/9] Intermediate changes for bar chart export --- js/packages/react-ui/package.json | 3 + .../components/Charts/BarChart/BarChart.tsx | 60 +- .../Charts/BarChart/chart-watermark.svg | 11 + .../Charts/BarChart/useExportBarChart.ts | 75 +++ .../Charts/BarChart/useExportChart.ts | 16 + .../Charts/hooks/useMaxLabelHeight.tsx | 8 +- js/pnpm-lock.yaml | 525 +++++++++++++++--- 7 files changed, 619 insertions(+), 79 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/BarChart/chart-watermark.svg create mode 100644 js/packages/react-ui/src/components/Charts/BarChart/useExportBarChart.ts create mode 100644 js/packages/react-ui/src/components/Charts/BarChart/useExportChart.ts diff --git a/js/packages/react-ui/package.json b/js/packages/react-ui/package.json index d667fdba..95221b0f 100644 --- a/js/packages/react-ui/package.json +++ b/js/packages/react-ui/package.json @@ -73,6 +73,7 @@ "@radix-ui/react-tooltip": "^1.2.7", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "html-to-image": "1.11.11", "lodash-es": "^4.17.21", "lucide-react": "^0.469.0", "react-day-picker": "^9.5.1", @@ -99,6 +100,7 @@ "@storybook/react-vite": "^8.5.3", "@storybook/test": "^8.5.3", "@storybook/theming": "^8.5.3", + "@tailwindcss/postcss": "^4.1.11", "@types/lodash-es": "^4.17.12", "@types/node": "^22.12.0", "@types/node-fetch": "2.6.11", @@ -106,6 +108,7 @@ "@types/react-dom": ">=17.0.0", "@types/react-syntax-highlighter": "^15.5.13", "@typescript-eslint/eslint-plugin": "^8.18.0", + "autoprefixer": "^10.4.21", "concurrently": "^9.2.0", "eslint": "^9.17.0", "eslint-config-prettier": "^9.1.0", 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 e65a5466..3c8d1578 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx @@ -22,6 +22,8 @@ import { XAxisTickVariant } from "../types"; import { type LegendItem } from "../types/Legend"; import { useChartPalette, type PaletteName } from "../utils/PalletUtils"; +import { Download } from "lucide-react"; +import { Button } from "../../Button"; import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip"; import { findNearestSnapPosition, @@ -35,6 +37,8 @@ import { getLegendItems, } from "../utils/dataUtils"; import { BarChartData, BarChartVariant } from "./types"; +import { useExportBarChart } from "./useExportBarChart"; +import { useExportChart } from "./useExportChart"; import { BAR_WIDTH, getPadding, @@ -65,6 +69,7 @@ export interface BarChartProps { className?: string; height?: number; width?: number; + exportMode?: boolean; } const BAR_GAP = 10; // Gap between bars @@ -91,10 +96,17 @@ const BarChartComponent = ({ className, height, width, + exportMode, }: BarChartProps) => { const widthOfGroup = getWidthOfGroup(data, categoryKey as string, variant); - const maxLabelHeight = useMaxLabelHeight(data, categoryKey as string, tickVariant, widthOfGroup); + const maxLabelHeight = useMaxLabelHeight( + data, + categoryKey as string, + tickVariant, + widthOfGroup, + exportMode, + ); const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); @@ -399,6 +411,46 @@ const BarChartComponent = ({ categoryKey, ]); + const barChartContainerRef = useRef(null); + + // const { exportChart: autoExportChart } = useExportChart( + // + // ); + + const { exportChart } = useExportBarChart( + chartContainerRef, + mainContainerRef, + barChartContainerRef, + dataWidth, + showYAxis, + yAxisWidth, + chartHeight, + ); + + // useEffect(() => { + // if (exportMode) { + // exportChart(); + // } + // }, [exportMode]); + return ( ({ style={{ width: width ? `${width}px` : undefined, }} + ref={barChartContainerRef} >
{/* Y-axis of the chart */} @@ -501,6 +554,11 @@ const BarChartComponent = ({ setIsExpanded={setIsLegendExpanded} /> )} +
+ +
diff --git a/js/packages/react-ui/src/components/Charts/BarChart/chart-watermark.svg b/js/packages/react-ui/src/components/Charts/BarChart/chart-watermark.svg new file mode 100644 index 00000000..56d4e130 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarChart/chart-watermark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/js/packages/react-ui/src/components/Charts/BarChart/useExportBarChart.ts b/js/packages/react-ui/src/components/Charts/BarChart/useExportBarChart.ts new file mode 100644 index 00000000..40257540 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarChart/useExportBarChart.ts @@ -0,0 +1,75 @@ +import { toJpeg } from "html-to-image"; +import { useTheme } from "../../ThemeProvider"; + +export const useExportBarChart = ( + chartContainerRef: React.RefObject, + mainContainerRef: React.RefObject, + barChartContainerRef: React.RefObject, + dataWidth: number, + showYAxis: boolean, + yAxisWidth: number, + chartHeight: number, +) => { + const { portalThemeClassName } = useTheme(); + + return { + exportChart: async () => { + const { current: chartContainer } = chartContainerRef; + const { current: mainContainer } = mainContainerRef; + const { current: barChartContainer } = barChartContainerRef; + + if (!chartContainer || !mainContainer || !barChartContainer) { + return; + } + + const screenshotableNode = barChartContainer.cloneNode(true) as HTMLDivElement; + // Position the clone off-screen + screenshotableNode.style.position = "fixed"; + screenshotableNode.style.top = "0"; + screenshotableNode.style.left = "0"; + screenshotableNode.style.zIndex = "1000"; + screenshotableNode.classList.add(portalThemeClassName); + ( + screenshotableNode.querySelector(".crayon-bar-chart-main-container") as HTMLDivElement + ).style.overflow = "visible"; + screenshotableNode.style.opacity = "0"; + // const responsiveContainer = screenshotableNode.querySelector( + // ".crayon-bar-chart-main-container .recharts-responsive-container", + // ) as HTMLElement; + // responsiveContainer.style.marginLeft = `${getComputedStyle(responsiveContainer).marginLeft + 8}px`; + // Append to the body to apply styles + document.body.appendChild(screenshotableNode); + + try { + const dataUrl = await toJpeg(screenshotableNode, { + filter(domNode) { + // do not include buttons in the screenshot + const ignoredClasses = ["crayon-button-base", "crayon-icon-button"]; + if (ignoredClasses.some((className) => domNode.classList?.contains(className))) { + return false; + } + return true; + }, + width: dataWidth + (showYAxis ? yAxisWidth : 0), + height: chartHeight + 100, + canvasHeight: chartHeight + 100, + backgroundColor: "white", + + style: { + overflow: "visible", + opacity: "100%", + }, + }); + const link = document.createElement("a"); + link.download = "chart.jpeg"; + link.href = dataUrl; + link.click(); + } catch (error) { + console.error("Failed to capture chart:", error); + } finally { + // Clean up by removing the clone from the DOM + document.body.removeChild(screenshotableNode); + } + }, + }; +}; diff --git a/js/packages/react-ui/src/components/Charts/BarChart/useExportChart.ts b/js/packages/react-ui/src/components/Charts/BarChart/useExportChart.ts new file mode 100644 index 00000000..13418685 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarChart/useExportChart.ts @@ -0,0 +1,16 @@ +import { createRoot } from "react-dom/client"; + +export const useExportChart = (chartComponent: React.ReactNode) => { + return { + exportChart: () => { + const div = document.createElement("div"); + div.style.position = "fixed"; + document.body.appendChild(div); + + const root = createRoot(div); + root.render(chartComponent); + root.unmount(); + document.body.removeChild(div); + }, + }; +}; diff --git a/js/packages/react-ui/src/components/Charts/hooks/useMaxLabelHeight.tsx b/js/packages/react-ui/src/components/Charts/hooks/useMaxLabelHeight.tsx index bec9aa8d..ccc51c34 100644 --- a/js/packages/react-ui/src/components/Charts/hooks/useMaxLabelHeight.tsx +++ b/js/packages/react-ui/src/components/Charts/hooks/useMaxLabelHeight.tsx @@ -10,6 +10,7 @@ export const useMaxLabelHeight = ( categoryKey: string, tickVariant: XAxisTickVariant, widthOfGroup = 70, + allowFullHeight?: boolean, ) => { const { theme: userTheme } = useTheme(); @@ -49,10 +50,9 @@ export const useMaxLabelHeight = ( document.body.append(div1); - const largestLabelHeight = Math.min( - div2.getBoundingClientRect().height, - div3.getBoundingClientRect().height * 3, - ); + const largestLabelHeight = allowFullHeight + ? div2.getBoundingClientRect().height + : Math.min(div2.getBoundingClientRect().height, div3.getBoundingClientRect().height * 3); div1.remove(); return largestLabelHeight; diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index 86ba67fd..bbb3081c 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -10,28 +10,28 @@ importers: devDependencies: '@typescript-eslint/eslint-plugin': specifier: ^8.21.0 - version: 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3) + version: 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3) eslint: specifier: ^9.19.0 - version: 9.29.0(jiti@1.21.7) + version: 9.29.0(jiti@2.5.1) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.0(eslint@9.29.0(jiti@1.21.7)) + version: 9.1.0(eslint@9.29.0(jiti@2.5.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.29.0(jiti@1.21.7)))(eslint@9.29.0(jiti@1.21.7))(prettier@3.5.3) + version: 5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.29.0(jiti@2.5.1)))(eslint@9.29.0(jiti@2.5.1))(prettier@3.5.3) eslint-plugin-react-hooks: specifier: ^5.1.0 - version: 5.2.0(eslint@9.29.0(jiti@1.21.7)) + version: 5.2.0(eslint@9.29.0(jiti@2.5.1)) eslint-plugin-react-refresh: specifier: ^0.4.18 - version: 0.4.20(eslint@9.29.0(jiti@1.21.7)) + version: 0.4.20(eslint@9.29.0(jiti@2.5.1)) eslint-plugin-storybook: specifier: ^0.11.2 - version: 0.11.6(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3) + version: 0.11.6(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3) eslint-plugin-unused-imports: specifier: ^3.2.0 - version: 3.2.0(@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7)) + version: 3.2.0(@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1)) prettier: specifier: ^3.2.5 version: 3.5.3 @@ -77,25 +77,25 @@ importers: version: 19.1.8 '@typescript-eslint/eslint-plugin': specifier: ^8.18.0 - version: 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3) + version: 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3) eslint: specifier: ^9.17.0 - version: 9.29.0(jiti@1.21.7) + version: 9.29.0(jiti@2.5.1) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.0(eslint@9.29.0(jiti@1.21.7)) + version: 9.1.0(eslint@9.29.0(jiti@2.5.1)) eslint-plugin-prettier: specifier: ^5.2.1 - version: 5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.29.0(jiti@1.21.7)))(eslint@9.29.0(jiti@1.21.7))(prettier@3.5.3) + version: 5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.29.0(jiti@2.5.1)))(eslint@9.29.0(jiti@2.5.1))(prettier@3.5.3) eslint-plugin-react-hooks: specifier: ^5.1.0 - version: 5.2.0(eslint@9.29.0(jiti@1.21.7)) + version: 5.2.0(eslint@9.29.0(jiti@2.5.1)) eslint-plugin-react-refresh: specifier: ^0.4.16 - version: 0.4.20(eslint@9.29.0(jiti@1.21.7)) + version: 0.4.20(eslint@9.29.0(jiti@2.5.1)) eslint-plugin-unused-imports: specifier: ^3.1.0 - version: 3.2.0(@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7)) + version: 3.2.0(@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1)) prettier-plugin-organize-imports: specifier: ^3.2.4 version: 3.2.4(prettier@3.5.3)(typescript@5.8.3) @@ -156,6 +156,9 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + html-to-image: + specifier: 1.11.11 + version: 1.11.11 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -231,13 +234,16 @@ importers: version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) '@storybook/react-vite': specifier: ^8.5.3 - version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.43.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(vite@5.4.19(@types/node@22.15.32)(sass@1.89.2)(terser@5.43.0)) + version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.43.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(vite@5.4.19(@types/node@22.15.32)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.0)) '@storybook/test': specifier: ^8.5.3 version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@storybook/theming': specifier: ^8.5.3 version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) + '@tailwindcss/postcss': + specifier: ^4.1.11 + version: 4.1.11 '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 @@ -258,31 +264,34 @@ importers: version: 15.5.13 '@typescript-eslint/eslint-plugin': specifier: ^8.18.0 - version: 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3) + version: 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3) + autoprefixer: + specifier: ^10.4.21 + version: 10.4.21(postcss@8.5.6) concurrently: specifier: ^9.2.0 version: 9.2.0 eslint: specifier: ^9.17.0 - version: 9.29.0(jiti@1.21.7) + version: 9.29.0(jiti@2.5.1) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.0(eslint@9.29.0(jiti@1.21.7)) + version: 9.1.0(eslint@9.29.0(jiti@2.5.1)) eslint-plugin-prettier: specifier: ^5.2.1 - version: 5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.29.0(jiti@1.21.7)))(eslint@9.29.0(jiti@1.21.7))(prettier@3.5.3) + version: 5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.29.0(jiti@2.5.1)))(eslint@9.29.0(jiti@2.5.1))(prettier@3.5.3) eslint-plugin-react-hooks: specifier: ^5.1.0 - version: 5.2.0(eslint@9.29.0(jiti@1.21.7)) + version: 5.2.0(eslint@9.29.0(jiti@2.5.1)) eslint-plugin-react-refresh: specifier: ^0.4.16 - version: 0.4.20(eslint@9.29.0(jiti@1.21.7)) + version: 0.4.20(eslint@9.29.0(jiti@2.5.1)) eslint-plugin-storybook: specifier: ^0.11.1 - version: 0.11.6(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3) + version: 0.11.6(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3) eslint-plugin-unused-imports: specifier: ^3.1.0 - version: 3.2.0(@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7)) + version: 3.2.0(@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1)) form-data: specifier: ^4.0.0 version: 4.0.3 @@ -315,7 +324,7 @@ importers: version: 4.20.3 vite: specifier: ^5.0.0 - version: 5.4.19(@types/node@22.15.32)(sass@1.89.2)(terser@5.43.0) + version: 5.4.19(@types/node@22.15.32)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.0) packages/stream: dependencies: @@ -340,25 +349,25 @@ importers: version: 22.15.32 '@typescript-eslint/eslint-plugin': specifier: ^8.18.0 - version: 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3) + version: 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3) eslint: specifier: ^9.17.0 - version: 9.29.0(jiti@1.21.7) + version: 9.29.0(jiti@2.5.1) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.0(eslint@9.29.0(jiti@1.21.7)) + version: 9.1.0(eslint@9.29.0(jiti@2.5.1)) eslint-plugin-prettier: specifier: ^5.2.1 - version: 5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.29.0(jiti@1.21.7)))(eslint@9.29.0(jiti@1.21.7))(prettier@3.5.3) + version: 5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.29.0(jiti@2.5.1)))(eslint@9.29.0(jiti@2.5.1))(prettier@3.5.3) eslint-plugin-react-hooks: specifier: ^5.1.0 - version: 5.2.0(eslint@9.29.0(jiti@1.21.7)) + version: 5.2.0(eslint@9.29.0(jiti@2.5.1)) eslint-plugin-react-refresh: specifier: ^0.4.16 - version: 0.4.20(eslint@9.29.0(jiti@1.21.7)) + version: 0.4.20(eslint@9.29.0(jiti@2.5.1)) eslint-plugin-unused-imports: specifier: ^3.1.0 - version: 3.2.0(@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7)) + version: 3.2.0(@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1)) prettier-plugin-organize-imports: specifier: ^3.2.4 version: 3.2.4(prettier@3.5.3)(typescript@5.8.3) @@ -821,6 +830,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0': resolution: {integrity: sha512-qYDdL7fPwLRI+bJNurVcis+tNgJmvWjH4YTBGXTA8xMuxFrnAz6E5o35iyzyKbq5J5Lr8mJGfrR5GXl+WGwhgQ==} peerDependencies: @@ -1729,6 +1742,94 @@ packages: peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + '@tailwindcss/node@4.1.11': + resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} + + '@tailwindcss/oxide-android-arm64@4.1.11': + resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.11': + resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.11': + resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.11': + resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.11': + resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.11': + resolution: {integrity: sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==} + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -2077,6 +2178,13 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -2194,6 +2302,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + chromatic@11.29.0: resolution: {integrity: sha512-yisBlntp9hHVj19lIQdpTlcYIXuU9H/DbFuu6tyWHmj6hWT2EtukCCcxYXL78XdQt1vm2GfIrtgtKpj/Rzmo4A==} hasBin: true @@ -2365,6 +2477,10 @@ packages: engines: {node: '>=0.10'} hasBin: true + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} @@ -2667,6 +2783,9 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2792,6 +2911,9 @@ packages: highlightjs-vue@1.0.0: resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} + html-to-image@1.11.11: + resolution: {integrity: sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -2923,6 +3045,10 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + jiti@2.5.1: + resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2973,6 +3099,70 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -3220,6 +3410,15 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3251,6 +3450,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3784,10 +3987,17 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + tailwindcss@4.1.11: + resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} + tapable@2.2.2: resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} engines: {node: '>=6'} + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + terser-webpack-plugin@5.3.14: resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} engines: {node: '>= 10.13.0'} @@ -4076,6 +4286,10 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml@2.8.0: resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} engines: {node: '>= 14.6'} @@ -4389,9 +4603,9 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.29.0(jiti@1.21.7))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.29.0(jiti@2.5.1))': dependencies: - eslint: 9.29.0(jiti@1.21.7) + eslint: 9.29.0(jiti@2.5.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -4476,12 +4690,16 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@5.4.19(@types/node@22.15.32)(sass@1.89.2)(terser@5.43.0))': + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@5.4.19(@types/node@22.15.32)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.0))': dependencies: glob: 10.4.5 magic-string: 0.27.0 react-docgen-typescript: 2.4.0(typescript@5.8.3) - vite: 5.4.19(@types/node@22.15.32)(sass@1.89.2)(terser@5.43.0) + vite: 5.4.19(@types/node@22.15.32)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.0) optionalDependencies: typescript: 5.8.3 @@ -5245,13 +5463,13 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@storybook/builder-vite@8.6.14(storybook@8.6.14(prettier@3.5.3))(vite@5.4.19(@types/node@22.15.32)(sass@1.89.2)(terser@5.43.0))': + '@storybook/builder-vite@8.6.14(storybook@8.6.14(prettier@3.5.3))(vite@5.4.19(@types/node@22.15.32)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.0))': dependencies: '@storybook/csf-plugin': 8.6.14(storybook@8.6.14(prettier@3.5.3)) browser-assert: 1.2.1 storybook: 8.6.14(prettier@3.5.3) ts-dedent: 2.2.0 - vite: 5.4.19(@types/node@22.15.32)(sass@1.89.2)(terser@5.43.0) + vite: 5.4.19(@types/node@22.15.32)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.0) '@storybook/components@8.6.14(storybook@8.6.14(prettier@3.5.3))': dependencies: @@ -5322,11 +5540,11 @@ snapshots: react-dom: 19.1.0(react@19.1.0) storybook: 8.6.14(prettier@3.5.3) - '@storybook/react-vite@8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.43.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(vite@5.4.19(@types/node@22.15.32)(sass@1.89.2)(terser@5.43.0))': + '@storybook/react-vite@8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.43.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(vite@5.4.19(@types/node@22.15.32)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.0))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@5.4.19(@types/node@22.15.32)(sass@1.89.2)(terser@5.43.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@5.4.19(@types/node@22.15.32)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.0)) '@rollup/pluginutils': 5.2.0(rollup@4.43.0) - '@storybook/builder-vite': 8.6.14(storybook@8.6.14(prettier@3.5.3))(vite@5.4.19(@types/node@22.15.32)(sass@1.89.2)(terser@5.43.0)) + '@storybook/builder-vite': 8.6.14(storybook@8.6.14(prettier@3.5.3))(vite@5.4.19(@types/node@22.15.32)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.0)) '@storybook/react': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) find-up: 5.0.0 magic-string: 0.30.17 @@ -5336,7 +5554,7 @@ snapshots: resolve: 1.22.10 storybook: 8.6.14(prettier@3.5.3) tsconfig-paths: 4.2.0 - vite: 5.4.19(@types/node@22.15.32)(sass@1.89.2)(terser@5.43.0) + vite: 5.4.19(@types/node@22.15.32)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.0) optionalDependencies: '@storybook/test': 8.6.14(storybook@8.6.14(prettier@3.5.3)) transitivePeerDependencies: @@ -5374,6 +5592,78 @@ snapshots: dependencies: storybook: 8.6.14(prettier@3.5.3) + '@tailwindcss/node@4.1.11': + dependencies: + '@ampproject/remapping': 2.3.0 + enhanced-resolve: 5.18.1 + jiti: 2.5.1 + lightningcss: 1.30.1 + magic-string: 0.30.17 + source-map-js: 1.2.1 + tailwindcss: 4.1.11 + + '@tailwindcss/oxide-android-arm64@4.1.11': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.11': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.11': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + optional: true + + '@tailwindcss/oxide@4.1.11': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-x64': 4.1.11 + '@tailwindcss/oxide-freebsd-x64': 4.1.11 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.11 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-x64-musl': 4.1.11 + '@tailwindcss/oxide-wasm32-wasi': 4.1.11 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 + + '@tailwindcss/postcss@4.1.11': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.11 + '@tailwindcss/oxide': 4.1.11 + postcss: 8.5.6 + tailwindcss: 4.1.11 + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 @@ -5525,15 +5815,15 @@ snapshots: '@types/uuid@9.0.8': {} - '@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.34.1 - '@typescript-eslint/type-utils': 8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.34.1 - eslint: 9.29.0(jiti@1.21.7) + eslint: 9.29.0(jiti@2.5.1) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -5542,14 +5832,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3)': + '@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.34.1 '@typescript-eslint/types': 8.34.1 '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.34.1 debug: 4.4.1 - eslint: 9.29.0(jiti@1.21.7) + eslint: 9.29.0(jiti@2.5.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -5572,12 +5862,12 @@ snapshots: dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3)': dependencies: '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3) debug: 4.4.1 - eslint: 9.29.0(jiti@1.21.7) + eslint: 9.29.0(jiti@2.5.1) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -5601,13 +5891,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3)': + '@typescript-eslint/utils@8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.5.1)) '@typescript-eslint/scope-manager': 8.34.1 '@typescript-eslint/types': 8.34.1 '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3) - eslint: 9.29.0(jiti@1.21.7) + eslint: 9.29.0(jiti@2.5.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -5801,6 +6091,16 @@ snapshots: asynckit@0.4.0: {} + autoprefixer@10.4.21(postcss@8.5.6): + dependencies: + browserslist: 4.25.0 + caniuse-lite: 1.0.30001723 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -5918,6 +6218,8 @@ snapshots: dependencies: readdirp: 4.1.2 + chownr@3.0.0: {} + chromatic@11.29.0: {} chrome-trace-event@1.0.4: {} @@ -6047,6 +6349,8 @@ snapshots: detect-libc@1.0.3: optional: true + detect-libc@2.0.4: {} + detect-node-es@1.1.0: {} devlop@1.1.0: @@ -6179,44 +6483,44 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-prettier@9.1.0(eslint@9.29.0(jiti@1.21.7)): + eslint-config-prettier@9.1.0(eslint@9.29.0(jiti@2.5.1)): dependencies: - eslint: 9.29.0(jiti@1.21.7) + eslint: 9.29.0(jiti@2.5.1) - eslint-plugin-prettier@5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.29.0(jiti@1.21.7)))(eslint@9.29.0(jiti@1.21.7))(prettier@3.5.3): + eslint-plugin-prettier@5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.29.0(jiti@2.5.1)))(eslint@9.29.0(jiti@2.5.1))(prettier@3.5.3): dependencies: - eslint: 9.29.0(jiti@1.21.7) + eslint: 9.29.0(jiti@2.5.1) prettier: 3.5.3 prettier-linter-helpers: 1.0.0 synckit: 0.11.8 optionalDependencies: '@types/eslint': 9.6.1 - eslint-config-prettier: 9.1.0(eslint@9.29.0(jiti@1.21.7)) + eslint-config-prettier: 9.1.0(eslint@9.29.0(jiti@2.5.1)) - eslint-plugin-react-hooks@5.2.0(eslint@9.29.0(jiti@1.21.7)): + eslint-plugin-react-hooks@5.2.0(eslint@9.29.0(jiti@2.5.1)): dependencies: - eslint: 9.29.0(jiti@1.21.7) + eslint: 9.29.0(jiti@2.5.1) - eslint-plugin-react-refresh@0.4.20(eslint@9.29.0(jiti@1.21.7)): + eslint-plugin-react-refresh@0.4.20(eslint@9.29.0(jiti@2.5.1)): dependencies: - eslint: 9.29.0(jiti@1.21.7) + eslint: 9.29.0(jiti@2.5.1) - eslint-plugin-storybook@0.11.6(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3): + eslint-plugin-storybook@0.11.6(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3): dependencies: '@storybook/csf': 0.1.13 - '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3) - eslint: 9.29.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3) + eslint: 9.29.0(jiti@2.5.1) ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-unused-imports@3.2.0(@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7)): + eslint-plugin-unused-imports@3.2.0(@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1)): dependencies: - eslint: 9.29.0(jiti@1.21.7) + eslint: 9.29.0(jiti@2.5.1) eslint-rule-composer: 0.3.0 optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.29.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/eslint-plugin': 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.29.0(jiti@2.5.1))(typescript@5.8.3) eslint-rule-composer@0.3.0: {} @@ -6234,9 +6538,9 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.29.0(jiti@1.21.7): + eslint@9.29.0(jiti@2.5.1): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.5.1)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.20.1 '@eslint/config-helpers': 0.2.3 @@ -6272,7 +6576,7 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 1.21.7 + jiti: 2.5.1 transitivePeerDependencies: - supports-color @@ -6385,6 +6689,8 @@ snapshots: format@0.2.2: {} + fraction.js@4.3.7: {} + fsevents@2.3.3: optional: true @@ -6557,6 +6863,8 @@ snapshots: highlightjs-vue@1.0.0: {} + html-to-image@1.11.11: {} + html-url-attributes@3.0.1: {} ignore@5.3.2: {} @@ -6669,6 +6977,8 @@ snapshots: jiti@1.21.7: {} + jiti@2.5.1: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -6710,6 +7020,51 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -7169,6 +7524,12 @@ snapshots: minipass@7.1.2: {} + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@3.0.1: {} + ms@2.1.3: {} mz@2.7.0: @@ -7197,6 +7558,8 @@ snapshots: normalize-path@3.0.0: {} + normalize-range@0.1.2: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -7826,8 +8189,19 @@ snapshots: transitivePeerDependencies: - ts-node + tailwindcss@4.1.11: {} + tapable@2.2.2: {} + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + terser-webpack-plugin@5.3.14(esbuild@0.25.5)(webpack@5.99.9(esbuild@0.25.5)): dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -8030,7 +8404,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@5.4.19(@types/node@22.15.32)(sass@1.89.2)(terser@5.43.0): + vite@5.4.19(@types/node@22.15.32)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.0): dependencies: esbuild: 0.21.5 postcss: 8.5.6 @@ -8038,6 +8412,7 @@ snapshots: optionalDependencies: '@types/node': 22.15.32 fsevents: 2.3.3 + lightningcss: 1.30.1 sass: 1.89.2 terser: 5.43.0 @@ -8119,6 +8494,8 @@ snapshots: yallist@3.1.1: {} + yallist@5.0.0: {} + yaml@2.8.0: {} yargs-parser@21.1.1: {} From a73857f9ecfd38d2206df83f8bc35d0e4668ae19 Mon Sep 17 00:00:00 2001 From: Hrijul Bhatnagar Date: Tue, 19 Aug 2025 16:59:45 +0530 Subject: [PATCH 2/9] Make bar chart export functional --- .../components/Charts/BarChart/BarChart.tsx | 100 ++++++++++-------- .../Charts/BarChart/ChartWatermark.tsx | 45 ++++++++ .../Charts/BarChart/chart-watermark.svg | 11 -- .../Charts/BarChart/useExportBarChart.ts | 19 ++-- .../Charts/BarChart/useExportChart.ts | 16 --- .../components/Charts/ExportContext/index.ts | 12 +++ .../Charts/shared/XAxisTick/XAxisTick.tsx | 4 + 7 files changed, 123 insertions(+), 84 deletions(-) create mode 100644 js/packages/react-ui/src/components/Charts/BarChart/ChartWatermark.tsx delete mode 100644 js/packages/react-ui/src/components/Charts/BarChart/chart-watermark.svg delete mode 100644 js/packages/react-ui/src/components/Charts/BarChart/useExportChart.ts create mode 100644 js/packages/react-ui/src/components/Charts/ExportContext/index.ts 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 3c8d1578..15b4e9d0 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx @@ -1,10 +1,13 @@ import clsx from "clsx"; +import { Download } from "lucide-react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Bar, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; import { useId } from "../../../polyfills"; +import { Button } from "../../Button"; import { useTheme } from "../../ThemeProvider"; import { ChartConfig, ChartContainer, ChartTooltip } from "../Charts"; import { SideBarChartData, SideBarTooltipProvider } from "../context/SideBarTooltipContext"; +import { ExportContextProvider, useExportContext } from "../ExportContext"; import { useMaxLabelHeight, useTransformedKeys, useYAxisLabelWidth } from "../hooks"; import { cartesianGrid, @@ -16,15 +19,10 @@ import { XAxisTickProps, YAxisTick, } from "../shared"; - +import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip"; import { ScrollButtonsHorizontal } from "../shared/ScrollButtonsHorizontal/ScrollButtonsHorizontal"; import { XAxisTickVariant } from "../types"; import { type LegendItem } from "../types/Legend"; -import { useChartPalette, type PaletteName } from "../utils/PalletUtils"; - -import { Download } from "lucide-react"; -import { Button } from "../../Button"; -import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip"; import { findNearestSnapPosition, getBarStackInfo, @@ -36,9 +34,10 @@ import { getDataKeys, getLegendItems, } from "../utils/dataUtils"; +import { useChartPalette, type PaletteName } from "../utils/PalletUtils"; +import { ChartWatermark } from "./ChartWatermark"; import { BarChartData, BarChartVariant } from "./types"; import { useExportBarChart } from "./useExportBarChart"; -import { useExportChart } from "./useExportChart"; import { BAR_WIDTH, getPadding, @@ -69,7 +68,7 @@ export interface BarChartProps { className?: string; height?: number; width?: number; - exportMode?: boolean; + exportRef?: React.RefObject; } const BAR_GAP = 10; // Gap between bars @@ -96,16 +95,17 @@ const BarChartComponent = ({ className, height, width, - exportMode, + exportRef, }: BarChartProps) => { const widthOfGroup = getWidthOfGroup(data, categoryKey as string, variant); + const exportContext = useExportContext(); const maxLabelHeight = useMaxLabelHeight( data, categoryKey as string, tickVariant, widthOfGroup, - exportMode, + !!exportRef, ); const dataKeys = useMemo(() => { @@ -411,46 +411,17 @@ const BarChartComponent = ({ categoryKey, ]); - const barChartContainerRef = useRef(null); - - // const { exportChart: autoExportChart } = useExportChart( - // - // ); - - const { exportChart } = useExportBarChart( + const exportChartRef = useRef(null); + + const { exportBarChart } = useExportBarChart( chartContainerRef, - mainContainerRef, - barChartContainerRef, + exportChartRef ?? null, dataWidth, showYAxis, yAxisWidth, chartHeight, ); - // useEffect(() => { - // if (exportMode) { - // exportChart(); - // } - // }, [exportMode]); - return ( ({ data={sideBarTooltipData} setData={setSideBarTooltipData} > + {!exportContext && ( + + + + )} +
} >
{/* Y-axis of the chart */} @@ -555,9 +553,21 @@ const BarChartComponent = ({ /> )}
- + {exportContext && ( +
+ +
+ )}
diff --git a/js/packages/react-ui/src/components/Charts/BarChart/ChartWatermark.tsx b/js/packages/react-ui/src/components/Charts/BarChart/ChartWatermark.tsx new file mode 100644 index 00000000..767330c7 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/BarChart/ChartWatermark.tsx @@ -0,0 +1,45 @@ +import * as React from "react"; +import { useTheme } from "../../ThemeProvider"; +export const ChartWatermark = (props: React.SVGProps) => { + const { mode } = useTheme(); + + if (mode === "dark") { + return ( + + + + + + + + + ); + } + + return ( + + + + + + + + + ); +}; diff --git a/js/packages/react-ui/src/components/Charts/BarChart/chart-watermark.svg b/js/packages/react-ui/src/components/Charts/BarChart/chart-watermark.svg deleted file mode 100644 index 56d4e130..00000000 --- a/js/packages/react-ui/src/components/Charts/BarChart/chart-watermark.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/js/packages/react-ui/src/components/Charts/BarChart/useExportBarChart.ts b/js/packages/react-ui/src/components/Charts/BarChart/useExportBarChart.ts index 40257540..6dadcaf3 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/useExportBarChart.ts +++ b/js/packages/react-ui/src/components/Charts/BarChart/useExportBarChart.ts @@ -3,22 +3,21 @@ import { useTheme } from "../../ThemeProvider"; export const useExportBarChart = ( chartContainerRef: React.RefObject, - mainContainerRef: React.RefObject, - barChartContainerRef: React.RefObject, + barChartContainerRef: React.RefObject | null, dataWidth: number, showYAxis: boolean, yAxisWidth: number, chartHeight: number, ) => { - const { portalThemeClassName } = useTheme(); + const { mode } = useTheme(); return { - exportChart: async () => { + exportBarChart: async () => { + if (!barChartContainerRef) return; const { current: chartContainer } = chartContainerRef; - const { current: mainContainer } = mainContainerRef; const { current: barChartContainer } = barChartContainerRef; - if (!chartContainer || !mainContainer || !barChartContainer) { + if (!chartContainer || !barChartContainer) { return; } @@ -28,20 +27,16 @@ export const useExportBarChart = ( screenshotableNode.style.top = "0"; screenshotableNode.style.left = "0"; screenshotableNode.style.zIndex = "1000"; - screenshotableNode.classList.add(portalThemeClassName); ( screenshotableNode.querySelector(".crayon-bar-chart-main-container") as HTMLDivElement ).style.overflow = "visible"; screenshotableNode.style.opacity = "0"; - // const responsiveContainer = screenshotableNode.querySelector( - // ".crayon-bar-chart-main-container .recharts-responsive-container", - // ) as HTMLElement; - // responsiveContainer.style.marginLeft = `${getComputedStyle(responsiveContainer).marginLeft + 8}px`; // Append to the body to apply styles document.body.appendChild(screenshotableNode); try { const dataUrl = await toJpeg(screenshotableNode, { + skipFonts: true, filter(domNode) { // do not include buttons in the screenshot const ignoredClasses = ["crayon-button-base", "crayon-icon-button"]; @@ -53,7 +48,7 @@ export const useExportBarChart = ( width: dataWidth + (showYAxis ? yAxisWidth : 0), height: chartHeight + 100, canvasHeight: chartHeight + 100, - backgroundColor: "white", + backgroundColor: mode === "dark" ? "rgba(28,28,28,1)" : "white", style: { overflow: "visible", diff --git a/js/packages/react-ui/src/components/Charts/BarChart/useExportChart.ts b/js/packages/react-ui/src/components/Charts/BarChart/useExportChart.ts deleted file mode 100644 index 13418685..00000000 --- a/js/packages/react-ui/src/components/Charts/BarChart/useExportChart.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createRoot } from "react-dom/client"; - -export const useExportChart = (chartComponent: React.ReactNode) => { - return { - exportChart: () => { - const div = document.createElement("div"); - div.style.position = "fixed"; - document.body.appendChild(div); - - const root = createRoot(div); - root.render(chartComponent); - root.unmount(); - document.body.removeChild(div); - }, - }; -}; diff --git a/js/packages/react-ui/src/components/Charts/ExportContext/index.ts b/js/packages/react-ui/src/components/Charts/ExportContext/index.ts new file mode 100644 index 00000000..d293ed0d --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/ExportContext/index.ts @@ -0,0 +1,12 @@ +import { createContext, useContext } from "react"; + +interface ExportContext { + format: 'image' +} + +export const ExportContext = createContext(null) +export const ExportContextProvider = ExportContext.Provider + +export const useExportContext = () => { + return useContext(ExportContext) +} diff --git a/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx index 99347585..867f6e7d 100644 --- a/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx @@ -1,5 +1,6 @@ import clsx from "clsx"; import React, { useLayoutEffect, useRef, useState } from "react"; +import { useExportContext } from "../../ExportContext"; import { XAxisTickVariant } from "../../types"; import { LabelTooltip } from "../LabelTooltip/LabelTooltip"; @@ -33,6 +34,8 @@ interface XAxisTickProps { } const XAxisTick = React.forwardRef((props, ref) => { + const exportContext = useExportContext(); + const { x, y, @@ -105,6 +108,7 @@ const XAxisTick = React.forwardRef((props, ref) => style={{ textAlign: "center", wordBreak: "break-word", + overflow: exportContext ? 'unset' : undefined }} className={clsx(spanClassName, className)} > From b73b95fe8fc29390c4a3d17607792b8142846b26 Mon Sep 17 00:00:00 2001 From: Hrijul Bhatnagar Date: Thu, 21 Aug 2025 11:50:23 +0530 Subject: [PATCH 3/9] Fix a couple of chart export problems Also refactor inline-styles into scss classes --- .../components/Charts/BarChart/BarChart.tsx | 64 ++++++++++--------- .../components/Charts/BarChart/barChart.scss | 28 ++++++++ .../Charts/shared/XAxisTick/XAxisTick.tsx | 9 ++- .../Charts/shared/XAxisTick/xAxisTick.scss | 4 ++ .../chartExportUtils.ts} | 40 ++++++------ 5 files changed, 94 insertions(+), 51 deletions(-) rename js/packages/react-ui/src/components/Charts/{BarChart/useExportBarChart.ts => utils/chartExportUtils.ts} (58%) 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 15b4e9d0..7eef5c57 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx @@ -3,7 +3,7 @@ import { Download } from "lucide-react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Bar, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; import { useId } from "../../../polyfills"; -import { Button } from "../../Button"; +import { IconButton } from "../../IconButton"; import { useTheme } from "../../ThemeProvider"; import { ChartConfig, ChartContainer, ChartTooltip } from "../Charts"; import { SideBarChartData, SideBarTooltipProvider } from "../context/SideBarTooltipContext"; @@ -28,6 +28,7 @@ import { getBarStackInfo, getRadiusArray, } from "../utils/BarCharts/BarChartsUtils"; +import { useExportChart } from "../utils/chartExportUtils"; import { get2dChartConfig, getColorForDataKey, @@ -37,7 +38,6 @@ import { import { useChartPalette, type PaletteName } from "../utils/PalletUtils"; import { ChartWatermark } from "./ChartWatermark"; import { BarChartData, BarChartVariant } from "./types"; -import { useExportBarChart } from "./useExportBarChart"; import { BAR_WIDTH, getPadding, @@ -134,11 +134,14 @@ const BarChartComponent = ({ const [canScrollRight, setCanScrollRight] = useState(false); const [hoveredCategory, setHoveredCategory] = useState(null); const [isLegendExpanded, setIsLegendExpanded] = useState(false); + const [isChartHovered, setIsChartHovered] = useState(false); const [isSideBarTooltipOpen, setIsSideBarTooltipOpen] = useState(false); const [sideBarTooltipData, setSideBarTooltipData] = useState({ title: "", values: [], }); + const exportChartRef = useRef(null); + const { exportChart } = useExportChart(exportChartRef ?? null, "crayon-bar-chart-main-container"); // Use provided width or observed width const effectiveWidth = useMemo(() => { @@ -411,17 +414,6 @@ const BarChartComponent = ({ categoryKey, ]); - const exportChartRef = useRef(null); - - const { exportBarChart } = useExportBarChart( - chartContainerRef, - exportChartRef ?? null, - dataWidth, - showYAxis, - yAxisWidth, - chartHeight, - ); - return ( ({ theme={theme} customPalette={customPalette} variant={variant} - tickVariant={tickVariant} + tickVariant={"multiLine"} grid={grid} radius={BAR_RADIUS} isAnimationActive={false} @@ -455,13 +447,18 @@ const BarChartComponent = ({ )}
setIsChartHovered(true)} + onMouseLeave={() => setIsChartHovered(false)} ref={exportRef as React.RefObject} >
@@ -548,23 +545,28 @@ const BarChartComponent = ({ yAxisLabel={yAxisLabel} xAxisLabel={xAxisLabel} containerWidth={effectiveWidth} - isExpanded={isLegendExpanded} + isExpanded={isLegendExpanded || !!exportContext} // legend should always be expanded in export mode setIsExpanded={setIsLegendExpanded} /> )} -
- + {isChartHovered && ( +
+ } + onClick={(e) => { + if (exportContext) { + e.preventDefault(); + return; + } + exportChart(); + }} + /> +
+ )} +
{exportContext && ( -
+
)} diff --git a/js/packages/react-ui/src/components/Charts/BarChart/barChart.scss b/js/packages/react-ui/src/components/Charts/BarChart/barChart.scss index 75c49e89..b1a06930 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/barChart.scss +++ b/js/packages/react-ui/src/components/Charts/BarChart/barChart.scss @@ -1,5 +1,9 @@ @use "../../../cssUtils" as cssUtils; +.crayon-bar-chart-container { + position: relative; +} + .crayon-bar-chart-container-inner { display: flex; width: 100%; @@ -23,3 +27,27 @@ /* Hide scrollbar for IE and Edge */ -ms-overflow-style: none; } + +.crayon-bar-chart-export-container { + opacity: 0; + position: fixed; + z-index: -1; +} + +.crayon-bar-chart-download-button-container { + position: absolute; + top: 8px; + right: 8px; +} + +.crayon-bar-chart-export-footer-container { + width: 100%; + display: flex; + justify-content: flex-end; + padding: 24px +} + +.crayon-bar-chart-export-watermark-container { + display: flex; + align-items: center; +} diff --git a/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx index 867f6e7d..74e4e068 100644 --- a/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx @@ -108,9 +108,14 @@ const XAxisTick = React.forwardRef((props, ref) => style={{ textAlign: "center", wordBreak: "break-word", - overflow: exportContext ? 'unset' : undefined }} - className={clsx(spanClassName, className)} + className={clsx( + spanClassName, + { + "crayon-chart-x-axis-tick-export-mode": exportContext, + }, + className, + )} > {value} diff --git a/js/packages/react-ui/src/components/Charts/shared/XAxisTick/xAxisTick.scss b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/xAxisTick.scss index d01cfc89..95fdfa24 100644 --- a/js/packages/react-ui/src/components/Charts/shared/XAxisTick/xAxisTick.scss +++ b/js/packages/react-ui/src/components/Charts/shared/XAxisTick/xAxisTick.scss @@ -24,3 +24,7 @@ width: 100%; display: block; } + +.crayon-chart-x-axis-tick-export-mode { + overflow: unset; +} diff --git a/js/packages/react-ui/src/components/Charts/BarChart/useExportBarChart.ts b/js/packages/react-ui/src/components/Charts/utils/chartExportUtils.ts similarity index 58% rename from js/packages/react-ui/src/components/Charts/BarChart/useExportBarChart.ts rename to js/packages/react-ui/src/components/Charts/utils/chartExportUtils.ts index 6dadcaf3..9627e2cf 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/useExportBarChart.ts +++ b/js/packages/react-ui/src/components/Charts/utils/chartExportUtils.ts @@ -1,34 +1,37 @@ import { toJpeg } from "html-to-image"; -import { useTheme } from "../../ThemeProvider"; +import { useTheme } from "../../ThemeProvider/index.js"; -export const useExportBarChart = ( - chartContainerRef: React.RefObject, - barChartContainerRef: React.RefObject | null, - dataWidth: number, - showYAxis: boolean, - yAxisWidth: number, - chartHeight: number, +export const useExportChart = ( + /** + * The top level container of the chart to screenshot + * + */ + exportChartContainerRef: React.RefObject | null, + /** + * The classname of the container which scrolls. This is used to expand the container before screenshotting to make the entire chart visible + * + */ + scrollableContainerClass?: string ) => { const { mode } = useTheme(); return { - exportBarChart: async () => { - if (!barChartContainerRef) return; - const { current: chartContainer } = chartContainerRef; - const { current: barChartContainer } = barChartContainerRef; + exportChart: async () => { + if (!exportChartContainerRef) return; + const { current: exportChartContainer } = exportChartContainerRef; - if (!chartContainer || !barChartContainer) { + if (!exportChartContainer) { return; } - const screenshotableNode = barChartContainer.cloneNode(true) as HTMLDivElement; + const screenshotableNode = exportChartContainer.cloneNode(true) as HTMLDivElement; // Position the clone off-screen screenshotableNode.style.position = "fixed"; screenshotableNode.style.top = "0"; screenshotableNode.style.left = "0"; screenshotableNode.style.zIndex = "1000"; ( - screenshotableNode.querySelector(".crayon-bar-chart-main-container") as HTMLDivElement + screenshotableNode.querySelector(`.${scrollableContainerClass}`) as HTMLDivElement ).style.overflow = "visible"; screenshotableNode.style.opacity = "0"; // Append to the body to apply styles @@ -45,9 +48,10 @@ export const useExportBarChart = ( } return true; }, - width: dataWidth + (showYAxis ? yAxisWidth : 0), - height: chartHeight + 100, - canvasHeight: chartHeight + 100, + width: Number(getComputedStyle(screenshotableNode).width), + canvasWidth: Number(getComputedStyle(screenshotableNode).width), + height: Number(getComputedStyle(screenshotableNode).height), + canvasHeight: Number(getComputedStyle(screenshotableNode).height), backgroundColor: mode === "dark" ? "rgba(28,28,28,1)" : "white", style: { From 97d5215b7f33fe97bb998f396a498ace739ad88c Mon Sep 17 00:00:00 2001 From: Hrijul Bhatnagar Date: Thu, 21 Aug 2025 12:17:40 +0530 Subject: [PATCH 4/9] Fix chart watermark styling --- .../Charts/BarChart/ChartWatermark.tsx | 18 ++++++++++++++++-- .../components/Charts/BarChart/barChart.scss | 4 +++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/BarChart/ChartWatermark.tsx b/js/packages/react-ui/src/components/Charts/BarChart/ChartWatermark.tsx index 767330c7..9a9a468f 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/ChartWatermark.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChart/ChartWatermark.tsx @@ -5,7 +5,14 @@ export const ChartWatermark = (props: React.SVGProps) => { if (mode === "dark") { return ( - + ) => { } return ( - + Date: Thu, 21 Aug 2025 12:19:35 +0530 Subject: [PATCH 5/9] Move chart watermark to shared folder --- .../react-ui/src/components/Charts/BarChart/BarChart.tsx | 2 +- .../ChartWatermark.tsx => shared/ChartWatermark/index.tsx} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename js/packages/react-ui/src/components/Charts/{BarChart/ChartWatermark.tsx => shared/ChartWatermark/index.tsx} (99%) 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 7eef5c57..b00ad73d 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx @@ -19,6 +19,7 @@ import { XAxisTickProps, YAxisTick, } from "../shared"; +import { ChartWatermark } from "../shared/ChartWatermark"; import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip"; import { ScrollButtonsHorizontal } from "../shared/ScrollButtonsHorizontal/ScrollButtonsHorizontal"; import { XAxisTickVariant } from "../types"; @@ -36,7 +37,6 @@ import { getLegendItems, } from "../utils/dataUtils"; import { useChartPalette, type PaletteName } from "../utils/PalletUtils"; -import { ChartWatermark } from "./ChartWatermark"; import { BarChartData, BarChartVariant } from "./types"; import { BAR_WIDTH, diff --git a/js/packages/react-ui/src/components/Charts/BarChart/ChartWatermark.tsx b/js/packages/react-ui/src/components/Charts/shared/ChartWatermark/index.tsx similarity index 99% rename from js/packages/react-ui/src/components/Charts/BarChart/ChartWatermark.tsx rename to js/packages/react-ui/src/components/Charts/shared/ChartWatermark/index.tsx index 9a9a468f..e365bbe8 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/ChartWatermark.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/ChartWatermark/index.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { useTheme } from "../../ThemeProvider"; +import { useTheme } from "../../../ThemeProvider"; export const ChartWatermark = (props: React.SVGProps) => { const { mode } = useTheme(); From 42fcbc6dfc769b1a71cc8218dc36629b41025b36 Mon Sep 17 00:00:00 2001 From: Hrijul Bhatnagar Date: Thu, 21 Aug 2025 15:20:34 +0530 Subject: [PATCH 6/9] Implement export chart for all charts except horizontal bar & mini --- .../components/Charts/AreaChart/AreaChart.tsx | 49 ++++++- .../Charts/AreaChart/areaChart.scss | 4 + .../components/Charts/BarChart/BarChart.tsx | 40 +---- .../components/Charts/BarChart/barChart.scss | 26 ---- .../components/Charts/LineChart/LineChart.tsx | 58 +++++++- .../Charts/LineChart/lineChart.scss | 4 + .../components/Charts/PieChart/PieChart.tsx | 66 ++++++++- .../components/Charts/PieChart/pieChart.scss | 7 + .../Charts/RadarChart/RadarChart.tsx | 138 ++++++++++++------ .../RadarChart/components/AxisLabel.tsx | 6 +- .../Charts/RadarChart/radarChart.scss | 7 + .../Charts/RadialChart/RadialChart.tsx | 89 +++++++++-- .../Charts/RadialChart/radialChart.scss | 7 + .../src/components/Charts/charts.scss | 8 + .../Charts/hooks/useMaxLabelHeight.tsx | 3 +- .../ChartWatermark.tsx} | 0 .../ChartExportFooter/chartExportFooter.scss | 13 ++ .../Charts/shared/ChartExportFooter/index.tsx | 11 ++ .../shared/ExportButton/exportButton.scss | 5 + .../Charts/shared/ExportButton/index.tsx | 27 ++++ .../Charts/utils/chartExportUtils.ts | 11 +- 21 files changed, 450 insertions(+), 129 deletions(-) rename js/packages/react-ui/src/components/Charts/shared/{ChartWatermark/index.tsx => ChartExportFooter/ChartWatermark.tsx} (100%) create mode 100644 js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/chartExportFooter.scss create mode 100644 js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/index.tsx create mode 100644 js/packages/react-ui/src/components/Charts/shared/ExportButton/exportButton.scss create mode 100644 js/packages/react-ui/src/components/Charts/shared/ExportButton/index.tsx 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 5730196a..12d4167e 100644 --- a/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import { Area, AreaChart as RechartsAreaChart, XAxis, YAxis } from "recharts"; import { useId } from "../../../polyfills"; import { ChartConfig, ChartContainer, ChartTooltip } from "../Charts"; +import { ExportContextProvider, useExportContext } from "../ExportContext"; import { SideBarChartData, SideBarTooltipProvider } from "../context/SideBarTooltipContext"; import { useMaxLabelHeight, useTransformedKeys, useYAxisLabelWidth } from "../hooks"; import { @@ -15,6 +16,8 @@ import { XAxisTick, YAxisTick, } from "../shared"; +import { ChartExportFooter } from "../shared/ChartExportFooter"; +import { ExportButton } from "../shared/ExportButton"; import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip"; import { LegendItem, XAxisTickVariant } from "../types"; import { @@ -24,6 +27,7 @@ import { getWidthOfGroup, } from "../utils/AreaAndLine/AreaAndLineUtils"; import { PaletteName, useChartPalette } from "../utils/PalletUtils"; +import { useExportChart } from "../utils/chartExportUtils"; import { get2dChartConfig, getColorForDataKey, @@ -54,6 +58,7 @@ export interface AreaChartProps { className?: string; height?: number; width?: number; + exportRef?: React.RefObject; } const X_AXIS_PADDING = 36; @@ -76,6 +81,7 @@ const AreaChartComponent = ({ className, height, width, + exportRef }: AreaChartProps) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); @@ -113,6 +119,11 @@ const AreaChartComponent = ({ title: "", values: [], }); + const [isChartHovered, setIsChartHovered] = useState(false); + const exportContext = useExportContext(); + + const exportChartRef = useRef(null); + const { exportChart } = useExportChart(exportChartRef, "crayon-area-chart-main-container"); // Use provided width or observed width const effectiveWidth = useMemo(() => { @@ -295,11 +306,43 @@ const AreaChartComponent = ({ data={sideBarTooltipData} setData={setSideBarTooltipData} > + {!exportContext && ( + + + + )}
setIsChartHovered(true)} + onMouseLeave={() => setIsChartHovered(false)} + ref={exportRef} >
{/* Y-axis of the chart */} @@ -408,10 +451,12 @@ const AreaChartComponent = ({ yAxisLabel={yAxisLabel} xAxisLabel={xAxisLabel} containerWidth={effectiveWidth} - isExpanded={isLegendExpanded} + isExpanded={isLegendExpanded || !!exportContext} // legend should always be expanded in export mode setIsExpanded={setIsLegendExpanded} /> )} + {isChartHovered && } + {exportContext && }
diff --git a/js/packages/react-ui/src/components/Charts/AreaChart/areaChart.scss b/js/packages/react-ui/src/components/Charts/AreaChart/areaChart.scss index 2ced964e..a4bafb59 100644 --- a/js/packages/react-ui/src/components/Charts/AreaChart/areaChart.scss +++ b/js/packages/react-ui/src/components/Charts/AreaChart/areaChart.scss @@ -1,5 +1,9 @@ @use "../../../cssUtils" as cssUtils; +.crayon-area-chart-container { + position: relative; +} + .crayon-area-chart-container-inner { display: flex; width: 100%; 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 b00ad73d..46810eae 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx @@ -1,9 +1,7 @@ import clsx from "clsx"; -import { Download } from "lucide-react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Bar, BarChart as RechartsBarChart, XAxis, YAxis } from "recharts"; import { useId } from "../../../polyfills"; -import { IconButton } from "../../IconButton"; import { useTheme } from "../../ThemeProvider"; import { ChartConfig, ChartContainer, ChartTooltip } from "../Charts"; import { SideBarChartData, SideBarTooltipProvider } from "../context/SideBarTooltipContext"; @@ -19,7 +17,8 @@ import { XAxisTickProps, YAxisTick, } from "../shared"; -import { ChartWatermark } from "../shared/ChartWatermark"; +import { ChartExportFooter } from "../shared/ChartExportFooter"; +import { ExportButton } from "../shared/ExportButton"; import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip"; import { ScrollButtonsHorizontal } from "../shared/ScrollButtonsHorizontal/ScrollButtonsHorizontal"; import { XAxisTickVariant } from "../types"; @@ -100,13 +99,7 @@ const BarChartComponent = ({ const widthOfGroup = getWidthOfGroup(data, categoryKey as string, variant); const exportContext = useExportContext(); - const maxLabelHeight = useMaxLabelHeight( - data, - categoryKey as string, - tickVariant, - widthOfGroup, - !!exportRef, - ); + const maxLabelHeight = useMaxLabelHeight(data, categoryKey as string, tickVariant, widthOfGroup); const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); @@ -442,6 +435,7 @@ const BarChartComponent = ({ height={height} width={width} exportRef={exportChartRef} + icons={icons} /> )} @@ -450,7 +444,7 @@ const BarChartComponent = ({ className={clsx( "crayon-bar-chart-container", { - "crayon-bar-chart-export-container": exportContext, + "crayon-chart-export-container": exportContext, }, className, )} @@ -549,28 +543,8 @@ const BarChartComponent = ({ setIsExpanded={setIsLegendExpanded} /> )} - {isChartHovered && ( -
- } - onClick={(e) => { - if (exportContext) { - e.preventDefault(); - return; - } - exportChart(); - }} - /> -
- )} -
- {exportContext && ( -
- -
- )} -
+ {isChartHovered && } + {exportContext && }
diff --git a/js/packages/react-ui/src/components/Charts/BarChart/barChart.scss b/js/packages/react-ui/src/components/Charts/BarChart/barChart.scss index 8ec05096..03823dfe 100644 --- a/js/packages/react-ui/src/components/Charts/BarChart/barChart.scss +++ b/js/packages/react-ui/src/components/Charts/BarChart/barChart.scss @@ -27,29 +27,3 @@ /* Hide scrollbar for IE and Edge */ -ms-overflow-style: none; } - -.crayon-bar-chart-export-container { - opacity: 0; - position: fixed; - z-index: -1; -} - -.crayon-bar-chart-download-button-container { - position: absolute; - top: 8px; - right: 8px; -} - -.crayon-bar-chart-export-footer-container { - width: 100%; - display: flex; - justify-content: flex-end; - padding: min(5%, 8px); -} - -.crayon-bar-chart-export-watermark-container { - display: flex; - align-items: center; - max-height: 24px; - max-width: min(25%, 146px); -} 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 509f9992..4154aba5 100644 --- a/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx +++ b/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx @@ -4,6 +4,7 @@ import { Line, LineChart as RechartsLineChart, XAxis, YAxis } from "recharts"; import { useId } from "../../../polyfills"; import { ChartConfig, ChartContainer, ChartTooltip } from "../Charts"; import { SideBarChartData, SideBarTooltipProvider } from "../context/SideBarTooltipContext"; +import { ExportContextProvider, useExportContext } from "../ExportContext"; import { useMaxLabelHeight, useTransformedKeys, useYAxisLabelWidth } from "../hooks"; import { ActiveDot, @@ -15,6 +16,8 @@ import { XAxisTick, YAxisTick, } from "../shared"; +import { ChartExportFooter } from "../shared/ChartExportFooter"; +import { ExportButton } from "../shared/ExportButton"; import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip"; import { LegendItem, XAxisTickVariant } from "../types"; import { @@ -23,13 +26,14 @@ import { getWidthOfData, getWidthOfGroup, } from "../utils/AreaAndLine/AreaAndLineUtils"; -import { PaletteName, useChartPalette } from "../utils/PalletUtils"; +import { useExportChart } from "../utils/chartExportUtils"; import { get2dChartConfig, getColorForDataKey, getDataKeys, getLegendItems, } from "../utils/dataUtils"; +import { PaletteName, useChartPalette } from "../utils/PalletUtils"; import { LineChartData, LineChartVariant } from "./types"; type LineChartOnClick = React.ComponentProps["onClick"]; @@ -53,12 +57,13 @@ export interface LineChartProps { height?: number; width?: number; strokeWidth?: number; + exportRef?: React.RefObject; } const X_AXIS_PADDING = 36; const CHART_CONTAINER_BOTTOM_MARGIN = 10; -export const LineChart = ({ +const LineChartComponent = ({ data, categoryKey, theme = "ocean", @@ -76,6 +81,7 @@ export const LineChart = ({ height, width, strokeWidth = 2, + exportRef, }: LineChartProps) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); @@ -113,6 +119,11 @@ export const LineChart = ({ title: "", values: [], }); + const [isChartHovered, setIsChartHovered] = useState(false); + const exportContext = useExportContext(); + + const exportChartRef = useRef(null); + const { exportChart } = useExportChart(exportChartRef, "crayon-line-chart-main-container"); // Use provided width or observed width const effectiveWidth = useMemo(() => { @@ -305,11 +316,45 @@ export const LineChart = ({ data={sideBarTooltipData} setData={setSideBarTooltipData} > + {!exportContext && ( + + + + )} +
setIsChartHovered(true)} + onMouseLeave={() => setIsChartHovered(false)} + ref={exportRef} >
{/* Y-axis of the chart */} @@ -397,12 +442,17 @@ export const LineChart = ({ yAxisLabel={yAxisLabel} xAxisLabel={xAxisLabel} containerWidth={effectiveWidth} - isExpanded={isLegendExpanded} + isExpanded={isLegendExpanded || !!exportContext} // legend should always be expanded in export mode setIsExpanded={setIsLegendExpanded} /> )} + {isChartHovered && } + {exportContext && }
); }; + +// Added React.memo for performance optimization to avoid unnecessary re-renders +export const LineChart = React.memo(LineChartComponent) as typeof LineChartComponent; diff --git a/js/packages/react-ui/src/components/Charts/LineChart/lineChart.scss b/js/packages/react-ui/src/components/Charts/LineChart/lineChart.scss index d62a61e5..c082a152 100644 --- a/js/packages/react-ui/src/components/Charts/LineChart/lineChart.scss +++ b/js/packages/react-ui/src/components/Charts/LineChart/lineChart.scss @@ -1,5 +1,9 @@ @use "../../../cssUtils" as cssUtils; +.crayon-line-chart-container { + position: relative; +} + .crayon-line-chart-container-inner { display: flex; width: 100%; 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 eb474c3a..42181a93 100644 --- a/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx +++ b/js/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx @@ -2,10 +2,14 @@ import clsx from "clsx"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Cell, Pie, PieChart as RechartsPieChart } from "recharts"; import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../Charts.js"; +import { ExportContextProvider, useExportContext } from "../ExportContext"; import { useTransformedKeys } from "../hooks/index.js"; +import { ChartExportFooter } from "../shared/ChartExportFooter/index.js"; import { DefaultLegend } from "../shared/DefaultLegend/DefaultLegend.js"; +import { ExportButton } from "../shared/ExportButton/index.js"; import { StackedLegend } from "../shared/StackedLegend/StackedLegend.js"; import { LegendItem } from "../types/Legend.js"; +import { useExportChart } from "../utils/chartExportUtils.js"; import { getCategoricalChartConfig } from "../utils/dataUtils.js"; import { PaletteName, useChartPalette } from "../utils/PalletUtils.js"; import { PieChartData } from "./types/index.js"; @@ -39,6 +43,7 @@ export interface PieChartProps { className?: string; maxChartSize?: number; minChartSize?: number; + exportRef?: React.RefObject; } const STACKED_LEGEND_BREAKPOINT = 400; @@ -65,12 +70,18 @@ const PieChartComponent = ({ className, maxChartSize = MAX_CHART_SIZE, minChartSize = MIN_CHART_SIZE, + exportRef, }: PieChartProps) => { const wrapperRef = useRef(null); const [wrapperRect, setWrapperRect] = useState({ width: 0, height: 0 }); const [hoveredLegendKey, setHoveredLegendKey] = useState(null); const [isLegendExpanded, setIsLegendExpanded] = useState(false); + const [isChartHovered, setIsChartHovered] = useState(false); const { activeIndex, handleMouseEnter, handleMouseLeave } = useChartHover(); + const exportContext = useExportContext(); + + const exportChartRef = useRef(null); + const { exportChart } = useExportChart(exportChartRef); // Determine layout mode based on container width const isRowLayout = @@ -360,7 +371,7 @@ const PieChartComponent = ({ ); @@ -374,6 +385,7 @@ const PieChartComponent = ({ isRowLayout, defaultLegendItems, isLegendExpanded, + exportContext, ]); const wrapperClassName = useMemo( @@ -387,8 +399,13 @@ const PieChartComponent = ({ [className, legend, legendVariant, isRowLayout], ); - return ( -
+ const pieChartJsx = ( +
setIsChartHovered(true)} + onMouseLeave={() => setIsChartHovered(false)} + >
@@ -406,10 +423,53 @@ const PieChartComponent = ({
+ {isChartHovered && }
{renderLegend()}
); + + return ( + <> + {!exportContext && ( + + + + )} + {exportContext ? ( +
+ {pieChartJsx} + {} +
+ ) : ( + pieChartJsx + )} + + ); }; export const PieChart = memo(PieChartComponent); diff --git a/js/packages/react-ui/src/components/Charts/PieChart/pieChart.scss b/js/packages/react-ui/src/components/Charts/PieChart/pieChart.scss index 9e9a51c6..4d60de53 100644 --- a/js/packages/react-ui/src/components/Charts/PieChart/pieChart.scss +++ b/js/packages/react-ui/src/components/Charts/PieChart/pieChart.scss @@ -80,3 +80,10 @@ align-items: center; } } + +.crayon-pie-chart-export-container { + display: flex; + flex-direction: column; + padding: 8px; + gap: 8px; +} 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 6fa5db04..22ab9c8f 100644 --- a/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx @@ -2,11 +2,15 @@ import clsx from "clsx"; import React, { memo, useEffect, useMemo, useRef, useState } from "react"; import { PolarAngleAxis, PolarGrid, Radar, RadarChart as RechartsRadarChart } from "recharts"; import { ChartConfig, ChartContainer, ChartTooltip } from "../Charts"; +import { ExportContextProvider, useExportContext } from "../ExportContext"; import { SideBarTooltipProvider } from "../context/SideBarTooltipContext"; import { useTransformedKeys } from "../hooks/useTransformKey"; import { ActiveDot, CustomTooltipContent, DefaultLegend } from "../shared"; +import { ChartExportFooter } from "../shared/ChartExportFooter"; +import { ExportButton } from "../shared/ExportButton"; import { LegendItem } from "../types"; import { useChartPalette } from "../utils/PalletUtils"; +import { useExportChart } from "../utils/chartExportUtils"; import { get2dChartConfig, getDataKeys, getLegendItems } from "../utils/dataUtils"; import { AxisLabel } from "./components/AxisLabel"; import { RadarChartData } from "./types"; @@ -26,6 +30,7 @@ export interface RadarChartProps { areaOpacity?: number; icons?: Partial>; isAnimationActive?: boolean; + exportRef?: React.RefObject; } const RadarChartComponent = ({ @@ -40,6 +45,7 @@ const RadarChartComponent = ({ areaOpacity = 0.2, icons = {}, isAnimationActive = false, + exportRef, }: RadarChartProps) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); @@ -64,8 +70,13 @@ const RadarChartComponent = ({ }, [dataKeys, colors, icons]); const [isLegendExpanded, setIsLegendExpanded] = useState(false); + const [isChartHovered, setIsChartHovered] = useState(false); const wrapperRef = useRef(null); const [wrapperRect, setWrapperRect] = useState({ width: 0, height: 0 }); + const exportContext = useExportContext(); + + const exportChartRef = useRef(null); + const { exportChart } = useExportChart(exportChartRef); useEffect(() => { const wrapper = wrapperRef.current; @@ -137,6 +148,60 @@ const RadarChartComponent = ({ [], ); + const radarChartJsx = ( +
setIsChartHovered(true)} + onMouseLeave={() => setIsChartHovered(false)} + > +
+
+
+ + + {grid && } + } + /> + + } + /> + {/* rendering the radars here */} + {radars} + + +
+
+
+ {legend && ( + + )} + {isChartHovered && } +
+ ); + return ( ({ data={undefined} setData={() => {}} > -
-
-
-
- - - {grid && } - } - /> - - } - /> - {/* rendering the radars here */} - {radars} - - -
-
-
- {legend && ( - + - )} -
+ + )} + + {!!exportContext ? ( +
+ {radarChartJsx} + {} +
+ ) : ( + radarChartJsx + )}
); }; diff --git a/js/packages/react-ui/src/components/Charts/RadarChart/components/AxisLabel.tsx b/js/packages/react-ui/src/components/Charts/RadarChart/components/AxisLabel.tsx index d03e3fa3..a19c9d16 100644 --- a/js/packages/react-ui/src/components/Charts/RadarChart/components/AxisLabel.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarChart/components/AxisLabel.tsx @@ -1,5 +1,6 @@ import clsx from "clsx"; import React, { useLayoutEffect, useMemo, useRef } from "react"; +import { useExportContext } from "../../ExportContext"; import { calculateAvailableWidth, truncateText } from "../utils"; // This is the props that are passed by recharts to the custom tick component @@ -19,6 +20,7 @@ interface AxisLabelProps { export const AxisLabel: React.FC = (props) => { const { x, y, payload, textAnchor, portalContainerRef, className } = props; const anchorRef = useRef(null); + const exportContext = useExportContext(); /** * Memoizes the calculation of truncated text for axis labels @@ -106,7 +108,9 @@ export const AxisLabel: React.FC = (props) => { textAnchor ?? "middle", 0, ); - const newTruncatedText = truncateText(payload?.value ?? "", availableWidth, 10); + const newTruncatedText = !!exportContext + ? (payload?.value ?? "") + : truncateText(payload?.value ?? "", availableWidth, 10); if (labelEl.textContent !== newTruncatedText) { labelEl.textContent = newTruncatedText; } diff --git a/js/packages/react-ui/src/components/Charts/RadarChart/radarChart.scss b/js/packages/react-ui/src/components/Charts/RadarChart/radarChart.scss index a1a5bfe7..9c834a1b 100644 --- a/js/packages/react-ui/src/components/Charts/RadarChart/radarChart.scss +++ b/js/packages/react-ui/src/components/Charts/RadarChart/radarChart.scss @@ -46,3 +46,10 @@ @include cssUtils.typography(label, 2-extra-small); color: cssUtils.$secondary-text; } + +.crayon-radar-chart-export-container { + display: flex; + flex-direction: column; + padding: 8px; + gap: 8px; +} 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 02bb8fea..ea225b11 100644 --- a/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx @@ -1,11 +1,15 @@ import clsx from "clsx"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Cell, PolarGrid, RadialBar, RadialBarChart } from "recharts"; import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../Charts"; +import { ExportContextProvider, useExportContext } from "../ExportContext"; import { useTransformedKeys } from "../hooks"; +import { ChartExportFooter } from "../shared/ChartExportFooter"; import { DefaultLegend } from "../shared/DefaultLegend/DefaultLegend"; +import { ExportButton } from "../shared/ExportButton"; import { StackedLegend } from "../shared/StackedLegend/StackedLegend"; import { LegendItem } from "../types/Legend"; +import { useExportChart } from "../utils/chartExportUtils"; import { getCategoricalChartConfig } from "../utils/dataUtils"; import { PaletteName, useChartPalette } from "../utils/PalletUtils"; import { RadialChartData } from "./types"; @@ -37,13 +41,14 @@ export interface RadialChartProps { className?: string; maxChartSize?: number; minChartSize?: number; + exportRef?: React.RefObject; } const STACKED_LEGEND_BREAKPOINT = 400; const MIN_CHART_SIZE = 150; const MAX_CHART_SIZE = 500; -export const RadialChart = ({ +const RadialChartComponent = ({ data, categoryKey, dataKey, @@ -62,12 +67,18 @@ export const RadialChart = ({ className, maxChartSize = MAX_CHART_SIZE, minChartSize = MIN_CHART_SIZE, + exportRef, }: RadialChartProps) => { const wrapperRef = useRef(null); const [wrapperRect, setWrapperRect] = useState({ width: 0, height: 0 }); const [hoveredLegendKey, setHoveredLegendKey] = useState(null); const [isLegendExpanded, setIsLegendExpanded] = useState(false); + const [isChartHovered, setIsChartHovered] = useState(false); const { activeIndex, handleMouseEnter, handleMouseLeave } = useRadialChartHover(); + const exportContext = useExportContext(); + + const exportChartRef = useRef(null); + const { exportChart } = useExportChart(exportChartRef); // Determine layout mode based on container width const isRowLayout = @@ -268,7 +279,7 @@ export const RadialChart = ({ ); @@ -282,21 +293,31 @@ export const RadialChart = ({ isRowLayout, defaultLegendItems, isLegendExpanded, + exportContext, ]); - const wrapperClassName = clsx("crayon-radial-chart-container-wrapper", className, { - "layout-row": isRowLayout, - "layout-column": !isRowLayout, - "legend-default": legend && legendVariant === "default", - "legend-stacked": legend && legendVariant === "stacked", - }); + const wrapperClassName = useMemo( + () => + clsx("crayon-radial-chart-container-wrapper", className, { + "layout-row": isRowLayout, + "layout-column": !isRowLayout, + "legend-default": legend && legendVariant === "default", + "legend-stacked": legend && legendVariant === "stacked", + }), + [className, legend, legendVariant, isRowLayout], + ); // Correct angles for semicircle (top half) const startAngle = variant === "semicircle" ? 180 : 0; const endAngle = variant === "semicircle" ? 0 : 360; - return ( -
+ const radialChartJsx = ( +
setIsChartHovered(true)} + onMouseLeave={() => setIsChartHovered(false)} + >
@@ -346,8 +367,54 @@ export const RadialChart = ({
+ {isChartHovered && }
{renderLegend()}
); + + return ( + <> + {!exportContext && ( + + + + )} + {exportContext ? ( +
+ {radialChartJsx} + {} +
+ ) : ( + radialChartJsx + )} + + ); }; + +export const RadialChart = memo(RadialChartComponent); + +RadialChart.displayName = "RadialChart"; diff --git a/js/packages/react-ui/src/components/Charts/RadialChart/radialChart.scss b/js/packages/react-ui/src/components/Charts/RadialChart/radialChart.scss index a89a478a..ef5ab455 100644 --- a/js/packages/react-ui/src/components/Charts/RadialChart/radialChart.scss +++ b/js/packages/react-ui/src/components/Charts/RadialChart/radialChart.scss @@ -80,3 +80,10 @@ align-items: center; // Vertically center legend content } } + +.crayon-radial-chart-export-container { + display: flex; + flex-direction: column; + padding: 8px; + gap: 8px; +} diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index faf37fbf..d589ccd1 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -30,6 +30,8 @@ @forward "./shared/ScrollButtonsHorizontal/scrollButtonsHorizontal.scss"; @forward "./shared/ScrollButtonsVertical/scrollButtonsVertical.scss"; @forward "./shared/LabelTooltip/labelTooltip.scss"; +@forward "./shared/ExportButton/exportButton.scss"; +@forward "./shared/ChartExportFooter/chartExportFooter.scss"; // portal tooltip css @forward "./shared/PortalTooltip/portalTooltip.scss"; @@ -88,3 +90,9 @@ .crayon-chart-label-list { fill: cssUtils.$secondary-text; } + +.crayon-chart-export-container { + opacity: 0; + position: fixed; + z-index: -1; +} diff --git a/js/packages/react-ui/src/components/Charts/hooks/useMaxLabelHeight.tsx b/js/packages/react-ui/src/components/Charts/hooks/useMaxLabelHeight.tsx index ccc51c34..dd40be58 100644 --- a/js/packages/react-ui/src/components/Charts/hooks/useMaxLabelHeight.tsx +++ b/js/packages/react-ui/src/components/Charts/hooks/useMaxLabelHeight.tsx @@ -2,6 +2,7 @@ import { useMemo } from "react"; import { useTheme } from "../../ThemeProvider"; import { XAxisTickVariant } from "../types"; +import { useExportContext } from "../ExportContext"; const DEFAULT_HEIGHT = 30; @@ -10,9 +11,9 @@ export const useMaxLabelHeight = ( categoryKey: string, tickVariant: XAxisTickVariant, widthOfGroup = 70, - allowFullHeight?: boolean, ) => { const { theme: userTheme } = useTheme(); + const allowFullHeight = !!useExportContext() const maxLabelHeight = useMemo(() => { if (typeof window === "undefined" || !data || data.length === 0) { diff --git a/js/packages/react-ui/src/components/Charts/shared/ChartWatermark/index.tsx b/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/ChartWatermark.tsx similarity index 100% rename from js/packages/react-ui/src/components/Charts/shared/ChartWatermark/index.tsx rename to js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/ChartWatermark.tsx diff --git a/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/chartExportFooter.scss b/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/chartExportFooter.scss new file mode 100644 index 00000000..88ad67fd --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/chartExportFooter.scss @@ -0,0 +1,13 @@ +.crayon-chart-export-footer-container { + width: 100%; + display: flex; + justify-content: flex-end; + padding: min(5%, 8px); +} + +.crayon-chart-export-watermark-container { + display: flex; + align-items: center; + max-height: 24px; + max-width: min(25%, 146px); +} diff --git a/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/index.tsx b/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/index.tsx new file mode 100644 index 00000000..41ef8eaa --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/index.tsx @@ -0,0 +1,11 @@ +import { ChartWatermark } from "./ChartWatermark"; + +export const ChartExportFooter = () => { + return ( +
+
+ +
+
+ ); +}; diff --git a/js/packages/react-ui/src/components/Charts/shared/ExportButton/exportButton.scss b/js/packages/react-ui/src/components/Charts/shared/ExportButton/exportButton.scss new file mode 100644 index 00000000..450910aa --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/ExportButton/exportButton.scss @@ -0,0 +1,5 @@ +.crayon-chart-export-button-container { + position: absolute; + top: 8px; + right: 8px; +} diff --git a/js/packages/react-ui/src/components/Charts/shared/ExportButton/index.tsx b/js/packages/react-ui/src/components/Charts/shared/ExportButton/index.tsx new file mode 100644 index 00000000..914b924e --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/ExportButton/index.tsx @@ -0,0 +1,27 @@ +import { Download } from "lucide-react"; +import { IconButton } from "../../../IconButton"; +import { useExportContext } from "../../ExportContext"; + +interface ExportButtonProps { + exportChart: () => void; +} + +export const ExportButton = ({ exportChart }: ExportButtonProps) => { + const exportContext = useExportContext(); + + return ( +
+ } + onClick={(e) => { + if (exportContext) { + e.preventDefault(); + return; + } + exportChart(); + }} + /> +
+ ); +}; diff --git a/js/packages/react-ui/src/components/Charts/utils/chartExportUtils.ts b/js/packages/react-ui/src/components/Charts/utils/chartExportUtils.ts index 9627e2cf..f32b7d5f 100644 --- a/js/packages/react-ui/src/components/Charts/utils/chartExportUtils.ts +++ b/js/packages/react-ui/src/components/Charts/utils/chartExportUtils.ts @@ -11,7 +11,7 @@ export const useExportChart = ( * The classname of the container which scrolls. This is used to expand the container before screenshotting to make the entire chart visible * */ - scrollableContainerClass?: string + scrollableContainerClass?: string, ) => { const { mode } = useTheme(); @@ -30,9 +30,12 @@ export const useExportChart = ( screenshotableNode.style.top = "0"; screenshotableNode.style.left = "0"; screenshotableNode.style.zIndex = "1000"; - ( - screenshotableNode.querySelector(`.${scrollableContainerClass}`) as HTMLDivElement - ).style.overflow = "visible"; + const scrollableContainer = screenshotableNode.querySelector( + `.${scrollableContainerClass}`, + ) as HTMLDivElement | null; + if (scrollableContainer) { + scrollableContainer.style.overflow = "visible"; + } screenshotableNode.style.opacity = "0"; // Append to the body to apply styles document.body.appendChild(screenshotableNode); From 9f31c253b57915ee843f610ea20bf2e9c1f34fe3 Mon Sep 17 00:00:00 2001 From: Hrijul Bhatnagar Date: Thu, 21 Aug 2025 17:08:21 +0530 Subject: [PATCH 7/9] Implement export for HorizontalBarChart --- .../HorizontalBarChart/HorizontalBarChart.tsx | 62 ++++++++++++++++++- .../components/CustomBarShape.tsx | 1 - .../horizontalBarChart.scss | 4 ++ .../Charts/RadarChart/RadarChart.tsx | 4 +- .../Charts/shared/ChartExportFooter/index.tsx | 9 ++- 5 files changed, 73 insertions(+), 7 deletions(-) diff --git a/js/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx b/js/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx index 8b4ab236..f09fcbce 100644 --- a/js/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx @@ -5,6 +5,7 @@ import { useId } from "../../../polyfills"; import { useTheme } from "../../ThemeProvider"; import { ChartConfig, ChartContainer, ChartTooltip } from "../Charts"; import { SideBarChartData, SideBarTooltipProvider } from "../context/SideBarTooltipContext"; +import { ExportContextProvider, useExportContext } from "../ExportContext"; import { useTransformedKeys } from "../hooks"; import { useHorizontalBarLabelHeight } from "../hooks/useMaxLabelHeight"; import { @@ -14,6 +15,8 @@ import { verticalCartesianGrid, YAxisTick, } from "../shared"; +import { ChartExportFooter } from "../shared/ChartExportFooter"; +import { ExportButton } from "../shared/ExportButton"; import { ScrollButtonsVertical } from "../shared/ScrollButtonsVertical"; import { type LegendItem } from "../types/Legend"; @@ -25,6 +28,7 @@ import { getBarStackInfo, getRadiusArray, } from "../utils/BarCharts/BarChartsUtils"; +import { useExportChart } from "../utils/chartExportUtils"; import { get2dChartConfig, getColorForDataKey, @@ -64,6 +68,7 @@ export interface HorizontalBarChartProps { className?: string; height?: number; width?: number; + exportRef?: React.RefObject; } const X_AXIS_HEIGHT = 40; // Height of X-axis chart when shown @@ -88,8 +93,10 @@ const HorizontalBarChartComponent = ({ className, height, width, + exportRef, }: HorizontalBarChartProps) => { const maxCategoryLabelWidth = useMaxCategoryLabelWidth(data, categoryKey as string); + const exportContext = useExportContext(); const chartContainerRef = useRef(null); const mainContainerRef = useRef(null); @@ -98,11 +105,17 @@ const HorizontalBarChartComponent = ({ const [canScrollDown, setCanScrollDown] = useState(false); const [hoveredCategory, setHoveredCategory] = useState(null); const [isLegendExpanded, setIsLegendExpanded] = useState(false); + const [isChartHovered, setIsChartHovered] = useState(false); const [isSideBarTooltipOpen, setIsSideBarTooltipOpen] = useState(false); const [sideBarTooltipData, setSideBarTooltipData] = useState({ title: "", values: [], }); + const exportChartRef = useRef(null); + const { exportChart } = useExportChart( + exportChartRef ?? null, + "crayon-horizontal-bar-chart-container-inner-wrapper", + ); // Calculate chart width for internal calculations (legend, xAxis, etc.) const effectiveWidth = useMemo(() => { @@ -342,11 +355,50 @@ const HorizontalBarChartComponent = ({ data={sideBarTooltipData} setData={setSideBarTooltipData} > -
+ {!exportContext && ( + + + + )} + +
setIsChartHovered(true)} + onMouseLeave={() => setIsChartHovered(false)} + ref={exportRef as React.RefObject} + >
@@ -475,10 +527,14 @@ const HorizontalBarChartComponent = ({ yAxisLabel={yAxisLabel} xAxisLabel={xAxisLabel} containerWidth={effectiveWidth} - isExpanded={isLegendExpanded} + isExpanded={isLegendExpanded || !!exportContext} // legend should always be expanded in export mode setIsExpanded={setIsLegendExpanded} /> )} + {isChartHovered && } + {exportContext && ( + + )}
diff --git a/js/packages/react-ui/src/components/Charts/HorizontalBarChart/components/CustomBarShape.tsx b/js/packages/react-ui/src/components/Charts/HorizontalBarChart/components/CustomBarShape.tsx index 2607c1e5..999f0acc 100644 --- a/js/packages/react-ui/src/components/Charts/HorizontalBarChart/components/CustomBarShape.tsx +++ b/js/packages/react-ui/src/components/Charts/HorizontalBarChart/components/CustomBarShape.tsx @@ -58,7 +58,6 @@ const CustomBarShapeComponent = (props: CustomBarShapeProps) => { width={labelWidth} height={labelHeight} style={{ pointerEvents: "none" }} - xmlns="http://www.w3.org/1999/xhtml" >
{payload[categoryKey]}
diff --git a/js/packages/react-ui/src/components/Charts/HorizontalBarChart/horizontalBarChart.scss b/js/packages/react-ui/src/components/Charts/HorizontalBarChart/horizontalBarChart.scss index b5f96492..11a45ddf 100644 --- a/js/packages/react-ui/src/components/Charts/HorizontalBarChart/horizontalBarChart.scss +++ b/js/packages/react-ui/src/components/Charts/HorizontalBarChart/horizontalBarChart.scss @@ -60,3 +60,7 @@ box-sizing: border-box; } } + +.crayon-horizontal-bar-chart-export-footer { + margin-top: 8px; +} 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 22ab9c8f..254abfa2 100644 --- a/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx +++ b/js/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx @@ -230,7 +230,9 @@ const RadarChartComponent = ({ {!!exportContext ? (
{radarChartJsx} diff --git a/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/index.tsx b/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/index.tsx index 41ef8eaa..c342de7e 100644 --- a/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/index.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/index.tsx @@ -1,8 +1,13 @@ +import clsx from "clsx"; import { ChartWatermark } from "./ChartWatermark"; -export const ChartExportFooter = () => { +interface ChartExportFooterProps { + className?: string; +} + +export const ChartExportFooter = ({ className }: ChartExportFooterProps) => { return ( -
+
From e1b66552d9c1d247c8de6905389878541ec6fea4 Mon Sep 17 00:00:00 2001 From: Hrijul Bhatnagar Date: Thu, 21 Aug 2025 17:08:44 +0530 Subject: [PATCH 8/9] Fix formatting --- js/package.json | 5 +++-- .../src/components/Charts/AreaChart/AreaChart.tsx | 2 +- .../src/components/Charts/ExportContext/index.ts | 10 +++++----- .../src/components/Charts/hooks/useMaxLabelHeight.tsx | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/js/package.json b/js/package.json index c7e2f013..c8344302 100644 --- a/js/package.json +++ b/js/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "devDependencies": { + "devDependencies": { "typescript": "^5.5.4", "@typescript-eslint/eslint-plugin": "^8.21.0", "eslint": "^9.19.0", @@ -21,5 +21,6 @@ }, "keywords": [], "author": "engineering@crayon.ai", - "license": "MIT" + "license": "MIT", + "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0" } 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 12d4167e..48395373 100644 --- a/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx @@ -81,7 +81,7 @@ const AreaChartComponent = ({ className, height, width, - exportRef + exportRef, }: AreaChartProps) => { const dataKeys = useMemo(() => { return getDataKeys(data, categoryKey as string); diff --git a/js/packages/react-ui/src/components/Charts/ExportContext/index.ts b/js/packages/react-ui/src/components/Charts/ExportContext/index.ts index d293ed0d..02017f6c 100644 --- a/js/packages/react-ui/src/components/Charts/ExportContext/index.ts +++ b/js/packages/react-ui/src/components/Charts/ExportContext/index.ts @@ -1,12 +1,12 @@ import { createContext, useContext } from "react"; interface ExportContext { - format: 'image' + format: "image"; } -export const ExportContext = createContext(null) -export const ExportContextProvider = ExportContext.Provider +export const ExportContext = createContext(null); +export const ExportContextProvider = ExportContext.Provider; export const useExportContext = () => { - return useContext(ExportContext) -} + return useContext(ExportContext); +}; diff --git a/js/packages/react-ui/src/components/Charts/hooks/useMaxLabelHeight.tsx b/js/packages/react-ui/src/components/Charts/hooks/useMaxLabelHeight.tsx index dd40be58..5755a75f 100644 --- a/js/packages/react-ui/src/components/Charts/hooks/useMaxLabelHeight.tsx +++ b/js/packages/react-ui/src/components/Charts/hooks/useMaxLabelHeight.tsx @@ -1,8 +1,8 @@ import { useMemo } from "react"; import { useTheme } from "../../ThemeProvider"; -import { XAxisTickVariant } from "../types"; import { useExportContext } from "../ExportContext"; +import { XAxisTickVariant } from "../types"; const DEFAULT_HEIGHT = 30; @@ -13,7 +13,7 @@ export const useMaxLabelHeight = ( widthOfGroup = 70, ) => { const { theme: userTheme } = useTheme(); - const allowFullHeight = !!useExportContext() + const allowFullHeight = !!useExportContext(); const maxLabelHeight = useMemo(() => { if (typeof window === "undefined" || !data || data.length === 0) { From 526dda189749b6f85c4b1a6db95451a015cc0563 Mon Sep 17 00:00:00 2001 From: Hrijul Bhatnagar Date: Thu, 21 Aug 2025 17:14:14 +0530 Subject: [PATCH 9/9] Fix a linting error --- .../Charts/shared/ChartExportFooter/ChartWatermark.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/ChartWatermark.tsx b/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/ChartWatermark.tsx index e365bbe8..12d34e17 100644 --- a/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/ChartWatermark.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/ChartExportFooter/ChartWatermark.tsx @@ -1,5 +1,6 @@ -import * as React from "react"; +import React from "react"; import { useTheme } from "../../../ThemeProvider"; + export const ChartWatermark = (props: React.SVGProps) => { const { mode } = useTheme();