if (typeof SQ4=='undefined'){
	SQ4 = {};
}

(function(){
	    var dateFormat = function () {
        var    token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
            timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
            timezoneClip = /[^-+\dA-Z]/g,
            pad = function (val, len) {
                val = String(val);
                len = len || 2;
                while (val.length < len) val = "0" + val;
                return val;
            };
    
        // Regexes and supporting functions are cached through closure
        return function (date, mask, utc) {
            var dF = dateFormat;
    
            // You can't provide utc if you skip other args (use the "UTC:" mask prefix)
            if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
                mask = date;
                date = undefined;
            }
    
            // Passing date through Date applies Date.parse, if necessary
            date = date ? new Date(date) : new Date;
            if (isNaN(date)) throw SyntaxError("invalid date");
    
            mask = String(dF.masks[mask] || mask || dF.masks["default"]);
    
            // Allow setting the utc argument via the mask
            if (mask.slice(0, 4) == "UTC:") {
                mask = mask.slice(4);
                utc = true;
            }
    
            var    _ = utc ? "getUTC" : "get",
                d = date[_ + "Date"](),
                D = date[_ + "Day"](),
                m = date[_ + "Month"](),
                y = date[_ + "FullYear"](),
                H = date[_ + "Hours"](),
                M = date[_ + "Minutes"](),
                s = date[_ + "Seconds"](),
                L = date[_ + "Milliseconds"](),
                o = utc ? 0 : date.getTimezoneOffset(),
                flags = {
                    d:    d,
                    dd:   pad(d),
                    ddd:  dF.i18n.dayNames[D],
                    dddd: dF.i18n.dayNames[D + 7],
                    m:    m + 1,
                    mm:   pad(m + 1),
                    mmm:  dF.i18n.monthNames[m],
                    mmmm: dF.i18n.monthNames[m + 12],
                    yy:   String(y).slice(2),
                    yyyy: y,
                    h:    H % 12 || 12,
                    hh:   pad(H % 12 || 12),
                    H:    H,
                    HH:   pad(H),
                    M:    M,
                    MM:   pad(M),
                    s:    s,
                    ss:   pad(s),
                    l:    pad(L, 3),
                    L:    pad(L > 99 ? Math.round(L / 10) : L),
                    t:    H < 12 ? "a"  : "p",
                    tt:   H < 12 ? "am" : "pm",
                    T:    H < 12 ? "A"  : "P",
                    TT:   H < 12 ? "AM" : "PM",
                    Z:    utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
                    o:    (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
                    S:    ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
                };
    
            return mask.replace(token, function ($0) {
                return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
            });
        };
    }();
    
    // Some common format strings
    dateFormat.masks = {
        "default":      "ddd mmm dd yyyy HH:MM:ss",
        shortDate:      "m/d/yy",
        mediumDate:     "mmm d, yyyy",
        longDate:       "mmmm d, yyyy",
        fullDate:       "dddd, mmmm d, yyyy",
        shortTime:      "h:MM TT",
        mediumTime:     "h:MM:ss TT",
        longTime:       "h:MM:ss TT Z",
        isoDate:        "yyyy-mm-dd",
        isoTime:        "HH:MM:ss",
        isoDateTime:    "yyyy-mm-dd'T'HH:MM:ss",
        isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
    };
    
    // Internationalization strings
    dateFormat.i18n = {
        dayNames: [
            "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
            "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
        ],
        monthNames: [
            "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
            "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
        ]
    };
    
    // For convenience...
    Date.prototype.format = function (mask, utc) {
        return dateFormat(this, mask, utc);
    };	
}());

SQ4.TimeConsts={
	milisec:1,
	sec:1000,
	minute:60*1000,
	hour:60*60*1000,
	day:24*60*60*1000,
	month:30*24*60*60*1000,
	year:365*24*60*60*1000
}

//Shared constants
SQ4.Consts={
	LOG_ERROR:0,
	LOG_INFO:2,
	LOG_DEBUG:5,
	LOG_TRACE:10
}

