//////////////////////////////////////////////////////////// //////////////////////// Set-up //////////////////////////// //////////////////////////////////////////////////////////// // Quick fix for resizing some things for mobile-ish viewers const mobileScreen = ($(window).innerWidth() < 500); // Scatterplot const margin = { left: 30, top: 20, right: 20, bottom: 20 }; const width = Math.min($('#chart').width(), 800) - 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', 'chordWrapper') .attr('transform', `translate(${margin.left}, ${margin.top})`); ////////////////////////////////////////////////////// ///////////// Initialize Axes & Scales /////////////// ////////////////////////////////////////////////////// const opacityCircles = 0.7; // Set the color for each region const color = d3.scale.ordinal() .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.scale.log() .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.svg.axis() .orient('bottom') .ticks(2) .tickFormat(d => { // Difficult function to create better ticks return xScale.tickFormat((mobileScreen ? 4 : 8), d => { const prefix = d3.formatPrefix(d); return `$${prefix.scale(d)}${prefix.symbol}`; })(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.scale.linear() .range([height, 0]) .domain(d3.extent(countries, d => d.lifeExpectancy)) .nice(); const yAxis = d3.svg.axis() .orient('left') .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.scale.sqrt() .range([mobileScreen ? 1 : 2, mobileScreen ? 10 : 16]) .domain(d3.extent(countries, d => d.GDP)); //////////////////////////////////////////////////////////// /////////////////// 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 => `countries ${d.CountryCode}`) .style('opacity', opacityCircles) .style('fill', d => color(d.Region)) .attr('cx', d => xScale(d.GDP_perCapita)) .attr('cy', d => yScale(d.lifeExpectancy)) .attr('r', 2); // d => rScale(d.GDP) ////////////////////////////////////////////////////////////// //////////////////////// Voronoi ///////////////////////////// ////////////////////////////////////////////////////////////// // Initiate the voronoi function // Use the same variables of the data in the .x and .y as used in the cx and cy of the circle call // The clip extent will make the boundaries end nicely along the chart area instead of splitting up the entire SVG // (if you do not do this it would mean that you already see a tooltip when your mouse is still in the axis area, which is confusing) const voronoi = d3.geom.voronoi() .x(d => xScale(d.GDP_perCapita)) .y(d => yScale(d.lifeExpectancy)) .clipExtent([[0, 0], [width, height]]); // Initiate a group element to place the voronoi diagram in const voronoiGroup = wrapper.append('g') .attr('class', 'voronoiWrapper'); // Create the Voronoi diagram voronoiGroup.selectAll('path') .data(voronoi(countries)) // Use vononoi() with your dataset inside .enter().append('path') .attr('d', d => `M${d.join('L')}Z`) .datum(d => d.point) // Give each cell a unique class where the unique part corresponds to the circle classes .attr('class', d => `voronoi ${d.CountryCode}`) .style('stroke', '#2074A0') // I use this to look at how the cells are dispersed as a check .style('stroke-opacity', 0.3) .style('fill', 'none') .style('pointer-events', 'all') .on('mouseover', showTooltip) .on('mouseout', removeTooltip); ////////////////////////////////////////////////////// ///////////////// Initialize Labels ////////////////// ////////////////////////////////////////////////////// const labelPadding = 5; // 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`) .style('opacity', 0.7) .attr('transform', `translate(${width - labelPadding}, ${(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`) .style('opacity', 0.7) .attr('transform', `translate(18, ${labelPadding}) rotate(-90)`) .text('Life expectancy'); /////////////////////////////////////////////////////////////////////////// ///////////////////////// Create the Legend//////////////////////////////// /////////////////////////////////////////////////////////////////////////// if (!mobileScreen) { // Legend const legendMargin = { left: 5, top: 10, right: 5, bottom: 10 }; const legendWidth = 160; 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})`); const rectSize = 16; // dimensions of the colored square const rowHeight = 22; // height of a row in the legend // const maxWidth = 125; // width of each row // 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)})`); // 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(25, ${(rectSize / 2)})`) .attr('class', 'legendText') .style('font-size', '11px') .attr('dy', '.35em') .text((d, i) => color.domain()[i]); } else { // if !mobileScreen d3.select('#legend') .style('display', 'none'); } /////////////////////////////////////////////////////////////////////////// /////////////////// Hover functions of the circles //////////////////////// /////////////////////////////////////////////////////////////////////////// // Show the tooltip on the hovered over circle function showTooltip(d) { $(this).popover({ placement: 'auto top', // place the tooltip above the item container: '#chart', // the name (class or id) of the container trigger: 'manual', html: true, // the html content to show inside the tooltip content: () => `${d.Country}` }); $(this).popover('show'); // highlight the border of the current voronoi cell d3.select(`path.${d.CountryCode}`) .style('stroke', 'black') .style('stroke-opacity', 0.5) .style('stroke-width', 2); }// function showTooltip // Hide the tooltip when the mouse moves away function removeTooltip(d) { // Hide the tooltip $('.popover').each(function () { return $(this).remove()}); // restore the border of the cell to its original color d3.select(`path.${d.CountryCode}`) .style('stroke', '#2074A0') .style('stroke-opacity', 0.3) .style('stroke-width', 1); }// function removeTooltip // iFrame handler const pymChild = new pym.Child(); pymChild.sendHeight(); setTimeout(() => pymChild.sendHeight(), 5000);