Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add move times tab to analysis #88

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions assets/css/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,18 @@ html, body {
padding: 20px;
}

.move-times-white {
fill: whitesmoke;
stroke-width: 1;
stroke: white
}

.move-times-black {
fill: dimgrey;
stroke-width: 1;
stroke: black;
}

#promotion-panel {
display: none;
position: absolute;
Expand Down
17 changes: 16 additions & 1 deletion play.html
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,13 @@ <h5 class="modal-title" id="play-computer-modal-label"><span class="fa-solid fa-
</container>
</li>
<li class="nav-item" role="presentation" style="position: relative; display: none">
<button class="nav-link" id="eval-graph-tab" style="padding-right: 30px" data-bs-toggle="tab" href="#eval-graph-panel" role="tab" aria-controls="eval-graph-panel" aria-selected="false">Eval Graph</button>
<button class="nav-link" id="eval-graph-tab" style="padding-right: 30px" data-bs-toggle="tab" href="#eval-graph-panel" role="tab" aria-controls="eval-graph-panel" aria-selected="false">Eval</button>
<container class="h-100 d-flex" style="position: absolute; top: 0; right: 12px; z-index: 10">
<span class="closeTab align-self-center btn btn-default btn-sm">×</span>
</container>
</li>
<li class="nav-item" role="presentation" style="position: relative; display: none">
<button class="nav-link" id="move-times-tab" style="padding-right: 30px" data-bs-toggle="tab" href="#move-times-panel" role="tab" aria-controls="move-times-panel" aria-selected="false">Moves</button>
<container class="h-100 d-flex" style="position: absolute; top: 0; right: 12px; z-index: 10">
<span class="closeTab align-self-center btn btn-default btn-sm">×</span>
</container>
Expand Down Expand Up @@ -417,6 +423,15 @@ <h5 class="modal-title" id="play-computer-modal-label"><span class="fa-solid fa-
</div>
</div>
</div>
<div class="tab-pane fade h-100 w-100" id="move-times-panel" role="tabpanel" aria-labelledby="move-times-panel">
<div id="move-times-container" class="h-100 w-100" style="display: none">
</div>
<div id="move-times" class="center h-100 w-100" style="display: none">
<div class="progress h-100 w-100">
<div class="progress-bar" role="progressbar" aria-label="Loading Move Times" style="width: 100%;" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">100%</div>
</div>
</div>
</div>
</div>
</div>
</div>
Expand Down
262 changes: 260 additions & 2 deletions src/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// Use of this source code is governed by a GPL-style
// license that can be found in the LICENSE file.

import { updateBoard } from './index';
import { gotoMove, updateBoard } from './index';
import * as d3 from 'd3';

export class History {
private board: any;
Expand Down Expand Up @@ -468,7 +469,264 @@ export class History {
}

return false;
}
}

public showMoveTimes() {
if(this.length() === 0 || !$('#move-times-panel').is(':visible'))
return;

$('#move-times-container').html('');
this.drawGraph();
}

