Skip to content

Instantly share code, notes, and snippets.

@jeroenjanssens
Last active March 1, 2016 14:08
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeroenjanssens/6395842 to your computer and use it in GitHub Desktop.
Save jeroenjanssens/6395842 to your computer and use it in GitHub Desktop.
Stem-and-Leaf Plot

Back in the old days, when many data sets were still small, stem-and-leaf plots were a popular method of representing quantitative data. The example data shown in the text area comes from the cover of John Tukey's Exploratory Data Analysis. The stem-and-leaf plot updates as you change the data. Try adding fractions and negative values. Hover over the leaves to see the original values.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
background: #fcfcfa;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
position: relative;
width: 960px;
}
ul.stems {
list-style-type: none;
}
ul.stems > li > div {
display: inline-block;
padding: .1em .5em;
border-right: 2px solid #000;
text-align: right;
font-weight: bold;
width: 2em;
}
ul.leaves {
display: inline-block;
padding-left: .5em;
}
ul.leaves > li {
display: inline-block;
width: .7em;
text-align: center;
position: relative;
cursor: default;
padding: .1em 0;
}
ul.leaves > li:hover {
background: #000;
color: #fff;
}
ul.leaves > li:hover:after {
background: #ccc;
border: 1px solid #000;
color: #000;
content: attr(data-value);
position: absolute;
padding: .2em .5em;
top: -2em;
left: 1em;
pointer-events: none;
font-size: 80%;
}
textarea {
border: 1px solid #333;
width: 90%;
}
</style>
<div></div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="stem.js"></script>
stem_and_leaf = (value, base) ->
sign = if value < 0 then "-" else "" # String because "-0" != "0"
value = Math.abs(Math.round(value))
stem = sign + Math.floor(value / base)
leaf = value % base
[stem, leaf]
stems_and_leaves = (data, base=10) ->
# Sort the data so that the leaves appear in the correct order
data.sort((a,b) -> a-b)
# Determine lowest and highest stem
min = data[0]
max = data[data.length - 1]
start = +stem_and_leaf(min, base)[0]
end = +stem_and_leaf(max, base)[0]
# Add all the necessary stems, including "-0" if necessary
stems = {}
for stem in [start..end]
if stem is 0 and min < 0
stems["-0"] = []
stems[""+stem] = []
# Add the leaves to the stems
for value in data
[stem, leaf] = stem_and_leaf(value, base)
stems[stem].push {'leaf': leaf, 'value': value}
stemdata = []
for stem, leaves of stems
stemdata.push({'stem': stem, 'leaves': leaves})
stemdata
update = ->
# Get data from textarea
data = text.node().value.split(",").map (x) -> parseFloat(x)
data = (x for x in data when not isNaN(x))
console.log data
stemdata = stems_and_leaves(data)
# Bind data to stems
stems = plot.selectAll("li.stem")
.data(stemdata, (d) ->
d.stem+(x.value for x in d.leaves).join(',')
)
# Stem enter
stem_enter = stems.enter()
.append("li")
.attr("class", "stem")
stem_enter.append("div")
# Leaves enter
stem_enter.append("ul")
.attr("class", "leaves")
.selectAll("li.leaf")
.data((d) -> d.leaves)
.enter()
.append("li")
.attr("class", "leaf")
.attr('data-value', (d) -> d.value)
.text((d) -> d.leaf)
# Re-order the stems
stems.sort((a, b) ->
x = a.stem
y = b.stem
if (x is "-0") and (y is "0") then return -1
if (y is "-0") and (x is "0") then return 1
if (+x < +y) then -1 else 1
)
# Stem update
stems.select("div").text((d) -> d.stem)
# Stem exit
stems.exit().remove()
initial_data = [17, 32, 47, 53, 60, 61, 64, 67, 70, 70, 71, 72, 73, 73, 74, 76, 77, 79, 81, 82, 83, 83, 83, 83, 84, 85, 86, 87, 87, 88, 89, 90, 91, 91, 92, 94, 94, 95, 96, 97, 98, 98, 99, 99, 99, 99, 99, 100, 100, 100, 101, 101, 102, 103, 103, 103, 103, 104, 106, 106, 106, 106, 107, 107, 107, 107, 108, 109, 109, 110, 111, 111, 111, 112, 112, 113, 114, 114, 114, 115, 116, 117, 117, 119, 120, 120, 120, 120, 121, 121, 122, 122, 122, 123, 124, 124, 125, 125, 126, 126, 127, 128, 129, 130, 131, 131, 131, 131, 132, 132, 132, 132, 133, 133, 134, 134, 134, 135, 135, 135, 136, 136, 136, 137, 138, 139, 140, 140, 142, 143, 144, 145, 145, 145, 145, 145, 147, 149, 152, 155, 157, 159]
div = d3.select("div")
text = div.append("textarea")
plot = div.append("ul").attr("class", "stems")
text.text(initial_data.join(","))
text.on("keyup", -> update())
update()
// Generated by CoffeeScript 1.6.1
(function() {
var div, initial_data, plot, stem_and_leaf, stems_and_leaves, text, update;
stem_and_leaf = function(value, base) {
var leaf, sign, stem;
sign = value < 0 ? "-" : "";
value = Math.abs(Math.round(value));
stem = sign + Math.floor(value / base);
leaf = value % base;
return [stem, leaf];
};
stems_and_leaves = function(data, base) {
var end, leaf, leaves, max, min, start, stem, stemdata, stems, value, _i, _j, _len, _ref;
if (base == null) {
base = 10;
}
data.sort(function(a, b) {
return a - b;
});
min = data[0];
max = data[data.length - 1];
start = +stem_and_leaf(min, base)[0];
end = +stem_and_leaf(max, base)[0];
stems = {};
for (stem = _i = start; start <= end ? _i <= end : _i >= end; stem = start <= end ? ++_i : --_i) {
if (stem === 0 && min < 0) {
stems["-0"] = [];
}
stems["" + stem] = [];
}
for (_j = 0, _len = data.length; _j < _len; _j++) {
value = data[_j];
_ref = stem_and_leaf(value, base), stem = _ref[0], leaf = _ref[1];
stems[stem].push({
'leaf': leaf,
'value': value
});
}
stemdata = [];
for (stem in stems) {
leaves = stems[stem];
stemdata.push({
'stem': stem,
'leaves': leaves
});
}
return stemdata;
};
update = function() {
var data, stem_enter, stemdata, stems, x;
data = text.node().value.split(",").map(function(x) {
return parseFloat(x);
});
data = (function() {
var _i, _len, _results;
_results = [];
for (_i = 0, _len = data.length; _i < _len; _i++) {
x = data[_i];
if (!isNaN(x)) {
_results.push(x);
}
}
return _results;
})();
console.log(data);
stemdata = stems_and_leaves(data);
stems = plot.selectAll("li.stem").data(stemdata, function(d) {
return d.stem + ((function() {
var _i, _len, _ref, _results;
_ref = d.leaves;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
x = _ref[_i];
_results.push(x.value);
}
return _results;
})()).join(',');
});
stem_enter = stems.enter().append("li").attr("class", "stem");
stem_enter.append("div");
stem_enter.append("ul").attr("class", "leaves").selectAll("li.leaf").data(function(d) {
return d.leaves;
}).enter().append("li").attr("class", "leaf").attr('data-value', function(d) {
return d.value;
}).text(function(d) {
return d.leaf;
});
stems.sort(function(a, b) {
var y;
x = a.stem;
y = b.stem;
if ((x === "-0") && (y === "0")) {
return -1;
}
if ((y === "-0") && (x === "0")) {
return 1;
}
if (+x < +y) {
return -1;
} else {
return 1;
}
});
stems.select("div").text(function(d) {
return d.stem;
});
return stems.exit().remove();
d3.select(self.frameElement).style("height", div.style("height"));
};
initial_data = [17, 32, 47, 53, 60, 61, 64, 67, 70, 70, 71, 72, 73, 73, 74, 76, 77, 79, 81, 82, 83, 83, 83, 83, 84, 85, 86, 87, 87, 88, 89, 90, 91, 91, 92, 94, 94, 95, 96, 97, 98, 98, 99, 99, 99, 99, 99, 100, 100, 100, 101, 101, 102, 103, 103, 103, 103, 104, 106, 106, 106, 106, 107, 107, 107, 107, 108, 109, 109, 110, 111, 111, 111, 112, 112, 113, 114, 114, 114, 115, 116, 117, 117, 119, 120, 120, 120, 120, 121, 121, 122, 122, 122, 123, 124, 124, 125, 125, 126, 126, 127, 128, 129, 130, 131, 131, 131, 131, 132, 132, 132, 132, 133, 133, 134, 134, 134, 135, 135, 135, 136, 136, 136, 137, 138, 139, 140, 140, 142, 143, 144, 145, 145, 145, 145, 145, 147, 149, 152, 155, 157, 159];
div = d3.select("div");
text = div.append("textarea");
plot = div.append("ul").attr("class", "stems");
text.text(initial_data.join(","));
text.on("keyup", function() {
return update();
});
update();
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment