UNPKG

@razorpay/blade

Version:

The Design System that powers Razorpay

534 lines (523 loc) 26.6 kB
import _typeof from '@babel/runtime/helpers/typeof'; import _slicedToArray from '@babel/runtime/helpers/slicedToArray'; import _objectWithoutProperties from '@babel/runtime/helpers/objectWithoutProperties'; import _defineProperty from '@babel/runtime/helpers/defineProperty'; import _objectDestructuringEmpty from '@babel/runtime/helpers/objectDestructuringEmpty'; import _extends from '@babel/runtime/helpers/extends'; import React__default, { useState, useMemo, isValidElement, useRef, useEffect, createElement } from 'react'; import '../../../node_modules/recharts/es6/index.js'; import '../utils/index.js'; import { componentId as componentId$1 } from '../CommonChartComponents/tokens.js'; import '../CommonChartComponents/index.js'; import { componentId, LABEL_FONT_STYLES, LABEL_DISTANCE_FROM_CENTER, RADIUS_MAPPING, BASE_CONTAINER_SIZE, START_AND_END_ANGLES } from './tokens.js'; import '../../../utils/metaAttribute/index.js'; import getIn from '../../../utils/lodashButBetter/get.js'; import '../../../utils/makeAnalyticsAttribute/index.js'; import '../../BladeProvider/index.js'; import '../../Box/BaseBox/index.js'; import '../../../utils/assignWithoutSideEffects/index.js'; import '../../../utils/isValidAllowedChildren/index.js'; import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import { Cell } from '../../../node_modules/recharts/es6/component/Cell.js'; import { assignWithoutSideEffects } from '../../../utils/assignWithoutSideEffects/assignWithoutSideEffects.js'; import useTheme from '../../BladeProvider/useTheme.js'; import { getComponentId } from '../../../utils/isValidAllowedChildren/isValidAllowedChildren.js'; import useChartsColorTheme from '../utils/useColorTheme.js'; import { sanitizeString } from '../utils/sanitizeString/sanitizeString.js'; import { assignDataColorMapping } from '../utils/assignDataColorMapping/assignDataColorMapping.js'; import { CommonChartComponentsContext, useCommonChartComponentsContext } from '../CommonChartComponents/CommonChartComponentsContext.js'; import { BaseBox } from '../../Box/BaseBox/BaseBox.web.js'; import { metaAttribute } from '../../../utils/metaAttribute/metaAttribute.web.js'; import { makeAnalyticsAttribute } from '../../../utils/makeAnalyticsAttribute/makeAnalyticsAttribute.js'; import { ResponsiveContainer } from '../../../node_modules/recharts/es6/component/ResponsiveContainer.js'; import { PieChart } from '../../../node_modules/recharts/es6/chart/PieChart.js'; import { Label } from '../../../node_modules/recharts/es6/component/Label.js'; import { getHighestColorInRange } from '../utils/getHighestColorInRange.js'; import { Pie } from '../../../node_modules/recharts/es6/polar/Pie.js'; var _excluded = ["children", "content", "testID"], _excluded2 = ["cx", "cy", "radius", "dataKey", "nameKey", "children", "data", "colorTheme", "type"]; function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } var DonutContainerContext = /*#__PURE__*/React__default.createContext({ containerWidth: 0, containerHeight: 0 }); // Helper to calculate scaled radius based on container size var getScaledRadius = function getScaledRadius(baseRadius, containerSize, baseContainerSize) { if (containerSize <= 0) return baseRadius; // Scale the radius proportionally, but cap it at the base value var scaleFactor = Math.min(containerSize / baseContainerSize, 1); return Math.round(baseRadius * scaleFactor); }; // Cell component var _Cell = function _Cell(_ref) { var rest = _extends({}, (_objectDestructuringEmpty(_ref), _ref)); return /*#__PURE__*/jsx(Cell, _objectSpread({}, rest)); }; var ChartDonutCell = /*#__PURE__*/assignWithoutSideEffects(_Cell, { componentId: componentId.cell }); var getTranslate = function getTranslate(legendLayout, legendAlignment, legendWidth, legendHeight) { if (legendLayout === 'vertical') { return "translate(calc(-50% + ".concat(legendAlignment === 'right' ? -legendWidth / 2 : legendWidth / 2, "px) , calc(-50%))"); } return "translate(-50%, calc(-50% - ".concat(legendHeight / 2, "px))"); }; /** * Gets the item name from data based on nameKey. * nameKey can be a string (used as property key) or a function (called with data item). */ var getItemName = function getItemName(item, nameKey) { if (!item) return undefined; if (!nameKey) return item.name; if (typeof nameKey === 'function') return nameKey(item); return item[nameKey]; }; var ChartDonutWrapper = function ChartDonutWrapper(_ref2) { var children = _ref2.children, content = _ref2.content, testID = _ref2.testID, restProps = _objectWithoutProperties(_ref2, _excluded); var _useTheme = useTheme(), theme = _useTheme.theme; // State to track which data keys are currently selected (visible) var _useState = useState(undefined), _useState2 = _slicedToArray(_useState, 2), selectedDataKeys = _useState2[0], setSelectedDataKeys = _useState2[1]; var colorTheme = useMemo(function () { if (Array.isArray(children)) { var _donutChild$props; var donutChild = children.find(function (child) { return getComponentId(child) === componentId.chartDonut; }); if (!donutChild || ! /*#__PURE__*/isValidElement(donutChild)) { return 'categorical'; } return (donutChild === null || donutChild === void 0 || (_donutChild$props = donutChild.props) === null || _donutChild$props === void 0 ? void 0 : _donutChild$props.colorTheme) || 'categorical'; } return 'categorical'; }, [children]); var themeColors = useChartsColorTheme({ colorTheme: colorTheme, chartName: 'donut' }); var _useState3 = useState(0), _useState4 = _slicedToArray(_useState3, 2), legendHeight = _useState4[0], setLegendHeight = _useState4[1]; var _useState5 = useState(0), _useState6 = _slicedToArray(_useState5, 2), legendWidth = _useState6[0], setLegendWidth = _useState6[1]; var _useState7 = useState({ width: 0, height: 0 }), _useState8 = _slicedToArray(_useState7, 2), containerDimensions = _useState8[0], setContainerDimensions = _useState8[1]; var chartRef = useRef(null); var isValuePresentInContent = content && _typeof(content) === 'object' && 'value' in content; var isLabelPresentInContent = content && _typeof(content) === 'object' && 'label' in content; useEffect(function () { var mutationObserver = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { if (mutation.type === 'childList') { var _chartRef$current; var legendWrapper = (_chartRef$current = chartRef.current) === null || _chartRef$current === void 0 ? void 0 : _chartRef$current.querySelector('.recharts-legend-wrapper'); if (legendWrapper) { var height = legendWrapper.getBoundingClientRect().height; var width = legendWrapper.getBoundingClientRect().width; setLegendHeight(height); setLegendWidth(width); } } }); }); // ResizeObserver to track container dimensions for responsive radius var resizeObserver = new ResizeObserver(function (entries) { var _iterator = _createForOfIteratorHelper(entries), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var entry = _step.value; var _entry$contentRect = entry.contentRect, width = _entry$contentRect.width, height = _entry$contentRect.height; setContainerDimensions({ width: width, height: height }); } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } }); if (chartRef.current) { mutationObserver.observe(chartRef.current, { childList: true, subtree: true }); resizeObserver.observe(chartRef.current); } return function () { mutationObserver.disconnect(); resizeObserver.disconnect(); }; }, []); var pieChartRadius = useMemo(function () { if (Array.isArray(children)) { var _donutChild$props2; var donutChild = children.find(function (child) { return getComponentId(child) === componentId.chartDonut; }); if (!donutChild || ! /*#__PURE__*/isValidElement(donutChild)) { return 'medium'; } return (donutChild === null || donutChild === void 0 || (_donutChild$props2 = donutChild.props) === null || _donutChild$props2 === void 0 ? void 0 : _donutChild$props2.radius) || 'medium'; } return 'medium'; }, [children]); var legendLayout = useMemo(function () { if (Array.isArray(children)) { var _legendChild$props; var legendChild = children.find(function (child) { return getComponentId(child) === componentId$1.chartLegend; }); if (!legendChild || ! /*#__PURE__*/isValidElement(legendChild)) { return 'horizontal'; } return (legendChild === null || legendChild === void 0 || (_legendChild$props = legendChild.props) === null || _legendChild$props === void 0 ? void 0 : _legendChild$props.layout) || 'horizontal'; } return 'horizontal'; }, [children]); var legendAlignment = useMemo(function () { if (Array.isArray(children)) { var _legendChild$props2; var legendChild = children.find(function (child) { return getComponentId(child) === componentId$1.chartLegend; }); if (!legendChild || ! /*#__PURE__*/isValidElement(legendChild)) { return 'right'; } return (legendChild === null || legendChild === void 0 || (_legendChild$props2 = legendChild.props) === null || _legendChild$props2 === void 0 ? void 0 : _legendChild$props2.align) || 'right'; } return 'right'; }, [children]); /** * We need to check child of ChartDonutWrapper. if they have any custom color we store that. * We need these mapping because colors of tooltip & legend is determine based on this * recharts do provide a color but it is hex code and we need blade color token . */ var dataColorMapping = useMemo(function () { var dataColorMapping = {}; if (Array.isArray(children)) { children.forEach(function (child) { if (getComponentId(child) === componentId.chartDonut) { var data = child.props.data; var nameKey = child.props.nameKey; // Donut Chart can also have <Cell/> which will come under donutChildren. var donutChildren = child.props.children; if (Array.isArray(donutChildren)) { donutChildren.forEach(function (child, index) { var itemName = getItemName(data[index], nameKey); if (getComponentId(child) === componentId.cell && itemName) { var _child$props, _child$props2; // assign colors to the dataColorMapping, if no color is assigned we assign color in `assignDataColorMapping` dataColorMapping[sanitizeString(itemName)] = { colorToken: (_child$props = child.props) === null || _child$props === void 0 ? void 0 : _child$props.color, isCustomColor: Boolean((_child$props2 = child.props) === null || _child$props2 === void 0 ? void 0 : _child$props2.color) }; } }); } else { // if we don't have cell as child component then we can we directly assign theme colors data.forEach(function (item, index) { var itemName = getItemName(item, nameKey); dataColorMapping[sanitizeString(itemName)] = { colorToken: themeColors[index], isCustomColor: false }; }); } } }); } assignDataColorMapping(dataColorMapping, themeColors); return dataColorMapping; }, [children, themeColors]); return /*#__PURE__*/jsx(CommonChartComponentsContext.Provider, { value: { chartName: 'donut', dataColorMapping: dataColorMapping, selectedDataKeys: selectedDataKeys, setSelectedDataKeys: setSelectedDataKeys }, children: /*#__PURE__*/jsx(DonutContainerContext.Provider, { value: { containerWidth: containerDimensions.width, containerHeight: containerDimensions.height }, children: /*#__PURE__*/jsxs(BaseBox, _objectSpread(_objectSpread(_objectSpread(_objectSpread({ ref: chartRef }, metaAttribute({ name: 'donut-chart', testID: testID })), makeAnalyticsAttribute(restProps)), {}, { width: "100%", height: "100%" }, restProps), {}, { position: /*#__PURE__*/isValidElement(content) ? 'relative' : undefined, children: [/*#__PURE__*/jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /*#__PURE__*/jsxs(PieChart, { children: [children, isLabelPresentInContent && /*#__PURE__*/jsx(Label, { position: "center", fill: theme.colors.surface.text.gray.muted, fontSize: theme.typography.fonts.size[LABEL_FONT_STYLES[pieChartRadius].fontSize.label], fontFamily: theme.typography.fonts.family.text, fontWeight: theme.typography.fonts.weight.medium, letterSpacing: theme.typography.letterSpacings[100], dy: isValuePresentInContent ? LABEL_DISTANCE_FROM_CENTER[pieChartRadius].withText : LABEL_DISTANCE_FROM_CENTER[pieChartRadius].normal, children: content === null || content === void 0 ? void 0 : content.label }), isValuePresentInContent && /*#__PURE__*/jsx(Label, { position: "center", fill: theme.colors.surface.text.gray.normal, fontSize: theme.typography.fonts.size[LABEL_FONT_STYLES[pieChartRadius].fontSize.text], fontFamily: theme.typography.fonts.family.heading, fontWeight: theme.typography.fonts.weight.bold, letterSpacing: theme.typography.letterSpacings[100], dy: isLabelPresentInContent ? LABEL_DISTANCE_FROM_CENTER[pieChartRadius].withLabel : LABEL_DISTANCE_FROM_CENTER[pieChartRadius].normal, children: content === null || content === void 0 ? void 0 : content.value })] }) }), /*#__PURE__*/isValidElement(content) && /*#__PURE__*/jsx(BaseBox, { position: "absolute", top: "50%", left: "50%", transform: getTranslate(legendLayout, legendAlignment, legendWidth, legendHeight), zIndex: 10, textAlign: "center", children: content })] })) }) }); }; var _ChartDonut = function _ChartDonut(_ref3) { var _ref3$cx = _ref3.cx, cx = _ref3$cx === void 0 ? '50%' : _ref3$cx, _ref3$cy = _ref3.cy, cy = _ref3$cy === void 0 ? '50%' : _ref3$cy, _ref3$radius = _ref3.radius, radius = _ref3$radius === void 0 ? 'medium' : _ref3$radius, dataKey = _ref3.dataKey, nameKey = _ref3.nameKey, children = _ref3.children, data = _ref3.data, _ref3$colorTheme = _ref3.colorTheme, colorTheme = _ref3$colorTheme === void 0 ? 'categorical' : _ref3$colorTheme, _ref3$type = _ref3.type, type = _ref3$type === void 0 ? 'circle' : _ref3$type, rest = _objectWithoutProperties(_ref3, _excluded2); var baseRadiusConfig = RADIUS_MAPPING[radius]; var baseContainerSize = BASE_CONTAINER_SIZE[radius]; var _React$useContext = React__default.useContext(DonutContainerContext), containerWidth = _React$useContext.containerWidth, containerHeight = _React$useContext.containerHeight; var themeColors = useChartsColorTheme({ colorTheme: colorTheme, chartName: 'donut' }); var _useState9 = useState(null), _useState0 = _slicedToArray(_useState9, 2), hoveredIndex = _useState0[0], setHoveredIndex = _useState0[1]; var _useTheme2 = useTheme(), theme = _useTheme2.theme; var _useCommonChartCompon = useCommonChartComponentsContext(), selectedDataKeys = _useCommonChartCompon.selectedDataKeys; // Filter data based on selectedDataKeys and build index mapping in a single pass // - filteredData: allows the donut chart to re-render with correct proportions // - filteredToOriginalIndexMap: ensures colors remain consistent even when data is filtered var _useMemo = useMemo(function () { if (!selectedDataKeys) return { filteredData: data, filteredToOriginalIndexMap: null }; var filteredData = []; var filteredToOriginalIndexMap = {}; data.forEach(function (item, originalIdx) { var itemName = getItemName(item, nameKey); if (selectedDataKeys.includes(sanitizeString(itemName))) { filteredToOriginalIndexMap[filteredData.length] = originalIdx; filteredData.push(item); } }); return { filteredData: filteredData, filteredToOriginalIndexMap: filteredToOriginalIndexMap }; }, [data, nameKey, selectedDataKeys]), filteredData = _useMemo.filteredData, filteredToOriginalIndexMap = _useMemo.filteredToOriginalIndexMap; // Calculate responsive radius based on container size var containerSize = Math.min(containerWidth, containerHeight); var scaledOuterRadius = getScaledRadius(baseRadiusConfig.outerRadius, containerSize, baseContainerSize); var scaledInnerRadius = getScaledRadius(baseRadiusConfig.innerRadius, containerSize, baseContainerSize); // Stroke inner radius is slightly smaller than outer for the border effect var scaledStrokeInnerRadius = scaledOuterRadius - 0.75; var getCellOpacity = function getCellOpacity(hoveredIndex, currentIndex) { if (hoveredIndex === null) return 1; if (hoveredIndex === currentIndex) return 1; return 0.2; }; var modifiedChildren = useMemo(function () { if (Array.isArray(children)) { // Filter children based on selectedDataKeys to match filtered data var filteredChildren = selectedDataKeys ? children.filter(function (child, index) { if (getComponentId(child) !== componentId.cell) return true; var itemName = getItemName(data[index], nameKey); return selectedDataKeys.includes(sanitizeString(itemName)); }) : children; return filteredChildren.map(function (child, filteredIndex) { if (getComponentId(child) === componentId.cell) { /* Why we are not using React.cloneElement ? just use ChartDonutCell no? cell can never be custom component in recharts. (as of v3.1.2) (https://github.com/recharts/recharts/issues/2788) https://github.com/recharts/recharts/discussions/5474 So we have placeholder component ChartDonutCell. which we replaced by RechartsCell internally so dev can see hover effects working out of box. */ // Use original index for color lookup to maintain consistent colors var originalIndex = filteredToOriginalIndexMap ? filteredToOriginalIndexMap[filteredIndex] : filteredIndex; var fill = getIn(theme.colors, child.props.color || themeColors[originalIndex]); return /*#__PURE__*/createElement(Cell, _objectSpread(_objectSpread({}, child.props), {}, { fill: fill, key: filteredIndex, opacity: getCellOpacity(hoveredIndex, filteredIndex), strokeWidth: 0 })); } else { return child; } }); } return filteredData === null || filteredData === void 0 ? void 0 : filteredData.map(function (_, index) { // Use original index for color lookup to maintain consistent colors var originalIndex = filteredToOriginalIndexMap ? filteredToOriginalIndexMap[index] : index; return /*#__PURE__*/jsx(Cell, { fill: getIn(theme.colors, themeColors[originalIndex]), opacity: getCellOpacity(hoveredIndex, index), strokeWidth: 0 }, index); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [children, data, filteredData, colorTheme, hoveredIndex, themeColors, selectedDataKeys, filteredToOriginalIndexMap, nameKey]); var modifiedExternalDonutChildren = useMemo(function () { if (Array.isArray(children)) { // Filter children based on selectedDataKeys to match filtered data var filteredChildren = selectedDataKeys ? children.filter(function (child, index) { if (getComponentId(child) !== componentId.cell) return true; var itemName = getItemName(data[index], nameKey); return selectedDataKeys.includes(sanitizeString(itemName)); }) : children; return filteredChildren.map(function (child, filteredIndex) { if (getComponentId(child) === componentId.cell) { /* Why we are not using React.cloneElement ? just use ChartDonutCell no? cell can never be custom component in recharts. (as of v3.1.2) (https://github.com/recharts/recharts/issues/2788) https://github.com/recharts/recharts/discussions/5474 So we have placeholder component ChartDonutCell. which we replaced by RechartsCell internally so dev can see hover effects working out of box. */ // Use original index for color lookup to maintain consistent colors var originalIndex = filteredToOriginalIndexMap ? filteredToOriginalIndexMap[filteredIndex] : filteredIndex; var fill = getIn(theme.colors, getHighestColorInRange({ colorToken: child.props.color || themeColors[originalIndex], followIntensityMapping: Boolean(child.props.color) })); return /*#__PURE__*/createElement(Cell, _objectSpread(_objectSpread({}, child.props), {}, { key: "stroke-".concat(filteredIndex), fill: "transparent", stroke: fill // Different stroke color for each cell , strokeWidth: 0.75, strokeOpacity: getCellOpacity(hoveredIndex, filteredIndex) })); } else { return child; } }); } return filteredData === null || filteredData === void 0 ? void 0 : filteredData.map(function (_, index) { // Use original index for color lookup to maintain consistent colors var originalIndex = filteredToOriginalIndexMap ? filteredToOriginalIndexMap[index] : index; return /*#__PURE__*/jsx(Cell, { fill: "transparent", stroke: getIn(theme.colors, getHighestColorInRange({ colorToken: themeColors[originalIndex] })) // Different stroke color for each cell , strokeWidth: 0.75, strokeOpacity: getCellOpacity(hoveredIndex, index) }, "stroke-".concat(index)); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [children, data, filteredData, colorTheme, hoveredIndex, themeColors, selectedDataKeys, filteredToOriginalIndexMap, nameKey]); return /*#__PURE__*/jsxs(Fragment, { children: [/*#__PURE__*/jsx(Pie, _objectSpread(_objectSpread({}, rest), {}, { dataKey: dataKey, nameKey: nameKey, cx: cx, cy: cy, outerRadius: scaledOuterRadius, innerRadius: scaledInnerRadius, data: filteredData, startAngle: START_AND_END_ANGLES[type].startAngle, endAngle: START_AND_END_ANGLES[type].endAngle, onMouseEnter: function onMouseEnter(_, index) { setHoveredIndex(index); }, onMouseLeave: function onMouseLeave() { setHoveredIndex(null); }, paddingAngle: 1.5, children: modifiedChildren })), /*#__PURE__*/jsx(Pie, _objectSpread(_objectSpread({}, rest), {}, { cx: cx, cy: cy, outerRadius: scaledOuterRadius, innerRadius: scaledStrokeInnerRadius, dataKey: dataKey, nameKey: nameKey, data: filteredData, startAngle: START_AND_END_ANGLES[type].startAngle, endAngle: START_AND_END_ANGLES[type].endAngle, fill: "transparent", legendType: "none", tooltipType: "none", paddingAngle: 1.5 // ToolTip is already disabled here.. need to disable pointer events to prevent the tooltip from being shown when the user hovers over the donut chart in some cases , style: { pointerEvents: 'none' }, children: modifiedExternalDonutChildren }))] }); }; var ChartDonut = /*#__PURE__*/assignWithoutSideEffects(_ChartDonut, { componentId: componentId.chartDonut }); export { ChartDonut, ChartDonutCell, ChartDonutWrapper }; //# sourceMappingURL=DonutChart.web.js.map