/** Graph.js * This file contains a Graph class definition used to represent and plot * mathematical functions on a graph, specifically designed for the * bargaining pages. It uses the Plotly library to render the graph. */ 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 {Array} graphDimensions - An array of two elements representing the max domain and max range of the graph. Defaults to [10,100]. */ constructor(functionsToPlot, colors, consensus, graphDimensions=[10,100]) { this.functions = functionsToPlot; // Default function this.colors = colors this.currentLineX = 0; // Default position for vertical line this.otherLineX = 0; this.graphDimensions = graphDimensions // index 0 is max domain, index 1 is max range (both start at 0) this.maxY = 0 this.minY = 0 this.currentFunctionValues = {} this.layout = { xaxis: { title: { text: 'Quantity' } }, yaxis: { title: { text: 'Payoff' }, range: [-102, 102], // <-- Fix the y-axis range here fixedrange: true // Optional: disables zoom/pan on y-axis}, }, 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 the indicator for consensus is on, then update other's proposal if(consensus != 1){ this.updateOthersFunctionValues(); }; }; /** * Evaluates a mathematical function at a given x-value. * @param {string} fnString - The string representation of the function. * @param {number} x - The x-value at which to evaluate the function. * @returns {number} The evaluated value of the function at x. */ evaluateFunctionAt(fnString, x) { try { // Parse and compile the math expression const node = math.parse(fnString); const code = node.compile(); // Evaluate at given x const result = code.evaluate({ x }); // Round to 2 decimal places and return return Math.round(result * 100) / 100; } catch (e) { console.error('Error evaluating function:', e); return 0; }; }; /** * Generates traces for each function to be plotted using Plotly. * @returns {Array} An array of trace objects for Plotly. */ getFunctionTraces() { console.log("Function Traces"); 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); let curY = this.evaluateFunctionAt(func, i); if (curY > this.maxY) { this.maxY = curY; }; if (curY < this.minY) { this.minY = curY; }; trace.y.push(this.evaluateFunctionAt(func, i)); }; console.log("Minimum Y", this.minY); return trace; }); }; /** * Creates a vertical line trace at a specified x-value. * @param {number} xValue - The x-value where the vertical line should be placed. * @returns {Object} A trace object for a vertical line. */ getVerticalLineTrace(xValue, finalGraph) { let graphText = 'Your Proposal'; if(finalGraph){ graphText = 'Final Decision'; }; console.log("Minimum Y", this.minY); return { x: [xValue, xValue], y: [this.minY, this.maxY], mode: 'lines', name: graphText, line: { color: 'rgb(170, 51, 119)', width: 2, dash: 'solid'} }; }; getConfirmVerticalLineTrace(xValue,moveOn = false) { let color = 'rgb(170, 51, 119)'; let dash = 'dashdot' if(moveOn){ color = 'rgb(34, 136, 51)'; dash = 'solid' }; console.log("Minimum Y", this.minY); return { x: [xValue, xValue], y: [this.minY, this.maxY], mode: 'lines', name: 'Your Proposal', line: { color: color, width: 2, dash: dash} }; }; getConfirmOtherVerticalLineTrace() { console.log("Minimum Y", this.minY); return { x: [this.otherLineX, this.otherLineX], y: [this.minY, this.maxY], mode: 'lines', name: 'Other\'s Proposal', line: { color: 'rgb(34, 136, 51)', width: 2, dash: 'solid'} }; }; /** * Creates a vertical line trace at the position of `this.otherLineX`. * @returns {Object} A trace object for the vertical line belonging to the other participant. */ getOtherVerticalLineTrace(moveOn = false,otherGray = false) { let color = 'gray'; let dash = 'dashdot' if(moveOn && !otherGray){ color = 'rgb(34, 136, 51)'; dash = 'solid' } return { x: [this.otherLineX, this.otherLineX], y: [this.minY, this.maxY], mode: 'lines', name: 'Other\'s Proposal', line: { color: color, width: 2, dash: dash} }; } /** * Plots the functions using Plotly. */ plotFunctions(finalGraph = false) { console.log("Here"); 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 plotDiv = gd.querySelector('.svg-container .plot'); const bbox = plotDiv.getBoundingClientRect(); const plotWidth = bbox.width; const slider = document.getElementById('lineSlider'); slider.style.width = `${plotWidth}px`; slider.style.marginLeft = `${bbox.left - gd.getBoundingClientRect().left}px`; }); } /** * Updates the displayed function values corresponding to the current position of the vertical line. * Requires a div with id='functionValues' on the page to function. */ updateFunctionValues(initialLoad = false) { let valuesHtml = `

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

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

Type ${playerType} ...

`; } else{ valuesHtml += `

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

`; } } document.getElementById('functionValues').innerHTML = valuesHtml; } evaluatePlayerFunction(playerType) { let value = this.evaluateFunctionAt(this.functions[playerType], this.currentLineX); // console.log(value); return value.toFixed(2); // Returning the value rounded to 1 decimal place } /** * Updates the displayed function values corresponding to the position of the other vertical line. * Requires a div with id='othersFunctionValues' on the page to function */ updateOthersFunctionValues(initialLoad = false) { let valuesHtml = `

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

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

Type ${playerType} ...

`; } else{ valuesHtml += `

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

`; } } document.getElementById('othersFunctionValues').innerHTML = valuesHtml; } /** * Moves the main vertical line to a new position and updates the graph. * @param {number} value - The new position for the vertical line. */ moveVerticalLine(value,moveOn = false, finalGraph = false) { // console.log("Here"); this.currentLineX = value; const functionTraces = this.getFunctionTraces(); const verticalLineTrace = this.getVerticalLineTrace(this.currentLineX, finalGraph); if (this.otherLineX <= 0) Plotly.react('graph_to_show', [...functionTraces, verticalLineTrace], this.layout); else { const otherLineTrace = this.getOtherVerticalLineTrace(moveOn); Plotly.react('graph_to_show', [...functionTraces, verticalLineTrace, otherLineTrace], this.layout); } this.updateFunctionValues() } confirmVerticalLine(moveOn = true) { const functionTraces = this.getFunctionTraces(); const verticalLineTrace = this.getConfirmVerticalLineTrace(this.currentLineX,moveOn); if (this.otherLineX <= 0) Plotly.react('graph_to_show', [...functionTraces, verticalLineTrace], this.layout); else { const otherLineTrace = this.getOtherVerticalLineTrace(); Plotly.react('graph_to_show', [...functionTraces, verticalLineTrace, otherLineTrace], this.layout); } // this.updateFunctionValues() } confirmOtherVerticalLine(value) { this.otherLineX = value; const functionTraces = this.getFunctionTraces(); const otherLineTrace = this.getConfirmOtherVerticalLineTrace(); const verticalLineTrace = this.getVerticalLineTrace(this.currentLineX) Plotly.react('graph_to_show', [...functionTraces, verticalLineTrace, otherLineTrace], this.layout); // this.updateFunctionValues() } /** * Matches the main vertical line position to a specified value. * @param {number} value - The value to match the vertical line's position. */ matchVerticalLine(value) { const valueToMatch = value // console.log("Value to Match:", valueToMatch) if (valueToMatch != this.otherLineX) { document.getElementById('lineSlider').value = this.otherLineX this.moveVerticalLine(this.otherLineX) } else { document.getElementById('lineSlider').value = valueToMatch this.moveVerticalLine(value) } } /** * Moves the other vertical line to a new position and updates the graph. * @param {number} value - The new position for the other vertical line. */ moveOtherVerticalLine(value,moveOn = false) { this.otherLineX = value; const functionTraces = this.getFunctionTraces(); const otherLineTrace = this.getOtherVerticalLineTrace(); const verticalLineTrace = this.getVerticalLineTrace(this.currentLineX,moveOn) if (this.otherLineX <= 0) Plotly.react('graph_to_show', [...functionTraces, verticalLineTrace], this.layout); else { const otherLineTrace = this.getOtherVerticalLineTrace(moveOn); Plotly.react('graph_to_show', [...functionTraces, verticalLineTrace, otherLineTrace], this.layout); } //this.updateFunctionValues() this.updateOthersFunctionValues() }; moveOtherVerticalLineTwo(value,moveOn = false,otherGray = false) { this.otherLineX = value; const functionTraces = this.getFunctionTraces(); const otherLineTrace = this.getOtherVerticalLineTrace(); const verticalLineTrace = this.getConfirmVerticalLineTrace(this.currentLineX,moveOn) if (this.otherLineX <= 0) Plotly.react('graph_to_show', [...functionTraces, verticalLineTrace], this.layout); else { const otherLineTrace = this.getOtherVerticalLineTrace(moveOn,otherGray); Plotly.react('graph_to_show', [...functionTraces, verticalLineTrace, otherLineTrace], this.layout); } // this.updateFunctionValues() }; }