nonlinear-canvas-gauges
Version:
Modification of canvas-gauges. Rendering nonlinear scale.
473 lines (399 loc) • 12.4 kB
JavaScript
/*!
* The MIT License (MIT)
*
* Copyright (c) 2016 Mykhailo Stadnyk <mikhus@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
require("./polyfill");
const SmartCanvas = require("./SmartCanvas");
const Animation = require("./Animation");
const Collection = require("./Collection");
const DomObserver = require("./DomObserver");
const EventEmitter = require("./EventEmitter");
const version = "%VERSION%";
const round = Math.round;
const abs = Math.abs;
let gauges = new Collection();
gauges.version = version;
/**
* Basic abstract BaseGauge class implementing common functionality
* for different type of gauges.
*
* It should not be instantiated directly but must be extended by a final
* gauge implementation.
*
* @abstract
* @example
*
* class MyCoolGauge extends BaseGauge {
*
* // theses methods below MUST be implemented:
*
* constructor(options) {
* // ... do something with options
* super(options);
* // ... implement anything else
* }
*
* draw() {
* // ... some implementation here
* return this;
* }
* }
*/
export default class BaseGauge extends EventEmitter {
/**
* Fired each time gauge is initialized on a page
*
* @event BaseGauge#init
*/
/**
* Fired each time gauge scene is rendered
*
* @event BaseGauge#render
*/
/**
* Fired each time gauge object is destroyed
*
* @event BaseGauge#destroy
*/
/**
* Fired each time before animation is started on the gauge
*
* @event BaseGauge#animationStart
*/
/**
* Fired each time animation scene is complete
*
* @event BaseGauge#animate
* @type {number} percent
* @type {number} value
*/
/**
* Fired each time animation is complete on the gauge
*
* @event BaseGauge#animationEnd
*/
/**
* @event BaseGauge#value
* @type {number} newValue
* @type {number} oldValue
*/
/**
* @constructor
* @abstract
* @param {GenericOptions} options
*/
constructor(options) {
super();
let className = this.constructor.name;
if (className === "BaseGauge") {
throw new TypeError("Attempt to instantiate abstract class!");
}
gauges.push(this);
if (options.listeners) {
Object.keys(options.listeners).forEach(event => {
let handlers =
options.listeners[event] instanceof Array
? options.listeners[event]
: [options.listeners[event]];
handlers.forEach(handler => {
this.on(event, handler);
});
});
}
//noinspection JSUnresolvedVariable
/**
* Gauges version string
*
* @type {string}
*/
this.version = version;
/**
* Gauge type class
*
* @type {BaseGauge} type
*/
this.type = ns[className] || BaseGauge;
/**
* True if gauge has been drawn for the first time, false otherwise.
*
* @type {boolean}
*/
this.initialized = false;
options.minValue = parseFloat(options.minValue);
options.maxValue = parseFloat(options.maxValue);
options.value = parseFloat(options.value) || 0;
if (!options.borders) {
options.borderInnerWidth = options.borderMiddleWidth = options.borderOuterWidth = 0;
}
if (!options.renderTo) {
throw TypeError(
"Canvas element was not specified when creating " +
"the Gauge object!"
);
}
let canvas = options.renderTo.tagName
? options.renderTo
: /* istanbul ignore next: to be tested with e2e tests */
document.getElementById(options.renderTo);
if (!(canvas instanceof HTMLCanvasElement)) {
throw TypeError("Given gauge canvas element is invalid!");
}
options.width = parseFloat(options.width) || 0;
options.height = parseFloat(options.height) || 0;
if (!options.width || !options.height) {
if (!options.width)
options.width = canvas.parentNode
? canvas.parentNode.offsetWidth
: canvas.offsetWidth;
if (!options.height)
options.height = canvas.parentNode
? canvas.parentNode.offsetHeight
: canvas.offsetHeight;
}
/**
* Gauge options
*
* @type {GenericOptions} options
*/
this.options = options || {};
if (this.options.animateOnInit) {
this._value = this.options.value;
this.options.value = this.options.minValue;
}
/**
* @type {SmartCanvas} canvas
*/
this.canvas = new SmartCanvas(canvas, options.width, options.height);
this.canvas.onRedraw = this.draw.bind(this);
/**
* @type {Animation} animation
*/
this.animation = new Animation(
options.animationRule,
options.animationDuration
);
}
/**
* Sets new value for this gauge.
* If gauge is animated by configuration it will trigger a proper animation.
* Upsetting a value triggers gauge redraw.
*
* @param {number} value
*/
set value(value) {
value = BaseGauge.ensureValue(value, this.options.minValue);
let fromValue = this.options.value;
if (value === fromValue) {
return;
}
if (this.options.animation) {
if (this.animation.frame) {
// animation is already in progress -
// forget related old animation value
// @see https://github.com/Mikhus/canvas-gauges/issues/94
this.options.value = this._value;
// if there is no actual value change requested stop it
if (this._value === value) {
this.animation.cancel();
delete this._value;
return;
}
}
/**
* @type {number}
* @access private
*/
if (this._value === undefined) {
this._value = value;
}
this.emit("animationStart");
this.animation.animate(
percent => {
let newValue = fromValue + (value - fromValue) * percent;
this.options.animatedValue &&
this.emit("value", newValue, this.value);
this.options.value = newValue;
this.draw();
this.emit("animate", percent, this.options.value);
},
() => {
if (this._value !== undefined) {
this.emit("value", this._value, this.value);
this.options.value = this._value;
delete this._value;
}
this.draw();
this.emit("animationEnd");
}
);
} else {
this.emit("value", value, this.value);
this.options.value = value;
this.draw();
}
}
/**
* Returns current value of the gauge
*
* @return {number}
*/
get value() {
return typeof this._value === "undefined"
? this.options.value
: this._value;
}
/**
* Updates gauge options
*
* @param {*} options
* @return {BaseGauge}
* @access protected
*/
static configure(options) {
return options;
}
/**
* Updates gauge configuration options at runtime and redraws the gauge
*
* @param {RadialGaugeOptions} options
* @returns {BaseGauge}
*/
update(options) {
Object.assign(this.options, this.type.configure(options || {}));
this.canvas.width = this.options.width;
this.canvas.height = this.options.height;
this.animation.rule = this.options.animationRule;
this.animation.duration = this.options.animationDuration;
this.canvas.redraw();
return this;
}
/**
* Performs destruction of this object properly
*/
destroy() {
let index = gauges.indexOf(this);
/* istanbul ignore else */
if (~index) {
//noinspection JSUnresolvedFunction
gauges.splice(index, 1);
}
this.canvas.destroy();
this.canvas = null;
this.animation.destroy();
this.animation = null;
this.emit("destroy");
}
/**
* Returns gauges version string
*
* @return {string}
*/
static get version() {
return version;
}
/**
* Triggering gauge render on a canvas.
*
* @abstract
* @returns {BaseGauge}
*/
draw() {
console.log("BaseGauge.draw");
if (this.options.animateOnInit && !this.initialized) {
this.value = this._value;
this.initialized = true;
this.emit("init");
}
this.emit("render");
return this;
}
/**
* Inject given gauge object into DOM
*
* @param {string} type
* @param {GenericOptions} options
*/
static initialize(type, options) {
return new DomObserver(options, "canvas", type);
}
/**
* Initializes gauge from a given HTML element
* (given element should be valid HTML canvas gauge definition)
*
* @param {HTMLElement} element
*/
static fromElement(element) {
let type = DomObserver.toCamelCase(element.getAttribute("data-type"));
let attributes = element.attributes;
let i = 0;
let s = attributes.length;
let options = {};
if (!type) {
return;
}
if (!/Gauge$/.test(type)) {
type += "Gauge";
}
for (; i < s; i++) {
options[
DomObserver.toCamelCase(
attributes[i].name.replace(/^data-/, ""),
false
)
] = DomObserver.parse(attributes[i].value);
}
new DomObserver(options, element.tagName, type).process(element);
}
/**
* Ensures value is proper number
*
* @param {*} value
* @param {number} min
* @return {number}
*/
static ensureValue(value, min = 0) {
value = parseFloat(value);
if (isNaN(value) || !isFinite(value)) {
value = parseFloat(min) || 0;
}
return value;
}
/**
* Corrects javascript modulus bug
* @param {number} n
* @param {number} m
* @return {number}
*/
static mod(n, m) {
return ((n % m) + m) % m;
}
}
/**
* @ignore
* @typedef {object} ns
*/
/* istanbul ignore if */
if (typeof ns !== "undefined") {
ns["BaseGauge"] = BaseGauge;
ns["gauges"] = (window.document || {})["gauges"] = gauges;
}
module.exports = BaseGauge;