block by jeremycflin 5924fa2345c9a66c6473ced22e67d9e4

Movie Genre, Rating and Budget

Full Screen

IMDB movie ratings by genre. Movies with bigger budgets have bigger bubbles.

Uses the d3.forceChart() plugin. Data are from the ggplot2 R package.

forked from bumbeishvili‘s block: Movie Genre, Rating and Budget

index.html

<html>
  <head>
    <style>
      body {
       font: 14px sans-serif; 
      }
      
      .axis path,
      .axis line {
        fill: none;
        stroke: black;
      }
      
      .axis path { stroke: none; }
    </style>
  </head>
  <body>
     <script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
     <script src="force-chart.js"></script>
     <script>
       var margin = { top: 10, left: 100, bottom: 30, right: 50 },
           width = 960 - margin.left - margin.right,
           height = 600 - margin.top - margin.bottom;
       
       var x = function(d) { return d.rating; },
           y = function(d) { return d.genre; },
           area = function(d) { return d.budget; };
       
       var xScale = d3.scale.linear()
             .domain([0, 10])
             .range([0, width]),
           yScale = d3.scale.ordinal()
             .domain(["Comedy", "Action", "Romance", "Animation", "Drama"])
             .rangeBands([height, 0]),
           areaScale = d3.scale.linear().range([0, 125]),
           colorScale = d3.scale.quantize()
            .domain([0, 10])
            .range(["#AB879C","#928EAB","#6C97B0","#3E9EA7","#1BA38F",
                    "#37A46C","#62A145","#8D991C","#B98A00","#E07423"]);
       
       var xValue = function(d) { return xScale(x(d)); },
           yValue = function(d) { return yScale(y(d)) + yScale.rangeBand()/2; },
           rValue = function(d) {
             var A = areaScale(area(d));
             return Math.sqrt(A / Math.PI);
           },
           colorValue = function(d) { return colorScale(x(d)); };
       
       var xAxis = d3.svg.axis().scale(xScale).orient("bottom"),
           yAxis = d3.svg.axis().scale(yScale).orient("left");
       
       var bubbleChart = d3.forceChart()
        .size([width, height])
        .x(xValue)
        .y(yValue)
        .r(rValue)
        .xGravity(3)    // make the x-position more accurate
        .yGravity(1/3); // ...and the y-position more flexible
        
       var svg = d3.select("body").append("svg")
          .attr("width", width + margin.left + margin.right)
          .attr("height", height + margin.top + margin.bottom)
        .append("g")
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
       
       d3.json("movies.json", function(error, movies) {
         if (error) throw error;
         
         areaScale.domain([0,d3.max(movies, area)]);
         
         // Draw axes
         svg.append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + height + ")")
            .call(xAxis)
          .append("text")
            .attr("dx", width)
            .attr("dy", -6)
            .style("text-anchor", "end")
            .text("IMDB Rating");
          
         svg.append("g")
            .attr("class", "y axis")
            .call(yAxis)
          .selectAll(".tick line")
            .attr("x2", width)
            .attr("stroke-dasharray", "1, 2")
            .style("stroke", "lightgrey");
         
         // Draw legend
         svg.append("g").call(legend);
            
         // Draw bubbles
         svg.append("g").call(bubbleChart, movies)
            .attr("class", "bubbles")
          .selectAll(".node").append("circle")
            .attr("r", function(d) { return d.r0; })
            .attr("fill", colorValue)
            .attr("stroke", "slategrey");
       });
       
       function legend(selection) {
         var legendData = [
           { budget: 200000000, text: "$200 million", dy: 0 },
           { budget: 100000000, text: "$100 million", dy: 20 },
           { budget: 50000000, text: "$50 million", dy: 40 },
           { budget: 10000000, text: "$10 million", dy: 60 }
         ];
         
         var legend = selection
            .attr("class", "legend")
            .attr("transform", "translate(" + xScale(9.5) + "," + (height/2 - 30) + ")");
         
         legend.append("text")
          .attr("dx", -6)
          .attr("dy", -16)
          .text("Budget");
          
         legend.selectAll(".item").data(legendData)
          .enter().append("g")
            .attr("transform", function(d) { return "translate(0," + d.dy + ")"; })
            .each(function(d) {
              d3.select(this).append("circle")
                .attr("r", rValue(d))
                .style("fill", "none")
                .style("stroke", "slategrey");
              d3.select(this).append("text")
                .attr("dx", 10)
                .attr("dy", 4)
                .text(d.text);
            });
       }
     </script>
  </body>
