├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── babel.config.js ├── dist ├── circliful.js └── main.css ├── docs ├── api.md ├── create-new-circle.md ├── dev-environment.md ├── options.md └── style-elements.md ├── package-lock.json ├── package.json ├── public ├── custom.css ├── fraction.html └── index.html ├── src ├── api.ts ├── base-class │ ├── base-circle.ts │ ├── circle-factory.ts │ ├── circle.ts │ ├── options.ts │ └── svg-tags.ts ├── circle-type │ ├── fraction-circle.ts │ ├── half-circle.ts │ ├── plain-circle.ts │ └── simple-circle.ts ├── helper │ ├── object-helper.ts │ ├── style-helper.ts │ └── svg-tags-helper.ts ├── index.ts └── interface │ ├── iattributes.ts │ ├── iavailable-options.ts │ ├── icalculation-params.ts │ ├── idictionary.ts │ ├── iprogress-color.ts │ ├── isize.ts │ ├── itag.ts │ ├── itype.ts │ └── iview-box-attributes.ts ├── style ├── main.scss └── modules │ ├── background-circle.scss │ ├── circle-container.scss │ ├── circle-icon.scss │ ├── circle-text.scss │ ├── foreground-circle.scss │ └── point-circle.scss ├── test ├── base-class │ ├── circle-factory.test.ts │ └── options.test.ts └── helper │ ├── object-helper.test.ts │ └── svg-tags-helper.test.ts ├── tsconfig.json ├── tslint.json ├── webpack.dev.config.js └── webpack.prod.config.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | coverage 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | coverage 4 | public 5 | docs 6 | src 7 | style 8 | test 9 | babel.config.js 10 | tsconfig.json 11 | tslint.json 12 | webpack.dev.config.js 13 | webpack.prod.config.js 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Patric Gutersohn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Circle Statistics # 2 | 3 | Test it https://stackblitz.com/edit/js-2m2bs7 4 | 5 | New implementation of circliful, without any dependencies - dependencies are only used for development like webpack, jest, typescript, tslint and babel. 6 | 7 | * show Infos as Circle Statistics, no images used 8 | * based on SVG 9 | * many options can be set 10 | * fully responsive 11 | 12 | ## How to use circliful 13 | 14 | Include circliful to your Site via script tag. If you want to use font-awesome icons you need to include the files separately. 15 | 16 | Github clone / download 17 | 18 | ``` 19 | 20 | 21 |
22 | 23 | 24 | 31 | ``` 32 | 33 | npm package 34 | 35 | ``` 36 | npm i js-plugin-circliful 37 | ``` 38 | 39 | ```javascript 40 | import {circliful} from 'js-plugin-circliful'; 41 | 42 | circliful.newCircle({ 43 | percent: 50, 44 | id: 'circle', 45 | type: 'simple', 46 | }); 47 | ``` 48 | 49 | ```css 50 | @import 'js-plugin-circliful/dist/main.css'; 51 | ``` 52 | 53 | ```html 54 |
55 | ``` 56 | 57 | ## Documentation 58 | 59 | * [Api](./docs/api.md) 60 | * [Create custom circle](./docs/create-new-circle.md) 61 | * [Setup dev enviroment (with webpack)](./docs/dev-environment.md) 62 | * [List of available options](./docs/options.md) 63 | * [Style your cirles via css](./docs/style-elements.md) 64 | 65 | If you feel there is something missing in the documentation or the library please open a issue. 66 | 67 | Donation 68 | -------- 69 | If you find this plugin useful or/and use it commercially feel free to donate me a cup of coffee :) 70 | 71 | [![](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=D3F2MMNDHQ9KQ) 72 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // for jest (unit testing) 2 | module.exports = { 3 | presets: [ 4 | ["@babel/preset-env", {targets: {node: "current"}}], 5 | "@babel/preset-typescript", 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /dist/circliful.js: -------------------------------------------------------------------------------- 1 | var circliful=function(t){var e={};function i(n){if(e[n])return e[n].exports;var r=e[n]={i:n,l:!1,exports:{}};return t[n].call(r.exports,r,r.exports,i),r.l=!0,r.exports}return i.m=t,i.c=e,i.d=function(t,e,n){i.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},i.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},i.t=function(t,e){if(1&e&&(t=i(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)i.d(n,r,function(e){return t[e]}.bind(null,r));return n},i.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return i.d(e,"a",e),e},i.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},i.p="",i(i.s=7)}([function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=function(){function t(){}return t.setAttributes=function(t,e){for(var i=0,n=Object.entries(e);ia.x&&(u=!0,a.x=a.x-.001),["M",a.x,a.y,"A",n,n,0,d,s,c.x,c.y,u?"Z":""].join(" ")},t.calculatePathEndCoordinates=function(e,i,n,r){return t.polarToCartesian(e,i,n,r)},t}();e.default=n},function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=function(){function t(){}return t.extractPropertyFromObject=function(t,e){var i;return t.hasOwnProperty(e)&&t[e]&&(i=t[e]),i},t}();e.default=n},function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=i(1),r=i(0),o=function(){function t(){}return t.addSvg=function(e){var i=document.createElementNS(t.namespaceURI,"svg");return e.class="circle-container "+n.default.extractPropertyFromObject(e,"class"),r.default.setAttributes(i,e),i},t.addCircle=function(e){var i=document.createElementNS(t.namespaceURI,"circle");return r.default.setAttributes(i,e),i},t.addArc=function(e){var i=document.createElementNS(t.namespaceURI,"path");return r.default.setAttributes(i,e),i},t.addText=function(e){var i=document.createElementNS(t.namespaceURI,"text");return i.setAttributeNS(null,"text-anchor","middle"),r.default.setAttributes(i,e),i},t.addDefs=function(e){var i=document.createElementNS(t.namespaceURI,"defs"),n=document.createElementNS(t.namespaceURI,"linearGradient");r.default.setAttributes(n,{id:"linearGradient"});var o=document.createElementNS(t.namespaceURI,"stop"),s={offset:"0","stop-color":e.gradientStart};r.default.setAttributes(o,s);var a=document.createElementNS(t.namespaceURI,"stop"),c={offset:"1","stop-color":e.gradientEnd};return r.default.setAttributes(a,c),n.appendChild(o),n.appendChild(a),i.appendChild(n),i},t.namespaceURI="http://www.w3.org/2000/svg",t}();e.default=o},function(t,e,i){"use strict";var n=this&&this.__assign||function(){return(n=Object.assign||function(t){for(var e,i=1,n=arguments.length;ie},t.prototype.drawContainer=function(t){var e=this.getViewBoxParams(),i=e.minX,o=e.minY,s=e.width,a=e.height,c=r.default.addSvg(n({width:"100%",height:"100%",viewBox:i+" "+o+" "+s+" "+a,id:"svg-"+this.options.id,preserveAspectRatio:"xMinYMin meet"},t));this.tags.push({element:c,parentId:this.options.id})},t.prototype.getViewBoxParams=function(){var t=this.options,e=t.foregroundCircleWidth,i=t.backgroundCircleWidth,n=i;e>i&&(n=e);var r=this.size.width,o=this.size.height;return(e>5||i>5)&&(r=this.size.width,o=this.size.height),{minX:0,minY:0,width:r,height:o}},t.prototype.append=function(){this.tags.forEach((function(t){document.getElementById(t.parentId).appendChild(t.element)}))},t.prototype.initialize=function(t,e){this.options=t,this.size=e},t}();e.BaseCircle=o},function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=i(0),r=function(){function t(){}return t.animateArc=function(e,i){var r=e.arc,o=e.arcParams,s=e.animationStep,a=e.progressColors,c=o.startAngle?o.startAngle:0,d=o.endAngleGrade?o.endAngleGrade:360,u=this.getMilliseconds(o.ms,o.endAngleGrade),p=Array.isArray(a)&&a.length>0,l=1,h=setInterval((function(e,r,a){var u=d/100*l,f=c<0&&u>286?"1":"0";n.default.setAttributes(e,{d:n.default.describeArc(o.x,o.y,o.radius,c,u,f)}),p&&t.updateCircleColor(l,e,a),((l+=s)>r||l>100)&&(clearInterval(h),"function"==typeof i&&i())}),u,r,o.percent,a)},t.updateCircleColor=function(t,e,i){var r=i.find((function(e){return e.percent===t}));r&&n.default.setAttributes(e,{style:"stroke: "+r.color})},t.getMilliseconds=function(t,e){var i=t||50;return e<=180&&(i/=3),i},t}();e.StyleHelper=r},function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=i(9),r=i(10),o=i(14),s=function(){function t(){}return t.getParentSize=function(t){return{maxSize:100,height:100,width:100}},t.initializeCircleType=function(e,i){void 0===i&&(i=!1);var n=t.getParentSize(e.id),s=r.CircleFactory.create(e.type),a=(new o.default).mergeOptions(e,i);return s.initialize(a,n),s.drawCircle(),s},t.prototype.newCircle=function(e){return t.initializeCircleType(e),new n.Api(e)},t.prototype.newCircleWithDataSet=function(e,i){var r={id:e,type:i,percent:1};return t.initializeCircleType(r,!0),new n.Api(r)},t}();e.default=s},function(t,e,i){"use strict";var n,r=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])})(t,e)},function(t,e){function i(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(i.prototype=e.prototype,new i)});Object.defineProperty(e,"__esModule",{value:!0});var o=i(3),s=i(2),a=i(1),c=i(4),d=i(0),u=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.coordinates={x:0,y:0},e.additionalCssClasses={},e}return r(e,t),e.prototype.initialize=function(e,i){t.prototype.initialize.call(this,e,i);var n=this.size.maxSize;this.coordinates={x:n/2,y:n/2},this.radius=n/2.2,this.options.additionalCssClasses&&(this.additionalCssClasses=this.options.additionalCssClasses),this.animateInView()},e.prototype.drawCircle=function(){var t={class:a.default.extractPropertyFromObject(this.additionalCssClasses,"svgContainer")};this.drawContainer(t),this.options.strokeGradient&&this.drawLinearGradient(),this.drawBackgroundCircle(),this.drawForegroundCircle(),this.options.point&&this.drawPoint(),this.options.icon&&this.drawIcon(),this.drawText(),!this.options.textReplacesPercentage&&this.options.text&&this.drawInfoText(),this.append()},e.prototype.drawBackgroundCircle=function(){var t=a.default.extractPropertyFromObject(this.additionalCssClasses,"backgroundCircle"),e=s.default.addCircle({id:"circle-"+this.options.id,class:"background-circle "+t,cx:String(this.coordinates.x),cy:String(this.coordinates.y),r:String(this.radius),"stroke-width":this.options.backgroundCircleWidth});this.tags.push({element:e,parentId:"svg-"+this.options.id})},e.prototype.drawPoint=function(){var t=this.radius/100*this.options.pointSize,e=a.default.extractPropertyFromObject(this.additionalCssClasses,"point"),i=s.default.addCircle({id:"point-"+this.options.id,class:"point-circle "+e,cx:String(this.coordinates.x),cy:String(this.coordinates.y),r:String(t)});this.tags.push({element:i,parentId:"svg-"+this.options.id})},e.prototype.drawForegroundCircle=function(){var t=3.6*this.options.percent+Number(this.options.startAngle),e=this.options.startAngle?this.options.startAngle:0,i=a.default.extractPropertyFromObject(this.additionalCssClasses,"foregroundCircle"),n={id:"arc-"+this.options.id,class:"foreground-circle "+i,d:d.default.describeArc(this.coordinates.x,this.coordinates.y,this.radius,e,t),"stroke-width":this.options.foregroundCircleWidth,"stroke-linecap":this.options.strokeLinecap};this.options.strokeGradient&&(n.stroke="url(#linearGradient)",n.class="foreground-circle-without-stroke-color "+i);var r=s.default.addArc(n);this.options.animation&&!this.options.startAngle?this.animate(r):this.drawArc(r),this.tags.push({element:r,parentId:"svg-"+this.options.id})},e.prototype.drawArc=function(t){var e={percent:this.options.percent,x:this.coordinates.x,y:this.coordinates.y,radius:this.radius},i=3.6*this.options.percent;d.default.setAttributes(t,{d:d.default.describeArc(e.x,e.y,e.radius,0,i,"0")})},e.prototype.animate=function(t,e){c.StyleHelper.animateArc({arc:t,arcParams:{percent:e||this.options.percent,x:this.coordinates.x,y:this.coordinates.y,radius:this.radius},animationStep:this.options.animationStep,progressColors:this.options.progressColors},this.options.onAnimationEnd)},e.prototype.drawIcon=function(){var t=this.options.icon,e=a.default.extractPropertyFromObject(this.additionalCssClasses,"icon"),i=s.default.addText({id:"text-"+this.options.id,x:String(this.coordinates.x),y:String(this.coordinates.y-25),class:"circle-icon fa "+e});i.innerHTML="&#x"+t+";",this.tags.push({element:i,parentId:"svg-"+this.options.id})},e.prototype.drawText=function(){var t=a.default.extractPropertyFromObject(this.additionalCssClasses,"text"),e=s.default.addText({id:"text-"+this.options.id,x:String(this.coordinates.x),y:String(this.coordinates.y),class:"circle-text "+t}),i=this.options.noPercentageSign?"":"%",n=""+this.options.percent+i;this.options.textReplacesPercentage&&this.options.text&&(n=this.options.text),e.textContent=n,this.tags.push({element:e,parentId:"svg-"+this.options.id})},e.prototype.drawInfoText=function(){var t=a.default.extractPropertyFromObject(this.additionalCssClasses,"infoText"),e=s.default.addText({id:"text-"+this.options.id,x:String(this.coordinates.x),y:String(this.coordinates.y+20),class:"circle-info-text "+t});e.textContent=this.options.text,this.tags.push({element:e,parentId:"svg-"+this.options.id})},e.prototype.drawLinearGradient=function(){var t={};t.gradientStart=this.options.strokeGradient[0],t.gradientEnd=this.options.strokeGradient[1];var e=s.default.addDefs(t);this.tags.push({element:e,parentId:"svg-"+this.options.id})},e}(o.BaseCircle);e.default=u},function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),i(8);var n=i(5);e.newCircle=function(t){return(new n.default).newCircle(t)},e.newCircleWithDataSet=function(t,e){return(new n.default).newCircleWithDataSet(t,e)}},function(t,e,i){},function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=i(5),r=i(1),o=function(){function t(t){this.options=t}return t.prototype.update=function(t){var e=this;document.getElementById("svg-"+this.options.id).remove(),Array.isArray(t)?t.forEach((function(t){return e.updateType(t.type,t.value)})):this.updateType(t.type,t.value),n.default.initializeCircleType(this.options)},t.prototype.updateType=function(t,e){switch(t){case"percent":this.options.percent=Number(e);break;case"point":this.options.point=Boolean(e);break;case"animation":this.options.animation=Boolean(e);break;case"pointSize":this.options.pointSize=Number(e);break;case"animationStep":this.options.animationStep=Number(e);break;case"strokeGradient":this.options.strokeGradient=e;break;case"icon":this.options.icon=String(e);break;case"text":this.options.text=String(e);break;case"textReplacesPercentage":this.options.textReplacesPercentage=Boolean(e);break;case"foregroundCircleWidth":this.options.foregroundCircleWidth=Number(e);break;case"backgroundCircleWidth":this.options.backgroundCircleWidth=Number(e);break;case"additionalCssClasses":this.options.additionalCssClasses=e;break;case"progressColors":this.options.progressColors=e}},t.prototype.get=function(t){return r.default.extractPropertyFromObject(this.options,t)},t}();e.Api=o},function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=i(11),r=i(12),o=i(13),s=i(6),a=function(){function t(){}return t.create=function(t){var e;switch(t.toLowerCase()){case"half":e=new r.default;break;case"plain":e=new o.default;break;case"simple":e=new s.default;break;case"fraction":e=new n.default;break;default:e=new s.default}return e},t}();e.CircleFactory=a},function(t,e,i){"use strict";var n,r=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])})(t,e)},function(t,e){function i(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(i.prototype=e.prototype,new i)});Object.defineProperty(e,"__esModule",{value:!0});var o=i(3),s=i(2),a=i(0),c=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.coordinates={x:0,y:0},e.additionalCssClasses={},e}return r(e,t),e.isOdd=function(t){return t%2},e.prototype.initialize=function(e,i){t.prototype.initialize.call(this,e,i);var n=this.size.maxSize;this.coordinates={x:n/2,y:n/2},this.radius=n/2.2,this.options.additionalCssClasses&&(this.additionalCssClasses=this.options.additionalCssClasses),this.animateInView()},e.prototype.drawCircle=function(){this.drawContainer(),this.drawFraction(),this.append()},e.prototype.drawFraction=function(){this.fractionAngle=360/this.options.fractionCount;for(var t=0;t=2){var n=this.options.fractionColors;i=e.isOdd(t)?n[0]:n[1]}t>=this.options.fractionFilledCount&&(i="none"),this.drawArc(i)}},e.prototype.drawArc=function(t){var e=s.default.addArc({id:"arc-"+this.options.id,class:"fraction",d:a.default.describeArc(this.coordinates.x,this.coordinates.y,this.radius,0,this.fractionAngle)+this.getLineToCenter(),"stroke-width":this.options.foregroundCircleWidth,fill:t,stroke:this.options.strokeColor,transform:"rotate("+this.rotateDegree+", "+this.coordinates.x+", "+this.coordinates.y+")"});this.tags.push({element:e,parentId:"svg-"+this.options.id})},e.prototype.getLineToCenter=function(){var t=a.default.calculatePathEndCoordinates(this.coordinates.x,this.coordinates.y,this.radius,this.fractionAngle);return" L "+this.coordinates.y+" "+this.coordinates.x+" M "+t.x+" "+t.y+" L "+this.coordinates.y+" "+this.coordinates.x},e.prototype.animate=function(t){},e}(o.BaseCircle);e.default=c},function(t,e,i){"use strict";var n,r=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])})(t,e)},function(t,e){function i(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(i.prototype=e.prototype,new i)});Object.defineProperty(e,"__esModule",{value:!0});var o=i(2),s=i(1),a=i(4),c=i(0),d=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return r(e,t),e.prototype.drawCircle=function(){var t={class:s.default.extractPropertyFromObject(this.additionalCssClasses,"svgContainer")};this.drawContainer(t),this.drawBackgroundCircle(),this.drawForegroundCircle(),this.drawText(),this.append()},e.prototype.drawBackgroundCircle=function(){var t=s.default.extractPropertyFromObject(this.additionalCssClasses,"backgroundCircle"),e=o.default.addArc({id:"bg-arc-"+this.options.id,d:c.default.describeArc(this.coordinates.x,this.coordinates.y,this.radius,270,90),class:"background-circle "+t,"stroke-width":this.options.backgroundCircleWidth});this.tags.push({element:e,parentId:"svg-"+this.options.id})},e.prototype.drawForegroundCircle=function(){var t=1.8*this.options.percent,e=s.default.extractPropertyFromObject(this.additionalCssClasses,"foregroundCircle"),i=o.default.addArc({id:"arc-"+this.options.id,class:"foreground-circle "+e,d:c.default.describeArc(this.coordinates.x,this.coordinates.y,this.radius,0,t),transform:"rotate(-90, "+this.coordinates.x+", "+this.coordinates.y+")","stroke-width":this.options.foregroundCircleWidth,"stroke-linecap":this.options.strokeLinecap});this.options.animation&&this.animate(i),this.tags.push({element:i,parentId:"svg-"+this.options.id})},e.prototype.animate=function(t){a.StyleHelper.animateArc({arc:t,arcParams:{percent:this.options.percent,x:this.coordinates.x,y:this.coordinates.y,radius:this.radius,endAngleGrade:180},animationStep:this.options.animationStep,progressColors:this.options.progressColors},this.options.onAnimationEnd)},e}(i(6).default);e.default=d},function(t,e,i){"use strict";var n,r=this&&this.__extends||(n=function(t,e){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])})(t,e)},function(t,e){function i(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(i.prototype=e.prototype,new i)});Object.defineProperty(e,"__esModule",{value:!0});var o=i(3),s=i(2),a=i(1),c=i(4),d=i(0),u=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.coordinates={x:0,y:0},e.additionalCssClasses={},e}return r(e,t),e.prototype.initialize=function(e,i){t.prototype.initialize.call(this,e,i);var n=this.size.maxSize;this.coordinates={x:n/2,y:n/2},this.radius=n/2.2,this.options.additionalCssClasses&&(this.additionalCssClasses=this.options.additionalCssClasses),this.animateInView()},e.prototype.drawCircle=function(){this.drawContainer(),this.drawPlainCircle(),this.append()},e.prototype.drawPlainCircle=function(){var t=this.options.startAngle?this.options.startAngle:0,e=3.6*this.options.percent+Number(t),i=a.default.extractPropertyFromObject(this.additionalCssClasses,"foregroundCircle"),n=s.default.addArc({id:"arc-"+this.options.id,class:"foreground-circle "+i,d:d.default.describeArc(this.coordinates.x,this.coordinates.y,this.radius,t,e),"stroke-width":this.options.foregroundCircleWidth,"stroke-linecap":this.options.strokeLinecap});this.options.animation&&!this.options.startAngle&&this.animate(n),this.tags.push({element:n,parentId:"svg-"+this.options.id})},e.prototype.animate=function(t){c.StyleHelper.animateArc({arc:t,arcParams:{percent:this.options.percent,x:this.coordinates.x,y:this.coordinates.y,radius:this.radius},animationStep:this.options.animationStep,progressColors:this.options.progressColors},this.options.onAnimationEnd)},e}(o.BaseCircle);e.default=u},function(t,e,i){"use strict";var n=this&&this.__assign||function(){return(n=Object.assign||function(t){for(var e,i=1,n=arguments.length;i 16 | 17 | 18 | // javascript call 19 | circliful.newCircleWithDataSet('circle', 'simple'); 20 | 21 | #### Set via config object #### 22 | 23 | // html tag 24 |
25 | 26 | // javascript call 27 | circliful.newCircle({ 28 | percent: 80, 29 | id: 'circle', 30 | type: 'simple', 31 | icon: 'f179', 32 | text: 'TP Wins', 33 | noPercentageSign: true, 34 | backgroundCircleWidth: 35, 35 | foregroundCircleWidth: 20, 36 | progressColors: [ 37 | {percent: 1, color: 'red'}, 38 | {percent: 30, color: 'orange'}, 39 | {percent: 60, color: 'green'} 40 | ] 41 | }); 42 | 43 | #### Available options #### 44 | 45 | | name | default | type | description 46 | | ------------- |------------- | ----- | ----- | 47 | | id | / | string | id of the html tag 48 | | type | "simple" | string | circle type 49 | | additionalCssClasses | / | object | on each element circle, text etc a custom css for styling can be set 50 | | point | false | boolean | a point in within the circle 51 | | pointSize | 60 | number | the point size in px 52 | | percent | 75 | number | the percentage of the circle 53 | | animation | true | boolean | if set to true, the circle percentage fill will be animated 54 | | animationStep | 1 | number | the animation speed 55 | | strokeGradient | / | [string, string] | will give the foreground circle a gradient 56 | | icon | / | string | font awesome icon definition for example 'f179', you need to integrate the font awesome library its not packed with circliful 57 | | text | / | string | will be shown below the percentage text 58 | | textReplacesPercentage | false | boolean | if set to true the text replaces the percentage 59 | | noPercentageSign | / | boolean | if set to true the % sign will be removed 60 | | animateInView | false | boolean | animates the circle as soon as its in the viewport 61 | | strokeLinecap | "butt" | string | the endings of the foreground circle, can be set to "butt" or "round" 62 | | foregroundCircleWidth | 5 | number | width of the foreground circle 63 | | backgroundCircleWidth | 15 | number | width of the background circle 64 | | progressColors | / | IProgressColor[] | the foreground circle changes the color if it comes above the given percentage colors for example [{percent: 50, color: "green"}] 65 | | onAnimationEnd | / | function | event that will be triggered when animation of circle finished 66 | 67 | -------------------------------------------------------------------------------- /docs/style-elements.md: -------------------------------------------------------------------------------- 1 | SVG style changes via CSS 2 | =================== 3 | 4 | Change Position of Element 5 | 6 | The first argument is the x (horizontal) coordinate and the second argument is the y (vertical) coordinate. 7 | 8 | transform: translate(0, 20px) 9 | 10 | Hover Effect 11 | 12 | .circle-container:hover { 13 | .background-circle { 14 | fill: #ccc; //change background color 15 | } 16 | 17 | .foreground-circle { 18 | stroke: blueviolet; //change stroke color 19 | stroke-width: 8; // change stroke width 20 | } 21 | } 22 | 23 | Change Circle Color 24 | 25 | .foreground-circle or .background-circle { 26 | stroke: blueviolet; 27 | } 28 | 29 | Change background color of Point 30 | 31 | .point-circle { 32 | fill: #999; 33 | } 34 | 35 | Change width of circle 36 | 37 | .foreground-circle or .background-circle { 38 | stroke-width: 50px; 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-plugin-circliful", 3 | "version": "2.0.15", 4 | "description": "circle statistic plugin without dependencies", 5 | "main": "dist/circliful.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/pguso/js-plugin-circliful" 9 | }, 10 | "scripts": { 11 | "start:dev": "webpack-dev-server --config webpack.dev.config.js --content-base public/", 12 | "build": "webpack --config webpack.prod.config.js", 13 | "lint": "tslint -c tslint.json -p tsconfig.json --fix", 14 | "test": "jest --collectCoverage", 15 | "test:watch": "jest --watch" 16 | }, 17 | "keywords": [ 18 | "circliful", 19 | "vanilla", 20 | "circle", 21 | "statistic", 22 | "stats", 23 | "presentation", 24 | "svg", 25 | "fraction" 26 | ], 27 | "author": { 28 | "name": "Patric Gutersohn", 29 | "email": "patric@gutersohn.com" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/pguso/js-plugin-circliful/issues" 33 | }, 34 | "_id": "js-plugin-circliful@2.0.12", 35 | "readmeFilename": "README.md", 36 | "license": "MIT", 37 | "devDependencies": { 38 | "@babel/core": "^7.9.0", 39 | "@babel/plugin-transform-modules-umd": "^7.9.0", 40 | "@babel/preset-env": "^7.9.0", 41 | "@babel/preset-typescript": "^7.9.0", 42 | "@purtuga/esm-webpack-plugin": "^1.2.1", 43 | "@types/jest": "^25.2.1", 44 | "@types/node": "^13.9.4", 45 | "babel-loader": "^8.1.0", 46 | "babel-plugin-syntax-async-functions": "^6.13.0", 47 | "babel-plugin-transform-class-properties": "^6.24.1", 48 | "clean-webpack-plugin": "^3.0.0", 49 | "css-loader": "^3.4.2", 50 | "es6-promise-promise": "^1.0.0", 51 | "html-webpack-plugin": "^4.0.2", 52 | "jest": "^25.2.7", 53 | "mini-css-extract-plugin": "^0.9.0", 54 | "node-sass": "^4.13.1", 55 | "optimize-css-assets-webpack-plugin": "^5.0.3", 56 | "sass-loader": "^8.0.2", 57 | "style-loader": "^1.1.3", 58 | "terser-webpack-plugin": "^2.3.5", 59 | "ts-loader": "^6.2.2", 60 | "tslint": "^5.20.1", 61 | "tslint-loader": "^3.5.4", 62 | "typescript": "^3.8.3", 63 | "webpack": "^4.42.0", 64 | "webpack-cli": "^3.3.11", 65 | "webpack-dev-server": "^3.10.3" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /public/custom.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, sans-serif; 3 | } 4 | 5 | .container { 6 | display: grid; 7 | grid-template-columns: 1fr 1fr 1fr; 8 | grid-template-rows: 1fr 1fr 1fr; 9 | } 10 | 11 | .circle-icon.fa { 12 | font-size: .8rem; 13 | fill: #3498DB; 14 | transform: translate(0, 5%); /* change position of circle */ 15 | } 16 | 17 | .circle-text { 18 | font-size: .9rem; 19 | } 20 | 21 | .circle-info-text { 22 | transform: translate(0, -6%); /* position text outside of circle */ 23 | font-size: .5rem; 24 | } 25 | 26 | #svg-circle2 { 27 | transform: translate(16%, 15%); 28 | } 29 | 30 | .circle-container { 31 | transform: translate(9%, 8%); 32 | } 33 | -------------------------------------------------------------------------------- /public/fraction.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Circliful - examples 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Circliful - examples 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
22 |
23 |
24 | 25 |
26 |
27 |
28 | 29 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import Circle from "./base-class/circle"; 2 | import ObjectHelper from "./helper/object-helper"; 3 | import {IAvailableOptions} from "./interface/iavailable-options"; 4 | import {IType} from "./interface/itype"; 5 | 6 | export class Api { 7 | public readonly options: IAvailableOptions; 8 | 9 | constructor(options: IAvailableOptions) { 10 | this.options = options; 11 | } 12 | 13 | /** 14 | * @description Update options and rerender circle 15 | * @param parameter 16 | */ 17 | public update(parameter: IType | IType[]) { 18 | const element = document.getElementById(`svg-${this.options.id}`); 19 | element.remove(); 20 | 21 | if (Array.isArray(parameter)) { 22 | parameter.forEach((p) => this.updateType(p.type, p.value)); 23 | } else { 24 | this.updateType(parameter.type, parameter.value); 25 | } 26 | 27 | Circle.initializeCircleType(this.options); 28 | } 29 | 30 | /** 31 | * @description Update options by given type 32 | * @param type 33 | * @param value 34 | */ 35 | private updateType(type: string, value: IType["value"]): void { 36 | switch (type) { 37 | case "percent": 38 | this.options.percent = Number(value); 39 | break; 40 | case "point": 41 | this.options.point = Boolean(value); 42 | break; 43 | case "animation": 44 | this.options.animation = Boolean(value); 45 | break; 46 | case "pointSize": 47 | this.options.pointSize = Number(value); 48 | break; 49 | case "animationStep": 50 | this.options.animationStep = Number(value); 51 | break; 52 | case "strokeGradient": 53 | // tslint:disable-next-line 54 | this.options.strokeGradient = value as any; 55 | break; 56 | case "icon": 57 | this.options.icon = String(value); 58 | break; 59 | case "text": 60 | this.options.text = String(value); 61 | break; 62 | case "textReplacesPercentage": 63 | this.options.textReplacesPercentage = Boolean(value); 64 | break; 65 | case "foregroundCircleWidth": 66 | this.options.foregroundCircleWidth = Number(value); 67 | break; 68 | case "backgroundCircleWidth": 69 | this.options.backgroundCircleWidth = Number(value); 70 | break; 71 | case "additionalCssClasses": 72 | // tslint:disable-next-line 73 | this.options.additionalCssClasses = value as any; 74 | break; 75 | case "progressColors": 76 | // tslint:disable-next-line 77 | this.options.progressColors = value as any; 78 | break; 79 | } 80 | } 81 | 82 | /** 83 | * @description Get property from object 84 | * @param type 85 | */ 86 | public get(type: string) { 87 | return ObjectHelper.extractPropertyFromObject(this.options, type); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/base-class/base-circle.ts: -------------------------------------------------------------------------------- 1 | import {IAvailableOptions} from "../interface/iavailable-options"; 2 | import {ISize} from "../interface/isize"; 3 | import {ITag} from "../interface/itag"; 4 | import {IViewBoxAttributes} from "../interface/iview-box-attributes"; 5 | import SvgTags from "./svg-tags"; 6 | 7 | /** 8 | * Base for circle type implementations 9 | */ 10 | export abstract class BaseCircle { 11 | /** 12 | * @description Options object second argument for initCircle method 13 | */ 14 | public options: IAvailableOptions; 15 | /** 16 | * @description Size of surrounding tag for svg tag 17 | */ 18 | public size: ISize; 19 | /** 20 | * @description Array of all tags that needs to be appended to the dom 21 | */ 22 | public tags: ITag[] = []; 23 | 24 | /** 25 | * @description Fires scroll event if animateInView is set to true and runs checkAnimation 26 | */ 27 | protected animateInView(): void { 28 | if (this.options.animateInView) { 29 | window.addEventListener("scroll", () => { 30 | this.checkAnimation(this.options.id); 31 | }); 32 | } 33 | } 34 | 35 | /** 36 | * @description When circle is in view port it animates the foreground circle 37 | */ 38 | protected checkAnimation(svgParentId: string) { 39 | const circleContainer = document.getElementById(svgParentId); 40 | const foregroundCircle = document.getElementById(`arc-${svgParentId}`); 41 | const inView = this.isElementInViewport(circleContainer); 42 | 43 | if (!circleContainer.classList.contains("reanimated") && inView) { 44 | circleContainer.classList.add("reanimated"); 45 | setTimeout(() => this.animate(foregroundCircle as Element), 250); 46 | } 47 | } 48 | 49 | /** 50 | * @description Calculates if the circle is in viewport 51 | * @param circleContainer 52 | */ 53 | protected isElementInViewport(circleContainer: HTMLElement) { 54 | const offsetTop = circleContainer.offsetTop; 55 | const scrollPositionTop = window.scrollY; 56 | const windowHeight = window.innerHeight; 57 | 58 | return scrollPositionTop < offsetTop && scrollPositionTop + windowHeight > offsetTop; 59 | } 60 | 61 | /** 62 | * @description Gets called in the circle class to draw the circle with all its child elements, the methods 63 | * drawContainer and append must be always called in own implementations if you dont implement own logic for them 64 | */ 65 | public abstract drawCircle(): void; 66 | 67 | /** 68 | * @description 69 | * @param element 70 | */ 71 | protected abstract animate(element: Element): void; 72 | 73 | /** 74 | * @description Draws the svg tag 75 | * @param additionalAttributes 76 | */ 77 | public drawContainer(additionalAttributes?: object) { 78 | const {minX, minY, width, height} = this.getViewBoxParams(); 79 | 80 | const container = SvgTags.addSvg({ 81 | width: "100%", 82 | height: "100%", 83 | viewBox: `${minX} ${minY} ${width} ${height}`, 84 | id: `svg-${this.options.id}`, 85 | preserveAspectRatio: "xMinYMin meet", 86 | ...additionalAttributes, 87 | }); 88 | 89 | this.tags.push({ 90 | element: container, 91 | parentId: this.options.id, 92 | }); 93 | } 94 | 95 | /** 96 | * @description Get viewBox parameters, resize the view if the border would overflow 97 | */ 98 | private getViewBoxParams(): IViewBoxAttributes { 99 | const {foregroundCircleWidth, backgroundCircleWidth} = this.options; 100 | let circleWidth = backgroundCircleWidth; 101 | // Get thicker circle stroke width, foreground or background 102 | if (foregroundCircleWidth > backgroundCircleWidth) { 103 | circleWidth = foregroundCircleWidth; 104 | } 105 | 106 | const minX = 0; 107 | const minY = 0; 108 | let width = this.size.width; 109 | let height = this.size.height; 110 | if (foregroundCircleWidth > 5 || backgroundCircleWidth > 5) { 111 | width = this.size.width; 112 | height = this.size.height; 113 | } 114 | 115 | return {minX, minY, width, height}; 116 | } 117 | 118 | /** 119 | * @description Appends the tags to the dom 120 | */ 121 | public append() { 122 | this.tags.forEach((tag) => { 123 | const parent = document.getElementById(tag.parentId); 124 | parent.appendChild(tag.element as Node); 125 | }); 126 | } 127 | 128 | /** 129 | * @description Initialize basic values for circle 130 | * @param options 131 | * @param size 132 | */ 133 | public initialize(options: IAvailableOptions, size: ISize): void { 134 | this.options = options; 135 | this.size = size; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/base-class/circle-factory.ts: -------------------------------------------------------------------------------- 1 | import FractionCircle from "../circle-type/fraction-circle"; 2 | import HalfCircle from "../circle-type/half-circle"; 3 | import PlainCircle from "../circle-type/plain-circle"; 4 | import SimpleCircle from "../circle-type/simple-circle"; 5 | import {BaseCircle} from "./base-circle"; 6 | 7 | export class CircleFactory { 8 | public static create(type: string): BaseCircle { 9 | let circleClass: BaseCircle; 10 | switch (type.toLowerCase()) { 11 | case "half": 12 | circleClass = new HalfCircle(); 13 | break; 14 | case "plain": 15 | circleClass = new PlainCircle(); 16 | break; 17 | case "simple": 18 | circleClass = new SimpleCircle(); 19 | break; 20 | case "fraction": 21 | circleClass = new FractionCircle(); 22 | break; 23 | default: 24 | circleClass = new SimpleCircle(); 25 | } 26 | 27 | return circleClass; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/base-class/circle.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "../api"; 2 | import {IAvailableOptions} from "../interface/iavailable-options"; 3 | import {CircleFactory} from "./circle-factory"; 4 | import Options from "./options"; 5 | 6 | class Circle { 7 | /** 8 | * @description Gets the size of the parent element, where the svg gets placed in 9 | * @param id 10 | * @returns {{width: number, maxSize: number, height: number}} 11 | */ 12 | private static getParentSize(id: string) { 13 | const width = 100; 14 | const height = 100; 15 | 16 | return { 17 | maxSize: width > height ? height : width, 18 | height, 19 | width, 20 | }; 21 | } 22 | 23 | /** 24 | * @description Initializes the circle by given type 25 | * @param options 26 | * @param checkDataAttributes 27 | */ 28 | public static initializeCircleType(options: IAvailableOptions, checkDataAttributes = false) { 29 | const size = Circle.getParentSize(options.id); 30 | const circle = CircleFactory.create(options.type); 31 | const optionsManager = new Options(); 32 | const mergedOptions = optionsManager.mergeOptions(options, checkDataAttributes); 33 | 34 | circle.initialize(mergedOptions, size); 35 | circle.drawCircle(); 36 | 37 | return circle; 38 | } 39 | 40 | /** 41 | * @description Creates a new circle 42 | * @param options 43 | */ 44 | public newCircle(options: IAvailableOptions): Api { 45 | Circle.initializeCircleType(options); 46 | 47 | return new Api(options); 48 | } 49 | 50 | /** 51 | * @description Creates a new circle with attributes set as data attributes on tag 52 | * @param parentId 53 | * @param type 54 | */ 55 | public newCircleWithDataSet(parentId: string, type: string): Api { 56 | const options: IAvailableOptions = { 57 | id: parentId, 58 | type, 59 | percent: 1, 60 | }; 61 | 62 | Circle.initializeCircleType(options, true); 63 | 64 | return new Api(options); 65 | } 66 | } 67 | 68 | export default Circle; 69 | -------------------------------------------------------------------------------- /src/base-class/options.ts: -------------------------------------------------------------------------------- 1 | import {IAvailableOptions} from "../interface/iavailable-options"; 2 | 3 | class Options { 4 | /** 5 | * @description Default options if option is not set on initialisation 6 | */ 7 | public defaultOptions: IAvailableOptions = { 8 | point: false, 9 | pointSize: 60, 10 | percent: 75, 11 | foregroundCircleWidth: 5, 12 | backgroundCircleWidth: 15, 13 | animation: true, 14 | animationStep: 1, 15 | noPercentageSign: false, 16 | animateInView: false, 17 | strokeLinecap: "butt", 18 | type: "SimpleCircle", 19 | textReplacesPercentage: false, 20 | }; 21 | 22 | /** 23 | * @description Get data attributes from tag 24 | * @param options 25 | */ 26 | private static getDataAttributes(options: IAvailableOptions): IAvailableOptions { 27 | const circleContainer = document.getElementById(options.id); 28 | const dataOptions: IAvailableOptions = {percent: options.percent}; 29 | 30 | for (const key in circleContainer.dataset) { 31 | if (circleContainer.dataset.hasOwnProperty(key)) { 32 | const value = circleContainer.dataset[key]; 33 | 34 | if (value === "false" || value === "true") { 35 | dataOptions[key] = value === "true"; 36 | } else if (Number(value)) { 37 | dataOptions[key] = Number(value); 38 | } else { 39 | dataOptions[key] = value; 40 | } 41 | } 42 | } 43 | 44 | return dataOptions; 45 | } 46 | 47 | /** 48 | * @description Merge default options and custom option on initialisation 49 | * @param options 50 | * @param checkDataAttributes 51 | * @returns Options['defaultOptions'] 52 | */ 53 | public mergeOptions(options: IAvailableOptions, checkDataAttributes = false) { 54 | let mergedOptions = {...this.defaultOptions, ...options}; 55 | if (checkDataAttributes) { 56 | const dataOptions = Options.getDataAttributes(options); 57 | mergedOptions = {...mergedOptions, ...dataOptions}; 58 | } 59 | 60 | return mergedOptions; 61 | } 62 | } 63 | 64 | export default Options; 65 | -------------------------------------------------------------------------------- /src/base-class/svg-tags.ts: -------------------------------------------------------------------------------- 1 | import ObjectHelper from "../helper/object-helper"; 2 | import SvgTagsHelper from "../helper/svg-tags-helper"; 3 | import {IAttributes} from "../interface/iattributes"; 4 | import {IDictionary} from "../interface/idictionary"; 5 | 6 | class SvgTags { 7 | public static namespaceURI = "http://www.w3.org/2000/svg"; 8 | 9 | /** 10 | * @description Adds a svg tag with attributes 11 | * @param attributes 12 | * @returns SVGElement 13 | */ 14 | public static addSvg(attributes: IAttributes): Element { 15 | const svg = document.createElementNS(SvgTags.namespaceURI, "svg"); 16 | attributes.class = "circle-container " + ObjectHelper.extractPropertyFromObject( 17 | attributes as IDictionary, 18 | "class", 19 | ); 20 | 21 | SvgTagsHelper.setAttributes(svg as SVGElement, attributes); 22 | 23 | return svg; 24 | } 25 | 26 | /** 27 | * @description Adds a circle tag with attributes 28 | * @param attributes 29 | * @returns SVGCircleElement 30 | */ 31 | public static addCircle(attributes: IAttributes): Element { 32 | const circle = document.createElementNS(SvgTags.namespaceURI, "circle"); 33 | SvgTagsHelper.setAttributes(circle as SVGElement, attributes); 34 | 35 | return circle; 36 | } 37 | 38 | /** 39 | * @description Adds a path tag with attributes 40 | * @param attributes 41 | * @returns SVGPathElement 42 | */ 43 | public static addArc(attributes: IAttributes): Element { 44 | const arc = document.createElementNS(SvgTags.namespaceURI, "path"); 45 | SvgTagsHelper.setAttributes(arc as SVGElement, attributes); 46 | 47 | return arc; 48 | } 49 | 50 | /** 51 | * @description Adds a text tag with attributes 52 | * @param attributes 53 | * @returns SVGTextElement 54 | */ 55 | public static addText(attributes: IAttributes): Element { 56 | const text = document.createElementNS(SvgTags.namespaceURI, "text"); 57 | text.setAttributeNS(null, "text-anchor", "middle"); 58 | SvgTagsHelper.setAttributes(text as SVGElement, attributes); 59 | 60 | return text; 61 | } 62 | 63 | /** 64 | * @description Adds defs tag to svg to draw a gradient 65 | * @param attributes 66 | */ 67 | public static addDefs(attributes: IAttributes): Element { 68 | const defs = document.createElementNS(SvgTags.namespaceURI, "defs"); 69 | const linearGradient = document.createElementNS(SvgTags.namespaceURI, "linearGradient"); 70 | const linearGradientAttributes = { 71 | id: "linearGradient", 72 | }; 73 | SvgTagsHelper.setAttributes(linearGradient as SVGElement, linearGradientAttributes); 74 | 75 | const firstStop = document.createElementNS(SvgTags.namespaceURI, "stop"); 76 | const firstStopAttributes = { 77 | "offset": "0", 78 | "stop-color": attributes.gradientStart, 79 | }; 80 | SvgTagsHelper.setAttributes(firstStop as SVGElement, firstStopAttributes); 81 | 82 | const secondStop = document.createElementNS(SvgTags.namespaceURI, "stop"); 83 | const secondStopAttributes = { 84 | "offset": "1", 85 | "stop-color": attributes.gradientEnd, 86 | }; 87 | SvgTagsHelper.setAttributes(secondStop as SVGElement, secondStopAttributes); 88 | 89 | linearGradient.appendChild(firstStop); 90 | linearGradient.appendChild(secondStop); 91 | defs.appendChild(linearGradient); 92 | 93 | return defs; 94 | } 95 | } 96 | 97 | export default SvgTags; 98 | -------------------------------------------------------------------------------- /src/circle-type/fraction-circle.ts: -------------------------------------------------------------------------------- 1 | import {BaseCircle} from "../base-class/base-circle"; 2 | import SvgTags from "../base-class/svg-tags"; 3 | import SvgTagsHelper from "../helper/svg-tags-helper"; 4 | import {IAvailableOptions} from "../interface/iavailable-options"; 5 | import {ISize} from "../interface/isize"; 6 | 7 | /** 8 | * Every circle gets dynamically called by the given type in the options object example: { type: 'PlainCircle' } 9 | */ 10 | class FractionCircle extends BaseCircle { 11 | private coordinates = { 12 | x: 0, 13 | y: 0, 14 | }; 15 | private fractionAngle: number; 16 | private rotateDegree: number; 17 | protected radius: number; 18 | protected additionalCssClasses: IAvailableOptions["additionalCssClasses"] = {}; 19 | 20 | private static isOdd(count: number) { 21 | return count % 2; 22 | } 23 | 24 | /** 25 | * @inheritDoc 26 | */ 27 | public initialize(options: IAvailableOptions, size: ISize) { 28 | super.initialize(options, size); 29 | 30 | const maxSize = this.size.maxSize; 31 | this.coordinates = { 32 | x: maxSize / 2, 33 | y: maxSize / 2, 34 | }; 35 | this.radius = maxSize / 2.2; 36 | 37 | if (this.options.additionalCssClasses) { 38 | this.additionalCssClasses = this.options.additionalCssClasses; 39 | } 40 | 41 | this.animateInView(); 42 | } 43 | 44 | /** 45 | * @inheritDoc 46 | */ 47 | public drawCircle() { 48 | this.drawContainer(); 49 | this.drawFraction(); 50 | this.append(); 51 | } 52 | 53 | /** 54 | * @description Draws the arc parts by given fraction count 55 | */ 56 | public drawFraction() { 57 | this.fractionAngle = 360 / this.options.fractionCount; 58 | 59 | for (let i = 0; i < this.options.fractionCount; i++) { 60 | this.rotateDegree = this.fractionAngle * i; 61 | 62 | let fillColor = this.options.fillColor; 63 | if (this.options.fractionColors && this.options.fractionColors.length >= 2) { 64 | const color = this.options.fractionColors; 65 | fillColor = FractionCircle.isOdd(i) ? color[0] : color[1]; 66 | } 67 | 68 | if (i >= this.options.fractionFilledCount) { 69 | fillColor = "none"; 70 | } 71 | 72 | this.drawArc(fillColor); 73 | } 74 | 75 | } 76 | 77 | private drawArc(fillColor: string): void { 78 | const arc = SvgTags.addArc({ 79 | "id": `arc-${this.options.id}`, 80 | "class": `fraction`, 81 | "d": SvgTagsHelper.describeArc( 82 | this.coordinates.x, 83 | this.coordinates.y, 84 | this.radius, 85 | 0, 86 | this.fractionAngle, 87 | ) + this.getLineToCenter(), 88 | "stroke-width": this.options.foregroundCircleWidth, 89 | "fill": fillColor, 90 | "stroke": this.options.strokeColor, 91 | "transform": `rotate(${this.rotateDegree}, ${this.coordinates.x}, ${this.coordinates.y})`, 92 | }); 93 | 94 | this.tags.push({ 95 | element: arc, 96 | parentId: `svg-${this.options.id}`, 97 | }); 98 | } 99 | 100 | public getLineToCenter(): string { 101 | const pathEndCoordinates = SvgTagsHelper.calculatePathEndCoordinates( 102 | this.coordinates.x, 103 | this.coordinates.y, 104 | this.radius, 105 | this.fractionAngle, 106 | ); 107 | 108 | return ` L ${this.coordinates.y} ${this.coordinates.x} M ${pathEndCoordinates.x} ${pathEndCoordinates.y} L ${this.coordinates.y} ${this.coordinates.x}`; 109 | } 110 | 111 | public animate(element: Element): void { 112 | } 113 | } 114 | 115 | export default FractionCircle; 116 | -------------------------------------------------------------------------------- /src/circle-type/half-circle.ts: -------------------------------------------------------------------------------- 1 | import SvgTags from "../base-class/svg-tags"; 2 | import ObjectHelper from "../helper/object-helper"; 3 | import {StyleHelper} from "../helper/style-helper"; 4 | import SvgTagsHelper from "../helper/svg-tags-helper"; 5 | import SimpleCircle from "./simple-circle"; 6 | 7 | /** 8 | * Every circle gets dynamically called by the given type in the options object example: { type: 'HalfCircle' } 9 | */ 10 | class HalfCircle extends SimpleCircle { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public drawCircle() { 15 | const additionalContainerAttributes = { 16 | class: ObjectHelper.extractPropertyFromObject(this.additionalCssClasses, "svgContainer"), 17 | }; 18 | this.drawContainer(additionalContainerAttributes); 19 | this.drawBackgroundCircle(); 20 | this.drawForegroundCircle(); 21 | this.drawText(); 22 | this.append(); 23 | } 24 | 25 | /** 26 | * @description Draws the background circle 27 | */ 28 | public drawBackgroundCircle() { 29 | const startAngle = 270; 30 | const endAngle = 90; 31 | const customCssClass = ObjectHelper.extractPropertyFromObject( 32 | this.additionalCssClasses, 33 | "backgroundCircle", 34 | ); 35 | const arc = SvgTags.addArc({ 36 | "id": `bg-arc-${this.options.id}`, 37 | "d": SvgTagsHelper.describeArc(this.coordinates.x, this.coordinates.y, this.radius, startAngle, endAngle), 38 | "class": `background-circle ${customCssClass}`, 39 | "stroke-width": this.options.backgroundCircleWidth, 40 | }); 41 | 42 | this.tags.push({ 43 | element: arc, 44 | parentId: `svg-${this.options.id}`, 45 | }); 46 | } 47 | 48 | /** 49 | * @description Draws the foreground circle by given percentage with optional animation 50 | */ 51 | public drawForegroundCircle() { 52 | const endAngle = 180 / 100 * this.options.percent; 53 | const customCssClass = ObjectHelper.extractPropertyFromObject( 54 | this.additionalCssClasses, 55 | "foregroundCircle", 56 | ); 57 | const arc = SvgTags.addArc({ 58 | "id": `arc-${this.options.id}`, 59 | "class": `foreground-circle ${customCssClass}`, 60 | "d": SvgTagsHelper.describeArc(this.coordinates.x, this.coordinates.y, this.radius, 0, endAngle), 61 | "transform": `rotate(-90, ${this.coordinates.x}, ${this.coordinates.y})`, 62 | "stroke-width": this.options.foregroundCircleWidth, 63 | "stroke-linecap": this.options.strokeLinecap, 64 | }); 65 | 66 | if (this.options.animation) { 67 | this.animate(arc); 68 | } 69 | 70 | this.tags.push({ 71 | element: arc, 72 | parentId: `svg-${this.options.id}`, 73 | }); 74 | } 75 | 76 | /** 77 | * @description Animates circle counter clock wise 78 | * @param arc 79 | */ 80 | public animate(arc: Element) { 81 | StyleHelper.animateArc({ 82 | arc, 83 | arcParams: { 84 | percent: this.options.percent, 85 | x: this.coordinates.x, 86 | y: this.coordinates.y, 87 | radius: this.radius, 88 | endAngleGrade: 180, 89 | }, 90 | animationStep: this.options.animationStep, 91 | progressColors: this.options.progressColors, 92 | }, this.options.onAnimationEnd); 93 | } 94 | } 95 | 96 | export default HalfCircle; 97 | -------------------------------------------------------------------------------- /src/circle-type/plain-circle.ts: -------------------------------------------------------------------------------- 1 | import {BaseCircle} from "../base-class/base-circle"; 2 | import SvgTags from "../base-class/svg-tags"; 3 | import ObjectHelper from "../helper/object-helper"; 4 | import {StyleHelper} from "../helper/style-helper"; 5 | import SvgTagsHelper from "../helper/svg-tags-helper"; 6 | import {IAvailableOptions} from "../interface/iavailable-options"; 7 | import {ISize} from "../interface/isize"; 8 | 9 | /** 10 | * Every circle gets dynamically called by the given type in the options object example: { type: 'PlainCircle' } 11 | */ 12 | class PlainCircle extends BaseCircle { 13 | private coordinates = { 14 | x: 0, 15 | y: 0, 16 | }; 17 | protected radius: number; 18 | protected additionalCssClasses: IAvailableOptions["additionalCssClasses"] = {}; 19 | 20 | /** 21 | * @inheritDoc 22 | */ 23 | public initialize(options: IAvailableOptions, size: ISize) { 24 | super.initialize(options, size); 25 | 26 | const maxSize = this.size.maxSize; 27 | this.coordinates = { 28 | x: maxSize / 2, 29 | y: maxSize / 2, 30 | }; 31 | this.radius = maxSize / 2.2; 32 | 33 | if (this.options.additionalCssClasses) { 34 | this.additionalCssClasses = this.options.additionalCssClasses; 35 | } 36 | 37 | this.animateInView(); 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | public drawCircle() { 44 | this.drawContainer(); 45 | this.drawPlainCircle(); 46 | this.append(); 47 | } 48 | 49 | /** 50 | * @description Draws the circle by given percentage with optional animation 51 | */ 52 | public drawPlainCircle() { 53 | const startAngle = this.options.startAngle ? this.options.startAngle : 0; 54 | const endAngle = 360 / 100 * this.options.percent + Number(startAngle); 55 | const customCssClass = ObjectHelper.extractPropertyFromObject( 56 | this.additionalCssClasses, 57 | "foregroundCircle", 58 | ); 59 | const arc = SvgTags.addArc({ 60 | "id": `arc-${this.options.id}`, 61 | "class": `foreground-circle ${customCssClass}`, 62 | "d": SvgTagsHelper.describeArc(this.coordinates.x, this.coordinates.y, this.radius, startAngle, endAngle), 63 | "stroke-width": this.options.foregroundCircleWidth, 64 | "stroke-linecap": this.options.strokeLinecap, 65 | }); 66 | 67 | if (this.options.animation && !this.options.startAngle) { 68 | this.animate(arc); 69 | } 70 | 71 | this.tags.push({ 72 | element: arc, 73 | parentId: `svg-${this.options.id}`, 74 | }); 75 | } 76 | 77 | /** 78 | * @description Animates circle counter clock wise 79 | * @param arc 80 | */ 81 | public animate(arc: Element) { 82 | StyleHelper.animateArc({ 83 | arc, 84 | arcParams: { 85 | percent: this.options.percent, 86 | x: this.coordinates.x, 87 | y: this.coordinates.y, 88 | radius: this.radius, 89 | }, 90 | animationStep: this.options.animationStep, 91 | progressColors: this.options.progressColors, 92 | }, this.options.onAnimationEnd); 93 | } 94 | } 95 | 96 | export default PlainCircle; 97 | -------------------------------------------------------------------------------- /src/circle-type/simple-circle.ts: -------------------------------------------------------------------------------- 1 | import {BaseCircle} from "../base-class/base-circle"; 2 | import SvgTags from "../base-class/svg-tags"; 3 | import ObjectHelper from "../helper/object-helper"; 4 | import {StyleHelper} from "../helper/style-helper"; 5 | import SvgTagsHelper from "../helper/svg-tags-helper"; 6 | import {IAttributes} from "../interface/iattributes"; 7 | import {IAvailableOptions} from "../interface/iavailable-options"; 8 | import {ISize} from "../interface/isize"; 9 | 10 | /** 11 | * Every circle gets dynamically called by the given type in the options object 12 | * example: { type: 'SimpleCircle' } 13 | */ 14 | class SimpleCircle extends BaseCircle { 15 | protected coordinates = { 16 | x: 0, 17 | y: 0, 18 | }; 19 | protected radius: number; 20 | protected additionalCssClasses: IAvailableOptions["additionalCssClasses"] = {}; 21 | 22 | /** 23 | * @inheritDoc 24 | */ 25 | public initialize(options: IAvailableOptions, size: ISize) { 26 | super.initialize(options, size); 27 | 28 | const maxSize = this.size.maxSize; 29 | this.coordinates = { 30 | x: maxSize / 2, 31 | y: maxSize / 2, 32 | }; 33 | this.radius = maxSize / 2.2; 34 | 35 | if (this.options.additionalCssClasses) { 36 | this.additionalCssClasses = this.options.additionalCssClasses; 37 | } 38 | 39 | this.animateInView(); 40 | } 41 | 42 | /** 43 | * @inheritDoc 44 | */ 45 | public drawCircle() { 46 | const additionalContainerAttributes = { 47 | class: ObjectHelper.extractPropertyFromObject(this.additionalCssClasses, "svgContainer"), 48 | }; 49 | this.drawContainer(additionalContainerAttributes); 50 | 51 | if (this.options.strokeGradient) { 52 | this.drawLinearGradient(); 53 | } 54 | 55 | this.drawBackgroundCircle(); 56 | this.drawForegroundCircle(); 57 | 58 | if (this.options.point) { 59 | this.drawPoint(); 60 | } 61 | 62 | if (this.options.icon) { 63 | this.drawIcon(); 64 | } 65 | 66 | this.drawText(); 67 | 68 | if (!this.options.textReplacesPercentage && this.options.text) { 69 | this.drawInfoText(); 70 | } 71 | 72 | this.append(); 73 | } 74 | 75 | /** 76 | * @description Draws background circle 77 | */ 78 | public drawBackgroundCircle() { 79 | const customCssClass = ObjectHelper.extractPropertyFromObject( 80 | this.additionalCssClasses, 81 | "backgroundCircle", 82 | ); 83 | 84 | const circle = SvgTags.addCircle({ 85 | "id": `circle-${this.options.id}`, 86 | "class": `background-circle ${customCssClass}`, 87 | "cx": String(this.coordinates.x), 88 | "cy": String(this.coordinates.y), 89 | "r": String(this.radius), 90 | "stroke-width": this.options.backgroundCircleWidth, 91 | }); 92 | 93 | this.tags.push({ 94 | element: circle, 95 | parentId: `svg-${this.options.id}`, 96 | }); 97 | } 98 | 99 | /** 100 | * @description Draws a point into the circle, behind the text 101 | */ 102 | public drawPoint() { 103 | const pointSize = this.radius / 100 * this.options.pointSize; 104 | const customCssClass = ObjectHelper.extractPropertyFromObject( 105 | this.additionalCssClasses, 106 | "point", 107 | ); 108 | const circle = SvgTags.addCircle({ 109 | id: `point-${this.options.id}`, 110 | class: `point-circle ${customCssClass}`, 111 | cx: String(this.coordinates.x), 112 | cy: String(this.coordinates.y), 113 | r: String(pointSize), 114 | }); 115 | 116 | this.tags.push({ 117 | element: circle, 118 | parentId: `svg-${this.options.id}`, 119 | }); 120 | } 121 | 122 | /** 123 | * @description Draws foreground circle 124 | */ 125 | public drawForegroundCircle() { 126 | const endAngle = (360 / 100 * this.options.percent) + Number(this.options.startAngle); 127 | const startAngle = this.options.startAngle ? this.options.startAngle : 0; 128 | const customCssClass = ObjectHelper.extractPropertyFromObject( 129 | this.additionalCssClasses, 130 | "foregroundCircle", 131 | ); 132 | const attributes: IAttributes = { 133 | "id": `arc-${this.options.id}`, 134 | "class": `foreground-circle ${customCssClass}`, 135 | "d": SvgTagsHelper.describeArc(this.coordinates.x, this.coordinates.y, this.radius, startAngle, endAngle), 136 | "stroke-width": this.options.foregroundCircleWidth, 137 | "stroke-linecap": this.options.strokeLinecap, 138 | }; 139 | 140 | if (this.options.strokeGradient) { 141 | attributes.stroke = "url(#linearGradient)"; 142 | attributes.class = `foreground-circle-without-stroke-color ${customCssClass}`; 143 | } 144 | 145 | const arc = SvgTags.addArc(attributes); 146 | if (this.options.animation && !this.options.startAngle) { 147 | this.animate(arc); 148 | } else { 149 | this.drawArc(arc); 150 | } 151 | 152 | this.tags.push({ 153 | element: arc, 154 | parentId: `svg-${this.options.id}`, 155 | }); 156 | } 157 | 158 | private drawArc(arc: Element) { 159 | const arcParams = { 160 | percent: this.options.percent, 161 | x: this.coordinates.x, 162 | y: this.coordinates.y, 163 | radius: this.radius, 164 | }; 165 | const endAngle = 360 / 100 * this.options.percent; 166 | SvgTagsHelper.setAttributes(arc, { 167 | d: SvgTagsHelper.describeArc( 168 | arcParams.x, 169 | arcParams.y, 170 | arcParams.radius, 171 | 0, 172 | endAngle, 173 | "0", 174 | ), 175 | }); 176 | } 177 | 178 | /** 179 | * @description Animates circle counter clock wise 180 | * @param arc 181 | * @param updatedPercent 182 | */ 183 | public animate(arc: Element, updatedPercent?: number) { 184 | StyleHelper.animateArc({ 185 | arc, 186 | arcParams: { 187 | percent: updatedPercent ? updatedPercent : this.options.percent, 188 | x: this.coordinates.x, 189 | y: this.coordinates.y, 190 | radius: this.radius, 191 | }, 192 | animationStep: this.options.animationStep, 193 | progressColors: this.options.progressColors, 194 | }, this.options.onAnimationEnd); 195 | } 196 | 197 | /** 198 | * @description Draws font awesome icon 199 | */ 200 | public drawIcon() { 201 | const icon = this.options.icon; 202 | const customCssClass = ObjectHelper.extractPropertyFromObject( 203 | this.additionalCssClasses, 204 | "icon", 205 | ); 206 | const text = SvgTags.addText({ 207 | id: `text-${this.options.id}`, 208 | x: String(this.coordinates.x), 209 | y: String(this.coordinates.y - 25), 210 | class: `circle-icon fa ${customCssClass}`, 211 | }); 212 | 213 | text.innerHTML = `&#x${icon};`; 214 | 215 | this.tags.push({ 216 | element: text, 217 | parentId: `svg-${this.options.id}`, 218 | }); 219 | } 220 | 221 | /** 222 | * @description Draws percentage 223 | */ 224 | public drawText() { 225 | const customCssClass = ObjectHelper.extractPropertyFromObject( 226 | this.additionalCssClasses, 227 | "text", 228 | ); 229 | const text = SvgTags.addText({ 230 | id: `text-${this.options.id}`, 231 | x: String(this.coordinates.x), 232 | y: String(this.coordinates.y), 233 | class: `circle-text ${customCssClass}`, 234 | }); 235 | 236 | const percentageSign = this.options.noPercentageSign ? "" : "%"; 237 | let content = `${this.options.percent}${percentageSign}`; 238 | if (this.options.textReplacesPercentage && this.options.text) { 239 | content = this.options.text; 240 | } 241 | text.textContent = content; 242 | 243 | this.tags.push({ 244 | element: text, 245 | parentId: `svg-${this.options.id}`, 246 | }); 247 | } 248 | 249 | /** 250 | * @description Draws info text below percentage 251 | */ 252 | public drawInfoText() { 253 | const customCssClass = ObjectHelper.extractPropertyFromObject( 254 | this.additionalCssClasses, 255 | "infoText", 256 | ); 257 | const text = SvgTags.addText({ 258 | id: `text-${this.options.id}`, 259 | x: String(this.coordinates.x), 260 | y: String(this.coordinates.y + 20), 261 | class: `circle-info-text ${customCssClass}`, 262 | }); 263 | 264 | text.textContent = this.options.text; 265 | 266 | this.tags.push({ 267 | element: text, 268 | parentId: `svg-${this.options.id}`, 269 | }); 270 | } 271 | 272 | /** 273 | * @description Draws a linear gradient into the foreground stroke 274 | */ 275 | private drawLinearGradient() { 276 | const attributes: IAttributes = {}; 277 | 278 | attributes.gradientStart = this.options.strokeGradient[0]; 279 | attributes.gradientEnd = this.options.strokeGradient[1]; 280 | 281 | const defs = SvgTags.addDefs(attributes); 282 | this.tags.push({ 283 | element: defs, 284 | parentId: `svg-${this.options.id}`, 285 | }); 286 | } 287 | } 288 | 289 | export default SimpleCircle; 290 | -------------------------------------------------------------------------------- /src/helper/object-helper.ts: -------------------------------------------------------------------------------- 1 | import {IDictionary} from "../interface/idictionary"; 2 | 3 | export default class ObjectHelper { 4 | public static extractPropertyFromObject(object: IDictionary, property: string) { 5 | let value: string | object | number | boolean | []; 6 | if (object.hasOwnProperty(property) && object[property]) { 7 | value = object[property]; 8 | } 9 | 10 | return value; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/helper/style-helper.ts: -------------------------------------------------------------------------------- 1 | import {ICalculationParams} from "../interface/icalculation-params"; 2 | import {IProgressColor} from "../interface/iprogress-color"; 3 | import SvgTagsHelper from "./svg-tags-helper"; 4 | 5 | export class StyleHelper { 6 | /** 7 | * @description Redraws the arc (circle) border 8 | * @param params 9 | * @param callback 10 | */ 11 | public static animateArc( 12 | params: { 13 | arc: Element, 14 | arcParams: ICalculationParams, 15 | animationStep: number, 16 | progressColors?: IProgressColor[], 17 | }, 18 | callback: () => {}, 19 | ): void { 20 | const {arc, arcParams, animationStep, progressColors} = params; 21 | const startAngle = arcParams.startAngle ? arcParams.startAngle : 0; 22 | const endAngleGrade = arcParams.endAngleGrade ? arcParams.endAngleGrade : 360; 23 | const ms = this.getMilliseconds(arcParams.ms, arcParams.endAngleGrade); 24 | const hasProgressColor = Array.isArray(progressColors) && progressColors.length > 0; 25 | 26 | let count = 1; 27 | const interval = setInterval((arc, percent, progressColors) => { 28 | const endAngle = endAngleGrade / 100 * count; 29 | const sweepFlag = startAngle < 0 && endAngle > 286 ? "1" : "0"; 30 | SvgTagsHelper.setAttributes(arc, { 31 | d: SvgTagsHelper.describeArc( 32 | arcParams.x, 33 | arcParams.y, 34 | arcParams.radius, 35 | startAngle, 36 | endAngle, 37 | sweepFlag, 38 | ), 39 | }); 40 | 41 | if (hasProgressColor) { 42 | StyleHelper.updateCircleColor(count, arc, progressColors); 43 | } 44 | 45 | count += animationStep; 46 | 47 | if (count > percent || count > 100) { 48 | clearInterval(interval); 49 | 50 | if (typeof callback === "function") { 51 | callback(); 52 | } 53 | } 54 | }, ms, arc, arcParams.percent, progressColors); 55 | } 56 | 57 | /** 58 | * @description If options.progressColors is set, colors are changed on given percentages 59 | * @param actualCount 60 | * @param arc 61 | * @param progressColors 62 | */ 63 | public static updateCircleColor(actualCount: number, arc: Element, progressColors: IProgressColor[]) { 64 | const progressColor = progressColors.find((progress: IProgressColor) => progress.percent === actualCount); 65 | if (progressColor) { 66 | SvgTagsHelper.setAttributes(arc, { 67 | style: `stroke: ${progressColor.color}`, 68 | }); 69 | } 70 | } 71 | 72 | /** 73 | * @param defaultMs 74 | * @param endAngleGrade 75 | */ 76 | private static getMilliseconds(defaultMs: number, endAngleGrade: number) { 77 | let ms = defaultMs ? defaultMs : 50; 78 | 79 | if (endAngleGrade <= 180) { 80 | ms = ms / 3; 81 | } 82 | 83 | return ms; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/helper/svg-tags-helper.ts: -------------------------------------------------------------------------------- 1 | import {IAttributes} from "../interface/iattributes"; 2 | import {ICalculationParams} from "../interface/icalculation-params"; 3 | 4 | class SvgTagsHelper { 5 | /** 6 | * @description 7 | * @param element SVGElement 8 | * @param attributes IAvailableOptions 9 | * @returns void 10 | */ 11 | public static setAttributes(element: Element, attributes: IAttributes): void { 12 | for (const [key, value] of Object.entries(attributes)) { 13 | element.setAttribute(key, value); 14 | } 15 | } 16 | 17 | /** 18 | * @description 19 | * @param element 20 | * @param attributes 21 | * @returns void 22 | */ 23 | public static setAttributeNamespace(element: Element, attributes: IAttributes): void { 24 | for (const [key, value] of Object.entries(attributes)) { 25 | element.setAttributeNS(null, key, value); 26 | } 27 | } 28 | 29 | /** 30 | * @description For easier handling polar coordinates are used and converted to cartesian coordinates 31 | * @param centerX 32 | * @param centerY 33 | * @param radius 34 | * @param angleInDegrees 35 | * @returns object 36 | */ 37 | public static polarToCartesian( 38 | centerX: number, 39 | centerY: number, 40 | radius: number, 41 | angleInDegrees: number, 42 | ): ICalculationParams { 43 | const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0; 44 | 45 | return { 46 | x: centerX + radius * Math.cos(angleInRadians), 47 | y: centerY + radius * Math.sin(angleInRadians), 48 | }; 49 | } 50 | 51 | /** 52 | * @description Returns the string for the data attribute in the path tag 53 | * @param x 54 | * @param y 55 | * @param radius 56 | * @param startAngle 57 | * @param endAngle 58 | * @param sweepFlag 59 | * @returns string 60 | */ 61 | public static describeArc( 62 | x: number, 63 | y: number, 64 | radius: number, 65 | startAngle: number, 66 | endAngle: number, 67 | sweepFlag = "0", 68 | ): string { 69 | const start = SvgTagsHelper.polarToCartesian(x, y, radius, endAngle); 70 | const end = SvgTagsHelper.polarToCartesian(x, y, radius, startAngle); 71 | const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; 72 | let closePath = false; 73 | 74 | if (endAngle === 360 && end.x > start.x) { 75 | closePath = true; 76 | start.x = start.x - 0.001; 77 | } 78 | 79 | return [ 80 | "M", start.x, start.y, 81 | "A", radius, radius, 0, largeArcFlag, sweepFlag, end.x, end.y, (closePath ? "Z" : ""), 82 | ].join(" "); 83 | } 84 | 85 | /** 86 | * @description Returns the end coordinates of the arc 87 | * @param x 88 | * @param y 89 | * @param radius 90 | * @param endAngle 91 | */ 92 | public static calculatePathEndCoordinates(x: number, y: number, radius: number, endAngle: number) { 93 | return SvgTagsHelper.polarToCartesian(x, y, radius, endAngle); 94 | } 95 | } 96 | 97 | export default SvgTagsHelper; 98 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "../style/main.scss"; 2 | import Circle from "./base-class/circle"; 3 | import {IAvailableOptions} from "./interface/iavailable-options"; 4 | 5 | /** 6 | * @description Gets called from html script tag 7 | * @param options 8 | * @returns void 9 | */ 10 | export function newCircle(options: IAvailableOptions) { 11 | const circle = new Circle(); 12 | return circle.newCircle(options); 13 | } 14 | 15 | /** 16 | * @description Gets called from html script tag 17 | * @param parentId 18 | * @param type 19 | */ 20 | export function newCircleWithDataSet(parentId: string, type: string) { 21 | const circle = new Circle(); 22 | return circle.newCircleWithDataSet(parentId, type); 23 | } 24 | -------------------------------------------------------------------------------- /src/interface/iattributes.ts: -------------------------------------------------------------------------------- 1 | export interface IAttributes { 2 | width?: string; 3 | height?: string; 4 | id?: string; 5 | class?: string; 6 | cx?: string; 7 | cy?: string; 8 | r?: string; 9 | fill?: string; 10 | "stroke-width"?: number; 11 | "stroke-linecap"?: string; 12 | stroke?: string; 13 | d?: string; 14 | x?: string; 15 | y?: string; 16 | viewBox?: string; 17 | transform?: string; 18 | offset?: string; 19 | "stop-color"?: string; 20 | gradientStart?: string; 21 | gradientEnd?: string; 22 | preserveAspectRatio?: string; 23 | style?: string; 24 | } 25 | -------------------------------------------------------------------------------- /src/interface/iavailable-options.ts: -------------------------------------------------------------------------------- 1 | import {IProgressColor} from "./iprogress-color"; 2 | 3 | export interface IAvailableOptions { 4 | id?: string; 5 | type?: string; 6 | additionalCssClasses?: { 7 | svgContainer?: string, 8 | backgroundCircle?: string, 9 | foregroundCircle?: string, 10 | text?: string, 11 | icon?: string, 12 | point?: string, 13 | infoText?: string, 14 | }; 15 | point?: boolean; 16 | pointSize?: number; 17 | percent: number; 18 | animation?: boolean; 19 | animationStep?: number; 20 | strokeGradient?: [string, string]; 21 | icon?: string; 22 | text?: string; 23 | textReplacesPercentage?: boolean; 24 | noPercentageSign?: boolean; 25 | animateInView?: boolean; 26 | strokeLinecap?: string; 27 | foregroundCircleWidth?: number; 28 | backgroundCircleWidth?: number; 29 | progressColors?: IProgressColor[]; 30 | onAnimationEnd?: () => {}; 31 | startAngle?: number; 32 | // tslint:disable-next-line 33 | [key: string]: any; 34 | } 35 | -------------------------------------------------------------------------------- /src/interface/icalculation-params.ts: -------------------------------------------------------------------------------- 1 | export interface ICalculationParams { 2 | x?: number; 3 | y?: number; 4 | startAngle?: number; 5 | endAngleGrade?: number; 6 | d?: string; 7 | radius?: number; 8 | percent?: number; 9 | ms?: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/interface/idictionary.ts: -------------------------------------------------------------------------------- 1 | export interface IDictionary { 2 | [key: string]: string | object | number | boolean | []; 3 | [key: number]: string | object | number | boolean | []; 4 | } 5 | -------------------------------------------------------------------------------- /src/interface/iprogress-color.ts: -------------------------------------------------------------------------------- 1 | export interface IProgressColor { 2 | percent: number; 3 | color: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/interface/isize.ts: -------------------------------------------------------------------------------- 1 | export interface ISize { 2 | maxSize: number; 3 | height: number; 4 | width: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/interface/itag.ts: -------------------------------------------------------------------------------- 1 | export interface ITag { 2 | element: object; 3 | parentId: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/interface/itype.ts: -------------------------------------------------------------------------------- 1 | export interface IType { 2 | type: string; 3 | value: number | boolean | string | [] | {}; 4 | } 5 | -------------------------------------------------------------------------------- /src/interface/iview-box-attributes.ts: -------------------------------------------------------------------------------- 1 | export interface IViewBoxAttributes { 2 | minX: number; 3 | minY: number; 4 | width: number; 5 | height: number; 6 | } 7 | -------------------------------------------------------------------------------- /style/main.scss: -------------------------------------------------------------------------------- 1 | @import "modules/foreground-circle"; 2 | @import "modules/background-circle"; 3 | @import "modules/circle-text"; 4 | @import "modules/circle-container"; 5 | @import "modules/point-circle"; 6 | @import "modules/circle-icon"; 7 | 8 | -------------------------------------------------------------------------------- /style/modules/background-circle.scss: -------------------------------------------------------------------------------- 1 | .background-circle { 2 | fill: none; 3 | stroke: #ccc; 4 | } 5 | -------------------------------------------------------------------------------- /style/modules/circle-container.scss: -------------------------------------------------------------------------------- 1 | .circle-container { 2 | overflow: visible; 3 | } 4 | -------------------------------------------------------------------------------- /style/modules/circle-icon.scss: -------------------------------------------------------------------------------- 1 | .circle-icon.fa { 2 | font-size: 30px; 3 | fill: #000; 4 | } 5 | -------------------------------------------------------------------------------- /style/modules/circle-text.scss: -------------------------------------------------------------------------------- 1 | .circle-text { 2 | font-family: Arial, sans-serif; 3 | color: #aaa; 4 | font-size: 20px; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /style/modules/foreground-circle.scss: -------------------------------------------------------------------------------- 1 | .foreground-circle { 2 | fill: none; 3 | stroke: #3498DB; 4 | } 5 | 6 | .foreground-circle-without-stroke-color { 7 | fill: none; 8 | } 9 | -------------------------------------------------------------------------------- /style/modules/point-circle.scss: -------------------------------------------------------------------------------- 1 | .point-circle { 2 | fill: aliceblue; 3 | } 4 | -------------------------------------------------------------------------------- /test/base-class/circle-factory.test.ts: -------------------------------------------------------------------------------- 1 | import {CircleFactory} from "../../src/base-class/circle-factory"; 2 | import HalfCircle from "../../src/circle-type/half-circle"; 3 | import PlainCircle from "../../src/circle-type/plain-circle"; 4 | import SimpleCircle from "../../src/circle-type/simple-circle"; 5 | 6 | describe("CircleFactory", () => { 7 | describe("create()", () => { 8 | test("half", () => { 9 | const result = CircleFactory.create("half"); 10 | 11 | expect(result).toBeInstanceOf(HalfCircle); 12 | }); 13 | 14 | test("plain", () => { 15 | const result = CircleFactory.create("plain"); 16 | 17 | expect(result).toBeInstanceOf(PlainCircle); 18 | }); 19 | 20 | test("simple", () => { 21 | const result = CircleFactory.create("simple"); 22 | 23 | expect(result).toBeInstanceOf(SimpleCircle); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/base-class/options.test.ts: -------------------------------------------------------------------------------- 1 | import Options from "../../src/base-class/options"; 2 | import {IAvailableOptions} from "../../src/interface/iavailable-options"; 3 | 4 | describe("Options", () => { 5 | describe("mergeOptions()", () => { 6 | test("all values custom set", () => { 7 | const expected = { 8 | additionalCssClasses: { 9 | backgroundCircle: "circle-background-circle", 10 | foregroundCircle: "circle-foreground-circle", 11 | icon: "circle-icon", 12 | infoText: "circle-info-text", 13 | point: "circle-point", 14 | svgContainer: "circle-container", 15 | text: "circle-text", 16 | }, 17 | animateInView: true, 18 | animation: true, 19 | animationStep: 3, 20 | backgroundCircleWidth: 150, 21 | foregroundCircleWidth: 50, 22 | icon: "f0d0", 23 | id: "circle", 24 | noPercentageSign: true, 25 | percent: 60, 26 | point: true, 27 | pointSize: 30, 28 | progressColors: [{color: "#000", percent: 50}], 29 | strokeGradient: ["#05a", "#0a5"], 30 | strokeLinecap: "round", 31 | text: "Lorem", 32 | textReplacesPercentage: false, 33 | type: "simple", 34 | }; 35 | 36 | const options: IAvailableOptions = { 37 | id: "circle", 38 | type: "simple", 39 | percent: 60, 40 | strokeGradient: ["#05a", "#0a5"], 41 | animationStep: 3, 42 | foregroundCircleWidth: 50, 43 | backgroundCircleWidth: 150, 44 | additionalCssClasses: { 45 | svgContainer: "circle-container", 46 | foregroundCircle: "circle-foreground-circle", 47 | backgroundCircle: "circle-background-circle", 48 | text: "circle-text", 49 | icon: "circle-icon", 50 | infoText: "circle-info-text", 51 | point: "circle-point", 52 | }, 53 | icon: "f0d0", 54 | text: "Lorem", 55 | noPercentageSign: true, 56 | strokeLinecap: "round", 57 | point: true, 58 | pointSize: 30, 59 | animation: true, 60 | textReplacesPercentage: false, 61 | animateInView: true, 62 | progressColors: [{percent: 50, color: "#000"}], 63 | }; 64 | const optionInstance = new Options(); 65 | const result = optionInstance.mergeOptions(options); 66 | 67 | expect(result).toEqual(expected); 68 | }); 69 | 70 | test("check defaults", () => { 71 | const expected = { 72 | animateInView: false, 73 | animation: true, 74 | animationStep: 1, 75 | backgroundCircleWidth: 15, 76 | foregroundCircleWidth: 5, 77 | noPercentageSign: false, 78 | percent: 75, 79 | point: false, 80 | pointSize: 60, 81 | strokeLinecap: "butt", 82 | textReplacesPercentage: false, 83 | type: "SimpleCircle", 84 | 85 | }; 86 | const optionInstance = new Options(); 87 | const result = optionInstance.mergeOptions({percent: 75}); 88 | 89 | expect(result).toEqual(expected); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/helper/object-helper.test.ts: -------------------------------------------------------------------------------- 1 | import ObjectHelper from "../../src/helper/object-helper"; 2 | 3 | describe("ObjectHelper", () => { 4 | describe("extractPropertyFromObject()", () => { 5 | test("extract number", () => { 6 | const expected = 55; 7 | const object = {percent: expected}; 8 | const result = ObjectHelper.extractPropertyFromObject(object, "percent"); 9 | 10 | expect(result).toBe(expected); 11 | }); 12 | 13 | test("extract string", () => { 14 | const expected = "simple"; 15 | const object = {type: expected}; 16 | const result = ObjectHelper.extractPropertyFromObject(object, "type"); 17 | 18 | expect(result).toBe(expected); 19 | }); 20 | 21 | test("extract boolean", () => { 22 | const expected = true; 23 | const object = {animation: expected}; 24 | const result = ObjectHelper.extractPropertyFromObject(object, "animation"); 25 | 26 | expect(result).toBe(expected); 27 | }); 28 | 29 | test("extract object", () => { 30 | const expected = { 31 | svgContainer: "svg-container", 32 | backgroundCircle: "background-circle", 33 | foregroundCircle: "foreground-circle", 34 | text: "circle-percentage-text", 35 | icon: "circle-icon", 36 | point: "circle-point", 37 | infoText: "circle-info-text", 38 | }; 39 | const object = {additionalCssClasses: expected}; 40 | const result = ObjectHelper.extractPropertyFromObject(object, "additionalCssClasses"); 41 | 42 | expect(result).toBe(expected); 43 | }); 44 | 45 | test("extract array", () => { 46 | const expected = ["orange", "green"]; 47 | const object = {strokeGradient: expected}; 48 | const result = ObjectHelper.extractPropertyFromObject(object, "strokeGradient"); 49 | 50 | expect(result).toBe(expected); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/helper/svg-tags-helper.test.ts: -------------------------------------------------------------------------------- 1 | import SvgTagsHelper from "../../src/helper/svg-tags-helper"; 2 | 3 | describe("SvgTagHelper", () => { 4 | test("polarToCartesian()", () => { 5 | const expected = { 6 | x: 501.2010330031063, 7 | y: 729.1250632847796, 8 | }; 9 | const result = SvgTagsHelper.polarToCartesian(391.5, 391.5, 355, 162); 10 | 11 | expect(result).toEqual(expected); 12 | }); 13 | 14 | test("describeArc()", () => { 15 | const expected = "M 679.4291482980439 182.3072287091088 A 355.9 355.9 0 0 0 391.5 35.60000000000002 "; 16 | const result = SvgTagsHelper.describeArc(391.5, 391.5, 355.90, 0, 54); 17 | 18 | expect(result).toEqual(expected); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": false 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | "public", 15 | "test", 16 | "coverage", 17 | "docs", 18 | "style" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:latest" 4 | ], 5 | "linterOptions": { 6 | "exclude": [ 7 | "webpack*", 8 | "coverage/*" 9 | ] 10 | }, 11 | "rules": { 12 | "no-any": true, 13 | "no-non-null-assertion": true, 14 | "no-shadowed-variable": false, 15 | "only-arrow-functions": false, 16 | "no-implicit-dependencies": false, 17 | "no-reference": false, 18 | "max-line-length": [ true, { "limit": 120, "ignore-pattern": "^import"}], 19 | "arrow-return-shorthand": true, 20 | "member-ordering": [ 21 | true, 22 | { 23 | "order": [ 24 | "static-field", 25 | "instance-field", 26 | "static-method", 27 | "instance-method" 28 | ] 29 | } 30 | ], 31 | "no-console": [ 32 | true, 33 | "debug", 34 | "info", 35 | "time", 36 | "timeEnd", 37 | "trace" 38 | ], 39 | "no-empty": false, 40 | "no-empty-interface": true, 41 | "no-string-literal": false, 42 | "no-string-throw": true, 43 | "no-switch-case-fall-through": true, 44 | "no-unnecessary-initializer": true, 45 | "no-unused-expression": true, 46 | "no-var-keyword": true, 47 | "object-literal-sort-keys": false, 48 | "prefer-const": true 49 | }, 50 | "ordered-imports": [ 51 | true, 52 | { 53 | "import-sources-order": "lowercase-last", 54 | "named-imports-order": "lowercase-first" 55 | } 56 | ], 57 | "no-magic-numbers": false, 58 | "no-duplicate-imports": true, 59 | "no-trailing-whitespace": [ 60 | true, 61 | "ignore-comments", 62 | "ignore-blank-lines" 63 | ], 64 | "file-name-casing": [true, "kebab-case"], 65 | "quotemark": [true, "single", "avoid-escape", "avoid-template"], 66 | "semicolon": [true, "always", "ignore-bound-class-methods"] 67 | } 68 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const {ProvidePlugin} = require("webpack"); 2 | 3 | const path = require("path"); 4 | const {CleanWebpackPlugin} = require("clean-webpack-plugin"); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | const TerserPlugin = require('terser-webpack-plugin'); 7 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 8 | 9 | const libraryName = "circliful"; 10 | 11 | module.exports = { 12 | entry: "./src/index.ts", 13 | output: { 14 | filename: `${libraryName}.js`, 15 | library: libraryName, 16 | path: path.resolve(__dirname, "./dist"), 17 | publicPath: '/dist/', 18 | }, 19 | target: "web", 20 | devtool: "inline-source-map", 21 | mode: "development", 22 | devServer: { 23 | port: 9090, 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.ts$/, 29 | exclude: ["/node_modules", path.resolve(__dirname, "./test")], 30 | use: "ts-loader", 31 | }, 32 | { 33 | test: /\.ts$/, 34 | enforce: "pre", 35 | use: [ 36 | { 37 | loader: "tslint-loader", 38 | options: { 39 | configFile: "tslint.json", 40 | }, 41 | }, 42 | ], 43 | }, 44 | { 45 | test: /\.(sa|sc|c)ss$/, 46 | exclude: path.resolve(__dirname, "./test"), 47 | use: [ 48 | { 49 | loader: MiniCssExtractPlugin.loader, 50 | options: { 51 | hmr: process.env.NODE_ENV === 'development', 52 | }, 53 | }, 54 | 'css-loader', 55 | 'sass-loader', 56 | ], 57 | } 58 | ], 59 | }, 60 | optimization: { 61 | minimize: false, 62 | minimizer: [new TerserPlugin({ 63 | terserOptions: { 64 | output: { 65 | comments: false, 66 | }, 67 | }, 68 | extractComments: false, 69 | }), new OptimizeCSSAssetsPlugin()], 70 | }, 71 | resolve: { 72 | extensions: [".ts", ".js"], 73 | modules: ["src", "node_modules"], 74 | }, 75 | plugins: [ 76 | new ProvidePlugin({ 77 | Promise: "es6-promise-promise", 78 | }), 79 | new CleanWebpackPlugin({ 80 | cleanOnceBeforeBuildPatterns: [ 81 | path.join(process.cwd(), 'dist/**/*') 82 | ], 83 | }), 84 | new MiniCssExtractPlugin() 85 | ], 86 | }; 87 | -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const { ProvidePlugin } = require("webpack"); 2 | 3 | const path = require("path"); 4 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | const TerserPlugin = require('terser-webpack-plugin'); 7 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 8 | const EsmWebpackPlugin = require("@purtuga/esm-webpack-plugin"); 9 | 10 | const libraryName = "circliful"; 11 | 12 | module.exports = { 13 | entry: "./src/index.ts", 14 | output: { 15 | filename: `${libraryName}.js`, 16 | library: libraryName, 17 | path: path.resolve(__dirname, "./dist"), 18 | libraryTarget: "var" 19 | }, 20 | target: "web", 21 | mode: "production", 22 | module: { 23 | rules: [{ 24 | test: /\.ts$/, 25 | exclude: ["/node_modules", path.resolve(__dirname, "./test")], 26 | use: "ts-loader", 27 | }, 28 | { 29 | test: /\.ts$/, 30 | enforce: "pre", 31 | use: [{ 32 | loader: "tslint-loader", 33 | options: { 34 | configFile: "tslint.json", 35 | }, 36 | }, ], 37 | }, 38 | { 39 | test: /\.(sa|sc|c)ss$/, 40 | exclude: path.resolve(__dirname, "./test"), 41 | use: [{ 42 | loader: MiniCssExtractPlugin.loader, 43 | options: { 44 | hmr: process.env.NODE_ENV === 'development', 45 | }, 46 | }, 47 | 'css-loader', 48 | 'sass-loader', 49 | ], 50 | } 51 | ], 52 | }, 53 | optimization: { 54 | nodeEnv: 'production', 55 | removeAvailableModules: true, 56 | minimize: true, 57 | minimizer: [new TerserPlugin({ 58 | cache: true, 59 | parallel: true, 60 | terserOptions: { 61 | output: { 62 | comments: false, 63 | }, 64 | }, 65 | extractComments: false, 66 | }), new OptimizeCSSAssetsPlugin()], 67 | }, 68 | resolve: { 69 | extensions: [".ts", ".js"], 70 | modules: ["src", "node_modules"], 71 | }, 72 | plugins: [ 73 | new ProvidePlugin({ 74 | Promise: "es6-promise-promise", 75 | }), 76 | new CleanWebpackPlugin({ 77 | cleanOnceBeforeBuildPatterns: [ 78 | path.join(process.cwd(), 'dist/**/*') 79 | ], 80 | }), 81 | new MiniCssExtractPlugin() 82 | ], 83 | }; --------------------------------------------------------------------------------