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
JavaScript
"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;