lib/LinearGauge.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.
*/
require('./polyfill');
const BaseGauge = require('./BaseGauge');
const GenericOptions = require('./GenericOptions');
const drawings = require('./drawings');
const SmartCanvas = require('./SmartCanvas');
/**
* Linear gauge configuration options
*
* @typedef {GenericOptions|{borderRadius: number, barBeginCircle: number, tickSide: string, needleSide: string, numberSide: string, ticksWidth: number, ticksWidthMinor: number, ticksPadding: number, barLength: number, colorBarEnd: string, colorBarProgressEnd: string}} LinearGaugeOptions
*/
/**
* Default linear gauge configuration options
*
* @type {LinearGaugeOptions}
*/
let defaultLinearGaugeOptions = Object.assign({}, GenericOptions, {
// basic options
borderRadius: 0,
// width: 150,
// height: 400,
// bar
barBeginCircle: 30, // percents
colorBarEnd: '',
colorBarProgressEnd: '',
needleWidth: 6,
tickSide: 'both', // available: 'left', 'right', 'both'
needleSide: 'both', // available: 'left', 'right', 'both'
numberSide: 'both', // available: 'left', 'right', 'both'
ticksWidth: 10,
ticksWidthMinor: 5,
ticksPadding: 5,
barLength: 85,
fontTitleSize: 26,
highlightsWidth: 10
});
/* istanbul ignore next: private, not testable */
/**
* Draws rectangle on a canvas
*
* @param {Canvas2DContext} context
* @param {number} r radius for founded corner rectangle if 0 or less won't be drawn
* @param {number} x x-coordinate of the top-left corner
* @param {number} y y-coordinate of the top-left corner
* @param {number} w width of the rectangle
* @param {number} h height of the rectangle
* @param {string} colorStart base fill color of the rectangle
* @param {string} [colorEnd] gradient color of the rectangle
*/
function drawRectangle(context, r, x, y, w, h, colorStart, colorEnd) {
context.beginPath();
context.fillStyle = colorEnd ?
drawings.linearGradient(context, colorStart, colorEnd,
w > h ? w: h, h > w, w > h ? x : y) : colorStart;
(r > 0) ?
drawings.roundRect(context, x, y, w, h, r) :
context.rect(x, y, w, h);
context.fill();
context.closePath();
}
/* istanbul ignore next: private, not testable */
/**
* Draws rectangle on a canvas
*
* @param {Canvas2DContext} context
* @param {number} width width of the border
* @param {number} r radius for founded corner rectangle if 0 or less won't be drawn
* @param {number} x x-coordinate of the top-left corner
* @param {number} y y-coordinate of the top-left corner
* @param {number} w width of the rectangle
* @param {number} h height of the rectangle
* @param {string} colorStart base fill color of the rectangle
* @param {string} [colorEnd] gradient color of the rectangle
*/
function drawLinearBorder(context, width, r, x, y, w, h, colorStart, colorEnd) {
context.beginPath();
context.lineWidth = width;
context.strokeStyle = colorEnd ?
drawings.linearGradient(context, colorStart, colorEnd, h, true, y) :
colorStart;
(r > 0) ?
drawings.roundRect(context, x, y, w, h, r) :
context.rect(x, y, w, h);
context.stroke();
context.closePath();
}
/* istanbul ignore next: private, not testable */
/**
* Draws linear gauge plate
*
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
* @param {number} x
* @param {number} y
* @param {number} w
* @param {number} h
*/
function drawLinearPlate(context, options, x, y, w, h) {
let pxRatio = SmartCanvas.pixelRatio;
context.save();
let r = options.borderRadius * pxRatio;
let w1 = w - options.borderShadowWidth - options.borderOuterWidth * pxRatio;
let w2 = w1 - options.borderOuterWidth * pxRatio -
options.borderMiddleWidth * pxRatio;
let w3 = w2 - options.borderMiddleWidth * pxRatio -
options.borderInnerWidth * pxRatio;
let w4 = w3 - options.borderInnerWidth * pxRatio;
let h1 = h - options.borderShadowWidth - options.borderOuterWidth * pxRatio;
let h2 = h1 - options.borderOuterWidth * pxRatio -
options.borderMiddleWidth * pxRatio;
let h3 = h2 - options.borderMiddleWidth * pxRatio -
options.borderInnerWidth * pxRatio;
let h4 = h3 - options.borderInnerWidth * pxRatio;
let x2 = x - (w2 - w1) / 2;
let x3 = x2 - (w3 - w2) / 2;
let x4 = x3 - (w4 - w3) / 2;
let y2 = y - (h2 - h1) / 2;
let y3 = y2 - (h3 - h2) / 2;
let y4 = y3 - (h4 - h3) / 2;
let aliasingOffset = 0;
let shadowDrawn = false;
if (options.borderOuterWidth) {
shadowDrawn = drawings.drawShadow(context, options, shadowDrawn);
drawLinearBorder(context, options.borderOuterWidth * pxRatio,
r,
x + options.borderOuterWidth * pxRatio / 2 - aliasingOffset,
y + options.borderOuterWidth * pxRatio / 2 - aliasingOffset,
w1, h1,
options.colorBorderOuter,
options.colorBorderOuterEnd);
aliasingOffset += 0.5 * pxRatio;
}
if (options.borderMiddleWidth) {
shadowDrawn = drawings.drawShadow(context, options, shadowDrawn);
drawLinearBorder(context, options.borderMiddleWidth * pxRatio,
(r -= 1 + aliasingOffset * 2),
x2 + options.borderMiddleWidth * pxRatio / 2 - aliasingOffset,
y2 + options.borderMiddleWidth * pxRatio / 2 - aliasingOffset,
w2 + aliasingOffset * 2,
h2 + aliasingOffset * 2,
options.colorBorderMiddle,
options.colorBorderMiddleEnd);
aliasingOffset += 0.5 * pxRatio;
}
if (options.borderInnerWidth) {
shadowDrawn = drawings.drawShadow(context, options, shadowDrawn);
drawLinearBorder(context,options.borderInnerWidth * pxRatio,
(r -= 1 + aliasingOffset * 2),
x3 + options.borderInnerWidth * pxRatio / 2 - aliasingOffset,
y3 + options.borderInnerWidth * pxRatio / 2 - aliasingOffset,
w3 + aliasingOffset * 2,
h3 + aliasingOffset * 2,
options.colorBorderInner,
options.colorBorderInnerEnd);
aliasingOffset += 0.5 * pxRatio;
}
drawings.drawShadow(context, options, shadowDrawn);
drawRectangle(context, r, x4, y4,
w4 + aliasingOffset * 2,
h4 + aliasingOffset * 2,
options.colorPlate,
options.colorPlateEnd);
context.restore();
return [x4, y4, w4, h4];
}
/* istanbul ignore next: private, not testable */
/**
* Calculates and returns linear gauge base bar dimensions.
*
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions|{barStrokeWidth: number, barBeginCircle: number, barWidth: number, hasLeft: boolean, hasRight: boolean}} options
* @param {number} x
* @param {number} y
* @param {number} w
* @param {number} h
* @return {{isVertical: boolean, width: number, length: number, barWidth: number, barLength: number, strokeWidth: number, barMargin: number, radius: number, x0: number, y0: number, barOffset: number, titleMargin: number, unitsMargin: number, X: number, Y: number, baseX: number, baseY: number, ticksPadding: number}}
*/
function barDimensions(context, options, x, y, w, h) {
let pixelRatio = SmartCanvas.pixelRatio;
let isVertical = h >= w;
let width = isVertical ? w * 0.85 : h;
let length = isVertical ? h : w;
//noinspection JSUnresolvedFunction
x = isVertical ? round(x + (w - width) / 2) : x;
let hasTitle = !!options.title;
let hasUnits = !!options.units;
let hasValue = !!options.valueBox;
let titleMargin;
let unitsMargin;
let valueMargin;
if (isVertical) {
//noinspection JSUnresolvedFunction
unitsMargin = round(length * 0.05);
//noinspection JSUnresolvedFunction
titleMargin = round(length * 0.075);
//noinspection JSUnresolvedFunction
valueMargin = round(length * 0.11);
if (hasTitle) {
length -= titleMargin;
y += titleMargin;
}
if (hasUnits) length -= unitsMargin;
if (hasValue) length -= valueMargin;
}
else {
//noinspection JSUnresolvedFunction
unitsMargin = titleMargin = round(width * 0.15);
if (hasTitle) {
width -= titleMargin;
y += titleMargin;
}
if (hasUnits) width -= unitsMargin;
}
let strokeWidth = options.barStrokeWidth * 2;
//noinspection JSUnresolvedFunction
let radius = options.barBeginCircle ?
round(width * options.barBeginCircle / 200 - strokeWidth / 2) : 0;
//noinspection JSUnresolvedFunction
let barWidth = round(width * options.barWidth / 100 - strokeWidth);
//noinspection JSUnresolvedFunction
let barLength = round(length * options.barLength / 100 - strokeWidth);
//noinspection JSUnresolvedFunction
let barMargin = round((length - barLength) / 2);
// coordinates for arc of the bar if configured
//noinspection JSUnresolvedFunction
let x0 = round(x + (isVertical ? width / 2 : barMargin + radius));
//noinspection JSUnresolvedFunction
let y0 = round(y + (isVertical ?
length - barMargin - radius + strokeWidth / 2:
width / 2));
let dx = isVertical && !(options.hasLeft && options.hasRight) ?
((options.hasRight ? -1 : 1) * options.ticksWidth / 100 * width) : 0;
let dy = !isVertical && !(options.hasLeft && options.hasRight) ?
((options.hasRight ? -1 : 1) * options.ticksWidth / 100 * width) : 0;
//noinspection JSUndefinedPropertyAssignment
context.barDimensions = {
isVertical: isVertical,
width: width,
length: length,
barWidth: barWidth,
barLength: barLength,
strokeWidth: strokeWidth,
barMargin: barMargin,
radius: radius,
pixelRatio: pixelRatio,
barOffset: null,
titleMargin: hasTitle ? titleMargin : 0,
unitsMargin: hasUnits ? unitsMargin : 0,
get ticksLength() {
return this.barLength - this.barOffset - this.strokeWidth;
},
X: x + dx,
Y: y + dy,
x0: x0 + dx,
y0: y0 + dy,
baseX: x,
baseY: y,
ticksPadding: options.ticksPadding / 100
};
return context.barDimensions;
}
/* istanbul ignore next: private, not testable */
/**
* Draws bar shape from the given options on a given canvas context
*
* @access private
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
* @param {string} type
* @param {number} x
* @param {number} y
* @param {number} w
* @param {number} h
*/
function drawLinearBarShape(context, options, type, x, y, w, h) {
let {isVertical, width, barWidth, barLength, strokeWidth, barMargin, radius,
x0, y0, X, Y} = barDimensions(context, options, x, y, w, h);
let fullBarLength = barLength;
context.save();
context.beginPath();
if (options.barBeginCircle) {
let direction = drawings.radians(isVertical ? 270 : 0);
let alpha = Math.asin(barWidth / 2 / radius);
let cosAlpha = Math.cos(alpha);
let sinAlpha = Math.sin(alpha);
let x1 = x0 + (isVertical ?
radius * sinAlpha :
radius * cosAlpha - strokeWidth / 2);
let y1 = isVertical ?
y0 - radius * cosAlpha:
y0 + radius * sinAlpha;
//noinspection JSUnresolvedFunction
let cutRadius = isVertical ? abs(y1 - y0) : abs(x1 - x0);
//noinspection JSUnresolvedFunction
context.barDimensions.barOffset = round(cutRadius + radius);
// bottom point
//noinspection JSUnresolvedFunction
let x2 = isVertical ? round(x0 - radius * sinAlpha) : x1;
//noinspection JSUnresolvedFunction
let y2 = isVertical ? y1 : round(y0 - radius * sinAlpha);
if (type === 'progress') {
barLength = context.barDimensions.barOffset +
(barLength - context.barDimensions.barOffset) *
(drawings.normalizedValue(options).normal - options.minValue) /
(options.maxValue - options.minValue);
}
// bar ends at
//noinspection JSUnresolvedFunction
let x3 = round(x1 + barLength - context.barDimensions.barOffset +
strokeWidth / 2); // h
//noinspection JSUnresolvedFunction
let y3 = round(y1 - barLength + context.barDimensions.barOffset -
strokeWidth / 2); // v
context.arc(x0, y0, radius, direction + alpha, direction - alpha);
if (isVertical) {
context.moveTo(x1, y2);
context.lineTo(x1, y3);
context.lineTo(x2, y3);
context.lineTo(x2, y2);
}
else {
context.moveTo(x1, y2);
context.lineTo(x3, y2);
context.lineTo(x3, y1);
context.lineTo(x1, y1);
}
}
else {
// simply rectangle
//noinspection JSUnresolvedFunction
let rx = round(isVertical ?
(X + (width - barWidth) / 2) : (X + barMargin));
//noinspection JSUnresolvedFunction
let ry = round(isVertical ?
(Y + barLength + barMargin) : (Y + (width - barWidth) / 2));
if (type === 'progress') {
barLength *= (options.value - options.minValue) /
(options.maxValue - options.minValue);
}
if (isVertical) context.rect(rx, ry, barWidth, -barLength);
else context.rect(rx, ry, barLength, barWidth);
}
if (type !== 'progress' && options.barStrokeWidth) {
context.lineWidth = strokeWidth;
context.strokeStyle = options.colorBarStroke;
//context.lineJoin = 'round';
context.stroke();
}
if (type !== 'progress' && options.colorBar) {
context.fillStyle = options.colorBarEnd ?
drawings.linearGradient(context, options.colorBar,
options.colorBarEnd, barLength, isVertical,
isVertical ? Y : X):
options.colorBar;
context.fill();
}
else if (type === 'progress' && options.colorBarProgress) {
context.fillStyle = options.colorBarProgressEnd ?
drawings.linearGradient(context, options.colorBarProgress,
options.colorBarProgressEnd, fullBarLength, isVertical,
isVertical ? Y : X):
options.colorBarProgress;
context.fill();
}
context.closePath();
// fix dimensions for further usage
if (options.barBeginCircle)
context.barDimensions.radius += strokeWidth;
context.barDimensions.barWidth += strokeWidth;
context.barDimensions.barLength += strokeWidth;
}
/**
* Draws gauge bar
*
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
* @param {number} x x-coordinate of the top-left corner of the gauge
* @param {number} y y-coordinate of the top-left corner of the gauge
* @param {number} w width of the gauge
* @param {number} h height of the gauge
*/
function drawLinearBar(context, options, x, y, w, h) {
drawLinearBarShape(context, options, '', x, y, w, h);
}
/* istanbul ignore next: private, not testable */
/**
* Helper function to calculate bar ticks presence on the sides
*
* @param {string} notWhich
* @param {LinearGaugeOptions} options
* @return {boolean}
*/
function hasTicksBar(notWhich, options) {
return options.needleSide !== notWhich ||
options.tickSide !== notWhich ||
options.numberSide !== notWhich;
}
/* istanbul ignore next: private, not testable */
/**
* Draws gauge bar progress
*
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
* @param {number} x x-coordinate of the top-left corner of the gauge
* @param {number} y y-coordinate of the top-left corner of the gauge
* @param {number} w width of the gauge
* @param {number} h height of the gauge
*/
function drawLinearBarProgress(context, options, x, y, w, h) {
options.barProgress &&
drawLinearBarShape(context, options, 'progress', x, y, w, h);
}
/* istanbul ignore next: private, not testable */
/**
* Draws gauge bar highlighted areas
*
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
*/
function drawLinearBarHighlights(context, options) {
let {isVertical, width, length, barWidth, barOffset, barMargin,
X, Y, ticksLength, ticksPadding} = context.barDimensions;
let hlWidth = width * (parseFloat(options.highlightsWidth) || 0) / 100;
if (!options.highlights || !hlWidth) return ;
let hasLeft = options.tickSide !== 'right';
let hasRight = options.tickSide !== 'left';
let i = 0;
let s = options.highlights.length;
let tickOffset = (width - barWidth) / 2;
let interval = options.maxValue - options.minValue;
//noinspection JSUnresolvedFunction
let eX = round(isVertical ? X + tickOffset : X + barMargin + barOffset);
let eH = hlWidth;
let eY = isVertical ? Y + length - barMargin - barOffset: Y + tickOffset;
//noinspection JSUnresolvedFunction
let hLeft = round((options.ticksWidth / 100 + ticksPadding) * width)
+ (hlWidth - options.ticksWidth / 100 * width);
//noinspection JSUnresolvedFunction
let hRight = round(barWidth + ticksPadding * width);
context.save();
for (; i < s; i++) {
let entry = options.highlights[i];
//noinspection JSUnresolvedFunction
let eStart = ticksLength * abs(options.minValue - entry.from) /
interval;
//noinspection JSUnresolvedFunction
let eW = ticksLength * abs((entry.to - entry.from) / interval);
context.beginPath();
context.fillStyle = entry.color;
if (isVertical) {
if (hasLeft)
context.rect(eX - hLeft, eY - eStart, eH, -eW);
if (hasRight)
context.rect(eX + hRight, eY - eStart, eH, -eW);
}
else {
if (hasLeft)
context.rect(eX + eStart, eY - hLeft, eW, eH);
if (hasRight)
context.rect(eX + eStart, eY + hRight, eW, eH);
}
context.fill();
context.closePath();
}
}
/* istanbul ignore next: private, not testable */
/**
* Draws a tick line on a linear gauge
*
* @param {Canvas2DContext} context
* @param x1
* @param y1
* @param x2
* @param y2
*/
function drawLinearTick(context, x1, y1, x2, y2) {
context.beginPath();
context.moveTo(x1, y1);
context.lineTo(x2, y2);
context.stroke();
context.closePath();
context.save();
}
/* istanbul ignore next: private, not testable */
/**
* Draws ticks
*
* @param {Canvas2DContext} context
* @param {string} color
* @param {number[]} ticks
* @param {number} minVal
* @param {number} maxVal
* @param {boolean} hasLeft
* @param {boolean} hasRight
* @param {number} lineWidth
* @param {number} lineLength
*/
function drawLinearTicks(context, color, ticks, minVal, maxVal,
hasLeft, hasRight, lineWidth, lineLength)
{
let {isVertical, length, barWidth, barOffset, barMargin,
pixelRatio, width, X, Y, ticksLength, ticksPadding} =
context.barDimensions;
let tickOffset = (width - barWidth) / 2;
let tickX, tickY;
let i = 0;
let s = ticks.length;
let val;
let tickLen = lineLength * width;
let tickLeft = tickOffset - ticksPadding * width;
let tickRight = tickOffset + barWidth + tickLen + ticksPadding * width;
let colors = color instanceof Array ?
color : new Array(ticks.length).fill(color);
context.lineWidth = lineWidth * pixelRatio;
context.save();
let ratio = ticksLength / (maxVal - minVal);
for (; i < s; i++) {
val = ticks[i];
context.strokeStyle = colors[i];
if (isVertical) {
tickY = Y + length - barMargin - barOffset
+ (minVal - val) * ratio;
if (hasLeft) {
tickX = X + tickLeft;
//noinspection JSUnresolvedFunction
drawLinearTick(context, tickX, tickY, round(tickX - tickLen),
tickY);
}
if (hasRight) {
tickX = X + tickRight;
//noinspection JSUnresolvedFunction
drawLinearTick(context, tickX, tickY, round(tickX - tickLen),
tickY);
}
}
else {
tickX = X + barMargin + barOffset
- (minVal - val) * ratio;
if (hasLeft) {
tickY = Y + tickLeft;
//noinspection JSUnresolvedFunction
drawLinearTick(context, tickX, tickY, tickX,
round(tickY - tickLen));
}
if (hasRight) {
tickY = Y + tickRight;
//noinspection JSUnresolvedFunction
drawLinearTick(context, tickX, round(tickY), tickX,
tickY - tickLen);
}
}
}
}
/* istanbul ignore next: private, not testable */
/**
* Draws major ticks
*
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
*/
function drawLinearMajorTicks(context, options) {
let [hasLeft, hasRight] = drawings.prepareTicks(options);
let lineWidth = 2;
let valuePerNonExactTick = (options.maxValue - options.minValue) /
(options.majorTicks.length - 1);
let colors = options.colorMajorTicks instanceof Array ?
options.colorMajorTicks :
new Array(options.majorTicks.length).fill(
options.colorStrokeTicks || options.colorMajorTicks);
let ticks = options.exactTicks ? options.majorTicks :
options.majorTicks.map((tick, i) => {
return options.minValue + valuePerNonExactTick * i;
});
drawLinearTicks(context, colors, ticks, options.minValue, options.maxValue,
hasLeft, hasRight, lineWidth, options.ticksWidth / 100);
if (options.strokeTicks) {
let {isVertical, length, width, barWidth, barMargin, barOffset, X, Y,
ticksLength, pixelRatio, ticksPadding} = context.barDimensions;
let rightTicks = (width - barWidth) / 2 + barWidth +
ticksPadding * width;
let leftTicks = (width - barWidth) / 2 - ticksPadding * width;
let sX, sY, eX, eY;
context.strokeStyle = options.colorStrokeTicks || colors[0];
lineWidth *= pixelRatio;
if (isVertical) {
sY = Y + length - barMargin - barOffset + lineWidth / 2;
eY = sY - ticksLength - lineWidth;
if (hasLeft) {
//noinspection JSUnresolvedFunction
eX = sX = round(X + leftTicks);
drawLinearTickStroke(context, sX, sY, eX, eY);
}
if (hasRight) {
//noinspection JSUnresolvedFunction
eX = sX = round(X + rightTicks);
drawLinearTickStroke(context, sX, sY, eX, eY);
}
}
else {
sX = X + barMargin + barOffset - lineWidth / 2;
eX = sX + ticksLength + lineWidth;
if (hasLeft) {
//noinspection JSUnresolvedFunction
eY = sY = round(Y + leftTicks);
drawLinearTickStroke(context, sX, sY, eX, eY);
}
if (hasRight) {
//noinspection JSUnresolvedFunction
eY = sY = round(Y + rightTicks);
drawLinearTickStroke(context, sX, sY, eX, eY);
}
}
}
}
/* istanbul ignore next: private, not testable */
/**
* Draws ticks stroke
*
* @param {Canvas2DContext} context
* @param {number} sX
* @param {number} sY
* @param {number} eX
* @param {number} eY
*/
function drawLinearTickStroke(context, sX, sY, eX, eY) {
context.beginPath();
context.moveTo(sX, sY);
context.lineTo(eX, eY);
context.stroke();
context.closePath();
}
/* istanbul ignore next: private, not testable */
/**
* Draws minor ticks
*
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
*/
function drawLinearMinorTicks(context, options) {
let [hasLeft, hasRight] = drawings.prepareTicks(options);
let ticks = [];
let i = options.minValue;
let minTicks = Math.abs(options.minorTicks) || 0;
let valuePerNonExactTick = minTicks ?
(options.maxValue - options.minValue) /
(minTicks * (options.majorTicks.length - 1)) : 0;
if (minTicks) {
if (options.exactTicks) {
let delta = BaseGauge.mod(options.majorTicks[0], minTicks) || 0;
for (; i < options.maxValue; i += minTicks) {
if ((delta+i) < options.maxValue) {
ticks.push(delta + i);
}
}
}
else {
for (; i < options.maxValue; i += valuePerNonExactTick) {
ticks.push(i);
}
}
}
drawLinearTicks(context,
options.colorMinorTicks || options.colorStrokeTicks,
ticks, options.minValue, options.maxValue,
hasLeft, hasRight, 1, options.ticksWidthMinor / 100);
}
/* istanbul ignore next: private, not testable */
/**
* Draws major tick numbers
*
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
*/
function drawLinearMajorTicksNumbers(context, options) {
let {isVertical, length, width, barWidth,
barMargin, barOffset, X, Y, ticksLength, ticksPadding} =
context.barDimensions;
let range = (options.maxValue - options.minValue);
let valuePerNonExactTick = range /
(options.majorTicks.length - 1);
let tickValues = options.exactTicks ? options.majorTicks :
options.majorTicks.map((tick, i) => {
return options.minValue + valuePerNonExactTick * i;
});
let ticks = tickValues.length;
let hasLeft = options.numberSide !== 'right';
let hasRight = options.numberSide !== 'left';
let textHeight = options.fontNumbersSize * width / 200;
let i = 0;
let ticksWidth = (options.ticksWidth / 100 + ticksPadding * 2) * width;
let numLeft = (width - barWidth) / 2 - ticksWidth;
let numRight = (width - barWidth) / 2 + barWidth + ticksWidth;
let textX, textY, textWidth, numberOffset, tick;
let colors = options.colorNumbers instanceof Array ?
options.colorNumbers : new Array(ticks).fill(options.colorNumbers);
let textMargin = options.numbersMargin / 100 * width;
context.font = drawings.font(options, 'Numbers', width / 200);
context.lineWidth = 0;
context.textAlign = 'center';
for (; i < ticks; i++) {
context.fillStyle = colors[i];
tick = options.majorTicks[i];
numberOffset = options.exactTicks ?
ticksLength * ((tickValues[i] - options.minValue) / range) :
i * ticksLength / (ticks - 1);
if (isVertical) {
textY = Y + length - barMargin - barOffset - numberOffset
+ textHeight / 3;
if (hasLeft) {
context.textAlign = 'right';
context.fillText(tick, X + numLeft - textMargin, textY);
}
if (hasRight) {
context.textAlign = 'left';
context.fillText(tick, X + numRight + textMargin, textY);
}
}
else {
textWidth = context.measureText(tick).width;
textX = X + barMargin + barOffset + numberOffset;
if (hasLeft) {
context.fillText(tick, textX, Y + numLeft - textMargin);
}
if (hasRight) {
context.fillText(tick, textX, Y + numRight + textHeight +
textMargin);
}
}
}
}
/* istanbul ignore next: private, not testable */
/**
* Draws linear gauge title
*
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
*/
function drawLinearTitle(context, options) {
if (!options.title) return ;
let {isVertical, width, length, baseX, baseY, titleMargin} =
context.barDimensions;
let textHeight = options.fontTitleSize * width / 200;
//noinspection JSUnresolvedFunction
let textX = round(baseX + (isVertical ? width : length) / 2);
//noinspection JSUnresolvedFunction
let textY = round(baseY + titleMargin / 2 -
(isVertical ? textHeight : textHeight / 2) -
0.025 * (isVertical ? length : width));
context.save();
context.textAlign = 'center';
context.fillStyle = options.colorTitle;
context.font = drawings.font(options, 'Title', width / 200);
context.lineWidth = 0;
context.fillText(options.title, textX, textY, isVertical ? width : length);
}
/* istanbul ignore next: private, not testable */
/**
* Draws linear gauge units
*
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
*/
function drawLinearUnits(context, options) {
if (!options.units) return ;
let {isVertical, width, length, baseX, baseY, unitsMargin} =
context.barDimensions;
let textHeight = options.fontUnitsSize * width / 200;
//noinspection JSUnresolvedFunction
let textX = round(baseX + (isVertical ? width : length) / 2);
//noinspection JSUnresolvedFunction
let textY = round(baseY + (isVertical ? length : width) +
unitsMargin / 2 - textHeight / 2);
context.save();
context.textAlign = 'center';
context.fillStyle = options.colorUnits;
context.font = drawings.font(options, 'Units', width / 200);
context.lineWidth = 0;
context.fillText(
drawings.formatContext(options, options.units),
textX,
textY,
isVertical ? width : length);
}
/* istanbul ignore next: private, not testable */
/**
* Draws linear gauge needles
*
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
*/
function drawLinearBarNeedle(context, options) {
if (!options.needle) return;
let {isVertical, width, length, barWidth, barOffset, barMargin,
ticksLength, X, Y, ticksPadding} = context.barDimensions;
let hasLeft = options.needleSide !== 'right';
let hasRight = options.needleSide !== 'left';
let position = ticksLength *
(drawings.normalizedValue(options).indented - options.minValue) /
(options.maxValue - options.minValue);
let tickWidth = (options.ticksWidth / 100 + ticksPadding) * width;
let baseLength = (barWidth / 2 + tickWidth);
let needleLength = baseLength * (options.needleEnd / 100);
let sX, eX, sY, eY;
let draw = options.needleType.toLowerCase() === 'arrow' ?
drawLinearArrowNeedle :
drawLinearLineNeedle;
let barStart = (width - barWidth) / 2;
let needleStart = baseLength * (options.needleStart / 100);
let nLeft = barStart - tickWidth - needleStart;
let nRight = barStart + barWidth + tickWidth + needleStart;
context.save();
drawings.drawNeedleShadow(context, options);
if (isVertical) {
//noinspection JSUnresolvedFunction
sY = round(Y + length - barMargin - barOffset - position);
if (hasLeft) {
//noinspection JSUnresolvedFunction
sX = round(X + nLeft);
eX = sX + needleLength;
draw(context, options, sX, sY, eX, sY, needleLength);
}
if (hasRight) {
//noinspection JSUnresolvedFunction
sX = round(X + nRight);
eX = sX - needleLength;
draw(context, options, sX, sY, eX, sY, needleLength, true);
}
}
else {
//noinspection JSUnresolvedFunction
sX = round(X + barMargin + barOffset + position);
if (hasLeft) {
//noinspection JSUnresolvedFunction
sY = round(Y + nLeft);
eY = sY + needleLength;
draw(context, options, sX, sY, sX, eY, needleLength);
}
if (hasRight) {
//noinspection JSUnresolvedFunction
sY = round(Y + nRight);
eY = sY - needleLength;
draw(context, options, sX, sY, sX, eY, needleLength, true);
}
}
context.restore();
}
/* istanbul ignore next: private, not testable */
/**
* Returns needle color style
*
* @access private
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
* @param {number} length
* @param {boolean} [isRight]
* @return {CanvasGradient|string}
*/
function needleStyle(context, options, length, isRight) {
return options.colorNeedleEnd ?
drawings.linearGradient(context,
isRight ? options.colorNeedleEnd : options.colorNeedle,
isRight ? options.colorNeedle : options.colorNeedleEnd,
length,
!context.barDimensions.isVertical
) : options.colorNeedle;
}
/* istanbul ignore next: private, not testable */
/**
* Draws line needle shape
*
* @access private
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
* @param {number} sX
* @param {number} sY
* @param {number} eX
* @param {number} eY
* @param {number} length
* @param {boolean} [isRight]
*/
function drawLinearLineNeedle(context, options, sX, sY, eX, eY, length,
isRight)
{
context.lineWidth = options.needleWidth;
context.strokeStyle = needleStyle(context, options, length, isRight);
context.beginPath();
context.moveTo(sX, sY);
context.lineTo(eX, eY);
context.stroke();
context.closePath();
}
/* istanbul ignore next: private, not testable */
/**
* Draws arrow needle shape
*
* @access private
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
* @param {number} sX
* @param {number} sY
* @param {number} eX
* @param {number} eY
* @param {number} length
* @param {boolean} [isRight]
*/
function drawLinearArrowNeedle(context, options, sX, sY, eX, eY, length,
isRight)
{
//noinspection JSUnresolvedFunction
let peakLength = round(length * 0.4);
let bodyLength = length - peakLength;
let isVertical = sX === eX;
let halfWidth = options.needleWidth / 2;
context.fillStyle = needleStyle(context, options, length, isRight);
context.beginPath();
if (isVertical) {
if (sY > eY) bodyLength *= -1;
context.moveTo(sX - halfWidth, sY);
context.lineTo(sX + halfWidth, sY);
context.lineTo(sX + halfWidth, sY + bodyLength);
context.lineTo(sX, eY);
context.lineTo(sX - halfWidth, sY + bodyLength);
context.lineTo(sX - halfWidth, sY);
}
else {
if (sX > eX) bodyLength *= -1;
context.moveTo(sX, sY - halfWidth);
context.lineTo(sX, sY + halfWidth);
context.lineTo(sX + bodyLength, sY + halfWidth);
context.lineTo(eX, sY);
context.lineTo(sX + bodyLength, sY - halfWidth);
context.lineTo(sX, sY - halfWidth);
}
context.fill();
context.closePath();
}
/* istanbul ignore next: private, not testable */
/**
* Draws value box for linear gauge
*
* @access private
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
* @param {number} value
* @param {number} x
* @param {number} y
* @param {number} w
* @param {number} h
*/
function drawLinearValueBox(context, options, value, x, y, w, h) {
// currently value box is available only for vertical linear gauge,
// as far as by design it is hard to find a proper place for
// horizontal ones
let boxWidth = (parseFloat(options.fontValueSize) || 0) * w / 200;
let dy = (0.11 * h - boxWidth) / 2;
context.barDimensions.isVertical &&
drawings.drawValueBox(context, options, value, x + w / 2,
y + h - boxWidth - dy, w);
}
/**
* Minimalistic HTML5 Canvas Linear Gauge
*/
export default class LinearGauge extends BaseGauge {
/**
* Fired each time before gauge plate is drawn
*
* @event LinearGauge#beforePlate
*/
/**
* Fired each time before gauge highlight areas are drawn
*
* @event LinearGauge#beforeHighlights
*/
/**
* Fired each time before gauge minor ticks are drawn
*
* @event LinearGauge#beforeMinorTicks
*/
/**
* Fired each time before gauge major ticks are drawn
*
* @event LinearGauge#beforeMajorTicks
*/
/**
* Fired each time before gauge tick numbers are drawn
*
* @event LinearGauge#beforeNumbers
*/
/**
* Fired each time before gauge title is drawn
*
* @event LinearGauge#beforeTitle
*/
/**
* Fired each time before gauge units text is drawn
*
* @event LinearGauge#beforeUnits
*/
/**
* Fired each time before gauge bar area is drawn
*
* @event LinearGauge#beforeBar
*/
/**
* Fired each time before gauge progress bar is drawn
*
* @event LinearGauge#beforeProgressBar
*/
/**
* Fired each time before gauge value box is drawn
*
* @event LinearGauge#beforeValueBox
*/
/**
* Fired each time before gauge needle is drawn
*
* @event LinearGauge#beforeNeedle
*/
/**
* @constructor
* @param {LinearGaugeOptions} options
*/
constructor(options) {
options = Object.assign({}, defaultLinearGaugeOptions, options || {});
super(LinearGauge.configure(options));
}
/**
* Checks and updates gauge options properly
*
* @param {*} options
* @return {*}
* @access protected
*/
static configure(options) {
/* istanbul ignore else */
if (options.barStrokeWidth >= options.barWidth) {
//noinspection JSUnresolvedFunction
options.barStrokeWidth = round(options.barWidth / 2);
}
//noinspection JSUndefinedPropertyAssignment
options.hasLeft = hasTicksBar('right', options);
//noinspection JSUndefinedPropertyAssignment
options.hasRight = hasTicksBar('left', options);
if (options.value > options.maxValue) {
options.value = options.maxValue;
}
if (options.value < options.minValue) {
options.value = options.minValue;
}
return BaseGauge.configure(options);
}
/* istanbul ignore next */
/**
* Triggering linear gauge render on a canvas.
*
* @returns {LinearGauge}
*/
draw() {
try {
let canvas = this.canvas;
let [x, y, w, h] = [
-canvas.drawX,
-canvas.drawY,
canvas.drawWidth,
canvas.drawHeight
];
let options = this.options;
if (!canvas.elementClone.initialized) {
let context = canvas.contextClone;
// clear the cache
context.clearRect(x, y, w, h);
context.save();
this.emit('beforePlate');
this.drawBox = drawLinearPlate(context, options, x, y, w, h);
this.emit('beforeBar');
drawLinearBar(context, options, ...this.drawBox);
canvas.context.barDimensions = context.barDimensions;
this.emit('beforeHighlights');
drawLinearBarHighlights(context, options);
this.emit('beforeMinorTicks');
drawLinearMinorTicks(context, options);
this.emit('beforeMajorTicks');
drawLinearMajorTicks(context, options);
this.emit('beforeNumbers');
drawLinearMajorTicksNumbers(context, options);
this.emit('beforeTitle');
drawLinearTitle(context, options);
this.emit('beforeUnits');
drawLinearUnits(context, options);
canvas.elementClone.initialized = true;
}
this.canvas.commit();
// clear the canvas
canvas.context.clearRect(x, y, w, h);
canvas.context.save();
canvas.context.drawImage(canvas.elementClone, x, y, w, h);
canvas.context.save();
this.emit('beforeProgressBar');
drawLinearBarProgress(canvas.context, options, ...this.drawBox);
this.emit('beforeNeedle');
drawLinearBarNeedle(canvas.context, options);
this.emit('beforeValueBox');
drawLinearValueBox(canvas.context, options, options.animatedValue ?
this.options.value : this.value, ...this.drawBox);
super.draw();
}
catch (err) {
drawings.verifyError(err);
}
return this;
}
}
/**
* @ignore
* @typedef {object} ns
*/
/* istanbul ignore if */
if (typeof ns !== 'undefined') {
ns['LinearGauge'] = LinearGauge;
}
BaseGauge.initialize('LinearGauge', defaultLinearGaugeOptions);
module.exports = LinearGauge;