Skip to content

Instantly share code, notes, and snippets.

@Agnishom
Last active June 27, 2025 05:20
Show Gist options
  • Save Agnishom/c35dcb0f29900985165a9c61be777d40 to your computer and use it in GitHub Desktop.
Save Agnishom/c35dcb0f29900985165a9c61be777d40 to your computer and use it in GitHub Desktop.
Connectivity Widget
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Graph Components</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Custom styles for the body to center the content */
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f7fafc; /* Lighter gray background */
margin: 0;
font-family: 'Inter', sans-serif;
}
/* Container for the canvas and info panel */
#main-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
padding: 2rem;
background-color: #ffffff;
border-radius: 0.75rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
canvas {
background-color: #ffffff;
border-radius: 0.5rem;
}
/* Styling for the info panel */
#info-panel {
font-size: 1rem;
color: #4a5568; /* gray-700 */
width: 100%;
max-width: 550px;
}
.component-info {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem;
border-bottom: 1px solid #e2e8f0; /* gray-300 */
border-radius: 0.25rem;
transition: background-color 0.3s;
}
.component-info:last-child {
border-bottom: none;
}
.component-stats {
display: flex;
gap: 1rem;
text-align: right;
}
.component-stats span {
font-weight: 600; /* font-semibold */
color: #2d3748; /* gray-800 */
min-width: 50px;
}
.color-swatch {
width: 1rem;
height: 1rem;
border-radius: 50%;
margin-right: 0.75rem;
border: 1px solid rgba(0,0,0,0.2);
}
/* Disconnect Message Style */
#disconnect-message {
color: #e53e3e; /* text-red-600 */
font-weight: 700; /* font-bold */
font-size: 1.25rem; /* text-xl */
display: none; /* Initially hidden */
}
/* Control bar styling */
#controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
width: 100%;
justify-content: center;
}
#grid-size-input {
width: 5rem;
padding: 0.25rem 0.5rem;
border: 1px solid #cbd5e0;
border-radius: 0.375rem;
}
label {
font-weight: 500;
color: #4a5568;
}
.button {
background-color: #4a5568; /* gray-600 */
color: white;
padding: 0.5rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
cursor: pointer;
border: none;
transition: background-color 0.2s;
}
.button:hover {
background-color: #2d3748; /* gray-800 */
}
</style>
</head>
<body class="bg-gray-100">
<div id="main-container">
<canvas id="gridCanvas"></canvas>
<div id="info-panel"></div>
<p id="disconnect-message">Board Disconnected</p>
<div id="controls">
<div>
<label for="grid-size-input">Size (N):</label>
<input type="number" id="grid-size-input" value="8" min="2" max="20">
</div>
<button id="reset-button" class="button">Reset</button>
<button id="randomize-button" class="button">Randomize</button>
<div>
<label for="randomize-slider">Cells:</label>
<input type="range" id="randomize-slider" min="1" value="1">
<span id="slider-value">1</span>
</div>
</div>
</div>
<script>
// --- Union-Find Data Structure ---
class UnionFind {
constructor(size) {
this.parent = Array.from({ length: size }, (_, i) => i);
}
find(i) {
if (this.parent[i] === i) return i;
return this.parent[i] = this.find(this.parent[i]);
}
union(i, j) {
const rootI = this.find(i);
const rootJ = this.find(j);
if (rootI !== rootJ) {
if (rootI < rootJ) this.parent[rootJ] = rootI;
else this.parent[rootI] = rootJ;
}
}
}
// --- Configuration ---
let CELL_GRID_SIZE;
let VERTEX_GRID_SIZE;
let CELL_SIZE = 60;
let GAP = 12;
let GRID_STEP = CELL_SIZE + GAP;
// --- Style ---
const COMPONENT_COLORS = ['#e53e3e', '#dd6b20', '#d69e2e', '#38a169', '#3182ce', '#5a67d8', '#805ad5', '#d53f8c', '#718096', '#000000'];
const VISIBLE_CELL_COLOR = '#b2f2bb';
const HIDDEN_CELL_COLOR = '#ffffff';
const CELL_BORDER_COLOR = '#000000';
const CELL_BORDER_WIDTH = 2;
const VERTEX_RADIUS = 7;
const VERTEX_BORDER_WIDTH = 2;
const EDGE_WIDTH = 3;
// --- Canvas and DOM Setup ---
const canvas = document.getElementById('gridCanvas');
const ctx = canvas.getContext('2d');
const PADDING = 35;
const infoPanelEl = document.getElementById('info-panel');
const resetButton = document.getElementById('reset-button');
const randomizeButton = document.getElementById('randomize-button');
const gridSizeInput = document.getElementById('grid-size-input');
const randomizeSlider = document.getElementById('randomize-slider');
const sliderValueEl = document.getElementById('slider-value');
const disconnectMessageEl = document.getElementById('disconnect-message');
let boardState;
// --- Drawing Logic ---
function drawCell(x, y, fillColor) {
ctx.beginPath();
const radius = 10;
ctx.moveTo(x + radius, y);
ctx.lineTo(x + CELL_SIZE - radius, y);
ctx.quadraticCurveTo(x + CELL_SIZE, y, x + CELL_SIZE, y + radius);
ctx.lineTo(x + CELL_SIZE, y + CELL_SIZE - radius);
ctx.quadraticCurveTo(x + CELL_SIZE, y + CELL_SIZE, x + CELL_SIZE - radius, y + CELL_SIZE);
ctx.lineTo(x + radius, y + CELL_SIZE);
ctx.quadraticCurveTo(x, y + CELL_SIZE, x, y + CELL_SIZE - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
ctx.strokeStyle = CELL_BORDER_COLOR;
ctx.lineWidth = CELL_BORDER_WIDTH;
ctx.fillStyle = fillColor;
ctx.fill();
ctx.stroke();
}
function drawCellGrid() {
for (let row = 0; row < CELL_GRID_SIZE; row++) {
for (let col = 0; col < CELL_GRID_SIZE; col++) {
const [v_col, v_row] = [col + 1, row + 1];
const centerX = PADDING + v_col * GRID_STEP;
const centerY = PADDING + v_row * GRID_STEP;
const cellX = centerX - CELL_SIZE / 2;
const cellY = centerY - CELL_SIZE / 2;
const fillColor = boardState[row][col] ? VISIBLE_CELL_COLOR : HIDDEN_CELL_COLOR;
drawCell(cellX, cellY, fillColor);
}
}
}
/**
* Analyzes components using Union-Find, including new diagonal rules.
*/
function analyzeComponents(fullBoardState) {
const totalVertices = VERTEX_GRID_SIZE * VERTEX_GRID_SIZE;
const uf = new UnionFind(totalVertices);
const to1D = (r, c) => r * VERTEX_GRID_SIZE + c;
// Pre-unify the outer boundary.
const boundaryAnchor = to1D(0, 0);
for (let i = 0; i < VERTEX_GRID_SIZE; i++) {
uf.union(boundaryAnchor, to1D(0, i));
uf.union(boundaryAnchor, to1D(VERTEX_GRID_SIZE - 1, i));
uf.union(boundaryAnchor, to1D(i, 0));
uf.union(boundaryAnchor, to1D(i, VERTEX_GRID_SIZE - 1));
}
// Perform union operations for all adjacent "off" cells
for (let r = 0; r < VERTEX_GRID_SIZE; r++) {
for (let c = 0; c < VERTEX_GRID_SIZE; c++) {
// --- Standard Edges ---
if (fullBoardState[r][c] === false) {
// Horizontal
if (c < VERTEX_GRID_SIZE - 1 && fullBoardState[r][c+1] === false) {
uf.union(to1D(r, c), to1D(r, c+1));
}
// Vertical
if (r < VERTEX_GRID_SIZE - 1 && fullBoardState[r+1][c] === false) {
uf.union(to1D(r, c), to1D(r+1, c));
}
}
// --- Diagonal Edges (check 2x2 blocks) ---
if (r < VERTEX_GRID_SIZE - 1 && c < VERTEX_GRID_SIZE - 1) {
// NW-SE diagonal: from (r,c) to (r+1,c+1)
if (fullBoardState[r][c] === false && fullBoardState[r+1][c+1] === false &&
fullBoardState[r][c+1] === true && fullBoardState[r+1][c] === true) {
uf.union(to1D(r,c), to1D(r+1,c+1));
}
// NE-SW diagonal: from (r,c+1) to (r+1,c)
if (fullBoardState[r][c+1] === false && fullBoardState[r+1][c] === false &&
fullBoardState[r][c] === true && fullBoardState[r+1][c+1] === true) {
uf.union(to1D(r,c+1), to1D(r+1,c));
}
}
}
}
const rootToComponentId = {};
let nextComponentId = 0;
const boundaryRoot = uf.find(boundaryAnchor);
rootToComponentId[boundaryRoot] = nextComponentId++;
const componentMap = Array(VERTEX_GRID_SIZE).fill(null).map(() => Array(VERTEX_GRID_SIZE).fill(-1));
for (let r = 0; r < VERTEX_GRID_SIZE; r++) {
for (let c = 0; c < VERTEX_GRID_SIZE; c++) {
if (fullBoardState[r][c] === false) {
const root = uf.find(to1D(r, c));
if (rootToComponentId[root] === undefined) {
rootToComponentId[root] = nextComponentId++;
}
componentMap[r][c] = rootToComponentId[root];
}
}
}
return componentMap;
}
function drawGraphAndCount(fullBoardState, componentMap) {
const counts = {};
// Initialize counts object
for (let r = 0; r < VERTEX_GRID_SIZE; r++) {
for (let c = 0; c < VERTEX_GRID_SIZE; c++) {
if (fullBoardState[r][c] === false) {
const componentId = componentMap[r][c];
if (!counts[componentId]) {
counts[componentId] = { nodes: 0, edges: 0, spuriousFaces: 0 };
}
}
}
}
// Draw edges and count them based on new rules
for (let r = 0; r < VERTEX_GRID_SIZE; r++) {
for (let c = 0; c < VERTEX_GRID_SIZE; c++) {
if (fullBoardState[r][c] === false) {
const componentId = componentMap[r][c];
// Horizontal Edge
if (c < VERTEX_GRID_SIZE - 1 && fullBoardState[r][c+1] === false) {
ctx.beginPath();
ctx.moveTo(PADDING + c * GRID_STEP, PADDING + r * GRID_STEP);
ctx.lineTo(PADDING + (c + 1) * GRID_STEP, PADDING + r * GRID_STEP);
ctx.strokeStyle = COMPONENT_COLORS[componentId % COMPONENT_COLORS.length];
ctx.lineWidth = EDGE_WIDTH;
ctx.stroke();
if(componentMap[r][c] === componentMap[r][c+1]) counts[componentId].edges++;
}
// Vertical Edge
if (r < VERTEX_GRID_SIZE - 1 && fullBoardState[r+1][c] === false) {
ctx.beginPath();
ctx.moveTo(PADDING + c * GRID_STEP, PADDING + r * GRID_STEP);
ctx.lineTo(PADDING + c * GRID_STEP, PADDING + (r + 1) * GRID_STEP);
ctx.strokeStyle = COMPONENT_COLORS[componentId % COMPONENT_COLORS.length];
ctx.lineWidth = EDGE_WIDTH;
ctx.stroke();
if(componentMap[r][c] === componentMap[r+1][c]) counts[componentId].edges++;
}
}
// Diagonal Edges (check 2x2 blocks)
if (r < VERTEX_GRID_SIZE - 1 && c < VERTEX_GRID_SIZE - 1) {
// NW-SE
if (fullBoardState[r][c] === false && fullBoardState[r+1][c+1] === false &&
fullBoardState[r][c+1] === true && fullBoardState[r+1][c] === true) {
const componentId = componentMap[r][c];
ctx.beginPath();
ctx.moveTo(PADDING + c * GRID_STEP, PADDING + r * GRID_STEP);
ctx.lineTo(PADDING + (c + 1) * GRID_STEP, PADDING + (r + 1) * GRID_STEP);
ctx.strokeStyle = COMPONENT_COLORS[componentId % COMPONENT_COLORS.length];
ctx.lineWidth = EDGE_WIDTH;
ctx.stroke();
if(componentMap[r][c] === componentMap[r+1][c+1]) counts[componentId].edges++;
}
// NE-SW
if (fullBoardState[r][c+1] === false && fullBoardState[r+1][c] === false &&
fullBoardState[r][c] === true && fullBoardState[r+1][c+1] === true) {
const componentId = componentMap[r][c+1];
ctx.beginPath();
ctx.moveTo(PADDING + (c + 1) * GRID_STEP, PADDING + r * GRID_STEP);
ctx.lineTo(PADDING + c * GRID_STEP, PADDING + (r + 1) * GRID_STEP);
ctx.strokeStyle = COMPONENT_COLORS[componentId % COMPONENT_COLORS.length];
ctx.lineWidth = EDGE_WIDTH;
ctx.stroke();
if(componentMap[r][c+1] === componentMap[r+1][c]) counts[componentId].edges++;
}
}
}
}
// Calculate Spurious Faces
for (let r = 0; r < VERTEX_GRID_SIZE - 1; r++) {
for (let c = 0; c < VERTEX_GRID_SIZE - 1; c++) {
if (fullBoardState[r][c] === false && fullBoardState[r+1][c] === false &&
fullBoardState[r][c+1] === false && fullBoardState[r+1][c+1] === false) {
const componentId = componentMap[r+1][c+1];
if (counts[componentId]) counts[componentId].spuriousFaces++;
}
}
}
// Draw vertices and count them
for (let r = 0; r < VERTEX_GRID_SIZE; r++) {
for (let c = 0; c < VERTEX_GRID_SIZE; c++) {
if (fullBoardState[r][c] === false) {
const componentId = componentMap[r][c];
counts[componentId].nodes++;
const x = PADDING + c * GRID_STEP;
const y = PADDING + r * GRID_STEP;
const color = COMPONENT_COLORS[componentId % COMPONENT_COLORS.length];
ctx.beginPath();
ctx.arc(x, y, VERTEX_RADIUS, 0, 2 * Math.PI, false);
ctx.fillStyle = color;
ctx.strokeStyle = 'black';
ctx.lineWidth = VERTEX_BORDER_WIDTH;
ctx.fill();
ctx.stroke();
}
}
}
return counts;
}
function updateInfoPanel(counts) {
infoPanelEl.innerHTML = '';
let isDisconnected = false;
Object.keys(counts).sort((a,b) => parseInt(a) - parseInt(b)).forEach(id_str => {
const id = parseInt(id_str, 10);
const { nodes, edges, spuriousFaces } = counts[id];
const color = COMPONENT_COLORS[id % COMPONENT_COLORS.length];
const newQuantity = 2 + edges - nodes - (spuriousFaces || 0);
const p = document.createElement('div');
p.className = 'component-info';
const shouldHighlight = (id === 0 && newQuantity > 2) || (id > 0 && newQuantity > 1);
if (shouldHighlight) {
p.style.backgroundColor = '#fffbeb';
isDisconnected = true;
}
p.innerHTML = `
<div style="display: flex; align-items: center;">
<div class="color-swatch" style="background-color: ${color};"></div>
<b>Component ${id}</b>
</div>
<div class="component-stats">
<span>V: ${nodes}</span>
<span>E: ${edges}</span>
<span>F<sub>s</sub>: ${spuriousFaces || 0}</span>
<span>F-F<sub>s</sub>: ${newQuantity}</span>
</div>
`;
infoPanelEl.appendChild(p);
});
disconnectMessageEl.style.display = isDisconnected ? 'block' : 'none';
}
function updateSlider() {
let onCellsCount = 0;
for (let r = 0; r < CELL_GRID_SIZE; r++) {
for (let c = 0; c < CELL_GRID_SIZE; c++) {
if (boardState[r][c]) onCellsCount++;
}
}
randomizeSlider.disabled = onCellsCount === 0;
if (onCellsCount > 0) {
randomizeSlider.max = onCellsCount;
if (parseInt(randomizeSlider.value) > onCellsCount || parseInt(randomizeSlider.value) === 0) {
randomizeSlider.value = 1;
}
} else {
randomizeSlider.max = 1;
randomizeSlider.value = 1;
}
sliderValueEl.textContent = onCellsCount > 0 ? randomizeSlider.value : '0';
}
function redrawAll() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const fullBoardState = Array(VERTEX_GRID_SIZE).fill(null).map(() => Array(VERTEX_GRID_SIZE).fill(false));
for (let r = 0; r < CELL_GRID_SIZE; r++) {
for (let c = 0; c < CELL_GRID_SIZE; c++) {
fullBoardState[r + 1][c + 1] = boardState[r][c];
}
}
const componentMap = analyzeComponents(fullBoardState);
drawCellGrid();
const counts = drawGraphAndCount(fullBoardState, componentMap);
updateInfoPanel(counts);
updateSlider();
}
function handleCanvasClick(event) {
const rect = canvas.getBoundingClientRect();
const clickX = event.clientX - rect.left;
const clickY = event.clientY - rect.top;
for (let row = 0; row < CELL_GRID_SIZE; row++) {
for (let col = 0; col < CELL_GRID_SIZE; col++) {
if (boardState[row][col] === true) {
const [v_col, v_row] = [col + 1, row + 1];
const centerX = PADDING + v_col * GRID_STEP;
const centerY = PADDING + v_row * GRID_STEP;
const cellX = centerX - CELL_SIZE / 2;
const cellY = centerY - CELL_SIZE / 2;
if (clickX >= cellX && clickX <= cellX + CELL_SIZE && clickY >= cellY && clickY <= cellY + CELL_SIZE) {
boardState[row][col] = false;
redrawAll();
return;
}
}
}
}
}
function resetGame() {
const N = parseInt(gridSizeInput.value, 10);
if (isNaN(N) || N < 2 || N > 20) {
gridSizeInput.value = CELL_GRID_SIZE || 8;
return;
}
CELL_GRID_SIZE = N;
VERTEX_GRID_SIZE = N + 2;
const canvasSize = (VERTEX_GRID_SIZE - 1) * GRID_STEP + 2 * PADDING;
canvas.width = canvasSize;
canvas.height = canvasSize;
boardState = Array(CELL_GRID_SIZE).fill(null).map(() => Array(CELL_GRID_SIZE).fill(true));
redrawAll();
}
function randomizeBoard() {
const onCells = [];
for (let r = 0; r < CELL_GRID_SIZE; r++) {
for (let c = 0; c < CELL_GRID_SIZE; c++) {
if (boardState[r][c]) onCells.push([r, c]);
}
}
if (onCells.length === 0) return;
let numToTurnOff = parseInt(randomizeSlider.value, 10);
numToTurnOff = Math.min(numToTurnOff, onCells.length);
for (let i = onCells.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[onCells[i], onCells[j]] = [onCells[j], onCells[i]];
}
for (let i = 0; i < numToTurnOff; i++) {
const [r, c] = onCells[i];
boardState[r][c] = false;
}
redrawAll();
}
// --- Initialization ---
canvas.addEventListener('click', handleCanvasClick);
resetButton.addEventListener('click', resetGame);
randomizeButton.addEventListener('click', randomizeBoard);
randomizeSlider.addEventListener('input', (e) => {
sliderValueEl.textContent = e.target.value;
});
resetGame();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment