angular-svg-round-progressbar
Version:
Angular module that uses SVG to create a circular progressbar
558 lines (550 loc) • 24.7 kB
JavaScript
import * as i0 from '@angular/core';
import { DOCUMENT, Injectable, Optional, Inject, InjectionToken, EventEmitter, Component, ChangeDetectionStrategy, ViewChild, Input, Output, NgModule } from '@angular/core';
const DEGREE_IN_RADIANS = Math.PI / 180;
class RoundProgressService {
constructor(document) {
this.supportsSvg = !!(document &&
document.createElementNS &&
document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect);
this.base = document?.head?.querySelector('base');
this.hasPerf =
typeof window !== 'undefined' &&
window.performance &&
window.performance.now &&
typeof window.performance.now() === 'number';
}
/**
* Resolves a SVG color against the page's `base` tag.
*/
resolveColor(color) {
if (this.base && this.base.href) {
const hashIndex = color.indexOf('#');
if (hashIndex > -1 && color.indexOf('url') > -1) {
return color.slice(0, hashIndex) + location.href + color.slice(hashIndex);
}
}
return color;
}
/**
* Generates a timestamp.
*/
getTimestamp() {
return this.hasPerf ? window.performance.now() : Date.now();
}
/**
* Generates the value for an SVG arc.
*
* @param current Current value.
* @param total Maximum value.
* @param pathRadius Radius of the SVG path.
* @param elementRadius Radius of the SVG container.
* @param isSemicircle Whether the element should be a semicircle.
*/
getArc(current, total, pathRadius, elementRadius, isSemicircle = false) {
const value = Math.max(0, Math.min(current || 0, total));
const maxAngle = isSemicircle ? 180 : 359.9999;
const percentage = total === 0 ? maxAngle : (value / total) * maxAngle;
const start = this._polarToCartesian(elementRadius, pathRadius, percentage);
const end = this._polarToCartesian(elementRadius, pathRadius, 0);
const arcSweep = percentage <= 180 ? 0 : 1;
return `M ${start} A ${pathRadius} ${pathRadius} 0 ${arcSweep} 0 ${end}`;
}
/**
* Converts polar cooradinates to Cartesian.
*
* @param elementRadius Radius of the wrapper element.
* @param pathRadius Radius of the path being described.
* @param angleInDegrees Degree to be converted.
*/
_polarToCartesian(elementRadius, pathRadius, angleInDegrees) {
const angleInRadians = (angleInDegrees - 90) * DEGREE_IN_RADIANS;
const x = elementRadius + pathRadius * Math.cos(angleInRadians);
const y = elementRadius + pathRadius * Math.sin(angleInRadians);
return x + ' ' + y;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressService, deps: [{ token: DOCUMENT, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: Document, decorators: [{
type: Optional
}, {
type: Inject,
args: [DOCUMENT]
}] }] });
const DEFAULTS = {
radius: 125,
animation: 'easeOutCubic',
animationDelay: undefined,
duration: 500,
stroke: 15,
color: '#45CCCE',
background: '#EAEAEA',
responsive: false,
clockwise: true,
semicircle: false,
rounded: false,
innerCircleFill: 'transparent',
};
const ROUND_PROGRESS_DEFAULTS = new InjectionToken('ROUND_PROGRESS_DEFAULTS', {
providedIn: 'root',
factory: () => DEFAULTS
});
const ROUND_PROGRESS_DEFAULTS_PROVIDER = {
provide: ROUND_PROGRESS_DEFAULTS,
useValue: DEFAULTS,
};
/**
* TERMS OF USE - EASING EQUATIONS
* Open source under the BSD License.
*
* Copyright © 2001 Robert Penner
* All rights reserved.
* Redistribution and use in source and binary forms, with or without modification, are permitted
* provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this list of conditions
* and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions
* and the following disclaimer in the documentation and/or other materials provided with the
* distribution.
*
* Neither the name of the author nor the names of contributors may be used to endorse or promote
* products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
class RoundProgressEase {
// t: current time (or position) of the neonate. This can be seconds or frames, steps,
// seconds, ms, whatever – as long as the unit is the same as is used for the total time.
// b: beginning value of the property.
// c: change between the beginning and destination value of the property.
// d: total time of the neonate.
linearEase(t, b, c, d) {
return (c * t) / d + b;
}
easeInQuad(t, b, c, d) {
return c * (t /= d) * t + b;
}
easeOutQuad(t, b, c, d) {
return -c * (t /= d) * (t - 2) + b;
}
easeInOutQuad(t, b, c, d) {
// eslint-disable-next-line no-cond-assign
if ((t /= d / 2) < 1) {
return (c / 2) * t * t + b;
}
return (-c / 2) * (--t * (t - 2) - 1) + b;
}
easeInCubic(t, b, c, d) {
return c * (t /= d) * t * t + b;
}
easeOutCubic(t, b, c, d) {
return c * ((t = t / d - 1) * t * t + 1) + b;
}
easeInOutCubic(t, b, c, d) {
// eslint-disable-next-line no-cond-assign
if ((t /= d / 2) < 1) {
return (c / 2) * t * t * t + b;
}
return (c / 2) * ((t -= 2) * t * t + 2) + b;
}
easeInQuart(t, b, c, d) {
return c * (t /= d) * t * t * t + b;
}
easeOutQuart(t, b, c, d) {
return -c * ((t = t / d - 1) * t * t * t - 1) + b;
}
easeInOutQuart(t, b, c, d) {
// eslint-disable-next-line no-cond-assign
if ((t /= d / 2) < 1) {
return (c / 2) * t * t * t * t + b;
}
return (-c / 2) * ((t -= 2) * t * t * t - 2) + b;
}
easeInQuint(t, b, c, d) {
return c * (t /= d) * t * t * t * t + b;
}
easeOutQuint(t, b, c, d) {
return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
}
easeInOutQuint(t, b, c, d) {
// eslint-disable-next-line no-cond-assign
if ((t /= d / 2) < 1) {
return (c / 2) * t * t * t * t * t + b;
}
return (c / 2) * ((t -= 2) * t * t * t * t + 2) + b;
}
easeInSine(t, b, c, d) {
return -c * Math.cos((t / d) * (Math.PI / 2)) + c + b;
}
easeOutSine(t, b, c, d) {
return c * Math.sin((t / d) * (Math.PI / 2)) + b;
}
easeInOutSine(t, b, c, d) {
return (-c / 2) * (Math.cos((Math.PI * t) / d) - 1) + b;
}
easeInExpo(t, b, c, d) {
return t === 0 ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
}
easeOutExpo(t, b, c, d) {
return t === d ? b + c : c * (-Math.pow(2, (-10 * t) / d) + 1) + b;
}
easeInOutExpo(t, b, c, d) {
if (t === 0) {
return b;
}
if (t === d) {
return b + c;
}
// eslint-disable-next-line no-cond-assign
if ((t /= d / 2) < 1) {
return (c / 2) * Math.pow(2, 10 * (t - 1)) + b;
}
return (c / 2) * (-Math.pow(2, -10 * --t) + 2) + b;
}
easeInCirc(t, b, c, d) {
return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
}
easeOutCirc(t, b, c, d) {
return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
}
easeInOutCirc(t, b, c, d) {
// eslint-disable-next-line no-cond-assign
if ((t /= d / 2) < 1) {
return (-c / 2) * (Math.sqrt(1 - t * t) - 1) + b;
}
return (c / 2) * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
}
easeInElastic(t, b, c, d) {
const p = d * 0.3;
let s = 1.70158;
let a = c;
if (t === 0) {
return b;
}
// eslint-disable-next-line no-cond-assign
if ((t /= d) === 1) {
return b + c;
}
if (a < Math.abs(c)) {
a = c;
s = p / 4;
}
else {
s = (p / (2 * Math.PI)) * Math.asin(c / a);
}
return -(a * Math.pow(2, 10 * t--) * Math.sin(((t * d - s) * (2 * Math.PI)) / p)) + b;
}
easeOutElastic(t, b, c, d) {
const p = d * 0.3;
let s = 1.70158;
let a = c;
if (t === 0) {
return b;
}
// eslint-disable-next-line no-cond-assign
if ((t /= d) === 1) {
return b + c;
}
if (a < Math.abs(c)) {
a = c;
s = p / 4;
}
else {
s = (p / (2 * Math.PI)) * Math.asin(c / a);
}
return a * Math.pow(2, -10 * t) * Math.sin(((t * d - s) * (2 * Math.PI)) / p) + c + b;
}
easeInOutElastic(t, b, c, d) {
const p = d * (0.3 * 1.5);
let s = 1.70158;
let a = c;
if (t === 0) {
return b;
}
// eslint-disable-next-line no-cond-assign
if ((t /= d / 2) === 2) {
return b + c;
}
if (a < Math.abs(c)) {
a = c;
s = p / 4;
}
else {
s = (p / (2 * Math.PI)) * Math.asin(c / a);
}
if (t < 1) {
return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) *
Math.sin(((t * d - s) * (2 * Math.PI)) / p)) + b;
}
return a * Math.pow(2, -10 * (t -= 1)) *
Math.sin(((t * d - s) * (2 * Math.PI)) / p) * 0.5 + c + b;
}
easeInBack(t, b, c, d, s = 1.70158) {
return c * (t /= d) * t * ((s + 1) * t - s) + b;
}
easeOutBack(t, b, c, d, s = 1.70158) {
return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
}
easeInOutBack(t, b, c, d, s = 1.70158) {
// eslint-disable-next-line no-cond-assign
if ((t /= d / 2) < 1) {
return (c / 2) * (t * t * (((s *= 1.525) + 1) * t - s)) + b;
}
return (c / 2) * ((t -= 2) * t * (((s *= 1.525) + 1) * t + s) + 2) + b;
}
easeInBounce(t, b, c, d) {
return c - this.easeOutBounce(d - t, 0, c, d) + b;
}
easeOutBounce(t, b, c, d) {
// eslint-disable-next-line no-cond-assign
if ((t /= d) < 1 / 2.75) {
return c * (7.5625 * t * t) + b;
}
else if (t < 2 / 2.75) {
return c * (7.5625 * (t -= 1.5 / 2.75) * t + 0.75) + b;
}
else if (t < 2.5 / 2.75) {
return c * (7.5625 * (t -= 2.25 / 2.75) * t + 0.9375) + b;
}
return c * (7.5625 * (t -= 2.625 / 2.75) * t + 0.984375) + b;
}
easeInOutBounce(t, b, c, d) {
if (t < d / 2) {
return this.easeInBounce(t * 2, 0, c, d) * 0.5 + b;
}
return this.easeOutBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressEase, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressEase, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressEase, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
class RoundProgressComponent {
constructor(service, easing, defaults, ngZone) {
this.service = service;
this.easing = easing;
this.defaults = defaults;
this.ngZone = ngZone;
this.currentLinecap = '';
/** Current value of the progress bar. */
this.current = 0;
/** Maximum value of the progress bar. */
this.max = 0;
/** Radius of the circle. */
this.radius = this.defaults.radius;
/** Name of the easing function to use when animating. */
this.animation = this.defaults.animation;
/** Time in millisconds by which to delay the animation. */
this.animationDelay = this.defaults.animationDelay;
/** Duration of the animation. */
this.duration = this.defaults.duration;
/** Width of the circle's stroke. */
this.stroke = this.defaults.stroke;
/** Color of the circle. */
this.color = this.defaults.color;
/** Background color of the circle. */
this.background = this.defaults.background;
/** Whether the circle should take up the width of its parent. */
this.responsive = this.defaults.responsive;
/** Whether the circle is filling up clockwise. */
this.clockwise = this.defaults.clockwise;
/** Whether to render a semicircle. */
this.semicircle = this.defaults.semicircle;
/** Whether the tip of the progress should be rounded off. */
this.rounded = this.defaults.rounded;
/** Fill color of the circle inside the progress bar */
this.innerCircleFill = this.defaults.innerCircleFill;
/** Emits when a new value has been rendered. */
this.onRender = new EventEmitter();
this.lastAnimationId = 0;
}
/** Animates a change in the current value. */
_animateChange(from, to) {
if (typeof from !== 'number') {
from = 0;
}
to = this._clamp(to);
from = this._clamp(from);
const changeInValue = to - from;
const duration = this.duration;
// Avoid firing change detection for each of the animation frames.
this.ngZone.runOutsideAngular(() => {
const start = () => {
const startTime = this.service.getTimestamp();
const id = ++this.lastAnimationId;
const animation = () => {
const currentTime = Math.min(this.service.getTimestamp() - startTime, duration);
const easingFn = this.easing[this.animation];
const value = easingFn(currentTime, from, changeInValue, duration);
this._updatePath(value);
if (this.onRender.observers.length > 0) {
this.onRender.emit(value);
}
if (id === this.lastAnimationId && currentTime < duration) {
requestAnimationFrame(animation);
}
};
requestAnimationFrame(animation);
};
if (this.animationDelay > 0) {
setTimeout(start, this.animationDelay);
}
else {
start();
}
});
}
/** Updates the path apperance. */
_updatePath(value) {
if (this.path) {
const arc = this.service.getArc(value, this.max, this.radius - this.stroke / 2, this.radius, this.semicircle);
const path = this.path.nativeElement;
// Remove the rounded line cap when the value is zero,
// because SVG won't allow it to disappear completely.
const linecap = this.rounded && value > 0 ? 'round' : '';
// This is called on each animation frame so avoid
// updating the line cap unless it has changed.
if (linecap !== this.currentLinecap) {
this.currentLinecap = linecap;
path.style.strokeLinecap = linecap;
}
path.setAttribute('d', arc);
}
}
/** Clamps a value between the maximum and 0. */
_clamp(value) {
return Math.max(0, Math.min(value || 0, this.max));
}
/** Determines the SVG transforms for the <path> node. */
getPathTransform() {
const diameter = this._getDiameter();
if (this.semicircle) {
return this.clockwise
? `translate(0, ${diameter}) rotate(-90)`
: `translate(${diameter + ',' + diameter}) rotate(90) scale(-1, 1)`;
}
else if (!this.clockwise) {
return `scale(-1, 1) translate(-${diameter} 0)`;
}
return null;
}
/** Resolves a color through the service. */
resolveColor(color) {
return this.service.resolveColor(color);
}
/** Change detection callback. */
ngOnChanges(changes) {
if (changes.current) {
this._animateChange(changes.current.previousValue, changes.current.currentValue);
}
else {
this._updatePath(this.current);
}
}
/** Diameter of the circle. */
_getDiameter() {
return this.radius * 2;
}
/** The CSS height of the wrapper element. */
_getElementHeight() {
if (!this.responsive) {
return (this.semicircle ? this.radius : this._getDiameter()) + 'px';
}
return null;
}
/** Viewbox for the SVG element. */
_getViewBox() {
const diameter = this._getDiameter();
return `0 0 ${diameter} ${this.semicircle ? this.radius : diameter}`;
}
/** Bottom padding for the wrapper element. */
_getPaddingBottom() {
if (this.responsive) {
return this.semicircle ? '50%' : '100%';
}
return null;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressComponent, deps: [{ token: RoundProgressService }, { token: RoundProgressEase }, { token: ROUND_PROGRESS_DEFAULTS }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.2.1", type: RoundProgressComponent, isStandalone: true, selector: "round-progress", inputs: { current: "current", max: "max", radius: "radius", animation: "animation", animationDelay: "animationDelay", duration: "duration", stroke: "stroke", color: "color", background: "background", responsive: "responsive", clockwise: "clockwise", semicircle: "semicircle", rounded: "rounded", innerCircleFill: "innerCircleFill" }, outputs: { onRender: "onRender" }, host: { attributes: { "role": "progressbar" }, properties: { "attr.aria-valuemin": "0", "attr.aria-valuemax": "max", "attr.aria-valuenow": "current", "style.width": "responsive ? \"\" : _getDiameter() + \"px\"", "style.height": "_getElementHeight()", "style.padding-bottom": "_getPaddingBottom()", "class.responsive": "responsive" } }, viewQueries: [{ propertyName: "path", first: true, predicate: ["path"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<svg xmlns=\"http://www.w3.org/2000/svg\" [attr.viewBox]=\"_getViewBox()\">\n <circle\n [attr.fill]=\"innerCircleFill\"\n [attr.cx]=\"radius\"\n [attr.cy]=\"radius\"\n [attr.r]=\"radius - stroke / 2\"\n [style.stroke]=\"resolveColor(background)\"\n [style.stroke-width]=\"stroke\"/>\n\n <path\n #path\n fill=\"none\"\n [style.stroke-width]=\"stroke\"\n [style.stroke]=\"resolveColor(color)\"\n [attr.transform]=\"getPathTransform()\"/>\n</svg>\n", styles: [":host{display:block;position:relative;overflow:hidden}:host(.responsive){width:100%;padding-bottom:100%}:host(.responsive)>svg{position:absolute;width:100%;height:100%;top:0;left:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressComponent, decorators: [{
type: Component,
args: [{ selector: 'round-progress', changeDetection: ChangeDetectionStrategy.OnPush, host: {
'role': 'progressbar',
'[attr.aria-valuemin]': '0',
'[attr.aria-valuemax]': 'max',
'[attr.aria-valuenow]': 'current',
'[style.width]': 'responsive ? "" : _getDiameter() + "px"',
'[style.height]': '_getElementHeight()',
'[style.padding-bottom]': '_getPaddingBottom()',
'[class.responsive]': 'responsive',
}, template: "<svg xmlns=\"http://www.w3.org/2000/svg\" [attr.viewBox]=\"_getViewBox()\">\n <circle\n [attr.fill]=\"innerCircleFill\"\n [attr.cx]=\"radius\"\n [attr.cy]=\"radius\"\n [attr.r]=\"radius - stroke / 2\"\n [style.stroke]=\"resolveColor(background)\"\n [style.stroke-width]=\"stroke\"/>\n\n <path\n #path\n fill=\"none\"\n [style.stroke-width]=\"stroke\"\n [style.stroke]=\"resolveColor(color)\"\n [attr.transform]=\"getPathTransform()\"/>\n</svg>\n", styles: [":host{display:block;position:relative;overflow:hidden}:host(.responsive){width:100%;padding-bottom:100%}:host(.responsive)>svg{position:absolute;width:100%;height:100%;top:0;left:0}\n"] }]
}], ctorParameters: () => [{ type: RoundProgressService }, { type: RoundProgressEase }, { type: undefined, decorators: [{
type: Inject,
args: [ROUND_PROGRESS_DEFAULTS]
}] }, { type: i0.NgZone }], propDecorators: { path: [{
type: ViewChild,
args: ['path']
}], current: [{
type: Input
}], max: [{
type: Input
}], radius: [{
type: Input
}], animation: [{
type: Input
}], animationDelay: [{
type: Input
}], duration: [{
type: Input
}], stroke: [{
type: Input
}], color: [{
type: Input
}], background: [{
type: Input
}], responsive: [{
type: Input
}], clockwise: [{
type: Input
}], semicircle: [{
type: Input
}], rounded: [{
type: Input
}], innerCircleFill: [{
type: Input
}], onRender: [{
type: Output
}] } });
class RoundProgressModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressModule, imports: [RoundProgressComponent], exports: [RoundProgressComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressModule, providers: [ROUND_PROGRESS_DEFAULTS_PROVIDER] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressModule, decorators: [{
type: NgModule,
args: [{
imports: [RoundProgressComponent],
exports: [RoundProgressComponent],
providers: [ROUND_PROGRESS_DEFAULTS_PROVIDER],
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { ROUND_PROGRESS_DEFAULTS, ROUND_PROGRESS_DEFAULTS_PROVIDER, RoundProgressComponent, RoundProgressEase, RoundProgressModule, RoundProgressService, RoundProgressModule as RoundprogressModule };
//# sourceMappingURL=angular-svg-round-progressbar.mjs.map