Skip to content

Instantly share code, notes, and snippets.

@christophermanning
Last active May 3, 2018 16:02
Show Gist options
  • Save christophermanning/6468148 to your computer and use it in GitHub Desktop.
Save christophermanning/6468148 to your computer and use it in GitHub Desktop.
Kaprekar Routine

Created by Christopher Manning

Summary

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.

References

<!DOCTYPE html>
<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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment