diff --git a/README.md b/README.md index 0acf219..cea9981 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# function-plot +# function-plot [![NPM][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] @@ -15,7 +15,7 @@ utility: [y = x * x](https://www.google.com/webhp?sourceid=chrome-instant&ion=1& The library currently supports interactive line charts and scatterplots, whenever the graph scale is modified the function is evaluated again with the new bounds, result: infinite graphs! -Have a look at [the homepage](http://maurizzzio.github.io/function-plot/) +[**homepage**](http://maurizzzio.github.io/function-plot/) ## Install @@ -50,9 +50,9 @@ var functionPlot = require('function-plot'); * `options` * `options.target` {string} the selector of the parent element to render the graph to * `options.title` {string} If set the chart will have it as a title on the top - * `options.xDomain` {array} domain of the linear scale (used in the x axis) + * `options.xDomain` {array} domain of the linear scale (used in the x axis) * `options.yDomain` {array} domain of the linear scale (used in the y axis) - * `options.xLabel` {string} x axis label + * `options.xLabel` {string} x axis label * `options.yLabel` {string} y axis label * `options.disableZoom` {boolean} true to disable drag and zoom on the graph * `options.grid` {boolean} true to show a grid @@ -65,50 +65,54 @@ var functionPlot = require('function-plot'); * `options.annotations` {array} An array defining parallel lines to the y-axis or the x-axis * `options.annotations[i].x` {number} x-coordinate of the line parallel to the y-axis * `options.annotations[i].y` {number} y-coordinate of the line parallel to the x-axis - * `options.annotations[i].text` {string} text shown next to the parallel line + * `options.annotations[i].text` {string} text shown next to the parallel line * `options.data` {array} *required* An array defining the functions to be rendered - * `options.data[i].title` {string} title of the function - * `options.data[i].color` {string} color of the function - * `options.data[i].skipTip` {boolean} true to avoid this function from being a target of the tip - * `options.data[i].fn` *required* {string} the function that represents the curve, this function is evaluated - with values which are in `range` limiting the values to the screen min/max coordinates for `x`, i.e. - at any given time the graph min/max x coordinates will limit the range of values to be plotted - * `options.data[i].range` {number[]} if given the function will only be evaluated with multiple values from this range - * `options.data[i].samples` {number} the fixed number of samples to be computed within the current domain ends - * `options.data[i].implicit` {boolean} true to creates samples for the function considering it implicit, it assumes - that the function depends on the variables *x* and *y* - * `options.data[i].secants` {Object[]} Secants of `options.data[i].fn` - * `options.data[i].secants[j].x0` {number} The abscissa of the first point - * `options.data[i].secants[j].x1` {number} (optional if `updateOnMouseMove` is set) The abscissa of the second point - * `options.data[i].secants[j].updateOnMouseMove` {boolean} (optional) True to update the secant line by evaluating - `options.data[i].fn` with the current mouse position (`x0` is the fixed point and `x1` is computed - dynamically based on the current mouse position) - * `options.data[i].derivative` {Object} Info of the instantaneous rate of change of y with respect to x - * `options.data[i].derivative.fn` {string} The derivative of `options.data[i].fn` - * `options.data[i].derivative.x0` {number} The abscissa of the point which belongs to the curve - represented by `options.data[i].fn` whose tangent will be computed (i.e. the tangent line to the point - `x0, fn(x0)`) - * `options.data[i].derivative.updateOnMouseMove` {boolean} True to compute the tangent line by evaluating - `options.data[i].derivative.fn` with the current mouse position (i.e. let `x0` be the abscissa of the - mouse position transformed to local coordinates, the tangent line to the point `x0, fn(x0)`) - * `options.data[i].graphOptions` {Object} options passed to the the files located in `lib/type/` which plot the data - generated by the library, the most useful property of this object is `type` which is used to determine the type of - graph to be rendered for a function * `options.plugins` {array} An array describing plugins to be run when the graph is initialized, check out the examples on the main page - -### `options.data[i].graphOptions` {Object} -* `type` {string} type of graph, currently `line`, `scatter` and `interval` are supported, `interval` is the default option -* `sampler` {string} the sampler to use, currently `interval` and `builtIn` are supported (`interval` should -be used with `type: 'interval'` and `builtIn` with `type: 'line'` or `type: 'scatter'`) +### `options.data` {Array} -Depending on the type option: +An array of objects, each object contains info of a function to render and can have the following options -* `options.closed` {boolean} (only if `type: 'interval'` or `type: 'line'`) True to close the path, for any segment of the closed area graph - `y0` will be 0 and `y1` will be `f(x)` - -#### if `options.data[i].parametric = true` +* `title` {string} title of the function +* `skipTip` {boolean=false} true to make the tip ignore this function +* `range` {number[]=[-Infinity, Infinity]} an array with two numbers, the function will only be evaluated with values that belong to this interval +* `nSamples` {number} The number of values to be taken from `range` to evaluate the function, note that if interval-arithmetic is used the function +will be evaluated with intervals instead of single values +* `graphType` {string='interval'} The type of graph to render, available values are `interval|polyline|scatter` +* `fnType` {string='linear'} The type of function to render, available values are `linear|parametric|implicit|polar|points|vector` +* `sampler` {string='interval'} The sampler to take samples from `range`, available values are `interval|builtIn` + * **NOTE: `builtIn` should only be used when `graphType` is `polyline|scatter`** + * **NOTE: when math.js is included in the webpage it will be used instead of the bundled sampler** + +Additional style related options + +* `color` {string} color of the function to render +* `closed` {boolean=false} (only if `graphType: 'polyline'` or `graphType: 'scatter'`) True to close the path, for any segment of the closed area graph + `y0` will be 0 and `y1` will be `f(x)` + +When `derivative` {Object} is present on a datum + +* `derivative.fn` {string} The derivative of `fn` +* `derivative.x0` {number} The abscissa of the point which belongs to the curve +represented by `fn` whose tangent will be computed (i.e. the tangent line to the point +`x0, fn(x0)`) +* `derivative.updateOnMouseMove` {boolean} True to compute the tangent line by evaluating +`derivative.fn` with the current mouse position (i.e. let `x0` be the abscissa of the +mouse position transformed to local coordinates, the tangent line to the point `x0, fn(x0)`) + +When `secants` {Array} is present on a datum + +* `secants[i].x0` {number} The abscissa of the first point +* `secants[i].x1` {number} (optional if `updateOnMouseMove` is set) The abscissa of the second point +* `secants[i].updateOnMouseMove` {boolean} (optional) True to update the secant line by evaluating +`fn` with the current mouse position (`x0` is the fixed point and `x1` is computed dynamically based on the current mouse position) + +#### if `fnType: 'linear'` (default) + +* `fn` {string} the function that represents the curve, this function is evaluated with values which are inside `range` + +#### if `fnType: 'parametric'` - `x` the x-coordinate of a point to be sampled with a parameter `t` - `y` the y-coordinate of a point to be sampled with a parameter `t` @@ -116,18 +120,31 @@ Depending on the type option: to determine the possible values of `t`, remember that the number of samples is set in the property `samples` -#### if `options.data[i].polar = true` +#### if `fnType: 'polar'` - `r` a polar equation in terms of `theta` - `range = [-Math.PI, Math.PI]` the `range` property in polar equations is used to determine the possible values of `theta`, remember that the number of samples is set in the property `samples` - -#### if `options.data[i].implicit = true` + +#### if `fnType: 'implicit'` - `fn` a function which needs to be expressed in terms of `x` and `y` -### `instance` +**NOTE: implicit functions can only be rendered using interval-arithmetic** + +#### if `fnType: 'points'` + +- `points` an array of 2-number array which hold the coordinates of the points to render + +**NOTE: make sure your type of graph is either `scatter` or `polyline`** + +#### if `fnType: 'vector'` + +- `vector` {Array} an 2-number array which has the ends of the vector +- `offset` {Array=[0, 0]} (optional) vector's offset + +### `instance` * `instance.id` {string} a random generated id made out of letters and numbers * `instance.linkedGraphs` {array} array of function-plot instances linked to the events of this instance, @@ -154,8 +171,8 @@ events can be triggered by doing `instance.emit([eventName][, params])` * `mousemove` fired whenever the mouse is moved inside the canvas, callback params `x`, `y` (in canvas space coordinates) * `mouseout` fired whenever the mouse is moved outside the canvas -* `before:draw` fired before drawing all the graphs -* `after:draw` fired after drawing all the graphs +* `before:draw` fired before drawing all the graphs +* `after:draw` fired after drawing all the graphs * `zoom:scaleUpdate` fired whenever the scale of another graph is updated, callback params `xScale`, `yScale` (x-scale and y-scale of another graph whose scales were updated) * `tip:update` fired whenever the tip position is updated, callback params `x`, `y`, `index` (in canvas @@ -184,7 +201,7 @@ When the `definite-integral` plugin is included the instance will fire the follo ## Recipes ### Evaluate a function at some value `x` - + ```javascript var y = functionPlot.eval.builtIn(datum, fnProperty, scope) ``` @@ -306,7 +323,7 @@ Selectors (sass) // d attribute defines the graph bounds } } - + .y.axis { .tick { line { @@ -334,7 +351,7 @@ npm start Open `127.0.0.1:5555` and that's it! Local development server powered [beefy](https://www.npmjs.com/package/beefy) -Plain demo: `127.0.0.1:5555/demo.html` +Development page: `127.0.0.1:5555/playground.html` ## License diff --git a/bower.json b/bower.json index 9ea9cc9..a5f222e 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "function-plot", - "main": "site/js/function-plot.js", + "main": "dist/function-plot.js", "homepage": "https://github.com/maurizzzio/function-plot", "authors": [ "Mauricio Poppe " diff --git a/index.js b/index.js index f38a51f..9615572 100644 --- a/index.js +++ b/index.js @@ -1,613 +1 @@ -/* - * function-plot - * - * Copyright (c) 2015 Mauricio Poppe - * Licensed under the MIT license. - */ -'use strict' -require('./lib/polyfills') - -var d3 = window.d3 - -var events = require('events') -var extend = require('extend') - -var mousetip = require('./lib/tip') -var utils = require('./lib/utils') -var helpers = require('./lib/helpers/') -var annotations = require('./lib/helpers/annotations') - -var assert = utils.assert - -var globals -var types -var cache = [] - -module.exports = function (options) { - options = options || {} - options.data = options.data || [] - - // globals - var width, height - var margin - var zoomBehavior - var xScale, yScale - var line = d3.svg.line() - .x(function (d) { return xScale(d[0]) }) - .y(function (d) { return yScale(d[1]) }) - - function Chart () { - var n = Math.random() - var letter = String.fromCharCode(Math.floor(n * 26) + 97) - this.id = options.id = letter + n.toString(16).substr(2) - this.linkedGraphs = [this] - this.options = options - cache[this.id] = this - this.setUpEventListeners() - } - - Chart.prototype = Object.create(events.prototype) - - /** - * Rebuilds the entire graph from scratch recomputing - * - * - the inner width/height - * - scales/axes - * - * After this is done it does a complete redraw of all the datums, - * if only the datums need to be redrawn call `instance.draw()` instead - * - * @returns {Chart} - */ - Chart.prototype.build = function () { - this.internalVars() - this.drawGraphWrapper() - return this - } - - Chart.prototype.updateScaleAxes = function () { - var xDomain = this.meta.xDomain - var yDomain = this.meta.yDomain - - var integerFormat = d3.format('s') - var format = function (scale) { - return function (d) { - var decimalFormat = scale.tickFormat(10) - var isInteger = d === +d && d === (d | 0) - // integers: d3.format('s'), see https://github.com/mbostock/d3/wiki/Formatting - // decimals: default d3.scale.linear() formatting see - // https://github.com/mbostock/d3/blob/master/src/svg/axis.js#L29 - return isInteger ? integerFormat(d) : decimalFormat(d) - } - } - - xScale = this.meta.xScale = d3.scale.linear() - .domain(xDomain) - .range([0, width]) - yScale = this.meta.yScale = d3.scale.linear() - .domain(yDomain) - .range([height, 0]) - this.meta.xAxis = d3.svg.axis() - .scale(xScale) - .tickSize(options.grid ? -height : 0) - .tickFormat(format(xScale)) - .orient('bottom') - this.meta.yAxis = d3.svg.axis() - .scale(yScale) - .tickSize(options.grid ? -width : 0) - .tickFormat(format(yScale)) - .orient('left') - } - - Chart.prototype.internalVars = function () { - - // measurements and other derived data - this.meta = {} - - margin = this.meta.margin = {left: 30, right: 30, top: 20, bottom: 20} - // if there's a title make the top margin bigger - if (options.title) { - this.meta.margin.top = 40 - } - - zoomBehavior = this.meta.zoomBehavior = d3.behavior.zoom() - - // inner width/height - width = this.meta.width = (options.width || globals.DEFAULT_WIDTH) - - margin.left - margin.right - height = this.meta.height = (options.height || globals.DEFAULT_HEIGHT) - - margin.top - margin.bottom - - function computeYScale (xScale) { - var xDiff = xScale[1] - xScale[0] - return height * xDiff / width - } - - var xLimit = 12 - var xDomain = this.meta.xDomain = options.xDomain || [-xLimit / 2, xLimit / 2] - var yLimit = computeYScale(xDomain) - var yDomain = this.meta.yDomain = options.yDomain || [-yLimit / 2, yLimit / 2] - - assert(xDomain[0] < xDomain[1]) - assert(yDomain[0] < yDomain[1]) - - // scale/axes - this.updateScaleAxes() - } - - Chart.prototype.drawGraphWrapper = function () { - var root = this.root = d3.select(options.target).selectAll('svg') - .data([options]) - - // enter - this.root.enter = root.enter() - .append('svg') - .attr('class', 'function-plot') - .attr('font-size', this.getFontSize()) - - // merge - root - .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom) - - this.buildTitle() - this.buildLegend() - this.buildCanvas() - this.buildClip() - this.buildAxis() - this.buildAxisLabel() - - // draw each datum after the wrapper was set up - this.draw() - - // helper to detect the closest fn to the cursor's current abscissa - var tip = this.tip = mousetip(extend(options.tip, { owner: this })) - this.canvas - .call(tip) - - this.buildZoomHelper() - this.setUpPlugins() - } - - Chart.prototype.buildTitle = function () { - // join - var selection = this.root.selectAll('text.title') - .data(function (d) { - return [d.title].filter(Boolean) - }) - - // enter - selection.enter() - .append('text') - .attr('class', 'title') - .attr('y', margin.top / 2) - .attr('x', margin.left + width / 2) - .attr('font-size', 25) - .attr('text-anchor', 'middle') - .attr('alignment-baseline', 'middle') - .text(options.title) - - // exit - selection.exit().remove() - } - - Chart.prototype.buildLegend = function () { - // enter - this.root.enter - .append('text') - .attr('class', 'top-right-legend') - .attr('text-anchor', 'end') - - // update + enter - this.root.select('.top-right-legend') - .attr('y', margin.top / 2) - .attr('x', width + margin.left) - } - - Chart.prototype.buildCanvas = function () { - var self = this - - this.meta.zoomBehavior - .x(xScale) - .y(yScale) - .on('zoom', function onZoom () { - self.emit('all:zoom', xScale, yScale) - }) - - // enter - var canvas = this.canvas = this.root - .selectAll('.canvas') - .data(function (d) { return [d] }) - - this.canvas.enter = canvas.enter() - .append('g') - .attr('class', 'canvas') - - // enter + update - } - - Chart.prototype.buildClip = function () { - // (so that the functions don't overflow on zoom or drag) - var id = this.id - var defs = this.canvas.enter.append('defs') - defs.append('clipPath') - .attr('id', 'function-plot-clip-' + id) - .append('rect') - .attr('class', 'clip static-clip') - - // enter + update - this.canvas.selectAll('.clip') - .attr('width', width) - .attr('height', height) - - // marker clip (for vectors) - this.markerId = this.id + '-marker' - defs.append('clipPath') - .append('marker') - .attr('id', this.markerId) - .attr('viewBox', '0 -5 10 10') - .attr('refX', 10) - .attr('markerWidth', 5) - .attr('markerHeight', 5) - .attr('orient', 'auto') - .append('svg:path') - .attr('d', 'M0,-5L10,0L0,5L0,0') - .attr('stroke-width', '0px') - .attr('fill-opacity', 1) - .attr('fill', '#777') - } - - Chart.prototype.buildAxis = function () { - // axis creation - var canvasEnter = this.canvas.enter - canvasEnter.append('g') - .attr('class', 'x axis') - canvasEnter.append('g') - .attr('class', 'y axis') - - // update - this.canvas.select('.x.axis') - .attr('transform', 'translate(0,' + height + ')') - .call(this.meta.xAxis) - this.canvas.select('.y.axis') - .call(this.meta.yAxis) - - } - - Chart.prototype.buildAxisLabel = function () { - // axis labeling - var xLabel, yLabel - var canvas = this.canvas - - xLabel = canvas.selectAll('text.x.axis-label') - .data(function (d) { - return [d.xLabel].filter(Boolean) - }) - xLabel.enter() - .append('text') - .attr('class', 'x axis-label') - .attr('text-anchor', 'end') - xLabel - .attr('x', width) - .attr('y', height - 6) - .text(function (d) { return d }) - xLabel.exit().remove() - - yLabel = canvas.selectAll('text.y.axis-label') - .data(function (d) { - return [d.yLabel].filter(Boolean) - }) - yLabel.enter() - .append('text') - .attr('class', 'y axis-label') - .attr('y', 6) - .attr('dy', '.75em') - .attr('text-anchor', 'end') - .attr('transform', 'rotate(-90)') - yLabel - .text(function (d) { return d }) - yLabel.exit().remove() - } - - /** - * @private - * - * Draws each of the datums stored in data.options, to do a full - * redraw call `instance.draw()` - */ - Chart.prototype.buildContent = function () { - var self = this - var canvas = this.canvas - - canvas - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') - .call(zoomBehavior) - .each(function () { - var el = d3.select(this) - // make a copy of all the listeners available to be removed/added later - var listeners = ['mousedown', 'mousewheel', 'mouseover', 'DOMMouseScroll', 'dblclick', 'wheel', 'MozMousePixelScroll'] - listeners = listeners.map(function (l) { return l + '.zoom' }) - if (!el._hasZoomListeners) { - listeners.forEach(function (l) { - el['_' + l] = el.on(l) - }) - } - function setState (state) { - listeners.forEach(function (l) { - state ? el.on(l, el['_' + l]) : el.on(l, null) - }) - } - setState(!options.disableZoom) - }) - - var content = this.content = canvas.selectAll(':scope > g.content') - .data(function (d) { return [d] }) - - // g tag clipped to hold the data - content.enter() - .append('g') - .attr('clip-path', 'url(#function-plot-clip-' + this.id + ')') - .attr('class', 'content') - - // helper line, x = 0 - var yOrigin = content.selectAll(':scope > path.y.origin') - .data([ [[0, yScale.domain()[0]], [0, yScale.domain()[1]]] ]) - yOrigin.enter() - .append('path') - .attr('class', 'y origin') - .attr('stroke', 'black') - .attr('opacity', 0.2) - yOrigin.attr('d', line) - - // helper line y = 0 - var xOrigin = content.selectAll(':scope > path.x.origin') - .data([ [[xScale.domain()[0], 0], [xScale.domain()[1], 0]] ]) - xOrigin.enter() - .append('path') - .attr('class', 'x origin') - .attr('stroke', 'black') - .attr('opacity', 0.2) - xOrigin.attr('d', line) - - // annotations (parallel to the y-axis) - content - .call(annotations({ owner: self })) - - // content construction (based on graphOptions) - // join - var graphs = content.selectAll(':scope > g.graph') - .data(function (d) { return d.data }) - // enter - graphs - .enter() - .append('g') - .attr('class', 'graph') - - function extendIf () { - var target = arguments[0] - for (var i = 1; i < arguments.length; i += 1) { - var source = arguments[i] - for (var p in source) { - if (source.hasOwnProperty(p) && !target.hasOwnProperty(p)) { - target[p] = source[p] - } - } - } - return target - } - - // enter + update - graphs - .each(function (data, index) { - var options = data.graphOptions = data.graphOptions || {} - extendIf(options, { type: 'interval' }) - - // if the type of graph chosen is not `interval` then default the sampler to `builtIn` - var sampler = options.type !== 'interval' - ? 'builtIn' - : 'interval' - - extendIf(options, { - sampler: sampler, - owner: self, - index: index - }) - // shortcuts - // - vector needs the builtIn evaluator - if (data.vector) { - options.sampler = 'builtIn' - options.type = 'line' - } - - d3.select(this) - .call(types[options.type](options)) - d3.select(this) - .call(helpers(options)) - }) - } - - Chart.prototype.buildZoomHelper = function () { - // dummy rect (detects the zoom + drag) - var self = this - - // enter - this.canvas.enter - .append('rect') - .attr('class', 'zoom-and-drag') - .style('fill', 'none') - .style('pointer-events', 'all') - - // update - this.canvas.select('.zoom-and-drag') - .attr('width', width) - .attr('height', height) - .on('mouseover', function () { - self.emit('all:mouseover') - }) - .on('mouseout', function () { - self.emit('all:mouseout') - }) - .on('mousemove', function () { - self.emit('all:mousemove') - }) - } - - Chart.prototype.setUpPlugins = function () { - var plugins = options.plugins || [] - var self = this - plugins.forEach(function (plugin) { - plugin(self) - }) - } - - Chart.prototype.addLink = function () { - for (var i = 0; i < arguments.length; i += 1) { - this.linkedGraphs.push(arguments[i]) - } - } - - Chart.prototype.updateAxes = function () { - var instance = this - var canvas = instance.canvas - canvas.select('.x.axis').call(instance.meta.xAxis) - canvas.select('.y.axis').call(instance.meta.yAxis) - - // updates the style of the axes - canvas.selectAll('.axis path, .axis line') - .attr('fill', 'none') - .attr('stroke', 'black') - .attr('shape-rendering', 'crispedges') - .attr('opacity', 0.1) - } - - Chart.prototype.syncOptions = function () { - // update the original options yDomain and xDomain - this.options.xDomain = this.meta.xScale.domain() - this.options.yDomain = this.meta.yScale.domain() - } - - Chart.prototype.programmaticZoom = function (xDomain, yDomain) { - var instance = this - d3.transition() - .duration(750) - .tween('zoom', function () { - var ix = d3.interpolate(xScale.domain(), xDomain) - var iy = d3.interpolate(yScale.domain(), yDomain) - return function (t) { - zoomBehavior - .x(xScale.domain(ix(t))) - .y(yScale.domain(iy(t))) - instance.draw() - } - }) - .each('end', function () { - instance.emit('programmatic-zoom') - }) - } - - Chart.prototype.getFontSize = function () { - return Math.max(Math.max(width, height) / 50, 8) - } - - Chart.prototype.draw = function () { - var instance = this - instance.emit('before:draw') - instance.syncOptions() - instance.updateAxes() - instance.buildContent() - instance.emit('after:draw') - } - - Chart.prototype.setUpEventListeners = function () { - var instance = this - - var events = { - mousemove: function (x, y) { - instance.tip.move(x, y) - }, - mouseover: function () { - instance.tip.show() - }, - mouseout: function () { - instance.tip.hide() - }, - 'zoom:scaleUpdate': function (xOther, yOther) { - zoomBehavior - .x(xScale.domain(xOther.domain())) - .y(yScale.domain(yOther.domain())) - }, - 'tip:update': function (x, y, index) { - var meta = instance.root.datum().data[index] - var title = meta.title || '' - var format = meta.renderer || function (x, y) { - return x.toFixed(3) + ', ' + y.toFixed(3) - } - - var text = [] - title && text.push(title) - text.push(format(x, y)) - - instance.root.select('.top-right-legend') - .attr('fill', globals.COLORS[index]) - // .text(x.toFixed(3) + ', ' + y.toFixed(3)) - .text(text.join(' ')) - } - } - - var all = { - mousemove: function () { - var mouse = d3.mouse(instance.root.select('rect.zoom-and-drag').node()) - var x = xScale.invert(mouse[0]) - var y = yScale.invert(mouse[1]) - instance.linkedGraphs.forEach(function (graph) { - graph.emit('mousemove', x, y) - }) - }, - - zoom: function (xScale, yScale) { - instance.linkedGraphs.forEach(function (graph, i) { - // since its scale was updated through d3.behavior.zoom - // we don't need to do it again - if (i) { - graph.emit('zoom:scaleUpdate', xScale, yScale) - } - // the first element is the instance who fired the event, - // content draw - graph.draw() - }) - - // emit the position of the mouse to all the registered graphs - instance.emit('all:mousemove') - } - } - - Object.keys(events).forEach(function (e) { - instance.on(e, events[e]) - // create an event for each event existing on `events` in the form 'all:' event - // e.g. all:mouseover all:mouseout - // the objective is that all the linked graphs receive the same event as the current graph - !all[e] && instance.on('all:' + e, function () { - var args = Array.prototype.slice.call(arguments) - instance.linkedGraphs.forEach(function (graph) { - var localArgs = args.slice() - localArgs.unshift(e) - graph.emit.apply(graph, localArgs) - }) - }) - }) - - Object.keys(all).forEach(function (e) { - instance.on('all:' + e, all[e]) - }) - } - - var instance = cache[options.id] - if (!instance) { - instance = new Chart() - } - return instance.build() -} -globals = module.exports.globals = require('./lib/globals') -types = module.exports.types = require('./lib/types/') -module.exports.plugins = require('./lib/plugins/') -module.exports.eval = require('./lib/helpers/eval') +module.exports = require('./lib/') diff --git a/lib/data.js b/lib/data.js index 26f72fa..0e6c8a9 100644 --- a/lib/data.js +++ b/lib/data.js @@ -3,7 +3,6 @@ */ 'use strict' var globals = require('./globals') - var evalTypeFn = { interval: require('./samplers/interval'), builtIn: require('./samplers/builtIn') @@ -15,11 +14,11 @@ var evaluator = { * from which the sampler will take samples * * @param {Chart} chart - * @param {options} datum An item from `data` - * @returns {[]} + * @param {Object} d An item from `data` + * @returns {Array} */ - range: function (chart, datum) { - var range = datum.range || [-Infinity, Infinity] + range: function (chart, d) { + var range = d.range || [-Infinity, Infinity] var scale = chart.meta.xScale var start = Math.max(scale.domain()[0], range[0]) var end = Math.min(scale.domain()[1], range[1]) @@ -30,23 +29,22 @@ var evaluator = { * Decides which sampler function to call based on the options * of `data` * - * @param {Object} options The options received originally in any of `lib/types/*` - * @param {Object} datum a.k.a a single item from `data` + * @param {Object} chart Chart instance which is orchestating this sampling operation + * @param {Object} d a.k.a a single item from `data` * @returns {Array} */ - eval: function (options, datum) { - var chart = options.owner - var range = this.range(chart, datum) + eval: function (chart, d) { + var range = this.range(chart, d) var data - var evalFn = evalTypeFn[datum.graphOptions.sampler] - var nSamples = datum.samples || Math.min( + var evalFn = evalTypeFn[d.sampler] + var nSamples = d.nSamples || Math.min( globals.MAX_ITERATIONS, globals.DEFAULT_ITERATIONS || (chart.meta.width * 2) ) - data = evalFn(chart, datum, range, nSamples) + data = evalFn(chart, d, range, nSamples) // NOTE: it's impossible to listen for the first eval event // as the event is already fired when a listener is attached - chart.emit('eval', data, options.index, options.isHelper) + chart.emit('eval', data, d.index, d.isHelper) return data } } diff --git a/lib/types/index.js b/lib/graph-types/index.js similarity index 80% rename from lib/types/index.js rename to lib/graph-types/index.js index c3d1622..b22aa89 100644 --- a/lib/types/index.js +++ b/lib/graph-types/index.js @@ -3,7 +3,7 @@ */ 'use strict' module.exports = { - line: require('./line'), + polyline: require('./polyline'), interval: require('./interval'), scatter: require('./scatter') } diff --git a/lib/types/interval.js b/lib/graph-types/interval.js similarity index 76% rename from lib/types/interval.js rename to lib/graph-types/interval.js index f76c4cf..8313b16 100644 --- a/lib/types/interval.js +++ b/lib/graph-types/interval.js @@ -6,10 +6,10 @@ var d3 = window.d3 var dataBuilder = require('../data') var utils = require('../utils') -module.exports = function (options) { +module.exports = function (chart) { var minWidthHeight - var xScale = options.owner.meta.xScale - var yScale = options.owner.meta.yScale + var xScale = chart.meta.xScale + var yScale = chart.meta.yScale function clampRange (vLo, vHi, gLo, gHi) { var hi = Math.min(vHi, gHi) @@ -21,7 +21,7 @@ module.exports = function (options) { return [lo, hi] } - var line = function (points) { + var line = function (points, closed) { var path = '' var minY = yScale.range()[1] var maxY = yScale.range()[0] @@ -32,7 +32,7 @@ module.exports = function (options) { var yLo = y.lo var yHi = y.hi // if options.closed is set to true then one of the bounds must be zero - if (options.closed) { + if (closed) { yLo = Math.min(yLo, 0) yHi = Math.max(yHi, 0) } @@ -53,11 +53,11 @@ module.exports = function (options) { } function plotLine (selection) { - var index = options.index - - selection.each(function (data) { + selection.each(function (d) { var el = plotLine.el = d3.select(this) - var evaluatedData = dataBuilder.eval(options, data) + var index = d.index + var closed = d.closed + var evaluatedData = dataBuilder.eval(chart, d) var innerSelection = el.selectAll(':scope > path.line') .data(evaluatedData) @@ -71,13 +71,11 @@ module.exports = function (options) { // enter + update innerSelection - .each(function () { - var path = d3.select(this) - path - .attr('stroke-width', minWidthHeight) - .attr('stroke', utils.color(data, options.index)) - .attr('opacity', options.closed ? 0.5 : 1) - .attr('d', line) + .attr('stroke-width', minWidthHeight) + .attr('stroke', utils.color(d, index)) + .attr('opacity', closed ? 0.5 : 1) + .attr('d', function (d) { + return line(d, closed) }) innerSelection.exit().remove() diff --git a/lib/types/line.js b/lib/graph-types/polyline.js similarity index 64% rename from lib/types/line.js rename to lib/graph-types/polyline.js index c8c683b..03193d0 100644 --- a/lib/types/line.js +++ b/lib/graph-types/polyline.js @@ -6,9 +6,9 @@ var d3 = window.d3 var dataBuilder = require('../data') var utils = require('../utils') -module.exports = function (options) { - var xScale = options.owner.meta.xScale - var yScale = options.owner.meta.yScale +module.exports = function (chart) { + var xScale = chart.meta.xScale + var yScale = chart.meta.yScale var line = d3.svg.line() .interpolate('linear') .x(function (d) { return xScale(d[0]) }) @@ -19,12 +19,12 @@ module.exports = function (options) { .y1(function (d) { return yScale(d[1]) }) function plotLine (selection) { - var index = options.index - selection.each(function (data) { + selection.each(function (d) { var el = plotLine.el = d3.select(this) - var evaluatedData = dataBuilder.eval(options, data) - var color = utils.color(data, options.index) + var index = d.index + var evaluatedData = dataBuilder.eval(chart, d) + var color = utils.color(d, index) var innerSelection = el.selectAll(':scope > path.line') .data(evaluatedData) @@ -33,30 +33,29 @@ module.exports = function (options) { .attr('class', 'line line-' + index) .attr('stroke-width', 1) .attr('stroke-linecap', 'round') - .each(function (d) { - var path = d3.select(this) - // special marker for vectors - if (data.vector) { - path.attr('marker-end', 'url(#' + options.owner.markerId + ')') - } - }) // enter + update innerSelection .each(function () { var path = d3.select(this) - var d - if (options.closed) { + var pathD + if (d.closed) { path.attr('fill', color) path.attr('fill-opacity', 0.3) - d = area + pathD = area } else { path.attr('fill', 'none') - d = line + pathD = line } path .attr('stroke', color) - .attr('d', d) + .attr('marker-end', function () { + // special marker for vectors + return d.fnType === 'vector' + ? 'url(#' + chart.markerId + ')' + : null + }) + .attr('d', pathD) }) innerSelection.exit().remove() diff --git a/lib/types/scatter.js b/lib/graph-types/scatter.js similarity index 78% rename from lib/types/scatter.js rename to lib/graph-types/scatter.js index 231446a..9f25c2b 100644 --- a/lib/types/scatter.js +++ b/lib/graph-types/scatter.js @@ -6,15 +6,16 @@ var d3 = window.d3 var dataBuilder = require('../data') var utils = require('../utils') -module.exports = function (options) { - var xScale = options.owner.meta.xScale - var yScale = options.owner.meta.yScale +module.exports = function (chart) { + var xScale = chart.meta.xScale + var yScale = chart.meta.yScale function scatter (selection) { - selection.each(function (data) { + selection.each(function (d) { var i, j - var color = utils.color(data, options.index) - var evaluatedData = dataBuilder.eval(options, data) + var index = d.index + var color = utils.color(d, index) + var evaluatedData = dataBuilder.eval(chart, d) // scatter doesn't need groups, therefore each group is // flattened into a single array diff --git a/lib/helpers/derivative.js b/lib/helpers/derivative.js index b40e251..c09a789 100644 --- a/lib/helpers/derivative.js +++ b/lib/helpers/derivative.js @@ -3,17 +3,16 @@ */ 'use strict' var d3 = window.d3 - var evaluate = require('./eval').builtIn -var line = require('../types/line') +var polyline = require('../graph-types/polyline') -module.exports = function (options) { - var dataBuilderConfig = { +module.exports = function (chart) { + var derivativeDatum = { skipTip: true, - samples: 2, - graphOptions: { - sampler: 'builtIn' - } + nSamples: 2, + sampler: 'builtIn', + graphType: 'polyline', + fnType: 'linear' } var derivative @@ -22,13 +21,14 @@ module.exports = function (options) { return [] } var x0 = typeof d.derivative.x0 === 'number' ? d.derivative.x0 : Infinity - dataBuilderConfig.scope = { + derivativeDatum.index = d.index + derivativeDatum.scope = { m: evaluate(d.derivative, 'fn', {x: x0}), x0: x0, y0: evaluate(d, 'fn', {x: x0}) } - dataBuilderConfig.fn = 'm * (x - x0) + y0' - return [dataBuilderConfig] + derivativeDatum.fn = 'm * (x - x0) + y0' + return [derivativeDatum] } function checkAutoUpdate (d) { @@ -39,13 +39,14 @@ module.exports = function (options) { if (d.derivative.updateOnMouseMove && !d.derivative.$$mouseListener) { d.derivative.$$mouseListener = function (x0) { // update initial value to be the position of the mouse + // scope's x0 will be updated on the next call to `derivative(self)` d.derivative.x0 = x0 // trigger update (selection = self) derivative(self) } // if d.derivative is destroyed and recreated, the tip:update event // will be fired on the new d.derivative :) - options.owner.on('tip:update', d.derivative.$$mouseListener) + chart.on('tip:update', d.derivative.$$mouseListener) } } @@ -63,8 +64,9 @@ module.exports = function (options) { // enter + update innerSelection - .call(line(options)) + .call(polyline(chart)) + // update // change the opacity of the line innerSelection.selectAll('path') .attr('opacity', 0.5) diff --git a/lib/helpers/index.js b/lib/helpers/index.js index 6434aaf..5f4985a 100644 --- a/lib/helpers/index.js +++ b/lib/helpers/index.js @@ -5,17 +5,13 @@ var d3 = window.d3 var derivative = require('./derivative') var secant = require('./secant') -var extend = require('extend') - -module.exports = function (options) { - // mark this type of datum as private - var extended = extend(true, { isHelper: true }, options) +module.exports = function (chart) { function helper (selection) { selection.each(function () { var el = d3.select(this) - el.call(derivative(extended)) - el.call(secant(extended)) + el.call(derivative(chart)) + el.call(secant(chart)) }) } diff --git a/lib/helpers/secant.js b/lib/helpers/secant.js index e8f49fe..172900a 100644 --- a/lib/helpers/secant.js +++ b/lib/helpers/secant.js @@ -6,17 +6,17 @@ var d3 = window.d3 var extend = require('extend') var evaluate = require('./eval').builtIn -var line = require('../types/line') +var polyline = require('../graph-types/polyline') var assert = require('../utils').assert -module.exports = function (options) { +module.exports = function (chart) { var secantDefaults = { isHelper: true, skipTip: true, - samples: 2, - graphOptions: { - sampler: 'builtIn' - } + nSamples: 2, + graphType: 'polyline', + sampler: 'builtIn', + fnType: 'linear' } var secant @@ -44,15 +44,15 @@ module.exports = function (options) { secant.fn = 'm * (x - x0) + y0' } - function setMouseListener (d, config) { + function setMouseListener (d, secantObject) { var self = this - if (config.updateOnMouseMove && !config.$$mouseListener) { - config.$$mouseListener = function (x1) { - config.x1 = x1 - updateLine(d, config) + if (secantObject.updateOnMouseMove && !secantObject.$$mouseListener) { + secantObject.$$mouseListener = function (x1) { + secantObject.x1 = x1 + updateLine(d, secantObject) secant(self) } - options.owner.on('tip:update', config.$$mouseListener) + chart.on('tip:update', secantObject.$$mouseListener) } } @@ -62,6 +62,8 @@ module.exports = function (options) { d.secants = d.secants || [] for (var i = 0; i < d.secants.length; i += 1) { var secant = d.secants[i] = extend({}, secantDefaults, d.secants[i]) + // necessary to make the secant have the same color as d + secant.index = d.index if (!secant.fn) { setFn.call(self, d, secant) setMouseListener.call(self, d, secant) @@ -84,7 +86,7 @@ module.exports = function (options) { // enter + update innerSelection - .call(line(options)) + .call(polyline(chart)) // change the opacity of the secants innerSelection.selectAll('path') diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..413ec75 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,604 @@ +/* + * function-plot + * + * Copyright (c) 2015 Mauricio Poppe + * Licensed under the MIT license. + */ +'use strict' +require('./polyfills') + +var d3 = window.d3 + +var events = require('events') +var extend = require('extend') + +var mousetip = require('./tip') +var utils = require('./utils') +var helpers = require('./helpers/') +var annotations = require('./helpers/annotations') + +var assert = utils.assert + +var globals +var graphTypes +var cache = [] + +module.exports = function (options) { + options = options || {} + options.data = options.data || [] + + // globals + var width, height + var margin + var zoomBehavior + var xScale, yScale + var line = d3.svg.line() + .x(function (d) { return xScale(d[0]) }) + .y(function (d) { return yScale(d[1]) }) + + function Chart () { + var n = Math.random() + var letter = String.fromCharCode(Math.floor(n * 26) + 97) + this.id = options.id = letter + n.toString(16).substr(2) + this.linkedGraphs = [this] + this.options = options + cache[this.id] = this + this.setUpEventListeners() + } + + Chart.prototype = Object.create(events.prototype) + + /** + * Rebuilds the entire graph from scratch recomputing + * + * - the inner width/height + * - scales/axes + * + * After this is done it does a complete redraw of all the datums, + * if only the datums need to be redrawn call `instance.draw()` instead + * + * @returns {Chart} + */ + Chart.prototype.build = function () { + this.internalVars() + this.drawGraphWrapper() + return this + } + + Chart.prototype.updateScaleAxes = function () { + var xDomain = this.meta.xDomain + var yDomain = this.meta.yDomain + + var integerFormat = d3.format('s') + var format = function (scale) { + return function (d) { + var decimalFormat = scale.tickFormat(10) + var isInteger = d === +d && d === (d | 0) + // integers: d3.format('s'), see https://github.com/mbostock/d3/wiki/Formatting + // decimals: default d3.scale.linear() formatting see + // https://github.com/mbostock/d3/blob/master/src/svg/axis.js#L29 + return isInteger ? integerFormat(d) : decimalFormat(d) + } + } + + xScale = this.meta.xScale = d3.scale.linear() + .domain(xDomain) + .range([0, width]) + yScale = this.meta.yScale = d3.scale.linear() + .domain(yDomain) + .range([height, 0]) + this.meta.xAxis = d3.svg.axis() + .scale(xScale) + .tickSize(options.grid ? -height : 0) + .tickFormat(format(xScale)) + .orient('bottom') + this.meta.yAxis = d3.svg.axis() + .scale(yScale) + .tickSize(options.grid ? -width : 0) + .tickFormat(format(yScale)) + .orient('left') + } + + Chart.prototype.internalVars = function () { + + // measurements and other derived data + this.meta = {} + + margin = this.meta.margin = {left: 30, right: 30, top: 20, bottom: 20} + // if there's a title make the top margin bigger + if (options.title) { + this.meta.margin.top = 40 + } + + zoomBehavior = this.meta.zoomBehavior = d3.behavior.zoom() + + // inner width/height + width = this.meta.width = (options.width || globals.DEFAULT_WIDTH) - + margin.left - margin.right + height = this.meta.height = (options.height || globals.DEFAULT_HEIGHT) - + margin.top - margin.bottom + + function computeYScale (xScale) { + var xDiff = xScale[1] - xScale[0] + return height * xDiff / width + } + + var xLimit = 12 + var xDomain = this.meta.xDomain = options.xDomain || [-xLimit / 2, xLimit / 2] + var yLimit = computeYScale(xDomain) + var yDomain = this.meta.yDomain = options.yDomain || [-yLimit / 2, yLimit / 2] + + assert(xDomain[0] < xDomain[1]) + assert(yDomain[0] < yDomain[1]) + + // scale/axes + this.updateScaleAxes() + } + + Chart.prototype.drawGraphWrapper = function () { + var root = this.root = d3.select(options.target).selectAll('svg') + .data([options]) + + // enter + this.root.enter = root.enter() + .append('svg') + .attr('class', 'function-plot') + .attr('font-size', this.getFontSize()) + + // merge + root + .attr('width', width + margin.left + margin.right) + .attr('height', height + margin.top + margin.bottom) + + this.buildTitle() + this.buildLegend() + this.buildCanvas() + this.buildClip() + this.buildAxis() + this.buildAxisLabel() + + // draw each datum after the wrapper was set up + this.draw() + + // helper to detect the closest fn to the cursor's current abscissa + var tip = this.tip = mousetip(extend(options.tip, { owner: this })) + this.canvas + .call(tip) + + this.buildZoomHelper() + this.setUpPlugins() + } + + Chart.prototype.buildTitle = function () { + // join + var selection = this.root.selectAll('text.title') + .data(function (d) { + return [d.title].filter(Boolean) + }) + + // enter + selection.enter() + .append('text') + .attr('class', 'title') + .attr('y', margin.top / 2) + .attr('x', margin.left + width / 2) + .attr('font-size', 25) + .attr('text-anchor', 'middle') + .attr('alignment-baseline', 'middle') + .text(options.title) + + // exit + selection.exit().remove() + } + + Chart.prototype.buildLegend = function () { + // enter + this.root.enter + .append('text') + .attr('class', 'top-right-legend') + .attr('text-anchor', 'end') + + // update + enter + this.root.select('.top-right-legend') + .attr('y', margin.top / 2) + .attr('x', width + margin.left) + } + + Chart.prototype.buildCanvas = function () { + var self = this + + this.meta.zoomBehavior + .x(xScale) + .y(yScale) + .on('zoom', function onZoom () { + self.emit('all:zoom', xScale, yScale) + }) + + // enter + var canvas = this.canvas = this.root + .selectAll('.canvas') + .data(function (d) { return [d] }) + + this.canvas.enter = canvas.enter() + .append('g') + .attr('class', 'canvas') + + // enter + update + } + + Chart.prototype.buildClip = function () { + // (so that the functions don't overflow on zoom or drag) + var id = this.id + var defs = this.canvas.enter.append('defs') + defs.append('clipPath') + .attr('id', 'function-plot-clip-' + id) + .append('rect') + .attr('class', 'clip static-clip') + + // enter + update + this.canvas.selectAll('.clip') + .attr('width', width) + .attr('height', height) + + // marker clip (for vectors) + this.markerId = this.id + '-marker' + defs.append('clipPath') + .append('marker') + .attr('id', this.markerId) + .attr('viewBox', '0 -5 10 10') + .attr('refX', 10) + .attr('markerWidth', 5) + .attr('markerHeight', 5) + .attr('orient', 'auto') + .append('svg:path') + .attr('d', 'M0,-5L10,0L0,5L0,0') + .attr('stroke-width', '0px') + .attr('fill-opacity', 1) + .attr('fill', '#777') + } + + Chart.prototype.buildAxis = function () { + // axis creation + var canvasEnter = this.canvas.enter + canvasEnter.append('g') + .attr('class', 'x axis') + canvasEnter.append('g') + .attr('class', 'y axis') + + // update + this.canvas.select('.x.axis') + .attr('transform', 'translate(0,' + height + ')') + .call(this.meta.xAxis) + this.canvas.select('.y.axis') + .call(this.meta.yAxis) + + } + + Chart.prototype.buildAxisLabel = function () { + // axis labeling + var xLabel, yLabel + var canvas = this.canvas + + xLabel = canvas.selectAll('text.x.axis-label') + .data(function (d) { + return [d.xLabel].filter(Boolean) + }) + xLabel.enter() + .append('text') + .attr('class', 'x axis-label') + .attr('text-anchor', 'end') + xLabel + .attr('x', width) + .attr('y', height - 6) + .text(function (d) { return d }) + xLabel.exit().remove() + + yLabel = canvas.selectAll('text.y.axis-label') + .data(function (d) { + return [d.yLabel].filter(Boolean) + }) + yLabel.enter() + .append('text') + .attr('class', 'y axis-label') + .attr('y', 6) + .attr('dy', '.75em') + .attr('text-anchor', 'end') + .attr('transform', 'rotate(-90)') + yLabel + .text(function (d) { return d }) + yLabel.exit().remove() + } + + /** + * @private + * + * Draws each of the datums stored in data.options, to do a full + * redraw call `instance.draw()` + */ + Chart.prototype.buildContent = function () { + var self = this + var canvas = this.canvas + + canvas + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') + .call(zoomBehavior) + .each(function () { + var el = d3.select(this) + // make a copy of all the listeners available to be removed/added later + var listeners = ['mousedown', 'mousewheel', 'mouseover', 'DOMMouseScroll', 'dblclick', 'wheel', 'MozMousePixelScroll'] + listeners = listeners.map(function (l) { return l + '.zoom' }) + if (!el._hasZoomListeners) { + listeners.forEach(function (l) { + el['_' + l] = el.on(l) + }) + } + function setState (state) { + listeners.forEach(function (l) { + state ? el.on(l, el['_' + l]) : el.on(l, null) + }) + } + setState(!options.disableZoom) + }) + + var content = this.content = canvas.selectAll(':scope > g.content') + .data(function (d) { return [d] }) + + // g tag clipped to hold the data + content.enter() + .append('g') + .attr('clip-path', 'url(#function-plot-clip-' + this.id + ')') + .attr('class', 'content') + + // helper line, x = 0 + var yOrigin = content.selectAll(':scope > path.y.origin') + .data([ [[0, yScale.domain()[0]], [0, yScale.domain()[1]]] ]) + yOrigin.enter() + .append('path') + .attr('class', 'y origin') + .attr('stroke', 'black') + .attr('opacity', 0.2) + yOrigin.attr('d', line) + + // helper line y = 0 + var xOrigin = content.selectAll(':scope > path.x.origin') + .data([ [[xScale.domain()[0], 0], [xScale.domain()[1], 0]] ]) + xOrigin.enter() + .append('path') + .attr('class', 'x origin') + .attr('stroke', 'black') + .attr('opacity', 0.2) + xOrigin.attr('d', line) + + // annotations + content + .call(annotations({ owner: self })) + + // content construction + // - join options.data to elements + // - for each datum determine the sampler to use + var graphs = content.selectAll(':scope > g.graph') + .data(function (d) { return d.data }) + + // enter + graphs + .enter() + .append('g') + .attr('class', 'graph') + + // enter + update + graphs + .each(function (d, index) { + // default graphType uses boxes i.e. 2d intervals + if (!(d.hasOwnProperty('graphType'))) { + d.graphType = 'interval' + } + + // if the graphType is not `interval` then default the sampler to `builtIn` + // because the interval sampler returns a box instead of a point + if (!(d.hasOwnProperty('sampler'))) { + d.sampler = d.graphType !== 'interval' + ? 'builtIn' + : 'interval' + } + + // TODO: handle default fnType + // default `fnType` is linear + if (!(d.hasOwnProperty('fnType'))) { + d.fnType = 'linear' + } + + // additional options needed in the graph-types/helpers + d.index = index + + d3.select(this) + .call(graphTypes[d.graphType](self)) + d3.select(this) + .call(helpers(self)) + }) + } + + Chart.prototype.buildZoomHelper = function () { + // dummy rect (detects the zoom + drag) + var self = this + + // enter + this.canvas.enter + .append('rect') + .attr('class', 'zoom-and-drag') + .style('fill', 'none') + .style('pointer-events', 'all') + + // update + this.canvas.select('.zoom-and-drag') + .attr('width', width) + .attr('height', height) + .on('mouseover', function () { + self.emit('all:mouseover') + }) + .on('mouseout', function () { + self.emit('all:mouseout') + }) + .on('mousemove', function () { + self.emit('all:mousemove') + }) + } + + Chart.prototype.setUpPlugins = function () { + var plugins = options.plugins || [] + var self = this + plugins.forEach(function (plugin) { + plugin(self) + }) + } + + Chart.prototype.addLink = function () { + for (var i = 0; i < arguments.length; i += 1) { + this.linkedGraphs.push(arguments[i]) + } + } + + Chart.prototype.updateAxes = function () { + var instance = this + var canvas = instance.canvas + canvas.select('.x.axis').call(instance.meta.xAxis) + canvas.select('.y.axis').call(instance.meta.yAxis) + + // updates the style of the axes + canvas.selectAll('.axis path, .axis line') + .attr('fill', 'none') + .attr('stroke', 'black') + .attr('shape-rendering', 'crispedges') + .attr('opacity', 0.1) + } + + Chart.prototype.syncOptions = function () { + // update the original options yDomain and xDomain + this.options.xDomain = this.meta.xScale.domain() + this.options.yDomain = this.meta.yScale.domain() + } + + Chart.prototype.programmaticZoom = function (xDomain, yDomain) { + var instance = this + d3.transition() + .duration(750) + .tween('zoom', function () { + var ix = d3.interpolate(xScale.domain(), xDomain) + var iy = d3.interpolate(yScale.domain(), yDomain) + return function (t) { + zoomBehavior + .x(xScale.domain(ix(t))) + .y(yScale.domain(iy(t))) + instance.draw() + } + }) + .each('end', function () { + instance.emit('programmatic-zoom') + }) + } + + Chart.prototype.getFontSize = function () { + return Math.max(Math.max(width, height) / 50, 8) + } + + Chart.prototype.draw = function () { + var instance = this + instance.emit('before:draw') + instance.syncOptions() + instance.updateAxes() + instance.buildContent() + instance.emit('after:draw') + } + + Chart.prototype.setUpEventListeners = function () { + var instance = this + + var events = { + mousemove: function (x, y) { + instance.tip.move(x, y) + }, + mouseover: function () { + instance.tip.show() + }, + mouseout: function () { + instance.tip.hide() + }, + 'zoom:scaleUpdate': function (xOther, yOther) { + zoomBehavior + .x(xScale.domain(xOther.domain())) + .y(yScale.domain(yOther.domain())) + }, + 'tip:update': function (x, y, index) { + var meta = instance.root.datum().data[index] + var title = meta.title || '' + var format = meta.renderer || function (x, y) { + return x.toFixed(3) + ', ' + y.toFixed(3) + } + + var text = [] + title && text.push(title) + text.push(format(x, y)) + + instance.root.select('.top-right-legend') + .attr('fill', globals.COLORS[index]) + // .text(x.toFixed(3) + ', ' + y.toFixed(3)) + .text(text.join(' ')) + } + } + + var all = { + mousemove: function () { + var mouse = d3.mouse(instance.root.select('rect.zoom-and-drag').node()) + var x = xScale.invert(mouse[0]) + var y = yScale.invert(mouse[1]) + instance.linkedGraphs.forEach(function (graph) { + graph.emit('mousemove', x, y) + }) + }, + + zoom: function (xScale, yScale) { + instance.linkedGraphs.forEach(function (graph, i) { + // since its scale was updated through d3.behavior.zoom + // we don't need to do it again + if (i) { + graph.emit('zoom:scaleUpdate', xScale, yScale) + } + // the first element is the instance who fired the event, + // content draw + graph.draw() + }) + + // emit the position of the mouse to all the registered graphs + instance.emit('all:mousemove') + } + } + + Object.keys(events).forEach(function (e) { + instance.on(e, events[e]) + // create an event for each event existing on `events` in the form 'all:' event + // e.g. all:mouseover all:mouseout + // the objective is that all the linked graphs receive the same event as the current graph + !all[e] && instance.on('all:' + e, function () { + var args = Array.prototype.slice.call(arguments) + instance.linkedGraphs.forEach(function (graph) { + var localArgs = args.slice() + localArgs.unshift(e) + graph.emit.apply(graph, localArgs) + }) + }) + }) + + Object.keys(all).forEach(function (e) { + instance.on('all:' + e, all[e]) + }) + } + + var instance = cache[options.id] + if (!instance) { + instance = new Chart() + } + return instance.build() +} +globals = module.exports.globals = require('./globals') +graphTypes = module.exports.graphTypes = require('./graph-types/') +module.exports.plugins = require('./plugins/') +module.exports.eval = require('./helpers/eval') diff --git a/lib/samplers/builtIn.js b/lib/samplers/builtIn.js index 58f5242..aa5368a 100644 --- a/lib/samplers/builtIn.js +++ b/lib/samplers/builtIn.js @@ -99,7 +99,7 @@ function split (chart, meta, data) { return sets } -var linear = function (chart, meta, range, n) { +function linear (chart, meta, range, n) { var allX = utils.linspace(range, n) var yDomain = chart.meta.yScale.domain() var yDomainMargin = (yDomain[1] - yDomain[0]) @@ -161,18 +161,18 @@ function vector (chart, meta, range, nSamples) { ]] } -var sampler = function (chart, meta, range, nSamples) { - if (meta.parametric) { - return parametric.apply(null, arguments) - } else if (meta.polar) { - return polar.apply(null, arguments) - } else if (meta.points) { - return points.apply(null, arguments) - } else if (meta.vector) { - return vector.apply(null, arguments) - } else { - return linear.apply(null, arguments) +var sampler = function (chart, d, range, nSamples) { + var fnTypes = { + parametric: parametric, + polar: polar, + points: points, + vector: vector, + linear: linear } + if (!(d.fnType in fnTypes)) { + throw Error(d.fnType + ' is not supported in the `builtIn` sampler') + } + return fnTypes[d.fnType].apply(null, arguments) } module.exports = sampler diff --git a/lib/samplers/interval.js b/lib/samplers/interval.js index d35fa1c..4595ffb 100644 --- a/lib/samplers/interval.js +++ b/lib/samplers/interval.js @@ -111,12 +111,15 @@ function interval2d (chart, meta) { return [samples] } -var sampler = function (chart, meta, range, nSamples) { - if (meta.implicit) { - return interval2d.apply(null, arguments) - } else { - return interval1d.apply(null, arguments) +var sampler = function (chart, d, range, nSamples) { + var fnTypes = { + implicit: interval2d, + linear: interval1d } + if (!(fnTypes.hasOwnProperty(d.fnType))) { + throw Error(d.fnType + ' is not supported in the `interval` sampler') + } + return fnTypes[d.fnType].apply(null, arguments) } module.exports = sampler diff --git a/lib/tip.js b/lib/tip.js index 3bbc014..32e282f 100644 --- a/lib/tip.js +++ b/lib/tip.js @@ -92,8 +92,7 @@ module.exports = function (config) { // implicit equations cannot be evaluated with a single point // parametric equations cannot be evaluated with a single point // polar equations cannot be evaluated with a single point - if (data[i].skipTip || data[i].parametric || data[i].points || - data[i].implicit || data[i].polar || data[i].vector) { + if (data[i].skipTip || data[i].fnType !== 'linear') { continue } diff --git a/package.json b/package.json index 70fcae9..e48570e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A simple 2d function plotter powered by d3", "bugs": "https://github.com/maurizzzio/function-plot/issues", "license": "MIT", - "main": "index.js", + "main": "lib/index.js", "homepage": "http://maurizzzio.github.io/function-plot/", "author": "Mauricio Poppe ", "files": [ diff --git a/site/js/function-plot.js b/site/js/function-plot.js index 13b1850..2bf24f0 100644 --- a/site/js/function-plot.js +++ b/site/js/function-plot.js @@ -1,7 +1,6180 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.functionPlot=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o g.content").data(function(d){return[d]});content.enter().append("g").attr("clip-path","url(#function-plot-clip-"+this.id+")").attr("class","content");var yOrigin=content.selectAll(":scope > path.y.origin").data([[[0,yScale.domain()[0]],[0,yScale.domain()[1]]]]);yOrigin.enter().append("path").attr("class","y origin").attr("stroke","black").attr("opacity",.2);yOrigin.attr("d",line);var xOrigin=content.selectAll(":scope > path.x.origin").data([[[xScale.domain()[0],0],[xScale.domain()[1],0]]]);xOrigin.enter().append("path").attr("class","x origin").attr("stroke","black").attr("opacity",.2);xOrigin.attr("d",line);content.call(annotations({owner:self}));var graphs=content.selectAll(":scope > g.graph").data(function(d){return d.data});graphs.enter().append("g").attr("class","graph");function extendIf(){var target=arguments[0];for(var i=1;i",toggle:false},options);var brush=d3.svg.brush();var kd=keydown(options.key);var visible=false;var cachedInstance;function wrapper(datum){return function(x){var functionPlot=window.functionPlot;return functionPlot.eval.builtIn(datum,"fn",{x:x})}}function setBrushState(visible){var brushEl=cachedInstance.canvas.selectAll(".definite-integral");brushEl.style("display",visible?null:"none")}function inner(instance){cachedInstance=instance;var oldDisableZoom;brush.x(instance.meta.xScale).on("brushstart",function(){if(!d3.event.sourceEvent)return;oldDisableZoom=!!instance.options.disableZoom;instance.options.disableZoom=true;instance.emit("draw")}).on("brushend",function(){if(!d3.event.sourceEvent)return;instance.options.disableZoom=oldDisableZoom;if(!brush.empty()){var a=brush.extent()[0];var b=brush.extent()[1];instance.options.data.forEach(function(datum,i){var value=integrateSimpson(wrapper(datum),a,b,options.tol,options.maxdepth);instance.emit("definite-integral",datum,i,value,a,b)})}instance.draw()});var brushEl=instance.canvas.append("g").attr("class","brush definite-integral");brushEl.call(brush).call(brush.event);instance.canvas.selectAll(".brush .extent").attr("stroke","#fff").attr("fill-opacity",.125).attr("shape-rendering","crispEdges");brushEl.selectAll("rect").attr("height",instance.meta.height);instance.canvas.on("mousemove.definiteIntegral",function(){if(!options.toggle){inner.visible(pressed(options.key))}});kd.on("pressed",function(){inner.visible(options.toggle?!inner.visible():true)});inner.visible(false)}inner.visible=function(_){if(!arguments.length){return visible}visible=_;setBrushState(_);return inner};return inner}},{extend:54,"integrate-adaptive-simpson":55,"key-pressed":102,keydown:104}],10:[function(require,module,exports){module.exports={zoomBox:require("./zoom-box"),definiteIntegral:require("./definite-integral")}},{"./definite-integral":9,"./zoom-box":11}],11:[function(require,module,exports){var d3=window.d3;var extend=require("extend");var pressed=require("key-pressed");var keydown=require("keydown");module.exports=function(options){options=extend({key:"",toggle:false},options);var brush=d3.svg.brush();var kd=keydown(options.key);var cachedInstance;var visible=false;function setBrushState(visible){var brushEl=cachedInstance.canvas.selectAll(".zoom-box");brushEl.style("display",visible?null:"none")}function inner(instance){cachedInstance=instance;var oldDisableZoom;brush.x(instance.meta.xScale).y(instance.meta.yScale).on("brushstart",function(){if(!d3.event.sourceEvent)return;oldDisableZoom=!!instance.options.disableZoom;instance.options.disableZoom=true;instance.draw()}).on("brushend",function(){if(!d3.event.sourceEvent)return;instance.options.disableZoom=oldDisableZoom;if(!brush.empty()){var lo=brush.extent()[0];var hi=brush.extent()[1];var x=[lo[0],hi[0]];var y=[lo[1],hi[1]];instance.programmaticZoom(x,y)}d3.select(this).transition().duration(1).call(brush.clear()).call(brush.event)});var brushEl=instance.canvas.append("g").attr("class","brush zoom-box");brushEl.call(brush).call(brush.event);instance.canvas.selectAll(".brush .extent").attr("stroke","#fff").attr("fill-opacity",.125).attr("shape-rendering","crispEdges");instance.canvas.on("mousemove.zoombox",function(){if(!options.toggle){inner.visible(pressed(options.key))}});kd.on("pressed",function(){inner.visible(options.toggle?!inner.visible():true)});inner.visible(false)}inner.visible=function(_){if(!arguments.length){return visible}visible=_;setBrushState(_);return inner};return inner}},{extend:54,"key-pressed":102,keydown:104}],12:[function(require,module,exports){(function(doc,proto){try{doc.querySelector(":scope body")}catch(err){["querySelector","querySelectorAll"].forEach(function(method){var native=proto[method];proto[method]=function(selectors){if(/(^|,)\s*:scope/.test(selectors)){var id=this.id;this.id="ID_"+Date.now();selectors=selectors.replace(/((^|,)\s*):scope/g,"$1#"+this.id);var result=doc[method](selectors);this.id=id;return result}else{return native.call(this,selectors)}}})}})(window.document,Element.prototype)},{}],13:[function(require,module,exports){"use strict";var utils=require("../utils");var clamp=require("clamp");var evaluate=require("../helpers/eval").builtIn;function checkAsymptote(d0,d1,meta,sign,level){if(!level){return{asymptote:true,d0:d0,d1:d1}}var i;var n=10;var x0=d0[0];var x1=d1[0];var samples=utils.linspace([x0,x1],n);var oldY,oldX;for(i=0;i1/zoomScale){var check=checkAsymptote(data[i-1],data[i],meta,newSign,3);if(check.asymptote){st.push(updateY(check.d0));sets.push(st);st=[updateY(check.d1)]}}oldSign=newSign;st.push(data[i]);++i}if(st.length){sets.push(st)}return sets}var linear=function(chart,meta,range,n){var allX=utils.linspace(range,n);var yDomain=chart.meta.yScale.domain();var yDomainMargin=yDomain[1]-yDomain[0];var yMin=yDomain[0]-yDomainMargin*1e5;var yMax=yDomain[1]+yDomainMargin*1e5;var data=[];var i;for(i=0;inext[1].hi){prev[1].hi=Math.max(yMax,prev[1].hi);next[1].lo=Math.min(yMin,next[1].lo)}if(prev[1].hirange[0]-globals.TIP_X_EPS&&x0hi){return[-minWidthHeight,0]}return[lo,hi]}var line=function(points){var path="";var minY=yScale.range()[1];var maxY=yScale.range()[0];for(var i=0,length=points.length;i path.line").data(evaluatedData);minWidthHeight=Math.max(evaluatedData[0].scaledDx,1);innerSelection.enter().append("path").attr("class","line line-"+index).attr("fill","none");innerSelection.each(function(){var path=d3.select(this);path.attr("stroke-width",minWidthHeight).attr("stroke",utils.color(data,options.index)).attr("opacity",options.closed?.5:1).attr("d",line)});innerSelection.exit().remove()})}return plotLine}},{"../data":2,"../utils":20}],18:[function(require,module,exports){"use strict";var d3=window.d3;var dataBuilder=require("../data");var utils=require("../utils");module.exports=function(options){var xScale=options.owner.meta.xScale;var yScale=options.owner.meta.yScale;var line=d3.svg.line().interpolate("linear").x(function(d){return xScale(d[0])}).y(function(d){return yScale(d[1])});var area=d3.svg.area().x(function(d){return xScale(d[0])}).y0(yScale(0)).y1(function(d){return yScale(d[1])});function plotLine(selection){var index=options.index;selection.each(function(data){var el=plotLine.el=d3.select(this);var evaluatedData=dataBuilder.eval(options,data);var color=utils.color(data,options.index);var innerSelection=el.selectAll(":scope > path.line").data(evaluatedData);innerSelection.enter().append("path").attr("class","line line-"+index).attr("stroke-width",1).attr("stroke-linecap","round").each(function(d){var path=d3.select(this);if(data.vector){path.attr("marker-end","url(#"+options.owner.markerId+")")}});innerSelection.each(function(){var path=d3.select(this);var d;if(options.closed){path.attr("fill",color);path.attr("fill-opacity",.3);d=area}else{path.attr("fill","none");d=line}path.attr("stroke",color).attr("d",d)});innerSelection.exit().remove()})}return plotLine}},{"../data":2,"../utils":20}],19:[function(require,module,exports){"use strict";var d3=window.d3;var dataBuilder=require("../data");var utils=require("../utils");module.exports=function(options){var xScale=options.owner.meta.xScale;var yScale=options.owner.meta.yScale;function scatter(selection){selection.each(function(data){var i,j;var color=utils.color(data,options.index);var evaluatedData=dataBuilder.eval(options,data);var joined=[];for(i=0;i circle").data(joined);innerSelection.enter().append("circle");innerSelection.attr("fill",d3.hsl(color.toString()).brighter(1.5)).attr("stroke",color).attr("opacity",.7).attr("r",1).attr("cx",function(d){return xScale(d[0])}).attr("cy",function(d){return yScale(d[1])});innerSelection.exit().remove()})}return scatter}},{"../data":2,"../utils":20}],20:[function(require,module,exports){"use strict";var globals=require("./globals");module.exports={isValidNumber:function(v){return typeof v==="number"&&!isNaN(v)},linspace:function(range,n){var samples=[];var delta=(range[1]-range[0])/(n-1);for(var i=0;i0){return 1}return 0},bySign:function(v,min,max){if(v<0){if(vmax){return v}return max}},assert:function(v,message){message=message||"assertion failed";if(!v){throw new Error(message)}},color:function(data,index){return data.color||globals.COLORS[index]}}},{"./globals":3}],21:[function(require,module,exports){"use strict";module.exports=require("./lib/eval")},{"./lib/eval":23}],22:[function(require,module,exports){"use strict";module.exports=function(){var math=Object.create(Math);math.factory=function(a){if(typeof a!=="number"){throw new TypeError("built-in math factory only accepts numbers")}return Number(a)};math.add=function(a,b){return a+b};math.sub=function(a,b){return a-b};math.mul=function(a,b){return a*b};math.div=function(a,b){return a/b};math.mod=function(a,b){return a%b};math.factorial=function(a){var res=1;for(var i=2;i<=a;i+=1){res*=i}return res};math.logicalOR=function(a,b){return a||b};math.logicalXOR=function(a,b){return a!=b};math.logicalAND=function(a,b){return a&&b};math.bitwiseOR=function(a,b){return a|b};math.bitwiseXOR=function(a,b){return a^b};math.bitwiseAND=function(a,b){return a&b};math.lessThan=function(a,b){return ab};math.greaterEqualThan=function(a,b){return a>=b};math.equal=function(a,b){return a==b};math.strictlyEqual=function(a,b){return a===b};math.notEqual=function(a,b){return a!=b};math.strictlyNotEqual=function(a,b){return a!==b};math.shiftRight=function(a,b){return a>>b};math.shiftLeft=function(a,b){return a<>>b};math.negative=function(a){return-a};math.positive=function(a){return a};return math}},{}],23:[function(require,module,exports){"use strict";var CodeGenerator=require("math-codegen");var math=require("./adapter")();function processScope(scope){Object.keys(scope).forEach(function(k){var value=scope[k];scope[k]=math.factory(value)})}module.exports=function(expression){return(new CodeGenerator).setDefs({$$processScope:processScope}).parse(expression).compile(math)};module.exports.math=math},{"./adapter":22,"math-codegen":24}],24:[function(require,module,exports){"use strict";module.exports=require("./lib/CodeGenerator")},{"./lib/CodeGenerator":25}],25:[function(require,module,exports){"use strict";var Parser=require("mr-parser").Parser;var Interpreter=require("./Interpreter");var extend=require("extend");function CodeGenerator(options,defs){this.statements=[];this.defs=defs||{};this.interpreter=new Interpreter(this,options)}CodeGenerator.prototype.setDefs=function(defs){this.defs=extend(this.defs,defs);return this};CodeGenerator.prototype.compile=function(namespace){if(!namespace||!(typeof namespace==="object"||typeof namespace==="function")){throw TypeError("namespace must be an object")}if(typeof namespace.factory!=="function"){throw TypeError("namespace.factory must be a function")}this.defs.ns=namespace;this.defs.$$mathCodegen={getProperty:function(symbol,scope,ns){if(symbol in scope){return scope[symbol]}if(symbol in ns){return ns[symbol]}throw SyntaxError('symbol "'+symbol+'" is undefined')},functionProxy:function(fn,name){if(typeof fn!=="function"){throw SyntaxError('symbol "'+name+'" must be a function')}return fn}};this.defs.$$processScope=this.defs.$$processScope||function(){};var defsCode=Object.keys(this.defs).map(function(name){return"var "+name+' = defs["'+name+'"]'});if(!this.statements.length){throw Error("there are no statements saved in this generator, make sure you parse an expression before compiling it")}this.statements[this.statements.length-1]="return "+this.statements[this.statements.length-1];var code=this.statements.join(";");var factoryCode=defsCode.join("\n")+"\n"+["return {"," eval: function (scope) {"," scope = scope || {}"," $$processScope(scope)"," "+code," },"," code: '"+code+"'","}"].join("\n");var factory=new Function("defs",factoryCode);return factory(this.defs)};CodeGenerator.prototype.parse=function(code){var self=this;var program=(new Parser).parse(code);this.statements=program.blocks.map(function(statement){return self.interpreter.next(statement)});return this};module.exports=CodeGenerator},{"./Interpreter":26,extend:37,"mr-parser":38}],26:[function(require,module,exports){"use strict";var extend=require("extend");var types={ArrayNode:require("./node/ArrayNode"),AssignmentNode:require("./node/AssignmentNode"),ConditionalNode:require("./node/ConditionalNode"),ConstantNode:require("./node/ConstantNode"),FunctionNode:require("./node/FunctionNode"),OperatorNode:require("./node/OperatorNode"),SymbolNode:require("./node/SymbolNode"),UnaryNode:require("./node/UnaryNode")};var Interpreter=function(owner,options){this.owner=owner;this.options=extend({factory:"ns.factory",raw:false,rawArrayExpressionElements:true,rawCallExpressionElements:false},options)};extend(Interpreter.prototype,types);Interpreter.prototype.next=function(node){if(!(node.type in this)){throw new TypeError("the node type "+node.type+" is not implemented")}return this[node.type](node)};Interpreter.prototype.rawify=function(test,fn){var oldRaw=this.options.raw;if(test){this.options.raw=true}fn();if(test){this.options.raw=oldRaw}};module.exports=Interpreter},{"./node/ArrayNode":29,"./node/AssignmentNode":30,"./node/ConditionalNode":31,"./node/ConstantNode":32,"./node/FunctionNode":33,"./node/OperatorNode":34,"./node/SymbolNode":35,"./node/UnaryNode":36,extend:37}],27:[function(require,module,exports){"use strict";module.exports={"+":"add","-":"sub","*":"mul","/":"div","^":"pow","%":"mod","!":"factorial","|":"bitwiseOR","^|":"bitwiseXOR","&":"bitwiseAND","||":"logicalOR",xor:"logicalXOR","&&":"logicalAND","<":"lessThan",">":"greaterThan","<=":"lessEqualThan",">=":"greaterEqualThan","===":"strictlyEqual","==":"equal","!==":"strictlyNotEqual","!=":"notEqual",">>":"shiftRight","<<":"shiftLeft",">>>":"unsignedRightShift"}},{}],28:[function(require,module,exports){"use strict";module.exports={"+":"positive","-":"negative","~":"oneComplement"}},{}],29:[function(require,module,exports){"use strict";module.exports=function(node){var self=this;var arr=[];this.rawify(this.options.rawArrayExpressionElements,function(){arr=node.nodes.map(function(el){return self.next(el)})});var arrString="["+arr.join(",")+"]";if(this.options.raw){return arrString}return this.options.factory+"("+arrString+")"}},{}],30:[function(require,module,exports){"use strict";module.exports=function(node){return'scope["'+node.name+'"] = '+this.next(node.expr)}},{}],31:[function(require,module,exports){"use strict";module.exports=function(node){var condition="!!("+this.next(node.condition)+")";var trueExpr=this.next(node.trueExpr);var falseExpr=this.next(node.falseExpr);return"("+condition+" ? ("+trueExpr+") : ("+falseExpr+") )"}},{}],32:[function(require,module,exports){"use strict";module.exports=function(node){if(this.options.raw){return node.value}return this.options.factory+"("+node.value+")"}},{}],33:[function(require,module,exports){"use strict";var SymbolNode=require("mr-parser").nodeTypes.SymbolNode;var functionProxy=function(node){return"$$mathCodegen.functionProxy("+this.next(new SymbolNode(node.name))+', "'+node.name+'")'};module.exports=function(node){var self=this;var method=functionProxy.call(this,node);var args=[];this.rawify(this.options.rawCallExpressionElements,function(){args=node.args.map(function(arg){return self.next(arg)})});return method+"("+args.join(", ")+")"};module.exports.functionProxy=functionProxy},{"mr-parser":38}],34:[function(require,module,exports){"use strict";var Operators=require("../misc/Operators");module.exports=function(node){if(this.options.raw){return["("+this.next(node.args[0]),node.op,this.next(node.args[1])+")"].join(" ")}var namedOperator=Operators[node.op];if(!namedOperator){throw TypeError("unidentified operator")}return this.FunctionNode({name:namedOperator,args:node.args})}},{"../misc/Operators":27}],35:[function(require,module,exports){"use strict";module.exports=function(node){var id=node.name;return'$$mathCodegen.getProperty("'+id+'", scope, ns)'}},{}],36:[function(require,module,exports){"use strict";var UnaryOperators=require("../misc/UnaryOperators");module.exports=function(node){if(this.options.raw){return node.op+this.next(node.argument)}if(!(node.op in UnaryOperators)){throw new SyntaxError(node.op+" not implemented")}var namedOperator=UnaryOperators[node.op];return this.FunctionNode({name:namedOperator,args:[node.argument]})}},{"../misc/UnaryOperators":28}],37:[function(require,module,exports){"use strict";var hasOwn=Object.prototype.hasOwnProperty;var toStr=Object.prototype.toString;var isArray=function isArray(arr){if(typeof Array.isArray==="function"){return Array.isArray(arr)}return toStr.call(arr)==="[object Array]"};var isPlainObject=function isPlainObject(obj){if(!obj||toStr.call(obj)!=="[object Object]"){return false}var hasOwnConstructor=hasOwn.call(obj,"constructor");var hasIsPrototypeOf=obj.constructor&&obj.constructor.prototype&&hasOwn.call(obj.constructor.prototype,"isPrototypeOf");if(obj.constructor&&!hasOwnConstructor&&!hasIsPrototypeOf){return false}var key;for(key in obj){}return typeof key==="undefined"||hasOwn.call(obj,key)};module.exports=function extend(){var options,name,src,copy,copyIsArray,clone,target=arguments[0],i=1,length=arguments.length,deep=false;if(typeof target==="boolean"){deep=target;target=arguments[1]||{};i=2}else if(typeof target!=="object"&&typeof target!=="function"||target==null){target={}}for(;i":true,">=":true,"<=":true,">>>":true,"<<":true,">>":true};function isDigit(c){return c>="0"&&c<="9"}function isIdentifier(c){return c>="a"&&c<="z"||c>="A"&&c<="Z"||c==="$"||c==="_"}function isWhitespace(c){return c===" "||c==="\r"||c===" "||c==="\n"||c===" "||c===" "}function isDelimiter(str){return DELIMITERS[str]}function isQuote(c){return c==="'"||c==='"'}function Lexer(){}Lexer.prototype.throwError=function(message,index){index=typeof index==="undefined"?this.index:index;var error=new Error(message+" at index "+index);error.index=index;error.description=message;throw error};Lexer.prototype.lex=function(text){this.text=text;this.index=0;this.tokens=[];while(this.index=this.text.length){return}return this.text.charAt(this.index+nth)};Lexer.prototype.consume=function(){var current=this.peek();this.index+=1;return current};Lexer.prototype.readNumber=function(){var number="";if(this.peek()==="."){number+=this.consume();if(!isDigit(this.peek())){this.throwError("number expected")}}else{while(isDigit(this.peek())){number+=this.consume()}if(this.peek()==="."){number+=this.consume()}}while(isDigit(this.peek())){number+=this.consume()}if(this.peek()==="e"||this.peek()==="E"){number+=this.consume();if(!(isDigit(this.peek())||this.peek()==="+"||this.peek()==="-")){this.throwError()}if(this.peek()==="+"||this.peek()==="-"){number+=this.consume()}if(!isDigit(this.peek())){this.throwError("number expected")}while(isDigit(this.peek())){number+=this.consume()}}return number};Lexer.prototype.readIdentifier=function(){var text="";while(isIdentifier(this.peek())||isDigit(this.peek())){text+=this.consume()}return text};Lexer.prototype.readString=function(){var quote=this.consume();var string="";var escape;while(true){var c=this.consume();if(!c){this.throwError("string is not closed")}if(escape){if(c==="u"){var hex=this.text.substring(this.index+1,this.index+5);if(!hex.match(/[\da-f]{4}/i)){this.throwError("invalid unicode escape")}this.index+=4;string+=String.fromCharCode(parseInt(hex,16))}else{var replacement=ESCAPES[c];if(replacement){string+=replacement}else{string+=c}}escape=false}else if(c===quote){break}else if(c==="\\"){escape=true}else{string+=c}}return string};module.exports=Lexer},{"./token-type":52}],40:[function(require,module,exports){var tokenType=require("./token-type");var Lexer=require("./Lexer");var ConstantNode=require("./node/ConstantNode");var OperatorNode=require("./node/OperatorNode");var UnaryNode=require("./node/UnaryNode");var SymbolNode=require("./node/SymbolNode");var FunctionNode=require("./node/FunctionNode");var ArrayNode=require("./node/ArrayNode");var ConditionalNode=require("./node/ConditionalNode");var AssignmentNode=require("./node/AssignmentNode");var BlockNode=require("./node/BlockNode");function Parser(){this.lexer=new Lexer;this.tokens=null}Parser.prototype.current=function(){return this.tokens[0]};Parser.prototype.next=function(){return this.tokens[1]};Parser.prototype.peek=function(){if(this.tokens.length){var first=this.tokens[0];for(var i=0;i=","<=",">","<")){var op=this.consume();var right=this.shift();return new OperatorNode(op.value,[left,right])}return left};Parser.prototype.shift=function(){var left=this.additive();if(this.peek(">>","<<",">>>")){var op=this.consume();var right=this.shift();return new OperatorNode(op.value,[left,right])}return left};Parser.prototype.additive=function(){var left=this.multiplicative();while(this.peek("+","-")){var op=this.consume();left=new OperatorNode(op.value,[left,this.multiplicative()])}return left};Parser.prototype.multiplicative=function(){var op,right;var left=this.unary();while(this.peek("*","/","%")){op=this.consume();left=new OperatorNode(op.value,[left,this.unary()])}if(this.current().type===tokenType.SYMBOL||this.peek("(")||!(left.type instanceof ConstantNode)&&this.current().type===tokenType.NUMBER){right=this.multiplicative();return new OperatorNode("*",[left,right])}return left};Parser.prototype.unary=function(){if(this.peek("-","+","~")){var op=this.consume();var right=this.unary();return new UnaryNode(op.value,right)}return this.pow()};Parser.prototype.pow=function(){var left=this.factorial();if(this.peek("^","**")){var op=this.consume();var right=this.unary();return new OperatorNode(op.value,[left,right])}return left};Parser.prototype.factorial=function(){var left=this.symbol();if(this.peek("!")){var op=this.consume();return new OperatorNode(op.value,[left])}return left};Parser.prototype.symbol=function(){var current=this.current();if(current.type===tokenType.SYMBOL){var symbol=this.consume();var node=this.functionCall(symbol);return node}return this.string()};Parser.prototype.functionCall=function(symbolToken){var name=symbolToken.value;if(this.peek("(")){this.consume();var params=[];while(!this.peek(")")&&!this.isEOF()){params.push(this.assignment());if(this.peek(",")){this.consume()}}this.expect(")");return new FunctionNode(name,params)}return new SymbolNode(name)};Parser.prototype.string=function(){if(this.current().type===tokenType.STRING){return new ConstantNode(this.consume().value,"string")}return this.array()};Parser.prototype.array=function(){if(this.peek("[")){this.consume();var params=[];while(!this.peek("]")&&!this.isEOF()){params.push(this.assignment());if(this.peek(",")){this.consume()}}this.expect("]");return new ArrayNode(params)}return this.number()};Parser.prototype.number=function(){var token=this.current();if(token.type===tokenType.NUMBER){return new ConstantNode(this.consume().value,"number")}return this.parentheses()};Parser.prototype.parentheses=function(){var token=this.current();if(token.value==="("){this.consume();var left=this.assignment();this.expect(")");return left}return this.end()};Parser.prototype.end=function(){var token=this.current();if(token.type!==tokenType.EOF){throw Error("unexpected end of expression")}};module.exports=Parser},{"./Lexer":39,"./node/ArrayNode":41,"./node/AssignmentNode":42,"./node/BlockNode":43,"./node/ConditionalNode":44,"./node/ConstantNode":45,"./node/FunctionNode":46,"./node/OperatorNode":48,"./node/SymbolNode":49,"./node/UnaryNode":50,"./token-type":52}],41:[function(require,module,exports){var Node=require("./Node");function ArrayNode(nodes){this.nodes=nodes}ArrayNode.prototype=Object.create(Node.prototype);ArrayNode.prototype.type="ArrayNode";module.exports=ArrayNode},{"./Node":47}],42:[function(require,module,exports){var Node=require("./Node");function AssignmentNode(name,expr){this.name=name;this.expr=expr}AssignmentNode.prototype=Object.create(Node.prototype);AssignmentNode.prototype.type="AssignmentNode";module.exports=AssignmentNode},{"./Node":47}],43:[function(require,module,exports){var Node=require("./Node");function BlockNode(blocks){this.blocks=blocks}BlockNode.prototype=Object.create(Node.prototype);BlockNode.prototype.type="BlockNode";module.exports=BlockNode},{"./Node":47}],44:[function(require,module,exports){var Node=require("./Node");function ConditionalNode(predicate,truthy,falsy){this.condition=predicate;this.trueExpr=truthy;this.falseExpr=falsy}ConditionalNode.prototype=Object.create(Node.prototype);ConditionalNode.prototype.type="ConditionalNode";module.exports=ConditionalNode},{"./Node":47}],45:[function(require,module,exports){var Node=require("./Node");var SUPPORTED_TYPES={number:true,string:true,"boolean":true,undefined:true,"null":true};function ConstantNode(value,type){if(!SUPPORTED_TYPES[type]){throw Error("unsupported type '"+type+"'")}this.value=value;this.valueType=type}ConstantNode.prototype=Object.create(Node.prototype);ConstantNode.prototype.type="ConstantNode";module.exports=ConstantNode},{"./Node":47}],46:[function(require,module,exports){var Node=require("./Node");function FunctionNode(name,args){this.name=name;this.args=args}FunctionNode.prototype=Object.create(Node.prototype);FunctionNode.prototype.type="FunctionNode";module.exports=FunctionNode},{"./Node":47}],47:[function(require,module,exports){function Node(){}Node.prototype.type="Node";module.exports=Node},{}],48:[function(require,module,exports){var Node=require("./Node");function OperatorNode(op,args){this.op=op;this.args=args||[]}OperatorNode.prototype=Object.create(Node.prototype);OperatorNode.prototype.type="OperatorNode";module.exports=OperatorNode},{"./Node":47}],49:[function(require,module,exports){var Node=require("./Node");function SymbolNode(name){this.name=name}SymbolNode.prototype=Object.create(Node.prototype);SymbolNode.prototype.type="SymbolNode";module.exports=SymbolNode},{"./Node":47}],50:[function(require,module,exports){var Node=require("./Node");function UnaryNode(op,argument){this.op=op;this.argument=argument}UnaryNode.prototype=Object.create(Node.prototype);UnaryNode.prototype.type="UnaryNode";module.exports=UnaryNode},{"./Node":47}],51:[function(require,module,exports){module.exports={ArrayNode:require("./ArrayNode"),AssignmentNode:require("./AssignmentNode"),BlockNode:require("./BlockNode"),ConditionalNode:require("./ConditionalNode"),ConstantNode:require("./ConstantNode"),FunctionNode:require("./FunctionNode"),Node:require("./Node"),OperatorNode:require("./OperatorNode"),SymbolNode:require("./SymbolNode"),UnaryNode:require("./UnaryNode")}},{"./ArrayNode":41,"./AssignmentNode":42,"./BlockNode":43,"./ConditionalNode":44,"./ConstantNode":45,"./FunctionNode":46,"./Node":47,"./OperatorNode":48,"./SymbolNode":49,"./UnaryNode":50}],52:[function(require,module,exports){module.exports={EOF:0,DELIMITER:1,NUMBER:2,STRING:3,SYMBOL:4}},{}],53:[function(require,module,exports){module.exports=clamp;function clamp(value,min,max){return minmax?max:value:valuemin?min:value}},{}],54:[function(require,module,exports){arguments[4][37][0].apply(exports,arguments)},{dup:37}],55:[function(require,module,exports){"use strict";module.exports=Integrator;function adsimp(f,a,b,fa,fm,fb,V0,tol,maxdepth,depth){var V,h,f1,f2,sl,sr,s2,m,V1,V2,err;h=b-a;f1=f(a+h*.25);f2=f(b-h*.25);sl=h*(fa+4*f1+fm)/12;sr=h*(fm+4*f2+fb)/12;s2=sl+sr;err=(s2-V0)/15;if(depth>maxdepth){console.log("integrate-adaptive-simpson: Warning: maximum recursion depth ("+maxdepth+") exceeded");return s2+err}else if(Math.abs(err)=0;i-=1){bytes[i]+=n;if(bytes[i]===256){n=1;bytes[i]=0}else if(bytes[i]===-1){n=-1;bytes[i]=255}else{n=0}}}function solve(a,b){if(a===Number.POSITIVE_INFINITY||a===Number.NEGATIVE_INFINITY||isNaN(a)){return a}var bytes=float64ToOctets(a);add(bytes,b);return octetsToFloat64(bytes)}exports.doubleToOctetArray=float64ToOctets;exports.ieee754NextDouble=function(n){return solve(n,1)};exports.ieee754PrevDouble=function(n){return solve(n,-1)}}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{typedarray:73}],63:[function(require,module,exports){"use strict";var utils=require("./operations/utils");var rmath=require("./round-math");function Interval(lo,hi){switch(arguments.length){case 1:if(typeof lo!=="number"){throw new TypeError("lo must be a number")}this.set(lo,lo);if(isNaN(lo)){this.setEmpty()}break;case 2:if(typeof lo!=="number"||typeof hi!=="number"){throw new TypeError("lo,hi must be numbers")}this.set(lo,hi);if(isNaN(lo)||isNaN(hi)||lo>hi){this.setEmpty()}break;default:this.lo=0;this.hi=0;break}}Interval.factory=function(a,b){function assert(a,message){if(!a){throw new Error(message||"assertion failed")}}function singleton(x){if(typeof x==="object"){assert(typeof x.lo==="number"&&typeof x.hi==="number","param must be an Interval");assert(utils.singleton(x),"param needs to be a singleton")}}function getNumber(x){if(typeof x==="object"){singleton(x);return x.lo}return x}assert(arguments.length<=2);var lo,hi;if(arguments.length===2){lo=getNumber(a);hi=getNumber(b)}else if(arguments.length===1){if(Array.isArray(a)){lo=a[0];hi=a[1]}else{lo=hi=getNumber(a)}}else{return new Interval}return new Interval(lo,hi)};Interval.prototype.singleton=function(v){return this.set(v,v)};Interval.prototype.bounded=function(lo,hi){return this.set(rmath.prev(lo),rmath.next(hi))};Interval.prototype.boundedSingleton=function(v){return this.bounded(v,v)};Interval.prototype.set=function(lo,hi){this.lo=lo;this.hi=hi;return this};Interval.prototype.assign=function(lo,hi){if(isNaN(lo)||isNaN(hi)||lo>hi){return this.setEmpty()}return this.set(lo,hi)};Interval.prototype.setEmpty=function(){return this.set(Number.POSITIVE_INFINITY,Number.NEGATIVE_INFINITY)};Interval.prototype.setWhole=function(){return this.set(Number.NEGATIVE_INFINITY,Number.POSITIVE_INFINITY)};Interval.prototype.toArray=function(){return[this.lo,this.hi]};module.exports=Interval},{"./operations/utils":70,"./round-math":72}],64:[function(require,module,exports){"use strict";var Interval=require("../interval");var rmath=require("../round-math");var utils=require("./utils");var arithmetic=require("./arithmetic");var constants=require("../constants");var algebra={};algebra.fmod=function(x,y){if(utils.empty(x)||utils.empty(y)){return constants.EMPTY}var yb=x.lo<0?y.lo:y.hi;var n=rmath.intLo(rmath.divLo(x.lo,yb));return arithmetic.sub(x,arithmetic.mul(y,new Interval(n,n)))};algebra.multiplicativeInverse=function(x){if(utils.empty(x)){return constants.EMPTY}if(utils.zeroIn(x)){if(x.lo!==0){if(x.hi!==0){return constants.WHOLE}else{return new Interval(Number.NEGATIVE_INFINITY,rmath.divHi(1,x.lo))}}else{if(x.hi!==0){return new Interval(rmath.divLo(1,x.hi),Number.POSITIVE_INFINITY)}else{return constants.EMPTY}}}else{return new Interval(rmath.divLo(1,x.hi),rmath.divHi(1,x.lo))}};algebra.pow=function(x,power){if(utils.empty(x)){return constants.EMPTY}if(typeof power==="object"){if(!utils.singleton(power)){return constants.EMPTY}power=power.lo}if(power===0){if(x.lo===0&&x.hi===0){return constants.EMPTY}else{return constants.ONE}}else if(power<0){return algebra.multiplicativeInverse(algebra.pow(x,-power))}if(x.hi<0){var yl=rmath.powLo(-x.hi,power);var yh=rmath.powHi(-x.lo,power);if(power&1){return new Interval(-yh,-yl)}else{return new Interval(yl,yh)}}else if(x.lo<0){if(power&1){return new Interval(-rmath.powLo(-x.lo,power),rmath.powHi(x.hi,power))}else{return new Interval(0,rmath.powHi(Math.max(-x.lo,x.hi),power))}}else{return new Interval(rmath.powLo(x.lo,power),rmath.powHi(x.hi,power))}};algebra.sqrt=function(x){if(utils.empty(x)||x.hi<0){return constants.EMPTY}var t=x.lo<=0?0:rmath.sqrtLo(x.lo);return new Interval(t,rmath.sqrtHi(x.hi))};module.exports=algebra},{"../constants":61,"../interval":63,"../round-math":72,"./arithmetic":65,"./utils":70}],65:[function(require,module,exports){"use strict";var Interval=require("../interval");var rmath=require("../round-math");var utils=require("./utils");var constants=require("../constants");var division=require("./division");var arithmetic={};arithmetic.add=function(a,b){return new Interval(rmath.addLo(a.lo,b.lo),rmath.addHi(a.hi,b.hi))};arithmetic.sub=function(a,b){return new Interval(rmath.subLo(a.lo,b.hi),rmath.subHi(a.hi,b.lo))};arithmetic.mul=function(a,b){if(utils.empty(a)||utils.empty(b)){return constants.EMPTY}var al=a.lo;var ah=a.hi;var bl=b.lo;var bh=b.hi;var out=new Interval;if(al<0){if(ah>0){if(bl<0){if(bh>0){out.lo=Math.min(rmath.mulLo(al,bh),rmath.mulLo(ah,bl));out.hi=Math.max(rmath.mulHi(al,bl),rmath.mulHi(ah,bh))}else{out.lo=rmath.mulLo(ah,bl);out.hi=rmath.mulHi(al,bl)}}else{if(bh>0){out.lo=rmath.mulLo(al,bh);out.hi=rmath.mulHi(ah,bh)}else{out.lo=0;out.hi=0}}}else{if(bl<0){if(bh>0){out.lo=rmath.mulLo(al,bh);out.hi=rmath.mulHi(al,bl)}else{out.lo=rmath.mulLo(ah,bh);out.hi=rmath.mulHi(al,bl)}}else{if(bh>0){out.lo=rmath.mulLo(al,bh);out.hi=rmath.mulHi(ah,bl)}else{out.lo=0;out.hi=0}}}}else{if(ah>0){if(bl<0){if(bh>0){out.lo=rmath.mulLo(ah,bl);out.hi=rmath.mulHi(ah,bh)}else{out.lo=rmath.mulLo(ah,bl);out.hi=rmath.mulHi(al,bh)}}else{if(bh>0){out.lo=rmath.mulLo(al,bl);out.hi=rmath.mulHi(ah,bh)}else{out.lo=0;out.hi=0}}}else{out.lo=0;out.hi=0}}return out};arithmetic.div=function(a,b){if(utils.empty(a)||utils.empty(b)){return constants.EMPTY}if(utils.zeroIn(b)){if(b.lo!==0){if(b.hi!==0){return division.zero(a)}else{return division.negative(a,b.lo)}}else{if(b.hi!==0){return division.positive(a,b.hi)}else{return constants.EMPTY}}}else{return division.nonZero(a,b)}};arithmetic.positive=function(a){return new Interval(a.lo,a.hi)};arithmetic.negative=function(a){return new Interval(-a.hi,-a.lo)};module.exports=arithmetic},{"../constants":61,"../interval":63,"../round-math":72,"./division":66,"./utils":70}],66:[function(require,module,exports){"use strict";var Interval=require("../interval");var rmath=require("../round-math");var utils=require("./utils");var constants=require("../constants");var division={nonZero:function(x,y){var xl=x.lo;var xh=x.hi;var yl=y.lo;var yh=y.hi;var out=new Interval;if(xh<0){if(yh<0){out.lo=rmath.divLo(xh,yl);out.hi=rmath.divHi(xl,yh)}else{out.lo=rmath.divLo(xl,yl);out.hi=rmath.divHi(xh,yh)}}else if(xl<0){if(yh<0){out.lo=rmath.divLo(xh,yh);out.hi=rmath.divHi(xl,yh)}else{out.lo=rmath.divLo(xl,yl);out.hi=rmath.divHi(xh,yl)}}else{if(yh<0){out.lo=rmath.divLo(xh,yh);out.hi=rmath.divHi(xl,yl)}else{out.lo=rmath.divLo(xl,yh);out.hi=rmath.divHi(xh,yl)}}return out},positive:function(x,v){if(x.lo===0&&x.hi===0){return x}if(utils.zeroIn(x)){return constants.WHOLE}if(x.hi<0){return new Interval(Number.NEGATIVE_INFINITY,rmath.divHi(x.hi,v))}else{return new Interval(rmath.divLo(x.lo,v),Number.POSITIVE_INFINITY)}},negative:function(x,v){if(x.lo===0&&x.hi===0){return x}if(utils.zeroIn(x)){return constants.WHOLE}if(x.hi<0){return new Interval(rmath.divLo(x.hi,v),Number.POSITIVE_INFINITY)}else{return new Interval(Number.NEGATIVE_INFINITY,rmath.divHi(x.lo,v))}},zero:function(x){if(x.lo===0&&x.hi===0){return x}return constants.WHOLE}};module.exports=division},{"../constants":61,"../interval":63,"../round-math":72,"./utils":70}],67:[function(require,module,exports){"use strict";var constants=require("../constants");var Interval=require("../interval");var rmath=require("../round-math");var utils=require("./utils");var arithmetic=require("./arithmetic");var misc={};misc.exp=function(x){if(utils.empty(x)){return constants.EMPTY}return new Interval(rmath.expLo(x.lo),rmath.expHi(x.hi))};misc.log=function(x){if(utils.empty(x)){return constants.EMPTY}var l=x.lo<=0?Number.NEGATIVE_INFINITY:rmath.logLo(x.lo);return new Interval(l,rmath.logHi(x.hi))};misc.ln=misc.log;misc.LOG_EXP_10=misc.log(new Interval(10,10));misc.log10=function(x){if(utils.empty(x)){return constants.EMPTY}return arithmetic.div(misc.log(x),misc.LOG_EXP_10)};misc.LOG_EXP_2=misc.log(new Interval(2,2));misc.log2=function(x){if(utils.empty(x)){return constants.EMPTY}return arithmetic.div(misc.log(x),misc.LOG_EXP_2)};misc.hull=function(x,y){var badX=utils.empty(x);var badY=utils.empty(y);if(badX){if(badY){return constants.EMPTY}else{return y}}else{if(badY){return x}else{return new Interval(Math.min(x.lo,y.lo),Math.max(x.hi,y.hi))}}};misc.intersect=function(x,y){if(utils.empty(x)||utils.empty(y)){return constants.EMPTY}var lo=Math.max(x.lo,y.lo);var hi=Math.min(x.hi,y.hi);if(lo<=hi){return new Interval(lo,hi)}return constants.EMPTY};misc.abs=function(x){if(utils.empty(x)){return constants.EMPTY}if(x.lo>=0){return x}if(x.hi<=0){return arithmetic.negative(x)}return new Interval(0,Math.max(-x.lo,x.hi))};misc.max=function(x,y){if(utils.empty(x)){return constants.EMPTY}return new Interval(Math.max(x.lo,y.lo),Math.max(x.hi,y.hi))};misc.min=function(x,y){if(utils.empty(x)){return constants.EMPTY}return new Interval(Math.min(x.lo,y.lo),Math.min(x.hi,y.hi))};misc.clone=function(x){return(new Interval).set(x.lo,x.hi)};module.exports=misc},{"../constants":61,"../interval":63,"../round-math":72,"./arithmetic":65,"./utils":70}],68:[function(require,module,exports){"use strict";var utils=require("./utils");var relational={};relational.equal=function(x,y){if(utils.empty(x)){return utils.empty(y)}return!utils.empty(y)&&x.lo===y.lo&&x.hi===y.hi};relational.almostEqual=function(x,y){var EPS=1e-7;function assert(a,message){if(!a){throw new Error(message||"assertion failed")}}function assertEps(a,b){assert(Math.abs(a-b)y.hi};relational.lt=function(x,y){if(utils.empty(x)||utils.empty(y)){return false}return x.hiy.hi};relational.leq=function(x,y){if(utils.empty(x)||utils.empty(y)){return false}return x.hi<=y.lo};relational.geq=function(x,y){if(utils.empty(x)||utils.empty(y)){return false}return x.lo>=y.hi};module.exports=relational},{"./utils":70}],69:[function(require,module,exports){"use strict";var constants=require("../constants");var Interval=require("../interval");var rmath=require("../round-math");var utils=require("./utils");var algebra=require("./algebra");var arithmetic=require("./arithmetic");var trigonometric={};trigonometric.cos=function(x){var rlo,rhi;if(utils.empty(x)){return constants.EMPTY}if(x.lo<0){var mult=1e7;x.lo+=2*Math.PI*mult;x.hi+=2*Math.PI*mult}var pi2=constants.PI_TWICE;var t=algebra.fmod(x,pi2);if(utils.width(t)>=pi2.lo){return new Interval(-1,1)}if(t.lo>=constants.PI_HIGH){var cos=trigonometric.cos(arithmetic.sub(t,constants.PI));return arithmetic.negative(cos)}var lo=t.lo;var hi=t.hi;rlo=rmath.cosLo(hi);rhi=rmath.cosHi(lo);if(hi<=constants.PI_LOW){return new Interval(rlo,rhi)}else if(hi<=pi2.lo){return new Interval(-1,Math.max(rlo,rhi))}else{return new Interval(-1,1)}};trigonometric.sin=function(x){if(utils.empty(x)){return constants.EMPTY}return trigonometric.cos(arithmetic.sub(x,constants.PI_HALF))};trigonometric.tan=function(x){if(utils.empty(x)){return constants.EMPTY}if(x.lo<0){var mult=1e7;x.lo+=2*Math.PI*mult;x.hi+=2*Math.PI*mult}var pi=constants.PI;var t=algebra.fmod(x,pi);if(t.lo>=constants.PI_HALF_LOW){t=arithmetic.sub(t,pi)}if(t.lo<=-constants.PI_HALF_LOW||t.hi>=constants.PI_HALF_LOW){return constants.WHOLE}return new Interval(rmath.tanLo(t.lo),rmath.tanHi(t.hi))};trigonometric.asin=function(x){if(utils.empty(x)||x.hi<-1||x.lo>1){return constants.EMPTY}var lo=x.lo<=-1?-constants.PI_HALF_HIGH:rmath.asinLo(x.lo);var hi=x.hi>=1?constants.PI_HALF_HIGH:rmath.asinHi(x.hi);return new Interval(lo,hi)};trigonometric.acos=function(x){if(utils.empty(x)||x.hi<-1||x.lo>1){return constants.EMPTY}var lo=x.hi>=1?0:rmath.acosLo(x.hi);var hi=x.lo<=-1?constants.PI_HIGH:rmath.acosHi(x.lo);return new Interval(lo,hi)};trigonometric.atan=function(x){if(utils.empty(x)){return constants.EMPTY}return new Interval(rmath.atanLo(x.lo),rmath.atanHi(x.hi))};trigonometric.sinh=function(x){if(utils.empty(x)){return constants.EMPTY}return new Interval(rmath.sinhLo(x.lo),rmath.sinhHi(x.hi))};trigonometric.cosh=function(x){if(utils.empty(x)){return constants.EMPTY}if(x.hi<0){return new Interval(rmath.coshLo(x.hi),rmath.coshHi(x.lo))}else if(x.lo>=0){return new Interval(rmath.coshLo(x.lo),rmath.coshHi(x.hi))}else{return new Interval(1,rmath.coshHi(-x.lo>x.hi?x.lo:x.hi))}};trigonometric.tanh=function(x){if(utils.empty(x)){return constants.EMPTY}return new Interval(rmath.tanhLo(x.lo),rmath.tanhHi(x.hi))};module.exports=trigonometric},{"../constants":61,"../interval":63,"../round-math":72,"./algebra":64,"./arithmetic":65,"./utils":70}],70:[function(require,module,exports){"use strict";var rmath=require("../round-math");var utils={};utils.empty=function(a){return a.lo>a.hi};utils.whole=function(a){return a.lo===-Infinity&&a.hi===Infinity};utils.zeroIn=function(a){return utils.in(a,0)};utils.in=function(a,v){if(utils.empty(a)){return false}return a.lo<=v&&v<=a.hi};utils.subset=function(a,b){if(utils.empty(a)){return true}return!utils.empty(b)&&b.lo<=a.lo&&a.hi<=b.hi};utils.overlap=function(a,b){if(utils.empty(a)||utils.empty(b)){return false}return a.lo<=b.lo&&b.lo<=a.hi||b.lo<=a.lo&&a.lo<=b.hi};utils.singleton=function(x){return!utils.empty(x)&&x.lo===x.hi};utils.width=function(x){if(utils.empty(x)){return 0}return rmath.subHi(x.hi,x.lo)};module.exports=utils},{"../round-math":72}],71:[function(require,module,exports){"use strict";Math.sinh=Math.sinh||function(x){var y=Math.exp(x);return(y-1/y)/2};Math.cosh=Math.cosh||function(x){var y=Math.exp(x);return(y+1/y)/2};Math.tanh=Math.tanh||function(x){if(x===Number.POSITIVE_INFINITY){return 1}else if(x===Number.NEGATIVE_INFINITY){return-1}else{var y=Math.exp(2*x);return(y-1)/(y+1)}}},{}],72:[function(require,module,exports){"use strict";var double=require("./double");var round={};var MIN_VALUE=double.ieee754NextDouble(0);round.POSITIVE_ZERO=+0;round.NEGATIVE_ZERO=-0;var oldNext;var next=oldNext=round.next=function(v){if(v===0){return MIN_VALUE}if(Math.abs(v)0){return double.ieee754NextDouble(v)}else{return double.ieee754PrevDouble(v)}}return v};var oldPrev;var prev=oldPrev=round.prev=function(v){return-next(-v)};round.addLo=function(x,y){return prev(x+y)};round.addHi=function(x,y){return next(x+y)};round.subLo=function(x,y){return prev(x-y)};round.subHi=function(x,y){return next(x-y)};round.mulLo=function(x,y){return prev(x*y)};round.mulHi=function(x,y){return next(x*y)};round.divLo=function(x,y){return prev(x/y)};round.divHi=function(x,y){return next(x/y)};function toInteger(x){return x<0?Math.ceil(x):Math.floor(x)}round.intLo=function(x){return toInteger(prev(x))};round.intHi=function(x){return toInteger(next(x))};round.logLo=function(x){return prev(Math.log(x))};round.logHi=function(x){return next(Math.log(x))};round.expLo=function(x){return prev(Math.exp(x))};round.expHi=function(x){return next(Math.exp(x))};round.sinLo=function(x){return prev(Math.sin(x))};round.sinHi=function(x){return next(Math.sin(x))};round.cosLo=function(x){return prev(Math.cos(x))};round.cosHi=function(x){return next(Math.cos(x))};round.tanLo=function(x){return prev(Math.tan(x))};round.tanHi=function(x){return next(Math.tan(x))};round.asinLo=function(x){return prev(Math.asin(x))};round.asinHi=function(x){return next(Math.asin(x))};round.acosLo=function(x){return prev(Math.acos(x))};round.acosHi=function(x){return next(Math.acos(x))};round.atanLo=function(x){return prev(Math.atan(x))};round.atanHi=function(x){return next(Math.atan(x))};round.sinhLo=function(x){return prev(Math.sinh(x))};round.sinhHi=function(x){return next(Math.sinh(x))};round.coshLo=function(x){return prev(Math.cosh(x))};round.coshHi=function(x){return next(Math.cosh(x))};round.tanhLo=function(x){return prev(Math.tanh(x))};round.tanhHi=function(x){return next(Math.tanh(x))};round.powLo=function(x,power){if(power%1!==0){return prev(Math.pow(x,power))}var y=power&1?x:1;power>>=1;while(power>0){x=round.mulLo(x,x);if(power&1){y=round.mulLo(x,y)}power>>=1}return y};round.powHi=function(x,power){if(power%1!==0){return next(Math.pow(x,power))}var y=power&1?x:1;power>>=1;while(power>0){x=round.mulHi(x,x);if(power&1){y=round.mulHi(x,y)}power>>=1}return y};round.sqrtLo=function(x){return prev(Math.sqrt(x))};round.sqrtHi=function(x){return next(Math.sqrt(x))};round.disable=function(){next=prev=round.next=round.prev=function(v){return v}};round.enable=function(){prev=round.prev=oldPrev;next=round.next=oldNext};module.exports=round},{"./double":62}],73:[function(require,module,exports){var undefined=void 0;var MAX_ARRAY_LENGTH=1e5;var ECMAScript=function(){var opts=Object.prototype.toString,ophop=Object.prototype.hasOwnProperty;return{Class:function(v){return opts.call(v).replace(/^\[object *|\]$/g,"")},HasProperty:function(o,p){return p in o},HasOwnProperty:function(o,p){return ophop.call(o,p)},IsCallable:function(o){return typeof o==="function"},ToInt32:function(v){return v>>0},ToUint32:function(v){return v>>>0}}}();var LN2=Math.LN2,abs=Math.abs,floor=Math.floor,log=Math.log,min=Math.min,pow=Math.pow,round=Math.round;function configureProperties(obj){if(getOwnPropNames&&defineProp){var props=getOwnPropNames(obj),i;for(i=0;iMAX_ARRAY_LENGTH)throw new RangeError("Array too large for polyfill");function makeArrayAccessor(index){defineProp(obj,index,{get:function(){return obj._getter(index)},set:function(v){obj._setter(index,v)},enumerable:true,configurable:false})}var i;for(i=0;i>s}function as_unsigned(value,bits){var s=32-bits;return value<>>s}function packI8(n){return[n&255]}function unpackI8(bytes){return as_signed(bytes[0],8)}function packU8(n){return[n&255]}function unpackU8(bytes){return as_unsigned(bytes[0],8)}function packU8Clamped(n){n=round(Number(n));return[n<0?0:n>255?255:n&255]}function packI16(n){return[n>>8&255,n&255]}function unpackI16(bytes){return as_signed(bytes[0]<<8|bytes[1],16)}function packU16(n){return[n>>8&255,n&255]}function unpackU16(bytes){return as_unsigned(bytes[0]<<8|bytes[1],16)}function packI32(n){return[n>>24&255,n>>16&255,n>>8&255,n&255]}function unpackI32(bytes){return as_signed(bytes[0]<<24|bytes[1]<<16|bytes[2]<<8|bytes[3],32)}function packU32(n){return[n>>24&255,n>>16&255,n>>8&255,n&255]}function unpackU32(bytes){return as_unsigned(bytes[0]<<24|bytes[1]<<16|bytes[2]<<8|bytes[3],32)}function packIEEE754(v,ebits,fbits){var bias=(1<.5)return w+1;return w%2?w+1:w}if(v!==v){e=(1<=pow(2,1-bias)){e=min(floor(log(v)/LN2),1023);f=roundToEven(v/pow(2,e)*pow(2,fbits));if(f/pow(2,fbits)>=2){e=e+1;f=1}if(e>bias){e=(1<>1}}bits.reverse();str=bits.join("");bias=(1<0){return s*pow(2,e-bias)*(1+f/pow(2,fbits))}else if(f!==0){return s*pow(2,-(bias-1))*(f/pow(2,fbits))}else{return s<0?-0:0}}function unpackF64(b){return unpackIEEE754(b,11,52)}function packF64(v){return packIEEE754(v,11,52)}function unpackF32(b){return unpackIEEE754(b,8,23)}function packF32(v){return packIEEE754(v,8,23)}(function(){var ArrayBuffer=function ArrayBuffer(length){length=ECMAScript.ToInt32(length);if(length<0)throw new RangeError("ArrayBuffer size is not a small enough positive integer");this.byteLength=length;this._bytes=[];this._bytes.length=length;var i;for(i=0;ithis.buffer.byteLength){throw new RangeError("byteOffset out of range")}if(this.byteOffset%this.BYTES_PER_ELEMENT){throw new RangeError("ArrayBuffer length minus the byteOffset is not a multiple of the element size.")}if(arguments.length<3){this.byteLength=this.buffer.byteLength-this.byteOffset;if(this.byteLength%this.BYTES_PER_ELEMENT){throw new RangeError("length of buffer minus byteOffset not a multiple of the element size")}this.length=this.byteLength/this.BYTES_PER_ELEMENT}else{this.length=ECMAScript.ToUint32(length);this.byteLength=this.length*this.BYTES_PER_ELEMENT}if(this.byteOffset+this.byteLength>this.buffer.byteLength){throw new RangeError("byteOffset and length reference an area beyond the end of the buffer")}}else{throw new TypeError("Unexpected argument type(s)")}this.constructor=ctor;configureProperties(this);makeArrayAccessors(this)};ctor.prototype=new ArrayBufferView;ctor.prototype.BYTES_PER_ELEMENT=bytesPerElement;ctor.prototype._pack=pack;ctor.prototype._unpack=unpack;ctor.BYTES_PER_ELEMENT=bytesPerElement;ctor.prototype._getter=function(index){if(arguments.length<1)throw new SyntaxError("Not enough arguments");index=ECMAScript.ToUint32(index);if(index>=this.length){return undefined}var bytes=[],i,o;for(i=0,o=this.byteOffset+index*this.BYTES_PER_ELEMENT;i=this.length){return undefined}var bytes=this._pack(value),i,o;for(i=0,o=this.byteOffset+index*this.BYTES_PER_ELEMENT;ithis.length){throw new RangeError("Offset plus length of array is out of range")}byteOffset=this.byteOffset+offset*this.BYTES_PER_ELEMENT;byteLength=array.length*this.BYTES_PER_ELEMENT;if(array.buffer===this.buffer){tmp=[];for(i=0,s=array.byteOffset;ithis.length){throw new RangeError("Offset plus length of array is out of range")}for(i=0;imax?max:v}start=ECMAScript.ToInt32(start);end=ECMAScript.ToInt32(end);if(arguments.length<1){start=0}if(arguments.length<2){end=this.length}if(start<0){start=this.length+start}if(end<0){end=this.length+end}start=clamp(start,0,this.length);end=clamp(end,0,this.length);var len=end-start;if(len<0){len=0}return new this.constructor(this.buffer,this.byteOffset+start*this.BYTES_PER_ELEMENT,len)};return ctor}var Int8Array=makeConstructor(1,packI8,unpackI8);var Uint8Array=makeConstructor(1,packU8,unpackU8);var Uint8ClampedArray=makeConstructor(1,packU8Clamped,unpackU8);var Int16Array=makeConstructor(2,packI16,unpackI16);var Uint16Array=makeConstructor(2,packU16,unpackU16);var Int32Array=makeConstructor(4,packI32,unpackI32);var Uint32Array=makeConstructor(4,packU32,unpackU32);var Float32Array=makeConstructor(4,packF32,unpackF32);var Float64Array=makeConstructor(8,packF64,unpackF64);exports.Int8Array=exports.Int8Array||Int8Array;exports.Uint8Array=exports.Uint8Array||Uint8Array;exports.Uint8ClampedArray=exports.Uint8ClampedArray||Uint8ClampedArray;exports.Int16Array=exports.Int16Array||Int16Array;exports.Uint16Array=exports.Uint16Array||Uint16Array;exports.Int32Array=exports.Int32Array||Int32Array;exports.Uint32Array=exports.Uint32Array||Uint32Array;exports.Float32Array=exports.Float32Array||Float32Array;exports.Float64Array=exports.Float64Array||Float64Array})();(function(){function r(array,index){return ECMAScript.IsCallable(array.get)?array.get(index):array[index]}var IS_BIG_ENDIAN=function(){var u16array=new exports.Uint16Array([4660]),u8array=new exports.Uint8Array(u16array.buffer);return r(u8array,0)===18}();var DataView=function DataView(buffer,byteOffset,byteLength){if(arguments.length===0){buffer=new exports.ArrayBuffer(0)}else if(!(buffer instanceof exports.ArrayBuffer||ECMAScript.Class(buffer)==="ArrayBuffer")){throw new TypeError("TypeError")}this.buffer=buffer||new exports.ArrayBuffer(0);this.byteOffset=ECMAScript.ToUint32(byteOffset);if(this.byteOffset>this.buffer.byteLength){throw new RangeError("byteOffset out of range")}if(arguments.length<3){this.byteLength=this.buffer.byteLength-this.byteOffset}else{this.byteLength=ECMAScript.ToUint32(byteLength)}if(this.byteOffset+this.byteLength>this.buffer.byteLength){throw new RangeError("byteOffset and length reference an area beyond the end of the buffer")}configureProperties(this)};function makeGetter(arrayType){return function(byteOffset,littleEndian){byteOffset=ECMAScript.ToUint32(byteOffset);if(byteOffset+arrayType.BYTES_PER_ELEMENT>this.byteLength){throw new RangeError("Array index out of range")}byteOffset+=this.byteOffset;var uint8Array=new exports.Uint8Array(this.buffer,byteOffset,arrayType.BYTES_PER_ELEMENT),bytes=[],i;for(i=0;ithis.byteLength){throw new RangeError("Array index out of range")}var typeArray=new arrayType([value]),byteArray=new exports.Uint8Array(typeArray.buffer),bytes=[],i,byteView;for(i=0;i":"",1:"",2:"",3:"",4:"",5:"",6:"",8:"",9:"",12:"",13:"",16:"",17:"",18:"",19:"",20:"",21:"",23:"",24:"",25:"",27:"",28:"",29:"",30:"",31:"",27:"",32:"",33:"",34:"",35:"",36:"",37:"",38:"",39:"",40:"",41:"' +, 42: '' +, 43: '' +, 44: '' +, 45: '' +, 46: '' +, 47: '' +, 91: '' // meta-left -- no one handles left and right properly, so we coerce into one. +, 92: '' // meta-right +, 93: isOSX ? '' : '' // chrome,opera,safari all report this for meta-right (osx mbp). +, 95: '' +, 106: '' +, 107: '' +, 108: '' +, 109: '' +, 110: '' +, 111: '' +, 144: '' +, 145: '' +, 160: '' +, 161: '' +, 162: '' +, 163: '' +, 164: '' +, 165: '' +, 166: '' +, 167: '' +, 168: '' +, 169: '' +, 170: '' +, 171: '' +, 172: '' + + // ff/osx reports '' for '-' +, 173: isOSX && maybeFirefox ? '-' : '' +, 174: '' +, 175: '' +, 176: '' +, 177: '' +, 178: '' +, 179: '' +, 180: '' +, 181: '' +, 182: '' +, 183: '' +, 186: ';' +, 187: '=' +, 188: ',' +, 189: '-' +, 190: '.' +, 191: '/' +, 192: '`' +, 219: '[' +, 220: '\\' +, 221: ']' +, 222: "'" +, 223: '' +, 224: '' // firefox reports meta here. +, 226: '' +, 229: '' +, 231: isOpera ? '`' : '' +, 246: '' +, 247: '' +, 248: '' +, 249: '' +, 250: '' +, 251: '' +, 252: '' +, 253: '' +, 254: '' +} + +for(i = 58; i < 65; ++i) { + output[i] = String.fromCharCode(i) +} + +// 0-9 +for(i = 48; i < 58; ++i) { + output[i] = (i - 48)+'' +} + +// A-Z +for(i = 65; i < 91; ++i) { + output[i] = String.fromCharCode(i) +} + +// num0-9 +for(i = 96; i < 106; ++i) { + output[i] = '' +} + +// F1-F24 +for(i = 112; i < 136; ++i) { + output[i] = 'F'+(i-111) +} + +},{}],105:[function(require,module,exports){ +var Emitter = require('events').EventEmitter +var vkey = require('vkey') + +module.exports = function(keys, el) { + if (typeof keys === 'string') keys = [keys] + if (!el) el = window + + var emitter = new Emitter() + emitter.pressed = {} + + el.addEventListener('blur', clearPressed) + el.addEventListener('focus', clearPressed) + + el.addEventListener('keydown', function(ev) { + var key = vkey[ev.keyCode] + emitter.pressed[key] = true + var allPressed = true + keys.forEach(function(k) { + if (!emitter.pressed[k]) allPressed = false + }) + if (allPressed) { + emitter.emit('pressed', emitter.pressed) + + // this seems to be necessary as keyup doesn't always fire during combos :/ + clearPressed() + } + }) + + el.addEventListener('keyup', function(ev) { + delete emitter.pressed[vkey[ev.keyCode]] + }) + + function clearPressed() { + emitter.pressed = {} + } + + return emitter +} + +},{"events":107,"vkey":106}],106:[function(require,module,exports){ +arguments[4][104][0].apply(exports,arguments) +},{"dup":104}],107:[function(require,module,exports){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +function EventEmitter() { + this._events = this._events || {}; + this._maxListeners = this._maxListeners || undefined; +} +module.exports = EventEmitter; + +// Backwards-compat with node 0.10.x +EventEmitter.EventEmitter = EventEmitter; + +EventEmitter.prototype._events = undefined; +EventEmitter.prototype._maxListeners = undefined; + +// By default EventEmitters will print a warning if more than 10 listeners are +// added to it. This is a useful default which helps finding memory leaks. +EventEmitter.defaultMaxListeners = 10; + +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +EventEmitter.prototype.setMaxListeners = function(n) { + if (!isNumber(n) || n < 0 || isNaN(n)) + throw TypeError('n must be a positive number'); + this._maxListeners = n; + return this; +}; + +EventEmitter.prototype.emit = function(type) { + var er, handler, len, args, i, listeners; + + if (!this._events) + this._events = {}; + + // If there is no 'error' event listener then throw. + if (type === 'error') { + if (!this._events.error || + (isObject(this._events.error) && !this._events.error.length)) { + er = arguments[1]; + if (er instanceof Error) { + throw er; // Unhandled 'error' event + } + throw TypeError('Uncaught, unspecified "error" event.'); + } + } + + handler = this._events[type]; + + if (isUndefined(handler)) + return false; + + if (isFunction(handler)) { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + len = arguments.length; + args = new Array(len - 1); + for (i = 1; i < len; i++) + args[i - 1] = arguments[i]; + handler.apply(this, args); + } + } else if (isObject(handler)) { + len = arguments.length; + args = new Array(len - 1); + for (i = 1; i < len; i++) + args[i - 1] = arguments[i]; + + listeners = handler.slice(); + len = listeners.length; + for (i = 0; i < len; i++) + listeners[i].apply(this, args); + } + + return true; +}; + +EventEmitter.prototype.addListener = function(type, listener) { + var m; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events) + this._events = {}; + + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (this._events.newListener) + this.emit('newListener', type, + isFunction(listener.listener) ? + listener.listener : listener); + + if (!this._events[type]) + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + else if (isObject(this._events[type])) + // If we've already got an array, just append. + this._events[type].push(listener); + else + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + + // Check for listener leak + if (isObject(this._events[type]) && !this._events[type].warned) { + var m; + if (!isUndefined(this._maxListeners)) { + m = this._maxListeners; + } else { + m = EventEmitter.defaultMaxListeners; + } + + if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + this._events[type].length); + if (typeof console.trace === 'function') { + // not supported in IE 10 + console.trace(); + } + } + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener) { + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + var fired = false; + + function g() { + this.removeListener(type, g); + + if (!fired) { + fired = true; + listener.apply(this, arguments); + } + } + + g.listener = listener; + this.on(type, g); + + return this; +}; + +// emits a 'removeListener' event iff the listener was removed +EventEmitter.prototype.removeListener = function(type, listener) { + var list, position, length, i; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events || !this._events[type]) + return this; + + list = this._events[type]; + length = list.length; + position = -1; + + if (list === listener || + (isFunction(list.listener) && list.listener === listener)) { + delete this._events[type]; + if (this._events.removeListener) + this.emit('removeListener', type, listener); + + } else if (isObject(list)) { + for (i = length; i-- > 0;) { + if (list[i] === listener || + (list[i].listener && list[i].listener === listener)) { + position = i; + break; + } + } + + if (position < 0) + return this; + + if (list.length === 1) { + list.length = 0; + delete this._events[type]; + } else { + list.splice(position, 1); + } + + if (this._events.removeListener) + this.emit('removeListener', type, listener); + } + + return this; +}; + +EventEmitter.prototype.removeAllListeners = function(type) { + var key, listeners; + + if (!this._events) + return this; + + // not listening for removeListener, no need to emit + if (!this._events.removeListener) { + if (arguments.length === 0) + this._events = {}; + else if (this._events[type]) + delete this._events[type]; + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (key in this._events) { + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = {}; + return this; + } + + listeners = this._events[type]; + + if (isFunction(listeners)) { + this.removeListener(type, listeners); + } else { + // LIFO order + while (listeners.length) + this.removeListener(type, listeners[listeners.length - 1]); + } + delete this._events[type]; + + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + var ret; + if (!this._events || !this._events[type]) + ret = []; + else if (isFunction(this._events[type])) + ret = [this._events[type]]; + else + ret = this._events[type].slice(); + return ret; +}; + +EventEmitter.listenerCount = function(emitter, type) { + var ret; + if (!emitter._events || !emitter._events[type]) + ret = 0; + else if (isFunction(emitter._events[type])) + ret = 1; + else + ret = emitter._events[type].length; + return ret; +}; + +function isFunction(arg) { + return typeof arg === 'function'; +} + +function isNumber(arg) { + return typeof arg === 'number'; +} + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} + +function isUndefined(arg) { + return arg === void 0; +} + +},{}],108:[function(require,module,exports){ +// shim for using process in browser + +var process = module.exports = {}; +var queue = []; +var draining = false; +var currentQueue; +var queueIndex = -1; + +function cleanUpNextTick() { + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } +} + +function drainQueue() { + if (draining) { + return; + } + var timeout = setTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while(len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + currentQueue[queueIndex].run(); + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + clearTimeout(timeout); +} + +process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + setTimeout(drainQueue, 0); + } +}; + +// v8 likes predictible objects +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +// TODO(shtylman) +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}]},{},[1])(1) +}); \ No newline at end of file diff --git a/site/js/playground.js b/site/js/playground.js index 35b1f33..96a2ff6 100644 --- a/site/js/playground.js +++ b/site/js/playground.js @@ -1,17 +1,18 @@ 'use strict'; -//var instance = functionPlot({ -// title: 'test', -// target: '#playground', -// grid: true, -// data: [{ -// fn: 'x^2', -// graphOptions: { -// type: 'line', -// sampler: 'builtIn' -// } -// }] -//}) +var instance = functionPlot({ + title: 'test', + target: '#playground', + grid: true, + data: [{ + fn: 'x * x + y * y - 1', + implicit: true, + graphOptions: { + sampler: 'builtIn', + type: 'line' + } + }] +}) //instance.on('programmatic-zoom', function () { // console.log('end') //}) @@ -114,46 +115,46 @@ // }] //}); -// update -var options = { - target: '#playground', - data: [{ - fn: 'x' - }] -}; -var instance -document.querySelector('#update').addEventListener('click', function () { - if (!options.title) { - // add a title, a tip and change the function to y = x * x - options.title = 'hello world'; - options.tip = { - xLine: true, - yLine: true - }; - options.data[0] = { - fn: 'x * x', - derivative: { - fn: '2 * x', - updateOnMouseMove: true - } - } - } else { - // remove the title and the tip - // update the function to be y = x - delete options.title; - delete options.tip; - options.data[0] = { - fn: 'x' - } - } - functionPlot(options); -}); -instance = functionPlot(options); -instance.on('eval', function (data, i, isHelper) { - if (!isHelper) { - console.log(data, i) - } -}) +//// update +//var options = { +// target: '#playground', +// data: [{ +// fn: 'x' +// }] +//}; +//var instance +//document.querySelector('#update').addEventListener('click', function () { +// if (!options.title) { +// // add a title, a tip and change the function to y = x * x +// options.title = 'hello world'; +// options.tip = { +// xLine: true, +// yLine: true +// }; +// options.data[0] = { +// fn: 'x * x', +// derivative: { +// fn: '2 * x', +// updateOnMouseMove: true +// } +// } +// } else { +// // remove the title and the tip +// // update the function to be y = x +// delete options.title; +// delete options.tip; +// options.data[0] = { +// fn: 'x' +// } +// } +// functionPlot(options); +//}); +//instance = functionPlot(options); +//instance.on('eval', function (data, i, isHelper) { +// if (!isHelper) { +// console.log(data, i) +// } +//}) // points //functionPlot({ diff --git a/site/js/site.js b/site/js/site.js index b90cab0..326acba 100644 --- a/site/js/site.js +++ b/site/js/site.js @@ -28,10 +28,8 @@ $(document).on('markupLoaded', function () { }, data: [{ fn: 'sin(exp(x))', - samples: 5000, - graphOptions: { - type: 'line' - } + nSamples: 5000, + graphType: 'polyline' }] }) @@ -139,13 +137,13 @@ $(document).on('markupLoaded', function () { }) /** - * ### Samples + * ### Number of samples * - * `samples` determine the number of equally spaced points in which the function will be + * `nSamples` determine the number of equally spaced points in which the function will be * evaluated in the current domain, increasing it will more accurately represent the function * using rectangles at the cost of processing speed * - * e.g. samples = 100 + * e.g. nSamples = 100 * * $$ * domain = [-5, 5] \\\ @@ -162,7 +160,7 @@ $(document).on('markupLoaded', function () { target: '#samples', data: [{ fn: 'sin(x)', - samples: 1000 + nSamples: 1000 }] }) @@ -198,14 +196,12 @@ $(document).on('markupLoaded', function () { /** * ### Range and closed path * - * You can restrict the values to be evaluated with the `range` option, - * this works really nice with the `closed` option of the `line` type to render - * for example a [definite integral](http://mathworld.wolfram.com/DefiniteIntegral.html) - * - * Additional graph options for the renderer of each graph can be set inside `graphOptions` + * The `x` values of the function will be taken from the current viewport limits, + * however you can define a custom range so that the function is evaluated only within this + * range, this works really nice with the `closed` option which will will render an area graph + * instead of a polyline, for example we can render a [definite integral](http://mathworld.wolfram.com/DefiniteIntegral.html) * - * - `type`: the type of graph, `line` (naive sampling), `scatter` (naive sampling) and - * `interval` (interval arithmetic sampling) are supported + * - `range` {Array} A 2-number array, the function will be evaluated only within this range * - `closed`: true to render a closed path, `y0` will always be 0 and `y1` will be $fn(x)$ */ functionPlot({ @@ -214,9 +210,7 @@ $(document).on('markupLoaded', function () { data: [{ fn: '3 + sin(x)', range: [2, 8], - graphOptions: { - closed: true - } + closed: true }] }) @@ -243,21 +237,30 @@ $(document).on('markupLoaded', function () { }) /** - * ### Scatter + * ### Graph types * - * A function can be represented with some points belonging to the curve - * instead of the actual curve, to render some points make sure to set a low value for - * `samples` and set the type option to `scatter` + * There are three ways to represent a function in `function-plot` * + * - `polyline` where $f(x)$ is evaluated with some $x$ values, after the evaluation the points are joined with line segments using + * ``s + * - `scatter` where $f(x)$ is evaluated with some $x$ values, after the evaluation the points are represented by ``s + * - `interval` where $f(x)$ is evaluated with intervals instead of a single point, after the evaluation 2d rects + * are painted on the screen (done using the `` svg element) + * + * Set the type of graph you want to render in the option `graphType` (defaults to `interval`) */ functionPlot({ - target: '#scatter', + target: '#graph-types', data: [{ - fn: 'x < 0 ? -sqrt(-x) : sqrt(x)', - samples: 100, - graphOptions: { - type: 'scatter' - } + fn: '-sqrt(-x)', + nSamples: 100, + graphType: 'scatter' + }, { + fn: 'sqrt(x)', + graphType: 'polyline' + }, { + fn: 'x^2', + graphType: 'interval' }] }) @@ -270,6 +273,11 @@ $(document).on('markupLoaded', function () { * - `xLine` true to show a dashed line parallel to $y = 0$ on the tip position * - `yLine` true to show a dashed line parallel to $x = 0$ on the tip position * - `renderer` a custom rendering function for the text shown in the tip + * + * NOTE: the tip only works with linear functions + * + * When you don't want a function to have a tip add the `skipTip: true` property to the object + * that holds the info of the function to render */ functionPlot({ target: '#tip', @@ -282,7 +290,11 @@ $(document).on('markupLoaded', function () { }, yDomain: [-1, 9], data: [ - { fn: 'x^2' } + { fn: 'x^2' }, + { + fn: 'x', + skipTip: true + } ] }) @@ -297,7 +309,7 @@ $(document).on('markupLoaded', function () { * Available options for each object: * * - `x0` the abscissa of the first point - * - `x1` (optional if `updateOnMouseMove` is set) the abscissa of the second point + * - `x1` *(optional if `updateOnMouseMove` is set) the abscissa of the second point * - `updateOnMouseMove` (optional) if set to `true` `x1` will be computed dynamically based on the current * position of the mouse */ @@ -590,16 +602,16 @@ $(document).on('markupLoaded', function () { * The options that tell `function-plot` to render a parametric equation are defined * inside each term of the `data` array and need to have the following properties set: * - * - `parametric = true` to mark this term as a parametric equation + * - `fnType: 'parametric'` to mark this term as a parametric equation * - `x` the x-coordinate of a point to be sampled with a parameter `t` * - `y` the y-coordinate of a point to be sampled with a parameter `t` * - `range = [0, 2 * Math.PI]` the `range` property in parametric equations is used * to determine the possible values of `t`, remember that the number of samples is - * set in the property `samples` + * set in the property `nSamples` * * NOTE: `function-plot` uses interval-arithmetic by default, to create a nice line * instead of rectangles generated by the interval-arithmetic sampler set - * `graphOptions.type` to `line` which uses the normal single point evaluation + * `graphType: 'polyline'` which uses the normal single point evaluation */ functionPlot({ target: '#parametric-circle', @@ -608,10 +620,8 @@ $(document).on('markupLoaded', function () { data: [{ x: 'cos(t)', y: 'sin(t)', - parametric: true, - graphOptions: { - type: 'line' - } + fnType: 'parametric', + graphType: 'polyline' }] }) @@ -635,10 +645,8 @@ $(document).on('markupLoaded', function () { x: 'sin(t) * (exp(cos(t)) - 2 cos(4t) - sin(t/12)^5)', y: 'cos(t) * (exp(cos(t)) - 2 cos(4t) - sin(t/12)^5)', range: [-10 * Math.PI, 10 * Math.PI], - parametric: true, - graphOptions: { - type: 'line' - } + fnType: 'parametric', + graphType: 'polyline' }] }) @@ -657,7 +665,7 @@ $(document).on('markupLoaded', function () { * The options that tell `function-plot` to render a polar equation are defined * inside each term of the `data` array and need to have the following properties set: * - * - `polar = true` to mark this term as a polar equation + * - `fnType: 'polar'` to tell function plot to render a polar equation * - `r` a polar equation in terms of `theta` * - `range = [-Math.PI, Math.PI]` the `range` property in polar equations is used * to determine the possible values of `theta`, remember that the number of samples is @@ -665,7 +673,7 @@ $(document).on('markupLoaded', function () { * * NOTE: `function-plot` uses interval-arithmetic by default, to create a nice line * instead of rectangles generated by the interval-arithmetic sampler set - * `graphOptions.type` to `line` which uses the normal single point evaluation + * `graphType: 'polyline'` which uses the normal single point evaluation */ functionPlot({ target: '#polar-circle', @@ -678,10 +686,8 @@ $(document).on('markupLoaded', function () { r0: 0, gamma: 0 }, - polar: true, - graphOptions: { - type: 'line' - } + fnType: 'polar', + graphType: 'polyline' }] }) @@ -700,10 +706,8 @@ $(document).on('markupLoaded', function () { xDomain: [-3, 3], data: [{ r: '2 * sin(4 theta)', - polar: true, - graphOptions: { - type: 'line' - } + fnType: 'polar', + graphType: 'polyline' }] }) @@ -727,8 +731,9 @@ $(document).on('markupLoaded', function () { * To render implicit equations you have to make sure of the following: * * - `fn(x, y)` means that the function `fn` needs to be expressed in terms of `x` and `y` - * - `implicit = true` is set on the data item that is an implicit equation + * - `fnType: 'implicit'` is set on the datum that is an implicit equation * + * NOTE: implicit functions can only be rendered with interval-arithmetic */ functionPlot({ target: '#circle-implicit', @@ -736,7 +741,7 @@ $(document).on('markupLoaded', function () { xDomain: [-3, 3], data: [{ fn: 'x * x + y * y - 1', - implicit: true + fnType: 'implicit' }] }) @@ -759,7 +764,7 @@ $(document).on('markupLoaded', function () { disableZoom: true, data: [{ fn: 'cos(PI * x) - cos(PI * y)', - implicit: true + fnType: 'implicit' }] }) @@ -769,10 +774,9 @@ $(document).on('markupLoaded', function () { * To plot a collection of points or a polyline the following options are required: * * - `points` An array of coordinates, each coordinate is represented by a 2-element array - * - `graphOptions.sampler = builtIn` interval arithmetic needs to be disabled for collections - * of points - * - `graphOptions.type = line` to render a polyline - * - `graphOptions.type = scatter` to render points + * - `fnType: 'points'` to tell function plot that the data is already available on `points` + * + * Note that you can use either `scatter` or `polyline` in the option graphType */ functionPlot({ target: '#points', @@ -784,10 +788,8 @@ $(document).on('markupLoaded', function () { [1, 2], [1, 1] ], - graphOptions: { - type: 'scatter', - sampler: 'builtIn' - } + fnType: 'points', + graphType: 'scatter' }] }) functionPlot({ @@ -800,10 +802,8 @@ $(document).on('markupLoaded', function () { [1, 2], [1, 1] ], - graphOptions: { - type: 'line', - sampler: 'builtIn' - } + fnType: 'points', + graphType: 'polyline' }] }) @@ -813,7 +813,9 @@ $(document).on('markupLoaded', function () { * To render 2d vectors set the following on each datum * * - `vector` {Array} the vector itself - * - `displacement` {Array} displacement from the origin + * - `offset` (optional) {Array} displacement from the origin + * - `fnType: 'vector'` to tell functoin plot that the data is already available on `vector` + * - `graphType: 'polyline'` to render a nice segment from `offset` to `offset + vector` */ functionPlot({ target: '#vector', @@ -821,7 +823,9 @@ $(document).on('markupLoaded', function () { grid: true, data: [{ vector: [2, 1], - displacement: [1, 2] + displacement: [1, 2], + graphType: 'polyline', + fnType: 'vector' }] }) @@ -884,10 +888,8 @@ $(document).on('markupLoaded', function () { yDomain: [-100, 100], data: [{ fn: '1/x * cos(1/x)', - graphOptions: { - // to make it look like a definite integral - closed: true - } + // to make it look like a definite integral + closed: true }], plugins: [ functionPlot.plugins.definiteIntegral({ @@ -916,28 +918,24 @@ $(document).on('markupLoaded', function () { * * - `sampler: 'builtIn'` the parser bundled with function-plot will be replaced with the one * in math.js - * - `type: 'line'` or `type: 'scatter'` + * - `graphType: 'polyline'` or `graphType: 'scatter'` */ functionPlot({ target: '#sampler-mathjs', disableZoom: true, data: [{ fn: 'gamma(x)', - graphOptions: { - sampler: 'builtIn', - type: 'line' - } + sampler: 'builtIn', + graphType: 'polyline' }] }) functionPlot({ target: '#sampler-tan-mathjs', data: [{ fn: 'tan(x)', - samples: 4000, - graphOptions: { - sampler: 'builtIn', - type: 'line' - } + nSamples: 4000, + sampler: 'builtIn', + graphType: 'polyline' }] }) /** */ diff --git a/site/partials/examples.html b/site/partials/examples.html index 2124988..5b83c5f 100644 --- a/site/partials/examples.html +++ b/site/partials/examples.html @@ -53,12 +53,12 @@ data: [{ fn: 'sin(x)' }] -})

