function hexGrid (config) {
const SQRT3_2 = Math.sqrt(3)/2;
config = Object.assign({size:15, numStates:2, margin:2, outline:'#000', shape:'parallelogram'}, config);
let {nx, ny, size, numStates, init, margin, colorArray, outline, shape} = config;
if (shape !== 'hexagon' && shape !== 'parallelogram' && shape !== 'rectangle')
throw new Error(`shape must be 'hexagon', 'parallelogram', or 'rectangle'`);
const hexagon = shape === 'hexagon';
if (init !== undefined) {
if (init.some(d => d.length !== init[0].length) ||
init.some(d => d.some(v => v<0 || v >= numStates || v != Math.round(v))) ||
(hexagon && init.length !== init[0].length))
throw new Error(`init should be undefined or a ${hexagon ? 'square' : 'rectangular'} matrix with integer entries between 0 and ${numStates-1}`);
if (nx === undefined && ny !== undefined) {
if (ny === init.length)
nx = init[0].length;
else throw new Error(`init length:${init.length} not equal to ny:${ny}`);
} else if (ny === undefined && nx !== undefined) {
if (nx === init[0].length)
ny = init.length;
else throw new Error(`init inner length:${init[0].length} not equal to nx:${nx}`);
}
else if (nx === undefined && ny === undefined) {
ny = init.length;
nx = init[0].length;
}
if (hexagon && nx !== ny)
throw new Error(`init should be undefined or a square matrix and ny and nx should be equal to its size`);
} else {
if (hexagon) {
if (nx === undefined && ny !== undefined)
nx = ny;
else if (ny === undefined && nx !== undefined)
ny = nx;
else if (nx === undefined && ny === undefined)
ny = nx = 5;
if (ny !== nx || ny < 3 || (ny % 2 === 0))
throw Error(`when 'hexagon' is 'true', ny and nx should be odd and equal to each other`);
} else {
if (nx === undefined && ny !== undefined)
nx = ny;
else if (ny === undefined && nx !== undefined)
ny = nx;
else if (nx === undefined && ny === undefined)
ny = nx = 5;
}
}
if (init !== undefined)
if (init.length !== ny || init.some(d => d.length !== nx || d.some(v => v<0 || v >= numStates)))
throw Error(`init should be undefined or an ${ny} by ${nx} with entries from 0 to ${numStates-1}`);
if (colorArray === undefined)
colorArray = Array.from({length:numStates}, (d,i) => i === 0 ? '#FFF' :
(i === 1 ? '#000' : d3.schemeSet3[(i-2) % 12]));
const unitHex = [0,1,2,3,4,5].map(d => {
const ang = Math.PI * (d + (shape === 'rectangle' ? 0 : 0.5)) / 3;
return [Math.cos(ang), Math.sin(ang)];
});
const scale = size*SQRT3_2;
const r = size / Math.sqrt(3);
let state = init ? init.map(d => d.slice()) : Array.from({length:ny}, d => Array(nx).fill(0));
const svg = hexagon ?
d3.select(DOM.svg(nx*(size+2*margin),
nx*(scale+Math.sqrt(3)*margin)+scale/2+Math.sqrt(3)*margin)) :
(shape === 'rectangle' ?
d3.select(DOM.svg(nx*(scale+2*margin) + scale,
ny*(size+2*margin)+size)) :
d3.select(DOM.svg(nx*(size+2*margin) + ny*(size/2+margin),
ny*(scale+Math.sqrt(3)*margin)+scale/2+Math.sqrt(3)*margin)));
const el = svg.node();
const hexData = [];
if (hexagon) {
for (let y=0; y<(ny+1)/2; y++)
for (let x=(nx-1)/2 - y; x<nx; x++)
hexData.push({x,y});
for (let y=(ny+1)/2; y<ny; y++)
for (let x=0; x<nx-(y-(ny-1)/2); x++)
hexData.push({x,y});
} else
for (let y=0; y<ny; y++)
for (let x=0; x<nx; x++)
hexData.push({x,y});
const grid = svg.append("g")
.attr("cursor", "pointer")
.selectAll("polygon")
.data(hexData)
.join("polygon")
.attr("points", hexPoints)
.on("click", gridClick)
.attr("fill", d => colorArray[state[d.y][d.x]])
.attr("stroke", outline);
function hexPoints({x,y}) {
if (shape === 'rectangle') {
const cx = (scale+margin*Math.sqrt(3))*x + scale/2+Math.sqrt(3)*margin;
const cy = (size+2*margin)*y + size + (x % 2)*(size/2 + SQRT3_2*margin);
return unitHex.map(([x,y]) => `${cx+r*x},${cy+r*y}`).join(' ');
}
const cx = hexagon ?
(size/2+margin)*y + (size+2*margin)*(x-(nx-1)/4) + size/2+margin :
(size/2+margin)*y + (size+2*margin)*x + size/2+2*margin;
const cy = (scale+margin*Math.sqrt(3))*y + scale;
return unitHex.map(([x,y]) => `${cx+r*x},${cy+r*y}`).join(' ');
}
function gridClick({x,y}) {
state[y][x] = (state[y][x] + 1) % numStates;
this.style.fill = colorArray[state[y][x]];
el.dispatchEvent(new CustomEvent('input'));
}
el.addEventListener('inputArray', ({detail}) => {
if (detail !== null && detail !== undefined) {
if (detail.length === ny &&
detail.every(d => d.length === nx && d.every(v => v < numStates && v >= 0 && v === Math.round(v)))) {
state = detail;
} else
return console.log(`failed inputArray event: ${detail}`);
} else {
state = init ? init.map(d => d.slice()) : Array.from({length:ny}, d => Array(nx).fill(0));
}
el.value = state;
grid.attr("fill", d => colorArray[state[d.y][d.x]]);
el.dispatchEvent(new CustomEvent('input'));
});
el.value = state;
return el;
}