block by armollica 2dcfd66a64922990995f905aa0dc4d7b

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.

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")