private drawGraph() {
const dataset = [];
let currIndex;
const that = this;

for(let i = 0, hIndex = 0; hIndex !== undefined; i++) {
if(hIndex === this.index())
currIndex = i;

const move = this.get(hIndex);
let y1 = move.wtime/1000;
let y2 = -move.btime/1000;
if (i > 0)
dataset.push({y1: y1, y1diff: dataset[i-1].y1 - y1, y2: y2, y2diff: dataset[i-1].y2 - y2});
else
dataset.push({y1: y1, y1diff: 0, y2: y2, y2diff: 0});
hIndex = this.next(hIndex);
}
console.log(dataset);

const container = $('#move-times-container');
container.show();

const margin = {top: 6, right: 6, bottom: 6, left: 24}
; const width = container.width() - margin.left - margin.right // Use the window's width
; const height = container.height() - margin.top - margin.bottom; // Use the window's height

// Prepare data set
const n = dataset.length;

// Define x and y scales
const xScale = d3.scaleLinear()
.domain([0, n-1]) // input
.range([0, width]); // output

const yScale = d3.scaleLinear()
.domain([d3.min(dataset, d => Math.min(d.y1, d.y2)), d3.max(dataset, d => Math.max(d.y1, d.y2))]) // input
.range([height, 0]); // output

const y2Scale = d3.scaleLinear()
.domain([d3.min(dataset, d => Math.min(d.y1diff, d.y2diff)), d3.max(dataset, d => Math.max(d.y1diff, d.y2diff))])
.range([height, 0]);

// Line generator
const line1 = d3.line()
.x(function(d, i) { return xScale(i); })
.y(function(d) { return yScale(d.y1); });

const line2 = d3.line()
.x(function(d, i) { return xScale(i); })
.y(function(d) { return yScale(d.y2); });

// Add SVG to panel
const svg = d3.select(container[0]).append('svg')
.attr('width', '100%')
.attr('height', '100%')
.style('cursor', 'pointer')
.on('mousemove', function() {
const mousePosition = d3.pointer(event);
const xPos = mousePosition[0] - margin.left;
const yPos = mousePosition[1] - margin.top;
const getDistanceFromPos = (d) => Math.abs(d - xScale.invert(xPos));
const closestIndex = d3.scan(
d3.range(n),
(a, b) => getDistanceFromPos(a) - getDistanceFromPos(b)
);

hoverLine
.attr('x', xScale(closestIndex))
.style('opacity', 1);

const oldIndex1 = Math.round(xScale.invert($('#hover-circle1').attr('cx')));
const oldIndex2 = Math.round(xScale.invert($('#hover-circle2').attr('cx')));

hoverCircle1
.attr('cx', xScale(closestIndex))
.attr('cy', yScale(dataset[closestIndex].y1))
.attr('title', dataset[closestIndex].y1)
.attr('data-bs-original-title', dataset[closestIndex].y1)
.style('opacity', 1);

hoverCircle2
.attr('cx', xScale(closestIndex))
.attr('cy', yScale(dataset[closestIndex].y2))
.attr('title', dataset[closestIndex].y2)
.attr('data-bs-original-title', dataset[closestIndex].y2)
.style('opacity', 1);

if(oldIndex1 !== closestIndex) {
$('#hover-circle1')
.tooltip('dispose')
.tooltip({
container: '#move-times-container',
placement: 'auto',
trigger: 'manual'
});
$('#hover-circle1').tooltip('show');
$('.tooltip').css('pointer-events', 'none');
}

if(oldIndex2 !== closestIndex) {
$('#hover-circle2')
.tooltip('dispose')
.tooltip({
container: '#move-times-container',
placement: 'auto',
trigger: 'manual'
});
$('#hover-circle2').tooltip('show');
$('.tooltip').css('pointer-events', 'none');
}
})
.on('mouseleave', function() {
hoverLine.style('opacity', 0);
hoverCircle1.style('opacity', 0)
.attr('cx', -1);
hoverCircle2.style('opacity', 0)
.attr('cx', -1);
$('#hover-circle1').tooltip('dispose');
$('#hover-circle2').tooltip('dispose');
})
.on('click', function(event) {
const mousePosition = d3.pointer(event);
const xPos = mousePosition[0] - margin.left;
const getDistanceFromPos = (d) => Math.abs(d - xScale.invert(xPos));
const closestIndex = d3.scan(
d3.range(n),
(a, b) => getDistanceFromPos(a) - getDistanceFromPos(b)
);

let historyIndex = 0;
for(let i = 0; i < closestIndex; i++)
historyIndex = that.next(historyIndex);
gotoMove(historyIndex);

if(historyIndex) {
selectCircle1
.attr('cx', xScale(closestIndex))
.attr('cy', yScale(dataset[closestIndex].y1))
.style('opacity', 1);
selectCircle2
.attr('cx', xScale(closestIndex))
.attr('cy', yScale(dataset[closestIndex].y2))
.style('opacity', 1);
}
else {
selectCircle1.style('opacity', 0);
selectCircle2.style('opacity', 0);
}
})
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

// Append bars for y1Diff values
svg.selectAll(".bar1")
.data(dataset)
.enter().append("rect")
.attr("class", "move-times-white")
.attr("x", (d, i) => xScale(i))
.attr("y", d => d.y1diff > 0 ? y2Scale(d.y1diff) : y2Scale(0)) // Adjust for negative values
.attr("width", width / ((n-1)/2))
.attr("height", d => Math.abs(y2Scale(d.y1diff) - y2Scale(0)));

// Append bars for y2Diff values
svg.selectAll(".bar2")
.data(dataset)
.enter().append("rect")
.attr("class", "move-times-black")
.attr("x", (d, i) => xScale(i))
.attr("y", d => d.y2diff > 0 ? y2Scale(d.y2diff) : y2Scale(0)) // Adjust for negative values
.attr("width", width / ((n-1)/2))
.attr("height", d => Math.abs(y2Scale(d.y2diff) - y2Scale(0)));

// Render y-axis
const yAxis = svg.append('g')
.attr('class', 'noselect')
.call(d3.axisLeft(yScale)); // Create an axis component with d3.axisLeft
yAxis.select('.domain').remove();

svg.append('path')
.datum(dataset)
.attr('class', 'eval-line-above')
.attr('d', line1);

svg.append('path')
.datum(dataset)
.attr('class', 'eval-line-below')
.attr('d', line2);

const hoverLine = svg.append('g')
.append('rect')
.attr('stroke-width', '1px')
.attr('width', '.5px')
.attr('height', height)
.style('opacity', 0);

const hoverCircle1 = svg.append('g')
.append('circle')
.attr('id', 'hover-circle1')
.attr('class', 'eval-circle')
.attr('r', 3)
.style('opacity', 0);

const hoverCircle2 = svg.append('g')
.append('circle')
.attr('id', 'hover-circle2')
.attr('class', 'eval-circle')
.attr('r', 3)
.style('opacity', 0);

const selectCircle1 = svg.append('g')
.append('circle')
.attr('class', 'eval-circle')
.attr('id', 'select-circle1')
.attr('r', 4)
.style('opacity', 0);

const selectCircle2 = svg.append('g')
.append('circle')
.attr('class', 'eval-circle')
.attr('id', 'select-circle2')
.attr('r', 4)
.style('opacity', 0);

const currMoveCircle1 = $('#select-circle1');
if(currMoveCircle1) {
if(currIndex)
currMoveCircle1
.attr('cx', xScale(currIndex))
.attr('cy', yScale(dataset[currIndex].y1))
.css('opacity', 1);
else
currMoveCircle1
.css('opacity', 0);
}
const currMoveCircle2 = $('#select-circle2');
if(currMoveCircle2) {
if(currIndex)
currMoveCircle2
.attr('cx', xScale(currIndex))
.attr('cy', yScale(dataset[currIndex].y2))
.css('opacity', 1);
else
currMoveCircle2
.css('opacity', 0);
}
}

}

