diff --git a/js/packages/react-ui/package.json b/js/packages/react-ui/package.json index 37c8ff193..1b893d457 100644 --- a/js/packages/react-ui/package.json +++ b/js/packages/react-ui/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@crayonai/react-ui", "license": "MIT", - "version": "0.8.4", + "version": "0.8.5", "description": "Component library for Generative UI SDK", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -66,6 +66,7 @@ "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", + "@radix-ui/react-tooltip": "^1.2.7", "clsx": "^2.1.1", "date-fns": "^4.1.0", "lodash-es": "^4.17.21", @@ -96,6 +97,7 @@ "@storybook/theming": "^8.5.3", "@types/lodash-es": "^4.17.12", "@types/node": "^22.12.0", + "@types/node-fetch": "2.6.11", "@types/react": ">=17.0.0", "@types/react-dom": ">=17.0.0", "@types/react-syntax-highlighter": "^15.5.13", @@ -108,7 +110,6 @@ "eslint-plugin-storybook": "^0.11.1", "eslint-plugin-unused-imports": "^3.1.0", "form-data": "^4.0.0", - "@types/node-fetch": "2.6.11", "postcss": "^8.5.1", "postcss-js": "^4.0.1", "postcss-remove-duplicate-values": "^1.0.0", 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 9b11960d4..cd546f97a 100644 --- a/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx @@ -15,6 +15,7 @@ import { XAxisTick, YAxisTick, } from "../shared"; +import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip"; import { LegendItem, XAxisTickVariant } from "../types"; import { findNearestSnapPosition, @@ -293,129 +294,131 @@ const AreaChartComponent = ({ }, [showYAxis, chartHeight, data, dataKeys, variant, id, maxLabelHeight]); return ( - -
+ -
- {/* Y-axis of the chart */} - {yAxis} -
- - +
+ {/* Y-axis of the chart */} + {yAxis} +
+ - {grid && cartesianGrid()} - - } - orientation="bottom" - padding={{ - left: X_AXIS_PADDING, - right: X_AXIS_PADDING, + - - } offset={15} /> - - {dataKeys.map((key) => { - const transformedKey = transformedKeys[key]; - const color = `var(--color-${transformedKey})`; - return ( - - - - - - - ); - })} - - {dataKeys.map((key) => { - const transformedKey = transformedKeys[key]; - const color = `var(--color-${transformedKey})`; - return ( - } - dot={false} - isAnimationActive={isAnimationActive} - /> - ); - })} - - + onClick={onAreaClick} + > + {grid && cartesianGrid()} + + } + orientation="bottom" + padding={{ + left: X_AXIS_PADDING, + right: X_AXIS_PADDING, + }} + /> + + } offset={15} /> + + {dataKeys.map((key) => { + const transformedKey = transformedKeys[key]; + const color = `var(--color-${transformedKey})`; + return ( + + + + + + + ); + })} + + {dataKeys.map((key) => { + const transformedKey = transformedKeys[key]; + const color = `var(--color-${transformedKey})`; + return ( + } + dot={false} + isAnimationActive={isAnimationActive} + /> + ); + })} + + +
+ {isSideBarTooltipOpen && }
- {isSideBarTooltipOpen && } -
- {/* if the data width is greater than the effective width, then show the scroll buttons */} - - {legend && ( - - )} -
-
+ {legend && ( + + )} +
+
+ ); }; 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 4f669ceb2..57032796e 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 { ScrollButtonsHorizontal } from "../shared/ScrollButtonsHorizontal/Scrol import { XAxisTickVariant, type LegendItem } from "../types"; import { useChartPalette, type PaletteName } from "../utils/PalletUtils"; +import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip"; import { get2dChartConfig, getColorForDataKey, @@ -332,139 +333,141 @@ const BarChartComponent = ({ ); return ( - -
+ -
- {/* Y-axis of the chart */} - {yAxis} -
- - +
+ {/* Y-axis of the chart */} + {yAxis} +
+ - {grid && cartesianGrid()} - - } - orientation="bottom" - // gives the padding on the 2 sides see the function for reference - padding={padding} - /> - {/* Y-axis is rendered in the separate synchronized chart */} - - } - cursor={{ - fill: "var(--crayon-sunk-fills)", - stroke: "var(--crayon-stroke-default)", - opacity: 1, - strokeWidth: 1, + } - offset={15} - /> - - {dataKeys.map((key, index) => { - const transformedKey = transformedKeys[key]; - const color = `var(--color-${transformedKey})`; - const isFirstInStack = index === 0; - const isLastInStack = index === dataKeys.length - 1; - - return ( - - } - /> - ); - })} - - + onClick={onBarsClick} + onMouseMove={handleChartMouseMove} + onMouseLeave={handleChartMouseLeave} + barGap={BAR_GAP} + barCategoryGap={BAR_CATEGORY_GAP} + > + {grid && cartesianGrid()} + + } + orientation="bottom" + // gives the padding on the 2 sides see the function for reference + padding={padding} + /> + {/* Y-axis is rendered in the separate synchronized chart */} + + } + cursor={{ + fill: "var(--crayon-sunk-fills)", + stroke: "var(--crayon-stroke-default)", + opacity: 1, + strokeWidth: 1, + }} + content={} + offset={15} + /> + + {dataKeys.map((key, index) => { + const transformedKey = transformedKeys[key]; + const color = `var(--color-${transformedKey})`; + const isFirstInStack = index === 0; + const isLastInStack = index === dataKeys.length - 1; + + return ( + + } + /> + ); + })} + + +
+ {isSideBarTooltipOpen && }
- {isSideBarTooltipOpen && } -
- {/* if the data width is greater than the effective width, then show the scroll buttons */} - - {legend && ( - - )} -
-
+ {legend && ( + + )} +
+
+ ); }; 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 5b1a0038b..b032162dc 100644 --- a/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx +++ b/js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx @@ -15,6 +15,7 @@ import { XAxisTick, YAxisTick, } from "../shared"; +import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip"; import { LegendItem, XAxisTickVariant } from "../types"; import { findNearestSnapPosition, @@ -302,107 +303,109 @@ export const LineChart = ({ ]); return ( - -
+ -
- {/* Y-axis of the chart */} - {yAxis} -
- - +
+ {/* Y-axis of the chart */} + {yAxis} +
+ - {grid && cartesianGrid()} - - } - orientation="bottom" - padding={{ - left: X_AXIS_PADDING, - right: X_AXIS_PADDING, + - - } offset={15} /> - - {dataKeys.map((key) => { - const transformedKey = transformedKeys[key]; - const color = `var(--color-${transformedKey})`; - return ( - } - isAnimationActive={isAnimationActive} - /> - ); - })} - - + onClick={onLineClick} + > + {grid && cartesianGrid()} + + } + orientation="bottom" + padding={{ + left: X_AXIS_PADDING, + right: X_AXIS_PADDING, + }} + /> + + } offset={15} /> + + {dataKeys.map((key) => { + const transformedKey = transformedKeys[key]; + const color = `var(--color-${transformedKey})`; + return ( + } + isAnimationActive={isAnimationActive} + /> + ); + })} + + +
+ {isSideBarTooltipOpen && }
- {isSideBarTooltipOpen && } -
- {/* if the data width is greater than the effective width, then show the scroll buttons */} - - {legend && ( - - )} -
-
+ {legend && ( + + )} +
+
+ ); }; diff --git a/js/packages/react-ui/src/components/Charts/charts.scss b/js/packages/react-ui/src/components/Charts/charts.scss index d364fc021..9b991dd0e 100644 --- a/js/packages/react-ui/src/components/Charts/charts.scss +++ b/js/packages/react-ui/src/components/Charts/charts.scss @@ -28,6 +28,7 @@ @forward "./shared/StackedLegend/stackedLegend.scss"; @forward "./shared/YAxisTick/yAxisTick.scss"; @forward "./shared/ScrollButtonsHorizontal/scrollButtonsHorizontal.scss"; +@forward "./shared/LabelTooltip/labelTooltip.scss"; // portal tooltip css @forward "./shared/PortalTooltip/portalTooltip.scss"; 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 ce719a78f..cc78f2d56 100644 --- a/js/packages/react-ui/src/components/Charts/hooks/useMaxLabelHeight.tsx +++ b/js/packages/react-ui/src/components/Charts/hooks/useMaxLabelHeight.tsx @@ -60,8 +60,6 @@ export const useMaxLabelHeight = ( if (tickVariant === "multiLine") { return Math.max(maxLabelHeight + 13, DEFAULT_HEIGHT); - } else if (tickVariant === "angled") { - return Math.max(maxLabelHeight + 13, DEFAULT_HEIGHT); } else { return DEFAULT_HEIGHT; } diff --git a/js/packages/react-ui/src/components/Charts/shared/LabelTooltip/LabelTooltip.tsx b/js/packages/react-ui/src/components/Charts/shared/LabelTooltip/LabelTooltip.tsx new file mode 100644 index 000000000..3a8ce48bb --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/LabelTooltip/LabelTooltip.tsx @@ -0,0 +1,85 @@ +import * as Tooltip from "@radix-ui/react-tooltip"; +import React from "react"; + +interface LabelTooltipProviderProps { + children: React.ReactNode; + delayDuration?: number; + skipDelayDuration?: number; + disableHoverableContent?: boolean; +} + +interface LabelTooltipProps { + children: React.ReactNode; + content: string; + side?: "top" | "bottom" | "left" | "right"; + sideOffset?: number; + delayDuration?: number; + className?: string; + disabled?: boolean; + open?: boolean; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; +} + +const DEFAULT_DELAY_DURATION = 300; +const DEFAULT_SKIP_DELAY_DURATION = 300; +const DEFAULT_DISABLE_HOVERABLE_CONTENT = false; + +const LabelTooltipProvider: React.FC = (props) => { + const { + children, + delayDuration = DEFAULT_DELAY_DURATION, + skipDelayDuration = DEFAULT_SKIP_DELAY_DURATION, + disableHoverableContent = DEFAULT_DISABLE_HOVERABLE_CONTENT, + } = props; + + return ( + + {children} + + ); +}; + +const LabelTooltip = React.forwardRef((props, ref) => { + const { + children, + content, + side = "top", + sideOffset = 1, + delayDuration = DEFAULT_DELAY_DURATION, + className = "crayon-chart-label-tooltip", + disabled = false, + open, + defaultOpen, + onOpenChange, + } = props; + + if (disabled) { + return children; + } + + return ( + + {children} + + + {content} + + + + ); +}); + +LabelTooltip.displayName = "LabelTooltip"; + +export { LabelTooltip, LabelTooltipProvider }; +export type { LabelTooltipProps, LabelTooltipProviderProps }; diff --git a/js/packages/react-ui/src/components/Charts/shared/LabelTooltip/labelTooltip.scss b/js/packages/react-ui/src/components/Charts/shared/LabelTooltip/labelTooltip.scss new file mode 100644 index 000000000..ce5ca9420 --- /dev/null +++ b/js/packages/react-ui/src/components/Charts/shared/LabelTooltip/labelTooltip.scss @@ -0,0 +1,88 @@ +@use "../../../../cssUtils" as cssUtils; + +.crayon-chart-label-tooltip { + z-index: 1000; + border-radius: cssUtils.$rounded-s; + padding: cssUtils.$spacing-xs cssUtils.$spacing-s; + background-color: cssUtils.$bg-container; + border: 1px solid cssUtils.$stroke-default; + box-shadow: cssUtils.$shadow-s; + color: cssUtils.$primary-text; + @include cssUtils.typography(label, extra-small); + max-width: 200px; + word-wrap: break-word; + animation-duration: 0.15s; + animation-timing-function: ease-out; + will-change: transform, opacity; +} + +.crayon-chart-label-tooltip[data-state="delayed-open"], +.crayon-chart-label-tooltip[data-state="instant-open"] { + animation-name: tooltipSlideUpAndFade; +} + +.crayon-chart-label-tooltip[data-side="top"] { + animation-name: tooltipSlideDownAndFade; +} + +.crayon-chart-label-tooltip[data-side="bottom"] { + animation-name: tooltipSlideUpAndFade; +} + +.crayon-chart-label-tooltip[data-side="left"] { + animation-name: tooltipSlideRightAndFade; +} + +.crayon-chart-label-tooltip[data-side="right"] { + animation-name: tooltipSlideLeftAndFade; +} + +.crayon-chart-label-tooltip-arrow { + fill: cssUtils.$bg-container; + stroke: cssUtils.$stroke-default; + stroke-width: 1px; +} + +@keyframes tooltipSlideUpAndFade { + from { + opacity: 0; + transform: translateY(2px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes tooltipSlideDownAndFade { + from { + opacity: 0; + transform: translateY(-2px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes tooltipSlideRightAndFade { + from { + opacity: 0; + transform: translateX(-2px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes tooltipSlideLeftAndFade { + from { + opacity: 0; + transform: translateX(2px); + } + to { + opacity: 1; + transform: translateX(0); + } +} 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 f33785593..67e9d698d 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,7 @@ -import React, { useRef } from "react"; +import React, { useLayoutEffect, useRef, useState } from "react"; import { XAxisTickVariant } from "../../types"; +import { LabelTooltip } from "../LabelTooltip/LabelTooltip"; + interface XAxisTickProps { x?: number; y?: number; @@ -43,6 +45,17 @@ const XAxisTick = React.forwardRef((props, ref) => const foreignObjectRef = useRef(null); const spanRef = useRef(null); + const [isMultiLineTruncated, setIsMultiLineTruncated] = useState(false); + + // Check if multiLine text is truncated + useLayoutEffect(() => { + if (variant === "multiLine" && spanRef.current) { + const element = spanRef.current; + // Check if the text is clamped by comparing scrollHeight with clientHeight + const isTruncated = element.scrollHeight > element.clientHeight; + setIsMultiLineTruncated(isTruncated); + } + }, [value, variant, widthOfGroup]); if (x === undefined || y === undefined) { return null; @@ -70,50 +83,35 @@ const XAxisTick = React.forwardRef((props, ref) => boxSizing: "border-box", }} > - - {value} - + + + {value} + + ); } - if (variant === "angled") { - const displayValue = value; - return ( - - - {value} - {displayValue} - - - ); - } - const displayValue = tickFormatter ? tickFormatter(payload?.value) : value; + // Check if displayValue ends with dots (indicating truncation) + const isTruncated = displayValue.endsWith("..."); return ( - - {value} - {displayValue} - + + + {displayValue} + + ); }); diff --git a/js/packages/react-ui/src/components/Charts/types/XAxisTicks.ts b/js/packages/react-ui/src/components/Charts/types/XAxisTicks.ts index 03e2e1370..d7eb6822f 100644 --- a/js/packages/react-ui/src/components/Charts/types/XAxisTicks.ts +++ b/js/packages/react-ui/src/components/Charts/types/XAxisTicks.ts @@ -1 +1 @@ -export type XAxisTickVariant = "singleLine" | "multiLine" | "angled"; +export type XAxisTickVariant = "singleLine" | "multiLine"; diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index 34977c713..090e95148 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -144,6 +144,9 @@ importers: '@radix-ui/react-toggle-group': specifier: ^1.1.2 version: 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-tooltip': + specifier: ^1.2.7 + version: 1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -1309,6 +1312,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-tooltip@1.2.7': + resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-callback-ref@1.1.1': resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: @@ -4898,6 +4914,26 @@ snapshots: '@types/react': 19.1.8 '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-tooltip@1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.8)(react@19.1.0)': dependencies: react: 19.1.0