We use cookies

We use cookies and other tracking technologies to improve your browsing experience on our website, to show you personalized content and targeted ads, to analyze our website traffic, and to understand where our visitors are coming from.

Tanya Shapiro
  • Home
  • About
  • Talks
  • Blog
  • Projects

RuPaul’s Drag Race

R
Observable
interactive
Web scraping and exploratory data analysis project of RuPaul’s Drag Race. Includes a custom Spotify playlist, an interactive map, and a network diagram.

Data Collection

In this personal project, I dove deep into the fabulous world of drag queens, scouring wiki pages about RuPaul’s Drag Race and other global franchises to gather data. With the help of rvest, a web scraping library in R, I was able to grab all the details I needed.

After I scraped the data, I sketch out a blueprint for what the ultimate drag queen database would look like, you can see the data model on my GitHub. With my new “database” of Drag Queen data, I set out to explore the data with a few cool projects.

Lip Sync Playlist

First - I wanted a playlist of RPDR lip sync songs. I needed some jamz to inspire me while I thought about some visualization projects.

Luckily, I had already scraped all the lip sync songs from Wikipedia. Using Spotify’s API service, I was able to lookup each song and get the Spotify track IDs. I then used their create playlist function to generate my own playlist using the track IDs. In a matter of seconds I had 300+ songs and 20hrs worth of lip sync songs in my very own playlist!

Where Are You Queen?

Where are the queens originally from? I used tidygeocoder to produce the geo coordinates for each hometown, and used leafletR (Leaflet wrapper) to generate the map. With crosstalk, I created a filter input - you can search for any queen featured on RPDR. It’s pretty cool to see how Ru’s family footprint has expanded around the world - you can find queens from almost all continents!

+−
Leaflet | © OpenStreetMap contributors © CARTO

Drag Race NetWerk

I also wanted to explore the connections and overlap among queens featured on the different drag show seasons and franchises. I created this network diagram with D3 in an Observable notebook.

Sometimes contestants from previous seasons reappear on newer seasons. There are also cross over shows like UK vs. The World, where contestants from different international franchises are invited to compete again for an international crown.

viewof collide = Inputs.toggle({ label: "Collide", value: true })
viewof useRadial = Inputs.toggle({ label: "Use Radial Force", value: true })
viewof radialForce = Inputs.range([0.01, 0.9], {
  step: 0.01,
  value: this?.value || 0.06,
  label: "Radial Force Strength",
  disabled: !useRadial
})
viewof height = Inputs.range([10, 1200], {label: "Height", value: 800, step:1})
collide = true
useRadial = true
radialForce = 0.06
height = 800
d3 = require("d3@7")
d3 = Object {format: ƒ(t), formatPrefix: ƒ(t, n), timeFormat: ƒ(t), timeParse: ƒ(t), utcFormat: ƒ(t), utcParse: ƒ(t), Adder: class, Delaunay: class, FormatSpecifier: ƒ(t), InternMap: class, InternSet: class, Node: ƒ(t), Path: class, Voronoi: class, ZoomTransform: ƒ(t, n, e), active: ƒ(t, n), arc: ƒ(), area: ƒ(t, n, e), areaRadial: ƒ(), ascending: ƒ(t, n), …}
data = d3.json('https://raw.githubusercontent.com/tashapiro/drag-race/main/data/network_20230824.json');

ref_queen_img = FileAttachment("data/ref_queen_img.csv").csv({ typed: true })
data = Object {nodes: Array(595), links: Array(699)}
Error: Unable to load file: data/ref_queen_img.csv
OJS Error

Error: Unable to load file: data/ref_queen_img.csv

