// set width and height of main svg element var width = 1000, height = 500; // adding svg element to body of html var svg = d3.select('body') .append('svg') .attr('width', width) .attr('height', height) //colorscale for data points var color = d3.scaleQuantize(); // the map projection we are using var projection = d3.geoAlbersUsa(); // data structures for the data we are showing in our cartogram var population = d3.map(); var medIncome = d3.map(); // scale for the radius of our circles (the circles represent the data- // either population or median income by state) // arbitrarily chose 8,60 because it most closely resembled desired output var radius = d3.scaleSqrt().range([8,60]); // our simulation, defined globally so we use it in the update function var simulation; // our legend, defined in globally so we can use it in the update function var legend; // boolean for whether we are showing population or median income var showPop = true; // use d3-queue to avoid callback hell, and manage how we load our datasets // asynchronously and ensure the data finishes loading before we call main d3.queue() .defer(d3.json, 'data/us-states-centroids.json') .defer(d3.csv, 'data/acs_pop_income.csv', function(d) { // format our data so all the id's are 2 digits if (d.id.length < 2) d.id = '0' + d.id; // populate our data structures with properly formatted and relevant // data (with values coerced as integers) population.set(d.id, +d.toal_pop); medIncome.set(d.id, +d.median_income); }) // await means not to call main until the previous two defers finish .await(main); // our main callback after we load the data function main(error, geojson) { if (error) throw error; // min and max values of our dataset var extent = d3.extent(population.values()); //set the domain of the radius scale to the extent radius.domain(extent); // setting up color scales // extent[1] is the max value of the data // I am essentially dividing the max population by 5, so I partition // the data into 5 buckets, which I use for the range of the colorscale // and the legend var partitions = []; for (var i = 1; i < 6; i++){ partitions.push((extent[1] / 5)*i); } color.domain([0, extent[1]]) .range(partitions.map(function(d, i) { return d3.interpolateYlGnBu(i / (5 - 1)); })); //refactoring the data into the structure that we want to create our cartogram var nodes = geojson.features.map(function(d) { var point = projection(d.geometry.coordinates), value = population.get(d.id); return { id: d.id, name: d.properties.name, label: d.properties.label, coords: d.geometry.coordinates, x: point[0], y: point[1], x0: point[0], y0: point[1], r: radius(value), value: value }; }); // our simulation simulation = d3.forceSimulation(nodes) .force('charge', d3.forceManyBody().strength(1)) .force('collision', d3.forceCollide().strength(1) .radius(function(d) { return d.r; })) .on('tick', ticked); // function for simulation, draws our circles and text labels in the // main svg element function ticked() { // draw our circles var bubbles = d3.select('svg') .selectAll('circle') .data(nodes, function(d) { return d.name; }); bubbles.enter() .append('circle') // allows us to create and update bubbles at same time .merge(bubbles) .attr('r', function(d) { return d.r; }) .attr('cx', function(d) { return d.x; }) .attr('cy', function(d) { return d.y; }) .attr('fill', function(d) { return color(d.value); }) .attr('stroke', '#333') // when mouseover event on circle occurs, show the tooltip .on('mouseover', function(d) { // localetostring -> adds commas to value if (showPop) { tooltip.html(d.name + "
" + d.value.toLocaleString()); } else { tooltip.html(d.name + "
" + "$" + d.value.toLocaleString()); } tooltip.style('visibility', 'visible'); d3.select(this).attr('stroke', 'green'); }) // padding so the cursor doesnt overlap .on('mousemove', function() { tooltip.style('top', (d3.event.pageY - 10) + 'px') .style('left', (d3.event.pageX + 10) + 'px'); }) //when mouseout event occurs, hide the tooltip .on('mouseout', function() { tooltip.style('visibility', 'hidden'); d3.select(this).attr('stroke', '#333'); }); // adding text labels for each state var textLabels = d3.select('svg') .selectAll('text') .data(nodes, function(d) { return d.name; }); textLabels.enter().append("text").merge(textLabels) .attr("x", function(d){return d.x}) .attr("y", function(d){return d.y}) .attr("text-anchor", "middle") .style("font-size", "10px") .text(function(d){return d.label}); } // adding our legend svg.append('g') .attr('class', 'legend') // put the legend to the left of the cartogram .attr('transform', 'translate(0, 300)'); // our legend is based on colors, so we use a legendcolor // with a scale set by our color scale legend = d3.legendColor() // format the labels to match desired output .labelFormat(d3.format(".2s")) .title('Total Population') .titleWidth(75) .scale(color); svg.select('.legend') .call(legend); } // adding our html button, by default it shows Median Income // on click, calls update fxn d3.select('body').append('br'); d3.select('body').append('text').text("Toggle category: "); var button = d3.select('body') .append('button') .text('Median Income') .on('click', update); // our tooltip, appends div element to body of html with css features // to display the tooltip var tooltip = d3.select('body') .append('div') .style('position', 'absolute') .style('visibility', 'hidden') .style('color', 'white') .style('padding', '8px') .style('background-color', '#626D71') .style('border-radius', '6px') .style('text-align', 'center') .style('font-family', 'monospace') .text(''); // update function called on button click function update() { // get the data already rendered in the DOM var selection = d3.select('svg').selectAll("circle").data(); // set attributes based on whether we are showing income or population // arbitrarily chose range for radius to match desired output var whichData; if (showPop) { whichData = medIncome; radius.range([23,27]); legend.title("Median Income") } else { whichData = population; radius.range([8,60]); legend.title("Total Population") } // set our scales to the desired data var extent = d3.extent(whichData.values()); radius.domain(extent); color.domain([0, extent[1]]); // reset the positions and value based on the desired data // x0 and y0 are the initial positions that never change, // so I reset the x and y to them selection.forEach(function(elem) { var value = whichData.get(elem.id); elem.x = elem.x0; elem.y = elem.y0; elem.value = value; elem.r = radius(value); }); // redraw legend svg.select('.legend').call(legend); // restart simulation simulation.nodes(selection).alpha(1).restart(); // switch boolean from previous value showPop = !showPop; // edit button text based on boolean if (!showPop) { button.text("Total Population"); } else { button.text("Median Income"); } };