//Utils methods
SQ4.Utils = {	
	colors:{
		"aliceblue": "#f0f8ff",
		"antiquewhite": "#faebd7",
		"aqua": "#00ffff",
		"aquamarine": "#7fffd4",
		"azure": "#f0ffff",
		"beige": "#f5f5dc",
		"bisque": "#ffe4c4",
		"black": "#000000",
		"blanchedalmond": "#ffebcd",
		"blue": "#0000ff",
		"blueviolet": "#8a2be2",
		"brown": "#a52a2a",
		"burlywood": "#deb887",
		"cadetblue": "#5f9ea0",
		"chartreuse": "#7fff00",
		"chocolate": "#d2691e",
		"coral": "#ff7f50",
		"cornflowerblue": "#6495ed",
		"cornsilk": "#fff8dc",
		"crimson": "#dc143c",
		"cyan": "#00ffff",
		"darkblue": "#00008b",
		"darkcyan": "#008b8b",
		"darkgoldenrod": "#b8860b",
		"darkgray": "#a9a9a9",
		"darkgreen": "#006400",
		"darkkhaki": "#bdb76b",
		"darkmagenta": "#8b008b",
		"darkolivegreen": "#556b2f",
		"darkorange": "#ff8c00",
		"darkorchid": "#9932cc",
		"darkred": "#8b0000",
		"darksalmon": "#e9967a",
		"darkseagreen": "#8fbc8f",
		"darkslateblue": "#483d8b",
		"darkslategray": "#2f4f4f",
		"darkturquoise": "#00ced1",
		"darkviolet": "#9400d3",
		"deeppink": "#ff1493",
		"deepskyblue": "#00bfff",
		"dimgray": "#696969",
		"dodgerblue": "#1e90ff",
		"firebrick": "#b22222",
		"floralwhite": "#fffaf0",
		"forestgreen": "#228b22",
		"fuchsia": "#ff00ff",
		"gainsboro": "#dcdcdc",
		"ghostwhite": "#f8f8ff",
		"gold": "#ffd700",
		"goldenrod": "#daa520",
		"gray": "#808080",
		"green": "#008000",
		"greenyellow": "#adff2f",
		"honeydew": "#f0fff0",
		"hotpink": "#ff69b4",
		"indianred ": "#cd5c5c",
		"indigo": "#4b0082",
		"ivory": "#fffff0",
		"khaki": "#f0e68c",
		"lavender": "#e6e6fa",
		"lavenderblush": "#fff0f5",
		"lawngreen": "#7cfc00",
		"lemonchiffon": "#fffacd",
		"lightblue": "#add8e6",
		"lightcoral": "#f08080",
		"lightcyan": "#e0ffff",
		"lightgoldenrodyellow": "#fafad2",
		"lightgrey": "#d3d3d3",
		"lightgreen": "#90ee90",
		"lightpink": "#ffb6c1",
		"lightsalmon": "#ffa07a",
		"lightseagreen": "#20b2aa",
		"lightskyblue": "#87cefa",
		"lightslategray": "#778899",
		"lightsteelblue": "#b0c4de",
		"lightyellow": "#ffffe0",
		"lime": "#00ff00",
		"limegreen": "#32cd32",
		"linen": "#faf0e6",
		"magenta": "#ff00ff",
		"maroon": "#800000",
		"mediumaquamarine": "#66cdaa",
		"mediumblue": "#0000cd",
		"mediumorchid": "#ba55d3",
		"mediumpurple": "#9370d8",
		"mediumseagreen": "#3cb371",
		"mediumslateblue": "#7b68ee",
		"mediumspringgreen": "#00fa9a",
		"mediumturquoise": "#48d1cc",
		"mediumvioletred": "#c71585",
		"midnightblue": "#191970",
		"mintcream": "#f5fffa",
		"mistyrose": "#ffe4e1",
		"moccasin": "#ffe4b5",
		"navajowhite": "#ffdead",
		"navy": "#000080",
		"oldlace": "#fdf5e6",
		"olive": "#808000",
		"olivedrab": "#6b8e23",
		"orange": "#ffa500",
		"orangered": "#ff4500",
		"orchid": "#da70d6",
		"palegoldenrod": "#eee8aa",
		"palegreen": "#98fb98",
		"paleturquoise": "#afeeee",
		"palevioletred": "#d87093",
		"papayawhip": "#ffefd5",
		"peachpuff": "#ffdab9",
		"peru": "#cd853f",
		"pink": "#ffc0cb",
		"plum": "#dda0dd",
		"powderblue": "#b0e0e6",
		"purple": "#800080",
		"red": "#ff0000",
		"rosybrown": "#bc8f8f",
		"royalblue": "#4169e1",
		"saddlebrown": "#8b4513",
		"salmon": "#fa8072",
		"sandybrown": "#f4a460",
		"seagreen": "#2e8b57",
		"seashell": "#fff5ee",
		"sienna": "#a0522d",
		"silver": "#c0c0c0",
		"skyblue": "#87ceeb",
		"slateblue": "#6a5acd",
		"slategray": "#708090",
		"snow": "#fffafa",
		"springgreen": "#00ff7f",
		"steelblue": "#4682b4",
		"tan": "#d2b48c",
		"teal": "#008080",
		"thistle": "#d8bfd8",
		"tomato": "#ff6347",
		"turquoise": "#40e0d0",
		"violet": "#ee82ee",
		"wheat": "#f5deb3",
		"white": "#ffffff",
		"whitesmoke": "#f5f5f5",
		"yellow": "#ffff00",
		"yellowgreen": "#9acd32"
	},
	logLevel : -1,	
	logCaption:null,
	setLogConfig:function(level, logCaption){
		if (level=='error'){
			this.logLevel=0;
		}else if (level=='info'){
			this.logLevel=2;
		}else if (level=='debug'){
			this.logLevel=5;
		}else if (level=='trace'){
			this.logLevel=10;
		}
		
		this.logCaption = logCaption;
	},
	getDayOfYear:function(date){
		var start = new Date(date.getFullYear(), 0, 0);
		var diff = date - start;
		var oneDay = 1000 * 60 * 60 * 24;
		var day = Math.floor(diff / oneDay);
		return day;
	},
	getMinuteOfDay:function(date){
		var start = new Date(date);
		start.setHours(0,0,0,0);
		
		var diff = date - start;
		var oneDay = 1000 * 60;
		var day = Math.floor(diff / oneDay);
		return day;
	},	
	getHourOfDay:function(date){
		var start = new Date(date);
		start.setHours(0,0,0,0);
		
		var diff = date - start;
		var oneDay = 1000 * 60 * 60;
		var day = Math.floor(diff / oneDay);
		return day;
	},	
	getSuitableOpositeColor:function(color){
		var hex = this.colors[color];
		if (!hex){
			hex = color;
		}
		
		var rgb = this.hexToRgb(hex);
		var minMax = this.getMinMax(rgb);
		
		if (minMax.max<128){
			return 'white';
		}else{
			return 'black'
		}
	},
	colorNameToHex:function(name){
		return this.colors[name];
	},
	normalizeHexColor:function(color){
		var colorHex = this.colors[color];
		if (colorHex){
			return colorHex;
		}
		
		if (color[0] !== '#') color = "#"+color;
		if (color.length==4){
			return "#"+color[1]+color[1]+color[2]+color[2]+color[3]+color[3];
		}
		
		return color;
	},
	hexToRgb : function(hex){
		if (hex[0] === '#') hex = hex.substr(1);
		var r = parseInt(hex.slice(0,2), 16),
			g = parseInt(hex.slice(2,4), 16),
			b = parseInt(hex.slice(4,6), 16);
		return [r,g,b];
	},
	rgbToHex:function(r, g, b) {
		function getSafe(value){
			if (value<0) value=0;
			if (value>255) value=255;
			return value;
		}
		//take hex value
		let hexValue = (( getSafe(r) << 16) | ( getSafe(g) << 8) |  getSafe(b)).toString(16);
		//ensure that value has 6 letters size
		while(hexValue.length<6){
			hexValue = '0'+hexValue;
		}
		
		return hexValue;
	},
	minLength:function(array1,array2){
		var len1 = array1.length;
		var len2 = array2.length;
		
		return len1<len2?len1:len2;
	},
	mergeMinMax:function(minMax1, minMax2){
		var min = null;
		var max = null;
		
		if (minMax1.min!=null){
			if (minMax2.min!=null){
				min = minMax1.min<minMax2.min?minMax1.min:minMax2.min;
			}else{
				min = minMax1.min;
			}
		}else{
			min = minMax2.min;
		}
		
		if (minMax1.max!=null){
			if (minMax2.max!=null){
				max = minMax1.max>minMax2.max?minMax1.max:minMax2.max;
			}else{
				max = minMax1.max;
			}
		}else{
			max = minMax2.max;
		}
		
		return {min:min, max:max};
	},
	getSizeFromFont:function(font){
		var pxIndex = font.indexOf('px');
		var sizePart = font.substr(0, pxIndex);
		return sizePart*1;
	},
	getMinMax : function(data){
		var min = null;
		var max = null;
		
		function evalMinMax(value){
			if (min==null || min>value){
				min = value;
			}
			
			if (max==null || max<value){
				max = value;
			}		
		}

		
		data.forEach(function(value){	
			if (typeof value!='object'){
				evalMinMax(value);
			}else{
				for (var attr in value) {
					evalMinMax(value[attr]);
				}
			}			
		});
		
		return {min:min, max:max};
	},
	getNormalizedMinMax:function(data){
		var minMax = SQ4.Utils.getMinMax(data);
		if (minMax.min==minMax.max){
			var delta = Math.abs(minMax.min*0.5);
			if (delta==0){
				delta = 1;
			}
			minMax.min -=delta;
			minMax.max +=delta;
		}
		
		return minMax;
	},
	extend : function(first, second) {
		for (var secondProp in second) {
			var secondVal = second[secondProp];
			// Is this value an object?  If so, iterate over its properties, copying them over
			if (secondVal && Object.prototype.toString.call(secondVal) === "[object Object]") {
				first[secondProp] = first[secondProp] || {};
				this.extend(first[secondProp], secondVal);
			}
			else if (typeof(first[secondProp]) == 'undefined' || first[secondProp] == null){
				first[secondProp] = secondVal;
			}
		}
		return first;
	},
	log : function(message, level){
		if (this.logLevel>=level){
			var caption = this.logCaption && this.logCaption!=null ? this.logCaption+': ' : '';
			console.log(caption+message);
		}
	},
	getNearestRound:function(number, mustBeInt){
		var str = number+"";
		var length = str.length;
		
		var strResult = "";
		var foundZero = false;
			
		var firstNonZero = 0;
		var previousDot = false;
		for(var i=0;i<length;i++){
			var char = str.charAt(i);
			if (char!='0' && char!='.'){
				firstNonZero = previousDot?i-1:i;
				break;
			}
			
			if (char=='.'){
				previousDot = true;
			}else{
				previousDot = false;
			}
		}
		firstNonZero--;
			
		var index = 0;
		for(var i=0;i<length;i++){
			var char = str.charAt(i);
			var targetChar;
			
			if (char=='.'){
				targetChar = '.';
			}else if (index==firstNonZero){
				targetChar = '1';
				index++;
			}else{
				targetChar = '0';
				index++;
			}
			strResult += targetChar;
			
			
		}
		
		if (firstNonZero==-1){
			strResult = '1'+strResult;
		}
		
		var result = strResult*1;
		if (result<1){
			return mustBeInt? 1 : result;
		}
		
		var result10 = result/10;
		if (result10>number && this.isInteger(result10)){
			return result10;
		}
		
		var result5 = result/5;
		if (result5>number && this.isInteger(result5)){
			return result5;
		}
		
		var result4 = result/4;
		if (result4>number && this.isInteger(result4)){
			return result4;
		}	
		
		var halfResult = result/2;
		
		return halfResult>number && this.isInteger(halfResult)? halfResult : result;
	},
	getElementPosition : function(element) {
		var top = 0, left = 0;
		do {
			top += element.offsetTop  || 0;
			left += element.offsetLeft || 0;
			element = element.offsetParent;
		} while(element);

		return {
			top: top,
			left: left
		};
	},
	isInteger:function(n){
		return n % 1 === 0;
	},
	isNullOrUndefined:function(i){
		return i==null || typeof (i)=='undefined';
	},
	isString:function(s){
		return typeof s === 'string';
	},
	isObject:function(i){
		return typeof i === 'object'
	},
	isNumeric:function(n) {
		return !isNaN(parseFloat(n)) && isFinite(n);
	},
	parseSize:function(val){
		if (this.isNumeric(val)){
			return {val:val, units:'px'};
		}
		
		
		val = val.toLowerCase();
		var pxIndex = val.indexOf('px');
		if (pxIndex!=-1){
			var numPart = val.substr(0,pxIndex);
			return {val:numPart*1, units:'px'};
		}
		
		var pcIndex = val.indexOf('%');
		if (pcIndex!=-1){
			var numPart = val.substr(0,pcIndex);
			return {val:numPart*1, units:'%'};
		}
	},
	cleanDateAttributes:function(date, level){
		switch(level){
			case 'year':
				date.setMonth(0);
				date.setDate(1);
				date.setHours(0);
				date.setMinutes(0);
				date.setSeconds(0);
				date.setMilliseconds(0);						
				break;
			case 'month':
				date.setDate(1);
				date.setHours(0);
				date.setMinutes(0);
				date.setSeconds(0);
				date.setMilliseconds(0);
				break;		
			case 'day':
				date.setHours(0);
				date.setMinutes(0);
				date.setSeconds(0);
				date.setMilliseconds(0);
				break;		
			case 'hour':
				date.setMinutes(0);
				date.setSeconds(0);
				date.setMilliseconds(0);						
				break;		
			case 'minute':
				date.setSeconds(0);
				date.setMilliseconds(0);						
				break;		
			case 'second':
				date.setMilliseconds(0);						
				break;						
			}	
		},	
	getNearestDate:function(timestamp){
		var date = new Date(timestamp);
		SQ4.Utils.cleanDateAttributes(date, 'day');
		var dat1 = date.getTime();
			
		date.setDate(date.getDate()+1);
			
		var dat2 = date.getTime();
			
		var leftArea = timestamp-dat1;
		var rightArea = dat2 - timestamp; 
		if (leftArea<rightArea){
			return dat1;
		}else{
			return dat2;
		}		
	},
	getDecimalCount:function(number){
		var str = number+"";
		var index = str.indexOf('.');
		if (index==-1){
			return 0;
		}
		return str.length-index-1;
	},
	getDaysBetween:function(dat1,dat2){
		var delta = Math.abs(dat1-dat2);
		var days = delta/SQ4.TimeConsts.day;
		return days;
	},	
	capitalizeFirstLetter:function(string) {
		return string.charAt(0).toUpperCase() + string.slice(1);
	},
	subArray:function (array, from, to){
		var result = [];
		
		if (from==null || to==null){
			return result;
		}
		
		var index = 0;
		array.forEach(function(value){
			if (index>=from && index<=to){
				result.push(value);
			}
				
			index++;
		});
			
		return result;
	},
	getArrayIndexes:function(xVals, minX, maxX){
		var index1=null;
		var index2=null;
			
		var index = 0;
		xVals.forEach(function(x){
			var xVal = new Date(x).getTime();
			if (index1==null && xVal>=minX && xVal<=maxX){
				index1 = index;
			}
			if (xVal<=maxX && index1!=null){
				index2=index;
			}
				
			index++
		});
			
		return {index1:index1, index2:index2};
	}	
};

