block by emeeks c478e0aac6373a6a4ec8

Ch. 9, Fig. 13 - D3.js in Action

Full Screen

This is the code for Chapter 9, Figure 13 from D3.js in Action that continues to develop a data dashboard, adding graphical representation of datapoints to the brush to improve usability.

index.html

<html>
<head>
  <title>D3 in Action Chapter 9 - Example 5</title>
  <meta charset="utf-8" />
<script src="//d3js.org/colorbrewer.v1.min.js" type="text/JavaScript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
</head>
<style>
body,html {
width: 100%;
height: 100%; #a
}
.svgDash {
width: 50%;
height: 50%;
background: #fcfcfc;
}

#leftSVG {
 float:left; 
}

#spreadsheet {
width: 100%;
height: 30%;
overflow:auto;
background: #fcfcfc;
}

circle.pack {
stroke: black;
stroke-width: 2px;
}

rect.bar {
fill: gray;
stroke: black;
stroke-width: 1px;
}


div.table {
  position:relative;
}
div.data {
  position: absolute;
  width: 90px;
  padding: 0 5px;
}

div.head {
  position: absolute;
}

div.datarow {
  position: absolute;
  width: 100%;
  border-top: 2px black solid;
  background: white;
  height: 35px;
  overflow: hidden;
}

#brush {
  width: 100%;
  height: 20%;
}

rect.extent {
  fill-opacity: .25;
  stroke: black;
  stroke-width: 2px;
}

.tick line {
  shape-rendering: crispEdges;
  stroke: #000;
}

line.minor  {
  stroke: #777;
  stroke-dasharray: 2,2;
}

path.domain {
  fill: none;
  stroke: black;
}
</style>
<body>
  <svg id="leftSVG" class="svgDash"></svg>
<svg id="rightSVG"  class="svgDash"></svg>
<div id="brush"></div>
<div id="spreadsheet"></div>
</body>
  <footer>
    
