Public
Edited
Feb 4, 2022
Insert cell
Insert cell
Insert cell
Insert cell
themes = ({
Blue: {
cmap: d3.interpolatePlasma,
neighborColoring: 'cell',
tileColoring: p => `hsl(200, ${50}%, ${(((p[0]&1) == 1)? 65 : 70) + (p[1]&1)*2.5}%)`
},
Google: {
cmap: d3.interpolatePlasma,
neighborColoring: 'text',
tileColoring: p => `hsl(200, ${50}%, ${(((p[0]&1) == 1)? 65 : 70) + (p[1]&1)*2.5}%)`
}
})
Insert cell
Insert cell
Insert cell
Insert cell
genMines = (exclude) => {
const ne = exclude ? neighbor_idxs(exclude).map(c => c.toString()) : [];
let mines = new Set();
while (mines.size < num_mines) {
let c = [Math.random()*size|0, Math.random()*size|0].toString();
if (!ne.includes(c))
mines.add(c);
}
return [...mines].map(m => eval(`[${m}]`)); // To array
}
Insert cell
mutable mines = genMines()
Insert cell
grid = {
let g = Array(size).fill().map(_=>Array(size).fill());
g = g.map((row, rowidx) => row.map((cell, colidx) => ({
position: [rowidx, colidx],
count: 0,
is_mine: mines.find(mp => mp[0] == rowidx && mp[1] == colidx) != undefined,
revealed: false
})))

// Simpler, stupider, and maybe better
mines.forEach(m => neighbor_idxs(m, true).forEach(([nx, ny]) => g[nx][ny].count += g[nx][ny].is_mine? 0 : 1));
return g;
}
Insert cell
neighbor_idxs = (p, self = false) => {
return [
[p[0] - 1, p[1]],
[p[0] + 1, p[1]],
[p[0] + ((p[1] % 2 == 0) ? 1 : -1), p[1] - 1],
[p[0], p[1] - 1],
[p[0] + ((p[1] % 2 == 0) ? 1 : -1), p[1] + 1],
[p[0], p[1] + 1]
].concat(self ? [p] : []).filter(c => c[0] >= 0 && c[1] >= 0 && c[0] < size && c[1] < size);
}
Insert cell
mine_count = p => neighbor_idxs(p, true).filter(n => grid[n[0]][n[1]].is_mine).length
Insert cell
renderflags = (flags) => {
let f = svg.selectAll('.flag')
.data(flags.map(f => hexpos(eval(f))), d => d)
f.enter()
.append('circle')
.merge(f, d => d)
.attr('class', 'flag')
.attr('cx', d => d[0])
.attr('cy', d => d[1])
.attr('r', 10)
.style('fill', 'red')
.style('pointer-events', 'none')
.exit()
.transition()
.attr('cy', d => d[1] - 10)
f.exit()
.transition()
//.duration(300)
.ease(d3.easeBackIn)
.attr('cy', d => d[1] + 100)
.style('opacity', 0.0)
.remove()
}
Insert cell
rendercursor = (cp) => {
svg.select('#uilayer').selectAll('path')
.data(neighbor_idxs(cp, true).map(f => hexpos(eval(f))))
.join('path')
.attr('d', hexpath)
.style('pointer-events', 'none')
.style('fill', 'white')
.style('opacity', 0.2)
.attr('transform', d => `matrix(${hexwidth/2 + 0.01} 0 0 ${hexwidth/2 + 0.01} ${d})`)
}
Insert cell
revealfrom = p => {
function floodrev(x,y) {
if (grid[x][y].revealed || grid[x][y].count > 0 || grid[x][y].is_mine) {
if (!grid.is_mine) grid[x][y].revealed = true;
} else {
grid[x][y].revealed = true;
neighbor_idxs([x,y]).forEach(c => floodrev(...c));
};
}
floodrev(...p);
}
Insert cell
hexpos = p => {
return [
((p[0]+(p[1] & 1 == 1 ? 0.5 : 1.0)) + 0.25) * cs,
(p[1]+1) * Math.sin(Math.PI/3) * cs
];
}
Insert cell
losescreen = () => {
let screen = svg.append('g')
let bg = screen.append('rect')
.attr('fill', 'black')
.attr('width', width)
.attr('height', width)
.style('opacity', 0.4);

let modal = screen.append('g');
modal.append('rect')
.attr('x', '25%')
.attr('y', '25%')
.attr('width', '50%')
.attr('height', '50%')
.attr('fill', 'lightblue')
.attr('rx', '30')
modal.append('text')
.attr('x', '50%')
.attr('y', '38%')
.attr('fill', 'white')
.style('text-anchor', 'middle')
// .style('alignment-baseline', 'center')
.style('font-size', '4em')
.style('font-family', 'monospace')
.text('Game Over');
}
Insert cell
rendermask = (s) => {
let mask = svg.select('#mask').selectAll('.mask')
.data(grid.flat().filter(t => !t.revealed), d => JSON.stringify(d));
mask.enter()
.append('path')
.merge(mask)
.attr('class', 'mask')
.attr('d', hexpath)
.attr('transform', d => {
let h = hexpos(d.position);
return `matrix(${hexwidth/2+1} 0 0 ${hexwidth/2+1} ${h})`
})
.attr('fill', d => theme.tileColoring(d.position))
.on('mouseover', (e, d) => rendercursor(d.position))
mask.exit()
.transition()
.attr('transform', d => {
let h = hexpos(d.position);
return ` translate(0 40) matrix(${hexwidth/2 - 10} 0 0 ${hexwidth/2 - 10} ${h}) rotate(45)`
})
.style('opacity', 0.0)
.remove();

s();
}
Insert cell
Insert cell
game = {
let flags = [];
let gamestarted = false;
svg.node().innerHTML = '';
svg.append('g').attr('id', 'board');
svg.append('g').attr('id', 'mask');
svg.append('g').attr('id', 'uilayer');

let cells = svg.select('#board').selectAll('path')
.data(grid.flat())

cells.enter()
.append('path')
.merge(cells)
.attr('d', hexpath)
.attr('transform', d => {
let h = hexpos(d.position);
return `matrix(${hexwidth/2} 0 0 ${hexwidth/2} ${h})`
})
.style('fill', d => d.is_mine ? 'white' : theme.cmap(d.count / 4)) //'green')
.style('stroke', 'lightgray')
.style('stroke-width', '0.1')
.on('mouseover', (e, d) => rendercursor(d.position))
let text = svg.select('#board').selectAll('.label')
.data(grid.flat().filter(c => c.count > 0))
.join('text')
.attr('class', 'label')
.text(d => d.count)
.attr('transform', d => `matrix(1 0 0 1 ${hexpos(d.position)})`)
.style('text-anchor', 'middle')
.style('alignment-baseline', 'middle')
.style('font-family', 'monospace')
.style('font-size', '20pt')
.style('pointer-events', 'none')

let initmask = () => {
svg.select('#mask').selectAll('.mask')
.on('click', (e, d) => {
if (grid[d.position[0]][d.position[1]].is_mine) {
losescreen();
} else {
revealfrom(d.position);
flags = flags.filter(f => !grid[eval(f)[0]][eval(f)[1]].revealed);
renderflags(flags);
rendermask(initmask);
}
})
.on('contextmenu', (e, d) => {
e.preventDefault();
let ps_str = `[${d.position.toString()}]`;
if (flags.includes(ps_str)) {
flags = flags.filter(f => f != ps_str)
} else {
flags.push(ps_str);
}
renderflags(flags);
})
}
rendermask(initmask);
return svg.node();
}
Insert cell
{
let s = d3.select(DOM.svg()).attr('viewBox', [0,0,100,100]).attr('height', 200).attr('width', 400)

let di = 0;
let dat = [
[0,1],
[0,0.5,0.75,1]
]
let but = html`<button>Click</button>`;
but.onclick = e => {
let vis = s.selectAll('.test').data(dat[di], d => d)

vis.enter()
.append('circle')
.merge(vis)
.attr('class', 'test')
.attr('cy', 50)
.attr('fill', 'red')
.attr('r', 10)
.transition()
.attr('cx', d => 80*d + 10);

vis.exit().transition().attr('cy', 0).remove();

di = (di+1)%2;
}
return html`${but}${s.node()}`;
}
Insert cell
size = 16
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more