diff --git a/.npmignore b/.npmignore index 388ba1e..8bfbfee 100644 --- a/.npmignore +++ b/.npmignore @@ -36,7 +36,10 @@ yarn.lock # Config files .babelrc +.github +.whitesource renovate.json webpack.config.js _config.yml _.config.yml +__test__ diff --git a/README.md b/README.md index 5c904ed..cd5afa1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ # React SVG Donuts -A ReactJS component for simple (and complex) SVG donuts. +> A ReactJS component for simple (and complex) SVG donuts. + +**The current version depends on the Hooks API introduced with React 16. If you need legacy React support, please use a 1.x.x version.** ## Demo diff --git a/__test__/__snapshots__/index.test.js.snap b/__test__/__snapshots__/index.test.js.snap index 991f1ac..c6b23e6 100644 --- a/__test__/__snapshots__/index.test.js.snap +++ b/__test__/__snapshots__/index.test.js.snap @@ -113,6 +113,8 @@ exports[`Donuts should render a complex donut 1`] = ` `; +exports[`Donuts should render a complex donut 2`] = `null`; + exports[`Donuts should render a simple donut with custom props 1`] = `
{ }); it('should render a complex donut', () => { - const tree = renderer.create( - - ); + let tree; + + act(() => { + tree = renderer.create( + + ); + }); + + expect(tree).toMatchSnapshot(); + + tree.unmount(); expect(tree).toMatchSnapshot(); }); diff --git a/dist/complex.js b/dist/complex.js index c7b38c2..a152d41 100644 --- a/dist/complex.js +++ b/dist/complex.js @@ -13,224 +13,170 @@ require("./complex.css"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } -function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } - function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } - -function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } +function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } -function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } -function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } -function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } -function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } +function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } -function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } - -function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } var rotateAngle = 0; -var ComplexDonut = /*#__PURE__*/function (_React$Component) { - _inherits(ComplexDonut, _React$Component); - - var _super = _createSuper(ComplexDonut); - - /** - * @type {NodeJS.Timeout} - */ - function ComplexDonut(props) { - var _this; - - _classCallCheck(this, ComplexDonut); - - _this = _super.call(this, props); - - _defineProperty(_assertThisInitialized(_this), "loadTimeout", null); - - _defineProperty(_assertThisInitialized(_this), "total", function (values) { - return values.reduce(function (acc, _ref) { - var value = _ref.value; - return acc + value; - }, 0); - }); - - _defineProperty(_assertThisInitialized(_this), "percent", function (value, total) { - return value / total; - }); - - _defineProperty(_assertThisInitialized(_this), "transforms", function () { - var rotations = []; - var textCoords = []; - var _this$props = _this.props, - startAngle = _this$props.startAngle, - segments = _this$props.segments; - - var total = _this.total(segments); - - rotateAngle = startAngle; - - _this.sortValues(segments).forEach(function (_ref2) { - var value = _ref2.value; - var data = rotateAngle; +var getTotal = function getTotal(values) { + return values.reduce(function (acc, _ref) { + var value = _ref.value; + return acc + value; + }, 0); +}; - var percent = _this.percent(value, total); +var getPercent = function getPercent(value, total) { + return value / total; +}; - var _this$textCoordinates = _this.textCoordinates(value, rotateAngle), - x = _this$textCoordinates.x, - y = _this$textCoordinates.y; +var sortValues = function sortValues(values) { + return values.sort(function (a, b) { + return b.value - a.value; + }); +}; - rotations.push(data); - textCoords.push({ - x: x, - y: y - }); - var result = rotations[rotations.length - 1] || startAngle; - rotateAngle = percent * 360 + result; - }); +var getCircumference = function getCircumference(radius) { + return 2 * Math.PI * radius; +}; - return { - rotations: rotations, - textCoords: textCoords - }; - }); +var convertDegreesToRadians = function convertDegreesToRadians(angle) { + return angle * (Math.PI / 180); +}; - _defineProperty(_assertThisInitialized(_this), "sortValues", function (values) { - return values.sort(function (a, b) { - return b.value - a.value; +var ComplexDonut = function ComplexDonut(props) { + var loadTimeout; + var total = getTotal(props.segments); + + var getTextCoordinates = function getTextCoordinates(value, angleOffset) { + var size = props.size, + radius = props.radius, + segments = props.segments; + var total = getTotal(segments); + var angle = getPercent(value, total) * 360 / 2 + angleOffset; + var radians = convertDegreesToRadians(angle); + return { + x: radius * Math.cos(radians) + size / 2, + y: radius * Math.sin(radians) + size / 2 + }; + }; + + var getTransforms = function getTransforms() { + var rotations = []; + var textCoords = []; + var startAngle = props.startAngle, + segments = props.segments; + var total = getTotal(segments); + rotateAngle = startAngle; + sortValues(segments).forEach(function (_ref2) { + var value = _ref2.value; + var data = rotateAngle; + var percent = getPercent(value, total); + + var _getTextCoordinates = getTextCoordinates(value, rotateAngle), + x = _getTextCoordinates.x, + y = _getTextCoordinates.y; + + rotations.push(data); + textCoords.push({ + x: x, + y: y }); + var result = rotations[rotations.length - 1] || startAngle; + rotateAngle = percent * 360 + result; }); - - _defineProperty(_assertThisInitialized(_this), "circumference", function (radius) { - return 2 * Math.PI * radius; - }); - - _defineProperty(_assertThisInitialized(_this), "degreesToRadians", function (angle) { - return angle * (Math.PI / 180); - }); - - _defineProperty(_assertThisInitialized(_this), "strokeDashOffset", function (value, circumference) { - var diff = _this.percent(value, _this.state.total) * circumference; - return circumference - diff; - }); - - _defineProperty(_assertThisInitialized(_this), "textCoordinates", function (value, angleOffset) { - var _this$props2 = _this.props, - size = _this$props2.size, - radius = _this$props2.radius, - segments = _this$props2.segments; - - var total = _this.total(segments); - - var angle = _this.percent(value, total) * 360 / 2 + angleOffset; - - var radians = _this.degreesToRadians(angle); - + return { + rotations: rotations, + textCoords: textCoords + }; + }; + + var getStrokeDashOffset = function getStrokeDashOffset(value, circumference) { + var diff = getPercent(value, total) * circumference; + return circumference - diff; + }; + + var _React$useState = _react["default"].useState([]), + _React$useState2 = _slicedToArray(_React$useState, 2), + segments = _React$useState2[0], + setSegments = _React$useState2[1]; + + var _React$useState3 = _react["default"].useState(false), + _React$useState4 = _slicedToArray(_React$useState3, 2), + isLoaded = _React$useState4[0], + setIsLoaded = _React$useState4[1]; + + _react["default"].useEffect(function () { + var segments = props.segments, + size = props.size; + + var _getTransforms = getTransforms(), + rotations = _getTransforms.rotations, + textCoords = _getTransforms.textCoords; + + setSegments(sortValues(segments).map(function (_ref3, i) { + var value = _ref3.value, + color = _ref3.color; return { - x: radius * Math.cos(radians) + size / 2, - y: radius * Math.sin(radians) + size / 2 + value: value, + color: color, + percent: getPercent(value, total), + rotate: "rotate(".concat(rotations[i], ", ").concat(size / 2, ", ").concat(size / 2, ")"), + textCoords: textCoords[i] }; - }); - - _defineProperty(_assertThisInitialized(_this), "componentDidMount", function () { - var _this$props3 = _this.props, - segments = _this$props3.segments, - size = _this$props3.size; - var _this$state = _this.state, - total = _this$state.total, - _this$state$transform = _this$state.transforms, - rotations = _this$state$transform.rotations, - textCoords = _this$state$transform.textCoords; - - _this.setState({ - segments: _this.sortValues(segments).map(function (_ref3, i) { - var value = _ref3.value, - color = _ref3.color; - return { - value: value, - color: color, - percent: _this.percent(value, total), - rotate: "rotate(".concat(rotations[i], ", ").concat(size / 2, ", ").concat(size / 2, ")"), - textCoords: textCoords[i] - }; - }) - }); - - _this.loadTimeout = setTimeout(function () { - _this.setState({ - isLoaded: true - }); - }, 100); - }); - - _this.state = { - total: _this.total(props.segments), - segments: [], - transforms: _this.transforms(), - isLoaded: false + })); + loadTimeout = setTimeout(function () { + setIsLoaded(true); + }, 100); + return function () { + clearTimeout(loadTimeout); }; - return _this; - } - - _createClass(ComplexDonut, [{ - key: "componentWillUnmount", - value: function componentWillUnmount() { - clearTimeout(this.loadTimeout); - } - }, { - key: "render", - value: function render() { - var _this2 = this; - - var _this$props4 = this.props, - size = _this$props4.size, - radius = _this$props4.radius, - thickness = _this$props4.thickness, - className = _this$props4.className, - circleProps = _this$props4.circleProps, - textProps = _this$props4.textProps; - var halfSize = size / 2; - var circumference = this.circumference(radius); - return /*#__PURE__*/_react["default"].createElement("div", { - className: "donut-complex".concat(this.state.isLoaded ? ' donut-complex--loaded ' : ' ').concat(className) - }, /*#__PURE__*/_react["default"].createElement("svg", { - height: size, - width: size, - viewBox: "0 0 ".concat(size, " ").concat(size) - }, this.state.segments.map(function (segment, i) { - return /*#__PURE__*/_react["default"].createElement("g", { - key: i - }, /*#__PURE__*/_react["default"].createElement("circle", _extends({}, circleProps, { - r: radius, - cx: halfSize, - cy: halfSize, - transform: segment.rotate, - stroke: segment.color, - strokeWidth: thickness, - strokeDasharray: circumference, - strokeDashoffset: _this2.strokeDashOffset(segment.value, circumference) - })), /*#__PURE__*/_react["default"].createElement("text", _extends({}, textProps, { - x: segment.textCoords.x, - y: segment.textCoords.y, - dy: "3px", - textAnchor: "middle" - }), "".concat(Math.round(segment.percent * 100), "%"))); - }))); - } - }]); - - return ComplexDonut; -}(_react["default"].Component); + }, []); + + var size = props.size, + radius = props.radius, + thickness = props.thickness, + className = props.className, + circleProps = props.circleProps, + textProps = props.textProps; + var halfSize = size / 2; + var circumference = getCircumference(radius); + return /*#__PURE__*/_react["default"].createElement("div", { + className: "donut-complex".concat(isLoaded ? ' donut-complex--loaded ' : ' ').concat(className) + }, /*#__PURE__*/_react["default"].createElement("svg", { + height: size, + width: size, + viewBox: "0 0 ".concat(size, " ").concat(size) + }, segments.map(function (segment, i) { + return /*#__PURE__*/_react["default"].createElement("g", { + key: i + }, /*#__PURE__*/_react["default"].createElement("circle", _extends({}, circleProps, { + r: radius, + cx: halfSize, + cy: halfSize, + transform: segment.rotate, + stroke: segment.color, + strokeWidth: thickness, + strokeDasharray: circumference, + strokeDashoffset: getStrokeDashOffset(segment.value, circumference) + })), /*#__PURE__*/_react["default"].createElement("text", _extends({}, textProps, { + x: segment.textCoords.x, + y: segment.textCoords.y, + dy: "3px", + textAnchor: "middle" + }), "".concat(Math.round(segment.percent * 100), "%"))); + }))); +}; exports.ComplexDonut = ComplexDonut; ComplexDonut.propTypes = { diff --git a/package.json b/package.json index 2f6ef7f..d313838 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-svg-donuts", - "version": "1.0.1", + "version": "2.0.0", "description": "A ReactJS component for simple SVG donut graphs.", "main": "dist/index.js", "style": "dist/index.css", diff --git a/src/complex.js b/src/complex.js index d87f6e6..4726457 100644 --- a/src/complex.js +++ b/src/complex.js @@ -5,39 +5,41 @@ let rotateAngle = 0; import './complex.css'; -class ComplexDonut extends React.Component { - /** - * @type {NodeJS.Timeout} - */ - loadTimeout = null; - - constructor(props) { - super(props); - - this.state = { - total: this.total(props.segments), - segments: [], - transforms: this.transforms(), - isLoaded: false - }; - } +const getTotal = values => values.reduce((acc, { value }) => acc + value, 0); +const getPercent = (value, total) => value / total; +const sortValues = values => values.sort((a, b) => b.value - a.value); +const getCircumference = radius => 2 * Math.PI * radius; +const convertDegreesToRadians = angle => angle * (Math.PI / 180); + +const ComplexDonut = props => { + let loadTimeout; + + const total = getTotal(props.segments); - total = values => values.reduce((acc, { value }) => acc + value, 0); + const getTextCoordinates = (value, angleOffset) => { + const { size, radius, segments } = props; + const total = getTotal(segments); + const angle = (getPercent(value, total) * 360) / 2 + angleOffset; + const radians = convertDegreesToRadians(angle); - percent = (value, total) => value / total; + return { + x: radius * Math.cos(radians) + size / 2, + y: radius * Math.sin(radians) + size / 2 + }; + }; - transforms = () => { + const getTransforms = () => { const rotations = []; const textCoords = []; - const { startAngle, segments } = this.props; - const total = this.total(segments); + const { startAngle, segments } = props; + const total = getTotal(segments); rotateAngle = startAngle; - this.sortValues(segments).forEach(({ value }) => { + sortValues(segments).forEach(({ value }) => { const data = rotateAngle; - const percent = this.percent(value, total); - const { x, y } = this.textCoordinates(value, rotateAngle); + const percent = getPercent(value, total); + const { x, y } = getTextCoordinates(value, rotateAngle); rotations.push(data); textCoords.push({ x, y }); @@ -50,94 +52,72 @@ class ComplexDonut extends React.Component { return { rotations, textCoords }; }; - sortValues = values => values.sort((a, b) => b.value - a.value); - - circumference = radius => 2 * Math.PI * radius; - - degreesToRadians = angle => angle * (Math.PI / 180); - - strokeDashOffset = (value, circumference) => { - const diff = this.percent(value, this.state.total) * circumference; + const getStrokeDashOffset = (value, circumference) => { + const diff = getPercent(value, total) * circumference; return circumference - diff; }; - textCoordinates = (value, angleOffset) => { - const { size, radius, segments } = this.props; - const total = this.total(segments); - const angle = (this.percent(value, total) * 360) / 2 + angleOffset; - const radians = this.degreesToRadians(angle); + const [segments, setSegments] = React.useState([]); + const [isLoaded, setIsLoaded] = React.useState(false); - return { - x: radius * Math.cos(radians) + size / 2, - y: radius * Math.sin(radians) + size / 2 - }; - }; - - componentDidMount = () => { - const { segments, size } = this.props; - const { - total, - transforms: { rotations, textCoords } - } = this.state; + React.useEffect(() => { + const { segments, size } = props; + const { rotations, textCoords } = getTransforms(); - this.setState({ - segments: this.sortValues(segments).map(({ value, color }, i) => ({ + setSegments( + sortValues(segments).map(({ value, color }, i) => ({ value, color, - percent: this.percent(value, total), + percent: getPercent(value, total), rotate: `rotate(${rotations[i]}, ${size / 2}, ${size / 2})`, textCoords: textCoords[i] })) - }); + ); - this.loadTimeout = setTimeout(() => { - this.setState({ - isLoaded: true - }); + loadTimeout = setTimeout(() => { + setIsLoaded(true); }, 100); - }; - componentWillUnmount() { - clearTimeout(this.loadTimeout); - } - - render() { - const { size, radius, thickness, className, circleProps, textProps } = this.props; - const halfSize = size / 2; - const circumference = this.circumference(radius); - - return ( -
- - {this.state.segments.map((segment, i) => ( - - - - {`${Math.round(segment.percent * 100)}%`} - - - ))} - -
- ); - } -} + return () => { + clearTimeout(loadTimeout); + }; + }, []); + + const { size, radius, thickness, className, circleProps, textProps } = props; + const halfSize = size / 2; + const circumference = getCircumference(radius); + + return ( +
+ + {segments.map((segment, i) => ( + + + + {`${Math.round(segment.percent * 100)}%`} + + + ))} + +
+ ); +}; ComplexDonut.propTypes = { size: PropTypes.number.isRequired,