block by timelyportfolio 5438143

5438143

Full Screen

index.html

<!DOCTYPE html>
<meta charset="utf-8">

<link href="d3.css" rel="stylesheet" type="text/css">

<style>
#test1 {
  margin: 0;
  padding: 0;
  overflow: none;
}
</style>


<body>

<div id="test1">
  <svg></svg>
</div>

<script src="//mbostock.github.com/d3/d3.v2.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="nvtooltip.js"></script>
<script src="d3legend.js"></script> 
<script src="d3line.js"></script> 
<script src="d3linewithlegend.js"></script> 
<script>

function log(text) {
  if (console && console.log) console.log(text);
  return text;
}



$(document).ready(function() {
  var margin = {top: 30, right: 10, bottom: 50, left: 60},
      chart = d3LineWithLegend()
                .xAxis.label('Time (ms)')
                .width(width(margin))
                .height(height(margin))
                .yAxis.label('Voltage (v)');


  var svg = d3.select('#test1 svg')
      .datum(generateData())

  svg.transition().duration(500)
      .attr('width', width(margin))
      .attr('height', height(margin))
      .call(chart);


  chart.dispatch.on('showTooltip', function(e) {
  var offset = $('#test1').offset(), // { left: 0, top: 0 }
        left = e.pos[0] + offset.left,
        top = e.pos[1] + offset.top,
        formatter = d3.format(".04f");

    var content = '<h3>' + e.series.label + '</h3>' +
                  '<p>' +
                  '<span class="value">[' + e.point[0] + ', ' + formatter(e.point[1]) + ']</span>' +
                  '</p>';

    nvtooltip.show([left, top], content);
  });

  chart.dispatch.on('hideTooltip', function(e) {
    nvtooltip.cleanup();
  });




  $(window).resize(function() {
    var margin = chart.margin();

    chart
      .width(width(margin))
      .height(height(margin));

    d3.select('#test1 svg')
      .attr('width', width(margin))
      .attr('height', height(margin))
      .call(chart);

    });




  function width(margin) {
    var w = $(window).width() - 20;

    return ( (w - margin.left - margin.right - 20) < 0 ) ? margin.left + margin.right + 2 : w;
  }

  function height(margin) {
    var h = $(window).height() - 20;

    return ( h - margin.top - margin.bottom - 20 < 0 ) ? 
              margin.top + margin.bottom + 2 : h;
  }


  //data
  function generateData() {
    var sin = [],
        sin2 = [],
        cos = [],
        cos2 = [],
        r1 = Math.random(),
        r2 = Math.random(),
        r3 = Math.random(),
        r4 = Math.random();

    for (var i = 0; i < 100; i++) {
      sin.push([ i, r1 * Math.sin( r2 +  i / (10 * (r4 + .5) ))]);
      cos.push([ i, r2 * Math.cos( r3 + i / (10 * (r3 + .5) ))]);
      sin2.push([ i, r3 * Math.sin( r1 + i / (10 * (r2 + .5) ))]);
      cos2.push([ i, r4 * Math.cos( r4 + i / (10 * (r1 + .5) ))]);
    }

    return [
      {
        data: sin,
        label: "Sine Wave"
      },
      {
        data: cos,
        label: "Cosine Wave"
      },
      {
        data: sin2,
        label: "Sine2 Wave"
      },
      {
        data: cos2,
        label: "Cosine2 Wave"
      }
    ];
  }

});


</script>

d3.css


/********************
 * TOOLTIP CSS
 */

.nvtooltip {
  position: absolute;
  background-color: rgba(255,255,255,1);
  padding: 10px;
  border: 1px solid #ddd;

  font-family: Arial;
  font-size: 13px;

  transition: opacity 500ms linear;
  -moz-transition: opacity 500ms linear;
  -webkit-transition: opacity 500ms linear;

  transition-delay: 500ms
  -moz-transition-delay: 500ms;
  -webkit-transition-delay: 500ms;

  -moz-box-shadow: 4px 4px 12px rgba(0,0,0,.5);
  -webkit-box-shadow: 4px 4px 12px rgba(0,0,0,.5);
  box-shadow: 4px 4px 12px rgba(0,0,0,.5);

  -moz-border-radius: 15px;
  border-radius: 15px;
}

