D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
saraquigley
Full window
Github gist
Undergraduate Major Disciplines that Co-Occur in Fall 2014
<!DOCTYPE html> <meta charset="utf-8"> <title>Multiple Major Co-occurrence</title> <link href='https://fonts.googleapis.com/css?family=Lato:400,900,700,300' rel='stylesheet' type='text/css'> <style> #container { min-height: 800px; margin: 0px; padding: 0px; display: -webkit-flex; display: flex; -webkit-flex-flow: row; flex-flow: row; } #container > graphic { margin: 4px; padding: 2px; min-width: 300px; background: #fff; -webkit-flex: 1 6 66%; flex: 1 6 66%; -webkit-order: 1; order: 1; } #container > aside { margin: 4px; padding: 5px; background: #fff; -webkit-flex: 1 6 34%; flex: 1 6 34%; -webkit-order: 2; order: 2; } header, footer { display: block; margin: 4px; /*padding: 5px;*/ min-height: 50px; /* background: #ffeebb;*/ font-family: 'Lato', sans-serif; font-size: 32px; font-weight: 300; /*letter-spacing: -2px;*/ padding: 0em 1em 0 0; text-rendering: optimizeLegibility; text-align: center; } /* Too narrow to support three columns */ @media all and (max-width: 640px) { #container, #page { -webkit-flex-flow: column; flex-direction: column; } #container > article, #container > nav, #container > aside { /* Return them to document order */ -webkit-order: 0; order: 0; } #container > nav, #container > aside, header, footer { min-height: 50px; max-height: 50px; } } .aside { color: #525252; font: 300 10pt "Lato", sans-serif; } #explain { font: 400 14pt "Lato", sans-serif; height: 110px; padding-top: 50px; } svg { font: 300 10pt 'Lato', sans-serif; } .background { fill: #eee; } line { stroke: #fff; } text.active { text-shadow: 2px 2px 2px yellow; } </style> <script src="https://d3js.org/d3.v3.js"></script> <!-- <script type="text/JavaScript" src="d3.js"></script> --> <script src="jsLouvain.js" type="text/JavaScript"></script> <header>Undergraduate Major Discipline Co-Occurence</header> <div id="container"> <graphic id="matrix"></graphic> <aside id="explain-aside"> <div id="explain"></div> <!-- explain --> <div class="aside"> <p>Year & Term: <select id="termyear"> </select> <p>Order By: <select id="order"> <option value="name">Major Name (Alphabetically)</option> <option value="count">Majors that Co-Occur Most Frequently</option> <option value="group">Clusters of Co-Occurring Majors</option> </select> <p>This adjacency matrix diagram shows major disciplines that co-occur as part of a double or triple major in a given term. <p>Each colored cell represents two majors that were each declared by at least four students. Mouseover a cell and the number of multiple majors will display in the upper right. <p>The darker the cell, the higher the number of multiple majors for that combination of disciplines. <!-- <p>Colors represent clusters<span id="colors"></span></p> --> <p>Colors represent groups (or clusters) that were determined using the <a href="https://en.wikipedia.org/wiki/Louvain_Modularity" target="_blank">Louvain method</a> of community detection; disciplines that are the same color are more likely to co-occur than disciplines that are colored differently. <p>Notice that some disciplines (such as Economics and Psychology) co-occur broadly across communities and therefore have several cells that are colored grey. <p>Don't forget to use the drop-down menu above to reorder the matrix and explore the data! <p>Please note that disciplines with less than four instances of multiple majors were excluded from this analysis. We found that if all co-occurrences were displayed, the matrix becomes quite large and the data more difficult to explore. If you want to see all of the multiple majors for a given discipline, please use the tab called Multiple Majors. </div> <!-- aside --> </aside> <!-- explain-aside --> </div> <!-- container --> <footer>...</footer><script> // set the dimensions of the graphic var margin = {top: 240, right: 0, bottom: 10, left: 200}, width = (window.innerWidth*0.66) - 240, height = width, orders , matrix = [], nodeHash = {}, nodes = [], edges = [], n; // nodes.length // define the scales var x = d3.scale.ordinal().rangeBands([0, width]), z = d3.scale.linear().domain([0, 18]).clamp(true), c = d3.scale.ordinal().range(["#5254a3","#8ca252","#ad494a","#0868ac","#bd9e39","#01665e","#8c510a","#a55194","#d94801"]); // add the svg container var svg = d3.select("#matrix").append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + (margin.left - 20) + "," + (margin.top - 50) + ")"); // add the explainer text which displays when you mouseover a colored cell var explainer = d3.select("#explain").append("foreignObject") .attr("width", screen.width * 0.2) .attr("height", 200) .append("xhtml:text") .attr("id", "linklabel") .html("<strong>hint:</strong> mouseover a colored cell<br> to view multiple-major parings"); d3.csv("data.csv",function(error,data) { // populate the term dropdown based on the data // var termyears = d3.nest() // .key(function(d) { return d.termyear}).sortKeys(d3.ascending) // .rollup(function(leaves) { return leaves.length; }) // .entries(data); var termyears = ["2005 Spring", "2005 Fall", "2006 Spring", "2006 Fall", "2007 Spring", "2007 Fall", "2008 Spring", "2008 Fall", "2009 Spring", "2009 Fall", "2010 Spring", "2010 Fall", "2011 Spring", "2011 Fall", "2012 Spring", "2012 Fall", "2013 Spring", "2013 Fall", "2014 Spring", "2014 Fall", "2015 Spring"] var list = d3.select("#termyear"); list.selectAll("option") .data(termyears) .enter() .append("option") .attr("value", function(d) {return d;}) .text(function(d) {return d; }); // set the default to the latest term /y ear d3.select("#termyear").property("selectedIndex", (d3.select("#termyear").property("length")-1)); // filter the data by term/year; spring 2015 is the current default var filtered_data = data.filter(function(d) {return d.termyear === d3.select("#termyear").property('value');}); // The matrix is a grey background svg.append("rect") .attr("class", "background") .attr("width", width) .attr("height", height); createNetwork(filtered_data); // Precompute the orders. orders = { name: d3.range(n).sort(function(a, b) { return d3.ascending(nodes[a].name, nodes[b].name); }), count: d3.range(n).sort(function(a, b) { return nodes[b].count - nodes[a].count; }), group: d3.range(n).sort(function(a, b) { return nodes[a].group - nodes[b].group; }) }; // The default sort order. x.domain(orders.name); // Each row is a group of things, such as a white horizontal line, a colored square, and a text label var row = svg.selectAll(".row") .data(matrix) .enter().append("g") .attr("class", "row") .attr("transform", function(d, i) { return "translate(0," + x(i) + ")"; }) .each(row); // call a row function to deal with the task of creating a square cell row.append("line") .attr("x2", width); row.append("text") .attr("x", -6) .attr("y", x.rangeBand() / 2) .attr("dy", ".32em") .attr("text-anchor", "end") .text(function(d, i) { return nodes[i].name.replace(/&/g, '&'); }) .style("fill", function(d,i) { return c(nodes[i].group); }) .on("mouseover", mouseover_row) .on("mouseout", mouseout_row); // each column is also a group containing a line and some text, both rotated -90 degrees var column = svg.selectAll(".column") .data(matrix) .enter().append("g") .attr("class", "column") .attr("transform", function(d, i) { return "translate(" + x(i) + ")rotate(-90)"; }); column.append("line") .attr("x1", -width); column.append("text") .attr("x", 6) .attr("y", x.rangeBand() / 2) .attr("dy", ".32em") .attr("text-anchor", "start") .text(function(d, i) { return nodes[i].name.replace(/&/g, '&'); }) .style("fill", function(d,i) { return c(nodes[i].group); }) .on("mouseover", mouseover_col) .on("mouseout", mouseout_col); // create the actual colored square cells and color them according to their group. function row(row) { var cell = d3.select(this).selectAll(".cell") .data(row.filter(function(d) { return d.z; })) .enter().append("rect") .attr("class", "cell") .attr("x", function(d) { return x(d.x); }) .attr("width", x.rangeBand()) .attr("height", x.rangeBand()) .style("fill-opacity", function(d) { return d.x == d.y ? 1 : z(d.z); }) .style("fill", function(d) { return d.x == d.y ? '#fff' : (nodes[d.x].group == nodes[d.y].group ? c(nodes[d.x].group) : '#696969');}) .on("mouseover", mouseover) .on("mouseout", mouseout) .append("title") .text(function(d) {return d.x == d.y ? "" : d.z + " multiple majors";}); } // when the Order By dropdown changes, d3.select("#order").on("change", function() { // clearTimeout(timeout); // This clears the delay set by WindowTimers.setTimeout (). order(this.value); }); // when the Term / Year dropdown changes, d3.select("#termyear").on("change", function() { var filtered_data = data.filter(function(d) {return d.termyear === d3.select("#termyear").property('value');}); createNetwork(filtered_data); updateMatrix(matrix); }); function order(value) { //re-order the x domain according to the predefined sort orders x.domain(orders[value]); // set up the transition to last a total of 3 seconds var t = svg.transition().duration(3000); // have each row and column move after a delay that is a function of the index of its location t.selectAll(".row") .delay(function(d, i) { return x(i) * 5; }) .attr("transform", function(d, i) { return "translate(0," + x(i) + ")"; }) .selectAll(".cell") .delay(function(d) { return x(d.x) * 5; }) .attr("x", function(d) { return x(d.x); }); t.selectAll(".column") .delay(function(d, i) { return x(i) * 5; }) .attr("transform", function(d, i) { return "translate(" + x(i) + ")rotate(-90)"; }); } // other options for the layout above that don't make the diagonal white: // .style("fill-opacity", function(d) { return d.x == d.y ? 0 : z(d.z); }) // make the diagonal grey // .style("fill", function(d) { return d.x == d.y ? '#696969' : (nodes[d.x].group == nodes[d.y].group ? c(nodes[d.x].group) : '#696969');}) // the row & column labels of the matrix and the square cells all have associated mouseover events function mouseover_row(p) { var activeCells = d3.selectAll("rect.cell").filter(function(d) { return d.y == p[0].y && d.x != d.y ; }).style('stroke-width',3).style('stroke','yellow'); } function mouseout_row(p) { d3.selectAll("rect.cell").filter(function(d) { return d.y == p[0].y && d.x != d.y ; }).style('stroke-width',0); } function mouseover_col(p) { var activeCells = d3.selectAll("rect.cell").filter(function(d) { return d.x == p[0].y && d.x != d.y ; }).style('stroke-width',3).style('stroke','yellow'); } function mouseout_col(p) { d3.selectAll("rect.cell").filter(function(d) { return d.x == p[0].y && d.x != d.y ; }).style('stroke-width',0); } function mouseover(p) { var rowtext = d3.selectAll(".row text").filter(function(d, i) { return i == p.y; }), coltext = d3.selectAll(".column text").filter(function(d, i) { return i == p.x; }); d3.selectAll(".row text").classed("active", function(d, i) { return i == p.y; }); d3.selectAll(".column text").classed("active", function(d, i) { return i == p.x; }); if(rowtext.text() === coltext.text()) { d3.select("#linklabel").html(""); } else { d3.select("#linklabel").html(rowtext.text() + ' | ' + coltext.text() + '<br><br><span style="font-size: 18pt;"> ' + p.z + "</span> multiple majors"); } } function mouseout() { d3.selectAll("text").classed("active", false); d3.select("#linklabel").html(""); } // take the data output and turn it into a network. function createNetwork(edgelist) { edges = [], nodeHash = {}; var nestedEdges, edgelist; // account for triple majors,such as cases where a student is triple major in say, econ-math-statistics : the Cal Answers query returns two rows for econ-stat, one for 1st/2nd major, other for 1st/3rd major so combine the two duplicative rows and sum their values. nestedEdges = d3.nest().key(function(d) {return d.source + '-' + d.target;}).rollup(function(v) { return d3.sum(v, function(d) { return +d.weight; }); }).entries(edgelist); // manipulate the combined data into a network data structure of edges and nodes nestedEdges.forEach(function(d) { d.source = d.key.slice(0,d.key.indexOf('-')); d.target = d.key.slice(d.key.indexOf('-') + 1, d.key.length); d.weight = d.values; }); // clean up the data structure and call it edgelist edgelist = nestedEdges.map(function(d) {return {'source':d.source, 'target':d.target, 'weight':d.weight};}); // for each edge whose value is greater than 4 edgelist.forEach(function (edge) { if (edge.weight >= 4) { // use nodeHash to keep track of things as the nodes and edges are created if (!nodeHash[edge.source]) { nodeHash[edge.source] = {id: edge.source, name: edge.source}; if(nodes.filter(function(d) {return d.name === edge.source;}).length == 0) {nodes.push(nodeHash[edge.source]);} } if (!nodeHash[edge.target]) { nodeHash[edge.target] = {id: edge.target, name: edge.target}; if(nodes.filter(function(d) {return d.name === edge.target;}).length == 0) {nodes.push(nodeHash[edge.target]); } } edges.push({source: nodeHash[edge.source], target: nodeHash[edge.target], weight: edge.weight}); } }); var newedges = edges.map(function(d) { return { 'sourceName' : d.source.name, 'targetName' : d.target.name, 'source' : "", 'target' : "", 'weight' : +d.weight } }); // remove nodes that don't belong in the array // nodes.filter(return function(d) {return d.name === edges;}); newnodes = []; nodes.forEach(function (node,i){ var b = newedges.filter(function(d) {return d.sourceName === node.name || d.targetName === node.name; }); if(b.length > 0){ newnodes.push(node); } }); nodes = newnodes; nodes.sort(function (a, b) { if (a.name > b.name) { return 1; } if (a.name < b.name) { return -1; } // a must be equal to b return 0; }); // convert the index to numeric for each node nodes = nodes.map(function(d,i) {return {name: d.name, index: i};}); // add the node id to the nodeHash to create numeric source/target values that the matrix expects nodes.forEach(function(node) { nodeHash[node.name] = {id: node.index, name: node.name};}); //update the edges with numeric source/target values that the matrix expects newedges = edges.map(function(d) { return { 'sourceName' : d.source.name, 'targetName' : d.target.name, 'source' : nodeHash[d.source.name].id, 'target' : nodeHash[d.target.name].id, 'weight' : +d.weight } }); // create the matrix createMatrix(nodes, newedges); } function modularityCensus(nodes, edges) { nodes.forEach(function (node) { var theseEdges = edges.filter(function(d) {return d.sourceName === node.name || d.targetName === node.name}); node.majors = d3.sum(theseEdges.map(function(d) {return +d.weight;})); }); } function createMatrix(nodes, edges) { // reset the matrix matrix = []; // set up the data for the Louvain method of detecting communities var node_data = nodes.map(function (d) {return d.name}); var edge_data = edges.map(function (d) {return {source: d.sourceName, target: d.targetName, weight: d.weight}; }); // pass the data to the Louvain algorithm. The logic in an external jsLouvain.js file is what returns the result: var community = jLouvain().nodes(node_data).edges(edge_data); var result = community(); // update each node with its group number nodes.forEach(function (node) { node.group = result[node.name] }); // sum up the total number of edges for each node modularityCensus(nodes, edges); n = nodes.length; // Compute index per node. nodes.forEach(function(node, i) { node.count = 0; matrix[i] = d3.range(n).map(function(j) { return {x: j, y: i, z: 0}; }); }); // Convert edges to a matrix; count major occurrences. edges.forEach(function(edge) { matrix[edge.source][edge.target].z += edge.weight; matrix[edge.target][edge.source].z += edge.weight; matrix[edge.source][edge.source].z += edge.weight; matrix[edge.target][edge.target].z += edge.weight; nodes[edge.source].count += edge.weight; nodes[edge.target].count += edge.weight; }); // setTimeout is a web browser function that can be used to execute a code snippet after a specified delay. // var timeout = setTimeout(function() { // }, 5000); } // end ? function updateMatrix(matrix) { orders = {}; // Precompute the orders. orders = { name: d3.range(n).sort(function(a, b) { return d3.ascending(nodes[a].name, nodes[b].name); }), count: d3.range(n).sort(function(a, b) { return nodes[b].count - nodes[a].count; }), group: d3.range(n).sort(function(a, b) { return nodes[a].group - nodes[b].group; }) }; // use the current sort selection x.domain(orders[d3.select("#order").property("value")]); var rows = svg.selectAll("g.row") .data(matrix) .each(row); rows.select("text") .attr("x", -6) .attr("y", x.rangeBand() / 2) .attr("dy", ".32em") .attr("text-anchor", "end") .style("fill", function(d,i) { return c(nodes[i].group); }) .text(function(d, i) { return nodes[i].name.replace(/&/g, '&'); }); rows.select("line") .attr("x2", width); var newrows = rows.enter() .append("g") .attr("class", "row") .each(row); newrows.append("line") .attr("x2", width); newrows.append("text") .attr("x", -6) .attr("y", x.rangeBand() / 2) .attr("dy", ".32em")
Modified
http://d3js.org/d3.v3.js
to a secure url
https://d3js.org/d3.v3.js