diff --git a/assets/css/application.css b/assets/css/application.css
index bea3d893..ca355683 100644
--- a/assets/css/application.css
+++ b/assets/css/application.css
@@ -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;
diff --git a/play.html b/play.html
index 8f3b7050..6fdcd190 100644
--- a/play.html
+++ b/play.html
@@ -378,7 +378,13 @@
-
+
+
+ ×
+
+
+
+
×
@@ -417,6 +423,15 @@
+
+
+
+
diff --git a/src/history.ts b/src/history.ts
index 4f068436..759e55bc 100644
--- a/src/history.ts
+++ b/src/history.ts
@@ -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;
@@ -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;
diff --git a/src/index.ts b/src/index.ts
index 22585c77..8ef6da7f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3001,6 +3001,7 @@ function hideAnalysis() {
stopEngine();
closeLeftBottomTab($('#engine-tab'));
closeLeftBottomTab($('#eval-graph-tab'));
+ closeLeftBottomTab($('#move-times-tab'));
showAnalyzeButton();
}
@@ -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(' ');
@@ -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');
@@ -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();
});