@syncfusion/ej2-progressbar
Version:
Essential JS 2 ProgressBar Component
402 lines (393 loc) • 23.1 kB
text/typescript
import { ProgressBar } from '../../progressbar';
import { ProgressAnimation } from '../utils/progress-animation';
import { PathOption, getElement, Size, measureText } from '@syncfusion/ej2-svg-base';
import { ITextRenderEventArgs } from '../model/progress-interface';
import { stringToNumber, getPathArc, degreeToLocation, ProgressLocation } from '../utils/helper';
import { Segment } from './segment-progress';
import { TextOption } from '../utils/helper';
import { ModeType } from '../utils';
import { animationMode } from '@syncfusion/ej2-base';
/**
* Progressbar of type circular
*/
export class Circular {
private progress: ProgressBar;
public delay: number;
private segment: Segment = new Segment();
private animation: ProgressAnimation = new ProgressAnimation();
private isRange: boolean;
private centerX: number;
private centerY: number;
private maxThickness: number;
private availableSize: number;
private trackEndAngle: number;
// Defines end position of circular progress.
public endPosition: ProgressLocation;
// Defines buffer end position of circular progress.
public bufferEndPosition: ProgressLocation;
constructor(progress: ProgressBar) {
this.progress = progress;
}
/**
* To render the circular track.
*
* @returns {void}
*/
public renderCircularTrack(): void {
const progress: ProgressBar = this.progress;
const circularTrackGroup: Element = progress.renderer.createGroup({ 'id': progress.element.id + '_CircularTrackGroup' });
let radius: number;
let endAngle: number;
const startAngle: number = progress.startAngle;
progress.totalAngle = (progress.endAngle - progress.startAngle) % 360;
progress.totalAngle = (progress.totalAngle <= 0 ? (360 + progress.totalAngle) : progress.totalAngle);
progress.totalAngle -= (progress.totalAngle === 360) ? 0.01 : 0;
this.trackEndAngle = endAngle = (progress.startAngle + (
(progress.enableRtl) ? -progress.totalAngle : +progress.totalAngle)) % 360;
this.centerX = progress.progressRect.x + (progress.progressRect.width / 2);
this.centerY = progress.progressRect.y + (progress.progressRect.height / 2);
this.maxThickness = Math.max(progress.trackThickness, progress.progressThickness) ||
Math.max(progress.themeStyle.circularProgressThickness, progress.themeStyle.circularTrackThickness);
this.availableSize = (Math.min(progress.progressRect.height, progress.progressRect.width) / 2) - this.maxThickness / 2;
radius = stringToNumber(progress.radius, this.availableSize);
radius = (radius === null) ? 0 : radius;
const stroke: string = (progress.argsData.trackColor || progress.themeStyle.circularTrackColor);
const fill: string = (progress.enablePieProgress) ? (progress.argsData.trackColor || progress.themeStyle.circularTrackColor) : 'none';
const strokeWidth: number = (progress.enablePieProgress) ? 0 :
(progress.trackThickness || progress.themeStyle.circularTrackThickness);
const circularPath: string = getPathArc(
this.centerX, this.centerY, radius, startAngle, endAngle, progress.enableRtl, progress.enablePieProgress);
this.isRange = (this.progress.rangeColors[0].color !== '' || this.progress.rangeColors[0].start !== null ||
this.progress.rangeColors[0].end !== null);
const option: PathOption = new PathOption(
progress.element.id + '_Circulartrack', fill, strokeWidth, stroke, progress.themeStyle.trackOpacity, '0', circularPath
);
const circularTrack: Element = progress.renderer.drawPath(option);
progress.trackWidth = (<SVGPathElement>circularTrack).getTotalLength();
if (progress.segmentCount > 1 && !progress.enableProgressSegments && !progress.enablePieProgress && !this.isRange) {
progress.segmentSize = progress.calculateSegmentSize(progress.trackWidth, strokeWidth);
circularTrack.setAttribute('stroke-dasharray', progress.segmentSize);
}
if (progress.cornerRadius === 'Round' && !progress.enablePieProgress && !this.isRange) {
circularTrack.setAttribute('stroke-linecap', 'round');
}
circularTrackGroup.appendChild(circularTrack);
progress.svgObject.appendChild(circularTrackGroup);
}
/**
* Renders circular progress to update previous progress.
*
* @param {number} previousEnd - The previous end value of the progress.
* @param {number} previousTotalEnd - The previous total end value of the progress.
* @param {boolean} refresh - Indicates whether to refresh the progress.
* @returns {void}
*/
public renderCircularProgress(previousEnd?: number, previousTotalEnd?: number, refresh?: boolean): void {
const progress: ProgressBar = this.progress;
const startAngle: number = progress.startAngle;
let endAngle: number; let totalAngle: number;
let radius: number; let previousPath: string;
let progressTotalAngle: number;
let progressEnd: number; let circularProgress: Element;
let linearClipPath: Element;
let circularProgressGroup: Element;
let segmentWidth: number;
if (!refresh) {
circularProgressGroup = progress.renderer.createGroup({ 'id': progress.element.id + '_CircularProgressGroup' });
} else {
circularProgressGroup = getElement(progress.element.id + '_CircularProgressGroup');
}
radius = stringToNumber(progress.innerRadius, this.availableSize);
radius = (radius === null) ? 0 : radius;
progress.previousTotalEnd = progressEnd = progress.calculateProgressRange(progress.argsData.value > progress.maximum ?
progress.maximum : progress.argsData.value);
const progressEndAngle: number = (progress.startAngle + ((progress.enableRtl) ? -progressEnd : progressEnd)) % 360;
progress.previousEndAngle = endAngle = ((progress.isIndeterminate && !progress.enableProgressSegments) ? (progress.startAngle + (
(progress.enableRtl) ? -progress.totalAngle : progress.totalAngle)) % 360 : progressEndAngle
);
progressTotalAngle = (progressEnd - progress.startAngle) % 360;
progressTotalAngle = (progressTotalAngle <= 0 ? (360 + progressTotalAngle) : progressTotalAngle);
progressTotalAngle -= (progressTotalAngle === 360) ? 0.01 : 0;
const circularPath: string = getPathArc(
this.centerX, this.centerY, radius, startAngle, endAngle, progress.enableRtl, progress.enablePieProgress);
const stroke: string = this.checkingCircularProgressColor();
const fill: string = (progress.enablePieProgress) ? stroke : 'none';
const thickness: number = (progress.progressThickness || progress.themeStyle.circularProgressThickness);
const strokeWidth: number = (progress.enablePieProgress) ? 0 : thickness;
const option: PathOption = new PathOption(
progress.element.id + '_Circularprogress', fill, strokeWidth, stroke, progress.themeStyle.progressOpacity, '0', circularPath
);
progress.progressWidth = (<SVGPathElement>progress.renderer.drawPath(option)).getTotalLength();
progress.segmentSize = this.validateSegmentSize(progress, thickness);
this.endPosition = degreeToLocation(this.centerX, this.centerY, radius, endAngle);
if (progress.secondaryProgress !== null && !progress.isIndeterminate) {
this.renderCircularBuffer(progress, radius, progressTotalAngle);
}
if (progress.argsData.value !== null) {
if (progress.segmentColor.length !== 0 && !progress.isIndeterminate && !progress.enablePieProgress) {
totalAngle = (!progress.enableProgressSegments) ? progress.totalAngle : progressTotalAngle;
segmentWidth = (!progress.enableProgressSegments) ? progress.trackWidth : progress.progressWidth;
circularProgress = this.segment.createCircularSegment(
progress, '_CircularProgressSegment', this.centerX, this.centerY, radius, progress.argsData.value,
progress.themeStyle.progressOpacity, thickness, totalAngle, segmentWidth
);
} else if (this.isRange && !progress.isIndeterminate) {
circularProgress = this.segment.createCircularRange(this.centerX, this.centerY, radius, progress);
} else {
if (!refresh) {
circularProgress = progress.renderer.drawPath(option);
} else {
circularProgress = getElement(progress.element.id + '_Circularprogress');
previousPath = circularProgress.getAttribute('d');
circularProgress.setAttribute('stroke', stroke);
circularProgress.setAttribute('d', circularPath);
}
if (progress.segmentCount > 1 && !progress.enablePieProgress) {
circularProgress.setAttribute('stroke-dasharray', progress.segmentSize);
}
if (progress.cornerRadius === 'Round' && startAngle !== endAngle) {
circularProgress.setAttribute('stroke-linecap', 'round');
}
}
circularProgressGroup.appendChild(circularProgress);
if (progress.isActive && !progress.isIndeterminate && !progress.enablePieProgress) {
this.renderActiveState(
circularProgressGroup, radius, strokeWidth, circularPath, progressEndAngle, progressEnd, refresh
);
}
if (((progress.animation.enable && animationMode !== 'Disable') || animationMode === 'Enable') || progress.isIndeterminate) {
this.delay = (progress.secondaryProgress !== null) ? 300 : progress.animation.delay;
linearClipPath = progress.createClipPath(progress.clipPath, null, refresh ? previousPath : '', refresh);
circularProgressGroup.appendChild(progress.clipPath);
if (((progress.animation.enable && animationMode !== 'Disable') || animationMode === 'Enable') && !progress.isIndeterminate && !progress.isActive) {
(<SVGPathElement>circularProgress).style.clipPath = 'url(#' + progress.element.id + '_clippath)';
this.animation.doCircularAnimation(
this.centerX, this.centerY, radius, progressEndAngle, progressEnd, linearClipPath, progress,
thickness, this.delay, refresh ? previousEnd : null, refresh ? previousTotalEnd : null
);
}
if (progress.isIndeterminate) {
if (progress.enableProgressSegments) {
linearClipPath.setAttribute('d', getPathArc(
this.centerX, this.centerY, radius + (thickness / 2), progress.startAngle,
this.trackEndAngle, progress.enableRtl, true)
);
}
circularProgress.setAttribute('style', 'clip-path:url(#' + progress.element.id + '_clippath)');
this.animation.doCircularIndeterminate(
(!progress.enableProgressSegments) ? linearClipPath : circularProgress, progress, startAngle,
progressEndAngle, this.centerX, this.centerY, radius, thickness, linearClipPath
);
}
}
progress.svgObject.appendChild(circularProgressGroup);
}
}
/**
* Renders circular buffer for the progress bar.
*
* @param {ProgressBar} progress - The progress bar control.
* @param {number} radius - The radius of the circular buffer.
* @param {number} progressTotalAngle - The total angle covered by the progress.
* @returns {void}
* @private
*/
private renderCircularBuffer(progress: ProgressBar, radius: number, progressTotalAngle: number): void {
let bufferClipPath: Element;
let circularBuffer: Element;
let segmentWidth: number;
let totalAngle: number;
const circularBufferGroup: Element = progress.renderer.createGroup({ 'id': progress.element.id + '_ CircularBufferGroup' });
const bufferEnd: number = progress.calculateProgressRange(progress.secondaryProgress > progress.maximum ?
progress.maximum : progress.secondaryProgress);
const endAngle: number = (progress.startAngle + ((progress.enableRtl) ? -bufferEnd : bufferEnd)) % 360;
const circularPath: string = getPathArc(
this.centerX, this.centerY, radius, progress.startAngle, endAngle, progress.enableRtl, progress.enablePieProgress
);
this.bufferEndPosition = degreeToLocation(this.centerX, this.centerY, radius, endAngle);
const stroke: string = progress.secondaryProgressColor ? progress.secondaryProgressColor : progress.themeStyle.bufferColor ||
this.checkingCircularProgressColor();
const fill: string = (progress.enablePieProgress) ? stroke : 'none';
const strokeWidth: number = (progress.enablePieProgress) ? 0 :
(progress.secondaryProgressThickness ? progress.secondaryProgressThickness :
(progress.progressThickness || progress.themeStyle.circularProgressThickness));
const option: PathOption = new PathOption(
progress.element.id + '_Circularbuffer', fill, strokeWidth, stroke,
progress.themeStyle.bufferOpacity, '0', circularPath
);
if (progress.segmentColor.length !== 0 && !progress.isIndeterminate && !progress.enablePieProgress && !this.isRange) {
totalAngle = (!progress.enableProgressSegments) ? progress.totalAngle : progressTotalAngle;
segmentWidth = (!progress.enableProgressSegments) ? progress.trackWidth : progress.progressWidth;
circularBuffer = this.segment.createCircularSegment(
progress, '_CircularBufferSegment', this.centerX, this.centerY, radius,
progress.secondaryProgress > progress.maximum ? progress.maximum : progress.secondaryProgress,
progress.themeStyle.bufferOpacity, strokeWidth, totalAngle, segmentWidth
);
} else {
circularBuffer = progress.renderer.drawPath(option);
if (progress.segmentCount > 1 && !progress.enablePieProgress && !this.isRange) {
circularBuffer.setAttribute('stroke-dasharray', progress.segmentSize);
}
if (progress.cornerRadius === 'Round' && !this.isRange) {
circularBuffer.setAttribute('stroke-linecap', 'round');
}
}
circularBufferGroup.appendChild(circularBuffer);
if (((progress.animation.enable && animationMode !== 'Disable') || animationMode === 'Enable') && !progress.isActive) {
bufferClipPath = progress.createClipPath(progress.bufferClipPath, null, '', false);
circularBufferGroup.appendChild(progress.bufferClipPath);
circularBuffer.setAttribute('style', 'clip-path:url(#' + progress.element.id + '_clippathBuffer)');
this.animation.doCircularAnimation(
this.centerX, this.centerY, radius, endAngle, bufferEnd, bufferClipPath, progress,
(progress.progressThickness || progress.themeStyle.circularProgressThickness), progress.animation.delay
);
}
progress.svgObject.appendChild(circularBufferGroup);
}
/**
* To render the circular Label.
*
* @param {boolean} isProgressRefresh - Indicates whether progress should be refreshed. Defaults to false.
* @returns {void}
*/
public renderCircularLabel(isProgressRefresh: boolean = false): void {
let end: number;
let circularLabel: Element;
let centerY: number;
let textSize: Size;
let option: TextOption;
const percentage: number = 100;
const progress: ProgressBar = this.progress;
const labelText: string = progress.labelStyle.text;
const circularLabelGroup: Element = progress.renderer.createGroup({ 'id': progress.element.id + '_CircularLabelGroup' });
if (document.getElementById(circularLabelGroup.id)) {
document.getElementById(circularLabelGroup.id).remove();
}
const labelValue: number = ((progress.value - progress.minimum) / (progress.maximum - progress.minimum)) * percentage;
const circularValue: number = (progress.value < progress.minimum) ? 0 : +labelValue.toFixed(2);
const argsData: ITextRenderEventArgs = {
cancel: false, text: labelText ? labelText : String(circularValue) + '%', color: progress.labelStyle.color || progress.themeStyle.circularLabelFont.color
};
progress.trigger('textRender', argsData);
if (!argsData.cancel) {
textSize = measureText(argsData.text, progress.labelStyle, progress.themeStyle.circularLabelFont);
centerY = this.centerY + (textSize.height / 2);
option = new TextOption(
progress.element.id + '_circularLabel', progress.labelStyle.size || progress.themeStyle.circularLabelFont.size,
progress.labelStyle.fontStyle || progress.themeStyle.circularLabelFont.fontStyle,
progress.labelStyle.fontFamily || progress.themeStyle.circularLabelFont.fontFamily, progress.labelStyle.fontWeight ||
progress.themeStyle.circularLabelFont.fontWeight,
'middle', argsData.color, this.centerX, centerY, progress.progressRect.width,
progress.progressRect.height
);
circularLabel = progress.renderer.createText(option, argsData.text);
circularLabelGroup.appendChild(circularLabel);
if (((progress.animation.enable && animationMode !== 'Disable') || animationMode === 'Enable') && !progress.isIndeterminate) {
end = ((progress.value - progress.minimum) / (progress.maximum - progress.minimum)) * progress.totalAngle;
end = (progress.value < progress.minimum) ? 0 : end;
this.animation.doLabelAnimation(circularLabel, isProgressRefresh ? progress.previousWidth :
progress.startAngle, end, progress, this.delay);
}
progress.svgObject.appendChild(circularLabelGroup);
progress.previousWidth = end;
}
}
/**
* Renders the active state of the circular progress.
*
* @param {Element} progressGroup - The group element containing the progress.
* @param {number} radius - The radius of the circular progress.
* @param {number} strokeWidth - The width of the progress stroke.
* @param {string} circularPath - The path representing the circular progress.
* @param {number} endAngle - The angle at which the progress ends.
* @param {number} totalEnd - The total end value of the progress.
* @param {boolean} refresh - Indicates whether the progress should be refreshed.
* @returns {void}
* @private
*/
private renderActiveState(
progressGroup: Element, radius: number, strokeWidth: number, circularPath: string,
endAngle: number, totalEnd: number, refresh: boolean
): void {
let circularActive: Element;
let option: PathOption;
const progress: ProgressBar = this.progress;
const thickness: number = strokeWidth + 1;
if (!refresh) {
option = new PathOption(
progress.element.id + '_CircularActiveProgress', 'none', thickness, '#ffffff', 0.5, '0', circularPath
);
circularActive = progress.renderer.drawPath(option);
} else {
circularActive = getElement(progress.element.id + '_CircularActiveProgress');
circularActive.setAttribute('d', circularPath);
}
if (progress.segmentCount > 1) {
circularActive.setAttribute('stroke-dasharray', progress.segmentSize);
}
if (progress.cornerRadius === 'Round') {
circularActive.setAttribute('stroke-linecap', 'round');
}
const activeClip: Element = progress.createClipPath(progress.clipPath, null, '', refresh);
circularActive.setAttribute('style', 'clip-path:url(#' + progress.element.id + '_clippath)');
progressGroup.appendChild(circularActive);
progressGroup.appendChild(progress.clipPath);
this.animation.doCircularAnimation(
this.centerX, this.centerY, radius, endAngle, totalEnd, activeClip, progress,
thickness, 0, null, null, circularActive
);
}
/**
* Validates the segment size for the progress bar.
*
* @param {ProgressBar} progress - The progress bar control.
* @param {number} thickness - The thickness of the progress segments.
* @returns {string} - The validated segment size.
* @private
*/
private validateSegmentSize(progress: ProgressBar, thickness: number): string {
let validSegment: string;
let progressSegment: number;
const rDiff: number = parseInt(progress.radius, 10) - parseInt(progress.innerRadius, 10);
if (rDiff !== 0 && !progress.enableProgressSegments) {
progressSegment = progress.trackWidth + (
(rDiff < 0) ? (progress.trackWidth * Math.abs(rDiff)) / parseInt(progress.radius, 10) :
-(progress.trackWidth * Math.abs(rDiff)) / parseInt(progress.radius, 10)
);
validSegment = progress.calculateSegmentSize(progressSegment, thickness);
} else if (progress.enableProgressSegments) {
validSegment = progress.calculateSegmentSize(progress.progressWidth, thickness);
} else {
validSegment = progress.segmentSize;
}
return validSegment;
}
/**
* Checks and retrieves the color for the circular progress.
*
* @returns {string} - The color for the circular progress.
* @private
*/
private checkingCircularProgressColor(): string {
let circularColor: string;
const progress: ProgressBar = this.progress;
const role: ModeType = progress.role;
switch (role) {
case 'Success':
circularColor = progress.themeStyle.success;
break;
case 'Info':
circularColor = progress.themeStyle.info;
break;
case 'Warning':
circularColor = progress.themeStyle.warning;
break;
case 'Danger':
circularColor = progress.themeStyle.danger;
break;
default:
circularColor = (progress.argsData.progressColor || progress.themeStyle.circularProgressColor);
}
return circularColor;
}
}