Samples

-

samples determine the number of equally spaced points in which the function will be evaluated in the current domain, increasing it will more accurately represent the function using rectangles at the cost of processing speed

e.g. samples = 100

$$ domain = [-5, 5] \\ values = -5, -4.9, -4.8, \ldots, 4.8, 4.9, 5.0 $$

$$ domain = [-10, 10] \\ values = -10, -9.8, -9.6, \ldots, 9.6, 9.8, 10 $$

functionPlot({
+})

Number of samples

+

nSamples determine the number of equally spaced points in which the function will be evaluated in the current domain, increasing it will more accurately represent the function using rectangles at the cost of processing speed

e.g. nSamples = 100

$$ domain = [-5, 5] \\ values = -5, -4.9, -4.8, \ldots, 4.8, 4.9, 5.0 $$

$$ domain = [-10, 10] \\ values = -10, -9.8, -9.6, \ldots, 9.6, 9.8, 10 $$

functionPlot({
   target: '#samples',
   data: [{
     fn: 'sin(x)',
-    samples: 1000
+    nSamples: 1000
   }]
 })

Annotations

Parallel lines to the y-axis or x-axis can be set in the annotations option:

    @@ -82,8 +82,8 @@ text: 'y = 2' }] })

Range and closed path

-

You can restrict the values to be evaluated with the range option, this works really nice with the closed option of the line type to render for example a definite integral