<script>
  window.onresize = function(event) {
      redraw();
  }
  
  function hover(hoverD) {
    d3.selectAll("circle.pack").filter(function (d) {return d == hoverD}).style("fill", "#94B8FF");
    d3.selectAll("div.datarow").filter(function (d) {return d == hoverD}).style("background", "#94B8FF");
    d3.selectAll("rect.bar").filter(function (d) {return d == hoverD}).style("fill", "#94B8FF");
    d3.selectAll("rect.bar").filter(function (d) {return d.values.indexOf(hoverD) > -1}).style("fill", "#94B8FF");
    }

  function mouseOut() {
    d3.selectAll("circle.pack").style("fill", function(d) {return depthScale(d.depth)});
    d3.selectAll("rect.bar").style("fill", "gray").style("stroke-width", 0);
    d3.selectAll("div.datarow").style("background", "white");    
  }
  
  d3.json("tweets.json",function(error,data) {main(data.tweets)});

  function main(incData) {
    
  createSpreadsheet(incData, "#spreadsheet");
  var nestedTweets = d3.nest()
    .key(function (el) {return el.user})
    .entries(incData);

    packableTweets = {id: "root", values: nestedTweets}

    createBar(nestedTweets, "#rightSVG");
    createPack(packableTweets, "#leftSVG");
    createBrush(incData);
    redraw();
  }
  
  function createBrush(incData) {
    timeRange = d3.extent(incData.map(function(d) {return new Date(d.timestamp)}));
    timeScale = d3.time.scale().domain(timeRange).range([10,990]);
    
    timeAxis = d3.svg.axis()
    .scale(timeScale)
    .orient('bottom')
    .ticks(d3.time.hours, 2)
    .tickFormat(d3.time.format('%I%p'));
    
    timeBrush = d3.svg.brush()
                    .x(timeScale)
                    .extent(timeRange)
                    .on("brush", brushed);

    
    var brushSVG = d3.select("#brush").append("svg").attr("height", "100%").attr("width", "100%");
    brushSVG.append("g").attr("transform", "translate(0,100)").attr("id", "brushAxis").call(timeAxis);
    brushSVG.append("g").attr("transform", "translate(0,50)").attr("id", "brushG").call(timeBrush)
    .selectAll("rect").attr("height", 50);
    
    
    function brushed() {
      var rightSize = canvasSize("#rightSVG");
      var e = timeBrush.extent();
      d3.selectAll("circle.pack").filter(function(d){return d.depth == 2})
      .style("display", function (d) {
        return new Date(d.timestamp) >= e[0] && new Date(d.timestamp) <= e[1] ? "block" : "none"
        });
      
      d3.select("#rightSVG")
      .selectAll("rect")
      .attr("x", function(d,i) {return barXScale(d.key) + 5})
      .attr("width", function() {return barXScale.rangeBand() - 5})
      .attr("y", function(d) {return barYScale(filteredLength(d))})
      .style("stroke", "black")
      .attr("height", function(d) {return rightSize[1] - barYScale(filteredLength(d))})
      
      function filteredLength(d) {
        var filteredValues = d.values.filter(function (p) {
          return new Date(p.timestamp) >= e[0] && new Date(p.timestamp) <= e[1]
        })
        return filteredValues.length;
      }

    }
    
  }
  
  function canvasSize(targetElement) {
    var newHeight = parseFloat(d3.select(targetElement).node().clientHeight);
    var newWidth = parseFloat(d3.select(targetElement).node().clientWidth);
    return [newWidth,newHeight];
  }
  
  function redraw() {
      var leftSize = canvasSize("#leftSVG");
      packChart.size(leftSize);

      d3.select("#leftSVG")
      .selectAll("circle")
      .attr("class", "pack")
      .data(packChart(packableTweets))
      .attr("r", function(d) {return d.r - (d.depth * 0)})
      .attr("cx", function(d) {return d.x})
      .attr("cy", function(d) {return d.y})
      .style("stroke", "black")
      .style("stroke", "2px")
      .on("brushstart", function() {d3.select(this).style("stroke-width", "4px")})
      .on("brushend", function() {d3.select(this).style("stroke-width", "2px")});
      
      var rectNumber = d3.select("#rightSVG").selectAll("rect").size();
      var rectData = d3.select("#rightSVG").selectAll("rect").data();
      var rectMax = d3.max(rectData, function(d) {return d.values.length});
      
      var rightSize = canvasSize("#rightSVG");

      barXScale = d3.scale.ordinal().domain(rectData.map(function(d){return d.key})).rangeBands([0, rightSize[0]]);

      barYScale = d3.scale.linear()
      .domain([0, rectMax])
      .range([rightSize[1],0])

    timeTickScale = d3.scale.linear().domain([0,1000]).rangeRound([10,1]).clamp(true);

    var bExtent = timeBrush.extent();
    timeScale.range([10,rightSize[0] + leftSize[0] - 10]);
    timeAxis.scale(timeScale).ticks(d3.time.hours, timeTickScale((rightSize[0] + leftSize[0])));
    timeBrush.x(timeScale);

      d3.select("#rightSVG")
      .selectAll("rect")
      .attr("x", function(d,i) {return barXScale(d.key) + 5})
      .attr("width", function() {return barXScale.rangeBand() - 5})
      .attr("y", function(d) {return barYScale(d.values.length)})
      .style("stroke", "black")
      .attr("height", function(d) {return rightSize[1] - barYScale(d.values.length)})

    d3.select("#brushAxis").call(timeAxis);
    d3.select("#brushG").call(timeBrush.extent(bExtent));
    d3.select("#brushG").selectAll("circle.timeline")
    .attr("cx", function(d) {return timeScale(new Date(d.timestamp))});
   
   
   var tweets = d3.selectAll("div.datarow").data();

   d3.selectAll("g.resize").append("circle")
.attr("r", 25)
.attr("cy",25)
.style("fill", "white")
.style("stroke", "black")
.style("stroke-width", "4px")
.style("opacity", .75)

d3.select("#brushG")
.selectAll("circle.timeline")
.data(tweets)
.enter()
.append("circle")
.style("fill","red").style("stroke", "black").style("stroke-width", "1px")
.style("pointer-events","none")
.attr("class","timeline").attr("r", 5).attr("cy", 25)
.attr("cx", function(d) {return timeScale(new Date(d.timestamp))}) 
 
  }
  
  function createBar(incData,targetSVG) {
      
      d3.select(targetSVG).selectAll("rect").data(incData)
      .enter()
      .append("rect")
      .attr("class", "bar")
      .attr("fill", "darkred")
      .on("mouseover", hover)
      .on("mouseout", mouseOut);
      }
  
  function createPack(incData,targetSVG) {

      depthScale = d3.scale.quantize().domain([0,1,2]).range(colorbrewer.Reds[3]);

      packChart = d3.layout.pack();
      packChart.size([500,500])
      .children(function(d) {return d.values})
      .value(function(d) {return 1})

      d3.select(targetSVG)
      .append("g")
      .attr("transform", "translate(0,0)")
      .selectAll("circle")
      .data(packChart(incData))
      .enter()
      .append("circle")
      .style("fill", function(d) {return depthScale(d.depth)})
      .on("mouseover", hover)
      .on("mouseout", mouseOut);
    }
      function createSpreadsheet(incData, targetDiv) {
        
        var keyValues = d3.keys(incData[0])
        
        d3.select(targetDiv)
        .append("div")
        .attr("class", "table")

        d3.select("div.table")
        .append("div")
        .attr("class", "head row")
        .selectAll("div.data")
        .data(keyValues)
        .enter()
        .append("div")
        .attr("class", "data")
        .html(function (d) {return d})
        .style("left", function(d,i) {return (i * 100) + "px"});

        d3.select("div.table")
        .selectAll("div.datarow")
        .data(incData, function(d) {return d.content}).enter()
        .append("div")
        .attr("class", "datarow row")
        .style("top", function(d,i) {return (40 + (i * 40)) + "px"})
        .on("mouseover", hover)
        .on("mouseout", mouseOut);
        
        d3.selectAll("div.datarow")
        .selectAll("div.data")
        .data(function(d) {return d3.entries(d)})
        .enter()
        .append("div")
        .attr("class", "data")
        .html(function (d) {return d.value})
        .style("left", function(d,i,j) {return (i * 100) + "px"});
      }  
