Published
Edited
Sep 20, 2022
1 fork
Importers
21 stars
Insert cell
Insert cell
Insert cell
chessboard()
Insert cell
Insert cell
Insert cell
chessboard('1K1k4/1P6/8/8/8/8/r7/2R5')
Insert cell
Insert cell
chessboard('start', { size: 150 })
Insert cell
Insert cell
chessboard('start', { theme: {
lightSquareFill: '#FFE',
darkSquareFill: '#263',
borderStroke: '#263',
labelColor: '#263',
}})
Insert cell
Insert cell
chessboard('start', { call: board => board.querySelector('.square-d1').setAttribute('fill', '#D00') })
Insert cell
Insert cell
chessboard('8/8/2r5/8/8/8/8/8', {
call: (board, state) => {
d3.select(board)
.append('g')
.selectAll('g')
.data([
['a6', 'b6'],
['c8', 'c7'],
['c1', 'c5'],
['h6', 'd6'],
])
.join('line')
.attr('x1', d => state.squareCentroid(d[0])[0])
.attr('y1', d => state.squareCentroid(d[0])[1])
.attr('x2', d => state.squareCentroid(d[1])[0])
.attr('y2', d => state.squareCentroid(d[1])[1])
.attr('stroke', 'hsla(200, 80%, 30%, 50%)')
.attr('stroke-width', '6')
.attr('stroke-linecap', 'round')
}
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
querySquare('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', 'd1')
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
positionModifier = fn => {
return (fenString, ...args) => {
const [position, ...fields] = fenString.split(' ')
const newPosition = fn(position, ...args)
return [newPosition, ...fields].join(' ')
}
}
Insert cell
Insert cell
squareToCoords = (square) => {
const [file, rank] = Array.from(square)
return [RANKS.indexOf(rank), FILES.indexOf(file)]
}
Insert cell
coordsToSquare = ([row, col]) => FILES[col] + RANKS[row]
Insert cell
Insert cell
// TODO: Add validation
rowToArray = (rowString) => Array.from(rowString).reduce((acc, char) => {
// If character is a number, add that many blank squares
if (RegExp(/[1-8]/).test(char)) return acc.concat(new Array(+char).fill(null))
// Otherwise, assume character is a piece marker and add it on its own
return acc.concat([char])
}, [])
Insert cell
// TODO: Add validation
fenToArray = (fenString) => fenString.split(' ')[0].split('/').map(rowToArray)
Insert cell
arrayToRow = (arr) => {
if (arr.length !== FILES.length) {
throw `Row array must have exactly ${FILES.length} entries, representing eight squares in a chessboard row`
}
let counter = 0
let emptySquareCounter = 0
let rowString = ''
while (counter < 8) {
// If the character is a chess piece, first add any built up empty square
// as a number, then add the piece to the string, then reset the empty square counter,
// then increment the counter
if (Array.from(PIECE_MAP.keys()).includes(arr[counter])) {
if (emptySquareCounter !== 0) rowString += emptySquareCounter
rowString += arr[counter]
emptySquareCounter = 0
counter++
// If the character is null, incement the empty square counter, then increment the counter
} else if (arr[counter] === null) {
emptySquareCounter++
counter++
// If the character is neither null nor a valid piece, throw an error
} else {
throw `${arr[counter]} is an invalid value for a chessboard square`
}
}
// Finally, flush the remaining empty squares if non-zero
if (emptySquareCounter !== 0) rowString += emptySquareCounter
return rowString
}
Insert cell
arrayToPosition = (arr) => {
if (arr.length !== RANKS.length) {
throw `Board array must have exactly ${RANKS.length} entries, representing eight rows in a chessboard`
}

return arr.map(arrayToRow).join('/')
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
Insert cell
Insert cell
chessboard = (fenString = 'start', opts) => {
const {
size: maxSize,
responsive,
call,
theme,
} = Object.assign({
size: 440,
responsive: true,
call: () => {},
theme: {
lightSquareFill: '#FFF',
darkSquareFill: '#EEE',
borderStroke: '#CCC',
labelColor: '#CCC',
}
}, opts)
const size = responsive ? Math.min(maxSize, width) : maxSize
const marginSize = 20
const innerSize = size - marginSize * 2
const squareSize = innerSize / 8

const coords = fenToArray(fenString === 'start' ? START_FEN : fenString)

const output = html`
<svg class="chessboard" viewBox="0 0 ${size} ${size}" width=${size} height=${size}>
<defs>
${pieceSVG}
</defs>
<g class="labels file-labels">
${FILES.map((label, i) => svg`
<text
class="label file-label file-label-top"
x=${marginSize + i * squareSize + squareSize / 2}
y="12"
text-anchor="middle"
font-family="var(--sans-serif)"
font-size="14"
fill=${theme.labelColor}
>${label}</text>
<text
class="label file-label file-label-bottom"
x=${marginSize + i * squareSize + squareSize / 2}
y=${marginSize + innerSize + 16}
text-anchor="middle"
font-family="var(--sans-serif)"
font-size="14"
fill=${theme.labelColor}
>${label}</text>
`)}
</g>
<g class="labels rank-labels">
${RANKS.map((label, i) => svg`
labelText(label, 0, marginSize + i * squareSize + squareSize / 2 + 7, theme)
<text
class="label rank-label rank-label-left"
x="0"
y=${marginSize + i * squareSize + squareSize / 2 + 7}
text-anchor="right"
font-family="var(--sans-serif)"
font-size="14"
fill=${theme.labelColor}
>${label}</text>
<text
class="label rank-label rank-label-right"
x=${marginSize + innerSize + 12}
y=${marginSize + i * squareSize + squareSize / 2 + 7}
text-anchor="left"
font-family="var(--sans-serif)"
font-size="14"
fill=${theme.labelColor}
>${label}</text>
`)}
</g>
<g class="squares" transform="translate(${marginSize} ${marginSize})">
${coords.map(
(row, i) => svg`${row.map(
(square, j) => svg`
<rect
class="square ${(i + j) % 2 === 0 ? 'light-square' : 'dark-square'} square-${coordsToSquare([i, j])}"
x=${j * squareSize}
y=${i * squareSize}
width=${squareSize}
height=${squareSize}
fill=${(i + j) % 2 === 0 ? theme.lightSquareFill : theme.darkSquareFill}
/>
`
)}`
)}
</g>
<g class="pieces" transform="translate(${marginSize} ${marginSize})">
${coords.map(
(row, i) => svg`${
row.map(
(square, j) => square ? svg`<use
class="piece piece-${PIECE_MAP.get(square)}"
href="${new URL(`#${PIECE_MAP.get(square)}`, location)}"
x=${j * squareSize}
y=${i * squareSize}
width=${squareSize}
height=${squareSize}
/>` : null
).filter(d => d)
}`
)}
</g>
<rect
class="board-outline"
x=${marginSize}
y=${marginSize}
width=${innerSize}
height=${innerSize}
stroke=${theme.borderStroke}
fill="none"
/>
</svg>
`
// Call the passed in modifier function with helpful utilities
call(output, {
fenString,
squareSize,
squareCoords: (square) => squareToCoords(square).map(n => n * squareSize + marginSize),
squareCentroid: (square) => {
return squareToCoords(square).map(n => n * squareSize + 0.5 * squareSize + marginSize)
},
})
return output
}
Insert cell
Insert cell
Insert cell
pieceSVG = FileAttachment("chesspieces.svg").text()
Insert cell
Insert cell
d3 = require('d3')
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