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 (
-
-
-
- );
- }
-}
+ return () => {
+ clearTimeout(loadTimeout);
+ };
+ }, []);
+
+ const { size, radius, thickness, className, circleProps, textProps } = props;
+ const halfSize = size / 2;
+ const circumference = getCircumference(radius);
+
+ return (
+
+
+
+ );
+};
ComplexDonut.propTypes = {
size: PropTypes.number.isRequired,