Additional graph options for the renderer of each graph can be set inside graphOptions

    -
  • type: the type of graph, line (naive sampling), scatter (naive sampling) and interval (interval arithmetic sampling) are supported
  • +

    The x values of the function will be taken from the current viewport limits, however you can define a custom range so that the function is evaluated only within this range, this works really nice with the closed option which will will render an area graph instead of a polyline, for example we can render a definite integral

      +
    • range {Array} A 2-number array, the function will be evaluated only within this range
    • closed: true to render a closed path, y0 will always be 0 and y1 will be $fn(x)$
functionPlot({
@@ -92,9 +92,7 @@
   data: [{
     fn: '3 + sin(x)',
     range: [2, 8],
-    graphOptions: {
-      closed: true
-    }
+    closed: true
   }]
 })

Multiple graphs

data as seen in the examples above is an array, which means that multiple functions can be rendered in the same graph

You can also change the color of each graph, by default the colors are set from functionPlot.globals.COLORS but you can override the color by setting the color option in each datum

functionPlot({
@@ -106,23 +104,32 @@
     { fn: 'x * x * x' },
     { fn: 'x * x * x * x' }
   ]
-})

Scatter

-

A function can be represented with some points belonging to the curve instead of the actual curve, to render some points make sure to set a low value for samples and set the type option to scatter

