D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
35degrees
Full window
Github gist
Gatesmoneybase
Built with
blockbuilder.org
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Bubble Chart Experiment</title> </head> <style> a, a:visited, a:active { color: #444; } .container { max-width: 900px; margin: auto; } .button { min-width: 130px; padding: 4px 5px; cursor: pointer; text-align: center; font-size: 13px; border: 1px solid #e0e0e0; text-decoration: none; } .button.active { background: #000; color: #fff; } #vis { width: 940px; height: 600px; clear: both; margin-bottom: 10px; } #toolbar { margin-top: 10px; } .year { font-size: 21px; fill: #aaa; cursor: default; } .tooltip { position: absolute; top: 100px; left: 100px; -moz-border-radius:5px; border-radius: 5px; border: 2px solid #000; background: #fff; opacity: .9; color: black; padding: 10px; width: 300px; font-size: 12px; z-index: 10; } .tooltip .title { font-size: 13px; } .tooltip .name { font-weight:bold; } .footer { text-align: center; } </style> <body> <div class="container"> <h1>Gates Foundation Educational Spending</h1> <div id="toolbar"> <a href="#" id="all" class="button active">All 2017 Films</a> <a href="#" id="year" class="button">By Rotten</a> </div> <div id="vis"></div> </body> <script src="https://d3js.org/d3.v4.min.js"></script> <script> function bubbleChart() { // Constants for sizing var width = 960; var height = 960; // tooltip for mouseover functionality var tooltip = floatingTooltip('gates_tooltip', 240); // Locations to move bubbles towards, depending // on which view mode is selected. var center = { x: width / 2, y: height / 3 }; var yearCenters = { "81-100": { x: width / 5, y: height / 2 }, "61-80": { x: width / 3, y: height / 2 }, "41-60": { x: width / 2, y: height / 2 } "21-40": { x: 2 * width / 3, y: height / 2 } "0-20": { x: 4 * width / 5, y: height / 2 } }; // X locations of the year titles. var yearsTitleX = { "81-100": 160, "61-80": 360, "41-60": width / 2, "21-40": width - 360, "0-20": width - 160 }; // @v4 strength to apply to the position forces var forceStrength = 0.03; // These will be set in create_nodes and create_vis var svg = null; var bubbles = null; var nodes = []; // Charge function that is called for each node. // As part of the ManyBody force. // This is what creates the repulsion between nodes. // // Charge is proportional to the diameter of the // circle (which is stored in the radius attribute // of the circle's associated data. // // This is done to allow for accurate collision // detection with nodes of different sizes. // // Charge is negative because we want nodes to repel. // @v4 Before the charge was a stand-alone attribute // of the force layout. Now we can use it as a separate force! function charge(d) { return -Math.pow(d.radius, 2.0) * forceStrength; } // Here we create a force layout and // @v4 We create a force simulation now and // add forces to it. var simulation = d3.forceSimulation() .velocityDecay(0.2) .force('x', d3.forceX().strength(forceStrength).x(center.x)) .force('y', d3.forceY().strength(forceStrength).y(center.y)) .force('charge', d3.forceManyBody().strength(charge)) .on('tick', ticked); // @v4 Force starts up automatically, // which we don't want as there aren't any nodes yet. simulation.stop(); // Nice looking colors - no reason to buck the trend // @v4 scales now have a flattened naming scheme var fillColor = d3.scaleOrdinal() .domain(['BV','WB','Universal','Sony','Paramount','Fox','LGF','Independent']) .range(['GoldenRod','SteelBlue','Orchid','#377eb8','Chartreuse','PowderBlue','LightSalmon','Yellow']); var strokeColor = d3.scaleLinear() .domain([13,28,40,60,72,87]) .range(['#ad1100','#ff220a', '#d49695', '#a1e1b7','#1ecc2c', '#1d8018']); /* * This data manipulation function takes the raw data from * the CSV file and converts it into an array of node objects. * Each node will store data and visualization values to visualize * a bubble. * * rawData is expected to be an array of data objects, read in from * one of d3's loading functions like d3.csv. * * This function returns the new node array, with a node in that * array for each element in the rawData input. */ function createNodes(rawData) { // Use the max total_amount in the data as the max in the scale's domain // note we have to ensure the total_amount is a number. var maxAmount = d3.max(rawData, function (d) { return +d.BO; }); // Sizes bubbles based on area. // @v4: new flattened scale names. var radiusScale = d3.scalePow() .exponent(0.5) .range([2, 85]) .domain([0, maxAmount]); // Use map() to convert raw data into node data. // Checkout https://learnjsdata.com/ for more on // working with data. var myNodes = rawData.map(function (d) { return { rank: d.Rank, radius: radiusScale(+d.BO), value: +d.BO, name: d.Title, org: d.Studio, group: d.Cat, open: d.Open, close: d.Close, score: d.RT, ow: d.Op, pct: ((d.Op/d.BO)*100).toFixed(1), x: Math.random() * 900, y: Math.random() * 800 }; }); // sort them to prevent occlusion of smaller nodes. myNodes.sort(function (a, b) { return b.value - a.value; }); return myNodes; } /* * Main entry point to the bubble chart. This function is returned * by the parent closure. It prepares the rawData for visualization * and adds an svg element to the provided selector and starts the * visualization creation process. * * selector is expected to be a DOM element or CSS selector that * points to the parent element of the bubble chart. Inside this * element, the code will add the SVG continer for the visualization. * * rawData is expected to be an array of data objects as provided by * a d3 loading function like d3.csv. */ var chart = function chart(selector, rawData) { // convert raw data into nodes data nodes = createNodes(rawData); // Create a SVG element inside the provided selector // with desired size. svg = d3.select(selector) .append('svg') .attr('width', width) .attr('height', height); // Bind nodes data to what will become DOM elements to represent them. bubbles = svg.selectAll('.bubble') .data(nodes, function (d) { return d.rank; }); // Create new circle elements each with class `bubble`. // There will be one circle.bubble for each object in the nodes array. // Initially, their radius (r attribute) will be 0. // @v4 Selections are immutable, so lets capture the // enter selection to apply our transtition to below. var bubblesE = bubbles.enter().append('circle') .classed('bubble', true) .attr('r', 0) .attr('fill', function (d) { return fillColor(d.group); }) .attr('stroke', function (d) { return strokeColor(d.score); }) .attr('stroke-width', 2) .on('mouseover', showDetail) .on('mouseout', hideDetail); // @v4 Merge the original empty selection and the enter selection bubbles = bubbles.merge(bubblesE); // Fancy transition to make bubbles appear, ending with the // correct radius bubbles.transition() .duration(2000) .attr('r', function (d) { return d.radius; }); // Set the simulation's nodes to our newly created nodes array. // @v4 Once we set the nodes, the simulation will start running automatically! simulation.nodes(nodes); // Set initial layout to single group. groupBubbles(); }; /* * Callback function that is called after every tick of the * force simulation. * Here we do the acutal repositioning of the SVG circles * based on the current x and y values of their bound node data. * These x and y values are modified by the force simulation. */ function ticked() { bubbles .attr('cx', function (d) { return d.x; }) .attr('cy', function (d) { return d.y; }); } /* * Provides a x value for each node to be used with the split by year * x force. */ function nodeYearPos(d) { return yearCenters[d.score].x; } /* * Sets visualization in "single group mode". * The year labels are hidden and the force layout * tick function is set to move all nodes to the * center of the visualization. */ function groupBubbles() { hideYearTitles(); // @v4 Reset the 'x' force to draw the bubbles to the center. simulation.force('x', d3.forceX().strength(forceStrength).x(center.x)); // @v4 We can reset the alpha value and restart the simulation simulation.alpha(1).restart(); } /* * Sets visualization in "split by year mode". * The year labels are shown and the force layout * tick function is set to move nodes to the * yearCenter of their data's year. */ function splitBubbles() { showYearTitles(); // @v4 Reset the 'x' force to draw the bubbles to their year centers simulation.force('x', d3.forceX().strength(forceStrength).x(nodeYearPos)); // @v4 We can reset the alpha value and restart the simulation simulation.alpha(1).restart(); } /* * Hides Year title displays. */ function hideYearTitles() { svg.selectAll('.year').remove(); } /* * Shows Year title displays. */ function showYearTitles() { // Another way to do this would be to create // the year texts once and then just hide them. var yearsData = d3.keys(yearsTitleX); var years = svg.selectAll('.year') .data(yearsData); years.enter().append('text') .attr('class', 'year') .attr('x', function (d) { return yearsTitleX[d]; }) .attr('y', 40) .attr('text-anchor', 'middle') .text(function (d) { return d; }); } /* * Function called on mouseover to display the * details of a bubble in the tooltip. */ function showDetail(d) { // change outline to indicate hover state. d3.select(this).attr('stroke', 'black'); var content = '<span class="name">Title: </span><span class="value">' + d.name + '</span><br/>' + '<span class="name">Amount: </span><span class="value">$' + addCommas(d.value) + '</span><br/>' + '<span class="name">Year: </span><span class="value">' + d.year + '</span>'; tooltip.showTooltip(content, d3.event); } /* * Hides tooltip */ function hideDetail(d) { // reset outline d3.select(this) .attr('stroke', d3.rgb(fillColor(d.group)).darker()); tooltip.hideTooltip(); } /* * Externally accessible function (this is attached to the * returned chart function). Allows the visualization to toggle * between "single group" and "split by year" modes. * * displayName is expected to be a string and either 'year' or 'all'. */ chart.toggleDisplay = function (displayName) { if (displayName === 'year') { splitBubbles(); } else { groupBubbles(); } }; // return the chart function from closure. return chart; } /* * Below is the initialization code as well as some helper functions * to create a new bubble chart instance, load the data, and display it. */ var myBubbleChart = bubbleChart(); /* * Function called once data is loaded from CSV. * Calls bubble chart function to display inside #vis div. */ function display(error, data) { if (error) { console.log(error); } myBubbleChart('#vis', data); } /* * Sets up the layout buttons to allow for toggling between view modes. */ function setupButtons() { d3.select('#toolbar') .selectAll('.button') .on('click', function () { // Remove active class from all buttons d3.selectAll('.button').classed('active', false); // Find the button just clicked var button = d3.select(this); // Set it as the active button button.classed('active', true); // Get the id of the button var buttonId = button.attr('id'); // Toggle the bubble chart based on // the currently clicked button. myBubbleChart.toggleDisplay(buttonId); }); } /* * Helper function to convert a number into a string * and add commas to it to improve presentation. */ function addCommas(nStr) { nStr += ''; var x = nStr.split('.'); var x1 = x[0]; var x2 = x.length > 1 ? '.' + x[1] : ''; var rgx = /(\d+)(\d{3})/; while (rgx.test(x1)) { x1 = x1.replace(rgx, '$1' + ',' + '$2'); } return x1 + x2; } // Load the data. d3.csv('bo2017c.csv', display); // setup the buttons. setupButtons(); function floatingTooltip(tooltipId, width) { // Local variable to hold tooltip div for // manipulation in other functions. var tt = d3.select('body') .append('div') .attr('class', 'tooltip') .attr('id', tooltipId) .style('pointer-events', 'none'); // Set a width if it is provided. if (width) { tt.style('width', width); } // Initially it is hidden. hideTooltip(); /* * Display tooltip with provided content. * * content is expected to be HTML string. * * event is d3.event for positioning. */ function showTooltip(content, event) { tt.style('opacity', 1.0) .html(content); updatePosition(event); } /* * Hide the tooltip div. */ function hideTooltip() { tt.style('opacity', 0.0); } /* * Figure out where to place the tooltip * based on d3 mouse event. */ function updatePosition(event) { var xOffset = 20; var yOffset = 10; var ttw = tt.style('width'); var tth = tt.style('height'); var wscrY = window.scrollY; var wscrX = window.scrollX; var curX = (document.all) ? event.clientX + wscrX : event.pageX; var curY = (document.all) ? event.clientY + wscrY : event.pageY; var ttleft = ((curX - wscrX + xOffset * 2 + ttw) > window.innerWidth) ? curX - ttw - xOffset * 2 : curX + xOffset; if (ttleft < wscrX + xOffset) { ttleft = wscrX + xOffset; } var tttop = ((curY - wscrY + yOffset * 2 + tth) > window.innerHeight) ? curY - tth - yOffset * 2 : curY + yOffset; if (tttop < wscrY + yOffset) { tttop = curY + yOffset; } tt .style('top', tttop + 'px') .style('left', ttleft + 'px'); } return { showTooltip: showTooltip, hideTooltip: hideTooltip, updatePosition: updatePosition }; } </script> </html>
https://d3js.org/d3.v4.min.js