View on GitHub

Youtaite Network

Technical description of the Youtaite Network website.

Summary

Find the deployed website at youtaite-network.netlify.app.

This is a project focused on a community of Youtube content creators who often collaborate with each other. I wanted to show how different collaborations are connected to each other through people - for example, if I am in two different collabs, those two collabs are connected through me. I hope to show the existence of different corners within the community, where groups of people collaborate with each other more often than with someone outside of their corner. I have a lot of other ideas for creatively displaying the data (once I have enough of it), such as organizing by the “strength” of a connection, restricting time ranges, and identifying closely-related clusters and “representative” collaborations of those clusters. The front end website is written in React.js, with D3.js for the data visualization. The back end API is written in Ruby on Rails and deployed on Fly.io. The main API routes are /collabs, /people, /roles, and /edges.

Home page and data visualization

Components

Home page components

Home

Home is a hook component that uses axios to fetch data and store it in a state variable. It also wraps the Network and NetworkSettings components. When a setting is changed in NetworkSettings, it calls a function in the Home component that sets a corresponding state variable. This state variable is given to Network as a prop, which triggers the network to update.

// Home.jsx
const [drag, setDrag] = useState(true);
// ...
return (
  <div className="container mt-3">
    <h2>Youtaite Network</h2>
    <NetworkSettings setDrag={setDrag} setRange={setRange} />
    <Network dragProp={drag} rangeProp={range} />
  </div>
);

NetworkSettings

Network settings component

NetworkSettings is a hook component. It contains two important settings for changing the view of the network. The first is called “mutual members”, and represents the number of members in common between two collabs that must exist for a link to be displayed. If mutual members has the range 3-10, then only collabs with 3-10 members in common between them will be linked together. In general, moving the lower bound up becomes more useful as more collabs are entered into the database. Without this feature, the network would quickly become messy and difficult to understand. The second setting is the ability to enable and disable draggable nodes. This is useful particularly on mobile, where draggable nodes might get in the way when the user might be trying to pan the network instead.

The mutual members range slider is implemented using d3-brush. The following code demonstrates the slider’s logic, which is adapted from this code example.

// NetworkSettings.jsx
const brush = d3.brushX()
  .extent([[x, y], [x + width, y + height]])
  .on('brush', function (event) {
    selection = event.selection;
  })
  .on('end', function (event) {
    const s = selection;
    const d0 = s.map(xScale.invert);
    const d1 = d0.map(Math.round);
    d3.select(this).transition().call(event.target.move, d1.map(xScale));
    // update view
    d3.select('#slider-event-handler').dispatch('change', { detail: { range: d1 } });
  });

When the brush event is triggered, we set the selection variable, which is the current range that has been selected (defined in pixels). When the end event is triggered, we convert this selection into the actual range using a linear scale. Then we round the lower and upper bounds and call the move event on the brush with the new rounded values. Finally, we dispatch a custom event, which is handled by calling the setRange function, passed down from the Home component (as described above).

// NetworkSettings.jsx
d3.select('#slider-event-handler')
  .on('change', (event) => {
    setRange(event.detail.range);
  });

Network

Network demo

Network is a hook component that uses D3’s force-directed layout to display the graph.

// Network.jsx
const simulation = d3.forceSimulation()
  .force('charge', d3.forceManyBody().strength(-600))
  .force('link', d3.forceLink().id((d) => d.id).distance(50))
  .force('x', d3.forceX())
  .force('y', d3.forceY())
  .force('center', d3.forceCenter().x(w / 2).y(h / 2))
  .on('tick', ticked);

The graph contains two types of nodes: one represents videos, or collabs; the other represents people. Collab nodes are represented as rectangles, while person nodes are represented as circles. In the initial view of the network, no person nodes are visible. However, when a collab node is clicked, a “closeup” view of that node is provided that shows the people who are directly connected to that node.

When the network is created, first, the svg is attached to the DOM. Then, empty groups are created for edges, person nodes, and collab nodes. We define a ticked function which handles node and edge movement over time; a zoomed function that handles user interaction through zooming and panning; and a dragNode function that handles user interaction through dragging nodes.

Finally, we define an update function updates the network. The first step of update is to get the graph components that should be currently displayed, based on current node and edge data (stored in refs to avoid stale state):

// Network.jsx
const currentGraphComponents = getCurrentGraphComponents(
  dataset.current, rangeCollabs.current, rangeEdges.current, focusedNode.current,
);

Using D3’s data join, we then bind the new data to nodes and edges. For collab nodes, this is done as follows (person nodes and edges are implemented similarly):

// Network.jsx
collab = collab
  .data(collabs.current, (d) => d.id)
  .join((enter) => {
    enter = enter.append('g')
      .classed('collab', true)
      .attr('id', (d) => `g-${d.id}`)
      .on('mouseover', /* mouseover event handler */)
      .on('mouseout', /* mouseout event handler */)
      .on('click', /* click event handler */);
    // omitted: clipPath, image, and rect are appended to each group to configure node appearance
    return enter;
  });

The mouseover, mouseout, and click event handlers handle user interaction with individual nodes. When a user mouses over a node, that node, as well as any connected edges and nodes, are given an active class, and corresponding style changes are handled in the CSS. When a user mouses out of a node, all nodes and edges have the active class removed.

When a user clicks a collab node, the view changes to show that collab node, any directly connected collab nodes and edges, and the person nodes that connect the clicked node to other collab nodes. The collab node that was clicked is stored in a ref called focusedNode. At the end, the network is updated to reflect these changes to the graph. This process is implemented as follows:

// Network.jsx
if (focusedNode.current) {
  // unfix currently selected node
  focusedNode.current.fx = null;
  focusedNode.current.fy = null;
}
// focus/fix clicked node
focusedNode.current = node;
node.fx = node.x;
node.fy = node.y;
// change force center
simulation.force('center', d3.forceCenter().x(node.fx).y(node.fy));
network.current.update();

CollabAutosuggest

Collab autosuggest demo

CollabAutosuggest is a hook component that uses the react-autosuggest package to automatically suggest collabs (based on the collab’s video title) as the user is typing. To do this, the user input is cleaned up and checked against each collab’s title. Currently suggested collabs are kept in a state variable.

const suggestValue = value.replace(/[ \t\n.]/g, '').toLowerCase();
const newSuggestions = suggestValue.length === 0 ? [] : allCollabs.filter((collab) => collab.title.replace(/[ \t\n.]/g, '').toLowerCase().includes(suggestValue));
setSuggestions(newSuggestions.slice(0, 20));

Coming soon

Descriptions of the submission form and back end API will be added soon.