functionPlot({
-  target: '#scatter',
+})

Graph types

+

There are three ways to represent a function in function-plot

    +
  • polyline where $f(x)$ is evaluated with some $x$ values, after the evaluation the points are joined with line segments using <path>s
  • +
  • scatter where $f(x)$ is evaluated with some $x$ values, after the evaluation the points are represented by <circle>s
  • +
  • interval where $f(x)$ is evaluated with intervals instead of a single point, after the evaluation 2d rects are painted on the screen (done using the <path> svg element)
  • +
+

Set the type of graph you want to render in the option graphType (defaults to interval)

functionPlot({
+  target: '#graph-types',
   data: [{
-    fn: 'x < 0 ? -sqrt(-x) : sqrt(x)',
-    samples: 100,
-    graphOptions: {
-      type: 'scatter'
-    }
+    fn: '-sqrt(-x)',
+    nSamples: 100,
+    graphType: 'scatter'
+  }, {
+    fn: 'sqrt(x)',
+    graphType: 'polyline'
+  }, {
+    fn: 'x^2',
+    graphType: 'interval'
   }]
-})

Tip

+})

Tip

The little circle that has the x-coordinate of the mouse position is called a "tip", the following options can be configured:

  • xLine true to show a dashed line parallel to $y = 0$ on the tip position
  • yLine true to show a dashed line parallel to $x = 0$ on the tip position
  • renderer a custom rendering function for the text shown in the tip
