Skip to content

Instantly share code, notes, and snippets.

@davidcdupuis
Last active October 26, 2025 11:35
Show Gist options
  • Select an option

  • Save davidcdupuis/3f9db940e27e07961fdbaba9f20c79ec to your computer and use it in GitHub Desktop.

Select an option

Save davidcdupuis/3f9db940e27e07961fdbaba9f20c79ec to your computer and use it in GitHub Desktop.

Revisions

  1. davidcdupuis revised this gist Nov 24, 2016. 1 changed file with 0 additions and 3 deletions.
    3 changes: 0 additions & 3 deletions script.js
    Original file line number Diff line number Diff line change
    @@ -19,9 +19,6 @@
    - Add new paper to graph and modify and save JSON file
    - Open PDF File in new Tab
    - Filter nodes by tags, authors, year, ...
    Generating a random number in an annulus:
    http://stackoverflow.com/questions/13064912/generate-a-uniformly-random-point-within-an-annulus-ring
    */


  2. davidcdupuis revised this gist Nov 24, 2016. No changes.
  3. David Dupuis revised this gist Nov 24, 2016. 1 changed file with 0 additions and 0 deletions.
    Binary file added preview.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  4. David Dupuis revised this gist Nov 24, 2016. 1 changed file with 0 additions and 0 deletions.
    Binary file added thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  5. davidcdupuis created this gist Nov 24, 2016.
    3,489 changes: 3,489 additions & 0 deletions data.json
    3,489 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
    10 changes: 10 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,10 @@
    <!DOCTYPE html>
    <html>
    <head>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <link rel="stylesheet" href="style.css">
    </head>
    <body>
    <script src="script.js"></script>
    </body>
    </html>
    398 changes: 398 additions & 0 deletions script.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,398 @@
    /* ------ DESCRIPTION ------
    Properties of the graph:
    BASIC:
    ✓ Graph represents all papers and relationships in RTB research
    ✓ Graph is force dynamic
    ✓ Nodes are coloured by publishing year
    ✓ Graph is draggable
    ✓ Graph is zoomable
    ✓ Graph is made of concentric circles where most recent year is in the middle and latest outside
    ~ Hovering over a Node will display it's title and year
    - Clicking a node will allow to visualize it's direct connections
    - Special click on a node will open an information menu about the node
    ADVANCED:
    - Display papers graph
    - Display authors graph
    - Display thesis graph
    - Search for paper based on info: id, title, author, year, ...
    - Add new paper to graph and modify and save JSON file
    - Open PDF File in new Tab
    - Filter nodes by tags, authors, year, ...
    Generating a random number in an annulus:
    http://stackoverflow.com/questions/13064912/generate-a-uniformly-random-point-within-an-annulus-ring
    */


    // ----- GLOBAL VARIABLES ------
    var w = window.innerWidth;
    var h = window.innerHeight;

    var currYear = 2016;

    var svg = d3.select("body").append("svg")
    .attr("width",w)
    .attr("height",h)
    .style("cursor","move")
    .style("background-color","black");
    var g = svg.append("g");

    // NODE COLORS
    var color = d3.scaleOrdinal(d3.schemeCategory20);


    // FORCE SIMULATION

    var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) { return d.id; }))
    .force("charge", d3.forceManyBody().strength(-2000))
    .force("center", d3.forceCenter(w / 2, h / 2))
    .force("collide", d3.forceCollide(100));

    // ZOOM PARAMETERS
    var min_zoom = 0.05;
    var max_zoom = 7;
    var zoom = d3.zoom()
    .scaleExtent([min_zoom,max_zoom])
    .on("zoom", zoomed);
    svg.call(zoom);
    var transform = d3.zoomIdentity
    .translate(w / 6, h / 6)
    .scale(0.5);

    svg.call(zoom.transform, transform);

    // BASIC NODE SIZE
    var nominal_stroke = 1;
    var nominal_node_size = 10;

    // GRAPH VARIABLES
    var cx = w/2;
    var cy = h/2;

    // HIGHLIGHT VARIABLES
    var focus_node = null,
    highlight_node = null;
    var highlight_color = "blue";
    var highlight_trans = 0.1;

    // ----- GLOBAL FUNCTIONS -----

    function dragStart(d){
    if (!d3.event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
    }

    function dragging(d){
    d.fx = d3.event.x;
    d.fy = d3.event.y;
    }

    function dragEnd(d){
    if (!d3.event.active) simulation.alphaTarget(0);
    d.fx = null;
    d.fy = null;
    }

    function zoomed() {
    g.attr("transform", d3.event.transform);
    // Manually offsets the zoom to compensate for the initial position. Should get fixed asap or the position variables made global.
    //svg.attr("transform", "translate(" + (d3.event.transform.x + 400) + "," + (d3.event.transform.y + 325) + ")scale(" + d3.event.transform.k + ")");
    }

    function isInList(el, list){
    for (var i = 0; i < list.length; i++){
    if (el == list[i]) return true;
    }
    return false;
    }

    // builds a graph dictionary based on paper references
    function referencesGraph(file_data){
    var nodes = [];
    var links = [];

    // we use these to add nodes to references that are missing as nodes
    var node_ids = [];
    var ref_ids = [];

    // for each paper in graph create a node and append result to node list
    for (var i = 0; i < file_data.length; i++ ){
    var node = {
    "id":file_data[i].id,
    "title":file_data[i].title,
    "year":file_data[i].year,
    "authors":file_data[i].authors
    };

    node_ids.push(file_data[i].id);
    nodes.push(node);

    // for each referenced paper in graph create a link and append result to link list
    for (var j = 0; j < file_data[i].references.length; j++){
    var link = {
    "source":file_data[i].id,
    "target":file_data[i].references[j]
    };

    ref_ids.push(file_data[i].references[j]);
    links.push(link);
    }
    }

    //check if all referenced elements have a node associated
    for (var i = 0; i < ref_ids.length; i++){
    if (!isInList(ref_ids[i],node_ids)){
    var node = {
    "id":ref_ids[i],
    "title":ref_ids[i],
    "year":""
    }

    nodes.push(node);
    }
    }

    var graph = {
    "nodes":nodes,
    "links":links
    };
    return graph;
    }

    // builds a graph dictionary based on author collaboration
    function authorsGraph(data){

    }

    //optional function
    function drawCircles(){
    for(var i=0; i < 20; i++){
    var radius = (i + 1) * 80;
    g.append("circle").attr("cx",cx).attr("cy",cy).attr("r", radius ).style("stroke","gray").style("fill","none");
    }
    }

    function set_highlight(d){
    svg.style("cursor","pointer");
    if (focus_node!==null) d = focus_node;
    highlight_node = d;

    if (highlight_color!="white"){
    nodes.style(towhite, function(o) { return isConnected(d, o) ? highlight_color : "white"; });

    //text.style("font-weight", function(o) { return isConnected(d, o) ? "bold" : "normal"; });

    link.style("stroke", function(o) { return o.source.index == d.index || o.target.index == d.index ? highlight_color : ((isNumber(o.score) && o.score>=0)?color(o.score):default_link_color); });
    }
    }

    function set_focus(d){
    if (highlight_trans<1){
    nodes.style("opacity", function(o) { return isConnected(d, o) ? 1 : highlight_trans; });

    //text.style("opacity", function(o) { return isConnected(d, o) ? 1 : highlight_trans; });

    link.style("opacity", function(o) { return o.source.index == d.index || o.target.index == d.index ? 1 : highlight_trans; });
    }
    }

    function exit_highlight(){
    highlight_node = null;
    if (focus_node===null){
    svg.style("cursor","move");
    if (highlight_color!="white"){
    nodes.style(towhite, "white");
    //text.style("font-weight", "normal");
    link.style("stroke", function(o) {return (isNumber(o.score) && o.score>=0)?color(o.score):default_link_color});
    }
    }
    }

    // ----- MANAGE JSON DATA -----
    d3.json("data.json",function(error,graph){
    if (error) throw error;

    // Read the JSON data and create a dictionary of nodes and links based on references
    var paper_graph_data = referencesGraph(graph.papers);

    //var authors_graph_data; //function not implemented yet

    // INITIALIZE THE LINKS
    var link = g.append("g")
    .attr("class","links")
    .selectAll("line")
    .data(paper_graph_data.links)
    .enter()
    .append("line")
    .attr("stroke-width",function(d){return nominal_stroke})

    /* FUNCTION THAT CREATES DIV ELEMENT TO HOLD NODE INFORMATION
    [ PAPER TITLE ]
    [ PUBLISHING YEAR ][ PERSONAL RATING ]
    [ AUTHORS & LINKS ]
    [ PROBLEMATIC ]
    [ SOLUTION ]
    [OPEN PDF FILE]
    */
    var div = d3.select("body").append("div")
    .attr("class", "tooltip")
    .style("opacity", 0);

    function createTooltip(d){
    //get node data, manage missing values
    div.transition()
    .duration(200)
    .style("opacity", .9);

    div.html("<table><tr><td>" + d.title + "</td></tr><tr><td>" + d.year + "</td></tr><tr><td>" + d.authors + "</td></tr><tr><td>" + d.problematic + "</td></tr><tr><td>" + d. solution + "</td></tr></table>")
    .style("left", (d3.event.pageX) + "px")
    .style("top", (d3.event.pageY - 28) + "px");
    }

    //drawCircles();

    // INITIALIZE THE NODES
    var node = g.append("g")
    .attr("class","nodes")
    .selectAll("circles")
    .data(paper_graph_data.nodes)
    .enter()
    .append("circle")
    .attr("r",nominal_node_size)
    .attr("fill",function(d){
    if (d.year !== "")
    return color(d.year);
    else
    return "black";
    })
    .style("cursor","pointer")
    .on("mouseover",createTooltip)
    .on("mouseout",function(d){
    div.transition()
    .duration(500)
    .style("opacity", 0);
    exit_highlight();
    })
    .on("mousedown",function(d){
    focus_node = d;
    set_focus(d);
    if (highlight_node === null) set_highlight(d);
    })
    .call(d3.drag()
    .on("start", dragStart)
    .on("drag", dragging)
    .on("end", dragEnd));

    simulation.nodes(paper_graph_data.nodes)
    .on("tick",annulus_ticked);

    simulation.force("link")
    .links(paper_graph_data.links);

    function getY(year){
    if(year !== ""){
    var multiplier = Math.abs(parseInt(year)-currYear);
    var separator = 30;
    return (multiplier + 1) * separator;
    } else {
    return 2010;
    }
    }

    //function returns small and big radiuses of annulus based on Point year
    function getAnnulus(year){
    var big_radius;
    var separator = 200;
    if(year !== ""){
    var multiplier = Math.abs(parseInt(year) - currYear);
    big_radius = (multiplier + 1) * separator;
    } else {
    big_radius = 2010;
    }
    return [big_radius - separator, big_radius];
    }

    //function to verify if X in the correct position
    function verifyPosition(x, y, small_r,big_r){
    var point;
    //verify if P is in annulus defined by small_r and big_r
    if ( (Math.pow(x - cx,2) + Math.pow(y - cy, 2)) <= Math.pow(small_r,2) ){
    // P inside small circle
    point = recalculateP(x, y, small_r);
    } else if ( (Math.pow(x - cx, 2) + Math.pow(y - cy, 2)) > Math.pow(big_r,2)){
    // P outside big circle
    point = recalculateP(x, y, big_r);
    } else {
    point = [x,y];
    }
    return point;
    }

    //places point off circle on circle ring
    function recalculateP(x, y, r){
    var vx = x - cx;
    var vy = y - cy;
    var norm = Math.sqrt(Math.pow(vx,2)+ Math.pow(vy,2));
    var new_x = cx + vx / norm * r;
    var new_y = cy + vy / norm * r;
    return [new_x,new_y];
    }

    // function to return link and node position when simulation is generated
    function ticked(){
    // Each year is placed on a different level to get chronological order of paper network

    node
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; });

    link
    .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; });
    }

    function annulus_ticked(){
    node
    .attr("cx", function(d){
    var annulus = getAnnulus(d.year);
    var position = verifyPosition(d.x, d.y, annulus[0], annulus[1]);
    d.x = position[0];
    return d.x;
    })
    .attr("cy", function(d){
    var annulus = getAnnulus(d.year);
    var position = verifyPosition(d.x, d.y, annulus[0], annulus[1]);
    d.y = position[1];
    return d.y;
    });

    link
    .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; });
    }

    function pythag(r, b, coord) {
    r += nodeBaseRad;

    // force use of b coord that exists in circle to avoid sqrt(x<0)
    b = Math.min(w - r - strokeWidth, Math.max(r + strokeWidth, b));

    var b2 = Math.pow((b - radius), 2),
    a = Math.sqrt(hyp2 - b2);

    // radius - sqrt(hyp^2 - b^2) < coord < sqrt(hyp^2 - b^2) + radius
    coord = Math.max(radius - a + r + strokeWidth,
    Math.min(a + radius - r - strokeWidth, coord));

    return coord;
    }

    });

    22 changes: 22 additions & 0 deletions style.css
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,22 @@
    /* Styles go here */

    .links line {
    stroke: #999;
    stroke-opacity: 0.6;
    }

    .nodes circle {
    stroke: #fff;
    stroke-width: 1.5px;
    }

    div.tooltip {
    position: absolute;
    text-align: center;
    padding: 2px;
    font: 12px sans-serif;
    background: lightsteelblue;
    border: 0px;
    border-radius: 8px;
    pointer-events: none;
    }