From 157e7c4cd5cf7e08f93de3d0c9afff45c00b84dd Mon Sep 17 00:00:00 2001 From: Tony Holdstock-Brown Date: Thu, 21 Mar 2013 01:43:38 -0400 Subject: [PATCH] Adds legend to line graph w/ hover & toggle This commit adds a non customisable legend to the graph via a 'line_name' option for each line. It takes the colour of each line and the optional 'line_name' argument and creates a legend in the top right corner of the graph showing what each line represents. When you hover over either a thumbnail or label for a line, the line on the graph glows (with a drop-shadow), and you can toggle a line's visibility by clicking its label/thumbnail in the legend. This adds the following methods to the Line class: * glow - adds a glow to the line and dots * remove_glow - removes the glow from the line and dots * hide - hides the line, dots and area * show - shows the line, dots and area * toggle - toggles the line, dot and area visibility These methods provide the basis for our extra functionality and can be called on the line directly. There's also a @lines property of the LineChart class which saves all Lines created when drawing the graph. --- compiled/charts.js | 157 +++++++++++++++++- compiled/charts.min.js | 96 ++++++----- src/coffeescript/charts/line.coffee | 65 +++++++- src/coffeescript/charts/line_chart.coffee | 121 +++++++++++++- .../charts/line_chart_options.coffee | 5 +- 5 files changed, 381 insertions(+), 63 deletions(-) diff --git a/compiled/charts.js b/compiled/charts.js index caf1c84..a6c110f 100644 --- a/compiled/charts.js +++ b/compiled/charts.js @@ -17,6 +17,8 @@ if (global.module == undefined) { LineChartOptions = (function() { LineChartOptions.DEFAULTS = { + show_line: true, + show_legend: false, dot_size: 5, dot_color: "#00aadd", dot_stroke_color: "#fff", @@ -1112,6 +1114,9 @@ Line = (function() { this.height = height; this.width = width; this.options = options != null ? options : {}; + this.glow_elements = []; + this.line_set = this.r.set(); + this.visible = options['show_line'] === true ? true : false; } Line.prototype.draw = function() { @@ -1124,19 +1129,27 @@ Line = (function() { if (this.options.dot_size > 0) { this.draw_dots_and_tooltips(this.scaled_points, this.raw_points); } + this.line_set.push(path); + this.line_set.hover(function() { + return this.glow(); + }, function() { + return this.remove_glow(); + }, this, this); }; Line.prototype.draw_curve = function(path) { var curve; curve = this.r.path(path); - return curve.attr({ + curve.attr({ "stroke": this.options.line_color, "stroke-width": this.options.line_width }).toFront(); + this.line_set.push(curve); + return curve; }; Line.prototype.draw_area = function(path) { - var area, final_point, first_point, padded_height, points; + var final_point, first_point, padded_height, points; points = this.scaled_points; padded_height = this.height - this.options.y_padding; final_point = points[points.length - 1]; @@ -1144,13 +1157,13 @@ Line = (function() { path += "L " + final_point.x + ", " + padded_height + " "; path += "L " + first_point.x + ", " + padded_height + " "; path += "Z"; - area = this.r.path(path); - area.attr({ + this.area = this.r.path(path); + this.area.attr({ "fill": this.options.area_color, "fill-opacity": this.options.area_opacity, "stroke": "none" }); - return area.toBack(); + return this.area.toBack(); }; Line.prototype.draw_dots_and_tooltips = function() { @@ -1174,8 +1187,10 @@ Line = (function() { options.hover_enabled = !raw_point.options.show_dot; dot = new Dot(this.r, point, options); tooltip = new Tooltip(this.r, dot.element, raw_point.options.tooltip || raw_point.y, options.hover_enabled); + tooltip.hide(); dots.push(dot); tooltips.push(tooltip); + this.line_set.push(dot.element); if (raw_point.options.no_dot === true) { dot.hide(); } @@ -1194,6 +1209,49 @@ Line = (function() { } }; + Line.prototype.hide = function() { + this.area.hide(); + this.line_set.hide(); + this.remove_glow(); + return this.visible = false; + }; + + Line.prototype.show = function() { + this.area.show(); + this.line_set.show(); + return this.visible = true; + }; + + Line.prototype.toggle = function() { + if (this.visible) { + return this.hide(); + } else { + return this.show(); + } + }; + + Line.prototype.glow = function() { + if (!this.visible) { + return; + } + return this.line_set.forEach(function(item) { + return this.glow_elements.push(item.glow({ + opacity: 0.1 + })); + }, this); + }; + + Line.prototype.remove_glow = function() { + var i, _i, _len, _ref, _results; + _ref = this.glow_elements; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + i = _ref[_i]; + _results.push(i.remove()); + } + return _results; + }; + return Line; })(); @@ -1646,6 +1704,8 @@ LineChart = (function(_super) { this.all_points = []; this.line_indices = []; this.line_options = []; + this.lines = []; + this.legend = this.r.set(); } LineChart.prototype.add_line = function(args) { @@ -1885,11 +1945,17 @@ LineChart = (function(_super) { }; LineChart.prototype.draw_line = function(raw_points, points, options) { + var line; if (this.options.render === "bar") { - return new LineBar(this.r, raw_points, points, this.height, this.width, options).draw(); + line = new LineBar(this.r, raw_points, points, this.height, this.width, options); } else { - return new Line(this.r, raw_points, points, this.height, this.width, options).draw(); + line = new Line(this.r, raw_points, points, this.height, this.width, options); } + line.draw(); + if (!options['show_line']) { + line.hide(); + } + return this.lines.push(line); }; LineChart.prototype.clear = function() { @@ -1910,7 +1976,7 @@ LineChart = (function(_super) { for (i = _i = 0, _len = _ref1.length; _i < _len; i = ++_i) { line_indices = _ref1[i]; begin = line_indices[0], end = line_indices[1]; - raw_points = this.all_points.slice(begin, end + 1 || 9e9); + raw_points = this.all_points.slice(begin, +end + 1 || 9e9); if (this.options.multi_axis) { _ref2 = this.all_points.length > 2 ? this.create_scalers(raw_points) : this.create_scalers_for_single_point(), line_x = _ref2[0], line_y = _ref2[1]; } else { @@ -1946,6 +2012,81 @@ LineChart = (function(_super) { } } } + if (this.options['show_legend']) { + this.draw_legend(); + } + }; + + LineChart.prototype.draw_legend = function() { + var count, current_x, label, legend_x, legend_y, line_name, line_option, set, thumbnail, _i, _len, _ref; + this.legend.clear(); + current_x = 0; + _ref = this.line_options; + for (count = _i = 0, _len = _ref.length; _i < _len; count = ++_i) { + line_option = _ref[count]; + set = this.r.set(); + thumbnail = this.r.rect(current_x, 1, 15, 10).attr({ + fill: line_option['line_color'], + stroke: line_option['line_color'], + cursor: 'pointer', + 'stroke-opacity': 0 + }); + thumbnail.line = this.lines[count]; + if (line_option['show_line']) { + thumbnail.full = true; + } else { + thumbnail.attr({ + 'stroke-opacity': 1, + 'fill-opacity': 0 + }); + thumbnail.full = false; + } + set.push(thumbnail); + this.legend.push(thumbnail); + current_x += 23; + line_name = line_option['line_name'] || 'Line ' + count; + label = this.r.text(current_x, 0, line_name); + label.attr({ + fill: '#333', + cursor: 'pointer', + 'font-size': 10, + 'font-weight': 'normal', + 'text-anchor': 'start', + 'font-family': 'Helvetica' + }); + label.line = this.lines[count]; + label.thumbnail = thumbnail; + label.transform("...t0," + (label.getBBox()['y'] * -1)); + current_x += label.getBBox()['width'] + 25; + set.push(label); + this.legend.push(label); + set.hover(function() { + return this.line.glow(); + }, function() { + return this.line.remove_glow(); + }); + set.click(function() { + this.line.toggle(); + this.line.glow(); + thumbnail = this.thumbnail || this; + if (thumbnail.full) { + thumbnail.attr({ + 'stroke-opacity': 1, + 'fill-opacity': 0 + }); + return thumbnail.full = false; + } else { + thumbnail.attr({ + 'stroke-opacity': 0, + 'fill-opacity': 1 + }); + return thumbnail.full = true; + } + }); + } + legend_x = this.width - (this.legend.getBBox()['width'] + this.options.x_padding); + legend_y = (this.options.y_padding / 2) - (this.legend.getBBox()['height'] / 2); + return this.legend.transform("...t" + legend_x + "," + legend_y); }; return LineChart; diff --git a/compiled/charts.min.js b/compiled/charts.min.js index 91f2190..9d5bc8e 100644 --- a/compiled/charts.min.js +++ b/compiled/charts.min.js @@ -1,5 +1,5 @@ (function(){var global=window;if(global.module==undefined)global.module=function(r,E){var x=global[r];if(x==undefined)global[r]=x={};E(x)}; -module("Charts",function(r){var E;E=function(){function g(b){var a,c,d,e;c={};e=g.DEFAULTS;for(a in e){d=e[a];c[a]=d}for(a in b){d=b[a];if(b.hasOwnProperty(a))c[a]=d}return c}g.DEFAULTS={dot_size:5,dot_color:"#00aadd",dot_stroke_color:"#fff",dot_stroke_size:2,line_width:3,line_color:"#00aadd",smoothing:0.4,fill_area:true,area_color:"#00aadd",area_opacity:0.2,show_x_labels:true,show_y_labels:true,label_max:true,label_min:true,max_x_labels:10,max_y_labels:3,font_family:"Helvetica, Arial, sans-serif", +module("Charts",function(r){var E;E=function(){function g(b){var a,c,d,e;c={};e=g.DEFAULTS;for(a in e){d=e[a];c[a]=d}for(a in b){d=b[a];if(b.hasOwnProperty(a))c[a]=d}return c}g.DEFAULTS={show_line:true,show_legend:false,dot_size:5,dot_color:"#00aadd",dot_stroke_color:"#fff",dot_stroke_size:2,line_width:3,line_color:"#00aadd",smoothing:0.4,fill_area:true,area_color:"#00aadd",area_opacity:0.2,show_x_labels:true,show_y_labels:true,label_max:true,label_min:true,max_x_labels:10,max_y_labels:3,font_family:"Helvetica, Arial, sans-serif", x_label_size:14,y_label_size:14,label_format:"%m/%d",show_grid:false,x_padding:45,y_padding:40,multi_axis:false,scale:"linear",y_axis_scale:[],render:"line",bar_width:20};g.merge=function(b,a){var c,d,e;if(b==null)b={};if(a==null)a={};d={};for(c in b){e=b[c];d[c]=e}for(c in a){e=a[c];if(a.hasOwnProperty(c))d[c]=e}return d};return g}();var x;x=function(){function g(){}g.clone=function(b){var a,c;if(!(b!=null&&typeof b==="object"))return b;a=new b.constructor;for(c in b)a[c]=g.clone(b[c]);return a}; g.comma=function(b){var a,c,d;d=b.toString();b="";a=0;for(c=d.length-1;c>=0;){b+=d.charAt(a);if(c%3===0&&c!==0)b+=",";a++;c--}return b};g.commas=function(b){b=b.toString().split(".");return b.length===1?this.comma(b[0]):this.comma(b[0])+"."+b[1]};return g}();var B,C,M,N,O;M=function(g){switch(g){case 1:return"Jan";case 2:return"Feb";case 3:return"Mar";case 4:return"Apr";case 5:return"May";case 6:return"Jun";case 7:return"Jul";case 8:return"Aug";case 9:return"Sep";case 10:return"Oct";case 11:return"Nov"; case 12:return"Dec"}};O=function(g,b){var a;if(b==null)b=2;a=b>0?Math.pow(10,b):1;return x.commas(Math.round(g*a)/a)};N=function(g,b){var a,c;if(b==null)b=2;c=b>0?Math.pow(10,b):1;if(g>1E6){a=g/1E6;a=Math.round(a*c)/c;return a+"m"}else if(g>1E3){a=g/1E3;return Math.round(a*c)/c+"k"}else return Math.round(g*c)/c};C=function(){function g(b,a){this.r=b;this.format=a!=null?a:"";this.num=0;this.font_family="Helvetica, Arial, sans-serif";this.color="#333"}g.prototype.x=function(b){this.x_func=b;return this}; @@ -25,48 +25,52 @@ function(b,a,c){var d=this;if(c==null)c=200;return b.animate({opacity:a,"fill-op b,a)};return g}();r.Tooltip=D;var T;T=function(){function g(b,a,c,d,e,f){this.r=b;this.raw_points=a;this.scaled_points=c;this.height=d;this.width=e;this.options=f!=null?f:{};this.effective_height=this.height-this.options.y_padding;this.x_offset=this.options.bar_width/2}g.prototype.draw=function(){return this.draw_bars()};g.prototype.draw_bars=function(){var b,a,c,d,e,f,h,i,j,k;e=this.r.set();f=[];c=a=0;k=this.scaled_points;b=i=0;for(j=k.length;i=this.raw_points[a].y)a=b;if(this.raw_points[b].y=b.length)c=b.length-1;return[b[d],b[c]]};g.get_tangent=function(b,a){return[a.x-b.x,a.y-b.y]};return g}();r.Bezier=I;var U;U=function(){function g(b,a,c,d,e,f){this.r=b;this.raw_points=a;this.scaled_points=c;this.height=d;this.width=e;this.options=f!=null?f:{}}g.prototype.draw=function(){var b;b=I.create_path(this.scaled_points,this.options.smoothing);this.options.fill_area&&this.draw_area(b);this.draw_curve(b);this.options.dot_size>0&&this.draw_dots_and_tooltips(this.scaled_points, -this.raw_points)};g.prototype.draw_curve=function(b){return this.r.path(b).attr({stroke:this.options.line_color,"stroke-width":this.options.line_width}).toFront()};g.prototype.draw_area=function(b){var a,c,d;c=this.scaled_points;d=this.height-this.options.y_padding;a=c[c.length-1];c=c[0];b+="L "+a.x+", "+d+" ";b+="L "+c.x+", "+d+" ";b+="Z";b=this.r.path(b);b.attr({fill:this.options.area_color,"fill-opacity":this.options.area_opacity,stroke:"none"});return b.toBack()};g.prototype.draw_dots_and_tooltips= -function(){var b,a,c,d,e,f,h,i,j,k,m;i=this.scaled_points;h=this.raw_points;j=[];a=[];c=k=e=d=0;for(m=i.length;k=h[d].y)d=c;if(f.ythis.center_point.x){d="start";e=30}else{d="end";e=-30}f.attr({"text-anchor":d,x:c._realx+e,opacity:0});return c._label=f};b.prototype.add_grandchildren=function(a,c){if(a.children){c._children=[];return this.create_circles_along_radius(a.children,this.child_radius,this.outer_radius2,function(d,e){c._active= -false;return c._children.push(e)})}};b.prototype.draw_circle=function(a,c,d,e){var f,h;f={fill:this.options.fill_color,stroke:"none"};h=this.r.set();h.push(this.r.circle(a,c,d).attr(f));h.push(this.r.text(a,c,"+").attr({"font-size":e,fill:"#fff"}));return h.attr({cursor:"pointer"}).toFront()};b.prototype.draw=function(){var a,c=this;a=this.draw_circle(this.center_point.x,this.center_point.y,this.main_radius,100);a._active=false;a._children=[];this.add_click_to_circle(a,true);this.create_circles_along_radius(this.children, -this.child_radius,this.outer_radius,function(d,e){c.add_grandchildren(d,e);c.add_click_to_circle(e);return a._children.push(e)});return a.toFront()};return b}(w);r.PathMenu=W;var Z,$;t={}.hasOwnProperty;s=function(g,b){function a(){this.constructor=g}for(var c in b)if(t.call(b,c))g[c]=b[c];a.prototype=b.prototype;g.prototype=new a;g.__super__=b.prototype;return g};$=function(g,b,a,c,d){var e,f,h;f=360/c*a;e=(90-f)*Math.PI/180;h=g+d*Math.cos(e);e=b-d*Math.sin(e);return{path:c===a?[["M",g,b-d],["A", -d,d,0,1,1,g-0.01,b-d]]:[["M",g,b-d],["A",d,d,0,+(f>180),1,h,e]]}};Z=function(g){function b(a){return b.__super__.constructor.call(this,a,b.DEFAULTS)}s(b,g);b.DEFAULTS={radius:55,stroke_width:30,font_color:"#333333",label_color:"#333333",fill_color:"#fff",stroke_color:"#81ae14",background_color:"#222222",text_shadow:false};return b}(v);v=function(g){function b(a,c,d,e){this.label=c;this.value=d;if(e==null)e={};b.__super__.constructor.call(this,a,new Z(e));this.center_point=new l(this.width/2,this.height/ -2);this.r.customAttributes.arc=$}s(b,g);b.prototype.draw=function(){var a;a=this.r.path().attr({"stroke-width":this.options.stroke_width,stroke:this.options.stroke_color,arc:[this.center_point.x,this.center_point.y,0,100,this.options.radius]});this.r.circle(this.center_point.x,this.center_point.y,this.options.radius).attr({fill:this.options.fill_color,stroke:"none","stroke-width":0});this.r.text(this.center_point.x,this.center_point.y,Math.round(this.value*100/100)+"%").attr({"font-size":this.options.radius/ -2.5,fill:this.options.font_color,"font-weight":"bold"});this.r.text(this.center_point.x,this.center_point.y+1.8*this.options.radius,this.label).attr({"font-size":this.options.radius/2.5,"font-weight":"bold",fill:this.options.label_color});this.options.text_shadow&&this.r.text(this.center_point.x,this.center_point.y+1.8*this.options.radius+1,this.label).attr({"font-size":this.options.radius/2.5,"font-weight":"bold",fill:this.options.text_shadow}).toBack();return a.animate({arc:[this.center_point.x, -this.center_point.y,this.value,100,this.options.radius]},1500,"<")};return b}(w);r.CircleProgress=v;t={}.hasOwnProperty;s=function(g,b){function a(){this.constructor=g}for(var c in b)if(t.call(b,c))g[c]=b[c];a.prototype=b.prototype;g.prototype=new a;g.__super__=b.prototype;return g};v=function(g){function b(a,c){if(c==null)c={};b.__super__.constructor.call(this,a,new E(c));this.padding=26;this.all_points=[];this.line_indices=[];this.line_options=[]}s(b,g);b.prototype.add_line=function(a){var c,d, -e,f,h;c=a.data;if(!(c.length<1)){e=[];f=0;for(h=c.length;f0?c:c+f;if(a.length===1){j=this.create_scalers_for_single_point();f=j[0];i=j[1]}else{j=this.create_scalers(a);f=j[0];i=j[1]}f=[];d=(new C(this.r,d)).x(function(){return h}).y(function(m){return i(a[m].y)}).size(e); -j=0;for(k=a.length;j1){a=Math.round(a);if(a===0)a=1}return a};b.prototype.draw_y_labels=function(a,c){var d,e,f,h,i,j;if(c==null)c=0;f=y.get_ranges_for_points(a);h=f[2];e=f[3];if(this.options.y_axis_scale.length===2){h=this.options.y_axis_scale;e=h[0];h=h[1]}if(h===e)return this._draw_y_labels([new l(0,h)],c);f=[];if(this.options.scale=== -"log"){d=new G;j=d(e);d=d(h);d=(d-j)/(this.options.max_y_labels-1);e=e;for(i=0;e<=h&&i1)f[f.length-1].y=Math.round(h);return this._draw_y_labels(f,c)};b.prototype.draw_x_label=function(a,c){var d,e,f;d=this.options.label_format;f=this.options.x_label_size;e=this.options.font_family;return(new B(this.r,c.x,this.height-f,a.is_date_type===true? -new Date(a.x):Math.round(a.x),d,f,e)).draw()};b.prototype.draw_x_labels=function(a,c){var d,e,f,h,i,j;e=[];h=this.options.max_x_labels;this.draw_x_label(a[0],c[0]);e.push(c[0].x);if(!(h<2)){f=c.length-1;this.draw_x_label(a[f],c[f]);e.push(c[f].x);if(!(h<3)){f=c.length-2;h=f/(h-1);d=Math.round(h);if(h!==d)h=d+1;for(d=h;d1?this.create_scalers(this.all_points):this.create_scalers_for_single_point();j=d[0];k=d[1];n=this.line_indices;d=m=0;for(o=n.length;m2?this.create_scalers(i):this.create_scalers_for_single_point();e=a[0];f=a[1]}else{e=j;f=k}c=function(){var p,q,u;u=[];p=0;for(q=i.length;pthis.index){e=c(this.index);c=this.render_bar(this.options.x_padding,d,e-this.options.x_padding);d=this.render_bar(e,d,h-e,this.options.bar2_color);f=new D(this.r,d,this.format_tooltip(a));f.translate(d.getBBox().width/ -2,0);c.mouseover(function(){return f.show()});return c.mouseout(function(){return f.hide()})}else{d=this.render_bar(this.options.x_padding,d,h-this.options.x_padding);f=new D(this.r,d,this.format_tooltip(a));return f.translate(d.getBBox().width/2,0)}};b.prototype.draw_guide_line=function(a,c,d,e){var f,h;if(e==null)e=1;h=new l(d,this.options.y_padding);f=new l(d,this.height);this.effects.vertical_dashed_line(h,f,this.options.dash_width).attr({fill:"rgba(0,0,0,"+e+")",stroke:"none"});e=(new C(this.r)).x(function(){return d}).y(function(i){return i* -15+15}).size(this.options.label_size).attr({fill:"rgba(0,0,0,"+e+")"});e.draw(a).attr({"font-weight":"bold"});return e.draw(c).attr({"font-size":10})};b.prototype.sort_bars_by_index=function(){var a,c;c=function(){var d,e,f,h;f=this.bars;h=[];d=0;for(e=f.length;d=b.length)c=b.length-1;return[b[d],b[c]]};g.get_tangent=function(b,a){return[a.x-b.x,a.y-b.y]};return g}();r.Bezier=I;var U;U=function(){function g(b,a,c,d,e,f){this.r=b;this.raw_points=a;this.scaled_points=c;this.height=d;this.width=e;this.options=f!=null?f:{};this.glow_elements=[];this.line_set=this.r.set();this.visible=f.show_line===true?true:false}g.prototype.draw=function(){var b;b=I.create_path(this.scaled_points,this.options.smoothing);this.options.fill_area&& +this.draw_area(b);this.draw_curve(b);this.options.dot_size>0&&this.draw_dots_and_tooltips(this.scaled_points,this.raw_points);this.line_set.push(b);this.line_set.hover(function(){return this.glow()},function(){return this.remove_glow()},this,this)};g.prototype.draw_curve=function(b){b=this.r.path(b);b.attr({stroke:this.options.line_color,"stroke-width":this.options.line_width}).toFront();this.line_set.push(b);return b};g.prototype.draw_area=function(b){var a,c,d;c=this.scaled_points;d=this.height- +this.options.y_padding;a=c[c.length-1];c=c[0];b+="L "+a.x+", "+d+" ";b+="L "+c.x+", "+d+" ";b+="Z";this.area=this.r.path(b);this.area.attr({fill:this.options.area_color,"fill-opacity":this.options.area_opacity,stroke:"none"});return this.area.toBack()};g.prototype.draw_dots_and_tooltips=function(){var b,a,c,d,e,f,h,i,j,k,m;i=this.scaled_points;h=this.raw_points;j=[];a=[];c=k=e=d=0;for(m=i.length;k=h[d].y)d=c;if(f.ythis.center_point.x){d="start";e=30}else{d="end";e=-30}f.attr({"text-anchor":d,x:c._realx+e,opacity:0});return c._label=f};b.prototype.add_grandchildren=function(a,c){if(a.children){c._children=[];return this.create_circles_along_radius(a.children,this.child_radius,this.outer_radius2,function(d,e){c._active=false;return c._children.push(e)})}};b.prototype.draw_circle=function(a,c,d,e){var f,h;f={fill:this.options.fill_color, +stroke:"none"};h=this.r.set();h.push(this.r.circle(a,c,d).attr(f));h.push(this.r.text(a,c,"+").attr({"font-size":e,fill:"#fff"}));return h.attr({cursor:"pointer"}).toFront()};b.prototype.draw=function(){var a,c=this;a=this.draw_circle(this.center_point.x,this.center_point.y,this.main_radius,100);a._active=false;a._children=[];this.add_click_to_circle(a,true);this.create_circles_along_radius(this.children,this.child_radius,this.outer_radius,function(d,e){c.add_grandchildren(d,e);c.add_click_to_circle(e); +return a._children.push(e)});return a.toFront()};return b}(w);r.PathMenu=W;var Z,$;t={}.hasOwnProperty;s=function(g,b){function a(){this.constructor=g}for(var c in b)if(t.call(b,c))g[c]=b[c];a.prototype=b.prototype;g.prototype=new a;g.__super__=b.prototype;return g};$=function(g,b,a,c,d){var e,f,h;f=360/c*a;e=(90-f)*Math.PI/180;h=g+d*Math.cos(e);e=b-d*Math.sin(e);return{path:c===a?[["M",g,b-d],["A",d,d,0,1,1,g-0.01,b-d]]:[["M",g,b-d],["A",d,d,0,+(f>180),1,h,e]]}};Z=function(g){function b(a){return b.__super__.constructor.call(this, +a,b.DEFAULTS)}s(b,g);b.DEFAULTS={radius:55,stroke_width:30,font_color:"#333333",label_color:"#333333",fill_color:"#fff",stroke_color:"#81ae14",background_color:"#222222",text_shadow:false};return b}(v);v=function(g){function b(a,c,d,e){this.label=c;this.value=d;if(e==null)e={};b.__super__.constructor.call(this,a,new Z(e));this.center_point=new l(this.width/2,this.height/2);this.r.customAttributes.arc=$}s(b,g);b.prototype.draw=function(){var a;a=this.r.path().attr({"stroke-width":this.options.stroke_width, +stroke:this.options.stroke_color,arc:[this.center_point.x,this.center_point.y,0,100,this.options.radius]});this.r.circle(this.center_point.x,this.center_point.y,this.options.radius).attr({fill:this.options.fill_color,stroke:"none","stroke-width":0});this.r.text(this.center_point.x,this.center_point.y,Math.round(this.value*100/100)+"%").attr({"font-size":this.options.radius/2.5,fill:this.options.font_color,"font-weight":"bold"});this.r.text(this.center_point.x,this.center_point.y+1.8*this.options.radius, +this.label).attr({"font-size":this.options.radius/2.5,"font-weight":"bold",fill:this.options.label_color});this.options.text_shadow&&this.r.text(this.center_point.x,this.center_point.y+1.8*this.options.radius+1,this.label).attr({"font-size":this.options.radius/2.5,"font-weight":"bold",fill:this.options.text_shadow}).toBack();return a.animate({arc:[this.center_point.x,this.center_point.y,this.value,100,this.options.radius]},1500,"<")};return b}(w);r.CircleProgress=v;t={}.hasOwnProperty;s=function(g, +b){function a(){this.constructor=g}for(var c in b)if(t.call(b,c))g[c]=b[c];a.prototype=b.prototype;g.prototype=new a;g.__super__=b.prototype;return g};v=function(g){function b(a,c){if(c==null)c={};b.__super__.constructor.call(this,a,new E(c));this.padding=26;this.all_points=[];this.line_indices=[];this.line_options=[];this.lines=[];this.legend=this.r.set()}s(b,g);b.prototype.add_line=function(a){var c,d,e,f,h;c=a.data;if(!(c.length<1)){e=[];f=0;for(h=c.length;f0?c:c+f;if(a.length===1){j=this.create_scalers_for_single_point();f=j[0];i=j[1]}else{j=this.create_scalers(a);f=j[0];i=j[1]}f=[];d=(new C(this.r,d)).x(function(){return h}).y(function(m){return i(a[m].y)}).size(e);j=0;for(k=a.length;j1){a=Math.round(a);if(a===0)a=1}return a};b.prototype.draw_y_labels=function(a,c){var d,e,f,h,i,j;if(c==null)c=0;f=y.get_ranges_for_points(a);h=f[2];e=f[3];if(this.options.y_axis_scale.length===2){h=this.options.y_axis_scale;e=h[0];h=h[1]}if(h===e)return this._draw_y_labels([new l(0,h)],c);f=[];if(this.options.scale==="log"){d=new G;j=d(e);d=d(h);d=(d-j)/(this.options.max_y_labels-1);e=e;for(i= +0;e<=h&&i1)f[f.length-1].y=Math.round(h);return this._draw_y_labels(f,c)};b.prototype.draw_x_label=function(a,c){var d,e,f;d=this.options.label_format;f=this.options.x_label_size;e=this.options.font_family;return(new B(this.r,c.x,this.height-f,a.is_date_type===true?new Date(a.x):Math.round(a.x),d,f,e)).draw()};b.prototype.draw_x_labels=function(a, +c){var d,e,f,h,i,j;e=[];h=this.options.max_x_labels;this.draw_x_label(a[0],c[0]);e.push(c[0].x);if(!(h<2)){f=c.length-1;this.draw_x_label(a[f],c[f]);e.push(c[f].x);if(!(h<3)){f=c.length-2;h=f/(h-1);d=Math.round(h);if(h!==d)h=d+1;for(d=h;d1?this.create_scalers(this.all_points):this.create_scalers_for_single_point();j=d[0];k=d[1];n=this.line_indices;d=m=0;for(o=n.length;m +2?this.create_scalers(i):this.create_scalers_for_single_point();e=a[0];f=a[1]}else{e=j;f=k}c=function(){var p,q,u;u=[];p=0;for(q=i.length;pthis.index){e=c(this.index);c=this.render_bar(this.options.x_padding,d,e- +this.options.x_padding);d=this.render_bar(e,d,h-e,this.options.bar2_color);f=new D(this.r,d,this.format_tooltip(a));f.translate(d.getBBox().width/2,0);c.mouseover(function(){return f.show()});return c.mouseout(function(){return f.hide()})}else{d=this.render_bar(this.options.x_padding,d,h-this.options.x_padding);f=new D(this.r,d,this.format_tooltip(a));return f.translate(d.getBBox().width/2,0)}};b.prototype.draw_guide_line=function(a,c,d,e){var f,h;if(e==null)e=1;h=new l(d,this.options.y_padding); +f=new l(d,this.height);this.effects.vertical_dashed_line(h,f,this.options.dash_width).attr({fill:"rgba(0,0,0,"+e+")",stroke:"none"});e=(new C(this.r)).x(function(){return d}).y(function(i){return i*15+15}).size(this.options.label_size).attr({fill:"rgba(0,0,0,"+e+")"});e.draw(a).attr({"font-weight":"bold"});return e.draw(c).attr({"font-size":10})};b.prototype.sort_bars_by_index=function(){var a,c;c=function(){var d,e,f,h;f=this.bars;h=[];d=0;for(e=f.length;d + # Contains an array of glow elements when a user is hovering over the line + @glow_elements = [] + @line_set = @r.set() + + @visible = if options['show_line'] == true then true else false draw: -> path = Bezier.create_path(@scaled_points, @options.smoothing) @draw_area(path) if @options.fill_area @draw_curve(path) @draw_dots_and_tooltips(@scaled_points, @raw_points) if @options.dot_size > 0 + + # Add this stuff to our set for future manipulation. This contains + # everything in this line but the area - because + @line_set.push path + + # Add an event listener to watch for hovers over + @line_set.hover( + -> + @glow() + , + -> + @remove_glow() + , + @, + @ + ) + return draw_curve: (path) -> @@ -36,6 +58,10 @@ class Line "stroke-width" : @options.line_width }).toFront() + # Add the curve to our set + @line_set.push curve + curve + draw_area: (path) -> points = @scaled_points padded_height = @height - @options.y_padding @@ -47,14 +73,13 @@ class Line path += "L #{first_point.x}, #{padded_height} " path += "Z" - area = @r.path(path) - area.attr({ + @area = @r.path(path) + @area.attr({ "fill" : @options.area_color "fill-opacity" : @options.area_opacity "stroke" : "none" }) - area.toBack() - + @area.toBack() draw_dots_and_tooltips: () -> scaled_points = @scaled_points @@ -75,9 +100,14 @@ class Line dot = new Dot(@r, point, options) tooltip = new Tooltip(@r, dot.element, raw_point.options.tooltip || raw_point.y, options.hover_enabled) + tooltip.hide() dots.push dot tooltips.push tooltip + # Push the dot to our set - we don't need the tooltip because we shouldn't + # be able to hover over a dot to show it. + @line_set.push dot.element + if raw_point.options.no_dot == true dot.hide() @@ -93,3 +123,30 @@ class Line tooltips[min_point].show() dots[min_point].activate() + hide: -> + @area.hide() + @line_set.hide() + @remove_glow() + @visible = false + + show: -> + @area.show() + @line_set.show() + @visible = true + + toggle: -> + if @visible + @hide() + else + @show() + + glow: -> + return unless @visible + @line_set.forEach( (item) -> + @glow_elements.push(item.glow({ opacity: 0.1 })) + , @) + + remove_glow: -> + for i in @glow_elements + i.remove() + diff --git a/src/coffeescript/charts/line_chart.coffee b/src/coffeescript/charts/line_chart.coffee index bcbc5a4..861bcc8 100644 --- a/src/coffeescript/charts/line_chart.coffee +++ b/src/coffeescript/charts/line_chart.coffee @@ -33,6 +33,8 @@ class LineChart extends BaseChart @all_points = [] @line_indices = [] @line_options = [] + @lines = [] + @legend = @r.set() add_line: (args) -> data = args.data @@ -251,23 +253,26 @@ class LineChart extends BaseChart draw_line: (raw_points, points, options) -> if @options.render == "bar" - new LineBar( + line = new LineBar( @r, raw_points, points, @height, @width, options - ).draw() + ) else - new Line( + line = new Line( @r, raw_points, points, @height, @width, options - ).draw() + ) + line.draw() + line.hide() unless options['show_line'] + @lines.push line clear: () -> @@ -284,6 +289,7 @@ class LineChart extends BaseChart [x, y] = if @all_points.length > 1 then @create_scalers(@all_points) else @create_scalers_for_single_point() for line_indices, i in @line_indices + [begin, end] = line_indices raw_points = @all_points[begin..end] @@ -311,7 +317,114 @@ class LineChart extends BaseChart else if i == 1 && @options.multi_axis @draw_y_labels(raw_points, @width - @options.x_padding) if @options.show_y_labels == true + # Create the legend for our graph, if we need to + @draw_legend() if @options['show_legend'] + return + # Draws a legend in the top right hand corner, giving a name to each line + # colour + # + draw_legend: -> + @legend.clear() + + # We need to remember our X position for each line create a legend for + # - this allows us to put them next to each other + current_x = 0 + + # Loop through each line option + for line_option, count in @line_options + # Create a set to hold the thumbnail and label for a particular line. This + # will allow us to add a hover and click state to the thumbnail and the + # label at the same time. + set = @r.set() + + thumbnail = @r.rect(current_x, 1, 15, 10).attr({ + fill: line_option['line_color'] + stroke: line_option['line_color'] + cursor: 'pointer' + 'stroke-opacity': 0 + }) + # We have to add this to the thumbnail and the label because you can't + # bind a scope to the click/mousedown element in raphael, therefore we + # can't access the "set" line property when we click one of the two. + thumbnail.line = @lines[count] + if line_option['show_line'] + thumbnail.full = true + else + thumbnail.attr({ + 'stroke-opacity': 1 + 'fill-opacity' : 0 + }) + thumbnail.full = false + set.push(thumbnail) + @legend.push(thumbnail) + + current_x += 23 + + line_name = line_option['line_name'] || 'Line ' + count + label = @r.text(current_x, 0, line_name) + label.attr({ + fill: '#333', + cursor: 'pointer' + 'font-size' : 10, + 'font-weight' : 'normal', + 'text-anchor' : 'start', + 'font-family' : 'Helvetica', + }) + label.line = @lines[count] + # Save a reference to the thumbnail in our label so we can set the stroke + # or fill when we click on the label + label.thumbnail = thumbnail + # Ensure the label's Y axis is actually 0, because for some reason setting + # the to 0 doesn't actually make it 0 + label.transform("...t0," + (label.getBBox()['y'] * -1)) + current_x += label.getBBox()['width'] + 25 + set.push(label) + @legend.push(label) + + # Add hover to the line set so the line glows when you're over it + set.hover( + -> + @line.glow() + , + -> + @line.remove_glow() + ) + + set.click(-> + @line.toggle() + # We're technically hovering so we should call glow anyway - this wont + # work because of the visible check if we've just hidden, and it will + # make the experience a little bit better, because the glow won't appear + # until the mouse moves otherwise. + @line.glow() + + # Switch between a stroke and a fill - this will show us + # a visible/hidden state for the line + thumbnail = this.thumbnail || this + if thumbnail.full + thumbnail.attr({ + 'stroke-opacity': 1 + 'fill-opacity' : 0 + }) + thumbnail.full = false + else + thumbnail.attr({ + 'stroke-opacity': 0 + 'fill-opacity' : 1 + }) + thumbnail.full = true + ) + + + # Take the entire legend's width and the padding at the right of the graph + # to determine our X transform + legend_x = (@width - (@legend.getBBox()['width'] + @options.x_padding)) + + legend_y = (@options.y_padding / 2) - (@legend.getBBox()['height'] / 2) + + @legend.transform("...t" + legend_x + "," + legend_y) + exports.LineChart = LineChart diff --git a/src/coffeescript/charts/line_chart_options.coffee b/src/coffeescript/charts/line_chart_options.coffee index 127df23..db36c11 100644 --- a/src/coffeescript/charts/line_chart_options.coffee +++ b/src/coffeescript/charts/line_chart_options.coffee @@ -15,7 +15,10 @@ class LineChartOptions @DEFAULTS: { - dot_size: 5 + show_line: true + show_legend: false + + dot_size: 5 dot_color: "#00aadd" dot_stroke_color: "#fff" dot_stroke_size: 2