diff --git a/examples/negative.html b/examples/negative.html new file mode 100644 index 0000000..073ae6a --- /dev/null +++ b/examples/negative.html @@ -0,0 +1,34 @@ + +
+ + + + + + + + + ++var neg_data = [ + {"period": "2011-08-12", "a": 100}, + {"period": "2011-03-03", "a": 75}, + {"period": "2010-08-08", "a": 50}, + {"period": "2010-05-10", "a": 25}, + {"period": "2010-03-14", "a": 0}, + {"period": "2010-01-10", "a": -25}, + {"period": "2009-12-10", "a": -50}, + {"period": "2009-10-07", "a": -75}, + {"period": "2009-09-25", "a": -100} +]; +Morris.Line({ + element: 'graph', + data: neg_data, + xkey: 'period', + ykeys: ['a'], + labels: ['Series A'] +}); ++ diff --git a/morris.coffee b/morris.coffee index b87c5af..87dd540 100644 --- a/morris.coffee +++ b/morris.coffee @@ -37,6 +37,7 @@ class Morris.Line '#9440ed' ] ymax: 'auto' + ymin: 'auto 0' marginTop: 25 marginRight: 25 marginBottom: 30 @@ -87,12 +88,18 @@ class Morris.Line # Compute the vertical range of the graph if desired if typeof @options.ymax is 'string' and @options.ymax[0..3] is 'auto' - # use Array.concat to flatten arrays and find the max y value - ymax = Math.max.apply null, Array.prototype.concat.apply([], @series) - if @options.ymax.length > 5 - @options.ymax = Math.max parseInt(@options.ymax[5..], 10), ymax - else - @options.ymax = ymax + # use Array.concat to flatten arrays and find the max y value + ymax = Math.max.apply null, Array.prototype.concat.apply([], @series) + if @options.ymax.length > 5 + @options.ymax = Math.max parseInt(@options.ymax[5..], 10), ymax + else + @options.ymax = ymax + if typeof @options.ymin is 'string' and @options.ymin[0..3] is 'auto' + ymin = Math.min.apply null, Array.prototype.concat.apply([], @series) + if @options.ymin.length > 5 + @options.ymin = Math.min parseInt(@options.ymin[5..], 10), ymin + else + @options.ymin = ymin # Clear and redraw the graph # @@ -104,11 +111,14 @@ class Morris.Line @r = new Raphael(@el[0]) # calculate grid dimensions - left = @measureText(@options.ymax, @options.gridTextSize).width + @options.marginLeft + maxYLabelWidth = Math.max( + @measureText(@options.ymin, @options.gridTextSize).width, + @measureText(@options.ymax, @options.gridTextSize).width) + left = maxYLabelWidth + @options.marginLeft width = @el.width() - left - @options.marginRight height = @el.height() - @options.marginTop - @options.marginBottom dx = width / (@xmax - @xmin) - dy = height / @options.ymax + dy = height / (@options.ymax - @options.ymin) # quick translation helpers transX = (x) => @@ -117,13 +127,15 @@ class Morris.Line else left + (x - @xmin) * dx transY = (y) => - return @options.marginTop + height - y * dy + return @options.marginTop + height - (y - @options.ymin) * dy # draw y axis labels, horizontal lines - lineInterval = height / (@options.numLines - 1) - for i in [0..@options.numLines-1] - y = @options.marginTop + i * lineInterval - v = Math.round((@options.numLines - 1 - i) * @options.ymax / (@options.numLines - 1)) + yInterval = (@options.ymax - @options.ymin) / (@options.numLines - 1) + firstY = Math.ceil(@options.ymin / yInterval) * yInterval + lastY = Math.floor(@options.ymax / yInterval) * yInterval + for lineY in [firstY..lastY] by yInterval + v = Math.floor(lineY) + y = transY(v) @r.text(left - @options.marginLeft/2, y, v) .attr('font-size', @options.gridTextSize) .attr('fill', @options.gridTextColor) @@ -334,7 +346,7 @@ class Morris.Line # eg: commas(1234567) -> '1,234,567' # commas: (num) -> - Math.max(0, num).toFixed(0).replace(/(?=(?:\d{3})+$)(?!^)/g, ',') + num.toFixed(0).replace(/(?=(?:\d{3})+$)(?!^)/g, ',') window.Morris = Morris # vim: set et ts=2 sw=2 sts=2 diff --git a/morris.js b/morris.js index 9a4eaad..b762f64 100644 --- a/morris.js +++ b/morris.js @@ -26,6 +26,7 @@ pointSize: 4, lineColors: ['#0b62a4', '#7A92A3', '#4da74d', '#afd8f8', '#edc240', '#cb4b4b', '#9440ed'], ymax: 'auto', + ymin: 'auto 0', marginTop: 25, marginRight: 25, marginBottom: 30, @@ -50,7 +51,7 @@ }; Line.prototype.precalc = function() { - var ykey, ymax, _i, _j, _len, _ref, _ref2, _results, + var ykey, ymax, ymin, _i, _j, _len, _ref, _ref2, _results, _this = this; this.options.data.sort(function(a, b) { return (a[_this.options.xkey] < b[_this.options.xkey]) - (b[_this.options.xkey] < a[_this.options.xkey]); @@ -87,23 +88,32 @@ if (typeof this.options.ymax === 'string' && this.options.ymax.slice(0, 4) === 'auto') { ymax = Math.max.apply(null, Array.prototype.concat.apply([], this.series)); if (this.options.ymax.length > 5) { - return this.options.ymax = Math.max(parseInt(this.options.ymax.slice(5), 10), ymax); + this.options.ymax = Math.max(parseInt(this.options.ymax.slice(5), 10), ymax); } else { - return this.options.ymax = ymax; + this.options.ymax = ymax; + } + } + if (typeof this.options.ymin === 'string' && this.options.ymin.slice(0, 4) === 'auto') { + ymin = Math.min.apply(null, Array.prototype.concat.apply([], this.series)); + if (this.options.ymin.length > 5) { + return this.options.ymin = Math.min(parseInt(this.options.ymin.slice(5), 10), ymin); + } else { + return this.options.ymin = ymin; } } }; Line.prototype.redraw = function() { - var c, circle, columns, coords, dx, dy, height, hideHover, hilight, hover, hoverHeight, hoverMargins, hoverSet, i, label, labelBox, labelText, left, lineInterval, path, pointGrow, pointShrink, prevHilight, prevLabelMargin, s, seriesCoords, seriesPoints, touchHandler, transX, transY, updateHilight, updateHover, v, width, x, xLabel, xLabelMargin, y, yLabel, yLabels, _i, _j, _len, _len2, _ref, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, + var c, circle, columns, coords, dx, dy, firstY, height, hideHover, hilight, hover, hoverHeight, hoverMargins, hoverSet, i, label, labelBox, labelText, lastY, left, lineY, maxYLabelWidth, path, pointGrow, pointShrink, prevHilight, prevLabelMargin, s, seriesCoords, seriesPoints, touchHandler, transX, transY, updateHilight, updateHover, v, width, x, xLabel, xLabelMargin, y, yInterval, yLabel, yLabels, _i, _j, _len, _len2, _ref, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _this = this; this.el.empty(); this.r = new Raphael(this.el[0]); - left = this.measureText(this.options.ymax, this.options.gridTextSize).width + this.options.marginLeft; + maxYLabelWidth = Math.max(this.measureText(this.options.ymin, this.options.gridTextSize).width, this.measureText(this.options.ymax, this.options.gridTextSize).width); + left = maxYLabelWidth + this.options.marginLeft; width = this.el.width() - left - this.options.marginRight; height = this.el.height() - this.options.marginTop - this.options.marginBottom; dx = width / (this.xmax - this.xmin); - dy = height / this.options.ymax; + dy = height / (this.options.ymax - this.options.ymin); transX = function(x) { if (_this.xvals.length === 1) { return left + width / 2; @@ -112,18 +122,20 @@ } }; transY = function(y) { - return _this.options.marginTop + height - y * dy; + return _this.options.marginTop + height - (y - _this.options.ymin) * dy; }; - lineInterval = height / (this.options.numLines - 1); - for (i = 0, _ref = this.options.numLines - 1; 0 <= _ref ? i <= _ref : i >= _ref; 0 <= _ref ? i++ : i--) { - y = this.options.marginTop + i * lineInterval; - v = Math.round((this.options.numLines - 1 - i) * this.options.ymax / (this.options.numLines - 1)); + yInterval = (this.options.ymax - this.options.ymin) / (this.options.numLines - 1); + firstY = Math.ceil(this.options.ymin / yInterval) * yInterval; + lastY = Math.floor(this.options.ymax / yInterval) * yInterval; + for (lineY = firstY; firstY <= lastY ? lineY <= lastY : lineY >= lastY; lineY += yInterval) { + v = Math.floor(lineY); + y = transY(v); this.r.text(left - this.options.marginLeft / 2, y, v).attr('font-size', this.options.gridTextSize).attr('fill', this.options.gridTextColor).attr('text-anchor', 'end'); this.r.path("M" + left + "," + y + 'H' + (left + width)).attr('stroke', this.options.gridLineColor).attr('stroke-width', this.options.gridStrokeWidth); } prevLabelMargin = null; xLabelMargin = 50; - for (i = _ref2 = Math.ceil(this.xmin), _ref3 = Math.floor(this.xmax); _ref2 <= _ref3 ? i <= _ref3 : i >= _ref3; _ref2 <= _ref3 ? i++ : i--) { + for (i = _ref = Math.ceil(this.xmin), _ref2 = Math.floor(this.xmax); _ref <= _ref2 ? i <= _ref2 : i >= _ref2; _ref <= _ref2 ? i++ : i--) { labelText = this.options.parseTime ? i : this.columnLabels[this.columnLabels.length - i - 1]; label = this.r.text(transX(i), this.options.marginTop + height + this.options.marginBottom / 2, labelText).attr('font-size', this.options.gridTextSize).attr('fill', this.options.gridTextColor); labelBox = label.getBBox(); @@ -134,19 +146,19 @@ } } columns = (function() { - var _i, _len, _ref4, _results; - _ref4 = this.xvals; + var _i, _len, _ref3, _results; + _ref3 = this.xvals; _results = []; - for (_i = 0, _len = _ref4.length; _i < _len; _i++) { - x = _ref4[_i]; + for (_i = 0, _len = _ref3.length; _i < _len; _i++) { + x = _ref3[_i]; _results.push(transX(x)); } return _results; }).call(this); seriesCoords = []; - _ref4 = this.series; - for (_i = 0, _len = _ref4.length; _i < _len; _i++) { - s = _ref4[_i]; + _ref3 = this.series; + for (_i = 0, _len = _ref3.length; _i < _len; _i++) { + s = _ref3[_i]; seriesCoords.push($.map(s, function(y, i) { return { x: columns[i], @@ -154,7 +166,7 @@ }; })); } - for (i = _ref5 = seriesCoords.length - 1; _ref5 <= 0 ? i <= 0 : i >= 0; _ref5 <= 0 ? i++ : i--) { + for (i = _ref4 = seriesCoords.length - 1; _ref4 <= 0 ? i <= 0 : i >= 0; _ref4 <= 0 ? i++ : i--) { coords = seriesCoords[i]; if (coords.length > 1) { path = this.createPath(coords, this.options.marginTop, left, this.options.marginTop + height, left + width); @@ -162,17 +174,17 @@ } } seriesPoints = (function() { - var _ref6, _results; + var _ref5, _results; _results = []; - for (i = 0, _ref6 = seriesCoords.length - 1; 0 <= _ref6 ? i <= _ref6 : i >= _ref6; 0 <= _ref6 ? i++ : i--) { + for (i = 0, _ref5 = seriesCoords.length - 1; 0 <= _ref5 ? i <= _ref5 : i >= _ref5; 0 <= _ref5 ? i++ : i--) { _results.push([]); } return _results; })(); - for (i = _ref6 = seriesCoords.length - 1; _ref6 <= 0 ? i <= 0 : i >= 0; _ref6 <= 0 ? i++ : i--) { - _ref7 = seriesCoords[i]; - for (_j = 0, _len2 = _ref7.length; _j < _len2; _j++) { - c = _ref7[_j]; + for (i = _ref5 = seriesCoords.length - 1; _ref5 <= 0 ? i <= 0 : i >= 0; _ref5 <= 0 ? i++ : i--) { + _ref6 = seriesCoords[i]; + for (_j = 0, _len2 = _ref6.length; _j < _len2; _j++) { + c = _ref6[_j]; circle = this.r.circle(c.x, c.y, this.options.pointSize).attr('fill', this.options.lineColors[i]).attr('stroke-width', 1).attr('stroke', '#ffffff'); seriesPoints[i].push(circle); } @@ -184,16 +196,16 @@ hoverSet.push(hover); hoverSet.push(xLabel); yLabels = []; - for (i = 0, _ref8 = this.series.length - 1; 0 <= _ref8 ? i <= _ref8 : i >= _ref8; 0 <= _ref8 ? i++ : i--) { + for (i = 0, _ref7 = this.series.length - 1; 0 <= _ref7 ? i <= _ref7 : i >= _ref7; 0 <= _ref7 ? i++ : i--) { yLabel = this.r.text(0, this.options.hoverFontSize * 1.5 * (i + 1.5) - hoverHeight / 2, '').attr('fill', this.options.lineColors[i]).attr('font-size', this.options.hoverFontSize); yLabels.push(yLabel); hoverSet.push(yLabel); } updateHover = function(index) { - var i, maxLabelWidth, xloc, yloc, _ref9; + var i, maxLabelWidth, xloc, yloc, _ref8; hoverSet.show(); xLabel.attr('text', _this.columnLabels[index]); - for (i = 0, _ref9 = _this.series.length - 1; 0 <= _ref9 ? i <= _ref9 : i >= _ref9; 0 <= _ref9 ? i++ : i--) { + for (i = 0, _ref8 = _this.series.length - 1; 0 <= _ref8 ? i <= _ref8 : i >= _ref8; 0 <= _ref8 ? i++ : i--) { yLabels[i].attr('text', "" + _this.seriesLabels[i] + ": " + (_this.commas(_this.series[i][index]))); } maxLabelWidth = Math.max.apply(null, $.map(yLabels, function(l) { @@ -230,14 +242,14 @@ r: this.options.pointSize }, 25, 'linear'); hilight = function(index) { - var i, _ref10, _ref9; + var i, _ref8, _ref9; if (prevHilight !== null && prevHilight !== index) { - for (i = 0, _ref9 = seriesPoints.length - 1; 0 <= _ref9 ? i <= _ref9 : i >= _ref9; 0 <= _ref9 ? i++ : i--) { + for (i = 0, _ref8 = seriesPoints.length - 1; 0 <= _ref8 ? i <= _ref8 : i >= _ref8; 0 <= _ref8 ? i++ : i--) { seriesPoints[i][prevHilight].animate(pointShrink); } } if (index !== null && prevHilight !== index) { - for (i = 0, _ref10 = seriesPoints.length - 1; 0 <= _ref10 ? i <= _ref10 : i >= _ref10; 0 <= _ref10 ? i++ : i--) { + for (i = 0, _ref9 = seriesPoints.length - 1; 0 <= _ref9 ? i <= _ref9 : i >= _ref9; 0 <= _ref9 ? i++ : i--) { seriesPoints[i][index].animate(pointGrow); } updateHover(index); @@ -246,10 +258,10 @@ if (index === null) return hideHover(); }; updateHilight = function(x) { - var hoverIndex, _ref9, _results; + var hoverIndex, _ref8, _results; x -= _this.el.offset().left; _results = []; - for (hoverIndex = _ref9 = hoverMargins.length; _ref9 <= 0 ? hoverIndex <= 0 : hoverIndex >= 0; _ref9 <= 0 ? hoverIndex++ : hoverIndex--) { + for (hoverIndex = _ref8 = hoverMargins.length; _ref8 <= 0 ? hoverIndex <= 0 : hoverIndex >= 0; _ref8 <= 0 ? hoverIndex++ : hoverIndex--) { if (hoverIndex === 0 || hoverMargins[hoverIndex - 1] > x) { hilight(hoverIndex); break; @@ -362,7 +374,7 @@ }; Line.prototype.commas = function(num) { - return Math.max(0, num).toFixed(0).replace(/(?=(?:\d{3})+$)(?!^)/g, ','); + return num.toFixed(0).replace(/(?=(?:\d{3})+$)(?!^)/g, ','); }; return Line; diff --git a/morris.min.js b/morris.min.js index c364af4..b1708da 100644 --- a/morris.min.js +++ b/morris.min.js @@ -1 +1 @@ -((function(){var a,b;a=jQuery,b={},b.Line=function(){function c(c){if(!(this instanceof b.Line))return new b.Line(c);typeof c.element=="string"?this.el=a(document.getElementById(c.element)):this.el=a(c.element),this.options=a.extend({},this.defaults,c);if(this.options.data===void 0||this.options.data.length===0)return;this.el.addClass("graph-initialised"),this.precalc(),this.redraw()}return c.prototype.defaults={lineWidth:3,pointSize:4,lineColors:["#0b62a4","#7A92A3","#4da74d","#afd8f8","#edc240","#cb4b4b","#9440ed"],ymax:"auto",marginTop:25,marginRight:25,marginBottom:30,marginLeft:25,numLines:5,gridLineColor:"#aaa",gridTextColor:"#888",gridTextSize:12,gridStrokeWidth:.5,hoverPaddingX:10,hoverPaddingY:5,hoverMargin:10,hoverFillColor:"#fff",hoverBorderColor:"#ccc",hoverBorderWidth:2,hoverOpacity:.95,hoverLabelColor:"#444",hoverFontSize:12,smooth:!0,hideHover:!1,parseTime:!0},c.prototype.precalc=function(){var b,c,d,e,f,g,h,i,j=this;this.options.data.sort(function(a,b){return(a[j.options.xkey]=0;h<=0?a++:a--)i.push(a);return i}.apply(this),this.xmin=Math.min.apply(null,this.xvals),this.xmax=Math.max.apply(null,this.xvals),this.xmin===this.xmax&&(this.xmin-=1,this.xmax+=1);if(typeof this.options.ymax=="string"&&this.options.ymax.slice(0,4)==="auto")return c=Math.max.apply(null,Array.prototype.concat.apply([],this.series)),this.options.ymax.length>5?this.options.ymax=Math.max(parseInt(this.options.ymax.slice(5),10),c):this.options.ymax=c},c.prototype.redraw=function(){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,_,ba=this;this.el.empty(),this.r=new Raphael(this.el[0]),s=this.measureText(this.options.ymax,this.options.gridTextSize).width+this.options.marginLeft,I=this.el.width()-s-this.options.marginRight,h=this.el.height()-this.options.marginTop-this.options.marginBottom,f=I/(this.xmax-this.xmin),g=h/this.options.ymax,D=function(a){return ba.xvals.length===1?s+I/2:s+(a-ba.xmin)*f},E=function(a){return ba.options.marginTop+h-a*g},t=h/(this.options.numLines-1);for(o=0,T=this.options.numLines-1;0<=T?o<=T:o>=T;0<=T?o++:o--)M=this.options.marginTop+o*t,H=Math.round((this.options.numLines-1-o)*this.options.ymax/(this.options.numLines-1)),this.r.text(s-this.options.marginLeft/2,M,H).attr("font-size",this.options.gridTextSize).attr("fill",this.options.gridTextColor).attr("text-anchor","end"),this.r.path("M"+s+","+M+"H"+(s+I)).attr("stroke",this.options.gridLineColor).attr("stroke-width",this.options.gridStrokeWidth);y=null,L=50;for(o=U=Math.ceil(this.xmin),V=Math.floor(this.xmax);U<=V?o<=V:o>=V;U<=V?o++:o--)r=this.options.parseTime?o:this.columnLabels[this.columnLabels.length-o-1],p=this.r.text(D(o),this.options.marginTop+h+this.options.marginBottom/2,r).attr("font-size",this.options.gridTextSize).attr("fill",this.options.gridTextColor),q=p.getBBox(),y===null||y<=q.x?y=q.x+q.width+L:p.remove();d=function(){var a,b,c,d;c=this.xvals,d=[];for(a=0,b=c.length;a=0;X<=0?o++:o--)e=A[o],e.length>1&&(u=this.createPath(e,this.options.marginTop,s,this.options.marginTop+h,s+I),this.r.path(u).attr("stroke",this.options.lineColors[o]).attr("stroke-width",this.options.lineWidth));B=function(){var a,b;b=[];for(o=0,a=A.length-1;0<=a?o<=a:o>=a;0<=a?o++:o--)b.push([]);return b}();for(o=Y=A.length-1;Y<=0?o<=0:o>=0;Y<=0?o++:o--){Z=A[o];for(Q=0,S=Z.length;Q