diff options
author | Yves Fischer <yvesf-git@xapek.org> | 2011-10-23 21:14:40 +0200 |
---|---|---|
committer | Yves Fischer <yvesf-git@xapek.org> | 2011-10-30 11:33:54 +0100 |
commit | 770ba5201f5c60b2bb36602ff9d359f641e33125 (patch) | |
tree | 2200757f04efbc281c4b3714bfe2e4fe911193b6 /schall/static/RGraph/libraries/RGraph.waterfall.js | |
parent | 77f5e686612898974e13f2a5df57c20e4e529363 (diff) | |
download | mini-octopus-770ba5201f5c60b2bb36602ff9d359f641e33125.tar.gz mini-octopus-770ba5201f5c60b2bb36602ff9d359f641e33125.zip |
Charting with flask, rgraph and custom "timeseries database"
Diffstat (limited to 'schall/static/RGraph/libraries/RGraph.waterfall.js')
-rw-r--r-- | schall/static/RGraph/libraries/RGraph.waterfall.js | 790 |
1 files changed, 790 insertions, 0 deletions
diff --git a/schall/static/RGraph/libraries/RGraph.waterfall.js b/schall/static/RGraph/libraries/RGraph.waterfall.js new file mode 100644 index 0000000..f3bf96c --- /dev/null +++ b/schall/static/RGraph/libraries/RGraph.waterfall.js @@ -0,0 +1,790 @@ +/** + * o------------------------------------------------------------------------------o + * | This file is part of the RGraph package - you can learn more at: | + * | | + * | http://www.rgraph.net | + * | | + * | This package is licensed under the RGraph license. For all kinds of business | + * | purposes there is a small one-time licensing fee to pay and for non | + * | commercial purposes it is free to use. You can read the full license here: | + * | | + * | http://www.rgraph.net/LICENSE.txt | + * o------------------------------------------------------------------------------o + */ + + if (typeof(RGraph) == 'undefined') RGraph = {}; + + /** + * The bar chart constructor + * + * @param object canvas The canvas object + * @param array data The chart data + */ + RGraph.Waterfall = function (id, data) + { + // Get the canvas and context objects + this.id = id; + this.canvas = document.getElementById(id); + this.context = this.canvas.getContext ? this.canvas.getContext("2d") : null; + this.canvas.__object__ = this; + this.type = 'waterfall'; + this.max = 0; + this.isRGraph = true; + this.coords = []; + + /** + * Compatibility with older browsers + */ + RGraph.OldBrowserCompat(this.context); + + + // Various config + this.properties = { + 'chart.background.barcolor1': 'rgba(0,0,0,0)', + 'chart.background.barcolor2': 'rgba(0,0,0,0)', + 'chart.background.grid': true, + 'chart.background.grid.color': '#ddd', + 'chart.background.grid.width': 1, + 'chart.background.grid.hsize': 20, + 'chart.background.grid.vsize': 20, + 'chart.background.grid.vlines': true, + 'chart.background.grid.hlines': true, + 'chart.background.grid.border': true, + 'chart.background.grid.autofit':true, + 'chart.background.grid.autofit.numhlines': 5, + 'chart.background.grid.autofit.numvlines': 20, + 'chart.background.grid.autofit.align': false, + 'chart.background.image': null, + 'chart.background.hbars': null, // ??? + 'chart.xaxispos': 'bottom', + 'chart.numyticks': 10, + 'chart.hmargin': 5, + 'chart.strokestyle': '#666', + 'chart.axis.color': 'black', + 'chart.gutter.left': 25, + 'chart.gutter.right': 25, + 'chart.gutter.top': 25, + 'chart.gutter.bottom': 25, + 'chart.labels': [], + 'chart.ylabels': true, + 'chart.text.color': 'black', + 'chart.text.size': 10, + 'chart.text.angle': 0, + 'chart.text.font': 'Verdana', + 'chart.ymax': null, + 'chart.title': '', + 'chart.title.color': 'black', + 'chart.title.background': null, + 'chart.title.hpos': null, + 'chart.title.vpos': null, + 'chart.title.bold': true, + 'chart.title.font': null, + 'chart.title.xaxis': '', + 'chart.title.yaxis': '', + 'chart.title.yaxis.bold': true, + 'chart.title.yaxis.size': null, + 'chart.title.yaxis.font': null, + 'chart.title.xaxis.pos': null, + 'chart.title.yaxis.pos': null, + 'chart.title.yaxis.align': 'left', + 'chart.title.xaxis.bold': true, + 'chart.title.xaxis.size': null, + 'chart.title.xaxis.font': null, + 'chart.colors': ['green', 'red', 'blue'], + 'chart.shadow': false, + 'chart.shadow.color': '#666', + 'chart.shadow.offsetx': 3, + 'chart.shadow.offsety': 3, + 'chart.shadow.blur': 3, + 'chart.tooltips': null, + 'chart.tooltips.effect': 'fade', + 'chart.tooltips.css.class': 'RGraph_tooltip', + 'chart.tooltips.event': 'onclick', + 'chart.tooltips.highlight': true, + 'chart.tooltips.override': null, + 'chart.highlight.stroke': 'black', + 'chart.highlight.fill': 'rgba(255,255,255,0.5)', + 'chart.contextmenu': null, + 'chart.units.pre': '', + 'chart.units.post': '', + //'chart.scale.decimals': 0, + //'chart.scale.point': '.', + //'chart.scale.thousand': ',', + //'chart.scale.formatter': null, + 'chart.crosshairs': false, + 'chart.crosshairs.color': '#333', + 'chart.crosshairs.hline': true, + 'chart.crosshairs.vline': true, + 'chart.annotatable': false, + 'chart.annotate.color': 'black', + 'chart.zoom.factor': 1.5, + 'chart.zoom.fade.in': true, + 'chart.zoom.fade.out': true, + 'chart.zoom.hdir': 'right', + 'chart.zoom.vdir': 'down', + 'chart.zoom.frames': 25, + 'chart.zoom.delay': 16.666, + 'chart.zoom.shadow': true, + 'chart.zoom.mode': 'canvas', + 'chart.zoom.thumbnail.width': 75, + 'chart.zoom.thumbnail.height': 75, + 'chart.zoom.background': true, + 'chart.resizable': false, + 'chart.resize.handle.background': null, + 'chart.noaxes': false, + 'chart.noxaxis': false, + 'chart.noyaxis': false, + 'chart.axis.color': 'black', + 'chart.total': true, + 'chart.multiplier.x': 1, + 'chart.multiplier.w': 1 + } + + // Check for support + if (!this.canvas) { + alert('[WATERFALL] No canvas support'); + return; + } + + // Store the data + this.data = data; + + + /** + * Set the .getShape commonly named method + */ + this.getShape = this.getBar; + } + + + /** + * A setter + * + * @param name string The name of the property to set + * @param value mixed The value of the property + */ + RGraph.Waterfall.prototype.Set = function (name, value) + { + this.properties[name.toLowerCase()] = value; + } + + + /** + * A getter + * + * @param name string The name of the property to get + */ + RGraph.Waterfall.prototype.Get = function (name) + { + return this.properties[name.toLowerCase()]; + } + + + /** + * The function you call to draw the bar chart + */ + RGraph.Waterfall.prototype.Draw = function () + { + // MUST be the first thing done! + if (typeof(this.Get('chart.background.image')) == 'string' && !this.__background_image__) { + RGraph.DrawBackgroundImage(this); + return; + } + + + /** + * Fire the onbeforedraw event + */ + RGraph.FireCustomEvent(this, 'onbeforedraw'); + + /** + * Clear all of this canvases event handlers (the ones installed by RGraph) + */ + RGraph.ClearEventListeners(this.id); + + /** + * This is new in May 2011 and facilitates indiviual gutter settings, + * eg chart.gutter.left + */ + this.gutterLeft = this.Get('chart.gutter.left'); + this.gutterRight = this.Get('chart.gutter.right'); + this.gutterTop = this.Get('chart.gutter.top'); + this.gutterBottom = this.Get('chart.gutter.bottom'); + + /** + * Stop the coords array from growing uncontrollably + */ + this.coords = []; + + /** + * This gets used a lot + */ + this.centery = ((this.canvas.height - this.gutterTop - this.gutterBottom) / 2) + this.gutterTop; + + /** + * Work out a few things. They need to be here because they depend on things you can change after you instantiate the object + */ + this.max = 0; + this.grapharea = this.canvas.height - this.gutterTop - this.gutterBottom; + this.graphwidth = this.canvas.width - this.gutterLeft - this.gutterRight; + this.halfTextHeight = this.Get('chart.text.size') / 2; + + + /** + * Work out the maximum value + * + * Normally the last bar is a total and not taken into account when the scale is generated. + * However it is not necessarily shown, so if it's not being shown it should not be + * accounted for. + */ + this.max = this.getMax(this.data); + this.scale = RGraph.getScale(typeof(this.Get('chart.ymax')) == 'number' ? this.Get('chart.ymax') : this.max, this); + this.max = this.scale[4]; + var decimals = this.Get('chart.scale.decimals'); + + this.scale = [ + (this.max * (1/5)).toFixed(decimals), + (this.max * (2/5)).toFixed(decimals), + (this.max * (3/5)).toFixed(decimals), + (this.max * (4/5)).toFixed(decimals), + typeof(this.max) == 'number' ? this.max.toFixed(decimals) : this.max + ]; + + + // Progressively Draw the chart + RGraph.background.Draw(this); + + this.DrawAxes(); + this.Drawbars(); + this.DrawLabels(); + + /** + * Setup the context menu if required + */ + if (this.Get('chart.contextmenu')) { + RGraph.ShowContext(this); + } + + + /** + * Draw crosschairs + */ + if (this.Get('chart.crosshairs')) { + RGraph.DrawCrosshairs(this); + } + + /** + * If the canvas is annotatable, do install the event handlers + */ + if (this.Get('chart.annotatable')) { + RGraph.Annotate(this); + } + + /** + * This bit shows the mini zoom window if requested + */ + if (this.Get('chart.zoom.mode') == 'thumbnail' || this.Get('chart.zoom.mode') == 'area') { + RGraph.ShowZoomWindow(this); + } + + + /** + * This function enables resizing + */ + if (this.Get('chart.resizable')) { + RGraph.AllowResizing(this); + } + + /** + * Tooltips + */ + if (this.Get('chart.tooltips')) { + + RGraph.Register(this); + + /** + * Install the onclick event handler for the tooltips + */ + var canvas_onclick_func = function (e) + { + e = RGraph.FixEventObject(e); + + var canvas = document.getElementById(this.id); + var context = canvas.getContext('2d'); + var obj = canvas.__object__; + var bar = obj.getBar(e); + + + /** + * Loop through the bars determining if the mouse is over a bar + */ + if (bar) { + + /** + * First, if the event is onmousemove end the tooltip is already being shown, do nothing + */ + if (obj.Get('chart.tooltips.event') == 'onmousemove' && RGraph.Registry.Get('chart.tooltip') && RGraph.Registry.Get('chart.tooltip').__index__ == bar[5]) { + return; + } + + /** + * Redraw the graph first, in effect resetting the graph to as it was when it was first drawn + * This "deselects" any already selected bar + */ + RGraph.Redraw(); + + var x = bar[1]; + var y = bar[2]; + var w = bar[3]; + var h = bar[4]; + + if (!obj.Get('chart.tooltips')[bar[5]]) { + return; + } + + + // Draw the highlight (if necessary) + if (obj.Get('chart.tooltips.highlight')) { + context.beginPath(); + context.fillStyle = obj.Get('chart.highlight.fill'); + context.strokeStyle = obj.Get('chart.highlight.stroke'); + context.strokeRect(x,y,w,h); + context.fillRect(x,y,w,h); + context.stroke(); + context.fill(); + } + + /** + * Get the tooltip text + */ + var text = RGraph.parseTooltipText(obj.Get('chart.tooltips'), bar[5]); + + canvas.style.cursor = text ? 'pointer' : 'default'; + + RGraph.Tooltip(canvas, text, e.pageX, e.pageY, bar[5]); + } + + /** + * Stop the event bubbling + */ + e.stopPropagation(); + + return false; + } + this.canvas.addEventListener(this.Get('chart.tooltips.event') == 'onclick' ? 'click' : 'mousemove', canvas_onclick_func, false); + RGraph.AddEventListener(this.id, this.Get('chart.tooltips.event') == 'onclick' ? 'click' : 'mousemove', canvas_onclick_func); + + /** + * Install the window onclick handler + */ + var window_onclick_func = function (){RGraph.Redraw();}; + window.addEventListener('click', window_onclick_func, false); + RGraph.AddEventListener('window_' + this.id, 'click', window_onclick_func); + + /** + * Install the onmousemove event handler for the tooltips + */ + var canvas_onmousemove_func = function (e) + { + e = RGraph.FixEventObject(e); + + var canvas = document.getElementById(this.id); + var context = canvas.getContext('2d'); + var obj = canvas.__object__; + var bar = obj.getBar(e); + + /** + * Loop through the bars determining if the mouse is over a bar + */ + if (bar) { + if (obj.Get('chart.tooltips')[bar[5]]) { + canvas.style.cursor = 'pointer'; + e.stopPropagation(); + } + + return; + } + + canvas.style.cursor = 'default'; + + /** + * Stop the event bubbling + */ + e.stopPropagation(); + + return false; + } + this.canvas.addEventListener('mousemove', canvas_onmousemove_func, false); + RGraph.AddEventListener(this.id, 'mousemove', canvas_onmousemove_func); + + + + + + + + + + + + + + } + + + /** + * Fire the RGraph ondraw event + */ + RGraph.FireCustomEvent(this, 'ondraw'); + } + + + /** + * Draws the charts axes + */ + RGraph.Waterfall.prototype.DrawAxes = function () + { + if (this.Get('chart.noaxes')) { + return; + } + + this.context.beginPath(); + this.context.strokeStyle = this.Get('chart.axis.color'); + this.context.lineWidth = 1; + + // Draw the Y axis + if (this.Get('chart.noyaxis') == false) { + this.context.moveTo(this.gutterLeft, this.gutterTop); + this.context.lineTo(this.gutterLeft, RGraph.GetHeight(this) - this.gutterBottom); + } + + // Draw the X axis + if (this.Get('chart.noxaxis') == false) { + // Center X axis + if (this.Get('chart.xaxispos') == 'center') { + this.context.moveTo(this.gutterLeft, ((this.canvas.height - this.gutterTop - this.gutterBottom) / 2) + this.gutterTop); + this.context.lineTo(this.canvas.width - this.gutterRight, ((this.canvas.height - this.gutterTop - this.gutterBottom) / 2) + this.gutterTop); + } else { + this.context.moveTo(this.gutterLeft, this.canvas.height - this.gutterBottom); + this.context.lineTo(this.canvas.width - this.gutterRight, this.canvas.height - this.gutterBottom); + } + } + + var numYTicks = this.Get('chart.numyticks'); + + // Draw the Y tickmarks + if (this.Get('chart.noyaxis') == false) { + + var yTickGap = (RGraph.GetHeight(this) - this.gutterTop - this.gutterBottom) / numYTicks; + + for (y=this.gutterTop; y < (this.canvas.height - this.gutterBottom); y += yTickGap) { + + if (this.Get('chart.xaxispos') == 'bottom' || (y != ((this.canvas.height - this.gutterTop - this.gutterBottom) / 2) + this.gutterTop)) { + this.context.moveTo(this.gutterLeft, y); + this.context.lineTo(this.gutterLeft - 3, y); + } + } + + /** + * If the X axis is not being shown, draw an extra tick + */ + if (this.Get('chart.noxaxis') || this.Get('chart.xaxispos') == 'center') { + this.context.moveTo(this.gutterLeft - 3, this.canvas.height - this.gutterBottom); + this.context.lineTo(this.gutterLeft, this.canvas.height - this.gutterBottom); + } + } + + + // Draw the X tickmarks + if (this.Get('chart.noxaxis') == false) { + + xTickGap = (this.canvas.width - this.gutterLeft - this.gutterRight ) / (this.data.length + (this.Get('chart.total') ? 1 : 0)); + + if (this.Get('chart.xaxispos') == 'center') { + yStart = ((this.canvas.height - this.gutterBottom - this.gutterTop) / 2) + this.gutterTop - 3; + yEnd = ((this.canvas.height - this.gutterBottom - this.gutterTop) / 2) + this.gutterTop + 3; + } else { + yStart = this.canvas.height - this.gutterBottom; + yEnd = (this.canvas.height - this.gutterBottom) + 3; + } + + for (x=this.gutterLeft + xTickGap; x<=RGraph.GetWidth(this) - this.gutterRight; x+=xTickGap) { + this.context.moveTo(x, yStart); + this.context.lineTo(x, yEnd); + } + + if (this.Get('chart.noyaxis')) { + this.context.moveTo(this.gutterLeft, yStart); + this.context.lineTo(this.gutterLeft, yEnd); + } + } + + /** + * If the Y axis is not being shown, draw an extra tick + */ + if (this.Get('chart.noyaxis') && this.Get('chart.noxaxis') == false) { + this.context.moveTo(this.gutterLeft, RGraph.GetHeight(this) - this.gutterBottom); + this.context.lineTo(this.gutterLeft, RGraph.GetHeight(this) - this.gutterBottom + 3); + } + + this.context.stroke(); + } + + + /** + * Draws the labels for the graph + */ + RGraph.Waterfall.prototype.DrawLabels = function () + { + var context = this.context; + var numYLabels = 5; // Make this configurable + var interval = this.grapharea / numYLabels; + var font = this.Get('chart.text.font'); + var size = this.Get('chart.text.size'); + var color = this.Get('chart.text.color'); + var units_pre = this.Get('chart.units.pre'); + var units_post = this.Get('chart.units.post'); + + this.context.beginPath(); + this.context.fillStyle = color; + + /** + * First, draw the Y labels + */ + if (this.Get('chart.ylabels')) { + if (this.Get('chart.xaxispos') == 'center') { + + var halfInterval = interval / 2; + var halfWay = ((this.canvas.height - this.gutterTop - this.gutterBottom) / 2) + this.gutterTop; + + for (var i=0; i<numYLabels; ++i) { + RGraph.Text(context, + font, + size, + this.gutterLeft - 5, + this.gutterTop + (i * halfInterval), + RGraph.number_format(this, this.scale[4 - i], units_pre, units_post), + 'center', + 'right'); + } + + for (var i=0; i<numYLabels; ++i) { + RGraph.Text(context, + font, + size, + this.gutterLeft - 5, + halfWay + (i * halfInterval) + halfInterval, + '-' + RGraph.number_format(this, this.scale[i], units_pre, units_post), + 'center', + 'right'); + } + + } else { + + for (var i=1; i<=numYLabels; ++i) { + RGraph.Text(context, + font, + size, + this.gutterLeft - 5, + this.canvas.height - this.gutterBottom - (i * interval), + RGraph.number_format(this, this.scale[i - 1], units_pre, units_post), + 'center', + 'right'); + + } + } + } + + + + /** + * Now, draw the X labels + */ + if (this.Get('chart.labels').length > 0) { + + // Recalculate the interval for the X labels + interval = (this.canvas.width - this.gutterLeft - this.gutterRight) / this.Get('chart.labels').length; + + var halign = 'center'; + var angle = this.Get('chart.text.angle'); + + if (angle) { + halign = 'right'; + angle *= -1; + } + + var labels = this.Get('chart.labels'); + + for (var i=0; i<labels.length; ++i) { + + // context, font, size, x, y, text[, valign[, halign[, border[, angle[, background[, bold[, indicator]]]]]]] + RGraph.Text(context, + font, + size, + this.gutterLeft + (i * interval) + (interval / 2), + this.canvas.height - this.gutterBottom + 5 + this.halfTextHeight, + labels[i], + 'center', + halign, + null, + angle); + + } + } + + this.context.stroke(); + this.context.fill(); + } + + + /** + * Draws the bars on to the chart + */ + RGraph.Waterfall.prototype.Drawbars = function () + { + var context = this.context; + var canvas = this.canvas; + var hmargin = this.Get('chart.hmargin'); + var runningTotal = 0; + + + for (var i=0; i<this.data.length; ++i) { + context.beginPath(); + context.strokeStyle = this.Get('chart.strokestyle'); + + var x = this.gutterLeft + hmargin + (((this.graphwidth / (this.data.length + (this.Get('chart.total') ? 1 : 0))) * i) * this.Get('chart.multiplier.x')); + var y = this.gutterTop + this.grapharea - (i == 0 ? ((this.data[0] / this.max) * this.grapharea) : (this.data[i] > 0 ? ((runningTotal + this.data[i]) / this.max) * this.grapharea : (runningTotal / this.max) * this.grapharea)); + var w = ((this.canvas.width - this.gutterLeft - this.gutterRight) / (this.data.length + (this.Get('chart.total') ? 1 : 0 )) ) - (2 * this.Get('chart.hmargin')); + w = w * this.Get('chart.multiplier.w'); + var h = (Math.abs(this.data[i]) / this.max) * this.grapharea; + + if (this.Get('chart.xaxispos') == 'center') { + h /= 2; + y = (i == 0 ? ((this.data[0] / this.max) * this.grapharea) : (this.data[i] > 0 ? ((runningTotal + this.data[i]) / this.max) * this.grapharea : (runningTotal / this.max) * this.grapharea)); + y = this.gutterTop + (this.grapharea/2) - (y / 2); + } + + // Color + context.fillStyle = this.data[i] >= 0 ? this.Get('chart.colors')[0] : this.Get('chart.colors')[1]; + + + if (this.Get('chart.shadow')) { + RGraph.SetShadow(this, this.Get('chart.shadow.color'), this.Get('chart.shadow.offsetx'), this.Get('chart.shadow.offsety'), this.Get('chart.shadow.blur')); + } else { + RGraph.NoShadow(this); + } + + context.strokeRect(x, y, w, h); + context.fillRect(x, y, w, h); + + this.coords.push([x, y, w, h]); + + runningTotal += this.data[i]; + + context.stroke(); + context.fill(); + } + +if (this.Get('chart.total')) { + + // This is the height of the final bar + h = (runningTotal / this.max) * (this.grapharea / (this.Get('chart.xaxispos') == 'center' ? 2 : 1)); + + /** + * Set the Y (ie the start point) value + */ + if (this.Get('chart.xaxispos') == 'center') { + y = runningTotal > 0 ? this.centery - h : this.centery - h; + } else { + y = this.canvas.height - this.gutterBottom - h; + } + + // This is the X position of the final bar + x = x + (this.Get('chart.hmargin') * 2) + w; + + + // Final color + this.context.fillStyle = this.Get('chart.colors')[2]; + + this.context.beginPath(); + this.context.strokeRect(x, y, w, h); + this.context.fillRect(x, y, w, h); + this.context.stroke(); + this.context.fill(); + + this.coords.push([x, y - (runningTotal > 0 ? 0 : Math.abs(h)), w, Math.abs(h)]); +} + + RGraph.NoShadow(this); + + /** + * This draws the connecting lines + */ + for (var i=1; i<this.coords.length; ++i) { + context.strokeStyle = 'gray'; + context.beginPath(); + if (this.data[i - 1] > 0) { + context.moveTo(this.coords[i - 1][0] + this.coords[i - 1][2], this.coords[i - 1][1]); + context.lineTo(this.coords[i - 1][0] + this.coords[i - 1][2] + (2 * hmargin), this.coords[i - 1][1]); + } else { + context.moveTo(this.coords[i - 1][0] + this.coords[i - 1][2], this.coords[i - 1][1] + this.coords[i - 1][3]); + context.lineTo(this.coords[i - 1][0] + this.coords[i - 1][2] + (2 * hmargin), this.coords[i - 1][1] + this.coords[i - 1][3]); + } + context.stroke(); + } + } + + + /** + * Not used by the class during creating the graph, but is used by event handlers + * to get the coordinates (if any) of the selected bar + * + * @param object e The event object + */ + RGraph.Waterfall.prototype.getBar = function (e) + { + var canvas = e.target; + var obj = e.target.__object__; + var mouseCoords = RGraph.getMouseXY(e); + + /** + * Loop through the bars determining if the mouse is over a bar + */ + for (var i=0; i<obj.coords.length; i++) { + + var mouseX = mouseCoords[0]; + var mouseY = mouseCoords[1]; + + var left = obj.coords[i][0]; + var top = obj.coords[i][1]; + var width = obj.coords[i][2]; + var height = obj.coords[i][3]; + + if ( mouseX >= left + && mouseX <= left + width + && mouseY >= top + && mouseY <= top + height) { + + return [obj, left, top, width, height, i]; + } + } + + return null; + } + + + /** + * The Waterfall is slightly different to Bar/Line charts so has this function to get the max value + */ + RGraph.Waterfall.prototype.getMax = function (data) + { + var runningTotal = 0; + var max = 0; + + for (var i=0; i<data.length; ++i) { + runningTotal += data[i]; + max = Math.max(max, Math.abs(runningTotal)); + } + + return max; + }
\ No newline at end of file |