.nvtooltip h3 {
  margin: 0;
  padding: 0;
  text-align: center;
}

.nvtooltip p {
  margin: 0;
  padding: 0;
  text-align: center;
}

.nvtooltip span {
  display: inline-block;
  margin: 2px 0;
}





/**********
*  General SVG CSS
*/


text {
  font: 12px sans-serif;
}



/**********
*  Legend
*/

.legend .series {
  cursor: pointer;
}

.legend circle {
  stroke-width: 2px;
}

.legend .disabled circle {
  fill-opacity: 0;
}



/**********
*  Axes
*/

.axis path {
  fill: none;
  stroke: #000;
  stroke-opacity: .75;
  shape-rendering: crispEdges;
}

.axis path.domain {
  stroke-opacity: .75;
}

.axis line {
  fill: none;
  stroke: #000;
  stroke-opacity: .25;
  shape-rendering: crispEdges;
}

.axis line.zero {
  stroke-opacity: .75;
}



/**********
*  Line chart
*/

.point-paths path {
  /*
  fill: #eee;
  stroke: #aaa;
  */
  stroke-opacity: 0;
  fill-opacity: 0;
}


.lines path {
  fill: none;
  stroke-width: 1.5px;
  stroke-linecap: round;

  transition: stroke-width 250ms linear;
  -moz-transition: stroke-width 250ms linear;
  -webkit-transition: stroke-width 250ms linear;

  transition-delay: 250ms
  -moz-transition-delay: 250ms;
  -webkit-transition-delay: 250ms;
}

.line.hover path {
  stroke-width: 6px;
}

.lines .point {
  transition: stroke-width 250ms linear;
  -moz-transition: stroke-width 250ms linear;
  -webkit-transition: stroke-width 250ms linear;
}

.lines .point.hover {
  stroke-width: 20px;
  stroke-opacity: .5;
}

d3legend.js

function d3Legend() {
  var margin = {top: 5, right: 0, bottom: 5, left: 10},
      width = 400,
      height = 20,
      color = d3.scale.category10().range(),
      dispatch = d3.dispatch('legendClick', 'legendMouseover', 'legendMouseout');


  function chart(selection) {
    selection.each(function(data) {
      /**
      *    Legend curently is setup to automaticaly expand vertically based on a max width.
      *    Should implement legend where EITHER a maxWidth or a maxHeight is defined, then
      *    the other dimension will automatically expand to fit, and anything that exceeds
      *    that will automatically be clipped.
      **/

      var wrap = d3.select(this).selectAll('g.legend').data([data]);
      var gEnter = wrap.enter().append('g').attr('class', 'legend').append('g');


      var g = wrap.select('g')
          .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');


      var series = g.selectAll('.series')
          .data(function(d) { return d });
      var seriesEnter = series.enter().append('g').attr('class', 'series')
          .on('click', function(d, i) {
            dispatch.legendClick(d, i);
          })
          .on('mouseover', function(d, i) {
            dispatch.legendMouseover(d, i);
          })
          .on('mouseout', function(d, i) {
            dispatch.legendMouseout(d, i);
          });
      seriesEnter.append('circle')
          .style('fill', function(d, i){ return d.color || color[i % 10] })
          .style('stroke', function(d, i){ return d.color || color[i % 10] })
          .attr('r', 5);
      seriesEnter.append('text')
          .text(function(d) { return d.label })
          .attr('text-anchor', 'start')
          .attr('dy', '.32em')
          .attr('dx', '8');
      series.classed('disabled', function(d) { return d.disabled });
      series.exit().remove();


      var ypos = 5,
          newxpos = 5,
          maxwidth = 0,
          xpos;
      series
          .attr('transform', function(d, i) {
             var length = d3.select(this).select('text').node().getComputedTextLength() + 28;
             xpos = newxpos;

             //TODO: 1) Make sure dot + text of every series fits horizontally, or clip text to fix
             //      2) Consider making columns in line so dots line up
             //         --all labels same width? or just all in the same column?
             //         --optional, or forced always?
             if (width < margin.left + margin.right + xpos + length) {
               newxpos = xpos = 5;
               ypos += 20;
             }

             newxpos += length;
             if (newxpos > maxwidth) maxwidth = newxpos;

             return 'translate(' + xpos + ',' + ypos + ')';
          });

      //position legend as far right as possible within the total width
      g.attr('transform', 'translate(' + (width - margin.right - maxwidth) + ',' + margin.top + ')');

      height = margin.top + margin.bottom + ypos + 15;
    });

    return chart;
  }

  chart.dispatch = dispatch;

  chart.margin = function(_) {
    if (!arguments.length) return margin;
    margin = _;
    return chart;
  };

  chart.width = function(_) {
    if (!arguments.length) return width;
    width = _;
    return chart;
  };

  chart.height = function(_) {
    if (!arguments.length) return height;
    height = _;
    return chart;
  };

  chart.color = function(_) {
    if (!arguments.length) return color;
    color = _;
    return chart;
  };

  return chart;
}