-
functionPlot({
+

NOTE: the tip only works with linear functions

When you don't want a function to have a tip add the skipTip: true property to the object that holds the info of the function to render

functionPlot({
   target: '#tip',
   tip: {
     xLine: true,    // dashed line parallel to y = 0
@@ -133,12 +140,16 @@
   },
   yDomain: [-1, 9],
   data: [
-    { fn: 'x^2' }
+    { fn: 'x^2' },
+    {
+      fn: 'x',
+      skipTip: true
+    }
   ]
 })

Secants

If a data object has a secants array, then each object will be used to compute secant lines between two points belonging to the function, additionally if updateOnMouseMove is a property set to true in the object then $(x_0, f(x_0))$ will be used as an anchored point and $(x_1, f(x_1))$ will be computed dynamically based on the mouse abscissa

Available options for each object:

  • x0 the abscissa of the first point
  • -
  • x1 (optional if updateOnMouseMove is set) the abscissa of the second point
  • +
  • x1 *(optional if updateOnMouseMove is set) the abscissa of the second point
  • updateOnMouseMove (optional) if set to true x1 will be computed dynamically based on the current position of the mouse
functionPlot({
@@ -315,22 +326,20 @@
   ]
 })

Parametric equations

The original equation of the circle $x^2 + y^2 = 1$ can be parametrized as

$$ x = cos(t) \\ y = sin(t) $$

For $0 \leq t \leq 2 \pi$

The options that tell function-plot to render a parametric equation are defined inside each term of the data array and need to have the following properties set:

    -
  • parametric = true to mark this term as a parametric equation
  • +
  • fnType: 'parametric' to mark this term as a parametric equation
  • x the x-coordinate of a point to be sampled with a parameter t
  • y the y-coordinate of a point to be sampled with a parameter t
  • -
  • range = [0, 2 * Math.PI] the range property in parametric equations is used to determine the possible values of t, remember that the number of samples is set in the property samples
  • +
  • range = [0, 2 * Math.PI] the range property in parametric equations is used to determine the possible values of t, remember that the number of samples is set in the property nSamples
-

NOTE: function-plot uses interval-arithmetic by default, to create a nice line instead of rectangles generated by the interval-arithmetic sampler set graphOptions.type to line which uses the normal single point evaluation

functionPlot({
+

NOTE: function-plot uses interval-arithmetic by default, to create a nice line instead of rectangles generated by the interval-arithmetic sampler set graphType: 'polyline' which uses the normal single point evaluation

functionPlot({
   target: '#parametric-circle',
   yDomain: [-1.897959183, 1.897959183],
   xDomain: [-3, 3],
   data: [{
     x: 'cos(t)',
     y: 'sin(t)',
-    parametric: true,
-    graphOptions: {
-      type: 'line'
-    }
+    fnType: 'parametric',
+    graphType: 'polyline'
   }]
 })

Parametric Equations
Butterfly curve

Let's render the famous equation of the butterfly curve using parametric equations, the equations are:

$$ x = sin(t)(e^{cos(t)} - 2cos(4t) - sin(\tfrac{t}{12})^5) \\ y = cos(t)(e^{cos(t)} - 2cos(4t) - sin(\tfrac{t}{12})^5) $$

functionPlot({
@@ -341,18 +350,16 @@
     x: 'sin(t) * (exp(cos(t)) - 2 cos(4t) - sin(t/12)^5)',
     y: 'cos(t) * (exp(cos(t)) - 2 cos(4t) - sin(t/12)^5)',
     range: [-10 * Math.PI, 10 * Math.PI],
-    parametric: true,
-    graphOptions: {
-      type: 'line'
-    }
+    fnType: 'parametric',
+    graphType: 'polyline'
   }]
 })

Polar equations

The original equation of the circle $x^2 + y^2 = 1$ can be expressed with the following polar equation

$$ r = r_0 \; cos(\theta - \gamma) + sqrt(a^2 -r_0^2 sin^2(\theta - \gamma)) $$

Where $\theta$ is the polar angle, $a$ is the radius of the circle with center $(r_0, \gamma)$

The options that tell function-plot to render a polar equation are defined inside each term of the data array and need to have the following properties set:

    -
  • polar = true to mark this term as a polar equation
  • +
  • fnType: 'polar' to tell function plot to render a polar equation
  • r a polar equation in terms of theta
  • range = [-Math.PI, Math.PI] the range property in polar equations is used to determine the possible values of theta, remember that the number of samples is set in the property samples
-

NOTE: function-plot uses interval-arithmetic by default, to create a nice line instead of rectangles generated by the interval-arithmetic sampler set graphOptions.type to line which uses the normal single point evaluation

functionPlot({
+

NOTE: function-plot uses interval-arithmetic by default, to create a nice line instead of rectangles generated by the interval-arithmetic sampler set graphType: 'polyline' which uses the normal single point evaluation

functionPlot({
   target: '#polar-circle',
   yDomain: [-1.897959183, 1.897959183],
   xDomain: [-3, 3],
@@ -363,10 +370,8 @@
       r0: 0,
       gamma: 0
     },
-    polar: true,
-    graphOptions: {
-      type: 'line'
-    }
+    fnType: 'polar',
+    graphType: 'polyline'
   }]
 })

