//////////////////////////////////////////////////////////// //////////////////////// Set-up //////////////////////////// //////////////////////////////////////////////////////////// //Quick fix for resizing some things for mobile-ish viewers // vanilla JS window width and height // https://gist.github.com/joshcarr/2f861bd37c3d0df40b30 const wV = window; const dV = document; const eV = dV.documentElement; const gV = dV.getElementsByTagName('body')[0]; const xV = wV.innerWidth || eV.clientWidth || gV.clientWidth; const yV = wV.innerHeight || eV.clientHeight || gV.clientHeight; // Quick fix for resizing some things for mobile-ish viewers const mobileScreen = (xV < 500); //Scatterplot const margin = {left: 60, top: 20, right: 20, bottom: 60}; const width = Math.min(document.getElementById('chart').offsetWidth, 840) - margin.left - margin.right; const height = width*2/3; const svg = d3.select("#chart").append("svg") .attr("width", (width + margin.left + margin.right)) .attr("height", (height + margin.top + margin.bottom)); const wrapper = svg.append("g").attr("class", "scatterWrapper") .attr("transform", `translate(${margin.left},${margin.top})`); let idVariable = "CountryCode"; // add a an id property to each entry in the countries data // if an idVariable is not defined already countries.forEach((d, i) => { if (typeof idVariable === 'undefined') { countries[i].id = `${i}`; } }) if (typeof idVariable === 'undefined') idVariable = 'id'; ////////////////////////////////////////////////////// ///////////// Initialize Axes & Scales /////////////// ////////////////////////////////////////////////////// const opacityCircles = 0.7; const maxDistanceFromPoint = 50; //Set the color for each region const color = d3.scaleOrdinal() .range([ "#EFB605", "#E58903", "#E01A25", "#C20049", "#991C71", "#66489F", "#2074A0", "#10A66E", "#7EB852" ]) .domain([ "Africa | North & East", "Africa | South & West", "America | North & Central", "America | South", "Asia | East & Central", "Asia | South & West", "Europe | North & West", "Europe | South & East", "Oceania" ]); //Set the new x axis range const xScale = d3.scaleLog() .range([0, width]) .domain([100,2e5]); //I prefer this exact scale over the true range and then using "nice" //.domain(d3.extent(countries, function(d) { return d.GDP_perCapita; })) //.nice(); //Set new x-axis const xAxis = d3.axisBottom() .ticks(2) .tickFormat(d => xScale.tickFormat((mobileScreen ? 4 : 8),d => d3.format('$.2s')(d))(d)) .scale(xScale); //Append the x-axis wrapper.append("g") .attr("class", "x axis") .attr("transform", `translate(${0},${height})`) .call(xAxis); //Set the new y axis range const yScale = d3.scaleLinear() .range([height,0]) .domain(d3.extent(countries, d => d.lifeExpectancy)) .nice(); const yAxis = d3.axisLeft() .ticks(6) //Set rough # of ticks .scale(yScale); //Append the y-axis wrapper.append("g") .attr("class", "y axis") .attr("transform", `translate(${0},${0})`) .call(yAxis); //Scale for the bubble size const rScale = d3.scaleSqrt() .range([mobileScreen ? 1 : 2, mobileScreen ? 10 : 16]) .domain(d3.extent(countries, d => d.GDP)); ////////////////////////////////////////////////////// ///////////////// Initialize Labels ////////////////// ////////////////////////////////////////////////////// //Set up X axis label wrapper.append("g") .append("text") .attr("class", "x title") .attr("text-anchor", "end") .style("font-size", `${mobileScreen ? 8 : 12}px`) .attr("transform", `translate(${width},${height - 10})`) .text("GDP per capita [US $] - Note the logarithmic scale"); //Set up y axis label wrapper.append("g") .append("text") .attr("class", "y title") .attr("text-anchor", "end") .style("font-size", `${mobileScreen ? 8 : 12}px`) .attr("transform", "translate(18, 0) rotate(-90)") .text("Life expectancy"); //////////////////////////////////////////////////////////// //////////////// Setup for the tooltip //////////////////// //////////////////////////////////////////////////////////// // initialize variable for popover tooltip let popoverTooltip; const tooltipVariables = [ { name: 'Country', valueOnly: true } ]; const xVariable = 'GDP_perCapita'; const yVariable = 'lifeExpectancy'; // strip out any white space const xSelector = xVariable.replace(/\s/g, ''); const ySelector = yVariable.replace(/\s/g, ''); const xDroplineTextFormat = ".0f"; const yDroplineTextFormat = ".0f"; //////////////////////////////////////////////////////////// ///// Capture mouse events and voronoi.find() the site ///// //////////////////////////////////////////////////////////// // Use the same variables of the data in the .x and .y as used in the cx and cy of the circle call svg._tooltipped = svg.diagram = null; svg.on('mousemove', function() { if (!svg.diagram) { console.log('computing the voronoi…'); svg.diagram = d3.voronoi() .x(d => xScale(d.GDP_perCapita)) .y(d => yScale(d.lifeExpectancy)) (countries); console.log('…done.'); } const p = d3.mouse(this); let site; p[0] -= margin.left; p[1] -= margin.top; // don't react if the mouse is close to one of the axis if (p[0] < 5 || p[1] < 5) { site = null; } else { site = svg.diagram.find(p[0], p[1], maxDistanceFromPoint); } if (site !== svg._tooltipped) { if (svg._tooltipped) { // removeTooltip(svg._tooltipped.data) const removeTooltipOptions = { idVariable, xVariable, yVariable, xSelector, ySelector, wrapper, height, width }; removeTooltip(svg._tooltipped.data, undefined, removeTooltipOptions, popoverTooltip) } if (site) { // showTooltip(site.data); const showTooltipOptions = { idVariable, xVariable, yVariable, xSelector, ySelector, wrapper, height, width, tooltipVariables, xDroplineTextFormat, yDroplineTextFormat }; // return the updated popoverTooltip popoverTooltip = showTooltip(site.data, undefined, showTooltipOptions, popoverTooltip); } svg._tooltipped = site; } }); //////////////////////////////////////////////////////////// /////////////////// Scatterplot Circles //////////////////// //////////////////////////////////////////////////////////// //Initiate a group element for the circles const circleGroup = wrapper.append("g") .attr("class", "circleWrapper"); //Place the country circles circleGroup.selectAll("countries") .data(countries.sort((a, b) => b.GDP > a.GDP)) //Sort so the biggest circles are below .enter().append("circle") .attr("class", (d, i) => `countries marks ${d.CountryCode}`) .attr("cx", d => xScale(d.GDP_perCapita)) .attr("cy", d => yScale(d.lifeExpectancy)) .attr("r", d => rScale(d.GDP)) .style("opacity", opacityCircles) .style("fill", d => color(d.Region)); /////////////////////////////////////////////////////////////////////////// ///////////////////////// Create the Legend//////////////////////////////// /////////////////////////////////////////////////////////////////////////// if (!mobileScreen) { //Legend const legendMargin = {left: 5, top: 10, right: 5, bottom: 10}; const legendWidth = 145; const legendHeight = 270; const svgLegend = d3.select("#legend").append("svg") .attr("width", (legendWidth + legendMargin.left + legendMargin.right)) .attr("height", (legendHeight + legendMargin.top + legendMargin.bottom)); const legendWrapper = svgLegend.append("g").attr("class", "legendWrapper") .attr("transform", `translate(${legendMargin.left},${legendMargin.top})`); //dimensions of the colored square const rectSize = 15; //width of each row //height of a row in the legend const rowHeight = 20; const maxWidth = 144; //Create container per rect/text pair const legend = legendWrapper.selectAll('.legendSquare') .data(color.range()) .enter().append('g') .attr('class', 'legendSquare') .attr("transform", (d, i) => `translate(${0},${i * rowHeight})`) .style("cursor", "pointer") .on("mouseover", selectLegend(0.02)) .on("mouseout", selectLegend(opacityCircles)); //Non visible white rectangle behind square and text for better hover legend.append('rect') .attr('width', maxWidth) .attr('height', rowHeight) .style('fill', "white"); //Append small squares to Legend legend.append('rect') .attr('width', rectSize) .attr('height', rectSize) .style('fill', d => d); //Append text to Legend legend.append('text') .attr('transform', `translate(${22},${rectSize/2})`) .attr("class", "legendText") .style("font-size", "10px") .attr("dy", ".35em") .text((d, i) => color.domain()[i]); //Create g element for bubble size legend const bubbleSizeLegend = legendWrapper.append("g") .attr("transform", `translate(${legendWidth/2 - 30},${color.domain().length*rowHeight + 20})`); //Draw the bubble size legend bubbleLegend(bubbleSizeLegend, rScale, legendSizes = [1e11,3e12,1e13], legendName = "GDP (Billion $)"); }//if !mobileScreen else { d3.select("#legend").style("display","none"); } ////////////////////////////////////////////////////// /////////////////// Bubble Legend //////////////////// ////////////////////////////////////////////////////// function bubbleLegend(wrapperVar, scale, sizes, titleName) { const legendSize1 = sizes[0]; const legendSize2 = sizes[1]; const legendSize3 = sizes[2]; const legendCenter = 0; const legendBottom = 50; const legendLineLength = 25; const textPadding = 5; const numFormat = d3.format(","); wrapperVar.append("text") .attr("class","legendTitle") .attr("transform", `translate(${legendCenter},${0})`) .attr("x", `${0}px`) .attr("y", `${0}px`) .attr("dy", "1em") .text(titleName); wrapperVar.append("circle") .attr('r', scale(legendSize1)) .attr('class',"legendCircle") .attr('cx', legendCenter) .attr('cy', (legendBottom-scale(legendSize1))); wrapperVar.append("circle") .attr('r', scale(legendSize2)) .attr('class',"legendCircle") .attr('cx', legendCenter) .attr('cy', (legendBottom-scale(legendSize2))); wrapperVar.append("circle") .attr('r', scale(legendSize3)) .attr('class',"legendCircle") .attr('cx', legendCenter) .attr('cy', (legendBottom-scale(legendSize3))); wrapperVar.append("line") .attr('class',"legendLine") .attr('x1', legendCenter) .attr('y1', (legendBottom-2*scale(legendSize1))) .attr('x2', (legendCenter + legendLineLength)) .attr('y2', (legendBottom-2*scale(legendSize1))); wrapperVar.append("line") .attr('class',"legendLine") .attr('x1', legendCenter) .attr('y1', (legendBottom-2*scale(legendSize2))) .attr('x2', (legendCenter + legendLineLength)) .attr('y2', (legendBottom-2*scale(legendSize2))); wrapperVar.append("line") .attr('class',"legendLine") .attr('x1', legendCenter) .attr('y1', (legendBottom-2*scale(legendSize3))) .attr('x2', (legendCenter + legendLineLength)) .attr('y2', (legendBottom-2*scale(legendSize3))); wrapperVar.append("text") .attr('class',"legendText") .attr('x', (legendCenter + legendLineLength + textPadding)) .attr('y', (legendBottom-2*scale(legendSize1))) .attr('dy', '0.25em') .text(`$ ${numFormat(Math.round(legendSize1/1e9))} B`); wrapperVar.append("text") .attr('class',"legendText") .attr('x', (legendCenter + legendLineLength + textPadding)) .attr('y', (legendBottom-2*scale(legendSize2))) .attr('dy', '0.25em') .text(`$ ${numFormat(Math.round(legendSize2/1e9))} B`); wrapperVar.append("text") .attr('class',"legendText") .attr('x', (legendCenter + legendLineLength + textPadding)) .attr('y', (legendBottom-2*scale(legendSize3))) .attr('dy', '0.25em') .text(`$ ${numFormat(Math.round(legendSize3/1e9))} B`); }//bubbleLegend /////////////////////////////////////////////////////////////////////////// //////////////////// Hover function for the legend //////////////////////// /////////////////////////////////////////////////////////////////////////// //Decrease opacity of non selected circles when hovering in the legend function selectLegend(opacity) { return (d, i) => { const chosen = color.domain()[i]; wrapper.selectAll(".countries") .filter(d => d.Region != chosen) .transition() .style("opacity", opacity); }; }//function selectLegend