Published
Edited
Oct 25, 2020
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof width = slider({
min: 0,
max: 800,
step: 10,
value: 600,
title: "SVG Width",
description: "How wide should the cellular automata grid be?"
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
createSVG = {
let backgroundColor = "#002";

//create svg artboard and give it an ID
let svg = d3
.create('svg')
.attr('width', width)
.attr('height', height)
.attr('id', 'mainSvg');

//background color
let background = svg
.append('rect')
.attr('width', width)
.attr('height', height)
.attr('x', 0)
.attr('y', 0)
.attr('fill', backgroundColor);

return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof jumpCA = {
//create button HTML entity
let button = html`<button>Jump Forward 100 Generations</button>`;
//attach event listener without D3's .on(event, function(){})
button.onclick = function() {
//keep track of actions
let counter = 0;
//start D3 timer
let ticker = d3.interval(elapsed => {
//run functions at interval
step();
counter++;
//if we have run 100 times...
if (counter > 100) {
//turn off the timer
ticker.stop();
}
}, generationalDelay); //how long the timer should wait between executions
};
//show in observable
return button;
}
Insert cell
Insert cell
Insert cell
Insert cell
function randomizeCA() {
//iterate throuh CA dataset
data.forEach(function(automaton) {
//check if the cell should become alive
if (Math.random() < lifeChance) {
//cell is alive at start
automaton.alive = 1;
} else {
//cell is dead at start
automaton.alive = 0;
}
});
}
Insert cell
Insert cell
function drawGrid() {
//determine width and height of each cell
let cellHeight = height / rows;
let cellWidth = width / columns;

//destroy any existing grid cells
d3.select('#mainSvg')
.selectAll('.automaton')
.remove();

//create grid based on synthetic dataset
d3.select('#mainSvg')
.selectAll('.automaton') //look for something that doesn't exist
.data(data) //compare it against data
.enter()
.append('rect')
.attr('width', cellWidth)
.attr('height', cellHeight)
.attr('x', function(d) {
return d.column * cellWidth;
})
.attr('y', function(d) {
return d.row * cellHeight;
})
.attr('fill', function(d) {
if (d.alive == 1) {
return '#002';
} else {
return 'white';
}
})
.attr('stroke', '#002')
.attr('stroke-width', '1')
.attr('class', 'automaton');
}
Insert cell
advancing_rules = md`
### Advancing the Simulation and Survival/Birth/Death Rules
`
Insert cell
function step() {
//iterate through the cells to determine if each should live or die
for (let i = 0; i < data.length; i++) {
//clarifying variable
let currentCell = data[i];

//holder to collect all of our living neighbors
let neighborSum = 0;

//check if this cell is the top-left corner
if (currentCell.row == 0 && currentCell.column == 0) {
//corners have 3 neighbors
neighborSum =
data[currentCell.b].alive +
data[currentCell.r].alive +
data[currentCell.br].alive;
}
//check if this cell is the top-right corner
else if (currentCell.row == 0 && currentCell.column == columns - 1) {
neighborSum =
data[currentCell.b].alive +
data[currentCell.l].alive +
data[currentCell.bl].alive;
}
//check if this cell is the bottom-left corner
else if (currentCell.row == rows - 1 && currentCell.column == 0) {
neighborSum =
data[currentCell.t].alive +
data[currentCell.r].alive +
data[currentCell.tr].alive;
}
//check if this cell is the bottom-right corner
else if (currentCell.row == rows - 1 && currentCell.column == columns - 1) {
neighborSum =
data[currentCell.t].alive +
data[currentCell.l].alive +
data[currentCell.tl].alive;
}
//check if this cell is on the top edge
else if (currentCell.row == 0) {
//edge cells have 5 neighbors
neighborSum =
data[currentCell.b].alive +
data[currentCell.l].alive +
data[currentCell.r].alive +
data[currentCell.br].alive +
data[currentCell.bl].alive;
}
//check if this cell is on the bottom edge
else if (currentCell.row == rows - 1) {
neighborSum =
data[currentCell.t].alive +
data[currentCell.l].alive +
data[currentCell.r].alive +
data[currentCell.tr].alive +
data[currentCell.tl].alive;
}
//check if this cell is on the left-hand edge
else if (currentCell.column == 0) {
neighborSum =
data[currentCell.t].alive +
data[currentCell.b].alive +
data[currentCell.r].alive +
data[currentCell.tr].alive +
data[currentCell.br].alive;
}
//check if this cell is on the right-hand edge
else if (currentCell.column == columns - 1) {
neighborSum =
data[currentCell.t].alive +
data[currentCell.b].alive +
data[currentCell.l].alive +
data[currentCell.tl].alive +
data[currentCell.bl].alive;
}
//if all the checks above fail, this cell is an interior cell
else {
//interior cells have 8 neighbors
neighborSum =
data[currentCell.t].alive +
data[currentCell.b].alive +
data[currentCell.l].alive +
data[currentCell.r].alive +
data[currentCell.tr].alive +
data[currentCell.tl].alive +
data[currentCell.br].alive +
data[currentCell.bl].alive;
}

//Rules of the Game
//this sim uses the *survive* property to determine if the cell should live or die in the next generation
// These rules are *Conway's Game of Life*
// Any live cell with two or three live neighbors survives.
// Any dead cell with three live neighbors becomes a live cell.
// All other live cells die in the next generation. Similarly, all other dead cells stay dead.

// is the cell dead?
if (currentCell.alive == 0) {
//the cell is dead
if (neighborSum == 3) {
//if the cell is dead with three live neighbors, becem alie
currentCell.survive = 1;
} else {
//otherwise stay dead
currentCell.survive = 0;
}
} else {
//the cell is alive...
if (neighborSum == 2 || neighborSum == 3) {
//the living cell has 2 or 3 live neighbors, so it survives
currentCell.survive = 1;
} else {
//the cell dies
currentCell.survive = 0;
}
}
}

//replace the alive property with survive
//this seems unnecessary, right? why not just update alive directly above?
//if we do, things get really buggy, can you think of why?
//zach didn't catch it, and pulled out lots of hair trying to solve it!
for (let i = 0; i < data.length; i++) {
let currentCell = data[i];
currentCell.alive = currentCell.survive;
}

//update the visualization with a data join
d3.select('#mainSvg')
.selectAll('.automaton')
.data(data)
.transition()
.duration(generationalDelay)
.delay((d, i) => i / 10)
.attr('fill', function(d) {
if (d.alive == 1) {
return 'black';
} else {
return 'white';
}
});
}
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more