improved JavaScript/DHTML RPN calculator/formula editor

2006-03-15 Thread Kragen Sitaker
Now it graphs.  http://pobox.com/ragen/sw/js-calc.html

Javascript/DHTML Reverse Polish Calculator

#output div, input { padding: 1px; width: 60% }
#output div { border: 1px solid lightslategrey; margin: 1px 0 }
.instructions { float: right; width: 35%; color: darkslategrey; 
background-color: wheat; padding: 0.5em; font-size: smaller }
input { border: 1px solid yellow; font: inherit; margin: 0 }
.expr { color: darkslategrey; font-size: smaller }



/ Debugging stuff

function td(textcontent) {
  var rv = document.createElement('TD')
  rv.appendChild(document.createTextNode(textcontent))
  return rv
}

// table row containing two pieces of text
function tablerow(a, b) {
  var rv = document.createElement('TR')
  rv.appendChild(td(a))
  rv.appendChild(td(b))
  return rv
}

// dom node containing table displaying object properties
function domdump(obj) {
  var rv = document.createElement('TABLE')
  for (var prop in obj)
rv.appendChild(tablerow(prop, obj[prop]))
  return rv
}

// subset of an object
function slice(obj, props) {
  var rv = {}
  for (var i = 0; i < props.length; i++) rv[props[i]] = obj[props[i]]
  return rv
}

function $(id) {
  return document.getElementById(id)
}

// display relevant properties of an event
function debug_output(key) {
  var out = $('debug_output')
  if (!out) return
  out.removeChild(out.firstChild)
  out.appendChild(domdump(slice(key, ['isChar', 'keyCode', 'charCode', 
  'shiftKey', 'type'])))
  // out.appendChild(domdump(key))
}

/ Calculator primitive stuff
// At first it seemed like a good idea to just use DOM nodes for the
// calculator's stack; now that each stack item has three attributes,
// it seems like a less good idea.

// find the input we're using for this
function thefield() {
  return document.forms.theform.the_input
}

// return the value from the top of the stack (can't fail)
function pop_stack() {
  var tos = $('output').lastChild
  if (!tos) return {value: '0', expr: 0, atomic: 1}  // crude hack
  var rv_value = tos.firstChild.nodeValue
  var rv_expr = tos.firstChild.nextSibling.nextSibling.firstChild.nodeValue
  var atomic = tos.getAttribute('atomic')
  $('output').removeChild(tos)
  return {value: rv_value, expr: rv_expr, atomic: atomic}
}

function min(values) {
  var rv = values[0]
  for (var ii = 0; ii < values.length; ii++) {
if (values[ii] < rv) rv = values[ii]
  }
  return rv
}

function max(values) {
  var rv = values[0]
  for (var ii = 0; ii < values.length; ii++) {
if (values[ii] > rv) rv = values[ii]
  }
  return rv
}

function isFinite(number) {
  if (isNaN(number)) return false
  if (number == Infinity) return false
  if (number == -Infinity) return false
  return true
}

function filter(fun, array) {
  var rv = []
  for (var ii = 0; ii < array.length; ii++) {
if (fun(array[ii])) rv.push(array[ii])
  }
  return rv
}

// put a graph of an array into a node
function graph(val, node) {
  var canvas = document.createElement('canvas')
  if (!canvas.getContext) return  // no canvas support!
  var w = 200
  var h = 20
  canvas.setAttribute('width', w)
  canvas.setAttribute('height', h)
  node.appendChild(canvas)
  var ctx = canvas.getContext('2d')

  // determine coordinate transformation
  var ww = w + 1
  var hh = h + 1
  var per_sample = ww / val.length
  var minval = min(filter(isFinite, val))
  if (minval > 0) minval = 0
  var maxval = max(filter(isFinite, val))
  if (maxval < 1) maxval = 1
  var range = maxval - minval
  var vertical_xform = function(datum) {
// maybe I should have the canvas do this?
return h - (datum - minval) * h / range
  }
  // x-axis
  ctx.strokeStyle = 'grey'
  ctx.moveTo(0, vertical_xform(0))
  ctx.lineTo(ww, vertical_xform(0))

  // plot points
  ctx.strokeStyle = 'black'
  var lastPoint = false
  for (var ii = 0; ii < val.length; ii++) {
if (!isFinite(val[ii])) {
  lastPoint = false
  continue
}
var x = per_sample * (ii + 0.5)
var y = vertical_xform(val[ii])
if (lastPoint) {
  ctx.lineTo(x, y)
} else {
  ctx.moveTo(x, y)
}
lastPoint = true
  }
  ctx.stroke()
}

// push a value
function push_stack(val) {
  var n = document.createElement('DIV')
  n.appendChild(document.createTextNode(val.value))
  var eq = document.createElement('SPAN')
  eq.appendChild(document.createTextNode(' = '))
  n.appendChild(eq)
  var m = document.createElement('SPAN')
  m.className = 'expr'
  m.appendChild(document.createTextNode(val.expr))
  n.appendChild(m)
  var tograph = asarray('' + val.value)
  if (tograph instanceof Array) graph(tograph, n)
  if (val.atomic) n.setAttribute('atomic', 1)
  $('output').appendChild(n)
}

/ Calculator user operations