export default History;
23 changes: 8 additions & 15 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3001,6 +3001,7 @@ function hideAnalysis() {
stopEngine();
closeLeftBottomTab($('#engine-tab'));
closeLeftBottomTab($('#eval-graph-tab'));
closeLeftBottomTab($('#move-times-tab'));
showAnalyzeButton();
}

Expand All @@ -3019,6 +3020,7 @@ function showAnalyzeButton() {
function showAnalysis() {
openLeftBottomTab($('#engine-tab'));
openLeftBottomTab($('#eval-graph-tab'));
openLeftBottomTab($('#move-times-tab'));
$('#engine-pvs').empty();
for(let i = 0; i < numPVs; i++)
$('#engine-pvs').append('<li>&nbsp;</li>');
Expand All @@ -3040,20 +3042,6 @@ function openLeftBottomTab(tab: any) {
tab.tab('show');
}

function getMoves() {
let moves = '';
const history = game.chess.history({verbose: true});
for (let i = 0; i < history.length; ++i) {
const move = history[i];
moves += ' ' + move.from + move.to + (move.promotion ? move.promotion : '');
}
return moves;
}

function getMoveNoFromFEN(fen: string) {
return +fen.split(/\s+/).pop();
}

$('#collapse-history').on('hidden.bs.collapse', (event) => {
$('#history-toggle-icon').removeClass('fa-toggle-up').addClass('fa-toggle-down');

Expand Down Expand Up @@ -4288,9 +4276,14 @@ $(document).on('shown.bs.tab', 'button[href="#eval-graph-panel"]', (e) => {
evalEngine.redraw();
});

$(document).on('shown.bs.tab', 'button[href="#move-times-panel"]', (e) => {
if(game.history)
game.history.showMoveTimes();
});

$('#left-bottom-tabs .closeTab').on('click', (event) => {
var id = $(event.target).parent().siblings('.nav-link').attr('id');
if(id === 'engine-tab' || id === 'eval-graph-tab')
if(id === 'engine-tab' || id === 'eval-graph-tab' || id === 'move-times-tab')
hideAnalysis();
});

Expand Down