</html>

force-chart.js

d3.forceChart = function() {
  var width = 400, 
      height = 300, 
      padding = 3,
      x = function(d) { return d[0]; },
      y = function(d) { return d[1]; },
      r = function(d) { return d[2]; },
      xStart = function(d) { return x(d) + 50*Math.random() - 25},
      yStart = function(d) { return y(d) + 50*Math.random() - 25},
      rStart = function(d) { return r(d); },
      draggable = true,
      xGravity = function(d) { return 1; },
      yGravity = function(d) { return 1; },
      rGravity = function(d) { return 1; },
      shape = "circle",
      tickUpdate = function() {};
  
  var force = d3.layout.force()
    .charge(0)
    .gravity(0);
  
  function chart(selection, nodes) {
    
    if (shape === "circle") { collide = collideCircle; }
    else if (shape === "square") { collide = collideSquare; }
    else { console.error("forceChart.shape must be 'circle' or 'square'"); }
    
    nodes = nodes
      .map(function(d) {
        d.x = xStart(d);
        d.y = yStart(d);
        d.r = rStart(d);
        d.x0 = x(d);
        d.y0 = y(d);
        d.r0 = r(d);
        return d;    
      });
      
    var gNodes = selection.selectAll(".node").data(nodes)
      .enter().append("g")
        .attr("class", "node")
        .call(draggable ? force.drag : null);
        
    force
      .size([width, height])
      .nodes(nodes)
      .on("tick", tick)
      .start();
      
    function tick(e) {
      gNodes
        .each(gravity(e.alpha * .1))
        .each(collide(.5))
        .attr("transform", function(d) {
          return "translate(" + d.x + "," + d.y + ")";
        })
        .call(tickUpdate);
    }

    function gravity(k) {
      return function(d) {
        var dx = d.x0 - d.x,
            dy = d.y0 - d.y,
            dr = d.r0 - d.r;
            
        d.x += dx * k * xGravity(d);
        d.y += dy * k * yGravity(d);
        d.r += dr * k * rGravity(d);
      };
    }

    function collideCircle(k) {
      var q = d3.geom.quadtree(nodes);
      return function(node) {
        var nr = node.r + padding,
            nx1 = node.x - nr,
            nx2 = node.x + nr,
            ny1 = node.y - nr,
            ny2 = node.y + nr;
        q.visit(function(quad, x1, y1, x2, y2) {
          if (quad.point && (quad.point !== node)) {
            var x = node.x - quad.point.x,
                y = node.y - quad.point.y,
                l = x * x + y * y,
                r = nr + quad.point.r;
            if (l < r * r) {
              l = ((l = Math.sqrt(l)) - r) / l * k;
              node.x -= x *= l;
              node.y -= y *= l;
              quad.point.x += x;
              quad.point.y += y;
            }
          }
          return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
        });
      };
    }
    
    function collideSquare(k) {
    var q = d3.geom.quadtree(nodes);
    return function(node) {
      var nr = node.r + padding,
          nx1 = node.x - nr,
          nx2 = node.x + nr,
          ny1 = node.y - nr,
          ny2 = node.y + nr;
      q.visit(function(quad, x1, y1, x2, y2) {
        if (quad.point && (quad.point !== node)) {
          var x = node.x - quad.point.x,
              y = node.y - quad.point.y,
              lx = Math.abs(x),
              ly = Math.abs(y),
              r = nr + quad.point.r;
          if (lx < r && ly < r) {
            if (lx > ly) {
              lx = (lx - r) * (x < 0 ? -k : k);
              node.x -= lx;
              quad.point.x += lx;
            } else {
              ly = (ly - r) * (y < 0 ? -k : k);
              node.y -= ly;
              quad.point.y += ly;
            }
          }
        }
        return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
      });
    };
  }
  }
  
  chart.size = function(_) {
    if (!arguments.length) return [width, height];
    width = _[0];
    height = _[1];
    return chart;
  };
  
  chart.x = function(_) {
    if (!arguments.length) return x;
    if (typeof _ === "number") {
      x = function() { return _; };
    }
    else if (typeof _ === "function") {
      x = _;
    }
    return chart;
  };
  
  chart.y = function(_) {
    if (!arguments.length) return y;
    if (typeof _ === "number") {
      y = function() { return _; };
    }
    else if (typeof _ === "function") {
      y = _;
    }
    return chart;
  };
  
  chart.r = function(_) {
    if (!arguments.length) return r;
    if (typeof _ === "number") {
      r = function() { return _; };
    }
    else if (typeof _ === "function") {
      r = _;
    }
    return chart;
  };
  
  chart.draggable = function(_) {
    if (!arguments.length) return draggable;
    draggable = _;
    return chart;
  };
  
  chart.padding = function(_) {
    if (!arguments.length) return padding;
    padding = _;
    return chart;
  };
  
  chart.xGravity = function(_) {
    if (!arguments.length) return xGravity;
    if (typeof _ === "number") {
      xGravity = function() { return _; };
    }
    else if (typeof _ === "function") {
      xGravity = _;
    }
    return chart;
  };
  
  chart.yGravity = function(_) {
    if (!arguments.length) return yGravity;
    if (typeof _ === "number") {
      yGravity = function() { return _; };
    }
    else if (typeof _ === "function") {
      yGravity = _;
    }
    return chart;
  };
  
  chart.rGravity = function(_) {
    if (!arguments.length) return rGravity;
    if (typeof _ === "number") {
      rGravity = function() { return _; };
    }
    else if (typeof _ === "function") {
      rGravity = _;
    }
    return chart;
  };
  
  chart.xStart = function(_) {
    if (!arguments.length) return xStart;
    if (typeof _ === "number") {
      xStart = function() { return _; };
    }
    else if (typeof _ === "function") {
      xStart = _;
    }
    return chart;
  };
  
  chart.yStart = function(_) {
    if (!arguments.length) return yStart;
    if (typeof _ === "number") {
      yStart = function() { return _; };
    }
    else if (typeof _ === "function") {
      yStart = _;
    }
    return chart;
  };
  
  chart.rStart = function(_) {
    if (!arguments.length) return rStart;
    if (typeof _ === "number") {
      rStart = function() { return _; };
    }
    else if (typeof _ === "function") {
      rStart = _;
    }
    return chart;
  };
  
  chart.shape = function(_) {
    if (!arguments.length) return shape;
    shape = _;
    return chart;
  };
  
  chart.tickUpdate = function(_) {
    if (!arguments.length) return tickUpdate;
    tickUpdate = _;
    return chart;
  };
  
  return chart;
};

get-data.r

library(dplyr)
library(ggplot2movies)
library(tidyr)
library(jsonlite)

movies %>%
  filter(!is.na(budget)) %>%
  gather(genre, isGenre, Action:Short) %>%
  filter(isGenre == 1, 
         mpaa != "", 
         !(genre %in% c("Short", "Documentary")),
         year > 2000) %>%
  select(title, year, length, rating, budget, votes, mpaa, genre) %>%
  toJSON %>%
  write("movies.json")