// before you invoke any operation, or when you hit Enter, push
// current text field onto stack
function append_output() {
  // "atomic" means no parens are ever needed around this expression
  var val = {value: thefield().value, expr: thefield

JavaScript/DHTML RPN calculator/formula editor

2005-04-08 Thread Kragen Sitaker
http://pobox.com/~kragen/sw/js-calc.html


Javascript/DHTML Reverse Polish Calculator


#output div, input { padding: 1px; width: 60% }
#output div { border: 1px solid lightslategrey; margin: 1px 0 }
.instructions { float: right; width: 35%; color: darkslategrey; 
background-color: wheat; padding: 0.5em; font-size: smaller }
input { border: 1px solid yellow; font: inherit; margin: 0 }
.expr { color: darkslategrey; font-size: smaller }



/ Debugging stuff

function td(textcontent) {
  var rv = document.createElement('TD')
  rv.appendChild(document.createTextNode(textcontent))
  return rv
}

// table row containing two pieces of text
function tablerow(a, b) {
  var rv = document.createElement('TR')
  rv.appendChild(td(a))
  rv.appendChild(td(b))
  return rv
}

// dom node containing table displaying object properties
function domdump(obj) {
  var rv = document.createElement('TABLE')
  for (var prop in obj)
rv.appendChild(tablerow(prop, obj[prop]))
  return rv
}

// subset of an object
function slice(obj, props) {
  var rv = {}
  for (var i = 0; i < props.length; i++) rv[props[i]] = obj[props[i]]
  return rv
}

// display relevant properties of an event
function debug_output(key) {
  var out = document.getElementById('debug_output')
  if (!out) return
  out.removeChild(out.firstChild)
  out.appendChild(domdump(slice(key, ['isChar', 'keyCode', 'charCode', 
'shiftKey', 'type'])))
  // out.appendChild(domdump(key))
}

/ Calculator primitive stuff
// At first it seemed like a good idea to just use DOM nodes for the
// calculator's stack; now that each stack item has three attributes,
// it seems like a less good idea.

// find the input we're using for this
function thefield() {
  return document.forms.theform.the_input
}

// return the value from the top of the stack (can't fail)
function pop_stack() {
  var o = document.getElementById('output')
  var tos = o.lastChild
  if (!tos) return {value: 0, expr: 0, atomic: 1}  // crude hack
  var rv_value = new Number(tos.firstChild.nodeValue)
  var rv_expr = tos.lastChild.firstChild.nodeValue
  var atomic = tos.getAttribute('atomic')
  o.removeChild(tos)
  return {value: rv_value, expr: rv_expr, atomic: atomic}
}

// push a value
function push_stack(val) {
  var n = document.createElement('DIV')
  n.appendChild(document.createTextNode(val.value))
  var eq = document.createElement('SPAN')
  eq.appendChild(document.createTextNode(' = '))
  n.appendChild(eq)
  var m = document.createElement('SPAN')
  m.className = 'expr'
  m.appendChild(document.createTextNode(val.expr))
  n.appendChild(m)
  if (val.atomic) n.setAttribute('atomic', 1)
  document.getElementById('output').appendChild(n)
}

/ Calculator user operations

// before you invoke any operation, or when you hit Enter, push
// current text field onto stack
function append_output() {
  // "atomic" means no parens are ever needed around this expression
  var val = {value: thefield().value, expr: thefield().value, atomic: 1}
  if (val.value == '') {
val = pop_stack()
push_stack(val)
  }
  push_stack(val)
  thefield().value = ''
  return false
}

// is the input field empty?
function empty_field() {
  return (thefield().value == '')
}

function handle_backspace(key) {
  if (empty_field()) {
pop_stack()
return false
  } else return true
}

// get properly wrapped value
function get_expr(obj) {
  if (obj.atomic) return obj.expr
  return "(" + obj.expr + ")"
}

// binary numerical operation
function bin_num_op(fun, op) {
  if (!empty_field()) append_output() 
  var x = pop_stack()
  var y = pop_stack()
  push_stack({value: fun(y.value, x.value), 
expr: get_expr(y) + " " + op + " " + get_expr(x)})
  return false
}

// unary numerical operation
function un_num_op(fun, op) {
  if (!empty_field()) append_output() 
  var v = pop_stack()
  push_stack({value: fun(v.value), expr: op + get_expr(v)})
  return false
}

// swap top two items on stack.
function swap_stack() {
  var x = pop_stack()
  var y = pop_stack()
  push_stack(x)
  push_stack(y)
}

// called when a non-modifier key is pressed and released in the input field
function handle_keypress(key) {
  var code = key.keyCode
  var chr = key.charCode
  debug_output(key)
  // I tried rewriting this with tables, and it was more complicated and 
  // no shorter.
  if (code == key.DOM_VK_RETURN) {
append_output()
return false
  } else if (code == key.DOM_VK_BACK_SPACE) {
return handle_backspace(key)
  } else if (chr == 43) { // +
return bin_num_op(function(a, b){return a + b}, '+')
  } else if (chr == 42) { // *
return bin_num_op(function(a, b){return a * b}, '*')
  } else if (chr == 47) { // '/'
return bin_num_op(function(a, b){return a / b}, '/')
  } else if (chr == 45) { // -
return bin_num_op(function(a, b){return a - b}, '-')
  } else if (chr == 94) { // ^
return bin_num_op(Math.pow, '^')
  } else if (code == 9) { // tab
if (!empty_field()) append_output()
swap_stack()
return fals