drag = simulation => {
  
  function dragstarted(event, d) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
  }
  
  function dragged(event, d) {
    d.fx = event.x;
    d.fy = event.y;
  }
  
  function dragended(event, d) {
    if (!event.active) simulation.alphaTarget(0);
    d.fx = null;
    d.fy = null;
  }
  
  return d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended);
}
drag = ƒ(simulation)
function createTooltipText (links, id) {
  const regex = /S\d+/;
  const sourceLinks = links.filter(link => link.source.id === id);
  const targetLinks = links.filter(link => link.target.id === id); 
  if (id.match(regex)) {
    const count = sourceLinks.length;
    return `<strong>${id}</strong> has a total of <strong>${count} queen${count === 1 ? '' : 's'}</strong>`;
  }
  else if (id.includes('Drag Race')) {
    const count = sourceLinks.length;
    return `<strong>${id}</strong> has a total of <strong>${count} season${count === 1 ? '' : 's'}</strong>`;
  }
  else {
    const count = targetLinks.length;
    return `<strong>${id}</strong> appeared on <strong>${count} season${count === 1 ? '' : 's'}</strong>`;
  }
}
createTooltipText = ƒ(links, id)
chart = {

  const strokeColor = "black";
  const highlightStroke = "pink";
  const imgLength = 24;
  const imgWidth = 24;
  const radius = 13;
  
  const width = 800;
  
  // const links = data.links.map(d => Object.create(d));
  // const nodes = data.nodes.map(d => Object.create(d));
  const links = data.links; // Reuse the same nodes to have the animation work when changing the inputs
  const nodes = data.nodes;


  const simulation = d3.forceSimulation(nodes)
      .alpha(1)
      .force("link", d3.forceLink(links).id(d => d.id).distance(30).strength(0.25))
      .force("charge", d3.forceManyBody())
      .force("collide", collide ? d3.forceCollide(11).iterations(4): null)
      .force(
        "position",
        useRadial ? 
          d3
            .forceRadial(
              (d) => (d.id.includes("Drag Race") ? width*0.01 : width * 0.5),
              width / 2,
              height / 2
            )
            .strength(radialForce)
        : null 
         
        )
      .force("x", d3.forceX(width/2))
      .force("y", d3.forceY(height/2).strength(0.1*width/height))
    // .force("center", d3.forceCenter(width / 2, height / 2));

  const svg = d3.create("svg")
      .style("overflow", "visible")
      .attr("viewBox", [0, 0, width, height]);


  const tooltip = d3.select("body").append("div")
  .attr("class", "toolTip")
    .style("position", "absolute")
    .style("visibility", "hidden")
    .text("Placeholder");

  //link connections taken straight from Raven Gao: https://observablehq.com/@ravengao/force-directed-graph-with-cola-grouping -------
  
  // create link reference
  let linkedByIndex = {};
  data.links.forEach(d => {
    linkedByIndex[`${d.source.id},${d.target.id}`] = true;
  });
  
  // nodes map
  let nodesById = {};
  data.nodes.forEach(d => {
    nodesById[d.id] = {...d};
  })

  const isConnectedAsSource = (a, b) => linkedByIndex[`${a},${b}`];
  const isConnectedAsTarget = (a, b) => linkedByIndex[`${b},${a}`];
  const isConnected = (a, b) => isConnectedAsTarget(a, b) || isConnectedAsSource(a, b) || a === b;

  //mouse over and mouse out functions adapted from Raven Gao: https://observablehq.com/@ravengao/force-directed-graph-with-cola-grouping -------

  const nodeMouseOver = (d, links) => {
    tooltip.style("visibility", "visible")
      .html(createTooltipText(links, d.id));

     node
      .transition(500)
        .style('opacity', o => {
          console.log("o", o, d);
          const isConnectedValue = isConnected(o.id, d.id);
          if (isConnectedValue) {
            return 1.0;
          }
          return 0.1;
        });

    link
      .transition(500)
        .style('stroke-opacity', o => {
          console.log(o.source.id === d.id)
          return (o.source.id === d.id || o.target.id === d.id ? 1 : 0.1)
        })
        .transition(500)
        .attr('marker-end', o => (o.source.id === d.id || o.target.id === d.id ? 'url(#arrowhead)' : 'url()'));
  }


  const nodeMouseOut = (e, d) => {

    tooltip.style("visibility", "hidden");

    node
      .transition(500)
      .style('opacity', 1);

    link
      .transition(500)
      .style("stroke-opacity", o => {
        console.log(o.value)
      });

  };

  const link = svg.append("g")
    .attr("stroke", "#D0D0D0")
    .attr("stroke-opacity", 0.6)
    .selectAll("line")
    .data(links)
    .join("line")
    .attr("stroke-width", d => Math.sqrt(d.value));

  const node = svg.append("g")
    .selectAll(".node")
    .data(nodes)
    .join("g")
      .attr('class', 'node')
      .call(drag(simulation));

  //include circle backdrop to create border around images
  node.append('circle')
      .attr('fill','black')
  //color & size of circle depends on what node represents (franchise? season? queen?)
      .attr("r", d => /S\d+/.test(d.id) ? 10 : d.id.includes('Drag Race') ? 12 : 10)
      .attr("fill", d => /S\d+/.test(d.id) ? "#B248F8" : d.id.includes('Drag Race') ? "#5448C8" : "black")
      .attr("stroke", d => /S\d+/.test(d.id) ? "white" : d.id.includes('Drag Race') ? "white" : "black");

// add text to nodes representing seasons
  node.append('text')
    .text(d => /S\d+/.test(d.id) ? d.id.match(/S\d+/)[0] : '')
    .attr('text-anchor', 'middle')
    .attr('font-size',10)
    .attr('font-family', 'Chivo Mono')
    .attr('dominant-baseline', 'central')
    .attr('fill', 'white');

//add images for queens
  node.append("image")
      .attr("xlink:href", d => {
        //set up custom functuon to look up the image url for each queen based on id (name)
    const matchingRefImg = ref_queen_img.find(i => i.name === d.id);
    return matchingRefImg ? matchingRefImg.link_image : '';
  })
      .attr("x", "-10px")
      .attr("clip-path",'inset(0 0 0 0 round 50%)')
      .attr("y", "-10px")
      .attr("width", "20px")
      .attr("height", "20px")
      .on("mouseover", (e, d) => nodeMouseOver(d, data.links))
    // hovering out returns image to regular size
      .on("mouseout", nodeMouseOut)
    //from Raven
      .on('mousemove', (event) => tooltip.style("top", (event.pageY-10)+"px").style("left",(event.pageX+10)+"px"));
  

  const tick = () => {
    link
        .attr("x1", d => d.source.x)
        .attr("y1", d => d.source.y)
        .attr("x2", d => d.target.x)
        .attr("y2", d => d.target.y);

    node
        .attr("transform", d => `translate(${d.x}, ${d.y})`);
  };

  simulation.on("tick", tick);

  invalidation.then(() => simulation.stop());


  tick();
  return svg.node();
}
RuntimeError: Unable to load file: data/ref_queen_img.csv
OJS Runtime Error

Unable to load file

 
    Created with Quarto
    Copyright © 2023 Tanya Shapiro. All rights reserved.
Cookie Preferences