SQ4.AxisRenderer = {
	NumberRenderer : function(mustBeInt){
		var mustBeInt = mustBeInt;
		
		this.normalize = function(x){
			return Math.round(x);
		}
		
		this.addSeparators = function(nStr){
			nStr += '';
			var x = nStr.split('.');
			var x1 = x[0];
			var x2 = x.length > 1 ? '.' + x[1] : '';
			var rgx = /(\d+)(\d{3})/;
			while (rgx.test(x1)) {
					x1 = x1.replace(rgx, '$1' + ',' + '$2');
			}
			return x1 + x2;
		}
		
		this.getCaption = function(value, shortValues, format){
			if (mustBeInt){
				value = Math.trunc(value);
			}else if (typeof format!=='undefined'){
				return this.addSeparators(value.toFixed(format));
			}
			
			if (shortValues){
				var thousands = value/1000;
				if (Math.abs(thousands)>=1){
					return thousands+"K";
				}
			}
			
			return this.addSeparators(value)+"";
		}
		
		this.getNextValue = function(obj){
			if (typeof(obj.current)=='undefined' || obj.current==null){
				obj.current = obj.start;
			}else{
				obj.current+=obj.step;
			}
		}
		
		this.getStartStep = function(size, minMax, minimalStepSize, precision){
			var min = minMax.min;
			var max = minMax.max;
			var delta = max - min;
			
			if (delta===0){
				return {start:minMax.min, step:1, dformat:SQ4.Utils.getDecimalCount(minMax.min)};
			}
		
			var oneStepSize = size/minimalStepSize;
			var part = delta / oneStepSize;
		
			var step = SQ4.Utils.getNearestRound(part, mustBeInt);
			if (precision){
				var minStep = 1/Math.pow(10, precision);
				if (step<minStep){
					step = minStep;
				}else{
					step = step.toFixed(precision)*1;
				}
			}

			var avg = min/step;
			avg = Math.trunc(avg);
			var start = avg*step;
		
			if (start<min){
				start+=step;
			}
		
			return {start:start, step:step, dformat:SQ4.Utils.getDecimalCount(step)};
		}
	},
	DateRenderer : function(){
		this.format = null;
		
		this.normalize = function(x, step){
			var date = new Date(x);
			SQ4.Utils.cleanDateAttributes(date, step);
			var dat1 = date.getTime();
			
			date.setDate(date.getDate()+1);
			
			var dat2 = date.getTime();
			
			var leftArea = x-dat1;
			var rightArea = dat2 - x; 
			if (leftArea<rightArea){
				return dat1;
			}else{
				return dat2;
			}
		}		
		
		this.getCaption = function(value, shortValues,format){
			if (!format){
				format = this.format;
			}
			
			var formatParts = format.split(';');
			var result = "";
			formatParts.forEach(function(iFormat){
				if (result.length>0){
					result=result+"\n";
				}
				try{
					result=result+new Date(value).format(iFormat);
				}catch(e){
					console.log("Invalid date: ", value," format: ",iFormat);
					result=result+"-";
				}
			});
			return result;
		}
		
		this.getNextValue = function(obj){
			if (typeof(obj.current)=='undefined' || obj.current==null){
				obj.current = obj.start;
			}else{
				var date = new Date(obj.current);
			    switch(obj.step){
				case 'year':
					date.setFullYear(date.getFullYear()+1);						
					break;
				case 'month':
					date.setMonth(date.getMonth()+1);
					break;		
				case 'day':
					date.setDate(date.getDate()+1);
					break;	
				case 'thirdDay':
					date.setHours(date.getHours()+8);
					break;	
				case 'halfDay':
					date.setHours(date.getHours()+12);
					break;		
				case 'quaterDay':
					date.setHours(date.getHours()+6);
					break;						
				case 'hour':
					date.setHours(date.getHours()+1);						
					break;		
				case 'minute':
					date.setMinutes(date.getMinutes()+1);					
					break;		
				case 'second':
					date.setSeconds(date.getSeconds()+1);					
					break;	
				case 'milisec':
					date.setMilliseconds(date.getMilliseconds()+1);
					break;					
				}					
				SQ4.Utils.cleanDateAttributes(date, obj.step);
				obj.current=date.getTime();
			}
		}
		
		this.getStartStep = function(size, minMax, minimalStepSize, timeHelpData){
			var min = minMax.min;
			var max = minMax.max;
			var minMaxDelta = max - min;
		
			var oneStepCount = size/minimalStepSize;
			
			var date = new Date(minMax.min);
			var start = minMax.min;
			
			var step;
			var suggestFormat = null;
			
			var bestRecord = null;
			var bestCount = 0;
			timeHelpData.forEach(function(record){
				var iCount = minMaxDelta/record.size;
				if (iCount<0 || iCount>oneStepCount){
					return;
				}
				
				var delta = oneStepCount - iCount;
				if (bestRecord==null || delta<bestCount){
					bestRecord = record;
					bestCount = delta;
				}
			});
			
			if (bestRecord==null){
				bestRecord = timeHelpData[0];
			}
			
			this.format = bestRecord.format;
			var result = {start:start, step:bestRecord.step, current:start, format:this.format};
			this.getNextValue(result);
			var nextValue = result.current;
			
			var firstSpaceLength = nextValue-start;
			var differenceFromFullSize = bestRecord.size - firstSpaceLength;
			var avgFromFullSize = differenceFromFullSize/bestRecord.size;
			if (avgFromFullSize>0.05){
				result.start = result.current;
			}
			
			result.current = result.start;
			return result;
		}	
	}
};

