Skip to content

Instantly share code, notes, and snippets.

@sjengle
Last active February 12, 2019 20:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sjengle/1e23258249638a508426470a48ff2924 to your computer and use it in GitHub Desktop.
Save sjengle/1e23258249638a508426470a48ff2924 to your computer and use it in GitHub Desktop.
Letter Count Bar Chart (d3v5)

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 v5.

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 v5 based on the following updated bar chart example:

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

Please see the original examples for videos and additional references.

/*
* this function will grab the latest text from our text area and update
* the letter counts
*/
var 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(text);
// get letter count
let count = countLetters(text);
// some browsers support console.table()
// javascript supports try/catch blocks
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
let count = updateData()
// get the svg to draw on
let 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
*/
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
* http://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("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"
let 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
let xAxis = d3.axisBottom(letterScale);
let yAxis = d3.axisRight(countScale);
// check if we have already drawn our axes
if (plot.select("g#y-axis").size() < 1) {
let 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
let 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)
*/
let 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);
})
.each(function(d, i, nodes) {
console.log("Added bar for:", d.key);
});
// 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()
.each(function(d, i, nodes) {
console.log("Removing bar for:", d.key);
})
.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
let 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) {
let text = onlyLetters(input);
let 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 (let 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.v5.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-fetch/blob/master/README.md#text
d3.text("peter.txt").then(function(text) {
// we will use the console and developer tools extensively
console.log("Data loaded:")
console.log(text);
// 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(text);
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