Home Reference Source

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;