diff --git a/calc/FuncGen.js b/calc/FuncGen.js new file mode 100644 index 0000000000..d1b6db03be --- /dev/null +++ b/calc/FuncGen.js @@ -0,0 +1,143 @@ +dojo.provide("dojox.calc.FuncGen"); +dojo.experimental("dojox.calc.FuncGen"); + +dojo.require("dijit._Templated"); +dojo.require("dojox.math._base"); +dojo.require("dijit.dijit"); +dojo.require("dijit.form.ComboBox"); +dojo.require("dijit.form.SimpleTextarea"); +dojo.require("dijit.form.Button"); +dojo.require("dojo.data.ItemFileWriteStore"); + + +dojo.declare( + "dojox.calc.FuncGen", + [dijit._Widget, dijit._Templated], +{ + // summary: + // The dialog layout for making functions + // + templateString: dojo.cache("dojox.calc", "templates/FuncGen.html"), + + widgetsInTemplate:true, + + onSelect: function(){ + // summary + // if they select something in the name combobox, then change the body and arguments to correspond to the function they selected + this.reset(); + }, + onClear: function(){ + // summary + // the clear button in the template calls this + // clear the name, arguments, and body if the user says yes + var answer = confirm("Do you want to clear the name, argument, and body text?"); + if(answer){ + this.clear(); + } + }, + saveFunction: function(name, args, body){ + // override me + }, + onSaved: function(){ + // this on save needs to be overriden if you want Executor parsing support + console.log("Save was pressed"); + }, + clear: function(){ + // summary + // clear the name, arguments, and body + this.textarea.set("value", ""); + this.args.set("value", ""); + this.combo.set("value", ""); + }, + reset: function(){ + // summary + // set the arguments and body to match a function selected if it exists in the function list + if(this.combo.get("value") in this.functions){ + this.textarea.set("value", this.functions[this.combo.get("value")].body); + this.args.set("value", this.functions[this.combo.get("value")].args); + } + }, + onReset: function(){ + // summary + // (Reset button on click event) reset the arguments and body to their previously saved state if the user says yes + console.log("Reset was pressed"); + if(this.combo.get("value") in this.functions){ + var answer = confirm("Do you want to reset this function?"); + if(answer){ + this.reset(); + this.status.set("value", "The function has been reset to its last save point."); + } + } + }, + deleteThing: function(item){ + // summary + // delete an item in the writestore + if (this.writeStore.isItem(item)){ + // delete it + console.log("Found item "+item); + this.writeStore.deleteItem(item); + this.writeStore.save(); + }else{ + console.log("Unable to locate the item"); + } + }, + deleteFunction: function(name){ + // override me + }, + onDelete: function(){ + // summary + // (Delete button on click event) delete a function if the user clicks yes + + //console.log("Delete was pressed"); + + var name; + if((name = this.combo.get("value")) in this.functions){ + var answer = confirm("Do you want to delete this function?"); + if(answer){ + var item = this.combo.item; + + //this.writeStore.fetchItemByIdentity({identity:name, onItem: this.deleteThing, onError:null}); + + this.writeStore.deleteItem(item); + this.writeStore.save(); + + this.deleteFunction(name); + delete this.functions[name]; + this.clear(); + } + }else{ + this.status.set("value", "Function cannot be deleted, it isn't saved."); + } + }, + readyStatus: function(){ + // summary + // set the status in the template to ready + this.status.set("value", "Ready"); + }, + writeStore:null, //the user can save functions to the writestore + readStore:null, // users cannot edit the read store contents, but they can use them + functions:null, // use the names to get to the function + + /*postCreate: function(){ + this.functions = []; // use the names to get to the function + this.writeStore = new dojo.data.ItemFileWriteStore({data: {identifier: 'name', items:[]}}); + + this.combo.set("store", this.writeStore); + },*/ + + startup: function(){ + // summary + // make sure the parent has a close button if it needs to be able to close + // link the write store too + this.combo.set("store", this.writeStore); + + this.inherited(arguments);// this is super class startup + // close is only valid if the parent is a widget with a close function + var parent = dijit.getEnclosingWidget(this.domNode.parentNode); + if(parent && typeof parent.close == "function"){ + this.closeButton.set("onClick", dojo.hitch(parent, 'close')); + }else{ + dojo.style(this.closeButton.domNode, "display", "none"); // hide the button + } + } +}); diff --git a/calc/GraphPro.js b/calc/GraphPro.js new file mode 100644 index 0000000000..e6c028a9ad --- /dev/null +++ b/calc/GraphPro.js @@ -0,0 +1,143 @@ +dojo.provide("dojox.calc.GraphPro"); +dojo.experimental("dojox.calc.GraphPro"); + +dojo.require("dojox.calc.Standard"); +dojo.require("dijit.dijit"); +dojo.require("dijit.form.ComboBox"); +dojo.require("dijit.form.Select"); +dojo.require("dijit.form.CheckBox"); +dojo.require("dijit.ColorPalette"); +dojo.require("dojox.charting.Chart2D"); +dojo.require("dojox.calc.Grapher"); +dojo.require("dojox.layout.FloatingPane"); +dojo.require("dojox.charting.themes.Tufte"); +dojo.require("dojo.colors"); + +dojo.declare( + "dojox.calc.GraphPro", + dojox.calc.Standard, +{ + // summary: + // The dialog widget for a graphing, scientific calculator + // + templateString: dojo.cache("dojox.calc", "templates/GraphPro.html"), + + grapher:null, + funcMaker:null, + aFloatingPane: null, + + executorLoaded: function(){ + // summary + // when executor loads check to see if the writestore is there + this.inherited(arguments); + dojo.addOnLoad(dojo.hitch(this, function(){ + if(this.writeStore == null && "functionMakerButton" in this){ + dojo.style(this.functionMakerButton.domNode, { visibility: "hidden" }); + } + })); + }, + makeFunctionWindow: function(){ + // summary + // use this function to create a function window (with the button on the layout) + var body = dojo.body(); + + var pane = dojo.create('div'); + body.appendChild(pane); + + this.aFloatingPane = new dojox.layout.FloatingPane({resizable:false, dockable:true, maxable:false, closable:true, duration:300, title:"Function Window", style:"position:absolute;left:10em;top:10em;width:50em;"}, pane); + var that = this; + var d = dojo.create("div"); + this.funcMaker = new dojox.calc.FuncGen({ + writeStore:that.writeStore, + readStore:that.readStore, + functions:that.functions, + deleteFunction: that.executor.deleteFunction, + onSaved:function(){ + var name, + body; + if((name = this.combo.get("value")) == ""){ + this.status.set("value", "The function needs a name"); + }else if ((body = this.textarea.get("value")) == ""){ + // i don't think users need empty functions for math + this.status.set("value", "The function needs a body"); + }else{ + var args = this.args.get("value"); + if(!(name in this.functions)){ + this.combo.item = this.writeStore.newItem({name: name, args: args, body: body}); + this.writeStore.save(); + } + this.saveFunction(name, args, body); + this.status.set("value", "Function "+name+" was saved"); + } + }, + saveFunction: dojo.hitch(that, that.saveFunction) + }, d); + this.aFloatingPane.set('content', this.funcMaker); + this.aFloatingPane.startup(); + this.aFloatingPane.bringToTop(); + }, + makeGrapherWindow: function(){ + // summary + // use this to make a Grapher window appear with a button + var body = dojo.body(); + + var pane = dojo.create('div'); + body.appendChild(pane); + + this.aFloatingPane = new dojox.layout.FloatingPane({resizable:false, dockable:true, maxable:false, closable:true, duration:300, title:"Graph Window", style:"position:absolute;left:10em;top:5em;width:50em;"}, pane); + var that = this; + + var d = dojo.create("div"); + this.grapher = new dojox.calc.Grapher({ + myPane: this.aFloatingPane, + drawOne: function(i){ + this.array[i][this.chartIndex].resize(this.graphWidth.get("value"), this.graphHeight.get("value")); + this.array[i][this.chartIndex].axes["x"].max = this.graphMaxX.get('value'); + if(this.array[i][this.expressionIndex].get("value")==""){ + this.setStatus(i, "Error"); + return; + } + var func; + var yEquals = (this.array[i][this.functionMode]=="y="); + if(this.array[i][this.expressionIndex].get("value")!=this.array[i][this.evaluatedExpression]){ + var args = 'x'; + if(!yEquals){ + args = 'y'; + } + func = that.executor.Function('', args, "return "+this.array[i][this.expressionIndex].get('value')); + this.array[i][this.evaluatedExpression] = this.array[i][this.expressionIndex].value; + this.array[i][this.functionRef] = func; + } + else{ + func = this.array[i][this.functionRef]; + } + var pickedColor = this.array[i][this.colorIndex].get("value"); + if(!pickedColor){ + pickedColor = 'black'; + } + dojox.calc.Grapher.draw(this.array[i][this.chartIndex], func, {graphNumber:this.array[i][this.funcNumberIndex], fOfX:yEquals, color:{stroke:{color:pickedColor}}}); + this.setStatus(i, "Drawn"); + }, + onDraw:function(){ + for(var i = 0; i < this.rowCount; i++){ + if((!this.dirty && this.array[i][this.checkboxIndex].get("checked")) || (this.dirty && this.array[i][this.statusIndex].innerHTML=="Drawn")){ + this.drawOne(i); + }else{ + this.array[i][this.chartIndex].resize(this.graphWidth.get("value"), this.graphHeight.get("value")); + this.array[i][this.chartIndex].axes["x"].max = this.graphMaxX.get('value'); + } + } + + var bufferY = dojo.position(this.outerDiv).y-dojo.position(this.myPane.domNode).y; + bufferY*=2; + bufferY=Math.abs(bufferY); + var height = "" + Math.max(parseInt(this.graphHeight.get('value'))+50, this.outerDiv.scrollHeight+bufferY); + var width = "" + (parseInt(this.graphWidth.get('value')) + this.outerDiv.scrollWidth); + this.myPane.resize({w:width, h:height}); + } + }, d); + this.aFloatingPane.set('content', this.grapher); + this.aFloatingPane.startup(); + this.aFloatingPane.bringToTop(); + } +}); diff --git a/calc/Grapher.js b/calc/Grapher.js new file mode 100644 index 0000000000..3ac77fc718 --- /dev/null +++ b/calc/Grapher.js @@ -0,0 +1,632 @@ +dojo.provide("dojox.calc.Grapher"); +dojo.experimental("dojox.calc.Grapher"); + +dojo.require("dijit._Templated"); +dojo.require("dojox.math._base"); +dojo.require("dijit.dijit"); +dojo.require("dijit.form.DropDownButton"); +dojo.require("dijit.TooltipDialog"); +dojo.require("dijit.form.TextBox"); +dojo.require("dijit.form.Button"); +dojo.require("dijit.form.ComboBox"); +dojo.require("dijit.form.Select"); +dojo.require("dijit.form.CheckBox"); +dojo.require("dijit.ColorPalette"); +dojo.require("dojox.charting.Chart2D"); +dojo.require("dojox.charting.themes.Tufte"); +dojo.require("dojo.colors"); + +dojo.declare( + "dojox.calc.Grapher", + [dijit._Widget, dijit._Templated], +{ + // summary: + // The dialog layout for making graphs + // + templateString: dojo.cache("dojox.calc", "templates/Grapher.html"), + + widgetsInTemplate:true, + + addXYAxes: function(chart){ + // summary: + // add or re-add the default x/y axes to the Chart2D provided + // params: + // chart is an instance of dojox.charting.Chart2D + + return chart.addAxis("x", { + max: parseInt(this.graphMaxX.get("value")), + min: parseInt(this.graphMinX.get("value")), + majorLabels: true, + minorLabels: true, + //includeZero: true, + minorTicks: false, + microTicks: false, + //majorTickStep: 1, + htmlLabels: true, + labelFunc: function(value){ + return value; + }, + maxLabelSize: 30, + fixUpper: "major", fixLower: "major", + majorTick: { length: 3 } + }). + addAxis("y", { + max: parseInt(this.graphMaxY.get("value")), + min: parseInt(this.graphMinY.get("value")), + labelFunc: function(value){ + return value; + }, + maxLabelSize: 50, + vertical: true, + // htmlLabels: false, + microTicks: false, + minorTicks: true, + majorTick: { stroke: "black", length: 3 } + }); + }, + selectAll: function(){ + // summary + // select all checkboxes inside the function table + for(var i = 0; i < this.rowCount; i++){ + this.array[i][this.checkboxIndex].set("checked", true); + } + }, + deselectAll: function(){ + // summary + // deselect all checkboxes inside the function table + for(var i = 0; i < this.rowCount; i++){ + this.array[i][this.checkboxIndex].set("checked", false); + } + }, + drawOne: function(i){ + // i is a the index to this.array + // override me + }, + onDraw: function(){ + console.log("Draw was pressed"); + // override me + }, + erase: function(i){ + // summary: + // erase the chart inside this.array with the index i + // params: + // i is the integer index to this.array that represents the current row number in the table + var nameNum = 0; + var name = "Series "+this.array[i][this.funcNumberIndex]+"_"+nameNum; + while(name in this.array[i][this.chartIndex].runs){ + this.array[i][this.chartIndex].removeSeries(name); + name = "Series "+this.array[i][this.funcNumberIndex]+"_"+nameNum; + } + this.array[i][this.chartIndex].render(); + this.setStatus(i, "Hidden"); + }, + onErase: function(){ + // summary: + // the erase button's onClick method + // it see's if the checkbox is checked and then erases it if it is. + for(var i = 0; i < this.rowCount; i++){ + if(this.array[i][this.checkboxIndex].get("checked")){ + this.erase(i); + } + } + }, + onDelete: function(){ + // summary: + // the delete button's onClick method + // delete all of the selected rows + for(var i = 0; i < this.rowCount; i++){ + if(this.array[i][this.checkboxIndex].get("checked")){ + this.erase(i); + for(var k = 0; k < this.functionRef; k++){ + if(this.array[i][k] && this.array[i][k]["destroy"]){ + this.array[i][k].destroy(); + } + } + this.graphTable.deleteRow(i); + this.array.splice(i, 1); + this.rowCount--; + i--; + } + } + }, + // attributes to name the indices of this.array + checkboxIndex: 0, + functionMode: 1, + expressionIndex: 2, + colorIndex: 3, + dropDownIndex: 4, + tooltipIndex: 5, + colorBoxFieldsetIndex: 6, + statusIndex: 7, + chartIndex: 8, + funcNumberIndex: 9, + evaluatedExpression: 10, + functionRef: 11, + + createFunction: function(){ + // summary: + // create a new row in the table with all of the dojo objects. + + var tr = this.graphTable.insertRow(-1); + this.array[tr.rowIndex] = []; + var td = tr.insertCell(-1); + var d = dojo.create('div'); + td.appendChild(d); + var checkBox = new dijit.form.CheckBox({}, d); + this.array[tr.rowIndex][this.checkboxIndex] = checkBox; + dojo.addClass(d, "dojoxCalcCheckBox"); + + td = tr.insertCell(-1); + var funcMode = this.funcMode.get("value"); + d = dojo.doc.createTextNode(funcMode); + td.appendChild(d); + this.array[tr.rowIndex][this.functionMode] = funcMode; + //dojo.addClass(d, "dojoxCalcFunctionMode");// cannot use text nodes + + td = tr.insertCell(-1); + d = dojo.create('div'); + td.appendChild(d); + var expression = new dijit.form.TextBox({}, d); + this.array[tr.rowIndex][this.expressionIndex] = expression; + dojo.addClass(d, "dojoxCalcExpressionBox"); + + var b = dojo.create('div'); + var color = new dijit.ColorPalette({changedColor:this.changedColor}, b); + dojo.addClass(b, "dojoxCalcColorPalette"); + + this.array[tr.rowIndex][this.colorIndex] = color; + + var c = dojo.create('div'); + var dialog = new dijit.TooltipDialog({content:color}, c); + this.array[tr.rowIndex][this.tooltipIndex] = dialog; + dojo.addClass(c, "dojoxCalcContainerOfColor"); + + td = tr.insertCell(-1); + d = dojo.create('div'); + td.appendChild(d); + + var colorBoxFieldset = dojo.create('fieldset'); + dojo.style(colorBoxFieldset, {backgroundColor: "black", width: "1em", height: "1em", display: "inline"}); + this.array[tr.rowIndex][this.colorBoxFieldsetIndex] = colorBoxFieldset; + + var drop = new dijit.form.DropDownButton({label:"Color ", dropDown:dialog}, d); + drop.containerNode.appendChild(colorBoxFieldset); + this.array[tr.rowIndex][this.dropDownIndex] = drop; + dojo.addClass(d, "dojoxCalcDropDownForColor"); + + /*td = tr.insertCell(-1); + d = dojo.create('div'); + td.appendChild(d); + var status = new dijit.form.TextBox({style:"width:50px", value:"Hidden", readOnly:true}, d);//hidden, drawn, or error + this.array[tr.rowIndex][this.statusIndex] = status; + dojo.addClass(d, "dojoxCalcStatusBox");*/ + + td = tr.insertCell(-1); + d = dojo.create('fieldset'); + d.innerHTML = "Hidden"; + this.array[tr.rowIndex][this.statusIndex] = d; + dojo.addClass(d, "dojoxCalcStatusBox"); + td.appendChild(d); + + d = dojo.create('div'); + dojo.style(d, {position:"absolute", left:"0px", top:"0px"}) + this.chartsParent.appendChild(d); + this.array[tr.rowIndex][this.chartNodeIndex] = d; + dojo.addClass(d, "dojoxCalcChart"); + var chart = new dojox.charting.Chart2D(d).setTheme(dojox.charting.themes.Tufte). + addPlot("default", { type: "Lines", shadow: {dx: 1, dy: 1, width: 2, color: [0, 0, 0, 0.3]} }); + this.addXYAxes(chart); + this.array[tr.rowIndex][this.chartIndex] = chart; + color.set("chart", chart); + color.set("colorBox", colorBoxFieldset); + color.set("onChange", dojo.hitch(color, 'changedColor')); + + this.array[tr.rowIndex][this.funcNumberIndex] = this.funcNumber++; + this.rowCount++; + }, + setStatus: function(i, status){ + // summary: + // set the status of the row i to be status + // params: + // i is an integer index of this.array as well as a row index + // status is a String, it is either Error, Hidden, or Drawn + this.array[i][this.statusIndex].innerHTML = status; //this.array[i][this.statusIndex].set("value", status); + }, + changedColor: function(){ + // summary: + // make the color of the chart the new color + // the context is changed to the colorPalette, and a reference to chart was added to it a an attribute + var chart = this.get("chart"); + var colorBoxFieldset = this.get("colorBox"); + for(var i = 0; i < chart.series.length; i++){ + if(chart.series[i]["stroke"]){ + if(chart.series[i].stroke["color"]){ + chart.series[i]["stroke"].color = this.get("value"); + chart.dirty = true; + } + } + } + chart.render(); + dojo.style(colorBoxFieldset, {backgroundColor:this.get("value")}); + }, + makeDirty: function(){ + // summary: + // if something in the window options is changed, this is called + this.dirty = true; + }, + checkDirty1: function(){ + // summary: + // to stay in sync with onChange, checkDirty is called with a timeout + setTimeout(dojo.hitch(this, 'checkDirty'), 0); + }, + checkDirty: function(){ + // summary: + // adjust all charts in this.array according to any changes in window options + if(this.dirty){ + // change the axes of all charts if it is dirty + for(var i = 0; i < this.rowCount; i++){ + this.array[i][this.chartIndex].removeAxis("x"); + this.array[i][this.chartIndex].removeAxis("y"); + this.addXYAxes(this.array[i][this.chartIndex]); + } + this.onDraw(); + } + this.dirty = false; + }, + postCreate: function(){ + // summary + // add Event handlers, some additional attributes, etc + this.inherited(arguments);// this is super class postCreate + this.createFunc.set("onClick", dojo.hitch(this, 'createFunction')); + + this.selectAllButton.set("onClick", dojo.hitch(this, 'selectAll')); + this.deselectAllButton.set("onClick", dojo.hitch(this, 'deselectAll')); + + this.drawButton.set("onClick", dojo.hitch(this, 'onDraw')); + this.eraseButton.set("onClick", dojo.hitch(this, 'onErase')); + this.deleteButton.set("onClick", dojo.hitch(this, 'onDelete')); + + this.dirty = false; + this.graphWidth.set("onChange", dojo.hitch(this, 'makeDirty')); + this.graphHeight.set("onChange", dojo.hitch(this, 'makeDirty')); + this.graphMaxX.set("onChange", dojo.hitch(this, 'makeDirty')); + this.graphMinX.set("onChange", dojo.hitch(this, 'makeDirty')); + this.graphMaxY.set("onChange", dojo.hitch(this, 'makeDirty')); + this.graphMinY.set("onChange", dojo.hitch(this, 'makeDirty')); + this.windowOptionsInside.set("onClose", dojo.hitch(this, 'checkDirty1')); + + this.funcNumber = 0; + this.rowCount = 0; + this.array = []; + + }, + startup: function(){ + // summary + // make sure the parent has a close button if it needs to be able to close + this.inherited(arguments);// this is super class startup + // close is only valid if the parent is a widget with a close function + var parent = dijit.getEnclosingWidget(this.domNode.parentNode); + if(parent && typeof parent.close == "function"){ + this.closeButton.set("onClick", dojo.hitch(parent, 'close')); + }else{ + dojo.style(this.closeButton.domNode, "display", "none"); // hide the button + } + // add one row at the start + this.createFunction(); + + // make the graph bounds appear initially + this.array[0][this.checkboxIndex].set("checked", true); + this.onDraw(); + this.erase(0); + this.array[0][this.expressionIndex].value = ""; + } +}); + +(function(){ + // summary + // provide static functions for Grapher + var + epsilon = 1e-15 / 9, + bigNumber = 1e200, + log2 = Math.log(2), + defaultParams = {graphNumber:0, fOfX:true, color:{stroke:"black"}}; + + dojox.calc.Grapher.draw = function(/*Chart2D*/ chart, /*Function*/ functionToGraph, params){ + // summary + // graph a chart with the given function. + // params + // chart is a dojox.charting.Chart2D object, functionToGraph is a function with one numeric parameter (x or y typically) + // and params is an Object the can contain the number of the graph in the chart it is (an integer), a boolean saying if the functionToGraph is a function of x (otherwise y) + // and the color, which is an object with a stroke with a color's name eg: color:{stroke:"black"} + + params = dojo.mixin({}, defaultParams, params); + chart.fullGeometry(); + var x; + var y; + var points; + if(params.fOfX==true){ + x = 'x'; + y = 'y'; + points = dojox.calc.Grapher.generatePoints(functionToGraph, x, y, chart.axes.x.scaler.bounds.span, chart.axes.x.scaler.bounds.lower, chart.axes.x.scaler.bounds.upper, chart.axes.y.scaler.bounds.lower, chart.axes.y.scaler.bounds.upper); + }else{ + x = 'y'; + y = 'x'; + points = dojox.calc.Grapher.generatePoints(functionToGraph, x, y, chart.axes.y.scaler.bounds.span, chart.axes.y.scaler.bounds.lower, chart.axes.y.scaler.bounds.upper, chart.axes.x.scaler.bounds.lower, chart.axes.x.scaler.bounds.upper); + } + + var i = 0; + + if(points.length > 0){ + for(; i < points.length; i++){ + if(points[i].length>0){ + chart.addSeries("Series "+params.graphNumber+"_"+i, points[i], params.color); + } + } + } + // you only need to remove the excess i's + var name = "Series "+params.graphNumber+"_"+i; + while(name in chart.runs){ + chart.removeSeries(name); + i++; + name = "Series "+params.graphNumber+"_"+i; + } + chart.render(); + return points; + } + + dojox.calc.Grapher.generatePoints = function(/*Function*/ funcToGraph, /*String*/ x, /*String*/ y, /*Number*/ width, /*Number*/ minX, /*Number*/ maxX, /*Number*/ minY, /*Number*/ maxY){ + // summary: + // create the points with information about the graph. + // params: + // funcToGraph is a function with one numeric parameter (x or y typically) + // x and y are Strings which always have the values of "x" or "y". If y="x" and x="y" then it is creating points for the function as though it was a function of y + // Number minX, Number maxX, Number minY, Number maxY are all bounds of the chart. If x="y" then maxY should be the maximum bound of x rather than y + // Number width is the pixel width of the chart + // output: + // an array of arrays of points + var pow2 = (1 << Math.ceil(Math.log(width) / log2)); + var + dx = (maxX - minX) / pow2, // divide by 2^n instead of width to avoid loss of precision + points = [], // [{x:value, y:value2},...] + series = 0, + slopeTrend, + slopeTrendTemp; + + points[series] = []; + + var i = minX, k, p; + for(var counter = 0; counter <= pow2; i += dx, counter++){ + p = {}; + p[x] = i; + p[y] = funcToGraph({_name:x, _value:i, _graphing:true});//funcToGraph(i); + if(p[x] == null || p[y] == null){ + return {};// someone pushed cancel in the val code + } + if(isNaN(p[y]) || isNaN(p[x])){ + continue; + } + points[series].push(p); + + if(points[series].length == 3){ + slopeTrend = getSlopePairTrend(slope(points[series][points[series].length - 3], points[series][points[series].length-2]), slope(points[series][points[series].length-2], points[series][points[series].length-1])); + continue; + } + if(points[series].length < 4){ + continue; + } + + slopeTrendTemp = getSlopePairTrend(slope(points[series][points[series].length - 3], points[series][points[series].length-2]), slope(points[series][points[series].length-2], points[series][points[series].length-1])); + if(slopeTrend.inc != slopeTrendTemp.inc || slopeTrend.pos != slopeTrendTemp.pos){ + // var a = asymptoteSearch(funcToGraph, points[series][points[series].length - 2], points[series][points[series].length-1]); + var a = asymptoteSearch(funcToGraph, points[series][points[series].length - 3], points[series][points[series].length-1]); + p = points[series].pop(); + // this pop was added after changing the var a line above + points[series].pop(); + for(var j = 0; j < a[0].length; j++){ + points[series].push(a[0][j]); + } + for(k = 1; k < a.length; k++){ + points[++series] = a.pop(); + } + points[series].push(p); + slopeTrend = slopeTrendTemp; + } + } + while(points.length > 1){ + for(k = 0; k < points[1].length; k++){ + if(points[0][points[0].length - 1][x] == points[1][k][x]){ + continue; + } + points[0].push(points[1][k]); + } + points.splice(1, 1); + } + points = points[0]; + + // make new series when it goes off the graph + var s = 0; + var points2 = [ [] ]; + for(k = 0; k < points.length; k++){ + var x1, y1, b, slope1; + if(isNaN(points[k][y]) || isNaN(points[k][x])){ + while(isNaN(points[k][y]) || isNaN(points[k][x])){ + points.splice(k, 1); + } + points2[++s] = []; + k--; + }else if(points[k][y] > maxY || points[k][y] < minY){ + // make the last point's y equal maxY and find a matching x + if(k > 0 && points[k - 1].y!=minY && points[k - 1].y!=maxY){ + slope1 = slope(points[k - 1], points[k]); + if(slope1 > bigNumber){ + slope1 = bigNumber; + }else if(slope1 < -bigNumber){ + slope1 = -bigNumber; + } + if(points[k][y] > maxY){ + y1 = maxY; + }else{ + y1 = minY; + } + b = points[k][y] - slope1 * points[k][x]; + x1 = (y1 - b) / slope1; + + p = {}; + p[x] = x1; + p[y] = funcToGraph(x1);//y1;// + + if(p[y]!=y1){ + p = findMinOrMaxY(funcToGraph, points[k - 1], points[k], y1); + } + + points2[s].push(p); + // setup the next series + points2[++s] = [] + } + var startK = k; + while(k < points.length && (points[k][y] > maxY || points[k][y] < minY)){ + k++; + } + if(k >= points.length){ + if(points2[s].length == 0){ + points2.splice(s, 1); + } + break; + } + // connect the end graph + if(k > 0 && points[k].y != minY && points[k].y != maxY){ + slope1 = slope(points[k - 1], points[k]); + if(slope1 > bigNumber){ + slope1 = bigNumber; + }else if(slope1 < -bigNumber){ + slope1 = -bigNumber; + } + if(points[k - 1][y] > maxY){ + y1 = maxY; + }else{ + y1 = minY; + } + b = points[k][y] - slope1 * points[k][x]; + x1 = (y1 - b) / slope1; + + p = {}; + p[x] = x1; + p[y] = funcToGraph(x1);//y1;// + if(p[y]!=y1){ + p = findMinOrMaxY(funcToGraph, points[k - 1], points[k], y1); + } + points2[s].push(p); + points2[s].push(points[k]); + } + }else{ + points2[s].push(points[k]); + } + } + return points2; + + function findMinOrMaxY(funcToGraph, left, right, minMaxY){ + + while(left<=right){ + var midX = (left[x]+right[x])/2; + var mid = {}; + mid[x] = midX; + mid[y] = funcToGraph(mid[x]); + + if(minMaxY==mid[y]||mid[x]==right[x]||mid[x]==left[x]){ + return mid; + } + + var moveTowardsLarger = true; + if(minMaxY= Math.abs(midpoint[y])){ + pointTemp[0].push(midpoint); + left = rightPoint; + }else{ + pointTemp[1].unshift(midpoint); + if(right[x] == midpoint[x]){ + break; + } + right = midpoint; + } + + } + return pointTemp; + } + + function getSlopePairTrend(slope1, slope2){ + var + isInc = false, + isPos = false; + + if (slope1 < slope2){ + isInc = true; + } + if (slope2 > 0){ + isPos = true; + } + return { inc: isInc, pos: isPos }; + } + + function nextNumber(v){ + var delta; + if(v > -1 && v < 1){ + if(v < 0){ // special handling as we approach 0 + if(v >= -epsilon){ + delta = -v; // always stop at 0 + }else{ + delta = v / Math.ceil(v / epsilon); // divide distance to 0 into equal tiny chunks + } + }else{ + delta = epsilon; + } + }else{ + delta = Math.abs(v) * epsilon; + } + return v + delta; + } + + function slope(p1, p2){ + return (p2[y] - p1[y]) / (p2[x] - p1[x]); + } + }; +})(); diff --git a/calc/Readme.txt b/calc/Readme.txt new file mode 100644 index 0000000000..b70d1c7cee --- /dev/null +++ b/calc/Readme.txt @@ -0,0 +1,175 @@ +Dojo Toolkit Graphing Calculator Project Readme +Author: Jason Hays +Trac ID: jason_hays22 + +Contents: + Expressions + Variables, Functions, and uninitializing variables + toFrac in GraphPro + Numbers and bases + Graphing equations + Substitutes for hard to type characters + Making Functions + Decimal points, commas, and semicolons in different languages + Important mathematical functions + + + +---------------Expressions---------------- +The calculator has the ability to simplify a valid expression. +With Augmented Mathematical Syntax, users are allowed to use nonstandard operators in their expressions. Those operators include ^, !, and radical. + +^ is used for exponentiation. + It is a binary operator, which means it needs a number on both the left and right side (like multiplication and division) + 2^5 is an example of valid use of ^, and represents two to the power of five. + +! is used for factorial. + It supports numbers that are not whole numbers through the use of the gamma function. It uses the number on its left side. Both 2! and 2.6! are examples of valid input in America. (2,6! is valid in some nations) + +radicals can be used for either square root or various other roots. + to use it as a square root sign, there should only be a number on its right. If you put a number on the left as well, then it will use that number as the root. + +To evaluate an expression, type in a valid expression, such as 2*(10+5), into the input box. If you are using GraphPro, then it is the smaller text box. + After you have chosen an expression, press Enter on either your keyboard or on the lower right of the calculator. + If it did not evaluate, make sure you correctly closed your parentheses. + In the Standard calculator, the answer will appear in the input box, in GraphPro, the answer will appear in the larger text box above. + On the keyboard, you can navigate through your previous inputs with the up and down arrow keys. + +If you enter an operator when the textbox is empty or highlighted (like *) then Ans* should appear. That means the answer you got before will be multiplied by whatever you input next. +So try Ans*3. Whenever you start the calculator, Ans is set to zero. + + +--------------Variables, Functions, and uninitializing variables---------------- +A variable is basically something that stores a value. If you saw Ans in the previous example, you've also seen a variable. +If you want to store your own number somewhere, you'll need to use the = operator. + + Valid variable and function names include cannot start with numbers, do not include spaces, but can start with the alphabet (a-z or A-Z) and can have numbers within the names "var1" is a valid name + + Input "myVar = 2" into the textbox and press "Enter." You've just saved a variable. Now if you ever type myVar into an expression, 2 will appear (unless you change it to something else). + Variables are best used to store Ans. Ans is overridden whenever you evaluate an expression, so it is good to store the value of Ans somewhere else before it is overridden. + + If you want a variable (like myVar) to become empty, or undefined, you just need to set it equal to undefined. + Now try "myVar = undefined" Now myVar is no longer defined. + + Functions are very useful for finding answers and gathering data. + You can use functions by inputting their name and their arguments. + For this example, I'll be using the functions named "sqrt" and "pow" + + sqrt is a function with one argument. That argument has a name too, its name is 'x.' x is a very common name amongst built in functions + So, let's run a function. Input "sqrt" then input a left parenthesis (all arguments of a function go within parentheses). Now type a value for x, like 2. + Now close the parentheses with the right parenthesis. If you used 2, you should have "sqrt(2)" in the text box. If you press enter, you should get the square root of 2 back from the calculator. + + Now for "pow" it has two arguments 'x' and 'y' + Type in "pow(" and pick a value for 'x' (I am picking 2 again) + but now, you need to separate the value you gave x with a list separator. Depending on your location, it is either a comma or a semicolon. I'm in America, and I use commas. + by this point, I have "pow(2," + Now we need a value for 'y' (I'm using 3). Put a ')' and now I have 'pow(2,3)' + Press Enter, and, following my example, you should get 8 + + In this calculator, there are several ways to input arguments. + You've already seen the first way, just input numbers in a specific order based on the names. + + The second way is with an arbitrary order, and storage. + With 'pow' I can input "pow(y=3, x=2)" and get the exact answer as before. x and y will retain their assigned values, so you will need to set them to undefined it you want to try the next way. + + The third way is to let the calculator ask you for the values. Input "pow()" If the values have been assigned globally, then it will use those values, but otherwise, it will ask for values of x and y. They will not be stored globally this way. + + I'll go ahead and mention that because of the way the calculator parses, underscores should not be used to name a variable like _#_ (where # is an integer of any length) + +---------------toFrac()---------------- +toFrac is a function that takes one parameter, x, and converts it to a fraction for you. It is only in GraphPro, not the Standard mode. + It will try to simplify pi, square roots, and rational numbers where the denominator is less than a set bound (100 right now). + Immediately after the calculator starts, toFrac may seen slow, but it just needs to finish loading when the calculator starts. After that, it will respond without delay. + For an example, input "toFrac(.5)" or (,5 for some). It will return "1/2" + For a more complicated example, input "toFrac(atan(1))" to get back "pi/4" (atan is also known as "arc tangent" or "inverse tangent") + +--------------Numbers and bases---------------- +This calculator supports multiple bases, and not only that, but non integer versions of multiple bases. + What is a base? Well, the numbers you know and love are base 10. That means that you count to all of the numbers up until 10 before you move on to add to the tenths place. + So, what about base 2? All of the numbers up until 2 are 0 and 1. If you want to type a base 2 number into the calculator, simply input "0b" (meaning base 2) followed by some number of 1's and 0's. 0b101 is 5 in base 10 + + Hexadecimal is 0x, and octal is 0o, but i won't go into too much detail on those here. + If you want an arbitrary integer base, type the number in the correct base, insert '#' and put the radix on the end. ".1#3" is the same as 1/3 in base 10 + Because there is not yet cause for it, you cannot have a base that is not a whole number. + +--------------Graphing Equations---------------- +First thing is first, in GraphPro only, the "Graph" button in the top left corner opens the Graph Window + So, now you should see a single text box adjacent to "y=" + Type the right side of the equation using 'x' as the independent variable. + "sin(x)" for example. To Graph it, make sure the checkbox to the left of the equation is selected, and press the Draw Selected button. + You can change the color in the color tab. By default, it is black. Under window options, you can change the window size and x/y boundaries + + Let's add a second function. Go to the Add Function button, select the mode you want, and press Create. Another input box will appear. + If you selected x= as the Mode, then y is the independent variable for the line (an example is "x=sin(y)"). + If you want to erase, check the checkboxes you want to erase, and press "Erase Selected" + And similarly, Delete Selected will delete the chosen functions + + "Close" will terminate the Graph Window completely + +---------------Substitutes for hard to type characters--------------- +Some characters are not simple to add in for keyboard users, so there are substitutes that are much easier to add into the text box. + + pi or PI can be used in place of the special character for it. + For epsilon, eps or E can be used. + radical has replacement functions. sqrt(x) or pow(x,y) can be used instead. + +--------------Making Functions----------------- +My favorite part. Before we start, I'll mention that Augmented Mathematical Syntax is allowed in the Function Generator (yay). + Ok, now the bad news: to prevent some security issues, keywords new and delete are forbidden. + Sorry, it is a math calculator, not a game container; not that that would be so bad, but it is to keep it from being used for some evil purposes. + + Ok, onto function making. Most JavaScript arithmetic is supported here, but, some syntax was overlapping mathematical syntax, so ++Variable no longer increases the contents of Variable because of ++1, but Variable++ does increase it (same deal with --) + Strings have incredibly limited support. Objects have near zero support + So, let's make a Function: + Press the "Func" button. A Function Window should pop up. + Enter a name into the "functionName" box. (it must follow the name guidelines in the variables section) I'm putting myFunc + Enter the variables you want into the arguments box (I'll put "x,y" so I have two arguments x and y) + Now enter the giant text box. + Type "return " and then the expression you want to give to the calculator. I'm putting "return x*2 + y/2" + Then press Save. Now your function should appear in the functionName list and you can call it in the Calculator. + + If you want to Delete a function you made, select it in the function Name list, and then press Delete. + If you altered a previously saved function (and haven't saved over the old one) you can reset the text back to its original state with the Reset button. + Clear will empty out all of the text boxes in the Function Window + Close terminates the Function Window + +---------------Decimal points, commas, and semicolons in different languages---------------- +In America, 3.5 is three and one half. Comma is used to separate function parameters and list members. + +In some nations, 3,5 is three and one half. In lists, ;'s are used to separate its members. +So, when you evaluate expressions, 3,5 will be valid, but in the function generator, some ambiguous texts prevent me from allowing the conversion of that format to JavaScript. So I cannot parse it in the Function Generator. +And here is my example: +var i = 3,5; + b = 2; +I cannot discern whether the semicolon between i and b are list separators or the JavaScript character for end the line. b could be intended as a global variable, and i is a local variable, but I don't know that. So language conversion isn't supported in Function Making. + +--------------Important mathematical Functions--------------- +Here is a list of functions you may find useful and their variable arguments: + +sqrt(x) returns the square root of x +x is in radians for all trig functions +sin(x) returns the sine of x +asin(x) returns the arc sine of x +cos(x) returns the cosine of x +acos(x) returns the arc cosine of x +tan(x) returns the tangent of x +atan(x) returns the arc tangent of x + +atan2(y, x) returns the arc tangent of y and x +Round(x) returns the rounded integer form of x +Int(x) Cuts off the decimal digits of x +Ceil(x) If x has decimal digits, get the next highest integer + +ln(x) return the natural log of x +log(x) return log base 10 of x +pow(x, y) return x to the power of y + +permutations(n, r) get the permutations for n choose r +P(n, r) see permutations +combinations(n, r) get the combinations for n choose r +C(n, r) see combinations + +toRadix(number, baseOut) convert a number to a different base (baseOut) +toBin(number) convert number to a binary number +toOct(number) convert number to an octal number +toHex(number) convert number to a hexadecimal number diff --git a/calc/Standard.js b/calc/Standard.js new file mode 100644 index 0000000000..6e6b315a6d --- /dev/null +++ b/calc/Standard.js @@ -0,0 +1,355 @@ +dojo.provide("dojox.calc.Standard"); +dojo.experimental("dojox.calc.Standard"); + +dojo.require("dijit._Templated"); +dojo.require("dojox.math._base"); +dojo.require("dijit.dijit"); +dojo.require("dijit.Menu"); +dojo.require("dijit.form.DropDownButton"); +dojo.require("dijit.TooltipDialog"); +dojo.require("dijit.form.TextBox"); +dojo.require("dijit.form.Button"); +dojo.require("dojox.calc._Executor"); + +dojo.declare( + "dojox.calc.Standard", + [dijit._Widget, dijit._Templated], +{ + // summary: + // The dialog layout for a standard 4 function/algebraic calculator + // + templateString: dojo.cache("dojox.calc", "templates/Standard.html"), + + readStore:null, + writeStore:null, + functions: [], + + widgetsInTemplate:true, + executorLoaded: function(){ + // summary + // load in the stores after executor is loaded (the stores need executor to be loaded because it parses them) + dojo.addOnLoad(dojo.hitch(this, function(){ + this.loadStore(this.readStore, true); + this.loadStore(this.writeStore); + })); + }, + + saveFunction: function(name, args, body){ + // summary + // make the function with executor + this.functions[name] = this.executor.normalizedFunction(name, args, body); + this.functions[name].args = args; + this.functions[name].body = body; + }, + + loadStore: function(store, isReadOnly){ + // summary + // load an entire store, and make it publicly editable/viewable based on isReadOnly + function saveFunctions(items){ + for(var i = 0; i < items.length; i++){ + this.saveFunction(items[i].name[0], items[i].args[0], items[i].body[0]); + } + } + function saveReadOnlyFunctions(items){ + // make the function + for(var i = 0; i < items.length; i++){ + this.executor.normalizedFunction(items[i].name[0], items[i].args[0], items[i].body[0]); + } + } + if(store==null){ + return; + } + if(isReadOnly){ + store.fetch({ + onComplete: dojo.hitch(this, saveReadOnlyFunctions), + onError: function(text){console.log(text)} + }); + }else{ + store.fetch({ + onComplete: dojo.hitch(this, saveFunctions), + onError: function(text){console.log(text)} + }); + } + }, + + parseTextbox: function(){ + // summary + // parse the contents of the textboxWidget and display the answer somewhere (depending on the layout) + var text = this.textboxWidget.textbox.value; + if(text == "" && this.commandList.length > 0){ + this.setTextboxValue(this.textboxWidget, this.commandList[this.commandList.length-1]); + text = this.textboxWidget.textbox.value; + } + if(text!=""){ + var ans = this.executor.eval(text); + + if(!(typeof ans == "number" && isNaN(ans)) && ((typeof ans == "object" && "length" in ans) || typeof ans != "object") && typeof ans != "function" && ans != null){ + this.executor.eval("Ans="+ans); + // add it to the command list as well + if(this.commandList.length == 0 || this.commandList[this.commandList.length - 1] != text){ + this.commandList.push(text); + } + this.print(text, false); + this.print(ans, true); + } + this.commandIndex = this.commandList.length-1; + //this.displayBox.textbox.scrollTop=this.displayBox.textbox.scrollHeight; + if(this.hasDisplay){ + this.displayBox.scrollTop=this.displayBox.scrollHeight; + } + //this.clearText(); + //this.textboxWidget.focus(); + dijit.selectInputText(this.textboxWidget.textbox); + + }else{ + this.textboxWidget.focus(); + } + }, + cycleCommands: function(count, node, event){ + // summary + // cycle through the commands that the user has entered + // it does not wrap around + if(count == -1 || this.commandList.length==0){ + return; + } + var keyNum = event.charOrCode; + //up arrow + if(keyNum == dojo.keys.UP_ARROW){ + this.cycleCommandUp(); + }else if(keyNum == dojo.keys.DOWN_ARROW){ + this.cycleCommandDown(); + } + }, + cycleCommandUp: function(){ + // summary + // cycle up through the list of commands the user has entered already + if(this.commandIndex-1<0){ + this.commandIndex=0; + }else{ + this.commandIndex--; + } + this.setTextboxValue(this.textboxWidget, this.commandList[this.commandIndex]); + }, + cycleCommandDown: function(){ + // summary + // cycle down through the list of commands the user has entered already + if(this.commandIndex+1>=this.commandList.length){ + this.commandIndex=this.commandList.length; + this.setTextboxValue(this.textboxWidget, ""); + }else{ + this.commandIndex++; + this.setTextboxValue(this.textboxWidget, this.commandList[this.commandIndex]); + } + + }, + onBlur: function(){ + // summary + // IE is lacking in function when it comes to the text boxes, so here, make it work like other browsers do by forcing a node.selectionStart and End onto it + if(dojo.isIE){ + var tr = dojo.doc.selection.createRange().duplicate(); + var selectedText = tr.text || ''; + var ntr = this.textboxWidget.textbox.createTextRange(); + tr.move("character",0); + ntr.move("character",0); + try{ + ntr.setEndPoint("EndToEnd", tr); + this.textboxWidget.textbox.selectionEnd = (this.textboxWidget.textbox.selectionStart = String(ntr.text).replace(/\r/g,"").length) + selectedText.length; + + }catch(e){} + } + }, + onKeyPress: function(event){ + // summary + // handle key input for Enter and operators + if(event.charOrCode == dojo.keys.ENTER){ + this.parseTextbox(); + // stop form submissions + dojo.stopEvent(event); + }else if(event.charOrCode == '!' || event.charOrCode == '^' || event.charOrCode == '*' || event.charOrCode == '/' || event.charOrCode == '-' || event.charOrCode == '+'){ + if(dojo.isIE){ + var tr = dojo.doc.selection.createRange().duplicate(); + var selectedText = tr.text || ''; + var ntr = this.textboxWidget.textbox.createTextRange(); + tr.move("character",0); + ntr.move("character",0); + try{ + ntr.setEndPoint("EndToEnd", tr); + this.textboxWidget.textbox.selectionEnd = (this.textboxWidget.textbox.selectionStart = String(ntr.text).replace(/\r/g,"").length) + selectedText.length; + + }catch(e){} + } + + if(this.textboxWidget.get("value")==""){ + this.setTextboxValue(this.textboxWidget, "Ans"); + }else if(this.putInAnsIfTextboxIsHighlighted(this.textboxWidget.textbox, event.charOrCode)){ + this.setTextboxValue(this.textboxWidget, "Ans");//this.insertText("Ans"); + // move the cursor to the end of "Ans" + dijit.selectInputText(this.textboxWidget.textbox, this.textboxWidget.textbox.value.length, this.textboxWidget.textbox.value.length); + } + } + }, + insertMinus: function(){ + // summary + // insert a minus sign when they press (-) in the combo button + this.insertText('-'); + }, + print: function(text, isRight){ + // summary + // print the answer (typically) to the display or the input box + var t = ""; + }else{ + t += "text-align:left;'>"; + } + t += text+"
"; + if(this.hasDisplay){ + this.displayBox.innerHTML += t; + }else{// if there is not a display box, put the answer in the input box + this.setTextboxValue(this.textboxWidget, text); + } + //this.setTextboxValue(this.displayBox, this.displayBox.get('value')+'\n'+text); + }, + setTextboxValue: function(widget, val){ + // summary + // set a widget's value + widget.set('value', val); + }, + putInAnsIfTextboxIsHighlighted: function(node){ + // summary + // try seeing if the textbox is highlighted completely so you know if Ans should be put in for an operator like + + //console.log("Entered "+node.selectionStart + " "+ node.selectionEnd); + if(typeof node.selectionStart == "number"){ // not-IE + if(node.selectionStart==0 && node.selectionEnd == node.value.length){ + //node.value = "Ans"; + //dijit.selectInputText(node, node.value.length, node.value.length); + return true; + } + }else if(document.selection){ // IE + //console.log("Entered 2"); + var range = document.selection.createRange(); + //console.log("Range: "+range.text +" Node: "+node.value); + if(node.value == range.text){ + //this.insertText("Ans"); + return true; + } + } + return false; + }, + clearText: function(){ + // summary + // this clears the input box if it has content, but if it does not it clears the display + if(this.hasDisplay && this.textboxWidget.get('value')==""){ + this.displayBox.innerHTML = "";//this.setTextboxValue(this.displayBox, ""); + }else{ + this.setTextboxValue(this.textboxWidget, ""); + } + this.textboxWidget.focus(); + }, + /*insertMinusSign: function(){ + // + var v = this.subtract.get('label'); + if(v != '(-)' && this.putInAnsIfTextboxIsHighlighted(this.textboxWidget.textbox)){ + this.insertText("Ans-"); + return; + } + this.insertText('-'); + },*/ + insertOperator: function(newText){ + // summary + // insert an operator with a button + if(typeof newText == "object"){ + newText = newText = dijit.getEnclosingWidget(newText["target"]).value; + } + if(this.textboxWidget.get("value") == "" || this.putInAnsIfTextboxIsHighlighted(this.textboxWidget.textbox)){ + newText = "Ans"+newText; + } + this.insertText(newText); + }, + insertText: function(newText){//(node, newText){ + // summary + // insert text to the textboxWidget node + setTimeout(dojo.hitch(this, function(){ + + var node = this.textboxWidget.textbox; + if(node.value==""){ + node.selectionStart = 0; + node.selectionEnd = 0; + } + if(typeof newText == "object"){ + newText = newText = dijit.getEnclosingWidget(newText["target"]).value; + } + + var value = node.value.replace(/\r/g,''); + if(typeof node.selectionStart == "number"){ // not-IE + var pos = node.selectionStart; + var cr = 0; + if(navigator.userAgent.indexOf("Opera") != -1){ // if(dojo.isOpera){ + cr = (node.value.substring(0,pos).match(/\r/g) || []).length; + } + node.value = value.substring(0, node.selectionStart-cr) + newText + value.substring(node.selectionEnd-cr); + node.focus(); + pos += newText.length; + //node.setSelectionRange(pos, pos); + dijit.selectInputText(this.textboxWidget.textbox, pos, pos); + }else if(document.selection){ // IE + if(this.handle){ + clearTimeout(this.handle); + this.handle = null; + } + node.focus(); + this.handle = setTimeout(function(){ + var range = document.selection.createRange(); + range.text = newText; + // show cursor + range.select(); + this.handle = null; + }, 0); + + } + }), 0); + }, + hasDisplay: false, + postCreate: function(){ + // summary + // run startup, see if there is an upper display box, etc + this.handle = null; + this.commandList = []; + this.commandIndex = 0; + + if(this.displayBox){ + this.hasDisplay = true; + } + if(this.toFracButton && !dojox.calc.toFrac){ + dojo.style(this.toFracButton.domNode, { visibility: "hidden" }); + } + if(this.functionMakerButton && !dojox.calc.FuncGen){ + dojo.style(this.functionMakerButton.domNode, { visibility: "hidden" }); + } + if(this.grapherMakerButton && !dojox.calc.Grapher){ + dojo.style(this.grapherMakerButton.domNode, { visibility: "hidden" }); + } + this._connects.push(dijit.typematic.addKeyListener(this.textboxWidget.textbox, + { + charOrCode:dojo.keys.UP_ARROW, + shiftKey:false, + metaKey:false, + ctrlKey:false // ALT is optional since its unspecified + }, + this, this.cycleCommands, 200, 200)); + this._connects.push(dijit.typematic.addKeyListener(this.textboxWidget.textbox, + { + charOrCode:dojo.keys.DOWN_ARROW, + shiftKey:false, + metaKey:false, + ctrlKey:false // ALT is optional since its unspecified + }, + this, this.cycleCommands, 200, 200)); + + + //onClick="this.insertText(document.getElementById('textbox'), '\u221A')" + //this.sqrt.set("onClick", dojo.hitch(this, "insertText", this.textboxWidget, '\u221A')); + //this.pi.set("onClick", dojo.hitch(this, "insertText", this.textboxWidget, '\u03C0')); + this.startup() + } +}); diff --git a/calc/_Executor.js b/calc/_Executor.js new file mode 100644 index 0000000000..6ee4898f82 --- /dev/null +++ b/calc/_Executor.js @@ -0,0 +1,145 @@ +dojo.provide("dojox.calc._Executor"); +dojo.experimental("dojox.calc._Executor"); + +dojo.require("dijit._Templated"); +dojo.require("dojox.math._base"); + + +(function(){ +var calcEnv; // private + +// do not override toFrac's pow function if it won the race +if(!("pow" in dojox.calc)){ + dojox.calc.pow = function(/*Number*/ base, /*Number*/ exponent){ + // summary: + // Computes base ^ exponent + // Wrapper to Math.pow(base, exponent) to handle (-27) ^ (1/3) + + function isInt(n){ + return Math.floor(n) == n; + } + + if(base >= 0 || isInt(exponent)){ + return Math.pow(base, exponent); + }else{ // e.g. (1/3) root of -27 = -3 + var inv = 1 / exponent; + // e.g. 1 / (1/3) must be an odd integer + return (isInt(inv) && (inv & 1)) ? -Math.pow(-base, exponent) : NaN; + } + }; +} + + +dojo.declare( + "dojox.calc._Executor", + [dijit._Widget, dijit._Templated], +{ + // summary: + // A graphing, scientific calculator + // + + templateString: '', + + _onLoad: function(env){ + // summary + // prepare for communications between the user and the calculator by saving the calculator environment, storing the prompt function locally, and making dojox.math available + // + calcEnv = env; + env.outerPrompt = window.prompt; // for IE who can't execute the iframe's prompt method without notifying the user first + // let the user call dojo math functions + env.dojox = {math: {}}; + for(var f in dojox.math){ env.dojox.math[f] = dojo.hitch(dojox.math, f); } + if("toFrac" in dojox.calc){ + env.toFracCall = dojo.hitch(dojox.calc, 'toFrac'); + this.Function('toFrac', 'x', "return toFracCall(x)"); + } + + env.isJavaScriptLanguage = dojo.number.format(1.5, {pattern:'#.#'}) == "1.5"; + env.Ans = 0; + env.pi = Math.PI; + env.eps = Math.E; + + env.powCall = dojo.hitch(dojox.calc, 'pow'); + + // TODO add Degrees support to trig functions + + + //this.normalizedFunction('toString', 'number, radix', "return number.toString(radix)"); + this.normalizedFunction('sqrt', 'x', "return Math.sqrt(x)"); + this.normalizedFunction('sin', 'x', "return Math.sin(x)"); + this.normalizedFunction('cos', 'x', "return Math.cos(x)"); + this.normalizedFunction('tan', 'x', "return Math.tan(x)"); + this.normalizedFunction('asin', 'x', "return Math.asin(x)"); + this.normalizedFunction('acos', 'x', "return Math.acos(x)"); + this.normalizedFunction('atan', 'x', "return Math.atan(x)"); + this.normalizedFunction('atan2', 'y, x', "return Math.atan2(y, x)"); + this.normalizedFunction('Round', 'x', "return Math.round(x)"); + this.normalizedFunction('Int', 'x', "return Math.floor(x)"); + this.normalizedFunction('Ceil', 'x', "return Math.ceil(x)"); + this.normalizedFunction('ln', 'x', "return Math.log(x)"); + this.normalizedFunction('log', 'x', "return Math.log(x)/Math.log(10)"); + this.normalizedFunction('pow', 'x, y', "return powCall(x,y)"); + this.normalizedFunction('permutations', 'n, r', "return dojox.math.permutations(n, r);"); + this.normalizedFunction('P', 'n, r', "return dojox.math.permutations(n, r);"); + this.normalizedFunction('combinations', 'n, r', "return dojox.math.combinations(n, r);"); + this.normalizedFunction('C', 'n, r', "return dojox.math.combinations(n, r)"); + + this.normalizedFunction('toRadix', 'number, baseOut', "if(!baseOut){ baseOut = 10; } if(typeof number == 'string'){ number = parseFloat(number); }return number.toString(baseOut);"); + this.normalizedFunction('toBin', 'number', "return toRadix(number, 2)"); + this.normalizedFunction('toOct', 'number', "return toRadix(number, 8)"); + this.normalizedFunction('toHex', 'number', "return toRadix(number, 16)"); + this.onLoad(); + }, + + onLoad: function(){ + // summary: + // this should be overwritten and become a great place for making user predefined functions + // + }, + Function: function(name, args, body){ + // summary + // create an anonymous function to run the code the parser generates from the user input. + // params + // name: this argument is simply a String that represents the name of the function being evaluated. It can be undefined, but in that case the function is a one time use. + // args: the function arguments (a String) + // body: the function body, also a String + // + return dojo.hitch(calcEnv, calcEnv.Function.apply(calcEnv, arguments)); + }, + normalizedFunction: function(name, args, body){ + return dojo.hitch(calcEnv, calcEnv.normalizedFunction.apply(calcEnv, arguments)); + }, + deleteFunction: function(name){ + calcEnv[name] = undefined; + delete calcEnv[name]; + }, + eval: function(text){ + // summary + // create an anonymous function to run the code the parser generates from the user input. + // params + // text, type String, is the user input that needs to be parsed + // + return calcEnv.eval.apply(calcEnv, arguments); + }, + destroy: function(){ + this.inherited(arguments); + calcEnv = null; // assist garbage collection + } +}); +})(); + +(function(){ + var magicBigInt = (1 << 30) - 35; // 2^30 - 35 is a prime that ensures approx(n/(2^k)) != n/(2^k) for k >= 1 and n < 2^k + dojo.mixin(dojox.calc, { + approx: function(r){ + // summary: + // Return a less exact approximation of r such that approx(r * (1 +- eps)) == approx(r) + if(typeof r == "number"){ + return Math.round(r * magicBigInt) / magicBigInt; + } + return r; + } + }); +})(); diff --git a/calc/_ExecutorIframe.html b/calc/_ExecutorIframe.html new file mode 100644 index 0000000000..d17d32632d --- /dev/null +++ b/calc/_ExecutorIframe.html @@ -0,0 +1,525 @@ + + + + + diff --git a/calc/resources/Common.css b/calc/resources/Common.css new file mode 100644 index 0000000000..012a5ce449 --- /dev/null +++ b/calc/resources/Common.css @@ -0,0 +1,146 @@ +.dojoxCalcLayout span.dijitButtonNode, +.dojoxCalcLayout span.dijitButtonContents{ + display:block; +} +.dojoxCalc .dojoxCalcLayout .dojoxCalcTextAreaContainer { + padding-left: 0.2em; + padding-right: 0.2em; + #border-right: .3em; +} +.dojoxCalcLayout .dojoxCalcInputContainer .dijitTextBox { + /* IE bug workaround where the input caret is not visible (extends into the table border) */ + position: relative; + border-left: 0 none; + border-right: 0 none; + width:100%; +} +.dojoxCalcLayout .dojoxCalcInputContainer, +.dojoxCalcLayout .dojoxCalcInputContainer .dijitInputField { + padding-left: 0; + padding-right: 0; +} +.dojoxCalcLayout .dojoxCalcInputContainer .dijitInputContainer { + padding-left: 0; + #padding-left: 0.5em; + padding-right: 0.3em; + #padding-right: 0; +} +.dojoxCalcLayout .dojoxCalcInputContainer .dijitInputInner { + text-align: left; + padding-left: 0.2em !important; + padding-right: 0 !important; + #padding-right: 0.3em !important; /* IE workaround to make sure the input caret is visible */ + overflow: hidden; +} +.dojoxCalcMinusButtonContainer { + width: 4em; + min-width: 4em; + padding: 0; +} +.dojoxCalcMinusButtonContainer .dijitButton { + margin: 0.1em; + width: 3.8em; +} +.dojoxCalcMinusButtonContainer .dijitArrowButtonInner { + width:1.3em; +} +.dojoxCalcMinusButtonContainer .dijitButtonNode .dijitButtonContents { + width: 2.1em; + min-width: 2.1em; +} +.dojoxCalcLayout .dojoxCalcMinusButtonContainer .dijitButtonText, +.dojoxCalcLayout .dojoxCalcMinusButtonContainer .dijitButtonNode { + padding-left: 0px; + padding-right: 0px; +} + +.dojoxCalcButtonContainer { + width: 4em; + min-width: 4em; + padding: 0; +} +.dojoxCalcButtonContainer .dijitButton { + margin: 0.1em; + width: 3.8em; +} + +.dojoxCalcLayout .dojoxCalcButtonContainer .dijitButtonText, +.dojoxCalcLayout .dojoxCalcButtonContainer .dijitButtonNode { + padding-left: 0px; + padding-right: 0px; +} +.dojoxCalcLayout { + table-layout:fixed; + border-width: 0; + border-style: none; + width:16.0em; + font:monospace; +} +.dojoxCalc { + border: 0.4em ridge #909090; +} + +.dojoxCalcGrapherLayout span.dijitButtonNode, +.dojoxCalcGrapherLayout span.dijitButtonContents{ + display:block; +} +.dojoxCalcGrapherButtonContainer { + width: 10em; + min-width: 10em; + padding: 0; +} +.dojoxCalcGrapherButton { + display:block; +} +.dojoxCalcGrapherLayout { + table-layout:fixed; + border-width: 0; + border-style: none; + width:20.0em; + font:monospace; +} +.dojoxCalcExpressionBox { + width:15em; +} +.dojoxCalcChartHolder { + position:absolute; + left:36em; + top:5em; +} +.dojoxCalcGraphOptionTable { + width:25em; +} +.dojoxCalcGraphWidth { + width:5em; +} +.dojoxCalcGraphHeight { + width:5em; +} +.dojoxCalcGraphMinX { + width:5em; +} +.dojoxCalcGraphMaxX { + width:5em; +} +.dojoxCalcGraphMinY { + width:5em; +} +.dojoxCalcGraphMaxY { + width:5em; +} +.dojoxCalcGrapherFuncOuterDiv { + width:35em; + height:15em; + overflow-y:scroll; +} +.dojoxCalcGrapherModeTable { + width:10em; +} +.dojoxCalcFunctionModeSelector { + width:5em; +} +.dojoxCalcStatusBox { + border:1px solid; + padding:1px; + display:inline; +} diff --git a/calc/resources/GraphPro.css b/calc/resources/GraphPro.css new file mode 100644 index 0000000000..7d1181d30a --- /dev/null +++ b/calc/resources/GraphPro.css @@ -0,0 +1,7 @@ +/* + Adds cosmetic styling to the dojox.calc.GraphPro widget. Users may swap with a custom theme CSS file. + + NOTES: + --- +*/ +@import url("Common.css"); diff --git a/calc/resources/Standard.css b/calc/resources/Standard.css new file mode 100644 index 0000000000..c4fdd94582 --- /dev/null +++ b/calc/resources/Standard.css @@ -0,0 +1,7 @@ +/* + Adds cosmetic styling to the dojox.calc.Standard widget. Users may swap with a custom theme CSS file. + + NOTES: + --- +*/ +@import url("Common.css"); diff --git a/calc/templates/FuncGen.html b/calc/templates/FuncGen.html new file mode 100644 index 0000000000..a669f17dfb --- /dev/null +++ b/calc/templates/FuncGen.html @@ -0,0 +1,17 @@ +
+ + + + +
+ +
+ + + + +

+ +
+ +
diff --git a/calc/templates/GraphPro.html b/calc/templates/GraphPro.html new file mode 100644 index 0000000000..c76ec0c9fa --- /dev/null +++ b/calc/templates/GraphPro.html @@ -0,0 +1,165 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + + + +
+
+ (-) +
+
+
+
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ +
diff --git a/calc/templates/Grapher.html b/calc/templates/Grapher.html new file mode 100644 index 0000000000..7323f3d9aa --- /dev/null +++ b/calc/templates/Grapher.html @@ -0,0 +1,117 @@ +
+
+ +
+
Window Options
+
+ + + + + + + + + + + + + + + + + + + + + +
+ Width: + + + + Height: + + +
+ X >= + + + + X <= + + +
+ Y >= + + + + Y <= + + +
+
+
+ +
+ +
+ +
+
+ +
+
Add Function
+
+ + + + + + + + + +
+ Mode: + + + +
+ +
+
+
+
+
+ + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+
+
diff --git a/calc/templates/Standard.html b/calc/templates/Standard.html new file mode 100644 index 0000000000..5d95c865db --- /dev/null +++ b/calc/templates/Standard.html @@ -0,0 +1,101 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ + + + + +
+
+ (-) +
+
+
+
+ + + +
+ + + +
+ +
diff --git a/calc/tests/test_Executor.html b/calc/tests/test_Executor.html new file mode 100644 index 0000000000..3797f10321 --- /dev/null +++ b/calc/tests/test_Executor.html @@ -0,0 +1,287 @@ + + + Calculator Executor Test + + + + + + + dojox.calc._Executor tests +
Loading...
+ + + diff --git a/calc/tests/test_GraphPro.html b/calc/tests/test_GraphPro.html new file mode 100644 index 0000000000..b6c8547484 --- /dev/null +++ b/calc/tests/test_GraphPro.html @@ -0,0 +1,42 @@ + + + + GraphPro Calculator Test + + + + + + + + +
+ + diff --git a/calc/tests/test_Standard.html b/calc/tests/test_Standard.html new file mode 100644 index 0000000000..fa71851605 --- /dev/null +++ b/calc/tests/test_Standard.html @@ -0,0 +1,38 @@ + + + + Standard Calculator Test + + + + + + + + +
+ + diff --git a/calc/toFrac.js b/calc/toFrac.js new file mode 100644 index 0000000000..aa354eefda --- /dev/null +++ b/calc/toFrac.js @@ -0,0 +1,141 @@ +dojo.provide("dojox.calc.toFrac"); + +(function(){ + + var a = []; + var sqrts = [2,3,5,6,7,10,11,13,14,15,17,19,21,22,23,26,29, + 30,31,33,34,35,37,38,39,41,42,43,46,47,51,53,55,57,58,59, + 61,62,65,66,67,69,70,71,73,74,77,78,79,82,83,85,86,87,89,91,93,94,95,97]; + var _fracHashInitialized = false; + var i = -3; + var d = 2; + var epsilon = 1e-15 / 9; + + + +function _fracHashInit(searchNumber){ + // summary + // make a fairly large hash table of some fractions, sqrts, etc + var m, mt; + while(i < sqrts.length){ + switch(i){ + case -3: + m = 1; + mt = ''; + break; + case -2: + m = Math.PI; + mt = 'pi'; + break; + case -1: + m = Math.sqrt(Math.PI); + mt = '\u221A(pi)'; + break; + default: + m = Math.sqrt(sqrts[i]); + mt = "\u221A(" + sqrts[i] + ")"; + } + while(d <= 100){ + for(n = 1; n < (m == 1 ? d : 100); n++){ + var r = m * n / d; + var f = dojox.calc.approx(r); + if(!(f in a)){ + // make sure that it is simplified so that toFrac(pi) doesn't get 2*pi/2 + if(n==d){ + n=1; + d=1; + } + a[f] = {n:n, d:d, m:m, mt:mt}; + if(f == searchNumber){ searchNumber = undefined; } // found number, so return and finish hash in nbackground + } + } + d++; + if(searchNumber == undefined){ + setTimeout(function(){ _fracHashInit() }, 1); + return; + } + } + d = 2; + i++; + } + _fracHashInitialized = true; +} + +// this 1 is standard and the other is advanced and could be a +// separate dojo.require if the user wants the function (and slow init) +function isInt(n){ + return Math.floor(n) == n; +} +// quick and dirty _fracLookup for people who don't need advanced function +// handles integer + 1/integer constructs +function _fracLookup(number){ + number = Math.abs(number); + var i = Math.floor(number); + number %= 1; + var inv = dojox.calc.approx(1 / number); + return isInt(inv) ? { m: 1, mt: 1, n: i * inv + 1, d: inv } : null; +} +// make the hash +_fracHashInit(); +// advanced _fracLookup +function _fracLookup(number){ + function retryWhenInitialized(){ + _fracHashInit(number); + return _fracLookup(number); + } + number = Math.abs(number); + var f = a[dojox.calc.approx(number)]; + if(!f && !_fracHashInitialized){ + return retryWhenInitialized(); + } + if(!f){ + var i = Math.floor(number); + if(i == 0) { return _fracHashInitialized ? null : retryWhenInitialized(); } + var n = number % 1; + if(n == 0){ + return { m: 1, mt: 1, n: number, d: 1 } + } + f = a[dojox.calc.approx(n)]; + if(!f || f.m != 1){ + var inv = dojox.calc.approx(1 / n); + return isInt(inv) ? { m: 1, mt: 1, n: 1, d: inv } : (_fracHashInitialized ? null : retryWhenInitialized()); + }else{ + return { m: 1, mt: 1, n: (i * f.d + f.n), d: f.d }; + } + } + return f; +} +// add toFrac to the calculator +dojo.mixin(dojox.calc, { + toFrac: function(number){// get a string fraction for a decimal with a set range of numbers, based on the hash + var f = _fracLookup(number); + return f ? ((number < 0 ? '-' : '') + (f.m == 1 ? '' : (f.n == 1 ? '' : (f.n + '*'))) + (f.m == 1 ? f.n : f.mt) + ((f.d == 1 ? '' : '/' + f.d))) : number; + //return f ? ((number < 0 ? '-' : '') + (f.m == 1 ? '' : (f.n == 1 ? '' : (f.n + '*'))) + (f.m == 1 ? f.n : f.mt) + '/' + f.d) : number; + }, + pow: function(base, exponent){// pow benefits from toFrac because it can overcome many of the limitations set before the standard Math.pow + // summary: + // Computes base ^ exponent + // Wrapper to Math.pow(base, exponent) to handle (-27) ^ (1/3) + + if(isInt(exponent)){ + return Math.pow(base, exponent); + }else{ + var f = _fracLookup(exponent); + if(base >= 0){ + return (f && f.m == 1) + ? Math.pow(Math.pow(base, 1 / f.d), exponent < 0 ? -f.n : f.n) // 32 ^ (2/5) is much more accurate if done as (32 ^ (1/5)) ^ 2 + : Math.pow(base, exponent); + }else{ // e.g. (1/3) root of -27 = -3, 1 / exponent must be an odd integer for a negative base + return (f && f.d & 1) ? Math.pow(Math.pow(-Math.pow(-base, 1 / f.d), exponent < 0 ? -f.n : f.n), f.m) : NaN; + } + } +} +}); +/* +function reduceError(number){ + var f = _fracLookup(number); + if(!f){ f = _fracLookup(number); } + return f ? ((number < 0 ? -1 : 1) * f.n * f.m / f.d) : number; +} +*/ +})();