d3line.js

function d3Line() {
  var margin = {top: 0, right: 0, bottom: 0, left: 0},
      width = 960,
      height = 500,
      dotRadius = function() { return 2.5 },
      color = d3.scale.category10().range(),
      id = Math.floor(Math.random() * 10000), //Create semi-unique ID incase user doesn't select one
      x = d3.scale.linear(),
      y = d3.scale.linear(),
      dispatch = d3.dispatch("pointMouseover", "pointMouseout"),
      x0, y0;


  function chart(selection) {
    selection.each(function(data) {
      var seriesData = data.map(function(d) { return d.data });

      x0 = x0 || x;
      y0 = y0 || y;

      //TODO: reconsider points {x: #, y: #} instead of [x,y]
      //TODO: data accessors so above won't really matter, but need to decide for internal use

      //add series data to each point for future ease of use
      data = data.map(function(series, i) {
        series.data = series.data.map(function(point) {
          point.series = i;
          return point;
        });
        return series;
      });


      x   .domain(d3.extent(d3.merge(seriesData), function(d) { return d[0] } ))
          .range([0, width - margin.left - margin.right]);

      y   .domain(d3.extent(d3.merge(seriesData), function(d) { return d[1] } ))
          .range([height - margin.top - margin.bottom, 0]);


      var vertices = d3.merge(data.map(function(line, lineIndex) {
            return line.data.map(function(point, pointIndex) {
              var pointKey = line.label + '-' + point[0];
              return [x(point[0]), y(point[1]), lineIndex, pointIndex]; //adding series index to point because data is being flattened
            })
          })
      );


      var wrap = d3.select(this).selectAll('g.d3line').data([data]);
      var gEnter = wrap.enter().append('g').attr('class', 'd3line').append('g');

      gEnter.append('g').attr('class', 'lines');
      gEnter.append('g').attr('class', 'point-clips');
      gEnter.append('g').attr('class', 'point-paths');


      var g = wrap.select('g')
          .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');



      var voronoiClip =  gEnter.append('g').attr('class', 'voronoi-clip')
        .append('clipPath')
          .attr('id', 'voronoi-clip-path-' + id) //this id should probably be set on update, unless ID is always set before render
        .append('rect');
      wrap.select('.voronoi-clip rect')
          .attr('x', -10)
          .attr('y', -10)
          .attr('width', width - margin.left - margin.right + 20)
          .attr('height', height - margin.top - margin.bottom + 20);
      wrap.select('.point-paths')
          .attr('clip-path', 'url(#voronoi-clip-path-' + id + ')');


      //var pointClips = wrap.select('.point-clips').selectAll('clipPath') // **BROWSER BUG** can't reselect camel cased elements
      var pointClips = wrap.select('.point-clips').selectAll('.clip-path')
          .data(vertices);
      pointClips.enter().append('clipPath').attr('class', 'clip-path')
        .append('circle')
          .attr('r', 25);
      pointClips.exit().remove();
      pointClips
          .attr('id', function(d, i) { return 'clip-' + id + '-' + d[2] + '-' + d[3] })
          .attr('transform', function(d) { return 'translate(' + d[0] + ',' + d[1] + ')' });


      var voronoi = d3.geom.voronoi(vertices).map(function(d,i) {
          return { 'data': d, 'series': vertices[i][2], 'point': vertices[i][3] }
      });

      //TODO: Add very small amount of noise to prevent duplicates
      var pointPaths = wrap.select('.point-paths').selectAll('path')
          .data(voronoi);
      pointPaths.enter().append('path')
          .attr('class', function(d,i) { return 'path-' + i; });
      pointPaths.exit().remove();
      pointPaths
          .attr('clip-path', function(d) { return 'url(#clip-' + id + '-' + d.series + '-' + d.point + ')'; })
          .attr('d', function(d) { return 'M' + d.data.join(',') + 'Z'; })
          .on('mouseover', function(d) {
            dispatch.pointMouseover({
              point: data[d.series].data[d.point],
              series: data[d.series],
              pos: [x(data[d.series].data[d.point][0]) + margin.left, y(data[d.series].data[d.point][1]) + margin.top],
              pointIndex: d.point,
              seriesIndex: d.series
            });
          })
          .on('mouseout', function(d) {
            dispatch.pointMouseout({
              point: d,
              series: data[d.series],
              pointIndex: d.point,
              seriesIndex: d.series
            });
          });


      dispatch.on('pointMouseover.point', function(d) { 
        wrap.select('.line-' + d.seriesIndex + ' .point-' + d.pointIndex)
          .classed('hover', true);
      });

      dispatch.on('pointMouseout.point', function(d) { 
        wrap.select('.line-' + d.seriesIndex + ' .point-' + d.pointIndex)
          .classed('hover', false);
      });



      var lines = wrap.select('.lines').selectAll('.line')
          .data(function(d) { return d }, function(d) { return d.label });
      lines.enter().append('g')
          .style('stroke-opacity', 1e-6)
          .style('fill-opacity', 1e-6);
      d3.transition(lines.exit())
          .style('stroke-opacity', 1e-6)
          .style('fill-opacity', 1e-6)
          .remove();
      lines.attr('class', function(d,i) { return 'line line-' + i })
          .classed('hover', function(d) { return d.hover })
          .style('fill', function(d,i) { return color[i % 10] })
          .style('stroke', function(d,i) { return color[i % 10] })
      d3.transition(lines)
          .style('stroke-opacity', 1)
          .style('fill-opacity', .5);


      var paths = lines.selectAll('path')
          .data(function(d, i) { return [d.data] });
      paths.enter().append('path')
          .attr('d', d3.svg.line()
            .x(function(d) { return x0(d[0]) })
            .y(function(d) { return y0(d[1]) })
          );
      paths.exit().remove();
      d3.transition(paths)
          .attr('d', d3.svg.line()
            .x(function(d) { return x(d[0]) })
            .y(function(d) { return y(d[1]) })
          );


      var points = lines.selectAll('circle.point')
          .data(function(d) { return d.data });
      points.enter().append('circle')
          .attr('cx', function(d) { return x0(d[0]) })
          .attr('cy', function(d) { return y0(d[1]) });
      points.exit().remove();
      points.attr('class', function(d,i) { return 'point point-' + i });
      d3.transition(points)
          .attr('cx', function(d) { return x(d[0]) })
          .attr('cy', function(d) { return y(d[1]) })
          .attr('r', dotRadius());

    });

    x0 = x;
    y0 = y;

    return chart;
  }



  chart.dispatch = dispatch;

  chart.margin = function(_) {
    if (!arguments.length) return margin;
    margin = _;
    return chart;
  };

  chart.width = function(_) {
    if (!arguments.length) return width;
    width = _;
    return chart;
  };

  chart.height = function(_) {
    if (!arguments.length) return height;
    height = _;
    return chart;
  };

  chart.dotRadius = function(_) {
    if (!arguments.length) return dotRadius;
    dotRadius = d3.functor(_);
    return chart;
  };

  chart.color = function(_) {
    if (!arguments.length) return color;
    color = _;
    return chart;
  };

  chart.id = function(_) {
    if (!arguments.length) return id;
    id = _;
    return chart;
  };


  return chart;
}

