diff --git a/src/components/Sidebar.js b/src/components/Sidebar.js index 1ba18ae8..883795e3 100644 --- a/src/components/Sidebar.js +++ b/src/components/Sidebar.js @@ -105,6 +105,7 @@ export default class Sidebar extends Component { gameCurrents, treePosition, + analysisType, showWinrateGraph, showGameGraph, showCommentBox, @@ -112,7 +113,8 @@ export default class Sidebar extends Component { graphGridSize, graphNodeSize, - winrateData + winrateData, + scoreLeadData }, {winrateGraphHeight, sidebarSplit} ) { @@ -139,7 +141,8 @@ export default class Sidebar extends Component { sideContent: h(WinrateGraph, { lastPlayer, width: winrateGraphWidth, - data: winrateData, + data: analysisType === 'winrate' ? winrateData : scoreLeadData, + analysisType, currentIndex: level, onCurrentIndexChange: this.handleWinrateGraphChange }), diff --git a/src/components/sidebars/WinrateGraph.js b/src/components/sidebars/WinrateGraph.js index a60a7e91..f49b9b22 100644 --- a/src/components/sidebars/WinrateGraph.js +++ b/src/components/sidebars/WinrateGraph.js @@ -7,11 +7,26 @@ import {noop} from '../../modules/helper.js' const t = i18n.context('WinrateGraph') const setting = remote.require('./setting') -const blunderThreshold = setting.get('view.winrategraph_blunderthreshold') +const blunderThresholdWinrate = setting.get( + 'view.winrategraph_blunderthreshold' +) +const blunderThresholdScoreLead = setting.get( + 'view.winrategraph_blunderthreshold_scorelead' +) + +const formatAnalysisValue = (value, analysisType) => { + if (analysisType === 'winrate') return `${i18n.formatNumber(value)}%` + return `${value >= 0 ? '+' : ''}${i18n.formatNumber(value)}` +} + +const transformAnalysisValue = (value, analysisType, dataMax) => { + if (analysisType === 'winrate') return value + return (value / Math.max(20, dataMax)) * 50 + 50 +} class WinrateStrip extends Component { render() { - let {player, winrate, change} = this.props + let {player, winrate, change, analysisType, blunderThreshold} = this.props return h( 'section', @@ -28,7 +43,7 @@ class WinrateStrip extends Component { h( 'span', {class: 'main'}, - winrate == null ? '–' : `${i18n.formatNumber(winrate)}%` + winrate == null ? '–' : formatAnalysisValue(winrate, analysisType) ), h( @@ -102,15 +117,24 @@ export default class WinrateGraph extends Component { } render() { - let {lastPlayer, width, currentIndex, data} = this.props + let {lastPlayer, width, currentIndex, data, analysisType} = this.props let {invert} = this.state + let blunderThreshold = + analysisType === 'winrate' + ? blunderThresholdWinrate + : blunderThresholdScoreLead + let metricString = analysisType === 'winrate' ? 'Winrate' : 'Score Lead' + let dataMax = Math.max(...data.map(x => (isFinite(x) ? Math.abs(x) : 0))) let dataDiff = data.map((x, i) => i === 0 || x == null || (data[i - 1] == null && data[i - 2] == null) ? null : x - data[data[i - 1] != null ? i - 1 : i - 2] ) - let dataDiffMax = Math.max(...dataDiff.map(Math.abs), 25) + let dataDiffMax = Math.max( + ...dataDiff.map(Math.abs), + analysisType === 'winrate' ? 25 : 10 + ) let round2 = x => Math.round(x * 100) / 100 let blackWinrate = @@ -118,7 +142,13 @@ export default class WinrateGraph extends Component { let blackWinrateDiff = dataDiff[currentIndex] == null ? null : round2(dataDiff[currentIndex]) let whiteWinrate = - data[currentIndex] == null ? null : round2(100 - data[currentIndex]) + data[currentIndex] == null + ? null + : round2( + analysisType === 'winrate' + ? 100 - data[currentIndex] + : -data[currentIndex] + ) let whiteWinrateDiff = dataDiff[currentIndex] == null ? null : -round2(dataDiff[currentIndex]) @@ -132,8 +162,10 @@ export default class WinrateGraph extends Component { .map( ([winrate, diff], i) => `${ - i === 0 ? t('Black Winrate:') : t('White Winrate:') - } ${i18n.formatNumber(winrate)}%${ + i === 0 + ? t(`Black ${metricString}:`) + : t(`White ${metricString}:`) + } ${formatAnalysisValue(winrate, analysisType)}${ diff == null ? '' : ` (${diff >= 0 ? '+' : '-'}${i18n.formatNumber( @@ -156,7 +188,9 @@ export default class WinrateGraph extends Component { h(WinrateStrip, { player: lastPlayer, winrate: lastPlayer > 0 ? blackWinrate : whiteWinrate, - change: lastPlayer > 0 ? blackWinrateDiff : whiteWinrateDiff + change: lastPlayer > 0 ? blackWinrateDiff : whiteWinrateDiff, + analysisType, + blunderThreshold }), h( @@ -215,7 +249,10 @@ export default class WinrateGraph extends Component { let instructions = data .map((x, i) => { if (x == null) return i === 0 ? [i, 50] : null - return [i, x] + return [ + i, + transformAnalysisValue(x, analysisType, dataMax) + ] }) .filter(x => x != null) @@ -309,7 +346,11 @@ export default class WinrateGraph extends Component { if (x == null) return '' let command = i === 0 || data[i - 1] == null ? 'M' : 'L' - return `${command} ${i},${x}` + return `${command} ${i},${transformAnalysisValue( + x, + analysisType, + dataMax + )}` }) .join(' ') }), @@ -326,9 +367,18 @@ export default class WinrateGraph extends Component { if (i === 0) return 'M 0,50' if (x == null && data[i - 1] != null) - return `M ${i - 1},${data[i - 1]}` - - if (x != null && data[i - 1] == null) return `L ${i},${x}` + return `M ${i - 1},${transformAnalysisValue( + data[i - 1], + analysisType, + dataMax + )}` + + if (x != null && data[i - 1] == null) + return `L ${i},${transformAnalysisValue( + x, + analysisType, + dataMax + )}` return '' }) @@ -343,7 +393,20 @@ export default class WinrateGraph extends Component { class: 'marker', style: { left: `${(currentIndex * 100) / width}%`, - top: `${!invert ? data[currentIndex] : 100 - data[currentIndex]}%` + top: `${ + !invert + ? transformAnalysisValue( + data[currentIndex], + analysisType, + dataMax + ) + : 100 - + transformAnalysisValue( + data[currentIndex], + analysisType, + dataMax + ) + }%` } }) ) diff --git a/src/menu.js b/src/menu.js index 8995180d..1f78cbfe 100644 --- a/src/menu.js +++ b/src/menu.js @@ -562,6 +562,39 @@ exports.get = function(props = {}) { click: () => sabaki.setState(({fullScreen}) => ({fullScreen: !fullScreen})) }, + { + label: i18n.t('menu.view', 'Analysis Metric'), + submenu: [ + { + label: i18n.t('menu.view', '&Win Rate'), + type: 'checkbox', + checked: analysisType === 'winrate', + accelerator: 'CmdOrCtrl+Shift+H', + click: () => { + setting.set( + 'board.analysis_type', + setting.get('board.analysis_type') === 'winrate' + ? 'scoreLead' + : 'winrate' + ) + } + }, + { + label: i18n.t('menu.view', '&Score Lead'), + type: 'checkbox', + checked: analysisType === 'scoreLead', + accelerator: 'CmdOrCtrl+Shift+H', + click: () => { + setting.set( + 'board.analysis_type', + setting.get('board.analysis_type') === 'scoreLead' + ? 'winrate' + : 'scoreLead' + ) + } + } + ] + }, {type: 'separator'}, { label: i18n.t('menu.view', 'Show &Coordinates'), @@ -629,50 +662,14 @@ exports.get = function(props = {}) { }, { label: i18n.t('menu.view', 'Show &Heatmap'), - submenu: [ - { - label: i18n.t('menu.view', '&Don’t Show'), - type: 'checkbox', - checked: !showAnalysis, - accelerator: 'CmdOrCtrl+H', - click: () => toggleSetting('board.show_analysis') - }, - {type: 'separator'}, - { - label: i18n.t('menu.view', 'Show &Win Rate'), - type: 'checkbox', - checked: !!showAnalysis && analysisType === 'winrate', - accelerator: 'CmdOrCtrl+Shift+H', - click: () => { - setting.set('board.show_analysis', true) - setting.set( - 'board.analysis_type', - setting.get('board.analysis_type') === 'winrate' - ? 'scoreLead' - : 'winrate' - ) - } - }, - { - label: i18n.t('menu.view', 'Show &Score Lead'), - type: 'checkbox', - checked: !!showAnalysis && analysisType === 'scoreLead', - accelerator: 'CmdOrCtrl+Shift+H', - click: () => { - setting.set('board.show_analysis', true) - setting.set( - 'board.analysis_type', - setting.get('board.analysis_type') === 'scoreLead' - ? 'winrate' - : 'scoreLead' - ) - } - } - ] + type: 'checkbox', + checked: !!showAnalysis, + accelerator: 'CmdOrCtrl+H', + click: () => toggleSetting('board.show_analysis') }, {type: 'separator'}, { - label: i18n.t('menu.view', 'Show &Winrate Graph'), + label: i18n.t('menu.view', 'Show &Analysis Graph'), type: 'checkbox', checked: !!showWinrateGraph, enabled: !!showGameGraph || !!showCommentBox, diff --git a/src/modules/enginesyncer.js b/src/modules/enginesyncer.js index 6a7dd185..3f88a66d 100644 --- a/src/modules/enginesyncer.js +++ b/src/modules/enginesyncer.js @@ -145,7 +145,12 @@ export default class EngineSyncer extends EventEmitter { this.analysis = { sign, variations, - winrate: Math.max(...variations.map(({winrate}) => winrate)) + winrate: Math.max(...variations.map(({winrate}) => winrate)), + scoreLead: Math.max( + ...variations.map(({scoreLead}) => + scoreLead == null ? NaN : scoreLead + ) + ) } } else if (line.startsWith('play ')) { sign = -sign diff --git a/src/modules/sabaki.js b/src/modules/sabaki.js index fa034bdf..9b3390b8 100644 --- a/src/modules/sabaki.js +++ b/src/modules/sabaki.js @@ -196,7 +196,12 @@ class Sabaki extends EventEmitter { get winrateData() { return [ ...this.gameTree.listCurrentNodes(state.gameCurrents[state.gameIndex]) - ].map(x => x.data.SBKV && x.data.SBKV[0]) + ].map(x => x.data.SBKV && +x.data.SBKV[0]) + }, + get scoreLeadData() { + return [ + ...this.gameTree.listCurrentNodes(state.gameCurrents[state.gameIndex]) + ].map(x => x.data.SBKS && +x.data.SBKS[0]) } } } @@ -882,14 +887,18 @@ class Sabaki extends EventEmitter { Math.round( (sign > 0 ? variation.winrate : 100 - variation.winrate) * 100 ) / 100 - + let startNodeProperties = { + [annotationProp]: [annotationValues[annotationProp]], + SBKV: [winrate.toString()] + } + if (variation.scoreLead != null) { + let scoreLead = Math.round(sign * variation.scoreLead * 100) / 100 + startNodeProperties.SBKS = [scoreLead.toString()] + } this.openVariationMenu(sign, variation.moves, { x, y, - startNodeProperties: { - [annotationProp]: [annotationValues[annotationProp]], - SBKV: [winrate.toString()] - } + startNodeProperties }) } } @@ -1691,13 +1700,21 @@ class Sabaki extends EventEmitter { if (syncer.analysis != null && syncer.treePosition != null) { let tree = this.state.gameTrees[this.state.gameIndex] - let {sign, winrate} = syncer.analysis - if (sign < 0) winrate = 100 - winrate + let {sign, winrate, scoreLead} = syncer.analysis + if (sign < 0) { + winrate = 100 - winrate + scoreLead = -scoreLead + } let newTree = tree.mutate(draft => { draft.updateProperty(syncer.treePosition, 'SBKV', [ (Math.round(winrate * 100) / 100).toString() ]) + if (isFinite(scoreLead)) { + draft.updateProperty(syncer.treePosition, 'SBKS', [ + (Math.round(scoreLead * 100) / 100).toString() + ]) + } }) this.setCurrentTreePosition(newTree, this.state.treePosition) diff --git a/src/setting.js b/src/setting.js index 282448d3..a4e1991d 100644 --- a/src/setting.js +++ b/src/setting.js @@ -205,6 +205,7 @@ let defaults = { 'view.sidebar_width': 200, 'view.sidebar_minwidth': 100, 'view.winrategraph_blunderthreshold': 5, + 'view.winrategraph_blunderthreshold_scorelead': 2, 'view.winrategraph_height': 90, 'view.winrategraph_minheight': 30, 'view.winrategraph_maxheight': 250,