var screenWidth = document.documentElement.clientWidth; var screenHeight = document.documentElement.clientHeight; var maxWidth = 600; var preferredWidth = Math.min(600, screenWidth); var nestedData; var margin = {top: 30, right: 30, bottom: 10, left: 65}, width = preferredWidth - margin.left - margin.right, height = 600 - margin.top - margin.bottom; const radius = preferredWidth > 200 ? 5 : 3; const opacity = {normal: 0.7, focus: 1, muted: 0.2, hidden: 0}; var xScale = d3.scaleLinear() .rangeRound([0, width]); var yScale = d3.scaleBand() .range([height, 0]); var xAxisTop = d3.axisTop(xScale).tickSize(0); var xAxisBottom = d3.axisBottom(xScale).tickSize(0); var familyLine = d3.line() .x(function(d) { return d.x; }) .y(function(d) { return d.y; }) .curve(d3.curveLinear); var colourOutcome = d3.scaleOrdinal() .range([siuColours.orange10, siuColours.teal10, siuColours.purple10, siuColours.green10, siuColours.greyDark]); var colourRegion = d3.scaleOrdinal() .range([siuColours.orange10, siuColours.teal10, siuColours.purple10, siuColours.green10, siuColours.greyDark]); var svg = d3.select("#roi-chart") .append("svg") //.attr("viewBox", "0 0 "+ preferredWidth + " " + (height + margin.top + margin.bottom) ) //.attr("preserveAspectRatio", "xMidYMid meet"); .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .on("click", function() { resetChart(); }); var g = svg.append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); // LOAD DATA AND CREATE CHARTS d3.csv("roi2.csv", convertTextToNumbers, function(error, data) { if (error) throw error; var averageROI = d3.nest() .key(function(d) { return d.outcome; } ) .rollup(function(v) { return d3.mean(v, function(d) { return d.roi; } ); } ) .entries(data); // SET THE SCALES //xScale.domain(d3.extent(data, function(d) { return d.roi; })).nice(); xScale.domain([ Math.floor(d3.min(data, function(d) { return d.roi; } )), Math.ceil(d3.max(data, function(d) { return d.roi; } )), ]); yScale.domain(data.sort(function(a, b) { return b.roi - a.roi; }) .map(function(d) { return d.outcome; }) ); colourOutcome.domain(xScale.domain()); colourRegion.domain(data.sort(function(a, b) { return b.roi - a.roi; }) .map(function(d) { return d.region; }) ); xAxisTop.ticks( (xScale.domain()[1] - xScale.domain()[0] + 1) ); xAxisBottom.ticks( (xScale.domain()[1] - xScale.domain()[0] + 1) ); // DRAW THE LABELS AND GRID LINES var yLabels = g.selectAll(".y-label") .data(yScale.domain()) .enter() .append("text") .attr("class", "y-label") .attr("y", function(d) { return yScale(d) + yScale.bandwidth()/2; }) .attr("x", -10) .text(function(d){ return capitalizeFirstLetter(d); }); var gXAxisTop = g.append("g") .attr("class", "axis-top") .attr("transform", "translate(0,0)") .call(xAxisTop); const arrowMargin = 10; gXAxisTop.append("text") .text("Negative return") .attr("x", xScale(1) - arrowMargin) .attr("y", -arrowMargin) .attr("class", "negative-label"); gXAxisTop.append("text") .text("Postive return") .attr("x", xScale(1) + arrowMargin) .attr("y", -arrowMargin) .attr("class", "positive-label"); //arrow head gXAxisTop.append("defs").append("marker") .attr("id", "arrowhead-bottom") .attr("class", "axis-arrow") .attr("markerWidth", "13") .attr("markerHeight", "13") .attr("refX", "8") .attr("refY", "6") .attr("orient", "auto") .append("svg:path") .attr("d", "M1,1 L8,6 L1,11 "); var arrow = gXAxisTop.append("g") .attr("class", "axis-arrow") .attr("transform", "translate(" + xScale(1) + "," + -6 +")"); arrow.append("line") .attr("class", "axis-arrow") .attr("x1", arrowMargin) .attr("y1", 0) .attr("x2", 100) .attr("y2", 0) .attr("marker-end", "url(#arrowhead-bottom)"); arrow.append("line") .attr("class", "axis-arrow") .attr("x1", -arrowMargin) .attr("y1", 0) .attr("x2", -100) .attr("y2", 0) .attr("marker-end", "url(#arrowhead-bottom)"); gXAxisTop.selectAll(".tick").selectAll("text") .text(function(d) { return d === 1 ? "" : "ROI: " + d; }); var gridLines = g.selectAll(".axis-top").selectAll(".tick") .append("line") .attr("class", "gridLine") .attr("x1", 0) .attr("y1", function(d) {return d === 1 ? -margin.top : 0; }) .attr("x2", 0) .attr("y2", height ); var gXAxisBottom = g.append("g") .attr("class", "axis-bottom") .attr("transform", "translate(0," + height + ")" ) .call(xAxisBottom); gXAxisBottom.selectAll(".tick").selectAll("text") .text(function(d) { return "ROI: " + d; }); var avgLines = g.selectAll(".avg-lines") .data(averageROI) .enter() .append("g") .attr("class", "avg-line") .attr("transform", function(d) { return "translate("+ xScale(d.value) + "," + yScale(d.key) + ")"; }); avgLines.append("line") .attr("x1", 0) .attr("x2", 0) .attr("y1", 16) .attr("y2", yScale.bandwidth() -5 ); avgLines.append("text") .text(function(d) { return "avg: "+ oneDecimalPlace(d.value); } ) .attr("x", 3 ) .attr("y", 13 ); // ENABLE THE DROP DOWN SELECTS var select = d3.selectAll("select"); select.on("change", function(d) { var selectedParents = d3.select("#no-of-parents").property("value"); var selectedChildren = d3.select("#no-of-children").property("value"); var selectedRegion = d3.select("#region").property("value"); var selectedIncome = d3.select("#income").property("value"); var selectedEthnicity = d3.select("#ethnicity").property("value"); highlightFamilies(selectedParents, selectedChildren, selectedRegion, selectedIncome, selectedEthnicity); }); // CREATE THE FORCE, TO LAYOUT THE CIRCLES - IE. SET THE X,Y COORDS. var simulation = d3.forceSimulation(data) .force("x", d3.forceX(function(d) { return xScale(d.roi); }).strength(1)) .force("y", d3.forceY(function(d) { return yScale(d.outcome) + yScale.bandwidth() * yScale.align(); })) .force("collide", d3.forceCollide(radius+1)) //.on('tick', ticked) .stop(); for (var i = 0; i < 120; ++i) simulation.tick(); // CREATE THE VORONOI LAYOUT TO ENABLE EASIER SELECTION var voronoi = d3.voronoi() .extent([[-margin.left, -margin.top], [width + margin.right, height + margin.top]]) .x(function(d) { return d.x; }) .y(function(d) { return d.y; }); voronoiData = voronoi(data).polygons(); // CREATE A NESTED DATASET FOR THE FAMILY LINES nestedData = d3.nest() .key(function(d) {return d.data.familyID2;}) .entries(voronoiData); var lineData = []; nestedData.forEach(function(d) { lineData.push({familyID2: d.key, points: [] }); }); lineData.forEach(function(d) { nestedData.forEach(function(p) { if (d.familyID2 === p.key) { var pointsToPush = []; p.values.forEach(function(v) { pointsToPush.push({x: v.data.x, y: v.data.y}) }); pointsToPush.sort(function(a, b) { return a.y - b.y; }); d.points = pointsToPush; }; }); }); // DRAW THE FAMILY LINES FIRST TO APPEAR UNDER THE CIRCLES var gLines = g.selectAll(".g-family-line") .data(lineData) .enter() .append("g") .attr("class", "g-family-line") .attr("id",function(d) { return "g-family-line-" + d.familyID2; } ); gLines.append("path") .attr("id", function(d) { return "family-line-" + d.familyID2; }) .datum(function(d) { return d.points; } ) .attr("class", "family-line") .attr("d", familyLine) .style("stroke-opacity", opacity.hidden) // DRAW THE THE CIRCLES var cell = g.append("g") .attr("class", "cells") .selectAll("g").data(voronoiData) .enter() .append("g") .attr("class", "polys"); cell.append("circle") .attr("r", radius) .attr("cx", function(d) { return d.data.x; }) .attr("cy", function(d) { return d.data.y; }) .style("fill", function(d) { return colourRegion(d.data.region); } ) .style("fill-opacity", opacity.normal ); var researchClip = g.append("defs").selectAll("clipPath") .data(voronoiData) .enter() .append("clipPath") .attr("id", function(d) { return "clip-research-" + d.data.id; }) .append("circle") .attr("cx", function(d) { return d.data.x; }) .attr("cy", function(d) { return d.data.y; }) .attr("r", radius + 20 ); cell.append("path") .attr("class", "clipped-paths") //.style("stroke", "grey") .attr("d", function(d) { return "M" + d.join("L") + "Z"; }) .attr("clip-path", function(d) { return "url(#clip-research-" + d.data.id + ")"; }) .on("click", showHideCircles); cell.append("title") .text(function(d) { return d.data.roi + "%" + "\nParents: " + d.data.parents + "\nChildren: " + d.data.children; }); }); // FUNCTIONS // RESET THE CIRCLE FORMATTING function resetChart() { setSelectValues("All", "All", "All", "All", "All"); resetLineCircles(); d3.selectAll(".clipped-paths") .each(function(d) { d.data.highlighted = false; } ); }; function resetLineCircles() { d3.selectAll(".family-line") .style("stroke-opacity", opacity.hidden ); d3.selectAll(".polys") .selectAll("circle") .style("stroke", "none") .style("fill-opacity", opacity.normal ); }; // handle the click function showHideCircles (d) { resetLineCircles(); if (d.data.highlighted) { d3.selectAll(".clipped-paths") .each(function(d) { d.data.highlighted = false; } ); setSelectValues("All", "All", "All", "All", "All"); } else { var thisFamily = d.data.familyID2; d3.selectAll(".clipped-paths").filter(function(d) { return d.data.familyID2 === thisFamily; } ) .each(function(d) { d.data.highlighted = true; } ); d.data.highlighted = true; highlightFamily(d.data.familyID2); setSelectValues(d.data.parents, d.data.children, d.data.region, d.data.income, d.data.ethnicity); }; d3.event.stopPropagation(); } ; // HIGHLIGHT ALL FAMILIES THAT MATCH THE SELECTED VALUES function highlightFamilies(noOfParents, noOfChildren, region, income, ethnicity) { var thisParents = noOfParents; var thisChildren = noOfChildren; var thisRegion = region; var thisIncome = income; var thisEthnicity = ethnicity; resetLineCircles(); if (thisParents !== "All" && thisChildren !== "All" && thisRegion !== "All" && thisIncome !== "All" && thisEthnicity !== "All") { var thisID = "" + thisParents + thisChildren + thisRegion + thisIncome + thisEthnicity; thisID = thisID.replace(/\s/g, ''); //remove white spaces highlightFamily(thisID); } else if (thisParents !== "All" || thisChildren !== "All" || thisRegion !== "All" || thisIncome !== "All" || thisEthnicity !== "All") { d3.selectAll(".polys") .selectAll("circle") .style("stroke", "none") .style("fill-opacity", opacity.muted); var filteredSelection = d3.selectAll(".polys") .selectAll("circle"); if (thisParents !== "All") { filteredSelection = filteredSelection.filter(function(d) { return d.data.parents == thisParents; }) }; if (thisChildren !== "All") { filteredSelection = filteredSelection.filter(function(d) { return d.data.children == thisChildren; }) }; if (thisRegion !== "All") { filteredSelection = filteredSelection.filter(function(d) { return d.data.region == thisRegion; }) }; if (thisIncome !== "All") { filteredSelection = filteredSelection.filter(function(d) { return d.data.income == thisIncome; }) }; if (thisEthnicity !== "All") { filteredSelection = filteredSelection.filter(function(d) { return d.data.ethnicity == thisEthnicity; }) }; filteredSelection .style("stroke", "black") .style("fill-opacity", opacity.focus); }; }; // end of function highlightFamilies // HIGHLIGHT A CERTAIN FAMILY TYPE function highlightFamily(familyID) { var thisID = familyID; d3.selectAll(".polys") .selectAll("circle") .style("stroke", function (d) { return d.data.familyID2 === thisID ? "black" : "none" ; }) .style("fill-opacity", function (d) { return d.data.familyID2 === thisID ? opacity.focus : opacity.muted ; }); d3.selectAll("#family-line-"+thisID) .style("stroke-opacity", opacity.focus ); }; // end of function highlightFamily // SET THE DROP DOWN VALUES BASED ON HIGHLIGHED CIRCLES function setSelectValues(parentToSelect, childrenToSelect, regionToSelect, incomeToSelect, ethnicityToSelect) { d3.select("#no-of-parents").property("value", parentToSelect); d3.select("#no-of-children").property("value", childrenToSelect); d3.select("#region").property("value", regionToSelect); d3.select("#income").property("value", incomeToSelect); d3.select("#ethnicity").property("value", ethnicityToSelect); }; //CONVERT CSV TEXT TO NUMBERS function convertTextToNumbers(d) { d.id = +d.id; d.familyID = +d.familyID; d.parents = +d.parents; d.children = +d.children; d.roi = +d.roi; return d; }; // RESIZE LISTENER addEvent(window, "resize", function(event) { console.log('resized'); screenWidth = document.documentElement.clientWidth; screenHeight = document.documentElement.clientHeight; console.log("w: " + screenWidth + " h: " + screenHeight ); preferredWidth = Math.min(600, screenWidth); console.log("p: " + preferredWidth); });