UNPKG

react-native-funnel-graph

Version:

A flexible, SVG-based funnel chart component for React Native to visualize sequential data and conversion rates.

177 lines (174 loc) 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var _react = _interopRequireDefault(require("react")); var _reactNative = require("react-native"); var _reactNativeSvg = _interopRequireWildcard(require("react-native-svg")); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); } function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } 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; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } var ConeStack = function ConeStack(_ref) { var yOffset = _ref.yOffset, _ref$color = _ref.color, color = _ref$color === void 0 ? {} : _ref$color, topWidth = _ref.topWidth, bottomWidth = _ref.bottomWidth, height = _ref.height, centerX = _ref.centerX, _ref$bottleneckHeight = _ref.bottleneckHeight, bottleneckHeight = _ref$bottleneckHeight === void 0 ? 0 : _ref$bottleneckHeight; var ellipseHeight = 22.7; var ellipseBaseRx = 177.073 / 2; var ellipseScaleX = topWidth > 0 ? topWidth / (ellipseBaseRx * 2) : 0; var sideStartY = ellipseHeight / 2; var sideColor = color.side || '#CCCCCC'; var topColor = color.top || '#AAAAAA'; var sidePath = bottleneckHeight > 0 ? "M ".concat(-topWidth / 2, ",").concat(sideStartY, "\n L ").concat(-bottomWidth / 2, ",").concat(sideStartY + height, "\n L ").concat(-bottomWidth / 2, ",").concat(sideStartY + height + bottleneckHeight, "\n L ").concat(bottomWidth / 2, ",").concat(sideStartY + height + bottleneckHeight, "\n L ").concat(bottomWidth / 2, ",").concat(sideStartY + height, "\n L ").concat(topWidth / 2, ",").concat(sideStartY, "\n Z") : "M ".concat(-topWidth / 2, ",").concat(sideStartY, "\n L ").concat(-bottomWidth / 2, ",").concat(sideStartY + height, "\n L ").concat(bottomWidth / 2, ",").concat(sideStartY + height, "\n L ").concat(topWidth / 2, ",").concat(sideStartY, "\n Z"); return /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.G, { transform: "translate(".concat(centerX, ", ").concat(yOffset, ")") }, /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Path, { d: sidePath, fill: sideColor }), /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.G, { transform: "scale(".concat(ellipseScaleX, ", 1)") }, /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Ellipse, { cx: 0, cy: sideStartY, rx: ellipseBaseRx, ry: ellipseHeight / 2, fill: topColor }))); }; var FunnelChart = function FunnelChart(_ref2) { var _data$2; var _ref2$width = _ref2.width, width = _ref2$width === void 0 ? 350 : _ref2$width, _ref2$height = _ref2.height, height = _ref2$height === void 0 ? 200 : _ref2$height, _ref2$data = _ref2.data, data = _ref2$data === void 0 ? [] : _ref2$data, _ref2$yAxisLabels = _ref2.yAxisLabels, yAxisLabels = _ref2$yAxisLabels === void 0 ? ['100%', '75%', '50%', '25%', '0%'] : _ref2$yAxisLabels, _ref2$maxFunnelWidthR = _ref2.maxFunnelWidthRatio, maxFunnelWidthRatio = _ref2$maxFunnelWidthR === void 0 ? 0.85 : _ref2$maxFunnelWidthR, _ref2$minFunnelWidthR = _ref2.minFunnelWidthRatio, minFunnelWidthRatio = _ref2$minFunnelWidthR === void 0 ? 0.2 : _ref2$minFunnelWidthR; // Chart layout constants var yAxisAreaWidth = 50; var paddingTop = 10; var paddingBottom = 20; var chartWidth = width - yAxisAreaWidth; var availableChartHeight = height - paddingTop - paddingBottom; var funnelCenterX = chartWidth / 2; var ellipseHeight = 22.7; // Funnel dimension constants var bottleneckStartValue = 20; var maxFunnelWidth = chartWidth * maxFunnelWidthRatio; var minFunnelWidth = chartWidth * minFunnelWidthRatio; var hasData = data && data.length > 0; // Pre-calculate properties for each funnel segment var segments = _react["default"].useMemo(function () { var _data$; if (!hasData) return []; var maxValue = ((_data$ = data[0]) === null || _data$ === void 0 ? void 0 : _data$.value) || 100; var heightPerValue = availableChartHeight / maxValue; var totalTaperedHeight = Math.max(0, (maxValue - bottleneckStartValue) * heightPerValue); var currentY = paddingTop - ellipseHeight / 2; var cumulativeHeight = 0; return data === null || data === void 0 ? void 0 : data.map(function (item, index) { var _data; var itemValue = item.value || 0; var itemLabel = item.label || ''; var nextValue = ((_data = data[index + 1]) === null || _data === void 0 ? void 0 : _data.value) || bottleneckStartValue; if (itemValue <= nextValue) return null; var topProgress = totalTaperedHeight > 0 ? cumulativeHeight / totalTaperedHeight : 0; var coneHeight = (itemValue - nextValue) * heightPerValue; var bottomProgress = totalTaperedHeight > 0 ? (cumulativeHeight + coneHeight) / totalTaperedHeight : 0; var topWidth = maxFunnelWidth - topProgress * (maxFunnelWidth - minFunnelWidth); var bottomWidth = maxFunnelWidth - bottomProgress * (maxFunnelWidth - minFunnelWidth); var yOffset = currentY; currentY += coneHeight; cumulativeHeight += coneHeight; var fontSizeFromWidth = bottomWidth / (itemLabel.length * 0.7); var fontSizeFromHeight = coneHeight * 0.4; var fontSize = Math.min(14, Math.max(8, Math.min(fontSizeFromWidth, fontSizeFromHeight))); return _objectSpread(_objectSpread({}, item), {}, { topWidth: topWidth, bottomWidth: bottomWidth, coneHeight: coneHeight, yOffset: yOffset, fontSize: fontSize, label: itemLabel }); }).filter(Boolean); }, [data, hasData, availableChartHeight, maxFunnelWidth, minFunnelWidth, paddingTop]); var bottleneckHeight = hasData ? bottleneckStartValue * (availableChartHeight / (((_data$2 = data[0]) === null || _data$2 === void 0 ? void 0 : _data$2.value) || 100)) : 0; return /*#__PURE__*/_react["default"].createElement(_reactNative.View, { style: { width: width, height: height, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', padding: 10 } }, /*#__PURE__*/_react["default"].createElement(_reactNativeSvg["default"], { width: width, height: height, viewBox: "0 0 ".concat(width, " ").concat(height) }, hasData && /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.G, null, yAxisLabels === null || yAxisLabels === void 0 ? void 0 : yAxisLabels.map(function (label, index) { var yPos = index * (availableChartHeight / (yAxisLabels.length - 1)) + paddingTop; return /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.G, { key: "y-axis-".concat(index) }, /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Line, { x1: 0, y1: yPos, x2: width, y2: yPos, stroke: "#EAEAEA", strokeDasharray: "4,4" }), /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Text, { x: yAxisAreaWidth - 10, y: yPos, dy: "4", fill: "#888", fontSize: 12, textAnchor: "end", alignmentBaseline: "hanging" }, label)); })), /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.G, { transform: "translate(".concat(yAxisAreaWidth, ", 0)") }, hasData && /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, segments === null || segments === void 0 ? void 0 : segments.map(function (segment, index) { var isLastSegment = index === segments.length - 1; return /*#__PURE__*/_react["default"].createElement(ConeStack, { key: "cone-".concat(index), yOffset: segment === null || segment === void 0 ? void 0 : segment.yOffset, color: segment === null || segment === void 0 ? void 0 : segment.colors, topWidth: segment === null || segment === void 0 ? void 0 : segment.topWidth, bottomWidth: segment === null || segment === void 0 ? void 0 : segment.bottomWidth, height: segment === null || segment === void 0 ? void 0 : segment.coneHeight, centerX: funnelCenterX, bottleneckHeight: isLastSegment ? bottleneckHeight : 0 }); }), segments === null || segments === void 0 ? void 0 : segments.map(function (segment, index) { var labelY = segment.yOffset + ellipseHeight / 2 + segment.coneHeight / 2; return /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Text, { key: "label-".concat(index), x: funnelCenterX, y: labelY, fill: segment.textColor || '#000', fontSize: segment === null || segment === void 0 ? void 0 : segment.fontSize, fontWeight: "500", textAnchor: "middle", alignmentBaseline: "middle" }, segment === null || segment === void 0 ? void 0 : segment.label); }))))); }; var _default = exports["default"] = FunnelChart;