/* * this function will grab the latest text from our text area and update * the letter counts */ let updateData = function() { // get the textarea "value" (i.e. the entered text) let text = d3.select("body").select("textarea").node().value; // make sure we got the right text // console.log('updated %d characters', text.length); // get letter count let count = countLetters(text); // some browsers support console.table() // javascript supports try/catch blocks try { // console.table(Array.from(count)); } catch (e) { // console.log(count); } return count; }; /* * our massive function to draw a bar chart. note some stuff in here * is bonus material (for transitions and updating the text) */ let drawBarChart = function() { // get the data to visualize let count = updateData(); // get the svg to draw on let svg = d3.select("body").select("svg"); // make sure we selected exactly 1 element console.assert(svg.size() == 1); /* * we will need to map our data domain to our svg range, which * means we need to calculate the min and max of our data * * since we have an iterable instead of an array, we should use * https://github.com/d3/d3-array/blob/master/README.md * * note: include the latest version of d3-array for this! */ let countMin = 0; // always include 0 in a bar chart! let countMax = d3.max(count.values()); // this catches the case where all the bars are removed, so there // is no maximum value to compute if (isNaN(countMax)) { countMax = 0; } console.log("count bounds:", [countMin, countMax]); /* * before we draw, we should decide what kind of margins we * want. this will be the space around the core plot area, * where the tick marks and axis labels will be placed * https://bl.ocks.org/mbostock/3019563 */ let margin = { top: 15, right: 35, // leave space for y-axis bottom: 30, // leave space for x-axis left: 10 }; // now we can calculate how much space we have to plot let bounds = svg.node().getBoundingClientRect(); let plotWidth = bounds.width - margin.right - margin.left; let plotHeight = bounds.height - margin.top - margin.bottom; /* * okay now somehow we have to figure out how to map a count value * to a bar height, decide bar widths, and figure out how to space * bars for each letter along the x-axis * * this is where the scales in d3 come in very handy * https://github.com/d3/d3-scale#api-reference */ /* * the counts are easiest because they are numbers and we can use * a simple linear scale, but the complicating matter is the * coordinate system in svgs: * https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Positions * * so we want to map our min count (0) to the max height of the plot area */ let countScale = d3.scaleLinear() .domain([countMin, countMax]) .range([plotHeight, 0]) .nice(); // rounds the domain a bit for nicer output /* * the letters need an ordinal scale instead, which is used for * categorical data. we want a bar space for all letters, not just * the ones we found, and spaces between bars. * https://github.com/d3/d3-scale#band-scales */ let letterScale = d3.scaleBand() .domain(letters) // all letters (not using the count here) .rangeRound([0, plotWidth]) .paddingInner(0.1); // space between bars // try using these scales in the console console.log("using count scale:", [countScale(countMin), countScale(countMax)]); console.log("using letter scale:", [letterScale('a'), letterScale('z')]); // we are actually going to draw on the "plot area" let plot = svg.append("g").attr("id", "plot"); // notice in the "elements" view we now have a g element! // shift the plot area over by our margins to leave room // for the x- and y-axis plot.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); console.assert(plot.size() == 1); // now lets draw our x- and y-axis // these require our x (letter) and y (count) scales let xAxis = d3.axisBottom(letterScale); let yAxis = d3.axisRight(countScale); let xGroup = plot.append("g").attr("id", "x-axis"); xGroup.call(xAxis); // notice it is at the top of our svg // we need to translate/shift it down to the bottom xGroup.attr("transform", "translate(0," + plotHeight + ")"); // do the same for our y axix let yGroup = plot.append("g").attr("id", "y-axis"); yGroup.call(yAxis); yGroup.attr("transform", "translate(" + plotWidth + ",0)"); // now how about some bars! /* * time to bind each data element to a rectangle in our visualization * hence the name data-driven documents (d3) */ /* * we need our data as an array of key, value pairs before binding */ let pairs = Array.from(count.entries()); console.log("pairs:", pairs); let bars = plot.selectAll("rect") .data(pairs, function(d) { return d[0]; }); // setting the "key" is important... this is how d3 will tell // what is existing data, new data, or old data /* * okay, this is where things get weird. d3 uses an enter, update, * exit pattern for dealing with data. think of it as new data, * existing data, and old data. for the first time, everything is new! * https://bost.ocks.org/mike/selection/ * https://bost.ocks.org/mike/join/ */ // we use the enter() selection to add new bars for new data bars.enter().append("rect") // we will style using css .attr("class", "bar") // the width of our bar is determined by our band scale .attr("width", letterScale.bandwidth()) // we must now map our letter to an x pixel position // note the use of arrow functions here // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions#Arrow_functions .attr("x", d => letterScale(d[0])) // // function(d) { // return letterScale(d[0]); // }) // and do something similar for our y pixel position .attr("y", d => countScale(d[1])) // here it gets weird again, how do we set the bar height? .attr("height", d => plotHeight - countScale(d[1])) .each(function(d, i, nodes) { console.log("Added bar for:", d[0]); }); // so we can access some of these elements later... // add them to our chart global chart.plotWidth = plotWidth; chart.plotHeight = plotHeight; chart.xAxis = xAxis; chart.yAxis = yAxis; chart.countScale = countScale; chart.letterScale = letterScale; }; /* * optional: code that allows us to update the chart when * the data changes. */ let updateBarChart = function() { // get latest version of data let count = updateData(); // recalculate counts let countMin = 0; // always include 0 in a bar chart! let countMax = d3.max(count.values()); if (isNaN(countMax)) { countMax = 0; } // update our scale based on the new data chart.countScale.domain([countMin, countMax]); console.log("count bounds:", chart.countScale.domain()); // re-select our elements let svg = d3.select("body").select("svg"); let plot = svg.select("g#plot"); console.assert(svg.size() == 1); console.assert(plot.size() == 1); // now lets update our y-axis plot.select("g#y-axis").call(chart.yAxis); // and lets re-create our data join let pairs = Array.from(count.entries()); let bars = plot.selectAll("rect") .data(pairs, function(d) { return d[0]; }); // so what happens when we change the text? // well our data changed, and there will be a new enter selection! // only new letters will get new bars bars.enter().append("rect") .attr("class", "bar") .attr("width", chart.letterScale.bandwidth()) .attr("x", d => chart.letterScale(d[0])) .attr("y", d => chart.countScale(d[1])) .attr("height", d => chart.plotHeight - chart.countScale(d[1])) // for bars that already existed, we must use the update selection // and then update their height accordingly // we use transitions for this to avoid change blindness bars.transition() .attr("y", d => chart.countScale(d[1])) .attr("height", d => chart.plotHeight - chart.countScale(d[1])); // what about letters that disappeared? // we use the exit selection for those to remove the bars bars.exit() .each(function(d, i, nodes) { console.log("Removing bar for:", d[0]); }) .transition() // can change how the transition happens // https://github.com/d3/d3-ease .ease(d3.easeBounceOut) .attr("y", d => chart.countScale(countMin)) .attr("height", d=> chart.plotHeight - chart.countScale(countMin)) .remove(); /* * we broke up the draw and update methods, but really it could be done * all in one method with a bit more logic. also possible to simplify with * using the new join() method, but it is important to understand the * enter/update/exit pattern. */ };