lib/LinearGauge.js
/*!
* @license
* Minimalistic HTML5 Canvas Gauge implementation
*
* This code is subject to MIT license.
*
* Copyright (c) 2012 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|{orientation: string, borderRadius: number, barBeginCircle: number, barWidth: number, barStrokeWidth: number, barProgress: boolean, colorBar: string, colorBarEnd: string, colorBarStroke: string, colorBarProgress: string, colorBarProgressEnd: string, tickSide: string, needleSide: string, numberSide: string, ticksWidth: number, ticksWidthMinor: number, ticksPadding: number, barLength: number, fontNumbersSize: number, fontTitleSize: number, fontUnitsSize: number}} LinearGaugeOptions
*/
const round = Math.round;
const abs = Math.abs;
let TICKS_WIDTH = .1;
let TICKS_WIDTH_MINOR = TICKS_WIDTH / 2;
let TICKS_PADDING = .05;
let BAR_LENGTH = .85;
let FONT_NUMBERS = 20;
let FONT_TITLE = 26;
let FONT_UNITS = 22;
/**
* Default linear gauge configuration options
*
* @type {LinearGaugeOptions}
*/
let defaultLinearGaugeOptions = Object.assign({}, GenericOptions, {
// basic options
borderRadius: 0,
// width: 150,
// height: 400,
// bar
barBeginCircle: 30, // percents
barWidth: 20, // percents
barStrokeWidth: 0, // pixels
barProgress: true,
colorBarStroke: '#222',
colorBar: '#ccc',
colorBarEnd: '',
colorBarProgress: '#888',
colorBarProgressEnd: '',
needleWidth: 6,
tickSide: 'both', // available: 'left', 'right', 'both'
needleSide: 'both', // available: 'left', 'right', 'both'
numberSide: 'both', // available: 'left', 'right', 'both'
ticksWidth: TICKS_WIDTH * 100,
ticksWidthMinor: TICKS_WIDTH_MINOR * 100,
ticksPadding: TICKS_PADDING * 100,
barLength: BAR_LENGTH * 100,
fontNumbersSize: FONT_NUMBERS * 100,
fontTitleSize: FONT_TITLE * 100,
fontUnitsSize: FONT_UNITS * 100
});
/* 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)
: 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 */
/**
* 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}}
*/
function barDimensions(context, options, x, y, w, h) {
let pixelRatio = SmartCanvas.pixelRatio;
let isVertical = h >= w;
let width = isVertical ? w * .85 : h;
let length = isVertical ? h : w;
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) {
unitsMargin = round(length * .05);
titleMargin = round(length * .075);
valueMargin = round(length * .075);
if (hasTitle) {
length -= titleMargin;
y += titleMargin;
}
if (hasUnits) length -= unitsMargin;
if (hasValue) length -= valueMargin;
}
else {
unitsMargin = titleMargin = round(width * .15);
if (hasTitle) {
width -= titleMargin;
y += titleMargin;
}
if (hasUnits) width -= unitsMargin;
}
let strokeWidth = options.barStrokeWidth * 2;
let radius = options.barBeginCircle ?
round(width * options.barBeginCircle / 200 - strokeWidth / 2) : 0;
let barWidth = round(width * options.barWidth / 100 - strokeWidth);
let barLength = round(length * BAR_LENGTH - strokeWidth);
let barMargin = round((length - barLength) / 2);
// coordinates for arc of the bar if configured
let x0 = round(x + (isVertical ? width / 2 : barMargin + radius));
let y0 = round(y + ( isVertical ?
length - barMargin - radius + strokeWidth / 2:
width / 2));
let dx = isVertical && !(options.hasLeft && options.hasRight) ?
(options.hasRight ? -1 : 1) * TICKS_WIDTH * width : 0;
let dy = !isVertical && !(options.hasLeft && options.hasRight) ?
(options.hasRight ? -1 : 1) * TICKS_WIDTH * 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
};
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);
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;
let cutRadius = isVertical ? abs(y1 - y0) : abs(x1 - x0);
// let radiusOffset = round(radius - cutRadius);
//
// if (isVertical) {
// y0 -= radiusOffset;
// y1 -= radiusOffset;
// }
//
// else {
// x0 -= radiusOffset;
// x1 -= radiusOffset;
// }
// barLength -= radiusOffset;
context.barDimensions.barOffset = round(cutRadius + radius);
// bottom point
let x2 = isVertical ? round(x0 - radius * sinAlpha) : x1;
let y2 = isVertical ? y1 : round(y0 - radius * sinAlpha);
if (type === 'progress') {
barLength = context.barDimensions.barOffset +
(barLength - context.barDimensions.barOffset) *
(options.value - options.minValue) /
(options.maxValue - options.minValue);
}
// bar ends at
let x3 = round(x1 + barLength - context.barDimensions.barOffset +
strokeWidth / 2); // h
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
let rx = round(isVertical ?
(X + (width - barWidth) / 2) : (X + barMargin));
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):
options.colorBar;
context.fill();
}
else if (type === 'progress' && options.colorBarProgress) {
context.fillStyle = options.colorBarProgressEnd ?
drawings.linearGradient(context, options.colorBarProgress,
options.colorBarProgressEnd, barLength):
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} = context.barDimensions;
if (!options.highlights) 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;
let eX = round(isVertical ? X + tickOffset : X + barMargin + barOffset);
let eH = (TICKS_WIDTH * width);
let eY = isVertical ? Y + length - barMargin - barOffset: Y + tickOffset;
let hLeft = round((TICKS_WIDTH + TICKS_PADDING) * width);
let hRight = round(barWidth + TICKS_PADDING * width);
context.save();
for (; i < s; i++) {
let entry = options.highlights[i];
let eW = ticksLength * abs((entry.to - entry.from) / interval);
context.beginPath();
context.fillStyle = entry.color;
if (isVertical) {
if (hasLeft)
context.rect(eX - hLeft, eY, eH, -eW);
if (hasRight)
context.rect(eX + hRight, eY, eH, -eW);
eY -= eW;
}
else {
if (hasLeft)
context.rect(eX, eY - hLeft, eW, eH);
if (hasRight)
context.rect(eX, eY + hRight, eW, eH);
eX += eW;
}
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} ticksSize
* @param {number} deltaLen
* @param {boolean} hasLeft
* @param {boolean} hasRight
* @param {number} lineWidth
* @param {number} lineLength
*/
function drawLinearTicks(context, color, ticksSize, deltaLen,
hasLeft, hasRight, lineWidth, lineLength)
{
let {isVertical, length, barWidth, barOffset, barMargin,
pixelRatio, width, X, Y, ticksLength} = context.barDimensions;
let tickOffset = (width - barWidth) / 2;
let tickX, tickY;
let i = 0;
let tickLen = lineLength * width;
let tickLeft = tickOffset - TICKS_PADDING * width;
let tickRight = tickOffset + barWidth + tickLen + TICKS_PADDING * width;
let tickSpace = ticksLength / (ticksSize - deltaLen);
context.lineWidth = lineWidth * pixelRatio;
context.strokeStyle = color;
context.save();
for (; i < ticksSize; i++) {
if (isVertical) {
tickY = Y + length - barMargin - barOffset - i * tickSpace;
if (hasLeft) {
tickX = X + tickLeft;
drawLinearTick(context, tickX, tickY, round(tickX - tickLen),
tickY);
}
if (hasRight) {
tickX = X + tickRight;
drawLinearTick(context, tickX, tickY, round(tickX - tickLen),
tickY);
}
}
else {
tickX = X + barMargin + barOffset + i * tickSpace;
if (hasLeft) {
tickY = Y + tickLeft;
drawLinearTick(context, tickX, tickY, tickX,
round(tickY - tickLen));
}
if (hasRight) {
tickY = Y + tickRight;
drawLinearTick(context, tickX, round(tickY), tickX,
tickY - tickLen);
}
}
}
}
/* istanbul ignore next: private, not testable */
/**
* Prepares major ticks data
*
* @access private
* @param {LinearGaugeOptions} options
* @return {[boolean, boolean]}
*/
function prepareTicks(options) {
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 major ticks
*
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
*/
function drawLinearMajorTicks(context, options) {
let [hasLeft, hasRight] = prepareTicks(options);
let lineWidth = 2;
drawLinearTicks(context, options.colorMajorTicks, options.majorTicks.length,
1, hasLeft, hasRight, lineWidth, TICKS_WIDTH);
if (options.strokeTicks) {
let {isVertical, length, width, barWidth, barMargin, barOffset, X, Y,
ticksLength, pixelRatio} = context.barDimensions;
let rightTicks = (width - barWidth) / 2 + barWidth +
TICKS_PADDING * width;
let leftTicks = (width - barWidth) / 2 - TICKS_PADDING * width;
let sX, sY, eX, eY;
lineWidth *= pixelRatio;
if (isVertical) {
sY = Y + length - barMargin - barOffset + lineWidth / 2;
eY = sY - ticksLength - lineWidth;
if (hasLeft) {
eX = sX = round(X + leftTicks);
drawLinearTickStroke(context, sX, sY, eX, eY);
}
if (hasRight) {
eX = sX = round(X + rightTicks);
drawLinearTickStroke(context, sX, sY, eX, eY);
}
}
else {
sX = X + barMargin + barOffset - lineWidth / 2;
eX = sX + ticksLength + lineWidth;
if (hasLeft) {
eY = sY = round(Y + leftTicks);
drawLinearTickStroke(context, sX, sY, eX, eY);
}
if (hasRight) {
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] = prepareTicks(options);
drawLinearTicks(context, options.colorMajorTicks,
options.minorTicks * (options.majorTicks.length - 1), 0,
hasLeft, hasRight, 1, TICKS_WIDTH_MINOR);
}
/* 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} = context.barDimensions;
let ticks = options.majorTicks.length;
let hasLeft = options.numberSide !== 'right';
let hasRight = options.numberSide !== 'left';
let textHeight = FONT_NUMBERS * width / 200;
let i = 0;
let ticksWidth = (TICKS_WIDTH + TICKS_PADDING * 2) * width;
let numLeft = (width - barWidth) / 2 - ticksWidth;
let numRight = (width - barWidth) / 2 + barWidth + ticksWidth;
let textX, textY, textWidth, numberOffset, tick;
context.font = textHeight + 'px ' + options.fontNumbers;
context.fillStyle = options.colorNumbers;
context.lineWidth = 0;
context.textAlign = 'center';
for (; i < ticks; i++) {
tick = options.majorTicks[i];
numberOffset = i * ticksLength / (ticks - 1);
if (isVertical) {
textY = Y + length - barMargin - barOffset - numberOffset
+ textHeight / 3;
if (hasLeft) {
context.textAlign = 'right';
context.fillText(tick, X + numLeft, textY);
}
if (hasRight) {
context.textAlign = 'left';
context.fillText(tick, X + numRight, textY);
}
}
else {
textWidth = context.measureText(tick).width;
textX = X + barMargin + barOffset + numberOffset;
if (hasLeft) {
context.fillText(tick, textX, Y + numLeft);
}
if (hasRight) {
context.fillText(tick, textX, Y + numRight + textHeight);
}
}
}
}
/* 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 = FONT_TITLE * width / 200;
let textX = round(baseX + (isVertical ? width : length) / 2);
let textY = round(baseY + titleMargin / 2 -
(isVertical ? textHeight : textHeight / 2) -
.025 * (isVertical ? length : width));
context.save();
context.textAlign = 'center';
context.fillStyle = options.colorTitle;
context.font = textHeight + 'px ' + options.fontTitle;
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 = FONT_UNITS * width / 200;
let textX = round(baseX + (isVertical ? width : length) / 2);
let textY = round(baseY + (isVertical ? length : width) +
unitsMargin / 2 - textHeight / 2);
context.save();
context.textAlign = 'center';
context.fillStyle = options.colorTitle;
context.font = textHeight + 'px ' + options.fontUnits;
context.lineWidth = 0;
context.fillText(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} = context.barDimensions;
let hasLeft = options.needleSide !== 'right';
let hasRight = options.needleSide !== 'left';
let position = ticksLength *
(options.value - options.minValue) /
(options.maxValue - options.minValue);
let tickWidth = (TICKS_WIDTH + TICKS_PADDING) * 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) {
sY = round(Y + length - barMargin - barOffset - position);
if (hasLeft) {
sX = round(X + nLeft);
eX = sX + needleLength;
draw(context, options, sX, sY, eX, sY, needleLength);
}
if (hasRight) {
sX = round(X + nRight);
eX = sX - needleLength;
draw(context, options, sX, sY, eX, sY, needleLength);
}
}
else {
sX = round(X + barMargin + barOffset + position);
if (hasLeft) {
sY = round(Y + nLeft);
eY = sY + needleLength;
draw(context, options, sX, sY, sX, eY, needleLength);
}
if (hasRight) {
sY = round(Y + nRight);
eY = sY - needleLength;
draw(context, options, sX, sY, sX, eY, needleLength);
}
}
context.restore();
}
/* istanbul ignore next: private, not testable */
/**
* Returns needle color style
*
* @access private
* @param {Canvas2DContext} context
* @param {LinearGaugeOptions} options
* @param {number} length
* @return {CanvasGradient|string}
*/
function needleStyle(context, options, length) {
return options.colorNeedleEnd ?
drawings.linearGradient(context, options.colorNeedle,
options.colorNeedleEnd, length) :
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
*/
function drawLinearLineNeedle(context, options, sX, sY, eX, eY, length) {
context.lineWidth = options.needleWidth;
context.strokeStyle = needleStyle(context, options, length);
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
*/
function drawLinearArrowNeedle(context, options, sX, sY, eX, eY, length) {
let peakLength = round(length *.4);
let bodyLength = length - peakLength;
let isVertical = sX === eX;
let halfWidth = options.needleWidth / 2;
context.fillStyle = needleStyle(context, options, length);
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
context.barDimensions.isVertical &&
drawings.drawValueBox(context, options, value, x + w / 2,
y + h - (40 * (w / 300)), w);
}
/* 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) {
context.save();
let r = options.borderRadius;
let w1 = w - options.borderShadowWidth;
let w2 = w1 - options.borderOuterWidth * 2;
let w3 = w2 - options.borderMiddleWidth * 2;
let w4 = w3 - options.borderInnerWidth * 2;
let h1 = h - options.borderShadowWidth * 2;
let h2 = h1 - options.borderOuterWidth * 2;
let h3 = h2 - options.borderMiddleWidth * 2;
let h4 = h3 - options.borderInnerWidth * 2;
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;
if (options.borderOuterWidth) {
drawRectangle(context, r, x, y, w1, h1,
options.colorBorderOuter, options.colorBorderOuterEnd);
}
if (options.borderMiddleWidth) {
drawRectangle(context, --r, x2, y2, w2, h2,
options.colorBorderMiddle, options.colorBorderMiddleEnd);
}
if (options.borderInnerWidth) {
drawRectangle(context, --r, x3, y3, w3, h3,
options.colorBorderInner, options.colorBorderInnerEnd);
}
if (options.borderShadowWidth) {
context.shadowBlur = options.borderShadowWidth;
context.shadowColor = options.colorBorderShadow;
}
drawRectangle(context, r, x4, y4, w4, h4, options.colorPlate);
context.restore();
return [x4, y4, w4, h4];
}
/**
* Minimalistic HTML5 Canvas Linear Gauge
*/
export default class LinearGauge extends BaseGauge {
/**
* @constructor
* @param {LinearGaugeOptions} options
*/
constructor(options) {
options = Object.assign({}, defaultLinearGaugeOptions, options || {});
if (options.ticksWidth) TICKS_WIDTH = options.ticksWidth / 100;
if (options.ticksWidthMinor) TICKS_WIDTH_MINOR =
options.ticksWidthMinor / 100;
if (options.ticksPadding) TICKS_PADDING = options.ticksPadding / 100;
if (options.barLength) BAR_LENGTH = options.barLength / 100;
if (options.fontNumbersSize) FONT_NUMBERS =
options.fontNumbersSize / 100;
if (options.fontTitleSize) FONT_TITLE = options.fontTitleSize / 100;
if (options.fontUnitsSize) FONT_UNITS = options.fontUnitsSize / 100;
/* istanbul ignore else */
if (options.barStrokeWidth >= options.barWidth) {
options.barStrokeWidth = round(options.barWidth / 2);
}
//noinspection JSUndefinedPropertyAssignment
options.hasLeft = hasTicksBar('right', options);
//noinspection JSUndefinedPropertyAssignment
options.hasRight = hasTicksBar('left', options);
super(options);
}
/* istanbul ignore next */
/**
* Triggering linear gauge render on a canvas.
*
* @returns {LinearGauge}
*/
draw() {
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.drawBox = drawLinearPlate(context, options, x, y, w, h);
drawLinearBar(context, options, ...this.drawBox);
canvas.context.barDimensions = context.barDimensions;
drawLinearBarHighlights(context, options);
drawLinearMinorTicks(context, options);
drawLinearMajorTicks(context, options);
drawLinearMajorTicksNumbers(context, options);
drawLinearTitle(context, options);
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();
drawLinearBarProgress(canvas.context, options, ...this.drawBox);
drawLinearBarNeedle(canvas.context, options);
drawLinearValueBox(canvas.context, options, options.animatedValue ?
this.options.value : this.value, ...this.drawBox);
return this;
}
}
/**
* @typedef {object} ns
*/
/* istanbul ignore if */
if (typeof ns !== 'undefined') {
ns['LinearGauge'] = LinearGauge;
}
BaseGauge.initialize('LinearGauge', defaultLinearGaugeOptions);
module.exports = LinearGauge;