Escargot Chart using D3
Aim
Import and parse a text file using HTML and JS. Update a D3 barchart as the words of the text file are processed.
Background
This idea was dreamed up while writing notes on a completely different task (see Piping Python using Bash. I called it an Escargot Chart because it runs at a snails pace. Each letter of the alphabet is a contender to win the race. As words are processed the bar grows in size. There are some rules; 1) only words with more than 2 letters are accepted, 2) no stop words are allowed (e.g. the, on, at), and 3) the first letter has to be in the alphabet.
All I wanted to get out of this was to see if I could use HTML to add a text file and to loop through the words to update a chart. I spent next to no time tweaking javascript and there is nothing special about my version of this chart, most of the D3 implementation follows "Lets Make a Bar Chart" by Mike Bostock .
This gif was created using Peek (https://github.com/phw/peek)
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'/>
<link rel="stylesheet" href="base.css">
<script src="data.js"></script>
<script src="stopwords.js"></script>
<script src="d3.min.js"></script>
</head>
<body>
<h1>Escargot Chart</h1>
<div class='inputzone'>
<input type='file' id='files' name='files[]'/>
<output id='list'></output>
<p id='counter'>Total: 0</p>
<p id="qty">Count: 0</p>
<button type="button"
onclick="countWords()">
Run
</button>
</div>
<div class='container'>
<div class='chart'></div>
</div>
<script src="FileParser.js"></script>
</body>
</html>
base.css
.container {
height: 400px;
width: 500px;
}
.chart {
font: 10px sans-serif;
background-color: lightgreen;
color: black;
border: solid;
border-width: 1px;
}
data.js
var data = {
"a": 0,
"b": 0,
"c": 0,
"d": 0,
"e": 0,
"f": 0,
"g": 0,
"h": 0,
"i": 0,
"j": 0,
"k": 0,
"l": 0,
"m": 0,
"n": 0,
"o": 0,
"p": 0,
"q": 0,
"r": 0,
"s": 0,
"t": 0,
"u": 0,
"v": 0,
"w": 0,
"x": 0,
"y": 0,
"z": 0,
};
FileParser.js
// Requires data object
var wordsList = [];
var alphabet = Object.keys(data);
var ESCARGOT = Object.values(data)
// D3 CHART
var margin = {top: 10, right: 30, bottom: 10, left: 30}
var width = 500 - margin.left - margin.right;
height = 400 - margin.top - margin.bottom;
barHeight = 10;
// donRange placeholder updated in handleFileSelect()
var domRange = 5000;
var svg = d3.select(".chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", barHeight * ESCARGOT.length + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var text = svg.selectAll(".text")
.data(alphabet)
.attr("transform", function(d, i) {
return "translate(0," + i * barHeight + ")"; });
text.enter().append("text")
.attr("class", "text")
.text(function(d) { return d })
.attr("x", -10)
.attr("y", function(d, i) { return i * barHeight-2 + margin.top})
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "black")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
function countWordsByLetter(arr, obj) {
/*
This function is called ONLY in the
handleFileSelect method. It is used
to set the scale of the D3 chart.
That is, it forces an update to
domRange variable.
arr is an array containing strings.
obj is an object containing the
alphabet as keys.
*/
temp = Object.assign({}, obj);
for (var i=0; i<arr.length; i++) {
var letter = arr[i][0]
if (alphabet.includes(letter)) {
temp[letter] = temp[letter] + 1
}
}
return Math.max.apply(null, Object.values(temp))
}
function handleFileSelect(evt) {
/*
This function handles the selected
file. It is also responsible for
parsing the text in gthe document.
The method of parsing text could/should
be drastically improved.
*/
var file = evt.target.files[0];
if (file) {
var r = new FileReader();
r.onload = function(e) {
var contents = e.target.result;
var ct = r.result;
// split on new lines and space
var words = ct.split(/-\n|\s/);
// lowercase
words = words.map(v => v.toLowerCase());
// first letter match alphabet
var wordMatch = [];
for (var item=0; item<words.length; item++){
var target = words[item]
var letter = target[0]
if (target.length > 2 &&
alphabet.includes(letter) &&
stopwords.indexOf(target) != -1) {
wordMatch.push(target)
}
}
// add to global variable
wordsList.push(wordMatch)
// set domRange for plotting scales
domRange = countWordsByLetter(wordMatch, data)
document.getElementById('counter').innerHTML =
"Total: " + wordMatch.length
}
r.readAsText(file);
} else {
alert("Failed to load file");
}
}
// D3 DRAW & UPDATE
function draw(dat) {
var x = d3.scaleLinear()
.domain([0, domRange])
.range([0, width]);
var bars = svg.selectAll(".bar")
.data(dat)
.attr("transform", function(d, i) {
return "translate(0," + i * barHeight + ")"; });
bars.exit()
.transition()
.duration(300)
.attr("width", function(d) { return x(d) })
.attr("height", barHeight - 1)
.style('fill-opacity', 1e-6)
.remove();
bars.enter().append("rect")
.attr("class", "bar")
.attr("width", function(d) { return x(d) })
.attr("height", barHeight - 1);
bars.transition().duration(300)
.attr("width", function(d) { return x(d) })
.attr("height", barHeight - 1);
}
function countWords() {
var total = wordsList[0].length;
var count = 0;
var x = setInterval(function() {
var d = count + 1;
if (d <= total) {
document.getElementById('qty').innerHTML =
"Count: " + d;
// slice word from list
var w = wordsList[0][d][0]
// add count of 1 in data
data[w] = data[w] + 1
count++;
draw(Object.values(data));
}
if (d > total) {
document.getElementById('qty').innerHTML =
"Count: " + total+1;
}
}, 10);
}
// HANDLE LOAD FILE EVENT
document.getElementById('files').addEventListener('change', handleFileSelect, false);
stopwords.js
// Stop words from https://kb.yoast.com/kb/list-stop-words/
var stopwords=[
"a",
"about",
"above",
"after",
"again",
"against",
"all",
"am",
"an",
"and",
"any",
"are",
"as",
"at",
"be",
"because",
"been",
"before",
"being",
"below",
"between",
"both",
"but",
"by",
"could",
"did",
"do",
"does",
"doing",
"down",
"during",
"each",
"few",
"for",
"from",
"further",
"had",
"has",
"have",
"having",
"he",
"he’d",
"he’ll",
"he’s",
"her",
"here",
"here’s",
"hers",
"herself",
"him",
"himself",
"his",
"how",
"how’s",
"I",
"I’d",
"I’ll",
"I’m",
"I’ve",
"if",
"in",
"into",
"is",
"it",
"it’s",
"its",
"itself",
"let’s",
"me",
"more",
"most",
"my",
"myself",
"nor",
"of",
"on",
"once",
"only",
"or",
"other",
"ought",
"our",
"ours",
"ourselves",
"out",
"over",
"own",
"same",
"she",
"she’d",
"she’ll",
"she’s",
"should",
"so",
"some",
"such",
"than",
"that",
"that’s",
"the",
"their",
"theirs",
"them",
"themselves",
"then",
"there",
"there’s",
"these",
"they",
"they’d",
"they’ll",
"they’re",
"they’ve",
"this",
"those",
"through",
"to",
"too",
"under",
"until",
"up",
"very",
"was",
"we",
"we’d",
"we’ll",
"we’re",
"we’ve",
"were",
"what",
"what’s",
"when",
"when’s",
"where",
"where’s",
"which",
"while",
"who",
"who’s",
"whom",
"why",
"why’s",
"with",
"would",
"you",
"you’d",
"you’ll",
"you’re",
"you’ve",
"your",
"yours",
"yourself",
"yourselves",
]