block by sxywu 2b4aaa88d469ede56c62

visfest block visualization 4

Full Screen

Built with blockbuilder.org

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.4.0/d3-legend.min.js"></script>
  <style>
    body {
      margin:0;
      position:absolute;
      top:0;right:0;bottom:0;left:0;
      overflow-y: scroll;
    }
    svg {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
    text {
      font-family: 'Helvetica';
      fill: #666;
    }
    .brush .extent {
      fill: #555;
      fill-opacity: .1;
      stroke: #fff;
      shape-rendering: crispEdges;
    }

    .hide {
      display: none
    }
  </style>
</head>

<body>
  <script>
    var margin = {top: 20, right: 10, bottom: 20, left: 10};
    var width = 1050 - margin.left - margin.right;
    var height = 1500 - margin.top - margin.bottom;
    var duration = 100;
    var radius = 250;
    var apiSize = 50;
    var ignoreApi = ['d3.select', 'd3.selectAll'];
    var selected = {};
    var hovered = false;
    var brushing = false;
    var zoom = d3.behavior.zoom()
      .size([window.innerWidth, window.innerHeight])
      .on('zoom', panSummary);
    var force = d3.layout.force()
      .size([850, height])
      .charge(function(d) {return d.type === 'api' ? -10 : -Math.pow(d.size, 2)})
      .linkStrength(function(d) {return d.strength})
      .on('tick', updateGraph);

    var blocks = {};
    var api = {};
    var apiNodes = [];
    var blockNodes = [];
    var nodes = [],
        links = [];
    var sizeScale;
    var widthScale;
    var strengthScale;
    var colors;
    var totalApiSize = 0;
  
    var circle, path, text, summary;
    var svg = d3.select("body").append("svg").append("g");
    var summaries = svg.append('g')
      .attr('transform', 'translate(' + width + ',0)');
    var graph = svg.append('g');

    graph.append('rect')
      .attr('width', width)
      .attr('height', height + margin.top + margin.bottom)
      .attr('fill', '#fff')
      .attr('opacity', .9);
    var svgBrush = d3.svg.brush()
      .x(d3.scale.identity().domain([(width / 2) - radius, (width / 2) + radius]))
      .y(d3.scale.identity().domain([(height / 2) - radius, (height / 2) + radius]))
      .on('brushstart', function() {
        summaries.attr('transform', 'translate(' + (width + margin.left + margin.right) + ',0)');
        brushing = true;
      }).on("brush", brush)
      .on('brushend', function() {brushing = false;});
    graph.append("g")
      .attr("class", "brush")
      .call(svgBrush);
    
    function enterGraph() {
      path = graph.selectAll('line')
        .data(links).enter().insert('line', '.brush')
        .attr('stroke', '#ccc')
        .attr('stroke-linecap', 'round');
      circle = graph.selectAll('circle')
        .data(nodes).enter().append('circle')
        .classed('node', true)
        .attr('stroke', 'none')
        .attr('stroke-width', 3)
        .style({'cursor': 'pointer'})
        .on('mouseover', hover)
        .on('mouseleave', unhover)
        .on('click', select);
      text = graph.selectAll('text')
        .data(_.filter(nodes, function(node) {return node.type === 'api';}))
        .enter().append('text')
        .classed('nodeText', true)
        .attr('dy', '.35em')
        .style({
          'font-size': '12px',
          'font-weight': 600,
          'cursor': 'pointer'
        }).on('mouseover', hover)
        .on('mouseleave', unhover)
        .on('click', select);
    };
    function updateGraph() { 
      circle.attr('r', function(d) {return d.size})
        .attr('fill', function(d) {return d.fill || '#fff'})
        .attr('cx', function(d) {return d.x})
        .attr('cy', function(d) {return d.y});
      text
        .attr('text-anchor', function(d, i) {
          return i < (apiSize / 2) ? 'start' : 'end';
        }).attr('transform', function(d, i) {
          var angle;
          if (i < (apiSize / 2)) {
            angle = (360 / apiSize) * i - 90;
          } else {
            angle = -(360 / apiSize) * (apiSize - i) + 90;
          }
          return 'translate(' + d.x + ',' + d.y +
            ')rotate(' + angle + ')';
        }).text(function(d) {return d.id});
      
      path.attr('stroke-width', function(d) {return d.size})
        .attr('x1', function(d) {return d.source.x})
        .attr('y1', function(d) {return d.source.y})
        .attr('x2', function(d) {return d.target.x})
        .attr('y2', function(d) {return d.target.y});

      applyHighlight();
    };
    
    function renderSummary() {
      var selectedData = _.chain(selected)
        .filter(function(d) {return d.type !== 'api'})
        .sortBy(function(d) {return d.image})
        .value();

      if (_.isEmpty(selectedData)) {
        summaries.classed('hide', true)
          .attr('transform', 'translate(' + (width + margin.left + margin.right) + ',0)');
      } else {
        summaries.classed('hide', false);
      }

      summary = summaries.selectAll('g').data(selectedData);
      
      var enterSummary = summary.enter().append('g')
        .classed('summary', true);
      enterSummary.append('rect');
      enterSummary.append('text')
        .classed('title', true);
      enterSummary.append('image');
      enterSummary.append('text')
        .classed('username', true);

      var padding = 7.5;
      var textHeight = 12;
      var imageWidth = 230;
      var imageHeight = 120;
      var summaryWidth = 300;
      var summaryHeight = (imageHeight + 8 * padding + textHeight);
      summary.select('rect')
        .attr('width', imageWidth + 2 * padding)
        .attr('height', imageHeight + 4 * padding + 2 * textHeight)
        .attr('rx', 3).attr('ry', 3)
        .attr('fill', '#fcfcfc')
        .attr('stroke-width', 3);

      summary.select('.title')
        .attr('dy', '.35em')
        .attr('transform', 'translate(' + padding + ',' + (padding + textHeight / 2) + ')')
        .style({
          'font-weight': 600
        }).text(function(d) {
          var title = d.title;
          if (title && title.length > 25) {
            title = title.substring(0, 25) + ' ...';
          }
          return title;
        });
      
      summary.select('image')
        .attr('xlink:href', function(d) {
          return d.image || 'visfest4.png';
        }).attr('width', imageWidth)
        .attr('height', imageHeight)
        .attr('transform', 'translate(' + padding + ',' + (textHeight + 2 * padding) + ')');

      summary.select('.username')
        .attr('dy', '.35em')
        .attr('transform', 'translate(' + padding + ',' +
          (4 * padding + textHeight + imageHeight) + ')')
        .style({
          'font-size': textHeight
        }).text(function(d) {return d.user});

      var heightSize = Math.floor(2 * radius / (imageHeight + 4 * padding));
      summary.attr('transform', function(d, i) {
        d.summaryX = summaryWidth * (Math.floor(i / heightSize));
        d.summaryY = summaryHeight * (i % heightSize) + ((height / 2) - radius);
        return 'translate(' + d.summaryX + ',' + d.summaryY + ')';
      }).style({
        'cursor': 'pointer'
      }).on('click', function(d) {
        window.open('//bl.ocks.org/' + d.user + '/' + d.id, '_blank');
      }).on('mouseover', hover)
      .on('mouseleave', unhover);
      
      summary.exit().remove();
    };
    
    function hover() {
      if (brushing) return;
      hovered = d3.select(this).datum();
      calculateHighlight(hovered, true, 'hovered');
      applyHighlight(hovered);
      renderSummary();
    }
    
    function unhover() {
      if (brushing) return;
      hovered = false;
      calculateHighlight(d3.select(this).datum(), false, 'hovered');
      applyHighlight();
      renderSummary();
    }
    
    function select() {
      if (brushing) return;

      var data = d3.select(this).datum();
      if (selected[data.id]) {
        delete selected[data.id];
        calculateHighlight(data, false, 'selected');
      } else {
        selected[data.id] = data;
        calculateHighlight(data, true, 'selected');
      }
      renderSummary();
    }
    
    function brush() {
      selected = {};
      var extent = d3.event.target.extent();
      circle.each(function(d) {
        if (d.type !== 'api' &&
          extent[0][0] <= d.x && d.x < extent[1][0]
            && extent[0][1] <= d.y && d.y < extent[1][1]) {
          selected[d.id] = d; 
        }
      });
      
      calculateHighlight(null, true, 'selected');
      applyHighlight();
      renderSummary();
    }

    function panSummary() {
      svg.attr('transform', 'translate(' + d3.event.translate + ')');
    }

    function searchUsername(e) {
      var chara = d3.event.keyCode;
      var value = d3.event.target.value;
      if (chara === 13) {
        selected = {};
        svgBrush.empty();
        var block = _.chain(blockNodes)
          .filter(function(block) {
            return value && block.user.match(value);
          }).each(function(block) {
            selected[block.id] = block;
          }).value();
        calculateHighlight(null, true, 'selected');
        applyHighlight();
        renderSummary();
      } else if (chara === 27) {
        selected = {};
        calculateHighlight(null, true, 'selected');
        applyHighlight();
        renderSummary();

        value = d3.event.target.value = null;
        d3.event.target.blur();
      }
      var matched = _.filter(blockNodes, function(block) {
        return value && block.user.match(value);
      });
      var matchedText = value ? (matched.length + ' matches') : '';
      numMatches.text(matchedText);
    }
    
    function calculateHighlight(data, highlight, type) {
      var nodesToHighlight = {};

      path.each(function(d) {
        d.selected = false;
        d.hovered = false;
        if (selected[d.source.id]) {
          // if it's highlighted because it's been previously selected
          if (nodesToHighlight[d.target.id] !== 'hovered') {
            nodesToHighlight[d.target.id] = 'selected';
          }
          d.selected = true;
        } else if (selected[d.target.id]) {
          if (nodesToHighlight[d.source.id] !== 'hovered') {
            nodesToHighlight[d.source.id] = 'selected';
          }
          d.selected = true;
        }
        if (highlight && data && d.source.id === data.id) {
          // if path is the one being hovered or selected
          if (nodesToHighlight[d.target.id] !== 'hovered') {
            nodesToHighlight[d.target.id] = type; 
          }
          d[type] = true;
        }  else if (highlight && data && d.target.id === data.id) {
          if (nodesToHighlight[d.source.id] !== 'hovered') {
            nodesToHighlight[d.source.id] = type;
          }
          d[type] = true;
        }
      });

      d3.selectAll('.node,.nodeText,.summary').each(function(d) {
        d.selected = false;
        d.hovered = false;
        if (selected[d.id]) {
          // if previously selected
          d.selected = true;
        }
        if (nodesToHighlight[d.id]) {
          // if on other side of a node being hovered or selected
          d[nodesToHighlight[d.id]] = true;
        }
        if (highlight && data && d.id === data.id) {
          // if circle is being hovered or selected
          d[type] = true;
        }
      });
    }
    
    function applyHighlight(hoveredData) {
      path.attr('stroke', function(d) {
          return d.hovered ? '#FFD84B' : '#ccc';
        }).transition().duration(brushing ? 0 : duration)
        .attr('stroke-opacity', function(d) {
          if (d.selected || d.hovered) {
            return 1;
          } else if (_.isEmpty(selected) && !hovered) {
            return .5;
          } else {
            return .15;
          }
        });
      circle.attr('stroke', function(d) {
          return d.hovered ? '#FFD84B' : 'none';
        }).transition().duration(brushing ? 0 : duration)
        .attr('fill-opacity', function(d) {
          if (d.selected || d.hovered) {
            return d.type === 'api' ? .5 : 1;
          } else if (_.isEmpty(selected) && !hovered) {
            return d.type === 'api' ? .25 : 1;
          } else {
            return .15;
          }
        });
      text.transition().duration(brushing ? 0 : duration)
        .attr('fill-opacity', function(d) {
          return d.selected || d.hovered ||
            (_.isEmpty(selected) && !hovered) ? 1 : .15;
        });
      summary && summary.select('rect')
        .attr('stroke', function(d) {
          return d.hovered ? '#FFD84B' : 'none';
        });
      hoveredData && selected[hoveredData.id] && summaries.transition().duration(duration * 3)
        .attr('transform', function() {
          var x = width + margin.left + margin.right - hoveredData.summaryX;
          return 'translate(' + x + ',0)';
        });
    }

    // load data
    d3.json('data.json', function(data) {
      var linkStrengths = [];
      blocks = _.chain(data)
        .uniq(function(block) {return block.id})
        .reduce(function(memo, block) {
          memo[block.id] = block;
          return memo;
        }, {}).value();
      api = _.chain(data)
        .pluck('api')
        .map(function(api) {return _.pairs(api)})
        .flatten().compact()
        .filter(function(api) {return !_.contains(ignoreApi, api[0])})
        .reduce(function(memo, api) {
          linkStrengths.push(api[1]);
          if (!memo[api[0]]) {
            memo[api[0]] = 0;
          }
          memo[api[0]] += api[1];
          return memo;
        }, {})
        .value();
      totalApiSize = _.size(api);
      colors = d3.scale.category20();
      sizeScale = d3.scale.linear()
        .domain([_.min(api), _.max(api)])
        .range([7.5, 35]);
      widthScale = d3.scale.linear()
        .domain([_.min(linkStrengths), _.max(linkStrengths)])
        .range([1, 4]);
      strengthScale = d3.scale.linear()
        .domain([_.min(linkStrengths), _.max(linkStrengths)])
        .range([0, 1]);
      apiNodes = _.chain(api)
        .map(function(count, name) {
          api[name] = {
            id: name,
            size: sizeScale(count),
            fill: colors(name),
            fixed: true,
            type: 'api'
          };
          return api[name];
        }).sortBy(function(node) {
          return -node.size;  
        }).map(function(node, i) {
          if (i > (apiSize - 1)) {
            delete api[node.id];
            return; 
          }
          var radian = (2 * Math.PI) / apiSize * i - (Math.PI / 2);
          node.x = radius * Math.cos(radian) + (width / 2);
          node.y = radius * Math.sin(radian) + (height / 2);
          return node;
        }).compact().value();
      blockNodes = _.chain(data)
        .filter(function(block) {
          // only keep if it has API's and at least one of them are in apiNodes
          return !_.isEmpty(block.api) && _.some(block.api, function(count, apiName) {
            return api[apiName];
          });
        }).uniq(function(block) {return block.id})
        .map(function(block) {
          var node = {
            id: block.id,
            title: block.description,
            user: block.owner.login,
            image: block.thumbnail,
            size: 4,
            fill: '#666'
          };
          var totalCount = _.reduce(block.api, function(memo, count, apiName) {
            return memo + (api[apiName] ? count : 0);
          }, 0);
          _.each(block.api, function(count, apiName) {
            if (!api[apiName]) return;
            links.push({
              source: node,
              target: api[apiName],
              size: widthScale(count),
              strength: (.75 / totalCount) * count
            });
          });

          return node;
        }).value();
      
      nodes = _.union(apiNodes, blockNodes);
      force.nodes(nodes).links(links);
      
      enterGraph();
      
      force.start();
    });
  </script>
</body>