Polar equations
Polar rose

Rendering the equation of the polar rose

$$ r = 2 sin(4 \theta) $$

functionPlot({
@@ -375,23 +380,21 @@
   xDomain: [-3, 3],
   data: [{
     r: '2 * sin(4 theta)',
-    polar: true,
-    graphOptions: {
-      type: 'line'
-    }
+    fnType: 'polar',
+    graphType: 'polyline'
   }]
 })

Implicit functions

The equation of a circle of radius 1 $x^2 + y^2 = 1$ expressed in an explicit way is:

$$ y = \sqrt{1 - x^2} \quad and \quad y = -\sqrt{1 - x^2} $$

This library can also plot implicit equations with the only requirement of making the equation equal to zero and adding the option implicit (the sampler expects that the function depends on the variables $x$ and $y$)

$$ 0 = x^2 + y^2 - 1 $$

To render implicit equations you have to make sure of the following:

  • fn(x, y) means that the function fn needs to be expressed in terms of x and y
  • -
  • implicit = true is set on the data item that is an implicit equation
  • +
  • fnType: 'implicit' is set on the datum that is an implicit equation
-
functionPlot({
+

NOTE: implicit functions can only be rendered with interval-arithmetic

functionPlot({
   target: '#circle-implicit',
   yDomain: [-1.897959183, 1.897959183],
   xDomain: [-3, 3],
   data: [{
     fn: 'x * x + y * y - 1',
-    implicit: true
+    fnType: 'implicit'
   }]
 })

Implicit function
complex implicit functions

Consider the following equation

$$ cos(\pi x) = cos(\pi y) $$

It's impossible to find an explicit version of it because we would need an infinite number of functions, however for a finite region of the plane a finite number of functions suffice

functionPlot({
@@ -401,18 +404,14 @@
   disableZoom: true,
   data: [{
     fn: 'cos(PI * x) - cos(PI * y)',
-    implicit: true
+    fnType: 'implicit'
   }]
 })

Points and polylines

To plot a collection of points or a polyline the following options are required:

  • points An array of coordinates, each coordinate is represented by a 2-element array
  • -
  • graphOptions.sampler = builtIn interval arithmetic needs to be disabled for collections of points
      -
    • graphOptions.type = line to render a polyline
    • -
    • graphOptions.type = scatter to render points
    • -
    -
  • +
  • fnType: 'points' to tell function plot that the data is already available on points
-
functionPlot({
+

Note that you can use either scatter or polyline in the option graphType

functionPlot({
   target: '#points',
   data: [{
     points: [
@@ -422,10 +421,8 @@
       [1, 2],
       [1, 1]
     ],
-    graphOptions: {
-      type: 'scatter',
-      sampler: 'builtIn'
-    }
+    fnType: 'points',
+    graphType: 'scatter'
   }]
 })
 functionPlot({
@@ -438,26 +435,25 @@
       [1, 2],
       [1, 1]
     ],
-    graphOptions: {
-      type: 'line',
-      sampler: 'builtIn'
-    }
+    fnType: 'points',
+    graphType: 'polyline'
   }]
 })

Vectors

To render 2d vectors set the following on each datum

  • vector {Array} the vector itself
  • -
  • displacement {Array} displacement from the origin
  • -
  • the same graphOptions as a polyline
  • +
  • offset (optional) {Array} displacement from the origin
  • +
  • fnType: 'vector' to tell functoin plot that the data is already available on vector
  • +
  • graphType: 'polyline' to render a nice segment from offset to offset + vector
functionPlot({
   target: '#vector',
+  xDomain: [-3, 8],
+  grid: true,
   data: [{
-    vector: [1, 2],
+    vector: [2, 1],
     displacement: [1, 2],
-    graphOptions: {
-      type: 'line',
-      sampler: 'builtIn'
-    }
+    graphType: 'polyline',
+    fnType: 'vector'
   }]
 })

Plugin: zoom box

The zoom box plugin allows the magnification of some section of the graph to enable it use the plugin configuration option

When the graph is rendered press <shift> and drag some portion of the screen

Configuration options:

    @@ -492,10 +488,8 @@ yDomain: [-100, 100], data: [{ fn: '1/x * cos(1/x)', - graphOptions: { - // to make it look like a definite integral - closed: true - } + // to make it look like a definite integral + closed: true }], plugins: [ functionPlot.plugins.definiteIntegral({ @@ -511,27 +505,23 @@

    And then set the following:

    • sampler: 'builtIn' the parser bundled with function-plot will be replaced with the one in math.js
    • -
    • type: 'line' or type: 'scatter'
    • +
    • graphType: 'polyline' or graphType: 'scatter'
functionPlot({
   target: '#sampler-mathjs',
   disableZoom: true,
   data: [{
     fn: 'gamma(x)',
-    graphOptions: {
-      sampler: 'builtIn',
-      type: 'line'
-    }
+    sampler: 'builtIn',
+    graphType: 'polyline'
   }]
 })
 functionPlot({
   target: '#sampler-tan-mathjs',
   data: [{
     fn: 'tan(x)',
-    samples: 4000,
-    graphOptions: {
-      sampler: 'builtIn',
-      type: 'line'
-    }
+    nSamples: 4000,
+    sampler: 'builtIn',
+    graphType: 'polyline'
   }]
 })
\ No newline at end of file diff --git a/site/partials/wzrd.html b/site/partials/wzrd.html index 6bba82a..eee0751 100644 --- a/site/partials/wzrd.html +++ b/site/partials/wzrd.html @@ -1,2 +1,2 @@
<script src="https://wzrd.in/standalone/function-plot@1.11.2"></script>
\ No newline at end of file +-->
<script src="https://wzrd.in/standalone/function-plot@1.12.4"></script>
\ No newline at end of file