SQ4.CanvasHelper = function(canvas, borders){
	var me = this;
	this.ctx = canvas.getContext("2d");
	this.borders = borders;
	this.heights = [];
	this.ratio = window.devicePixelRatio || 1;
		
	this.setSmoothing = function(enabled){
		this.ctx.webkitImageSmoothingEnabled = enabled;
		this.ctx.mozImageSmoothingEnabled = enabled;
		this.ctx.imageSmoothingEnabled = enabled;
	}
	
	this.clear = function(color){
		SQ4.Utils.log('Clearing', SQ4.Consts.LOG_DEBUG);		
		this.drawFilledRectangle(0, 0, canvas.width,canvas.height, color);
	};
	
	this.drawFilledPolygon = function(points, strokeColor, fillcolor){
		this.ctx.save();
		this.ctx.fillStyle = fillcolor;
		this.ctx.strokeStyle = strokeColor;
		
		this.ctx.translate(0.5,0.5);
		
		this.ctx.beginPath();
		var first = true;
		points.forEach(function(point){
			if (first){
				me.ctx.moveTo(Math.round(point.x), Math.round(point.y));
				first = false;
			}else{
				me.ctx.lineTo(Math.round(point.x), Math.round(point.y));
			}
		});
		this.ctx.closePath();
		this.ctx.fill();
		this.ctx.stroke();
		this.ctx.restore();		
	}
	
	this.closePath = function(){
		this.ctx.closePath();
	}
	
	this.getWidth = function(){
		return canvas.width;
	}
	
	this.getHeight = function(){
		return canvas.height;
	}
	
	this.drawRoundedRectangle = function(x, y, width, height, options){
		this.ctx.save();		
		
		var stroke, fill, fillColor, strokeColor, radius;
		
		stroke = options.stroke;
		fill = options.fill;
		fillColor = options.fillColor;
		strokeColor = options.strokeColor;
		radius = options.radius;
		
		if (fillColor && fill){
			this.ctx.fillStyle = fillColor;
		}
		
		if (strokeColor && stroke){
			this.ctx.strokeStyle = strokeColor;
		}
		
		if (typeof stroke == "undefined" ) {
		  stroke = true;
		}
		if (typeof radius === "undefined") {
		  radius = 5;
		}
		
		this.ctx.beginPath();
		
		var antialias = options && options.antialias===true;
		if (!antialias){
			this.ctx.translate(0.5,0.5);
			x = Math.round(x);
			y = Math.round(y);
			width = Math.round(width);
			height = Math.round(height);
			radius = Math.round(radius);
		}
		
		this.ctx.moveTo(x + radius, y);
		this.ctx.lineTo(x + width - radius, y);
		this.ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
		this.ctx.lineTo(x + width, y + height - radius);
		this.ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
		this.ctx.lineTo(x + radius, y + height);
		this.ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
		this.ctx.lineTo(x, y + radius);
		this.ctx.quadraticCurveTo(x, y, x + radius, y);
		this.ctx.closePath();
		 
		if (stroke) {
			this.ctx.stroke();
		}
		if (fill) {
			this.ctx.fill();
		}   
		this.ctx.restore();
	}
	
	this.drawFilledRectangle = function(x1, y1, width, height, color, options){
		SQ4.Utils.log('Drawing filled rectangle: '+x1+','+y1+' with color:'+color, SQ4.Consts.LOG_TRACE);
		this.ctx.save();
		
		if (options && options.opacity){
			this.ctx.globalAlpha = options.opacity;
		}
		
		var antialias = options && options.antialias===true;
		if (!antialias){
			x1 = Math.round(x1);
			y1 = Math.round(y1);
			width = Math.round(width);
			height = Math.round(height);
		}		
		
		this.ctx.fillStyle = color;
		this.ctx.fillRect(x1,y1,width,height);
		
		this.ctx.restore();
	}
	
	this.drawRectangle = function(x1, y1, color, width, height, options){
		SQ4.Utils.log('Drawing rectangle: '+x1+','+y1+' width:'+width+', height:'+height+' with color:'+color, SQ4.Consts.LOG_TRACE);
		this.ctx.save();
		this.ctx.beginPath();
		
		var antialias = options && options.antialias===true;
		if (!antialias){
			var x2 = x1+width;
			var y2 = y1+height;
			
			x1 = Math.round(x1);
			y1 = Math.round(y1);
			width = Math.round(x2)-x1;
			height = Math.round(y2)-y1;
		}
		
		this.ctx.strokeStyle = color;
		this.ctx.rect(x1,y1,width,height);
		this.ctx.stroke();
		this.ctx.restore();
	}	
	
	this.beginPath = function(x1, y1, options){
		this.ctx.save();
		
		if (options){
			if (options.lineDash){
				this.ctx.setLineDash(options.lineDash);
			}
			if (options.width){
				this.ctx.lineWidth=options.width;
			}
			if (options.strokeColor){
				this.ctx.strokeStyle=options.strokeColor;
			}
			if (options.fillColor){
				this.ctx.fillStyle=options.fillColor;
			}			
		}
		
		this.ctx.beginPath();
		this.ctx.moveTo(x1,y1);		
	}
	
	this.setLineWidth = function(width){		
		this.ctx.lineWidth = width;
	}
	
	this.lineTo = function(x2, y2){
		this.ctx.lineTo(x2,y2);
	}
	
	this.stroke = function(noRestore){
		this.ctx.stroke();
		if (noRestore!==false){
			this.ctx.restore();
		}
	}
	
	this.fill = function(noRestore){
		this.ctx.fill();
		if (noRestore!==false){
			this.ctx.restore();
		}
	}
	
	this.drawLine = function(x1, y1, x2, y2, color, options){
		SQ4.Utils.log('Drawing line: '+x1+','+y1+' - '+x2+','+y2+' with color:'+color, SQ4.Consts.LOG_TRACE);
		this.ctx.save();
		this.ctx.beginPath();
			
		var antialias = options && options.antialias===true;
		
		if (options){
			if (options.lineDash){
				this.ctx.setLineDash(options.lineDash);
			}
			if (options.width){
				this.ctx.lineWidth=options.width;
			}
		}
		
		if (!antialias){
			this.ctx.translate(0.5,0.5);
			x1 = Math.round(x1);
			x2 = Math.round(x2);
			y1 = Math.round(y1);
			y2 = Math.round(y2);
		}
		
		this.ctx.moveTo(x1,y1);
		this.ctx.lineTo(x2,y2);
		this.ctx.strokeStyle=color;
		this.ctx.stroke();
		this.ctx.restore();
	};
	
	this.moveTo = function(x,y){
		this.ctx.moveTo(x,y);
	}
	
	this.drawFilledCircle = function(x, y, color, radius){
		this.ctx.save();
		this.ctx.beginPath();
		this.ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
		this.ctx.fillStyle = color;
		this.ctx.fill();
		this.ctx.lineWidth = 1;
		this.ctx.strokeStyle = color;
		this.ctx.stroke();
		this.ctx.restore();
	};
	
	this.getTextWidth = function(caption, font){
		this.ctx.save();
		
		this.ctx.font = font;
		var lines = caption.split("\n");
		
		var width = 0;
		lines.forEach(function(line){
			var textSize = me.ctx.measureText(line);
			if (textSize.width>width){
				width = textSize.width;
			}
		});

		this.ctx.restore();
		return width;
	}
	
	this.drawTextRotated = function(caption, color, font, x, y){
		this.drawText(caption, color, font, x, y, {rotation:-90*Math.PI/180});
	};
	
	this.getTextHeightOld = function(font){
		var fontSize = SQ4.Utils.getSizeFromFont(font);
		var textHeight = fontSize?1.5*fontSize:1;
		return textHeight;
	}
	
	this.getTextHeight = function(font){
		return this.getTextHeightOld(font);
	}
	
	this.getTextHeightNew = function(fontStyle){
		var result = this.heights[fontStyle];

		if (!result)
		{
			var body = document.getElementsByTagName('body')[0];
			var dummy = document.createElement('div');

			var dummyText = document.createTextNode('M');
			dummy.appendChild(dummyText);
			dummy.setAttribute('style', fontStyle + ';position:absolute;top:0;left:-500px');
			body.appendChild(dummy);
			result = dummy.offsetHeight;

			this.heights[fontStyle] = result;
			body.removeChild(dummy);
		}

		return result;		
	}
	
	this.centerText = function(caption, color, font, borders, options){
		var textWidth = this.getTextWidth(caption, font);
		
		if (textWidth>borders.width && (!options || !options.force)){
			return false;
		}

		var x = borders.x+borders.width/2-textWidth/2;
		var y = borders.y+borders.height/2;	
		
		if (options && typeof(options.minX)!='undefined' && options.minX>x){
			x = options.minX;
		}
		
		this.drawText(caption, color, font, x, y);
		
		return true;
	};
	
	this.drawText = function(caption, color, font, x, y, options){
		this.ctx.save();
		this.ctx.font = font;
		this.ctx.fillStyle = color;

		SQ4.Utils.log('Drawing text to :'+x+', '+y+' with color:'+color+' and font:'+font, SQ4.Consts.LOG_DEBUG);
		if (options && options.rotation){
			this.ctx.translate(x, y );
			this.ctx.rotate(options.rotation);
			x = 0;
			y = 0;
		}
		
		this.ctx.textAlign="left";
		this.ctx.fillText(caption, x, y);
		this.ctx.restore();
	};
	
	this.getImageData = function(x,y,width,height){
		return this.ctx.getImageData(x,y,width,height);
	}
	
	this.putImageData = function(data, x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight){
		if (typeof dirtyX=='undefined'){
			dirtyX = 0;
		}
		if (typeof dirtyY=='undefined'){
			dirtyY = 0;
		}
		if (typeof dirtyWidth=='undefined'){
			dirtyWidth = data.width;
		}	
		if (typeof dirtyHeight=='undefined'){
			dirtyHeight = data.height;
		}			
		this.ctx.putImageData(data,x,y, dirtyX, dirtyY, dirtyWidth, dirtyHeight);
	}
	
	this.updateCanvasSize = function(canvas, canvasWrapper, widthInfo, heightInfo){
		function setCanvasSize(attr, info){
			if (!info){
				return;
			}
			
			var success = true;
			var valueForSet;
			if (info.units=='px'){
				valueForSet=info.val;
			}else if (info.units=='%'){						
				var offsetAttr = 'offset'+SQ4.Utils.capitalizeFirstLetter(attr);
				var wraperParent = canvasWrapper.parentElement;
				
				var valueFromParent = wraperParent[offsetAttr];
				valueForSet = valueFromParent*(info.val/100);	

				if (valueFromParent==0){
					success = false;
				}
			}
			
			valueForSet = Math.trunc(valueForSet);
			canvas[attr] = valueForSet*me.ratio;
			canvas.style[attr] = valueForSet+'px';	

			return success;			
		}
	
		setCanvasSize('width', widthInfo);
		var result = setCanvasSize('height', heightInfo);
		
		canvasWrapper.style.height=canvas.style.height;		
		canvasWrapper.style.width=canvas.style.width;	
		
		return result;
	}
};
