class Graph { /** * Constructs a Graph object. * @param {Object} functionsToPlot - A key-value pair where the key is a player type and value is a string representation of a function. * @param {Object} colors - A key-value pair assigning a color to each player type. * @param {Number} consensus - A flag indicating consensus setting (1 = off, 0 = on). * @param {Array} graphDimensions - [max domain, max range], defaulting to [10, 100]. */ constructor(functionsToPlot, colors, consensus, graphDimensions = [10, 100]) { this.functions = functionsToPlot; this.colors = colors; this.currentLineX = 0; this.otherLineX = 0; this.graphDimensions = graphDimensions; this.currentFunctionValues = {}; // Fixed Y-axis range this.fixedMinY = -80; this.fixedMaxY = 80; this.layout = { xaxis: { title: { text: 'Quantity' } }, yaxis: { title: { text: 'Payoff per Player' }, range: [this.fixedMinY, this.fixedMaxY], fixedrange: true }, paper_bgcolor: "rgba(0,0,0,0)", plot_bgcolor: "rgba(0,0,0,0)", legend: { orientation: "h", yanchor: "top", y: 10.3, xanchor: "center", x: 0.5 } }; this.plotFunctions(); if (consensus !== 1) { this.updateOthersFunctionValues(); } } evaluateFunctionAt(fnString, x) { try { const node = math.parse(fnString); const code = node.compile(); return Math.round(code.evaluate({ x }) * 100) / 100; } catch (e) { console.error('Error evaluating function:', e); return 0; } } getFunctionTraces() { return Object.entries(this.functions).map(([playerType, func]) => { let trace = { x: [], y: [], mode: 'lines', name: `${playerType}`, line: { color: this.colors[playerType] } }; for (let i = 0; i <= this.graphDimensions[0]; i += 0.1) { trace.x.push(i); trace.y.push(this.evaluateFunctionAt(func, i)); } return trace; }); } getVerticalLineTrace(xValue, finalGraph) { const label = finalGraph ? 'Final Decision' : 'Your Proposal'; return { x: [xValue, xValue], y: [this.fixedMinY, this.fixedMaxY], mode: 'lines', name: label, line: { color: 'rgb(170, 51, 119)', width: 2, dash: 'solid' } }; } getConfirmVerticalLineTrace(xValue, moveOn = false) { let color = moveOn ? 'rgb(34, 136, 51)' : 'rgb(170, 51, 119)'; let dash = moveOn ? 'solid' : 'dashdot'; return { x: [xValue, xValue], y: [this.fixedMinY, this.fixedMaxY], mode: 'lines', name: 'Your Proposal', line: { color, width: 2, dash } }; } getConfirmOtherVerticalLineTrace() { return { x: [this.otherLineX, this.otherLineX], y: [this.fixedMinY, this.fixedMaxY], mode: 'lines', name: 'Other\'s Proposal', line: { color: 'rgb(34, 136, 51)', width: 2, dash: 'solid' } }; } getOtherVerticalLineTrace(moveOn = false, otherGray = false) { let color = otherGray ? 'gray' : 'rgb(34, 136, 51)'; let dash = moveOn && !otherGray ? 'solid' : 'dashdot'; return { x: [this.otherLineX, this.otherLineX], y: [this.fixedMinY, this.fixedMaxY], mode: 'lines', name: 'Other\'s Proposal', line: { color, width: 2, dash } }; } plotFunctions(finalGraph = false) { const traces = this.getFunctionTraces(); if (finalGraph) { this.updateFunctionValues(); } else { this.updateFunctionValues(true); } Plotly.newPlot('graph_to_show', [...traces, this.getVerticalLineTrace(this.currentLineX, finalGraph)], this.layout, { staticPlot: true }) .then(gd => { const bbox = gd.querySelector('.svg-container .plot').getBoundingClientRect(); const slider = document.getElementById('lineSlider'); slider.style.width = `${bbox.width}px`; slider.style.marginLeft = `${bbox.left - gd.getBoundingClientRect().left}px`; }); } updateFunctionValues(initialLoad = false) { let valuesHtml = `

Quantity ${this.currentLineX.toFixed(1)}

`; valuesHtml += `

Payoff per Player:

`; for (const [playerType, func] of Object.entries(this.functions)) { let value = this.evaluateFunctionAt(func, this.currentLineX); if (initialLoad) { valuesHtml += `

Type ${playerType} ...

`; } else { valuesHtml += `

Type ${playerType} ${value.toFixed(2)}

`; } } document.getElementById('functionValues').innerHTML = valuesHtml; } updateOthersFunctionValues(initialLoad = false) { let valuesHtml = `

Quantity ${this.otherLineX.toFixed(1)}

`; valuesHtml += `

Payoff per Player:

`; for (const [playerType, func] of Object.entries(this.functions)) { let value = this.evaluateFunctionAt(func, this.otherLineX); if (initialLoad) { valuesHtml += `

Type ${playerType} ...

`; } else { valuesHtml += `

Type ${playerType} ${value.toFixed(2)}

`; } } document.getElementById('othersFunctionValues').innerHTML = valuesHtml; } evaluatePlayerFunction(playerType) { return this.evaluateFunctionAt(this.functions[playerType], this.currentLineX).toFixed(2); } moveVerticalLine(value, moveOn = false, finalGraph = false) { this.currentLineX = value; const functionTraces = this.getFunctionTraces(); const verticalLineTrace = this.getVerticalLineTrace(this.currentLineX, finalGraph); const traces = this.otherLineX > 0 ? [...functionTraces, verticalLineTrace, this.getOtherVerticalLineTrace(moveOn)] : [...functionTraces, verticalLineTrace]; Plotly.react('graph_to_show', traces, this.layout); this.updateFunctionValues(); } confirmVerticalLine(moveOn = true) { const functionTraces = this.getFunctionTraces(); const verticalLineTrace = this.getConfirmVerticalLineTrace(this.currentLineX, moveOn); const traces = this.otherLineX > 0 ? [...functionTraces, verticalLineTrace, this.getOtherVerticalLineTrace()] : [...functionTraces, verticalLineTrace]; Plotly.react('graph_to_show', traces, this.layout); } confirmOtherVerticalLine(value) { this.otherLineX = value; const traces = [ ...this.getFunctionTraces(), this.getVerticalLineTrace(this.currentLineX), this.getConfirmOtherVerticalLineTrace() ]; Plotly.react('graph_to_show', traces, this.layout); } matchVerticalLine(value) { const slider = document.getElementById('lineSlider'); if (value !== this.otherLineX) { slider.value = this.otherLineX; this.moveVerticalLine(this.otherLineX); } else { slider.value = value; this.moveVerticalLine(value); } } moveOtherVerticalLine(value, moveOn = false) { this.otherLineX = value; const traces = [ ...this.getFunctionTraces(), this.getVerticalLineTrace(this.currentLineX, moveOn), this.getOtherVerticalLineTrace(moveOn) ]; Plotly.react('graph_to_show', traces, this.layout); this.updateOthersFunctionValues(); } moveOtherVerticalLineTwo(value, moveOn = false, otherGray = false) { this.otherLineX = value; const traces = [ ...this.getFunctionTraces(), this.getConfirmVerticalLineTrace(this.currentLineX, moveOn), this.getOtherVerticalLineTrace(moveOn, otherGray) ]; Plotly.react('graph_to_show', traces, this.layout); } }