d3linewithlegend.js

function d3LineWithLegend() {
  var margin = {top: 30, right: 10, bottom: 40, left: 60},
      width = 960,
      height = 500,
      dotRadius = function() { return 2.5 },
      xAxisLabelText = false,
      yAxisLabelText = false,
      color = d3.scale.category10().range(),
      dispatch = d3.dispatch('showTooltip', 'hideTooltip');

  var x = d3.scale.linear(),
      y = d3.scale.linear(),
      xAxis = d3.svg.axis().scale(x).orient('bottom'),
      yAxis = d3.svg.axis().scale(y).orient('left'),
      legend = d3Legend().height(30).color(color),
      lines = d3Line();


  function chart(selection) {
    selection.each(function(data) {
      var series = data.filter(function(d) { return !d.disabled })
            .map(function(d) { return d.data });

      x   .domain(d3.extent(d3.merge(series), function(d) { return d[0] } ))
          .range([0, width - margin.left - margin.right]);

      y   .domain(d3.extent(d3.merge(series), function(d) { return d[1] } ))
          .range([height - margin.top - margin.bottom, 0]);

      lines
        .width(width - margin.left - margin.right)
        .height(height - margin.top - margin.bottom)
        .color(data.map(function(d,i) {
          return d.color || color[i % 10];
        }).filter(function(d,i) { return !data[i].disabled }))

      xAxis
        .ticks( width / 100 )
        .tickSize(-(height - margin.top - margin.bottom), 0);
      yAxis
        .ticks( height / 36 )
        .tickSize(-(width - margin.right - margin.left), 0);


      var wrap = d3.select(this).selectAll('g.wrap').data([data]);
      var gEnter = wrap.enter().append('g').attr('class', 'wrap d3lineWithLegend').append('g');

      gEnter.append('g').attr('class', 'legendWrap');
      gEnter.append('g').attr('class', 'x axis');
      gEnter.append('g').attr('class', 'y axis');
      gEnter.append('g').attr('class', 'linesWrap');


      legend.dispatch.on('legendClick', function(d, i) { 
        d.disabled = !d.disabled;

        if (!data.filter(function(d) { return !d.disabled }).length) {
          data.forEach(function(d) {
            d.disabled = false;
          });
        }

        selection.transition().call(chart)
      });


      legend.dispatch.on('legendMouseover', function(d, i) {
        d.hover = true;
        selection.transition().call(chart)
      });

      legend.dispatch.on('legendMouseout', function(d, i) {
        d.hover = false;
        selection.transition().call(chart)
      });


      lines.dispatch.on('pointMouseover.tooltip', function(e) {
        dispatch.showTooltip({
          point: e.point,
          series: e.series,
          pos: [e.pos[0] + margin.left, e.pos[1] + margin.top],
          seriesIndex: e.seriesIndex,
          pointIndex: e.pointIndex
        });
      });

      lines.dispatch.on('pointMouseout.tooltip', function(e) {
        dispatch.hideTooltip(e);
      });




      legend
          .color(color)
          .width(width / 2 - margin.right);

      wrap.select('.legendWrap')
          .datum(data)
          .attr('transform', 'translate(' + (width/2 - margin.left) + ',' + (-legend.height()) +')')
          .call(legend);


      //TODO: maybe margins should be adjusted based on what components are used: axes, axis labels, legend
      margin.top = legend.height();  //need to re-render to see update

      var g = wrap.select('g')
          .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');



      var linesWrap = wrap.select('.linesWrap')
          .datum(data.filter(function(d) { return !d.disabled }));

      d3.transition(linesWrap).call(lines);



      var xAxisLabel = g.select('.x.axis').selectAll('text.axislabel')
          .data([xAxisLabelText || null]);
      xAxisLabel.enter().append('text').attr('class', 'axislabel')
          .attr('text-anchor', 'middle')
          .attr('x', x.range()[1] / 2)
          .attr('y', margin.bottom - 20);
      xAxisLabel.exit().remove();
      xAxisLabel.text(function(d) { return d });

      g.select('.x.axis')
          .attr('transform', 'translate(0,' + y.range()[0] + ')')
          .call(xAxis)
        .selectAll('line.tick')
        .filter(function(d) { return !d })
          .classed('zero', true);


      var yAxisLabel = g.select('.y.axis').selectAll('text.axislabel')
          .data([yAxisLabelText || null]);
      yAxisLabel.enter().append('text').attr('class', 'axislabel')
          .attr('transform', 'rotate(-90)')
          .attr('text-anchor', 'middle')
          .attr('y', 20 - margin.left);
      yAxisLabel.exit().remove();
      yAxisLabel
          .attr('x', -y.range()[0] / 2)
          .text(function(d) { return d });

      g.select('.y.axis')
          .call(yAxis)
        .selectAll('line.tick')
        .filter(function(d) { return !d })
          .classed('zero', true);
    });

    return chart;
  }

  chart.dispatch = dispatch;

  chart.margin = function(_) {
    if (!arguments.length) return margin;
    margin = _;
    return chart;
  };

  chart.width = function(_) {
    if (!arguments.length) return width;
    width = _;
    return chart;
  };

  chart.height = function(_) {
    if (!arguments.length) return height;
    height = _;
    return chart;
  };

  chart.color = function(_) {
    if (!arguments.length) return color;
    color = _;
    return chart;
  };

  chart.dotRadius = function(_) {
    if (!arguments.length) return dotRadius;
    dotRadius = d3.functor(_);
    lines.dotRadius = d3.functor(_);
    return chart;
  };

  //TODO: consider directly exposing both axes
  //chart.xAxis = xAxis;

  //Expose the x-axis' tickFormat method.
  chart.xAxis = {};
  d3.rebind(chart.xAxis, xAxis, 'tickFormat');

  chart.xAxis.label = function(_) {
    if (!arguments.length) return xAxisLabelText;
    xAxisLabelText = _;
    return chart;
  }

  // Expose the y-axis' tickFormat method.
  //chart.yAxis = yAxis;

  chart.yAxis = {};
  d3.rebind(chart.yAxis, yAxis, 'tickFormat');

  chart.yAxis.label = function(_) {
    if (!arguments.length) return yAxisLabelText;
    yAxisLabelText = _;
    return chart;
  }

  return chart;
}

