/* * updates the letter counts based on the textarea and redraws the * entire histogram */ var updateData = function() { // get the textarea "value" (i.e. currently entered text) var text = d3.select("body").select("textarea").node().value; // calculate the letter count var count = countLetters(text); /* * you will find there are a lot of JavaScript functions that are not * supported in all browsers. for consistency, we will convert our * count object to a d3 map to avoid any issues. */ count = d3.map(count); /* * speaking of non-standard functions, try out console.table! * remove this line if it doesn't work on your browser */ try { console.table(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) */ var drawBarChart = function() { // get the data we want to visualize var count = updateData(); // make sure we have at least 1 letter to draw if (count.keys().length < 1) { return; } // get the svg we want to draw on var svg = d3.select("body").select("svg"); /* * 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 */ var countMin = 0; // always include 0 on a bar chart var countMax = d3.max(count.values()); 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 * http://bl.ocks.org/mbostock/3019563 */ var 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 var bounds = svg.node().getBoundingClientRect(); var plotWidth = bounds.width - margin.right - margin.left; var 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/mbostock/d3/wiki/Scales */ /* * 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 */ var countScale = d3.scale.linear() .domain([countMin, countMax]) .range([plotHeight, 0]) .nice(); // this 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 */ var letterScale = d3.scale.ordinal() // range, between-bar padding, outside padding .rangeRoundBands([0, plotWidth], 0.1, 0) .domain(letters); // global /* * to make translating and scaling easier, we place elements into * svg groups first */ var plot = svg.select("g#plot"); if (plot.size() < 1) { // need if statement if we redraw bar chart plot = svg.append("g") .attr("id", "plot") .attr("transform", translate(margin.left, margin.top)); } /* * time to bind each data element to a rectangle in our visualization */ var bars = plot.selectAll("rect") .data(count.entries(), function(d) { return d.key; }); /* * 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! * http://bost.ocks.org/mike/join/ */ /* * we use the enter() selection to add new bars for every * new data element */ bars.enter() .append("rect") .attr("class", "bar") .attr("x", function(d) { return letterScale(d.key);}) .attr("width", letterScale.rangeBand()) .attr("y", function(d) { return countScale(d.value);}) .attr("height", function(d) { return plotHeight - countScale(d.value); }); /* start optional for updating bar chart each keypress */ /* * what about when we get new text? can we do something fancy? * OF COURSE WE CAN. but, we will get to that stuff later. here * is just a preview. * * we will transition form the old bar height to the new one. */ bars.transition() .attr("y", function(d) { return countScale(d.value);}) .attr("height", function(d) { return plotHeight - countScale(d.value); }); /* * some letters may no longer be present, so lets * remove those bars */ bars.exit() .transition() .attr("y", function(d) { return countScale(countMin);}) .attr("height", function(d) { return plotHeight - countScale(countMin); }) .remove(); /* end optional for updating bar chart each keypress */ /* * okay we need some axis labels. thankfully, d3 has built-in * functionality for this so we don't have to calculate how to * draw each label or tick mark. */ // we use these to automatically generate axis lines and tick marks // explicitly, we are using d3.svg.axis() to generate a line function // that we will call later to generate actual lines. (yes, a function // can return another function in javascript!) var xAxis = d3.svg.axis() .scale(letterScale) .orient("bottom"); var yAxis = d3.svg.axis() .scale(countScale) .orient("right"); if (plot.select("g#y-axis").size() < 1) { // add x-axis (remember where 0, 0 is located) plot.append("g") .attr("id", "x-axis") .attr("transform", "translate(0, " + plotHeight + ")") .call(xAxis); // add y-axis plot.append("g") .attr("id", "y-axis") .attr("transform", "translate(" + plotWidth + ", 0)") .call(yAxis); } else { // need the if/else if we keep redrawing to update scale // instead of re-plot it plot.select("g#y-axis").call(yAxis); } // we will style these in css! }; /* * helper method to easily create translate commands */ var translate = function(x, y) { return "translate(" + String(x) + ", " + String(y) + ")"; };