lib/drawings.js
/*!
* The MIT License (MIT)
*
* Copyright (c) 2016 Mykhailo Stadnyk <[email protected]>
*
* 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.
*/
/**
* @access private
* @typedef {CanvasRenderingContext2D | {
* max: number,
* maxRadius: number,
* barDimensions: object,
* }} Canvas2DContext
*/
/* istanbul ignore next: private, not testable */
/**
* Examines if a given error is something to throw or to ignore
*
* @param {Error} err
*/
export function verifyError(err) {
// there is some unpredictable error in FF in some circumstances
// which we found simply safe to ignore than to fight with it
// noinspection JSUnresolvedVariable
if (err instanceof DOMException && err.result === 0x8053000b) {
return ; // ignore it
}
throw err;
}
const validMember = /{([_a-zA-Z]+[_a-zA-Z0-9]*)}/g;
/**
* Format string unit string format using option members
* Format option to set the “units” attribute.
* For example “{value} % {title}” which replaces the attributes inside {} to
* the same member in the option object.
* So if title is set to “Hour” and value to “50” the units will be “50% Hour”.
*
* @param {GenericOptions|any} options
* @param {string} format
* @return {string}
*/
export function formatContext(options, format) {
// "{value} % {Title}"
return format.replace(validMember, function (match, member){
const value = options[member];
return (typeof value !== 'undefined') ? value : match;
});
}
/* istanbul ignore next: private, not testable */
/**
* Prepares major ticks data
*
* @access private
* @param {GenericOptions|{ tickSide: string }} options
* @return {[boolean, boolean]}
*/
export function prepareTicks(options) {
if (!(options.majorTicks instanceof Array)) {
options.majorTicks = options.majorTicks ? [options.majorTicks] : [];
}
if (!options.majorTicks.length) {
options.majorTicks.push(drawings.formatMajorTickNumber(
options.minValue, options));
options.majorTicks.push(drawings.formatMajorTickNumber(
options.maxValue, options));
}
return [options.tickSide !== 'right', options.tickSide !== 'left'];
}
/* istanbul ignore next: private, not testable */
/**
* Draws rounded corners rectangle
*
* @param {Canvas2DContext} context
* @param {number} x
* @param {number} y
* @param {number} w
* @param {number} h
* @param {number} r
*/
export function roundRect(
context,
x,
y,
w,
h,
r
) {
context.beginPath();
context.moveTo(x + r, y);
context.lineTo(x + w - r, y);
context.quadraticCurveTo(x + w, y, x + w, y + r);
context.lineTo(x + w, y + h - r);
context.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
context.lineTo(x + r, y + h);
context.quadraticCurveTo(x, y + h, x, y + h - r);
context.lineTo(x, y + r);
context.quadraticCurveTo(x, y, x + r, y);
context.closePath();
}
/* istanbul ignore next: private, not testable */
/**
* Pads a given value with leading zeros using the given options
*
* @param {number} val
* @param {RadialGaugeOptions|{valueInt: number, valueDec: number}} options
* @returns {string}
*/
export function padValue(val, options) {
let dec = options.valueDec;
let int = options.valueInt;
let i = 0;
let s, strVal, n;
val = parseFloat(val);
n = (val < 0);
val = Math.abs(val);
if (dec > 0) {
strVal = val.toFixed(dec).toString().split('.');
s = int - strVal[0].length;
for (; i < s; ++i) {
strVal[0] = '0' + strVal[0];
}
strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];
}
else {
strVal = Math.round(val).toString();
s = int - strVal.length;
for (; i < s; ++i) {
strVal = '0' + strVal;
}
strVal = (n ? '-' : '') + strVal;
}
return strVal;
}
/* istanbul ignore next: private, not testable */
/**
* Formats a number for display on the dial's plate using the majorTicksFormat
* config option.
*
* @param {number} num number to format
* @param {object} options
* @returns {string} formatted number
*/
export function formatMajorTickNumber(num, options) {
let right, hasDec = false;
// First, force the correct number of digits right of the decimal.
if (options.majorTicksDec === 0) {
right = Math.round(num).toString();
}
else {
right = num.toFixed(options.majorTicksDec);
}
// Second, force the correct number of digits left of the decimal.
if (options.majorTicksInt > 1) {
// Does this number have a decimal?
hasDec = ~right.indexOf('.');
// Is this number a negative number?
if (~right.indexOf('-')) {
return '-' + [
options.majorTicksInt +
options.majorTicksDec +
2 + (hasDec ? 1 : 0) - right.length
].join('0') + right.replace('-', '');
}
else {
return [
options.majorTicksInt +
options.majorTicksDec +
1 + (hasDec ? 1 : 0) - right.length
].join('0') + right;
}
}
return right;
}
/* istanbul ignore next: private, not testable */
/**
* Transforms degrees to radians
*
* @param {number} degrees
* @returns {number}
*/
export function radians(degrees) {
return degrees * Math.PI / 180;
}
/* istanbul ignore next: private, not testable */
/**
* Calculates and returns radial point coordinates
*
* @param {number} radius
* @param {number} angle
* @returns {{x: number, y: number}}
*/
export function radialPoint(radius, angle) {
return { x: -radius * Math.sin(angle), y: radius * Math.cos(angle) };
}
/* istanbul ignore next: private, not testable */
/**
* Creates and returns linear gradient canvas object
*
* @param {Canvas2DContext} context
* @param {string} colorFrom
* @param {string} colorTo
* @param {number} length
* @param {boolean} [isVertical]
* @param {number} [from]
* @returns {CanvasGradient}
*/
export function linearGradient(context, colorFrom, colorTo, length,
isVertical = true, from = 0)
{
let grad = context.createLinearGradient(
isVertical ? 0 : from,
isVertical ? from : 0,
isVertical ? 0 : length,
isVertical ? length : 0);
grad.addColorStop(0, colorFrom);
grad.addColorStop(1, colorTo);
return grad;
}
/* istanbul ignore next: private, not testable */
/**
* Draws the shadow if it was not drawn
*
* @param {Canvas2DContext} context
* @param {GenericOptions} options
* @param {boolean} shadowDrawn
* @return {boolean}
*/
export function drawShadow(context, options, shadowDrawn = false) {
if (shadowDrawn) {
context.restore();
return true;
}
context.save();
let w = options.borderShadowWidth;
if (w) {
context.shadowBlur = w;
context.shadowColor = options.colorBorderShadow;
}
return true;
}
/* istanbul ignore next: private, not testable */
/**
* Draws gauge needle shadow
*
* @access private
* @param {Canvas2DContext} context
* @param {RadialGaugeOptions} options
*/
export function drawNeedleShadow(
context,
options
) {
if (!options.needleShadow) return;
context.shadowOffsetX = 2;
context.shadowOffsetY = 2;
context.shadowBlur = 10;
context.shadowColor = options.colorNeedleShadowDown;
}
/* istanbul ignore next: private, not testable */
/**
* Constructs font styles for canvas fonts
*
* @param {GenericOptions} options
* @param {string} target
* @param {number} baseSize
*/
export function font(options, target, baseSize) {
return options['font' + target + 'Style'] + ' ' +
options['font' + target + 'Weight'] + ' ' +
options['font' + target + 'Size'] * baseSize + 'px ' +
options['font' + target];
}
/* istanbul ignore next: private, not testable */
/**
* Resets some context settings
*
* @param {Canvas2DContext} context
*/
function reset(context) {
context.shadowOffsetX = null;
context.shadowOffsetY = null;
context.shadowBlur = null;
context.shadowColor = '';
context.strokeStyle = null;
context.lineWidth = 0;
context.save();
}
/* istanbul ignore next: private, not testable */
/**
* Declares to drow value text shadow if configured
*
* @param context
* @param options
* @param offset
* @param blur
*/
function drawValueTextShadow(context, options, offset, blur) {
if (options.valueTextShadow) {
context.shadowOffsetX = offset;
context.shadowOffsetY = offset;
context.shadowBlur = blur;
context.shadowColor = options.colorValueTextShadow;
}
}
/* istanbul ignore next: private, not testable */
/**
* Draws value box at given position
*
* @param {Canvas2DContext} context
* @param {GenericOptions} options
* @param {number|string} value
* @param {number} x
* @param {number} y
* @param {number} max
*/
export function drawValueBox(context, options, value, x, y, max) {
if (!options.valueBox) return;
reset(context);
let addLength = (options.valueDec ? 1 + options.valueDec : 0);
let maxValueWidth = '9'.repeat(Math.max.apply(null,
[String(parseInt(value)).length + addLength]
.concat(options.majorTicks.map(val =>
String(parseInt(val, 10)).length + addLength
))));
let text = options.valueText || padValue(value, options);
let tunit = max / 200;
let runit = max / 100;
let offset = 0.4 * runit;
let blur = 1.2 * runit;
context.font = font(options, 'Value', tunit);
drawValueTextShadow(context, options, offset, blur);
let tw = context.measureText(options.valueText ?
text : ('-' + padValue(Number(maxValueWidth), options))).width;
reset(context);
let th = parseFloat(options.fontValueSize) * tunit + offset + blur;
let sw = runit * parseFloat(options.valueBoxStroke);
let bmax = max * 2 - sw * 2;
let bw = tw + 10 * runit;
let bh = 1.1 * th + offset + blur;
let br = runit * options.valueBoxBorderRadius;
let obw = (parseFloat(options.valueBoxWidth) || 0) / 100 * bmax;
(obw > bw) && (bw = obw);
(bw > bmax) && (bw = bmax);
let bx = x - bw / 2;
let by = y - bh / 2;
let gy = y - 5.75 * runit;
context.beginPath();
if (br) roundRect(context, bx, by, bw, bh, br);
else context.rect(bx, by, bw, bh);
if (sw) {
let grd = context.createRadialGradient(
x, gy, runit * 10, x, gy, runit * 20);
grd.addColorStop(0, options.colorValueBoxRect);
grd.addColorStop(1, options.colorValueBoxRectEnd);
context.strokeStyle = grd;
context.lineWidth = sw;
context.stroke();
}
if (options.colorValueBoxShadow) {
context.shadowBlur = 1.2 * runit;
context.shadowColor = options.colorValueBoxShadow;
}
if (options.colorValueBoxBackground) {
context.fillStyle = options.colorValueBoxBackground;
context.fill();
}
context.closePath();
context.restore();
drawValueTextShadow(context, options, offset, blur);
context.fillStyle = options.colorValueText;
context.textAlign = 'center';
context.textBaseline = 'alphabetic';
context.fillText(text, bx + bw / 2, y + bh / 2 - th / 3);
context.restore();
}
/* istanbul ignore next: private, not testable */
/**
* Returns normalized value
*
* @param {GenericOptions} options
* @return {{normal: number, indented: number}}
*/
export function normalizedValue(options) {
let value = options.value;
let min = options.minValue;
let max = options.maxValue;
let dt = (max - min) * 0.01;
return {
normal: value < min ? min : value > max ? max : value,
indented: value < min ? min - dt : value > max ? max + dt : value
};
}
const drawings = {
roundRect,
padValue,
formatMajorTickNumber,
radians,
radialPoint,
linearGradient,
drawNeedleShadow,
drawValueBox,
verifyError,
prepareTicks,
drawShadow,
font,
normalizedValue,
formatContext,
};
export default drawings;
module.exports = drawings;