|
| 1 | +import "./circular-progress-bar.less"; |
| 2 | + |
| 3 | +const classesPrefix = "circular-progress-bar"; |
| 4 | + |
| 5 | +/** |
| 6 | + * Create a new html element |
| 7 | + * @param {String} classes - Some css classes |
| 8 | + * @param {HTMLElement} [parent] - Parent to append the element |
| 9 | + * @return {HTMLDivElement} |
| 10 | + */ |
| 11 | +const wrap = (classes, parent) => { |
| 12 | + const element = document.createElement("div"); |
| 13 | + element.className = classesPrefix + (classes ? `-${classes}` : ""); |
| 14 | + if (parent) { |
| 15 | + parent.appendChild(element); |
| 16 | + } |
| 17 | + return element; |
| 18 | +}; |
| 19 | + |
| 20 | +const getLooped = (array, index) => array[index % array.length]; |
| 21 | + |
| 22 | +const toDeg = percent => percent * (360 / 100); |
| 23 | + |
| 24 | +/** |
| 25 | + * Class for CircularProgressBar's Bar |
| 26 | + * @class |
| 27 | + */ |
| 28 | +class Bar { |
| 29 | + /** |
| 30 | + * Bar constructor |
| 31 | + */ |
| 32 | + constructor () { |
| 33 | + this.html = wrap("bar"); |
| 34 | + |
| 35 | + this._nodes = (new Array(2)).fill().map(() => { |
| 36 | + const clip = wrap("clip", this.html); |
| 37 | + return { |
| 38 | + clip, |
| 39 | + part: wrap("part", clip), |
| 40 | + }; |
| 41 | + }); |
| 42 | + |
| 43 | + /** |
| 44 | + * @private |
| 45 | + */ |
| 46 | + this._value = 0; |
| 47 | + } |
| 48 | + |
| 49 | + /** |
| 50 | + * Returns this bar's value |
| 51 | + * @return {Number} |
| 52 | + */ |
| 53 | + get value () { |
| 54 | + return this._value; |
| 55 | + } |
| 56 | + |
| 57 | + /** |
| 58 | + * Change the bar value and look |
| 59 | + * @param {Number} value - New value in % |
| 60 | + * @param {Number} time - Time in ms to change |
| 61 | + * @param {String} color - Color to use |
| 62 | + * @param {Number} [offset=0] - Starting position in % |
| 63 | + */ |
| 64 | + update (value, time, color, offset = 0) { |
| 65 | + const rotate = `rotate3d(0, 0, 1, ${toDeg(value / 2) - 179}deg)`; |
| 66 | + this._nodes.forEach((node) => { |
| 67 | + node.clip.style.transitionDuration = `${time}ms`; |
| 68 | + node.part.style.transitionDuration = `${time}ms`; |
| 69 | + node.part.style.transform = rotate; |
| 70 | + node.part.style.backgroundColor = color; |
| 71 | + }); |
| 72 | + |
| 73 | + this._nodes[0].clip.style.transform = `rotate3d(0, 0, 1, ${toDeg(offset)}deg)`; |
| 74 | + this._nodes[1].clip.style.transform = `rotate3d(0, 0, 1, ${toDeg((value / 2) + offset)}deg)`; |
| 75 | + this._value = value; |
| 76 | + } |
| 77 | + |
| 78 | + /** |
| 79 | + * Delete the bar from the DOM |
| 80 | + */ |
| 81 | + remove () { |
| 82 | + this.html.remove(); |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +/** |
| 87 | + * Class for CircularProgressBar |
| 88 | + * @class |
| 89 | + */ |
| 90 | +export default class CircularProgressBar { |
| 91 | + /** |
| 92 | + * CircularProgressBar constructor |
| 93 | + * @param {Number|Array<Number>} [value=0] - Starting value or a set of values |
| 94 | + * @param {CPBOptions} [options] - Some options |
| 95 | + */ |
| 96 | + constructor (value = 0, options) { |
| 97 | + this.options = Object.assign(CircularProgressBar.defaultOptions, options); |
| 98 | + |
| 99 | + this.html = wrap(); |
| 100 | + const size = `${this.options.size}px`; |
| 101 | + this.html.style.width = size; |
| 102 | + this.html.style.height = size; |
| 103 | + this.html.style.backgroundColor = this.options.background; |
| 104 | + |
| 105 | + this.wrapper = wrap("wrapper", this.html); |
| 106 | + |
| 107 | + this.valueNode = wrap("value", this.html); |
| 108 | + this.valueNode.style.backgroundColor = this.options.valueBackground; |
| 109 | + const valueSize = `${this.options.size - (this.options.barsWidth * 2)}px`; |
| 110 | + this.valueNode.style.width = valueSize; |
| 111 | + this.valueNode.style.height = valueSize; |
| 112 | + this.valueNode.style.lineHeight = valueSize; |
| 113 | + const valueOffset = `${this.options.barsWidth}px`; |
| 114 | + this.valueNode.style.top = valueOffset; |
| 115 | + this.valueNode.style.left = valueOffset; |
| 116 | + this.valueTextNode = wrap("text", this.valueNode); |
| 117 | + this.valueTextNode.style.fontSize = `${this.options.size / 5}px`; |
| 118 | + |
| 119 | + /** |
| 120 | + * @type {Array<Bar>} |
| 121 | + * @private |
| 122 | + */ |
| 123 | + this._bars = []; |
| 124 | + |
| 125 | + this.values = Array.isArray(value) ? value : [value]; |
| 126 | + } |
| 127 | + |
| 128 | + /** |
| 129 | + * Change value with only one bar |
| 130 | + * @param {Number} value - Any value |
| 131 | + */ |
| 132 | + set value (value) { |
| 133 | + this.values = [value]; |
| 134 | + } |
| 135 | + |
| 136 | + /** |
| 137 | + * Change values with multiple bars |
| 138 | + * @param {Array<Number>} values - Any set of value |
| 139 | + */ |
| 140 | + set values (values) { |
| 141 | + if (this.options.showValue) { |
| 142 | + this.valueNode.style.visibility = ""; |
| 143 | + const sum = values.reduce((acc, value) => acc + value, 0); |
| 144 | + const used = (values.length === 1 ? values[0] : sum); |
| 145 | + const displayed = this.options.valueUnit === "%" ? (used / this.options.max) * 100 : used; |
| 146 | + this.valueTextNode.textContent = displayed.toFixed(this.options.valueDecimals) + this.options.valueUnit; |
| 147 | + } |
| 148 | + else { |
| 149 | + this.valueNode.style.visibility = "hidden"; |
| 150 | + } |
| 151 | + |
| 152 | + let offset = 0; |
| 153 | + let lastIndex = 0; |
| 154 | + values.forEach((value, index) => { |
| 155 | + let bar = this._bars[index]; |
| 156 | + if (!bar) { |
| 157 | + bar = new Bar(this.options.colors[index]); |
| 158 | + this._bars.push(bar); |
| 159 | + this.wrapper.appendChild(bar.html); |
| 160 | + } |
| 161 | + const percentage = (value / this.options.max) * 100; |
| 162 | + bar.update(percentage, this.options.transitionTime, getLooped(this.options.colors, index), offset); |
| 163 | + offset += percentage; |
| 164 | + lastIndex = index; |
| 165 | + }); |
| 166 | + this._bars.splice(lastIndex + 1, this._bars.length).forEach(bar => bar.remove()); |
| 167 | + } |
| 168 | + |
| 169 | + /** |
| 170 | + * Returns current value with only one bar |
| 171 | + * @return {Number} |
| 172 | + */ |
| 173 | + get value () { |
| 174 | + return this._bars[0].value; |
| 175 | + } |
| 176 | + |
| 177 | + /** |
| 178 | + * Returns current value of all bars |
| 179 | + * @return {Array<Number>} |
| 180 | + */ |
| 181 | + get values () { |
| 182 | + return this._bars.map(bar => bar.value); |
| 183 | + } |
| 184 | + |
| 185 | + /** |
| 186 | + * Append the component to another element |
| 187 | + * @param {HTMLElement} parent - Another DOM element |
| 188 | + */ |
| 189 | + appendTo (parent) { |
| 190 | + parent.appendChild(this.html); |
| 191 | + } |
| 192 | + |
| 193 | + |
| 194 | + /** |
| 195 | + * @typedef {Object} CPBOptions |
| 196 | + * @prop {Number} [size=150] - Component diameter in pixels |
| 197 | + * @prop {Number} [barsWidth=10] - Width of bars |
| 198 | + * @prop {Number} [max=100] - Value for a full 360° rotation |
| 199 | + * @prop {Boolean} [showValue=true] - Whether or not to display current value inside (if multiple value, sum is displayed) |
| 200 | + * @prop {Number} [valueDecimals=0] - Number of decimals to display |
| 201 | + * @prop {String} [valueUnit="%"] - Unit used for display (if set to "%", value is calculated over max) |
| 202 | + * @prop {String} [valueBackground="#333"] - Background color for value |
| 203 | + * @prop {Array<String>} [colors] - Set of colors to use for bars |
| 204 | + * @prop {String} [background="#666"] - Background color where there's no bar |
| 205 | + * @prop {Number} [transitionTime=500] - Transition duration |
| 206 | + */ |
| 207 | + /** |
| 208 | + * Returns the default options of the component |
| 209 | + * @return {CPBOptions} |
| 210 | + */ |
| 211 | + static get defaultOptions () { |
| 212 | + return { |
| 213 | + size: 150, |
| 214 | + barsWidth: 10, |
| 215 | + max: 100, |
| 216 | + showValue: true, |
| 217 | + valueDecimals: 0, |
| 218 | + valueUnit: "%", |
| 219 | + valueBackground: "#333", |
| 220 | + colors: ["#ffa114", "#4714ff", "#ff14c8", "#c8ff14", "#ff203a", "#3aff20", "#204dff"], |
| 221 | + background: "rgba(0, 0, 0, .3)", |
| 222 | + transitionTime: 500, |
| 223 | + }; |
| 224 | + } |
| 225 | +} |
0 commit comments