// o---------------------------------------------------------------------------------o // | This file is part of the RGraph package - you can learn more at: | // | | // | https://www.rgraph.net/license.html | // | | // | RGraph is dual-licensed under the Open Source GPL license. That means that it's | // | free to use and there are no restrictions on what you can use RGraph for! | // | If the GPL license does not suit you however, then there's an inexpensive | // | commercial license option available. See the URL above for more details. | // o---------------------------------------------------------------------------------o RGraph = window.RGraph || {isrgraph:true,isRGraph:true,rgraph:true}; // // The progress bar constructor // RGraph.SemiCircularProgress = function (conf) { this.id = conf.id; this.canvas = document.getElementById(this.id); this.context = this.canvas.getContext('2d'); this.canvas.__object__ = this; this.min = RGraph.stringsToNumbers(conf.min); this.max = RGraph.stringsToNumbers(conf.max); this.value = RGraph.stringsToNumbers(conf.value); this.type = 'semicircularprogress'; this.isRGraph = true; this.isrgraph = true; this.rgraph = true; this.currentValue = null; this.uid = RGraph.createUID(); this.canvas.uid = this.canvas.uid ? this.canvas.uid : RGraph.createUID(); this.colorsParsed = false; this.coords = []; this.coordsText = []; this.original_colors = []; this.firstDraw = true; // After the first draw this will be false this.stopAnimationRequested = false;// Used to control the animations this.properties = { backgroundColor: 'rgba(0,0,0,0)', backgroundGrid: false, backgroundGridMargin: 20, backgroundGridColor: '#ddd', backgroundGridLinewidth: 1, backgroundGridCircles: true, backgroundGridRadials: true, backgroundGridRadialsCount: 10, backgroundFill: true, backgroundFillColor: null, colors: ['#0c0', '#f66', '#66f', 'yellow', 'pink','#ccc','#cc0','#0cc','#c0c'], linewidth: 2, colorsStroke: '#666', marginLeft: 35, marginRight: 35, marginTop: 35, marginBottom: 35, radius: null, centerx: null, centery: null, width: null, anglesStart: Math.PI, anglesEnd: (2 * Math.PI), scale: false, scaleMin: null, scaleMax: null, // Defaults to the charts max value scaleDecimals: 0, scalePoint: '.', scaleThousand: ',', scaleFormatter: null, scaleUnitsPre: '', scaleUnitsPost: '', scaleLabelsCount: 10, scaleLabelsFont: null, scaleLabelsSize: null, scaleLabelsColor: null, scaleLabelsBold: null, scaleLabelsItalic: null, scaleLabelsOffsetr: 0, scaleLabelsOffsetx: 0, scaleLabelsOffsety: 0, shadow: false, shadowColor: 'rgba(220,220,220,1)', shadowBlur: 2, shadowOffsetx: 2, shadowOffsety: 2, labelsCenter: true, labelsCenterIndex: 0, labelsCenterFade: false, labelsCenterSize: 40, labelsCenterColor: null, labelsCenterBold: null, labelsCenterItalic: null, labelsCenterFont: null, labelsCenterValign: 'bottom', labelsCenterOffsetx: 0, labelsCenterOffsety: 0, labelsCenterThousand: ',', labelsCenterPoint: '.', labelsCenterDecimals: 0, labelsCenterUnitsPost: '', labelsCenterUnitsPre: '', labelsCenterSpecific: null, labelsMin: true, labelsMinColor: null, labelsMinFont: null, labelsMinBold: null, labelsMinSize: null, labelsMinItalic: null, labelsMinOffsetAngle: 0, labelsMinOffsetx: 0, labelsMinOffsety: 5, labelsMinThousand: ',', labelsMinPoint: '.', labelsMinDecimals: 0, labelsMinUnitsPost: '', labelsMinUnitsPre: '', labelsMinSpecific: null, labelsMax: true, labelsMaxColor: null, labelsMaxFont: null, labelsMaxBold: null, labelsMaxSize: null, labelsMaxItalic: null, labelsMaxOffsetAngle: 0, labelsMaxOffsetx: 0, labelsMaxOffsety: 5, labelsMaxThousand: ',', labelsMaxPoint: '.', labelsMaxDecimals: 0, labelsMaxUnitsPost: '', labelsMaxUnitsPre: '', labelsMaxSpecific: null, title: '', titleBold: null, titleItalic: null, titleFont: null, titleSize: null, titleColor: null, titleHalign: null, titleValign: null, titleOffsetx: 0, titleOffsety: 0, titleSubtitle: '', titleSubtitleSize: null, titleSubtitleColor: '#aaa', titleSubtitleFont: null, titleSubtitleBold: null, titleSubtitleItalic: null, titleSubtitleOffsetx: 0, titleSubtitleOffsety: 0, textSize: 12, textColor: 'black', textFont: 'Arial, Verdana, sans-serif', textBold: false, textItalic: false, textAccessible: false, textAccessibleOverflow: 'visible', textAccessiblePointerevents:false, text: null, contextmenu: null, tooltips: null, tooltipsEffect: 'slide', tooltipsOverride: null, tooltipsCssClass: 'RGraph_tooltip', tooltipsCss: null, tooltipsEvent: 'onclick', tooltipsHighlight: true, tooltipsHotspotXonly: false, tooltipsFormattedThousand: ',', tooltipsFormattedPoint: '.', tooltipsFormattedDecimals: 0, tooltipsFormattedUnitsPre: '', tooltipsFormattedUnitsPost: '', tooltipsFormattedKeyColors: null, tooltipsFormattedKeyColorsShape: 'square', tooltipsFormattedKeyColorsCss: null, tooltipsFormattedKeyLabels: [], tooltipsFormattedListType: 'ul', tooltipsFormattedListItems: null, tooltipsFormattedTableHeaders: null, tooltipsFormattedTableData: null, tooltipsPointer: true, tooltipsPointerCss: null, tooltipsPointerOffsetx: 0, tooltipsPointerOffsety: 0, tooltipsPositionStatic: true, tooltipsOffsetx: 0, tooltipsOffsety: 0, tooltipsHotspotIgnore: null, highlightStyle: null, highlightStroke: 'rgba(0,0,0,0)', highlightFill: 'rgba(255,255,255,0.7)', annotatable: false, annotatebleColor: 'black', annotatebleLinewidth: 1, key: null, keyBackground: 'white', keyPosition: 'margin', keyHalign: 'right', keyShadow: false, keyShadowColor: '#666', keyShadowBlur: 3, keyShadowOffsetx: 2, keyShadowOffsety: 2, keyPositionMarginBoxed: false, keyPositionMarginHSpace: 0, keyPositionX: null, keyPositionY: null, keyColorShape: 'square', keyRounded: true, keyLinewidth: 1, keyColors: null, keyInteractive: false, keyInteractiveHighlightChartStroke: 'rgba(0,0,0,0)', keyInteractiveHighlightChartFill: 'rgba(255,255,255,0.7)', keyInteractiveHighlightLabel: 'rgba(255,0,0,0.2)', keyLabelsColor: null, keyLabelsFont: null, keyLabelsSize: null, keyLabelsBold: null, keyLabelsItalic: null, keyLabelsOffsetx: 0, keyLabelsOffsety: 0, keyFormattedDecimals: 0, keyFormattedPoint: '.', keyFormattedThousand: ',', keyFormattedUnitsPre: '', keyFormattedUnitsPost: '', keyFormattedValueSpecific: null, keyFormattedItemsCount: null, adjustable: false, variant: 'default', clearto: 'rgba(0,0,0,0)' } // // Add the reverse look-up table for property names // so that property names can be specified in any case. // this.properties_lowercase_map = []; for (var i in this.properties) { if (typeof i === 'string') { this.properties_lowercase_map[i.toLowerCase()] = i; } } // Check for support if (!this.canvas) { alert('[SEMICIRCULARPROGRESS] No canvas support'); return; } // Easy access to properties and the path function var properties = this.properties; this.path = RGraph.pathObjectFunction; // // "Decorate" the object with the generic effects if the effects library has been included // if (RGraph.Effects && typeof RGraph.Effects.decorate === 'function') { RGraph.Effects.decorate(this); } // Add the responsive method. This method resides in the common file. this.responsive = RGraph.responsive; // // A generic setter // // @param string name The name of the property to set or it can also be an object containing // object style configuration // this.set = function (name) { var value = typeof arguments[1] === 'undefined' ? null : arguments[1]; // Go through all of the properties and make sure // that they're using the correct capitalisation if (typeof name === 'string') { name = this.properties_lowercase_map[name.toLowerCase()] || name; } // backgroundBackdrop => backgroundFill // backgroundBackdropColor => backgroundFillColor if (name === 'backgroundBackdrop') name = 'backgroundFill'; if (name === 'backgroundBackdropColor') name = 'backgroundFillColor'; // Set the colorsParsed flag to false if the colors // property is being set if ( name === 'colors' || name === 'keyColors' || name === 'backgroundColor' || name === 'backgroundFill' || name === 'backgroundFillOpacity' ) { this.colorsParsed = false; } // the number of arguments is only one and it's an // object - parse it for configuration data and return. if (arguments.length === 1 && typeof arguments[0] === 'object') { for (i in arguments[0]) { if (typeof i === 'string') { this.set(i, arguments[0][i]); } } return this; } properties[name] = value; return this; }; // // A generic getter // // @param string name The name of the property to get // this.get = function (name) { // Go through all of the properties and make sure // that they're using the correct capitalisation name = this.properties_lowercase_map[name.toLowerCase()] || name; return properties[name]; }; // // Draws the progress bar // this.draw = function () { // // Fire the onbeforedraw event // RGraph.fireCustomEvent(this, 'onbeforedraw'); // Translate half a pixel for antialiasing purposes - but only if it hasn't been // done already // // MUST be the first thing done! // if (!this.canvas.__rgraph_aa_translated__) { this.context.translate(0.5,0.5); this.canvas.__rgraph_aa_translated__ = true; } // // Constrain the value to be within the min and max // if (this.value > this.max) this.value = this.max; if (this.value < this.min) this.value = this.min; if (RGraph.isArray(this.value) && RGraph.arraySum(this.value) > (this.max - this.min) ) { alert('[SEMI-CIRCULAR PROGRESS] Total value is over the maximum value'); } // // Parse the colors. This allows for simple gradient syntax // if (!this.colorsParsed) { this.parseColors(); // Don't want to do this again this.colorsParsed = true; } // // Set the current value // this.currentValue = this.value; // // Make the margins easy ro access // this.marginLeft = properties.marginLeft; this.marginRight = properties.marginRight; this.marginTop = properties.marginTop; this.marginBottom = properties.marginBottom; // Figure out the width and height this.radius = Math.min( (this.canvas.width - properties.marginLeft - properties.marginRight) / 2, this.canvas.height - properties.marginTop - properties.marginBottom ); this.centerx = ((this.canvas.width - this.marginLeft - this.marginRight) / 2) + this.marginLeft; this.centery = this.canvas.height - this.marginBottom; this.width = this.radius / 3; // User specified centerx/y/radius if (typeof properties.radius === 'number') this.radius = properties.radius; if (typeof properties.centerx === 'number') this.centerx = properties.centerx; if (typeof properties.centery === 'number') this.centery = properties.centery; if (typeof properties.width === 'number') this.width = properties.width; // // Allow the centerx/centery/radius to be a plus/minus // if (typeof properties.radius === 'string' && properties.radius.match(/^\+|-\d+$/) ) this.radius += Number(properties.radius); if (typeof properties.width === 'string' && properties.width.match(/^\+|-\d+$/) ) this.width += Number(properties.width); if (typeof properties.centerx === 'string' && properties.centerx.match(/^\+|-\d+$/) ) this.centerx += Number(properties.centerx); if (typeof properties.centery === 'string' && properties.centery.match(/^\+|-\d+$/) ) this.centery += Number(properties.centery); this.coords = []; // // Stop this growing uncontrollably // this.coordsText = []; // // Install clipping // // MUST be the first thing that's done after the // beforedraw event // if (!RGraph.isNull(this.properties.clip)) { RGraph.clipTo.start(this, this.properties.clip); } // // Draw the meter // this.drawBackgroundGrid(); this.drawMeter(); this.drawLabels(); this.drawScale(); // // Setup the context menu if required // if (properties.contextmenu) { RGraph.showContext(this); } // Draw the key if necessary if (properties.key && properties.key.length) { RGraph.drawKey( this, properties.key, properties.colors ); } // // Add custom text thats specified // RGraph.addCustomText(this); // // This installs the event listeners // RGraph.installEventListeners(this); // // End clipping // if (!RGraph.isNull(this.properties.clip)) { RGraph.clipTo.end(); } // // Instead of using RGraph.common.adjusting.js, handle them here // this.allowAdjusting(); // // Fire the onfirstdraw event // if (this.firstDraw) { this.firstDraw = false; RGraph.fireCustomEvent(this, 'onfirstdraw'); this.firstDrawFunc(); } // // Fire the RGraph draw event // RGraph.fireCustomEvent(this, 'ondraw'); // // Install any inline responsive configuration. This // should be last in the draw function - even after // the draw events. // RGraph.installInlineResponsive(this); return this; }; // // Draw the bar itself // this.drawMeter = function () { // Reset the .coords array this.coords = []; // // The start/end angles // var start = properties.anglesStart, end = properties.anglesEnd; // // Calculate a scale // this.scale2 = RGraph.getScale({object: this, options: { 'scale.max': this.max, 'scale.strict': true, 'scale.min': this.min, 'scale.thousand': properties.scaleThousand, 'scale.point': properties.scalePoint, 'scale.decimals': properties.scaleDecimals, 'scale.labels.count': 5, 'scale.units.pre': properties.scaleUnitsPre, 'scale.units.post': properties.scaleUnitsPost }}); // Draw the backgroundColor if (properties.backgroundColor !== 'rgba(0,0,0,0)') { this.path( 'fs % fr % % % %', properties.backgroundColor, 0,0,this.canvas.width, this.canvas.height ); } // // Draw the background for the bar which is a similar // color to the bar - just faded out a bit. Now // (13/10/2024) you customise this color with the // backgroundFillColor property // if (properties.backgroundFill) { this.path( 'lw % b ', this.linewidth ); // Draw the path for the bar this.pathBar({ startValue: this.min, endValue: this.max }); // Finish the paths and stroke/fill it this.path( 'c s % f % sx % sy % sc % sb % f % sx 0 sy 0 sb 0 sc rgba(0,0,0,0) lw 1', properties.colorsStroke, properties.backgroundFillColor ? properties.backgroundFillColor: properties.colors[0], properties.shadowOffsetx, properties.shadowOffsety, properties.shadow ? properties.shadowColor : 'rgba(0,0,0,0)', properties.shadowBlur, properties.backgroundFillColor ? 'transparent' : 'rgba(255,255,255,0.85)' ); } // // Draw a single value on the meter // if (RGraph.isNumber(this.value)) { // Begin anew... this.context.beginPath(); // Draw the path for the indicator bar (not // stroking or filling it just yet) this.pathBar({ startValue: this.min, endValue: this.value }); // Close the path and fill the bar with the // requested color this.path('c f %', properties.colors[0]); this.coords = [[ this.centerx, this.centery, this.radius, this.getAngle(this.min), this.getAngle(this.value), this.width, this.getAngle(this.value) - this.getAngle(this.min) ]]; // Draw multiple values on the meter } else if (RGraph.isArray(this.value)) { for (var i=0,accValue=this.min; i 0 ); } }; // // The function that draws the labels // this.drawLabels = function () { // Draw the labelsMin label if (properties.labelsMin) { // // Allow for a specific label // if (!RGraph.isNull(properties.labelsMinSpecific)) { var text = properties.labelsMinSpecific; } else { var text = RGraph.numberFormat({ object: this, number: this.scale2.min.toFixed(typeof properties.labelsMinDecimals === 'number'? properties.labelsMinDecimals : properties.scaleDecimals), unitspre: properties.labelsMinUnitsPre, unitspost: properties.labelsMinUnitsPost, point: properties.labelsMinPoint, thousand: properties.labelsMinThousand }); } // Determine the horizontal and vertical alignment for the text if (properties.anglesStart === RGraph.PI) { var halign = 'center'; var valign = 'top'; } else if (properties.anglesStart <= RGraph.PI) { var halign = 'left'; var valign = 'center'; } else if (properties.anglesStart >= RGraph.PI) { var halign = 'right'; var valign = 'center'; } // Get the X/Y for the min label // cx, cy, angle, radius var xy = RGraph.getRadiusEndPoint( this.centerx, this.centery, properties.anglesStart + properties.labelsMinOffsetAngle, this.radius - (this.width / 2) ); var textConf = RGraph.getTextConf({ object: this, prefix: 'labelsMin' }); // Draw the min label RGraph.text({ object: this, font: textConf.font, size: textConf.size, color: textConf.color, bold: textConf.bold, italic: textConf.italic, x: xy[0] + properties.labelsMinOffsetx, y: xy[1] + properties.labelsMinOffsety, valign: valign, halign: halign, text: text }); } // Draw the labelsMax label if (properties.labelsMax) { // Determine the horizontal and vertical alignment for the text if (properties.anglesEnd === RGraph.TWOPI) { var halign = 'center'; var valign = 'top'; } else if (properties.anglesEnd >= RGraph.TWOPI) { var halign = 'right'; var valign = 'center'; } else if (properties.anglesEnd <= RGraph.TWOPI) { var halign = 'left'; var valign = 'center'; } // Get the formatted max label number // // Allow for a specific label // if (!RGraph.isNull(properties.labelsMaxSpecific)) { var text = properties.labelsMaxSpecific; } else { var text = RGraph.numberFormat({ object: this, number: this.scale2.max.toFixed(typeof properties.labelsMaxDecimals === 'number'? properties.labelsMaxDecimals : properties.scaleDecimals), unitspre: properties.labelsMaxUnitsPre, unitspost: properties.labelsMaxUnitsPost, point: properties.labelsMaxPoint, thousand: properties.labelsMaxThousand }); } // Get the X/Y for the max label // cx, cy, angle, radius var xy = RGraph.getRadiusEndPoint( this.centerx, this.centery, properties.anglesEnd + properties.labelsMaxOffsetAngle, this.radius - (this.width / 2) ); var textConf = RGraph.getTextConf({ object: this, prefix: 'labelsMax' }); // Draw the max label RGraph.text({ object: this, font: textConf.font, size: textConf.size, color: textConf.color, bold: textConf.bold, italic: textConf.italic, x: xy[0] + properties.labelsMaxOffsetx, y: xy[1] + properties.labelsMaxOffsety, valign: valign, halign: halign, text: text }); } // Draw the big label in the center if (properties.labelsCenter) { var textConf = RGraph.getTextConf({ object: this, prefix: 'labelsCenter' }); // // Allow for a specific label // if (!RGraph.isNull(properties.labelsCenterSpecific)) { var text = properties.labelsCenterSpecific; } else { var text = RGraph.numberFormat({ object: this, number: RGraph.isNumber(this.value) ? this.value.toFixed( RGraph.isNumber(properties.labelsCenterDecimals) ? properties.labelsCenterDecimals : properties.scaleDecimals ) : this.value[properties.labelsCenterIndex].toFixed( RGraph.isNumber(properties.labelsCenterDecimals) ? properties.labelsCenterDecimals : properties.scaleDecimals ), unitspre: properties.labelsCenterUnitsPre, unitspost: properties.labelsCenterUnitsPost, point: properties.labelsCenterPoint, thousand: properties.labelsCenterThousand }) } var ret = RGraph.text({ object: this, font: textConf.font, size: textConf.size, color: textConf.color, bold: textConf.bold, italic: textConf.italic, x: this.centerx + properties.labelsCenterOffsetx, y: this.centery + properties.labelsCenterOffsety, valign: properties.labelsCenterValign, halign: 'center', text: text }); // Allows the center label to fade in if (properties.labelsCenterFade && ret.node) { ret.node.style.opacity = 0; var delay = 25, incr = 0.05; for (var i=0; i<20; ++i) { (function (index) { setTimeout(function () { ret.node.style.opacity = incr * index; }, delay * (index + 1)); })(i); } } } // Draw the title RGraph.drawTitle(this); }; // // Draws the background "grid" // this.drawBackgroundGrid = function () { if (properties.backgroundGrid) { var margin = properties.backgroundGridMargin; var outerRadius = this.radius + margin; var innerRadius = this.radius - this.width - margin; // Draw the background grid "circles" if (properties.backgroundGridCircles) { // Draw the outer arc this.path( 'b lw % a % % % % % false', properties.backgroundGridLinewidth, this.centerx, this.centery, outerRadius, properties.anglesStart, properties.anglesEnd ); // Draw the inner arc this.path( p = 'a % % % % % true c s %', this.centerx, this.centery, innerRadius, properties.anglesEnd, properties.anglesStart, properties.backgroundGridColor ); } // // Draw the background grid radials // if (properties.backgroundGridRadials) { // Calculate the radius increment var increment = (properties.anglesEnd - properties.anglesStart) / properties.backgroundGridRadialsCount; var angle = properties.anglesStart; for (var i=0; i= this.centerx && mouseY > this.centery ) { angle += RGraph.TWOPI; } if (angle < properties.anglesStart && mouseX > this.centerx) { angle = properties.anglesEnd; } if (angle < properties.anglesStart) { angle = properties.anglesStart; } var value = (((angle - properties.anglesStart) / (properties.anglesEnd - properties.anglesStart)) * (this.max - this.min)) + this.min; value = Math.max(value, this.min); value = Math.min(value, this.max); return value; }; // // Each object type has its own Highlight() function which highlights the appropriate shape // // @param object shape The shape to highlight // this.highlight = function (shape) { if (typeof properties.highlightStyle === 'function') { (properties.highlightStyle)(shape); } else { this.context.beginPath(); this.pathBar({ startAngle: shape.angleStart, endAngle: shape.angleEnd, index: shape.index }); this.path( 'c f % s % lw 1', properties.highlightFill, properties.highlightStroke ); } }; // // The getObjectByXY() worker method. Don't call this call: // // RGraph.ObjectRegistry.getObjectByXY(e) // // @param object e The event object // this.getObjectByXY = function (e) { var [mx, my] = RGraph.getMouseXY(e), rounded = properties.variant === 'rounded', cx = this.centerx cy = this.centery, start = properties.anglesStart, end = properties.anglesEnd, radius = this.radius; // Draw a Path so that the coords can be tested // (but don't stroke/fill it this.path( 'b a % % % % % false', cx, cy, radius, start - (rounded ? 0.25 : 0), end + (rounded ? 0.25 : 0) ); this.path( 'a % % % % % true', cx, cy, radius - this.width, end + (rounded ? 0.25 : 0), start - (rounded ? 0.25 : 0) ); return this.context.isPointInPath(mx, my) ? this : null; }; // // This function allows the progress to be adjustable. // UPDATE: Not any more // this.allowAdjusting = function () {}; // // This method handles the adjusting calculation for when the mouse is moved // // @param object e The event object // this.adjusting_mousemove = function (e) { // // Handle adjusting for the HProgress // if (properties.adjustable && RGraph.Registry.get('adjusting') && RGraph.Registry.get('adjusting').uid == this.uid) { var value = this.getValue(e); if (typeof value === 'number') { // Fire the onadjust event RGraph.fireCustomEvent(this, 'onadjust'); this.value = Number(value.toFixed(properties.scaleDecimals)); RGraph.redrawCanvas(this.canvas); } } }; // // This function returns the appropriate angle (in radians) // for the given value. // // @param int value The Y value you want the angle for // @returm int The angle // this.getAngle = function (value) { if (value > this.max) value = this.max; if (value < this.min) value = this.min; //if (value > (this.max - this.min) || value < this.min) { // return null; //} var angle = ( (value - this.min) / (this.max - this.min)) * (properties.anglesEnd - properties.anglesStart) angle += properties.anglesStart; return angle; }; // // This returns true/false as to whether the cursor is // over the chart area. The cursor does not necessarily // have to be over the bar itself - just the bar or the // background to the bar. // this.overChartArea = function (e) { var mouseXY = RGraph.getMouseXY(e), mouseX = mouseXY[0], mouseY = mouseXY[1] // Draw the background to the Progress but don't stroke or fill it // so that it can be tested with isPointInPath() this.context.beginPath(); this.pathBar({ startValue: this.min, endValue: this.max }); return this.context.isPointInPath(mouseX, mouseY); }; // // // this.parseColors = function () { // Save the original colors so that they can be restored when the canvas is reset if (this.original_colors.length === 0) { this.original_colors.backgroundColor = RGraph.arrayClone(properties.backgroundColor); this.original_colors.colors = RGraph.arrayClone(properties.colors); this.original_colors.keyColors = RGraph.arrayClone(properties.keyColors); } properties.colors[0] = this.parseSingleColorForGradient(properties.colors[0]); properties.colors[1] = this.parseSingleColorForGradient(properties.colors[1]); // keyColors var colors = properties.keyColors; if (colors) { for (var i=0; i