Created by Christopher Manning
Kaprekar's constant is 6174. The Kaprekar Routine arranges four digits (zeros are appended to the number if it's less than 4 digits) in descending and ascending order, subtracts those two numbers, and repeats the process until the difference is 0 (degenrate case) or 6174 (Kaprekar's constant). The color is HSL with a scale of 0 to 300 in the domain of 1 to 7 (minimimum and maxium iterations to reach Kaprekar's constant) for hue, 1 for saturation, and .5 for lightness.
I created this because I wanted to visualize Kaprekar's constant in some way. This Wolfram MathWorld article on the Kaprekar Routine has a compelling image that is inspired by the cover of The Mathematics Teacher. From there, I decided to research some visualization techniques by recreating it with d3.js and adding some extra features (sorting, highlighting, and showing the routine).
I first heard of Kaprekar's constant after looking at the Google New York office. Apparently, there are about one-half of Kaprekar's constant Googlers there.
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8">
<title>Kaprekar Routine</title>
<script src="//d3js.org/d3.v3.min.js"></script>
<style type="text/css">
body {
padding: 0;
margin: 0;
font-family: "Helvetica Neue", Helvetica, sans-serif;
font-size: 1.25em;
}
.axis path {
display: none;
}
.axis line {
stroke: #000;
stroke-width: .25px;
}
rect {
stroke: black;
stroke-width: .25px;
}
</style>
</head>
<body>
<div style="position: absolute; top: 30px; left: 300px;">
<b>Sort</b> by
<i><a href="#" class="sort-btn" data-sort="n">n</a></i> or
<i><a href="#" class="sort-btn" data-sort="i">iterations</a></i>
</div>
<script type="text/javascript">
var margin = {top: 100, right: 20, bottom: 0, left: 300},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var z = 3,
xBlocks = 100,
yBlocks = 100,
s = .1,
bh = ((s*100)+(z*100))
// index of the array is the value
data = d3.range(100 * 100).map(function(n) { return {i: n, o: n, k: kaprekarProcess(n).length } })
svg = d3.select("body")
.on("keydown", clearFocused)
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
chart = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
var color = d3.scale.linear().domain([1, 7]).range([0, 300])
var xScale = d3.scale.linear().range([(s*100)+(z*100), 0]).domain([100, 0])
var yScale = d3.scale.linear().range([0, (s*100)+(z*100)]).domain([100, 0])
var axis = d3.svg.axis().ticks(25)
// only show 0, 25, 50, and 100 ticks
.tickFormat(function(d) { return d % 25 == 0 ? d : "" })
chart
.append("g")
.attr("class", "y axis")
.attr("transform", "translate(0," + z + ")")
.call(axis.scale(yScale).orient("left"))
.append("text")
.attr("class", "y label")
.attr("text-anchor", "middle")
.attr("transform", "translate(-50,"+ bh/2 +")rotate(-90)")
.text("[n / 100]");
chart
.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (bh+z) + ")")
.call(axis.scale(xScale).orient("bottom"))
.append("text")
.attr("class", "y label")
.attr("text-anchor", "middle")
.attr("transform", "translate(" + bh/2 + ", 50)")
.text("n mod 100");
chart
.append("text")
.attr("class", "desc")
.attr("transform", "translate(" + (bh + margin.right) + ", 0)")
d3.selectAll(".sort-btn")
.on("click", function(d) {
d3.event.preventDefault();
sort(this.dataset.sort)
update()
});
var matrix = []
// draw data the first time
update()
// teaser - focus a random rect
d3.select("rect:nth-child("+(Math.floor(Math.random() * (100*100)+1))+")").each(highlight)
function sort(m) {
var dataCopy = []
for (var i=0, length = data.length; i < length; i++) {
dataCopy.push({
i: i,
v: data[i]
});
}
if (m == "i") {
dataCopy.sort(function(a, b) {
// stable sort
return a.v.k == b.v.k ? a.v.i - b.v.i : a.v.k - b.v.k
})
} else {
dataCopy.sort(function(a, b) { return a.v.i - b.v.i })
}
for (var i=0, length = dataCopy.length; i < length; i++) {
data[dataCopy[i].i].o = i
}
}
function transform(d) {
var x = (d.x * z) + (s * d.x),
y = bh - ((d.y * z) + (s * d.y))
return "translate(" + x + "," + y + ")" + ( d.focused ? "scale(15)rotate(180)" : "")
}
function update() {
data.forEach(function(d) {
d.x = d.o % xBlocks
d.y = Math.floor(d.o / yBlocks)
})
var rects = chart.selectAll("rect")
// so when reordering the dom it keeps the correct data mapping
.data(data, function(d) { return d.i })
rects.enter()
.append("rect")
.attr("width", z)
.attr("height", z)
.attr("transform", transform)
.style("fill", function(d) { return d3.hsl(color(d.k), 1, .5) })
.on("mouseover", highlight)
.on("click", highlight)
rects
.transition()
.duration(1000)
.attr("transform", transform)
}
function clearFocused() {
// reset any previously focused rects
d3.select("rect.focused")
.datum(function(d, i) {
d.focused = false
return d
})
.classed("focused", function(d) { return d.focused })
.transition()
.delay(250)
.attr("transform", transform)
.each("end", function(d) {
d3.select(this).style("pointer-events", "auto")
})
d3.selectAll("text.desc tspan").remove()
}
function highlight(d, i) {
clearFocused()
kaprekarProcessText(d.i)
// so the element has the highest z-index
this.parentNode.appendChild(this);
// temporarily disable pointer-events so transition isn't triggered on a transitioning element
d3.select(this)
.datum(function(d, i) {
d.focused = true
return d
})
.style("pointer-events", "none")
.classed("focused", function(d) { return d.focused })
.transition()
.duration(500)
.attr("transform", transform)
}
function kaprekarProcessText(d) {
var lines = []
var k = kaprekarProcess(d)
lines.push(String(k[0]))
lines = lines.concat(k.slice(1).map(function(d) {
return d[0] + " - " + d[1] + " = " + d[2]
}))
var t = d3.select("text.desc")
.selectAll("tspan")
.data(lines)
t.enter()
.append("tspan")
.style("font-weight", function(d, i) { return i == 0 ? "bold" : "normal" })
.attr("x", 0)
.attr("dy", 30)
t.text(String)
t.exit().remove()
}
function kaprekarProcess(v) {
var o = [v]
for(var i=1; i <= 8; i++) {
var digits = String(v).split('').map(parseFloat)
// pad with zeros so we have 4 digits
for(var j=digits.length; j < 4; j++) {
digits.push(0)
}
var asc = digits.slice().sort().join('')
var desc = digits.slice().sort().reverse().join('')
v = +desc - +asc
o.push([desc, asc, v])
if(v == 6174 || v == 0) break
}
return o
}
</script>
</body>
</html>
https://d3js.org/d3.v3.min.js