HumbleFinance is an open source project distributed under the MIT License. It is hosted on GitHub. Feedback is welcome!

Download

HumbleFinance tar (GitHub)
HumbleFinance zip (GitHub)

HumbleFinance.js: (hide)

////////////////////////////////////////////////////////////////////
// Copyright (c) 2010 Humble Software Development
//
// 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.
////////////////////////////////////////////////////////////////////

/**
 * Humble Flinance Flotr Financial Charts
 *
 * @license MIT License <http://www.opensource.org/licenses/mit-license.php>
 * @author Carl Sutherland
 * @version 1.0
 */

var HumbleFlinance = {

   
/**
     * ID of element to attach chart
     *
     * @member String
     */

    id
: null,
   
/**
     * Graphs used to display data
     *
     * @member Object
     */

    graphs
: {price: null, volume: null, summary: null},
   
/**
     * Div containers for graphs
     *
     * @member Object
     */

    containers
: {price: null, volume: null, summary: null},
   
/**
     * Div handles for interaction with graphs
     *
     * @member Object
     */

    handles
: {left: null, right: null, scroll: null},
   
/**
     * Bounds on data
     *
     * @member Object
     */

    bounds
: {xmin: null, xmax: null, ymin: null, ymax: null},
   
/**
     * Array of data displayed in first graph.
     *
     * @member Array
     */

    priceData
: [],
   
/**
     * Array of data displayed in second graph
     *
     * @member Array
     */

    volumeData
: [],
   
/**
     * Array of data to serve as a visual summary of the above graphs
     *
     * @member Array
     */

    summaryData
: [],
   
/**
     * Formatter for x axis ticks
     *
     * @member function
     */

    xTickFormatter
: Flotr.defaultTickFormatter,
   
/**
     * Formatter for y axis ticks
     *
     * @member function
     */

    yTickFormatter
: Flotr.defaultTickFormatter,
   
/**
     * Formatter for mouse tracking
     *
     * @member function
     */

    trackFormatter
: Flotr.defaultTrackFormatter,
   
   
   
/**
     * Initialization function
     *
     * @param String id
     * @param Array priceData
     * @param Array volumeData
     * @param Array summaryData
     */

    init
: function(id, priceData, volumeData, summaryData) {
       
       
// Set members
       
this.id = id;
       
this.priceData = priceData;
       
this.volumeData = volumeData;
       
this.summaryData = summaryData;
       
       
// Set bounds to scale automatically in the y direction
       
this.bounds.xmin = 0;
       
this.bounds.xmax = this.priceData.length;
       
this.bounds.ymin = null;
       
this.bounds.ymax = null;
       
       
// Set up DOM
       
this.buildDOM();
       
this.attachEventObservers();
       
       
// Initialize graphs, setting selection on summary
       
var area = {
            x1
: 0,
            y1
: this.bounds.ymin,
            x2
: 100,
            y2
: this.bounds.ymax
       
};
       
this.graphs.summary = this.summaryGraph(this.summaryData, this.bounds);
       
this.graphs.summary.setSelection(area);
   
},
   
   
/**
     * Build DOM elements and insert into container.
     */

    buildDOM
: function () {
       
       
var container = $(this.id);
//        container.onselectstart = function () {return false;};

       
// Build DOM element
       
this.containers.price = new Element('div', {id: 'priceGraph', style: 'width: 800px; height: 240px;'});
       
this.containers.volume = new Element('div', {id: 'volumeGraph', style: 'width: 800px; height: 80px;'});
       
this.containers.summary = new Element('div', {id: 'summaryGraph', style: 'width: 800px; height: 60px;'});
       
this.handles.left = new Element('div', {id: 'leftHandle', 'class': 'handle zoomHandle', style: 'display: none;'});
       
this.handles.right = new Element('div', {id: 'rightHandle', 'class': 'handle zoomHandle', style: 'display: none;'});
       
this.handles.scroll = new Element('div', {id: 'scrollHandle', 'class': 'handle scrollHandle', style: 'display: none;'});

       
this.handles.left.onselectstart = function () { return false; };
       
this.handles.right.onselectstart = function () { return false; };
       
this.handles.scroll.onselectstart = function () { return false; };
       
       
// Insert into container
        container
.insert(this.containers.price);
        container
.insert(this.containers.volume);
        container
.insert(this.containers.summary);
        container
.insert(this.handles.left);
        container
.insert(this.handles.right);
        container
.insert(this.handles.scroll);
   
},
   
   
/**
     * Attach event observers
     */

    attachEventObservers
: function() {
       
       
// Attach summary click event to clear selection
       
Event.observe(this.containers.summary, 'flotr:click', this.reset.bind(this));
       
       
// Attach observers for hit tracking on price and volume points
       
Event.observe(this.containers.volume, 'flotr:hit', this.volumeHitObserver.bind(this));
       
Event.observe(this.containers.volume, 'flotr:clearhit', this.clearHit.bind(this));
       
Event.observe(this.containers.price, 'flotr:hit', this.priceHitObserver.bind(this));
       
Event.observe(this.containers.price, 'flotr:clearhit', this.clearHit.bind(this));
       
       
// Handle observers
       
Event.observe(this.containers.summary, 'flotr:select', this.positionScrollHandle.bind(this));
       
Event.observe(this.containers.summary, 'flotr:select', this.positionZoomHandles.bind(this));
       
Event.observe(this.handles.left, 'mousedown', this.zoomObserver.bind(this));
       
Event.observe(this.handles.right, 'mousedown', this.zoomObserver.bind(this));
       
Event.observe(this.handles.scroll, 'mousedown', this.scrollObserver.bind(this));
       
       
// On manual selection, hide zoom and scroll handles
       
Event.observe(this.containers.summary, 'mousedown', this.hideSelection.bind(this));
       
       
// Attach summary selection event to redraw price and volume charts
       
Event.observe(this.containers.summary, 'flotr:select', this.selectObserver.bind(this));
   
},
   
   
/**
     * Summary Graph Selection Observer
     *
     * @param e MouseEvent
     */

    selectObserver
: function (e) {
           
       
var area = e.memo[0];
        xmin
= Math.floor(area.x1);
        xmax
= Math.ceil(area.x2);
       
       
var newBounds = {'xmin': xmin, 'xmax': xmax, 'ymin': null, 'ymax': null};
       
       
this.graphs.price = this.priceGraph(this.priceData.slice(xmin, xmax+1), newBounds);
       
this.graphs.volume = this.volumeGraph(this.volumeData.slice(xmin, xmax+1), newBounds);
   
},
   
   
/**
     * Reset to null selection
     */

    reset
: function () {
       
this.graphs.price = this.priceGraph(this.priceData, this.bounds);
       
this.graphs.volume = this.volumeGraph(this.volumeData, this.bounds);
       
this.handles.left.hide();
       
this.handles.right.hide();
       
this.handles.scroll.hide();
   
},
   
   
/**
     * Hide selection and handles
     */

    hideSelection
: function () {

       
// Hide handles
       
this.handles.left.hide();
       
this.handles.right.hide();
       
this.handles.scroll.hide();
       
       
// Clear selection
       
this.graphs.summary.clearSelection();
   
},
   
   
/**
     * Set the position of the scroll handle
     *
     * @param e MouseEvent
     */

    positionScrollHandle
: function (e) {

       
var x1 = e.memo[0].x1;
       
var x2 = e.memo[0].x2;
       
var xaxis = e.memo[1].axes.x;
       
var plotOffset = e.memo[1].plotOffset;
       
var graphOffset = this.containers.summary.positionedOffset();
       
var graphHeight = this.containers.summary.getHeight();
       
var height = this.handles.scroll.getHeight();
       
       
// Set width
       
var width = Math.floor(xaxis.d2p(x2) - xaxis.d2p(x1)) + 8;
        width
= (width < 10) ? 18 : width;
       
       
// Set positions
       
var xPosLeft = Math.floor(graphOffset[0] + plotOffset.left + xaxis.d2p(x1) + (xaxis.d2p(x2) - xaxis.d2p(x1) - width)/2);
       
var yPos = Math.ceil(graphOffset[1] + graphHeight - 2);
       
       
this.handles.scroll.setStyle({position: 'absolute', left: xPosLeft+'px', top: yPos+'px', width: width+'px'});
       
this.handles.scroll.show();
   
},
   
   
/**
     * Begin scrolling observer
     *
     * @param e MouseEvent
     */

    scrollObserver
: function (e) {
       
       
var x = e.clientX;
       
var offset = this.handles.scroll.cumulativeOffset();
       
var prevSelection = this.graphs.summary.prevSelection;
       
       
/**
         * Perform scroll on handle move, observer
         *
         * @param e MouseEvent
         */

       
var handleObserver = function (e) {
           
           
Event.stopObserving(document, 'mousemove', handleObserver);
           
           
var deltaX = e.clientX - x;
           
var xAxis = this.graphs.summary.axes.x;
           
           
var x1 = xAxis.p2d(prevSelection.first.x + deltaX);
           
var x2 = xAxis.p2d(prevSelection.second.x + deltaX);
           
           
// Check and handle boundary conditions
           
if (x1 < 0) {
                x2
= 0 + (x2 - x1);
                x1
= 0;
           
}
           
if (x2 > this.priceData.length) {
                x1
= this.priceData.length - (x2 - x1);
                x2
= this.priceData.length;
           
}
           
           
// Set selection area object
           
var area = {
                x1
: x1,
                y1
: prevSelection.first.y,
                x2
: x2,
                y2
: prevSelection.second.y
           
};
           
           
// If selection varies from previous, set new selection
           
if (area.x1 != prevSelection.first.x) {
               
this.graphs.summary.setSelection(area);
           
}
           
           
Event.observe(document, 'mousemove', handleObserver);
       
}.bind(this);
       
       
/**
         * End scroll observer to detach event listeners
         *
         * @param e MouseEvent
         */

       
function handleEndObserver (e) {
           
Event.stopObserving(document, 'mousemove', handleObserver);
           
Event.stopObserving(document, 'mouseup', handleEndObserver);
       
};
       
       
// Attach scroll handle observers
       
Event.observe(document, 'mousemove', handleObserver);
       
Event.observe(document, 'mouseup', handleEndObserver);
   
},
   
   
/**
     * Begin zooming observer
     *
     * @param e MouseEvent
     */

    zoomObserver
: function (e) {

       
var zoomHandle = e.element();
       
var x = e.clientX;
       
var offset = zoomHandle.cumulativeOffset();
       
var prevSelection = this.graphs.summary.prevSelection;
       
       
/**
         * Perform zoom on handle move, observer
         *
         * @param e MouseEvent
         */

       
var handleObserver = function (e) {
           
           
Event.stopObserving(document, 'mousemove', handleObserver);
           
           
var deltaX = e.clientX - x;
           
var xAxis = this.graphs.summary.axes.x;
           
           
// Set initial new x bounds
           
var x1, x2;
           
if (Element.identify(zoomHandle) == 'rightHandle') {
                x1
= xAxis.p2d(Math.min(prevSelection.first.x, prevSelection.second.x));
                x2
= xAxis.p2d(Math.max(prevSelection.first.x, prevSelection.second.x) + deltaX);
           
} else if (Element.identify(zoomHandle) == 'leftHandle') {
                x1
= xAxis.p2d(Math.min(prevSelection.first.x, prevSelection.second.x) + deltaX);
                x2
= xAxis.p2d(Math.max(prevSelection.first.x, prevSelection.second.x));
           
}
           
           
// Check and handle boundary conditions
           
if (x1 < 0) {
                x1
= 0;
           
}
           
if (x2 > this.priceData.length) {
                x2
= this.priceData.length;
           
}
           
           
// Set selection area object
           
var area = {
                x1
: x1,
                y1
: prevSelection.first.y,
                x2
: x2,
                y2
: prevSelection.second.y
           
};
           
           
// If selection varies from previous, set new selection
           
if (area.x1 != prevSelection.first.x || area.x2 != prevSelection.second.x) {
               
this.graphs.summary.setSelection(area);
           
}
           
           
Event.observe(document, 'mousemove', handleObserver);
       
}.bind(this);

       
/**
         * End zoom observer to detach event listeners
         *
         * @param e MouseEvent
         */

       
function handleEndObserver (e) {
           
Event.stopObserving(document, 'mousemove', handleObserver);
           
Event.stopObserving(document, 'mouseup', handleEndObserver);
       
};
       
       
// Attach handler slide event listeners
       
Event.observe(document, 'mousemove', handleObserver);
       
Event.observe(document, 'mouseup', handleEndObserver);
   
},
   
   
/**
     * Perform a manual zoom.
     *
     * Zoom backwards from the right zoom handle.  If no selection or handles
     * are present, zoom backwards from the right hand side.
     *
     * @param integer x
     */

    zoom
: function (x) {
       
       
var prevSelection = this.graphs.summary.prevSelection;
       
var xAxis = this.graphs.summary.axes.x;
       
       
var x2 = xAxis.p2d(prevSelection.first.x) + Number(x);
       
       
var area = {
            x1
: xAxis.p2d(prevSelection.first.x),
            y1
: prevSelection.first.y,
            x2
: x2,
            y2
: prevSelection.second.y
       
};
       
       
this.graphs.summary.setSelection(area);
   
},
   
   
/**
     * Set the position of the zoom handles
     *
     * @param e MouseEvent
     */

    positionZoomHandles
: function (e) {
       
       
var x1 = e.memo[0].x1;
       
var x2 = e.memo[0].x2;
       
var xaxis = e.memo[1].axes.x;
       
var plotOffset = e.memo[1].plotOffset;
       
var height = this.containers.summary.getHeight();
       
var offset = this.containers.summary.positionedOffset();
       
this.handles.left.show();
       
var dimensions = this.handles.left.getDimensions();
       
       
// Set positions
       
var xPosOne = Math.floor(offset[0]+plotOffset.left+xaxis.d2p(x1)-dimensions.width/2+1);
       
var xPosTwo = Math.ceil(offset[0]+plotOffset.left+xaxis.d2p(x2)-dimensions.width/2);
       
var xPosLeft = Math.min(xPosOne, xPosTwo);
       
var xPosRight = Math.max(xPosOne, xPosTwo);
       
var yPos = Math.floor(offset[1]+height/2 - dimensions.height/2);
       
       
this.handles.left.setStyle({position: 'absolute', left: xPosLeft+'px', top: yPos+'px'});
       
this.handles.right.setStyle({position: 'absolute', left: xPosRight+'px', top: yPos+'px'});
       
this.handles.left.show();
       
this.handles.right.show();
   
},

   
/**
     * Clear point hits for price and volume graphs.
     *
     * @param e MouseEvent
     */

    clearHit
: function(e) {
       
this.graphs.price.clearHit();//.mouseTrack.hide();
       
this.graphs.volume.clearHit();
   
},
   
   
/**
     * Observer for volume hit to set price hit
     *
     * @param e MouseEvent
     */

    volumeHitObserver
: function (e) {
       
       
// Hide mouse track on volume graph
       
this.graphs.volume.mouseTrack.hide();
       
       
// Display hit on price graph
       
var point = this.priceData[e.memo[0].x];
       
Event.stopObserving(this.containers.volume, 'flotr:hit');
       
this.doHit(this.graphs.price, point, this.containers.volume);
       
Event.observe(this.containers.volume, 'flotr:hit', this.volumeHitObserver.bind(this));
   
},
   
   
/**
     * Observer for price hit to set volume hit
     *
     * @param e MouseEvent
     */

    priceHitObserver
: function (e) {
       
       
// Display hit on volume graph
       
var point = this.volumeData[e.memo[0].x];
       
Event.stopObserving(this.containers.price, 'flotr:hit');
       
this.doHit(this.graphs.volume, point, this.containers.price);
       
Event.observe(this.containers.price, 'flotr:hit', this.priceHitObserver.bind(this));
       
       
// Hide mouse track on volume graph
       
this.graphs.volume.mouseTrack.hide();
   
},
   
   
/**
     * Calculate hit location from one graph and perform hit on another
     *
     * @param Flotr.Graph graph  Destination graph.
     * @param Array point  The coordinates of the hit as [x, y].
     * @param Element container  The container of the source graph.
     */

    doHit
: function (graph, point, container) {
       
       
var offset = container.cumulativeOffset();
       
       
var xaxis = graph.axes.x;
       
var yaxis = graph.axes.y;
       
       
var relX = xaxis.d2p(point[0]);
       
var relY = yaxis.d2p(point[1]);
       
       
var absX = offset[0]+relX;
       
var absY = offset[1]+relY;
       
       
var mouse = {'relX': relX, 'relY': relY, 'absX': absX, 'absY': absY};
       
        graph
.hit(mouse);
   
},
   
   
/**
     * Draw the price graph
     *
     * @param Array data
     * @param Array bounds
     * @return Flotr.Graph
     */

    priceGraph
: function (data, bounds) {
       
       
var xmin = bounds.xmin;
       
var xmax = bounds.xmax;
       
var ymin = bounds.ymin;
       
var ymax = bounds.ymax;
       
       
var p = Flotr.draw(
            $
('priceGraph'),
           
[data],
           
{
                lines
: {show: true, fill: true, fillOpacity: .1, lineWidth: 1},
                yaxis
: {min: ymin, max: ymax, tickFormatter: this.yTickFormatter, noTicks: 3, autoscaleMargin: .5,  tickDecimals: 0},
                xaxis
: {min: xmin, max: xmax, showLabels: false},
                grid
: {outlineWidth: 0, labelMargin: 0},
                mouse
: {track: true, sensibility: 1, trackDecimals: 4, trackFormatter: this.trackFormatter, position: 'ne'},
                shadowSize
: false,
               
HtmlText: true
           
}
       
);
       
       
return p;
   
},

   
/**
     * Draw the volume graph
     *
     * @param Array data
     * @param Array bounds
     * @return Flotr.Graph
     */

    volumeGraph
: function (data, bounds) {
       
       
var xmin = bounds.xmin;
       
var xmax = bounds.xmax;
       
var ymin = bounds.ymin;
       
var ymax = bounds.ymax;

       
var v = Flotr.draw(
            $
('volumeGraph'),
           
[data],
           
{
                bars
: {show: true, 'barWidth': .5, 'fill': true, 'lineWidth': 2, 'fillOpacity': 1},
                yaxis
: {min: ymin, max: ymax, autoscaleMargin: .5, showLabels: false, tickDecimals: 0},
                xaxis
: {min: xmin, max: xmax, showLabels: false, labelsAngle: 60},
                grid
: {verticalLines: false, horizontalLines: false, outlineWidth: 0, labelMargin: 0},
                mouse
: {track: true, sensibility: .3, position: 'ne', trackDecimals: 0},
                shadowSize
: false,
               
HtmlText: true
           
}
       
);
       
       
return v;
   
},

   
/**
     * Draw the summary graph
     *
     * @param Array data
     * @param Array bounds
     * @return Flotr.Graph
     */

    summaryGraph
: function (data, bounds) {
       
       
var xmin = bounds.xmin;
       
var xmax = bounds.xmax;
       
var ymin = bounds.ymin;
       
var ymax = bounds.ymax;
       
       
var p = Flotr.draw(
            $
('summaryGraph'),
           
[data],
           
{
                lines
: {show: true, fill: true, fillOpacity: .1, lineWidth: 1},
                yaxis
: {min: ymin, max: ymax, autoscaleMargin: .5, showLabels: false, tickDecimals: 1},
                xaxis
: {min: xmin, max: xmax, noTicks: 5, tickFormatter: this.xTickFormatter, labelsAngle: 60},
                grid
: {verticalLines: false, horizontalLines: false, labelMargin: 0, outlineWidth: 0},
                selection
: {mode: 'x'},
                shadowSize
: false,
               
HtmlText: true
           
}
       
);
       
       
return p;
   
}
}