Create a gist now

Instantly share code, notes, and snippets.

@bricof /.block
Last active Apr 21, 2017

What would you like to do?
State Transitions

This is a cleaned and simplified version of the state transitions animation used in the State Machines and Demand Modeling sections of the Stitch Fix Algorithms Tour.

The underlying simulation is based on a state transitions probability matrix, updating the states at each timestep. The multi-foci force layout acts on the associated updated foci associations for the circles. The bar chart on the right simply records the number of nodes in each state at each timestep.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.axis path,
.axis line {
fill: none;
stroke: #847c77;
shape-rendering: crispEdges;
}
.axis text {
fill: #847c77;
}
</style>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="state-machines.js"></script>
<script>
var width = 960
var height = 500
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", "0 -30 960 470")
var color = d3.scaleOrdinal().range(["#aaa", "#CCDE66", "#F3A54A", "#4B90A6"])
// history chart axes
var lines_margin = {top: 50, right: 25, bottom: 150, left: 500},
lines_width = width - lines_margin.left - lines_margin.right,
lines_height = height - lines_margin.top - lines_margin.bottom
var x = d3.scaleLinear()
.domain([0,40])
.range([0, lines_width])
var y = d3.scaleLinear()
.domain([0,300])
.range([lines_height, 0])
var xAxis = d3.axisBottom()
.scale(x)
var yAxis = d3.axisLeft()
.scale(y)
var history_g = svg.append("g")
.attr("transform", "translate(" + lines_margin.left + "," + lines_margin.top + ")")
var history_g_bars = history_g.append("g")
var xa = history_g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + lines_height + ")")
xa
.append("line")
.attr("x1", x(0))
.attr("x2", x(40))
.attr("y1", 0)
.attr("y2", 0)
xa
.append("text")
.attr("x", x(40))
.attr("dy", "1.5em")
.style("text-anchor", "end")
.text("time")
var ya = history_g.append("g")
.attr("class", "y axis")
ya
.append("line")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", y(0))
.attr("y2", y(300))
ya
.append("text")
.attr("transform", "rotate(-90)")
.attr("dy", "-0.5em")
.style("text-anchor", "end")
.text("number of circles in each state")
// force graph
var foci = [
{"text": "Initial", "x": 100, "y": -100},
{"text": "State 1", "x": 100, "y": 170},
{"text": "State 2", "x": 250, "y": 250},
{"text": "State 3", "x": 350, "y": 100}
]
foci.forEach(function(d,i){
svg.append('text')
.attr("class", "state_count_" + i)
.attr("x", d.x)
.attr("y", d.y - 50)
.attr("fill", color(i))
.attr("text-anchor", "middle")
.text(foci[i].text)
})
var nodes = []
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody().strength(-2))
.force("x", d3.forceX(function(d){ return foci[d.state].x}).strength(0.1))
.force("y", d3.forceY(function(d){ return foci[d.state].y}).strength(0.1))
.nodes(nodes)
.alphaTarget(1)
.on("tick", ticked)
function ticked() {
d3.selectAll(".node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
}
function restart() {
var node = svg.selectAll(".node").data(nodes, function(d) { return d.id;})
node.enter().append("circle")
.attr("class", "node")
.attr("stroke", "#333")
.attr("r", 3)
.style("opacity", 0)
node
.transition().delay(10).duration(0) // avoids strange hiccup
.attr("fill", function(d) { return color(d.state) })
.style("opacity", 1)
simulation.nodes(nodes)
simulation.alpha(1).restart()
}
// state transitions animation
state_transitions(svg, history_g_bars, foci, color)
</script>
function state_transitions(svg, history_g_bars, foci, color){
var state_transition_probabilities = [
[0, 1, 0, 0],
[0, 0.90, 0.09, 0.01],
[0, 0, 0.96, 0.04],
[0, 0, 0.1, 0.90]
]
var t = 0
var history_data = []
var state_machine_animation_interval = d3.interval(function(){
// *** SIMULATION ***
// update time
t += 1
// stop simulation after 40 timesteps
if (t > 40) { state_machine_animation_interval.stop(); simulation.stop(); return true }
// change node states according to transition probabilities matrix
// (and compute updated foci counts while at it)
var focus_node_count = []
foci.forEach(function(d){
focus_node_count.push(0)
})
nodes.forEach(function(o, i) {
var trans_probs = state_transition_probabilities[o.state]
var cum_trans_probs = []
trans_probs.reduce(function(a,b,i) { return cum_trans_probs[i] = a+b },0)
var r = Math.random()
var new_state = cum_trans_probs.findIndex(function(d){ return d >= r })
if (new_state != -1) {
o.previous_state = o.state
o.state = new_state
}
focus_node_count[o.state] += 1
})
if (t > 1){
history_data.push({
t: t-1,
n1: focus_node_count[1],
n2: focus_node_count[2],
n3: focus_node_count[3]
})
}
// possibly add some new nodes
var num_new = 100
if (t > 1) { num_new = Math.floor(Math.random() * 5) }
for (var i=0; i<num_new; i++) {
nodes.push({id: nodes.length, state: 0, previous_state: 0})
}
// *** UPDATE SVG ANIMATION ***
// update history chart
var new_stacked_column = history_g_bars.selectAll(".state_history")
.data(history_data)
.enter().append("g")
.attr("class", "state_history")
var d = new_stacked_column.data()[0]
if (d) {
var base_height = 0
for (var i=1; i<4; i++) {
var height = d['n' + i]
new_stacked_column.append("rect")
.attr("x", x(d.t))
.attr("y", y(base_height + height))
.attr("width", 12)
.attr("height", y(base_height) - y(height + base_height))
.style("fill", color(i))
base_height += height
}
}
// update force graph
restart()
foci.forEach(function(d,i){
d3.select(".state_count_" + i)
.transition().duration(500)
.attr("y", d.y - 20 - Math.pow(focus_node_count[i] * 10, 0.5) )
})
}, 800, d3.now() - 800)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment