[ckan-changes] commit/ckanext-datapreview: 43 new changesets

Bitbucket commits-noreply at bitbucket.org
Thu Jul 28 14:46:54 UTC 2011


43 new changesets in ckanext-datapreview:

http://bitbucket.org/okfn/ckanext-datapreview/changeset/8c497a149d3d/
changeset:   8c497a149d3d
user:        John Glover
date:        2011-07-13 13:34:21
summary:     Change datapreview to use a webstore rather than the dataproxy API
affected #:  2 files (480 bytes)

--- a/ckanext/datapreview/__init__.py	Tue Mar 29 12:07:10 2011 +0100
+++ b/ckanext/datapreview/__init__.py	Wed Jul 13 12:34:21 2011 +0100
@@ -8,9 +8,9 @@
 
 Optional configuration options::
 
-    ## a data proxy URL
-    ## Default proxy URL is http://jsonpdataproxy.appspot.com
-    ckanext.dataapi.data_proxy_url = http://...
+    ## a webstore URL
+    ## Default webstore URL is http://127.0.0.1:8080/ofkn
+    ckanext.dataapi.webstore_url = http://...
     
 This extension provides preview functionality (using javascript) in relation to
 Packge Resources (i.e. urls pointing to datasets).
@@ -25,29 +25,21 @@
     __path__ = pkgutil.extend_path(__path__, __name__)
 
 import os
-import urllib
-import urlparse
 from logging import getLogger
-
-from pylons import config
-from pylons.decorators import jsonify
 from genshi.input import HTML
 from genshi.filters import Transformer
-
-import ckan.model as model
 from ckan.plugins import implements, SingletonPlugin, IConfigurable
-from ckan.plugins import IGenshiStreamFilter, IRoutes, IConfigurer
-from ckan.lib.base import BaseController, c, g, request, response, session
+from ckan.plugins import IGenshiStreamFilter, IConfigurer
+from ckan.lib.base import c, request
 
 log = getLogger(__name__)
 
-default_data_proxy_url = 'http://jsonpdataproxy.appspot.com'
+default_webstore_url = 'http://127.0.0.1:8080/okfn'
 
 
 class DataPreviewPlugin(SingletonPlugin):
     """Insert javascript fragments into package pages to allow users to preview
     a package resource."""
-    
     implements(IConfigurable)
     implements(IGenshiStreamFilter)
     implements(IConfigurer, inherit=True)
@@ -56,15 +48,17 @@
         """Called upon CKAN setup, will pass current configuration dict to the
         plugin to read custom options.
         """
-        self.data_proxy_url = config.get('ckanext.dataapi.data_proxy_url',
-                default_data_proxy_url)
+        self.webstore_url = config.get(
+            'ckanext.dataapi.webstore_url',
+            default_webstore_url
+        )
 
     def update_config(self, config):
         here = os.path.dirname(__file__)
         rootdir = os.path.dirname(os.path.dirname(here))
         our_public_dir = os.path.join(rootdir, 'public')
-        config['extra_public_paths'] = ','.join([our_public_dir,
-                config.get('extra_public_paths', '')])
+        config['extra_public_paths'] = \
+            ','.join([our_public_dir, config.get('extra_public_paths', '')])
 
     BOTTOM_CODE = """
 <div id="ckanext-datapreview-dialog"></div>
@@ -72,9 +66,9 @@
 <script type="text/javascript" src="/ckanext/datapreview/data-preview.js"></script><script type="text/javascript">
   jQuery('document').ready(function($) {
-    var dataproxyUrl = '%(data_proxy_url)s';
-    var dataproxyDialogId = 'ckanext-datapreview-dialog';
-    CKANEXT.DATAPREVIEW.initialize(dataproxyUrl, dataproxyDialogId);
+    var webstoreUrl = '%(webstore_url)s';
+    var dialogId = 'ckanext-datapreview-dialog';
+    CKANEXT.DATAPREVIEW.initialize(webstoreUrl, dialogId);
   });
 </script>
 """
@@ -100,19 +94,14 @@
         """Required to implement IGenshiStreamFilter; will apply some HTML
         transformations to the page currently rendered.
         """
-        
-        from pylons import request, tmpl_context as c 
         routes = request.environ.get('pylons.routes_dict')
         
         if routes.get('controller') == 'package' and \
             routes.get('action') == 'read' and c.pkg.id:
-            data = {
-                'data_proxy_url': self.data_proxy_url,
-                }
+            data = {'webstore_url': self.webstore_url}
             stream = stream | Transformer('body')\
                 .append(HTML(self.BOTTOM_CODE % data))
             stream = stream | Transformer('head')\
                 .append(HTML(self.HEAD_CODE))
         
         return stream
-


--- a/public/ckanext/datapreview/data-preview.js	Tue Mar 29 12:07:10 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Wed Jul 13 12:34:21 2011 +0100
@@ -3,7 +3,7 @@
 CKANEXT.DATAPREVIEW = (function($, my) {
   // set up in document.ready
   my.$dialog = null;
-  my.dataproxy = null;
+  my.webstore = null;
 
   my.normalizeFormat = function(format) {
     var out = format.toLowerCase();
@@ -21,9 +21,8 @@
   }
 
   my.getResourceDataDirect = function(url, type, callback) {
-    var apiurl = my.dataproxy + '?url=' + url + '&type=' + type + '&max-results=30';
     $.ajax({
-      url: apiurl,
+      url: url,
       dataType: 'jsonp',
       success: function(data) {
         callback(url, type, data)
@@ -44,10 +43,11 @@
     my.$dialog.empty();
     my.$dialog.dialog('option', 'title', 'Preview: ' + url);
     if(data.error) {
-      _html = '<h3>Unable to Preview - Had an error from dataproxy:</h2>'
+      _html = '<h3>Unable to Preview - Had an error from webstore:</h2>'
       _html += '<p></p><p><strong>' + data.error.title + '\n(' + data.error.message + '<strong></p>';
       my.$dialog.html(_html);
-    } else {
+    } 
+    else {
       var content = $('<table></table>');
       content.html('');
       if (data.fields) {
@@ -72,10 +72,11 @@
     my.$dialog.empty();
     my.$dialog.dialog('option', 'title', 'Preview: ' + url);
     if(data.error) {
-      _html = '<h3>Unable to Preview - Had an error from dataproxy:</h2>'
+      _html = '<h3>Unable to Preview - Had an error from webstore:</h2>'
       _html += '<p></p><p><strong>' + data.error.title + '\n(' + data.error.message + '<strong></p>';
       my.$dialog.html(_html);
-    } else {
+    } 
+    else {
       var content = $('<pre></pre>');
       for (var i=0; i<data.data.length; i++) { 
         var row = data.data[i].join(',') + '\n';
@@ -103,7 +104,9 @@
     var _type = $(previewLinkClick.target).attr('format');
     _type = my.normalizeFormat(_type);
 
-    my.$dialog.empty().html('<h2>Loading preview ...</h2><img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" />');
+    var _html = '<h2>Loading preview ...</h2>' +
+        '<img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" />';
+    my.$dialog.empty().html(_html);
     my.$dialog.dialog('open');
 
     if (_type === '') {
@@ -119,7 +122,8 @@
 
     if (_type in {'csv': '', 'xls': ''}) {
       my.getResourceDataDirect(_url, _type, my.showData);
-    } else if (_type in {
+    } 
+    else if (_type in {
         'rdf+xml': '',
         'owl+xml': '',
         'xml': '',
@@ -131,8 +135,7 @@
         'tsv': '',
         'rss': '',
         'txt': ''
-        })
-          {
+        }) {
       // treat as plain text
       my.getResourceDataDirect(_url, 'csv', my.showPlainTextData);
     }
@@ -173,7 +176,14 @@
         return;
       }
 
-      var _url = element.find('a').first().attr('href');
+      // can not preview if hash value doesn't exist
+      var _hash = element.next().next().first().text().trim();
+      if (_hash === '') {
+          return;
+      }
+
+      // TODO: add ability to change the limit in this url
+      var _url = my.webstore + "/" + _hash + "/resource.jsonp?_limit=30";
       var _previewSpan =  '<a class="resource-preview-button" href="HREF" format="FORMAT">Preview</a>';
       _previewSpan = _previewSpan.replace('HREF', _url);
       _previewSpan = _previewSpan.replace('FORMAT', _format);
@@ -181,14 +191,14 @@
       _previewSpan.click(function(e) {
         e.preventDefault();
         my.showPreviewDialog(e);
-        });
+      });
       element.append(_previewSpan);
     });
   };
 
-  my.initialize = function(dataproxyUrl, dataproxyDialogId) {
-    my.dataproxy = dataproxyUrl;
-    my.$dialog = $('#' + dataproxyDialogId);
+  my.initialize = function(webstoreUrl, dialogId) {
+    my.webstore = webstoreUrl;
+    my.$dialog = $('#' + dialogId);
     var _height = Math.round($(window).height() * 0.6);
     my.$dialog.dialog({
       autoOpen: false


http://bitbucket.org/okfn/ckanext-datapreview/changeset/6bd567900578/
changeset:   6bd567900578
user:        aron_
date:        2011-07-12 19:58:06
summary:     Added timeout to JSONP requests to check for failed response

jQuery.ajax() does not call the "error" callback for "script"
and "jsonp" data types. This caused the lightbox to appear to
hang when the dataproxy failed to response. So we instead set
a timeout to wait x seconds and provide an error if the
"success" callback has not been called in that time.

The timeout can be customised by providing an object literal
with a "timeout" key as the third argument to .initialise().

    CKANEXT.DATAPREVIEW.initialize(url, el, {timeout: 30000});

This defaults to 5 seconds if no timeout is provided.
affected #:  1 file (320 bytes)

--- a/public/ckanext/datapreview/data-preview.js	Tue Mar 29 12:07:10 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Tue Jul 12 18:58:06 2011 +0100
@@ -4,6 +4,7 @@
   // set up in document.ready
   my.$dialog = null;
   my.dataproxy = null;
+  my.timeout = 5000;
 
   my.normalizeFormat = function(format) {
     var out = format.toLowerCase();
@@ -22,20 +23,24 @@
 
   my.getResourceDataDirect = function(url, type, callback) {
     var apiurl = my.dataproxy + '?url=' + url + '&type=' + type + '&max-results=30';
+
+    // $.ajax() does not call the "error" callback for JSONP requests so we
+    // set a timeout to provide the callback with an error after x seconds.
+    var timer = setTimeout(function error() {
+      callback(url, type, {
+        error: {
+          title: 'Request Error',
+          message: 'Dataproxy server did not respond after ' + (my.timeout / 1000) + ' seconds'
+        }
+      });
+    }, my.timeout);
+
     $.ajax({
       url: apiurl,
       dataType: 'jsonp',
       success: function(data) {
-        callback(url, type, data)
-      },
-      error: function(request, _status, error) {
-        // fake data
-        var _data = {}
-        data.error = {
-            title: _status,
-            message: _error
-        };
-        callback(url, type, _data);
+        clearTimeout(timer);
+        callback(url, type, data);
       }
     });
   };
@@ -52,14 +57,14 @@
       content.html('');
       if (data.fields) {
         var row = $('<tr></tr>');
-        for (var j=0; j<data.fields.length; j++) { 
+        for (var j=0; j<data.fields.length; j++) {
           row.append($('<td></td>').html(data.fields[j]));
         }
         content.append(row);
       }
-      for (var i=0; i<data.data.length; i++) { 
+      for (var i=0; i<data.data.length; i++) {
         var row = $('<tr></tr>');
-        for (var j=0; j<data.data[i].length; j++) { 
+        for (var j=0; j<data.data[i].length; j++) {
           row.append($('<td></td>').html(data.data[i][j]));
         }
         content.append(row);
@@ -77,7 +82,7 @@
       my.$dialog.html(_html);
     } else {
       var content = $('<pre></pre>');
-      for (var i=0; i<data.data.length; i++) { 
+      for (var i=0; i<data.data.length; i++) {
         var row = data.data[i].join(',') + '\n';
         row = row.replace('<', '<');
         row = row.replace('>', '>');
@@ -154,7 +159,7 @@
       var _tformat = _format.toLowerCase();
       if (
         _tformat.indexOf('zip') != -1
-        || 
+        ||
         _tformat.indexOf('tgz') != -1
         ||
         _tformat.indexOf('targz') != -1
@@ -186,7 +191,10 @@
     });
   };
 
-  my.initialize = function(dataproxyUrl, dataproxyDialogId) {
+  my.initialize = function(dataproxyUrl, dataproxyDialogId, options) {
+    options = options || {};
+
+    my.timeout = options.timeout || my.timeout;
     my.dataproxy = dataproxyUrl;
     my.$dialog = $('#' + dataproxyDialogId);
     var _height = Math.round($(window).height() * 0.6);


http://bitbucket.org/okfn/ckanext-datapreview/changeset/9745457516c5/
changeset:   9745457516c5
user:        aron_
date:        2011-07-13 12:44:43
summary:     Improved formatting of error messages

Moved display of error messages into a separate showError()
method. Fixed broken closing tags and added missing closing
parentheses.
affected #:  1 file (94 bytes)

--- a/public/ckanext/datapreview/data-preview.js	Tue Jul 12 18:58:06 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Wed Jul 13 11:44:43 2011 +0100
@@ -45,13 +45,17 @@
     });
   };
 
+  my.showError = function (error) {
+    var _html = '<h3>Unable to Preview - Had an error from dataproxy:</h3>';
+    _html += '<p></p><p><strong>' + $.trim(error.title) + '</strong> (' + $.trim(error.message) + ')</p>';
+    my.$dialog.html(_html);
+  };
+
   my.showData = function(url, type, data) {
     my.$dialog.empty();
     my.$dialog.dialog('option', 'title', 'Preview: ' + url);
     if(data.error) {
-      _html = '<h3>Unable to Preview - Had an error from dataproxy:</h2>'
-      _html += '<p></p><p><strong>' + data.error.title + '\n(' + data.error.message + '<strong></p>';
-      my.$dialog.html(_html);
+      my.showError(data.error);
     } else {
       var content = $('<table></table>');
       content.html('');
@@ -77,9 +81,7 @@
     my.$dialog.empty();
     my.$dialog.dialog('option', 'title', 'Preview: ' + url);
     if(data.error) {
-      _html = '<h3>Unable to Preview - Had an error from dataproxy:</h2>'
-      _html += '<p></p><p><strong>' + data.error.title + '\n(' + data.error.message + '<strong></p>';
-      my.$dialog.html(_html);
+      my.showError(data.error);
     } else {
       var content = $('<pre></pre>');
       for (var i=0; i<data.data.length; i++) {


http://bitbucket.org/okfn/ckanext-datapreview/changeset/83ab30f975a4/
changeset:   83ab30f975a4
user:        aron_
date:        2011-07-13 16:06:12
summary:     Restyled the "preview" buttons

Now displaying the buttons in a new column to ensure consistent
formatting. Added a stylesheet that brings the button style inline
with the view/edit tabs and added an magnifying glass icon.
affected #:  5 files (4.4 KB)

--- a/ckanext/datapreview/__init__.py	Wed Jul 13 11:44:43 2011 +0100
+++ b/ckanext/datapreview/__init__.py	Wed Jul 13 15:06:12 2011 +0100
@@ -83,17 +83,7 @@
 <link rel="stylesheet"
     href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/ui-lightness/jquery-ui.css"
     type="text/css" media="screen, print" />
-<style type="text/css">
-a.resource-preview-button {
-  float: right;
-  padding-right: 2px;
-  padding-left: 2px;
-  border-top: #d7d7d7 1px solid;
-  border-left: #d7d7d7 1px solid;
-  border-bottom: #666 1px solid;
-  border-right: #666 1px solid;
-}
-</style>
+<link rel="stylesheet" href="/ckanext/datapreview/data-preview.css" />
 '''
         
     def filter(self, stream):


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/public/ckanext/datapreview/data-preview.css	Wed Jul 13 15:06:12 2011 +0100
@@ -0,0 +1,37 @@
+/* @override http://127.0.0.1:5000/ckanext/datapreview/data-preview.css */
+
+a.resource-preview-button {
+	border: 1px solid #eaeaea;
+	color: #444;
+	padding: 3px 6px 2px 21px;
+	font-size: 12px;
+	font-weight: bold;
+	background: #fff url(./icon-preview.png) no-repeat 5px 6px;
+	-webkit-border-radius: 4px;
+	-moz-border-radius: 4px;
+	-ms-border-radius: 4px;
+	-o-border-radius: 4px;
+	border-radius: 4px;
+	display: block;
+	margin: 0 -5px;
+}
+
+a.resource-preview-button:hover {
+	color: #000;
+	text-decoration: none;
+	border-color: #aaa;
+	background-color: #fafafa;
+	background-position: 5px -25px;
+}
+
+a.resource-preview-button:active {
+	color: #b22;
+	border-color: #b22;
+	background-position: 5px -54px;
+}
+
+a.resource-preview-loading {
+	color: #aaa;
+	background-image: url(./loading.gif);
+	background-position: 3px 5px
+}


--- a/public/ckanext/datapreview/data-preview.js	Wed Jul 13 11:44:43 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Wed Jul 13 15:06:12 2011 +0100
@@ -150,6 +150,7 @@
   };
 
   my.createPreviewButtons = function(resources) {
+    $(resources).find('tr:first th:first').before($('<th>Preview</th>'));
     /*
        :param resources: resource section div or table.
      */
@@ -157,6 +158,8 @@
       var element = $(element);
       var _format = element.next().first().text().trim();
 
+      var preview = $('<td class="preview"></td>').prependTo(element.parent());
+
       // do not create previews for some items
       var _tformat = _format.toLowerCase();
       if (
@@ -181,15 +184,15 @@
       }
 
       var _url = element.find('a').first().attr('href');
-      var _previewSpan =  '<a class="resource-preview-button" href="HREF" format="FORMAT">Preview</a>';
+      var _previewSpan = '<a class="resource-preview-button" href="HREF" format="FORMAT">Preview</a>';
       _previewSpan = _previewSpan.replace('HREF', _url);
       _previewSpan = _previewSpan.replace('FORMAT', _format);
       var _previewSpan = $(_previewSpan);
       _previewSpan.click(function(e) {
         e.preventDefault();
         my.showPreviewDialog(e);
-        });
-      element.append(_previewSpan);
+      });
+      preview.append(_previewSpan);
     });
   };
 


Binary file public/ckanext/datapreview/icon-preview.png has changed


Binary file public/ckanext/datapreview/loading.gif has changed


http://bitbucket.org/okfn/ckanext-datapreview/changeset/c322fd3867dd/
changeset:   c322fd3867dd
user:        aron_
date:        2011-07-13 16:58:45
summary:     Revised the loading of the data preview lightbox

Now when a button is clicked it displays a loading indicator until
a response is recieved from the server. If successful the standard
lightbox is displayed otherwise a modal dialog with an error message
is used instead.
affected #:  2 files (762 bytes)

--- a/public/ckanext/datapreview/data-preview.css	Wed Jul 13 15:06:12 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.css	Wed Jul 13 15:58:45 2011 +0100
@@ -30,8 +30,17 @@
 	background-position: 5px -54px;
 }
 
-a.resource-preview-loading {
+a.resource-preview-loading,
+a.resource-preview-loading:hover,
+a.resource-preview-loading:active {
 	color: #aaa;
+	border: 1px solid #eaeaea;
 	background-image: url(./loading.gif);
-	background-position: 3px 5px
+	background-position: 3px 5px;
+	cursor: default;
 }
+
+.ui-button-text-only .ui-button-text {
+	padding: 3px 16px 1px;
+}
+


--- a/public/ckanext/datapreview/data-preview.js	Wed Jul 13 15:06:12 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Wed Jul 13 15:58:45 2011 +0100
@@ -46,9 +46,8 @@
   };
 
   my.showError = function (error) {
-    var _html = '<h3>Unable to Preview - Had an error from dataproxy:</h3>';
-    _html += '<p></p><p><strong>' + $.trim(error.title) + '</strong> (' + $.trim(error.message) + ')</p>';
-    my.$dialog.html(_html);
+    var _html = '<strong>' + $.trim(error.title) + '</strong><br />' + $.trim(error.message);
+    my.$dialog.html(_html).dialog(my.errorDialogOptions);
   };
 
   my.showData = function(url, type, data) {
@@ -73,7 +72,7 @@
         }
         content.append(row);
       }
-      my.$dialog.append(content);
+      my.$dialog.dialog(my.dialogOptions).append(content);
     }
   };
 
@@ -90,7 +89,7 @@
         row = row.replace('>', '>');
         content.append(row);
       }
-      my.$dialog.append(content);
+      my.$dialog.dialog(my.dialogOptions).append(content);
     }
   };
 
@@ -101,17 +100,24 @@
     el.attr('src', url);
     el.attr('width', '100%');
     el.attr('height', '100%');
-    my.$dialog.append(el);
+    my.$dialog.append(el).dialog('open');;
   };
 
-  my.showPreviewDialog = function(previewLinkClick) {
-    var _url = previewLinkClick.target.href;
+  my.loadPreviewDialog = function(link) {
+    var _url = link.href;
     _url = my.normalizeUrl(_url);
-    var _type = $(previewLinkClick.target).attr('format');
+    var _type = $(link).attr('format');
     _type = my.normalizeFormat(_type);
 
-    my.$dialog.empty().html('<h2>Loading preview ...</h2><img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" />');
-    my.$dialog.dialog('open');
+    function callbackWrapper(callback) {
+      return function () {
+        $(link).removeClass('resource-preview-loading').text('Preview');
+        callback.apply(this, arguments);
+        my.$dialog.dialog('open');
+      };
+    }
+
+    $(link).addClass('resource-preview-loading').text('Loading');
 
     if (_type === '') {
       var tmp = _url.split('/');
@@ -125,7 +131,7 @@
     }
 
     if (_type in {'csv': '', 'xls': ''}) {
-      my.getResourceDataDirect(_url, _type, my.showData);
+      my.getResourceDataDirect(_url, _type, callbackWrapper(my.showData));
     } else if (_type in {
         'rdf+xml': '',
         'owl+xml': '',
@@ -141,11 +147,11 @@
         })
           {
       // treat as plain text
-      my.getResourceDataDirect(_url, 'csv', my.showPlainTextData);
+      my.getResourceDataDirect(_url, 'csv', callbackWrapper(my.showPlainTextData));
     }
     else {
       // very hacky but should work
-      my.showHtml(_url);
+      callbackWrapper(my.showHtml)(_url);
     }
   };
 
@@ -184,15 +190,16 @@
       }
 
       var _url = element.find('a').first().attr('href');
-      var _previewSpan = '<a class="resource-preview-button" href="HREF" format="FORMAT">Preview</a>';
-      _previewSpan = _previewSpan.replace('HREF', _url);
-      _previewSpan = _previewSpan.replace('FORMAT', _format);
-      var _previewSpan = $(_previewSpan);
-      _previewSpan.click(function(e) {
-        e.preventDefault();
-        my.showPreviewDialog(e);
-      });
-      preview.append(_previewSpan);
+      var _previewSpan = $('<a />', {
+        text: 'Preview',
+        href: _url,
+        format: _format,
+        className: 'resource-preview-button',
+        click: function(e) {
+          e.preventDefault();
+          my.loadPreviewDialog(e.target);
+        }
+      }).appendTo(preview);
     });
   };
 
@@ -203,14 +210,35 @@
     my.dataproxy = dataproxyUrl;
     my.$dialog = $('#' + dataproxyDialogId);
     var _height = Math.round($(window).height() * 0.6);
-    my.$dialog.dialog({
+
+    // Large stylable dialog for displaying data.
+    my.dialogOptions = {
       autoOpen: false
       // does not seem to work for width ...
       , position: ['center', 'center']
+      , buttons: []
       , width: '60%'
       , height: _height
       , resize: 'auto'
-      });
+      , modal: false
+      , draggable: true
+      , resizable: true
+    };
+
+    // Smaller alert style dialog for error messages.
+    my.errorDialogOptions = {
+      title: 'Unable to Preview - Had an error from dataproxy'
+      , position: ['center', 'center']
+      , buttons: [{
+          text: "OK"
+      ,   click: function () { $(this).dialog("close"); }
+      , }]
+      , width: 360
+      , height: 180
+      , resizable: false
+      , draggable: false
+      , modal: true
+    };
     my.createPreviewButtons($('.resources'));
   };
 


http://bitbucket.org/okfn/ckanext-datapreview/changeset/bbd569eb4eea/
changeset:   bbd569eb4eea
user:        aron_
date:        2011-07-13 18:00:58
summary:     Now escaping HTML characters before insertion into the lightbox

This prevents the possibility of XSS attacks or broken layout
due to HTML in field content. Also building the table in one
large string before inserting into the DOM this should be much
quicker to load large datasets in older browsers.
affected #:  2 files (732 bytes)

--- a/public/ckanext/datapreview/data-preview.css	Wed Jul 13 15:58:45 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.css	Wed Jul 13 17:00:58 2011 +0100
@@ -1,5 +1,19 @@
 /* @override http://127.0.0.1:5000/ckanext/datapreview/data-preview.css */
 
+#ckanext-datapreview-dialog table td {
+	background: #fff;
+	border: 1px solid #eee;
+	padding: 6px;
+	color: #555;
+}
+
+#ckanext-datapreview-dialog table thead td {
+	background: #444;
+	color: #fff;
+	border-color: #444;
+	border-bottom: none;
+}
+
 a.resource-preview-button {
 	border: 1px solid #eaeaea;
 	color: #444;


--- a/public/ckanext/datapreview/data-preview.js	Wed Jul 13 15:58:45 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Wed Jul 13 17:00:58 2011 +0100
@@ -21,6 +21,14 @@
     }
   }
 
+  my.escapeHTML = function (string) {
+    return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&')
+                 .replace(/</g, '<').replace(/>/g, '>')
+                 .replace(/"/g, '"')
+                 .replace(/'/g, '&#x27')
+                 .replace(/\//g,'&#x2F;');
+  };
+
   my.getResourceDataDirect = function(url, type, callback) {
     var apiurl = my.dataproxy + '?url=' + url + '&type=' + type + '&max-results=30';
 
@@ -53,26 +61,30 @@
   my.showData = function(url, type, data) {
     my.$dialog.empty();
     my.$dialog.dialog('option', 'title', 'Preview: ' + url);
+
+    // Escapes HTML content in fields and returns table row string.
+    function buildRow(fields) {
+      var row = ['<tr>'];
+      for (var i = 0, c = fields.length; i < c; i++) {
+        row.push('<td>', my.escapeHTML(fields[i]), '</td>');
+      }
+      row.push('</tr>');
+      return row.join('');
+    }
+
     if(data.error) {
       my.showError(data.error);
     } else {
-      var content = $('<table></table>');
-      content.html('');
+      // Use an array to build an HTML string and inject it into the dom at the
+      // end as it's much faster on older browsers.
+      var content = ['<table>'];
       if (data.fields) {
-        var row = $('<tr></tr>');
-        for (var j=0; j<data.fields.length; j++) {
-          row.append($('<td></td>').html(data.fields[j]));
-        }
-        content.append(row);
+        content.push('<thead>', buildRow(data.fields), '</thead>');
       }
-      for (var i=0; i<data.data.length; i++) {
-        var row = $('<tr></tr>');
-        for (var j=0; j<data.data[i].length; j++) {
-          row.append($('<td></td>').html(data.data[i][j]));
-        }
-        content.append(row);
-      }
-      my.$dialog.dialog(my.dialogOptions).append(content);
+      content.push('<tbody>');
+      content.push.apply(content, $.map(data.data, buildRow));
+      content.push('</tbody></table>');
+      my.$dialog.dialog('option', my.dialogOptions).append(content.join(''));
     }
   };
 
@@ -85,11 +97,9 @@
       var content = $('<pre></pre>');
       for (var i=0; i<data.data.length; i++) {
         var row = data.data[i].join(',') + '\n';
-        row = row.replace('<', '<');
-        row = row.replace('>', '>');
-        content.append(row);
+        content.append(my.escapeHTML(row));
       }
-      my.$dialog.dialog(my.dialogOptions).append(content);
+      my.$dialog.dialog('option', my.dialogOptions).append(content);
     }
   };
 
@@ -239,6 +249,7 @@
       , draggable: false
       , modal: true
     };
+    my.$dialog.dialog(my.dialogOptions);
     my.createPreviewButtons($('.resources'));
   };
 


http://bitbucket.org/okfn/ckanext-datapreview/changeset/863405d8756d/
changeset:   863405d8756d
user:        John Glover
date:        2011-07-13 18:37:47
summary:     Merge in Aron's changes
affected #:  5 files (6.0 KB)

--- a/ckanext/datapreview/__init__.py	Wed Jul 13 12:34:21 2011 +0100
+++ b/ckanext/datapreview/__init__.py	Wed Jul 13 17:37:47 2011 +0100
@@ -77,17 +77,7 @@
 <link rel="stylesheet"
     href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/ui-lightness/jquery-ui.css"
     type="text/css" media="screen, print" />
-<style type="text/css">
-a.resource-preview-button {
-  float: right;
-  padding-right: 2px;
-  padding-left: 2px;
-  border-top: #d7d7d7 1px solid;
-  border-left: #d7d7d7 1px solid;
-  border-bottom: #666 1px solid;
-  border-right: #666 1px solid;
-}
-</style>
+<link rel="stylesheet" href="/ckanext/datapreview/data-preview.css" />
 '''
         
     def filter(self, stream):


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/public/ckanext/datapreview/data-preview.css	Wed Jul 13 17:37:47 2011 +0100
@@ -0,0 +1,60 @@
+/* @override http://127.0.0.1:5000/ckanext/datapreview/data-preview.css */
+
+#ckanext-datapreview-dialog table td {
+	background: #fff;
+	border: 1px solid #eee;
+	padding: 6px;
+	color: #555;
+}
+
+#ckanext-datapreview-dialog table thead td {
+	background: #444;
+	color: #fff;
+	border-color: #444;
+	border-bottom: none;
+}
+
+a.resource-preview-button {
+	border: 1px solid #eaeaea;
+	color: #444;
+	padding: 3px 6px 2px 21px;
+	font-size: 12px;
+	font-weight: bold;
+	background: #fff url(./icon-preview.png) no-repeat 5px 6px;
+	-webkit-border-radius: 4px;
+	-moz-border-radius: 4px;
+	-ms-border-radius: 4px;
+	-o-border-radius: 4px;
+	border-radius: 4px;
+	display: block;
+	margin: 0 -5px;
+}
+
+a.resource-preview-button:hover {
+	color: #000;
+	text-decoration: none;
+	border-color: #aaa;
+	background-color: #fafafa;
+	background-position: 5px -25px;
+}
+
+a.resource-preview-button:active {
+	color: #b22;
+	border-color: #b22;
+	background-position: 5px -54px;
+}
+
+a.resource-preview-loading,
+a.resource-preview-loading:hover,
+a.resource-preview-loading:active {
+	color: #aaa;
+	border: 1px solid #eaeaea;
+	background-image: url(./loading.gif);
+	background-position: 3px 5px;
+	cursor: default;
+}
+
+.ui-button-text-only .ui-button-text {
+	padding: 3px 16px 1px;
+}
+


--- a/public/ckanext/datapreview/data-preview.js	Wed Jul 13 12:34:21 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Wed Jul 13 17:37:47 2011 +0100
@@ -4,6 +4,7 @@
   // set up in document.ready
   my.$dialog = null;
   my.webstore = null;
+  my.timeout = 5000;
 
   my.normalizeFormat = function(format) {
     var out = format.toLowerCase();
@@ -20,51 +21,68 @@
     }
   }
 
+  my.escapeHTML = function (string) {
+    return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&')
+                 .replace(/</g, '<').replace(/>/g, '>')
+                 .replace(/"/g, '"')
+                 .replace(/'/g, '&#x27')
+                 .replace(/\//g,'&#x2F;');
+  };
+
   my.getResourceDataDirect = function(url, type, callback) {
+    // $.ajax() does not call the "error" callback for JSONP requests so we
+    // set a timeout to provide the callback with an error after x seconds.
+    var timer = setTimeout(function error() {
+      callback(url, type, {
+        error: {
+          title: 'Request Error',
+          message: 'Dataproxy server did not respond after ' + (my.timeout / 1000) + ' seconds'
+        }
+      });
+    }, my.timeout);
+
     $.ajax({
       url: url,
       dataType: 'jsonp',
       success: function(data) {
-        callback(url, type, data)
-      },
-      error: function(request, _status, error) {
-        // fake data
-        var _data = {}
-        data.error = {
-            title: _status,
-            message: _error
-        };
-        callback(url, type, _data);
+        clearTimeout(timer);
+        callback(url, type, data);
       }
     });
   };
 
+  my.showError = function (error) {
+    var _html = '<strong>' + $.trim(error.title) + '</strong><br />' + $.trim(error.message);
+    my.$dialog.html(_html).dialog(my.errorDialogOptions);
+  };
+
   my.showData = function(url, type, data) {
     my.$dialog.empty();
     my.$dialog.dialog('option', 'title', 'Preview: ' + url);
+
+    // Escapes HTML content in fields and returns table row string.
+    function buildRow(fields) {
+      var row = ['<tr>'];
+      for (var i = 0, c = fields.length; i < c; i++) {
+        row.push('<td>', my.escapeHTML(fields[i]), '</td>');
+      }
+      row.push('</tr>');
+      return row.join('');
+    }
+
     if(data.error) {
-      _html = '<h3>Unable to Preview - Had an error from webstore:</h2>'
-      _html += '<p></p><p><strong>' + data.error.title + '\n(' + data.error.message + '<strong></p>';
-      my.$dialog.html(_html);
-    } 
-    else {
-      var content = $('<table></table>');
-      content.html('');
+      my.showError(data.error);
+    } else {
+      // Use an array to build an HTML string and inject it into the dom at the
+      // end as it's much faster on older browsers.
+      var content = ['<table>'];
       if (data.fields) {
-        var row = $('<tr></tr>');
-        for (var j=0; j<data.fields.length; j++) { 
-          row.append($('<td></td>').html(data.fields[j]));
-        }
-        content.append(row);
+        content.push('<thead>', buildRow(data.fields), '</thead>');
       }
-      for (var i=0; i<data.data.length; i++) { 
-        var row = $('<tr></tr>');
-        for (var j=0; j<data.data[i].length; j++) { 
-          row.append($('<td></td>').html(data.data[i][j]));
-        }
-        content.append(row);
-      }
-      my.$dialog.append(content);
+      content.push('<tbody>');
+      content.push.apply(content, $.map(data.data, buildRow));
+      content.push('</tbody></table>');
+      my.$dialog.dialog('option', my.dialogOptions).append(content.join(''));
     }
   };
 
@@ -72,19 +90,14 @@
     my.$dialog.empty();
     my.$dialog.dialog('option', 'title', 'Preview: ' + url);
     if(data.error) {
-      _html = '<h3>Unable to Preview - Had an error from webstore:</h2>'
-      _html += '<p></p><p><strong>' + data.error.title + '\n(' + data.error.message + '<strong></p>';
-      my.$dialog.html(_html);
-    } 
-    else {
+      my.showError(data.error);
+    } else {
       var content = $('<pre></pre>');
-      for (var i=0; i<data.data.length; i++) { 
+      for (var i=0; i<data.data.length; i++) {
         var row = data.data[i].join(',') + '\n';
-        row = row.replace('<', '<');
-        row = row.replace('>', '>');
-        content.append(row);
+        content.append(my.escapeHTML(row));
       }
-      my.$dialog.append(content);
+      my.$dialog.dialog('option', my.dialogOptions).append(content);
     }
   };
 
@@ -95,19 +108,24 @@
     el.attr('src', url);
     el.attr('width', '100%');
     el.attr('height', '100%');
-    my.$dialog.append(el);
+    my.$dialog.append(el).dialog('open');;
   };
 
-  my.showPreviewDialog = function(previewLinkClick) {
-    var _url = previewLinkClick.target.href;
+  my.loadPreviewDialog = function(link) {
+    var _url = link.href;
     _url = my.normalizeUrl(_url);
-    var _type = $(previewLinkClick.target).attr('format');
+    var _type = $(link).attr('format');
     _type = my.normalizeFormat(_type);
 
-    var _html = '<h2>Loading preview ...</h2>' +
-        '<img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" />';
-    my.$dialog.empty().html(_html);
-    my.$dialog.dialog('open');
+    function callbackWrapper(callback) {
+      return function () {
+        $(link).removeClass('resource-preview-loading').text('Preview');
+        callback.apply(this, arguments);
+        my.$dialog.dialog('open');
+      };
+    }
+
+    $(link).addClass('resource-preview-loading').text('Loading');
 
     if (_type === '') {
       var tmp = _url.split('/');
@@ -121,7 +139,7 @@
     }
 
     if (_type in {'csv': '', 'xls': ''}) {
-      my.getResourceDataDirect(_url, _type, my.showData);
+      my.getResourceDataDirect(_url, _type, callbackWrapper(my.showData));
     } 
     else if (_type in {
         'rdf+xml': '',
@@ -137,15 +155,16 @@
         'txt': ''
         }) {
       // treat as plain text
-      my.getResourceDataDirect(_url, 'csv', my.showPlainTextData);
+      my.getResourceDataDirect(_url, 'csv', callbackWrapper(my.showPlainTextData));
     }
     else {
       // very hacky but should work
-      my.showHtml(_url);
+      callbackWrapper(my.showHtml)(_url);
     }
   };
 
   my.createPreviewButtons = function(resources) {
+    $(resources).find('tr:first th:first').before($('<th>Preview</th>'));
     /*
        :param resources: resource section div or table.
      */
@@ -153,11 +172,13 @@
       var element = $(element);
       var _format = element.next().first().text().trim();
 
+      var preview = $('<td class="preview"></td>').prependTo(element.parent());
+
       // do not create previews for some items
       var _tformat = _format.toLowerCase();
       if (
         _tformat.indexOf('zip') != -1
-        || 
+        ||
         _tformat.indexOf('tgz') != -1
         ||
         _tformat.indexOf('targz') != -1
@@ -184,30 +205,56 @@
 
       // TODO: add ability to change the limit in this url
       var _url = my.webstore + "/" + _hash + "/resource.jsonp?_limit=30";
-      var _previewSpan =  '<a class="resource-preview-button" href="HREF" format="FORMAT">Preview</a>';
-      _previewSpan = _previewSpan.replace('HREF', _url);
-      _previewSpan = _previewSpan.replace('FORMAT', _format);
-      var _previewSpan = $(_previewSpan);
-      _previewSpan.click(function(e) {
-        e.preventDefault();
-        my.showPreviewDialog(e);
-      });
-      element.append(_previewSpan);
+      var _previewSpan = $('<a />', {
+        text: 'Preview',
+        href: _url,
+        format: _format,
+        className: 'resource-preview-button',
+        click: function(e) {
+          e.preventDefault();
+          my.loadPreviewDialog(e.target);
+        }
+      }).appendTo(preview);
     });
   };
 
-  my.initialize = function(webstoreUrl, dialogId) {
+  my.initialize = function(webstoreUrl, dialogId, options) {
     my.webstore = webstoreUrl;
     my.$dialog = $('#' + dialogId);
+    options = options || {};
+    my.timeout = options.timeout || my.timeout;
+
     var _height = Math.round($(window).height() * 0.6);
-    my.$dialog.dialog({
+
+    // Large stylable dialog for displaying data.
+    my.dialogOptions = {
       autoOpen: false
       // does not seem to work for width ...
       , position: ['center', 'center']
+      , buttons: []
       , width: '60%'
       , height: _height
       , resize: 'auto'
-      });
+      , modal: false
+      , draggable: true
+      , resizable: true
+    };
+
+    // Smaller alert style dialog for error messages.
+    my.errorDialogOptions = {
+      title: 'Unable to Preview - Had an error from dataproxy'
+      , position: ['center', 'center']
+      , buttons: [{
+          text: "OK"
+      ,   click: function () { $(this).dialog("close"); }
+      , }]
+      , width: 360
+      , height: 180
+      , resizable: false
+      , draggable: false
+      , modal: true
+    };
+    my.$dialog.dialog(my.dialogOptions);
     my.createPreviewButtons($('.resources'));
   };
 


Binary file public/ckanext/datapreview/icon-preview.png has changed


Binary file public/ckanext/datapreview/loading.gif has changed


http://bitbucket.org/okfn/ckanext-datapreview/changeset/1baed8e419df/
changeset:   1baed8e419df
user:        John Glover
date:        2011-07-18 13:25:27
summary:     Bug fix: change extension structure so that entry point is in plugin.py rather than __init__.py
affected #:  3 files (5.7 KB)

--- a/ckanext/datapreview/__init__.py	Wed Jul 13 17:37:47 2011 +0100
+++ b/ckanext/datapreview/__init__.py	Mon Jul 18 12:25:27 2011 +0100
@@ -15,7 +15,6 @@
 This extension provides preview functionality (using javascript) in relation to
 Packge Resources (i.e. urls pointing to datasets).
 '''
-__version__ = '0.1'
 # this is a namespace package
 try:
     import pkg_resources
@@ -23,75 +22,3 @@
 except ImportError:
     import pkgutil
     __path__ = pkgutil.extend_path(__path__, __name__)
-
-import os
-from logging import getLogger
-from genshi.input import HTML
-from genshi.filters import Transformer
-from ckan.plugins import implements, SingletonPlugin, IConfigurable
-from ckan.plugins import IGenshiStreamFilter, IConfigurer
-from ckan.lib.base import c, request
-
-log = getLogger(__name__)
-
-default_webstore_url = 'http://127.0.0.1:8080/okfn'
-
-
-class DataPreviewPlugin(SingletonPlugin):
-    """Insert javascript fragments into package pages to allow users to preview
-    a package resource."""
-    implements(IConfigurable)
-    implements(IGenshiStreamFilter)
-    implements(IConfigurer, inherit=True)
-    
-    def configure(self, config):
-        """Called upon CKAN setup, will pass current configuration dict to the
-        plugin to read custom options.
-        """
-        self.webstore_url = config.get(
-            'ckanext.dataapi.webstore_url',
-            default_webstore_url
-        )
-
-    def update_config(self, config):
-        here = os.path.dirname(__file__)
-        rootdir = os.path.dirname(os.path.dirname(here))
-        our_public_dir = os.path.join(rootdir, 'public')
-        config['extra_public_paths'] = \
-            ','.join([our_public_dir, config.get('extra_public_paths', '')])
-
-    BOTTOM_CODE = """
-<div id="ckanext-datapreview-dialog"></div>
-<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.8/jquery-ui.min.js"></script>
-<script type="text/javascript" src="/ckanext/datapreview/data-preview.js"></script>
-<script type="text/javascript">
-  jQuery('document').ready(function($) {
-    var webstoreUrl = '%(webstore_url)s';
-    var dialogId = 'ckanext-datapreview-dialog';
-    CKANEXT.DATAPREVIEW.initialize(webstoreUrl, dialogId);
-  });
-</script>
-"""
-    
-    HEAD_CODE = '''
-<link rel="stylesheet"
-    href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/ui-lightness/jquery-ui.css"
-    type="text/css" media="screen, print" />
-<link rel="stylesheet" href="/ckanext/datapreview/data-preview.css" />
-'''
-        
-    def filter(self, stream):
-        """Required to implement IGenshiStreamFilter; will apply some HTML
-        transformations to the page currently rendered.
-        """
-        routes = request.environ.get('pylons.routes_dict')
-        
-        if routes.get('controller') == 'package' and \
-            routes.get('action') == 'read' and c.pkg.id:
-            data = {'webstore_url': self.webstore_url}
-            stream = stream | Transformer('body')\
-                .append(HTML(self.BOTTOM_CODE % data))
-            stream = stream | Transformer('head')\
-                .append(HTML(self.HEAD_CODE))
-        
-        return stream


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckanext/datapreview/plugin.py	Mon Jul 18 12:25:27 2011 +0100
@@ -0,0 +1,90 @@
+'''CKAN Extension Data Preview - preview package resources.
+
+Data Preview is a CKAN extension - http://ckan.org/wiki/Extensions.
+
+Enable by adding to your ckan.plugins line in the CKAN config::
+
+  ckan.plugins = datapreview
+
+Optional configuration options::
+
+    ## a webstore URL
+    ## Default webstore URL is http://127.0.0.1:8080/ofkn
+    ckanext.dataapi.webstore_url = http://...
+    
+This extension provides preview functionality (using javascript) in relation to
+Packge Resources (i.e. urls pointing to datasets).
+'''
+__version__ = '0.1'
+
+import os
+from logging import getLogger
+from genshi.input import HTML
+from genshi.filters import Transformer
+from ckan.plugins import implements, SingletonPlugin, IConfigurable
+from ckan.plugins import IGenshiStreamFilter, IConfigurer
+from ckan.lib.base import c, request
+
+log = getLogger(__name__)
+
+default_webstore_url = 'http://127.0.0.1:8080/okfn'
+
+
+class DataPreviewPlugin(SingletonPlugin):
+    """Insert javascript fragments into package pages to allow users to preview
+    a package resource."""
+    implements(IConfigurable)
+    implements(IGenshiStreamFilter)
+    implements(IConfigurer, inherit=True)
+    
+    def configure(self, config):
+        """Called upon CKAN setup, will pass current configuration dict to the
+        plugin to read custom options.
+        """
+        self.webstore_url = config.get(
+            'ckanext.dataapi.webstore_url',
+            default_webstore_url
+        )
+
+    def update_config(self, config):
+        here = os.path.dirname(__file__)
+        rootdir = os.path.dirname(os.path.dirname(here))
+        our_public_dir = os.path.join(rootdir, 'public')
+        config['extra_public_paths'] = \
+            ','.join([our_public_dir, config.get('extra_public_paths', '')])
+
+    BOTTOM_CODE = """
+<div id="ckanext-datapreview-dialog"></div>
+<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.8/jquery-ui.min.js"></script>
+<script type="text/javascript" src="/ckanext/datapreview/data-preview.js"></script>
+<script type="text/javascript">
+  jQuery('document').ready(function($) {
+    var webstoreUrl = '%(webstore_url)s';
+    var dialogId = 'ckanext-datapreview-dialog';
+    CKANEXT.DATAPREVIEW.initialize(webstoreUrl, dialogId);
+  });
+</script>
+"""
+    
+    HEAD_CODE = '''
+<link rel="stylesheet"
+    href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/ui-lightness/jquery-ui.css"
+    type="text/css" media="screen, print" />
+<link rel="stylesheet" href="/ckanext/datapreview/data-preview.css" />
+'''
+        
+    def filter(self, stream):
+        """Required to implement IGenshiStreamFilter; will apply some HTML
+        transformations to the page currently rendered.
+        """
+        routes = request.environ.get('pylons.routes_dict')
+        
+        if routes.get('controller') == 'package' and \
+            routes.get('action') == 'read' and c.pkg.id:
+            data = {'webstore_url': self.webstore_url}
+            stream = stream | Transformer('body')\
+                .append(HTML(self.BOTTOM_CODE % data))
+            stream = stream | Transformer('head')\
+                .append(HTML(self.HEAD_CODE))
+        
+        return stream


--- a/setup.py	Wed Jul 13 17:37:47 2011 +0100
+++ b/setup.py	Mon Jul 18 12:25:27 2011 +0100
@@ -2,7 +2,7 @@
 import sys, os
 
 version = '0.0'
-from ckanext.datapreview import __version__, __doc__ as long_description
+from ckanext.datapreview.plugin import __version__, __doc__ as long_description
 
 setup(
 	name='ckanext-datapreview',
@@ -25,7 +25,6 @@
 	entry_points=\
 	"""
     [ckan.plugins]
-	# Add plugins here, eg
-	datapreview=ckanext.datapreview:DataPreviewPlugin
+	datapreview=ckanext.datapreview.plugin:DataPreviewPlugin
 	""",
 )


http://bitbucket.org/okfn/ckanext-datapreview/changeset/99006d606654/
changeset:   99006d606654
user:        aron_
date:        2011-07-14 11:33:35
summary:     Set a fixed width on the preview column
affected #:  2 files (23 bytes)

--- a/public/ckanext/datapreview/data-preview.css	Wed Jul 13 17:00:58 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.css	Thu Jul 14 10:33:35 2011 +0100
@@ -1,5 +1,3 @@
-/* @override http://127.0.0.1:5000/ckanext/datapreview/data-preview.css */
-
 #ckanext-datapreview-dialog table td {
 	background: #fff;
 	border: 1px solid #eee;
@@ -14,7 +12,11 @@
 	border-bottom: none;
 }
 
-a.resource-preview-button {
+.preview {
+	width: 65px;
+}
+
+.preview .resource-preview-button {
 	border: 1px solid #eaeaea;
 	color: #444;
 	padding: 3px 6px 2px 21px;
@@ -30,7 +32,7 @@
 	margin: 0 -5px;
 }
 
-a.resource-preview-button:hover {
+.preview .resource-preview-button:hover {
 	color: #000;
 	text-decoration: none;
 	border-color: #aaa;
@@ -38,15 +40,15 @@
 	background-position: 5px -25px;
 }
 
-a.resource-preview-button:active {
+.preview .resource-preview-button:active {
 	color: #b22;
 	border-color: #b22;
 	background-position: 5px -54px;
 }
 
-a.resource-preview-loading,
-a.resource-preview-loading:hover,
-a.resource-preview-loading:active {
+.preview .resource-preview-loading,
+.preview .resource-preview-loading:hover,
+.preview .resource-preview-loading:active {
 	color: #aaa;
 	border: 1px solid #eaeaea;
 	background-image: url(./loading.gif);


--- a/public/ckanext/datapreview/data-preview.js	Wed Jul 13 17:00:58 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Thu Jul 14 10:33:35 2011 +0100
@@ -84,7 +84,8 @@
       content.push('<tbody>');
       content.push.apply(content, $.map(data.data, buildRow));
       content.push('</tbody></table>');
-      my.$dialog.dialog('option', my.dialogOptions).append(content.join(''));
+
+      my.$dialog.dialog('option', my.dialogOptions).append(content.join(''));      
     }
   };
 
@@ -166,7 +167,7 @@
   };
 
   my.createPreviewButtons = function(resources) {
-    $(resources).find('tr:first th:first').before($('<th>Preview</th>'));
+    $(resources).find('tr:first th:first').before($('<th class="preview">Preview</th>'));
     /*
        :param resources: resource section div or table.
      */


http://bitbucket.org/okfn/ckanext-datapreview/changeset/d57f6573beef/
changeset:   d57f6573beef
user:        aron_
date:        2011-07-14 11:37:27
summary:     Added a local copy of jQuery UI and a custom theme

The theme takes elements and colours from the CKAN UI and is
hopefully simpler and cleaner.
affected #:  18 files (265.9 KB)
Diff too large to display.
http://bitbucket.org/okfn/ckanext-datapreview/changeset/9b668a62238d/
changeset:   9b668a62238d
user:        aron_
date:        2011-07-14 16:11:26
summary:     Now rendering the table view using the SlickGrid jQuery plugin

This displays the tabular data in a much nicer fashion allowing
the user to resize and sort columns.
affected #:  42 files (134.9 KB)

--- a/ckanext/datapreview/__init__.py	Thu Jul 14 10:37:27 2011 +0100
+++ b/ckanext/datapreview/__init__.py	Thu Jul 14 15:11:26 2011 +0100
@@ -69,6 +69,9 @@
     BOTTOM_CODE = """
 <div id="ckanext-datapreview-dialog"></div><script type="text/javascript" src="/ckanext/datapreview/jquery-ui/js/jquery-ui-1.8.14.custom.min.js"></script>
+<script type="text/javascript" src="/ckanext/datapreview/jquery-ui/js/jquery.event.drag-2.0.min.js"></script>
+<script type="text/javascript" src="/ckanext/datapreview/slickgrid/slick.grid.js"></script>
+<script type="text/javascript" src="/ckanext/datapreview/slickgrid/slick.columnpicker.js"></script><script type="text/javascript" src="/ckanext/datapreview/data-preview.js"></script><script type="text/javascript">
   jQuery('document').ready(function($) {
@@ -81,6 +84,8 @@
     
     HEAD_CODE = '''
 <link rel="stylesheet" href="/ckanext/datapreview/jquery-ui/css/ckan/jquery-ui-1.8.14.custom.css" />
+<link rel="stylesheet" href="/ckanext/datapreview/slickgrid/slick.grid.css" />
+<link rel="stylesheet" href="/ckanext/datapreview/slickgrid/slick.columnpicker.css" /><link rel="stylesheet" href="/ckanext/datapreview/data-preview.css" />
 '''
         


--- a/public/ckanext/datapreview/data-preview.css	Thu Jul 14 10:37:27 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.css	Thu Jul 14 15:11:26 2011 +0100
@@ -12,6 +12,7 @@
 	border-bottom: none;
 }
 
+/* Style the preview buttons */
 .preview {
 	width: 65px;
 }
@@ -56,6 +57,13 @@
 	cursor: default;
 }
 
+/* Adds border to left of data table */
+.slick-viewport .c0,
+.slick-header-columns .slick-header-column:first-child {
+	border-left: 1px solid #ccc;
+}
+
+/* Reduce the default size of the alert dialog */
 .ui-button-text-only .ui-button-text {
 	padding: 3px 16px 1px;
 }


--- a/public/ckanext/datapreview/data-preview.js	Thu Jul 14 10:37:27 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Thu Jul 14 15:11:26 2011 +0100
@@ -53,6 +53,46 @@
     });
   };
 
+  my.createDataGrid = function createDataGrid(columns, data) {
+    var dialog = my.$dialog;
+
+    // Need to create the grid once the dialog is open for cells to render
+    // correctly.
+    dialog.one("dialogopen", function() {
+      var grid = new Slick.Grid(dialog, data, columns, {
+        enableColumnReorder: false,
+        forceFitColumns: true,
+        syncColumnCellResize: false,
+        enableCellRangeSelection: false
+      });
+
+      // Sort the data and redraw the grid.
+      grid.onSort = function (column, sortAsc) {
+        data.sort(function (a, b) {
+          var x = a[column.field],
+              y = b[column.field];
+
+          if (x == y) {
+            return 0;
+          }
+          return (x > y ? 1 : -1) * (sortAsc ? 1 : -1);
+        });
+        grid.invalidate();
+      };
+
+      // Redraw the grid when the dialog resizes.
+      dialog.bind("dialogresizestop.data-preview", function(event, ui) {
+        grid.resizeCanvas();
+        grid.autosizeColumns();
+      });
+
+      // Remove bindings when dialog is closed.
+      dialog.bind("dialogbeforeclose", function () {
+        my.$dialog.unbind(".data-preview");
+      });
+    });
+  };
+
   my.showError = function (error) {
     var _html = '<strong>' + $.trim(error.title) + '</strong><br />' + $.trim(error.message);
     my.$dialog.html(_html).dialog(my.errorDialogOptions);
@@ -62,31 +102,20 @@
     my.$dialog.empty();
     my.$dialog.dialog('option', 'title', 'Preview: ' + url);
 
-    // Escapes HTML content in fields and returns table row string.
-    function buildRow(fields) {
-      var row = ['<tr>'];
-      for (var i = 0, c = fields.length; i < c; i++) {
-        row.push('<td>', my.escapeHTML(fields[i]), '</td>');
+    var columns = $.map(data.fields || [], function (column, i) {
+      return {id: 'header-' + i, name: column, field: 'column-' + i, sortable: true};
+    });
+
+    var data = $.map(data.data || [], function (row, id) {
+      var cells = {id: id};
+      for (var i = 0, c = row.length; i < c; i++) {
+        var isNumeric = (/^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/).test(row[i]);
+        cells['column-' + i] = isNumeric ? parseFloat(row[i]) : row[i];
       }
-      row.push('</tr>');
-      return row.join('');
-    }
+      return cells;
+    });
 
-    if(data.error) {
-      my.showError(data.error);
-    } else {
-      // Use an array to build an HTML string and inject it into the dom at the
-      // end as it's much faster on older browsers.
-      var content = ['<table>'];
-      if (data.fields) {
-        content.push('<thead>', buildRow(data.fields), '</thead>');
-      }
-      content.push('<tbody>');
-      content.push.apply(content, $.map(data.data, buildRow));
-      content.push('</tbody></table>');
-
-      my.$dialog.dialog('option', my.dialogOptions).append(content.join(''));      
-    }
+    my.createDataGrid(columns, data);
   };
 
   my.showPlainTextData = function(url, type, data) {
@@ -201,15 +230,15 @@
       }
 
       var _url = element.find('a').first().attr('href');
-      var _previewSpan = $('<a />', {
+      var _previewSpan = $('<a/>', {
         text: 'Preview',
         href: _url,
         format: _format,
-        className: 'resource-preview-button',
         click: function(e) {
           e.preventDefault();
           my.loadPreviewDialog(e.target);
-        }
+        },
+        'class': 'resource-preview-button'
       }).appendTo(preview);
     });
   };
@@ -228,8 +257,8 @@
       // does not seem to work for width ...
       , position: ['center', 'center']
       , buttons: []
-      , width: '60%'
-      , height: _height
+      , width:  $(window).width()  - 20
+      , height: $(window).height() - 20
       , resize: 'auto'
       , modal: false
       , draggable: true


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/public/ckanext/datapreview/jquery-ui/js/jquery.event.drag-2.0.min.js	Thu Jul 14 15:11:26 2011 +0100
@@ -0,0 +1,6 @@
+/*! 
+ * jquery.event.drag - v 2.0.0 
+ * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com
+ * Open Source MIT License - http://threedubmedia.com/code/license
+ */
+;(function(f){f.fn.drag=function(b,a,d){var e=typeof b=="string"?b:"",k=f.isFunction(b)?b:f.isFunction(a)?a:null;if(e.indexOf("drag")!==0)e="drag"+e;d=(b==k?a:d)||{};return k?this.bind(e,d,k):this.trigger(e)};var i=f.event,h=i.special,c=h.drag={defaults:{which:1,distance:0,not:":input",handle:null,relative:false,drop:true,click:false},datakey:"dragdata",livekey:"livedrag",add:function(b){var a=f.data(this,c.datakey),d=b.data||{};a.related+=1;if(!a.live&&b.selector){a.live=true;i.add(this,"draginit."+ c.livekey,c.delegate)}f.each(c.defaults,function(e){if(d[e]!==undefined)a[e]=d[e]})},remove:function(){f.data(this,c.datakey).related-=1},setup:function(){if(!f.data(this,c.datakey)){var b=f.extend({related:0},c.defaults);f.data(this,c.datakey,b);i.add(this,"mousedown",c.init,b);this.attachEvent&&this.attachEvent("ondragstart",c.dontstart)}},teardown:function(){if(!f.data(this,c.datakey).related){f.removeData(this,c.datakey);i.remove(this,"mousedown",c.init);i.remove(this,"draginit",c.delegate);c.textselect(true); this.detachEvent&&this.detachEvent("ondragstart",c.dontstart)}},init:function(b){var a=b.data,d;if(!(a.which>0&&b.which!=a.which))if(!f(b.target).is(a.not))if(!(a.handle&&!f(b.target).closest(a.handle,b.currentTarget).length)){a.propagates=1;a.interactions=[c.interaction(this,a)];a.target=b.target;a.pageX=b.pageX;a.pageY=b.pageY;a.dragging=null;d=c.hijack(b,"draginit",a);if(a.propagates){if((d=c.flatten(d))&&d.length){a.interactions=[];f.each(d,function(){a.interactions.push(c.interaction(this,a))})}a.propagates= a.interactions.length;a.drop!==false&&h.drop&&h.drop.handler(b,a);c.textselect(false);i.add(document,"mousemove mouseup",c.handler,a);return false}}},interaction:function(b,a){return{drag:b,callback:new c.callback,droppable:[],offset:f(b)[a.relative?"position":"offset"]()||{top:0,left:0}}},handler:function(b){var a=b.data;switch(b.type){case !a.dragging&&"mousemove":if(Math.pow(b.pageX-a.pageX,2)+Math.pow(b.pageY-a.pageY,2)<Math.pow(a.distance,2))break;b.target=a.target;c.hijack(b,"dragstart",a); if(a.propagates)a.dragging=true;case "mousemove":if(a.dragging){c.hijack(b,"drag",a);if(a.propagates){a.drop!==false&&h.drop&&h.drop.handler(b,a);break}b.type="mouseup"}case "mouseup":i.remove(document,"mousemove mouseup",c.handler);if(a.dragging){a.drop!==false&&h.drop&&h.drop.handler(b,a);c.hijack(b,"dragend",a)}c.textselect(true);if(a.click===false&&a.dragging){jQuery.event.triggered=true;setTimeout(function(){jQuery.event.triggered=false},20);a.dragging=false}break}},delegate:function(b){var a= [],d,e=f.data(this,"events")||{};f.each(e.live||[],function(k,j){if(j.preType.indexOf("drag")===0)if(d=f(b.target).closest(j.selector,b.currentTarget)[0]){i.add(d,j.origType+"."+c.livekey,j.origHandler,j.data);f.inArray(d,a)<0&&a.push(d)}});if(!a.length)return false;return f(a).bind("dragend."+c.livekey,function(){i.remove(this,"."+c.livekey)})},hijack:function(b,a,d,e,k){if(d){var j={event:b.originalEvent,type:b.type},n=a.indexOf("drop")?"drag":"drop",l,o=e||0,g,m;e=!isNaN(e)?e:d.interactions.length; b.type=a;b.originalEvent=null;d.results=[];do if(g=d.interactions[o])if(!(a!=="dragend"&&g.cancelled)){m=c.properties(b,d,g);g.results=[];f(k||g[n]||d.droppable).each(function(q,p){l=(m.target=p)?i.handle.call(p,b,m):null;if(l===false){if(n=="drag"){g.cancelled=true;d.propagates-=1}if(a=="drop")g[n][q]=null}else if(a=="dropinit")g.droppable.push(c.element(l)||p);if(a=="dragstart")g.proxy=f(c.element(l)||g.drag)[0];g.results.push(l);delete b.result;if(a!=="dropinit")return l});d.results[o]=c.flatten(g.results); if(a=="dropinit")g.droppable=c.flatten(g.droppable);a=="dragstart"&&!g.cancelled&&m.update()}while(++o<e);b.type=j.type;b.originalEvent=j.event;return c.flatten(d.results)}},properties:function(b,a,d){var e=d.callback;e.drag=d.drag;e.proxy=d.proxy||d.drag;e.startX=a.pageX;e.startY=a.pageY;e.deltaX=b.pageX-a.pageX;e.deltaY=b.pageY-a.pageY;e.originalX=d.offset.left;e.originalY=d.offset.top;e.offsetX=b.pageX-(a.pageX-e.originalX);e.offsetY=b.pageY-(a.pageY-e.originalY);e.drop=c.flatten((d.drop||[]).slice()); e.available=c.flatten((d.droppable||[]).slice());return e},element:function(b){if(b&&(b.jquery||b.nodeType==1))return b},flatten:function(b){return f.map(b,function(a){return a&&a.jquery?f.makeArray(a):a&&a.length?c.flatten(a):a})},textselect:function(b){f(document)[b?"unbind":"bind"]("selectstart",c.dontstart).attr("unselectable",b?"off":"on").css("MozUserSelect",b?"":"none")},dontstart:function(){return false},callback:function(){}};c.callback.prototype={update:function(){h.drop&&this.available.length&& f.each(this.available,function(b){h.drop.locate(this,b)})}};h.draginit=h.dragstart=h.dragend=c})(jQuery);
\ No newline at end of file


Binary file public/ckanext/datapreview/slickgrid/images/actions.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/ajax-loader-small.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/arrow_redo.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/arrow_right_peppermint.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/arrow_right_spearmint.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/arrow_undo.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/bullet_blue.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/bullet_star.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/bullet_toggle_minus.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/bullet_toggle_plus.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/calendar.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/collapse.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/comment_yellow.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/down.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/drag-handle.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/editor-helper-bg.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/expand.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/header-bg.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/header-columns-bg.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/header-columns-over-bg.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/help.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/info.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/listview.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/pencil.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/row-over-bg.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/sort-asc.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/sort-asc.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/sort-desc.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/sort-desc.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/stripes.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/tag_red.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/tick.png has changed


Binary file public/ckanext/datapreview/slickgrid/images/user_identity.gif has changed


Binary file public/ckanext/datapreview/slickgrid/images/user_identity_plus.gif has changed


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/public/ckanext/datapreview/slickgrid/slick.columnpicker.css	Thu Jul 14 15:11:26 2011 +0100
@@ -0,0 +1,30 @@
+.slick-columnpicker {
+	border: 1px solid #718BB7;
+	background: #f0f0f0;
+	padding: 6px;
+	-moz-box-shadow: 2px 2px 2px silver;
+	-webkit-box-shadow: 2px 2px 2px silver;
+	min-width: 100px;
+	cursor: default;
+}
+
+.slick-columnpicker li {
+	list-style: none;
+	margin: 0;
+	padding: 0;
+	background: none;
+}
+
+.slick-columnpicker input {
+	margin: 4px;
+}
+
+.slick-columnpicker li a {
+	display: block;
+	padding: 4px;
+	font-weight: bold;
+}
+
+.slick-columnpicker li a:hover {
+	background: white;
+}
\ No newline at end of file


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/public/ckanext/datapreview/slickgrid/slick.columnpicker.js	Thu Jul 14 15:11:26 2011 +0100
@@ -0,0 +1,105 @@
+(function($) {
+	function SlickColumnPicker(columns,grid,options)
+	{
+		var $menu;
+
+		var defaults = {
+			fadeSpeed: 250
+		};
+
+		function init() {
+			grid.onHeaderContextMenu = displayContextMenu;
+			options = $.extend({}, defaults, options);
+
+			$menu = $("<span class='slick-columnpicker' style='display:none;position:absolute;z-index:20;' />").appendTo(document.body);
+
+			$menu.bind("mouseleave", function(e) { $(this).fadeOut(options.fadeSpeed) });
+			$menu.bind("click", updateColumn);
+
+		}
+
+		function displayContextMenu(e)
+		{
+			$menu.empty();
+
+            var visibleColumns = grid.getColumns();
+			var $li, $input;
+			for (var i=0; i<columns.length; i++) {
+				$li = $("<li />").appendTo($menu);
+
+				$input = $("<input type='checkbox' />")
+                        .attr("id", "columnpicker_" + i)
+                        .data("id", columns[i].id)
+                        .appendTo($li);
+
+                if (grid.getColumnIndex(columns[i].id) != null)
+                    $input.attr("checked","checked");
+
+				$("<label for='columnpicker_" + i + "' />")
+					.text(columns[i].name)
+					.appendTo($li);
+			}
+
+			$("<hr/>").appendTo($menu);
+			$li = $("<li />").appendTo($menu);
+			$input = $("<input type='checkbox' id='autoresize' />").appendTo($li);
+			$("<label for='autoresize'>Force Fit Columns</label>").appendTo($li);
+			if (grid.getOptions().forceFitColumns)
+				$input.attr("checked", "checked");
+
+			$li = $("<li />").appendTo($menu);
+			$input = $("<input type='checkbox' id='syncresize' />").appendTo($li);
+			$("<label for='syncresize'>Synchronous Resizing</label>").appendTo($li);
+			if (grid.getOptions().syncColumnCellResize)
+				$input.attr("checked", "checked");
+
+			$menu
+				.css("top", e.pageY - 10)
+				.css("left", e.pageX - 10)
+				.fadeIn(options.fadeSpeed);
+		}
+
+		function updateColumn(e)
+		{
+			if (e.target.id == 'autoresize') {
+				if (e.target.checked) {
+					grid.setOptions({forceFitColumns: true});
+					grid.autosizeColumns();
+				} else {
+					grid.setOptions({forceFitColumns: false});
+				}
+				return;
+			}
+
+			if (e.target.id == 'syncresize') {
+				if (e.target.checked) {
+					grid.setOptions({syncColumnCellResize: true});
+				} else {
+					grid.setOptions({syncColumnCellResize: false});
+				}
+				return;
+			}
+
+			if ($(e.target).is(":checkbox")) {
+				if ($menu.find(":checkbox:checked").length == 0) {
+					$(e.target).attr("checked","checked");
+					return;
+				}
+
+                var visibleColumns = [];
+                $menu.find(":checkbox[id^=columnpicker]").each(function(i,e) {
+                    if ($(this).is(":checked")) {
+                        visibleColumns.push(columns[i]);
+                    }
+                });
+                grid.setColumns(visibleColumns);
+			}
+		}
+
+
+		init();
+	}
+
+	// Slick.Controls.ColumnPicker
+	$.extend(true, window, { Slick: { Controls: { ColumnPicker: SlickColumnPicker }}});
+})(jQuery);


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/public/ckanext/datapreview/slickgrid/slick.grid.css	Thu Jul 14 15:11:26 2011 +0100
@@ -0,0 +1,153 @@
+/*
+IMPORTANT:
+In order to preserve the uniform grid appearance, all cell styles need to have padding, margin and border sizes.
+No built-in (selected, editable, highlight, flashing, invalid, loading, :focus) or user-specified CSS
+classes should alter those!
+*/
+
+
+.slick-header.ui-state-default {
+	width: 100%;
+	overflow: hidden;
+	border-left: 0px;
+}
+
+.slick-header-columns {
+	width: 999999px;
+	position: relative;
+	white-space: nowrap;
+	cursor: default;
+	overflow: hidden;
+}
+
+.slick-header-column.ui-state-default {
+	position: relative;
+	display: inline-block;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	height: 16px;
+	line-height: 16px;
+	margin: 0;
+	padding: 4px;
+	border-right: 1px solid silver;
+	border-left: 0px;
+	border-top: 0px;
+	border-bottom: 0px;
+	float: left;
+}
+
+.slick-header-column-sorted {
+	font-style: italic;
+}
+
+.slick-sort-indicator {
+	display: inline-block;
+	width: 8px;
+	height: 5px;
+	margin-left: 4px;
+}
+
+.slick-sort-indicator-desc {
+	background: url(images/sort-desc.gif);
+}
+
+.slick-sort-indicator-asc {
+	background: url(images/sort-asc.gif);
+}
+
+.slick-resizable-handle {
+	position: absolute;
+	font-size: 0.1px;
+	display: block;
+	cursor: col-resize;
+	width: 4px;
+	right: 0px;
+	top: 0;
+	height: 100%;
+}
+
+.slick-sortable-placeholder {
+	background: silver;
+}
+
+.grid-canvas {
+	position: relative;
+	outline: 0;
+}
+
+.slick-row.ui-widget-content, .slick-row.ui-state-active {
+	position: absolute;
+	border: 0px;
+}
+
+.slick-cell {
+	float: left;
+
+	border: 1px solid transparent;
+	border-right: 1px dotted silver;
+	border-bottom-color: silver;
+
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+	vertical-align: middle;
+	z-index: 1;
+	padding: 1px 2px 2px 1px;
+    margin: 0;
+    
+	white-space: nowrap;
+
+	cursor: default;
+}
+
+.slick-cell.highlighted {
+    background: lightskyblue;
+    background: rgba(0,0,255,0.2);
+    -webkit-transition: all 0.5s;
+    -moz-transition: all 0.5s;
+    transition: all 0.5s;
+}
+
+.slick-cell.flashing {
+    border: 1px solid red !important;
+}
+
+.slick-cell.editable {
+	z-index: 11;
+	overflow: visible;
+	background: white;
+	border-color: black;
+	border-style: solid;
+}
+
+.slick-cell:focus {
+	outline: none;
+}
+
+.slick-reorder-proxy {
+	display: inline-block;
+	background: blue;
+	opacity: 0.15;
+	filter: alpha(opacity=15);
+	cursor: move;
+}
+
+.slick-reorder-guide {
+	display: inline-block;
+	height: 2px;
+	background: blue;
+	opacity: 0.7;
+	filter: alpha(opacity=70);
+}
+
+.slick-selection {
+    z-index: 10;
+    position: absolute;
+    background: gray;
+    border: 1px solid black;
+    opacity: 0.3;
+    filter: alpha(opacity=30);
+    -webkit-box-shadow: 0px 0px 10px black;
+    -moz-box-shadow: 0px 0px 10px black;
+    box-shadow: 0px 0px 10px black;
+}
\ No newline at end of file


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/public/ckanext/datapreview/slickgrid/slick.grid.js	Thu Jul 14 15:11:26 2011 +0100
@@ -0,0 +1,2504 @@
+/**
+ * @license
+ * (c) 2009-2010 Michael Leibman (michael.leibman at gmail.com)
+ * http://github.com/mleibman/slickgrid
+ * Distributed under MIT license.
+ * All rights reserved.
+ *
+ * SlickGrid v1.4.3
+ *
+ * TODO:
+ * - frozen columns
+ * - consistent events (EventHelper?  jQuery events?)
+ *
+ *
+ * OPTIONS:
+ *     rowHeight                - (default 25px) Row height in pixels.
+ *     enableAddRow             - (default false) If true, a blank row will be displayed at the bottom - typing values in that row will add a new one.
+ *     leaveSpaceForNewRows     - (default false)
+ *     editable                 - (default false) If false, no cells will be switched into edit mode.
+ *     autoEdit                 - (default true) Cell will not automatically go into edit mode when selected.
+ *     enableCellNavigation     - (default true) If false, no cells will be selectable.
+ *     enableCellRangeSelection - (default false) If true, user will be able to select a cell range.  onCellRangeSelected event will be fired.
+ *     defaultColumnWidth       - (default 80px) Default column width in pixels (if columns[cell].width is not specified).
+ *     enableColumnReorder      - (default true) Allows the user to reorder columns.
+ *     asyncEditorLoading       - (default false) Makes cell editors load asynchronously after a small delay.
+ *                                This greatly increases keyboard navigation speed.
+ *     asyncEditorLoadDelay     - (default 100msec) Delay after which cell editor is loaded. Ignored unless asyncEditorLoading is true.
+ *     forceFitColumns          - (default false) Force column sizes to fit into the viewport (avoid horizontal scrolling).
+ *     enableAsyncPostRender    - (default false) If true, async post rendering will occur and asyncPostRender delegates on columns will be called.
+ *     asyncPostRenderDelay     - (default 60msec) Delay after which async post renderer delegate is called.
+ *     autoHeight               - (default false) If true, vertically resizes to fit all rows.
+ *     editorLock               - (default Slick.GlobalEditorLock) A Slick.EditorLock instance to use for controlling concurrent data edits.
+ *     showSecondaryHeaderRow   - (default false) If true, an extra blank (to be populated externally) row will be displayed just below the header columns.
+ *     secondaryHeaderRowHeight - (default 25px) The height of the secondary header row.
+ *     syncColumnCellResize     - (default false) Synchronously resize column cells when column headers are resized
+ *     rowCssClasses            - (default null) A function which (given a row's data item as an argument) returns a space-delimited string of CSS classes that will be applied to the slick-row element. Note that this should be fast, as it is called every time a row is displayed.
+ *     cellHighlightCssClass    - (default "highlighted") A CSS class to apply to cells highlighted via setHighlightedCells().
+ *     cellFlashingCssClass     - (default "flashing") A CSS class to apply to flashing cells (flashCell()).
+ *     formatterFactory         - (default null) A factory object responsible to creating a formatter for a given cell.
+ *                                Must implement getFormatter(column).
+ *     editorFactory            - (default null) A factory object responsible to creating an editor for a given cell.
+ *                                Must implement getEditor(column).
+ *     multiSelect              - (default true) Enable multiple row selection.
+ *
+ * COLUMN DEFINITION (columns) OPTIONS:
+ *     id                  - Column ID.
+ *     name                - Column name to put in the header.
+ *     toolTip             - Tooltip (if different from name).
+ *     field               - Property of the data context to bind to.
+ *     formatter           - (default 'return value || ""') Function responsible for rendering the contents of a cell. Signature: function formatter(row, cell, value, columnDef, dataContext) { ... return "..."; }
+ *     editor              - An Editor class.
+ *     validator           - An extra validation function to be passed to the editor.
+ *     unselectable        - If true, the cell cannot be selected (and therefore edited).
+ *     cannotTriggerInsert - If true, a new row cannot be created from just the value of this cell.
+ *     width               - Width of the column in pixels.
+ *     resizable           - (default true) If false, the column cannot be resized.
+ *     sortable            - (default false) If true, the column can be sorted (onSort will be called).
+ *     minWidth            - Minimum allowed column width for resizing.
+ *     maxWidth            - Maximum allowed column width for resizing.
+ *     cssClass            - A CSS class to add to the cell.
+ *     rerenderOnResize    - Rerender the column when it is resized (useful for columns relying on cell width or adaptive formatters).
+ *     asyncPostRender     - Function responsible for manipulating the cell DOM node after it has been rendered (called in the background).
+ *     behavior            - Configures the column with one of several available predefined behaviors:  "select", "move", "selectAndMove".
+ *
+ *
+ * EVENTS:
+ *     onSort                -
+ *     onHeaderContextMenu   -
+ *     onHeaderClick         -
+ *     onClick               -
+ *     onDblClick            -
+ *     onContextMenu         -
+ *     onKeyDown             -
+ *     onAddNewRow           -
+ *     onValidationError     -
+ *     onViewportChanged     -
+ *     onSelectedRowsChanged -
+ *     onColumnsReordered    -
+ *     onColumnsResized      -
+ *     onBeforeMoveRows      -
+ *     onMoveRows            -
+ *     onCellChange          -  Raised when cell has been edited.   Args: row,cell,dataContext.
+ *     onBeforeEditCell      -  Raised before a cell goes into edit mode.  Return false to cancel.  Args: row,cell,dataContext.
+ *     onBeforeCellEditorDestroy    - Raised before a cell editor is destroyed.  Args: current cell editor.
+ *     onBeforeDestroy       -  Raised just before the grid control is destroyed (part of the destroy() method).
+ *     onCurrentCellChanged  -  Raised when the selected (active) cell changed.  Args: {row:currentRow, cell:currentCell}.
+ *     onCellRangeSelected   -  Raised when a user selects a range of cells.  Args: {from:{row,cell}, to:{row,cell}}.
+ *
+ * NOTES:
+ *     Cell/row DOM manipulations are done directly bypassing jQuery's DOM manipulation methods.
+ *     This increases the speed dramatically, but can only be done safely because there are no event handlers
+ *     or data associated with any cell/row DOM nodes.  Cell editors must make sure they implement .destroy()
+ *     and do proper cleanup.
+ *
+ *
+ * @param {Node}              container   Container node to create the grid in.
+ * @param {Array} or {Object} data        An array of objects for databinding.
+ * @param {Array}             columns     An array of column definitions.
+ * @param {Object}            options     Grid options.
+ */
+
+// make sure required JavaScript modules are loaded
+if (typeof jQuery === "undefined") {
+    throw new Error("SlickGrid requires jquery module to be loaded");
+}
+if (!jQuery.fn.drag) {
+    throw new Error("SlickGrid requires jquery.event.drag module to be loaded");
+}
+
+(function($) {
+    var scrollbarDimensions; // shared across all grids on this page
+
+
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    // EditorLock class implementation (available as Slick.EditorLock)
+
+    /** @constructor */
+    function EditorLock() {
+        /// <summary>
+        /// Track currently active edit controller and ensure
+        /// that onle a single controller can be active at a time.
+        /// Edit controller is an object that is responsible for
+        /// gory details of looking after editor in the browser,
+        /// and allowing EditorLock clients to either accept
+        /// or cancel editor changes without knowing any of the
+        /// implementation details. SlickGrid instance is used
+        /// as edit controller for cell editors.
+        /// </summary>
+
+        var currentEditController = null;
+
+        this.isActive = function isActive(editController) {
+            /// <summary>
+            /// Return true if the specified editController
+            /// is currently active in this lock instance
+            /// (i.e. if that controller acquired edit lock).
+            /// If invoked without parameters ("editorLock.isActive()"),
+            /// return true if any editController is currently
+            /// active in this lock instance.
+            /// </summary>
+            return (editController ? currentEditController === editController : currentEditController !== null);
+        };
+
+        this.activate = function activate(editController) {
+            /// <summary>
+            /// Set the specified editController as the active
+            /// controller in this lock instance (acquire edit lock).
+            /// If another editController is already active,
+            /// an error will be thrown (i.e. before calling
+            /// this method isActive() must be false,
+            /// afterwards isActive() will be true).
+            /// </summary>
+            if (editController === currentEditController) { // already activated?
+                return;
+            }
+            if (currentEditController !== null) {
+                throw "SlickGrid.EditorLock.activate: an editController is still active, can't activate another editController";
+            }
+            if (!editController.commitCurrentEdit) {
+                throw "SlickGrid.EditorLock.activate: editController must implement .commitCurrentEdit()";
+            }
+            if (!editController.cancelCurrentEdit) {
+                throw "SlickGrid.EditorLock.activate: editController must implement .cancelCurrentEdit()";
+            }
+            currentEditController = editController;
+        };
+
+        this.deactivate = function deactivate(editController) {
+            /// <summary>
+            /// Unset the specified editController as the active
+            /// controller in this lock instance (release edit lock).
+            /// If the specified editController is not the editController
+            /// that is currently active in this lock instance,
+            /// an error will be thrown.
+            /// </summary>
+            if (currentEditController !== editController) {
+                throw "SlickGrid.EditorLock.deactivate: specified editController is not the currently active one";
+            }
+            currentEditController = null;
+        };
+
+        this.commitCurrentEdit = function commitCurrentEdit() {
+            /// <summary>
+            /// Invoke the "commitCurrentEdit" method on the
+            /// editController that is active in this lock
+            /// instance and return the return value of that method
+            /// (if no controller is active, return true).
+            /// "commitCurrentEdit" is expected to return true
+            /// to indicate successful commit, false otherwise.
+            /// </summary>
+            return (currentEditController ? currentEditController.commitCurrentEdit() : true);
+        };
+
+        this.cancelCurrentEdit = function cancelCurrentEdit() {
+            /// <summary>
+            /// Invoke the "cancelCurrentEdit" method on the
+            /// editController that is active in this lock
+            /// instance (if no controller is active, do nothing).
+            /// Returns true if the edit was succesfully cancelled.
+            /// </summary>
+            return (currentEditController ? currentEditController.cancelCurrentEdit() : true);
+        };
+    } // end of EditorLock function (class)
+
+
+
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    // SlickGrid class implementation (available as Slick.Grid)
+
+    /** @constructor */
+    function SlickGrid(container,data,columns,options) {
+        /// <summary>
+        /// Create and manage virtual grid in the specified $container,
+        /// connecting it to the specified data source. Data is presented
+        /// as a grid with the specified columns and data.length rows.
+        /// Options alter behaviour of the grid.
+        /// </summary>
+
+        // settings
+        var defaults = {
+            rowHeight: 25,
+            defaultColumnWidth: 80,
+            enableAddRow: false,
+            leaveSpaceForNewRows: false,
+            editable: false,
+            autoEdit: true,
+            enableCellNavigation: true,
+            enableCellRangeSelection: false,
+            enableColumnReorder: true,
+            asyncEditorLoading: false,
+            asyncEditorLoadDelay: 100,
+            forceFitColumns: false,
+            enableAsyncPostRender: false,
+            asyncPostRenderDelay: 60,
+            autoHeight: false,
+            editorLock: Slick.GlobalEditorLock,
+            showSecondaryHeaderRow: false,
+            secondaryHeaderRowHeight: 25,
+            syncColumnCellResize: false,
+            enableAutoTooltips: true,
+            toolTipMaxLength: null,
+            formatterFactory: null,
+            editorFactory: null,
+            cellHighlightCssClass: "highlighted",
+            cellFlashingCssClass: "flashing",
+            multiSelect: true
+        },
+        gridData, gridDataGetLength, gridDataGetItem;
+
+        var columnDefaults = {
+            name: "",
+            resizable: true,
+            sortable: false,
+            minWidth: 30
+        };
+
+        // scroller
+        var maxSupportedCssHeight;      // browser's breaking point
+        var th;                         // virtual height
+        var h;                          // real scrollable height
+        var ph;                         // page height
+        var n;                          // number of pages
+        var cj;                         // "jumpiness" coefficient
+
+        var page = 0;                   // current page
+        var offset = 0;                 // current page offset
+        var scrollDir = 1;
+
+        // private
+        var $container;
+        var uid = "slickgrid_" + Math.round(1000000 * Math.random());
+        var self = this;
+        var $headerScroller;
+        var $headers;
+        var $secondaryHeaderScroller;
+        var $secondaryHeaders;
+        var $viewport;
+        var $canvas;
+        var $style;
+        var stylesheet;
+        var viewportH, viewportW;
+        var viewportHasHScroll;
+        var headerColumnWidthDiff, headerColumnHeightDiff, cellWidthDiff, cellHeightDiff;  // padding+border
+        var absoluteColumnMinWidth;
+
+        var currentRow, currentCell;
+        var currentCellNode = null;
+        var currentEditor = null;
+        var serializedEditorValue;
+        var editController;
+
+        var rowsCache = {};
+        var renderedRows = 0;
+        var numVisibleRows;
+        var prevScrollTop = 0;
+        var scrollTop = 0;
+        var lastRenderedScrollTop = 0;
+        var prevScrollLeft = 0;
+        var avgRowRenderTime = 10;
+
+        var selectedRows = [];
+        var selectedRowsLookup = {};
+        var columnsById = {};
+        var highlightedCells;
+        var sortColumnId;
+        var sortAsc = true;
+
+        // async call handles
+        var h_editorLoader = null;
+        var h_render = null;
+        var h_postrender = null;
+        var postProcessedRows = {};
+        var postProcessToRow = null;
+        var postProcessFromRow = null;
+
+        // perf counters
+        var counter_rows_rendered = 0;
+        var counter_rows_removed = 0;
+
+
+        //////////////////////////////////////////////////////////////////////////////////////////////
+        // Initialization
+
+        function init() {
+            /// <summary>
+            /// Initialize 'this' (self) instance of a SlickGrid.
+            /// This function is called by the constructor.
+            /// </summary>
+
+            $container = $(container);
+
+            gridData = data;
+            gridDataGetLength = gridData.getLength || defaultGetLength;
+            gridDataGetItem = gridData.getItem || defaultGetItem;
+
+            maxSupportedCssHeight = getMaxSupportedCssHeight();
+
+            scrollbarDimensions = scrollbarDimensions || measureScrollbar(); // skip measurement if already have dimensions
+            options = $.extend({},defaults,options);
+            columnDefaults.width = options.defaultColumnWidth;
+
+            // validate loaded JavaScript modules against requested options
+            if (options.enableColumnReorder && !$.fn.sortable) {
+                throw new Error("SlickGrid's \"enableColumnReorder = true\" option requires jquery-ui.sortable module to be loaded");
+            }
+
+            editController = {
+                "commitCurrentEdit": commitCurrentEdit,
+                "cancelCurrentEdit": cancelCurrentEdit
+            };
+
+            $container
+                .empty()
+                .attr("tabIndex",0)
+                .attr("hideFocus",true)
+                .css("overflow","hidden")
+                .css("outline",0)
+                .addClass(uid)
+                .addClass("ui-widget");
+
+            // set up a positioning container if needed
+            if (!/relative|absolute|fixed/.test($container.css("position")))
+                $container.css("position","relative");
+
+            $headerScroller = $("<div class='slick-header ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
+            $headers = $("<div class='slick-header-columns' style='width:100000px; left:-10000px' />").appendTo($headerScroller);
+
+            $secondaryHeaderScroller = $("<div class='slick-header-secondary ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
+            $secondaryHeaders = $("<div class='slick-header-columns-secondary' style='width:100000px' />").appendTo($secondaryHeaderScroller);
+
+            if (!options.showSecondaryHeaderRow) {
+                $secondaryHeaderScroller.hide();
+            }
+
+            $viewport = $("<div class='slick-viewport' tabIndex='0' hideFocus style='width:100%;overflow-x:auto;outline:0;position:relative;overflow-y:auto;'>").appendTo($container);
+            $canvas = $("<div class='grid-canvas' tabIndex='0' hideFocus />").appendTo($viewport);
+
+            // header columns and cells may have different padding/border skewing width calculations (box-sizing, hello?)
+            // calculate the diff so we can set consistent sizes
+            measureCellPaddingAndBorder();
+
+            $viewport.height(
+                $container.innerHeight() -
+                $headerScroller.outerHeight() -
+                (options.showSecondaryHeaderRow ? $secondaryHeaderScroller.outerHeight() : 0));
+
+            // for usability reasons, all text selection in SlickGrid is disabled
+            // with the exception of input and textarea elements (selection must
+            // be enabled there so that editors work as expected); note that
+            // selection in grid cells (grid body) is already unavailable in
+            // all browsers except IE
+            disableSelection($headers); // disable all text selection in header (including input and textarea)
+            $viewport.bind("selectstart.ui", function (event) { return $(event.target).is("input,textarea"); }); // disable text selection in grid cells except in input and textarea elements (this is IE-specific, because selectstart event will only fire in IE)
+
+            createColumnHeaders();
+            setupColumnSort();
+            setupDragEvents();
+            createCssRules();
+
+            resizeAndRender();
+
+            bindAncestorScrollEvents();
+            $viewport.bind("scroll.slickgrid", handleScroll);
+            $container.bind("resize.slickgrid", resizeAndRender);
+            $canvas.bind("keydown.slickgrid", handleKeyDown);
+            $canvas.bind("click.slickgrid", handleClick);
+            $canvas.bind("dblclick.slickgrid", handleDblClick);
+            $canvas.bind("contextmenu.slickgrid", handleContextMenu);
+            $canvas.bind("mouseover.slickgrid", handleHover);
+            $headerScroller.bind("contextmenu.slickgrid", handleHeaderContextMenu);
+            $headerScroller.bind("click.slickgrid", handleHeaderClick);
+        }
+
+        function measureScrollbar() {
+            /// <summary>
+            /// Measure width of a vertical scrollbar
+            /// and height of a horizontal scrollbar.
+            /// </summary
+            /// <returns>
+            /// { width: pixelWidth, height: pixelHeight }
+            /// </returns>
+            var $c = $("<div style='position:absolute; top:-10000px; left:-10000px; width:100px; height:100px; overflow:scroll;'></div>").appendTo("body");
+            var dim = { width: $c.width() - $c[0].clientWidth, height: $c.height() - $c[0].clientHeight };
+            $c.remove();
+            return dim;
+        }
+
+        function setCanvasWidth(width) {
+            $canvas.width(width);
+            viewportHasHScroll = (width > viewportW - scrollbarDimensions.width);
+        }
+
+        function disableSelection($target) {
+            /// <summary>
+            /// Disable text selection (using mouse) in
+            /// the specified target.
+            /// </summary
+            if ($target && $target.jquery) {
+                $target.attr('unselectable', 'on').css('MozUserSelect', 'none').bind('selectstart.ui', function() { return false; }); // from jquery:ui.core.js 1.7.2
+            }
+        }
+
+        function defaultGetLength() {
+            /// <summary>
+            /// Default implementation of getLength method
+            /// returns the length of the array.
+            /// </summary
+            return gridData.length;
+        }
+
+        function defaultGetItem(i) {
+            /// <summary>
+            /// Default implementation of getItem method
+            /// returns the item at specified position in
+            /// the array.
+            /// </summary
+            return gridData[i];
+        }
+
+        function getMaxSupportedCssHeight() {
+            var increment = 1000000;
+            var supportedHeight = 0;
+            // FF reports the height back but still renders blank after ~6M px
+            var testUpTo = ($.browser.mozilla) ? 5000000 : 1000000000;
+            var div = $("<div style='display:none' />").appendTo(document.body);
+
+            while (supportedHeight <= testUpTo) {
+                div.css("height", supportedHeight + increment);
+                if (div.height() !== supportedHeight + increment)
+                    break;
+                else
+                    supportedHeight += increment;
+            }
+
+            div.remove();
+            return supportedHeight;
+        }
+
+        // TODO:  this is static.  need to handle page mutation.
+        function bindAncestorScrollEvents() {
+            var elem = $canvas[0];
+            while ((elem = elem.parentNode) != document.body) {
+                // bind to scroll containers only
+                if (elem == $viewport[0] || elem.scrollWidth != elem.clientWidth || elem.scrollHeight != elem.clientHeight)
+                    $(elem).bind("scroll.slickgrid", handleCurrentCellPositionChange);
+            }
+        }
+
+        function unbindAncestorScrollEvents() {
+            $canvas.parents().unbind("scroll.slickgrid");
+        }
+
+        function createColumnHeaders() {
+            var i;
+
+            function hoverBegin() {
+                $(this).addClass("ui-state-hover");
+            }
+            function hoverEnd() {
+                $(this).removeClass("ui-state-hover");
+            }
+
+            $headers.empty();
+            columnsById = {};
+
+            for (i = 0; i < columns.length; i++) {
+                var m = columns[i] = $.extend({},columnDefaults,columns[i]);
+                columnsById[m.id] = i;
+
+                var header = $("<div class='ui-state-default slick-header-column' id='" + uid + m.id + "' />")
+                    .html("<span class='slick-column-name'>" + m.name + "</span>")
+                    .width((m.currentWidth || m.width) - headerColumnWidthDiff)
+                    .attr("title", m.toolTip || m.name || "")
+                    .data("fieldId", m.id)
+                    .appendTo($headers);
+
+                if (options.enableColumnReorder || m.sortable) {
+                    header.hover(hoverBegin, hoverEnd);
+                }
+
+                if (m.sortable) {
+                    header.append("<span class='slick-sort-indicator' />");
+                }
+            }
+
+            setSortColumn(sortColumnId,sortAsc);
+            setupColumnResize();
+            if (options.enableColumnReorder) {
+                setupColumnReorder();
+            }
+        }
+
+        function setupColumnSort() {
+            $headers.click(function(e) {
+                if ($(e.target).hasClass("slick-resizable-handle")) {
+                    return;
+                }
+
+                if (self.onSort) {
+                    var $col = $(e.target).closest(".slick-header-column");
+                    if (!$col.length)
+                        return;
+
+                    var column = columns[getSiblingIndex($col[0])];
+                    if (column.sortable) {
+                        if (!options.editorLock.commitCurrentEdit())
+                            return;
+
+                        if (column.id === sortColumnId) {
+                            sortAsc = !sortAsc;
+                        }
+                        else {
+                            sortColumnId = column.id;
+                            sortAsc = true;
+                        }
+
+                        setSortColumn(sortColumnId,sortAsc);
+                        self.onSort(column,sortAsc);
+                    }
+                }
+            });
+        }
+
+        function setupColumnReorder() {
+            $headers.sortable({
+                containment: "parent",
+                axis: "x",
+                cursor: "default",
+                tolerance: "intersection",
+                helper: "clone",
+                placeholder: "slick-sortable-placeholder ui-state-default slick-header-column",
+                forcePlaceholderSize: true,
+                start: function(e, ui) { $(ui.helper).addClass("slick-header-column-active"); },
+                beforeStop: function(e, ui) { $(ui.helper).removeClass("slick-header-column-active"); },
+                stop: function(e) {
+                    if (!options.editorLock.commitCurrentEdit()) {
+                        $(this).sortable("cancel");
+                        return;
+                    }
+
+                    var reorderedIds = $headers.sortable("toArray");
+                    var reorderedColumns = [];
+                    for (var i=0; i<reorderedIds.length; i++) {
+                        reorderedColumns.push(columns[getColumnIndex(reorderedIds[i].replace(uid,""))]);
+                    }
+                    setColumns(reorderedColumns);
+
+                    if (self.onColumnsReordered) {
+                        self.onColumnsReordered();
+                    }
+                    e.stopPropagation();
+                    setupColumnResize();
+                }
+            });
+        }
+
+        function setupColumnResize() {
+            var $col, j, c, pageX, columnElements, minPageX, maxPageX, firstResizable, lastResizable, originalCanvasWidth;
+            columnElements = $headers.children();
+            columnElements.find(".slick-resizable-handle").remove();
+            columnElements.each(function(i,e) {
+                if (columns[i].resizable) {
+                    if (firstResizable === undefined) { firstResizable = i; }
+                    lastResizable = i;
+                }
+            });
+            columnElements.each(function(i,e) {
+                if ((firstResizable !== undefined && i < firstResizable) || (options.forceFitColumns && i >= lastResizable)) { return; }
+                $col = $(e);
+                $("<div class='slick-resizable-handle' />")
+                    .appendTo(e)
+                    .bind("dragstart", function(e,dd) {
+                        if (!options.editorLock.commitCurrentEdit()) { return false; }
+                        pageX = e.pageX;
+                        $(this).parent().addClass("slick-header-column-active");
+                        var shrinkLeewayOnRight = null, stretchLeewayOnRight = null;
+                        // lock each column's width option to current width
+                        columnElements.each(function(i,e) { columns[i].previousWidth = $(e).outerWidth(); });
+                        if (options.forceFitColumns) {
+                            shrinkLeewayOnRight = 0;
+                            stretchLeewayOnRight = 0;
+                            // colums on right affect maxPageX/minPageX
+                            for (j = i + 1; j < columnElements.length; j++) {
+                                c = columns[j];
+                                if (c.resizable) {
+                                    if (stretchLeewayOnRight !== null) {
+                                        if (c.maxWidth) {
+                                            stretchLeewayOnRight += c.maxWidth - c.previousWidth;
+                                        }
+                                        else {
+                                            stretchLeewayOnRight = null;
+                                        }
+                                    }
+                                    shrinkLeewayOnRight += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth);
+                                }
+                            }
+                        }
+                        var shrinkLeewayOnLeft = 0, stretchLeewayOnLeft = 0;
+                        for (j = 0; j <= i; j++) {
+                            // columns on left only affect minPageX
+                            c = columns[j];
+                            if (c.resizable) {
+                                if (stretchLeewayOnLeft !== null) {
+                                    if (c.maxWidth) {
+                                        stretchLeewayOnLeft += c.maxWidth - c.previousWidth;
+                                    }
+                                    else {
+                                        stretchLeewayOnLeft = null;
+                                    }
+                                }
+                                shrinkLeewayOnLeft += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth);
+                            }
+                        }
+                        if (shrinkLeewayOnRight === null) { shrinkLeewayOnRight = 100000; }
+                        if (shrinkLeewayOnLeft === null) { shrinkLeewayOnLeft = 100000; }
+                        if (stretchLeewayOnRight === null) { stretchLeewayOnRight = 100000; }
+                        if (stretchLeewayOnLeft === null) { stretchLeewayOnLeft = 100000; }
+                        maxPageX = pageX + Math.min(shrinkLeewayOnRight, stretchLeewayOnLeft);
+                        minPageX = pageX - Math.min(shrinkLeewayOnLeft, stretchLeewayOnRight);
+                        originalCanvasWidth = $canvas.width();
+                    })
+                    .bind("drag", function(e,dd) {
+                        var actualMinWidth, d = Math.min(maxPageX, Math.max(minPageX, e.pageX)) - pageX, x, ci;
+                        if (d < 0) { // shrink column
+                            x = d;
+                            for (j = i; j >= 0; j--) {
+                                c = columns[j];
+                                if (c.resizable) {
+                                    actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth);
+                                    if (x && c.previousWidth + x < actualMinWidth) {
+                                        x += c.previousWidth - actualMinWidth;
+                                        styleColumnWidth(j, actualMinWidth, options.syncColumnCellResize);
+                                    } else {
+                                        styleColumnWidth(j, c.previousWidth + x, options.syncColumnCellResize);
+                                        x = 0;
+                                    }
+                                }
+                            }
+
+                            if (options.forceFitColumns) {
+                                x = -d;
+                                for (j = i + 1; j < columnElements.length; j++) {
+                                    c = columns[j];
+                                    if (c.resizable) {
+                                        if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) {
+                                            x -= c.maxWidth - c.previousWidth;
+                                            styleColumnWidth(j, c.maxWidth, options.syncColumnCellResize);
+                                        } else {
+                                            styleColumnWidth(j, c.previousWidth + x, options.syncColumnCellResize);
+                                            x = 0;
+                                        }
+                                    }
+                                }
+                            } else if (options.syncColumnCellResize) {
+                                setCanvasWidth(originalCanvasWidth + d);
+                            }
+                        } else { // stretch column
+                            x = d;
+                            for (j = i; j >= 0; j--) {
+                                c = columns[j];
+                                if (c.resizable) {
+                                    if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) {
+                                        x -= c.maxWidth - c.previousWidth;
+                                        styleColumnWidth(j, c.maxWidth, options.syncColumnCellResize);
+                                    } else {
+                                        styleColumnWidth(j, c.previousWidth + x, options.syncColumnCellResize);
+                                        x = 0;
+                                    }
+                                }
+                            }
+
+                            if (options.forceFitColumns) {
+                                x = -d;
+                                for (j = i + 1; j < columnElements.length; j++) {
+                                    c = columns[j];
+                                    if (c.resizable) {
+                                        actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth);
+                                        if (x && c.previousWidth + x < actualMinWidth) {
+                                            x += c.previousWidth - actualMinWidth;
+                                            styleColumnWidth(j, actualMinWidth, options.syncColumnCellResize);
+                                        } else {
+                                            styleColumnWidth(j, c.previousWidth + x, options.syncColumnCellResize);
+                                            x = 0;
+                                        }
+                                    }
+                                }
+                            } else if (options.syncColumnCellResize) {
+                                setCanvasWidth(originalCanvasWidth + d);
+                            }
+                        }
+                    })
+                    .bind("dragend", function(e,dd) {
+                        var newWidth;
+                        $(this).parent().removeClass("slick-header-column-active");
+                        for (j = 0; j < columnElements.length; j++) {
+                            c = columns[j];
+                            newWidth = $(columnElements[j]).outerWidth();
+
+                            if (c.previousWidth !== newWidth && c.rerenderOnResize) {
+                                removeAllRows();
+                            }
+                            if (options.forceFitColumns) {
+                                c.width = Math.floor(c.width * (newWidth - c.previousWidth) / c.previousWidth) + c.width;
+                            } else {
+                                c.width = newWidth;
+                            }
+                            if (!options.syncColumnCellResize && c.previousWidth !== newWidth) {
+                                styleColumnWidth(j, newWidth, true);
+                            }
+                        }
+                        resizeCanvas();
+                        if (self.onColumnsResized) {
+                            self.onColumnsResized();
+                        }
+                    });
+                });
+        }
+
+        function setupDragEvents() {
+            var MOVE_ROWS = 1;
+            var SELECT_CELLS = 2;
+
+            function fixUpRange(range) {
+                var r1 = Math.min(range.start.row,range.end.row);
+                var c1 = Math.min(range.start.cell,range.end.cell);
+                var r2 = Math.max(range.start.row,range.end.row);
+                var c2 = Math.max(range.start.cell,range.end.cell);
+                return {
+                    start: {row:r1, cell:c1},
+                    end: {row:r2, cell:c2}
+                };
+            }
+
+            $canvas
+                .bind("draginit", function(e,dd) {
+                    var $cell = $(e.target).closest(".slick-cell");
+                    if ($cell.length === 0) { return false; }
+                    if (parseInt($cell.parent().attr("row"), 10) >= gridDataGetLength())
+                        return false;
+
+                    var colDef = columns[getSiblingIndex($cell[0])];
+                    if (colDef.behavior == "move" || colDef.behavior == "selectAndMove") {
+                        dd.mode = MOVE_ROWS;
+                    }
+                    else if (options.enableCellRangeSelection) {
+                        dd.mode = SELECT_CELLS;
+                    }
+                    else
+                        return false;
+                })
+                .bind("dragstart", function(e,dd) {
+                    if (!options.editorLock.commitCurrentEdit()) { return false; }
+                    var row = parseInt($(e.target).closest(".slick-row").attr("row"), 10);
+
+                    if (dd.mode == MOVE_ROWS) {
+                        if (!selectedRowsLookup[row]) {
+                            setSelectedRows([row]);
+                        }
+
+                        dd.selectionProxy = $("<div class='slick-reorder-proxy'/>")
+                            .css("position", "absolute")
+                            .css("zIndex", "99999")
+                            .css("width", $(this).innerWidth())
+                            .css("height", options.rowHeight*selectedRows.length)
+                            .appendTo($viewport);
+
+                        dd.guide = $("<div class='slick-reorder-guide'/>")
+                            .css("position", "absolute")
+                            .css("zIndex", "99998")
+                            .css("width", $(this).innerWidth())
+                            .css("top", -1000)
+                            .appendTo($viewport);
+
+                        dd.insertBefore = -1;
+                    }
+
+                    if (dd.mode == SELECT_CELLS) {
+                        var start = getCellFromPoint(dd.startX - $canvas.offset().left, dd.startY - $canvas.offset().top);
+                        if (!cellExists(start.row,start.cell))
+                            return false;
+
+                        dd.range = {start:start,end:{}};
+                        return $("<div class='slick-selection'></div>").appendTo($canvas);
+                    }
+                })
+                .bind("drag", function(e,dd) {
+                    if (dd.mode == MOVE_ROWS) {
+                        var top = e.pageY - $(this).offset().top;
+                        dd.selectionProxy.css("top",top-5);
+
+                        var insertBefore = Math.max(0,Math.min(Math.round(top/options.rowHeight),gridDataGetLength()));
+                        if (insertBefore !== dd.insertBefore) {
+                            if (self.onBeforeMoveRows && self.onBeforeMoveRows(getSelectedRows(),insertBefore) === false) {
+                                dd.guide.css("top", -1000);
+                                dd.canMove = false;
+                            }
+                            else {
+                                dd.guide.css("top",insertBefore*options.rowHeight);
+                                dd.canMove = true;
+                            }
+                            dd.insertBefore = insertBefore;
+                        }
+                    }
+
+                    if (dd.mode == SELECT_CELLS) {
+                        var end = getCellFromPoint(e.clientX - $canvas.offset().left, e.clientY - $canvas.offset().top);
+                        if (!cellExists(end.row,end.cell))
+                            return;
+
+                        dd.range.end = end;
+                        var r = fixUpRange(dd.range);
+                        var from = getCellNodeBox(r.start.row,r.start.cell);
+                        var to = getCellNodeBox(r.end.row,r.end.cell);
+                        $(dd.proxy).css({
+                            top: from.top,
+                            left: from.left,
+                            height: to.bottom - from.top - 2,
+                            width: to.right - from.left - 2
+                        });
+                    }
+                })
+                .bind("dragend", function(e,dd) {
+                    if (dd.mode == MOVE_ROWS) {
+                        dd.guide.remove();
+                        dd.selectionProxy.remove();
+                        if (self.onMoveRows && dd.canMove) {
+                            self.onMoveRows(getSelectedRows(),dd.insertBefore);
+                        }
+                    }
+
+                    if (dd.mode == SELECT_CELLS) {
+                        $(dd.proxy).remove();
+
+                        if (self.onCellRangeSelected)
+                            self.onCellRangeSelected(fixUpRange(dd.range));
+                    }
+                });
+        }
+
+        function measureCellPaddingAndBorder() {
+            var tmp = $("<div class='ui-state-default slick-header-column' style='visibility:hidden'>-</div>").appendTo($headers);
+            headerColumnWidthDiff = tmp.outerWidth() - tmp.width();
+            headerColumnHeightDiff = tmp.outerHeight() - tmp.height();
+            tmp.remove();
+
+            var r = $("<div class='slick-row' />").appendTo($canvas);
+            tmp = $("<div class='slick-cell' id='' style='visibility:hidden'>-</div>").appendTo(r);
+            cellWidthDiff = tmp.outerWidth() - tmp.width();
+            cellHeightDiff = tmp.outerHeight() - tmp.height();
+            r.remove();
+
+            absoluteColumnMinWidth = Math.max(headerColumnWidthDiff,cellWidthDiff);
+        }
+
+        function createCssRules() {
+            $style = $("<style type='text/css' rel='stylesheet' />").appendTo($("head"));
+            var rowHeight = (options.rowHeight - cellHeightDiff);
+
+            var rules = [
+                "." + uid + " .slick-header-column { left: 10000px; }",
+                "." + uid + " .slick-header-columns-secondary {  height:" + options.secondaryHeaderRowHeight + "px; }",
+                "." + uid + " .slick-cell { height:" + rowHeight + "px; }"
+            ];
+
+            for (var i=0; i<columns.length; i++) {
+                rules.push(
+                    "." + uid + " .c" + i + " { " +
+                    "width:" + ((columns[i].currentWidth || columns[i].width) - cellWidthDiff) + "px; " +
+                    " } ");
+            }
+
+            if ($style[0].styleSheet) { // IE
+                $style[0].styleSheet.cssText = rules.join("");
+            }
+            else {
+                $style[0].appendChild(document.createTextNode(rules.join(" ")));
+            }
+
+            var sheets = document.styleSheets;
+            for (var i=0; i<sheets.length; i++) {
+                if ((sheets[i].ownerNode || sheets[i].owningElement) == $style[0]) {
+                    stylesheet = sheets[i];
+                    break;
+                }
+            }
+        }
+
+        function findCssRule(selector) {
+            var rules = (stylesheet.cssRules || stylesheet.rules);
+
+            for (var i=0; i<rules.length; i++) {
+                if (rules[i].selectorText == selector)
+                    return rules[i];
+            }
+
+            return null;
+        }
+
+        function findCssRuleForCell(index) {
+            return findCssRule("." + uid + " .c" + index);
+        }
+
+        function removeCssRules() {
+            $style.remove();
+        }
+
+        function destroy() {
+            options.editorLock.cancelCurrentEdit();
+
+            if (self.onBeforeDestroy)
+                self.onBeforeDestroy();
+
+            if (options.enableColumnReorder && $headers.sortable) 
+                $headers.sortable("destroy");
+
+            unbindAncestorScrollEvents();
+            $container.unbind(".slickgrid");
+            removeCssRules();
+
+            $canvas.unbind("draginit dragstart dragend drag");
+            $container.empty().removeClass(uid);
+        }
+
+
+        //////////////////////////////////////////////////////////////////////////////////////////////
+        // General
+
+        function getEditController() {
+            return editController;
+        }
+
+        function getColumnIndex(id) {
+            return columnsById[id];
+        }
+
+        function autosizeColumns() {
+            var i, c,
+                widths = [],
+                shrinkLeeway = 0,
+                viewportW = $viewport.innerWidth(), // may not be initialized yet
+                availWidth = (options.autoHeight ? viewportW : viewportW - scrollbarDimensions.width), // with AutoHeight, we do not need to accomodate the vertical scroll bar
+                total = 0,
+                existingTotal = 0;
+
+            for (i = 0; i < columns.length; i++) {
+                c = columns[i];
+                widths.push(c.width);
+                existingTotal += c.width;
+                shrinkLeeway += c.width - Math.max(c.minWidth || 0, absoluteColumnMinWidth);
+            }
+
+            total = existingTotal;
+
+            removeAllRows();
+
+            // shrink
+            while (total > availWidth) {
+                if (!shrinkLeeway) { return; }
+                var shrinkProportion = (total - availWidth) / shrinkLeeway;
+                for (i = 0; i < columns.length && total > availWidth; i++) {
+                    c = columns[i];
+                    if (!c.resizable || c.minWidth === c.width || c.width === absoluteColumnMinWidth) { continue; }
+                    var shrinkSize = Math.floor(shrinkProportion * (c.width - Math.max(c.minWidth || 0, absoluteColumnMinWidth))) || 1;
+                    total -= shrinkSize;
+                    widths[i] -= shrinkSize;
+                }
+            }
+
+            // grow
+            var previousTotal = total;
+            while (total < availWidth) {
+                var growProportion = availWidth / total;
+                for (i = 0; i < columns.length && total < availWidth; i++) {
+                    c = columns[i];
+                    if (!c.resizable || c.maxWidth <= c.width) { continue; }
+                    var growSize = Math.min(Math.floor(growProportion * c.width) - c.width, (c.maxWidth - c.width) || 1000000) || 1;
+                    total += growSize;
+                    widths[i] += growSize;
+                }
+                if (previousTotal == total) break; // if total is not changing, will result in infinite loop
+                previousTotal = total;
+            }
+
+            for (i=0; i<columns.length; i++) {
+                styleColumnWidth(i, columns[i].currentWidth = widths[i], true);
+            }
+
+            resizeCanvas();
+        }
+
+        function styleColumnWidth(index,width,styleCells) {
+            columns[index].currentWidth = width;
+            $headers.children().eq(index).css("width", width - headerColumnWidthDiff);
+            if (styleCells) {
+                findCssRuleForCell(index).style.width = (width - cellWidthDiff) + "px";
+            }
+        }
+
+        function setSortColumn(columnId, ascending) {
+            sortColumnId = columnId;
+            sortAsc = ascending;
+            var columnIndex = getColumnIndex(sortColumnId);
+
+            $headers.children().removeClass("slick-header-column-sorted");
+            $headers.find(".slick-sort-indicator").removeClass("slick-sort-indicator-asc slick-sort-indicator-desc");
+
+            if (columnIndex != null) {
+                $headers.children().eq(columnIndex)
+                    .addClass("slick-header-column-sorted")
+                    .find(".slick-sort-indicator")
+                        .addClass(sortAsc ? "slick-sort-indicator-asc" : "slick-sort-indicator-desc");
+            }
+        }
+
+        function getSelectedRows() {
+            return selectedRows.concat();
+        }
+
+        function setSelectedRows(rows) {
+            var i, row;
+            var lookup = {};
+            for (i=0; i<rows.length; i++) {
+                lookup[rows[i]] = true;
+            }
+
+            // unselect old rows
+            for (i=0; i<selectedRows.length; i++) {
+                row = selectedRows[i];
+                if (rowsCache[row] && !lookup[row]) {
+                    $(rowsCache[row]).removeClass("ui-state-active selected");
+                }
+            }
+
+            // select new ones
+            for (i=0; i<rows.length; i++) {
+                row = rows[i];
+                if (rowsCache[row] && !selectedRowsLookup[row]) {
+                    $(rowsCache[row]).addClass("ui-state-active selected");
+                }
+            }
+
+            selectedRows = rows.concat();
+            selectedRowsLookup = lookup;
+        }
+
+        function getColumns() {
+            return columns;
+        }
+
+        function setColumns(columnDefinitions) {
+            columns = columnDefinitions;
+            removeAllRows();
+            createColumnHeaders();
+            removeCssRules();
+            createCssRules();
+            resizeAndRender();
+            handleScroll();
+        }
+
+        function getOptions() {
+            return options;
+        }
+
+        function setOptions(args) {
+            if (!options.editorLock.commitCurrentEdit()) {
+                return;
+            }
+
+            makeSelectedCellNormal();
+
+            if (options.enableAddRow !== args.enableAddRow) {
+                removeRow(gridDataGetLength());
+            }
+
+            options = $.extend(options,args);
+
+            render();
+        }
+
+        function setData(newData,scrollToTop) {
+            removeAllRows();
+            data = newData;
+            gridData = data;
+            gridDataGetLength = gridData.getLength || defaultGetLength;
+            gridDataGetItem = gridData.getItem || defaultGetItem;
+            if (scrollToTop)
+                scrollTo(0);
+        }
+
+        function getData() {
+            return gridData;
+        }
+
+        function getSecondaryHeaderRow() {
+            return $secondaryHeaders[0];
+        }
+
+        function showSecondaryHeaderRow() {
+            options.showSecondaryHeaderRow = true;
+            $secondaryHeaderScroller.slideDown("fast", resizeCanvas);
+        }
+
+        function hideSecondaryHeaderRow() {
+            options.showSecondaryHeaderRow = false;
+            $secondaryHeaderScroller.slideUp("fast", resizeCanvas);
+        }
+
+        //////////////////////////////////////////////////////////////////////////////////////////////
+        // Rendering / Scrolling
+
+        function scrollTo(y) {
+            var oldOffset = offset;
+
+            page = Math.min(n-1, Math.floor(y / ph));
+            offset = Math.round(page * cj);
+            var newScrollTop = y - offset;
+
+            if (offset != oldOffset) {
+                var range = getVisibleRange(newScrollTop);
+                cleanupRows(range.top,range.bottom);
+                updateRowPositions();
+            }
+
+            if (prevScrollTop != newScrollTop) {
+                scrollDir = (prevScrollTop + oldOffset < newScrollTop + offset) ? 1 : -1;
+                $viewport[0].scrollTop = (lastRenderedScrollTop = scrollTop = prevScrollTop = newScrollTop);
+
+                if (self.onViewportChanged) {
+                    self.onViewportChanged();
+                }
+            }
+        }
+
+        function defaultFormatter(row, cell, value, columnDef, dataContext) {
+            return (value === null || value === undefined) ? "" : value;
+        }
+
+        function getFormatter(column) {
+            return column.formatter ||
+                    (options.formatterFactory && options.formatterFactory.getFormatter(column)) ||
+                    defaultFormatter;
+        }
+
+        function getEditor(column) {
+            return column.editor || (options.editorFactory && options.editorFactory.getEditor(column));
+        }
+
+        function appendRowHtml(stringArray,row) {
+            var d = gridDataGetItem(row);
+            var dataLoading = row < gridDataGetLength() && !d;
+            var cellCss;
+            var css = "slick-row " +
+                (dataLoading ? " loading" : "") +
+                (selectedRowsLookup[row] ? " selected ui-state-active" : "") +
+                (row % 2 == 1 ? ' odd' : ' even');
+
+            // if the user has specified a function to provide additional per-row css classes, call it here
+            if (options.rowCssClasses) {
+                css += ' ' + options.rowCssClasses(d);
+            }
+
+            stringArray.push("<div class='ui-widget-content " + css + "' row='" + row + "' style='top:" + (options.rowHeight*row-offset) + "px'>");
+
+            for (var i=0, cols=columns.length; i<cols; i++) {
+                var m = columns[i];
+
+                cellCss = "slick-cell c" + i + (m.cssClass ? " " + m.cssClass : "");
+                if (highlightedCells && highlightedCells[row] && highlightedCells[row][m.id])
+                    cellCss += (" " + options.cellHighlightCssClass);
+
+                stringArray.push("<div class='" + cellCss + "'>");
+
+                // if there is a corresponding row (if not, this is the Add New row or this data hasn't been loaded yet)
+                if (d) {
+                    stringArray.push(getFormatter(m)(row, i, d[m.field], m, d));
+                }
+
+                stringArray.push("</div>");
+            }
+
+            stringArray.push("</div>");
+        }
+
+        function cleanupRows(rangeToKeep) {
+            for (var i in rowsCache) {
+                if (((i = parseInt(i, 10)) !== currentRow) && (i < rangeToKeep.top || i > rangeToKeep.bottom)) {
+                    removeRowFromCache(i);
+                }
+            }
+        }
+
+        function invalidate() {
+           updateRowCount();
+           removeAllRows();
+           render();
+        }
+
+        function removeAllRows() {
+            if (currentEditor) {
+                makeSelectedCellNormal();
+            }
+            $canvas[0].innerHTML = "";
+            rowsCache= {};
+            postProcessedRows = {};
+            counter_rows_removed += renderedRows;
+            renderedRows = 0;
+        }
+
+        function removeRowFromCache(row) {
+            var node = rowsCache[row];
+            if (!node) { return; }
+            $canvas[0].removeChild(node);
+
+            delete rowsCache[row];
+            delete postProcessedRows[row];
+            renderedRows--;
+            counter_rows_removed++;
+        }
+
+        function removeRows(rows) {
+            var i, rl, nl;
+            if (!rows || !rows.length) { return; }
+            scrollDir = 0;
+            var nodes = [];
+            for (i=0, rl=rows.length; i<rl; i++) {
+                if (currentEditor && currentRow === i) {
+                    makeSelectedCellNormal();
+                }
+
+                if (rowsCache[rows[i]]) {
+                    nodes.push(rows[i]);
+                }
+            }
+
+            if (renderedRows > 10 && nodes.length === renderedRows) {
+                removeAllRows();
+            }
+            else {
+                for (i=0, nl=nodes.length; i<nl; i++) {
+                    removeRowFromCache(nodes[i]);
+                }
+            }
+        }
+
+        function removeRow(row) {
+            removeRows([row]);
+        }
+
+        function updateCell(row,cell) {
+            if (!rowsCache[row]) { return; }
+            var $cell = $(rowsCache[row]).children().eq(cell);
+            if ($cell.length === 0) { return; }
+
+            var m = columns[cell], d = gridDataGetItem(row);
+            if (currentEditor && currentRow === row && currentCell === cell) {
+                currentEditor.loadValue(d);
+            }
+            else {
+                $cell[0].innerHTML = d ? getFormatter(m)(row, cell, d[m.field], m, d) : "";
+                invalidatePostProcessingResults(row);
+            }
+        }
+
+        function updateRow(row) {
+            if (!rowsCache[row]) { return; }
+
+            $(rowsCache[row]).children().each(function(i) {
+                var m = columns[i];
+                if (row === currentRow && i === currentCell && currentEditor) {
+                    currentEditor.loadValue(gridDataGetItem(currentRow));
+                }
+                else if (gridDataGetItem(row)) {
+                    this.innerHTML = getFormatter(m)(row, i, gridDataGetItem(row)[m.field], m, gridDataGetItem(row));
+                }
+                else {
+                    this.innerHTML = "";
+                }
+            });
+
+            invalidatePostProcessingResults(row);
+        }
+
+        function resizeCanvas() {
+            var newViewportH = options.rowHeight * (gridDataGetLength() + (options.enableAddRow ? 1 : 0) + (options.leaveSpaceForNewRows? numVisibleRows - 1 : 0));
+            if (options.autoHeight) { // use computed height to set both canvas _and_ divMainScroller, effectively hiding scroll bars.
+                $viewport.height(newViewportH);
+            }
+            else {
+                $viewport.height(
+                        $container.innerHeight() -
+                        $headerScroller.outerHeight() -
+                        (options.showSecondaryHeaderRow ? $secondaryHeaderScroller.outerHeight() : 0));
+            }
+
+            viewportW = $viewport.innerWidth();
+            viewportH = $viewport.innerHeight();
+            numVisibleRows = Math.ceil(viewportH / options.rowHeight);
+
+            var totalWidth = 0;
+            $headers.find(".slick-header-column").each(function() {
+                totalWidth += $(this).outerWidth();
+            });
+            setCanvasWidth(totalWidth);
+
+            updateRowCount();
+            render();
+        }
+
+        function resizeAndRender() {
+            if (options.forceFitColumns) {
+                autosizeColumns();
+            } else {
+                resizeCanvas();
+            }
+        }
+
+        function updateRowCount() {
+            var newRowCount = gridDataGetLength() + (options.enableAddRow?1:0) + (options.leaveSpaceForNewRows?numVisibleRows-1:0);
+            var oldH = h;
+
+            // remove the rows that are now outside of the data range
+            // this helps avoid redundant calls to .removeRow() when the size of the data decreased by thousands of rows
+            var l = options.enableAddRow ? gridDataGetLength() : gridDataGetLength() - 1;
+            for (var i in rowsCache) {
+                if (i >= l) {
+                    removeRowFromCache(i);
+                }
+            }
+            th = Math.max(options.rowHeight * newRowCount, viewportH - scrollbarDimensions.height);
+            if (th < maxSupportedCssHeight) {
+                // just one page
+                h = ph = th;
+                n = 1;
+                cj = 0;
+            }
+            else {
+                // break into pages
+                h = maxSupportedCssHeight;
+                ph = h / 100;
+                n = Math.floor(th / ph);
+                cj = (th - h) / (n - 1);
+            }
+
+            if (h !== oldH) {
+                $canvas.css("height",h);
+                scrollTop = $viewport[0].scrollTop;
+            }
+
+            var oldScrollTopInRange = (scrollTop + offset <= th - viewportH);
+
+            if (th == 0 || scrollTop == 0) {
+                page = offset = 0;
+            }
+            else if (oldScrollTopInRange) {
+                // maintain virtual position
+                scrollTo(scrollTop+offset);
+            }
+            else {
+                // scroll to bottom
+                scrollTo(th-viewportH);
+            }
+
+            if (h != oldH && options.autoHeight) {
+                resizeCanvas();
+            }
+        }
+
+        function getVisibleRange(viewportTop) {
+            if (viewportTop == null)
+                viewportTop = scrollTop;
+
+            return {
+                top: Math.floor((scrollTop+offset)/options.rowHeight),
+                bottom: Math.ceil((scrollTop+offset+viewportH)/options.rowHeight)
+            };
+        }
+
+        function getRenderedRange(viewportTop) {
+            var range = getVisibleRange(viewportTop);
+            var buffer = Math.round(viewportH/options.rowHeight);
+            var minBuffer = 3;
+
+            if (scrollDir == -1) {
+                range.top -= buffer;
+                range.bottom += minBuffer;
+            }
+            else if (scrollDir == 1) {
+                range.top -= minBuffer;
+                range.bottom += buffer;
+            }
+            else {
+                range.top -= minBuffer;
+                range.bottom += minBuffer;
+            }
+
+            range.top = Math.max(0,range.top);
+            range.bottom = Math.min(options.enableAddRow ? gridDataGetLength() : gridDataGetLength() - 1,range.bottom);
+
+            return range;
+        }
+
+        function renderRows(range) {
+            var i, l,
+                parentNode = $canvas[0],
+                rowsBefore = renderedRows,
+                stringArray = [],
+                rows = [],
+                startTimestamp = new Date(),
+                needToReselectCell = false;
+
+            for (i = range.top; i <= range.bottom; i++) {
+                if (rowsCache[i]) { continue; }
+                renderedRows++;
+                rows.push(i);
+                appendRowHtml(stringArray,i);
+                if (currentCellNode && currentRow === i)
+                    needToReselectCell = true;
+                counter_rows_rendered++;
+            }
+
+            var x = document.createElement("div");
+            x.innerHTML = stringArray.join("");
+
+            for (i = 0, l = x.childNodes.length; i < l; i++) {
+                rowsCache[rows[i]] = parentNode.appendChild(x.firstChild);
+            }
+
+            if (needToReselectCell) {
+                currentCellNode = $(rowsCache[currentRow]).children().eq(currentCell)[0];
+                setSelectedCell(currentCellNode,false);
+            }
+
+            if (renderedRows - rowsBefore > 5) {
+                avgRowRenderTime = (new Date() - startTimestamp) / (renderedRows - rowsBefore);
+            }
+        }
+
+        function startPostProcessing() {
+            if (!options.enableAsyncPostRender) { return; }
+            clearTimeout(h_postrender);
+            h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
+        }
+
+        function invalidatePostProcessingResults(row) {
+            delete postProcessedRows[row];
+            postProcessFromRow = Math.min(postProcessFromRow,row);
+            postProcessToRow = Math.max(postProcessToRow,row);
+            startPostProcessing();
+        }
+
+        function updateRowPositions() {
+            for (var row in rowsCache) {
+                rowsCache[row].style.top = (row*options.rowHeight-offset) + "px";
+            }
+        }
+
+        function render() {
+            var visible = getVisibleRange();
+            var rendered = getRenderedRange();
+
+            // remove rows no longer in the viewport
+            cleanupRows(rendered);
+
+            // add new rows
+            renderRows(rendered);
+
+            postProcessFromRow = visible.top;
+            postProcessToRow = Math.min(options.enableAddRow ? gridDataGetLength() : gridDataGetLength() - 1, visible.bottom);
+            startPostProcessing();
+
+            lastRenderedScrollTop = scrollTop;
+            h_render = null;
+        }
+
+        function handleScroll() {
+            scrollTop = $viewport[0].scrollTop;
+            var scrollLeft = $viewport[0].scrollLeft;
+            var scrollDist = Math.abs(scrollTop - prevScrollTop);
+
+            if (scrollLeft !== prevScrollLeft) {
+                prevScrollLeft = scrollLeft;
+                $headerScroller[0].scrollLeft = scrollLeft;
+                $secondaryHeaderScroller[0].scrollLeft = scrollLeft;
+            }
+
+            if (!scrollDist) return;
+
+            scrollDir = prevScrollTop < scrollTop ? 1 : -1;
+            prevScrollTop = scrollTop;
+
+            // switch virtual pages if needed
+            if (scrollDist < viewportH) {
+                scrollTo(scrollTop + offset);
+            }
+            else {
+                var oldOffset = offset;
+                page = Math.min(n - 1, Math.floor(scrollTop * ((th - viewportH) / (h - viewportH)) * (1 / ph)));
+                offset = Math.round(page * cj);
+                if (oldOffset != offset)
+                    removeAllRows();
+            }
+
+            if (h_render)
+                clearTimeout(h_render);
+
+            if (Math.abs(lastRenderedScrollTop - scrollTop) < viewportH)
+                render();
+            else
+                h_render = setTimeout(render, 50);
+
+            if (self.onViewportChanged) {
+                self.onViewportChanged();
+            }
+        }
+
+        function asyncPostProcessRows() {
+            while (postProcessFromRow <= postProcessToRow) {
+                var row = (scrollDir >= 0) ? postProcessFromRow++ : postProcessToRow--;
+                var rowNode = rowsCache[row];
+                if (!rowNode || postProcessedRows[row] || row>=gridDataGetLength()) { continue; }
+
+                var d = gridDataGetItem(row), cellNodes = rowNode.childNodes;
+                for (var i=0, j=0, l=columns.length; i<l; ++i) {
+                    var m = columns[i];
+                    if (m.asyncPostRender) { m.asyncPostRender(cellNodes[j], postProcessFromRow, d, m); }
+                    ++j;
+                }
+
+                postProcessedRows[row] = true;
+                h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
+                return;
+            }
+        }
+
+        function setHighlightedCells(cellsToHighlight) {
+            var i, $cell, hasHighlight, hadHighlight;
+
+            for (var row in rowsCache) {
+                for (i=0; i<columns.length; i++) {
+                    hadHighlight = highlightedCells && highlightedCells[row] && highlightedCells[row][columns[i].id];
+                    hasHighlight = cellsToHighlight && cellsToHighlight[row] && cellsToHighlight[row][columns[i].id];
+
+                    if (hadHighlight != hasHighlight) {
+                        $cell = $(rowsCache[row]).children().eq(i);
+                        if ($cell.length) {
+                            $cell.toggleClass(options.cellHighlightCssClass);
+                        }
+                    }
+                }
+            }
+
+            highlightedCells = cellsToHighlight;
+        }
+
+        function flashCell(row, cell, speed) {
+            speed = speed || 100;
+            if (rowsCache[row]) {
+                var $cell = $(rowsCache[row]).children().eq(cell);
+
+                function toggleCellClass(times) {
+                    if (!times) return;
+                    setTimeout(function() {
+                        $cell.queue(function() {
+                            $cell.toggleClass(options.cellFlashingCssClass).dequeue();
+                            toggleCellClass(times-1);
+                        });
+                    },
+                    speed);
+                }
+
+                toggleCellClass(4);
+            }
+        }
+
+        //////////////////////////////////////////////////////////////////////////////////////////////
+        // Interactivity
+
+        function getSiblingIndex(node) {
+            var idx = 0;
+            while (node && node.previousSibling) {
+                idx++;
+                node = node.previousSibling;
+            }
+            return idx;
+        }
+
+        function handleKeyDown(e) {
+            // give registered handler chance to process the keyboard event
+            var handled = (self.onKeyDown && // a handler must be registered
+                !options.editorLock.isActive() && // grid must not be in edit mode;
+                self.onKeyDown(e, currentRow, currentCell)); // handler must return truthy-value to indicate it handled the event
+
+            if (!handled) {
+                if (!e.shiftKey && !e.altKey && !e.ctrlKey) {
+                    if (e.which == 27) {
+                        if (!options.editorLock.isActive()) {
+                            return; // no editing mode to cancel, allow bubbling and default processing (exit without cancelling the event)
+                        }
+                        cancelEditAndSetFocus();
+                    }
+                    else if (e.which == 37) {
+                        navigateLeft();
+                    }
+                    else if (e.which == 39) {
+                        navigateRight();
+                    }
+                    else if (e.which == 38) {
+                        navigateUp();
+                    }
+                    else if (e.which == 40) {
+                        navigateDown();
+                    }
+                    else if (e.which == 9) {
+                        navigateNext();
+                    }
+                    else if (e.which == 13) {
+                        if (options.editable) {
+                            if (currentEditor) {
+                                // adding new row
+                                if (currentRow === defaultGetLength()) {
+                                    navigateDown();
+                                }
+                                else {
+                                    commitEditAndSetFocus();
+                                }
+                            } else {
+                                if (options.editorLock.commitCurrentEdit()) {
+                                    makeSelectedCellEditable();
+                                }
+                            }
+                        }
+                    }
+                    else
+                        return;
+                }
+                else if (e.which == 9 && e.shiftKey && !e.ctrlKey && !e.altKey) {
+                        navigatePrev();
+                }
+                else
+                    return;
+            }
+
+            // the event has been handled so don't let parent element (bubbling/propagation) or browser (default) handle it
+            e.stopPropagation();
+            e.preventDefault();
+            try {
+                e.originalEvent.keyCode = 0; // prevent default behaviour for special keys in IE browsers (F3, F5, etc.)
+            }
+            catch (error) {} // ignore exceptions - setting the original event's keycode throws access denied exception for "Ctrl" (hitting control key only, nothing else), "Shift" (maybe others)
+        }
+
+        function handleClick(e) {
+            var $cell = $(e.target).closest(".slick-cell", $canvas);
+            if ($cell.length === 0) { return; }
+
+            // are we editing this cell?
+            if (currentCellNode === $cell[0] && currentEditor !== null) { return; }
+
+            var row = parseInt($cell.parent().attr("row"), 10);
+            var cell = getSiblingIndex($cell[0]);
+            var validated = null;
+            var c = columns[cell];
+            var item = gridDataGetItem(row);
+
+            // is this a 'select' column or a Ctrl/Shift-click?
+            if (item && (c.behavior === "selectAndMove" || c.behavior === "select" || (e.ctrlKey || e.shiftKey))) {
+                // grid must not be in edit mode
+                validated = options.editorLock.commitCurrentEdit();
+                if (validated) {
+                    var selection = getSelectedRows();
+                    var idx = $.inArray(row, selection);
+
+                    if (!e.ctrlKey && !e.shiftKey && !e.metaKey) {
+                        selection = [row];
+                    }
+                    else if (options.multiSelect) {
+                        if (idx === -1 && (e.ctrlKey || e.metaKey)) {
+                            selection.push(row);
+                        }
+                        else if (idx !== -1 && (e.ctrlKey || e.metaKey)) {
+                            selection = $.grep(selection, function(o, i) { return (o !== row); });
+                        }
+                        else if (selection.length && e.shiftKey) {
+                            var last = selection.pop();
+                            var from = Math.min(row, last);
+                            var to = Math.max(row, last);
+                            selection = [];
+                            for (var i = from; i <= to; i++) {
+                                if (i !== last) {
+                                    selection.push(i);
+                                }
+                            }
+                            selection.push(last);
+                        }
+                    }
+                    resetCurrentCell();
+                    setSelectedRows(selection);
+                    if (self.onSelectedRowsChanged) {
+                        self.onSelectedRowsChanged();
+                    }
+
+                    if (!$.browser.msie) {
+                        $canvas[0].focus();
+                    }
+
+                    return false;
+                }
+            }
+
+            // do we have any registered handlers?
+            if (item && self.onClick) {
+                // grid must not be in edit mode
+                validated = options.editorLock.commitCurrentEdit();
+                if (validated) {
+                    // handler will return true if the event was handled
+                    if (self.onClick(e, row, cell)) {
+                        e.stopPropagation();
+                        e.preventDefault();
+                        return false;
+                    }
+                }
+            }
+
+            if (options.enableCellNavigation && !columns[cell].unselectable) {
+                // commit current edit before proceeding
+                if (validated === true || (validated === null && options.editorLock.commitCurrentEdit())) {
+                    scrollRowIntoView(row,false);
+                    setSelectedCellAndRow($cell[0], (row === defaultGetLength()) || options.autoEdit);
+                }
+            }
+        }
+
+        function handleContextMenu(e) {
+            var $cell = $(e.target).closest(".slick-cell", $canvas);
+            if ($cell.length === 0) { return; }
+
+            // are we editing this cell?
+            if (currentCellNode === $cell[0] && currentEditor !== null) { return; }
+
+            var row = parseInt($cell.parent().attr("row"), 10);
+            var cell = getSiblingIndex($cell[0]);
+            var validated = null;
+
+            // do we have any registered handlers?
+            if (gridDataGetItem(row) && self.onContextMenu) {
+                // grid must not be in edit mode
+                validated = options.editorLock.commitCurrentEdit();
+                if (validated) {
+                    // handler will return true if the event was handled
+                    if (self.onContextMenu(e, row, cell)) {
+                        e.stopPropagation();
+                        e.preventDefault();
+                        return false;
+                    }
+                }
+            }
+        }
+
+        function handleDblClick(e) {
+            var $cell = $(e.target).closest(".slick-cell", $canvas);
+            if ($cell.length === 0) { return; }
+
+            // are we editing this cell?
+            if (currentCellNode === $cell[0] && currentEditor !== null) { return; }
+
+            var row = parseInt($cell.parent().attr("row"), 10);
+            var cell = getSiblingIndex($cell[0]);
+            var validated = null;
+
+            // do we have any registered handlers?
+            if (gridDataGetItem(row) && self.onDblClick) {
+                // grid must not be in edit mode
+                validated = options.editorLock.commitCurrentEdit();
+                if (validated) {
+                    // handler will return true if the event was handled
+                    if (self.onDblClick(e, row, cell)) {
+                        e.stopPropagation();
+                        e.preventDefault();
+                        return false;
+                    }
+                }
+            }
+
+            if (options.editable) {
+                gotoCell(row, cell, true);
+            }
+        }
+
+        function handleHeaderContextMenu(e) {
+            if (self.onHeaderContextMenu && options.editorLock.commitCurrentEdit()) {
+                e.preventDefault();
+                var selectedElement = $(e.target).closest(".slick-header-column", ".slick-header-columns");
+                self.onHeaderContextMenu(e, columns[self.getColumnIndex(selectedElement.data("fieldId"))]);
+            }
+        }
+
+        function handleHeaderClick(e) {
+            var $col = $(e.target).closest(".slick-header-column");
+            if ($col.length ==0) { return; }
+            var column = columns[getSiblingIndex($col[0])];
+
+            if (self.onHeaderClick && options.editorLock.commitCurrentEdit()) {
+                e.preventDefault();
+                self.onHeaderClick(e, column);
+            }
+        }
+
+        function handleHover(e) {
+            if (!options.enableAutoTooltips) return;
+            var $cell = $(e.target).closest(".slick-cell",$canvas);
+            if ($cell.length) {
+                if ($cell.innerWidth() < $cell[0].scrollWidth) {
+                    var text = $.trim($cell.text());
+                    $cell.attr("title", (options.toolTipMaxLength && text.length > options.toolTipMaxLength) ?  text.substr(0, options.toolTipMaxLength - 3) + "..." : text);
+                }
+                else {
+                    $cell.attr("title","");
+                }
+            }
+        }
+
+        function cellExists(row,cell) {
+            return !(row < 0 || row >= gridDataGetLength() || cell < 0 || cell >= columns.length);
+        }
+
+        function getCellFromPoint(x,y) {
+            var row = Math.floor((y+offset)/options.rowHeight);
+            var cell = 0;
+
+            var w = 0;
+            for (var i=0; i<columns.length && w<x; i++) {
+                w += columns[i].width;
+                cell++;
+            }
+
+            return {row:row,cell:cell-1};
+        }
+
+        function getCellFromEvent(e) {
+            var $cell = $(e.target).closest(".slick-cell", $canvas);
+            if (!$cell.length)
+                return null;
+
+            return {
+                row: $cell.parent().attr("row") | 0,
+                cell: getSiblingIndex($cell[0])
+            };
+        }
+
+        function getCellNodeBox(row,cell) {
+             if (!cellExists(row,cell))
+                 return null;
+
+             var y1 = row * options.rowHeight - offset;
+             var y2 = y1 + options.rowHeight - 1;
+             var x1 = 0;
+             for (var i=0; i<cell; i++) {
+                 x1 += columns[i].width;
+             }
+             var x2 = x1 + columns[cell].width;
+
+             return {
+                 top: y1,
+                 left: x1,
+                 bottom: y2,
+                 right: x2
+             };
+         }
+
+        //////////////////////////////////////////////////////////////////////////////////////////////
+        // Cell switching
+
+        function resetCurrentCell() {
+            setSelectedCell(null,false);
+        }
+
+        function focusOnCurrentCell() {
+            // lazily enable the cell to receive keyboard focus
+            $(currentCellNode)
+                .attr("tabIndex",0)
+                .attr("hideFocus",true);
+
+            // IE7 tries to scroll the viewport so that the item being focused is aligned to the left border
+            // IE-specific .setActive() sets the focus, but doesn't scroll
+            if ($.browser.msie && parseInt($.browser.version) < 8)
+                currentCellNode.setActive();
+            else
+                currentCellNode.focus();
+
+            var left = $(currentCellNode).position().left,
+                right = left + $(currentCellNode).outerWidth(),
+                scrollLeft = $viewport.scrollLeft(),
+                scrollRight = scrollLeft + $viewport.width();
+
+            if (left < scrollLeft)
+                $viewport.scrollLeft(left);
+            else if (right > scrollRight)
+                $viewport.scrollLeft(Math.min(left, right - $viewport[0].clientWidth));
+        }
+
+        function setSelectedCell(newCell,editMode) {
+            if (currentCellNode !== null) {
+                makeSelectedCellNormal();
+                $(currentCellNode).removeClass("selected");
+            }
+
+            currentCellNode = newCell;
+
+            if (currentCellNode != null) {
+                currentRow = parseInt($(currentCellNode).parent().attr("row"), 10);
+                currentCell = getSiblingIndex(currentCellNode);
+
+                $(currentCellNode).addClass("selected");
+
+                if (options.editable && editMode && isCellPotentiallyEditable(currentRow,currentCell)) {
+                    clearTimeout(h_editorLoader);
+
+                    if (options.asyncEditorLoading) {
+                        h_editorLoader = setTimeout(makeSelectedCellEditable, options.asyncEditorLoadDelay);
+                    }
+                    else {
+                        makeSelectedCellEditable();
+                    }
+                }
+                else {
+                    focusOnCurrentCell()
+                }
+                if (self.onCurrentCellChanged)
+                    self.onCurrentCellChanged(getCurrentCell());
+            }
+            else {
+                currentRow = null;
+                currentCell = null;
+            }
+        }
+
+        function setSelectedCellAndRow(newCell,editMode) {
+            setSelectedCell(newCell,editMode);
+
+            if (newCell) {
+                setSelectedRows([currentRow]);
+            }
+            else {
+                setSelectedRows([]);
+            }
+
+            if (self.onSelectedRowsChanged) {
+                self.onSelectedRowsChanged();
+            }
+        }
+
+        function clearTextSelection() {
+            if (document.selection && document.selection.empty) {
+                document.selection.empty();
+            }
+            else if (window.getSelection) {
+                var sel = window.getSelection();
+                if (sel && sel.removeAllRanges) {
+                    sel.removeAllRanges();
+                }
+            }
+        }
+
+        function isCellPotentiallyEditable(row,cell) {
+            // is the data for this row loaded?
+            if (row < gridDataGetLength() && !gridDataGetItem(row)) {
+                return false;
+            }
+
+            // are we in the Add New row?  can we create new from this cell?
+            if (columns[cell].cannotTriggerInsert && row >= gridDataGetLength()) {
+                return false;
+            }
+
+            // does this cell have an editor?
+            if (!getEditor(columns[cell])) {
+                return false;
+            }
+
+            return true;
+        }
+
+        function makeSelectedCellNormal() {
+            if (!currentEditor) { return; }
+
+            if (self.onBeforeCellEditorDestroy) {
+                self.onBeforeCellEditorDestroy(currentEditor);
+            }
+            currentEditor.destroy();
+            currentEditor = null;
+
+            if (currentCellNode) {
+                $(currentCellNode).removeClass("editable invalid");
+
+                if (gridDataGetItem(currentRow)) {
+                    var column = columns[currentCell];
+                    currentCellNode.innerHTML = getFormatter(column)(currentRow, currentCell, gridDataGetItem(currentRow)[column.field], column, gridDataGetItem(currentRow));
+                    invalidatePostProcessingResults(currentRow);
+                }
+            }
+
+            // if there previously was text selected on a page (such as selected text in the edit cell just removed),
+            // IE can't set focus to anything else correctly
+            if ($.browser.msie) { clearTextSelection(); }
+
+            options.editorLock.deactivate(editController);
+        }
+
+        function makeSelectedCellEditable() {
+            if (!currentCellNode) { return; }
+            if (!options.editable) {
+                throw "Grid : makeSelectedCellEditable : should never get called when options.editable is false";
+            }
+
+            // cancel pending async call if there is one
+            clearTimeout(h_editorLoader);
+
+            if (!isCellPotentiallyEditable(currentRow,currentCell)) {
+                return;
+            }
+
+            if (self.onBeforeEditCell && self.onBeforeEditCell(currentRow,currentCell,gridDataGetItem(currentRow)) === false) {
+                focusOnCurrentCell();
+                return;
+            }
+
+            options.editorLock.activate(editController);
+            $(currentCellNode).addClass("editable");
+
+            currentCellNode.innerHTML = "";
+
+            var columnDef = columns[currentCell];
+            var item = gridDataGetItem(currentRow);
+
+            currentEditor = new (getEditor(columnDef))({
+                grid: self,
+                gridPosition: absBox($container[0]),
+                position: absBox(currentCellNode),
+                container: currentCellNode,
+                column: columnDef,
+                item: item || {},
+                commitChanges: commitEditAndSetFocus,
+                cancelChanges: cancelEditAndSetFocus
+            });
+
+            if (item)
+                currentEditor.loadValue(item);
+
+            serializedEditorValue = currentEditor.serializeValue();
+
+            if (currentEditor.position)
+                handleCurrentCellPositionChange();
+        }
+
+        function commitEditAndSetFocus() {
+            // if the commit fails, it would do so due to a validation error
+            // if so, do not steal the focus from the editor
+            if (options.editorLock.commitCurrentEdit()) {
+                focusOnCurrentCell();
+
+                if (options.autoEdit) {
+                    navigateDown();
+                }
+            }
+        }
+
+        function cancelEditAndSetFocus() {
+            if (options.editorLock.cancelCurrentEdit()) {
+                focusOnCurrentCell();
+            }
+        }
+
+        function absBox(elem) {
+            var box = {top:elem.offsetTop, left:elem.offsetLeft, bottom:0, right:0, width:$(elem).outerWidth(), height:$(elem).outerHeight(), visible:true};
+            box.bottom = box.top + box.height;
+            box.right = box.left + box.width;
+
+            // walk up the tree
+            var offsetParent = elem.offsetParent;
+            while ((elem = elem.parentNode) != document.body) {
+                if (box.visible && elem.scrollHeight != elem.offsetHeight && $(elem).css("overflowY") != "visible")
+                    box.visible = box.bottom > elem.scrollTop && box.top < elem.scrollTop + elem.clientHeight;
+
+                if (box.visible && elem.scrollWidth != elem.offsetWidth && $(elem).css("overflowX") != "visible")
+                    box.visible = box.right > elem.scrollLeft && box.left < elem.scrollLeft + elem.clientWidth;
+
+                box.left -= elem.scrollLeft;
+                box.top -= elem.scrollTop;
+
+                if (elem === offsetParent) {
+                    box.left += elem.offsetLeft;
+                    box.top += elem.offsetTop;
+                    offsetParent = elem.offsetParent;
+                }
+
+                box.bottom = box.top + box.height;
+                box.right = box.left + box.width;
+            }
+
+            return box;
+        }
+
+        function getCurrentCellPosition(){
+            return absBox(currentCellNode);
+        }
+
+        function getGridPosition(){
+            return absBox($container[0])
+        }
+
+        function handleCurrentCellPositionChange() {
+            if (!currentCellNode) return;
+            var cellBox;
+
+            if (self.onCurrentCellPositionChanged){
+                cellBox = getCurrentCellPosition();
+                self.onCurrentCellPositionChanged(cellBox);
+            }
+
+            if (currentEditor) {
+                cellBox = cellBox || getCurrentCellPosition();
+                if (currentEditor.show && currentEditor.hide) {
+                    if (!cellBox.visible)
+                        currentEditor.hide();
+                    else
+                        currentEditor.show();
+                }
+
+                if (currentEditor.position)
+                    currentEditor.position(cellBox);
+            }
+        }
+
+        function getCellEditor() {
+            return currentEditor;
+        }
+
+        function getCurrentCell() {
+            if (!currentCellNode)
+                return null;
+            else
+                return {row: currentRow, cell: currentCell};
+        }
+
+        function getCurrentCellNode() {
+            return currentCellNode;
+        }
+
+        function scrollRowIntoView(row, doPaging) {
+            var rowAtTop = row * options.rowHeight;
+            var rowAtBottom = (row + 1) * options.rowHeight - viewportH + (viewportHasHScroll?scrollbarDimensions.height:0);
+
+            // need to page down?
+            if ((row + 1) * options.rowHeight > scrollTop + viewportH + offset) {
+                scrollTo(doPaging ? rowAtTop : rowAtBottom);
+                render();
+            }
+
+            // or page up?
+            else if (row * options.rowHeight < scrollTop + offset) {
+                scrollTo(doPaging ? rowAtBottom : rowAtTop);
+                render();
+            }
+        }
+
+        function gotoDir(dy, dx, rollover) {
+            if (!currentCellNode || !options.enableCellNavigation) { return; }
+            if (!options.editorLock.commitCurrentEdit()) { return; }
+
+            function selectableCellFilter() {
+                return !columns[getSiblingIndex(this)].unselectable
+            }
+
+            var nextRow = rowsCache[currentRow + dy];
+            var nextCell = (nextRow && currentCell + dx >= 0)
+                    ? $(nextRow).children().eq(currentCell+dx).filter(selectableCellFilter)
+                    : null;
+
+            if (nextCell && !nextCell.length) {
+                var nodes = $(nextRow).children()
+                        .filter(function(index) { return (dx>0) ? index > currentCell + dx : index < currentCell + dx })
+                        .filter(selectableCellFilter);
+
+                if (nodes && nodes.length) {
+                nextCell = (dx>0)
+                            ? nodes.eq(0)
+                            : nodes.eq(nodes.length-1);
+                }
+            }
+
+            if (rollover && dy === 0 && !(nextRow && nextCell && nextCell.length)) {
+                if (!nextCell || !nextCell.length) {
+                    nextRow = rowsCache[currentRow + dy + ((dx>0)?1:-1)];
+                    var nodes = $(nextRow).children().filter(selectableCellFilter);
+                    if (dx > 0) {
+                        nextCell = nextRow
+                                ? nodes.eq(0)
+                                : null;
+                    }
+                    else {
+                        nextCell = nextRow
+                                ? nodes.eq(nodes.length-1)
+                                : null;
+                    }
+                }
+            }
+
+            if (nextRow && nextCell && nextCell.length) {
+                // if selecting the 'add new' row, start editing right away
+                var row = parseInt($(nextRow).attr("row"), 10);
+                var isAddNewRow = (row == defaultGetLength());
+                scrollRowIntoView(row,!isAddNewRow);
+                setSelectedCellAndRow(nextCell[0], isAddNewRow || options.autoEdit);
+
+                // if no editor was created, set the focus back on the cell
+                if (!currentEditor) {
+                    focusOnCurrentCell();
+                }
+            }
+            else {
+                focusOnCurrentCell();
+            }
+        }
+
+        function gotoCell(row, cell, forceEdit) {
+            if (row > gridDataGetLength() || row < 0 || cell >= columns.length || cell < 0) { return; }
+            if (!options.enableCellNavigation || columns[cell].unselectable) { return; }
+
+            if (!options.editorLock.commitCurrentEdit()) { return; }
+
+            scrollRowIntoView(row,false);
+
+            var newCell = null;
+            if (!columns[cell].unselectable) {
+                newCell = $(rowsCache[row]).children().eq(cell)[0];
+            }
+
+            // if selecting the 'add new' row, start editing right away
+            setSelectedCellAndRow(newCell, forceEdit || (row === gridDataGetLength()) || options.autoEdit);
+
+            // if no editor was created, set the focus back on the cell
+            if (!currentEditor) {
+                focusOnCurrentCell();
+            }
+        }
+
+        function navigateUp() {
+            gotoDir(-1, 0, false);
+        }
+
+        function navigateDown() {
+            gotoDir(1, 0, false);
+        }
+
+        function navigateLeft() {
+            gotoDir(0, -1, false);
+        }
+
+        function navigateRight() {
+            gotoDir(0, 1, false);
+        }
+
+        function navigatePrev() {
+            gotoDir(0, -1, true);
+        }
+
+        function navigateNext() {
+            gotoDir(0, 1, true);
+        }
+
+        //////////////////////////////////////////////////////////////////////////////////////////////
+        // IEditor implementation for the editor lock
+
+        function commitCurrentEdit() {
+            var item = gridDataGetItem(currentRow);
+            var column = columns[currentCell];
+
+            if (currentEditor) {
+                if (currentEditor.isValueChanged()) {
+                    var validationResults = currentEditor.validate();
+
+                    if (validationResults.valid) {
+                        if (currentRow < gridDataGetLength()) {
+                            var editCommand = {
+                                row: currentRow,
+                                cell: currentCell,
+                                editor: currentEditor,
+                                serializedValue: currentEditor.serializeValue(),
+                                prevSerializedValue: serializedEditorValue,
+                                execute: function() {
+                                    this.editor.applyValue(item,this.serializedValue);
+                                    updateRow(this.row);
+                                },
+                                undo: function() {
+                                    this.editor.applyValue(item,this.prevSerializedValue);
+                                    updateRow(this.row);
+                                }
+                            };
+
+                            if (options.editCommandHandler) {
+                                makeSelectedCellNormal();
+                                options.editCommandHandler(item,column,editCommand);
+
+                            }
+                            else {
+                                editCommand.execute();
+                                makeSelectedCellNormal();
+                            }
+
+                            if (self.onCellChange) {
+                                self.onCellChange(currentRow,currentCell,item);
+                            }
+                        }
+                        else if (self.onAddNewRow) {
+                            var newItem = {};
+                            currentEditor.applyValue(newItem,currentEditor.serializeValue());
+                            makeSelectedCellNormal();
+                            self.onAddNewRow(newItem,column);
+                        }
+
+                        // check whether the lock has been re-acquired by event handlers
+                        return !options.editorLock.isActive();
+                    }
+                    else {
+                        // TODO: remove and put in onValidationError handlers in examples
+                        $(currentCellNode).addClass("invalid");
+                        $(currentCellNode).stop(true,true).effect("highlight", {color:"red"}, 300);
+
+                        if (self.onValidationError) {
+                            self.onValidationError(currentCellNode, validationResults, currentRow, currentCell, column);
+                        }
+
+                        currentEditor.focus();
+                        return false;
+                    }
+                }
+
+                makeSelectedCellNormal();
+            }
+            return true;
+        }
+
+        function cancelCurrentEdit() {
+            makeSelectedCellNormal();
+            return true;
+        }
+
+
+        //////////////////////////////////////////////////////////////////////////////////////////////
+        // Debug
+
+        this.debug = function() {
+            var s = "";
+
+            s += ("\n" + "counter_rows_rendered:  " + counter_rows_rendered);
+            s += ("\n" + "counter_rows_removed:  " + counter_rows_removed);
+            s += ("\n" + "renderedRows:  " + renderedRows);
+            s += ("\n" + "numVisibleRows:  " + numVisibleRows);
+            s += ("\n" + "maxSupportedCssHeight:  " + maxSupportedCssHeight);
+            s += ("\n" + "n(umber of pages):  " + n);
+            s += ("\n" + "(current) page:  " + page);
+            s += ("\n" + "page height (ph):  " + ph);
+            s += ("\n" + "scrollDir:  " + scrollDir);
+
+            alert(s);
+        };
+
+        // a debug helper to be able to access private members
+        this.eval = function(expr) {
+            return eval(expr);
+        };
+
+        init();
+
+
+        //////////////////////////////////////////////////////////////////////////////////////////////
+        // Public API
+
+        $.extend(this, {
+            "slickGridVersion": "1.4.3",
+
+            // Events
+            "onSort":                null,
+            "onHeaderContextMenu":   null,
+            "onClick":               null,
+            "onDblClick":            null,
+            "onContextMenu":         null,
+            "onKeyDown":             null,
+            "onAddNewRow":           null,
+            "onValidationError":     null,
+            "onViewportChanged":     null,
+            "onSelectedRowsChanged": null,
+            "onColumnsReordered":    null,
+            "onColumnsResized":      null,
+            "onBeforeMoveRows":      null,
+            "onMoveRows":            null,
+            "onCellChange":          null,
+            "onBeforeEditCell":      null,
+            "onBeforeCellEditorDestroy":    null,
+            "onBeforeDestroy":       null,
+            "onCurrentCellChanged":  null,
+            "onCurrentCellPositionChanged":  null,
+            "onCellRangeSelected":   null,
+
+            // Methods
+            "getColumns":          getColumns,
+            "setColumns":          setColumns,
+            "getOptions":          getOptions,
+            "setOptions":          setOptions,
+            "getData":             getData,
+            "setData":             setData,
+            "destroy":             destroy,
+            "getColumnIndex":      getColumnIndex,
+            "autosizeColumns":     autosizeColumns,
+            "updateCell":          updateCell,
+            "updateRow":           updateRow,
+            "removeRow":           removeRow,
+            "removeRows":          removeRows,
+            "removeAllRows":       removeAllRows,
+            "render":              render,
+            "invalidate":          invalidate,
+            "setHighlightedCells": setHighlightedCells,
+            "flashCell":           flashCell,
+            "getViewport":         getVisibleRange,
+            "resizeCanvas":        resizeCanvas,
+            "updateRowCount":      updateRowCount,
+            "getCellFromPoint":    getCellFromPoint,
+            "getCellFromEvent":    getCellFromEvent,
+            "getCurrentCell":      getCurrentCell,
+            "getCurrentCellNode":  getCurrentCellNode,
+            "resetCurrentCell":    resetCurrentCell,
+            "navigatePrev":        navigatePrev,
+            "navigateNext":        navigateNext,
+            "navigateUp":          navigateUp,
+            "navigateDown":        navigateDown,
+            "navigateLeft":        navigateLeft,
+            "navigateRight":       navigateRight,
+            "gotoCell":            gotoCell,
+            "editCurrentCell":     makeSelectedCellEditable,
+            "getCellEditor":       getCellEditor,
+            "scrollRowIntoView":   scrollRowIntoView,
+            "getSelectedRows":     getSelectedRows,
+            "setSelectedRows":     setSelectedRows,
+            "getSecondaryHeaderRow":    getSecondaryHeaderRow,
+            "showSecondaryHeaderRow":   showSecondaryHeaderRow,
+            "hideSecondaryHeaderRow":   hideSecondaryHeaderRow,
+            "setSortColumn":       setSortColumn,
+            "getCurrentCellPosition" : getCurrentCellPosition,
+            "getGridPosition": getGridPosition,
+
+            // IEditor implementation
+            "getEditController":    getEditController
+        });
+    }
+
+    // Slick.Grid
+    $.extend(true, window, {
+        Slick: {
+            Grid: SlickGrid,
+            EditorLock: EditorLock,
+            GlobalEditorLock: new EditorLock()
+        }
+    });
+}(jQuery));
\ No newline at end of file


http://bitbucket.org/okfn/ckanext-datapreview/changeset/69c09be25763/
changeset:   69c09be25763
user:        aron_
date:        2011-07-14 16:24:29
summary:     Added a wrapper div around the table

This fixes an issue with the header columns of the data table
wrapping onto a new line.
affected #:  2 files (110 bytes)

--- a/public/ckanext/datapreview/data-preview.css	Thu Jul 14 15:11:26 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.css	Thu Jul 14 15:24:29 2011 +0100
@@ -12,6 +12,13 @@
 	border-bottom: none;
 }
 
+/* Adds border to left of data table */
+.ckanext-datapreview-grid {
+	border-left: 1px solid #ccc;
+	height: 100%;
+	width: 100%;
+}
+
 /* Style the preview buttons */
 .preview {
 	width: 65px;
@@ -57,12 +64,6 @@
 	cursor: default;
 }
 
-/* Adds border to left of data table */
-.slick-viewport .c0,
-.slick-header-columns .slick-header-column:first-child {
-	border-left: 1px solid #ccc;
-}
-
 /* Reduce the default size of the alert dialog */
 .ui-button-text-only .ui-button-text {
 	padding: 3px 16px 1px;


--- a/public/ckanext/datapreview/data-preview.js	Thu Jul 14 15:11:26 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Thu Jul 14 15:24:29 2011 +0100
@@ -59,7 +59,8 @@
     // Need to create the grid once the dialog is open for cells to render
     // correctly.
     dialog.one("dialogopen", function() {
-      var grid = new Slick.Grid(dialog, data, columns, {
+      var container = $('<div class="ckanext-datapreview-grid" />').appendTo(dialog);
+      var grid = new Slick.Grid(container, data, columns, {
         enableColumnReorder: false,
         forceFitColumns: true,
         syncColumnCellResize: false,


http://bitbucket.org/okfn/ckanext-datapreview/changeset/a9462e5c5924/
changeset:   a9462e5c5924
user:        aron_
date:        2011-07-14 17:01:25
summary:     Fixed some issues with the display of the error dialog
affected #:  2 files (172 bytes)

--- a/public/ckanext/datapreview/data-preview.css	Thu Jul 14 15:24:29 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.css	Thu Jul 14 16:01:25 2011 +0100
@@ -65,7 +65,7 @@
 }
 
 /* Reduce the default size of the alert dialog */
-.ui-button-text-only .ui-button-text {
+.ui-dialog .ui-button-text-only .ui-button-text {
 	padding: 3px 16px 1px;
 }
 


--- a/public/ckanext/datapreview/data-preview.js	Thu Jul 14 15:24:29 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Thu Jul 14 16:01:25 2011 +0100
@@ -58,7 +58,7 @@
 
     // Need to create the grid once the dialog is open for cells to render
     // correctly.
-    dialog.one("dialogopen", function() {
+    dialog.dialog(my.dialogOptions).one("dialogopen", function() {
       var container = $('<div class="ckanext-datapreview-grid" />').appendTo(dialog);
       var grid = new Slick.Grid(container, data, columns, {
         enableColumnReorder: false,
@@ -103,6 +103,10 @@
     my.$dialog.empty();
     my.$dialog.dialog('option', 'title', 'Preview: ' + url);
 
+    if(data.error) {
+      return my.showError(data.error);
+    }
+
     var columns = $.map(data.fields || [], function (column, i) {
       return {id: 'header-' + i, name: column, field: 'column-' + i, sortable: true};
     });
@@ -279,8 +283,10 @@
       , resizable: false
       , draggable: false
       , modal: true
+      , position: 'fixed'
     };
-    my.$dialog.dialog(my.dialogOptions);
+
+    my.$dialog.dialog(my.dialogOptions).dialog("widget").css('position', 'fixed');
     my.createPreviewButtons($('.resources'));
   };
 


http://bitbucket.org/okfn/ckanext-datapreview/changeset/7e128ab6f94f/
changeset:   7e128ab6f94f
user:        aron_
date:        2011-07-14 18:52:42
summary:     Imrovements to the column resizing of the data grid

Column cells are now all resized at once and imroved the hit area
of the resize widget in each column header.
affected #:  2 files (928 bytes)

--- a/public/ckanext/datapreview/data-preview.css	Thu Jul 14 16:01:25 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.css	Thu Jul 14 17:52:42 2011 +0100
@@ -69,3 +69,26 @@
 	padding: 3px 16px 1px;
 }
 
+/* Extend the resize handle into the adjacent column */
+.slick-resizable-handle {
+	width: 12px;
+	right: -6px;
+}
+
+/* Fix to crop the text correctly */
+.slick-header-wrapper {
+	overflow: hidden;
+	display: inline-block;
+	width: 100%;
+	text-overflow: ellipsis;
+}
+
+.slick-columnpicker {
+	border-color: #888;
+	z-index: 99999!important;
+}
+
+.slick-columnpicker label,
+.slick-columnpicker input {
+	cursor: pointer;
+}


--- a/public/ckanext/datapreview/data-preview.js	Thu Jul 14 16:01:25 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Thu Jul 14 17:52:42 2011 +0100
@@ -63,7 +63,7 @@
       var grid = new Slick.Grid(container, data, columns, {
         enableColumnReorder: false,
         forceFitColumns: true,
-        syncColumnCellResize: false,
+        syncColumnCellResize: true,
         enableCellRangeSelection: false
       });
 
@@ -91,6 +91,18 @@
       dialog.bind("dialogbeforeclose", function () {
         my.$dialog.unbind(".data-preview");
       });
+
+      // In order to extend the resize handles across into the adjacent column
+      // we need to disable overflow hidden and increase each cells z-index.
+      // We then wrap the contents in order to reapply the overflow hidden.
+      dialog.find('.slick-header-column')
+        .wrapInner('<div class="slick-header-wrapper" />')
+        .css('overflow', 'visible')
+        .css('z-index', function (index) {
+          return columns.length - index;
+        });
+
+      new Slick.Controls.ColumnPicker(columns, grid);
     });
   };
 


http://bitbucket.org/okfn/ckanext-datapreview/changeset/deb909eb7568/
changeset:   deb909eb7568
user:        aron_
date:        2011-07-18 15:05:08
summary:     Now lazy loading script & stylesheet dependancies

Required scripts and stylesheets will now be loaded the first time a
"preview" button is clicked. This speeds up the initial page load.
affected #:  2 files (2.3 KB)

--- a/ckanext/datapreview/__init__.py	Thu Jul 14 17:52:42 2011 +0100
+++ b/ckanext/datapreview/__init__.py	Mon Jul 18 14:05:08 2011 +0100
@@ -68,10 +68,6 @@
 
     BOTTOM_CODE = """
 <div id="ckanext-datapreview-dialog"></div>
-<script type="text/javascript" src="/ckanext/datapreview/jquery-ui/js/jquery-ui-1.8.14.custom.min.js"></script>
-<script type="text/javascript" src="/ckanext/datapreview/jquery-ui/js/jquery.event.drag-2.0.min.js"></script>
-<script type="text/javascript" src="/ckanext/datapreview/slickgrid/slick.grid.js"></script>
-<script type="text/javascript" src="/ckanext/datapreview/slickgrid/slick.columnpicker.js"></script><script type="text/javascript" src="/ckanext/datapreview/data-preview.js"></script><script type="text/javascript">
   jQuery('document').ready(function($) {
@@ -83,10 +79,7 @@
 """
     
     HEAD_CODE = '''
-<link rel="stylesheet" href="/ckanext/datapreview/jquery-ui/css/ckan/jquery-ui-1.8.14.custom.css" />
-<link rel="stylesheet" href="/ckanext/datapreview/slickgrid/slick.grid.css" />
-<link rel="stylesheet" href="/ckanext/datapreview/slickgrid/slick.columnpicker.css" />
-<link rel="stylesheet" href="/ckanext/datapreview/data-preview.css" />
+<link rel="stylesheet" href="/ckanext/datapreview/data-preview.css" class="ckanext-datapreview-stylesheet" />
 '''
         
     def filter(self, stream):


--- a/public/ckanext/datapreview/data-preview.js	Thu Jul 14 17:52:42 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Mon Jul 18 14:05:08 2011 +0100
@@ -5,6 +5,23 @@
   my.$dialog = null;
   my.dataproxy = null;
   my.timeout = 5000;
+  my.areDependanciesLoaded = false;
+  my.stylesheets = [
+    '/ckanext/datapreview/jquery-ui/css/ckan/jquery-ui-1.8.14.custom.css',
+    '/ckanext/datapreview/slickgrid/slick.grid.css',
+    '/ckanext/datapreview/slickgrid/slick.columnpicker.css'
+  ];
+  
+  my.scripts = {
+    'jquery-ui': [
+      '/ckanext/datapreview/jquery-ui/js/jquery-ui-1.8.14.custom.min.js',
+      '/ckanext/datapreview/jquery-ui/js/jquery.event.drag-2.0.min.js'
+    ],
+    'slickgrid': [
+      '/ckanext/datapreview/slickgrid/slick.grid.js',
+      '/ckanext/datapreview/slickgrid/slick.columnpicker.js'
+    ]
+  };
 
   my.normalizeFormat = function(format) {
     var out = format.toLowerCase();
@@ -29,6 +46,41 @@
                  .replace(/\//g,'&#x2F;');
   };
 
+  // Loads dependant scripts and stylesheets.
+  my.loadDependancies = function (callback) {
+    if (my.areDependanciesLoaded) {
+      return callback();
+    }
+
+    var uiVersion = ($.ui && $.ui.version || '').split('.'),
+        scripts;
+
+    // Don't load jQuery UI if it exists on the page.
+    if (uiVersion[0] >= 1 && uiVersion[1] >= 8 && uiVersion[2] >= 14) {
+      my.scripts['jquery-ui'].shift();
+    }
+
+    scripts = $.map(my.scripts['jquery-ui'], $.getScript);
+    $.when.apply($, scripts).then(function () {
+      scripts = $.map(my.scripts['slickgrid'], $.getScript);
+      $.when.apply($, scripts).then(function () {
+        my.areDependanciesLoaded = true;
+        my.$dialog.dialog(my.dialogOptions).dialog("widget").css('position', 'fixed');
+        callback();
+      });
+    });
+
+    // Prepend dependant stylesheets to the page (before the plugin stylesheet
+    // so we can take advantage of the cascade).
+    var pluginStylesheet = $('.ckanext-datapreview-stylesheet');
+    $.each(my.stylesheets, function () {
+      pluginStylesheet.before($('<link />', {
+        rel: 'stylesheet',
+        href: this
+      }));
+    });
+  };
+
   my.getResourceDataDirect = function(url, type, callback) {
     var apiurl = my.dataproxy + '?url=' + url + '&type=' + type + '&max-results=30';
 
@@ -168,9 +220,13 @@
 
     function callbackWrapper(callback) {
       return function () {
-        $(link).removeClass('resource-preview-loading').text('Preview');
-        callback.apply(this, arguments);
-        my.$dialog.dialog('open');
+        var context = this, args = arguments;
+
+        my.loadDependancies(function () {
+          $(link).removeClass('resource-preview-loading').text('Preview');
+          callback.apply(context, args);
+          my.$dialog.dialog('open');
+        });
       };
     }
 
@@ -298,7 +354,6 @@
       , position: 'fixed'
     };
 
-    my.$dialog.dialog(my.dialogOptions).dialog("widget").css('position', 'fixed');
     my.createPreviewButtons($('.resources'));
   };
 


http://bitbucket.org/okfn/ckanext-datapreview/changeset/49476a6d9024/
changeset:   49476a6d9024
user:        John Glover
date:        2011-07-18 15:27:17
summary:     [merge] Aron's repo
affected #:  59 files (403.1 KB)
Diff too large to display.
http://bitbucket.org/okfn/ckanext-datapreview/changeset/3e679ba91dab/
changeset:   3e679ba91dab
user:        aron_
date:        2011-07-18 16:26:09
summary:     Fixed an issue with jQuery 1.5+ appending params to the query string

jQuery 1.5 adds an additional _ parameter to all JSONP request by
default to prevent the browser caching the request. The webstore
currently refuses requests with unknown parameters so we reenable
caching by providing the cache: true parameter to $.ajax().
affected #:  2 files (471 bytes)

--- a/ckanext/datapreview/plugin.py	Mon Jul 18 14:27:17 2011 +0100
+++ b/ckanext/datapreview/plugin.py	Mon Jul 18 15:26:09 2011 +0100
@@ -55,7 +55,6 @@
 
     BOTTOM_CODE = """
 <div id="ckanext-datapreview-dialog"></div>
-<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.8/jquery-ui.min.js"></script><script type="text/javascript" src="/ckanext/datapreview/data-preview.js"></script><script type="text/javascript">
   jQuery('document').ready(function($) {
@@ -67,10 +66,7 @@
 """
     
     HEAD_CODE = '''
-<link rel="stylesheet"
-    href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/ui-lightness/jquery-ui.css"
-    type="text/css" media="screen, print" />
-<link rel="stylesheet" href="/ckanext/datapreview/data-preview.css" />
+<link rel="stylesheet" href="/ckanext/datapreview/data-preview.css" class="ckanext-datapreview-stylesheet" />
 '''
         
     def filter(self, stream):


--- a/public/ckanext/datapreview/data-preview.js	Mon Jul 18 14:27:17 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Mon Jul 18 15:26:09 2011 +0100
@@ -93,8 +93,12 @@
       });
     }, my.timeout);
 
+    // We need to provide the `cache: true` parameter to prevent jQuery appending
+    // a cache busting `={timestamp}` parameter to the query as the webstore
+    // currently cannot handle custom parameters.
     $.ajax({
       url: url,
+      cache: true,
       dataType: 'jsonp',
       success: function(data) {
         clearTimeout(timer);


http://bitbucket.org/okfn/ckanext-datapreview/changeset/7b9b80ebcb82/
changeset:   7b9b80ebcb82
branch:      graph
user:        aron_
date:        2011-07-20 11:29:16
summary:     Added basic support for creating line charts from data sets

Added a new data-preview.ui.js file that now contains all the datagrid
and charting code. The charts themselves use the Flot jQuery plug-in and
the HTML template is loaded in via ajax with the other dependancies.
affected #:  5 files (116.9 KB)

--- a/public/ckanext/datapreview/data-preview.css	Mon Jul 18 15:26:09 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.css	Wed Jul 20 10:29:16 2011 +0100
@@ -12,13 +12,96 @@
 	border-bottom: none;
 }
 
+.ckanext-datapreview-viewer  {
+	position: relative;
+	height: 100%;
+	overflow: hidden;
+}
+
+.ckanext-datapreview-nav  {
+	list-style-type: none;
+	margin: 0;
+	padding: 0;
+	height: 22px;
+}
+
+.ckanext-datapreview-nav-toggle  {
+	float: left;
+}
+
+.ckanext-datapreview-nav li {
+	float: left;
+}
+
+#ckanext-datapreview-nav-editor,
+label[for=ckanext-datapreview-nav-editor]  {
+	float: right;
+}
+
 /* Adds border to left of data table */
+.ckanext-datapreview-grid,
+.ckanext-datapreview-graph,
+.ckanext-datapreview-editor {
+	position: absolute;
+	left: 0;
+	right: 220px;
+	top: 28px;
+	bottom: 0;
+	z-index: 0;
+}
+
 .ckanext-datapreview-grid {
 	border-left: 1px solid #ccc;
-	height: 100%;
+}
+
+.ckanext-datapreview-graph {
+	z-index: 1;
+	background-color: #fff;
+}
+
+.ckanext-datapreview-editor {
+	z-index: 1;
+	background-color: #efefef;
+	right: 0;
+	left: auto;
+	width: 198px;
+	padding: 5px 10px;
+	border: 1px solid #ccc;
+}
+
+.ckanext-datapreview-editor ul {
+	list-style-type: none;
+	margin: 0;
+	padding: 0;
+}
+
+.ckanext-datapreview-editor li {
+	margin-bottom: 10px;
+}
+
+.ckanext-datapreview-editor label {
+	display: block;
+	font-weight: bold;
+	color: #555;
+	line-height: 1.4;
+}
+
+.ckanext-datapreview-editor select {
 	width: 100%;
 }
 
+.ckanext-datapreview-editor button {
+	float: right;
+}
+
+.ckanext-datapreview-hide-editor .ckanext-datapreview-editor {
+	display: none;
+}
+
+.ckanext-datapreview-hide-editor .ckanext-datapreview-panel {
+	right: 0;
+}
+
 /* Style the preview buttons */
 .preview {
 	width: 65px;


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/public/ckanext/datapreview/data-preview.html	Wed Jul 20 10:29:16 2011 +0100
@@ -0,0 +1,35 @@
+<div class="ckanext-datapreview-viewer">
+  <div class="ckanext-datapreview-nav">
+    <span class="ckanext-datapreview-nav-toggle">
+      <input type="radio" id="ckanext-datapreview-nav-grid" name="ckanext-datapreview-nav-toggle" value="grid" checked="checked" />
+      <label for="ckanext-datapreview-nav-grid">Grid</label>
+      <input type="radio" id="ckanext-datapreview-nav-graph" name="ckanext-datapreview-nav-toggle" value="chart" />
+      <label for="ckanext-datapreview-nav-graph">Graph</label>
+    </span>
+    <input type="checkbox" id="ckanext-datapreview-nav-editor" checked="checked" />
+    <label for="ckanext-datapreview-nav-editor">Toggle Editor</label>
+  </div>
+  <div class="ckanext-datapreview-panel ckanext-datapreview-grid"></div>
+  <div class="ckanext-datapreview-panel ckanext-datapreview-graph"></div>
+  <div class="ckanext-datapreview-editor">
+    <form>
+      <ul>
+        <li class="ckanext-datapreview-editor-type">
+          <label>Graph Type</label>
+          <select></select>
+        </li>
+        <li class="ckanext-datapreview-editor-group">
+          <label>Group Column</label>
+          <select></select>
+        </li>
+        <li class="ckanext-datapreview-editor-series">
+          <label>Series A</label>
+          <select></select>
+        </li>
+      </ul>
+      <div class="ckanext-datapreview-editor-submit">
+        <button>Draw</button>
+      </div>
+    </form>
+  </div>
+</div>


--- a/public/ckanext/datapreview/data-preview.js	Mon Jul 18 15:26:09 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Wed Jul 20 10:29:16 2011 +0100
@@ -6,12 +6,13 @@
   my.webstore = null;
   my.timeout = 5000;
   my.areDependanciesLoaded = false;
+
   my.stylesheets = [
     '/ckanext/datapreview/jquery-ui/css/ckan/jquery-ui-1.8.14.custom.css',
     '/ckanext/datapreview/slickgrid/slick.grid.css',
     '/ckanext/datapreview/slickgrid/slick.columnpicker.css'
   ];
-  
+
   my.scripts = {
     'jquery-ui': [
       '/ckanext/datapreview/jquery-ui/js/jquery-ui-1.8.14.custom.min.js',
@@ -20,9 +21,18 @@
     'slickgrid': [
       '/ckanext/datapreview/slickgrid/slick.grid.js',
       '/ckanext/datapreview/slickgrid/slick.columnpicker.js'
+    ],
+    'flot': [
+      '/ckanext/datapreview/data-preview.ui.js',
+      '/ckanext/datapreview/flot/jquery.flot.js'
     ]
   };
 
+  my.template = {
+    html: '',
+    src: '/ckanext/datapreview/data-preview.html'
+  };
+
   my.normalizeFormat = function(format) {
     var out = format.toLowerCase();
     out = out.split('/');
@@ -63,6 +73,13 @@
     scripts = $.map(my.scripts['jquery-ui'], $.getScript);
     $.when.apply($, scripts).then(function () {
       scripts = $.map(my.scripts['slickgrid'], $.getScript);
+      scripts = scripts.concat($.map(my.scripts['flot'], $.getScript));
+
+      // Load the template file from the server.
+      scripts.push($.get(my.template.src, function (html) {
+        my.template.html = html;
+      }));
+
       $.when.apply($, scripts).then(function () {
         my.areDependanciesLoaded = true;
         my.$dialog.dialog(my.dialogOptions).dialog("widget").css('position', 'fixed');
@@ -107,56 +124,21 @@
     });
   };
 
-  my.createDataGrid = function createDataGrid(columns, data) {
+  my.loadDataPreview = function (columns, data) {
     var dialog = my.$dialog;
 
     // Need to create the grid once the dialog is open for cells to render
     // correctly.
-    dialog.dialog(my.dialogOptions).one("dialogopen", function() {
-      var container = $('<div class="ckanext-datapreview-grid" />').appendTo(dialog);
-      var grid = new Slick.Grid(container, data, columns, {
-        enableColumnReorder: false,
-        forceFitColumns: true,
-        syncColumnCellResize: true,
-        enableCellRangeSelection: false
-      });
+    dialog.dialog(my.dialogOptions).one("dialogopen", function () {
+      var element  = $(my.template.html).appendTo(dialog);
+      var viewer   = new my.createDataPreview(element, columns, data);
 
-      // Sort the data and redraw the grid.
-      grid.onSort = function (column, sortAsc) {
-        data.sort(function (a, b) {
-          var x = a[column.field],
-              y = b[column.field];
-
-          if (x == y) {
-            return 0;
-          }
-          return (x > y ? 1 : -1) * (sortAsc ? 1 : -1);
-        });
-        grid.invalidate();
-      };
-
-      // Redraw the grid when the dialog resizes.
-      dialog.bind("dialogresizestop.data-preview", function(event, ui) {
-        grid.resizeCanvas();
-        grid.autosizeColumns();
-      });
+      dialog.bind("dialogresizestop.data-preview", viewer.redraw);
 
       // Remove bindings when dialog is closed.
       dialog.bind("dialogbeforeclose", function () {
-        my.$dialog.unbind(".data-preview");
+        dialog.unbind(".data-preview");
       });
-
-      // In order to extend the resize handles across into the adjacent column
-      // we need to disable overflow hidden and increase each cells z-index.
-      // We then wrap the contents in order to reapply the overflow hidden.
-      dialog.find('.slick-header-column')
-        .wrapInner('<div class="slick-header-wrapper" />')
-        .css('overflow', 'visible')
-        .css('z-index', function (index) {
-          return columns.length - index;
-        });
-
-      new Slick.Controls.ColumnPicker(columns, grid);
     });
   };
 
@@ -186,7 +168,7 @@
       return cells;
     });
 
-    my.createDataGrid(columns, data);
+    my.loadDataPreview(columns, data);
   };
 
   my.showPlainTextData = function(url, type, data) {
@@ -247,7 +229,7 @@
 
     if (_type in {'csv': '', 'xls': ''}) {
       my.getResourceDataDirect(_url, _type, callbackWrapper(my.showData));
-    } 
+    }
     else if (_type in {
         'rdf+xml': '',
         'owl+xml': '',


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/public/ckanext/datapreview/data-preview.ui.js	Wed Jul 20 10:29:16 2011 +0100
@@ -0,0 +1,320 @@
+(function ($, undefined) {
+
+  var ui = {};
+
+  function bindAll(context) {
+    var args = [].slice.call(arguments, 0), i = 0, count = args.length;
+    for (; i < count; i += 1) {
+      context[args[i]] = $.proxy(context[args[i]], context);
+    }
+    return context;
+  }
+
+  // Taken from https://github.com/aron/inheritance.js
+  function create(proto) {
+    if (typeof proto !== 'object') {
+      return {};
+    }
+    else if (Object.create) {
+      return Object.create(proto);
+    }
+    function DummyObject() {}
+    DummyObject.prototype = proto;
+    return new DummyObject();
+  }
+
+  function inherit(parent, methods, properties) {
+    methods = methods || {};
+
+    var Child = methods.hasOwnProperty('constructor') ?
+                methods.constructor : inherit.constructor(parent);
+
+    Child.prototype = create(parent.prototype);
+    Child.prototype.constructor = Child;
+
+    delete methods.constructor;
+    $.extend(Child.prototype, methods, {__super__: parent.prototype});
+
+    return $.extend(Child, parent, properties);
+  }
+
+  // Implements Ben Allmans Tiny PubSub, https://gist.github.com/661855
+  ui.BaseView = inherit({}, {
+    constructor: function BaseView(element) {
+      this.el = element;
+
+      // Use a custom empty jQuery wrapper rather than this.el to prevent
+      // browser events being triggered.
+      this.events = $({});
+    },
+    $: function (selector) {
+      return this.el.find(selector);
+    },
+    bind: function (topic, fn) {
+      if (arguments.length === 1) {
+        for (var key in topic) {
+          if (topic.hasOwnProperty(key)) {
+            this.bind(key, topic[key]);
+          }
+        }
+        return this;
+      }
+
+      function wrapper() {
+        return fn.apply(this, Array.prototype.slice.call(arguments, 1));
+      }
+      wrapper.guid = fn.guid = fn.guid || ($.guid ? $.guid++ : $.event.guid++);
+      this.events.bind(topic, wrapper);
+      return this;
+    },
+    unbind: function () {
+      this.events.unbind.apply(this.events, arguments);
+      return this;
+    },
+    trigger: function () {
+      this.events.triggerHandler.apply(this.events, arguments);
+      return this;
+    },
+    show: function () {
+      this.el.show();
+      return this.trigger('show');
+    },
+    hide: function () {
+      this.el.hide();
+      return this.trigger('hide');
+    }
+  });
+
+  ui.MainView = inherit(ui.BaseView, {
+    constructor: function MainView(element, columns, data) {
+      this.__super__.constructor.apply(this, arguments);
+
+      bindAll(this, 'redraw', 'onNavChange', 'onNavToggleEditor', 'onEditorSubmit');
+
+      var view = this;
+      this.nav = new ui.NavigationView(this.$('.ckanext-datapreview-nav'));
+      this.grid = new ui.GridView(this.$('.ckanext-datapreview-grid'), columns, data);
+      this.chart = new ui.ChartView(this.$('.ckanext-datapreview-graph'), data);
+      this.editor = new ui.EditorView(this.$('.ckanext-datapreview-editor'), columns);
+
+      this.nav.bind({
+        'change': this.onNavChange,
+        'toggle-editor': this.onNavToggleEditor
+      });
+      this.editor.bind({
+        'show hide': this.redraw,
+        'submit': this.onEditorSubmit
+      });
+
+      this.chart.hide();
+    },
+    redraw: function () {
+      this.chart.redraw();
+      this.grid.redraw();
+    },
+    onNavChange: function (selected) {
+      var isGrid = selected === 'grid';
+      this.grid[isGrid ? 'show' : 'hide']();
+      this.chart[isGrid ? 'hide' : 'show']();
+    },
+    onNavToggleEditor: function (showEditor) {
+      this.el.toggleClass('ckanext-datapreview-hide-editor', !showEditor);
+      this.redraw();
+    },
+    onEditorSubmit: function (chart) {
+      this.nav.toggle('chart');
+      this.chart.update(chart);
+    }
+  });
+
+  ui.NavigationView = inherit(ui.BaseView, {
+    constructor: function NavigationView(element) {
+      this.__super__.constructor.apply(this, arguments);
+
+      bindAll(this, 'onEditorToggleChange', 'onPanelToggleChange');
+
+      this.panelButtons = this.$('.ckanext-datapreview-nav-toggle').buttonset();
+      this.panelButtons.change(this.onPanelToggleChange);
+
+      this.editorButton = this.$('#ckanext-datapreview-nav-editor').button();
+      this.editorButton.change(this.onEditorToggleChange);
+    },
+    toggle: function (panel) {
+      // Need to fire all these events just to get jQuery UI to change state.
+      this.$('input[value="' + panel + '"]').click().change().next().click();
+      return this;
+    },
+    onPanelToggleChange: function (event) {
+      this.trigger('change', [event.target.value]);
+    },
+    onEditorToggleChange: function (event) {
+      this.trigger('toggle-editor', [event.target.checked]);
+    }
+  });
+
+  ui.GridView = inherit(ui.BaseView, {
+    constructor: function GridView(element, columns, data, options) {
+      this.__super__.constructor.apply(this, arguments);
+
+      bindAll(this, '_onSort', 'redraw');
+
+      this.dirty = false;
+      this.columns = columns;
+      this.data = data;
+      this.grid = new Slick.Grid(element, data, columns, {
+        enableColumnReorder: false,
+        forceFitColumns: true,
+        syncColumnCellResize: true,
+        enableCellRangeSelection: false
+      });
+
+      this.grid.onSort = this._onSort;
+
+      // In order to extend the resize handles across into the adjacent column
+      // we need to disable overflow hidden and increase each cells z-index.
+      // We then wrap the contents in order to reapply the overflow hidden.
+      this.$('.slick-header-column')
+        .wrapInner('<div class="slick-header-wrapper" />')
+        .css('overflow', 'visible')
+        .css('z-index', function (index) {
+          return columns.length - index;
+        });
+
+      new Slick.Controls.ColumnPicker(this.columns, this.grid);
+    },
+    show: function () {
+      this.__super__.show.apply(this, arguments);
+      if (this.dirty) {
+        this.redraw();
+        this.dirty = false;
+      }
+      return this;
+    },
+    redraw: function () {
+      if (this.el.is(':visible')) {
+        this.grid.resizeCanvas();
+        this.grid.autosizeColumns();
+      } else {
+        this.dirty = true;
+      }
+    },
+    _onSort: function (column, sortAsc) {
+      this.data.sort(function (a, b) {
+        var x = a[column.field],
+            y = b[column.field];
+
+        if (x == y) {
+          return 0;
+        }
+        return (x > y ? 1 : -1) * (sortAsc ? 1 : -1);
+      });
+      this.grid.invalidate();
+    }
+  });
+
+  ui.ChartView = inherit(ui.BaseView, {
+    constructor: function ChartView(element, gridData, chart) {
+      this.__super__.constructor.apply(this, arguments);
+      this.gridData = gridData;
+      this.chart = chart;
+      this.plot = $.plot(element, this.createData());
+      this.draw();
+    },
+    createData: function () {
+      var data = [], gridData = this.gridData, chart = this.chart;
+      if (chart) {
+        $.each(this.chart.series, function () {
+          var points = [], name = this;
+          $.each(gridData, function (index) {
+            var x = this[chart.groups], y = this[name];
+            if (typeof x === 'string') {
+              x = index;
+            }
+            points.push([x, y]);
+          });
+          data.push({data: points});
+        });
+      }
+      return data;
+    },
+    draw: function () {
+      this.plot.setData(this.createData());
+      return this.redraw();
+    },
+    update: function (chart) {
+      this.chart = chart;
+      this.draw();
+      return this;
+    },
+    redraw: function () {
+      this.plot.resize();
+      this.plot.setupGrid();
+      this.plot.draw();
+      return this;
+    }
+  });
+
+  ui.EditorView = inherit(ui.BaseView, {
+    constructor: function EditorView(element, columns) {
+      this.__super__.constructor.apply(this, arguments);
+
+      bindAll(this, 'onSubmit');
+
+      this.columns = columns;
+      this.type    = this.$('.ckanext-datapreview-editor-type select');
+      this.groups  = this.$('.ckanext-datapreview-editor-group select');
+      this.series  = this.$('.ckanext-datapreview-editor-series select');
+
+      this.$('button').button().click(this.onSubmit);
+      this.el.bind('submit', this.onSubmit);
+
+      this.setupTypeOptions().setupColumnOptions();
+    },
+    setupTypeOptions: function () {
+      var types = {};
+      $.each({line: {name: 'Line Chart'}}, function (id, type) {
+        types[id] = type.name;
+      });
+      this.type.html(this._createOptions(types));
+      return this;
+    },
+    setupColumnOptions: function () {
+      var options = {}, optionsString = '';
+      $.each(this.columns, function (index, column) {
+        options[column.field] = column.name;
+      });
+      optionsString = this._createOptions(options);
+
+      this.groups.html(optionsString);
+      this.series.html(optionsString);
+      return this;
+    },
+    onSubmit: function (event) {
+      event && event.preventDefault();
+
+      var series = this.series.map(function () {
+        return $(this).val();
+      });
+
+      this.trigger('submit', [{
+        type: this.type.val(),
+        groups: this.groups.val(),
+        series: $.makeArray(series)
+      }]);
+    },
+    _createOptions: function (options) {
+      var html = [];
+      $.each(options, function (value, text) {
+        html.push('<option value="' + value + '">' + text + '</option>');
+      });
+      return html.join('');
+    }
+  });
+
+  $.extend(true, this, {CKANEXT: {DATAPREVIEW: {
+    createDataPreview: function (element, columns, data) {
+      return new ui.MainView(element, columns, data);
+    }
+  }}});
+
+})(jQuery);


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/public/ckanext/datapreview/flot/jquery.flot.js	Wed Jul 20 10:29:16 2011 +0100
@@ -0,0 +1,2599 @@
+/*! Javascript plotting library for jQuery, v. 0.7.
+ *
+ * Released under the MIT license by IOLA, December 2007.
+ *
+ */
+
+// first an inline dependency, jquery.colorhelpers.js, we inline it here
+// for convenience
+
+/* Plugin for jQuery for working with colors.
+ * 
+ * Version 1.1.
+ * 
+ * Inspiration from jQuery color animation plugin by John Resig.
+ *
+ * Released under the MIT license by Ole Laursen, October 2009.
+ *
+ * Examples:
+ *
+ *   $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
+ *   var c = $.color.extract($("#mydiv"), 'background-color');
+ *   console.log(c.r, c.g, c.b, c.a);
+ *   $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
+ *
+ * Note that .scale() and .add() return the same modified object
+ * instead of making a new one.
+ *
+ * V. 1.1: Fix error handling so e.g. parsing an empty string does
+ * produce a color rather than just crashing.
+ */ 
+(function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]+=I}return G.normalize()};G.scale=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]*=I}return G.normalize()};G.toString=function(){if(G.a>=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return K<J?J:(K>I?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);
+
+// the actual Flot code
+(function($) {
+    function Plot(placeholder, data_, options_, plugins) {
+        // data is on the form:
+        //   [ series1, series2 ... ]
+        // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
+        // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
+        
+        var series = [],
+            options = {
+                // the color theme used for graphs
+                colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
+                legend: {
+                    show: true,
+                    noColumns: 1, // number of colums in legend table
+                    labelFormatter: null, // fn: string -> string
+                    labelBoxBorderColor: "#ccc", // border color for the little label boxes
+                    container: null, // container (as jQuery object) to put legend in, null means default on top of graph
+                    position: "ne", // position of default legend container within plot
+                    margin: 5, // distance from grid edge to default legend container within plot
+                    backgroundColor: null, // null means auto-detect
+                    backgroundOpacity: 0.85 // set to 0 to avoid background
+                },
+                xaxis: {
+                    show: null, // null = auto-detect, true = always, false = never
+                    position: "bottom", // or "top"
+                    mode: null, // null or "time"
+                    color: null, // base color, labels, ticks
+                    tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"
+                    transform: null, // null or f: number -> number to transform axis
+                    inverseTransform: null, // if transform is set, this should be the inverse function
+                    min: null, // min. value to show, null means set automatically
+                    max: null, // max. value to show, null means set automatically
+                    autoscaleMargin: null, // margin in % to add if auto-setting min/max
+                    ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
+                    tickFormatter: null, // fn: number -> string
+                    labelWidth: null, // size of tick labels in pixels
+                    labelHeight: null,
+                    reserveSpace: null, // whether to reserve space even if axis isn't shown
+                    tickLength: null, // size in pixels of ticks, or "full" for whole line
+                    alignTicksWithAxis: null, // axis number or null for no sync
+                    
+                    // mode specific options
+                    tickDecimals: null, // no. of decimals, null means auto
+                    tickSize: null, // number or [number, "unit"]
+                    minTickSize: null, // number or [number, "unit"]
+                    monthNames: null, // list of names of months
+                    timeformat: null, // format string to use
+                    twelveHourClock: false // 12 or 24 time in time mode
+                },
+                yaxis: {
+                    autoscaleMargin: 0.02,
+                    position: "left" // or "right"
+                },
+                xaxes: [],
+                yaxes: [],
+                series: {
+                    points: {
+                        show: false,
+                        radius: 3,
+                        lineWidth: 2, // in pixels
+                        fill: true,
+                        fillColor: "#ffffff",
+                        symbol: "circle" // or callback
+                    },
+                    lines: {
+                        // we don't put in show: false so we can see
+                        // whether lines were actively disabled 
+                        lineWidth: 2, // in pixels
+                        fill: false,
+                        fillColor: null,
+                        steps: false
+                    },
+                    bars: {
+                        show: false,
+                        lineWidth: 2, // in pixels
+                        barWidth: 1, // in units of the x axis
+                        fill: true,
+                        fillColor: null,
+                        align: "left", // or "center" 
+                        horizontal: false
+                    },
+                    shadowSize: 3
+                },
+                grid: {
+                    show: true,
+                    aboveData: false,
+                    color: "#545454", // primary color used for outline and labels
+                    backgroundColor: null, // null for transparent, else color
+                    borderColor: null, // set if different from the grid color
+                    tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
+                    labelMargin: 5, // in pixels
+                    axisMargin: 8, // in pixels
+                    borderWidth: 2, // in pixels
+                    minBorderMargin: null, // in pixels, null means taken from points radius
+                    markings: null, // array of ranges or fn: axes -> array of ranges
+                    markingsColor: "#f4f4f4",
+                    markingsLineWidth: 2,
+                    // interactive stuff
+                    clickable: false,
+                    hoverable: false,
+                    autoHighlight: true, // highlight in case mouse is near
+                    mouseActiveRadius: 10 // how far the mouse can be away to activate an item
+                },
+                hooks: {}
+            },
+        canvas = null,      // the canvas for the plot itself
+        overlay = null,     // canvas for interactive stuff on top of plot
+        eventHolder = null, // jQuery object that events should be bound to
+        ctx = null, octx = null,
+        xaxes = [], yaxes = [],
+        plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
+        canvasWidth = 0, canvasHeight = 0,
+        plotWidth = 0, plotHeight = 0,
+        hooks = {
+            processOptions: [],
+            processRawData: [],
+            processDatapoints: [],
+            drawSeries: [],
+            draw: [],
+            bindEvents: [],
+            drawOverlay: [],
+            shutdown: []
+        },
+        plot = this;
+
+        // public functions
+        plot.setData = setData;
+        plot.setupGrid = setupGrid;
+        plot.draw = draw;
+        plot.getPlaceholder = function() { return placeholder; };
+        plot.getCanvas = function() { return canvas; };
+        plot.getPlotOffset = function() { return plotOffset; };
+        plot.width = function () { return plotWidth; };
+        plot.height = function () { return plotHeight; };
+        plot.offset = function () {
+            var o = eventHolder.offset();
+            o.left += plotOffset.left;
+            o.top += plotOffset.top;
+            return o;
+        };
+        plot.getData = function () { return series; };
+        plot.getAxes = function () {
+            var res = {}, i;
+            $.each(xaxes.concat(yaxes), function (_, axis) {
+                if (axis)
+                    res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;
+            });
+            return res;
+        };
+        plot.getXAxes = function () { return xaxes; };
+        plot.getYAxes = function () { return yaxes; };
+        plot.c2p = canvasToAxisCoords;
+        plot.p2c = axisToCanvasCoords;
+        plot.getOptions = function () { return options; };
+        plot.highlight = highlight;
+        plot.unhighlight = unhighlight;
+        plot.triggerRedrawOverlay = triggerRedrawOverlay;
+        plot.pointOffset = function(point) {
+            return {
+                left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left),
+                top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top)
+            };
+        };
+        plot.shutdown = shutdown;
+        plot.resize = function () {
+            getCanvasDimensions();
+            resizeCanvas(canvas);
+            resizeCanvas(overlay);
+        };
+
+        // public attributes
+        plot.hooks = hooks;
+        
+        // initialize
+        initPlugins(plot);
+        parseOptions(options_);
+        setupCanvases();
+        setData(data_);
+        setupGrid();
+        draw();
+        bindEvents();
+
+
+        function executeHooks(hook, args) {
+            args = [plot].concat(args);
+            for (var i = 0; i < hook.length; ++i)
+                hook[i].apply(this, args);
+        }
+
+        function initPlugins() {
+            for (var i = 0; i < plugins.length; ++i) {
+                var p = plugins[i];
+                p.init(plot);
+                if (p.options)
+                    $.extend(true, options, p.options);
+            }
+        }
+        
+        function parseOptions(opts) {
+            var i;
+            
+            $.extend(true, options, opts);
+            
+            if (options.xaxis.color == null)
+                options.xaxis.color = options.grid.color;
+            if (options.yaxis.color == null)
+                options.yaxis.color = options.grid.color;
+            
+            if (options.xaxis.tickColor == null) // backwards-compatibility
+                options.xaxis.tickColor = options.grid.tickColor;
+            if (options.yaxis.tickColor == null) // backwards-compatibility
+                options.yaxis.tickColor = options.grid.tickColor;
+
+            if (options.grid.borderColor == null)
+                options.grid.borderColor = options.grid.color;
+            if (options.grid.tickColor == null)
+                options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();
+            
+            // fill in defaults in axes, copy at least always the
+            // first as the rest of the code assumes it'll be there
+            for (i = 0; i < Math.max(1, options.xaxes.length); ++i)
+                options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]);
+            for (i = 0; i < Math.max(1, options.yaxes.length); ++i)
+                options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]);
+
+            // backwards compatibility, to be removed in future
+            if (options.xaxis.noTicks && options.xaxis.ticks == null)
+                options.xaxis.ticks = options.xaxis.noTicks;
+            if (options.yaxis.noTicks && options.yaxis.ticks == null)
+                options.yaxis.ticks = options.yaxis.noTicks;
+            if (options.x2axis) {
+                options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
+                options.xaxes[1].position = "top";
+            }
+            if (options.y2axis) {
+                options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);
+                options.yaxes[1].position = "right";
+            }
+            if (options.grid.coloredAreas)
+                options.grid.markings = options.grid.coloredAreas;
+            if (options.grid.coloredAreasColor)
+                options.grid.markingsColor = options.grid.coloredAreasColor;
+            if (options.lines)
+                $.extend(true, options.series.lines, options.lines);
+            if (options.points)
+                $.extend(true, options.series.points, options.points);
+            if (options.bars)
+                $.extend(true, options.series.bars, options.bars);
+            if (options.shadowSize != null)
+                options.series.shadowSize = options.shadowSize;
+
+            // save options on axes for future reference
+            for (i = 0; i < options.xaxes.length; ++i)
+                getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
+            for (i = 0; i < options.yaxes.length; ++i)
+                getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];
+
+            // add hooks from options
+            for (var n in hooks)
+                if (options.hooks[n] && options.hooks[n].length)
+                    hooks[n] = hooks[n].concat(options.hooks[n]);
+
+            executeHooks(hooks.processOptions, [options]);
+        }
+
+        function setData(d) {
+            series = parseData(d);
+            fillInSeriesOptions();
+            processData();
+        }
+        
+        function parseData(d) {
+            var res = [];
+            for (var i = 0; i < d.length; ++i) {
+                var s = $.extend(true, {}, options.series);
+
+                if (d[i].data != null) {
+                    s.data = d[i].data; // move the data instead of deep-copy
+                    delete d[i].data;
+
+                    $.extend(true, s, d[i]);
+
+                    d[i].data = s.data;
+                }
+                else
+                    s.data = d[i];
+                res.push(s);
+            }
+
+            return res;
+        }
+        
+        function axisNumber(obj, coord) {
+            var a = obj[coord + "axis"];
+            if (typeof a == "object") // if we got a real axis, extract number
+                a = a.n;
+            if (typeof a != "number")
+                a = 1; // default to first axis
+            return a;
+        }
+
+        function allAxes() {
+            // return flat array without annoying null entries
+            return $.grep(xaxes.concat(yaxes), function (a) { return a; });
+        }
+        
+        function canvasToAxisCoords(pos) {
+            // return an object with x/y corresponding to all used axes 
+            var res = {}, i, axis;
+            for (i = 0; i < xaxes.length; ++i) {
+                axis = xaxes[i];
+                if (axis && axis.used)
+                    res["x" + axis.n] = axis.c2p(pos.left);
+            }
+
+            for (i = 0; i < yaxes.length; ++i) {
+                axis = yaxes[i];
+                if (axis && axis.used)
+                    res["y" + axis.n] = axis.c2p(pos.top);
+            }
+            
+            if (res.x1 !== undefined)
+                res.x = res.x1;
+            if (res.y1 !== undefined)
+                res.y = res.y1;
+
+            return res;
+        }
+        
+        function axisToCanvasCoords(pos) {
+            // get canvas coords from the first pair of x/y found in pos
+            var res = {}, i, axis, key;
+
+            for (i = 0; i < xaxes.length; ++i) {
+                axis = xaxes[i];
+                if (axis && axis.used) {
+                    key = "x" + axis.n;
+                    if (pos[key] == null && axis.n == 1)
+                        key = "x";
+
+                    if (pos[key] != null) {
+                        res.left = axis.p2c(pos[key]);
+                        break;
+                    }
+                }
+            }
+            
+            for (i = 0; i < yaxes.length; ++i) {
+                axis = yaxes[i];
+                if (axis && axis.used) {
+                    key = "y" + axis.n;
+                    if (pos[key] == null && axis.n == 1)
+                        key = "y";
+
+                    if (pos[key] != null) {
+                        res.top = axis.p2c(pos[key]);
+                        break;
+                    }
+                }
+            }
+            
+            return res;
+        }
+        
+        function getOrCreateAxis(axes, number) {
+            if (!axes[number - 1])
+                axes[number - 1] = {
+                    n: number, // save the number for future reference
+                    direction: axes == xaxes ? "x" : "y",
+                    options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)
+                };
+                
+            return axes[number - 1];
+        }
+
+        function fillInSeriesOptions() {
+            var i;
+            
+            // collect what we already got of colors
+            var neededColors = series.length,
+                usedColors = [],
+                assignedColors = [];
+            for (i = 0; i < series.length; ++i) {
+                var sc = series[i].color;
+                if (sc != null) {
+                    --neededColors;
+                    if (typeof sc == "number")
+                        assignedColors.push(sc);
+                    else
+                        usedColors.push($.color.parse(series[i].color));
+                }
+            }
+            
+            // we might need to generate more colors if higher indices
+            // are assigned
+            for (i = 0; i < assignedColors.length; ++i) {
+                neededColors = Math.max(neededColors, assignedColors[i] + 1);
+            }
+
+            // produce colors as needed
+            var colors = [], variation = 0;
+            i = 0;
+            while (colors.length < neededColors) {
+                var c;
+                if (options.colors.length == i) // check degenerate case
+                    c = $.color.make(100, 100, 100);
+                else
+                    c = $.color.parse(options.colors[i]);
+
+                // vary color if needed
+                var sign = variation % 2 == 1 ? -1 : 1;
+                c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2)
+
+                // FIXME: if we're getting to close to something else,
+                // we should probably skip this one
+                colors.push(c);
+                
+                ++i;
+                if (i >= options.colors.length) {
+                    i = 0;
+                    ++variation;
+                }
+            }
+
+            // fill in the options
+            var colori = 0, s;
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                
+                // assign colors
+                if (s.color == null) {
+                    s.color = colors[colori].toString();
+                    ++colori;
+                }
+                else if (typeof s.color == "number")
+                    s.color = colors[s.color].toString();
+
+                // turn on lines automatically in case nothing is set
+                if (s.lines.show == null) {
+                    var v, show = true;
+                    for (v in s)
+                        if (s[v] && s[v].show) {
+                            show = false;
+                            break;
+                        }
+                    if (show)
+                        s.lines.show = true;
+                }
+
+                // setup axes
+                s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x"));
+                s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y"));
+            }
+        }
+        
+        function processData() {
+            var topSentry = Number.POSITIVE_INFINITY,
+                bottomSentry = Number.NEGATIVE_INFINITY,
+                fakeInfinity = Number.MAX_VALUE,
+                i, j, k, m, length,
+                s, points, ps, x, y, axis, val, f, p;
+
+            function updateAxis(axis, min, max) {
+                if (min < axis.datamin && min != -fakeInfinity)
+                    axis.datamin = min;
+                if (max > axis.datamax && max != fakeInfinity)
+                    axis.datamax = max;
+            }
+
+            $.each(allAxes(), function (_, axis) {
+                // init axis
+                axis.datamin = topSentry;
+                axis.datamax = bottomSentry;
+                axis.used = false;
+            });
+            
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                s.datapoints = { points: [] };
+                
+                executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
+            }
+            
+            // first pass: clean and copy data
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+
+                var data = s.data, format = s.datapoints.format;
+
+                if (!format) {
+                    format = [];
+                    // find out how to copy
+                    format.push({ x: true, number: true, required: true });
+                    format.push({ y: true, number: true, required: true });
+
+                    if (s.bars.show || (s.lines.show && s.lines.fill)) {
+                        format.push({ y: true, number: true, required: false, defaultValue: 0 });
+                        if (s.bars.horizontal) {
+                            delete format[format.length - 1].y;
+                            format[format.length - 1].x = true;
+                        }
+                    }
+                    
+                    s.datapoints.format = format;
+                }
+
+                if (s.datapoints.pointsize != null)
+                    continue; // already filled in
+
+                s.datapoints.pointsize = format.length;
+                
+                ps = s.datapoints.pointsize;
+                points = s.datapoints.points;
+
+                insertSteps = s.lines.show && s.lines.steps;
+                s.xaxis.used = s.yaxis.used = true;
+                
+                for (j = k = 0; j < data.length; ++j, k += ps) {
+                    p = data[j];
+
+                    var nullify = p == null;
+                    if (!nullify) {
+                        for (m = 0; m < ps; ++m) {
+                            val = p[m];
+                            f = format[m];
+
+                            if (f) {
+                                if (f.number && val != null) {
+                                    val = +val; // convert to number
+                                    if (isNaN(val))
+                                        val = null;
+                                    else if (val == Infinity)
+                                        val = fakeInfinity;
+                                    else if (val == -Infinity)
+                                        val = -fakeInfinity;
+                                }
+
+                                if (val == null) {
+                                    if (f.required)
+                                        nullify = true;
+                                    
+                                    if (f.defaultValue != null)
+                                        val = f.defaultValue;
+                                }
+                            }
+                            
+                            points[k + m] = val;
+                        }
+                    }
+                    
+                    if (nullify) {
+                        for (m = 0; m < ps; ++m) {
+                            val = points[k + m];
+                            if (val != null) {
+                                f = format[m];
+                                // extract min/max info
+                                if (f.x)
+                                    updateAxis(s.xaxis, val, val);
+                                if (f.y)
+                                    updateAxis(s.yaxis, val, val);
+                            }
+                            points[k + m] = null;
+                        }
+                    }
+                    else {
+                        // a little bit of line specific stuff that
+                        // perhaps shouldn't be here, but lacking
+                        // better means...
+                        if (insertSteps && k > 0
+                            && points[k - ps] != null
+                            && points[k - ps] != points[k]
+                            && points[k - ps + 1] != points[k + 1]) {
+                            // copy the point to make room for a middle point
+                            for (m = 0; m < ps; ++m)
+                                points[k + ps + m] = points[k + m];
+
+                            // middle point has same y
+                            points[k + 1] = points[k - ps + 1];
+
+                            // we've added a point, better reflect that
+                            k += ps;
+                        }
+                    }
+                }
+            }
+
+            // give the hooks a chance to run
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                
+                executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
+            }
+
+            // second pass: find datamax/datamin for auto-scaling
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                points = s.datapoints.points,
+                ps = s.datapoints.pointsize;
+
+                var xmin = topSentry, ymin = topSentry,
+                    xmax = bottomSentry, ymax = bottomSentry;
+                
+                for (j = 0; j < points.length; j += ps) {
+                    if (points[j] == null)
+                        continue;
+
+                    for (m = 0; m < ps; ++m) {
+                        val = points[j + m];
+                        f = format[m];
+                        if (!f || val == fakeInfinity || val == -fakeInfinity)
+                            continue;
+                        
+                        if (f.x) {
+                            if (val < xmin)
+                                xmin = val;
+                            if (val > xmax)
+                                xmax = val;
+                        }
+                        if (f.y) {
+                            if (val < ymin)
+                                ymin = val;
+                            if (val > ymax)
+                                ymax = val;
+                        }
+                    }
+                }
+                
+                if (s.bars.show) {
+                    // make sure we got room for the bar on the dancing floor
+                    var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2;
+                    if (s.bars.horizontal) {
+                        ymin += delta;
+                        ymax += delta + s.bars.barWidth;
+                    }
+                    else {
+                        xmin += delta;
+                        xmax += delta + s.bars.barWidth;
+                    }
+                }
+                
+                updateAxis(s.xaxis, xmin, xmax);
+                updateAxis(s.yaxis, ymin, ymax);
+            }
+
+            $.each(allAxes(), function (_, axis) {
+                if (axis.datamin == topSentry)
+                    axis.datamin = null;
+                if (axis.datamax == bottomSentry)
+                    axis.datamax = null;
+            });
+        }
+
+        function makeCanvas(skipPositioning, cls) {
+            var c = document.createElement('canvas');
+            c.className = cls;
+            c.width = canvasWidth;
+            c.height = canvasHeight;
+                    
+            if (!skipPositioning)
+                $(c).css({ position: 'absolute', left: 0, top: 0 });
+                
+            $(c).appendTo(placeholder);
+                
+            if (!c.getContext) // excanvas hack
+                c = window.G_vmlCanvasManager.initElement(c);
+
+            // used for resetting in case we get replotted
+            c.getContext("2d").save();
+            
+            return c;
+        }
+
+        function getCanvasDimensions() {
+            canvasWidth = placeholder.width();
+            canvasHeight = placeholder.height();
+            
+            if (canvasWidth <= 0 || canvasHeight <= 0)
+                throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
+        }
+
+        function resizeCanvas(c) {
+            // resizing should reset the state (excanvas seems to be
+            // buggy though)
+            if (c.width != canvasWidth)
+                c.width = canvasWidth;
+
+            if (c.height != canvasHeight)
+                c.height = canvasHeight;
+
+            // so try to get back to the initial state (even if it's
+            // gone now, this should be safe according to the spec)
+            var cctx = c.getContext("2d");
+            cctx.restore();
+
+            // and save again
+            cctx.save();
+        }
+        
+        function setupCanvases() {
+            var reused,
+                existingCanvas = placeholder.children("canvas.base"),
+                existingOverlay = placeholder.children("canvas.overlay");
+
+            if (existingCanvas.length == 0 || existingOverlay == 0) {
+                // init everything
+                
+                placeholder.html(""); // make sure placeholder is clear
+            
+                placeholder.css({ padding: 0 }); // padding messes up the positioning
+                
+                if (placeholder.css("position") == 'static')
+                    placeholder.css("position", "relative"); // for positioning labels and overlay
+
+                getCanvasDimensions();
+                
+                canvas = makeCanvas(true, "base");
+                overlay = makeCanvas(false, "overlay"); // overlay canvas for interactive features
+
+                reused = false;
+            }
+            else {
+                // reuse existing elements
+
+                canvas = existingCanvas.get(0);
+                overlay = existingOverlay.get(0);
+
+                reused = true;
+            }
+
+            ctx = canvas.getContext("2d");
+            octx = overlay.getContext("2d");
+
+            // we include the canvas in the event holder too, because IE 7
+            // sometimes has trouble with the stacking order
+            eventHolder = $([overlay, canvas]);
+
+            if (reused) {
+                // run shutdown in the old plot object
+                placeholder.data("plot").shutdown();
+
+                // reset reused canvases
+                plot.resize();
+                
+                // make sure overlay pixels are cleared (canvas is cleared when we redraw)
+                octx.clearRect(0, 0, canvasWidth, canvasHeight);
+                
+                // then whack any remaining obvious garbage left
+                eventHolder.unbind();
+                placeholder.children().not([canvas, overlay]).remove();
+            }
+
+            // save in case we get replotted
+            placeholder.data("plot", plot);
+        }
+
+        function bindEvents() {
+            // bind events
+            if (options.grid.hoverable) {
+                eventHolder.mousemove(onMouseMove);
+                eventHolder.mouseleave(onMouseLeave);
+            }
+
+            if (options.grid.clickable)
+                eventHolder.click(onClick);
+
+            executeHooks(hooks.bindEvents, [eventHolder]);
+        }
+
+        function shutdown() {
+            if (redrawTimeout)
+                clearTimeout(redrawTimeout);
+            
+            eventHolder.unbind("mousemove", onMouseMove);
+            eventHolder.unbind("mouseleave", onMouseLeave);
+            eventHolder.unbind("click", onClick);
+            
+            executeHooks(hooks.shutdown, [eventHolder]);
+        }
+
+        function setTransformationHelpers(axis) {
+            // set helper functions on the axis, assumes plot area
+            // has been computed already
+            
+            function identity(x) { return x; }
+            
+            var s, m, t = axis.options.transform || identity,
+                it = axis.options.inverseTransform;
+            
+            // precompute how much the axis is scaling a point
+            // in canvas space
+            if (axis.direction == "x") {
+                s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
+                m = Math.min(t(axis.max), t(axis.min));
+            }
+            else {
+                s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
+                s = -s;
+                m = Math.max(t(axis.max), t(axis.min));
+            }
+
+            // data point to canvas coordinate
+            if (t == identity) // slight optimization
+                axis.p2c = function (p) { return (p - m) * s; };
+            else
+                axis.p2c = function (p) { return (t(p) - m) * s; };
+            // canvas coordinate to data point
+            if (!it)
+                axis.c2p = function (c) { return m + c / s; };
+            else
+                axis.c2p = function (c) { return it(m + c / s); };
+        }
+
+        function measureTickLabels(axis) {
+            var opts = axis.options, i, ticks = axis.ticks || [], labels = [],
+                l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv;
+
+            function makeDummyDiv(labels, width) {
+                return $('<div style="position:absolute;top:-10000px;' + width + 'font-size:smaller">' +
+                         '<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis">'
+                         + labels.join("") + '</div></div>')
+                    .appendTo(placeholder);
+            }
+            
+            if (axis.direction == "x") {
+                // to avoid measuring the widths of the labels (it's slow), we
+                // construct fixed-size boxes and put the labels inside
+                // them, we don't need the exact figures and the
+                // fixed-size box content is easy to center
+                if (w == null)
+                    w = Math.floor(canvasWidth / (ticks.length > 0 ? ticks.length : 1));
+
+                // measure x label heights
+                if (h == null) {
+                    labels = [];
+                    for (i = 0; i < ticks.length; ++i) {
+                        l = ticks[i].label;
+                        if (l)
+                            labels.push('<div class="tickLabel" style="float:left;width:' + w + 'px">' + l + '</div>');
+                    }
+
+                    if (labels.length > 0) {
+                        // stick them all in the same div and measure
+                        // collective height
+                        labels.push('<div style="clear:left"></div>');
+                        dummyDiv = makeDummyDiv(labels, "width:10000px;");
+                        h = dummyDiv.height();
+                        dummyDiv.remove();
+                    }
+                }
+            }
+            else if (w == null || h == null) {
+                // calculate y label dimensions
+                for (i = 0; i < ticks.length; ++i) {
+                    l = ticks[i].label;
+                    if (l)
+                        labels.push('<div class="tickLabel">' + l + '</div>');
+                }
+                
+                if (labels.length > 0) {
+                    dummyDiv = makeDummyDiv(labels, "");
+                    if (w == null)
+                        w = dummyDiv.children().width();
+                    if (h == null)
+                        h = dummyDiv.find("div.tickLabel").height();
+                    dummyDiv.remove();
+                }
+            }
+
+            if (w == null)
+                w = 0;
+            if (h == null)
+                h = 0;
+
+            axis.labelWidth = w;
+            axis.labelHeight = h;
+        }
+
+        function allocateAxisBoxFirstPhase(axis) {
+            // find the bounding box of the axis by looking at label
+            // widths/heights and ticks, make room by diminishing the
+            // plotOffset
+
+            var lw = axis.labelWidth,
+                lh = axis.labelHeight,
+                pos = axis.options.position,
+                tickLength = axis.options.tickLength,
+                axismargin = options.grid.axisMargin,
+                padding = options.grid.labelMargin,
+                all = axis.direction == "x" ? xaxes : yaxes,
+                index;
+
+            // determine axis margin
+            var samePosition = $.grep(all, function (a) {
+                return a && a.options.position == pos && a.reserveSpace;
+            });
+            if ($.inArray(axis, samePosition) == samePosition.length - 1)
+                axismargin = 0; // outermost
+
+            // determine tick length - if we're innermost, we can use "full"
+            if (tickLength == null)
+                tickLength = "full";
+
+            var sameDirection = $.grep(all, function (a) {
+                return a && a.reserveSpace;
+            });
+
+            var innermost = $.inArray(axis, sameDirection) == 0;
+            if (!innermost && tickLength == "full")
+                tickLength = 5;
+                
+            if (!isNaN(+tickLength))
+                padding += +tickLength;
+
+            // compute box
+            if (axis.direction == "x") {
+                lh += padding;
+                
+                if (pos == "bottom") {
+                    plotOffset.bottom += lh + axismargin;
+                    axis.box = { top: canvasHeight - plotOffset.bottom, height: lh };
+                }
+                else {
+                    axis.box = { top: plotOffset.top + axismargin, height: lh };
+                    plotOffset.top += lh + axismargin;
+                }
+            }
+            else {
+                lw += padding;
+                
+                if (pos == "left") {
+                    axis.box = { left: plotOffset.left + axismargin, width: lw };
+                    plotOffset.left += lw + axismargin;
+                }
+                else {
+                    plotOffset.right += lw + axismargin;
+                    axis.box = { left: canvasWidth - plotOffset.right, width: lw };
+                }
+            }
+
+             // save for future reference
+            axis.position = pos;
+            axis.tickLength = tickLength;
+            axis.box.padding = padding;
+            axis.innermost = innermost;
+        }
+
+        function allocateAxisBoxSecondPhase(axis) {
+            // set remaining bounding box coordinates
+            if (axis.direction == "x") {
+                axis.box.left = plotOffset.left;
+                axis.box.width = plotWidth;
+            }
+            else {
+                axis.box.top = plotOffset.top;
+                axis.box.height = plotHeight;
+            }
+        }
+        
+        function setupGrid() {
+            var i, axes = allAxes();
+
+            // first calculate the plot and axis box dimensions
+
+            $.each(axes, function (_, axis) {
+                axis.show = axis.options.show;
+                if (axis.show == null)
+                    axis.show = axis.used; // by default an axis is visible if it's got data
+                
+                axis.reserveSpace = axis.show || axis.options.reserveSpace;
+
+                setRange(axis);
+            });
+
+            allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; });
+
+            plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0;
+            if (options.grid.show) {
+                $.each(allocatedAxes, function (_, axis) {
+                    // make the ticks
+                    setupTickGeneration(axis);
+                    setTicks(axis);
+                    snapRangeToTicks(axis, axis.ticks);
+
+                    // find labelWidth/Height for axis
+                    measureTickLabels(axis);
+                });
+
+                // with all dimensions in house, we can compute the
+                // axis boxes, start from the outside (reverse order)
+                for (i = allocatedAxes.length - 1; i >= 0; --i)
+                    allocateAxisBoxFirstPhase(allocatedAxes[i]);
+
+                // make sure we've got enough space for things that
+                // might stick out
+                var minMargin = options.grid.minBorderMargin;
+                if (minMargin == null) {
+                    minMargin = 0;
+                    for (i = 0; i < series.length; ++i)
+                        minMargin = Math.max(minMargin, series[i].points.radius + series[i].points.lineWidth/2);
+                }
+                    
+                for (var a in plotOffset) {
+                    plotOffset[a] += options.grid.borderWidth;
+                    plotOffset[a] = Math.max(minMargin, plotOffset[a]);
+                }
+            }
+            
+            plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
+            plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
+
+            // now we got the proper plotWidth/Height, we can compute the scaling
+            $.each(axes, function (_, axis) {
+                setTransformationHelpers(axis);
+            });
+
+            if (options.grid.show) {
+                $.each(allocatedAxes, function (_, axis) {
+                    allocateAxisBoxSecondPhase(axis);
+                });
+
+                insertAxisLabels();
+            }
+            
+            insertLegend();
+        }
+        
+        function setRange(axis) {
+            var opts = axis.options,
+                min = +(opts.min != null ? opts.min : axis.datamin),
+                max = +(opts.max != null ? opts.max : axis.datamax),
+                delta = max - min;
+
+            if (delta == 0.0) {
+                // degenerate case
+                var widen = max == 0 ? 1 : 0.01;
+
+                if (opts.min == null)
+                    min -= widen;
+                // always widen max if we couldn't widen min to ensure we
+                // don't fall into min == max which doesn't work
+                if (opts.max == null || opts.min != null)
+                    max += widen;
+            }
+            else {
+                // consider autoscaling
+                var margin = opts.autoscaleMargin;
+                if (margin != null) {
+                    if (opts.min == null) {
+                        min -= delta * margin;
+                        // make sure we don't go below zero if all values
+                        // are positive
+                        if (min < 0 && axis.datamin != null && axis.datamin >= 0)
+                            min = 0;
+                    }
+                    if (opts.max == null) {
+                        max += delta * margin;
+                        if (max > 0 && axis.datamax != null && axis.datamax <= 0)
+                            max = 0;
+                    }
+                }
+            }
+            axis.min = min;
+            axis.max = max;
+        }
+
+        function setupTickGeneration(axis) {
+            var opts = axis.options;
+                
+            // estimate number of ticks
+            var noTicks;
+            if (typeof opts.ticks == "number" && opts.ticks > 0)
+                noTicks = opts.ticks;
+            else
+                // heuristic based on the model a*sqrt(x) fitted to
+                // some data points that seemed reasonable
+                noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight);
+
+            var delta = (axis.max - axis.min) / noTicks,
+                size, generator, unit, formatter, i, magn, norm;
+
+            if (opts.mode == "time") {
+                // pretty handling of time
+                
+                // map of app. size of time units in milliseconds
+                var timeUnitSize = {
+                    "second": 1000,
+                    "minute": 60 * 1000,
+                    "hour": 60 * 60 * 1000,
+                    "day": 24 * 60 * 60 * 1000,
+                    "month": 30 * 24 * 60 * 60 * 1000,
+                    "year": 365.2425 * 24 * 60 * 60 * 1000
+                };
+
+
+                // the allowed tick sizes, after 1 year we use
+                // an integer algorithm
+                var spec = [
+                    [1, "second"], [2, "second"], [5, "second"], [10, "second"],
+                    [30, "second"], 
+                    [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
+                    [30, "minute"], 
+                    [1, "hour"], [2, "hour"], [4, "hour"],
+                    [8, "hour"], [12, "hour"],
+                    [1, "day"], [2, "day"], [3, "day"],
+                    [0.25, "month"], [0.5, "month"], [1, "month"],
+                    [2, "month"], [3, "month"], [6, "month"],
+                    [1, "year"]
+                ];
+
+                var minSize = 0;
+                if (opts.minTickSize != null) {
+                    if (typeof opts.tickSize == "number")
+                        minSize = opts.tickSize;
+                    else
+                        minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
+                }
+
+                for (var i = 0; i < spec.length - 1; ++i)
+                    if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]
+                                 + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
+                       && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
+                        break;
+                size = spec[i][0];
+                unit = spec[i][1];
+                
+                // special-case the possibility of several years
+                if (unit == "year") {
+                    magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
+                    norm = (delta / timeUnitSize.year) / magn;
+                    if (norm < 1.5)
+                        size = 1;
+                    else if (norm < 3)
+                        size = 2;
+                    else if (norm < 7.5)
+                        size = 5;
+                    else
+                        size = 10;
+
+                    size *= magn;
+                }
+
+                axis.tickSize = opts.tickSize || [size, unit];
+                
+                generator = function(axis) {
+                    var ticks = [],
+                        tickSize = axis.tickSize[0], unit = axis.tickSize[1],
+                        d = new Date(axis.min);
+                    
+                    var step = tickSize * timeUnitSize[unit];
+
+                    if (unit == "second")
+                        d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize));
+                    if (unit == "minute")
+                        d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize));
+                    if (unit == "hour")
+                        d.setUTCHours(floorInBase(d.getUTCHours(), tickSize));
+                    if (unit == "month")
+                        d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize));
+                    if (unit == "year")
+                        d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize));
+                    
+                    // reset smaller components
+                    d.setUTCMilliseconds(0);
+                    if (step >= timeUnitSize.minute)
+                        d.setUTCSeconds(0);
+                    if (step >= timeUnitSize.hour)
+                        d.setUTCMinutes(0);
+                    if (step >= timeUnitSize.day)
+                        d.setUTCHours(0);
+                    if (step >= timeUnitSize.day * 4)
+                        d.setUTCDate(1);
+                    if (step >= timeUnitSize.year)
+                        d.setUTCMonth(0);
+
+
+                    var carry = 0, v = Number.NaN, prev;
+                    do {
+                        prev = v;
+                        v = d.getTime();
+                        ticks.push(v);
+                        if (unit == "month") {
+                            if (tickSize < 1) {
+                                // a bit complicated - we'll divide the month
+                                // up but we need to take care of fractions
+                                // so we don't end up in the middle of a day
+                                d.setUTCDate(1);
+                                var start = d.getTime();
+                                d.setUTCMonth(d.getUTCMonth() + 1);
+                                var end = d.getTime();
+                                d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
+                                carry = d.getUTCHours();
+                                d.setUTCHours(0);
+                            }
+                            else
+                                d.setUTCMonth(d.getUTCMonth() + tickSize);
+                        }
+                        else if (unit == "year") {
+                            d.setUTCFullYear(d.getUTCFullYear() + tickSize);
+                        }
+                        else
+                            d.setTime(v + step);
+                    } while (v < axis.max && v != prev);
+
+                    return ticks;
+                };
+
+                formatter = function (v, axis) {
+                    var d = new Date(v);
+
+                    // first check global format
+                    if (opts.timeformat != null)
+                        return $.plot.formatDate(d, opts.timeformat, opts.monthNames);
+                    
+                    var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
+                    var span = axis.max - axis.min;
+                    var suffix = (opts.twelveHourClock) ? " %p" : "";
+                    
+                    if (t < timeUnitSize.minute)
+                        fmt = "%h:%M:%S" + suffix;
+                    else if (t < timeUnitSize.day) {
+                        if (span < 2 * timeUnitSize.day)
+                            fmt = "%h:%M" + suffix;
+                        else
+                            fmt = "%b %d %h:%M" + suffix;
+                    }
+                    else if (t < timeUnitSize.month)
+                        fmt = "%b %d";
+                    else if (t < timeUnitSize.year) {
+                        if (span < timeUnitSize.year)
+                            fmt = "%b";
+                        else
+                            fmt = "%b %y";
+                    }
+                    else
+                        fmt = "%y";
+                    
+                    return $.plot.formatDate(d, fmt, opts.monthNames);
+                };
+            }
+            else {
+                // pretty rounding of base-10 numbers
+                var maxDec = opts.tickDecimals;
+                var dec = -Math.floor(Math.log(delta) / Math.LN10);
+                if (maxDec != null && dec > maxDec)
+                    dec = maxDec;
+
+                magn = Math.pow(10, -dec);
+                norm = delta / magn; // norm is between 1.0 and 10.0
+                
+                if (norm < 1.5)
+                    size = 1;
+                else if (norm < 3) {
+                    size = 2;
+                    // special case for 2.5, requires an extra decimal
+                    if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
+                        size = 2.5;
+                        ++dec;
+                    }
+                }
+                else if (norm < 7.5)
+                    size = 5;
+                else
+                    size = 10;
+
+                size *= magn;
+                
+                if (opts.minTickSize != null && size < opts.minTickSize)
+                    size = opts.minTickSize;
+
+                axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
+                axis.tickSize = opts.tickSize || size;
+
+                generator = function (axis) {
+                    var ticks = [];
+
+                    // spew out all possible ticks
+                    var start = floorInBase(axis.min, axis.tickSize),
+                        i = 0, v = Number.NaN, prev;
+                    do {
+                        prev = v;
+                        v = start + i * axis.tickSize;
+                        ticks.push(v);
+                        ++i;
+                    } while (v < axis.max && v != prev);
+                    return ticks;
+                };
+
+                formatter = function (v, axis) {
+                    return v.toFixed(axis.tickDecimals);
+                };
+            }
+
+            if (opts.alignTicksWithAxis != null) {
+                var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];
+                if (otherAxis && otherAxis.used && otherAxis != axis) {
+                    // consider snapping min/max to outermost nice ticks
+                    var niceTicks = generator(axis);
+                    if (niceTicks.length > 0) {
+                        if (opts.min == null)
+                            axis.min = Math.min(axis.min, niceTicks[0]);
+                        if (opts.max == null && niceTicks.length > 1)
+                            axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);
+                    }
+                    
+                    generator = function (axis) {
+                        // copy ticks, scaled to this axis
+                        var ticks = [], v, i;
+                        for (i = 0; i < otherAxis.ticks.length; ++i) {
+                            v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);
+                            v = axis.min + v * (axis.max - axis.min);
+                            ticks.push(v);
+                        }
+                        return ticks;
+                    };
+                    
+                    // we might need an extra decimal since forced
+                    // ticks don't necessarily fit naturally
+                    if (axis.mode != "time" && opts.tickDecimals == null) {
+                        var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1),
+                            ts = generator(axis);
+
+                        // only proceed if the tick interval rounded
+                        // with an extra decimal doesn't give us a
+                        // zero at end
+                        if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))
+                            axis.tickDecimals = extraDec;
+                    }
+                }
+            }
+
+            axis.tickGenerator = generator;
+            if ($.isFunction(opts.tickFormatter))
+                axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); };
+            else
+                axis.tickFormatter = formatter;
+        }
+        
+        function setTicks(axis) {
+            var oticks = axis.options.ticks, ticks = [];
+            if (oticks == null || (typeof oticks == "number" && oticks > 0))
+                ticks = axis.tickGenerator(axis);
+            else if (oticks) {
+                if ($.isFunction(oticks))
+                    // generate the ticks
+                    ticks = oticks({ min: axis.min, max: axis.max });
+                else
+                    ticks = oticks;
+            }
+
+            // clean up/labelify the supplied ticks, copy them over
+            var i, v;
+            axis.ticks = [];
+            for (i = 0; i < ticks.length; ++i) {
+                var label = null;
+                var t = ticks[i];
+                if (typeof t == "object") {
+                    v = +t[0];
+                    if (t.length > 1)
+                        label = t[1];
+                }
+                else
+                    v = +t;
+                if (label == null)
+                    label = axis.tickFormatter(v, axis);
+                if (!isNaN(v))
+                    axis.ticks.push({ v: v, label: label });
+            }
+        }
+
+        function snapRangeToTicks(axis, ticks) {
+            if (axis.options.autoscaleMargin && ticks.length > 0) {
+                // snap to ticks
+                if (axis.options.min == null)
+                    axis.min = Math.min(axis.min, ticks[0].v);
+                if (axis.options.max == null && ticks.length > 1)
+                    axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
+            }
+        }
+      
+        function draw() {
+            ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+
+            var grid = options.grid;
+
+            // draw background, if any
+            if (grid.show && grid.backgroundColor)
+                drawBackground();
+            
+            if (grid.show && !grid.aboveData)
+                drawGrid();
+
+            for (var i = 0; i < series.length; ++i) {
+                executeHooks(hooks.drawSeries, [ctx, series[i]]);
+                drawSeries(series[i]);
+            }
+
+            executeHooks(hooks.draw, [ctx]);
+            
+            if (grid.show && grid.aboveData)
+                drawGrid();
+        }
+
+        function extractRange(ranges, coord) {
+            var axis, from, to, key, axes = allAxes();
+
+            for (i = 0; i < axes.length; ++i) {
+                axis = axes[i];
+                if (axis.direction == coord) {
+                    key = coord + axis.n + "axis";
+                    if (!ranges[key] && axis.n == 1)
+                        key = coord + "axis"; // support x1axis as xaxis
+                    if (ranges[key]) {
+                        from = ranges[key].from;
+                        to = ranges[key].to;
+                        break;
+                    }
+                }
+            }
+
+            // backwards-compat stuff - to be removed in future
+            if (!ranges[key]) {
+                axis = coord == "x" ? xaxes[0] : yaxes[0];
+                from = ranges[coord + "1"];
+                to = ranges[coord + "2"];
+            }
+
+            // auto-reverse as an added bonus
+            if (from != null && to != null && from > to) {
+                var tmp = from;
+                from = to;
+                to = tmp;
+            }
+            
+            return { from: from, to: to, axis: axis };
+        }
+        
+        function drawBackground() {
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
+            ctx.fillRect(0, 0, plotWidth, plotHeight);
+            ctx.restore();
+        }
+
+        function drawGrid() {
+            var i;
+            
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            // draw markings
+            var markings = options.grid.markings;
+            if (markings) {
+                if ($.isFunction(markings)) {
+                    var axes = plot.getAxes();
+                    // xmin etc. is backwards compatibility, to be
+                    // removed in the future
+                    axes.xmin = axes.xaxis.min;
+                    axes.xmax = axes.xaxis.max;
+                    axes.ymin = axes.yaxis.min;
+                    axes.ymax = axes.yaxis.max;
+                    
+                    markings = markings(axes);
+                }
+
+                for (i = 0; i < markings.length; ++i) {
+                    var m = markings[i],
+                        xrange = extractRange(m, "x"),
+                        yrange = extractRange(m, "y");
+
+                    // fill in missing
+                    if (xrange.from == null)
+                        xrange.from = xrange.axis.min;
+                    if (xrange.to == null)
+                        xrange.to = xrange.axis.max;
+                    if (yrange.from == null)
+                        yrange.from = yrange.axis.min;
+                    if (yrange.to == null)
+                        yrange.to = yrange.axis.max;
+
+                    // clip
+                    if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
+                        yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
+                        continue;
+
+                    xrange.from = Math.max(xrange.from, xrange.axis.min);
+                    xrange.to = Math.min(xrange.to, xrange.axis.max);
+                    yrange.from = Math.max(yrange.from, yrange.axis.min);
+                    yrange.to = Math.min(yrange.to, yrange.axis.max);
+
+                    if (xrange.from == xrange.to && yrange.from == yrange.to)
+                        continue;
+
+                    // then draw
+                    xrange.from = xrange.axis.p2c(xrange.from);
+                    xrange.to = xrange.axis.p2c(xrange.to);
+                    yrange.from = yrange.axis.p2c(yrange.from);
+                    yrange.to = yrange.axis.p2c(yrange.to);
+                    
+                    if (xrange.from == xrange.to || yrange.from == yrange.to) {
+                        // draw line
+                        ctx.beginPath();
+                        ctx.strokeStyle = m.color || options.grid.markingsColor;
+                        ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
+                        ctx.moveTo(xrange.from, yrange.from);
+                        ctx.lineTo(xrange.to, yrange.to);
+                        ctx.stroke();
+                    }
+                    else {
+                        // fill area
+                        ctx.fillStyle = m.color || options.grid.markingsColor;
+                        ctx.fillRect(xrange.from, yrange.to,
+                                     xrange.to - xrange.from,
+                                     yrange.from - yrange.to);
+                    }
+                }
+            }
+            
+            // draw the ticks
+            var axes = allAxes(), bw = options.grid.borderWidth;
+
+            for (var j = 0; j < axes.length; ++j) {
+                var axis = axes[j], box = axis.box,
+                    t = axis.tickLength, x, y, xoff, yoff;
+                if (!axis.show || axis.ticks.length == 0)
+                    continue
+                
+                ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString();
+                ctx.lineWidth = 1;
+
+                // find the edges
+                if (axis.direction == "x") {
+                    x = 0;
+                    if (t == "full")
+                        y = (axis.position == "top" ? 0 : plotHeight);
+                    else
+                        y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0);
+                }
+                else {
+                    y = 0;
+                    if (t == "full")
+                        x = (axis.position == "left" ? 0 : plotWidth);
+                    else
+                        x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0);
+                }
+                
+                // draw tick bar
+                if (!axis.innermost) {
+                    ctx.beginPath();
+                    xoff = yoff = 0;
+                    if (axis.direction == "x")
+                        xoff = plotWidth;
+                    else
+                        yoff = plotHeight;
+                    
+                    if (ctx.lineWidth == 1) {
+                        x = Math.floor(x) + 0.5;
+                        y = Math.floor(y) + 0.5;
+                    }
+
+                    ctx.moveTo(x, y);
+                    ctx.lineTo(x + xoff, y + yoff);
+                    ctx.stroke();
+                }
+
+                // draw ticks
+                ctx.beginPath();
+                for (i = 0; i < axis.ticks.length; ++i) {
+                    var v = axis.ticks[i].v;
+                    
+                    xoff = yoff = 0;
+
+                    if (v < axis.min || v > axis.max
+                        // skip those lying on the axes if we got a border
+                        || (t == "full" && bw > 0
+                            && (v == axis.min || v == axis.max)))
+                        continue;
+
+                    if (axis.direction == "x") {
+                        x = axis.p2c(v);
+                        yoff = t == "full" ? -plotHeight : t;
+                        
+                        if (axis.position == "top")
+                            yoff = -yoff;
+                    }
+                    else {
+                        y = axis.p2c(v);
+                        xoff = t == "full" ? -plotWidth : t;
+                        
+                        if (axis.position == "left")
+                            xoff = -xoff;
+                    }
+
+                    if (ctx.lineWidth == 1) {
+                        if (axis.direction == "x")
+                            x = Math.floor(x) + 0.5;
+                        else
+                            y = Math.floor(y) + 0.5;
+                    }
+
+                    ctx.moveTo(x, y);
+                    ctx.lineTo(x + xoff, y + yoff);
+                }
+                
+                ctx.stroke();
+            }
+            
+            
+            // draw border
+            if (bw) {
+                ctx.lineWidth = bw;
+                ctx.strokeStyle = options.grid.borderColor;
+                ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
+            }
+
+            ctx.restore();
+        }
+
+        function insertAxisLabels() {
+            placeholder.find(".tickLabels").remove();
+            
+            var html = ['<div class="tickLabels" style="font-size:smaller">'];
+
+            var axes = allAxes();
+            for (var j = 0; j < axes.length; ++j) {
+                var axis = axes[j], box = axis.box;
+                if (!axis.show)
+                    continue;
+                //debug: html.push('<div style="position:absolute;opacity:0.10;background-color:red;left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width +  'px;height:' + box.height + 'px"></div>')
+                html.push('<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis" style="color:' + axis.options.color + '">');
+                for (var i = 0; i < axis.ticks.length; ++i) {
+                    var tick = axis.ticks[i];
+                    if (!tick.label || tick.v < axis.min || tick.v > axis.max)
+                        continue;
+
+                    var pos = {}, align;
+                    
+                    if (axis.direction == "x") {
+                        align = "center";
+                        pos.left = Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2);
+                        if (axis.position == "bottom")
+                            pos.top = box.top + box.padding;
+                        else
+                            pos.bottom = canvasHeight - (box.top + box.height - box.padding);
+                    }
+                    else {
+                        pos.top = Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2);
+                        if (axis.position == "left") {
+                            pos.right = canvasWidth - (box.left + box.width - box.padding)
+                            align = "right";
+                        }
+                        else {
+                            pos.left = box.left + box.padding;
+                            align = "left";
+                        }
+                    }
+
+                    pos.width = axis.labelWidth;
+
+                    var style = ["position:absolute", "text-align:" + align ];
+                    for (var a in pos)
+                        style.push(a + ":" + pos[a] + "px")
+                    
+                    html.push('<div class="tickLabel" style="' + style.join(';') + '">' + tick.label + '</div>');
+                }
+                html.push('</div>');
+            }
+
+            html.push('</div>');
+
+            placeholder.append(html.join(""));
+        }
+
+        function drawSeries(series) {
+            if (series.lines.show)
+                drawSeriesLines(series);
+            if (series.bars.show)
+                drawSeriesBars(series);
+            if (series.points.show)
+                drawSeriesPoints(series);
+        }
+        
+        function drawSeriesLines(series) {
+            function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
+                var points = datapoints.points,
+                    ps = datapoints.pointsize,
+                    prevx = null, prevy = null;
+                
+                ctx.beginPath();
+                for (var i = ps; i < points.length; i += ps) {
+                    var x1 = points[i - ps], y1 = points[i - ps + 1],
+                        x2 = points[i], y2 = points[i + 1];
+                    
+                    if (x1 == null || x2 == null)
+                        continue;
+
+                    // clip with ymin
+                    if (y1 <= y2 && y1 < axisy.min) {
+                        if (y2 < axisy.min)
+                            continue;   // line segment is outside
+                        // compute new intersection point
+                        x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.min;
+                    }
+                    else if (y2 <= y1 && y2 < axisy.min) {
+                        if (y1 < axisy.min)
+                            continue;
+                        x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.min;
+                    }
+
+                    // clip with ymax
+                    if (y1 >= y2 && y1 > axisy.max) {
+                        if (y2 > axisy.max)
+                            continue;
+                        x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.max;
+                    }
+                    else if (y2 >= y1 && y2 > axisy.max) {
+                        if (y1 > axisy.max)
+                            continue;
+                        x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.max;
+                    }
+
+                    // clip with xmin
+                    if (x1 <= x2 && x1 < axisx.min) {
+                        if (x2 < axisx.min)
+                            continue;
+                        y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.min;
+                    }
+                    else if (x2 <= x1 && x2 < axisx.min) {
+                        if (x1 < axisx.min)
+                            continue;
+                        y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.min;
+                    }
+
+                    // clip with xmax
+                    if (x1 >= x2 && x1 > axisx.max) {
+                        if (x2 > axisx.max)
+                            continue;
+                        y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.max;
+                    }
+                    else if (x2 >= x1 && x2 > axisx.max) {
+                        if (x1 > axisx.max)
+                            continue;
+                        y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.max;
+                    }
+
+                    if (x1 != prevx || y1 != prevy)
+                        ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
+                    
+                    prevx = x2;
+                    prevy = y2;
+                    ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
+                }
+                ctx.stroke();
+            }
+
+            function plotLineArea(datapoints, axisx, axisy) {
+                var points = datapoints.points,
+                    ps = datapoints.pointsize,
+                    bottom = Math.min(Math.max(0, axisy.min), axisy.max),
+                    i = 0, top, areaOpen = false,
+                    ypos = 1, segmentStart = 0, segmentEnd = 0;
+
+                // we process each segment in two turns, first forward
+                // direction to sketch out top, then once we hit the
+                // end we go backwards to sketch the bottom
+                while (true) {
+                    if (ps > 0 && i > points.length + ps)
+                        break;
+
+                    i += ps; // ps is negative if going backwards
+
+                    var x1 = points[i - ps],
+                        y1 = points[i - ps + ypos],
+                        x2 = points[i], y2 = points[i + ypos];
+
+                    if (areaOpen) {
+                        if (ps > 0 && x1 != null && x2 == null) {
+                            // at turning point
+                            segmentEnd = i;
+                            ps = -ps;
+                            ypos = 2;
+                            continue;
+                        }
+
+                        if (ps < 0 && i == segmentStart + ps) {
+                            // done with the reverse sweep
+                            ctx.fill();
+                            areaOpen = false;
+                            ps = -ps;
+                            ypos = 1;
+                            i = segmentStart = segmentEnd + ps;
+                            continue;
+                        }
+                    }
+
+                    if (x1 == null || x2 == null)
+                        continue;
+
+                    // clip x values
+                    
+                    // clip with xmin
+                    if (x1 <= x2 && x1 < axisx.min) {
+                        if (x2 < axisx.min)
+                            continue;
+                        y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.min;
+                    }
+                    else if (x2 <= x1 && x2 < axisx.min) {
+                        if (x1 < axisx.min)
+                            continue;
+                        y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.min;
+                    }
+
+                    // clip with xmax
+                    if (x1 >= x2 && x1 > axisx.max) {
+                        if (x2 > axisx.max)
+                            continue;
+                        y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.max;
+                    }
+                    else if (x2 >= x1 && x2 > axisx.max) {
+                        if (x1 > axisx.max)
+                            continue;
+                        y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.max;
+                    }
+
+                    if (!areaOpen) {
+                        // open area
+                        ctx.beginPath();
+                        ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
+                        areaOpen = true;
+                    }
+                    
+                    // now first check the case where both is outside
+                    if (y1 >= axisy.max && y2 >= axisy.max) {
+                        ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
+                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
+                        continue;
+                    }
+                    else if (y1 <= axisy.min && y2 <= axisy.min) {
+                        ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
+                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
+                        continue;
+                    }
+                    
+                    // else it's a bit more complicated, there might
+                    // be a flat maxed out rectangle first, then a
+                    // triangular cutout or reverse; to find these
+                    // keep track of the current x values
+                    var x1old = x1, x2old = x2;
+
+                    // clip the y values, without shortcutting, we
+                    // go through all cases in turn
+                    
+                    // clip with ymin
+                    if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
+                        x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.min;
+                    }
+                    else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
+                        x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.min;
+                    }
+
+                    // clip with ymax
+                    if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
+                        x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.max;
+                    }
+                    else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
+                        x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.max;
+                    }
+
+                    // if the x value was changed we got a rectangle
+                    // to fill
+                    if (x1 != x1old) {
+                        ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
+                        // it goes to (x1, y1), but we fill that below
+                    }
+                    
+                    // fill triangular section, this sometimes result
+                    // in redundant points if (x1, y1) hasn't changed
+                    // from previous line to, but we just ignore that
+                    ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
+                    ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
+
+                    // fill the other rectangle if it's there
+                    if (x2 != x2old) {
+                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
+                        ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
+                    }
+                }
+            }
+
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+            ctx.lineJoin = "round";
+
+            var lw = series.lines.lineWidth,
+                sw = series.shadowSize;
+            // FIXME: consider another form of shadow when filling is turned on
+            if (lw > 0 && sw > 0) {
+                // draw shadow as a thick and thin line with transparency
+                ctx.lineWidth = sw;
+                ctx.strokeStyle = "rgba(0,0,0,0.1)";
+                // position shadow at angle from the mid of line
+                var angle = Math.PI/18;
+                plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
+                ctx.lineWidth = sw/2;
+                plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
+            }
+
+            ctx.lineWidth = lw;
+            ctx.strokeStyle = series.color;
+            var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
+            if (fillStyle) {
+                ctx.fillStyle = fillStyle;
+                plotLineArea(series.datapoints, series.xaxis, series.yaxis);
+            }
+
+            if (lw > 0)
+                plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
+            ctx.restore();
+        }
+
+        function drawSeriesPoints(series) {
+            function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {
+                var points = datapoints.points, ps = datapoints.pointsize;
+
+                for (var i = 0; i < points.length; i += ps) {
+                    var x = points[i], y = points[i + 1];
+                    if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
+                        continue;
+                    
+                    ctx.beginPath();
+                    x = axisx.p2c(x);
+                    y = axisy.p2c(y) + offset;
+                    if (symbol == "circle")
+                        ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
+                    else
+                        symbol(ctx, x, y, radius, shadow);
+                    ctx.closePath();
+                    
+                    if (fillStyle) {
+                        ctx.fillStyle = fillStyle;
+                        ctx.fill();
+                    }
+                    ctx.stroke();
+                }
+            }
+            
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            var lw = series.points.lineWidth,
+                sw = series.shadowSize,
+                radius = series.points.radius,
+                symbol = series.points.symbol;
+            if (lw > 0 && sw > 0) {
+                // draw shadow in two steps
+                var w = sw / 2;
+                ctx.lineWidth = w;
+                ctx.strokeStyle = "rgba(0,0,0,0.1)";
+                plotPoints(series.datapoints, radius, null, w + w/2, true,
+                           series.xaxis, series.yaxis, symbol);
+
+                ctx.strokeStyle = "rgba(0,0,0,0.2)";
+                plotPoints(series.datapoints, radius, null, w/2, true,
+                           series.xaxis, series.yaxis, symbol);
+            }
+
+            ctx.lineWidth = lw;
+            ctx.strokeStyle = series.color;
+            plotPoints(series.datapoints, radius,
+                       getFillStyle(series.points, series.color), 0, false,
+                       series.xaxis, series.yaxis, symbol);
+            ctx.restore();
+        }
+
+        function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
+            var left, right, bottom, top,
+                drawLeft, drawRight, drawTop, drawBottom,
+                tmp;
+
+            // in horizontal mode, we start the bar from the left
+            // instead of from the bottom so it appears to be
+            // horizontal rather than vertical
+            if (horizontal) {
+                drawBottom = drawRight = drawTop = true;
+                drawLeft = false;
+                left = b;
+                right = x;
+                top = y + barLeft;
+                bottom = y + barRight;
+
+                // account for negative bars
+                if (right < left) {
+                    tmp = right;
+                    right = left;
+                    left = tmp;
+                    drawLeft = true;
+                    drawRight = false;
+                }
+            }
+            else {
+                drawLeft = drawRight = drawTop = true;
+                drawBottom = false;
+                left = x + barLeft;
+                right = x + barRight;
+                bottom = b;
+                top = y;
+
+                // account for negative bars
+                if (top < bottom) {
+                    tmp = top;
+                    top = bottom;
+                    bottom = tmp;
+                    drawBottom = true;
+                    drawTop = false;
+                }
+            }
+           
+            // clip
+            if (right < axisx.min || left > axisx.max ||
+                top < axisy.min || bottom > axisy.max)
+                return;
+            
+            if (left < axisx.min) {
+                left = axisx.min;
+                drawLeft = false;
+            }
+
+            if (right > axisx.max) {
+                right = axisx.max;
+                drawRight = false;
+            }
+
+            if (bottom < axisy.min) {
+                bottom = axisy.min;
+                drawBottom = false;
+            }
+            
+            if (top > axisy.max) {
+                top = axisy.max;
+                drawTop = false;
+            }
+
+            left = axisx.p2c(left);
+            bottom = axisy.p2c(bottom);
+            right = axisx.p2c(right);
+            top = axisy.p2c(top);
+            
+            // fill the bar
+            if (fillStyleCallback) {
+                c.beginPath();
+                c.moveTo(left, bottom);
+                c.lineTo(left, top);
+                c.lineTo(right, top);
+                c.lineTo(right, bottom);
+                c.fillStyle = fillStyleCallback(bottom, top);
+                c.fill();
+            }
+
+            // draw outline
+            if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
+                c.beginPath();
+
+                // FIXME: inline moveTo is buggy with excanvas
+                c.moveTo(left, bottom + offset);
+                if (drawLeft)
+                    c.lineTo(left, top + offset);
+                else
+                    c.moveTo(left, top + offset);
+                if (drawTop)
+                    c.lineTo(right, top + offset);
+                else
+                    c.moveTo(right, top + offset);
+                if (drawRight)
+                    c.lineTo(right, bottom + offset);
+                else
+                    c.moveTo(right, bottom + offset);
+                if (drawBottom)
+                    c.lineTo(left, bottom + offset);
+                else
+                    c.moveTo(left, bottom + offset);
+                c.stroke();
+            }
+        }
+        
+        function drawSeriesBars(series) {
+            function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
+                var points = datapoints.points, ps = datapoints.pointsize;
+                
+                for (var i = 0; i < points.length; i += ps) {
+                    if (points[i] == null)
+                        continue;
+                    drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
+                }
+            }
+
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            // FIXME: figure out a way to add shadows (for instance along the right edge)
+            ctx.lineWidth = series.bars.lineWidth;
+            ctx.strokeStyle = series.color;
+            var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
+            var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
+            plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
+            ctx.restore();
+        }
+
+        function getFillStyle(filloptions, seriesColor, bottom, top) {
+            var fill = filloptions.fill;
+            if (!fill)
+                return null;
+
+            if (filloptions.fillColor)
+                return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
+            
+            var c = $.color.parse(seriesColor);
+            c.a = typeof fill == "number" ? fill : 0.4;
+            c.normalize();
+            return c.toString();
+        }
+        
+        function insertLegend() {
+            placeholder.find(".legend").remove();
+
+            if (!options.legend.show)
+                return;
+            
+            var fragments = [], rowStarted = false,
+                lf = options.legend.labelFormatter, s, label;
+            for (var i = 0; i < series.length; ++i) {
+                s = series[i];
+                label = s.label;
+                if (!label)
+                    continue;
+                
+                if (i % options.legend.noColumns == 0) {
+                    if (rowStarted)
+                        fragments.push('</tr>');
+                    fragments.push('<tr>');
+                    rowStarted = true;
+                }
+
+                if (lf)
+                    label = lf(label, s);
+                
+                fragments.push(
+                    '<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + s.color + ';overflow:hidden"></div></div></td>' +
+                    '<td class="legendLabel">' + label + '</td>');
+            }
+            if (rowStarted)
+                fragments.push('</tr>');
+            
+            if (fragments.length == 0)
+                return;
+
+            var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
+            if (options.legend.container != null)
+                $(options.legend.container).html(table);
+            else {
+                var pos = "",
+                    p = options.legend.position,
+                    m = options.legend.margin;
+                if (m[0] == null)
+                    m = [m, m];
+                if (p.charAt(0) == "n")
+                    pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
+                else if (p.charAt(0) == "s")
+                    pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
+                if (p.charAt(1) == "e")
+                    pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
+                else if (p.charAt(1) == "w")
+                    pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
+                var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
+                if (options.legend.backgroundOpacity != 0.0) {
+                    // put in the transparent background
+                    // separately to avoid blended labels and
+                    // label boxes
+                    var c = options.legend.backgroundColor;
+                    if (c == null) {
+                        c = options.grid.backgroundColor;
+                        if (c && typeof c == "string")
+                            c = $.color.parse(c);
+                        else
+                            c = $.color.extract(legend, 'background-color');
+                        c.a = 1;
+                        c = c.toString();
+                    }
+                    var div = legend.children();
+                    $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"></div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
+                }
+            }
+        }
+
+
+        // interactive features
+        
+        var highlights = [],
+            redrawTimeout = null;
+        
+        // returns the data item the mouse is over, or null if none is found
+        function findNearbyItem(mouseX, mouseY, seriesFilter) {
+            var maxDistance = options.grid.mouseActiveRadius,
+                smallestDistance = maxDistance * maxDistance + 1,
+                item = null, foundPoint = false, i, j;
+
+            for (i = series.length - 1; i >= 0; --i) {
+                if (!seriesFilter(series[i]))
+                    continue;
+                
+                var s = series[i],
+                    axisx = s.xaxis,
+                    axisy = s.yaxis,
+                    points = s.datapoints.points,
+                    ps = s.datapoints.pointsize,
+                    mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
+                    my = axisy.c2p(mouseY),
+                    maxx = maxDistance / axisx.scale,
+                    maxy = maxDistance / axisy.scale;
+
+                // with inverse transforms, we can't use the maxx/maxy
+                // optimization, sadly
+                if (axisx.options.inverseTransform)
+                    maxx = Number.MAX_VALUE;
+                if (axisy.options.inverseTransform)
+                    maxy = Number.MAX_VALUE;
+                
+                if (s.lines.show || s.points.show) {
+                    for (j = 0; j < points.length; j += ps) {
+                        var x = points[j], y = points[j + 1];
+                        if (x == null)
+                            continue;
+                        
+                        // For points and lines, the cursor must be within a
+                        // certain distance to the data point
+                        if (x - mx > maxx || x - mx < -maxx ||
+                            y - my > maxy || y - my < -maxy)
+                            continue;
+
+                        // We have to calculate distances in pixels, not in
+                        // data units, because the scales of the axes may be different
+                        var dx = Math.abs(axisx.p2c(x) - mouseX),
+                            dy = Math.abs(axisy.p2c(y) - mouseY),
+                            dist = dx * dx + dy * dy; // we save the sqrt
+
+                        // use <= to ensure last point takes precedence
+                        // (last generally means on top of)
+                        if (dist < smallestDistance) {
+                            smallestDistance = dist;
+                            item = [i, j / ps];
+                        }
+                    }
+                }
+                    
+                if (s.bars.show && !item) { // no other point can be nearby
+                    var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
+                        barRight = barLeft + s.bars.barWidth;
+                    
+                    for (j = 0; j < points.length; j += ps) {
+                        var x = points[j], y = points[j + 1], b = points[j + 2];
+                        if (x == null)
+                            continue;
+  
+                        // for a bar graph, the cursor must be inside the bar
+                        if (series[i].bars.horizontal ? 
+                            (mx <= Math.max(b, x) && mx >= Math.min(b, x) && 
+                             my >= y + barLeft && my <= y + barRight) :
+                            (mx >= x + barLeft && mx <= x + barRight &&
+                             my >= Math.min(b, y) && my <= Math.max(b, y)))
+                                item = [i, j / ps];
+                    }
+                }
+            }
+
+            if (item) {
+                i = item[0];
+                j = item[1];
+                ps = series[i].datapoints.pointsize;
+                
+                return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
+                         dataIndex: j,
+                         series: series[i],
+                         seriesIndex: i };
+            }
+            
+            return null;
+        }
+
+        function onMouseMove(e) {
+            if (options.grid.hoverable)
+                triggerClickHoverEvent("plothover", e,
+                                       function (s) { return s["hoverable"] != false; });
+        }
+
+        function onMouseLeave(e) {
+            if (options.grid.hoverable)
+                triggerClickHoverEvent("plothover", e,
+                                       function (s) { return false; });
+        }
+
+        function onClick(e) {
+            triggerClickHoverEvent("plotclick", e,
+                                   function (s) { return s["clickable"] != false; });
+        }
+
+        // trigger click or hover event (they send the same parameters
+        // so we share their code)
+        function triggerClickHoverEvent(eventname, event, seriesFilter) {
+            var offset = eventHolder.offset(),
+                canvasX = event.pageX - offset.left - plotOffset.left,
+                canvasY = event.pageY - offset.top - plotOffset.top,
+            pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
+
+            pos.pageX = event.pageX;
+            pos.pageY = event.pageY;
+
+            var item = findNearbyItem(canvasX, canvasY, seriesFilter);
+
+            if (item) {
+                // fill in mouse pos for any listeners out there
+                item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left);
+                item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top);
+            }
+
+            if (options.grid.autoHighlight) {
+                // clear auto-highlights
+                for (var i = 0; i < highlights.length; ++i) {
+                    var h = highlights[i];
+                    if (h.auto == eventname &&
+                        !(item && h.series == item.series &&
+                          h.point[0] == item.datapoint[0] &&
+                          h.point[1] == item.datapoint[1]))
+                        unhighlight(h.series, h.point);
+                }
+                
+                if (item)
+                    highlight(item.series, item.datapoint, eventname);
+            }
+            
+            placeholder.trigger(eventname, [ pos, item ]);
+        }
+
+        function triggerRedrawOverlay() {
+            if (!redrawTimeout)
+                redrawTimeout = setTimeout(drawOverlay, 30);
+        }
+
+        function drawOverlay() {
+            redrawTimeout = null;
+
+            // draw highlights
+            octx.save();
+            octx.clearRect(0, 0, canvasWidth, canvasHeight);
+            octx.translate(plotOffset.left, plotOffset.top);
+            
+            var i, hi;
+            for (i = 0; i < highlights.length; ++i) {
+                hi = highlights[i];
+
+                if (hi.series.bars.show)
+                    drawBarHighlight(hi.series, hi.point);
+                else
+                    drawPointHighlight(hi.series, hi.point);
+            }
+            octx.restore();
+            
+            executeHooks(hooks.drawOverlay, [octx]);
+        }
+        
+        function highlight(s, point, auto) {
+            if (typeof s == "number")
+                s = series[s];
+
+            if (typeof point == "number") {
+                var ps = s.datapoints.pointsize;
+                point = s.datapoints.points.slice(ps * point, ps * (point + 1));
+            }
+
+            var i = indexOfHighlight(s, point);
+            if (i == -1) {
+                highlights.push({ series: s, point: point, auto: auto });
+
+                triggerRedrawOverlay();
+            }
+            else if (!auto)
+                highlights[i].auto = false;
+        }
+            
+        function unhighlight(s, point) {
+            if (s == null && point == null) {
+                highlights = [];
+                triggerRedrawOverlay();
+            }
+            
+            if (typeof s == "number")
+                s = series[s];
+
+            if (typeof point == "number")
+                point = s.data[point];
+
+            var i = indexOfHighlight(s, point);
+            if (i != -1) {
+                highlights.splice(i, 1);
+
+                triggerRedrawOverlay();
+            }
+        }
+        
+        function indexOfHighlight(s, p) {
+            for (var i = 0; i < highlights.length; ++i) {
+                var h = highlights[i];
+                if (h.series == s && h.point[0] == p[0]
+                    && h.point[1] == p[1])
+                    return i;
+            }
+            return -1;
+        }
+        
+        function drawPointHighlight(series, point) {
+            var x = point[0], y = point[1],
+                axisx = series.xaxis, axisy = series.yaxis;
+            
+            if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
+                return;
+            
+            var pointRadius = series.points.radius + series.points.lineWidth / 2;
+            octx.lineWidth = pointRadius;
+            octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+            var radius = 1.5 * pointRadius,
+                x = axisx.p2c(x),
+                y = axisy.p2c(y);
+            
+            octx.beginPath();
+            if (series.points.symbol == "circle")
+                octx.arc(x, y, radius, 0, 2 * Math.PI, false);
+            else
+                series.points.symbol(octx, x, y, radius, false);
+            octx.closePath();
+            octx.stroke();
+        }
+
+        function drawBarHighlight(series, point) {
+            octx.lineWidth = series.bars.lineWidth;
+            octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+            var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+            var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
+            drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
+                    0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
+        }
+
+        function getColorOrGradient(spec, bottom, top, defaultColor) {
+            if (typeof spec == "string")
+                return spec;
+            else {
+                // assume this is a gradient spec; IE currently only
+                // supports a simple vertical gradient properly, so that's
+                // what we support too
+                var gradient = ctx.createLinearGradient(0, top, 0, bottom);
+                
+                for (var i = 0, l = spec.colors.length; i < l; ++i) {
+                    var c = spec.colors[i];
+                    if (typeof c != "string") {
+                        var co = $.color.parse(defaultColor);
+                        if (c.brightness != null)
+                            co = co.scale('rgb', c.brightness)
+                        if (c.opacity != null)
+                            co.a *= c.opacity;
+                        c = co.toString();
+                    }
+                    gradient.addColorStop(i / (l - 1), c);
+                }
+                
+                return gradient;
+            }
+        }
+    }
+
+    $.plot = function(placeholder, data, options) {
+        //var t0 = new Date();
+        var plot = new Plot($(placeholder), data, options, $.plot.plugins);
+        //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
+        return plot;
+    };
+
+    $.plot.version = "0.7";
+    
+    $.plot.plugins = [];
+
+    // returns a string with the date d formatted according to fmt
+    $.plot.formatDate = function(d, fmt, monthNames) {
+        var leftPad = function(n) {
+            n = "" + n;
+            return n.length == 1 ? "0" + n : n;
+        };
+        
+        var r = [];
+        var escape = false, padNext = false;
+        var hours = d.getUTCHours();
+        var isAM = hours < 12;
+        if (monthNames == null)
+            monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+        if (fmt.search(/%p|%P/) != -1) {
+            if (hours > 12) {
+                hours = hours - 12;
+            } else if (hours == 0) {
+                hours = 12;
+            }
+        }
+        for (var i = 0; i < fmt.length; ++i) {
+            var c = fmt.charAt(i);
+            
+            if (escape) {
+                switch (c) {
+                case 'h': c = "" + hours; break;
+                case 'H': c = leftPad(hours); break;
+                case 'M': c = leftPad(d.getUTCMinutes()); break;
+                case 'S': c = leftPad(d.getUTCSeconds()); break;
+                case 'd': c = "" + d.getUTCDate(); break;
+                case 'm': c = "" + (d.getUTCMonth() + 1); break;
+                case 'y': c = "" + d.getUTCFullYear(); break;
+                case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
+                case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
+                case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
+                case '0': c = ""; padNext = true; break;
+                }
+                if (c && padNext) {
+                    c = leftPad(c);
+                    padNext = false;
+                }
+                r.push(c);
+                if (!padNext)
+                    escape = false;
+            }
+            else {
+                if (c == "%")
+                    escape = true;
+                else
+                    r.push(c);
+            }
+        }
+        return r.join("");
+    };
+    
+    // round to nearby lower multiple of base
+    function floorInBase(n, base) {
+        return base * Math.floor(n / base);
+    }
+    
+})(jQuery);


http://bitbucket.org/okfn/ckanext-datapreview/changeset/1a1f1140fca2/
changeset:   1a1f1140fca2
branch:      graph
user:        aron_
date:        2011-07-20 11:30:07
summary:     Added a ui.html file for testing datapreview outside of CKAN
affected #:  1 file (1.5 KB)

--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/ui.html	Wed Jul 20 10:30:07 2011 +0100
@@ -0,0 +1,40 @@
+<!-- Test/demo page for the data preview UI without requiring a CKAN instance -->
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8" />
+  <title>Data Preview Plugin</title>
+  <link rel="stylesheet" href="../public/ckanext/datapreview/data-preview.css" class="ckanext-datapreview-stylesheet" />
+</head>
+<body>
+  <div id="ckanext-datapreview-dialog"></div>
+  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.0/jquery.js"></script> 
+  <script src="../public/ckanext/datapreview/data-preview.js"></script>
+  <script>
+    jQuery(function ($) {
+      var dp = CKANEXT.DATAPREVIEW;
+      var dataproxyUrl = 'http://test-webstore.ckan.net/okfn';
+
+      // Update the sources to point to the public directory.
+      var sources = [dp.stylesheets, dp.scripts['jquery-ui'], dp.scripts['slickgrid'], dp.scripts['flot']];
+      $.each(sources, function (index, array) {
+        $.each(this, function (index, uri) {
+          array[index] = '../public' + uri;
+        });
+      });
+      dp.template.src = '../public' + dp.template.src;
+
+      dp.initialize(dataproxyUrl, 'ckanext-datapreview-dialog');
+
+      // Load a datasource without seting up the download links.
+      var dataSource = 'http://test-webstore.ckan.net/okfn/b21ae9c691445b73773156380336e2fab821cb64/resource.jsonp?_limit=30';
+      dp.loadDependancies(function () {
+        dp.getResourceDataDirect(dataSource, 'csv', function (url, type, data) {
+          dp.showData(url, type, data);
+          dp.$dialog.dialog('open');
+        });
+      });
+    });
+  </script>
+</body>
+</html>


http://bitbucket.org/okfn/ckanext-datapreview/changeset/81cfbe540089/
changeset:   81cfbe540089
user:        aron_
date:        2011-07-20 11:31:55
summary:     Merged "graph" branch into "default"
affected #:  6 files (118.5 KB)

--- a/public/ckanext/datapreview/data-preview.css	Mon Jul 18 15:26:09 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.css	Wed Jul 20 10:31:55 2011 +0100
@@ -12,13 +12,96 @@
 	border-bottom: none;
 }
 
+.ckanext-datapreview-viewer  {
+	position: relative;
+	height: 100%;
+	overflow: hidden;
+}
+
+.ckanext-datapreview-nav  {
+	list-style-type: none;
+	margin: 0;
+	padding: 0;
+	height: 22px;
+}
+
+.ckanext-datapreview-nav-toggle  {
+	float: left;
+}
+
+.ckanext-datapreview-nav li {
+	float: left;
+}
+
+#ckanext-datapreview-nav-editor,
+label[for=ckanext-datapreview-nav-editor]  {
+	float: right;
+}
+
 /* Adds border to left of data table */
+.ckanext-datapreview-grid,
+.ckanext-datapreview-graph,
+.ckanext-datapreview-editor {
+	position: absolute;
+	left: 0;
+	right: 220px;
+	top: 28px;
+	bottom: 0;
+	z-index: 0;
+}
+
 .ckanext-datapreview-grid {
 	border-left: 1px solid #ccc;
-	height: 100%;
+}
+
+.ckanext-datapreview-graph {
+	z-index: 1;
+	background-color: #fff;
+}
+
+.ckanext-datapreview-editor {
+	z-index: 1;
+	background-color: #efefef;
+	right: 0;
+	left: auto;
+	width: 198px;
+	padding: 5px 10px;
+	border: 1px solid #ccc;
+}
+
+.ckanext-datapreview-editor ul {
+	list-style-type: none;
+	margin: 0;
+	padding: 0;
+}
+
+.ckanext-datapreview-editor li {
+	margin-bottom: 10px;
+}
+
+.ckanext-datapreview-editor label {
+	display: block;
+	font-weight: bold;
+	color: #555;
+	line-height: 1.4;
+}
+
+.ckanext-datapreview-editor select {
 	width: 100%;
 }
 
+.ckanext-datapreview-editor button {
+	float: right;
+}
+
+.ckanext-datapreview-hide-editor .ckanext-datapreview-editor {
+	display: none;
+}
+
+.ckanext-datapreview-hide-editor .ckanext-datapreview-panel {
+	right: 0;
+}
+
 /* Style the preview buttons */
 .preview {
 	width: 65px;


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/public/ckanext/datapreview/data-preview.html	Wed Jul 20 10:31:55 2011 +0100
@@ -0,0 +1,35 @@
+<div class="ckanext-datapreview-viewer">
+  <div class="ckanext-datapreview-nav">
+    <span class="ckanext-datapreview-nav-toggle">
+      <input type="radio" id="ckanext-datapreview-nav-grid" name="ckanext-datapreview-nav-toggle" value="grid" checked="checked" />
+      <label for="ckanext-datapreview-nav-grid">Grid</label>
+      <input type="radio" id="ckanext-datapreview-nav-graph" name="ckanext-datapreview-nav-toggle" value="chart" />
+      <label for="ckanext-datapreview-nav-graph">Graph</label>
+    </span>
+    <input type="checkbox" id="ckanext-datapreview-nav-editor" checked="checked" />
+    <label for="ckanext-datapreview-nav-editor">Toggle Editor</label>
+  </div>
+  <div class="ckanext-datapreview-panel ckanext-datapreview-grid"></div>
+  <div class="ckanext-datapreview-panel ckanext-datapreview-graph"></div>
+  <div class="ckanext-datapreview-editor">
+    <form>
+      <ul>
+        <li class="ckanext-datapreview-editor-type">
+          <label>Graph Type</label>
+          <select></select>
+        </li>
+        <li class="ckanext-datapreview-editor-group">
+          <label>Group Column</label>
+          <select></select>
+        </li>
+        <li class="ckanext-datapreview-editor-series">
+          <label>Series A</label>
+          <select></select>
+        </li>
+      </ul>
+      <div class="ckanext-datapreview-editor-submit">
+        <button>Draw</button>
+      </div>
+    </form>
+  </div>
+</div>


--- a/public/ckanext/datapreview/data-preview.js	Mon Jul 18 15:26:09 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Wed Jul 20 10:31:55 2011 +0100
@@ -6,12 +6,13 @@
   my.webstore = null;
   my.timeout = 5000;
   my.areDependanciesLoaded = false;
+
   my.stylesheets = [
     '/ckanext/datapreview/jquery-ui/css/ckan/jquery-ui-1.8.14.custom.css',
     '/ckanext/datapreview/slickgrid/slick.grid.css',
     '/ckanext/datapreview/slickgrid/slick.columnpicker.css'
   ];
-  
+
   my.scripts = {
     'jquery-ui': [
       '/ckanext/datapreview/jquery-ui/js/jquery-ui-1.8.14.custom.min.js',
@@ -20,9 +21,18 @@
     'slickgrid': [
       '/ckanext/datapreview/slickgrid/slick.grid.js',
       '/ckanext/datapreview/slickgrid/slick.columnpicker.js'
+    ],
+    'flot': [
+      '/ckanext/datapreview/data-preview.ui.js',
+      '/ckanext/datapreview/flot/jquery.flot.js'
     ]
   };
 
+  my.template = {
+    html: '',
+    src: '/ckanext/datapreview/data-preview.html'
+  };
+
   my.normalizeFormat = function(format) {
     var out = format.toLowerCase();
     out = out.split('/');
@@ -63,6 +73,13 @@
     scripts = $.map(my.scripts['jquery-ui'], $.getScript);
     $.when.apply($, scripts).then(function () {
       scripts = $.map(my.scripts['slickgrid'], $.getScript);
+      scripts = scripts.concat($.map(my.scripts['flot'], $.getScript));
+
+      // Load the template file from the server.
+      scripts.push($.get(my.template.src, function (html) {
+        my.template.html = html;
+      }));
+
       $.when.apply($, scripts).then(function () {
         my.areDependanciesLoaded = true;
         my.$dialog.dialog(my.dialogOptions).dialog("widget").css('position', 'fixed');
@@ -107,56 +124,21 @@
     });
   };
 
-  my.createDataGrid = function createDataGrid(columns, data) {
+  my.loadDataPreview = function (columns, data) {
     var dialog = my.$dialog;
 
     // Need to create the grid once the dialog is open for cells to render
     // correctly.
-    dialog.dialog(my.dialogOptions).one("dialogopen", function() {
-      var container = $('<div class="ckanext-datapreview-grid" />').appendTo(dialog);
-      var grid = new Slick.Grid(container, data, columns, {
-        enableColumnReorder: false,
-        forceFitColumns: true,
-        syncColumnCellResize: true,
-        enableCellRangeSelection: false
-      });
+    dialog.dialog(my.dialogOptions).one("dialogopen", function () {
+      var element  = $(my.template.html).appendTo(dialog);
+      var viewer   = new my.createDataPreview(element, columns, data);
 
-      // Sort the data and redraw the grid.
-      grid.onSort = function (column, sortAsc) {
-        data.sort(function (a, b) {
-          var x = a[column.field],
-              y = b[column.field];
-
-          if (x == y) {
-            return 0;
-          }
-          return (x > y ? 1 : -1) * (sortAsc ? 1 : -1);
-        });
-        grid.invalidate();
-      };
-
-      // Redraw the grid when the dialog resizes.
-      dialog.bind("dialogresizestop.data-preview", function(event, ui) {
-        grid.resizeCanvas();
-        grid.autosizeColumns();
-      });
+      dialog.bind("dialogresizestop.data-preview", viewer.redraw);
 
       // Remove bindings when dialog is closed.
       dialog.bind("dialogbeforeclose", function () {
-        my.$dialog.unbind(".data-preview");
+        dialog.unbind(".data-preview");
       });
-
-      // In order to extend the resize handles across into the adjacent column
-      // we need to disable overflow hidden and increase each cells z-index.
-      // We then wrap the contents in order to reapply the overflow hidden.
-      dialog.find('.slick-header-column')
-        .wrapInner('<div class="slick-header-wrapper" />')
-        .css('overflow', 'visible')
-        .css('z-index', function (index) {
-          return columns.length - index;
-        });
-
-      new Slick.Controls.ColumnPicker(columns, grid);
     });
   };
 
@@ -186,7 +168,7 @@
       return cells;
     });
 
-    my.createDataGrid(columns, data);
+    my.loadDataPreview(columns, data);
   };
 
   my.showPlainTextData = function(url, type, data) {
@@ -247,7 +229,7 @@
 
     if (_type in {'csv': '', 'xls': ''}) {
       my.getResourceDataDirect(_url, _type, callbackWrapper(my.showData));
-    } 
+    }
     else if (_type in {
         'rdf+xml': '',
         'owl+xml': '',


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/public/ckanext/datapreview/data-preview.ui.js	Wed Jul 20 10:31:55 2011 +0100
@@ -0,0 +1,320 @@
+(function ($, undefined) {
+
+  var ui = {};
+
+  function bindAll(context) {
+    var args = [].slice.call(arguments, 0), i = 0, count = args.length;
+    for (; i < count; i += 1) {
+      context[args[i]] = $.proxy(context[args[i]], context);
+    }
+    return context;
+  }
+
+  // Taken from https://github.com/aron/inheritance.js
+  function create(proto) {
+    if (typeof proto !== 'object') {
+      return {};
+    }
+    else if (Object.create) {
+      return Object.create(proto);
+    }
+    function DummyObject() {}
+    DummyObject.prototype = proto;
+    return new DummyObject();
+  }
+
+  function inherit(parent, methods, properties) {
+    methods = methods || {};
+
+    var Child = methods.hasOwnProperty('constructor') ?
+                methods.constructor : inherit.constructor(parent);
+
+    Child.prototype = create(parent.prototype);
+    Child.prototype.constructor = Child;
+
+    delete methods.constructor;
+    $.extend(Child.prototype, methods, {__super__: parent.prototype});
+
+    return $.extend(Child, parent, properties);
+  }
+
+  // Implements Ben Allmans Tiny PubSub, https://gist.github.com/661855
+  ui.BaseView = inherit({}, {
+    constructor: function BaseView(element) {
+      this.el = element;
+
+      // Use a custom empty jQuery wrapper rather than this.el to prevent
+      // browser events being triggered.
+      this.events = $({});
+    },
+    $: function (selector) {
+      return this.el.find(selector);
+    },
+    bind: function (topic, fn) {
+      if (arguments.length === 1) {
+        for (var key in topic) {
+          if (topic.hasOwnProperty(key)) {
+            this.bind(key, topic[key]);
+          }
+        }
+        return this;
+      }
+
+      function wrapper() {
+        return fn.apply(this, Array.prototype.slice.call(arguments, 1));
+      }
+      wrapper.guid = fn.guid = fn.guid || ($.guid ? $.guid++ : $.event.guid++);
+      this.events.bind(topic, wrapper);
+      return this;
+    },
+    unbind: function () {
+      this.events.unbind.apply(this.events, arguments);
+      return this;
+    },
+    trigger: function () {
+      this.events.triggerHandler.apply(this.events, arguments);
+      return this;
+    },
+    show: function () {
+      this.el.show();
+      return this.trigger('show');
+    },
+    hide: function () {
+      this.el.hide();
+      return this.trigger('hide');
+    }
+  });
+
+  ui.MainView = inherit(ui.BaseView, {
+    constructor: function MainView(element, columns, data) {
+      this.__super__.constructor.apply(this, arguments);
+
+      bindAll(this, 'redraw', 'onNavChange', 'onNavToggleEditor', 'onEditorSubmit');
+
+      var view = this;
+      this.nav = new ui.NavigationView(this.$('.ckanext-datapreview-nav'));
+      this.grid = new ui.GridView(this.$('.ckanext-datapreview-grid'), columns, data);
+      this.chart = new ui.ChartView(this.$('.ckanext-datapreview-graph'), data);
+      this.editor = new ui.EditorView(this.$('.ckanext-datapreview-editor'), columns);
+
+      this.nav.bind({
+        'change': this.onNavChange,
+        'toggle-editor': this.onNavToggleEditor
+      });
+      this.editor.bind({
+        'show hide': this.redraw,
+        'submit': this.onEditorSubmit
+      });
+
+      this.chart.hide();
+    },
+    redraw: function () {
+      this.chart.redraw();
+      this.grid.redraw();
+    },
+    onNavChange: function (selected) {
+      var isGrid = selected === 'grid';
+      this.grid[isGrid ? 'show' : 'hide']();
+      this.chart[isGrid ? 'hide' : 'show']();
+    },
+    onNavToggleEditor: function (showEditor) {
+      this.el.toggleClass('ckanext-datapreview-hide-editor', !showEditor);
+      this.redraw();
+    },
+    onEditorSubmit: function (chart) {
+      this.nav.toggle('chart');
+      this.chart.update(chart);
+    }
+  });
+
+  ui.NavigationView = inherit(ui.BaseView, {
+    constructor: function NavigationView(element) {
+      this.__super__.constructor.apply(this, arguments);
+
+      bindAll(this, 'onEditorToggleChange', 'onPanelToggleChange');
+
+      this.panelButtons = this.$('.ckanext-datapreview-nav-toggle').buttonset();
+      this.panelButtons.change(this.onPanelToggleChange);
+
+      this.editorButton = this.$('#ckanext-datapreview-nav-editor').button();
+      this.editorButton.change(this.onEditorToggleChange);
+    },
+    toggle: function (panel) {
+      // Need to fire all these events just to get jQuery UI to change state.
+      this.$('input[value="' + panel + '"]').click().change().next().click();
+      return this;
+    },
+    onPanelToggleChange: function (event) {
+      this.trigger('change', [event.target.value]);
+    },
+    onEditorToggleChange: function (event) {
+      this.trigger('toggle-editor', [event.target.checked]);
+    }
+  });
+
+  ui.GridView = inherit(ui.BaseView, {
+    constructor: function GridView(element, columns, data, options) {
+      this.__super__.constructor.apply(this, arguments);
+
+      bindAll(this, '_onSort', 'redraw');
+
+      this.dirty = false;
+      this.columns = columns;
+      this.data = data;
+      this.grid = new Slick.Grid(element, data, columns, {
+        enableColumnReorder: false,
+        forceFitColumns: true,
+        syncColumnCellResize: true,
+        enableCellRangeSelection: false
+      });
+
+      this.grid.onSort = this._onSort;
+
+      // In order to extend the resize handles across into the adjacent column
+      // we need to disable overflow hidden and increase each cells z-index.
+      // We then wrap the contents in order to reapply the overflow hidden.
+      this.$('.slick-header-column')
+        .wrapInner('<div class="slick-header-wrapper" />')
+        .css('overflow', 'visible')
+        .css('z-index', function (index) {
+          return columns.length - index;
+        });
+
+      new Slick.Controls.ColumnPicker(this.columns, this.grid);
+    },
+    show: function () {
+      this.__super__.show.apply(this, arguments);
+      if (this.dirty) {
+        this.redraw();
+        this.dirty = false;
+      }
+      return this;
+    },
+    redraw: function () {
+      if (this.el.is(':visible')) {
+        this.grid.resizeCanvas();
+        this.grid.autosizeColumns();
+      } else {
+        this.dirty = true;
+      }
+    },
+    _onSort: function (column, sortAsc) {
+      this.data.sort(function (a, b) {
+        var x = a[column.field],
+            y = b[column.field];
+
+        if (x == y) {
+          return 0;
+        }
+        return (x > y ? 1 : -1) * (sortAsc ? 1 : -1);
+      });
+      this.grid.invalidate();
+    }
+  });
+
+  ui.ChartView = inherit(ui.BaseView, {
+    constructor: function ChartView(element, gridData, chart) {
+      this.__super__.constructor.apply(this, arguments);
+      this.gridData = gridData;
+      this.chart = chart;
+      this.plot = $.plot(element, this.createData());
+      this.draw();
+    },
+    createData: function () {
+      var data = [], gridData = this.gridData, chart = this.chart;
+      if (chart) {
+        $.each(this.chart.series, function () {
+          var points = [], name = this;
+          $.each(gridData, function (index) {
+            var x = this[chart.groups], y = this[name];
+            if (typeof x === 'string') {
+              x = index;
+            }
+            points.push([x, y]);
+          });
+          data.push({data: points});
+        });
+      }
+      return data;
+    },
+    draw: function () {
+      this.plot.setData(this.createData());
+      return this.redraw();
+    },
+    update: function (chart) {
+      this.chart = chart;
+      this.draw();
+      return this;
+    },
+    redraw: function () {
+      this.plot.resize();
+      this.plot.setupGrid();
+      this.plot.draw();
+      return this;
+    }
+  });
+
+  ui.EditorView = inherit(ui.BaseView, {
+    constructor: function EditorView(element, columns) {
+      this.__super__.constructor.apply(this, arguments);
+
+      bindAll(this, 'onSubmit');
+
+      this.columns = columns;
+      this.type    = this.$('.ckanext-datapreview-editor-type select');
+      this.groups  = this.$('.ckanext-datapreview-editor-group select');
+      this.series  = this.$('.ckanext-datapreview-editor-series select');
+
+      this.$('button').button().click(this.onSubmit);
+      this.el.bind('submit', this.onSubmit);
+
+      this.setupTypeOptions().setupColumnOptions();
+    },
+    setupTypeOptions: function () {
+      var types = {};
+      $.each({line: {name: 'Line Chart'}}, function (id, type) {
+        types[id] = type.name;
+      });
+      this.type.html(this._createOptions(types));
+      return this;
+    },
+    setupColumnOptions: function () {
+      var options = {}, optionsString = '';
+      $.each(this.columns, function (index, column) {
+        options[column.field] = column.name;
+      });
+      optionsString = this._createOptions(options);
+
+      this.groups.html(optionsString);
+      this.series.html(optionsString);
+      return this;
+    },
+    onSubmit: function (event) {
+      event && event.preventDefault();
+
+      var series = this.series.map(function () {
+        return $(this).val();
+      });
+
+      this.trigger('submit', [{
+        type: this.type.val(),
+        groups: this.groups.val(),
+        series: $.makeArray(series)
+      }]);
+    },
+    _createOptions: function (options) {
+      var html = [];
+      $.each(options, function (value, text) {
+        html.push('<option value="' + value + '">' + text + '</option>');
+      });
+      return html.join('');
+    }
+  });
+
+  $.extend(true, this, {CKANEXT: {DATAPREVIEW: {
+    createDataPreview: function (element, columns, data) {
+      return new ui.MainView(element, columns, data);
+    }
+  }}});
+
+})(jQuery);


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/public/ckanext/datapreview/flot/jquery.flot.js	Wed Jul 20 10:31:55 2011 +0100
@@ -0,0 +1,2599 @@
+/*! Javascript plotting library for jQuery, v. 0.7.
+ *
+ * Released under the MIT license by IOLA, December 2007.
+ *
+ */
+
+// first an inline dependency, jquery.colorhelpers.js, we inline it here
+// for convenience
+
+/* Plugin for jQuery for working with colors.
+ * 
+ * Version 1.1.
+ * 
+ * Inspiration from jQuery color animation plugin by John Resig.
+ *
+ * Released under the MIT license by Ole Laursen, October 2009.
+ *
+ * Examples:
+ *
+ *   $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
+ *   var c = $.color.extract($("#mydiv"), 'background-color');
+ *   console.log(c.r, c.g, c.b, c.a);
+ *   $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
+ *
+ * Note that .scale() and .add() return the same modified object
+ * instead of making a new one.
+ *
+ * V. 1.1: Fix error handling so e.g. parsing an empty string does
+ * produce a color rather than just crashing.
+ */ 
+(function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]+=I}return G.normalize()};G.scale=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]*=I}return G.normalize()};G.toString=function(){if(G.a>=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return K<J?J:(K>I?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);
+
+// the actual Flot code
+(function($) {
+    function Plot(placeholder, data_, options_, plugins) {
+        // data is on the form:
+        //   [ series1, series2 ... ]
+        // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
+        // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
+        
+        var series = [],
+            options = {
+                // the color theme used for graphs
+                colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
+                legend: {
+                    show: true,
+                    noColumns: 1, // number of colums in legend table
+                    labelFormatter: null, // fn: string -> string
+                    labelBoxBorderColor: "#ccc", // border color for the little label boxes
+                    container: null, // container (as jQuery object) to put legend in, null means default on top of graph
+                    position: "ne", // position of default legend container within plot
+                    margin: 5, // distance from grid edge to default legend container within plot
+                    backgroundColor: null, // null means auto-detect
+                    backgroundOpacity: 0.85 // set to 0 to avoid background
+                },
+                xaxis: {
+                    show: null, // null = auto-detect, true = always, false = never
+                    position: "bottom", // or "top"
+                    mode: null, // null or "time"
+                    color: null, // base color, labels, ticks
+                    tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"
+                    transform: null, // null or f: number -> number to transform axis
+                    inverseTransform: null, // if transform is set, this should be the inverse function
+                    min: null, // min. value to show, null means set automatically
+                    max: null, // max. value to show, null means set automatically
+                    autoscaleMargin: null, // margin in % to add if auto-setting min/max
+                    ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
+                    tickFormatter: null, // fn: number -> string
+                    labelWidth: null, // size of tick labels in pixels
+                    labelHeight: null,
+                    reserveSpace: null, // whether to reserve space even if axis isn't shown
+                    tickLength: null, // size in pixels of ticks, or "full" for whole line
+                    alignTicksWithAxis: null, // axis number or null for no sync
+                    
+                    // mode specific options
+                    tickDecimals: null, // no. of decimals, null means auto
+                    tickSize: null, // number or [number, "unit"]
+                    minTickSize: null, // number or [number, "unit"]
+                    monthNames: null, // list of names of months
+                    timeformat: null, // format string to use
+                    twelveHourClock: false // 12 or 24 time in time mode
+                },
+                yaxis: {
+                    autoscaleMargin: 0.02,
+                    position: "left" // or "right"
+                },
+                xaxes: [],
+                yaxes: [],
+                series: {
+                    points: {
+                        show: false,
+                        radius: 3,
+                        lineWidth: 2, // in pixels
+                        fill: true,
+                        fillColor: "#ffffff",
+                        symbol: "circle" // or callback
+                    },
+                    lines: {
+                        // we don't put in show: false so we can see
+                        // whether lines were actively disabled 
+                        lineWidth: 2, // in pixels
+                        fill: false,
+                        fillColor: null,
+                        steps: false
+                    },
+                    bars: {
+                        show: false,
+                        lineWidth: 2, // in pixels
+                        barWidth: 1, // in units of the x axis
+                        fill: true,
+                        fillColor: null,
+                        align: "left", // or "center" 
+                        horizontal: false
+                    },
+                    shadowSize: 3
+                },
+                grid: {
+                    show: true,
+                    aboveData: false,
+                    color: "#545454", // primary color used for outline and labels
+                    backgroundColor: null, // null for transparent, else color
+                    borderColor: null, // set if different from the grid color
+                    tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
+                    labelMargin: 5, // in pixels
+                    axisMargin: 8, // in pixels
+                    borderWidth: 2, // in pixels
+                    minBorderMargin: null, // in pixels, null means taken from points radius
+                    markings: null, // array of ranges or fn: axes -> array of ranges
+                    markingsColor: "#f4f4f4",
+                    markingsLineWidth: 2,
+                    // interactive stuff
+                    clickable: false,
+                    hoverable: false,
+                    autoHighlight: true, // highlight in case mouse is near
+                    mouseActiveRadius: 10 // how far the mouse can be away to activate an item
+                },
+                hooks: {}
+            },
+        canvas = null,      // the canvas for the plot itself
+        overlay = null,     // canvas for interactive stuff on top of plot
+        eventHolder = null, // jQuery object that events should be bound to
+        ctx = null, octx = null,
+        xaxes = [], yaxes = [],
+        plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
+        canvasWidth = 0, canvasHeight = 0,
+        plotWidth = 0, plotHeight = 0,
+        hooks = {
+            processOptions: [],
+            processRawData: [],
+            processDatapoints: [],
+            drawSeries: [],
+            draw: [],
+            bindEvents: [],
+            drawOverlay: [],
+            shutdown: []
+        },
+        plot = this;
+
+        // public functions
+        plot.setData = setData;
+        plot.setupGrid = setupGrid;
+        plot.draw = draw;
+        plot.getPlaceholder = function() { return placeholder; };
+        plot.getCanvas = function() { return canvas; };
+        plot.getPlotOffset = function() { return plotOffset; };
+        plot.width = function () { return plotWidth; };
+        plot.height = function () { return plotHeight; };
+        plot.offset = function () {
+            var o = eventHolder.offset();
+            o.left += plotOffset.left;
+            o.top += plotOffset.top;
+            return o;
+        };
+        plot.getData = function () { return series; };
+        plot.getAxes = function () {
+            var res = {}, i;
+            $.each(xaxes.concat(yaxes), function (_, axis) {
+                if (axis)
+                    res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;
+            });
+            return res;
+        };
+        plot.getXAxes = function () { return xaxes; };
+        plot.getYAxes = function () { return yaxes; };
+        plot.c2p = canvasToAxisCoords;
+        plot.p2c = axisToCanvasCoords;
+        plot.getOptions = function () { return options; };
+        plot.highlight = highlight;
+        plot.unhighlight = unhighlight;
+        plot.triggerRedrawOverlay = triggerRedrawOverlay;
+        plot.pointOffset = function(point) {
+            return {
+                left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left),
+                top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top)
+            };
+        };
+        plot.shutdown = shutdown;
+        plot.resize = function () {
+            getCanvasDimensions();
+            resizeCanvas(canvas);
+            resizeCanvas(overlay);
+        };
+
+        // public attributes
+        plot.hooks = hooks;
+        
+        // initialize
+        initPlugins(plot);
+        parseOptions(options_);
+        setupCanvases();
+        setData(data_);
+        setupGrid();
+        draw();
+        bindEvents();
+
+
+        function executeHooks(hook, args) {
+            args = [plot].concat(args);
+            for (var i = 0; i < hook.length; ++i)
+                hook[i].apply(this, args);
+        }
+
+        function initPlugins() {
+            for (var i = 0; i < plugins.length; ++i) {
+                var p = plugins[i];
+                p.init(plot);
+                if (p.options)
+                    $.extend(true, options, p.options);
+            }
+        }
+        
+        function parseOptions(opts) {
+            var i;
+            
+            $.extend(true, options, opts);
+            
+            if (options.xaxis.color == null)
+                options.xaxis.color = options.grid.color;
+            if (options.yaxis.color == null)
+                options.yaxis.color = options.grid.color;
+            
+            if (options.xaxis.tickColor == null) // backwards-compatibility
+                options.xaxis.tickColor = options.grid.tickColor;
+            if (options.yaxis.tickColor == null) // backwards-compatibility
+                options.yaxis.tickColor = options.grid.tickColor;
+
+            if (options.grid.borderColor == null)
+                options.grid.borderColor = options.grid.color;
+            if (options.grid.tickColor == null)
+                options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();
+            
+            // fill in defaults in axes, copy at least always the
+            // first as the rest of the code assumes it'll be there
+            for (i = 0; i < Math.max(1, options.xaxes.length); ++i)
+                options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]);
+            for (i = 0; i < Math.max(1, options.yaxes.length); ++i)
+                options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]);
+
+            // backwards compatibility, to be removed in future
+            if (options.xaxis.noTicks && options.xaxis.ticks == null)
+                options.xaxis.ticks = options.xaxis.noTicks;
+            if (options.yaxis.noTicks && options.yaxis.ticks == null)
+                options.yaxis.ticks = options.yaxis.noTicks;
+            if (options.x2axis) {
+                options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
+                options.xaxes[1].position = "top";
+            }
+            if (options.y2axis) {
+                options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);
+                options.yaxes[1].position = "right";
+            }
+            if (options.grid.coloredAreas)
+                options.grid.markings = options.grid.coloredAreas;
+            if (options.grid.coloredAreasColor)
+                options.grid.markingsColor = options.grid.coloredAreasColor;
+            if (options.lines)
+                $.extend(true, options.series.lines, options.lines);
+            if (options.points)
+                $.extend(true, options.series.points, options.points);
+            if (options.bars)
+                $.extend(true, options.series.bars, options.bars);
+            if (options.shadowSize != null)
+                options.series.shadowSize = options.shadowSize;
+
+            // save options on axes for future reference
+            for (i = 0; i < options.xaxes.length; ++i)
+                getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
+            for (i = 0; i < options.yaxes.length; ++i)
+                getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];
+
+            // add hooks from options
+            for (var n in hooks)
+                if (options.hooks[n] && options.hooks[n].length)
+                    hooks[n] = hooks[n].concat(options.hooks[n]);
+
+            executeHooks(hooks.processOptions, [options]);
+        }
+
+        function setData(d) {
+            series = parseData(d);
+            fillInSeriesOptions();
+            processData();
+        }
+        
+        function parseData(d) {
+            var res = [];
+            for (var i = 0; i < d.length; ++i) {
+                var s = $.extend(true, {}, options.series);
+
+                if (d[i].data != null) {
+                    s.data = d[i].data; // move the data instead of deep-copy
+                    delete d[i].data;
+
+                    $.extend(true, s, d[i]);
+
+                    d[i].data = s.data;
+                }
+                else
+                    s.data = d[i];
+                res.push(s);
+            }
+
+            return res;
+        }
+        
+        function axisNumber(obj, coord) {
+            var a = obj[coord + "axis"];
+            if (typeof a == "object") // if we got a real axis, extract number
+                a = a.n;
+            if (typeof a != "number")
+                a = 1; // default to first axis
+            return a;
+        }
+
+        function allAxes() {
+            // return flat array without annoying null entries
+            return $.grep(xaxes.concat(yaxes), function (a) { return a; });
+        }
+        
+        function canvasToAxisCoords(pos) {
+            // return an object with x/y corresponding to all used axes 
+            var res = {}, i, axis;
+            for (i = 0; i < xaxes.length; ++i) {
+                axis = xaxes[i];
+                if (axis && axis.used)
+                    res["x" + axis.n] = axis.c2p(pos.left);
+            }
+
+            for (i = 0; i < yaxes.length; ++i) {
+                axis = yaxes[i];
+                if (axis && axis.used)
+                    res["y" + axis.n] = axis.c2p(pos.top);
+            }
+            
+            if (res.x1 !== undefined)
+                res.x = res.x1;
+            if (res.y1 !== undefined)
+                res.y = res.y1;
+
+            return res;
+        }
+        
+        function axisToCanvasCoords(pos) {
+            // get canvas coords from the first pair of x/y found in pos
+            var res = {}, i, axis, key;
+
+            for (i = 0; i < xaxes.length; ++i) {
+                axis = xaxes[i];
+                if (axis && axis.used) {
+                    key = "x" + axis.n;
+                    if (pos[key] == null && axis.n == 1)
+                        key = "x";
+
+                    if (pos[key] != null) {
+                        res.left = axis.p2c(pos[key]);
+                        break;
+                    }
+                }
+            }
+            
+            for (i = 0; i < yaxes.length; ++i) {
+                axis = yaxes[i];
+                if (axis && axis.used) {
+                    key = "y" + axis.n;
+                    if (pos[key] == null && axis.n == 1)
+                        key = "y";
+
+                    if (pos[key] != null) {
+                        res.top = axis.p2c(pos[key]);
+                        break;
+                    }
+                }
+            }
+            
+            return res;
+        }
+        
+        function getOrCreateAxis(axes, number) {
+            if (!axes[number - 1])
+                axes[number - 1] = {
+                    n: number, // save the number for future reference
+                    direction: axes == xaxes ? "x" : "y",
+                    options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)
+                };
+                
+            return axes[number - 1];
+        }
+
+        function fillInSeriesOptions() {
+            var i;
+            
+            // collect what we already got of colors
+            var neededColors = series.length,
+                usedColors = [],
+                assignedColors = [];
+            for (i = 0; i < series.length; ++i) {
+                var sc = series[i].color;
+                if (sc != null) {
+                    --neededColors;
+                    if (typeof sc == "number")
+                        assignedColors.push(sc);
+                    else
+                        usedColors.push($.color.parse(series[i].color));
+                }
+            }
+            
+            // we might need to generate more colors if higher indices
+            // are assigned
+            for (i = 0; i < assignedColors.length; ++i) {
+                neededColors = Math.max(neededColors, assignedColors[i] + 1);
+            }
+
+            // produce colors as needed
+            var colors = [], variation = 0;
+            i = 0;
+            while (colors.length < neededColors) {
+                var c;
+                if (options.colors.length == i) // check degenerate case
+                    c = $.color.make(100, 100, 100);
+                else
+                    c = $.color.parse(options.colors[i]);
+
+                // vary color if needed
+                var sign = variation % 2 == 1 ? -1 : 1;
+                c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2)
+
+                // FIXME: if we're getting to close to something else,
+                // we should probably skip this one
+                colors.push(c);
+                
+                ++i;
+                if (i >= options.colors.length) {
+                    i = 0;
+                    ++variation;
+                }
+            }
+
+            // fill in the options
+            var colori = 0, s;
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                
+                // assign colors
+                if (s.color == null) {
+                    s.color = colors[colori].toString();
+                    ++colori;
+                }
+                else if (typeof s.color == "number")
+                    s.color = colors[s.color].toString();
+
+                // turn on lines automatically in case nothing is set
+                if (s.lines.show == null) {
+                    var v, show = true;
+                    for (v in s)
+                        if (s[v] && s[v].show) {
+                            show = false;
+                            break;
+                        }
+                    if (show)
+                        s.lines.show = true;
+                }
+
+                // setup axes
+                s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x"));
+                s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y"));
+            }
+        }
+        
+        function processData() {
+            var topSentry = Number.POSITIVE_INFINITY,
+                bottomSentry = Number.NEGATIVE_INFINITY,
+                fakeInfinity = Number.MAX_VALUE,
+                i, j, k, m, length,
+                s, points, ps, x, y, axis, val, f, p;
+
+            function updateAxis(axis, min, max) {
+                if (min < axis.datamin && min != -fakeInfinity)
+                    axis.datamin = min;
+                if (max > axis.datamax && max != fakeInfinity)
+                    axis.datamax = max;
+            }
+
+            $.each(allAxes(), function (_, axis) {
+                // init axis
+                axis.datamin = topSentry;
+                axis.datamax = bottomSentry;
+                axis.used = false;
+            });
+            
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                s.datapoints = { points: [] };
+                
+                executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
+            }
+            
+            // first pass: clean and copy data
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+
+                var data = s.data, format = s.datapoints.format;
+
+                if (!format) {
+                    format = [];
+                    // find out how to copy
+                    format.push({ x: true, number: true, required: true });
+                    format.push({ y: true, number: true, required: true });
+
+                    if (s.bars.show || (s.lines.show && s.lines.fill)) {
+                        format.push({ y: true, number: true, required: false, defaultValue: 0 });
+                        if (s.bars.horizontal) {
+                            delete format[format.length - 1].y;
+                            format[format.length - 1].x = true;
+                        }
+                    }
+                    
+                    s.datapoints.format = format;
+                }
+
+                if (s.datapoints.pointsize != null)
+                    continue; // already filled in
+
+                s.datapoints.pointsize = format.length;
+                
+                ps = s.datapoints.pointsize;
+                points = s.datapoints.points;
+
+                insertSteps = s.lines.show && s.lines.steps;
+                s.xaxis.used = s.yaxis.used = true;
+                
+                for (j = k = 0; j < data.length; ++j, k += ps) {
+                    p = data[j];
+
+                    var nullify = p == null;
+                    if (!nullify) {
+                        for (m = 0; m < ps; ++m) {
+                            val = p[m];
+                            f = format[m];
+
+                            if (f) {
+                                if (f.number && val != null) {
+                                    val = +val; // convert to number
+                                    if (isNaN(val))
+                                        val = null;
+                                    else if (val == Infinity)
+                                        val = fakeInfinity;
+                                    else if (val == -Infinity)
+                                        val = -fakeInfinity;
+                                }
+
+                                if (val == null) {
+                                    if (f.required)
+                                        nullify = true;
+                                    
+                                    if (f.defaultValue != null)
+                                        val = f.defaultValue;
+                                }
+                            }
+                            
+                            points[k + m] = val;
+                        }
+                    }
+                    
+                    if (nullify) {
+                        for (m = 0; m < ps; ++m) {
+                            val = points[k + m];
+                            if (val != null) {
+                                f = format[m];
+                                // extract min/max info
+                                if (f.x)
+                                    updateAxis(s.xaxis, val, val);
+                                if (f.y)
+                                    updateAxis(s.yaxis, val, val);
+                            }
+                            points[k + m] = null;
+                        }
+                    }
+                    else {
+                        // a little bit of line specific stuff that
+                        // perhaps shouldn't be here, but lacking
+                        // better means...
+                        if (insertSteps && k > 0
+                            && points[k - ps] != null
+                            && points[k - ps] != points[k]
+                            && points[k - ps + 1] != points[k + 1]) {
+                            // copy the point to make room for a middle point
+                            for (m = 0; m < ps; ++m)
+                                points[k + ps + m] = points[k + m];
+
+                            // middle point has same y
+                            points[k + 1] = points[k - ps + 1];
+
+                            // we've added a point, better reflect that
+                            k += ps;
+                        }
+                    }
+                }
+            }
+
+            // give the hooks a chance to run
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                
+                executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
+            }
+
+            // second pass: find datamax/datamin for auto-scaling
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                points = s.datapoints.points,
+                ps = s.datapoints.pointsize;
+
+                var xmin = topSentry, ymin = topSentry,
+                    xmax = bottomSentry, ymax = bottomSentry;
+                
+                for (j = 0; j < points.length; j += ps) {
+                    if (points[j] == null)
+                        continue;
+
+                    for (m = 0; m < ps; ++m) {
+                        val = points[j + m];
+                        f = format[m];
+                        if (!f || val == fakeInfinity || val == -fakeInfinity)
+                            continue;
+                        
+                        if (f.x) {
+                            if (val < xmin)
+                                xmin = val;
+                            if (val > xmax)
+                                xmax = val;
+                        }
+                        if (f.y) {
+                            if (val < ymin)
+                                ymin = val;
+                            if (val > ymax)
+                                ymax = val;
+                        }
+                    }
+                }
+                
+                if (s.bars.show) {
+                    // make sure we got room for the bar on the dancing floor
+                    var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2;
+                    if (s.bars.horizontal) {
+                        ymin += delta;
+                        ymax += delta + s.bars.barWidth;
+                    }
+                    else {
+                        xmin += delta;
+                        xmax += delta + s.bars.barWidth;
+                    }
+                }
+                
+                updateAxis(s.xaxis, xmin, xmax);
+                updateAxis(s.yaxis, ymin, ymax);
+            }
+
+            $.each(allAxes(), function (_, axis) {
+                if (axis.datamin == topSentry)
+                    axis.datamin = null;
+                if (axis.datamax == bottomSentry)
+                    axis.datamax = null;
+            });
+        }
+
+        function makeCanvas(skipPositioning, cls) {
+            var c = document.createElement('canvas');
+            c.className = cls;
+            c.width = canvasWidth;
+            c.height = canvasHeight;
+                    
+            if (!skipPositioning)
+                $(c).css({ position: 'absolute', left: 0, top: 0 });
+                
+            $(c).appendTo(placeholder);
+                
+            if (!c.getContext) // excanvas hack
+                c = window.G_vmlCanvasManager.initElement(c);
+
+            // used for resetting in case we get replotted
+            c.getContext("2d").save();
+            
+            return c;
+        }
+
+        function getCanvasDimensions() {
+            canvasWidth = placeholder.width();
+            canvasHeight = placeholder.height();
+            
+            if (canvasWidth <= 0 || canvasHeight <= 0)
+                throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
+        }
+
+        function resizeCanvas(c) {
+            // resizing should reset the state (excanvas seems to be
+            // buggy though)
+            if (c.width != canvasWidth)
+                c.width = canvasWidth;
+
+            if (c.height != canvasHeight)
+                c.height = canvasHeight;
+
+            // so try to get back to the initial state (even if it's
+            // gone now, this should be safe according to the spec)
+            var cctx = c.getContext("2d");
+            cctx.restore();
+
+            // and save again
+            cctx.save();
+        }
+        
+        function setupCanvases() {
+            var reused,
+                existingCanvas = placeholder.children("canvas.base"),
+                existingOverlay = placeholder.children("canvas.overlay");
+
+            if (existingCanvas.length == 0 || existingOverlay == 0) {
+                // init everything
+                
+                placeholder.html(""); // make sure placeholder is clear
+            
+                placeholder.css({ padding: 0 }); // padding messes up the positioning
+                
+                if (placeholder.css("position") == 'static')
+                    placeholder.css("position", "relative"); // for positioning labels and overlay
+
+                getCanvasDimensions();
+                
+                canvas = makeCanvas(true, "base");
+                overlay = makeCanvas(false, "overlay"); // overlay canvas for interactive features
+
+                reused = false;
+            }
+            else {
+                // reuse existing elements
+
+                canvas = existingCanvas.get(0);
+                overlay = existingOverlay.get(0);
+
+                reused = true;
+            }
+
+            ctx = canvas.getContext("2d");
+            octx = overlay.getContext("2d");
+
+            // we include the canvas in the event holder too, because IE 7
+            // sometimes has trouble with the stacking order
+            eventHolder = $([overlay, canvas]);
+
+            if (reused) {
+                // run shutdown in the old plot object
+                placeholder.data("plot").shutdown();
+
+                // reset reused canvases
+                plot.resize();
+                
+                // make sure overlay pixels are cleared (canvas is cleared when we redraw)
+                octx.clearRect(0, 0, canvasWidth, canvasHeight);
+                
+                // then whack any remaining obvious garbage left
+                eventHolder.unbind();
+                placeholder.children().not([canvas, overlay]).remove();
+            }
+
+            // save in case we get replotted
+            placeholder.data("plot", plot);
+        }
+
+        function bindEvents() {
+            // bind events
+            if (options.grid.hoverable) {
+                eventHolder.mousemove(onMouseMove);
+                eventHolder.mouseleave(onMouseLeave);
+            }
+
+            if (options.grid.clickable)
+                eventHolder.click(onClick);
+
+            executeHooks(hooks.bindEvents, [eventHolder]);
+        }
+
+        function shutdown() {
+            if (redrawTimeout)
+                clearTimeout(redrawTimeout);
+            
+            eventHolder.unbind("mousemove", onMouseMove);
+            eventHolder.unbind("mouseleave", onMouseLeave);
+            eventHolder.unbind("click", onClick);
+            
+            executeHooks(hooks.shutdown, [eventHolder]);
+        }
+
+        function setTransformationHelpers(axis) {
+            // set helper functions on the axis, assumes plot area
+            // has been computed already
+            
+            function identity(x) { return x; }
+            
+            var s, m, t = axis.options.transform || identity,
+                it = axis.options.inverseTransform;
+            
+            // precompute how much the axis is scaling a point
+            // in canvas space
+            if (axis.direction == "x") {
+                s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
+                m = Math.min(t(axis.max), t(axis.min));
+            }
+            else {
+                s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
+                s = -s;
+                m = Math.max(t(axis.max), t(axis.min));
+            }
+
+            // data point to canvas coordinate
+            if (t == identity) // slight optimization
+                axis.p2c = function (p) { return (p - m) * s; };
+            else
+                axis.p2c = function (p) { return (t(p) - m) * s; };
+            // canvas coordinate to data point
+            if (!it)
+                axis.c2p = function (c) { return m + c / s; };
+            else
+                axis.c2p = function (c) { return it(m + c / s); };
+        }
+
+        function measureTickLabels(axis) {
+            var opts = axis.options, i, ticks = axis.ticks || [], labels = [],
+                l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv;
+
+            function makeDummyDiv(labels, width) {
+                return $('<div style="position:absolute;top:-10000px;' + width + 'font-size:smaller">' +
+                         '<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis">'
+                         + labels.join("") + '</div></div>')
+                    .appendTo(placeholder);
+            }
+            
+            if (axis.direction == "x") {
+                // to avoid measuring the widths of the labels (it's slow), we
+                // construct fixed-size boxes and put the labels inside
+                // them, we don't need the exact figures and the
+                // fixed-size box content is easy to center
+                if (w == null)
+                    w = Math.floor(canvasWidth / (ticks.length > 0 ? ticks.length : 1));
+
+                // measure x label heights
+                if (h == null) {
+                    labels = [];
+                    for (i = 0; i < ticks.length; ++i) {
+                        l = ticks[i].label;
+                        if (l)
+                            labels.push('<div class="tickLabel" style="float:left;width:' + w + 'px">' + l + '</div>');
+                    }
+
+                    if (labels.length > 0) {
+                        // stick them all in the same div and measure
+                        // collective height
+                        labels.push('<div style="clear:left"></div>');
+                        dummyDiv = makeDummyDiv(labels, "width:10000px;");
+                        h = dummyDiv.height();
+                        dummyDiv.remove();
+                    }
+                }
+            }
+            else if (w == null || h == null) {
+                // calculate y label dimensions
+                for (i = 0; i < ticks.length; ++i) {
+                    l = ticks[i].label;
+                    if (l)
+                        labels.push('<div class="tickLabel">' + l + '</div>');
+                }
+                
+                if (labels.length > 0) {
+                    dummyDiv = makeDummyDiv(labels, "");
+                    if (w == null)
+                        w = dummyDiv.children().width();
+                    if (h == null)
+                        h = dummyDiv.find("div.tickLabel").height();
+                    dummyDiv.remove();
+                }
+            }
+
+            if (w == null)
+                w = 0;
+            if (h == null)
+                h = 0;
+
+            axis.labelWidth = w;
+            axis.labelHeight = h;
+        }
+
+        function allocateAxisBoxFirstPhase(axis) {
+            // find the bounding box of the axis by looking at label
+            // widths/heights and ticks, make room by diminishing the
+            // plotOffset
+
+            var lw = axis.labelWidth,
+                lh = axis.labelHeight,
+                pos = axis.options.position,
+                tickLength = axis.options.tickLength,
+                axismargin = options.grid.axisMargin,
+                padding = options.grid.labelMargin,
+                all = axis.direction == "x" ? xaxes : yaxes,
+                index;
+
+            // determine axis margin
+            var samePosition = $.grep(all, function (a) {
+                return a && a.options.position == pos && a.reserveSpace;
+            });
+            if ($.inArray(axis, samePosition) == samePosition.length - 1)
+                axismargin = 0; // outermost
+
+            // determine tick length - if we're innermost, we can use "full"
+            if (tickLength == null)
+                tickLength = "full";
+
+            var sameDirection = $.grep(all, function (a) {
+                return a && a.reserveSpace;
+            });
+
+            var innermost = $.inArray(axis, sameDirection) == 0;
+            if (!innermost && tickLength == "full")
+                tickLength = 5;
+                
+            if (!isNaN(+tickLength))
+                padding += +tickLength;
+
+            // compute box
+            if (axis.direction == "x") {
+                lh += padding;
+                
+                if (pos == "bottom") {
+                    plotOffset.bottom += lh + axismargin;
+                    axis.box = { top: canvasHeight - plotOffset.bottom, height: lh };
+                }
+                else {
+                    axis.box = { top: plotOffset.top + axismargin, height: lh };
+                    plotOffset.top += lh + axismargin;
+                }
+            }
+            else {
+                lw += padding;
+                
+                if (pos == "left") {
+                    axis.box = { left: plotOffset.left + axismargin, width: lw };
+                    plotOffset.left += lw + axismargin;
+                }
+                else {
+                    plotOffset.right += lw + axismargin;
+                    axis.box = { left: canvasWidth - plotOffset.right, width: lw };
+                }
+            }
+
+             // save for future reference
+            axis.position = pos;
+            axis.tickLength = tickLength;
+            axis.box.padding = padding;
+            axis.innermost = innermost;
+        }
+
+        function allocateAxisBoxSecondPhase(axis) {
+            // set remaining bounding box coordinates
+            if (axis.direction == "x") {
+                axis.box.left = plotOffset.left;
+                axis.box.width = plotWidth;
+            }
+            else {
+                axis.box.top = plotOffset.top;
+                axis.box.height = plotHeight;
+            }
+        }
+        
+        function setupGrid() {
+            var i, axes = allAxes();
+
+            // first calculate the plot and axis box dimensions
+
+            $.each(axes, function (_, axis) {
+                axis.show = axis.options.show;
+                if (axis.show == null)
+                    axis.show = axis.used; // by default an axis is visible if it's got data
+                
+                axis.reserveSpace = axis.show || axis.options.reserveSpace;
+
+                setRange(axis);
+            });
+
+            allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; });
+
+            plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0;
+            if (options.grid.show) {
+                $.each(allocatedAxes, function (_, axis) {
+                    // make the ticks
+                    setupTickGeneration(axis);
+                    setTicks(axis);
+                    snapRangeToTicks(axis, axis.ticks);
+
+                    // find labelWidth/Height for axis
+                    measureTickLabels(axis);
+                });
+
+                // with all dimensions in house, we can compute the
+                // axis boxes, start from the outside (reverse order)
+                for (i = allocatedAxes.length - 1; i >= 0; --i)
+                    allocateAxisBoxFirstPhase(allocatedAxes[i]);
+
+                // make sure we've got enough space for things that
+                // might stick out
+                var minMargin = options.grid.minBorderMargin;
+                if (minMargin == null) {
+                    minMargin = 0;
+                    for (i = 0; i < series.length; ++i)
+                        minMargin = Math.max(minMargin, series[i].points.radius + series[i].points.lineWidth/2);
+                }
+                    
+                for (var a in plotOffset) {
+                    plotOffset[a] += options.grid.borderWidth;
+                    plotOffset[a] = Math.max(minMargin, plotOffset[a]);
+                }
+            }
+            
+            plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
+            plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
+
+            // now we got the proper plotWidth/Height, we can compute the scaling
+            $.each(axes, function (_, axis) {
+                setTransformationHelpers(axis);
+            });
+
+            if (options.grid.show) {
+                $.each(allocatedAxes, function (_, axis) {
+                    allocateAxisBoxSecondPhase(axis);
+                });
+
+                insertAxisLabels();
+            }
+            
+            insertLegend();
+        }
+        
+        function setRange(axis) {
+            var opts = axis.options,
+                min = +(opts.min != null ? opts.min : axis.datamin),
+                max = +(opts.max != null ? opts.max : axis.datamax),
+                delta = max - min;
+
+            if (delta == 0.0) {
+                // degenerate case
+                var widen = max == 0 ? 1 : 0.01;
+
+                if (opts.min == null)
+                    min -= widen;
+                // always widen max if we couldn't widen min to ensure we
+                // don't fall into min == max which doesn't work
+                if (opts.max == null || opts.min != null)
+                    max += widen;
+            }
+            else {
+                // consider autoscaling
+                var margin = opts.autoscaleMargin;
+                if (margin != null) {
+                    if (opts.min == null) {
+                        min -= delta * margin;
+                        // make sure we don't go below zero if all values
+                        // are positive
+                        if (min < 0 && axis.datamin != null && axis.datamin >= 0)
+                            min = 0;
+                    }
+                    if (opts.max == null) {
+                        max += delta * margin;
+                        if (max > 0 && axis.datamax != null && axis.datamax <= 0)
+                            max = 0;
+                    }
+                }
+            }
+            axis.min = min;
+            axis.max = max;
+        }
+
+        function setupTickGeneration(axis) {
+            var opts = axis.options;
+                
+            // estimate number of ticks
+            var noTicks;
+            if (typeof opts.ticks == "number" && opts.ticks > 0)
+                noTicks = opts.ticks;
+            else
+                // heuristic based on the model a*sqrt(x) fitted to
+                // some data points that seemed reasonable
+                noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight);
+
+            var delta = (axis.max - axis.min) / noTicks,
+                size, generator, unit, formatter, i, magn, norm;
+
+            if (opts.mode == "time") {
+                // pretty handling of time
+                
+                // map of app. size of time units in milliseconds
+                var timeUnitSize = {
+                    "second": 1000,
+                    "minute": 60 * 1000,
+                    "hour": 60 * 60 * 1000,
+                    "day": 24 * 60 * 60 * 1000,
+                    "month": 30 * 24 * 60 * 60 * 1000,
+                    "year": 365.2425 * 24 * 60 * 60 * 1000
+                };
+
+
+                // the allowed tick sizes, after 1 year we use
+                // an integer algorithm
+                var spec = [
+                    [1, "second"], [2, "second"], [5, "second"], [10, "second"],
+                    [30, "second"], 
+                    [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
+                    [30, "minute"], 
+                    [1, "hour"], [2, "hour"], [4, "hour"],
+                    [8, "hour"], [12, "hour"],
+                    [1, "day"], [2, "day"], [3, "day"],
+                    [0.25, "month"], [0.5, "month"], [1, "month"],
+                    [2, "month"], [3, "month"], [6, "month"],
+                    [1, "year"]
+                ];
+
+                var minSize = 0;
+                if (opts.minTickSize != null) {
+                    if (typeof opts.tickSize == "number")
+                        minSize = opts.tickSize;
+                    else
+                        minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
+                }
+
+                for (var i = 0; i < spec.length - 1; ++i)
+                    if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]
+                                 + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
+                       && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
+                        break;
+                size = spec[i][0];
+                unit = spec[i][1];
+                
+                // special-case the possibility of several years
+                if (unit == "year") {
+                    magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
+                    norm = (delta / timeUnitSize.year) / magn;
+                    if (norm < 1.5)
+                        size = 1;
+                    else if (norm < 3)
+                        size = 2;
+                    else if (norm < 7.5)
+                        size = 5;
+                    else
+                        size = 10;
+
+                    size *= magn;
+                }
+
+                axis.tickSize = opts.tickSize || [size, unit];
+                
+                generator = function(axis) {
+                    var ticks = [],
+                        tickSize = axis.tickSize[0], unit = axis.tickSize[1],
+                        d = new Date(axis.min);
+                    
+                    var step = tickSize * timeUnitSize[unit];
+
+                    if (unit == "second")
+                        d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize));
+                    if (unit == "minute")
+                        d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize));
+                    if (unit == "hour")
+                        d.setUTCHours(floorInBase(d.getUTCHours(), tickSize));
+                    if (unit == "month")
+                        d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize));
+                    if (unit == "year")
+                        d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize));
+                    
+                    // reset smaller components
+                    d.setUTCMilliseconds(0);
+                    if (step >= timeUnitSize.minute)
+                        d.setUTCSeconds(0);
+                    if (step >= timeUnitSize.hour)
+                        d.setUTCMinutes(0);
+                    if (step >= timeUnitSize.day)
+                        d.setUTCHours(0);
+                    if (step >= timeUnitSize.day * 4)
+                        d.setUTCDate(1);
+                    if (step >= timeUnitSize.year)
+                        d.setUTCMonth(0);
+
+
+                    var carry = 0, v = Number.NaN, prev;
+                    do {
+                        prev = v;
+                        v = d.getTime();
+                        ticks.push(v);
+                        if (unit == "month") {
+                            if (tickSize < 1) {
+                                // a bit complicated - we'll divide the month
+                                // up but we need to take care of fractions
+                                // so we don't end up in the middle of a day
+                                d.setUTCDate(1);
+                                var start = d.getTime();
+                                d.setUTCMonth(d.getUTCMonth() + 1);
+                                var end = d.getTime();
+                                d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
+                                carry = d.getUTCHours();
+                                d.setUTCHours(0);
+                            }
+                            else
+                                d.setUTCMonth(d.getUTCMonth() + tickSize);
+                        }
+                        else if (unit == "year") {
+                            d.setUTCFullYear(d.getUTCFullYear() + tickSize);
+                        }
+                        else
+                            d.setTime(v + step);
+                    } while (v < axis.max && v != prev);
+
+                    return ticks;
+                };
+
+                formatter = function (v, axis) {
+                    var d = new Date(v);
+
+                    // first check global format
+                    if (opts.timeformat != null)
+                        return $.plot.formatDate(d, opts.timeformat, opts.monthNames);
+                    
+                    var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
+                    var span = axis.max - axis.min;
+                    var suffix = (opts.twelveHourClock) ? " %p" : "";
+                    
+                    if (t < timeUnitSize.minute)
+                        fmt = "%h:%M:%S" + suffix;
+                    else if (t < timeUnitSize.day) {
+                        if (span < 2 * timeUnitSize.day)
+                            fmt = "%h:%M" + suffix;
+                        else
+                            fmt = "%b %d %h:%M" + suffix;
+                    }
+                    else if (t < timeUnitSize.month)
+                        fmt = "%b %d";
+                    else if (t < timeUnitSize.year) {
+                        if (span < timeUnitSize.year)
+                            fmt = "%b";
+                        else
+                            fmt = "%b %y";
+                    }
+                    else
+                        fmt = "%y";
+                    
+                    return $.plot.formatDate(d, fmt, opts.monthNames);
+                };
+            }
+            else {
+                // pretty rounding of base-10 numbers
+                var maxDec = opts.tickDecimals;
+                var dec = -Math.floor(Math.log(delta) / Math.LN10);
+                if (maxDec != null && dec > maxDec)
+                    dec = maxDec;
+
+                magn = Math.pow(10, -dec);
+                norm = delta / magn; // norm is between 1.0 and 10.0
+                
+                if (norm < 1.5)
+                    size = 1;
+                else if (norm < 3) {
+                    size = 2;
+                    // special case for 2.5, requires an extra decimal
+                    if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
+                        size = 2.5;
+                        ++dec;
+                    }
+                }
+                else if (norm < 7.5)
+                    size = 5;
+                else
+                    size = 10;
+
+                size *= magn;
+                
+                if (opts.minTickSize != null && size < opts.minTickSize)
+                    size = opts.minTickSize;
+
+                axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
+                axis.tickSize = opts.tickSize || size;
+
+                generator = function (axis) {
+                    var ticks = [];
+
+                    // spew out all possible ticks
+                    var start = floorInBase(axis.min, axis.tickSize),
+                        i = 0, v = Number.NaN, prev;
+                    do {
+                        prev = v;
+                        v = start + i * axis.tickSize;
+                        ticks.push(v);
+                        ++i;
+                    } while (v < axis.max && v != prev);
+                    return ticks;
+                };
+
+                formatter = function (v, axis) {
+                    return v.toFixed(axis.tickDecimals);
+                };
+            }
+
+            if (opts.alignTicksWithAxis != null) {
+                var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];
+                if (otherAxis && otherAxis.used && otherAxis != axis) {
+                    // consider snapping min/max to outermost nice ticks
+                    var niceTicks = generator(axis);
+                    if (niceTicks.length > 0) {
+                        if (opts.min == null)
+                            axis.min = Math.min(axis.min, niceTicks[0]);
+                        if (opts.max == null && niceTicks.length > 1)
+                            axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);
+                    }
+                    
+                    generator = function (axis) {
+                        // copy ticks, scaled to this axis
+                        var ticks = [], v, i;
+                        for (i = 0; i < otherAxis.ticks.length; ++i) {
+                            v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);
+                            v = axis.min + v * (axis.max - axis.min);
+                            ticks.push(v);
+                        }
+                        return ticks;
+                    };
+                    
+                    // we might need an extra decimal since forced
+                    // ticks don't necessarily fit naturally
+                    if (axis.mode != "time" && opts.tickDecimals == null) {
+                        var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1),
+                            ts = generator(axis);
+
+                        // only proceed if the tick interval rounded
+                        // with an extra decimal doesn't give us a
+                        // zero at end
+                        if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))
+                            axis.tickDecimals = extraDec;
+                    }
+                }
+            }
+
+            axis.tickGenerator = generator;
+            if ($.isFunction(opts.tickFormatter))
+                axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); };
+            else
+                axis.tickFormatter = formatter;
+        }
+        
+        function setTicks(axis) {
+            var oticks = axis.options.ticks, ticks = [];
+            if (oticks == null || (typeof oticks == "number" && oticks > 0))
+                ticks = axis.tickGenerator(axis);
+            else if (oticks) {
+                if ($.isFunction(oticks))
+                    // generate the ticks
+                    ticks = oticks({ min: axis.min, max: axis.max });
+                else
+                    ticks = oticks;
+            }
+
+            // clean up/labelify the supplied ticks, copy them over
+            var i, v;
+            axis.ticks = [];
+            for (i = 0; i < ticks.length; ++i) {
+                var label = null;
+                var t = ticks[i];
+                if (typeof t == "object") {
+                    v = +t[0];
+                    if (t.length > 1)
+                        label = t[1];
+                }
+                else
+                    v = +t;
+                if (label == null)
+                    label = axis.tickFormatter(v, axis);
+                if (!isNaN(v))
+                    axis.ticks.push({ v: v, label: label });
+            }
+        }
+
+        function snapRangeToTicks(axis, ticks) {
+            if (axis.options.autoscaleMargin && ticks.length > 0) {
+                // snap to ticks
+                if (axis.options.min == null)
+                    axis.min = Math.min(axis.min, ticks[0].v);
+                if (axis.options.max == null && ticks.length > 1)
+                    axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
+            }
+        }
+      
+        function draw() {
+            ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+
+            var grid = options.grid;
+
+            // draw background, if any
+            if (grid.show && grid.backgroundColor)
+                drawBackground();
+            
+            if (grid.show && !grid.aboveData)
+                drawGrid();
+
+            for (var i = 0; i < series.length; ++i) {
+                executeHooks(hooks.drawSeries, [ctx, series[i]]);
+                drawSeries(series[i]);
+            }
+
+            executeHooks(hooks.draw, [ctx]);
+            
+            if (grid.show && grid.aboveData)
+                drawGrid();
+        }
+
+        function extractRange(ranges, coord) {
+            var axis, from, to, key, axes = allAxes();
+
+            for (i = 0; i < axes.length; ++i) {
+                axis = axes[i];
+                if (axis.direction == coord) {
+                    key = coord + axis.n + "axis";
+                    if (!ranges[key] && axis.n == 1)
+                        key = coord + "axis"; // support x1axis as xaxis
+                    if (ranges[key]) {
+                        from = ranges[key].from;
+                        to = ranges[key].to;
+                        break;
+                    }
+                }
+            }
+
+            // backwards-compat stuff - to be removed in future
+            if (!ranges[key]) {
+                axis = coord == "x" ? xaxes[0] : yaxes[0];
+                from = ranges[coord + "1"];
+                to = ranges[coord + "2"];
+            }
+
+            // auto-reverse as an added bonus
+            if (from != null && to != null && from > to) {
+                var tmp = from;
+                from = to;
+                to = tmp;
+            }
+            
+            return { from: from, to: to, axis: axis };
+        }
+        
+        function drawBackground() {
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
+            ctx.fillRect(0, 0, plotWidth, plotHeight);
+            ctx.restore();
+        }
+
+        function drawGrid() {
+            var i;
+            
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            // draw markings
+            var markings = options.grid.markings;
+            if (markings) {
+                if ($.isFunction(markings)) {
+                    var axes = plot.getAxes();
+                    // xmin etc. is backwards compatibility, to be
+                    // removed in the future
+                    axes.xmin = axes.xaxis.min;
+                    axes.xmax = axes.xaxis.max;
+                    axes.ymin = axes.yaxis.min;
+                    axes.ymax = axes.yaxis.max;
+                    
+                    markings = markings(axes);
+                }
+
+                for (i = 0; i < markings.length; ++i) {
+                    var m = markings[i],
+                        xrange = extractRange(m, "x"),
+                        yrange = extractRange(m, "y");
+
+                    // fill in missing
+                    if (xrange.from == null)
+                        xrange.from = xrange.axis.min;
+                    if (xrange.to == null)
+                        xrange.to = xrange.axis.max;
+                    if (yrange.from == null)
+                        yrange.from = yrange.axis.min;
+                    if (yrange.to == null)
+                        yrange.to = yrange.axis.max;
+
+                    // clip
+                    if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
+                        yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
+                        continue;
+
+                    xrange.from = Math.max(xrange.from, xrange.axis.min);
+                    xrange.to = Math.min(xrange.to, xrange.axis.max);
+                    yrange.from = Math.max(yrange.from, yrange.axis.min);
+                    yrange.to = Math.min(yrange.to, yrange.axis.max);
+
+                    if (xrange.from == xrange.to && yrange.from == yrange.to)
+                        continue;
+
+                    // then draw
+                    xrange.from = xrange.axis.p2c(xrange.from);
+                    xrange.to = xrange.axis.p2c(xrange.to);
+                    yrange.from = yrange.axis.p2c(yrange.from);
+                    yrange.to = yrange.axis.p2c(yrange.to);
+                    
+                    if (xrange.from == xrange.to || yrange.from == yrange.to) {
+                        // draw line
+                        ctx.beginPath();
+                        ctx.strokeStyle = m.color || options.grid.markingsColor;
+                        ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
+                        ctx.moveTo(xrange.from, yrange.from);
+                        ctx.lineTo(xrange.to, yrange.to);
+                        ctx.stroke();
+                    }
+                    else {
+                        // fill area
+                        ctx.fillStyle = m.color || options.grid.markingsColor;
+                        ctx.fillRect(xrange.from, yrange.to,
+                                     xrange.to - xrange.from,
+                                     yrange.from - yrange.to);
+                    }
+                }
+            }
+            
+            // draw the ticks
+            var axes = allAxes(), bw = options.grid.borderWidth;
+
+            for (var j = 0; j < axes.length; ++j) {
+                var axis = axes[j], box = axis.box,
+                    t = axis.tickLength, x, y, xoff, yoff;
+                if (!axis.show || axis.ticks.length == 0)
+                    continue
+                
+                ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString();
+                ctx.lineWidth = 1;
+
+                // find the edges
+                if (axis.direction == "x") {
+                    x = 0;
+                    if (t == "full")
+                        y = (axis.position == "top" ? 0 : plotHeight);
+                    else
+                        y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0);
+                }
+                else {
+                    y = 0;
+                    if (t == "full")
+                        x = (axis.position == "left" ? 0 : plotWidth);
+                    else
+                        x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0);
+                }
+                
+                // draw tick bar
+                if (!axis.innermost) {
+                    ctx.beginPath();
+                    xoff = yoff = 0;
+                    if (axis.direction == "x")
+                        xoff = plotWidth;
+                    else
+                        yoff = plotHeight;
+                    
+                    if (ctx.lineWidth == 1) {
+                        x = Math.floor(x) + 0.5;
+                        y = Math.floor(y) + 0.5;
+                    }
+
+                    ctx.moveTo(x, y);
+                    ctx.lineTo(x + xoff, y + yoff);
+                    ctx.stroke();
+                }
+
+                // draw ticks
+                ctx.beginPath();
+                for (i = 0; i < axis.ticks.length; ++i) {
+                    var v = axis.ticks[i].v;
+                    
+                    xoff = yoff = 0;
+
+                    if (v < axis.min || v > axis.max
+                        // skip those lying on the axes if we got a border
+                        || (t == "full" && bw > 0
+                            && (v == axis.min || v == axis.max)))
+                        continue;
+
+                    if (axis.direction == "x") {
+                        x = axis.p2c(v);
+                        yoff = t == "full" ? -plotHeight : t;
+                        
+                        if (axis.position == "top")
+                            yoff = -yoff;
+                    }
+                    else {
+                        y = axis.p2c(v);
+                        xoff = t == "full" ? -plotWidth : t;
+                        
+                        if (axis.position == "left")
+                            xoff = -xoff;
+                    }
+
+                    if (ctx.lineWidth == 1) {
+                        if (axis.direction == "x")
+                            x = Math.floor(x) + 0.5;
+                        else
+                            y = Math.floor(y) + 0.5;
+                    }
+
+                    ctx.moveTo(x, y);
+                    ctx.lineTo(x + xoff, y + yoff);
+                }
+                
+                ctx.stroke();
+            }
+            
+            
+            // draw border
+            if (bw) {
+                ctx.lineWidth = bw;
+                ctx.strokeStyle = options.grid.borderColor;
+                ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
+            }
+
+            ctx.restore();
+        }
+
+        function insertAxisLabels() {
+            placeholder.find(".tickLabels").remove();
+            
+            var html = ['<div class="tickLabels" style="font-size:smaller">'];
+
+            var axes = allAxes();
+            for (var j = 0; j < axes.length; ++j) {
+                var axis = axes[j], box = axis.box;
+                if (!axis.show)
+                    continue;
+                //debug: html.push('<div style="position:absolute;opacity:0.10;background-color:red;left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width +  'px;height:' + box.height + 'px"></div>')
+                html.push('<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis" style="color:' + axis.options.color + '">');
+                for (var i = 0; i < axis.ticks.length; ++i) {
+                    var tick = axis.ticks[i];
+                    if (!tick.label || tick.v < axis.min || tick.v > axis.max)
+                        continue;
+
+                    var pos = {}, align;
+                    
+                    if (axis.direction == "x") {
+                        align = "center";
+                        pos.left = Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2);
+                        if (axis.position == "bottom")
+                            pos.top = box.top + box.padding;
+                        else
+                            pos.bottom = canvasHeight - (box.top + box.height - box.padding);
+                    }
+                    else {
+                        pos.top = Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2);
+                        if (axis.position == "left") {
+                            pos.right = canvasWidth - (box.left + box.width - box.padding)
+                            align = "right";
+                        }
+                        else {
+                            pos.left = box.left + box.padding;
+                            align = "left";
+                        }
+                    }
+
+                    pos.width = axis.labelWidth;
+
+                    var style = ["position:absolute", "text-align:" + align ];
+                    for (var a in pos)
+                        style.push(a + ":" + pos[a] + "px")
+                    
+                    html.push('<div class="tickLabel" style="' + style.join(';') + '">' + tick.label + '</div>');
+                }
+                html.push('</div>');
+            }
+
+            html.push('</div>');
+
+            placeholder.append(html.join(""));
+        }
+
+        function drawSeries(series) {
+            if (series.lines.show)
+                drawSeriesLines(series);
+            if (series.bars.show)
+                drawSeriesBars(series);
+            if (series.points.show)
+                drawSeriesPoints(series);
+        }
+        
+        function drawSeriesLines(series) {
+            function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
+                var points = datapoints.points,
+                    ps = datapoints.pointsize,
+                    prevx = null, prevy = null;
+                
+                ctx.beginPath();
+                for (var i = ps; i < points.length; i += ps) {
+                    var x1 = points[i - ps], y1 = points[i - ps + 1],
+                        x2 = points[i], y2 = points[i + 1];
+                    
+                    if (x1 == null || x2 == null)
+                        continue;
+
+                    // clip with ymin
+                    if (y1 <= y2 && y1 < axisy.min) {
+                        if (y2 < axisy.min)
+                            continue;   // line segment is outside
+                        // compute new intersection point
+                        x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.min;
+                    }
+                    else if (y2 <= y1 && y2 < axisy.min) {
+                        if (y1 < axisy.min)
+                            continue;
+                        x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.min;
+                    }
+
+                    // clip with ymax
+                    if (y1 >= y2 && y1 > axisy.max) {
+                        if (y2 > axisy.max)
+                            continue;
+                        x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.max;
+                    }
+                    else if (y2 >= y1 && y2 > axisy.max) {
+                        if (y1 > axisy.max)
+                            continue;
+                        x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.max;
+                    }
+
+                    // clip with xmin
+                    if (x1 <= x2 && x1 < axisx.min) {
+                        if (x2 < axisx.min)
+                            continue;
+                        y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.min;
+                    }
+                    else if (x2 <= x1 && x2 < axisx.min) {
+                        if (x1 < axisx.min)
+                            continue;
+                        y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.min;
+                    }
+
+                    // clip with xmax
+                    if (x1 >= x2 && x1 > axisx.max) {
+                        if (x2 > axisx.max)
+                            continue;
+                        y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.max;
+                    }
+                    else if (x2 >= x1 && x2 > axisx.max) {
+                        if (x1 > axisx.max)
+                            continue;
+                        y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.max;
+                    }
+
+                    if (x1 != prevx || y1 != prevy)
+                        ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
+                    
+                    prevx = x2;
+                    prevy = y2;
+                    ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
+                }
+                ctx.stroke();
+            }
+
+            function plotLineArea(datapoints, axisx, axisy) {
+                var points = datapoints.points,
+                    ps = datapoints.pointsize,
+                    bottom = Math.min(Math.max(0, axisy.min), axisy.max),
+                    i = 0, top, areaOpen = false,
+                    ypos = 1, segmentStart = 0, segmentEnd = 0;
+
+                // we process each segment in two turns, first forward
+                // direction to sketch out top, then once we hit the
+                // end we go backwards to sketch the bottom
+                while (true) {
+                    if (ps > 0 && i > points.length + ps)
+                        break;
+
+                    i += ps; // ps is negative if going backwards
+
+                    var x1 = points[i - ps],
+                        y1 = points[i - ps + ypos],
+                        x2 = points[i], y2 = points[i + ypos];
+
+                    if (areaOpen) {
+                        if (ps > 0 && x1 != null && x2 == null) {
+                            // at turning point
+                            segmentEnd = i;
+                            ps = -ps;
+                            ypos = 2;
+                            continue;
+                        }
+
+                        if (ps < 0 && i == segmentStart + ps) {
+                            // done with the reverse sweep
+                            ctx.fill();
+                            areaOpen = false;
+                            ps = -ps;
+                            ypos = 1;
+                            i = segmentStart = segmentEnd + ps;
+                            continue;
+                        }
+                    }
+
+                    if (x1 == null || x2 == null)
+                        continue;
+
+                    // clip x values
+                    
+                    // clip with xmin
+                    if (x1 <= x2 && x1 < axisx.min) {
+                        if (x2 < axisx.min)
+                            continue;
+                        y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.min;
+                    }
+                    else if (x2 <= x1 && x2 < axisx.min) {
+                        if (x1 < axisx.min)
+                            continue;
+                        y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.min;
+                    }
+
+                    // clip with xmax
+                    if (x1 >= x2 && x1 > axisx.max) {
+                        if (x2 > axisx.max)
+                            continue;
+                        y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.max;
+                    }
+                    else if (x2 >= x1 && x2 > axisx.max) {
+                        if (x1 > axisx.max)
+                            continue;
+                        y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.max;
+                    }
+
+                    if (!areaOpen) {
+                        // open area
+                        ctx.beginPath();
+                        ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
+                        areaOpen = true;
+                    }
+                    
+                    // now first check the case where both is outside
+                    if (y1 >= axisy.max && y2 >= axisy.max) {
+                        ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
+                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
+                        continue;
+                    }
+                    else if (y1 <= axisy.min && y2 <= axisy.min) {
+                        ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
+                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
+                        continue;
+                    }
+                    
+                    // else it's a bit more complicated, there might
+                    // be a flat maxed out rectangle first, then a
+                    // triangular cutout or reverse; to find these
+                    // keep track of the current x values
+                    var x1old = x1, x2old = x2;
+
+                    // clip the y values, without shortcutting, we
+                    // go through all cases in turn
+                    
+                    // clip with ymin
+                    if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
+                        x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.min;
+                    }
+                    else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
+                        x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.min;
+                    }
+
+                    // clip with ymax
+                    if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
+                        x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.max;
+                    }
+                    else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
+                        x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.max;
+                    }
+
+                    // if the x value was changed we got a rectangle
+                    // to fill
+                    if (x1 != x1old) {
+                        ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
+                        // it goes to (x1, y1), but we fill that below
+                    }
+                    
+                    // fill triangular section, this sometimes result
+                    // in redundant points if (x1, y1) hasn't changed
+                    // from previous line to, but we just ignore that
+                    ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
+                    ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
+
+                    // fill the other rectangle if it's there
+                    if (x2 != x2old) {
+                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
+                        ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
+                    }
+                }
+            }
+
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+            ctx.lineJoin = "round";
+
+            var lw = series.lines.lineWidth,
+                sw = series.shadowSize;
+            // FIXME: consider another form of shadow when filling is turned on
+            if (lw > 0 && sw > 0) {
+                // draw shadow as a thick and thin line with transparency
+                ctx.lineWidth = sw;
+                ctx.strokeStyle = "rgba(0,0,0,0.1)";
+                // position shadow at angle from the mid of line
+                var angle = Math.PI/18;
+                plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
+                ctx.lineWidth = sw/2;
+                plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
+            }
+
+            ctx.lineWidth = lw;
+            ctx.strokeStyle = series.color;
+            var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
+            if (fillStyle) {
+                ctx.fillStyle = fillStyle;
+                plotLineArea(series.datapoints, series.xaxis, series.yaxis);
+            }
+
+            if (lw > 0)
+                plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
+            ctx.restore();
+        }
+
+        function drawSeriesPoints(series) {
+            function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {
+                var points = datapoints.points, ps = datapoints.pointsize;
+
+                for (var i = 0; i < points.length; i += ps) {
+                    var x = points[i], y = points[i + 1];
+                    if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
+                        continue;
+                    
+                    ctx.beginPath();
+                    x = axisx.p2c(x);
+                    y = axisy.p2c(y) + offset;
+                    if (symbol == "circle")
+                        ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
+                    else
+                        symbol(ctx, x, y, radius, shadow);
+                    ctx.closePath();
+                    
+                    if (fillStyle) {
+                        ctx.fillStyle = fillStyle;
+                        ctx.fill();
+                    }
+                    ctx.stroke();
+                }
+            }
+            
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            var lw = series.points.lineWidth,
+                sw = series.shadowSize,
+                radius = series.points.radius,
+                symbol = series.points.symbol;
+            if (lw > 0 && sw > 0) {
+                // draw shadow in two steps
+                var w = sw / 2;
+                ctx.lineWidth = w;
+                ctx.strokeStyle = "rgba(0,0,0,0.1)";
+                plotPoints(series.datapoints, radius, null, w + w/2, true,
+                           series.xaxis, series.yaxis, symbol);
+
+                ctx.strokeStyle = "rgba(0,0,0,0.2)";
+                plotPoints(series.datapoints, radius, null, w/2, true,
+                           series.xaxis, series.yaxis, symbol);
+            }
+
+            ctx.lineWidth = lw;
+            ctx.strokeStyle = series.color;
+            plotPoints(series.datapoints, radius,
+                       getFillStyle(series.points, series.color), 0, false,
+                       series.xaxis, series.yaxis, symbol);
+            ctx.restore();
+        }
+
+        function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
+            var left, right, bottom, top,
+                drawLeft, drawRight, drawTop, drawBottom,
+                tmp;
+
+            // in horizontal mode, we start the bar from the left
+            // instead of from the bottom so it appears to be
+            // horizontal rather than vertical
+            if (horizontal) {
+                drawBottom = drawRight = drawTop = true;
+                drawLeft = false;
+                left = b;
+                right = x;
+                top = y + barLeft;
+                bottom = y + barRight;
+
+                // account for negative bars
+                if (right < left) {
+                    tmp = right;
+                    right = left;
+                    left = tmp;
+                    drawLeft = true;
+                    drawRight = false;
+                }
+            }
+            else {
+                drawLeft = drawRight = drawTop = true;
+                drawBottom = false;
+                left = x + barLeft;
+                right = x + barRight;
+                bottom = b;
+                top = y;
+
+                // account for negative bars
+                if (top < bottom) {
+                    tmp = top;
+                    top = bottom;
+                    bottom = tmp;
+                    drawBottom = true;
+                    drawTop = false;
+                }
+            }
+           
+            // clip
+            if (right < axisx.min || left > axisx.max ||
+                top < axisy.min || bottom > axisy.max)
+                return;
+            
+            if (left < axisx.min) {
+                left = axisx.min;
+                drawLeft = false;
+            }
+
+            if (right > axisx.max) {
+                right = axisx.max;
+                drawRight = false;
+            }
+
+            if (bottom < axisy.min) {
+                bottom = axisy.min;
+                drawBottom = false;
+            }
+            
+            if (top > axisy.max) {
+                top = axisy.max;
+                drawTop = false;
+            }
+
+            left = axisx.p2c(left);
+            bottom = axisy.p2c(bottom);
+            right = axisx.p2c(right);
+            top = axisy.p2c(top);
+            
+            // fill the bar
+            if (fillStyleCallback) {
+                c.beginPath();
+                c.moveTo(left, bottom);
+                c.lineTo(left, top);
+                c.lineTo(right, top);
+                c.lineTo(right, bottom);
+                c.fillStyle = fillStyleCallback(bottom, top);
+                c.fill();
+            }
+
+            // draw outline
+            if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
+                c.beginPath();
+
+                // FIXME: inline moveTo is buggy with excanvas
+                c.moveTo(left, bottom + offset);
+                if (drawLeft)
+                    c.lineTo(left, top + offset);
+                else
+                    c.moveTo(left, top + offset);
+                if (drawTop)
+                    c.lineTo(right, top + offset);
+                else
+                    c.moveTo(right, top + offset);
+                if (drawRight)
+                    c.lineTo(right, bottom + offset);
+                else
+                    c.moveTo(right, bottom + offset);
+                if (drawBottom)
+                    c.lineTo(left, bottom + offset);
+                else
+                    c.moveTo(left, bottom + offset);
+                c.stroke();
+            }
+        }
+        
+        function drawSeriesBars(series) {
+            function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
+                var points = datapoints.points, ps = datapoints.pointsize;
+                
+                for (var i = 0; i < points.length; i += ps) {
+                    if (points[i] == null)
+                        continue;
+                    drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
+                }
+            }
+
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            // FIXME: figure out a way to add shadows (for instance along the right edge)
+            ctx.lineWidth = series.bars.lineWidth;
+            ctx.strokeStyle = series.color;
+            var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
+            var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
+            plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
+            ctx.restore();
+        }
+
+        function getFillStyle(filloptions, seriesColor, bottom, top) {
+            var fill = filloptions.fill;
+            if (!fill)
+                return null;
+
+            if (filloptions.fillColor)
+                return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
+            
+            var c = $.color.parse(seriesColor);
+            c.a = typeof fill == "number" ? fill : 0.4;
+            c.normalize();
+            return c.toString();
+        }
+        
+        function insertLegend() {
+            placeholder.find(".legend").remove();
+
+            if (!options.legend.show)
+                return;
+            
+            var fragments = [], rowStarted = false,
+                lf = options.legend.labelFormatter, s, label;
+            for (var i = 0; i < series.length; ++i) {
+                s = series[i];
+                label = s.label;
+                if (!label)
+                    continue;
+                
+                if (i % options.legend.noColumns == 0) {
+                    if (rowStarted)
+                        fragments.push('</tr>');
+                    fragments.push('<tr>');
+                    rowStarted = true;
+                }
+
+                if (lf)
+                    label = lf(label, s);
+                
+                fragments.push(
+                    '<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + s.color + ';overflow:hidden"></div></div></td>' +
+                    '<td class="legendLabel">' + label + '</td>');
+            }
+            if (rowStarted)
+                fragments.push('</tr>');
+            
+            if (fragments.length == 0)
+                return;
+
+            var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
+            if (options.legend.container != null)
+                $(options.legend.container).html(table);
+            else {
+                var pos = "",
+                    p = options.legend.position,
+                    m = options.legend.margin;
+                if (m[0] == null)
+                    m = [m, m];
+                if (p.charAt(0) == "n")
+                    pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
+                else if (p.charAt(0) == "s")
+                    pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
+                if (p.charAt(1) == "e")
+                    pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
+                else if (p.charAt(1) == "w")
+                    pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
+                var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
+                if (options.legend.backgroundOpacity != 0.0) {
+                    // put in the transparent background
+                    // separately to avoid blended labels and
+                    // label boxes
+                    var c = options.legend.backgroundColor;
+                    if (c == null) {
+                        c = options.grid.backgroundColor;
+                        if (c && typeof c == "string")
+                            c = $.color.parse(c);
+                        else
+                            c = $.color.extract(legend, 'background-color');
+                        c.a = 1;
+                        c = c.toString();
+                    }
+                    var div = legend.children();
+                    $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"></div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
+                }
+            }
+        }
+
+
+        // interactive features
+        
+        var highlights = [],
+            redrawTimeout = null;
+        
+        // returns the data item the mouse is over, or null if none is found
+        function findNearbyItem(mouseX, mouseY, seriesFilter) {
+            var maxDistance = options.grid.mouseActiveRadius,
+                smallestDistance = maxDistance * maxDistance + 1,
+                item = null, foundPoint = false, i, j;
+
+            for (i = series.length - 1; i >= 0; --i) {
+                if (!seriesFilter(series[i]))
+                    continue;
+                
+                var s = series[i],
+                    axisx = s.xaxis,
+                    axisy = s.yaxis,
+                    points = s.datapoints.points,
+                    ps = s.datapoints.pointsize,
+                    mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
+                    my = axisy.c2p(mouseY),
+                    maxx = maxDistance / axisx.scale,
+                    maxy = maxDistance / axisy.scale;
+
+                // with inverse transforms, we can't use the maxx/maxy
+                // optimization, sadly
+                if (axisx.options.inverseTransform)
+                    maxx = Number.MAX_VALUE;
+                if (axisy.options.inverseTransform)
+                    maxy = Number.MAX_VALUE;
+                
+                if (s.lines.show || s.points.show) {
+                    for (j = 0; j < points.length; j += ps) {
+                        var x = points[j], y = points[j + 1];
+                        if (x == null)
+                            continue;
+                        
+                        // For points and lines, the cursor must be within a
+                        // certain distance to the data point
+                        if (x - mx > maxx || x - mx < -maxx ||
+                            y - my > maxy || y - my < -maxy)
+                            continue;
+
+                        // We have to calculate distances in pixels, not in
+                        // data units, because the scales of the axes may be different
+                        var dx = Math.abs(axisx.p2c(x) - mouseX),
+                            dy = Math.abs(axisy.p2c(y) - mouseY),
+                            dist = dx * dx + dy * dy; // we save the sqrt
+
+                        // use <= to ensure last point takes precedence
+                        // (last generally means on top of)
+                        if (dist < smallestDistance) {
+                            smallestDistance = dist;
+                            item = [i, j / ps];
+                        }
+                    }
+                }
+                    
+                if (s.bars.show && !item) { // no other point can be nearby
+                    var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
+                        barRight = barLeft + s.bars.barWidth;
+                    
+                    for (j = 0; j < points.length; j += ps) {
+                        var x = points[j], y = points[j + 1], b = points[j + 2];
+                        if (x == null)
+                            continue;
+  
+                        // for a bar graph, the cursor must be inside the bar
+                        if (series[i].bars.horizontal ? 
+                            (mx <= Math.max(b, x) && mx >= Math.min(b, x) && 
+                             my >= y + barLeft && my <= y + barRight) :
+                            (mx >= x + barLeft && mx <= x + barRight &&
+                             my >= Math.min(b, y) && my <= Math.max(b, y)))
+                                item = [i, j / ps];
+                    }
+                }
+            }
+
+            if (item) {
+                i = item[0];
+                j = item[1];
+                ps = series[i].datapoints.pointsize;
+                
+                return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
+                         dataIndex: j,
+                         series: series[i],
+                         seriesIndex: i };
+            }
+            
+            return null;
+        }
+
+        function onMouseMove(e) {
+            if (options.grid.hoverable)
+                triggerClickHoverEvent("plothover", e,
+                                       function (s) { return s["hoverable"] != false; });
+        }
+
+        function onMouseLeave(e) {
+            if (options.grid.hoverable)
+                triggerClickHoverEvent("plothover", e,
+                                       function (s) { return false; });
+        }
+
+        function onClick(e) {
+            triggerClickHoverEvent("plotclick", e,
+                                   function (s) { return s["clickable"] != false; });
+        }
+
+        // trigger click or hover event (they send the same parameters
+        // so we share their code)
+        function triggerClickHoverEvent(eventname, event, seriesFilter) {
+            var offset = eventHolder.offset(),
+                canvasX = event.pageX - offset.left - plotOffset.left,
+                canvasY = event.pageY - offset.top - plotOffset.top,
+            pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
+
+            pos.pageX = event.pageX;
+            pos.pageY = event.pageY;
+
+            var item = findNearbyItem(canvasX, canvasY, seriesFilter);
+
+            if (item) {
+                // fill in mouse pos for any listeners out there
+                item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left);
+                item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top);
+            }
+
+            if (options.grid.autoHighlight) {
+                // clear auto-highlights
+                for (var i = 0; i < highlights.length; ++i) {
+                    var h = highlights[i];
+                    if (h.auto == eventname &&
+                        !(item && h.series == item.series &&
+                          h.point[0] == item.datapoint[0] &&
+                          h.point[1] == item.datapoint[1]))
+                        unhighlight(h.series, h.point);
+                }
+                
+                if (item)
+                    highlight(item.series, item.datapoint, eventname);
+            }
+            
+            placeholder.trigger(eventname, [ pos, item ]);
+        }
+
+        function triggerRedrawOverlay() {
+            if (!redrawTimeout)
+                redrawTimeout = setTimeout(drawOverlay, 30);
+        }
+
+        function drawOverlay() {
+            redrawTimeout = null;
+
+            // draw highlights
+            octx.save();
+            octx.clearRect(0, 0, canvasWidth, canvasHeight);
+            octx.translate(plotOffset.left, plotOffset.top);
+            
+            var i, hi;
+            for (i = 0; i < highlights.length; ++i) {
+                hi = highlights[i];
+
+                if (hi.series.bars.show)
+                    drawBarHighlight(hi.series, hi.point);
+                else
+                    drawPointHighlight(hi.series, hi.point);
+            }
+            octx.restore();
+            
+            executeHooks(hooks.drawOverlay, [octx]);
+        }
+        
+        function highlight(s, point, auto) {
+            if (typeof s == "number")
+                s = series[s];
+
+            if (typeof point == "number") {
+                var ps = s.datapoints.pointsize;
+                point = s.datapoints.points.slice(ps * point, ps * (point + 1));
+            }
+
+            var i = indexOfHighlight(s, point);
+            if (i == -1) {
+                highlights.push({ series: s, point: point, auto: auto });
+
+                triggerRedrawOverlay();
+            }
+            else if (!auto)
+                highlights[i].auto = false;
+        }
+            
+        function unhighlight(s, point) {
+            if (s == null && point == null) {
+                highlights = [];
+                triggerRedrawOverlay();
+            }
+            
+            if (typeof s == "number")
+                s = series[s];
+
+            if (typeof point == "number")
+                point = s.data[point];
+
+            var i = indexOfHighlight(s, point);
+            if (i != -1) {
+                highlights.splice(i, 1);
+
+                triggerRedrawOverlay();
+            }
+        }
+        
+        function indexOfHighlight(s, p) {
+            for (var i = 0; i < highlights.length; ++i) {
+                var h = highlights[i];
+                if (h.series == s && h.point[0] == p[0]
+                    && h.point[1] == p[1])
+                    return i;
+            }
+            return -1;
+        }
+        
+        function drawPointHighlight(series, point) {
+            var x = point[0], y = point[1],
+                axisx = series.xaxis, axisy = series.yaxis;
+            
+            if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
+                return;
+            
+            var pointRadius = series.points.radius + series.points.lineWidth / 2;
+            octx.lineWidth = pointRadius;
+            octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+            var radius = 1.5 * pointRadius,
+                x = axisx.p2c(x),
+                y = axisy.p2c(y);
+            
+            octx.beginPath();
+            if (series.points.symbol == "circle")
+                octx.arc(x, y, radius, 0, 2 * Math.PI, false);
+            else
+                series.points.symbol(octx, x, y, radius, false);
+            octx.closePath();
+            octx.stroke();
+        }
+
+        function drawBarHighlight(series, point) {
+            octx.lineWidth = series.bars.lineWidth;
+            octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+            var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+            var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
+            drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
+                    0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
+        }
+
+        function getColorOrGradient(spec, bottom, top, defaultColor) {
+            if (typeof spec == "string")
+                return spec;
+            else {
+                // assume this is a gradient spec; IE currently only
+                // supports a simple vertical gradient properly, so that's
+                // what we support too
+                var gradient = ctx.createLinearGradient(0, top, 0, bottom);
+                
+                for (var i = 0, l = spec.colors.length; i < l; ++i) {
+                    var c = spec.colors[i];
+                    if (typeof c != "string") {
+                        var co = $.color.parse(defaultColor);
+                        if (c.brightness != null)
+                            co = co.scale('rgb', c.brightness)
+                        if (c.opacity != null)
+                            co.a *= c.opacity;
+                        c = co.toString();
+                    }
+                    gradient.addColorStop(i / (l - 1), c);
+                }
+                
+                return gradient;
+            }
+        }
+    }
+
+    $.plot = function(placeholder, data, options) {
+        //var t0 = new Date();
+        var plot = new Plot($(placeholder), data, options, $.plot.plugins);
+        //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
+        return plot;
+    };
+
+    $.plot.version = "0.7";
+    
+    $.plot.plugins = [];
+
+    // returns a string with the date d formatted according to fmt
+    $.plot.formatDate = function(d, fmt, monthNames) {
+        var leftPad = function(n) {
+            n = "" + n;
+            return n.length == 1 ? "0" + n : n;
+        };
+        
+        var r = [];
+        var escape = false, padNext = false;
+        var hours = d.getUTCHours();
+        var isAM = hours < 12;
+        if (monthNames == null)
+            monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+        if (fmt.search(/%p|%P/) != -1) {
+            if (hours > 12) {
+                hours = hours - 12;
+            } else if (hours == 0) {
+                hours = 12;
+            }
+        }
+        for (var i = 0; i < fmt.length; ++i) {
+            var c = fmt.charAt(i);
+            
+            if (escape) {
+                switch (c) {
+                case 'h': c = "" + hours; break;
+                case 'H': c = leftPad(hours); break;
+                case 'M': c = leftPad(d.getUTCMinutes()); break;
+                case 'S': c = leftPad(d.getUTCSeconds()); break;
+                case 'd': c = "" + d.getUTCDate(); break;
+                case 'm': c = "" + (d.getUTCMonth() + 1); break;
+                case 'y': c = "" + d.getUTCFullYear(); break;
+                case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
+                case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
+                case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
+                case '0': c = ""; padNext = true; break;
+                }
+                if (c && padNext) {
+                    c = leftPad(c);
+                    padNext = false;
+                }
+                r.push(c);
+                if (!padNext)
+                    escape = false;
+            }
+            else {
+                if (c == "%")
+                    escape = true;
+                else
+                    r.push(c);
+            }
+        }
+        return r.join("");
+    };
+    
+    // round to nearby lower multiple of base
+    function floorInBase(n, base) {
+        return base * Math.floor(n / base);
+    }
+    
+})(jQuery);


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/ui.html	Wed Jul 20 10:31:55 2011 +0100
@@ -0,0 +1,40 @@
+<!-- Test/demo page for the data preview UI without requiring a CKAN instance -->
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8" />
+  <title>Data Preview Plugin</title>
+  <link rel="stylesheet" href="../public/ckanext/datapreview/data-preview.css" class="ckanext-datapreview-stylesheet" />
+</head>
+<body>
+  <div id="ckanext-datapreview-dialog"></div>
+  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.0/jquery.js"></script> 
+  <script src="../public/ckanext/datapreview/data-preview.js"></script>
+  <script>
+    jQuery(function ($) {
+      var dp = CKANEXT.DATAPREVIEW;
+      var dataproxyUrl = 'http://test-webstore.ckan.net/okfn';
+
+      // Update the sources to point to the public directory.
+      var sources = [dp.stylesheets, dp.scripts['jquery-ui'], dp.scripts['slickgrid'], dp.scripts['flot']];
+      $.each(sources, function (index, array) {
+        $.each(this, function (index, uri) {
+          array[index] = '../public' + uri;
+        });
+      });
+      dp.template.src = '../public' + dp.template.src;
+
+      dp.initialize(dataproxyUrl, 'ckanext-datapreview-dialog');
+
+      // Load a datasource without seting up the download links.
+      var dataSource = 'http://test-webstore.ckan.net/okfn/b21ae9c691445b73773156380336e2fab821cb64/resource.jsonp?_limit=30';
+      dp.loadDependancies(function () {
+        dp.getResourceDataDirect(dataSource, 'csv', function (url, type, data) {
+          dp.showData(url, type, data);
+          dp.$dialog.dialog('open');
+        });
+      });
+    });
+  </script>
+</body>
+</html>


http://bitbucket.org/okfn/ckanext-datapreview/changeset/e056163b3bc1/
changeset:   e056163b3bc1
user:        aron_
date:        2011-07-20 13:47:51
summary:     Began adding support for saving charts to the server

Now updating the UI while saving/loading data from the server. Currently
mocking the requests to the webstore.
affected #:  3 files (2.4 KB)

--- a/public/ckanext/datapreview/data-preview.html	Wed Jul 20 10:31:55 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.html	Wed Jul 20 12:47:51 2011 +0100
@@ -28,7 +28,8 @@
         </li></ul><div class="ckanext-datapreview-editor-submit">
-        <button>Draw</button>
+        <button class="ckanext-datapreview-editor-update">Update</button>
+        <button class="ckanext-datapreview-editor-save">Save</button></div></form></div>


--- a/public/ckanext/datapreview/data-preview.js	Wed Jul 20 10:31:55 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Wed Jul 20 12:47:51 2011 +0100
@@ -124,14 +124,40 @@
     });
   };
 
-  my.loadDataPreview = function (columns, data) {
+  my.getResourceChart = function (hash, callback) {
+    setTimeout(function () {
+      callback && callback();
+    }, 800);
+  };
+
+  my.updateResourceChart = function (hash, chart, callback) {
+    setTimeout(function () {
+      callback && callback();
+    }, 800);
+  };
+
+  my.loadDataPreview = function (columns, data, chart) {
     var dialog = my.$dialog;
 
     // Need to create the grid once the dialog is open for cells to render
     // correctly.
     dialog.dialog(my.dialogOptions).one("dialogopen", function () {
       var element  = $(my.template.html).appendTo(dialog);
-      var viewer   = new my.createDataPreview(element, columns, data);
+      var viewer   = new my.createDataPreview(element, columns, data, chart);
+
+      // Load chart data from the webstore.
+      viewer.editor.loading();
+      my.getResourceChart('', function () {
+        viewer.editor.loading(false);
+      });
+
+      // Save chart data to the webstore.
+      viewer.editor.bind('save', function (chart) {
+        viewer.editor.saving();
+        my.updateResourceChart('', chart, function () {
+          viewer.editor.saving(false);
+        });
+      });
 
       dialog.bind("dialogresizestop.data-preview", viewer.redraw);
 


--- a/public/ckanext/datapreview/data-preview.ui.js	Wed Jul 20 10:31:55 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.ui.js	Wed Jul 20 12:47:51 2011 +0100
@@ -86,7 +86,7 @@
   });
 
   ui.MainView = inherit(ui.BaseView, {
-    constructor: function MainView(element, columns, data) {
+    constructor: function MainView(element, columns, data, chart) {
       this.__super__.constructor.apply(this, arguments);
 
       bindAll(this, 'redraw', 'onNavChange', 'onNavToggleEditor', 'onEditorSubmit');
@@ -95,7 +95,7 @@
       this.nav = new ui.NavigationView(this.$('.ckanext-datapreview-nav'));
       this.grid = new ui.GridView(this.$('.ckanext-datapreview-grid'), columns, data);
       this.chart = new ui.ChartView(this.$('.ckanext-datapreview-graph'), data);
-      this.editor = new ui.EditorView(this.$('.ckanext-datapreview-editor'), columns);
+      this.editor = new ui.EditorView(this.$('.ckanext-datapreview-editor'), columns, chart);
 
       this.nav.bind({
         'change': this.onNavChange,
@@ -111,6 +111,7 @@
     redraw: function () {
       this.chart.redraw();
       this.grid.redraw();
+      return this;
     },
     onNavChange: function (selected) {
       var isGrid = selected === 'grid';
@@ -255,20 +256,26 @@
   });
 
   ui.EditorView = inherit(ui.BaseView, {
-    constructor: function EditorView(element, columns) {
+    constructor: function EditorView(element, columns, chart) {
       this.__super__.constructor.apply(this, arguments);
 
-      bindAll(this, 'onSubmit');
+      bindAll(this, 'onSubmit', 'onSave');
 
       this.columns = columns;
       this.type    = this.$('.ckanext-datapreview-editor-type select');
       this.groups  = this.$('.ckanext-datapreview-editor-group select');
       this.series  = this.$('.ckanext-datapreview-editor-series select');
 
-      this.$('button').button().click(this.onSubmit);
+      this.$('button').button();
+      this.update = this.$('.ckanext-datapreview-editor-update').click(this.onSubmit);
+      this.save = this.$('.ckanext-datapreview-editor-save').click(this.onSave);
       this.el.bind('submit', this.onSubmit);
 
       this.setupTypeOptions().setupColumnOptions();
+
+      if (chart) {
+        this.load(chart);
+      }
     },
     setupTypeOptions: function () {
       var types = {};
@@ -289,19 +296,62 @@
       this.series.html(optionsString);
       return this;
     },
+    load: function (chart) {
+      this._selectOption(this.type, chart.type);
+      this._selectOption(this.groups, chart.groups);
+      $.each(chart.series, $.proxy(function (index, option) {
+        this._selectOption(this.series.eq(index), option);
+      }, this));
+      return this;
+    },
+    loading: function (show) {
+      var action = show === false ? 'enable' : 'disable';
+
+      this.$('select').attr('disabled', show !== false);
+      this.save.button(action);
+      this.update.button(action);
+
+      this._updateSaveText(show === false ? null : 'Loading...');
+
+      return this;
+    },
+    saving: function (show) {
+      this.save.button(show === false ? 'enable' : 'disable');
+      this._updateSaveText(show === false ? null : 'Saving...');
+      return this;
+    },
+    onSave: function (event) {
+      event.preventDefault();
+      this._triggerChartData('save');
+    },
     onSubmit: function (event) {
       event && event.preventDefault();
+      this._triggerChartData('submit');
+    },
+    _updateSaveText: function (text) {
+      var span = this.save.find('span'),
+          original = span.data('default');
 
+      if (!original) {
+        span.data('default', span.text());
+      }
+
+      span.text(text || original);
+    },
+    _triggerChartData: function (topic) {
       var series = this.series.map(function () {
         return $(this).val();
       });
 
-      this.trigger('submit', [{
+      this.trigger(topic, [{
         type: this.type.val(),
         groups: this.groups.val(),
         series: $.makeArray(series)
       }]);
     },
+    _selectOption: function (select, option) {
+      select.find('[value="' + option + '"]').attr('selected', 'selected');
+    },
     _createOptions: function (options) {
       var html = [];
       $.each(options, function (value, text) {


http://bitbucket.org/okfn/ckanext-datapreview/changeset/9b9f5e135b6e/
changeset:   9b9f5e135b6e
user:        aron_
date:        2011-07-20 14:21:39
summary:     Now passing around a resource object rather than just url & type arguments

This allows additional information about the resource to be store such as
source and hash properties.
affected #:  2 files (395 bytes)

--- a/public/ckanext/datapreview/data-preview.js	Wed Jul 20 12:47:51 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Wed Jul 20 13:21:39 2011 +0100
@@ -98,11 +98,11 @@
     });
   };
 
-  my.getResourceDataDirect = function(url, type, callback) {
+  my.getResourceDataDirect = function(resource, callback) {
     // $.ajax() does not call the "error" callback for JSONP requests so we
     // set a timeout to provide the callback with an error after x seconds.
     var timer = setTimeout(function error() {
-      callback(url, type, {
+      callback(resource, {
         error: {
           title: 'Request Error',
           message: 'Dataproxy server did not respond after ' + (my.timeout / 1000) + ' seconds'
@@ -114,12 +114,12 @@
     // a cache busting `={timestamp}` parameter to the query as the webstore
     // currently cannot handle custom parameters.
     $.ajax({
-      url: url,
+      url: resource.url,
       cache: true,
       dataType: 'jsonp',
       success: function(data) {
         clearTimeout(timer);
-        callback(url, type, data);
+        callback(resource, data);
       }
     });
   };
@@ -136,25 +136,25 @@
     }, 800);
   };
 
-  my.loadDataPreview = function (columns, data, chart) {
+  my.loadDataPreview = function (resource, columns, data) {
     var dialog = my.$dialog;
 
     // Need to create the grid once the dialog is open for cells to render
     // correctly.
     dialog.dialog(my.dialogOptions).one("dialogopen", function () {
       var element  = $(my.template.html).appendTo(dialog);
-      var viewer   = new my.createDataPreview(element, columns, data, chart);
+      var viewer   = new my.createDataPreview(element, columns, data);
 
       // Load chart data from the webstore.
       viewer.editor.loading();
-      my.getResourceChart('', function () {
+      my.getResourceChart(resource.hash, function () {
         viewer.editor.loading(false);
       });
 
       // Save chart data to the webstore.
       viewer.editor.bind('save', function (chart) {
         viewer.editor.saving();
-        my.updateResourceChart('', chart, function () {
+        my.updateResourceChart(resource.hash, chart, function () {
           viewer.editor.saving(false);
         });
       });
@@ -173,9 +173,9 @@
     my.$dialog.html(_html).dialog(my.errorDialogOptions);
   };
 
-  my.showData = function(url, type, data) {
+  my.showData = function(resource, data) {
     my.$dialog.empty();
-    my.$dialog.dialog('option', 'title', 'Preview: ' + url);
+    my.$dialog.dialog('option', 'title', 'Preview: ' + resource.source);
 
     if(data.error) {
       return my.showError(data.error);
@@ -194,12 +194,12 @@
       return cells;
     });
 
-    my.loadDataPreview(columns, data);
+    my.loadDataPreview(resource, columns, data);
   };
 
-  my.showPlainTextData = function(url, type, data) {
+  my.showPlainTextData = function(resource, data) {
     my.$dialog.empty();
-    my.$dialog.dialog('option', 'title', 'Preview: ' + url);
+    my.$dialog.dialog('option', 'title', 'Preview: ' + resource.source);
     if(data.error) {
       my.showError(data.error);
     } else {
@@ -223,10 +223,9 @@
   };
 
   my.loadPreviewDialog = function(link) {
-    var _url = link.href;
-    _url = my.normalizeUrl(_url);
-    var _type = $(link).attr('format');
-    _type = my.normalizeFormat(_type);
+    var resource = $(link).data();
+    resource.url = my.normalizeUrl(link.href);
+    resource.type = my.normalizeFormat(resource.format);
 
     function callbackWrapper(callback) {
       return function () {
@@ -242,21 +241,21 @@
 
     $(link).addClass('resource-preview-loading').text('Loading');
 
-    if (_type === '') {
-      var tmp = _url.split('/');
+    if (resource.type === '') {
+      var tmp = resource.url.split('/');
       tmp = tmp[tmp.length - 1];
       tmp = tmp.split('?'); // query strings
       tmp = tmp[0];
-      ext = tmp.split('.');
+      var ext = tmp.split('.');
       if (ext.length > 1) {
-        _type = ext[ext.length-1];
+        resource.type = ext[ext.length-1];
       }
     }
 
-    if (_type in {'csv': '', 'xls': ''}) {
-      my.getResourceDataDirect(_url, _type, callbackWrapper(my.showData));
+    if (resource.type in {'csv': '', 'xls': ''}) {
+      my.getResourceDataDirect(resource, callbackWrapper(my.showData));
     }
-    else if (_type in {
+    else if (resource.type in {
         'rdf+xml': '',
         'owl+xml': '',
         'xml': '',
@@ -270,22 +269,22 @@
         'txt': ''
         }) {
       // treat as plain text
-      my.getResourceDataDirect(_url, 'csv', callbackWrapper(my.showPlainTextData));
+      my.getResourceDataDirect(resource, callbackWrapper(my.showPlainTextData));
     }
     else {
       // very hacky but should work
-      callbackWrapper(my.showHtml)(_url);
+      callbackWrapper(my.showHtml)(resource.url);
     }
   };
 
   my.createPreviewButtons = function(resources) {
-    $(resources).find('tr:first th:first').before($('<th class="preview">Preview</th>'));
+    resources.find('tr:first th:first').before($('<th class="preview">Preview</th>'));
     /*
        :param resources: resource section div or table.
      */
-    $(resources).find('tr td:first-child').each(function(idx, element) {
+    resources.find('tr td:first-child').each(function(idx, element) {
       var element = $(element);
-      var _format = element.next().first().text().trim();
+      var _format = $.trim(element.next().text());
 
       var preview = $('<td class="preview"></td>').prependTo(element.parent());
 
@@ -313,7 +312,7 @@
       }
 
       // can not preview if hash value doesn't exist
-      var _hash = element.next().next().first().text().trim();
+      var _hash = $.trim(element.siblings().last().text());
       if (_hash === '') {
           return;
       }
@@ -323,12 +322,14 @@
       var _previewSpan = $('<a />', {
         text: 'Preview',
         href: _url,
-        format: _format,
         click: function(e) {
           e.preventDefault();
           my.loadPreviewDialog(e.target);
         },
-        'class': 'resource-preview-button'
+        'class': 'resource-preview-button',
+        'data-source': element.find('a').attr('href'),
+        'data-format': _format,
+        'data-hash': _hash
       }).appendTo(preview);
     });
   };


--- a/tests/ui.html	Wed Jul 20 12:47:51 2011 +0100
+++ b/tests/ui.html	Wed Jul 20 13:21:39 2011 +0100
@@ -29,7 +29,14 @@
       // Load a datasource without seting up the download links.
       var dataSource = 'http://test-webstore.ckan.net/okfn/b21ae9c691445b73773156380336e2fab821cb64/resource.jsonp?_limit=30';
       dp.loadDependancies(function () {
-        dp.getResourceDataDirect(dataSource, 'csv', function (url, type, data) {
+        var resource = {
+          url: dataSource,
+          hash: 'b21ae9c691445b73773156380336e2fab821cb64',
+          format: 'csv',
+          source: 'http://test.ckan.net/package/bank-of-england-interest-rate'
+        };
+
+        dp.getResourceDataDirect(resource, function (url, type, data) {
           dp.showData(url, type, data);
           dp.$dialog.dialog('open');
         });


http://bitbucket.org/okfn/ckanext-datapreview/changeset/f82fc9d369f5/
changeset:   f82fc9d369f5
user:        aron_
date:        2011-07-20 15:51:20
summary:     Added support for multiple series on a single chart
affected #:  3 files (1.9 KB)

--- a/public/ckanext/datapreview/data-preview.css	Wed Jul 20 13:21:39 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.css	Wed Jul 20 14:51:20 2011 +0100
@@ -1,3 +1,5 @@
+/* @override http://localhost:8000/public/ckanext/datapreview/data-preview.css */
+
 #ckanext-datapreview-dialog table td {
 	background: #fff;
 	border: 1px solid #eee;
@@ -73,6 +75,7 @@
 	list-style-type: none;
 	margin: 0;
 	padding: 0;
+	overflow: hidden;
 }
 
 .ckanext-datapreview-editor li {
@@ -86,6 +89,13 @@
 	line-height: 1.4;
 }
 
+.ckanext-datapreview-editor label a {
+	float: right;
+	font-size: 11px;
+	color: #999;
+	font-weight: normal;
+}
+
 .ckanext-datapreview-editor select {
 	width: 100%;
 }
@@ -94,6 +104,17 @@
 	float: right;
 }
 
+.ckanext-datapreview-editor-buttons {
+	clear: right;
+	overflow: hidden;
+}
+
+.ckanext-datapreview-editor-submit {
+	margin-top: 10px;
+	padding-top: 10px;
+	border-top: 1px solid #ddd;
+}
+
 .ckanext-datapreview-hide-editor .ckanext-datapreview-editor {
 	display: none;
 }


--- a/public/ckanext/datapreview/data-preview.html	Wed Jul 20 13:21:39 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.html	Wed Jul 20 14:51:20 2011 +0100
@@ -19,15 +19,18 @@
           <select></select></li><li class="ckanext-datapreview-editor-group">
-          <label>Group Column</label>
+          <label>Group Column (x-axis)</label><select></select></li><li class="ckanext-datapreview-editor-series">
-          <label>Series A</label>
+          <label>Series <span>A</span> (y-axis)</label><select></select></li></ul>
-      <div class="ckanext-datapreview-editor-submit">
+      <div class="ckanext-datapreview-editor-buttons">
+         <button class="ckanext-datapreview-editor-add">Add Series</button>
+      </div>
+      <div class="ckanext-datapreview-editor-buttons ckanext-datapreview-editor-submit"><button class="ckanext-datapreview-editor-update">Update</button><button class="ckanext-datapreview-editor-save">Save</button></div>


--- a/public/ckanext/datapreview/data-preview.ui.js	Wed Jul 20 13:21:39 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.ui.js	Wed Jul 20 14:51:20 2011 +0100
@@ -259,7 +259,7 @@
     constructor: function EditorView(element, columns, chart) {
       this.__super__.constructor.apply(this, arguments);
 
-      bindAll(this, 'onSubmit', 'onSave');
+      bindAll(this, 'onAdd', 'onRemove', 'onSubmit', 'onSave');
 
       this.columns = columns;
       this.type    = this.$('.ckanext-datapreview-editor-type select');
@@ -270,9 +270,14 @@
       this.update = this.$('.ckanext-datapreview-editor-update').click(this.onSubmit);
       this.save = this.$('.ckanext-datapreview-editor-save').click(this.onSave);
       this.el.bind('submit', this.onSubmit);
+      this.el.delegate('a[href="#remove"]', 'click', this.onRemove);
+
+      this.$('.ckanext-datapreview-editor-add').click(this.onAdd);
 
       this.setupTypeOptions().setupColumnOptions();
 
+      this.seriesClone = this.series.parent().clone();
+
       if (chart) {
         this.load(chart);
       }
@@ -296,12 +301,42 @@
       this.series.html(optionsString);
       return this;
     },
+    addSeries: function () {
+      var element = this.seriesClone.clone(),
+          label   = element.find('label'),
+          index   = this.series.length;
+
+      this.$('ul').append(element);
+      this.updateSeries()
+
+      label.append('<a href="#remove" data-index="' + index + '">Remove</a>');
+      label.find('span').text(String.fromCharCode(this.series.length + 64));
+
+      return this;
+    },
+    removeSeries: function (index) {
+      console.log('index', index);
+      this.series.eq(index).parent().remove();
+      return this.updateSeries();
+    },
+    updateSeries: function () {
+      this.series = this.$('.ckanext-datapreview-editor-series select');
+      return this;
+    },
     load: function (chart) {
+      var editor = this;
       this._selectOption(this.type, chart.type);
       this._selectOption(this.groups, chart.groups);
-      $.each(chart.series, $.proxy(function (index, option) {
-        this._selectOption(this.series.eq(index), option);
-      }, this));
+
+      $.each(chart.series, function update(index, option) {
+        var element = editor.series.eq(index);
+        if (!element.length) {
+          editor.addSeries();
+          return update(index, option);
+        }
+        editor._selectOption(element, option);
+      });
+
       return this;
     },
     loading: function (show) {
@@ -320,6 +355,15 @@
       this._updateSaveText(show === false ? null : 'Saving...');
       return this;
     },
+    onAdd: function (event) {
+      event.preventDefault();
+      this.addSeries();
+    },
+    onRemove: function (event) {
+      event.preventDefault();
+      console.log(event.target, event.target.getAttribute('data-index'));
+      this.removeSeries(event.target.getAttribute('data-index'));
+    },
     onSave: function (event) {
       event.preventDefault();
       this._triggerChartData('save');


http://bitbucket.org/okfn/ckanext-datapreview/changeset/0f969bb364a2/
changeset:   0f969bb364a2
user:        aron_
date:        2011-07-20 16:35:42
summary:     Now resizing data preview dialog when window resizes
affected #:  2 files (687 bytes)

--- a/public/ckanext/datapreview/data-preview.js	Wed Jul 20 14:51:20 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Wed Jul 20 15:35:42 2011 +0100
@@ -82,7 +82,8 @@
 
       $.when.apply($, scripts).then(function () {
         my.areDependanciesLoaded = true;
-        my.$dialog.dialog(my.dialogOptions).dialog("widget").css('position', 'fixed');
+        var dialog = my.$dialog;
+        dialog.dialog(my.dialogOptions).dialog("widget").css('position', 'fixed');
         callback();
       });
     });
@@ -168,14 +169,35 @@
     });
   };
 
+  my.setupFullscreenDialog = function (resource) {
+    var dialog = my.$dialog, $window = $(window), timer;
+
+    dialog.empty().dialog('option', 'title', 'Preview: ' + resource.source);
+
+    // Ensure the lightbox always fills the screen.
+    $window.bind('resize.data-preview', function () {
+      clearTimeout(timer);
+      timer = setTimeout(function () {
+        dialog.dialog('option', {
+          width:  $window.width()  - 20,
+          height: $window.height() - 20
+        });
+        dialog.trigger('dialogresizestop');
+      }, 100);
+    });
+
+    dialog.bind("dialogbeforeclose", function () {
+      $window.unbind("resize.data-preview");
+    });
+  }
+
   my.showError = function (error) {
     var _html = '<strong>' + $.trim(error.title) + '</strong><br />' + $.trim(error.message);
     my.$dialog.html(_html).dialog(my.errorDialogOptions);
   };
 
   my.showData = function(resource, data) {
-    my.$dialog.empty();
-    my.$dialog.dialog('option', 'title', 'Preview: ' + resource.source);
+    my.setupFullscreenDialog(resource);
 
     if(data.error) {
       return my.showError(data.error);
@@ -198,8 +220,8 @@
   };
 
   my.showPlainTextData = function(resource, data) {
-    my.$dialog.empty();
-    my.$dialog.dialog('option', 'title', 'Preview: ' + resource.source);
+    my.setupFullscreenDialog(resource);
+
     if(data.error) {
       my.showError(data.error);
     } else {


--- a/public/ckanext/datapreview/data-preview.ui.js	Wed Jul 20 14:51:20 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.ui.js	Wed Jul 20 15:35:42 2011 +0100
@@ -307,7 +307,7 @@
           index   = this.series.length;
 
       this.$('ul').append(element);
-      this.updateSeries()
+      this.updateSeries();
 
       label.append('<a href="#remove" data-index="' + index + '">Remove</a>');
       label.find('span').text(String.fromCharCode(this.series.length + 64));
@@ -315,7 +315,6 @@
       return this;
     },
     removeSeries: function (index) {
-      console.log('index', index);
       this.series.eq(index).parent().remove();
       return this.updateSeries();
     },
@@ -361,7 +360,6 @@
     },
     onRemove: function (event) {
       event.preventDefault();
-      console.log(event.target, event.target.getAttribute('data-index'));
       this.removeSeries(event.target.getAttribute('data-index'));
     },
     onSave: function (event) {


http://bitbucket.org/okfn/ckanext-datapreview/changeset/61fd0a3f6aee/
changeset:   61fd0a3f6aee
user:        aron_
date:        2011-07-20 17:51:36
summary:     Added a legend to the chart
affected #:  1 file (308 bytes)

--- a/public/ckanext/datapreview/data-preview.ui.js	Wed Jul 20 15:35:42 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.ui.js	Wed Jul 20 16:51:36 2011 +0100
@@ -94,7 +94,7 @@
       var view = this;
       this.nav = new ui.NavigationView(this.$('.ckanext-datapreview-nav'));
       this.grid = new ui.GridView(this.$('.ckanext-datapreview-grid'), columns, data);
-      this.chart = new ui.ChartView(this.$('.ckanext-datapreview-graph'), data);
+      this.chart = new ui.ChartView(this.$('.ckanext-datapreview-graph'), columns, data);
       this.editor = new ui.EditorView(this.$('.ckanext-datapreview-editor'), columns, chart);
 
       this.nav.bind({
@@ -214,32 +214,33 @@
   });
 
   ui.ChartView = inherit(ui.BaseView, {
-    constructor: function ChartView(element, gridData, chart) {
+    constructor: function ChartView(element, columns, data, chart) {
       this.__super__.constructor.apply(this, arguments);
-      this.gridData = gridData;
+      this.data = data;
+      this.columns = columns;
       this.chart = chart;
-      this.plot = $.plot(element, this.createData());
+      this.plot = $.plot(element, this.createSeries());
       this.draw();
     },
-    createData: function () {
-      var data = [], gridData = this.gridData, chart = this.chart;
-      if (chart) {
-        $.each(this.chart.series, function () {
-          var points = [], name = this;
-          $.each(gridData, function (index) {
-            var x = this[chart.groups], y = this[name];
+    createSeries: function () {
+      var series = [], view = this;
+      if (this.chart) {
+        $.each(this.chart.series, function (seriesIndex, field) {
+          var points = [];
+          $.each(view.data, function (index) {
+            var x = this[view.chart.groups], y = this[field];
             if (typeof x === 'string') {
               x = index;
             }
             points.push([x, y]);
           });
-          data.push({data: points});
+          series.push({data: points, label: view._getColumnName(field)});
         });
       }
-      return data;
+      return series;
     },
     draw: function () {
-      this.plot.setData(this.createData());
+      this.plot.setData(this.createSeries());
       return this.redraw();
     },
     update: function (chart) {
@@ -252,6 +253,14 @@
       this.plot.setupGrid();
       this.plot.draw();
       return this;
+    },
+    _getColumnName: function (field) {
+      for (var i = 0, count = this.columns.length; i < count; i += 1) {
+        if (this.columns[i].field === field) {
+          return this.columns[i].name;
+        }
+      }
+      return name;
     }
   });
 


http://bitbucket.org/okfn/ckanext-datapreview/changeset/94bf256bed23/
changeset:   94bf256bed23
user:        aron_
date:        2011-07-21 13:23:01
summary:     Added support for saving charts to the resource metadata

Currently this is only available to logged in users as an API key is
required to update packages via the API. Chart data is saved in the
"datapreview-charts" key on the resource.
affected #:  4 files (2.6 KB)

--- a/public/ckanext/datapreview/data-preview.html	Wed Jul 20 16:51:36 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.html	Thu Jul 21 12:23:01 2011 +0100
@@ -33,6 +33,7 @@
       <div class="ckanext-datapreview-editor-buttons ckanext-datapreview-editor-submit"><button class="ckanext-datapreview-editor-update">Update</button><button class="ckanext-datapreview-editor-save">Save</button>
+        <input type="hidden" class="ckanext-datapreview-editor-id" value="chart-1" /></div></form></div>


--- a/public/ckanext/datapreview/data-preview.js	Wed Jul 20 16:51:36 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Thu Jul 21 12:23:01 2011 +0100
@@ -6,6 +6,7 @@
   my.webstore = null;
   my.timeout = 5000;
   my.areDependanciesLoaded = false;
+  my.resourceChartKey = 'datapreview-charts';
 
   my.stylesheets = [
     '/ckanext/datapreview/jquery-ui/css/ckan/jquery-ui-1.8.14.custom.css',
@@ -30,7 +31,7 @@
 
   my.template = {
     html: '',
-    src: '/ckanext/datapreview/data-preview.html'
+    src: '/ckanext/datapreview/data-preview.html?'
   };
 
   my.normalizeFormat = function(format) {
@@ -70,7 +71,10 @@
       my.scripts['jquery-ui'].shift();
     }
 
+    // Build an array of promise objects for each script to load.
     scripts = $.map(my.scripts['jquery-ui'], $.getScript);
+
+    // When all promises have completed load the next set of libraries.
     $.when.apply($, scripts).then(function () {
       scripts = $.map(my.scripts['slickgrid'], $.getScript);
       scripts = scripts.concat($.map(my.scripts['flot'], $.getScript));
@@ -82,6 +86,7 @@
 
       $.when.apply($, scripts).then(function () {
         my.areDependanciesLoaded = true;
+
         var dialog = my.$dialog;
         dialog.dialog(my.dialogOptions).dialog("widget").css('position', 'fixed');
         callback();
@@ -125,16 +130,62 @@
     });
   };
 
-  my.getResourceChart = function (hash, callback) {
-    setTimeout(function () {
-      callback && callback();
-    }, 800);
+  my.getChartsFromPackage = function (resource, dataset) {
+    var resources = dataset.resources, i = 0, count = resources.length, charts;
+    for (; i < count; i += 1) {
+      if (resources[i].hash === resource.hash) {
+        resource.resourceIndex = i;
+        charts = resources[i][my.resourceChartKey];
+        if (charts) {
+          // Return the first chart.
+          try {
+            return $.parseJSON(charts);
+          } catch (e) {}
+        }
+      }
+    }
+    return null;
   };
 
-  my.updateResourceChart = function (hash, chart, callback) {
-    setTimeout(function () {
+  my.getResourceCharts = function (resource, callback) {
+    return $.getJSON(resource['dataset-uri'], function (dataset) {
+      var charts = my.getChartsFromPackage(resource, dataset);
+      resource.dataset = dataset;
+      callback && callback(charts);
+    });
+  };
+
+  my.updateResourceChart = function (resource, chart, apiKey, callback) {
+    var apiResource = resource.dataset.resources[resource.resourceIndex],
+        charts = $.parseJSON(apiResource[my.resourceChartKey] || '{}'),
+        resourceData = {};
+
+    function success() {
       callback && callback();
-    }, 800);
+    }
+
+    if (!resource) {
+      return $.Deferred().promise().done(success).resolve();
+    }
+
+    charts[chart.id] = chart;
+
+    resourceData.id = apiResource.id;
+    resourceData.url = apiResource.url;
+    resourceData[my.resourceChartKey] = JSON.stringify(charts);
+
+    return $.ajax({
+      url: resource['dataset-uri'],
+      data: JSON.stringify({resources: [resourceData]}),
+      type: 'PUT',
+      dataType: 'json',
+      processData: false,
+      contentType: 'application/json',
+      headers: {
+        'X-CKAN-API-KEY': apiKey
+      },
+      success: success
+    });
   };
 
   my.loadDataPreview = function (resource, columns, data) {
@@ -145,17 +196,35 @@
     dialog.dialog(my.dialogOptions).one("dialogopen", function () {
       var element  = $(my.template.html).appendTo(dialog);
       var viewer   = new my.createDataPreview(element, columns, data);
+      var apiKey   = $.cookie('ckan_apikey');
 
       // Load chart data from the webstore.
       viewer.editor.loading();
-      my.getResourceChart(resource.hash, function () {
+      my.getResourceCharts(resource, function (charts) {
+        // Load the first chart in the object until the editor supports
+        // loading multiple charts.
+        for (var key in charts) {
+          if (charts.hasOwnProperty(key)) {
+            viewer.editor.load(charts[key]).el.submit();
+            viewer.nav.toggle('chart');
+            viewer.chart.redraw();
+            break;
+          }
+        }
+
         viewer.editor.loading(false);
+        if (!apiKey) {
+          viewer.editor.disableSave();
+        }
+      }).error(function () {
+        // Could not contact API, disable saving.
+        viewer.editor.loading(false).disableSave();
       });
 
       // Save chart data to the webstore.
       viewer.editor.bind('save', function (chart) {
         viewer.editor.saving();
-        my.updateResourceChart(resource.hash, chart, function () {
+        my.updateResourceChart(resource, chart, apiKey, function () {
           viewer.editor.saving(false);
         });
       });
@@ -351,16 +420,18 @@
         'class': 'resource-preview-button',
         'data-source': element.find('a').attr('href'),
         'data-format': _format,
-        'data-hash': _hash
+        'data-hash': _hash,
+        'data-dataset-uri': $('.api code:first a').attr('href')
       }).appendTo(preview);
     });
   };
 
   my.initialize = function(webstoreUrl, dialogId, options) {
-    my.webstore = webstoreUrl;
     my.$dialog = $('#' + dialogId);
     options = options || {};
+
     my.timeout = options.timeout || my.timeout;
+    my.webstore = webstoreUrl;
 
     var _height = Math.round($(window).height() * 0.6);
 


--- a/public/ckanext/datapreview/data-preview.ui.js	Wed Jul 20 16:51:36 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.ui.js	Thu Jul 21 12:23:01 2011 +0100
@@ -274,6 +274,7 @@
       this.type    = this.$('.ckanext-datapreview-editor-type select');
       this.groups  = this.$('.ckanext-datapreview-editor-group select');
       this.series  = this.$('.ckanext-datapreview-editor-series select');
+      this.id      = this.$('.ckanext-datapreview-editor-id');
 
       this.$('button').button();
       this.update = this.$('.ckanext-datapreview-editor-update').click(this.onSubmit);
@@ -336,6 +337,7 @@
       this._selectOption(this.type, chart.type);
       this._selectOption(this.groups, chart.groups);
 
+      this.id.val(chart.id);
       $.each(chart.series, function update(index, option) {
         var element = editor.series.eq(index);
         if (!element.length) {
@@ -359,10 +361,14 @@
       return this;
     },
     saving: function (show) {
-      this.save.button(show === false ? 'enable' : 'disable');
+      this.disableSave(show);
       this._updateSaveText(show === false ? null : 'Saving...');
       return this;
     },
+    disableSave: function (disable) {
+      this.save.button(disable === false ? 'enable' : 'disable');
+      return this;
+    },
     onAdd: function (event) {
       event.preventDefault();
       this.addSeries();
@@ -395,6 +401,7 @@
       });
 
       this.trigger(topic, [{
+        id: this.id.val(),
         type: this.type.val(),
         groups: this.groups.val(),
         series: $.makeArray(series)


--- a/tests/ui.html	Wed Jul 20 16:51:36 2011 +0100
+++ b/tests/ui.html	Thu Jul 21 12:23:01 2011 +0100
@@ -30,10 +30,11 @@
       var dataSource = 'http://test-webstore.ckan.net/okfn/b21ae9c691445b73773156380336e2fab821cb64/resource.jsonp?_limit=30';
       dp.loadDependancies(function () {
         var resource = {
-          url: dataSource,
-          hash: 'b21ae9c691445b73773156380336e2fab821cb64',
-          format: 'csv',
-          source: 'http://test.ckan.net/package/bank-of-england-interest-rate'
+          'url': dataSource,
+          'hash': 'b21ae9c691445b73773156380336e2fab821cb64',
+          'format': 'csv',
+          'source': 'http://test.ckan.net/package/bank-of-england-interest-rate',
+          'dataset-uri': '/api/rest/package/uk-population-estimates-1520-to-1851'
         };
 
         dp.getResourceDataDirect(resource, function (url, type, data) {
@@ -41,6 +42,8 @@
           dp.$dialog.dialog('open');
         });
       });
+
+      $.cookie = function () { return 'tester'; };
     });
   </script></body>


http://bitbucket.org/okfn/ckanext-datapreview/changeset/fcb28d308b4b/
changeset:   fcb28d308b4b
user:        aron_
date:        2011-07-21 15:43:34
summary:     Fixing inconsistent code style
affected #:  1 file (21 bytes)

--- a/public/ckanext/datapreview/data-preview.js	Thu Jul 21 12:23:01 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Thu Jul 21 14:43:34 2011 +0100
@@ -437,32 +437,32 @@
 
     // Large stylable dialog for displaying data.
     my.dialogOptions = {
-      autoOpen: false
+      autoOpen: false,
       // does not seem to work for width ...
-      , position: ['center', 'center']
-      , buttons: []
-      , width:  $(window).width()  - 20
-      , height: $(window).height() - 20
-      , resize: 'auto'
-      , modal: false
-      , draggable: true
-      , resizable: true
+      position: ['center', 'center'],
+      buttons: [],
+      width:  $(window).width()  - 20,
+      height: $(window).height() - 20,
+      resize: 'auto',
+      modal: false,
+      draggable: true,
+      resizable: true
     };
 
     // Smaller alert style dialog for error messages.
     my.errorDialogOptions = {
-      title: 'Unable to Preview - Had an error from dataproxy'
-      , position: ['center', 'center']
-      , buttons: [{
-          text: "OK"
-      ,   click: function () { $(this).dialog("close"); }
-      , }]
-      , width: 360
-      , height: 180
-      , resizable: false
-      , draggable: false
-      , modal: true
-      , position: 'fixed'
+      title: 'Unable to Preview - Had an error from dataproxy',
+      position: ['center', 'center'],
+      buttons: [{
+        text: "OK",
+        click: function () { $(this).dialog("close"); }
+      }],
+      width: 360,
+      height: 180,
+      resizable: false,
+      draggable: false,
+      modal: true,
+      position: 'fixed'
     };
 
     my.createPreviewButtons($('.resources'));


http://bitbucket.org/okfn/ckanext-datapreview/changeset/e870263d864a/
changeset:   e870263d864a
user:        aron_
date:        2011-07-21 16:32:23
summary:     Added some instructional information to the chart editor
affected #:  3 files (1.3 KB)

--- a/public/ckanext/datapreview/data-preview.css	Thu Jul 21 14:43:34 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.css	Thu Jul 21 15:32:23 2011 +0100
@@ -115,6 +115,42 @@
 	border-top: 1px solid #ddd;
 }
 
+.ckanext-datapreview-editor-info {
+	border-bottom: 1px solid #ddd;
+	margin-bottom: 10px;
+}
+
+.ckanext-datapreview-editor-info h1,
+.ckanext-datapreview-editor-info p {
+	font-size: 12px;
+	margin: 0 0 10px;
+	color: #555;
+}
+
+.ckanext-datapreview-editor-info h1 {
+	line-height: 16px;
+	cursor: pointer;
+	font-size: 13px;
+	margin: 0 0 4px;
+}
+
+.ckanext-datapreview-editor-info h1 span {
+	position: relative;
+	top: 1px;
+	display: inline-block;
+	width: 12px;
+	height: 12px;
+	background: url(jquery-ui/css/ckan/images/ui-icons_444444_256x240.png) no-repeat -68px -17px;
+}
+
+.ckanext-datapreview-editor-hide-info h1 span {
+	background-position: -36px -18px;
+}
+
+.ckanext-datapreview-editor-hide-info p {
+	display: none;
+}
+
 .ckanext-datapreview-hide-editor .ckanext-datapreview-editor {
 	display: none;
 }


--- a/public/ckanext/datapreview/data-preview.html	Thu Jul 21 14:43:34 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.html	Thu Jul 21 15:32:23 2011 +0100
@@ -12,6 +12,14 @@
   <div class="ckanext-datapreview-panel ckanext-datapreview-grid"></div><div class="ckanext-datapreview-panel ckanext-datapreview-graph"></div><div class="ckanext-datapreview-editor">
+    <div class="ckanext-datapreview-editor-info ckanext-datapreview-editor-hide-info">
+      <h1><span></span>Help</h1>
+      <p>To create a chart select a column (group) to use as the x-axis
+         then another column (Series A) to plot against it.</p>
+      <p>You can add add
+         additional series by clicking the "Add series" button</p>
+      <p>Please note you must be logged in to save charts.</p>
+    </div><form><ul><li class="ckanext-datapreview-editor-type">
@@ -23,12 +31,12 @@
           <select></select></li><li class="ckanext-datapreview-editor-series">
-          <label>Series <span>A</span> (y-axis)</label>
+          <label>Series <span>A (y-axis)</span></label><select></select></li></ul><div class="ckanext-datapreview-editor-buttons">
-         <button class="ckanext-datapreview-editor-add">Add Series</button>
+        <button class="ckanext-datapreview-editor-add">Add Series</button></div><div class="ckanext-datapreview-editor-buttons ckanext-datapreview-editor-submit"><button class="ckanext-datapreview-editor-update">Update</button>


--- a/public/ckanext/datapreview/data-preview.ui.js	Thu Jul 21 14:43:34 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.ui.js	Thu Jul 21 15:32:23 2011 +0100
@@ -106,6 +106,10 @@
         'submit': this.onEditorSubmit
       });
 
+      this.$('.ckanext-datapreview-editor-info h1').click(function () {
+        $(this).parent().toggleClass('ckanext-datapreview-editor-hide-info');
+      });
+
       this.chart.hide();
     },
     redraw: function () {


http://bitbucket.org/okfn/ckanext-datapreview/changeset/7aa518937f16/
changeset:   7aa518937f16
user:        aron_
date:        2011-07-21 16:45:23
summary:     Removed the update button

Graph now automatically redraws when the inputs change.
affected #:  4 files (221 bytes)

--- a/public/ckanext/datapreview/data-preview.css	Thu Jul 21 15:32:23 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.css	Thu Jul 21 15:45:23 2011 +0100
@@ -69,6 +69,8 @@
 	width: 198px;
 	padding: 5px 10px;
 	border: 1px solid #ccc;
+	overflow: auto;
+	overflow-x: hidden;
 }
 
 .ckanext-datapreview-editor ul {
@@ -130,7 +132,9 @@
 .ckanext-datapreview-editor-info h1 {
 	line-height: 16px;
 	cursor: pointer;
+	font-family: sans-serif;
 	font-size: 13px;
+	font-weight: bold;
 	margin: 0 0 4px;
 }
 


--- a/public/ckanext/datapreview/data-preview.html	Thu Jul 21 15:32:23 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.html	Thu Jul 21 15:45:23 2011 +0100
@@ -39,7 +39,6 @@
         <button class="ckanext-datapreview-editor-add">Add Series</button></div><div class="ckanext-datapreview-editor-buttons ckanext-datapreview-editor-submit">
-        <button class="ckanext-datapreview-editor-update">Update</button><button class="ckanext-datapreview-editor-save">Save</button><input type="hidden" class="ckanext-datapreview-editor-id" value="chart-1" /></div>


--- a/public/ckanext/datapreview/data-preview.js	Thu Jul 21 15:32:23 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Thu Jul 21 15:45:23 2011 +0100
@@ -31,7 +31,7 @@
 
   my.template = {
     html: '',
-    src: '/ckanext/datapreview/data-preview.html?'
+    src: '/ckanext/datapreview/data-preview.html'
   };
 
   my.normalizeFormat = function(format) {


--- a/public/ckanext/datapreview/data-preview.ui.js	Thu Jul 21 15:32:23 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.ui.js	Thu Jul 21 15:45:23 2011 +0100
@@ -281,10 +281,10 @@
       this.id      = this.$('.ckanext-datapreview-editor-id');
 
       this.$('button').button();
-      this.update = this.$('.ckanext-datapreview-editor-update').click(this.onSubmit);
       this.save = this.$('.ckanext-datapreview-editor-save').click(this.onSave);
       this.el.bind('submit', this.onSubmit);
       this.el.delegate('a[href="#remove"]', 'click', this.onRemove);
+      this.el.delegate('select', 'change', this.onSubmit);
 
       this.$('.ckanext-datapreview-editor-add').click(this.onAdd);
 
@@ -358,7 +358,6 @@
 
       this.$('select').attr('disabled', show !== false);
       this.save.button(action);
-      this.update.button(action);
 
       this._updateSaveText(show === false ? null : 'Loading...');
 


http://bitbucket.org/okfn/ckanext-datapreview/changeset/b19dad18d82a/
changeset:   b19dad18d82a
user:        aron_
date:        2011-07-21 16:49:58
summary:     Added a divider below the group options in the editor
affected #:  1 file (144 bytes)

--- a/public/ckanext/datapreview/data-preview.css	Thu Jul 21 15:45:23 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.css	Thu Jul 21 15:49:58 2011 +0100
@@ -84,6 +84,12 @@
 	margin-bottom: 10px;
 }
 
+.ckanext-datapreview-editor .ckanext-datapreview-editor-group {
+	padding-bottom: 10px;
+	margin-bottom: 10px;
+	border-bottom: 1px solid #ddd;
+}
+
 .ckanext-datapreview-editor label {
 	display: block;
 	font-weight: bold;


http://bitbucket.org/okfn/ckanext-datapreview/changeset/c43179c3955b/
changeset:   c43179c3955b
user:        aron_
date:        2011-07-22 11:00:40
summary:     Added #444 (grey) icon set to the jQuery UI images folder
affected #:  1 file (4.3 KB)

Binary file public/ckanext/datapreview/jquery-ui/css/ckan/images/ui-icons_444444_256x240.png has changed


http://bitbucket.org/okfn/ckanext-datapreview/changeset/0cf0ba708e1e/
changeset:   0cf0ba708e1e
user:        aron_
date:        2011-07-22 11:03:52
summary:     Now updating series names when removing them from the editor
affected #:  1 file (293 bytes)

--- a/public/ckanext/datapreview/data-preview.ui.js	Fri Jul 22 10:00:40 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.ui.js	Fri Jul 22 10:03:52 2011 +0100
@@ -323,14 +323,21 @@
       this.$('ul').append(element);
       this.updateSeries();
 
-      label.append('<a href="#remove" data-index="' + index + '">Remove</a>');
+      label.append('<a href="#remove">Remove</a>');
       label.find('span').text(String.fromCharCode(this.series.length + 64));
 
       return this;
     },
-    removeSeries: function (index) {
-      this.series.eq(index).parent().remove();
-      return this.updateSeries();
+    removeSeries: function (element) {
+      element.remove();
+      this.updateSeries();
+      this.series.each(function (index) {
+        if (index > 0) {
+          var labelSpan = $(this).prev().find('span');
+          labelSpan.text(String.fromCharCode(index + 65));
+        }
+      });
+      return this.submit();
     },
     updateSeries: function () {
       this.series = this.$('.ckanext-datapreview-editor-series select');
@@ -353,6 +360,9 @@
 
       return this;
     },
+    submit: function () {
+      return this._triggerChartData('submit');
+    },
     loading: function (show) {
       var action = show === false ? 'enable' : 'disable';
 
@@ -378,7 +388,8 @@
     },
     onRemove: function (event) {
       event.preventDefault();
-      this.removeSeries(event.target.getAttribute('data-index'));
+      var element = $(event.target).parents('.ckanext-datapreview-editor-series');
+      this.removeSeries(element);
     },
     onSave: function (event) {
       event.preventDefault();
@@ -386,7 +397,7 @@
     },
     onSubmit: function (event) {
       event && event.preventDefault();
-      this._triggerChartData('submit');
+      this.submit();
     },
     _updateSaveText: function (text) {
       var span = this.save.find('span'),
@@ -403,7 +414,7 @@
         return $(this).val();
       });
 
-      this.trigger(topic, [{
+      return this.trigger(topic, [{
         id: this.id.val(),
         type: this.type.val(),
         groups: this.groups.val(),


http://bitbucket.org/okfn/ckanext-datapreview/changeset/70aab96d17ee/
changeset:   70aab96d17ee
user:        aron_
date:        2011-07-22 11:53:21
summary:     Now requesting package info via api on page load

This will allow an icon to be displayed if the current dataset has chart
data associated with it. Also now passing around a preview object with
metadata about the currently previewed resource.
affected #:  1 file (481 bytes)

--- a/public/ckanext/datapreview/data-preview.js	Fri Jul 22 10:03:52 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Fri Jul 22 10:53:21 2011 +0100
@@ -104,11 +104,11 @@
     });
   };
 
-  my.getResourceDataDirect = function(resource, callback) {
+  my.getResourceDataDirect = function(preview, callback) {
     // $.ajax() does not call the "error" callback for JSONP requests so we
     // set a timeout to provide the callback with an error after x seconds.
     var timer = setTimeout(function error() {
-      callback(resource, {
+      callback(preview, {
         error: {
           title: 'Request Error',
           message: 'Dataproxy server did not respond after ' + (my.timeout / 1000) + ' seconds'
@@ -120,44 +120,35 @@
     // a cache busting `={timestamp}` parameter to the query as the webstore
     // currently cannot handle custom parameters.
     $.ajax({
-      url: resource.url,
+      url: preview.url,
       cache: true,
       dataType: 'jsonp',
       success: function(data) {
         clearTimeout(timer);
-        callback(resource, data);
+        callback(preview, data);
       }
     });
   };
 
-  my.getChartsFromPackage = function (resource, dataset) {
+  my.getResourceFromDataset = function (hash, dataset) {
     var resources = dataset.resources, i = 0, count = resources.length, charts;
     for (; i < count; i += 1) {
-      if (resources[i].hash === resource.hash) {
-        resource.resourceIndex = i;
-        charts = resources[i][my.resourceChartKey];
-        if (charts) {
-          // Return the first chart.
-          try {
-            return $.parseJSON(charts);
-          } catch (e) {}
-        }
+      if (resources[i].hash === hash) {
+        return resources[i];
       }
     }
     return null;
   };
 
-  my.getResourceCharts = function (resource, callback) {
-    return $.getJSON(resource['dataset-uri'], function (dataset) {
-      var charts = my.getChartsFromPackage(resource, dataset);
-      resource.dataset = dataset;
-      callback && callback(charts);
+  my.getResourceDataset = function (uri, callback) {
+    return $.getJSON(uri, function (dataset) {
+      callback && callback(dataset);
     });
   };
 
-  my.updateResourceChart = function (resource, chart, apiKey, callback) {
-    var apiResource = resource.dataset.resources[resource.resourceIndex],
-        charts = $.parseJSON(apiResource[my.resourceChartKey] || '{}'),
+  my.updateResourceChart = function (preview, chart, apiKey, callback) {
+    var resource = preview.resource,
+        charts   = preview.charts || {},
         resourceData = {};
 
     function success() {
@@ -170,12 +161,12 @@
 
     charts[chart.id] = chart;
 
-    resourceData.id = apiResource.id;
-    resourceData.url = apiResource.url;
+    resourceData.id  = resource.id;
+    resourceData.url = resource.url;
     resourceData[my.resourceChartKey] = JSON.stringify(charts);
 
     return $.ajax({
-      url: resource['dataset-uri'],
+      url: preview['dataset-uri'],
       data: JSON.stringify({resources: [resourceData]}),
       type: 'PUT',
       dataType: 'json',
@@ -188,7 +179,7 @@
     });
   };
 
-  my.loadDataPreview = function (resource, columns, data) {
+  my.loadDataPreview = function (preview, columns, data) {
     var dialog = my.$dialog;
 
     // Need to create the grid once the dialog is open for cells to render
@@ -200,7 +191,9 @@
 
       // Load chart data from the webstore.
       viewer.editor.loading();
-      my.getResourceCharts(resource, function (charts) {
+      preview.datasetRequest.success(function () {
+        var charts = preview.charts;
+
         // Load the first chart in the object until the editor supports
         // loading multiple charts.
         for (var key in charts) {
@@ -224,7 +217,7 @@
       // Save chart data to the webstore.
       viewer.editor.bind('save', function (chart) {
         viewer.editor.saving();
-        my.updateResourceChart(resource, chart, apiKey, function () {
+        my.updateResourceChart(preview, chart, apiKey, function () {
           viewer.editor.saving(false);
         });
       });
@@ -238,10 +231,10 @@
     });
   };
 
-  my.setupFullscreenDialog = function (resource) {
+  my.setupFullscreenDialog = function (preview) {
     var dialog = my.$dialog, $window = $(window), timer;
 
-    dialog.empty().dialog('option', 'title', 'Preview: ' + resource.source);
+    dialog.empty().dialog('option', 'title', 'Preview: ' + preview.source);
 
     // Ensure the lightbox always fills the screen.
     $window.bind('resize.data-preview', function () {
@@ -265,8 +258,8 @@
     my.$dialog.html(_html).dialog(my.errorDialogOptions);
   };
 
-  my.showData = function(resource, data) {
-    my.setupFullscreenDialog(resource);
+  my.showData = function(preview, data) {
+    my.setupFullscreenDialog(preview);
 
     if(data.error) {
       return my.showError(data.error);
@@ -285,11 +278,11 @@
       return cells;
     });
 
-    my.loadDataPreview(resource, columns, data);
+    my.loadDataPreview(preview, columns, data);
   };
 
-  my.showPlainTextData = function(resource, data) {
-    my.setupFullscreenDialog(resource);
+  my.showPlainTextData = function(preview, data) {
+    my.setupFullscreenDialog(preview);
 
     if(data.error) {
       my.showError(data.error);
@@ -314,39 +307,41 @@
   };
 
   my.loadPreviewDialog = function(link) {
-    var resource = $(link).data();
-    resource.url = my.normalizeUrl(link.href);
-    resource.type = my.normalizeFormat(resource.format);
+    var preview  = $(link).data('preview');
+    preview.url  = my.normalizeUrl(link.href);
+    preview.type = my.normalizeFormat(preview.format);
 
     function callbackWrapper(callback) {
       return function () {
         var context = this, args = arguments;
 
-        my.loadDependancies(function () {
-          $(link).removeClass('resource-preview-loading').text('Preview');
-          callback.apply(context, args);
-          my.$dialog.dialog('open');
+        preview.datasetRequest.complete(function () {
+          my.loadDependancies(function () {
+            $(link).removeClass('resource-preview-loading').text('Preview');
+            callback.apply(context, args);
+            my.$dialog.dialog('open');
+          });
         });
       };
     }
 
     $(link).addClass('resource-preview-loading').text('Loading');
 
-    if (resource.type === '') {
-      var tmp = resource.url.split('/');
+    if (preview.type === '') {
+      var tmp = preview.url.split('/');
       tmp = tmp[tmp.length - 1];
       tmp = tmp.split('?'); // query strings
       tmp = tmp[0];
       var ext = tmp.split('.');
       if (ext.length > 1) {
-        resource.type = ext[ext.length-1];
+        preview.type = ext[ext.length-1];
       }
     }
 
-    if (resource.type in {'csv': '', 'xls': ''}) {
-      my.getResourceDataDirect(resource, callbackWrapper(my.showData));
+    if (preview.type in {'csv': '', 'xls': ''}) {
+      my.getResourceDataDirect(preview, callbackWrapper(my.showData));
     }
-    else if (resource.type in {
+    else if (preview.type in {
         'rdf+xml': '',
         'owl+xml': '',
         'xml': '',
@@ -360,11 +355,11 @@
         'txt': ''
         }) {
       // treat as plain text
-      my.getResourceDataDirect(resource, callbackWrapper(my.showPlainTextData));
+      my.getResourceDataDirect(preview, callbackWrapper(my.showPlainTextData));
     }
     else {
       // very hacky but should work
-      callbackWrapper(my.showHtml)(resource.url);
+      callbackWrapper(my.showHtml)(preview.url);
     }
   };
 
@@ -410,6 +405,18 @@
 
       // TODO: add ability to change the limit in this url
       var _url = my.webstore + "/" + _hash + "/resource.jsonp?_limit=30";
+      
+      // The API enpoint for the current package.
+      var _datasetUri = $('.api code:first a').attr('href');
+
+      // An object that holds information about the currently previewed data.
+      var _preview = {
+        'source': element.find('a').attr('href'),
+        'format': _format,
+        'hash': _hash,
+        'dataset-uri': _datasetUri
+      };
+
       var _previewSpan = $('<a />', {
         text: 'Preview',
         href: _url,
@@ -417,12 +424,27 @@
           e.preventDefault();
           my.loadPreviewDialog(e.target);
         },
-        'class': 'resource-preview-button',
-        'data-source': element.find('a').attr('href'),
-        'data-format': _format,
-        'data-hash': _hash,
-        'data-dataset-uri': $('.api code:first a').attr('href')
-      }).appendTo(preview);
+        'class': 'resource-preview-button'
+      }).data('preview', _preview).appendTo(preview);
+
+      // Request representation from the API.
+      _preview.datasetRequest = my.getResourceDataset(_datasetUri, function (dataset) {
+        var resource = my.getResourceFromDataset(_preview.hash, dataset),
+            chartString, charts = {};
+
+        if (resource) {
+          chartString = resource[my.resourceChartKey];
+          if (chartString) {
+            try {
+              charts = $.parseJSON(chartString);
+            } catch (e) {}
+          }
+        }
+
+        _preview.dataset = dataset;
+        _preview.resource = resource;
+        _preview.charts = charts;
+      });
     });
   };
 


http://bitbucket.org/okfn/ckanext-datapreview/changeset/919126156949/
changeset:   919126156949
user:        aron_
date:        2011-07-22 12:26:51
summary:     Added a chart icon to the preview button if chart data exists
affected #:  4 files (1.9 KB)

--- a/public/ckanext/datapreview/data-preview.css	Fri Jul 22 10:53:21 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.css	Fri Jul 22 11:26:51 2011 +0100
@@ -1,5 +1,3 @@
-/* @override http://localhost:8000/public/ckanext/datapreview/data-preview.css */
-
 #ckanext-datapreview-dialog table td {
 	background: #fff;
 	border: 1px solid #eee;
@@ -180,7 +178,7 @@
 	padding: 3px 6px 2px 21px;
 	font-size: 12px;
 	font-weight: bold;
-	background: #fff url(./icon-preview.png) no-repeat 5px 6px;
+	background: #fff url(./icon-sprite.png) no-repeat 5px 6px;
 	-webkit-border-radius: 4px;
 	-moz-border-radius: 4px;
 	-ms-border-radius: 4px;
@@ -195,7 +193,7 @@
 	text-decoration: none;
 	border-color: #aaa;
 	background-color: #fafafa;
-	background-position: 5px -25px;
+	background-position: 5px -24px;
 }
 
 .preview .resource-preview-button:active {
@@ -204,6 +202,18 @@
 	background-position: 5px -54px;
 }
 
+.preview .resource-preview-chart {
+	background-position: 5px -83px;
+}
+
+.preview .resource-preview-chart:hover {
+	background-position: 5px -113px;
+}
+
+.preview .resource-preview-chart:active {
+	background-position: 5px -143px;
+}
+
 .preview .resource-preview-loading,
 .preview .resource-preview-loading:hover,
 .preview .resource-preview-loading:active {


--- a/public/ckanext/datapreview/data-preview.js	Fri Jul 22 10:53:21 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Fri Jul 22 11:26:51 2011 +0100
@@ -437,6 +437,9 @@
           if (chartString) {
             try {
               charts = $.parseJSON(chartString);
+
+              // If parsing succeeds add a class to the preview button.
+              _previewSpan.addClass('resource-preview-chart');
             } catch (e) {}
           }
         }


Binary file public/ckanext/datapreview/icon-preview.png has changed


Binary file public/ckanext/datapreview/icon-sprite.png has changed


http://bitbucket.org/okfn/ckanext-datapreview/changeset/2bb8e9c1192f/
changeset:   2bb8e9c1192f
user:        aron_
date:        2011-07-22 12:50:53
summary:     Renamed my variable to dp
affected #:  2 files (119 bytes)

--- a/public/ckanext/datapreview/data-preview.js	Fri Jul 22 11:26:51 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Fri Jul 22 11:50:53 2011 +0100
@@ -1,20 +1,20 @@
-var CKANEXT = CKANEXT || {};
+(function ($) {
+  var dp = {};
 
-CKANEXT.DATAPREVIEW = (function($, my) {
   // set up in document.ready
-  my.$dialog = null;
-  my.webstore = null;
-  my.timeout = 5000;
-  my.areDependanciesLoaded = false;
-  my.resourceChartKey = 'datapreview-charts';
+  dp.$dialog = null;
+  dp.webstore = null;
+  dp.timeout = 5000;
+  dp.areDependanciesLoaded = false;
+  dp.resourceChartKey = 'datapreview-charts';
 
-  my.stylesheets = [
+  dp.stylesheets = [
     '/ckanext/datapreview/jquery-ui/css/ckan/jquery-ui-1.8.14.custom.css',
     '/ckanext/datapreview/slickgrid/slick.grid.css',
     '/ckanext/datapreview/slickgrid/slick.columnpicker.css'
   ];
 
-  my.scripts = {
+  dp.scripts = {
     'jquery-ui': [
       '/ckanext/datapreview/jquery-ui/js/jquery-ui-1.8.14.custom.min.js',
       '/ckanext/datapreview/jquery-ui/js/jquery.event.drag-2.0.min.js'
@@ -29,19 +29,19 @@
     ]
   };
 
-  my.template = {
+  dp.template = {
     html: '',
     src: '/ckanext/datapreview/data-preview.html'
   };
 
-  my.normalizeFormat = function(format) {
+  dp.normalizeFormat = function(format) {
     var out = format.toLowerCase();
     out = out.split('/');
     out = out[out.length-1];
     return out;
   };
 
-  my.normalizeUrl = function(url) {
+  dp.normalizeUrl = function(url) {
     if (url.indexOf('https') === 0) {
       return 'http' + url.slice(5);
     } else {
@@ -49,7 +49,7 @@
     }
   }
 
-  my.escapeHTML = function (string) {
+  dp.escapeHTML = function (string) {
     return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&')
                  .replace(/</g, '<').replace(/>/g, '>')
                  .replace(/"/g, '"')
@@ -58,8 +58,8 @@
   };
 
   // Loads dependant scripts and stylesheets.
-  my.loadDependancies = function (callback) {
-    if (my.areDependanciesLoaded) {
+  dp.loadDependancies = function (callback) {
+    if (dp.areDependanciesLoaded) {
       return callback();
     }
 
@@ -68,27 +68,27 @@
 
     // Don't load jQuery UI if it exists on the page.
     if (uiVersion[0] >= 1 && uiVersion[1] >= 8 && uiVersion[2] >= 14) {
-      my.scripts['jquery-ui'].shift();
+      dp.scripts['jquery-ui'].shift();
     }
 
     // Build an array of promise objects for each script to load.
-    scripts = $.map(my.scripts['jquery-ui'], $.getScript);
+    scripts = $.map(dp.scripts['jquery-ui'], $.getScript);
 
     // When all promises have completed load the next set of libraries.
     $.when.apply($, scripts).then(function () {
-      scripts = $.map(my.scripts['slickgrid'], $.getScript);
-      scripts = scripts.concat($.map(my.scripts['flot'], $.getScript));
+      scripts = $.map(dp.scripts['slickgrid'], $.getScript);
+      scripts = scripts.concat($.map(dp.scripts['flot'], $.getScript));
 
       // Load the template file from the server.
-      scripts.push($.get(my.template.src, function (html) {
-        my.template.html = html;
+      scripts.push($.get(dp.template.src, function (html) {
+        dp.template.html = html;
       }));
 
       $.when.apply($, scripts).then(function () {
-        my.areDependanciesLoaded = true;
+        dp.areDependanciesLoaded = true;
 
-        var dialog = my.$dialog;
-        dialog.dialog(my.dialogOptions).dialog("widget").css('position', 'fixed');
+        var dialog = dp.$dialog;
+        dialog.dialog(dp.dialogOptions).dialog("widget").css('position', 'fixed');
         callback();
       });
     });
@@ -96,7 +96,7 @@
     // Prepend dependant stylesheets to the page (before the plugin stylesheet
     // so we can take advantage of the cascade).
     var pluginStylesheet = $('.ckanext-datapreview-stylesheet');
-    $.each(my.stylesheets, function () {
+    $.each(dp.stylesheets, function () {
       pluginStylesheet.before($('<link />', {
         rel: 'stylesheet',
         href: this
@@ -104,17 +104,17 @@
     });
   };
 
-  my.getResourceDataDirect = function(preview, callback) {
+  dp.getResourceDataDirect = function(preview, callback) {
     // $.ajax() does not call the "error" callback for JSONP requests so we
     // set a timeout to provide the callback with an error after x seconds.
     var timer = setTimeout(function error() {
       callback(preview, {
         error: {
           title: 'Request Error',
-          message: 'Dataproxy server did not respond after ' + (my.timeout / 1000) + ' seconds'
+          message: 'Dataproxy server did not respond after ' + (dp.timeout / 1000) + ' seconds'
         }
       });
-    }, my.timeout);
+    }, dp.timeout);
 
     // We need to provide the `cache: true` parameter to prevent jQuery appending
     // a cache busting `={timestamp}` parameter to the query as the webstore
@@ -130,7 +130,7 @@
     });
   };
 
-  my.getResourceFromDataset = function (hash, dataset) {
+  dp.getResourceFromDataset = function (hash, dataset) {
     var resources = dataset.resources, i = 0, count = resources.length, charts;
     for (; i < count; i += 1) {
       if (resources[i].hash === hash) {
@@ -140,13 +140,13 @@
     return null;
   };
 
-  my.getResourceDataset = function (uri, callback) {
+  dp.getResourceDataset = function (uri, callback) {
     return $.getJSON(uri, function (dataset) {
       callback && callback(dataset);
     });
   };
 
-  my.updateResourceChart = function (preview, chart, apiKey, callback) {
+  dp.updateResourceChart = function (preview, chart, apiKey, callback) {
     var resource = preview.resource,
         charts   = preview.charts || {},
         resourceData = {};
@@ -156,14 +156,14 @@
     }
 
     if (!resource) {
-      return $.Deferred().promise().done(success).resolve();
+      return $.Deferred().done(success).resolve().promise();
     }
 
     charts[chart.id] = chart;
 
     resourceData.id  = resource.id;
     resourceData.url = resource.url;
-    resourceData[my.resourceChartKey] = JSON.stringify(charts);
+    resourceData[dp.resourceChartKey] = JSON.stringify(charts);
 
     return $.ajax({
       url: preview['dataset-uri'],
@@ -179,14 +179,14 @@
     });
   };
 
-  my.loadDataPreview = function (preview, columns, data) {
-    var dialog = my.$dialog;
+  dp.loadDataPreview = function (preview, columns, data) {
+    var dialog = dp.$dialog;
 
     // Need to create the grid once the dialog is open for cells to render
     // correctly.
-    dialog.dialog(my.dialogOptions).one("dialogopen", function () {
-      var element  = $(my.template.html).appendTo(dialog);
-      var viewer   = new my.createDataPreview(element, columns, data);
+    dialog.dialog(dp.dialogOptions).one("dialogopen", function () {
+      var element  = $(dp.template.html).appendTo(dialog);
+      var viewer   = new dp.createDataPreview(element, columns, data);
       var apiKey   = $.cookie('ckan_apikey');
 
       // Load chart data from the webstore.
@@ -217,7 +217,7 @@
       // Save chart data to the webstore.
       viewer.editor.bind('save', function (chart) {
         viewer.editor.saving();
-        my.updateResourceChart(preview, chart, apiKey, function () {
+        dp.updateResourceChart(preview, chart, apiKey, function () {
           viewer.editor.saving(false);
         });
       });
@@ -231,8 +231,8 @@
     });
   };
 
-  my.setupFullscreenDialog = function (preview) {
-    var dialog = my.$dialog, $window = $(window), timer;
+  dp.setupFullscreenDialog = function (preview) {
+    var dialog = dp.$dialog, $window = $(window), timer;
 
     dialog.empty().dialog('option', 'title', 'Preview: ' + preview.source);
 
@@ -253,16 +253,16 @@
     });
   }
 
-  my.showError = function (error) {
+  dp.showError = function (error) {
     var _html = '<strong>' + $.trim(error.title) + '</strong><br />' + $.trim(error.message);
-    my.$dialog.html(_html).dialog(my.errorDialogOptions);
+    dp.$dialog.html(_html).dialog(dp.errorDialogOptions);
   };
 
-  my.showData = function(preview, data) {
-    my.setupFullscreenDialog(preview);
+  dp.showData = function(preview, data) {
+    dp.setupFullscreenDialog(preview);
 
     if(data.error) {
-      return my.showError(data.error);
+      return dp.showError(data.error);
     }
 
     var columns = $.map(data.fields || [], function (column, i) {
@@ -278,48 +278,48 @@
       return cells;
     });
 
-    my.loadDataPreview(preview, columns, data);
+    dp.loadDataPreview(preview, columns, data);
   };
 
-  my.showPlainTextData = function(preview, data) {
-    my.setupFullscreenDialog(preview);
+  dp.showPlainTextData = function(preview, data) {
+    dp.setupFullscreenDialog(preview);
 
     if(data.error) {
-      my.showError(data.error);
+      dp.showError(data.error);
     } else {
       var content = $('<pre></pre>');
       for (var i=0; i<data.data.length; i++) {
         var row = data.data[i].join(',') + '\n';
-        content.append(my.escapeHTML(row));
+        content.append(dp.escapeHTML(row));
       }
-      my.$dialog.dialog('option', my.dialogOptions).append(content);
+      dp.$dialog.dialog('option', dp.dialogOptions).append(content);
     }
   };
 
-  my.showHtml = function(url, type) {
-    my.$dialog.empty();
-    my.$dialog.dialog('option', 'title', 'Preview: ' + url);
+  dp.showHtml = function(url, type) {
+    dp.$dialog.empty();
+    dp.$dialog.dialog('option', 'title', 'Preview: ' + url);
     var el = $('<iframe></iframe>');
     el.attr('src', url);
     el.attr('width', '100%');
     el.attr('height', '100%');
-    my.$dialog.append(el).dialog('open');;
+    dp.$dialog.append(el).dialog('open');;
   };
 
-  my.loadPreviewDialog = function(link) {
+  dp.loadPreviewDialog = function(link) {
     var preview  = $(link).data('preview');
-    preview.url  = my.normalizeUrl(link.href);
-    preview.type = my.normalizeFormat(preview.format);
+    preview.url  = dp.normalizeUrl(link.href);
+    preview.type = dp.normalizeFormat(preview.format);
 
     function callbackWrapper(callback) {
       return function () {
         var context = this, args = arguments;
 
         preview.datasetRequest.complete(function () {
-          my.loadDependancies(function () {
+          dp.loadDependancies(function () {
             $(link).removeClass('resource-preview-loading').text('Preview');
             callback.apply(context, args);
-            my.$dialog.dialog('open');
+            dp.$dialog.dialog('open');
           });
         });
       };
@@ -339,7 +339,7 @@
     }
 
     if (preview.type in {'csv': '', 'xls': ''}) {
-      my.getResourceDataDirect(preview, callbackWrapper(my.showData));
+      dp.getResourceDataDirect(preview, callbackWrapper(dp.showData));
     }
     else if (preview.type in {
         'rdf+xml': '',
@@ -355,15 +355,15 @@
         'txt': ''
         }) {
       // treat as plain text
-      my.getResourceDataDirect(preview, callbackWrapper(my.showPlainTextData));
+      dp.getResourceDataDirect(preview, callbackWrapper(dp.showPlainTextData));
     }
     else {
       // very hacky but should work
-      callbackWrapper(my.showHtml)(preview.url);
+      callbackWrapper(dp.showHtml)(preview.url);
     }
   };
 
-  my.createPreviewButtons = function(resources) {
+  dp.createPreviewButtons = function(resources) {
     resources.find('tr:first th:first').before($('<th class="preview">Preview</th>'));
     /*
        :param resources: resource section div or table.
@@ -404,7 +404,7 @@
       }
 
       // TODO: add ability to change the limit in this url
-      var _url = my.webstore + "/" + _hash + "/resource.jsonp?_limit=30";
+      var _url = dp.webstore + "/" + _hash + "/resource.jsonp?_limit=30";
       
       // The API enpoint for the current package.
       var _datasetUri = $('.api code:first a').attr('href');
@@ -422,18 +422,18 @@
         href: _url,
         click: function(e) {
           e.preventDefault();
-          my.loadPreviewDialog(e.target);
+          dp.loadPreviewDialog(e.target);
         },
         'class': 'resource-preview-button'
       }).data('preview', _preview).appendTo(preview);
 
       // Request representation from the API.
-      _preview.datasetRequest = my.getResourceDataset(_datasetUri, function (dataset) {
-        var resource = my.getResourceFromDataset(_preview.hash, dataset),
+      _preview.datasetRequest = dp.getResourceDataset(_datasetUri, function (dataset) {
+        var resource = dp.getResourceFromDataset(_preview.hash, dataset),
             chartString, charts = {};
 
         if (resource) {
-          chartString = resource[my.resourceChartKey];
+          chartString = resource[dp.resourceChartKey];
           if (chartString) {
             try {
               charts = $.parseJSON(chartString);
@@ -451,17 +451,17 @@
     });
   };
 
-  my.initialize = function(webstoreUrl, dialogId, options) {
-    my.$dialog = $('#' + dialogId);
+  dp.initialize = function(webstoreUrl, dialogId, options) {
+    dp.$dialog = $('#' + dialogId);
     options = options || {};
 
-    my.timeout = options.timeout || my.timeout;
-    my.webstore = webstoreUrl;
+    dp.timeout = options.timeout || dp.timeout;
+    dp.webstore = webstoreUrl;
 
     var _height = Math.round($(window).height() * 0.6);
 
     // Large stylable dialog for displaying data.
-    my.dialogOptions = {
+    dp.dialogOptions = {
       autoOpen: false,
       // does not seem to work for width ...
       position: ['center', 'center'],
@@ -475,7 +475,7 @@
     };
 
     // Smaller alert style dialog for error messages.
-    my.errorDialogOptions = {
+    dp.errorDialogOptions = {
       title: 'Unable to Preview - Had an error from dataproxy',
       position: ['center', 'center'],
       buttons: [{
@@ -490,8 +490,10 @@
       position: 'fixed'
     };
 
-    my.createPreviewButtons($('.resources'));
+    dp.createPreviewButtons($('.resources'));
   };
 
-  return my;
-}(jQuery, CKANEXT.DATAPREVIEW || {}));
+  $.extend(true, window, {CKANEXT: {}});
+  CKANEXT.DATAPREVIEW = dp;
+
+})(jQuery);


--- a/tests/ui.html	Fri Jul 22 11:26:51 2011 +0100
+++ b/tests/ui.html	Fri Jul 22 11:50:53 2011 +0100
@@ -17,6 +17,7 @@
 
       // Update the sources to point to the public directory.
       var sources = [dp.stylesheets, dp.scripts['jquery-ui'], dp.scripts['slickgrid'], dp.scripts['flot']];
+
       $.each(sources, function (index, array) {
         $.each(this, function (index, uri) {
           array[index] = '../public' + uri;
@@ -34,7 +35,8 @@
           'hash': 'b21ae9c691445b73773156380336e2fab821cb64',
           'format': 'csv',
           'source': 'http://test.ckan.net/package/bank-of-england-interest-rate',
-          'dataset-uri': '/api/rest/package/uk-population-estimates-1520-to-1851'
+          'dataset-uri': '/api/rest/package/uk-population-estimates-1520-to-1851',
+          'datasetRequest': dp.getResourceDataset('/api/rest/package/uk-population-estimates-1520-to-1851')
         };
 
         dp.getResourceDataDirect(resource, function (url, type, data) {


http://bitbucket.org/okfn/ckanext-datapreview/changeset/90482545d911/
changeset:   90482545d911
user:        aron_
date:        2011-07-22 16:34:04
summary:     Added very rough support for bar charts

This was more as an exercise for modularising the Flot options to allow
different graph types to be created.
affected #:  1 file (1.3 KB)

--- a/public/ckanext/datapreview/data-preview.ui.js	Fri Jul 22 11:50:53 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.ui.js	Fri Jul 22 15:34:04 2011 +0100
@@ -223,9 +223,16 @@
       this.data = data;
       this.columns = columns;
       this.chart = chart;
-      this.plot = $.plot(element, this.createSeries());
+      this.createGrid((chart && chart.type) || 'line');
       this.draw();
     },
+    createGrid: function (typeId) {
+      var type = ui.ChartView.findTypeById(typeId),
+          options = type && type.getOptions ? type.getOptions(this) : {};
+
+      this.plot = $.plot(this.el, this.createSeries(), options);
+      return this;
+    },
     createSeries: function () {
       var series = [], view = this;
       if (this.chart) {
@@ -248,6 +255,9 @@
       return this.redraw();
     },
     update: function (chart) {
+      if (!this.chart || chart.type !== this.chart.type) {
+        this.createGrid(chart.type);
+      }
       this.chart = chart;
       this.draw();
       return this;
@@ -266,6 +276,43 @@
       }
       return name;
     }
+  }, {
+    TYPES: [{
+      id: 'line',
+      name: 'Line Chart'
+    }, {
+      id: 'bar',
+      name: 'Bar Chart (draft)',
+      getOptions: function (view) {
+        return {
+          series: {
+            lines: {show: false},
+            bars: {
+              show: true,
+              barWidth: 1,
+              align: "left",
+              fill: true
+            }
+          },
+          xaxis: {
+            tickSize: 1,
+            tickLength: 1,
+            tickFormatter: function (val) {
+              if (view.data[val]) {
+                return view.data[val][view.chart.groups];
+              }
+              return '';
+            }
+          }
+        }
+      }
+    }],
+    findTypeById: function (id) {
+      var filtered = $.grep(this.TYPES, function (type) {
+        return type.id === id;
+      });
+      return filtered.length ? filtered[0] : null;
+    }
   });
 
   ui.EditorView = inherit(ui.BaseView, {
@@ -298,9 +345,11 @@
     },
     setupTypeOptions: function () {
       var types = {};
-      $.each({line: {name: 'Line Chart'}}, function (id, type) {
-        types[id] = type.name;
+      // TODO: This shouldn't be referenced directly but passed in as an option.
+      $.each(ui.ChartView.TYPES, function () {
+        types[this.id] = this.name;
       });
+
       this.type.html(this._createOptions(types));
       return this;
     },
@@ -349,6 +398,7 @@
       this._selectOption(this.groups, chart.groups);
 
       this.id.val(chart.id);
+      this.type.val(chart.type);
       $.each(chart.series, function update(index, option) {
         var element = editor.series.eq(index);
         if (!element.length) {
@@ -434,6 +484,7 @@
   });
 
   $.extend(true, this, {CKANEXT: {DATAPREVIEW: {
+    UI: ui,
     createDataPreview: function (element, columns, data) {
       return new ui.MainView(element, columns, data);
     }


http://bitbucket.org/okfn/ckanext-datapreview/changeset/9019ee8d032c/
changeset:   9019ee8d032c
user:        aron_
date:        2011-07-22 17:44:58
summary:     Added inline documentation for the UI classes
affected #:  1 file (7.1 KB)

--- a/public/ckanext/datapreview/data-preview.ui.js	Fri Jul 22 15:34:04 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.ui.js	Fri Jul 22 16:44:58 2011 +0100
@@ -2,6 +2,27 @@
 
   var ui = {};
 
+  /* Binds methods on an object to always be called with the object as the
+   * method context.
+   *
+   * context    - An object with methods to bind.
+   * arguments* - Following arguments should be method names to bind.
+   *
+   * Examples
+   *
+   *   var object = {
+   *     method1: function () {
+   *       return this;
+   *     },
+   *     method2: function () {}
+   *   };
+   *
+   *   bindAll(object, 'method1', 'method2');
+   *
+   *   object.method1.call(window) === object //=> true;
+   *
+   * Returns the context argument.
+   */
   function bindAll(context) {
     var args = [].slice.call(arguments, 0), i = 0, count = args.length;
     for (; i < count; i += 1) {
@@ -10,7 +31,27 @@
     return context;
   }
 
-  // Taken from https://github.com/aron/inheritance.js
+  /* Creates a new object that inherits from the proto argument.
+   *
+   * Source: http://github.com/aron/inheritance.js
+   *
+   * This function will use Object.create() if it exists otherwise falls back
+   * to using a dummy constructor function to create a new object instance.
+   * Unlike Object.create() this function will always return a new object even
+   * if a non object is provided as an argument.
+   *
+   * proto - An object to use for the new objects internal prototype.
+   *
+   * Examples
+   *
+   *   var appleObject = {color: 'green'}
+   *   var appleInstance = create(appleObject);
+   *
+   *   appleInstance.hasOwnProperty('color'); //=> false
+   *   appleInstance.color === appleObject.color; //=> true
+   *
+   * Returns a newly created object.
+   */
   function create(proto) {
     if (typeof proto !== 'object') {
       return {};
@@ -23,6 +64,34 @@
     return new DummyObject();
   }
 
+  /* Public: Creates a new constructor function that inherits from a parent.
+   *
+   * Source: http://github.com/aron/inheritance.js
+   *
+   * Instance and static methods can also be provided as additional arguments.
+   * if the methods argument has a property called "constructor" this will be
+   * used as the constructor function.
+   *
+   * Static methods will also be copied over from the parent object. However
+   * these will not be inheritied prototypally as with the instance methods.
+   *
+   * parent     - A constructor Function to inherit from.
+   * methods    - An Object literal of instance methods that are added to the
+   *              constructors prototype.
+   * properties - An Object literal of static/class methods to add to the
+   *              constructor itself.
+   *
+   * Examples
+   *
+   *   function MyObject() {};
+   *
+   *   var SubClass = inherit(MyObject, {method: function () {}});
+   *   var instance = new SubClass();
+   *
+   *   instance instanceof MyObject //=> true
+   *
+   * Returns the new constructor Function.
+   */
   function inherit(parent, methods, properties) {
     methods = methods || {};
 
@@ -38,10 +107,24 @@
     return $.extend(Child, parent, properties);
   }
 
-  // Implements Ben Allmans Tiny PubSub, https://gist.github.com/661855
-  ui.BaseView = inherit({}, {
-    constructor: function BaseView(element) {
-      this.el = element;
+  /* Public: Base view object that other views should inherit from.
+   *
+   * A wrapper around a dom element (itself wrapped in jQuery). Provides useful
+   * features such as pub/sub methods, and show/hide toggling of the element.
+   *
+   * Implements Ben Allman's Tiny PubSub, https://gist.github.com/661855
+   *
+   * element - A jQuery wrapper, DOM Element or selector String.
+   *
+   * Examples
+   *
+   *   var myView = new View('my-element');
+   *
+   * Returns a new View instance.
+   */
+  ui.View = inherit({}, {
+    constructor: function View(element) {
+      this.el = element instanceof $ ? element : $(element);
 
       // Use a custom empty jQuery wrapper rather than this.el to prevent
       // browser events being triggered.
@@ -85,7 +168,23 @@
     }
   });
 
-  ui.MainView = inherit(ui.BaseView, {
+  /* Public: Main view object for the data preview plugin.
+   *
+   * Contains the main interface elements and acts as a controller binding
+   * them together.
+   *
+   * element - The main DOM Element used for the plugin.
+   * columns - The columns array for the data rows formatted for SlickGrid.
+   * data    - A data object formatted for use in SlickGrid.
+   * chart   - A chart object to load.
+   *
+   * Examples
+   *
+   *   new MainView($('.datapraview-wrapper'), columns, data);
+   *
+   * Returns a new instance of MainView.
+   */
+  ui.MainView = inherit(ui.View, {
     constructor: function MainView(element, columns, data, chart) {
       this.__super__.constructor.apply(this, arguments);
 
@@ -132,7 +231,23 @@
     }
   });
 
-  ui.NavigationView = inherit(ui.BaseView, {
+  /* Public: Navigation element for switching between views.
+   *
+   * Handles the toggling of views within the plugin by firing events when
+   * buttons are clicked within the view.
+   *
+   * element - The Element to use as navigation.
+   *
+   * Examples
+   *
+   *   var nav = new NavigationView($('.ckanext-datapreview-nav'));
+   *
+   *   // Recieve events when the navigation buttons are clicked.
+   *   nav.bind('change', onNavigationChangeHandler);
+   *
+   * Returns a new instance of NavigationView.
+   */
+  ui.NavigationView = inherit(ui.View, {
     constructor: function NavigationView(element) {
       this.__super__.constructor.apply(this, arguments);
 
@@ -157,7 +272,23 @@
     }
   });
 
-  ui.GridView = inherit(ui.BaseView, {
+  /* Public: Creates and manages a SlickGrid instance for displaying the
+   * resource data in a useful grid.
+   *
+   * SlickGrid documentation: http://github.com/mleibman/SlickGrid/wiki
+   *
+   * element - The Element to use as a container for the SlickGrid.
+   * columns - Column options formatted for use in the SlickGrid container.
+   * data    - Data Object formatted for use in the SlickGrid.
+   * options - Additional instance and SlickGrid options.
+   *
+   * Examples
+   *
+   *   var grid = new GridView($('.ckanext-datapreview-grid'), columns, data);
+   *
+   * Returns a new instance of GridView.
+   */
+  ui.GridView = inherit(ui.View, {
     constructor: function GridView(element, columns, data, options) {
       this.__super__.constructor.apply(this, arguments);
 
@@ -166,12 +297,12 @@
       this.dirty = false;
       this.columns = columns;
       this.data = data;
-      this.grid = new Slick.Grid(element, data, columns, {
+      this.grid = new Slick.Grid(element, data, columns, $.extend({
         enableColumnReorder: false,
         forceFitColumns: true,
         syncColumnCellResize: true,
         enableCellRangeSelection: false
-      });
+      }, options));
 
       this.grid.onSort = this._onSort;
 
@@ -217,7 +348,31 @@
     }
   });
 
-  ui.ChartView = inherit(ui.BaseView, {
+  /* Public: Creates a wrapper around a jQuery.Flot() chart.
+   *
+   * Currently a very basic implementation that accepts data prepared for the
+   * SlickGrid, ie columns and data objects and uses them to generate a canvas
+   * chart.
+   *
+   * Flot documentation: http://people.iola.dk/olau/flot/API.txt
+   *
+   * element - Element to use as a container for the Flot canvas.
+   * columns - Array of column data.
+   * data    - Data Object.
+   * chart   - Optional chart data to load.
+   *
+   * Examples
+   *
+   *   new ChartView($('.ckanext-datapreview-chart'), columns, data, {
+   *     id: 'my-chart-id',  // Any unique id for the chart used for storage.
+   *     type: 'line',       // ID of one of the ChartView.TYPES.
+   *     groups: 'column-2', // The column to use as the x-axis.
+   *     series: ['column-3', 'column-4'] // Columns to use as the series.
+   *   });
+   *
+   * Returns a new instance of ChartView.
+   */
+  ui.ChartView = inherit(ui.View, {
     constructor: function ChartView(element, columns, data, chart) {
       this.__super__.constructor.apply(this, arguments);
       this.data = data;
@@ -277,6 +432,14 @@
       return name;
     }
   }, {
+    /* Array of chart formatters. They require an id and name attribute and
+     * and optional getOptions() method. Used to generate different chart types.
+     *
+     * id         - A unique id String for the chart type.
+     * name       - A human readable name for the type.
+     * getOptions - Function that accepts an instance of ChartView and returns
+     *              an options object suitable for use in $.plot();
+     */
     TYPES: [{
       id: 'line',
       name: 'Line Chart'
@@ -304,9 +467,19 @@
               return '';
             }
           }
-        }
+        };
       }
     }],
+    /* Public: Helper method for findind a chart type by id key.
+     *
+     * id - The id key to search for in the ChartView.TYPES Array.
+     *
+     * Examples
+     *
+     *   var type = ChartView.findTypeById('line');
+     *
+     * Returns the type object or null if not found.
+     */
     findTypeById: function (id) {
       var filtered = $.grep(this.TYPES, function (type) {
         return type.id === id;
@@ -315,7 +488,27 @@
     }
   });
 
-  ui.EditorView = inherit(ui.BaseView, {
+  /* Public: Creates a form for editing chart metadata.
+   *
+   * Publishes "submit" and "save" events providing a chart Obejct to all
+   * registered callbacks.
+   *
+   * element - The Element to use as the form wrapper.
+   * columns - Array of columns that are used by the data set.
+   * chart   - Optional chart Object to display on load.
+   *
+   * Examples
+   *
+   *   new EditorView($('.ckanext-datapreview-editor'), columns, {
+   *     id: 'my-chart-id',
+   *     type: 'line',
+   *     groups: 'column-2',
+   *     series: ['column-3', 'column-4']
+   *   });
+   *
+   * Returns a new instance of EditorView.
+   */
+  ui.EditorView = inherit(ui.View, {
     constructor: function EditorView(element, columns, chart) {
       this.__super__.constructor.apply(this, arguments);
 
@@ -483,10 +676,26 @@
     }
   });
 
+  // Exports the UI and createDataPreview() methods onto the plugin object.
   $.extend(true, this, {CKANEXT: {DATAPREVIEW: {
+
     UI: ui,
-    createDataPreview: function (element, columns, data) {
-      return new ui.MainView(element, columns, data);
+
+    /* Public: Helper method for creating a new view.
+     *
+     * element - The main DOM Element used for the plugin.
+     * columns - The columns array for the data rows formatted for SlickGrid.
+     * data    - A data object formatted for use in SlickGrid.
+     * chart   - An optional chart object to load.
+     *
+     * Examples
+     *
+     *   DATAPREVIEW.createDataPreview($('my-view'), columns, data);
+     *
+     * Returns a new instance of MainView.
+     */
+    createDataPreview: function (element, columns, data, chart) {
+      return new ui.MainView(element, columns, data, chart);
     }
   }}});
 


http://bitbucket.org/okfn/ckanext-datapreview/changeset/94359fb14744/
changeset:   94359fb14744
user:        aron_
date:        2011-07-22 18:28:05
summary:     Further inline documentation of UI methods
affected #:  1 file (4.1 KB)

--- a/public/ckanext/datapreview/data-preview.ui.js	Fri Jul 22 16:44:58 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.ui.js	Fri Jul 22 17:28:05 2011 +0100
@@ -130,9 +130,39 @@
       // browser events being triggered.
       this.events = $({});
     },
+
+    /* Public: Performs a jQuery lookup within the views element.
+     *
+     * selector - A selector String to query.
+     *
+     * Examples
+     *
+     *   this.$('.some-child-class');
+     *
+     * Returns a jQuery collection.
+     */
     $: function (selector) {
       return this.el.find(selector);
     },
+
+    /* Public: Registers a listener for a topic that will be called when the
+     * event is triggered. Optionally an Object of topic/callback pairs can
+     * be passed to the method. Built on top of the jQuery .bind() method
+     * so other features like namespaces can also be used.
+     *
+     * topic - Topic string to subscribe to.
+     * fn    - Callback function to be called when the topic is triggered.
+     *
+     * Examples
+     *
+     *   view.bind('my-event', onMyEvent);
+     *   view.bind({
+     *     'my-event', onMyEvent,
+     *     'my-other-events': onMyOtherEvent
+     *   });
+     *
+     * Returns itself for chaining.
+     */
     bind: function (topic, fn) {
       if (arguments.length === 1) {
         for (var key in topic) {
@@ -150,18 +180,54 @@
       this.events.bind(topic, wrapper);
       return this;
     },
+
+    /* Public: Unbinds a callback for a topic.
+     *
+     * Accepts the same arguments as jQuery's .unbind().
+     *
+     * topic - The topic to unbind.
+     * fn    - A specific function to unbind from the topic.
+     *
+     * Examples
+     *
+     *   view.unbind('my-event');
+     *
+     * Returns itself for chaining.
+     */
     unbind: function () {
       this.events.unbind.apply(this.events, arguments);
       return this;
     },
+
+    /* Public: Triggers a topic providing an array of arguments to all listeners.
+     *
+     * topic - A topic to publish.
+     * args  - An Array of arguments to pass into registered listeners.
+     *
+     * Examples
+     *
+     *   view.trigger('my-event', [anArg, anotherArg]);
+     *
+     * Returns itself.
+     */
     trigger: function () {
       this.events.triggerHandler.apply(this.events, arguments);
       return this;
     },
+
+    /* Public: Shows the element if hidden.
+     *
+     * Returns itself.
+     */
     show: function () {
       this.el.show();
       return this.trigger('show');
     },
+
+    /* Public: Hides the element if shown.
+     *
+     * Returns itself.
+     */
     hide: function () {
       this.el.hide();
       return this.trigger('hide');
@@ -211,20 +277,56 @@
 
       this.chart.hide();
     },
+
+    /* Public: Redraws the both the grid and chart views.
+     *
+     * Useful if the viewport changes or is resized.
+     *
+     * Examples
+     *
+     *   view.resize();
+     *
+     * Returns itself.
+     */
     redraw: function () {
       this.chart.redraw();
       this.grid.redraw();
       return this;
     },
+
+    /* Public: Toggles the display of the grid and chart views.
+     *
+     * Used as a callback function for the NavigationView "change" event.
+     *
+     * selected - The name of the newly selected view.
+     *
+     * Returns nothing.
+     */
     onNavChange: function (selected) {
       var isGrid = selected === 'grid';
       this.grid[isGrid ? 'show' : 'hide']();
       this.chart[isGrid ? 'hide' : 'show']();
     },
+
+    /* Public: Toggles the display of the editor panel.
+     *
+     * Used as a callback function for the NavigationView "toggle-editor" event.
+     *
+     * showEditor - True if the editor should be visible.
+     *
+     * Returns nothing.
+     */
     onNavToggleEditor: function (showEditor) {
       this.el.toggleClass('ckanext-datapreview-hide-editor', !showEditor);
       this.redraw();
     },
+
+    /* Public: Updates the chart view when the editor is submitted.
+     *
+     * chart - The chart object to render.
+     *
+     * Returns nothing.
+     */
     onEditorSubmit: function (chart) {
       this.nav.toggle('chart');
       this.chart.update(chart);
@@ -259,14 +361,44 @@
       this.editorButton = this.$('#ckanext-datapreview-nav-editor').button();
       this.editorButton.change(this.onEditorToggleChange);
     },
+
+    /* Public: Toggles a navigation button.
+     *
+     * Triggers the "change" event with the panel name provided.
+     *
+     * panel - The name of a button to be selected.
+     *
+     * Examples
+     *
+     *   nav.toggle("grid");
+     *
+     * Returns itself.
+     */
     toggle: function (panel) {
       // Need to fire all these events just to get jQuery UI to change state.
       this.$('input[value="' + panel + '"]').click().change().next().click();
       return this;
     },
+
+    /* Public: Triggers the "change" event when the navgation changes.
+     *
+     * Passes the name of the selected item into all callbacks.
+     *
+     * event - An event object.
+     *
+     * Returns nothing
+     */
     onPanelToggleChange: function (event) {
       this.trigger('change', [event.target.value]);
     },
+
+    /* Public: Triggers the "toggle-editor" event when the editor button is
+     * clicked. Passes true into callbacks if the button is active.
+     *
+     * event - An event object.
+     *
+     * Returns nothing
+     */
     onEditorToggleChange: function (event) {
       this.trigger('toggle-editor', [event.target.checked]);
     }
@@ -318,6 +450,17 @@
 
       new Slick.Controls.ColumnPicker(this.columns, this.grid);
     },
+
+    /* Public: Reveals the view.
+     *
+     * If the dirty property is true then it will redraw the grid.
+     *
+     * Examples
+     *
+     *   grid.show();
+     *
+     * Returns itself.
+     */
     show: function () {
       this.__super__.show.apply(this, arguments);
       if (this.dirty) {
@@ -326,6 +469,19 @@
       }
       return this;
     },
+
+    /* Public: Redraws the grid.
+     *
+     * The grid will only be drawn if the element is visible. If hidden the
+     * dirty property will be set to true and the grid redrawn the next time
+     * the view is shown.
+     *
+     * Examples
+     *
+     *   grid.redraw();
+     *
+     * Returns itself.
+     */
     redraw: function () {
       if (this.el.is(':visible')) {
         this.grid.resizeCanvas();
@@ -334,6 +490,17 @@
         this.dirty = true;
       }
     },
+
+    /* Public: Sort callback for the SlickGrid grid.
+     *
+     * Called when the grids columns are re-ordered. Accepts the selected
+     * column and the direction and should sort the data property.
+     *
+     * column  - The column object being sorted.
+     * sortAsc - True if the solumn should be sorted by ascending items.
+     *
+     * Returns nothing.
+     */
     _onSort: function (column, sortAsc) {
       this.data.sort(function (a, b) {
         var x = a[column.field],


http://bitbucket.org/okfn/ckanext-datapreview/changeset/7c8e9b87c674/
changeset:   7c8e9b87c674
user:        aron_
date:        2011-07-22 18:42:31
summary:     More inline documentation of the UI methods
affected #:  1 file (2.3 KB)

--- a/public/ckanext/datapreview/data-preview.ui.js	Fri Jul 22 17:28:05 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.ui.js	Fri Jul 22 17:42:31 2011 +0100
@@ -703,6 +703,11 @@
         this.load(chart);
       }
     },
+
+    /* Public: Fills the "type" select box with options.
+     *
+     * Returns itself.
+     */
     setupTypeOptions: function () {
       var types = {};
       // TODO: This shouldn't be referenced directly but passed in as an option.
@@ -713,6 +718,11 @@
       this.type.html(this._createOptions(types));
       return this;
     },
+
+    /* Public: Fills the groups and series select elements with options.
+     *
+     * Returns nothing.
+     */
     setupColumnOptions: function () {
       var options = {}, optionsString = '';
       $.each(this.columns, function (index, column) {
@@ -724,6 +734,18 @@
       this.series.html(optionsString);
       return this;
     },
+
+    /* Public: Adds a new empty series select box to the editor.
+     *
+     * All but the first select box will have a remove button that allows them
+     * to be removed.
+     *
+     * Examples
+     *
+     *   editor.addSeries();
+     *
+     * Returns itself.
+     */
     addSeries: function () {
       var element = this.seriesClone.clone(),
           label   = element.find('label'),
@@ -737,6 +759,20 @@
 
       return this;
     },
+
+    /* Public: Removes a series list item from the editor.
+     *
+     * Also updates the labels of the remianing series elements.
+     *
+     * element - A jQuery wrapped list item Element to remove.
+     *
+     * Examples
+     *
+     *   // Remove the third series element.
+     *   editor.removeSeries(editor.series.eq(2).parent());
+     *
+     * Returns itself.
+     */
     removeSeries: function (element) {
       element.remove();
       this.updateSeries();
@@ -748,10 +784,28 @@
       });
       return this.submit();
     },
+
+    /* Public: Resets the series property to reference the select elements.
+     *
+     * Returns itself.
+     */
     updateSeries: function () {
       this.series = this.$('.ckanext-datapreview-editor-series select');
       return this;
     },
+
+    /* Public: Loads a chart into the editor.
+     *
+     * For an example of the chart object structure see the ChartView docs.
+     *
+     * chart - A chart Object to be loaded.
+     *
+     * Examples
+     *
+     *   editor.load(chart);
+     *
+     * Returns itself.
+     */
     load: function (chart) {
       var editor = this;
       this._selectOption(this.type, chart.type);
@@ -770,9 +824,38 @@
 
       return this;
     },
+
+    /* Public: Submits the current form.
+     *
+     * Triggers the "submit" event providing a chart object to all listeners.
+     *
+     * Examples
+     *
+     *   editor.bind("submit", chart.update);
+     *   editor.submit();
+     *
+     * Returns itself.
+     */
     submit: function () {
       return this._triggerChartData('submit');
     },
+
+    /* Public: Toggles the loading state on the view.
+     *
+     * Freezes all interface elements and displays a loading message.
+     *
+     * show - If false disables the loading state.
+     *
+     * Examples
+     *
+     *   // Set the state to loading.
+     *   editor.loading();
+     *
+     *   // Disable the loading state.
+     *   editor.loading(false);
+     *
+     * Returns itself.
+     */
     loading: function (show) {
       var action = show === false ? 'enable' : 'disable';
 
@@ -783,11 +866,33 @@
 
       return this;
     },
+
+    /* Public: Toggles the saving state on the view.
+     *
+     * show - If false disables the saving state.
+     *
+     * Examples
+     *
+     *   // Set the state to saving.
+     *   editor.saving();
+     *
+     *   // Disable the saving state.
+     *   editor.saving(false);
+     *
+     * Returns itself.
+     */
     saving: function (show) {
       this.disableSave(show);
       this._updateSaveText(show === false ? null : 'Saving...');
       return this;
     },
+
+    /* Public: Toggles the save button state between enabled/disabled.
+     *
+     * disable - If false enables the button.
+     *
+     * Returns itself.
+     */
     disableSave: function (disable) {
       this.save.button(disable === false ? 'enable' : 'disable');
       return this;


http://bitbucket.org/okfn/ckanext-datapreview/changeset/a0d3dc5915c9/
changeset:   a0d3dc5915c9
user:        aron_
date:        2011-07-24 11:47:14
summary:     Completed the documentation of the UI methods
affected #:  1 file (4.2 KB)

--- a/public/ckanext/datapreview/data-preview.ui.js	Fri Jul 22 17:42:31 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.ui.js	Sun Jul 24 10:47:14 2011 +0100
@@ -1,3 +1,16 @@
+/* This file contains all of the UI elements aside from the containing element
+ * (ie. lightbox) used to build the datapreview widget. The MainView should
+ * be initiated with an element containing the elements required by the
+ * subviews.
+ *
+ * Use DATAPREVIEW.createDataPreview() to create new instances of MainView.
+ *
+ * Examples
+ *
+ *   var $element = $(templateString);
+ *   var datapreview = DATAPREVIEW.createDataPreview($element);
+ *
+ */
 (function ($, undefined) {
 
   var ui = {};
@@ -545,16 +558,37 @@
       this.data = data;
       this.columns = columns;
       this.chart = chart;
-      this.createGrid((chart && chart.type) || 'line');
+      this.createPlot((chart && chart.type) || 'line');
       this.draw();
     },
-    createGrid: function (typeId) {
+
+    /* Public: Creates a new Flot chart and assigns it to the plot property.
+     *
+     * typeId - The id String of the grid to create used to load chart
+     *          specific options on creation.
+     *
+     * Examples
+     *
+     *   chart.createPlot('line');
+     *
+     * Returns itself.
+     */
+    createPlot: function (typeId) {
       var type = ui.ChartView.findTypeById(typeId),
           options = type && type.getOptions ? type.getOptions(this) : {};
 
       this.plot = $.plot(this.el, this.createSeries(), options);
       return this;
     },
+
+    /* Public: Creates the series/data Array required by jQuery.plot()
+     *
+     * Examples
+     *
+     *   $.plot(editor.el, editor.createSeries(), options);
+     *
+     * Returns an Array containing points for each series in the chart.
+     */
     createSeries: function () {
       var series = [], view = this;
       if (this.chart) {
@@ -572,24 +606,68 @@
       }
       return series;
     },
+
+    /* Public: Redraws the chart with regenerated series data.
+     *
+     * Usually .update() will be called instead.
+     *
+     * Returns itself.
+     */
     draw: function () {
       this.plot.setData(this.createSeries());
       return this.redraw();
     },
+
+    /* Public: Updates the current plot with a new chart Object.
+     *
+     * chart - A chart Object usually provided by the EditorView.
+     *
+     * Examples
+     *
+     *   editor.bind('submit', function (chart) {
+     *     chart.update(chart);
+     *   });
+     *
+     * Returns itself.
+     */
     update: function (chart) {
       if (!this.chart || chart.type !== this.chart.type) {
-        this.createGrid(chart.type);
+        this.createPlot(chart.type);
       }
       this.chart = chart;
       this.draw();
       return this;
     },
+
+    /* Public: Redraws the current chart in the canvas.
+     *
+     * Used if the chart data has changed or the viewport has been resized.
+     *
+     * Examples
+     *
+     *   $(window).resize(function () {
+     *     chart.redraw();
+     *   });
+     *
+     * Returns itself.
+     */
     redraw: function () {
       this.plot.resize();
       this.plot.setupGrid();
       this.plot.draw();
       return this;
     },
+
+    /* Public: Gets the human readable column name for the field id.
+     *
+     * field - A field id String used in the data object.
+     *
+     * Examples
+     *
+     *   chart._getColumnName('column-1');
+     *
+     * Returns the String column name.
+     */
     _getColumnName: function (field) {
       for (var i = 0, count = this.columns.length; i < count; i += 1) {
         if (this.columns[i].field === field) {
@@ -637,6 +715,7 @@
         };
       }
     }],
+
     /* Public: Helper method for findind a chart type by id key.
      *
      * id - The id key to search for in the ChartView.TYPES Array.
@@ -897,23 +976,83 @@
       this.save.button(disable === false ? 'enable' : 'disable');
       return this;
     },
+
+    /* Public: Event callback for the "Add series" button.
+     *
+     * event - A jQuery Event object.
+     *
+     * Examples
+     *
+     *   $('button').click(event.onAdd);
+     *
+     * Returns nothing.
+     */
     onAdd: function (event) {
       event.preventDefault();
       this.addSeries();
     },
+
+    /* Public: Event callback for the "Remove series" button.
+     *
+     * event - A jQuery Event object.
+     *
+     * Examples
+     *
+     *   $('button').click(event.onRemove);
+     *
+     * Returns nothing.
+     */
     onRemove: function (event) {
       event.preventDefault();
       var element = $(event.target).parents('.ckanext-datapreview-editor-series');
       this.removeSeries(element);
     },
+
+    /* Public: Event callback for the "Save" button.
+     *
+     * Triggers the "save" event passing a chart object to all registered
+     * callbacks.
+     *
+     * event - A jQuery Event object.
+     *
+     * Examples
+     *
+     *   $('button.save').click(editor.onSave);
+     *
+     * Returns nothing.
+     */
     onSave: function (event) {
       event.preventDefault();
       this._triggerChartData('save');
     },
+
+    /* Public: Event callback for the editor form.
+     *
+     * event - A jQuery Event object.
+     *
+     * Examples
+     *
+     *   $('form.editor').submit(editor.onSubmit);
+     *
+     * Returns nothing.
+     */
     onSubmit: function (event) {
       event && event.preventDefault();
       this.submit();
     },
+
+    /* Updates the text on the save button.
+     *
+     * If no text is provided reverts to the original button text.
+     *
+     * text - A text String to use in the button.
+     *
+     * Examples
+     *
+     *   editor._updateSaveText('Now saving!');
+     *
+     * Returns nothing.
+     */
     _updateSaveText: function (text) {
       var span = this.save.find('span'),
           original = span.data('default');
@@ -924,6 +1063,20 @@
 
       span.text(text || original);
     },
+
+    /* Triggers an event on the editor and passes a chart object to callbacks.
+     *
+     * topic - Topic String for the event to fire.
+     *
+     * Examples
+     *
+     *   editor.bind('save', function (chart) {
+     *     // DO something with the chart.
+     *   });
+     *   editor._triggerChartData('save');
+     *
+     * Returns
+     */
     _triggerChartData: function (topic) {
       var series = this.series.map(function () {
         return $(this).val();
@@ -936,9 +1089,36 @@
         series: $.makeArray(series)
       }]);
     },
+
+    /* Finds an option by "value" in a select element and makes it selected.
+     *
+     * select - A jQuery wrapped select Element.
+     * option - The String value of the options "value" attribute.
+     *
+     * Examples
+     *
+     *   // For <select><option value="bill">Bill</option></select>
+     *   editor._selectOption(mySelect, 'bill');
+     *
+     * Returns nothing.
+     */
     _selectOption: function (select, option) {
       select.find('[value="' + option + '"]').attr('selected', 'selected');
     },
+
+    /* Creates a String of option elements.
+     *
+     * options - An object of value/text pairs.
+     *
+     * Examples
+     *
+     *   var html = editor._createOptions({
+     *     value1: 'Value 1',
+     *     value2: 'Value 2'
+     *   });
+     *
+     * Returns a String of HTML.
+     */
     _createOptions: function (options) {
       var html = [];
       $.each(options, function (value, text) {


http://bitbucket.org/okfn/ckanext-datapreview/changeset/1e80ce74d0ca/
changeset:   1e80ce74d0ca
user:        aron_
date:        2011-07-24 12:23:39
summary:     Documented the data preview.js file
affected #:  1 file (5.3 KB)

--- a/public/ckanext/datapreview/data-preview.js	Sun Jul 24 10:47:14 2011 +0100
+++ b/public/ckanext/datapreview/data-preview.js	Sun Jul 24 11:23:39 2011 +0100
@@ -1,19 +1,27 @@
 (function ($) {
   var dp = {};
 
-  // set up in document.ready
+  // Set up in DOM ready.
   dp.$dialog = null;
   dp.webstore = null;
+
+  // Time to wait for a JSONP request to timeout.
   dp.timeout = 5000;
+
+  // True when plugin dependancies have been loaded.
   dp.areDependanciesLoaded = false;
+
+  // Key to use when saving the charts onto a resource.
   dp.resourceChartKey = 'datapreview-charts';
 
+  // An array of stylsheets to be loaded into the head.
   dp.stylesheets = [
     '/ckanext/datapreview/jquery-ui/css/ckan/jquery-ui-1.8.14.custom.css',
     '/ckanext/datapreview/slickgrid/slick.grid.css',
     '/ckanext/datapreview/slickgrid/slick.columnpicker.css'
   ];
 
+  // Scripts to be loaded when required namspaced by plugin.
   dp.scripts = {
     'jquery-ui': [
       '/ckanext/datapreview/jquery-ui/js/jquery-ui-1.8.14.custom.min.js',
@@ -29,6 +37,7 @@
     ]
   };
 
+  // Template url. The html property is populated on load.
   dp.template = {
     html: '',
     src: '/ckanext/datapreview/data-preview.html'
@@ -49,6 +58,13 @@
     }
   }
 
+  /* Public: Escapes HTML entities to prevent broken layout and XSS attacks
+   * when inserting user generated or external content.
+   *
+   * string - A String of HTML.
+   *
+   * Returns a String with HTML special characters converted to entities.
+   */
   dp.escapeHTML = function (string) {
     return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&')
                  .replace(/</g, '<').replace(/>/g, '>')
@@ -57,7 +73,16 @@
                  .replace(/\//g,'&#x2F;');
   };
 
-  // Loads dependant scripts and stylesheets.
+  /* Public: Loads the dependancies required by the plugin.
+   *
+   * This allows the page to load quicly with only a minimal bootstrap
+   * to set up the UI. Then the rest of the script, stylesheets and templates
+   * are loaded when the user intiates the plugin.
+   *
+   * callback - A callback to fire once all dependancies are ready.
+   *
+   * Returns nothing.
+   */
   dp.loadDependancies = function (callback) {
     if (dp.areDependanciesLoaded) {
       return callback();
@@ -104,6 +129,14 @@
     });
   };
 
+  /* Public: Requests the formatted resource data from the webstore and
+   * passes the data into the callback provided.
+   *
+   * preview - A preview object containing resource metadata.
+   * callback - A Function to call with the data when loaded.
+   *
+   * Returns nothing.
+   */
   dp.getResourceDataDirect = function(preview, callback) {
     // $.ajax() does not call the "error" callback for JSONP requests so we
     // set a timeout to provide the callback with an error after x seconds.
@@ -130,6 +163,14 @@
     });
   };
 
+  /* Public: Searches a dataset object returned by the CKAN api for a specific
+   * resource using the hash as identification.
+   *
+   * hash    - A hash String to search for.
+   * dataset - A package dataset object.
+   *
+   * Returns the resource object or null if not found.
+   */
   dp.getResourceFromDataset = function (hash, dataset) {
     var resources = dataset.resources, i = 0, count = resources.length, charts;
     for (; i < count; i += 1) {
@@ -140,12 +181,54 @@
     return null;
   };
 
+  /* Public: Requests a dataset from th CKAN api.
+   *
+   * This method returns a jQuery jqXHR object onto which other additonal
+   * callbacks can be added to handle error requests etc.
+   *
+   * uri      - The uri of the package/dataset on the server.
+   * callback - A Function to call with the dataset on completion.
+   *
+   * Examples
+   *
+   *   var uri = '/api/rest/package/uk-population-estimates-1520-to-1851';
+   *   var request = dp.getResourceDataset(uri, function (dataset) {
+   *     // Do something with the dataset.
+   *   });
+   *
+   *   // Additional callbacks can be added to the returned jqXHR object.
+   *   request.error(onError);
+   *
+   * Returns a jqXHR object for the request.
+   */
   dp.getResourceDataset = function (uri, callback) {
     return $.getJSON(uri, function (dataset) {
       callback && callback(dataset);
     });
   };
 
+  /* Public: Updates a chart on a package resource using the CKAN API.
+   *
+   * Charts are currently stored on an object on the resource namespaced by
+   * id key. This enables each resource to store multiple charts.
+   *
+   * This method returns the jqXHR object onto which additonal callbacks
+   * can be bound.
+   *
+   * preview  - A preview object containing resource data.
+   * chart    - The current chart object to save.
+   * apiKey   - The current logged in users api key String.
+   * callback - A callback Function to fire when the request succeeds.
+   *
+   * Examples
+   *
+   *   datapreview.editor.bind('save', function (chart) {
+   *     var request = dp.updateResourceChart(preview, chart, 'Some-String');
+   *     request.then(onSuccess, onError);
+   *   });
+   *
+   * Returns a jqXHR object.
+   */
   dp.updateResourceChart = function (preview, chart, apiKey, callback) {
     var resource = preview.resource,
         charts   = preview.charts || {},
@@ -179,6 +262,14 @@
     });
   };
 
+  /* Public: Loads the plugin UI into the dialog and sets up event listeners.
+   *
+   * preview - A preview object containing resource data.
+   * columns - Column Array formatted for use in SlickGrid.
+   * data    - A data Array for use in SlickGrid.
+   *
+   * Returns nothing.
+   */
   dp.loadDataPreview = function (preview, columns, data) {
     var dialog = dp.$dialog;
 
@@ -231,6 +322,12 @@
     });
   };
 
+  /* Public: Sets up the dialog for displaying a full screen of data.
+   *
+   * preview - A preview object containing resource data.
+   *
+   * Returns nothing.
+   */
   dp.setupFullscreenDialog = function (preview) {
     var dialog = dp.$dialog, $window = $(window), timer;
 
@@ -253,11 +350,27 @@
     });
   }
 
+  /* Public: Displays a smaller alert style dialog with an error message.
+   *
+   * error - An error object to display.
+   *
+   * Returns nothing.
+   */
   dp.showError = function (error) {
     var _html = '<strong>' + $.trim(error.title) + '</strong><br />' + $.trim(error.message);
     dp.$dialog.html(_html).dialog(dp.errorDialogOptions);
   };
 
+  /* Public: Displays the datapreview UI in a fullscreen dialog.
+   *
+   * This method also parses the data returned by the webstore for use in
+   * the data preview UI.
+   *
+   * preview - A preview object containing resource data.
+   * data    - An object of parsed CSV data returned by the webstore.
+   *
+   * Returns nothing.
+   */
   dp.showData = function(preview, data) {
     dp.setupFullscreenDialog(preview);
 
@@ -281,6 +394,13 @@
     dp.loadDataPreview(preview, columns, data);
   };
 
+  /* Public: Displays a String of data in a fullscreen dialog.
+   *
+   * preview - A preview object containing resource data.
+   * data    - An object of parsed CSV data returned by the webstore.
+   *
+   * Returns nothing.
+   */
   dp.showPlainTextData = function(preview, data) {
     dp.setupFullscreenDialog(preview);
 
@@ -296,7 +416,13 @@
     }
   };
 
-  dp.showHtml = function(url, type) {
+  /* Public: Displays a fullscreen dialog with the url in an iframe.
+   *
+   * url - The URL to load into an iframe.
+   *
+   * Returns nothing.
+   */
+  dp.showHtml = function(url) {
     dp.$dialog.empty();
     dp.$dialog.dialog('option', 'title', 'Preview: ' + url);
     var el = $('<iframe></iframe>');
@@ -306,6 +432,16 @@
     dp.$dialog.append(el).dialog('open');;
   };
 
+  /* Public: Loads a data preview dialog for a preview button.
+   *
+   * Fetches the preview data object from the link provided and loads the
+   * parsed data from the webstore displaying it in the most appropriate
+   * manner.
+   *
+   * link - An anchor Element.
+   *
+   * Returns nothing.
+   */
   dp.loadPreviewDialog = function(link) {
     var preview  = $(link).data('preview');
     preview.url  = dp.normalizeUrl(link.href);
@@ -363,6 +499,16 @@
     }
   };
 
+  /* Public: Creates the base UI for the plugin.
+   *
+   * Adds an additonal preview column to the resources table in the CKAN
+   * UI. Also requests the package from the api to see if there is any chart
+   * data stored and updates the preview icons accordingly.
+   *
+   * resources - The resources table wrapped in jQuery.
+   *
+   * Returns nothing.
+   */
   dp.createPreviewButtons = function(resources) {
     resources.find('tr:first th:first').before($('<th class="preview">Preview</th>'));
     /*
@@ -405,7 +551,7 @@
 
       // TODO: add ability to change the limit in this url
       var _url = dp.webstore + "/" + _hash + "/resource.jsonp?_limit=30";
-      
+
       // The API enpoint for the current package.
       var _datasetUri = $('.api code:first a').attr('href');
 
@@ -451,6 +597,20 @@
     });
   };
 
+  /* Public: Kickstarts the plugin.
+   *
+   * webstoreUrl - URL string for the webstore to use.
+   * dialogId    - The id of the dialog Element in the page.
+   * options     - An object containing aditional options.
+   *               timeout: Time in seconds to wait for a JSONP timeout.
+   *
+   * Examples
+   *
+   *   var url = 'http://test-webstore.ckan.net/okfn';
+   *   dp.initialize(url, '#dialog', {timeout: 3000});
+   *
+   * Returns nothing.
+   */
   dp.initialize = function(webstoreUrl, dialogId, options) {
     dp.$dialog = $('#' + dialogId);
     options = options || {};
@@ -493,6 +653,7 @@
     dp.createPreviewButtons($('.resources'));
   };
 
+  // Export the CKANEXT object onto the window.
   $.extend(true, window, {CKANEXT: {}});
   CKANEXT.DATAPREVIEW = dp;
 


http://bitbucket.org/okfn/ckanext-datapreview/changeset/1158ba0520e0/
changeset:   1158ba0520e0
user:        aron_
date:        2011-07-24 12:45:30
summary:     Added TODO.txt and updated the README.txt file
affected #:  2 files (1.8 KB)

--- a/README.txt	Sun Jul 24 11:23:39 2011 +0100
+++ b/README.txt	Sun Jul 24 11:45:30 2011 +0100
@@ -1,2 +1,31 @@
-See extension docstring in ckanext/dataapi/__init__.py.
+CKAN Extension Data Preview
+===========================
 
+Installation
+------------
+
+See extension docstring in ckanext/datapreview/__init__.py.
+
+Client Side Development
+-----------------------
+
+The front end code can be found in public/ckanext/datapreview. The codebase
+utilises the following libraries.
+
+ - [jQuery][#jquery]: As the base framework.
+ - [jQuery UI][#ui]: For building the user interface.
+ - [Flot][#flot]: For generating charts.
+ - [SlickGrid][#slick]: For creating data grid.
+
+General utilities and plugin initialization can be found in data-preview.js,
+this contains the bootstrap, utility and api methods.
+
+The data-preview.ui.js file contains all the user interface objects, these are
+designed to mimic (a very simplified version) of the View objects in
+[Backbone][#backbone].
+
+[#jquery]: http://jquery.com/
+[#ui]: http://jqueryui.com/
+[#flot]: http://code.google.com/p/flot/
+[#slick]: http://github.com/mleibman/SlickGrid/wiki
+[#backbone]: http://documentcloud.github.com/backbone/


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/TODO.txt	Sun Jul 24 11:45:30 2011 +0100
@@ -0,0 +1,19 @@
+Improvements
+
+ - Allow users to select columns using the data grid rather than select boxes.
+ - Refactor datapreview.js into smaller objects.
+ - Allow customisation of series names and colours.
+ - Remove the plain text and iframe methods as they're not currently being
+   used. Or implement them to work with the new webstore logic, currently
+   only resources with a hash have preview buttons.
+
+New Features
+
+- Filtering of data grid to display graphs on a subset of data.
+- Add additional chart types, may require refactoring the current way types
+  are implemented to allow more complex construction of Flot options.
+- Incorporate more Flot options into the editor UI.
+- Allow multiple views to be loaded/viewed.
+- Full screen UI, remove the jQuery UI dialog and make the preview go
+  fullscreen.
+- Create a split view to allow viewing of grid and chart at once.


http://bitbucket.org/okfn/ckanext-datapreview/changeset/0c5d3859ef1c/
changeset:   0c5d3859ef1c
branch:      graph
user:        John Glover
date:        2011-07-28 16:45:34
summary:     Closing graph branch, not used
affected #:  0 files (0 bytes)

Repository URL: https://bitbucket.org/okfn/ckanext-datapreview/

--

This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.




More information about the ckan-changes mailing list