nvtooltip.js

/*****
 * Really simple tooltip implementation.
 * I may build upon it, but really trying to keep it minimal.
 *****/

(function($) {

  var nvtooltip = window.nvtooltip = {};

  nvtooltip.show = function(pos, content, gravity, dist) {
    var container = $('<div class="nvtooltip">');

    gravity = gravity || 's';
    dist = dist || 20;

    container
      .html(content)
      .css({left: -1000, top: -1000, opacity: 0})
      .appendTo('body');

    var height = container.height() + parseInt(container.css('padding-top'))  + parseInt(container.css('padding-bottom')),
        width = container.width() + parseInt(container.css('padding-left'))  + parseInt(container.css('padding-right')),
        windowWidth = $(window).width(),
        windowHeight = $(window).height(),
        scrollTop = $('body').scrollTop(),  //TODO: also adjust horizontal scroll
        left, top;


    //TODO: implement other gravities
    switch (gravity) {
      case 'e':
      case 'w':
      case 'n':
        left = pos[0] - (width / 2);
        top = pos[1] + dist;
        if (left < 0) left = 5;
        if (left + width > windowWidth) left = windowWidth - width - 5;
        if (scrollTop + windowHeight < top + height) top = pos[1] - height - dist;
        break;
      case 's':
        left = pos[0] - (width / 2);
        top = pos[1] - height - dist;
        if (left < 0) left = 5;
        if (left + width > windowWidth) left = windowWidth - width - 5;
        if (scrollTop > top) top = pos[1] + dist;
        break;
    }

    container
        .css({
          left: left,
          top: top,
          opacity: 1
        });
  };

  nvtooltip.cleanup = function() {
    var tooltips = $('.nvtooltip');

    // remove right away, but delay the show with css
    tooltips.css({
        'transition-delay': '0 !important',
        '-moz-transition-delay': '0 !important',
        '-webkit-transition-delay': '0 !important'
    });

    tooltips.css('opacity',0);

    setTimeout(function() {
      tooltips.remove()
    }, 500);
  };

})(jQuery);