Skip to content

Instantly share code, notes, and snippets.

@15Dkatz
Created February 3, 2017 22:21
Show Gist options
  • Save 15Dkatz/a2a2b1fb551fe6f2b65c183745aa68fe to your computer and use it in GitHub Desktop.
Save 15Dkatz/a2a2b1fb551fe6f2b65c183745aa68fe to your computer and use it in GitHub Desktop.
Letter Count Bar Chart (d3v4)
license: mit

Letter Count Bar Chart

In this demo, we asynchronously load a text file, use JavaScript to count the number of times each letter appears in that file, and generate a bar chart showing the letter count in D3.js v4.

This is meant to be an introductory demo to expose students to HTML, CSS, JavaScript, D3.js, bl.ocks.org, and blockbuilder.org for the first time.

References

This is based off the following example:

http://bl.ocks.org/sjengle/e8c0d6abc0a8d52d4b11

It has been updated for D3.js v4 based on the following updated bar chart example:

https://bl.ocks.org/mbostock/3885304

Please see the original examples for videos and additional references.

forked from sjengle's block: Letter Count Bar Chart (d3v4)

forked from anonymous's block: Letter Count Bar Chart (d3v4)

var updateData = function() {
// get the textarea "value" (i.e. the entered text)
var text = d3.select("body").select("textarea").node().value;
// make sure we got the right text
// console.log(text);
// get letter count
var count = countLetters(text);
// some browsers support console.table()
try {
console.table(count.entries());
}
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 to visualize
var count = updateData();
// get the svg 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 in 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/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
*/
var 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
*/
var letterScale = d3.scaleBand()
.domain(letters)
.rangeRound([0, plotWidth])
.paddingInner(0.1); // space between bars
// try using these scales in the console
console.log("count scale [0, 36]:", [countScale(0), countScale(36)]);
console.log("letter scale [a, z]:", [letterScale('a'), letterScale('z')]);
// we are actually going to draw on the "plot area"
var plot = svg.select("g#plot");
if (plot.size() < 1) {
// this is the first time we called this function
// we need to steup the plot area
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 + ")");
}
// now lets draw our x- and y-axis
// these require our x (letter) and y (count) scales
var xAxis = d3.axisBottom(letterScale);
var yAxis = d3.axisRight(countScale);
// check if we have already drawn our axes
if (plot.select("g#y-axis").size() < 1) {
var xGroup = plot.append("g").attr("id", "x-axis");
// the drawing is triggered by call()
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
var yGroup = plot.append("g").attr("id", "y-axis");
yGroup.call(yAxis);
yGroup.attr("transform", "translate(" + plotWidth + ",0)");
}
else {
// we need to do this so our chart updates
// as we type new letters in our box
plot.select("g#y-axis").call(yAxis);
}
// now how about some bars!
/*
* time to bind each data element to a rectangle in our visualization
* hence the name data-driven documents (d3)
*/
var bars = plot.selectAll("rect")
.data(count.entries(), function(d) { return d.key; });
// 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!
* http://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
.attr("x", function(d) {
return letterScale(d.key);
})
// and do something similar for our y pixel position
.attr("y", function(d) {
return countScale(d.value);
})
// here it gets weird again, how do we set the bar height?
.attr("height", function(d) {
return plotHeight - countScale(d.value);
});
// notice there will not be bars created for missing letters!
// 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
// but we have to bind this draw function to textarea events
// (see index.html)
// 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", function(d) { return countScale(d.value); })
.attr("height", function(d) { return plotHeight - countScale(d.value); });
// what about letters that disappeared?
// we use the exit selection for those to remove the bars
bars.exit().transition()
.attr("y", function(d) { return countScale(countMin); })
.attr("height", function(d) { return plotHeight - countScale(countMin); })
.remove();
};
// this file includes code for the letter count
// array of all lowercase letters
var letters = "abcdefghijklmnopqrstuvwxyz".split("");
/*
* try this out in the console! you can access any variable or function
* defined globally in the console
*
* and, you can right-click output in the console to make it global too!
*/
/*
* removes any character (including spaces) that is not a letter
* and converts all remaining letters to lowercase
*/
var onlyLetters = function(text) {
// there are multiple ways to define a function in javascript!
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
var notLetter = /[^a-z]/g;
return text.toLowerCase().replace(notLetter, "");
};
// in console try: onlyLetters("hello world!");
/*
* counts all of the letters in the input text and stores the counts as
* a d3 map object
* https://github.com/d3/d3-collection/blob/master/README.md#map
*/
var countLetters = function(input) {
var text = onlyLetters(input);
var count = d3.map();
/*
* you can loop through strings as if they are arrays
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for
*/
for (var i = 0; i < text.length; i++) {
var letter = text[i];
// check if we have seen this letter before
if (count.has(letter)) {
count.set(letter, count.get(letter) + 1);
}
else {
count.set(letter, 1);
}
}
return count;
};
// in console try: countLetters("hello world!");
// in console try: countLetters("hello world!").keys();
// in console try: countLetters("hello world!").entries();
<!DOCTYPE html>
<!-- we are using html 5 -->
<head>
<meta charset="utf-8">
<title>Letter Count Bar Chart</title>
<!-- this allows us to use the non-standard Roboto web font -->
<link href="https://fonts.googleapis.com/css?family=Roboto:300,300italic" rel="stylesheet" type="text/css">
<!-- this is our custom css stylesheet -->
<link href="style.css" rel="stylesheet" type="text/css">
</head>
<body>
<!-- we will place our visualization in this svg using d3.js -->
<svg></svg>
<!-- we will place the text to analyze here using javascript -->
<textarea></textarea>
<!-- include d3.js -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<!-- include custom javascript -->
<script src="count.js"></script>
<script src="chart.js"></script>
<!-- here is our core javascript -->
<script type="text/javascript">
// inside the script tag, // and /* */ are comments
// outside the script tag, <!-- --> are comments
// we need to load the text file into the textarea
// this will be done asynchronously!
// https://github.com/d3/d3-request/blob/master/README.md#text
d3.text("peter.txt", function(error, data) {
// we are creating a function within a method call here
if (error) throw error;
// we will use the console and developer tools extensively
console.log(data);
// now we select the textarea from the DOM and update
// https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model
// https://github.com/d3/d3-selection/blob/master/README.md#select
d3.select("body").select("textarea").text(data);
// updateData();
drawBarChart();
});
// this message will appear BEFORE the text is logged!
console.log("after d3.text() call");
// add an event listener to our text area and
// update our chart every time new data is entered
d3.select("body").select("textarea")
.on("keyup", drawBarChart);
</script>
</body>
Peter Piper picked a peck of pickled peppers.
A peck of pickled peppers Peter Piper picked.
If Peter Piper picked a peck of pickled peppers,
Where's the peck of pickled peppers that Peter Piper picked?
/*
* we use css to style our page and our svg elements
* the classes/ids defined here must match our d3 code
*/
body, textarea {
font-family: 'Roboto', sans-serif;
font-weight: 300;
font-size: 11pt;
}
body {
margin: 5px;
padding: 0px;
/* see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value */
background-color: whitesmoke;
}
textarea {
/* position the textarea on top of the svg */
position: fixed;
top: 5px;
left: 5px;
margin: 0px;
padding: 5px;
width: 400px;
height: 75px;
/* try changing this color in blockbuilder! */
background-color: rgba(255, 255, 255, 0.8);
}
textarea, svg {
border: 1px solid gainsboro;
border-radius: 10px;
}
svg {
/* bl.ocks.org defaults to 960px by 500px */
width: 950px;
height: 490px;
background-color: white;
}
/* svg elements are styled differently from html elements */
rect.bar {
stroke: none;
fill: #00543c;
}
#x-axis text,
#y-axis text {
font-size: 10pt;
fill: #888888;
}
#x-axis line {
/* tick marks */
fill: none;
stroke: none;
}
#x-axis path,
#y-axis path,
#y-axis line {
fill: none;
stroke: #bbbbbb;
stroke-width: 1px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment