diff --git a/package.json b/package.json index d4dfbdc..a70e88a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svg-gauge", - "version": "1.0.6", + "version": "1.1.0", "description": "Minimal SVG animated gauge with zero dependencies", "main": "index.js", "directories": { @@ -30,4 +30,4 @@ "gulp-concat": "~2.6.1", "gulp-uglify": "~3.0.1" } -} +} \ No newline at end of file diff --git a/src/gauge.js b/src/gauge.js index 5722945..b2048e1 100644 --- a/src/gauge.js +++ b/src/gauge.js @@ -1,27 +1,27 @@ /* global window, define, module */ -(function(global, factory) { +(function (global, factory) { var Gauge = factory(global); - if(typeof define === "function" && define.amd) { + if (typeof define === "function" && define.amd) { // AMD support - define(function() {return Gauge;}); - }else if(typeof module === "object" && module.exports) { + define(function () { return Gauge; }); + } else if (typeof module === "object" && module.exports) { // CommonJS support module.exports = Gauge; - }else { + } else { // We are probably running in the browser global.Gauge = Gauge; } -})(typeof window === "undefined" ? this : window, function(global, undefined) { +})(typeof window === "undefined" ? this : window, function (global, undefined) { var document = global.document, slice = Array.prototype.slice, requestAnimationFrame = (global.requestAnimationFrame || - global.mozRequestAnimationFrame || - global.webkitRequestAnimationFrame || - global.msRequestAnimationFrame || - function(cb) { - return setTimeout(cb, 1000 / 60); - }); + global.mozRequestAnimationFrame || + global.webkitRequestAnimationFrame || + global.msRequestAnimationFrame || + function (cb) { + return setTimeout(cb, 1000 / 60); + }); // EXPERIMENTAL!! /** @@ -37,26 +37,26 @@ */ function Animation(options) { var duration = options.duration, - currentIteration = 1, - iterations = 60 * duration, - start = options.start || 0, - end = options.end, - change = end - start, - step = options.step, - easing = options.easing || function easeInOutCubic(pos) { - // https://github.com/danro/easing-js/blob/master/easing.js - if ((pos/=0.5) < 1) return 0.5*Math.pow(pos,3); - return 0.5 * (Math.pow((pos-2),3) + 2); - }; + currentIteration = 1, + iterations = 60 * duration, + start = options.start || 0, + end = options.end, + change = end - start, + step = options.step, + easing = options.easing || function easeInOutCubic(pos) { + // https://github.com/danro/easing-js/blob/master/easing.js + if ((pos /= 0.5) < 1) return 0.5 * Math.pow(pos, 3); + return 0.5 * (Math.pow((pos - 2), 3) + 2); + }; function animate() { - var progress = currentIteration / iterations, - value = change * easing(progress) + start; + var progress = currentIteration / iterations, + value = change * easing(progress) + start; // console.log(progress + ", " + value); step(value, currentIteration); currentIteration += 1; - if(progress < 1) { + if (progress < 1) { requestAnimationFrame(animate); } } @@ -66,7 +66,7 @@ - var Gauge = (function() { + var Gauge = (function () { var SVG_NS = "http://www.w3.org/2000/svg"; var GaugeDefaults = { @@ -78,6 +78,7 @@ dialRadius: 40, dialStartAngle: 135, dialEndAngle: 45, + dialAnglePadding: 0, value: 0, max: 100, min: 0, @@ -87,14 +88,14 @@ gaugeClass: "gauge", showValue: true, gaugeColor: null, - label: function(val) {return Math.round(val);} + label: function (val) { return Math.round(val); } }; function shallowCopy(/* source, ...targets*/) { var target = arguments[0], sources = slice.call(arguments, 1); - sources.forEach(function(s) { - for(var k in s) { - if(s.hasOwnProperty(k)) { + sources.forEach(function (s) { + for (var k in s) { + if (s.hasOwnProperty(k)) { target[k] = s[k]; } } @@ -111,12 +112,12 @@ */ function svg(name, attrs, children) { var elem = document.createElementNS(SVG_NS, name); - for(var attrName in attrs) { + for (var attrName in attrs) { elem.setAttribute(attrName, attrs[attrName]); } - if(children) { - children.forEach(function(c) { + if (children) { + children.forEach(function (c) { elem.appendChild(c); }); } @@ -133,8 +134,8 @@ function normalize(value, min, limit) { var val = Number(value); - if(val > limit) return limit; - if(val < min) return min; + if (val > limit) return limit; + if (val < min) return min; return val; } @@ -166,10 +167,10 @@ // REMEMBER!! angle=0 starts on X axis and then increases clockwise function getDialCoords(radius, startAngle, endAngle) { var cx = GaugeDefaults.centerX, - cy = GaugeDefaults.centerY; + cy = GaugeDefaults.centerY; return { end: getCartesian(cx, cy, radius, endAngle), - start: getCartesian(cx, cy, radius, startAngle) + start: getCartesian(cx, cy, radius, startAngle) }; } @@ -191,48 +192,69 @@ return function Gauge(elem, opts) { opts = shallowCopy({}, defaultOptions, opts); var gaugeContainer = elem, - limit = opts.max, - min = opts.min, - value = normalize(opts.value, min, limit), - radius = opts.dialRadius, - displayValue = opts.showValue, - startAngle = opts.dialStartAngle, - endAngle = opts.dialEndAngle, - valueDialClass = opts.valueDialClass, - valueTextClass = opts.valueClass, - valueLabelClass = opts.valueLabelClass, - dialClass = opts.dialClass, - gaugeClass = opts.gaugeClass, - gaugeColor = opts.color, - gaugeValueElem, - gaugeValuePath, - label = opts.label, - viewBox = opts.viewBox, - instance; + limit = opts.max, + min = opts.min, + value = normalize(opts.value, min, limit), + radius = opts.dialRadius, + displayValue = opts.showValue, + startAngle = opts.dialStartAngle, + endAngle = opts.dialEndAngle, + dialAnglePadding = opts.dialAnglePadding, + valueDialClass = opts.valueDialClass, + valueTextClass = opts.valueClass, + valueLabelClass = opts.valueLabelClass, + valueToBackground = opts.valueToBackground, + showValueCursor = opts.showValueCursor, + dialClass = opts.dialClass, + gaugeClass = opts.gaugeClass, + gaugeColor = opts.color, + gaugeValueElem, + gaugeValuePath, + gaugeValueCursorLinePath, + gaugeValueCursorTrianglePath, + label = opts.label, + viewBox = opts.viewBox, + instance; - if(startAngle < endAngle) { + if (startAngle < endAngle) { console.log("WARN! startAngle < endAngle, Swapping"); var tmp = startAngle; startAngle = endAngle; endAngle = tmp; } + function getValueCursorParams(radius, startAngle, endAngle, elem) { + var valueStyles = getComputedStyle((elem.getElementsByClassName("value") || [])[0]); + var strokeWidth = parseInt(valueStyles.strokeWidth.replace("px", "")); + + var x1 = GaugeDefaults.centerX - radius - (strokeWidth / 2) + 2; + var y1 = GaugeDefaults.centerY; + var angle = 180 + startAngle + endAngle; + return { + x1, + y1, + x2: GaugeDefaults.centerX - radius + (strokeWidth / 2), + y2: y1, + transform: `rotate(${angle} ${GaugeDefaults.centerX} ${GaugeDefaults.centerY})`, + } + } + function pathString(radius, startAngle, endAngle, largeArc) { var coords = getDialCoords(radius, startAngle, endAngle), - start = coords.start, - end = coords.end, - largeArcFlag = typeof(largeArc) === "undefined" ? 1 : largeArc; + start = coords.start, + end = coords.end, + largeArcFlag = typeof (largeArc) === "undefined" ? 1 : largeArc; return [ - "M", start.x, start.y, + "M", start.x, start.y, "A", radius, radius, 0, largeArcFlag, 1, end.x, end.y ].join(" "); } function initializeGauge(elem) { gaugeValueElem = svg("text", { - x: 50, - y: 50, + x: GaugeDefaults.centerX, + y: GaugeDefaults.centerY, fill: "#999", "class": valueTextClass, "font-size": "100%", @@ -244,46 +266,112 @@ }); gaugeValuePath = svg("path", { - "class": valueDialClass, + class: valueDialClass, fill: "none", stroke: "#666", "stroke-width": 2.5, d: pathString(radius, startAngle, startAngle) // value of 0 }); + if (showValueCursor) { + gaugeValueCursorLinePath = svg("line", { + fill: "none", + stroke: "black", + class: "cursor-line", + opacity: "0", + "stroke-dasharray": "2", + "stroke-dashoffset": "0.5", + "stroke-width": 0.5, + }); + + gaugeValueCursorTrianglePath = svg("path", { + fill: "black", + class: "cursor-triangle", + stroke: "black", + opacity: "0", + "stroke-width": 0.5 + }); + + var lineOpacityTransition = "opacity 0.5s"; + var lineOpacityTransitionDelay = "0.8s"; + + gaugeValueCursorLinePath.style["-webkit-transition"] = lineOpacityTransition; + gaugeValueCursorLinePath.style["-moz-transition"] = lineOpacityTransition; + gaugeValueCursorLinePath.style["transition"] = lineOpacityTransition; + gaugeValueCursorLinePath.style["transition-delay"] = lineOpacityTransitionDelay; + + var triangleOpacityTransition = "opacity 0.5s"; + var triangleOpacityTransitionDelay = "1.6s"; + + gaugeValueCursorTrianglePath.style["-webkit-transition"] = triangleOpacityTransition; + gaugeValueCursorTrianglePath.style["-moz-transition"] = triangleOpacityTransition; + gaugeValueCursorTrianglePath.style["transition"] = triangleOpacityTransition; + gaugeValueCursorTrianglePath.style["transition-delay"] = triangleOpacityTransitionDelay; + } + var angle = getAngle(100, 360 - Math.abs(startAngle - endAngle)); var flag = angle <= 180 ? 0 : 1; - var gaugeElement = svg("svg", {"viewBox": viewBox || "0 0 100 100", "class": gaugeClass}, [ - svg("path", { - "class": dialClass, - fill: "none", - stroke: "#eee", - "stroke-width": 2, - d: pathString(radius, startAngle, endAngle, flag) - }), + + var gaugeDialPath = svg("path", { + "class": dialClass, + fill: "none", + stroke: "#eee", + "stroke-width": 2, + d: pathString(radius, startAngle + dialAnglePadding, endAngle - dialAnglePadding, flag) + }); + + var gaugeElement = svg("svg", { "viewBox": viewBox || "0 0 100 100", "class": gaugeClass }, [ + !valueToBackground && gaugeDialPath, + gaugeValuePath, svg("g", { "class": "text-container" }, [gaugeValueElem]), - gaugeValuePath - ]); + valueToBackground && gaugeDialPath, + showValueCursor && gaugeValueCursorLinePath, + showValueCursor && gaugeValueCursorTrianglePath + ].filter(Boolean)); elem.appendChild(gaugeElement); } function updateGauge(theValue, frame) { var val = getValueInPercentage(theValue, min, limit), - // angle = getAngle(val, 360 - Math.abs(endAngle - startAngle)), - angle = getAngle(val, 360 - Math.abs(startAngle - endAngle)), - // this is because we are using arc greater than 180deg - flag = angle <= 180 ? 0 : 1; - if(displayValue) { + // angle = getAngle(val, 360 - Math.abs(endAngle - startAngle)), + angle = getAngle(val, 360 - Math.abs(startAngle - endAngle)), + // this is because we are using arc greater than 180deg + flag = angle <= 180 ? 0 : 1; + if (displayValue) { gaugeValueElem.textContent = label.call(opts, theValue); } gaugeValuePath.setAttribute("d", pathString(radius, startAngle, angle + startAngle, flag)); + + if (showValueCursor && theValue === value && 0 < theValue) { + + // Show the cursor line. + var { x1, x2, y1, y2, transform } = getValueCursorParams(radius, startAngle, angle, elem); + gaugeValueCursorLinePath.setAttribute("x1", x1); + gaugeValueCursorLinePath.setAttribute("y1", y1); + gaugeValueCursorLinePath.setAttribute("x2", x2); + gaugeValueCursorLinePath.setAttribute("y2", y2); + gaugeValueCursorLinePath.setAttribute("transform", transform); + gaugeValueCursorLinePath.setAttribute("opacity", "1"); + + // Add margin between the cursor triangle to the line. + x1 -= 1; + + // Show the cursor triangle. + var start = `${x1} ${y1}`; + var left = `${x1 - 1} ${y1 + 1}`; + var right = `${x1 - 1} ${y1 - 1}`; + gaugeValueCursorTrianglePath.setAttribute("d", `M ${start} L ${left} L ${right} Z`) + + gaugeValueCursorTrianglePath.setAttribute("transform", transform); + gaugeValueCursorTrianglePath.setAttribute("opacity", "1"); + } } - function setGaugeColor(value, duration) { - var c = gaugeColor.call(opts, value), - dur = duration * 1000, - pathTransition = "stroke " + dur + "ms ease"; - // textTransition = "fill " + dur + "ms ease"; + function setGaugeColor(value, duration) { + var c = gaugeColor.call(opts, value); + var dur = duration * 1000; + var pathTransition = "stroke " + dur + "ms ease"; + // textTransition = "fill " + dur + "ms ease"; gaugeValuePath.style.stroke = c; gaugeValuePath.style["-webkit-transition"] = pathTransition; @@ -300,36 +388,37 @@ } instance = { - setMaxValue: function(max) { + setMaxValue: function (max) { limit = max; }, - setValue: function(val) { + setValue: function (val) { value = normalize(val, min, limit); - if(gaugeColor) { + if (gaugeColor) { setGaugeColor(value, 0) } updateGauge(value); }, - setValueAnimated: function(val, duration) { - var oldVal = value; + setValueAnimated: function (val, duration) { + var oldVal = value; value = normalize(val, min, limit); - if(oldVal === value) { + + if (oldVal === value) { return; } - if(gaugeColor) { + if (gaugeColor) { setGaugeColor(value, duration); } Animation({ start: oldVal || 0, end: value, duration: duration || 1, - step: function(val, frame) { + step: function (val, frame) { updateGauge(val, frame); } }); }, - getValue: function() { + getValue: function () { return value; } };