</script>
  </footer>

</html>

tweets.json

{
"tweets": [
{"user": "Al", "content": "I really love seafood.", "timestamp": " Mon Dec 23 2013 21:30 GMT-0800 (PST)", "retweets": ["Raj","Pris","Roy"], "favorites": ["Sam"]},
{"user": "Al", "content": "I take that back, this doesn't taste so good.", "timestamp": "Mon Dec 23 2013 21:55 GMT-0800 (PST)", "retweets": ["Roy"], "favorites": []},
{"user": "Al", "content": "From now on, I'm only eating cheese sandwiches.", "timestamp": "Mon Dec 23 2013 22:22 GMT-0800 (PST)", "retweets": [], "favorites": ["Roy","Sam"]},
{"user": "Roy", "content": "Great workout!", "timestamp": " Mon Dec 23 2013 7:20 GMT-0800 (PST)", "retweets": [], "favorites": []},
{"user": "Roy", "content": "Spectacular oatmeal!", "timestamp": " Mon Dec 23 2013 7:23 GMT-0800 (PST)", "retweets": [], "favorites": []},
{"user": "Roy", "content": "Amazing traffic!", "timestamp": " Mon Dec 23 2013 7:47  GMT-0800 (PST)", "retweets": [], "favorites": []},
{"user": "Roy", "content": "Just got a ticket for texting and driving!", "timestamp": " Mon Dec 23 2013 8:05 GMT-0800 (PST)", "retweets": [], "favorites": ["Sam", "Sally", "Pris"]},
{"user": "Pris", "content": "Going to have some boiled eggs.", "timestamp": " Mon Dec 23 2013 18:23 GMT-0800 (PST)", "retweets": [], "favorites": ["Sally"]},
{"user": "Pris", "content": "Maybe practice some gymnastics.", "timestamp": " Mon Dec 23 2013 19:47  GMT-0800 (PST)", "retweets": [], "favorites": ["Sally"]},
{"user": "Sam", "content": "@Roy Let's get lunch", "timestamp": " Mon Dec 23 2013 11:05 GMT-0800 (PST)", "retweets": ["Pris"], "favorites": ["Sally", "Pris"]}
]
}