Another example of the D3 library to create a linechart with tooltips.
This uses Tipsy for tooltips. This chart is based on work by Nate Ross: http://natescodevault.com/2011/11/28/d3-js-data-visualizations/.
Another example of the D3 library to create a linechart with tooltips.
This uses Tipsy for tooltips. This chart is based on work by Nate Ross: http://natescodevault.com/2011/11/28/d3-js-data-visualizations/.
| .data-line { | |
| stroke: steelblue; | |
| stroke-width: 2; | |
| fill: none; | |
| stroke-dasharray: 0; | |
| } | |
| path { | |
| stroke: #eee; | |
| stroke-width: 2; | |
| stroke-dasharray: 0; | |
| fill: none; | |
| } | |
| .area { | |
| fill: lightsteelblue; | |
| } | |
| text { | |
| font-family: Arial; | |
| font-size: 8pt; | |
| fill: #787878; | |
| } | |
| line { | |
| stroke: #CDCDCD; | |
| stroke-width: 0.5; | |
| /* stroke-dasharray: 3 3; */ | |
| fill: none; | |
| } | |
| .data-point { | |
| stroke: steelblue; | |
| stroke-width: 2; | |
| fill: #FFF; | |
| } | |
| #chart { | |
| margin: 0; | |
| } |
| /** | |
| * User: nross | |
| */ | |
| var w = 900, | |
| h = 450; | |
| var monthNames = [ "January", "February", "March", "April", "May", "June", | |
| "July", "August", "September", "October", "November", "December" ]; | |
| var maxDataPointsForDots = 50, | |
| transitionDuration = 1000; | |
| var svg = null, | |
| yAxisGroup = null, | |
| xAxisGroup = null, | |
| dataCirclesGroup = null, | |
| dataLinesGroup = null; | |
| function draw() { | |
| var data = generateData(); | |
| var margin = 40; | |
| var max = d3.max(data, function(d) { return d.value }); | |
| var min = 0; | |
| var pointRadius = 4; | |
| var x = d3.time.scale().range([0, w - margin * 2]).domain([data[0].date, data[data.length - 1].date]); | |
| var y = d3.scale.linear().range([h - margin * 2, 0]).domain([min, max]); | |
| var xAxis = d3.svg.axis().scale(x).tickSize(h - margin * 2).tickPadding(10).ticks(7); | |
| var yAxis = d3.svg.axis().scale(y).orient('left').tickSize(-w + margin * 2).tickPadding(10); | |
| var t = null; | |
| svg = d3.select('#chart').select('svg').select('g'); | |
| if (svg.empty()) { | |
| svg = d3.select('#chart') | |
| .append('svg:svg') | |
| .attr('width', w) | |
| .attr('height', h) | |
| .attr('class', 'viz') | |
| .append('svg:g') | |
| .attr('transform', 'translate(' + margin + ',' + margin + ')'); | |
| } | |
| t = svg.transition().duration(transitionDuration); | |
| // y ticks and labels | |
| if (!yAxisGroup) { | |
| yAxisGroup = svg.append('svg:g') | |
| .attr('class', 'yTick') | |
| .call(yAxis); | |
| } | |
| else { | |
| t.select('.yTick').call(yAxis); | |
| } | |
| // x ticks and labels | |
| if (!xAxisGroup) { | |
| xAxisGroup = svg.append('svg:g') | |
| .attr('class', 'xTick') | |
| .call(xAxis); | |
| } | |
| else { | |
| t.select('.xTick').call(xAxis); | |
| } | |
| // Draw the lines | |
| if (!dataLinesGroup) { | |
| dataLinesGroup = svg.append('svg:g'); | |
| } | |
| var dataLines = dataLinesGroup.selectAll('.data-line') | |
| .data([data]); | |
| var line = d3.svg.line() | |
| // assign the X function to plot our line as we wish | |
| .x(function(d,i) { | |
| // verbose logging to show what's actually being done | |
| //console.log('Plotting X value for date: ' + d.date + ' using index: ' + i + ' to be at: ' + x(d.date) + ' using our xScale.'); | |
| // return the X coordinate where we want to plot this datapoint | |
| //return x(i); | |
| return x(d.date); | |
| }) | |
| .y(function(d) { | |
| // verbose logging to show what's actually being done | |
| //console.log('Plotting Y value for data value: ' + d.value + ' to be at: ' + y(d.value) + " using our yScale."); | |
| // return the Y coordinate where we want to plot this datapoint | |
| //return y(d); | |
| return y(d.value); | |
| }) | |
| .interpolate("linear"); | |
| /* | |
| .attr("d", d3.svg.line() | |
| .x(function(d) { return x(d.date); }) | |
| .y(function(d) { return y(0); })) | |
| .transition() | |
| .delay(transitionDuration / 2) | |
| .duration(transitionDuration) | |
| .style('opacity', 1) | |
| .attr("transform", function(d) { return "translate(" + x(d.date) + "," + y(d.value) + ")"; }); | |
| */ | |
| var garea = d3.svg.area() | |
| .interpolate("linear") | |
| .x(function(d) { | |
| // verbose logging to show what's actually being done | |
| return x(d.date); | |
| }) | |
| .y0(h - margin * 2) | |
| .y1(function(d) { | |
| // verbose logging to show what's actually being done | |
| return y(d.value); | |
| }); | |
| dataLines | |
| .enter() | |
| .append('svg:path') | |
| .attr("class", "area") | |
| .attr("d", garea(data)); | |
| dataLines.enter().append('path') | |
| .attr('class', 'data-line') | |
| .style('opacity', 0.3) | |
| .attr("d", line(data)); | |
| /* | |
| .transition() | |
| .delay(transitionDuration / 2) | |
| .duration(transitionDuration) | |
| .style('opacity', 1) | |
| .attr('x1', function(d, i) { return (i > 0) ? xScale(data[i - 1].date) : xScale(d.date); }) | |
| .attr('y1', function(d, i) { return (i > 0) ? yScale(data[i - 1].value) : yScale(d.value); }) | |
| .attr('x2', function(d) { return xScale(d.date); }) | |
| .attr('y2', function(d) { return yScale(d.value); }); | |
| */ | |
| dataLines.transition() | |
| .attr("d", line) | |
| .duration(transitionDuration) | |
| .style('opacity', 1) | |
| .attr("transform", function(d) { return "translate(" + x(d.date) + "," + y(d.value) + ")"; }); | |
| dataLines.exit() | |
| .transition() | |
| .attr("d", line) | |
| .duration(transitionDuration) | |
| .attr("transform", function(d) { return "translate(" + x(d.date) + "," + y(0) + ")"; }) | |
| .style('opacity', 1e-6) | |
| .remove(); | |
| d3.selectAll(".area").transition() | |
| .duration(transitionDuration) | |
| .attr("d", garea(data)); | |
| // Draw the points | |
| if (!dataCirclesGroup) { | |
| dataCirclesGroup = svg.append('svg:g'); | |
| } | |
| var circles = dataCirclesGroup.selectAll('.data-point') | |
| .data(data); | |
| circles | |
| .enter() | |
| .append('svg:circle') | |
| .attr('class', 'data-point') | |
| .style('opacity', 1e-6) | |
| .attr('cx', function(d) { return x(d.date) }) | |
| .attr('cy', function() { return y(0) }) | |
| .attr('r', function() { return (data.length <= maxDataPointsForDots) ? pointRadius : 0 }) | |
| .transition() | |
| .duration(transitionDuration) | |
| .style('opacity', 1) | |
| .attr('cx', function(d) { return x(d.date) }) | |
| .attr('cy', function(d) { return y(d.value) }); | |
| circles | |
| .transition() | |
| .duration(transitionDuration) | |
| .attr('cx', function(d) { return x(d.date) }) | |
| .attr('cy', function(d) { return y(d.value) }) | |
| .attr('r', function() { return (data.length <= maxDataPointsForDots) ? pointRadius : 0 }) | |
| .style('opacity', 1); | |
| circles | |
| .exit() | |
| .transition() | |
| .duration(transitionDuration) | |
| // Leave the cx transition off. Allowing the points to fall where they lie is best. | |
| //.attr('cx', function(d, i) { return xScale(i) }) | |
| .attr('cy', function() { return y(0) }) | |
| .style("opacity", 1e-6) | |
| .remove(); | |
| $('svg circle').tipsy({ | |
| gravity: 'w', | |
| html: true, | |
| title: function() { | |
| var d = this.__data__; | |
| var pDate = d.date; | |
| return 'Date: ' + pDate.getDate() + " " + monthNames[pDate.getMonth()] + " " + pDate.getFullYear() + '<br>Value: ' + d.value; | |
| } | |
| }); | |
| } | |
| function generateData() { | |
| var data = []; | |
| var i = Math.max(Math.round(Math.random()*100), 3); | |
| while (i--) { | |
| var date = new Date(); | |
| date.setDate(date.getDate() - i); | |
| date.setHours(0, 0, 0, 0); | |
| data.push({'value' : Math.round(Math.random()*1200), 'date' : date}); | |
| } | |
| return data; | |
| } | |
| d3.select('#button').on('click', draw); | |
| draw(); |
| <!DOCTYPE html> | |
| <!-- Based on url http://natescodevault.com/js/line/ --> | |
| <html><head><meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> | |
| <title>D3 Line Chart</title> | |
| <link rel="stylesheet" type="text/css" href="d3LineChart.css"> | |
| <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js" type="text/javascript"></script> | |
| <script src="http://d3js.org/d3.v3.min.js" type="text/javascript"></script> | |
| <script type="text/javascript" src="jquery.tipsy.js"></script> | |
| <link href="tipsy.css" rel="stylesheet" type="text/css" /> | |
| </head> | |
| <body> | |
| <button id="button">Redraw</button> | |
| <div id="chart"></div> | |
| <script src="d3LineChart.js" type="text/javascript"></script> | |
| </body></html> |
| // tipsy, facebook style tooltips for jquery | |
| // version 1.0.0a | |
| // (c) 2008-2010 jason frame [jason@onehackoranother.com] | |
| // released under the MIT license | |
| (function($) { | |
| function maybeCall(thing, ctx) { | |
| return (typeof thing == 'function') ? (thing.call(ctx)) : thing; | |
| } | |
| function Tipsy(element, options) { | |
| this.$element = $(element); | |
| this.options = options; | |
| this.enabled = true; | |
| this.fixTitle(); | |
| } | |
| Tipsy.prototype = { | |
| show: function() { | |
| var title = this.getTitle(); | |
| if (title && this.enabled) { | |
| var $tip = this.tip(); | |
| $tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title); | |
| $tip[0].className = 'tipsy'; // reset classname in case of dynamic gravity | |
| $tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).prependTo(document.body); | |
| var pos = $.extend({}, this.$element.offset(), { | |
| width: this.$element[0].offsetWidth || 0, | |
| height: this.$element[0].offsetHeight || 0 | |
| }); | |
| if (typeof this.$element[0].nearestViewportElement == 'object') { | |
| // SVG | |
| var el = this.$element[0]; | |
| var rect = el.getBoundingClientRect(); | |
| pos.width = rect.width; | |
| pos.height = rect.height; | |
| } | |
| var actualWidth = $tip[0].offsetWidth, | |
| actualHeight = $tip[0].offsetHeight, | |
| gravity = maybeCall(this.options.gravity, this.$element[0]); | |
| var tp; | |
| switch (gravity.charAt(0)) { | |
| case 'n': | |
| tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}; | |
| break; | |
| case 's': | |
| tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}; | |
| break; | |
| case 'e': | |
| tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset}; | |
| break; | |
| case 'w': | |
| tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset}; | |
| break; | |
| } | |
| if (gravity.length == 2) { | |
| if (gravity.charAt(1) == 'w') { | |
| tp.left = pos.left + pos.width / 2 - 15; | |
| } else { | |
| tp.left = pos.left + pos.width / 2 - actualWidth + 15; | |
| } | |
| } | |
| $tip.css(tp).addClass('tipsy-' + gravity); | |
| $tip.find('.tipsy-arrow')[0].className = 'tipsy-arrow tipsy-arrow-' + gravity.charAt(0); | |
| if (this.options.className) { | |
| $tip.addClass(maybeCall(this.options.className, this.$element[0])); | |
| } | |
| if (this.options.fade) { | |
| $tip.stop().css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: this.options.opacity}); | |
| } else { | |
| $tip.css({visibility: 'visible', opacity: this.options.opacity}); | |
| } | |
| var t = this; | |
| var set_hovered = function(set_hover){ | |
| return function(){ | |
| t.$tip.stop(); | |
| t.tipHovered = set_hover; | |
| if (!set_hover){ | |
| if (t.options.delayOut === 0) { | |
| t.hide(); | |
| } else { | |
| setTimeout(function() { | |
| if (t.hoverState == 'out') t.hide(); }, t.options.delayOut); | |
| } | |
| } | |
| }; | |
| }; | |
| $tip.hover(set_hovered(true), set_hovered(false)); | |
| } | |
| }, | |
| hide: function() { | |
| if (this.options.fade) { | |
| this.tip().stop().fadeOut(function() { $(this).remove(); }); | |
| } else { | |
| this.tip().remove(); | |
| } | |
| }, | |
| fixTitle: function() { | |
| var $e = this.$element; | |
| if ($e.attr('title') || typeof($e.attr('original-title')) != 'string') { | |
| $e.attr('original-title', $e.attr('title') || '').removeAttr('title'); | |
| } | |
| if (typeof $e.context.nearestViewportElement == 'object'){ | |
| if ($e.children('title').length){ | |
| $e.append('<original-title>' + ($e.children('title').text() || '') + '</original-title>') | |
| .children('title').remove(); | |
| } | |
| } | |
| }, | |
| getTitle: function() { | |
| var title, $e = this.$element, o = this.options; | |
| this.fixTitle(); | |
| if (typeof o.title == 'string') { | |
| var title_name = o.title == 'title' ? 'original-title' : o.title; | |
| if ($e.children(title_name).length){ | |
| title = $e.children(title_name).html(); | |
| } else{ | |
| title = $e.attr(title_name); | |
| } | |
| } else if (typeof o.title == 'function') { | |
| title = o.title.call($e[0]); | |
| } | |
| title = ('' + title).replace(/(^\s*|\s*$)/, ""); | |
| return title || o.fallback; | |
| }, | |
| tip: function() { | |
| if (!this.$tip) { | |
| this.$tip = $('<div class="tipsy"></div>').html('<div class="tipsy-arrow"></div><div class="tipsy-inner"></div>'); | |
| } | |
| return this.$tip; | |
| }, | |
| validate: function() { | |
| if (!this.$element[0].parentNode) { | |
| this.hide(); | |
| this.$element = null; | |
| this.options = null; | |
| } | |
| }, | |
| enable: function() { this.enabled = true; }, | |
| disable: function() { this.enabled = false; }, | |
| toggleEnabled: function() { this.enabled = !this.enabled; } | |
| }; | |
| $.fn.tipsy = function(options) { | |
| if (options === true) { | |
| return this.data('tipsy'); | |
| } else if (typeof options == 'string') { | |
| var tipsy = this.data('tipsy'); | |
| if (tipsy) tipsy[options](); | |
| return this; | |
| } | |
| options = $.extend({}, $.fn.tipsy.defaults, options); | |
| if (options.hoverlock && options.delayOut === 0) { | |
| options.delayOut = 100; | |
| } | |
| function get(ele) { | |
| var tipsy = $.data(ele, 'tipsy'); | |
| if (!tipsy) { | |
| tipsy = new Tipsy(ele, $.fn.tipsy.elementOptions(ele, options)); | |
| $.data(ele, 'tipsy', tipsy); | |
| } | |
| return tipsy; | |
| } | |
| function enter() { | |
| var tipsy = get(this); | |
| tipsy.hoverState = 'in'; | |
| if (options.delayIn === 0) { | |
| tipsy.show(); | |
| } else { | |
| tipsy.fixTitle(); | |
| setTimeout(function() { if (tipsy.hoverState == 'in') tipsy.show(); }, options.delayIn); | |
| } | |
| } | |
| function leave() { | |
| var tipsy = get(this); | |
| tipsy.hoverState = 'out'; | |
| if (options.delayOut === 0) { | |
| tipsy.hide(); | |
| } else { | |
| var to = function() { | |
| if (!tipsy.tipHovered || !options.hoverlock){ | |
| if (tipsy.hoverState == 'out') tipsy.hide(); | |
| } | |
| }; | |
| setTimeout(to, options.delayOut); | |
| } | |
| } | |
| if (options.trigger != 'manual') { | |
| var binder = options.live ? 'live' : 'bind', | |
| eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus', | |
| eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur'; | |
| this[binder](eventIn, enter)[binder](eventOut, leave); | |
| } | |
| return this; | |
| }; | |
| $.fn.tipsy.defaults = { | |
| className: null, | |
| delayIn: 0, | |
| delayOut: 0, | |
| fade: false, | |
| fallback: '', | |
| gravity: 'n', | |
| html: false, | |
| live: false, | |
| offset: 0, | |
| opacity: 0.8, | |
| title: 'title', | |
| trigger: 'hover', | |
| hoverlock: false | |
| }; | |
| // Overwrite this method to provide options on a per-element basis. | |
| // For example, you could store the gravity in a 'tipsy-gravity' attribute: | |
| // return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' }); | |
| // (remember - do not modify 'options' in place!) | |
| $.fn.tipsy.elementOptions = function(ele, options) { | |
| return $.metadata ? $.extend({}, options, $(ele).metadata()) : options; | |
| }; | |
| $.fn.tipsy.autoNS = function() { | |
| return $(this).offset().top > ($(document).scrollTop() + $(window).height() / 2) ? 's' : 'n'; | |
| }; | |
| $.fn.tipsy.autoWE = function() { | |
| return $(this).offset().left > ($(document).scrollLeft() + $(window).width() / 2) ? 'e' : 'w'; | |
| }; | |
| /** | |
| * yields a closure of the supplied parameters, producing a function that takes | |
| * no arguments and is suitable for use as an autogravity function like so: | |
| * | |
| * @param margin (int) - distance from the viewable region edge that an | |
| * element should be before setting its tooltip's gravity to be away | |
| * from that edge. | |
| * @param prefer (string, e.g. 'n', 'sw', 'w') - the direction to prefer | |
| * if there are no viewable region edges effecting the tooltip's | |
| * gravity. It will try to vary from this minimally, for example, | |
| * if 'sw' is preferred and an element is near the right viewable | |
| * region edge, but not the top edge, it will set the gravity for | |
| * that element's tooltip to be 'se', preserving the southern | |
| * component. | |
| */ | |
| $.fn.tipsy.autoBounds = function(margin, prefer) { | |
| return function() { | |
| var dir = {ns: prefer[0], ew: (prefer.length > 1 ? prefer[1] : false)}, | |
| boundTop = $(document).scrollTop() + margin, | |
| boundLeft = $(document).scrollLeft() + margin, | |
| $this = $(this); | |
| if ($this.offset().top < boundTop) dir.ns = 'n'; | |
| if ($this.offset().left < boundLeft) dir.ew = 'w'; | |
| if ($(window).width() + $(document).scrollLeft() - $this.offset().left < margin) dir.ew = 'e'; | |
| if ($(window).height() + $(document).scrollTop() - $this.offset().top < margin) dir.ns = 's'; | |
| return dir.ns + (dir.ew ? dir.ew : ''); | |
| }; | |
| }; | |
| })(jQuery); |
| .tipsy { font-size: 10px; position: absolute; padding: 5px; z-index: 100000; } | |
| .tipsy-inner { background-color: #bfbfbf; color: #000; font-family: Verdana,Arial; max-width: 200px; padding: 5px 8px 4px 8px; text-align: center; } | |
| /* Rounded corners */ | |
| .tipsy-inner { border-radius: 3px; -moz-border-radius: 3px; -webkit-border-radius: 3px; } | |
| /* Uncomment for shadow */ | |
| /*.tipsy-inner { box-shadow: 0 0 5px #000000; -webkit-box-shadow: 0 0 5px #000000; -moz-box-shadow: 0 0 5px #000000; }*/ | |
| .tipsy-arrow { position: absolute; width: 0; height: 0; line-height: 0; border: 5px dashed #000; } | |
| /* Rules to colour arrows */ | |
| .tipsy-arrow-n { border-bottom-color: #bfbfbf; } | |
| .tipsy-arrow-s { border-top-color: #bfbfbf; } | |
| .tipsy-arrow-e { border-left-color: #bfbfbf; } | |
| .tipsy-arrow-w { border-right-color: #bfbfbf; } | |
| .tipsy-n .tipsy-arrow { top: 0px; left: 50%; margin-left: -5px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent; } | |
| .tipsy-nw .tipsy-arrow { top: 0; left: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;} | |
| .tipsy-ne .tipsy-arrow { top: 0; right: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;} | |
| .tipsy-s .tipsy-arrow { bottom: 0; left: 50%; margin-left: -5px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } | |
| .tipsy-sw .tipsy-arrow { bottom: 0; left: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } | |
| .tipsy-se .tipsy-arrow { bottom: 0; right: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } | |
| .tipsy-e .tipsy-arrow { right: 0; top: 50%; margin-top: -5px; border-left-style: solid; border-right: none; border-top-color: transparent; border-bottom-color: transparent; } | |
| .tipsy-w .tipsy-arrow { left: 0; top: 50%; margin-top: -5px; border-right-style: solid; border-left: none; border-top-color: transparent; border-bottom-color: transparent; } |
This is what I was looking for. Thank you man.