diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 4366acc91f..0058c827d0 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -32,6 +32,7 @@ class GraphicalSliderToolModule(XModule): resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'), resource_string(__name__, 'js/src/graphical_slider_tool/inputs.js'), resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/el_output.js'), resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') ] diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/el_output.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/el_output.js new file mode 100644 index 0000000000..e1cac3156e --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/el_output.js @@ -0,0 +1,97 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('ElOutput', ['logme'], function (logme) { + + return ElOutput; + + function ElOutput(config, state) { + + if ($.isPlainObject(config.plot.function)) { + processFuncObj(config.plot.function); + } else if ($.isArray(config.plot.function)) { + (function (c1) { + while (c1 < config.plot.function.length) { + if ($.isPlainObject(config.plot.function[c1])) { + processFuncObj(config.plot.function[c1]); + } + + c1 += 1; + } + }(0)); + } + + return; + + function processFuncObj(obj) { + var outputEl, paramNames, funcString, func; + + // We are only interested in functions that are meant for output to an + // element. + if (typeof obj['@output'] !== 'string') { + return; + } + + if (obj['@output'].toLowerCase() !== 'element') { + return; + } + + if (typeof obj['@el_id'] !== 'string') { + logme('ERROR: You specified "output" as "element", but did not spify "el_id".'); + + return; + } + + if (typeof obj['#text'] !== 'string') { + logme('ERROR: Function body is not defined.'); + + return; + } + + funcString = obj['#text']; + + // Make sure that all HTML entities are converted to their proper + // ASCII text equivalents. + funcString = $('
').html(funcString).text(); + + outputEl = $('#' + obj['@el_id']); + + if (outputEl.length !== 1) { + logme('ERROR: The element with id "' + obj['@el_id'] + '" was not found.'); + + return; + } + + paramNames = state.getAllParameterNames(); + paramNames.push(funcString); + + try { + func = Function.apply(null, paramNames); + } catch (err) { + logme( + 'ERROR: The function body "' + + funcString + + '" was not converted by the Function constructor.' + ); + + paramNames.pop(); + + return; + } + + paramNames.pop(); + + outputEl.html(func.apply(window, state.getAllParameterValues())); + + state.addDynamicEl(outputEl, func); + } + + } +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index 0df84c35e2..7911496639 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -32,16 +32,21 @@ define('Graph', ['logme'], function (logme) { // Configure some settings for the graph. if (setGraphXRange() === false) { - logme('ERROR: could not configure the xrange. Will not continue.'); + logme('ERROR: Could not configure the xrange. Will not continue.'); return; } - setGraphAxes(); + if (setGraphAxes() === false) { + logme('ERROR: Could not process configuration for the axes.'); + + return; + } // Get the user defined functions. If there aren't any, don't do // anything else. createFunctions(); + if (functions.length === 0) { logme('ERROR: No functions were specified, or something went wrong.'); @@ -49,8 +54,9 @@ define('Graph', ['logme'], function (logme) { } // Create the initial graph and plot it for the user to see. - generateData(); - updatePlot(); + if (generateData() === true) { + updatePlot(); + } // Bind an event. Whenever some constant changes, the graph will be // redrawn @@ -59,33 +65,33 @@ define('Graph', ['logme'], function (logme) { return; function setGraphAxes() { - // Define the xaxis Flot configuration, and then see if the user - // supplied custom values. - xaxis = { - 'min': 0, - 'tickSize': 1, - 'max': 10 - }; + xaxis = {}; if (typeof config.plot['xticks'] === 'string') { - processTicks(config.plot['xticks'], xaxis, 'xunits'); + if (processTicks(config.plot['xticks'], xaxis, 'xunits') === false) { + logme('ERROR: Could not process the ticks for x-axis.'); + + return false; + } } else { logme('MESSAGE: "xticks" were not specified. Using defaults.'); + + return false; } - // Define the yaxis Flot configuration, and then see if the user - // supplied custom values. - yaxis = { - 'min': 0, - 'tickSize': 1, - 'max': 10 - }; + yaxis = {}; if (typeof config.plot['yticks'] === 'string') { - processTicks(config.plot['yticks'], yaxis, 'yunits'); + if (processTicks(config.plot['yticks'], yaxis, 'yunits') === false) { + logme('ERROR: Could not process the ticks for y-axis.'); + + return false; + } } else { logme('MESSAGE: "yticks" were not specified. Using defaults.'); + + return false; } - return; + return true; function processTicks(ticksStr, ticksObj, unitsType) { var ticksBlobs, tempFloat, tempTicks, c1, c2; @@ -97,7 +103,7 @@ define('Graph', ['logme'], function (logme) { if (ticksBlobs.length !== 3) { logme('ERROR: Did not get 3 blobs from ticksStr = "' + ticksStr + '".'); - return; + return false; } tempFloat = parseFloat(ticksBlobs[0]); @@ -105,6 +111,8 @@ define('Graph', ['logme'], function (logme) { ticksObj.min = tempFloat; } else { logme('ERROR: Invalid "min". ticksBlobs[0] = ', ticksBlobs[0]); + + return false; } tempFloat = parseFloat(ticksBlobs[1]); @@ -112,6 +120,8 @@ define('Graph', ['logme'], function (logme) { ticksObj.tickSize = tempFloat; } else { logme('ERROR: Invalid "tickSize". ticksBlobs[1] = ', ticksBlobs[1]); + + return false; } tempFloat = parseFloat(ticksBlobs[2]); @@ -119,24 +129,25 @@ define('Graph', ['logme'], function (logme) { ticksObj.max = tempFloat; } else { logme('ERROR: Invalid "max". ticksBlobs[2] = ', ticksBlobs[2]); + + return false; } // Is the starting tick to the left of the ending tick (on the // x-axis)? If not, set default starting and ending tick. if (ticksObj.min >= ticksObj.max) { - logme('ERROR: min >= max. Setting defaults.'); + logme('ERROR: Ticks min >= max.'); - ticksObj.min = 0; - ticksObj.max = 10; + return false; } // Make sure the range makes sense - i.e. that there are at // least 3 ticks. If not, set a tickSize which will produce // 11 ticks. tickSize is the spacing between the ticks. - if (ticksObj.tickSize * 2 >= ticksObj.max - ticksObj.min) { - logme('ERROR: tickSize * 2 >= max - min. Setting defaults.'); + if (ticksObj.tickSize > ticksObj.max - ticksObj.min) { + logme('ERROR: tickSize > max - min.'); - ticksObj.tickSize = (ticksObj.max - ticksObj.min) / 10.0; + return false; } // units: change last tick to units @@ -158,7 +169,11 @@ define('Graph', ['logme'], function (logme) { ticksObj.ticks = tempTicks; } - return; + // ticksObj.font = { + // 'size': '16px' + // }; + + return true; function roundToPrec(num, prec) { var c1, tn1, tn2, digitsBefore, digitsAfter; @@ -354,16 +369,36 @@ define('Graph', ['logme'], function (logme) { // properties as parameters. Rather than writing them out every // time, we will have a single place where it is done. function callAddFunction(obj) { + if (typeof obj['@output'] === 'string') { + + // If this function is meant to be calculated for an + // element then skip it. + if (obj['@output'].toLowerCase() === 'element') { + return; + } + + // It is an error if "output" is not "element" or "graph". + // Though you can ommit the "output" attribute. + else if (obj['@output'].toLowerCase() !== 'graph') { + logme('ERROR: Function "output" attribute can be either "div" or "graph".'); + + return; + } + } + + // The user did not specify an "output" attribute, or it is + // "graph". addFunction( obj['#text'], obj['@color'], obj['@line'], obj['@dot'], - obj['@label'] + obj['@label'], + obj['@point_size'] ); } - function addFunction(funcString, color, line, dot, label) { + function addFunction(funcString, color, line, dot, label, pointSize) { var newFunctionObject, func, paramNames; // The main requirement is function string. Without it we can't @@ -407,14 +442,15 @@ define('Graph', ['logme'], function (logme) { try { func = Function.apply(null, paramNames); } catch (err) { - // Let's tell the user. He will see a nice red error - // message instead of a graph. - plotDiv.html( - '' + - 'Error while parsing JavaScript function body string!' + - '' + logme( + 'ERROR: The function body "' + + funcString + + '" was not converted by the Function constructor.' ); + paramNames.pop(); + paramNames.pop(); + return; } @@ -445,6 +481,10 @@ define('Graph', ['logme'], function (logme) { } } + if (typeof pointSize === 'string') { + newFunctionObject['pointSize'] = pointSize; + } + // If the preference is conflicting (we must have either line // or dot, none is not an option), we will show line. if ( @@ -465,8 +505,9 @@ define('Graph', ['logme'], function (logme) { // The callback that will be called whenever a constant changes (gets // updated via a slider or a text input). function onUpdatePlot(event) { - generateData(); - updatePlot(); + if (generateData() === true) { + updatePlot(); + } } function generateData() { @@ -501,7 +542,14 @@ define('Graph', ['logme'], function (logme) { // We call the user defined function, passing all of the // available parameter values. Inside this function they // will be accessible by their names. - y = functionObj.func.apply(window, paramValues); + try { + y = functionObj.func.apply(window, paramValues); + } catch (err) { + logme('ERROR: Could not generate data.'); + logme('Error message: "' + err.message + '".'); + + return false; + } // Return the paramValues array to how it was before we // added 'x' variable to the end of it. @@ -549,14 +597,21 @@ define('Graph', ['logme'], function (logme) { 'show': functionObj.dot }; + if (functionObj.hasOwnProperty('pointSize')) { + seriesObj.points.radius = functionObj.pointSize; + } + // Add the newly created series object to the series set which // will be plotted by Flot. dataSeries.push(seriesObj); } - } + + return true; + } // End-of: function generateData function updatePlot() { // Tell Flot to draw the graph to our specification. + $.plot( plotDiv, dataSeries, diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 3c06cf9544..bdbe317f36 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -8,8 +8,8 @@ define( // Even though it is not explicitly in this module, we have to specify // 'GeneralMethods' as a dependency. It expands some of the core JS objects // with additional useful methods that are used in other modules. - ['State', 'GeneralMethods', 'Sliders', 'Inputs', 'Graph', 'logme'], - function (State, GeneralMethods, Sliders, Inputs, Graph, logme) { + ['State', 'GeneralMethods', 'Sliders', 'Inputs', 'Graph', 'ElOutput', 'logme'], + function (State, GeneralMethods, Sliders, Inputs, Graph, ElOutput, logme) { return GstMain; @@ -67,6 +67,9 @@ define( // Configure and display the graph. Attach event for the graph to be // updated on any change of a slider or a text input. Graph(gstId, config, state); + + // Configure functions that output to an element instead of the graph. + ElOutput(config, state); } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js index 19bb557b24..04c0b0a60e 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -18,7 +18,9 @@ define('State', ['logme'], function (logme) { function State(gstId, config) { var parameters, allParameterNames, allParameterValues, - plotDiv; + plotDiv, dynamicEl; + + dynamicEl = []; stateInst += 1; logme('MESSAGE: Creating state instance # ' + stateInst + '.'); @@ -92,7 +94,8 @@ define('State', ['logme'], function (logme) { 'getAllParameterNames': getAllParameterNames, 'getAllParameterValues': getAllParameterValues, - 'bindUpdatePlotEvent': bindUpdatePlotEvent + 'bindUpdatePlotEvent': bindUpdatePlotEvent, + 'addDynamicEl': addDynamicEl }; function getAllParameterNames() { @@ -119,6 +122,13 @@ define('State', ['logme'], function (logme) { plotDiv.bind('update_plot', callback); } + function addDynamicEl(outputEl, func) { + dynamicEl.push({ + 'outputEl': outputEl, + 'func': func + }); + } + function getParameterValue(paramName) { // If the name of the constant is not tracked by state, return an @@ -238,6 +248,10 @@ define('State', ['logme'], function (logme) { // Update the helper array with the new parameter's value. allParameterValues[parameters[paramName].helperArrayIndex] = paramValueNum; + for (c1 = 0; c1 < dynamicEl.length; c1++) { + dynamicEl[c1].outputEl.html(dynamicEl[c1].func.apply(window, allParameterValues)); + } + return true; } // End-of: function setParameterValue