diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index c867fca228..86636ef05a 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -36,6 +36,7 @@ setup( "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor", "discussion = xmodule.discussion_module:DiscussionDescriptor", + "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor", ] } ) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py new file mode 100644 index 0000000000..3b8d96ee81 --- /dev/null +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -0,0 +1,195 @@ +""" +Graphical slider tool module is ungraded xmodule used by students to +understand functional dependencies. +""" + +import json +import logging +from lxml import etree +from lxml import html +import xmltodict + +from xmodule.mako_module import MakoModuleDescriptor +from xmodule.xml_module import XmlDescriptor +from xmodule.x_module import XModule +from xmodule.stringify import stringify_children +from pkg_resources import resource_string + + +log = logging.getLogger(__name__) + + +class GraphicalSliderToolModule(XModule): + ''' Graphical-Slider-Tool Module + ''' + + js = { + 'js': [ + # 3rd party libraries used by graphic slider tool. + # TODO - where to store them - outside xmodule? + resource_string(__name__, 'js/src/graphical_slider_tool/jstat-1.0.0.min.js'), + + resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/state.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'), + 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/g_label_el_output.js'), + + resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') + ] + } + js_module_name = "GraphicalSliderTool" + + def __init__(self, system, location, definition, descriptor, instance_state=None, + shared_state=None, **kwargs): + """ + For XML file format please look at documentation. TODO - receive + information where to store XML documentation. + """ + XModule.__init__(self, system, location, definition, descriptor, + instance_state, shared_state, **kwargs) + + def get_html(self): + """ Renders parameters to template. """ + + # these 3 will be used in class methods + self.html_id = self.location.html_id() + self.html_class = self.location.category + self.configuration_json = self.build_configuration_json() + params = { + 'gst_html': self.substitute_controls(self.definition['render']), + 'element_id': self.html_id, + 'element_class': self.html_class, + 'configuration_json': self.configuration_json + } + self.content = self.system.render_template( + 'graphical_slider_tool.html', params) + return self.content + + def substitute_controls(self, html_string): + """ Substitutes control elements (slider, textbox and plot) in + html_string with their divs. Html_string is content of tag + inside tag. Documentation on how information in + tag is organized and processed is located in: + mitx/docs/build/html/graphical_slider_tool.html. + + Args: + html_string: content of tag, with controls as xml tags, + e.g. . + + Returns: + html_string with control tags replaced by proper divs + ( ->
) + """ + + xml = html.fromstring(html_string) + + #substitute plot, if presented + plot_div = '
' + plot_el = xml.xpath('//plot') + if plot_el: + plot_el = plot_el[0] + plot_el.getparent().replace(plot_el, html.fromstring( + plot_div.format(element_class=self.html_class, + element_id=self.html_id, + style=plot_el.get('style', "")))) + + #substitute sliders + slider_div = '
\ +
' + slider_els = xml.xpath('//slider') + for slider_el in slider_els: + slider_el.getparent().replace(slider_el, html.fromstring( + slider_div.format(element_class=self.html_class, + element_id=self.html_id, + var=slider_el.get('var', ""), + style=slider_el.get('style', "")))) + + # substitute inputs aka textboxes + input_div = '' + input_els = xml.xpath('//textbox') + for input_index, input_el in enumerate(input_els): + input_el.getparent().replace(input_el, html.fromstring( + input_div.format(element_class=self.html_class, + element_id=self.html_id, + var=input_el.get('var', ""), + style=input_el.get('style', ""), + input_index=input_index))) + + return html.tostring(xml) + + def build_configuration_json(self): + """Creates json element from xml element (with aim to transfer later + directly to javascript via hidden field in template). Steps: + + 1. Convert xml tree to python dict. + + 2. Dump dict to json. + + """ + # added for interface compatibility with xmltodict.parse + # class added for javascript's part purposes + return json.dumps(xmltodict.parse('' + self.definition['configuration'] + '')) + + +class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor): + module_class = GraphicalSliderToolModule + template_dir_name = 'graphical_slider_tool' + + @classmethod + def definition_from_xml(cls, xml_object, system): + """ + Pull out the data into dictionary. + + Args: + xml_object: xml from file. + + Returns: + dict + """ + # check for presense of required tags in xml + expected_children_level_0 = ['render', 'configuration'] + for child in expected_children_level_0: + if len(xml_object.xpath(child)) != 1: + raise ValueError("Graphical Slider Tool definition must include \ + exactly one '{0}' tag".format(child)) + + expected_children_level_1 = ['functions'] + for child in expected_children_level_1: + if len(xml_object.xpath('configuration')[0].xpath(child)) != 1: + raise ValueError("Graphical Slider Tool definition must include \ + exactly one '{0}' tag".format(child)) + # finished + + def parse(k): + """Assumes that xml_object has child k""" + return stringify_children(xml_object.xpath(k)[0]) + return { + 'render': parse('render'), + 'configuration': parse('configuration') + } + + def definition_to_xml(self, resource_fs): + '''Return an xml element representing this definition.''' + xml_object = etree.Element('graphical_slider_tool') + + def add_child(k): + child_str = '<{tag}>{body}'.format(tag=k, body=self.definition[k]) + child_node = etree.fromstring(child_str) + xml_object.append(child_node) + + for child in ['render', 'configuration']: + add_child(child) + + return xml_object 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..3175aae3f0 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/el_output.js @@ -0,0 +1,139 @@ +// 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.functions.function)) { + processFuncObj(config.functions.function); + } else if ($.isArray(config.functions.function)) { + (function (c1) { + while (c1 < config.functions.function.length) { + if ($.isPlainObject(config.functions.function[c1])) { + processFuncObj(config.functions.function[c1]); + } + + c1 += 1; + } + }(0)); + } + + return; + + function processFuncObj(obj) { + var paramNames, funcString, func, el, disableAutoReturn, updateOnEvent; + + // We are only interested in functions that are meant for output to an + // element. + if ( + (typeof obj['@output'] !== 'string') || + ((obj['@output'].toLowerCase() !== 'element') && (obj['@output'].toLowerCase() !== 'none')) + ) { + 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; + } + + updateOnEvent = 'slide'; + if ( + (obj.hasOwnProperty('@update_on') === true) && + (typeof obj['@update_on'] === 'string') && + ((obj['@update_on'].toLowerCase() === 'slide') || (obj['@update_on'].toLowerCase() === 'change')) + ) { + updateOnEvent = obj['@update_on'].toLowerCase(); + } + + disableAutoReturn = obj['@disable_auto_return']; + + funcString = obj['#text']; + + if ( + (disableAutoReturn === undefined) || + ( + (typeof disableAutoReturn === 'string') && + (disableAutoReturn.toLowerCase() !== 'true') + ) + ) { + if (funcString.search(/return/i) === -1) { + funcString = 'return ' + funcString; + } + } else { + if (funcString.search(/return/i) === -1) { + logme( + 'ERROR: You have specified a JavaScript ' + + 'function without a "return" statemnt. Your ' + + 'function will return "undefined" by default.' + ); + } + } + + // Make sure that all HTML entities are converted to their proper + // ASCII text equivalents. + funcString = $('
').html(funcString).text(); + + 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.' + ); + logme('Error message: "' + err.message + '".'); + + $('#' + gstId).html('
' + 'ERROR IN XML: Could not create a function from string "' + funcString + '".' + '
'); + $('#' + gstId).append('
' + 'Error message: "' + err.message + '".' + '
'); + + paramNames.pop(); + + return; + } + + paramNames.pop(); + + if (obj['@output'].toLowerCase() !== 'none') { + el = $('#' + obj['@el_id']); + + if (el.length !== 1) { + logme( + 'ERROR: DOM element with ID "' + obj['@el_id'] + '" ' + + 'not found. Dynamic element not created.' + ); + + return; + } + + el.html(func.apply(window, state.getAllParameterValues())); + } else { + el = null; + func.apply(window, state.getAllParameterValues()); + } + + state.addDynamicEl(el, func, obj['@el_id'], updateOnEvent); + } + + } +}); + +// 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/g_label_el_output.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/g_label_el_output.js new file mode 100644 index 0000000000..13c9dd3389 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/g_label_el_output.js @@ -0,0 +1,113 @@ +// 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('GLabelElOutput', ['logme'], function (logme) { + return GLabelElOutput; + + function GLabelElOutput(config, state) { + if ($.isPlainObject(config.functions.function)) { + processFuncObj(config.functions.function); + } else if ($.isArray(config.functions.function)) { + (function (c1) { + while (c1 < config.functions.function.length) { + if ($.isPlainObject(config.functions.function[c1])) { + processFuncObj(config.functions.function[c1]); + } + + c1 += 1; + } + }(0)); + } + + return; + + function processFuncObj(obj) { + var paramNames, funcString, func, disableAutoReturn; + + // We are only interested in functions that are meant for output to an + // element. + if ( + (typeof obj['@output'] !== 'string') || + (obj['@output'].toLowerCase() !== 'plot_label') + ) { + return; + } + + if (typeof obj['@el_id'] !== 'string') { + logme('ERROR: You specified "output" as "plot_label", but did not spify "el_id".'); + + return; + } + + if (typeof obj['#text'] !== 'string') { + logme('ERROR: Function body is not defined.'); + + return; + } + + disableAutoReturn = obj['@disable_auto_return']; + + funcString = obj['#text']; + + if ( + (disableAutoReturn === undefined) || + ( + (typeof disableAutoReturn === 'string') && + (disableAutoReturn.toLowerCase() !== 'true') + ) + ) { + if (funcString.search(/return/i) === -1) { + funcString = 'return ' + funcString; + } + } else { + if (funcString.search(/return/i) === -1) { + logme( + 'ERROR: You have specified a JavaScript ' + + 'function without a "return" statemnt. Your ' + + 'function will return "undefined" by default.' + ); + } + } + + // Make sure that all HTML entities are converted to their proper + // ASCII text equivalents. + funcString = $('
').html(funcString).text(); + + 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.' + ); + logme('Error message: "' + err.message + '".'); + + $('#' + gstId).html('
' + 'ERROR IN XML: Could not create a function from string "' + funcString + '".' + '
'); + $('#' + gstId).append('
' + 'Error message: "' + err.message + '".' + '
'); + + paramNames.pop(); + + return; + } + + paramNames.pop(); + + state.plde.push({ + 'elId': obj['@el_id'], + 'func': 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/general_methods.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/general_methods.js new file mode 100644 index 0000000000..9cdd4fff0f --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/general_methods.js @@ -0,0 +1,23 @@ +// 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('GeneralMethods', [], function () { + if (!String.prototype.trim) { + // http://blog.stevenlevithan.com/archives/faster-trim-javascript + String.prototype.trim = function trim(str) { + return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); + }; + } + + return { + 'module_name': 'GeneralMethods', + 'module_status': 'OK' + }; +}); + +// 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 new file mode 100644 index 0000000000..2520f0b12f --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -0,0 +1,1496 @@ +// 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('Graph', ['logme'], function (logme) { + + return Graph; + + function Graph(gstId, config, state) { + var plotDiv, dataSeries, functions, xaxis, yaxis, numPoints, xrange, + asymptotes, movingLabels, xTicksNames, yTicksNames, graphBarWidth, graphBarAlign; + + // We need plot configuration settings. Without them we can't continue. + if ($.isPlainObject(config.plot) === false) { + return; + } + + // We must have a graph container DIV element available in order to + // proceed. + plotDiv = $('#' + gstId + '_plot'); + if (plotDiv.length === 0) { + logme('ERROR: Could not find the plot DIV with ID "' + gstId + '_plot".'); + + return; + } + + if (plotDiv.width() === 0) { + plotDiv.width(300); + } + + // Sometimes, when height is not explicitly set via CSS (or by some + // other means), it is 0 pixels by default. When Flot will try to plot + // a graph in this DIV with 0 height, then it will raise an error. To + // prevent this, we will set it to be equal to the width. + if (plotDiv.height() === 0) { + plotDiv.height(plotDiv.width()); + } + + plotDiv.css('position', 'relative'); + + // Configure some settings for the graph. + if (setGraphXRange() === false) { + logme('ERROR: Could not configure the xrange. Will not continue.'); + + return; + } + + if (setGraphAxes() === false) { + logme('ERROR: Could not process configuration for the axes.'); + + return; + } + + graphBarWidth = 1; + graphBarAlign = null; + + getBarWidth(); + getBarAlign(); + + // 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.'); + + return; + } + + if (createMarkingsFunctions() === false) { + return; + } + if (createMovingLabelFunctions() === false) { + return; + } + + // Create the initial graph and plot it for the user to see. + if (generateData() === true) { + updatePlot(); + } + + // Bind an event. Whenever some constant changes, the graph will be + // redrawn + state.bindUpdatePlotEvent(plotDiv, onUpdatePlot); + + return; + + function getBarWidth() { + if (config.plot.hasOwnProperty('bar_width') === false) { + return; + } + + if (typeof config.plot.bar_width !== 'string') { + logme('ERROR: The parameter config.plot.bar_width must be a string.'); + + return; + } + + if (isFinite(graphBarWidth = parseFloat(config.plot.bar_width)) === false) { + logme('ERROR: The parameter config.plot.bar_width is not a valid floating number.'); + graphBarWidth = 1; + + return; + } + + return; + } + + function getBarAlign() { + if (config.plot.hasOwnProperty('bar_align') === false) { + return; + } + + if (typeof config.plot.bar_align !== 'string') { + logme('ERROR: The parameter config.plot.bar_align must be a string.'); + + return; + } + + if ( + (config.plot.bar_align.toLowerCase() !== 'left') && + (config.plot.bar_align.toLowerCase() !== 'center') + ) { + logme('ERROR: Property config.plot.bar_align can be one of "left", or "center".'); + + return; + } + + graphBarAlign = config.plot.bar_align.toLowerCase(); + + return; + } + + function createMovingLabelFunctions() { + var c1, returnStatus; + + returnStatus = true; + movingLabels = []; + + if (config.plot.hasOwnProperty('moving_label') !== true) { + returnStatus = true; + } else if ($.isPlainObject(config.plot.moving_label) === true) { + if (processMovingLabel(config.plot.moving_label) === false) { + returnStatus = false; + } + } else if ($.isArray(config.plot.moving_label) === true) { + for (c1 = 0; c1 < config.plot.moving_label.length; c1++) { + if (processMovingLabel(config.plot.moving_label[c1]) === false) { + returnStatus = false; + } + } + } + + return returnStatus; + } + + function processMovingLabel(obj) { + var labelText, funcString, disableAutoReturn, paramNames, func, + fontWeight, fontColor; + + if (obj.hasOwnProperty('@text') === false) { + logme('ERROR: You did not define a "text" attribute for the moving_label.'); + + return false; + } + if (typeof obj['@text'] !== 'string') { + logme('ERROR: "text" attribute is not a string.'); + + return false; + } + labelText = obj['@text']; + + if (obj.hasOwnProperty('#text') === false) { + logme('ERROR: moving_label is missing function declaration.'); + + return false; + } + if (typeof obj['#text'] !== 'string') { + logme('ERROR: Function declaration is not a string.'); + + return false; + } + funcString = obj['#text']; + + fontColor = 'black'; + if ( + (obj.hasOwnProperty('@color') === true) && + (typeof obj['@color'] === 'string') + ) { + fontColor = obj['@color']; + } + + fontWeight = 'normal'; + if ( + (obj.hasOwnProperty('@weight') === true) && + (typeof obj['@weight'] === 'string') + ) { + if ( + (obj['@weight'].toLowerCase() === 'normal') || + (obj['@weight'].toLowerCase() === 'bold') + ) { + fontWeight = obj['@weight']; + } else { + logme('ERROR: Moving label can have a weight property of "normal" or "bold".'); + } + } + + disableAutoReturn = obj['@disable_auto_return']; + + funcString = $('
').html(funcString).text(); + + if ( + (disableAutoReturn === undefined) || + ( + (typeof disableAutoReturn === 'string') && + (disableAutoReturn.toLowerCase() !== 'true') + ) + ) { + if (funcString.search(/return/i) === -1) { + funcString = 'return ' + funcString; + } + } else { + if (funcString.search(/return/i) === -1) { + logme( + 'ERROR: You have specified a JavaScript ' + + 'function without a "return" statemnt. Your ' + + 'function will return "undefined" by default.' + ); + } + } + + 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.' + ); + logme('Error message: "' + err.message + '"'); + + $('#' + gstId).html('
' + 'ERROR IN XML: Could not create a function from the string "' + funcString + '".' + '
'); + $('#' + gstId).append('
' + 'Error message: "' + err.message + '".' + '
'); + + paramNames.pop(); + + return false; + } + + paramNames.pop(); + + movingLabels.push({ + 'labelText': labelText, + 'func': func, + 'el': null, + 'fontColor': fontColor, + 'fontWeight': fontWeight + }); + + return true; + } + + function createMarkingsFunctions() { + var c1, paramNames, returnStatus; + + returnStatus = true; + + asymptotes = []; + paramNames = state.getAllParameterNames(); + + if ($.isPlainObject(config.plot.asymptote)) { + if (processAsymptote(config.plot.asymptote) === false) { + returnStatus = false; + } + } else if ($.isArray(config.plot.asymptote)) { + for (c1 = 0; c1 < config.plot.asymptote.length; c1 += 1) { + if (processAsymptote(config.plot.asymptote[c1]) === false) { + returnStatus = false; + } + } + } + + return returnStatus; + + // Read configuration options for asymptotes, and store them as + // an array of objects. Each object will have 3 properties: + // + // - color: the color of the asymptote line + // - type: 'x' (vertical), or 'y' (horizontal) + // - func: the function that will generate the value at which + // the asymptote will be plotted; i.e. x = func(), or + // y = func(); for now only horizontal and vertical + // asymptotes are supported + // + // Since each asymptote can have a variable function - function + // that relies on some parameter specified in the config - we will + // generate each asymptote just before we draw the graph. See: + // + // function updatePlot() + // function generateMarkings() + // + // Asymptotes are really thin rectangles implemented via the Flot's + // markings option. + function processAsymptote(asyObj) { + var newAsyObj, funcString, func; + + newAsyObj = {}; + + if (typeof asyObj['@type'] === 'string') { + if (asyObj['@type'].toLowerCase() === 'x') { + newAsyObj.type = 'x'; + } else if (asyObj['@type'].toLowerCase() === 'y') { + newAsyObj.type = 'y'; + } else { + logme('ERROR: Attribute "type" for asymptote can be "x" or "y".'); + + return false; + } + } else { + logme('ERROR: Attribute "type" for asymptote is not specified.'); + + return false; + } + + if (typeof asyObj['#text'] === 'string') { + funcString = asyObj['#text']; + } else { + logme('ERROR: Function body for asymptote is not specified.'); + + return false; + } + + newAsyObj.color = '#000'; + if (typeof asyObj['@color'] === 'string') { + newAsyObj.color = asyObj['@color']; + } + + newAsyObj.label = false; + if ( + (asyObj.hasOwnProperty('@label') === true) && + (typeof asyObj['@label'] === 'string') + ) { + newAsyObj.label = asyObj['@label']; + } + + funcString = $('
').html(funcString).text(); + + disableAutoReturn = asyObj['@disable_auto_return']; + if ( + (disableAutoReturn === undefined) || + ( + (typeof disableAutoReturn === 'string') && + (disableAutoReturn.toLowerCase() !== 'true') + ) + ) { + if (funcString.search(/return/i) === -1) { + funcString = 'return ' + funcString; + } + } else { + if (funcString.search(/return/i) === -1) { + logme( + 'ERROR: You have specified a JavaScript ' + + 'function without a "return" statemnt. Your ' + + 'function will return "undefined" by default.' + ); + } + } + + paramNames.push(funcString); + + try { + func = Function.apply(null, paramNames); + } catch (err) { + logme('ERROR: Asymptote function body could not be converted to function object.'); + logme('Error message: "".' + err.message); + + return false; + } + + paramNames.pop(); + + newAsyObj.func = func; + asymptotes.push(newAsyObj); + + return true; + } + } + + function setGraphAxes() { + xaxis = { + 'tickFormatter': null + }; + + if (typeof config.plot['xticks'] === 'string') { + 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; + } + + yaxis = { + 'tickFormatter': null + }; + if (typeof config.plot['yticks'] === 'string') { + 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; + } + + xTicksNames = null; + yTicksNames = null; + + if (checkForTicksNames('x') === false) { + return false; + } + + if (checkForTicksNames('y') === false) { + return false; + } + + return true; + + // + // function checkForTicksNames(axisName) + // + // The parameter "axisName" can be either "x" or "y" (string). Depending on it, the function + // will set "xTicksNames" or "yTicksNames" private variable. + // + // This function does not return anything. It sets the private variable "xTicksNames" ("yTicksNames") + // to the object converted by JSON.parse from the XML parameter "plot.xticks_names" ("plot.yticks_names"). + // If the "plot.xticks_names" ("plot.yticks_names") is missing or it is not a valid JSON string, then + // "xTicksNames" ("yTicksNames") will be set to "null". + // + // Depending on the "xTicksNames" ("yTicksNames") being "null" or an object, the plot will either draw + // number ticks, or use the names specified by the opbject. + // + function checkForTicksNames(axisName) { + var tmpObj; + + if ((axisName !== 'x') && (axisName !== 'y')) { + // This is not an error. This funcion should simply stop executing. + + return true; + } + + if ( + (config.plot.hasOwnProperty(axisName + 'ticks_names') === true) || + (typeof config.plot[axisName + 'ticks_names'] === 'string') + ) { + try { + tmpObj = JSON.parse(config.plot[axisName + 'ticks_names']); + } catch (err) { + logme( + 'ERROR: plot.' + axisName + 'ticks_names is not a valid JSON string.', + 'Error message: "' + err.message + '".' + ); + + return false; + } + + if (axisName === 'x') { + xTicksNames = tmpObj; + xaxis.tickFormatter = xAxisTickFormatter; + } + // At this point, we are certain that axisName = 'y'. + else { + yTicksNames = tmpObj; + yaxis.tickFormatter = yAxisTickFormatter; + } + } + } + + function processTicks(ticksStr, ticksObj, unitsType) { + var ticksBlobs, tempFloat, tempTicks, c1, c2; + + // The 'ticks' setting is a string containing 3 floating-point + // numbers. + ticksBlobs = ticksStr.split(','); + + if (ticksBlobs.length !== 3) { + logme('ERROR: Did not get 3 blobs from ticksStr = "' + ticksStr + '".'); + + return false; + } + + tempFloat = parseFloat(ticksBlobs[0]); + if (isNaN(tempFloat) === false) { + ticksObj.min = tempFloat; + } else { + logme('ERROR: Invalid "min". ticksBlobs[0] = ', ticksBlobs[0]); + + return false; + } + + tempFloat = parseFloat(ticksBlobs[1]); + if (isNaN(tempFloat) === false) { + ticksObj.tickSize = tempFloat; + } else { + logme('ERROR: Invalid "tickSize". ticksBlobs[1] = ', ticksBlobs[1]); + + return false; + } + + tempFloat = parseFloat(ticksBlobs[2]); + if (isNaN(tempFloat) === false) { + 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: Ticks min >= max.'); + + 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 > ticksObj.max - ticksObj.min) { + logme('ERROR: tickSize > max - min.'); + + return false; + } + + // units: change last tick to units + if (typeof config.plot[unitsType] === 'string') { + tempTicks = []; + + for (c1 = ticksObj.min; c1 <= ticksObj.max; c1 += ticksObj.tickSize) { + c2 = roundToPrec(c1, ticksObj.tickSize); + tempTicks.push([c2, c2]); + } + + tempTicks.pop(); + tempTicks.push([ + roundToPrec(ticksObj.max, ticksObj.tickSize), + config.plot[unitsType] + ]); + + ticksObj.tickSize = null; + ticksObj.ticks = tempTicks; + } + + return true; + + function roundToPrec(num, prec) { + var c1, tn1, tn2, digitsBefore, digitsAfter; + + tn1 = Math.abs(num); + tn2 = Math.abs(prec); + + // Find out number of digits BEFORE the decimal point. + c1 = 0; + tn1 = Math.abs(num); + while (tn1 >= 1) { + c1 += 1; + + tn1 /= 10; + } + digitsBefore = c1; + + // Find out number of digits AFTER the decimal point. + c1 = 0; + tn1 = Math.abs(num); + while (Math.round(tn1) !== tn1) { + c1 += 1; + + tn1 *= 10; + } + digitsAfter = c1; + + // For precision, find out number of digits AFTER the + // decimal point. + c1 = 0; + while (Math.round(tn2) !== tn2) { + c1 += 1; + + tn2 *= 10; + } + + // If precision is more than 1 (no digits after decimal + // points). + if (c1 === 0) { + return num; + } + + // If the precision contains digits after the decimal + // point, we apply special rules. + else { + tn1 = Math.abs(num); + + // if (digitsAfter > c1) { + tn1 = tn1.toFixed(c1); + // } else { + // tn1 = tn1.toPrecision(digitsBefore + digitsAfter); + // } + } + + if (num < 0) { + return -tn1; + } + + return tn1; + } + } + } + + function setGraphXRange() { + var xRangeStr, xRangeBlobs, tempNum, allParamNames, funcString, + disableAutoReturn; + + xrange = {}; + + if ($.isPlainObject(config.plot.xrange) === false) { + logme( + 'ERROR: Expected config.plot.xrange to be an object. ' + + 'It is not.' + ); + logme('config.plot.xrange = ', config.plot.xrange); + + return false; + } + + if (config.plot.xrange.hasOwnProperty('min') === false) { + logme( + 'ERROR: Expected config.plot.xrange.min to be ' + + 'present. It is not.' + ); + + return false; + } + + disableAutoReturn = false; + if (typeof config.plot.xrange.min === 'string') { + funcString = config.plot.xrange.min; + } else if ( + ($.isPlainObject(config.plot.xrange.min) === true) && + (config.plot.xrange.min.hasOwnProperty('#text') === true) && + (typeof config.plot.xrange.min['#text'] === 'string') + ) { + funcString = config.plot.xrange.min['#text']; + + disableAutoReturn = + config.plot.xrange.min['@disable_auto_return']; + if ( + (disableAutoReturn === undefined) || + ( + (typeof disableAutoReturn === 'string') && + (disableAutoReturn.toLowerCase() !== 'true') + ) + ) { + disableAutoReturn = false; + } else { + disableAutoReturn = true; + } + } else { + logme( + 'ERROR: Could not get a function definition for ' + + 'xrange.min property.' + ); + + return false; + } + + funcString = $('
').html(funcString).text(); + + if (disableAutoReturn === false) { + if (funcString.search(/return/i) === -1) { + funcString = 'return ' + funcString; + } + } else { + if (funcString.search(/return/i) === -1) { + logme( + 'ERROR: You have specified a JavaScript ' + + 'function without a "return" statemnt. Your ' + + 'function will return "undefined" by default.' + ); + } + } + + allParamNames = state.getAllParameterNames(); + + allParamNames.push(funcString); + try { + xrange.min = Function.apply(null, allParamNames); + } catch (err) { + logme( + 'ERROR: could not create a function from the string "' + + funcString + '" for xrange.min.' + ); + logme('Error message: "' + err.message + '"'); + + $('#' + gstId).html( + '
' + 'ERROR IN ' + + 'XML: Could not create a function from the string "' + + funcString + '" for xrange.min.' + '
' + ); + $('#' + gstId).append( + '
' + 'Error ' + + 'message: "' + err.message + '".' + '
' + ); + + return false; + } + allParamNames.pop(); + + if (config.plot.xrange.hasOwnProperty('max') === false) { + logme( + 'ERROR: Expected config.plot.xrange.max to be ' + + 'present. It is not.' + ); + + return false; + } + + disableAutoReturn = false; + if (typeof config.plot.xrange.max === 'string') { + funcString = config.plot.xrange.max; + } else if ( + ($.isPlainObject(config.plot.xrange.max) === true) && + (config.plot.xrange.max.hasOwnProperty('#text') === true) && + (typeof config.plot.xrange.max['#text'] === 'string') + ) { + funcString = config.plot.xrange.max['#text']; + + disableAutoReturn = + config.plot.xrange.max['@disable_auto_return']; + if ( + (disableAutoReturn === undefined) || + ( + (typeof disableAutoReturn === 'string') && + (disableAutoReturn.toLowerCase() !== 'true') + ) + ) { + disableAutoReturn = false; + } else { + disableAutoReturn = true; + } + } else { + logme( + 'ERROR: Could not get a function definition for ' + + 'xrange.max property.' + ); + + return false; + } + + funcString = $('
').html(funcString).text(); + + if (disableAutoReturn === false) { + if (funcString.search(/return/i) === -1) { + funcString = 'return ' + funcString; + } + } else { + if (funcString.search(/return/i) === -1) { + logme( + 'ERROR: You have specified a JavaScript ' + + 'function without a "return" statemnt. Your ' + + 'function will return "undefined" by default.' + ); + } + } + + allParamNames.push(funcString); + try { + xrange.max = Function.apply(null, allParamNames); + } catch (err) { + logme( + 'ERROR: could not create a function from the string "' + + funcString + '" for xrange.max.' + ); + logme('Error message: "' + err.message + '"'); + + $('#' + gstId).html( + '
' + 'ERROR IN ' + + 'XML: Could not create a function from the string "' + + funcString + '" for xrange.max.' + '
' + ); + $('#' + gstId).append( + '
' + 'Error message: "' + + err.message + '".' + '
' + ); + + return false; + } + allParamNames.pop(); + + tempNum = parseInt(config.plot.num_points, 10); + if (isFinite(tempNum) === false) { + tempNum = plotDiv.width() / 5.0; + } + + if ( + (tempNum < 2) && + (tempNum > 1000) + ) { + logme( + 'ERROR: Number of points is outside the allowed range ' + + '[2, 1000]' + ); + logme('config.plot.num_points = ' + tempNum); + + return false; + } + + numPoints = tempNum; + + return true; + } + + function createFunctions() { + var c1; + + functions = []; + + if (typeof config.functions === 'undefined') { + logme('ERROR: config.functions is undefined.'); + + return; + } + + if (typeof config.functions.function === 'string') { + + // If just one function string is present. + addFunction(config.functions.function); + + } else if ($.isPlainObject(config.functions.function) === true) { + + // If a function is present, but it also has properties + // defined. + callAddFunction(config.functions.function); + + } else if ($.isArray(config.functions.function)) { + + // If more than one function is defined. + for (c1 = 0; c1 < config.functions.function.length; c1 += 1) { + + // For each definition, we must check if it is a simple + // string definition, or a complex one with properties. + if (typeof config.functions.function[c1] === 'string') { + + // Simple string. + addFunction(config.functions.function[c1]); + + } else if ($.isPlainObject(config.functions.function[c1])) { + + // Properties are present. + callAddFunction(config.functions.function[c1]); + + } + } + } else { + logme('ERROR: config.functions.function is of an unsupported type.'); + + return; + } + + return; + + // This function will reduce code duplication. We have to call + // the function addFunction() several times passing object + // properties as parameters. Rather than writing them out every + // time, we will have a single place where it is done. + function callAddFunction(obj) { + if ( + (obj.hasOwnProperty('@output')) && + (typeof obj['@output'] === 'string') + ) { + + // If this function is meant to be calculated for an + // element then skip it. + if ((obj['@output'].toLowerCase() === 'element') || + (obj['@output'].toLowerCase() === 'none')) { + return; + } + + // If this function is meant to be calculated for a + // dynamic element in a label then skip it. + else if (obj['@output'].toLowerCase() === 'plot_label') { + return; + } + + // It is an error if '@output' is not 'element', + // 'plot_label', or 'graph'. However, if the '@output' + // attribute is omitted, we will not have reached this. + else if (obj['@output'].toLowerCase() !== 'graph') { + logme( + 'ERROR: Function "output" attribute can be ' + + 'either "element", "plot_label", "none" 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['@point_size'], + obj['@fill_area'], + obj['@bar'], + obj['@disable_auto_return'] + ); + } + + function addFunction(funcString, color, line, dot, label, + pointSize, fillArea, bar, disableAutoReturn) { + + var newFunctionObject, func, paramNames, c1, rgxp; + + // The main requirement is function string. Without it we can't + // create a function, and the series cannot be calculated. + if (typeof funcString !== 'string') { + return; + } + + // Make sure that any HTML entities that were escaped will be + // unescaped. This is done because if a string with escaped + // HTML entities is passed to the Function() constructor, it + // will break. + funcString = $('
').html(funcString).text(); + + // If the user did not specifically turn off this feature, + // check if the function string contains a 'return', and + // prepend a 'return ' to the string if one, or more, is not + // found. + if ( + (disableAutoReturn === undefined) || + ( + (typeof disableAutoReturn === 'string') && + (disableAutoReturn.toLowerCase() !== 'true') + ) + ) { + if (funcString.search(/return/i) === -1) { + funcString = 'return ' + funcString; + } + } else { + if (funcString.search(/return/i) === -1) { + logme( + 'ERROR: You have specified a JavaScript ' + + 'function without a "return" statemnt. Your ' + + 'function will return "undefined" by default.' + ); + } + } + + // Some defaults. If no options are set for the graph, we will + // make sure that at least a line is drawn for a function. + newFunctionObject = { + 'line': true, + 'dot': false, + 'bars': false + }; + + // Get all of the parameter names defined by the user in the + // XML. + paramNames = state.getAllParameterNames(); + + // The 'x' is always one of the function parameters. + paramNames.push('x'); + + // Must make sure that the function body also gets passed to + // the Function constructor. + paramNames.push(funcString); + + // Create the function from the function string, and all of the + // available parameters AND the 'x' variable as it's parameters. + // For this we will use the built-in Function object + // constructor. + // + // If something goes wrong during this step, most + // likely the user supplied an invalid JavaScript function body + // string. In this case we will not proceed. + try { + func = Function.apply(null, paramNames); + } catch (err) { + logme( + 'ERROR: The function body "' + + funcString + + '" was not converted by the Function constructor.' + ); + logme('Error message: "' + err.message + '"'); + + $('#' + gstId).html('
' + 'ERROR IN XML: Could not create a function from the string "' + funcString + '".' + '
'); + $('#' + gstId).append('
' + 'Error message: "' + err.message + '".' + '
'); + + paramNames.pop(); + paramNames.pop(); + + return; + } + + // Return the array back to original state. Remember that it is + // a pointer to original array which is stored in state object. + paramNames.pop(); + paramNames.pop(); + + newFunctionObject['func'] = func; + + if (typeof color === 'string') { + newFunctionObject['color'] = color; + } + + if (typeof line === 'string') { + if (line.toLowerCase() === 'true') { + newFunctionObject['line'] = true; + } else if (line.toLowerCase() === 'false') { + newFunctionObject['line'] = false; + } + } + + if (typeof dot === 'string') { + if (dot.toLowerCase() === 'true') { + newFunctionObject['dot'] = true; + } else if (dot.toLowerCase() === 'false') { + newFunctionObject['dot'] = false; + } + } + + if (typeof pointSize === 'string') { + newFunctionObject['pointSize'] = pointSize; + } + + if (typeof bar === 'string') { + if (bar.toLowerCase() === 'true') { + newFunctionObject['bars'] = true; + } else if (bar.toLowerCase() === 'false') { + newFunctionObject['bars'] = false; + } + } + + if (newFunctionObject['bars'] === true) { + newFunctionObject['line'] = false; + newFunctionObject['dot'] = false; + // To do: See if need to do anything here. + } else if ( + (newFunctionObject['dot'] === false) && + (newFunctionObject['line'] === false) + ) { + newFunctionObject['line'] = true; + } + + if (newFunctionObject['line'] === true) { + if (typeof fillArea === 'string') { + if (fillArea.toLowerCase() === 'true') { + newFunctionObject['fillArea'] = true; + } else if (fillArea.toLowerCase() === 'false') { + newFunctionObject['fillArea'] = false; + } else { + logme('ERROR: The attribute fill_area should be either "true" or "false".'); + logme('fill_area = "' + fillArea + '".'); + + return; + } + } + } + + if (typeof label === 'string') { + + newFunctionObject.specialLabel = false; + newFunctionObject.pldeHash = []; + + // Let's check the label against all of the plde objects. + // plde is an abbreviation for Plot Label Dynamic Elements. + for (c1 = 0; c1 < state.plde.length; c1 += 1) { + rgxp = new RegExp(state.plde[c1].elId, 'g'); + + // If we find a dynamic element in the label, we will + // hash the current plde object, and indicate that this + // is a special label. + if (rgxp.test(label) === true) { + newFunctionObject.specialLabel = true; + newFunctionObject.pldeHash.push(state.plde[c1]); + } + } + + newFunctionObject.label = label; + } else { + newFunctionObject.label = false; + } + + functions.push(newFunctionObject); + } + } + + // The callback that will be called whenever a constant changes (gets + // updated via a slider or a text input). + function onUpdatePlot(event) { + if (generateData() === true) { + updatePlot(); + } + } + + function generateData() { + var c0, c1, c3, functionObj, seriesObj, dataPoints, paramValues, x, y, + start, end, step, numNotUndefined; + + paramValues = state.getAllParameterValues(); + + dataSeries = []; + + for (c0 = 0; c0 < functions.length; c0 += 1) { + functionObj = functions[c0]; + + try { + start = xrange.min.apply(window, paramValues); + } catch (err) { + logme('ERROR: Could not determine xrange start.'); + logme('Error message: "' + err.message + '".'); + + $('#' + gstId).html('
' + 'ERROR IN XML: Could not determine xrange start from defined function.' + '
'); + $('#' + gstId).append('
' + 'Error message: "' + err.message + '".' + '
'); + + return false; + } + try { + end = xrange.max.apply(window, paramValues); + } catch (err) { + logme('ERROR: Could not determine xrange end.'); + logme('Error message: "' + err.message + '".'); + + $('#' + gstId).html('
' + 'ERROR IN XML: Could not determine xrange end from defined function.' + '
'); + $('#' + gstId).append('
' + 'Error message: "' + err.message + '".' + '
'); + + return false; + } + + seriesObj = {}; + dataPoints = []; + + // For counting number of points added. In the end we will + // compare this number to 'numPoints' specified in the config + // JSON. + c1 = 0; + + step = (end - start) / (numPoints - 1); + + // Generate the data points. + for (x = start; x <= end; x += step) { + + // Push the 'x' variable to the end of the parameter array. + paramValues.push(x); + + // We call the user defined function, passing all of the + // available parameter values. Inside this function they + // will be accessible by their names. + try { + y = functionObj.func.apply(window, paramValues); + } catch (err) { + logme('ERROR: Could not generate data.'); + logme('Error message: "' + err.message + '".'); + + $('#' + gstId).html('
' + 'ERROR IN XML: Could not generate data from defined function.' + '
'); + $('#' + gstId).append('
' + 'Error message: "' + err.message + '".' + '
'); + + return false; + } + + // Return the paramValues array to how it was before we + // added 'x' variable to the end of it. + paramValues.pop(); + + // Add the generated point to the data points set. + dataPoints.push([x, y]); + + c1 += 1; + + } + + // If the last point did not get included because of rounding + // of floating-point number addition, then we will include it + // manually. + if (c1 != numPoints) { + x = end; + paramValues.push(x); + try { + y = functionObj.func.apply(window, paramValues); + } catch (err) { + logme('ERROR: Could not generate data.'); + logme('Error message: "' + err.message + '".'); + + $('#' + gstId).html('
' + 'ERROR IN XML: Could not generate data from function.' + '
'); + $('#' + gstId).append('
' + 'Error message: "' + err.message + '".' + '
'); + + return false; + } + paramValues.pop(); + dataPoints.push([x, y]); + } + + // Put the entire data points set into the series object. + seriesObj.data = dataPoints; + + // See if user defined a specific color for this function. + if (functionObj.hasOwnProperty('color') === true) { + seriesObj.color = functionObj.color; + } + + // See if a user defined a label for this function. + if (functionObj.label !== false) { + if (functionObj.specialLabel === true) { + (function (c1) { + var tempLabel; + + tempLabel = functionObj.label; + + while (c1 < functionObj.pldeHash.length) { + tempLabel = tempLabel.replace( + functionObj.pldeHash[c1].elId, + functionObj.pldeHash[c1].func.apply( + window, + state.getAllParameterValues() + ) + ); + + c1 += 1; + } + + seriesObj.label = tempLabel; + }(0)); + } else { + seriesObj.label = functionObj.label; + } + } + + // Should the data points be connected by a line? + seriesObj.lines = { + 'show': functionObj.line + }; + + if (functionObj.hasOwnProperty('fillArea') === true) { + seriesObj.lines.fill = functionObj.fillArea; + } + + // Should each data point be represented by a point on the + // graph? + seriesObj.points = { + 'show': functionObj.dot + }; + + seriesObj.bars = { + 'show': functionObj.bars, + 'barWidth': graphBarWidth + }; + + if (graphBarAlign !== null) { + seriesObj.bars.align = graphBarAlign; + } + + 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); + } + + if (graphBarAlign === null) { + for (c0 = 0; c0 < numPoints; c0 += 1) { + // Number of points that have a value other than 'undefined' (undefined). + numNotUndefined = 0; + + for (c1 = 0; c1 < dataSeries.length; c1 += 1) { + if (dataSeries[c1].bars.show === false) { + continue; + } + + if (isFinite(parseInt(dataSeries[c1].data[c0][1])) === true) { + numNotUndefined += 1; + } + } + + c3 = 0; + for (c1 = 0; c1 < dataSeries.length; c1 += 1) { + if (dataSeries[c1].bars.show === false) { + continue; + } + + dataSeries[c1].data[c0][0] -= graphBarWidth * (0.5 * numNotUndefined - c3); + + if (isFinite(parseInt(dataSeries[c1].data[c0][1])) === true) { + c3 += 1; + } + } + } + } + + for (c0 = 0; c0 < asymptotes.length; c0 += 1) { + + // If the user defined a label for this asympote, then the + // property 'label' will be a string (in the other case it is + // a boolean value 'false'). We will create an empty data set, + // and add to it a label. This solution is a bit _wrong_ , but + // it will have to do for now. Flot JS does not provide a way + // to add labels to markings, and we use markings to generate + // asymptotes. + if (asymptotes[c0].label !== false) { + dataSeries.push({ + 'data': [], + 'label': asymptotes[c0].label, + 'color': asymptotes[c0].color + }); + } + + } + + return true; + } // End-of: function generateData + + function updatePlot() { + var paramValues, plotObj; + + paramValues = state.getAllParameterValues(); + + if (xaxis.tickFormatter !== null) { + xaxis.ticks = null; + } + + if (yaxis.tickFormatter !== null) { + yaxis.ticks = null; + } + + // Tell Flot to draw the graph to our specification. + plotObj = $.plot( + plotDiv, + dataSeries, + { + 'xaxis': xaxis, + 'yaxis': yaxis, + 'legend': { + + // To show the legend or not. Note, even if 'show' is + // 'true', the legend will only show if labels are + // provided for at least one of the series that are + // going to be plotted. + 'show': true, + + // A floating point number in the range [0, 1]. The + // smaller the number, the more transparent will the + // legend background become. + 'backgroundOpacity': 0 + + }, + 'grid': { + 'markings': generateMarkings() + } + } + ); + + updateMovingLabels(); + + // The first time that the graph gets added to the page, the legend + // is created from scratch. When it appears, MathJax works some + // magic, and all of the specially marked TeX gets rendered nicely. + // The next time when we update the graph, no such thing happens. + // We must ask MathJax to typeset the legend again (well, we will + // ask it to look at our entire graph DIV), the next time it's + // worker queue is available. + MathJax.Hub.Queue([ + 'Typeset', + MathJax.Hub, + plotDiv.attr('id') + ]); + + return; + + function updateMovingLabels() { + var c1, labelCoord, pointOffset; + + for (c1 = 0; c1 < movingLabels.length; c1 += 1) { + if (movingLabels[c1].el === null) { + movingLabels[c1].el = $( + '
' + + movingLabels[c1].labelText + + '
' + ); + movingLabels[c1].el.css('position', 'absolute'); + movingLabels[c1].el.css('color', movingLabels[c1].fontColor); + movingLabels[c1].el.css('font-weight', movingLabels[c1].fontWeight); + movingLabels[c1].el.appendTo(plotDiv); + + movingLabels[c1].elWidth = movingLabels[c1].el.width(); + movingLabels[c1].elHeight = movingLabels[c1].el.height(); + } else { + movingLabels[c1].el.detach(); + movingLabels[c1].el.appendTo(plotDiv); + } + + labelCoord = movingLabels[c1].func.apply(window, paramValues); + + pointOffset = plotObj.pointOffset({'x': labelCoord.x, 'y': labelCoord.y}); + + movingLabels[c1].el.css('left', pointOffset.left - 0.5 * movingLabels[c1].elWidth); + movingLabels[c1].el.css('top', pointOffset.top - 0.5 * movingLabels[c1].elHeight); + } + } + + // Generate markings to represent asymptotes defined by the user. + // See the following function for more details: + // + // function processAsymptote() + // + function generateMarkings() { + var c1, asymptote, markings, val; + + markings = []; + + for (c1 = 0; c1 < asymptotes.length; c1 += 1) { + asymptote = asymptotes[c1]; + + try { + val = asymptote.func.apply(window, paramValues); + } catch (err) { + logme('ERROR: Could not generate value from asymptote function.'); + logme('Error message: ', err.message); + + continue; + } + + if (asymptote.type === 'x') { + markings.push({ + 'color': asymptote.color, + 'lineWidth': 2, + 'xaxis': { + 'from': val, + 'to': val + } + }); + } else { + markings.push({ + 'color': asymptote.color, + 'lineWidth': 2, + 'yaxis': { + 'from': val, + 'to': val + } + }); + + } + } + + return markings; + } + } + + function xAxisTickFormatter(val, axis) { + if (xTicksNames.hasOwnProperty(val.toFixed(axis.tickDecimals)) === true) { + return xTicksNames[val.toFixed(axis.tickDecimals)]; + } + + return ''; + } + + function yAxisTickFormatter(val, axis) { + if (yTicksNames.hasOwnProperty(val.toFixed(axis.tickDecimals)) === true) { + return yTicksNames[val.toFixed(axis.tickDecimals)]; + } + + return ''; + } + } + + +}); + +// 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/gst.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js new file mode 100644 index 0000000000..1434d05f70 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js @@ -0,0 +1,20 @@ +/* + * We will add a function that will be called for all GraphicalSliderTool + * xmodule module instances. It must be available globally by design of + * xmodule. + */ +window.GraphicalSliderTool = function (el) { + // All the work will be performed by the GstMain module. We will get access + // to it, and all it's dependencies, via Require JS. Currently Require JS + // is namespaced and is available via a global object RequireJS. + RequireJS.require(['GstMain'], function (GstMain) { + // The GstMain module expects the DOM ID of a Graphical Slider Tool + // element. Since we are given a
element which might in + // theory contain multiple graphical_slider_tool
elements (each + // with a unique DOM ID), we will iterate over all children, and for + // each match, we will call GstMain module. + $(el).children('.graphical_slider_tool').each(function (index, value) { + GstMain($(value).attr('id')); + }); + }); +}; 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 new file mode 100644 index 0000000000..34b54b4216 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -0,0 +1,84 @@ +// 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( + 'GstMain', + + // 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', 'ElOutput', 'GLabelElOutput', 'logme'], + function (State, GeneralMethods, Sliders, Inputs, Graph, ElOutput, GLabelElOutput, logme) { + + return GstMain; + + function GstMain(gstId) { + var config, gstClass, state; + + if ($('#' + gstId).attr('data-processed') !== 'processed') { + $('#' + gstId).attr('data-processed', 'processed'); + } else { + logme('MESSAGE: Already processed GST with ID ' + gstId + '. Skipping.'); + + return; + } + + // Get the JSON configuration, parse it, and store as an object. + try { + config = JSON.parse($('#' + gstId + '_json').html()).root; + } catch (err) { + logme('ERROR: could not parse config JSON.'); + logme('$("#" + gstId + "_json").html() = ', $('#' + gstId + '_json').html()); + logme('JSON.parse(...) = ', JSON.parse($('#' + gstId + '_json').html())); + logme('config = ', config); + + return; + } + + // Get the class name of the GST. All elements are assigned a class + // name that is based on the class name of the GST. For example, inputs + // are assigned a class name '{GST class name}_input'. + if (typeof config['@class'] !== 'string') { + logme('ERROR: Could not get the class name of GST.'); + logme('config["@class"] = ', config['@class']); + + return; + } + gstClass = config['@class']; + + // Parse the configuration settings for parameters, and store them in a + // state object. + state = State(gstId, config); + + // It is possible that something goes wrong while extracting parameters + // from the JSON config object. In this case, we will not continue. + if (state === undefined) { + logme('ERROR: The state object was not initialized properly.'); + + return; + } + + // Create the sliders and the text inputs, attaching them to + // appropriate parameters. + Sliders(gstId, state); + Inputs(gstId, gstClass, state); + + // Configure functions that output to an element instead of the graph. + ElOutput(config, state); + + // Configure functions that output to an element instead of the graph + // label. + GLabelElOutput(config, state); + + // 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); + } +}); + +// 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/inputs.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js new file mode 100644 index 0000000000..a04ed113ec --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js @@ -0,0 +1,88 @@ +// 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('Inputs', ['logme'], function (logme) { + return Inputs; + + function Inputs(gstId, gstClass, state) { + var c1, paramName, allParamNames; + + allParamNames = state.getAllParameterNames(); + + for (c1 = 0; c1 < allParamNames.length; c1 += 1) { + $('#' + gstId).find('.' + gstClass + '_input').each(function (index, value) { + var inputDiv, paramName; + + paramName = allParamNames[c1]; + inputDiv = $(value); + + if (paramName === inputDiv.data('var')) { + createInput(inputDiv, paramName); + } + }); + } + + return; + + function createInput(inputDiv, paramName) { + var paramObj; + + paramObj = state.getParamObj(paramName); + + // Check that the retrieval went OK. + if (paramObj === undefined) { + logme('ERROR: Could not get a paramObj for parameter "' + paramName + '".'); + + return; + } + + // Bind a function to the 'change' event. Whenever the user changes + // the value of this text input, and presses 'enter' (or clicks + // somewhere else on the page), this event will be triggered, and + // our callback will be called. + inputDiv.bind('change', inputOnChange); + + inputDiv.val(paramObj.value); + + // Lets style the input element nicely. We will use the button() + // widget for this since there is no native widget for the text + // input. + inputDiv.button().css({ + 'font': 'inherit', + 'color': 'inherit', + 'text-align': 'left', + 'outline': 'none', + 'cursor': 'text', + 'height': '15px' + }); + + // Tell the parameter object from state that we are attaching a + // text input to it. Next time the parameter will be updated with + // a new value, tis input will also be updated. + paramObj.inputDivs.push(inputDiv); + + return; + + // Update the 'state' - i.e. set the value of the parameter this + // input is attached to to a new value. + // + // This will cause the plot to be redrawn each time after the user + // changes the value in the input. Note that he has to either press + // 'Enter', or click somewhere else on the page in order for the + // 'change' event to be tiggered. + function inputOnChange(event) { + var inputDiv; + + inputDiv = $(this); + state.setParameterValue(paramName, inputDiv.val(), inputDiv); + } + } + } +}); + +// 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/jstat-1.0.0.min.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/jstat-1.0.0.min.js new file mode 100644 index 0000000000..7f9cd4a124 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/jstat-1.0.0.min.js @@ -0,0 +1,236 @@ +function jstat(){} +j=jstat;(function(){var initializing=false,fnTest=/xyz/.test(function(){xyz;})?/\b_super\b/:/.*/;this.Class=function(){};Class.extend=function(prop){var _super=this.prototype;initializing=true;var prototype=new this();initializing=false;for(var name in prop){prototype[name]=typeof prop[name]=="function"&&typeof _super[name]=="function"&&fnTest.test(prop[name])?(function(name,fn){return function(){var tmp=this._super;this._super=_super[name];var ret=fn.apply(this,arguments);this._super=tmp;return ret;};})(name,prop[name]):prop[name];} +function Class(){if(!initializing&&this.init) +this.init.apply(this,arguments);} +Class.prototype=prototype;Class.constructor=Class;Class.extend=arguments.callee;return Class;};})();jstat.ONE_SQRT_2PI=0.3989422804014327;jstat.LN_SQRT_2PI=0.9189385332046727417803297;jstat.LN_SQRT_PId2=0.225791352644727432363097614947;jstat.DBL_MIN=2.22507e-308;jstat.DBL_EPSILON=2.220446049250313e-16;jstat.SQRT_32=5.656854249492380195206754896838;jstat.TWO_PI=6.283185307179586;jstat.DBL_MIN_EXP=-999;jstat.SQRT_2dPI=0.79788456080287;jstat.LN_SQRT_PI=0.5723649429247;jstat.seq=function(min,max,length){var r=new Range(min,max,length);return r.getPoints();} +jstat.dnorm=function(x,mean,sd,log){if(mean==null)mean=0;if(sd==null)sd=1;if(log==null)log=false;var n=new NormalDistribution(mean,sd);if(!isNaN(x)){return n._pdf(x,log);}else if(x.length){var res=[];for(var i=0;i
');$('#'+hash).dialog({modal:false,width:475,height:475,resizable:true,resize:function(){$.plot($('#graph-'+hash),[series],flotOpt);},open:function(event,ui){var id='#graph-'+hash;$.plot($('#graph-'+hash),[series],flotOpt);}})} +jstat.log10=function(arg){return Math.log(arg)/Math.LN10;} +jstat.toSigFig=function(num,n){if(num==0){return 0;} +var d=Math.ceil(jstat.log10(num<0?-num:num));var power=n-parseInt(d);var magnitude=Math.pow(10,power);var shifted=Math.round(num*magnitude);return shifted/magnitude;} +jstat.trunc=function(x){return(x>0)?Math.floor(x):Math.ceil(x);} +jstat.isFinite=function(x){return(!isNaN(x)&&(x!=Number.POSITIVE_INFINITY)&&(x!=Number.NEGATIVE_INFINITY));} +jstat.dopois_raw=function(x,lambda,give_log){if(lambda==0){if(x==0){return(give_log)?0.0:1.0;} +return(give_log)?Number.NEGATIVE_INFINITY:0.0;} +if(!jstat.isFinite(lambda))return(give_log)?Number.NEGATIVE_INFINITY:0.0;if(x<0)return(give_log)?Number.NEGATIVE_INFINITY:0.0;if(x<=lambda*jstat.DBL_MIN){return(give_log)?-lambda:Math.exp(-lambda);} +if(lambda0.1*(x+np)){v=(x-np)/(x+np);s=(x-np)*v;ej=2*x*v;v=v*v;for(j=1;;j++){ej*=v;s1=s+ej/((j<<1)+1);if(s1==s) +return(s1);s=s1;}} +return(x*Math.log(x/np)+np-x);} +jstat.stirlerr=function(n){var S0=0.083333333333333333333;var S1=0.00277777777777777777778;var S2=0.00079365079365079365079365;var S3=0.000595238095238095238095238;var S4=0.0008417508417508417508417508;var sferr_halves=[0.0,0.1534264097200273452913848,0.0810614667953272582196702,0.0548141210519176538961390,0.0413406959554092940938221,0.03316287351993628748511048,0.02767792568499833914878929,0.02374616365629749597132920,0.02079067210376509311152277,0.01848845053267318523077934,0.01664469118982119216319487,0.01513497322191737887351255,0.01387612882307074799874573,0.01281046524292022692424986,0.01189670994589177009505572,0.01110455975820691732662991,0.010411265261972096497478567,0.009799416126158803298389475,0.009255462182712732917728637,0.008768700134139385462952823,0.008330563433362871256469318,0.007934114564314020547248100,0.007573675487951840794972024,0.007244554301320383179543912,0.006942840107209529865664152,0.006665247032707682442354394,0.006408994188004207068439631,0.006171712263039457647532867,0.005951370112758847735624416,0.005746216513010115682023589,0.005554733551962801371038690];var nn;if(n<=15.0){nn=n+n;if(nn==parseInt(nn))return(sferr_halves[parseInt(nn)]);return(jstat.lgamma(n+1.0)-(n+0.5)*Math.log(n)+n-jstat.LN_SQRT_2PI);} +nn=n*n;if(n>500)return((S0-S1/nn)/n);if(n>80)return((S0-(S1-S2/nn)/nn)/n);if(n>35)return((S0-(S1-(S2-S3/nn)/nn)/nn)/n);return((S0-(S1-(S2-(S3-S4/nn)/nn)/nn)/nn)/n);} +jstat.lgamma=function(x){function lgammafn_sign(x,sgn){var ans,y,sinpiy;var xmax=2.5327372760800758e+305;var dxrel=1.490116119384765696e-8;if(sgn!=null)sgn=1;if(isNaN(x))return x;if(x<0&&(Math.floor(-x)%2.0)==0) +if(sgn!=null)sgn=-1;if(x<=0&&x==jstat.trunc(x)){console.warn("Negative integer argument in lgammafn_sign");return Number.POSITIVE_INFINITY;} +y=Math.abs(x);if(y<=10)return Math.log(Math.abs(jstat.gamma(x)));if(y>xmax){console.warn("Illegal arguement passed to lgammafn_sign");return Number.POSITIVE_INFINITY;} +if(x>0){if(x>1e17){return(x*(Math.log(x)-1.0));}else if(x>4934720.0){return(jstat.LN_SQRT_2PI+(x-0.5)*Math.log(x)-x);}else{return jstat.LN_SQRT_2PI+(x-0.5)*Math.log(x)-x+jstat.lgammacor(x);}} +sinpiy=Math.abs(Math.sin(Math.PI*y));if(sinpiy==0){throw"Should never happen!!";} +ans=jstat.LN_SQRT_PId2+(x-0.5)*Math.log(y)-x-Math.log(sinpiy)-jstat.lgammacor(y);if(Math.abs((x-jstat.trunc(x-0.5))*ans/x)=jstat.DBL_MIN){res=1.0/y;}else{return(Number.POSITIVE_INFINITY);}}else if(y<12.0){yi=y;if(y<1.0){z=y;y+=1.0;}else{n=parseInt(y)-1;y-=parseFloat(n);z=y-1.0;} +xnum=0.0;xden=1.0;for(i=0;i<8;++i){xnum=(xnum+p[i])*z;xden=xden*z+q[i];} +res=xnum/xden+1.0;if(yiy){for(i=0;i=xmax){throw"Underflow error in lgammacor";}else if(xMAXIT){console.warn("a or b too big, or MAXIT too small in betacf: "+a+", "+b+", "+x+", "+h);return h;} +if(isNaN(h)){console.warn(a+", "+b+", "+x);} +return h;} +var bt;if(x<0.0||x>1.0){throw"bad x in routine incompleteBeta";} +if(x==0.0||x==1.0){bt=0.0;}else{bt=Math.exp(jstat.lgamma(a+b)-jstat.lgamma(a)-jstat.lgamma(b)+a*Math.log(x)+b*Math.log(1.0-x));} +if(x<(a+1.0)/(a+b+2.0)){return bt*betacf(a,b,x)/a;}else{return 1.0-bt*betacf(b,a,1.0-x)/b;}} +jstat.chebyshev=function(x,a,n){var b0,b1,b2,twox;var i;if(n<1||n>1000)return Number.NaN;if(x<-1.1||x>1.1)return Number.NaN;twox=x*2;b2=b1=0;b0=0;for(i=1;i<=n;i++){b2=b1;b1=b0;b0=twox*b1-b2+a[n-i];} +return(b0-b2)*0.5;} +jstat.fmin2=function(x,y){return(x1){return Math.log(1+x);} +for(var i=1;i0.697)return Math.exp(x)-1;if(a>1e-8){y=Math.exp(x)-1;}else{y=(x/2+1)*x;} +y-=(1+y)*(jstat.log1p(y)-x);return y;} +jstat.logBeta=function(a,b){var corr,p,q;p=q=a;if(bq)q=b;if(p<0){console.warn('Both arguements must be >= 0');return Number.NaN;} +else if(p==0){return Number.POSITIVE_INFINITY;} +else if(!jstat.isFinite(q)){return Number.NEGATIVE_INFINITY;} +if(p>=10){corr=jstat.lgammacor(p)+jstat.lgammacor(q)-jstat.lgammacor(p+q);return Math.log(q)*-0.5+jstat.LN_SQRT_2PI+corr ++(p-0.5)*Math.log(p/(p+q))+q*jstat.log1p(-p/(p+q));} +else if(q>=10){corr=jstat.lgammacor(q)-jstat.lgammacor(p+q);return jstat.lgamma(p)+corr+p-p*Math.log(p+q) ++(q-0.5)*jstat.log1p(-p/(p+q));} +else +return Math.log(jstat.gamma(p)*(jstat.gamma(q)/jstat.gamma(p+q)));} +jstat.dbinom_raw=function(x,n,p,q,give_log){if(give_log==null)give_log=false;var lf,lc;if(p==0){if(x==0){return(give_log)?0.0:1.0;}else{return(give_log)?Number.NEGATIVE_INFINITY:0.0;}} +if(q==0){if(x==n){return(give_log)?0.0:1.0;}else{return(give_log)?Number.NEGATIVE_INFINITY:0.0;}} +if(x==0){if(n==0)return(give_log)?0.0:1.0;lc=(p<0.1)?-jstat.bd0(n,n*q)-n*p:n*Math.log(q);return(give_log)?lc:Math.exp(lc);} +if(x==n){lc=(q<0.1)?-jstat.bd0(n,n*p)-n*q:n*Math.log(p);return(give_log)?lc:Math.exp(lc);} +if(x<0||x>n)return(give_log)?Number.NEGATIVE_INFINITY:0.0;lc=jstat.stirlerr(n)-jstat.stirlerr(x)-jstat.stirlerr(n-x)-jstat.bd0(x,n*p)-jstat.bd0(n-x,n*q);lf=Math.log(jstat.TWO_PI)+Math.log(x)+jstat.log1p(-x/n);return(give_log)?lc-0.5*lf:Math.exp(lc-0.5*lf);} +jstat.max=function(values){var max=Number.NEGATIVE_INFINITY;for(var i=0;imax){max=values[i];}} +return max;} +var Range=Class.extend({init:function(min,max,numPoints){this._minimum=parseFloat(min);this._maximum=parseFloat(max);this._numPoints=parseFloat(numPoints);},getMinimum:function(){return this._minimum;},getMaximum:function(){return this._maximum;},getNumPoints:function(){return this._numPoints;},getPoints:function(){var results=[];var x=this._minimum;var step=(this._maximum-this._minimum)/(this._numPoints-1);for(var i=0;ieps){xsq=x*x;xnum=a[4]*xsq;xden=xsq;for(i=0;i<3;++i){xnum=(xnum+a[i])*xsq;xden=(xden+b[i])*xsq;}}else{xnum=xden=0.0;} +temp=x*(xnum+a[3])/(xden+b[3]);if(lower)cum=0.5+temp;if(upper)ccum=0.5-temp;if(log_p){if(lower)cum=Math.log(cum);if(upper)ccum=Math.log(ccum);}}else if(y<=jstat.SQRT_32){xnum=c[8]*y;xden=y;for(i=0;i<7;++i){xnum=(xnum+c[i])*y;xden=(xden+d[i])*y;} +temp=(xnum+c[7])/(xden+d[7]);xsq=jstat.trunc(x*16)/16;del=(x-xsq)*(x+xsq);if(log_p){cum=(-xsq*xsq*0.5)+(-del*0.5)+Math.log(temp);if((lower&&x>0.)||(upper&&x<=0.)) +ccum=jstat.log1p(-Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp);} +else{cum=Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp;ccum=1.0-cum;} +if(x>0.0){temp=cum;if(lower){cum=ccum;} +ccum=temp;}} +else if((log_p&&y<1e170)||(lower&&-37.51930.)||(upper&&x<=0.)) +ccum=jstat.log1p(-Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp);} +else{cum=Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp;ccum=1.0-cum;} +if(x>0.0){temp=cum;if(lower){cum=ccum;} +ccum=temp;}}else{if(x>0){cum=(log_p)?0.0:1.0;ccum=(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{cum=(log_p)?Number.NEGATIVE_INFINITY:0.0;ccum=(log_p)?0.0:1.0;}} +return[cum,ccum];} +var p,cp;var mu=this._mean;var sigma=this._sigma;var R_DT_0,R_DT_1;if(lower_tail){if(log_p){R_DT_0=Number.NEGATIVE_INFINITY;R_DT_1=0.0;}else{R_DT_0=0.0;R_DT_1=1.0;}}else{if(log_p){R_DT_0=0.0;R_DT_1=Number.NEGATIVE_INFINITY;}else{R_DT_0=1.0;R_DT_1=0.0;}} +if(!jstat.isFinite(x)&&mu==x)return Number.NaN;if(sigma<=0){if(sigma<0){console.warn("Sigma is less than 0");return Number.NaN;} +return(x0){var nd=new NormalDistribution(meanlog,sdlog);return nd._cdf(Math.log(x),lower_tail,log_p);} +if(lower_tail){return(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{return(log_p)?0.0:1.0;}},getLocation:function(){return this._location;},getScale:function(){return this._scale;},getMean:function(){return Math.exp((this._location+this._scale)/2);},getVariance:function(){var ans=(Math.exp(this._scale)-1)*Math.exp(2*this._location+this._scale);return ans;}});var GammaDistribution=ContinuousDistribution.extend({init:function(shape,scale){this._super('Gamma');this._shape=parseFloat(shape);this._scale=parseFloat(scale);this._string="Gamma ("+this._shape.toFixed(2)+", "+this._scale.toFixed(2)+")";},_pdf:function(x,give_log){var pr;var shape=this._shape;var scale=this._scale;if(give_log==null){give_log=false;} +if(shape<0||scale<=0){throw"Illegal argument in _pdf";} +if(x<0){return(give_log)?Number.NEGATIVE_INFINITY:0.0;} +if(shape==0){return(x==0)?Number.POSITIVE_INFINITY:(give_log)?Number.NEGATIVE_INFINITY:0.0;} +if(x==0){if(shape<1)return Number.POSITIVE_INFINITY;if(shape>1)return(give_log)?Number.NEGATIVE_INFINITY:0.0;return(give_log)?-Math.log(scale):1/scale;} +if(shape<1){pr=jstat.dopois_raw(shape,x/scale,give_log);return give_log?pr+Math.log(shape/x):pr*shape/x;} +pr=jstat.dopois_raw(shape-1,x/scale,give_log);return give_log?pr-Math.log(scale):pr/scale;},_cdf:function(x,lower_tail,log_p){function USE_PNORM(){pn1=Math.sqrt(alph)*3.0*(Math.pow(x/alph,1.0/3.0)+1.0/(9.0*alph)-1.0);var norm_dist=new NormalDistribution(0.0,1.0);return norm_dist._cdf(pn1,lower_tail,log_p);} +if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;var alph=this._shape;var scale=this._scale;var xbig=1.0e+8;var xlarge=1.0e+37;var alphlimit=1e5;var pn1,pn2,pn3,pn4,pn5,pn6,arg,a,b,c,an,osum,sum,n,pearson;if(alph<=0.||scale<=0.){console.warn('Invalid gamma params in _cdf');return Number.NaN;} +x/=scale;if(isNaN(x))return x;if(x<=0.0){if(lower_tail){return(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{return(log_p)?0.0:1.0;}} +if(alph>alphlimit){return USE_PNORM();} +if(x>xbig*alph){if(x>jstat.DBL_MAX*alph){if(lower_tail){return(log_p)?0.0:1.0;}else{return(log_p)?Number.NEGATIVE_INFINITY:0.0;}}else{return USE_PNORM();}} +if(x<=1.0||xjstat.DBL_EPSILON*sum);}else{pearson=0;arg=alph*Math.log(x)-x-jstat.lgamma(alph);a=1.-alph;b=a+x+1.;pn1=1.;pn2=x;pn3=x+1.;pn4=x*b;sum=pn3/pn4;for(n=1;;n++){a+=1.;b+=2.;an=a*n;pn5=b*pn3-an*pn1;pn6=b*pn4-an*pn2;if(Math.abs(pn6)>0.){osum=sum;sum=pn5/pn6;if(Math.abs(osum-sum)<=jstat.DBL_EPSILON*jstat.fmin2(1.0,sum)) +break;} +pn1=pn3;pn2=pn4;pn3=pn5;pn4=pn6;if(Math.abs(pn5)>=xlarge){pn1/=xlarge;pn2/=xlarge;pn3/=xlarge;pn4/=xlarge;}}} +arg+=Math.log(sum);lower_tail=(lower_tail==pearson);if(log_p&&lower_tail) +return(arg);if(lower_tail){return Math.exp(arg);}else{if(log_p){return(arg>-Math.LN2)?Math.log(-jstat.expm1(arg)):jstat.log1p(-Math.exp(arg));}else{return-jstat.expm1(arg);}}},getShape:function(){return this._shape;},getScale:function(){return this._scale;},getMean:function(){return this._shape*this._scale;},getVariance:function(){return this._shape*Math.pow(this._scale,2);}});var BetaDistribution=ContinuousDistribution.extend({init:function(alpha,beta){this._super('Beta');this._alpha=parseFloat(alpha);this._beta=parseFloat(beta);this._string="Beta ("+this._alpha.toFixed(2)+", "+this._beta.toFixed(2)+")";},_pdf:function(x,give_log){if(give_log==null)give_log=false;var a=this._alpha;var b=this._beta;var lval;if(a<=0||b<=0){console.warn('Illegal arguments in _pdf');return Number.NaN;} +if(x<0||x>1){return(give_log)?Number.NEGATIVE_INFINITY:0.0;} +if(x==0){if(a>1){return(give_log)?Number.NEGATIVE_INFINITY:0.0;} +if(a<1){return Number.POSITIVE_INFINITY;} +return(give_log)?Math.log(b):b;} +if(x==1){if(b>1){return(give_log)?Number.NEGATIVE_INFINITY:0.0;} +if(b<1){return Number.POSITIVE_INFINITY;} +return(give_log)?Math.log(a):a;} +if(a<=2||b<=2){lval=(a-1)*Math.log(x)+(b-1)*jstat.log1p(-x)-jstat.logBeta(a,b);}else{lval=Math.log(a+b-1)+jstat.dbinom_raw(a-1,a+b-2,x,1-x,true);} +return(give_log)?lval:Math.exp(lval);},_cdf:function(x,lower_tail,log_p){if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;var pin=this._alpha;var qin=this._beta;if(pin<=0||qin<=0){console.warn('Invalid argument in _cdf');return Number.NaN;} +if(x<=0){if(lower_tail){return(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{return(log_p)?0.1:1.0;}} +if(x>=1){if(lower_tail){return(log_p)?0.1:1.0;}else{return(log_p)?Number.NEGATIVE_INFINITY:0.0;}} +return jstat.incompleteBeta(pin,qin,x);},getAlpha:function(){return this._alpha;},getBeta:function(){return this._beta;},getMean:function(){return this._alpha/(this._alpha+this._beta);},getVariance:function(){var ans=(this._alpha*this._beta)/(Math.pow(this._alpha+this._beta,2)*(this._alpha+this._beta+1));return ans;}});var StudentTDistribution=ContinuousDistribution.extend({init:function(degreesOfFreedom,mu){this._super('StudentT');this._dof=parseFloat(degreesOfFreedom);if(mu!=null){this._mu=parseFloat(mu);this._string="StudentT ("+this._dof.toFixed(2)+", "+this._mu.toFixed(2)+")";}else{this._mu=0.0;this._string="StudentT ("+this._dof.toFixed(2)+")";}},_pdf:function(x,give_log){if(give_log==null)give_log=false;if(this._mu==null){return this._dt(x,give_log);}else{var y=this._dnt(x,give_log);if(y>1){console.warn('x:'+x+', y: '+y);} +return y;}},_cdf:function(x,lower_tail,give_log){if(lower_tail==null)lower_tail=true;if(give_log==null)give_log=false;if(this._mu==null){return this._pt(x,lower_tail,give_log);}else{return this._pnt(x,lower_tail,give_log);}},_dt:function(x,give_log){var t,u;var n=this._dof;if(n<=0){console.warn('Invalid parameters in _dt');return Number.NaN;} +if(!jstat.isFinite(x)){return(give_log)?Number.NEGATIVE_INFINITY:0.0;} +if(!jstat.isFinite(n)){var norm=new NormalDistribution(0.0,1.0);return norm.density(x,give_log);} +t=-jstat.bd0(n/2.0,(n+1)/2.0)+jstat.stirlerr((n+1)/2.0)-jstat.stirlerr(n/2.0);if(x*x>0.2*n) +u=Math.log(1+x*x/n)*n/2;else +u=-jstat.bd0(n/2.0,(n+x*x)/2.0)+x*x/2.0;var p1=jstat.TWO_PI*(1+x*x/n);var p2=t-u;return(give_log)?-0.5*Math.log(p1)+p2:Math.exp(p2)/Math.sqrt(p1);},_dnt:function(x,give_log){if(give_log==null)give_log=false;var df=this._dof;var ncp=this._mu;var u;if(df<=0.0){console.warn("Illegal arguments _dnf");return Number.NaN;} +if(ncp==0.0){return this._dt(x,give_log);} +if(!jstat.isFinite(x)){if(give_log){return Number.NEGATIVE_INFINITY;}else{return 0.0;}} +if(!isFinite(df)||df>1e8){var dist=new NormalDistribution(ncp,1.);return dist.density(x,give_log);} +if(Math.abs(x)>Math.sqrt(df*jstat.DBL_EPSILON)){var newT=new StudentTDistribution(df+2,ncp);u=Math.log(df)-Math.log(Math.abs(x))+ +Math.log(Math.abs(newT._pnt(x*Math.sqrt((df+2)/df),true,false)- +this._pnt(x,true,false)));} +else{u=jstat.lgamma((df+1)/2)-jstat.lgamma(df/2) +-.5*(Math.log(Math.PI)+Math.log(df)+ncp*ncp);} +return(give_log?u:Math.exp(u));},_pt:function(x,lower_tail,log_p){if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;var val,nx;var n=this._dof;var DT_0,DT_1;if(lower_tail){if(log_p){DT_0=Number.NEGATIVE_INFINITY;DT_1=1.;}else{DT_0=0.;DT_1=1.;}}else{if(log_p){DT_0=0.;DT_1=Number.NEGATIVE_INFINITY;}else{DT_0=1.;DT_1=0.;}} +if(n<=0.0){console.warn("Invalid T distribution _pt");return Number.NaN;} +var norm=new NormalDistribution(0,1);if(!jstat.isFinite(x)){return(x<0)?DT_0:DT_1;} +if(!jstat.isFinite(n)){return norm._cdf(x,lower_tail,log_p);} +if(n>4e5){val=1./(4.*n);return norm._cdf(x*(1.-val)/sqrt(1.+x*x*2.*val),lower_tail,log_p);} +nx=1+(x/n)*x;if(nx>1e100){var lval;lval=-0.5*n*(2*Math.log(Math.abs(x))-Math.log(n)) +-jstat.logBeta(0.5*n,0.5)-Math.log(0.5*n);val=log_p?lval:Math.exp(lval);}else{if(n>x*x){var beta=new BetaDistribution(0.5,n/2.);return beta._cdf(x*x/(n+x*x),false,log_p);}else{beta=new BetaDistribution(n/2.,0.5);return beta._cdf(1./nx,true,log_p);}} +if(x<=0.) +lower_tail=!lower_tail;if(log_p){if(lower_tail)return jstat.log1p(-0.5*Math.exp(val));else return val-M_LN2;} +else{val/=2.;if(lower_tail){return(0.5-val+0.5);}else{return val;}}},_pnt:function(t,lower_tail,log_p){var dof=this._dof;var ncp=this._mu;var DT_0,DT_1;if(lower_tail){if(log_p){DT_0=Number.NEGATIVE_INFINITY;DT_1=1.;}else{DT_0=0.;DT_1=1.;}}else{if(log_p){DT_0=0.;DT_1=Number.NEGATIVE_INFINITY;}else{DT_0=1.;DT_1=0.;}} +var albeta,a,b,del,errbd,lambda,rxb,tt,x;var geven,godd,p,q,s,tnc,xeven,xodd;var it,negdel;var ITRMAX=1000;var ERRMAX=1.e-7;if(dof<=0.0){return Number.NaN;}else if(dof==0.0){return this._pt(t);} +if(!jstat.isFinite(t)){return(t<0)?DT_0:DT_1;} +if(t>=0.){negdel=false;tt=t;del=ncp;}else{if(ncp>=40&&(!log_p||!lower_tail)){return DT_0;} +negdel=true;tt=-t;del=-ncp;} +if(dof>4e5||del*del>2*Math.LN2*(-(jstat.DBL_MIN_EXP))){s=1./(4.*dof);var norm=new NormalDistribution(del,Math.sqrt(1.+tt*tt*2.*s));var result=norm._cdf(tt*(1.-s),lower_tail!=negdel,log_p);return result;} +x=t*t;rxb=dof/(x+dof);x=x/(x+dof);if(x>0.){lambda=del*del;p=.5*Math.exp(-.5*lambda);if(p==0.){console.warn("underflow in _pnt");return DT_0;} +q=jstat.SQRT_2dPI*p*del;s=.5-p;if(s<1e-7){s=-0.5*jstat.expm1(-0.5*lambda);} +a=.5;b=.5*dof;rxb=Math.pow(rxb,b);albeta=jstat.LN_SQRT_PI+jstat.lgamma(b)-jstat.lgamma(.5+b);xodd=jstat.incompleteBeta(a,b,x);godd=2.*rxb*Math.exp(a*Math.log(x)-albeta);tnc=b*x;xeven=(tnc1)break;errbd=2.*s*(xodd-godd);if(Math.abs(errbd)1-1e-10&&lower_tail){console.warn("precision error _pnt");} +var res=jstat.fmin2(tnc,1.);if(lower_tail){if(log_p){return Math.log(res);}else{return res;}}else{if(log_p){return jstat.log1p(-(res));}else{return(0.5-(res)+0.5);}}},getDegreesOfFreedom:function(){return this._dof;},getNonCentralityParameter:function(){return this._mu;},getMean:function(){if(this._dof>1){var ans=(1/2)*Math.log(this._dof/2)+jstat.lgamma((this._dof-1)/2)-jstat.lgamma(this._dof/2) +return Math.exp(ans)*this._mu;}else{return Number.NaN;}},getVariance:function(){if(this._dof>2){var ans=this._dof*(1+this._mu*this._mu)/(this._dof-2)-(((this._mu*this._mu*this._dof)/2)*Math.pow(Math.exp(jstat.lgamma((this._dof-1)/2)-jstat.lgamma(this._dof/2)),2));return ans;}else{return Number.NaN;}}});var Plot=Class.extend({init:function(id,options){this._container='#'+String(id);this._plots=[];this._flotObj=null;this._locked=false;if(options!=null){this._options=options;}else{this._options={};}},getContainer:function(){return this._container;},getGraph:function(){return this._flotObj;},setData:function(data){this._plots=data;},clear:function(){this._plots=[];},showLegend:function(){this._options.legend={show:true} +this.render();},hideLegend:function(){this._options.legend={show:false} +this.render();},render:function(){this._flotObj=null;this._flotObj=$.plot($(this._container),this._plots,this._options);}});var DistributionPlot=Plot.extend({init:function(id,distribution,range,options){this._super(id,options);this._showPDF=true;this._showCDF=false;this._pdfValues=[];this._cdfValues=[];this._maxY=1;this._plotType='line';this._fill=false;this._distribution=distribution;if(range!=null&&Range.validate(range)){this._range=range;}else{this._range=this._distribution.getRange();} +if(this._distribution!=null){this._maxY=this._generateValues();}else{this._options.xaxis={min:range.getMinimum(),max:range.getMaximum()} +this._options.yaxis={max:1}} +this.render();},setHover:function(bool){if(bool){if(this._options.grid==null){this._options.grid={hoverable:true,mouseActiveRadius:25}}else{this._options.grid.hoverable=true,this._options.grid.mouseActiveRadius=25} +function showTooltip(x,y,contents,color){$('
'+contents+'
').css({position:'absolute',display:'none',top:y+15,'font-size':'small',left:x+5,border:'1px solid '+color[1],color:color[2],padding:'5px','background-color':color[0],opacity:0.80}).appendTo("body").show();} +var previousPoint=null;$(this._container).bind("plothover",function(event,pos,item){$("#x").text(pos.x.toFixed(2));$("#y").text(pos.y.toFixed(2));if(item){if(previousPoint!=item.datapoint){previousPoint=item.datapoint;$("#jstat_tooltip").remove();var x=jstat.toSigFig(item.datapoint[0],2),y=jstat.toSigFig(item.datapoint[1],2);var text=null;var color=item.series.color;if(item.series.label=='PDF'){text="P("+x+") = "+y;color=["#fee","#fdd","#C05F5F"];}else{text="F("+x+") = "+y;color=["#eef","#ddf","#4A4AC0"];} +showTooltip(item.pageX,item.pageY,text,color);}} +else{$("#jstat_tooltip").remove();previousPoint=null;}});$(this._container).bind("mouseleave",function(){if($('#jstat_tooltip').is(':visible')){$('#jstat_tooltip').remove();previousPoint=null;}});}else{if(this._options.grid==null){this._options.grid={hoverable:false}}else{this._options.grid.hoverable=false} +$(this._container).unbind("plothover");} +this.render();},setType:function(type){this._plotType=type;var lines={};var points={};if(this._plotType=='line'){lines.show=true;points.show=false;}else if(this._plotType=='points'){lines.show=false;points.show=true;}else if(this._plotType=='both'){lines.show=true;points.show=true;} +if(this._options.series==null){this._options.series={lines:lines,points:points}}else{if(this._options.series.lines==null){this._options.series.lines=lines;}else{this._options.series.lines.show=lines.show;} +if(this._options.series.points==null){this._options.series.points=points;}else{this._options.series.points.show=points.show;}} +this.render();},setFill:function(bool){this._fill=bool;if(this._options.series==null){this._options.series={lines:{fill:bool}}}else{if(this._options.series.lines==null){this._options.series.lines={fill:bool}}else{this._options.series.lines.fill=bool;}} +this.render();},clear:function(){this._super();this._distribution=null;this._pdfValues=[];this._cdfValues=[];this.render();},_generateValues:function(){this._cdfValues=[];this._pdfValues=[];var xs=this._range.getPoints();this._options.xaxis={min:xs[0],max:xs[xs.length-1]} +var pdfs=this._distribution.density(this._range);var cdfs=this._distribution.cumulativeDensity(this._range);for(var i=0;i 1) { + logme('ERROR: Found more than one slider for the parameter "' + paramName + '".'); + logme('sliderDiv.length = ', sliderDiv.length); + } else { + logme('MESSAGE: Did not find a slider for the parameter "' + paramName + '".'); + } + } + + function createSlider(sliderDiv, paramName) { + var paramObj; + + paramObj = state.getParamObj(paramName); + + // Check that the retrieval went OK. + if (paramObj === undefined) { + logme('ERROR: Could not get a paramObj for parameter "' + paramName + '".'); + + return; + } + + // Create a jQuery UI slider from the slider DIV. We will set + // starting parameters, and will also attach a handler to update + // the 'state' on the 'slide' event. + sliderDiv.slider({ + 'min': paramObj.min, + 'max': paramObj.max, + 'value': paramObj.value, + 'step': paramObj.step + }); + + // Tell the parameter object stored in state that we have a slider + // that is attached to it. Next time when the parameter changes, it + // will also update the value of this slider. + paramObj.sliderDiv = sliderDiv; + + // Atach callbacks to update the slider's parameter. + paramObj.sliderDiv.on('slide', sliderOnSlide); + paramObj.sliderDiv.on('slidechange', sliderOnChange); + + return; + + // Update the 'state' - i.e. set the value of the parameter this + // slider is attached to to a new value. + // + // This will cause the plot to be redrawn each time after the user + // drags the slider handle and releases it. + function sliderOnSlide(event, ui) { + // Last parameter passed to setParameterValue() will be 'true' + // so that the function knows we are a slider, and it can + // change the our value back in the case when the new value is + // invalid for some reason. + if (state.setParameterValue(paramName, ui.value, sliderDiv, true, 'slide') === undefined) { + logme('ERROR: Could not update the parameter named "' + paramName + '" with the value "' + ui.value + '".'); + } + } + + function sliderOnChange(event, ui) { + if (state.setParameterValue(paramName, ui.value, sliderDiv, true, 'change') === undefined) { + logme('ERROR: Could not update the parameter named "' + paramName + '" with the value "' + ui.value + '".'); + } + } + } + } +}); + +// 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/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js new file mode 100644 index 0000000000..8b534fd19d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -0,0 +1,395 @@ +// 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('State', ['logme'], function (logme) { + var stateInst; + + // Since there will be (can be) multiple GST on a page, and each will have + // a separate state, we will create a factory constructor function. The + // constructor will expect the ID of the DIV with the GST contents, and the + // configuration object (parsed from a JSON string). It will return an + // object containing methods to set and get the private state properties. + + stateInst = 0; + + // This module defines and returns a factory constructor. + return State; + + function State(gstId, config) { + var parameters, allParameterNames, allParameterValues, + plotDiv, dynamicEl, dynamicElByElId; + + dynamicEl = []; + dynamicElByElId = {}; + + stateInst += 1; + logme('MESSAGE: Creating state instance # ' + stateInst + '.'); + + // Initially, there are no parameters to track. So, we will instantiate + // an empty object. + // + // As we parse the JSON config object, we will add parameters as + // named properties. For example + // + // parameters.a = {...}; + // + // will be created for the parameter 'a'. + parameters = {}; + + // Check that the required parameters config object is available. + if ($.isPlainObject(config.parameters) === false) { + logme('ERROR: Expected config.parameters to be an object. It is not.'); + logme('config.parameters = ', config.parameters); + + return; + } + + // If config.parameters.param is an array, pass it to the processor + // element by element. + if ($.isArray(config.parameters.param) === true) { + (function (c1) { + while (c1 < config.parameters.param.length) { + processParameter(config.parameters.param[c1]); + c1 += 1; + } + }(0)); + } + + // If config.parameters.param is an object, pass this object to the + // processor directly. + else if ($.isPlainObject(config.parameters.param) === true) { + processParameter(config.parameters.param); + } + + // If config.parameters.param is some other type, report an error and + // do not continue. + else { + logme('ERROR: config.parameters.param is of an unsupported type.'); + logme('config.parameters.param = ', config.parameters.param); + + return; + } + + // Instead of building these arrays every time when some component + // requests them, we will create them in the beginning, and then update + // each element individually when some parameter's value changes. + // + // Then we can just return the required array, instead of iterating + // over all of the properties of the 'parameters' object, and + // extracting their names/values one by one. + allParameterNames = []; + allParameterValues = []; + + // Populate 'allParameterNames', and 'allParameterValues' with data. + generateHelperArrays(); + + // The constructor will return an object with methods to operate on + // it's private properties. + return { + 'getParameterValue': getParameterValue, + 'setParameterValue': setParameterValue, + + 'getParamObj': getParamObj, + + 'getAllParameterNames': getAllParameterNames, + 'getAllParameterValues': getAllParameterValues, + + 'bindUpdatePlotEvent': bindUpdatePlotEvent, + 'addDynamicEl': addDynamicEl, + + // plde is an abbreviation for Plot Label Dynamic Elements. + plde: [] + }; + + function getAllParameterNames() { + return allParameterNames; + } + + function getAllParameterValues() { + return allParameterValues; + } + + function getParamObj(paramName) { + if (parameters.hasOwnProperty(paramName) === false) { + logme('ERROR: Object parameters does not have a property named "' + paramName + '".'); + + return; + } + + return parameters[paramName]; + } + + function bindUpdatePlotEvent(newPlotDiv, callback) { + plotDiv = newPlotDiv; + + plotDiv.bind('update_plot', callback); + } + + function addDynamicEl(el, func, elId, updateOnEvent) { + var newLength; + + newLength = dynamicEl.push({ + 'el': el, + 'func': func, + 'elId': elId, + 'updateOnEvent': updateOnEvent + }); + + if (typeof dynamicElByElId[elId] !== 'undefined') { + logme( + 'ERROR: Duplicate dynamic element ID "' + elId + '" found.' + ); + } else { + dynamicElByElId[elId] = dynamicEl[newLength - 1]; + } + } + + function getParameterValue(paramName) { + + // If the name of the constant is not tracked by state, return an + // 'undefined' value. + if (parameters.hasOwnProperty(paramName) === false) { + logme('ERROR: Object parameters does not have a property named "' + paramName + '".'); + + return; + } + + return parameters[paramname].value; + } + + // #################################################################### + // + // Function: setParameterValue(paramName, paramValue, element) + // -------------------------------------------------- + // + // + // This function can be called from a callback, registered by a slider + // or a text input, when specific events ('slide' or 'change') are + // triggered. + // + // The 'paramName' is the name of the parameter in 'parameters' object + // whose value must be updated to the new value of 'paramValue'. + // + // Before we update the value, we must check that: + // + // 1.) the parameter named as 'paramName' actually exists in the + // 'parameters' object; + // 2.) the value 'paramValue' is a valid floating-point number, and + // it lies within the range specified by the 'min' and 'max' + // properties of the stored parameter object. + // + // If 'paramName' and 'paramValue' turn out to be valid, we will update + // the stored value in the parameter with the new value, and also + // update all of the text inputs and the slider that correspond to this + // parameter (if any), so that they reflect the new parameter's value. + // Finally, the helper array 'allParameterValues' will also be updated + // to reflect the change. + // + // If something went wrong (for example the new value is outside the + // allowed range), then we will reset the 'element' to display the + // original value. + // + // #################################################################### + function setParameterValue(paramName, paramValue, element, slider, updateOnEvent) { + var paramValueNum, c1; + + // If a parameter with the name specified by the 'paramName' + // parameter is not tracked by state, do not do anything. + if (parameters.hasOwnProperty(paramName) === false) { + logme('ERROR: Object parameters does not have a property named "' + paramName + '".'); + + return; + } + + // Try to convert the passed value to a valid floating-point + // number. + paramValueNum = parseFloat(paramValue); + + // We are interested only in valid float values. NaN, -INF, + // +INF we will disregard. + if (isFinite(paramValueNum) === false) { + logme('ERROR: New parameter value is not a floating-point number.'); + logme('paramValue = ', paramValue); + + return; + } + + if (paramValueNum < parameters[paramName].min) { + paramValueNum = parameters[paramName].min; + } else if (paramValueNum > parameters[paramName].max) { + paramValueNum = parameters[paramName].max; + } + + parameters[paramName].value = paramValueNum; + + // Update all text inputs with the new parameter's value. + for (c1 = 0; c1 < parameters[paramName].inputDivs.length; c1 += 1) { + parameters[paramName].inputDivs[c1].val(paramValueNum); + } + + // Update the single slider with the new parameter's value. + if ((slider === false) && (parameters[paramName].sliderDiv !== null)) { + parameters[paramName].sliderDiv.slider('value', paramValueNum); + } + + // Update the helper array with the new parameter's value. + allParameterValues[parameters[paramName].helperArrayIndex] = paramValueNum; + + for (c1 = 0; c1 < dynamicEl.length; c1++) { + if ( + ((updateOnEvent !== undefined) && (dynamicEl[c1].updateOnEvent === updateOnEvent)) || + (updateOnEvent === undefined) + ) { + // If we have a DOM element, call the function "paste" the answer into the DIV. + if (dynamicEl[c1].el !== null) { + dynamicEl[c1].el.html(dynamicEl[c1].func.apply(window, allParameterValues)); + } + // If we DO NOT have an element, simply call the function. The function can then + // manipulate all the DOM elements it wants, without the fear of them being overwritten + // by us afterwards. + else { + dynamicEl[c1].func.apply(window, allParameterValues); + } + } + } + + // If we have a plot DIV to work with, tell to update. + if (plotDiv !== undefined) { + plotDiv.trigger('update_plot'); + } + + return true; + } // End-of: function setParameterValue + + // #################################################################### + // + // Function: processParameter(obj) + // ------------------------------- + // + // + // This function will be run once for each instance of a GST when + // parsing the JSON config object. + // + // 'newParamObj' must be empty from the start for each invocation of + // this function, that's why we will declare it locally. + // + // We will parse the passed object 'obj' and populate the 'newParamObj' + // object with required properties. + // + // Since there will be many properties that are of type floating-point + // number, we will have a separate function for parsing them. + // + // processParameter() will fail right away if 'obj' does not have a + // '@var' property which represents the name of the parameter we want + // to process. + // + // If, after all of the properties have been processed, we reached the + // end of the function successfully, the 'newParamObj' will be added to + // the 'parameters' object (that is defined in the scope of State() + // function) as a property named as the name of the parameter. + // + // If at least one of the properties from 'obj' does not get correctly + // parsed, then the parameter represented by 'obj' will be disregarded. + // It will not be available to user-defined plotting functions, and + // things will most likely break. We will notify the user about this. + // + // #################################################################### + function processParameter(obj) { + var paramName, newParamObj; + + if (typeof obj['@var'] !== 'string') { + logme('ERROR: Expected obj["@var"] to be a string. It is not.'); + logme('obj["@var"] = ', obj['@var']); + + return; + } + + paramName = obj['@var']; + newParamObj = {}; + + if ( + (processFloat('@min', 'min') === false) || + (processFloat('@max', 'max') === false) || + (processFloat('@step', 'step') === false) || + (processFloat('@initial', 'value') === false) + ) { + logme('ERROR: A required property is missing. Not creating parameter "' + paramName + '"'); + + return; + } + + // Pointers to text input and slider DIV elements that this + // parameter will be attached to. Initially there are none. When we + // will create text inputs and sliders, we will update these + // properties. + newParamObj.inputDivs = []; + newParamObj.sliderDiv = null; + + // Everything went well, so save the new parameter object. + parameters[paramName] = newParamObj; + + return; + + function processFloat(attrName, newAttrName) { + var attrValue; + + if (typeof obj[attrName] !== 'string') { + logme('ERROR: Expected obj["' + attrName + '"] to be a string. It is not.'); + logme('obj["' + attrName + '"] = ', obj[attrName]); + + return false; + } else { + attrValue = parseFloat(obj[attrName]); + + if (isFinite(attrValue) === false) { + logme('ERROR: Expected obj["' + attrName + '"] to be a valid floating-point number. It is not.'); + logme('obj["' + attrName + '"] = ', obj[attrName]); + + return false; + } + } + + newParamObj[newAttrName] = attrValue; + + return true; + } // End-of: function processFloat + } // End-of: function processParameter + + // #################################################################### + // + // Function: generateHelperArrays() + // ------------------------------- + // + // + // Populate 'allParameterNames' and 'allParameterValues' with data. + // Link each parameter object with the corresponding helper array via + // an index 'helperArrayIndex'. It will be the same for both of the + // arrays. + // + // NOTE: It is important to remember to update these helper arrays + // whenever a new parameter is added (or one is removed), or when a + // parameter's value changes. + // + // #################################################################### + function generateHelperArrays() { + var paramName, c1; + + c1 = 0; + for (paramName in parameters) { + allParameterNames.push(paramName); + allParameterValues.push(parameters[paramName].value); + + parameters[paramName].helperArrayIndex = c1; + + c1 += 1; + } + } + } // End-of: function State +}); + +// 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/tests/test_export.py b/common/lib/xmodule/xmodule/tests/test_export.py index aeebc6da6b..7605155a6c 100644 --- a/common/lib/xmodule/xmodule/tests/test_export.py +++ b/common/lib/xmodule/xmodule/tests/test_export.py @@ -39,9 +39,12 @@ def strip_filenames(descriptor): class RoundTripTestCase(unittest.TestCase): - '''Check that our test courses roundtrip properly''' + ''' Check that our test courses roundtrip properly. + Same course imported , than exported, then imported again. + And we compare original import with second import (after export). + Thus we make sure that export and import work properly. + ''' def check_export_roundtrip(self, data_dir, course_dir): - root_dir = path(mkdtemp()) print "Copying test course to temp dir {0}".format(root_dir) @@ -117,3 +120,7 @@ class RoundTripTestCase(unittest.TestCase): def test_selfassessment_roundtrip(self): #Test selfassessment xmodule to see if it exports correctly self.check_export_roundtrip(DATA_DIR,"self_assessment") + + def test_graphicslidertool_roundtrip(self): + #Test graphicslidertool xmodule to see if it exports correctly + self.check_export_roundtrip(DATA_DIR,"graphic_slider_tool") diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py index 77532959d7..90ec112f19 100644 --- a/common/lib/xmodule/xmodule/tests/test_import.py +++ b/common/lib/xmodule/xmodule/tests/test_import.py @@ -352,3 +352,19 @@ class ImportTestCase(unittest.TestCase): sa_sample = modulestore.get_instance(sa_id, location) #10 attempts is hard coded into SampleQuestion, which is the url_name of a selfassessment xml tag self.assertEqual(sa_sample.metadata['attempts'], '10') + + def test_graphicslidertool_import(self): + ''' + Check to see if definition_from_xml in gst_module.py + works properly. Pulls data from the graphic_slider_tool directory + in the test data directory. + ''' + modulestore = XMLModuleStore(DATA_DIR, course_dirs=['graphic_slider_tool']) + + sa_id = "edX/gst_test/2012_Fall" + location = Location(["i4x", "edX", "gst_test", "graphical_slider_tool", "sample_gst"]) + gst_sample = modulestore.get_instance(sa_id, location) + render_string_from_sample_gst_xml = """ + \ +""".strip() + self.assertEqual(gst_sample.definition['render'], render_string_from_sample_gst_xml) diff --git a/common/test/data/graphic_slider_tool/README.md b/common/test/data/graphic_slider_tool/README.md new file mode 100644 index 0000000000..ec4f121ad8 --- /dev/null +++ b/common/test/data/graphic_slider_tool/README.md @@ -0,0 +1,2 @@ +This is a very very simple course, useful for debugging graphical slider tool +code. diff --git a/common/test/data/graphic_slider_tool/course.xml b/common/test/data/graphic_slider_tool/course.xml new file mode 120000 index 0000000000..49041310f6 --- /dev/null +++ b/common/test/data/graphic_slider_tool/course.xml @@ -0,0 +1 @@ +roots/2012_Fall.xml \ No newline at end of file diff --git a/common/test/data/graphic_slider_tool/course/2012_Fall.xml b/common/test/data/graphic_slider_tool/course/2012_Fall.xml new file mode 100644 index 0000000000..2983c85dd5 --- /dev/null +++ b/common/test/data/graphic_slider_tool/course/2012_Fall.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/common/test/data/graphic_slider_tool/graphical_slider_tool/sample_gst.xml b/common/test/data/graphic_slider_tool/graphical_slider_tool/sample_gst.xml new file mode 100644 index 0000000000..bd0360fde8 --- /dev/null +++ b/common/test/data/graphic_slider_tool/graphical_slider_tool/sample_gst.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + return Math.sqrt(a * a - x * x); + return -Math.sqrt(a * a - x * x); + + + + + + return -a; + + + return a; + + + 1000 + -30, 6, 30 + -30, 6, 30 + + + + diff --git a/common/test/data/graphic_slider_tool/policies/2012_Fall.json b/common/test/data/graphic_slider_tool/policies/2012_Fall.json new file mode 100644 index 0000000000..6958f8432c --- /dev/null +++ b/common/test/data/graphic_slider_tool/policies/2012_Fall.json @@ -0,0 +1,14 @@ +{ + "course/2012_Fall": { + "graceperiod": "2 days 5 hours 59 minutes 59 seconds", + "start": "2015-07-17T12:00", + "display_name": "GST Test", + "graded": "false" + }, + "chapter/Overview": { + "display_name": "Overview" + }, + "graphical_slider_tool/sample_gst": { + "display_name": "Sample GST", + }, +} diff --git a/common/test/data/graphic_slider_tool/roots/2012_Fall.xml b/common/test/data/graphic_slider_tool/roots/2012_Fall.xml new file mode 100644 index 0000000000..1dc86c4afc --- /dev/null +++ b/common/test/data/graphic_slider_tool/roots/2012_Fall.xml @@ -0,0 +1 @@ + diff --git a/common/test/data/self_assessment/course.xml b/common/test/data/self_assessment/course.xml deleted file mode 120000 index 49041310f6..0000000000 --- a/common/test/data/self_assessment/course.xml +++ /dev/null @@ -1 +0,0 @@ -roots/2012_Fall.xml \ No newline at end of file diff --git a/common/test/data/self_assessment/course.xml b/common/test/data/self_assessment/course.xml new file mode 100644 index 0000000000..ea7d5c420d --- /dev/null +++ b/common/test/data/self_assessment/course.xml @@ -0,0 +1 @@ + diff --git a/docs/source/graphical_slider_tool.rst b/docs/source/graphical_slider_tool.rst new file mode 100644 index 0000000000..37b17136e8 --- /dev/null +++ b/docs/source/graphical_slider_tool.rst @@ -0,0 +1,563 @@ +********************************************* +Xml format of graphical slider tool [xmodule] +********************************************* + +.. module:: xml_format_gst + + +Format description +================== + +Graphical slider tool (GST) main tag is:: + + BODY + +``graphical_slider_tool`` tag must have two children tags: ``render`` +and ``configuration``. + + +Render tag +---------- + +Render tag can contain usual html tags mixed with some GST specific tags:: + + - represents jQuery slider for changing a parameter's value + - represents a text input field for changing a parameter's value + - represents Flot JS plot element + +Also GST will track all elements inside ```` where ``id`` +attribute is set, and a corresponding parameter referencing that ``id`` is present +in the configuration section below. These will be referred to as dynamic elements. + +The contents of the section will be shown to the user after +all occurrences of:: + + + + + +have been converted to actual sliders, text inputs, and a plot graph. +Everything in square brackets is optional. After initialization, all +text input fields, sliders, and dynamic elements will be set to the initial +values of the parameters that they are assigned to. + +``{parameter name}`` specifies the parameter to which the slider or text +input will be attached to. + +[style="{CSS statements}"] specifies valid CSS styling. It will be passed +directly to the browser without any parsing. + +There is a one-to-one relationship between a slider and a parameter. +I.e. for one parameter you can put only one ```` in the +```` section. However, you don't have to specify a slider - they +are optional. + +There is a many-to-one relationship between text inputs and a +parameter. I.e. for one parameter you can put many '' elements in +the ```` section. However, you don't have to specify a text +input - they are optional. + +You can put only one ```` in the ```` section. It is not +required. + + +Slider tag +.......... + +Slider tag must have ``var`` attribute and optional ``style`` attribute:: + + + +After processing, slider tags will be replaced by jQuery UI sliders with applied +``style`` attribute. + +``var`` attribute must correspond to a parameter. Parameters can be used in any +of the ``function`` tags in ``functions`` tag. By moving slider, value of +parameter ``a`` will change, and so result of function, that depends on parameter +``a``, will also change. + + +Textbox tag +........... + +Texbox tag must have ``var`` attribute and optional ``style`` attribute:: + + + +After processing, textbox tags will be replaced by html text inputs with applied +``style`` attribute. If you want a readonly text input, then you should use a +dynamic element instead (see section below "HTML tagsd with ID"). + +``var`` attribute must correspond to a parameter. Parameters can be used in any +of the ``function`` tags in ``functions`` tag. By changing the value on the text input, +value of parameter ``a`` will change, and so result of function, that depends on +parameter ``a``, will also change. + + +Plot tag +........ + +Plot tag may have optional ``style`` attribute:: + + + +After processing plot tags will be replaced by Flot JS plot with applied +``style`` attribute. + + +HTML tags with ID (dynamic elements) +.................................... + +Any HTML tag with ID, e.g. ```` can be used as a +place where result of function can be inserted. To insert function result to +an element, element ID must be included in ``function`` tag as ``el_id`` attribute +and ``output`` value must be ``"element"``:: + + + function add(a, b, precision) { + var x = Math.pow(10, precision || 2); + return (Math.round(a * x) + Math.round(b * x)) / x; + } + + return add(a, b, 5); + + + +Configuration tag +----------------- + +The configuration tag contains parameter settings, graph +settings, and function definitions which are to be plotted on the +graph and that use specified parameters. + +Configuration tag contains two mandatory tag ``functions`` and ``parameters`` and +may contain another ``plot`` tag. + + +Parameters tag +.............. + +``Parameters`` tag contains ``parameter`` tags. Each ``parameter`` tag must have +``var``, ``max``, ``min``, ``step`` and ``initial`` attributes:: + + + + + + +``var`` attribute links min, max, step and initial values to parameter name. + +``min`` attribute is the minimal value that a parameter can take. Slider and input +values can not go below it. + +``max`` attribute is the maximal value that a parameter can take. Slider and input +values can not go over it. + +``step`` attribute is value of slider step. When a slider increase or decreases +the specified parameter, it will do so by the amount specified with 'step' + +``initial`` attribute is the initial value that the specified parameter should be +set to. Sliders and inputs will initially show this value. + +The parameter's name is specified by the ``var`` property. All occurrences +of sliders and/or text inputs that specify a ``var`` property, will be +connected to this parameter - i.e. they will reflect the current +value of the parameter, and will be updated when the parameter +changes. + +If at lest one of these attributes is not set, then the parameter +will not be used, slider's and/or text input elements that specify +this parameter will not be activated, and the specified functions +which use this parameter will not return a numeric value. This means +that neglecting to specify at least one of the attributes for some +parameter will have the result of the whole GST instance not working +properly. + + +Functions tag +............. + +For the GST to do something, you must defined at least one +function, which can use any of the specified parameter values. The +function expects to take the ``x`` value, do some calculations, and +return the ``y`` value. I.e. this is a 2D plot in Cartesian +coordinates. This is how the default function is meant to be used for +the graph. + +There are other special cases of functions. They are used mainly for +outputting to elements, plot labels, or for custom output. Because +the return a single value, and that value is meant for a single element, +these function are invoked only with the set of all of the parameters. +I.e. no ``x`` value is available inside them. They are useful for +showing the current value of a parameter, showing complex static +formulas where some parameter's value must change, and other useful +things. + +The different style of function is specified by the ``output`` attribute. + +Each function must be defined inside ``function`` tag in ``functions`` tag:: + + + + function add(a, b, precision) { + var x = Math.pow(10, precision || 2); + return (Math.round(a * x) + Math.round(b * x)) / x; + } + + return add(a, b, 5); + + + +The parameter names (along with their values, as provided from text +inputs and/or sliders), will be available inside all defined +functions. A defined function body string will be parsed internally +by the browser's JavaScript engine and converted to a true JS +function. + +The function's parameter list will automatically be created and +populated, and will include the ``x`` (when ``output`` is not specified or +is set to ``"graph"``), and all of the specified parameter values (from sliders +and text inputs). This means that each of the defined functions will have +access to all of the parameter values. You don't have to use them, but +they will be there. + +Examples:: + + + return x; + + + + return (x + a) * Math.sin(x * b); + + + + function helperFunc(c1) { + return c1 * c1 - a; + } + return helperFunc(x + 10 * a * b) + Math.sin(a - x); + + +Required parameters:: + + function body: + + A string composing a normal JavaScript function + except that there is no function declaration + (along with parameters), and no closing bracket. + + So if you normally would have written your + JavaScript function like this: + + function myFunc(x, a, b) { + return x * a + b; + } + + here you must specify just the function body + (everything that goes between '{' and '}'). So, + you would specify the above function like so (the + bare-bone minimum): + + return x * a + b; + + VERY IMPORTANT: Because the function will be passed + to the browser as a single string, depending on implementation + specifics, the end-of-line characters can be stripped. This + means that single line JavaScript comments (starting with "//") + can lead to the effect that everything after the first such comment + will be treated as a comment. Therefore, it is absolutely + necessary that such single line comments are not used when + defining functions for GST. You can safely use the alternative + multiple line JavaScript comments (such comments start with "/*" + and end with "*/). + + VERY IMPORTANT: If you have a large function body, and decide to + split it into several lines, than you must wrap it in "CDATA" like + so: + + + + + +Optional parameters:: + + + color: Color name ('red', 'green', etc.) or in the form of + '#FFFF00'. If not specified, a default color (different + one for each graphed function) will be given by Flot JS. + line: A string - 'true' or 'false'. Should the data points be + connected by a line on the graph? Default is 'true'. + dot: A string - 'true' or 'false'. Should points be shown for + each data point on the graph? Default is 'false'. + bar: A string - 'true' or 'false'. When set to 'true', points + will be plotted as bars. + label: A string. If provided, will be shown in the legend, along + with the color that was used to plot the function. + output: 'element', 'none', 'plot_label', or 'graph'. If not defined, + function will be plotted (same as setting 'output' to 'graph'). + If defined, and other than 'graph', function will not be + plotted, but it's output will be inserted into the element + with ID specified by 'el_id' attribute. + el_id: Id of HTML element, defined in '' section. Value of + function will be inserted as content of this element. + disable_auto_return: By default, if JavaScript function string is written + without a "return" statement, the "return" will be + prepended to it. Set to "true" to disable this + functionality. This is done so that simple functions + can be defined in an easy fashion (for example, "a", + which will be translated into "return a"). + update_on: A string - 'change', or 'slide'. Default (if not set) is + 'slide'. This defines the event on which a given function is + called, and its result is inserted into an element. This + setting is relevant only when "output" is other than "graph". + +When specifying ``el_id``, it is essential to set "output" to one of + element - GST will invoke the function, and the return of it will be + inserted into a HTML element with id specified by ``el_id``. + none - GST will simply inoke the function. It is left to the instructor + who writes the JavaScript function body to update all necesary + HTML elements inside the function, before it exits. This is done + so that extra steps can be preformed after an HTML element has + been updated with a value. Note, that because the return value + from this function is not actually used, it will be tempting to + omit the "return" statement. However, in this case, the attribute + "disable_auto_return" must be set to "true" in order to prevent + GST from inserting a "return" statement automatically. + plot_label - GST will process all plot labels (which are strings), and + will replace the all instances of substrings specified by + ``el_id`` with the returned value of the function. This is + necessary if you want a label in the graph to have some changing + number. Because of the nature of Flot JS, it is impossible to + achieve the same effect by setting the "output" attribute + to "element", and including a HTML element in the label. + +The above values for "output" will tell GST that the function is meant for an +HTML element (not for graph), and that it should not get an 'x' parameter (along +with some value). + + +[Note on MathJax and labels] +............................ + +Independently of this module, will render all TeX code +within the ```` section into nice mathematical formulas. Just +remember to wrap it in one of:: + + \( and \) - for inline formulas (formulas surrounded by + standard text) + \[ and \] - if you want the formula to be a separate line + +It is possible to define a label in standard TeX notation. The JS +library MathJax will work on these labels also because they are +inserted on top of the plot as standard HTML (text within a DIV). + +If the label is dynamic, i.e. it will contain some text (numeric, or other) +that has to be updated on a parameter's change, then one can define +a special function to handle this. The "output" of such a function must be +set to "none", and the JavaScript code inside this function must update the +MathJax element by itself. Before exiting, MathJax typeset function should +be called so that the new text will be re-rendered by MathJax. For example, + + + ... + + + ... + + + + ... + + +Plot tag +........ + +``Plot`` tag inside ``configuration`` tag defines settings for plot output. + +Required parameters:: + + xrange: 2 functions that must return value. Value is constant (3.1415) + or depend on parameter from parameters section: + + return 0; + return 30; + + or + + return -a; + return a; + + + All functions will be calculated over domain between xrange:min + and xrange:max. Xrange depending on parameter is extremely + useful when domain(s) of your function(s) depends on parameter + (like circle, when parameter is radius and you want to allow + to change it). + +Optional parameters:: + + num_points: Number of data points to generated for the plot. If + this is not set, the number of points will be + calculated as width / 5. + + bar_width: If functions are present which are to be plotted as bars, + then this parameter specifies the width of the bars. A + numeric value for this parameter is expected. + + bar_align: If functions are present which are to be plotted as bars, + then this parameter specifies how to align the bars relative + to the tick. Available values are "left" and "center". + + xticks, + yticks: 3 floating point numbers separated by commas. This + specifies how many ticks are created, what number they + start at, and what number they end at. This is different + from the 'xrange' setting in that it has nothing to do + with the data points - it control what area of the + Cartesian space you will see. The first number is the + first tick's value, the second number is the step + between each tick, the third number is the value of the + last tick. If these configurations are not specified, + Flot will chose them for you based on the data points + set that he is currently plotting. Usually, this results + in a nice graph, however, sometimes you need to fine + grain the controls. For example, when you want to show + a fixed area of the Cartesian space, even when the data + set changes. On it's own, Flot will recalculate the + ticks, which will result in a different graph each time. + By specifying the xticks, yticks configurations, only + the plotted data will change - the axes (ticks) will + remain as you have defined them. + + xticks_names, yticks_names: + A JSON string which represents a mapping of xticks, yticks + values to some defined strings. If specified, the graph will + not have any xticks, yticks except those for which a string + value has been defined in the JSON string. Note that the + matching will be string-based and not numeric. I.e. if a tick + value was "3.70" before, then inside the JSON there should be + a mapping like {..., "3.70": "Some string", ...}. Example: + + + + + + + + + + xunits, + yunits: Units values to be set on axes. Use MathJax. Example: + \(cm\) + \(m\) + + moving_label: + A way to specify a label that should be positioned dynamically, + based on the values of some parameters, or some other factors. + It is similar to a , but it is only valid for a plot + because it is drawn relative to the plot coordinate system. + + Multiple "moving_label" configurations can be provided, each one + with a unique text and a unique set of functions that determine + it's dynamic positioning. + + Each "moving_label" can have a "color" attribute (CSS color notation), + and a "weight" attribute. "weight" can be one of "normal" or "bold", + and determines the styling of moving label's text. + + Each "moving_label" function should return an object with a 'x' + and 'y properties. Within those functions, all of the parameter + names along with their value are available. + + Example (note that "return" statement is missing; it will be automatically + inserted by GST): + + + + +

Graphic slider tool: Bar graph example.

+ +

We can request the API to plot us a bar graph.

+
+

a

+ + +


+

b

+ + +
+ +
+ + + + + + + + 0.9) && (x<1.1)) || ((x>4.9) && (x<5.1))) { return Math.sin(a * 0.01 * Math.PI + 2.952 * x); } + else {return undefined;}]]> + + + 1.9) && (x<2.1)) || ((x>3.9) && (x<4.1))) { return Math.cos(b * 0.01 * Math.PI + 3.432 * x); } + else {return undefined;}]]> + + + 1.9) && (x<2.1)) || ((x>3.9) && (x<4.1))) { return Math.cos((b - 10 * a) * 0.01 * Math.PI + 3.432 * x); } + else {return undefined;}]]> + + + 1.9) && (x<2.1)) || ((x>3.9) && (x<4.1))) { return Math.cos((b + 7 * a) * 0.01 * Math.PI + 3.432 * x); } + else {return undefined;}]]> + + + + 15 + 5 + 0, 0.5, 6 + -1.5, 0.1, 1.5 + + + + + + + 0.4 + + +
+ diff --git a/docs/source/gst_example_dynamic_labels.xml b/docs/source/gst_example_dynamic_labels.xml new file mode 100644 index 0000000000..05cbe407fb --- /dev/null +++ b/docs/source/gst_example_dynamic_labels.xml @@ -0,0 +1,40 @@ + + + +

Graphic slider tool: Dynamic labels.

+

There are two kinds of dynamic lables. + 1) Dynamic changing values in graph legends. + 2) Dynamic labels, which coordinates depend on parameters

+

a:

+
+

b:

+

+ +
+ + + + + + + + a * x + b + + a + + + 030 + 10 + 0, 6, 30 + -9, 1, 9 + + + + + + + + + +
+
\ No newline at end of file diff --git a/docs/source/gst_example_dynamic_range.xml b/docs/source/gst_example_dynamic_range.xml new file mode 100644 index 0000000000..0ce4263d62 --- /dev/null +++ b/docs/source/gst_example_dynamic_range.xml @@ -0,0 +1,37 @@ + + + +

Graphic slider tool: Dynamic range and implicit functions.

+ +

You can make x range (not ticks of x axis) of functions to depend on + parameter value. This can be useful when function domain depends + on parameter.

+

Also implicit functons like circle can be plotted as 2 separate + functions of same color.

+
+ + +
+ +
+ + + + + + Math.sqrt(a * a - x * x) + -Math.sqrt(a * a - x * x) + + + + + -a + a + + 1000 + -30, 6, 30 + -30, 6, 30 + + +
+
diff --git a/docs/source/gst_example_html_element_output.xml b/docs/source/gst_example_html_element_output.xml new file mode 100644 index 0000000000..340783871a --- /dev/null +++ b/docs/source/gst_example_html_element_output.xml @@ -0,0 +1,40 @@ + + + +

Graphic slider tool: Output to DOM element.

+ +

a + b =

+ +
+

a

+ + +
+ +
+

b

+ + +
+


+ +
+ + + + + + + + + function add(a, b, precision) { + var x = Math.pow(10, precision || 2); + return (Math.round(a * x) + Math.round(b * x)) / x; + } + + return add(a, b, 5); + + + +
+
diff --git a/docs/source/gst_example_with_documentation.xml b/docs/source/gst_example_with_documentation.xml new file mode 100644 index 0000000000..addada5b10 --- /dev/null +++ b/docs/source/gst_example_with_documentation.xml @@ -0,0 +1,91 @@ + + + + +

Graphic slider tool: full example.

+

+ A simple equation + \( + y_1 = 10 \times b \times \frac{sin(a \times x) \times sin(b \times x)}{cos(b \times x) + 10} + \) + can be plotted. +

+ + +
+

Currently \(a\) is

+ + +
+ +

This one + \( + y_2 = sin(a \times x) + \) + will be overlayed on top. +

+
+

Currently \(b\) is

+ +
+
+

To change \(a\) use:

+ +
+
+

To change \(b\) use:

+ +
+ +
+

Second input for b:

+ + +
+
+ + + + + + + + + + + + return 10.0 * b * Math.sin(a * x) * Math.sin(b * x) / (Math.cos(b * x) + 10); + + + + Math.sin(a * x); + + + function helperFunc(c1) { + return c1 * c1 - a; + } + + return helperFunc(x + 10 * a * b) + Math.sin(a - x); + + a + + + + + + return 0; + + 30 + + + 120 + + 0, 3, 30 + -1.5, 1.5, 13.5 + + \(cm\) + \(m\) + + +
+
diff --git a/docs/source/index.rst b/docs/source/index.rst index 92c535a624..d2082ff3a0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -14,7 +14,7 @@ Contents: overview.rst common-lib.rst djangoapps.rst - + xml_formats.rst Indices and tables ================== diff --git a/docs/source/xml_formats.rst b/docs/source/xml_formats.rst new file mode 100644 index 0000000000..b76ee11642 --- /dev/null +++ b/docs/source/xml_formats.rst @@ -0,0 +1,8 @@ +XML formats of Inputtypes and Xmodule +===================================== +Contents: + +.. toctree:: + :maxdepth: 2 + + graphical_slider_tool.rst \ No newline at end of file diff --git a/lms/envs/common.py b/lms/envs/common.py index 0364a8b6f8..6790e5b714 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -430,6 +430,7 @@ courseware_only_js += [ main_vendor_js = [ 'js/vendor/RequireJS.js', 'js/vendor/json2.js', + 'js/vendor/RequireJS.js', 'js/vendor/jquery.min.js', 'js/vendor/jquery-ui.min.js', 'js/vendor/jquery.cookie.js', diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html new file mode 100644 index 0000000000..93a6761784 --- /dev/null +++ b/lms/templates/graphical_slider_tool.html @@ -0,0 +1,9 @@ +
+ + + + + ${gst_html} +
diff --git a/requirements.txt b/requirements.txt index a3e1e3e6e5..08cfe57e2e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -56,3 +56,4 @@ dogstatsd-python==0.2.1 sphinx==1.1.3 Shapely==1.2.16 ipython==0.13.1 +xmltodict==0.4.1