Commits

Chris Leonello committed 460d5f9

Created date axis renderer plugin with very flexible date inputs.
Added capability to pass in custom tick values for linear and data axes.

  • Participants
  • Parent commits 7001cd9

Comments (0)

Files changed (4)

File jqplot.categoryAxisRenderer.js

         var pos1, pos2;
         var tt, i;
 
-        //////////////////////////
-        //////////////////////////
-        // fix me
-        //////////////////////////
         // if we already have ticks, use them.
-        // ticks must be in order of increasing value.
         if (ticks.length) {
-            // for (i=0; i<ticks.length; i++){
-            //     var t = ticks[i];
-            //     if (!t.label) t.label = t.value.toString();
-            //     // set iitial css positioning styles for the ticks.
-            //     var pox = i*15+'px';
-            //     switch (name) {
-            //         case 'xaxis':
-            //             t._styles = {position:'absolute', top:'0px', left:pox, paddingTop:'10px'};
-            //             break;
-            //         case 'x2axis':
-            //             t._styles = {position:'absolute', bottom:'0px', left:pox, paddingBottom:'10px'};
-            //             break;
-            //         case 'yaxis':
-            //             t._styles = {position:'absolute', right:'0px', top:pox, paddingRight:'10px'};
-            //             break;
-            //         case 'y2axis':
-            //             t._styles = {position:'absolute', left:'0px', top:pox, paddingLeft:'10px'};
-            //             break;
-            //     }
-            // }
-            // axis.numberTicks = ticks.length;
-            // axis.min = ticks[0].value;
-            // axis.max = ticks[axis.numberTicks-1].value;
-            // axis.tickInterval = (axis.max - axis.min) / (axis.numberTicks - 1);
+            var newticks = [];
+            this.min = 0.5;
+            this.max = ticks.length + 0.5;
+            var range = this.max - this.min;
+            this.numberTicks = 2*ticks.length + 1;
+            for (i=0; i<ticks.length; i++){
+                tt = this.min + i * range / (this.numberTicks-1);
+                // need a marker before and after the tick
+                var t = new this.tickRenderer(this.tickOptions);
+                t.showLabel = false;
+                t.showMark = true;
+                t.showGridline = true;
+                t.setTick(tt, this.name);
+                newticks.push(t);
+                var t = new this.tickRenderer(this.tickOptions);
+                t.showLabel = true;
+                t.showMark = false;
+                t.showGridline = false;
+                t.
+
+            }
         }
 
         // we don't have any ticks yet, let's make some!
             var max;
             for (var i=0; i<this._series.length; i++) {
                 var s = this._series[i];
-                log(s);
                 for (var j=0; j<s.data.length; j++) {
-                    var val = String(s.data[j][0]);
-                    log(val);
+                    var val = s.data[j][0];
                     if ($.inArray(val, labels) == -1) {
                         numcats += 1;      
                         labels.push(val);
             var range = max - min;
             this.min = min;
             this.max = max;
-            var maxVisibleTicks = parseInt(3+dim/50);
+            var track = 0;
+            var maxVisibleTicks = parseInt(3+dim/45);
+            log(numcats);
+            log(maxVisibleTicks);
+            var skip = parseInt(numcats/maxVisibleTicks);
+            log(skip);
 
 
             this.tickInterval = range / (this.numberTicks-1);
                     t.showGridline = true;
                 }
                 else {
-                    t.showLabel = true;
-                    t.label = labels[(i-1)/2];
+                    if (skip>0 && track<skip) {
+                        t.showLabel = false;
+                        track += 1;
+                    }
+                    else {
+                        t.showLabel = true;
+                        track = 0;
+                    } 
+                    t.label = t.formatter(t.formatString, labels[(i-1)/2]);
                     t.showMark = false;
                     t.showGridline = false;
                 }

File jqplot.logAxisRenderer.js

         for (var d in this.renderer.seriesDefaults) {
             if (this[d] == null) this[d] = this.renderer.seriesDefaults[d];
         }
+        var db = this._dataBounds;
+        // Go through all the series attached to this axis and find
+        // the min/max bounds for this axis.
+        for (var i=0; i<this._series.length; i++) {
+            var s = this._series[i];
+            var d = s.data;
+            
+            for (var j=0; j<d.length; j++) { 
+                if (this.name == 'xaxis' || this.name == 'x2axis') {
+                    if (d[j][0] < db.min || db.min == null) db.min = d[j][0];
+                    if (d[j][0] > db.max || db.max == null) db.max = d[j][0];
+                }              
+                else {
+                    if (d[j][1] < db.min || db.min == null) db.min = d[j][1];
+                    if (d[j][1] > db.max || db.max == null) db.max = d[j][1];
+                }              
+            }
+        }
     };
 
     $.jqplot.LogAxisRenderer.prototype.draw = function() {

File jquery.dateAxisRenderer.js

+(function($) {  
+    var debug = 1;
+        
+	// Convienence function that won't hang IE.
+	function log() {
+	    if (window.console && debug) {
+	       if (arguments.length == 1) console.log (arguments[0]);
+	       else console.log(arguments);
+	    }
+	};
+    
+    $.jqplot.DateAxisRenderer = function() {
+        $.jqplot.LinearAxisRenderer.call(this);
+    };
+    
+    $.jqplot.DateAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
+    $.jqplot.DateAxisRenderer.prototype.constructor = $.jqplot.DateAxisRenderer;
+    
+    $.jqplot.DateTickFormatter = function(format, val) {
+        if (!format) format = Date.ISO;
+        return Date.create(val).strftime(format);
+    }
+    
+    $.jqplot.DateAxisRenderer.prototype.init = function(options){
+        this.tickOptions.formatter = $.jqplot.DateTickFormatter;
+        $.extend(true, this, options);
+        var db = this._dataBounds;
+        // Go through all the series attached to this axis and find
+        // the min/max bounds for this axis.
+        for (var i=0; i<this._series.length; i++) {
+            var s = this._series[i];
+            var d = s.data;
+            
+            for (var j=0; j<d.length; j++) { 
+                if (this.name == 'xaxis' || this.name == 'x2axis') {
+                    d[j][0] = Date.create(d[j][0]).getTime();
+                    if (d[j][0] < db.min || db.min == null) db.min = d[j][0];
+                    if (d[j][0] > db.max || db.max == null) db.max = d[j][0];
+                }              
+                else {
+                    d[j][1] = Date.create(d[j][1]).getTime();
+                    if (d[j][1] < db.min || db.min == null) db.min = d[j][1];
+                    if (d[j][1] > db.max || db.max == null) db.max = d[j][1];
+                }              
+            }
+        }
+    };
+    
+    $.jqplot.DateAxisRenderer.prototype.createTicks = function() {
+        // we're are operating on an axis here
+        var ticks = this._ticks;
+        var userTicks = this.ticks;
+        var name = this.name;
+        // databounds were set on axis initialization.
+        var db = this._dataBounds;
+        var dim, interval;
+        var min, max;
+        var pos1, pos2;
+        var tt, i;
+        
+        // if we already have ticks, use them.
+        // ticks must be in order of increasing value.
+        
+        if (userTicks.length) {
+            // ticks could be 1D or 2D array of [val, val, ,,,] or [[val, label], [val, label], ...] or mixed
+            for (i=0; i<userTicks.length; i++){
+                var ut = userTicks[i];
+                var t = new this.tickRenderer(this.tickOptions);
+                if (ut.constructor == Array) {
+                    t.value = Date.create(ut[0]).getTime();
+                    t.label = ut[1];
+                    if (!this.showTicks) {
+                        t.showLabel = false;
+                        t.showMark = false;
+                    }
+                    else if (!this.showTickMarks) t.showMark = false;
+                    t.setTick(ut[0], this.name);
+                    this._ticks.push(t);
+                }
+                
+                else {
+                    t.value = Date.create(ut).getTime();
+                    if (!this.showTicks) {
+                        t.showLabel = false;
+                        t.showMark = false;
+                    }
+                    else if (!this.showTickMarks) t.showMark = false;
+                    t.setTick(t.value, this.name);
+                    this._ticks.push(t);
+                }
+            }
+            this.numberTicks = userTicks.length;
+            this.min = this._ticks[0].value;
+            this.max = this._ticks[this.numberTicks-1].value;
+            this.tickInterval = (this.max - this.min) / (this.numberTicks - 1);
+        }
+        
+        // we don't have any ticks yet, let's make some!
+        else {
+            if (name == 'xaxis' || name == 'x2axis') {
+                dim = this._plotDimensions.width;
+            }
+            else {
+                dim = this._plotDimensions.height;
+            }
+        
+            min = ((this.min != null) ? this.min : db.min);
+            max = ((this.max != null) ? this.max : db.max);
+
+            var range = max - min;
+            var rmin, rmax;
+        
+            rmin = (this.min != null) ? this.min : min - range/2*(this.pad - 1);
+            rmax = (this.max != null) ? this.max : max + range/2*(this.pad - 1);
+            this.min = rmin;
+            this.max = rmax;
+            range = this.max - this.min;
+    
+            if (this.numberTicks == null){
+                if (dim > 100) {
+                    this.numberTicks = parseInt(3+(dim-100)/75);
+                }
+                else this.numberTicks = 2;
+            }
+    
+            this.tickInterval = range / (this.numberTicks-1);
+            for (var i=0; i<this.numberTicks; i++){
+                tt = this.min + i * range / (this.numberTicks-1);
+                var t = new this.tickRenderer(this.tickOptions);
+                // var t = new $.jqplot.AxisTickRenderer(this.tickOptions);
+                if (!this.showTicks) {
+                    t.showLabel = false;
+                    t.showMark = false;
+                }
+                else if (!this.showTickMarks) t.showMark = false;
+                t.setTick(tt, this.name);
+                this._ticks.push(t);
+            }
+        }
+    };
+    
+    
+    
+    /**
+     * Date instance methods
+     *
+     * @author Ken Snyder (ken d snyder at gmail dot com)
+     * @date 2008-09-10
+     * @version 2.0.2 (http://kendsnyder.com/sandbox/date/)
+     * @license Creative Commons Attribution License 3.0 (http://creativecommons.org/licenses/by/3.0/)
+     *
+     * @contributions Chris Leonello
+     * @comment Bug fix to 12 hour time and additions to handle milliseconds and 
+     * @comment 24 hour time without am/pm suffix
+     *
+     */
+ 
+    // begin by creating a scope for utility variables
+    
+    //
+    // pre-calculate the number of milliseconds in a day
+    //  
+    
+    var day = 24 * 60 * 60 * 1000;
+    //
+    // function to add leading zeros
+    //
+    var zeroPad = function(number, digits) {
+        number = String(number);
+        while (number.length < digits) number = '0' + number;
+        return number;
+    };
+    //
+    // set up integers and functions for adding to a date or subtracting two dates
+    //
+    var multipliers = {
+        millisecond: 1,
+        second: 1000,
+        minute: 60 * 1000,
+        hour: 60 * 60 * 1000,
+        day: day,
+        week: 7 * day,
+        month: {
+            // add a number of months
+            add: function(d, number) {
+                // add any years needed (increments of 12)
+                multipliers.year.add(d, Math[number > 0 ? 'floor' : 'ceil'](number / 12));
+                // ensure that we properly wrap betwen December and January
+                var prevMonth = d.getMonth() + (number % 12);
+                if (prevMonth == 12) {
+                    prevMonth = 0;
+                    d.setYear(d.getFullYear() + 1);
+                } else if (prevMonth == -1) {
+                    prevMonth = 11;
+                    d.setYear(d.getFullYear() - 1);
+                }
+                d.setMonth(prevMonth);
+            },
+            // get the number of months between two Date objects (decimal to the nearest day)
+            diff: function(d1, d2) {
+                // get the number of years
+                var diffYears = d1.getFullYear() - d2.getFullYear();
+                // get the number of remaining months
+                var diffMonths = d1.getMonth() - d2.getMonth() + (diffYears * 12);
+                // get the number of remaining days
+                var diffDays = d1.getDate() - d2.getDate();
+                // return the month difference with the days difference as a decimal
+                return diffMonths + (diffDays / 30);
+            }
+        },
+        year: {
+            // add a number of years
+            add: function(d, number) {
+                d.setYear(d.getFullYear() + Math[number > 0 ? 'floor' : 'ceil'](number));
+            },
+            // get the number of years between two Date objects (decimal to the nearest day)
+            diff: function(d1, d2) {
+                return multipliers.month.diff(d1, d2) / 12;
+            }
+        }        
+    };
+    //
+    // alias each multiplier with an 's' to allow 'year' and 'years' for example
+    //
+    for (var unit in multipliers) {
+        if (unit.substring(unit.length - 1) != 's') { // IE will iterate newly added properties :|
+            multipliers[unit + 's'] = multipliers[unit];
+        }
+    }
+    //
+    // take a date instance and a format code and return the formatted value
+    //
+    var format = function(d, code) {
+            if (Date.prototype.strftime.formatShortcuts[code]) {
+                    // process any shortcuts recursively
+                    return d.strftime(Date.prototype.strftime.formatShortcuts[code]);
+            } else {
+                    // get the format code function and toPaddedString() argument
+                    var getter = (Date.prototype.strftime.formatCodes[code] || '').split('.');
+                    var nbr = d['get' + getter[0]] ? d['get' + getter[0]]() : '';
+                    // run toPaddedString() if specified
+                    if (getter[1]) nbr = zeroPad(nbr, getter[1]);
+                    // prepend the leading character
+                    return nbr;
+            }       
+    };
+    //
+    // Add methods to Date instances
+    //
+    var instanceMethods = {
+        //
+        // Return a date one day ahead (or any other unit)
+        //
+        // @param string unit
+        // units: year | month | day | week | hour | minute | second | millisecond
+        // @return object Date
+        //
+        succ: function(unit) {
+            return this.clone().add(1, unit);
+        },
+        //
+        // Add an arbitrary amount to the currently stored date
+        //
+        // @param integer/float number      
+        // @param string unit
+        // @return object Date (chainable)      
+        //
+        add: function(number, unit) {
+            var factor = multipliers[unit] || multipliers.day;
+            if (typeof factor == 'number') {
+                this.setTime(this.getTime() + (factor * number));
+            } else {
+                factor.add(this, number);
+            }
+            return this;
+        },
+        //
+        // Find the difference between the current and another date
+        //
+        // @param string/object dateObj
+        // @param string unit
+        // @param boolean allowDecimal
+        // @return integer/float
+        //
+        diff: function(dateObj, unit, allowDecimal) {
+            // ensure we have a Date object
+            dateObj = Date.create(dateObj);
+            if (dateObj === null) return null;
+            // get the multiplying factor integer or factor function
+            var factor = multipliers[unit] || multipliers.day;
+            if (typeof factor == 'number') {
+                // multiply
+                var unitDiff = (this.getTime() - dateObj.getTime()) / factor;
+            } else {
+                // run function
+                var unitDiff = factor.diff(this, dateObj);
+            }
+            // if decimals are not allowed, round toward zero
+            return (allowDecimal ? unitDiff : Math[unitDiff > 0 ? 'floor' : 'ceil'](unitDiff));          
+        },
+        //
+        // Convert a date to a string using traditional strftime format codes
+        //
+        // @param string formatStr
+        // @return string
+        //
+        strftime: function(formatStr) {
+            // default the format string to year-month-day
+            var source = formatStr || '%Y-%m-%d', result = '', match;
+            // replace each format code
+            while (source.length > 0) {
+                if (match = source.match(Date.prototype.strftime.formatCodes.matcher)) {
+                    result += source.slice(0, match.index);
+                    result += (match[1] || '') + format(this, match[2]);
+                    source = source.slice(match.index + match[0].length);
+                } else {
+                    result += source, source = '';
+                }
+            }
+            return result;
+        },
+        //
+        // Return a proper two-digit year integer
+        //
+        // @return integer
+        //
+        getShortYear: function() {
+            return this.getYear() % 100;
+        },
+        //
+        // Get the number of the current month, 1-12
+        //
+        // @return integer
+        //
+        getMonthNumber: function() {
+            return this.getMonth() + 1;
+        },
+        //
+        // Get the name of the current month
+        //
+        // @return string
+        //
+        getMonthName: function() {
+            return Date.MONTHNAMES[this.getMonth()];
+        },
+        //
+        // Get the abbreviated name of the current month
+        //
+        // @return string
+        //
+        getAbbrMonthName: function() {
+            return Date.ABBR_MONTHNAMES[this.getMonth()];
+        },
+        //
+        // Get the name of the current week day
+        //
+        // @return string
+        //      
+        getDayName: function() {
+            return Date.DAYNAMES[this.getDay()];
+        },
+        //
+        // Get the abbreviated name of the current week day
+        //
+        // @return string
+        //      
+        getAbbrDayName: function() {
+            return Date.ABBR_DAYNAMES[this.getDay()];
+        },
+        //
+        // Get the ordinal string associated with the day of the month (i.e. st, nd, rd, th)
+        //
+        // @return string
+        //      
+        getDayOrdinal: function() {
+            return Date.ORDINALNAMES[this.getDate() % 10];
+        },
+        //
+        // Get the current hour on a 12-hour scheme
+        //
+        // @return integer
+        //
+        getHours12: function() {
+            var hours = this.getHours();
+            return hours > 12 ? hours - 12 : (hours == 0 ? 12 : hours);
+        },
+        //
+        // Get the AM or PM for the current time
+        //
+        // @return string
+        //
+        getAmPm: function() {
+            return this.getHours() >= 12 ? 'PM' : 'AM';
+        },
+        //
+        // Get the current date as a Unix timestamp
+        //
+        // @return integer
+        //
+        getUnix: function() {
+            return Math.round(this.getTime() / 1000, 0);
+        },
+        //
+        // Get the GMT offset in hours and minutes (e.g. +06:30)
+        //
+        // @return string
+        //
+        getGmtOffset: function() {
+            // divide the minutes offset by 60
+            var hours = this.getTimezoneOffset() / 60;
+            // decide if we are ahead of or behind GMT
+            var prefix = hours < 0 ? '+' : '-';
+            // remove the negative sign if any
+            hours = Math.abs(hours);
+            // add the +/- to the padded number of hours to : to the padded minutes
+            return prefix + zeroPad(Math.floor(hours), 2) + ':' + zeroPad((hours % 1) * 60, 2);
+        },
+        //
+        // Get the browser-reported name for the current timezone (e.g. MDT, Mountain Daylight Time)
+        //
+        // @return string
+        //
+        getTimezoneName: function() {
+            var match = /(?:\((.+)\)$| ([A-Z]{3}) )/.exec(this.toString());
+            return match[1] || match[2] || 'GMT' + this.getGmtOffset();
+        },
+        //
+        // Convert the current date to an 8-digit integer (%Y%m%d)
+        //
+        // @return int
+        //
+        toYmdInt: function() {
+            return (this.getFullYear() * 10000) + (this.getMonthNumber() * 100) + this.getDate();
+        },  
+        //
+        // Create a copy of a date object
+        //
+        // @return object
+        //       
+        clone: function() {
+                return new Date(this.getTime());
+        }
+    };
+    for (var name in instanceMethods) Date.prototype[name] = instanceMethods[name];
+    //
+    // Add static methods to the date object
+    //
+    var staticMethods = {
+        //
+        // The heart of the date functionality: returns a date object if given a convertable value
+        //
+        // @param string/object/integer date
+        // @return object Date
+        //
+        create: function(date) {
+            // If the passed value is already a date object, return it
+            if (date instanceof Date) return date;
+            // If the passed value is an integer, interpret it as a unix timestamp
+            // if (typeof date == 'number') return new Date(date * 1000);
+            // If the passed value is an integer, interpret it as a javascript timestamp
+            if (typeof date == 'number') return new Date(date);
+            // If the passed value is a string, attempt to parse it using Date.parse()
+            var parsable = String(date).replace(/^\s*(.+)\s*$/, '$1'), i = 0, length = Date.create.patterns.length, pattern;
+            var current = parsable;
+            while (i < length) {
+                ms = Date.parse(current);
+                if (!isNaN(ms)) return new Date(ms);
+                pattern = Date.create.patterns[i];
+                if (typeof pattern == 'function') {
+                    obj = pattern(current);
+                    if (obj instanceof Date) return obj;
+                } else {
+                    current = parsable.replace(pattern[0], pattern[1]);
+                }
+                i++;
+            }
+            return NaN;
+        },
+        //
+        // constants representing month names, day names, and ordinal names
+        // (same names as Ruby Date constants)
+        //
+        MONTHNAMES          : 'January February March April May June July August September October November December'.split(' '),
+        ABBR_MONTHNAMES : 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' '),
+        DAYNAMES                : 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday'.split(' '),
+        ABBR_DAYNAMES       : 'Sun Mon Tue Wed Thu Fri Sat'.split(' '),
+        ORDINALNAMES        : 'th st nd rd th th th th th th'.split(' '),
+        //
+        // Shortcut for full ISO-8601 date conversion
+        //
+        ISO: '%Y-%m-%dT%H:%M:%S.%N%G',
+        //
+        // Shortcut for SQL-type formatting
+        //
+        SQL: '%Y-%m-%d %H:%M:%S',
+        //
+        // Setter method for month, day, and ordinal names for i18n
+        //
+        // @param object newNames
+        //
+        daysInMonth: function(year, month) {
+            if (month == 2) return new Date(year, 1, 29).getDate() == 29 ? 29 : 28;
+            return [undefined,31,undefined,31,30,31,30,31,31,30,31,30,31][month];
+        }
+    };
+    for (var name in staticMethods) Date[name] = staticMethods[name];
+    //
+    // format codes for strftime
+    //
+    // each code must be an array where the first member is the name of a Date.prototype function
+    // and optionally a second member indicating the number to pass to Number#toPaddedString()
+    //
+    Date.prototype.strftime.formatCodes = {
+        //
+        // 2-part regex matcher for format codes
+        //
+        // first match must be the character before the code (to account for escaping)
+        // second match must be the format code character(s)
+        //
+        matcher: /()%(#?(%|[a-z]))/i,
+        // year
+        Y: 'FullYear',
+        y: 'ShortYear.2',
+        // month
+        m: 'MonthNumber.2',
+        '#m': 'MonthNumber',
+        B: 'MonthName',
+        b: 'AbbrMonthName',
+        // day
+        d: 'Date.2',
+        '#d': 'Date',
+        e: 'Date',
+        A: 'DayName',
+        a: 'AbbrDayName',
+        w: 'Day',
+        o: 'DayOrdinal',
+        // hours
+        H: 'Hours.2',
+        '#H': 'Hours',
+        I: 'Hours12.2',
+        '#I': 'Hours12',
+        p: 'AmPm',
+        // minutes
+        M: 'Minutes.2',
+        '#M': 'Minutes',
+        // seconds
+        S: 'Seconds.2',
+        '#S': 'Seconds',
+        s: 'Unix',
+        // milliseconds
+        N: 'Milliseconds.3',
+        '#N': 'Milliseconds',
+        // timezone
+        O: 'TimezoneOffset',
+        Z: 'TimezoneName',
+        G: 'GmtOffset'  
+    };
+    //
+    // shortcuts that will be translated into their longer version
+    //
+    // be sure that format shortcuts do not refer to themselves: this will cause an infinite loop
+    //
+    Date.prototype.strftime.formatShortcuts = {
+        // date
+        F: '%Y-%m-%d',
+        // time
+        T: '%H:%M:%S',
+        X: '%H:%M:%S',
+        // local format date
+        x: '%m/%d/%y',
+        D: '%m/%d/%y',
+        // local format extended
+        '#c': '%a %b %e %H:%M:%S %Y',
+        // local format short
+        v: '%e-%b-%Y',
+        R: '%H:%M',
+        r: '%I:%M:%S %p',
+        // tab and newline
+        t: '\t',
+        n: '\n',
+        '%': '%'
+    };
+    //
+    // A list of conversion patterns (array arguments sent directly to gsub)
+    // Add, remove or splice a patterns to customize date parsing ability
+    //
+    Date.create.patterns = [
+        [/-/g, '/'], // US-style time with dashes => Parsable US-style time
+        [/st|nd|rd|th/g, ''], // remove st, nd, rd and th        
+        [/(3[01]|[0-2]\d)\s*\.\s*(1[0-2]|0\d)\s*\.\s*([1-9]\d{3})/, '$2/$1/$3'], // World time => Parsable US-style time
+        [/([1-9]\d{3})\s*-\s*(1[0-2]|0\d)\s*-\s*(3[01]|[0-2]\d)/, '$2/$3/$1'], // ISO-style time => Parsable US-style time
+        function(str) { // 12-hour or 24 hour time with milliseconds
+            // var match = str.match(/^(?:(.+)\s+)?([1-9]|1[012])(?:\s*\:\s*(\d\d))?(?:\s*\:\s*(\d\d))?\s*(am|pm)\s*$/i);
+            var match = str.match(/^(?:(.+)\s+)?([012]?\d)(?:\s*\:\s*(\d\d))?(?:\s*\:\s*(\d\d(\.\d*)?))?\s*(am|pm)?\s*$/i);
+            //                   opt. date      hour       opt. minute     opt. second       opt. msec   opt. am or pm
+            if (match) {
+                if (match[1]) {
+                    var d = Date.create(match[1]);
+                    if (isNaN(d)) return;
+                } else {
+                    var d = new Date();
+                    d.setMilliseconds(0);
+                }
+                var hour = parseFloat(match[2]);
+                if (match[6]) {
+                    hour = match[6].toLowerCase() == 'am' ? (hour == 12 ? 0 : hour) : (hour == 12 ? 12 : hour + 12);
+                }
+                d.setHours(hour, parseInt(match[3] || 0), parseInt(match[4] || 0), ((parseFloat(match[5] || 0)) || 0)*1000);
+                return d;
+            }
+            else {
+                return str;
+            }
+        },
+        function(str) { // ISO timestamp with time zone.
+            var match = str.match(/^(?:(.+))[T|\s+]([012]\d)(?:\:(\d\d))(?:\:(\d\d))(?:\.\d+)([\+\-]\d\d\:\d\d)$/i);
+            if (match) {
+                if (match[1]) {
+                    var d = Date.create(match[1]);
+                    if (isNaN(d)) return;
+                } else {
+                    var d = new Date();
+                    d.setMilliseconds(0);
+                }
+                var hour = parseFloat(match[2]);
+                d.setHours(hour, parseInt(match[3]), parseInt(match[4]), parseFloat(match[5])*1000);
+                return d;
+            }
+            else {
+                    return str;
+            }
+        }               
+    ];
+    
+    if (debug) $.date = Date.create;
+   
+})(jQuery);
+

File jquery.jqplot.js

         // Padding to extend the range above and below the data bounds.
         // Factor to multiply by the data range when setting the axis bounds
         this.pad = 1.2;
+        // prop: ticks
+        // 1D or 2D array of [val, val, ,,,] or [[val, label], [val, label], ...]
+        this.ticks = [];
         // prop: numberTicks
         // Desired number of ticks.  Computed automatically by default
         this.numberTicks;
         this.tickRenderer = $.jqplot.AxisTickRenderer;
         // prop: tickOptions
         // Options that will be passed to the tickRenderer.
-        // 
-        // Properties:
-        // mark - tick markings.  One of 'inside', 'outside', 'cross', '' or null.
-        //     The latter 2 options will hide the tick marks.
-        // markSize - length of the tick marks in pixels.  For 'cross' style, length
-        //     will be stoked above and below axis, so total length will be twice this.
-        // size - uhhhh, hmmm.
-        // formatter - a class of a formatter which formats the value of the tick for display.
-        // showLabel - Wether to show labels or not.
-        // formatString - formatting string passed to the tick formatter.
-        // fontFamily - css font-family spec.
-        // fontSize -css font-size spec.
-        // textColor - css color spec.
-        this.tickOptions = {mark:'outside', markSize:4, size:4, showLabel:true, formatter:$.jqplot.sprintf, formatString:'%.1f', fontFamily:'', fontSize:'0.75em', textColor:''};
+        this.tickOptions = {};
         // prop: showTicks
         // wether to show the ticks (marks and labels) or not.
         this.showTicks = true;
     Axis.prototype.constructor = Axis;
     
     Axis.prototype.init = function() {
-        var db = this._dataBounds;
-        // Go through all the series attached to this axis and find
-        // the min/max bounds for this axis.
-        for (var i=0; i<this._series.length; i++) {
-            var s = this._series[i];
-            var d = s.data;
-            
-            for (var j=0; j<d.length; j++) { 
-                if (this.name == 'xaxis' || this.name == 'x2axis') {
-                    if (d[j][0] < db.min || db.min == null) db.min = d[j][0];
-                    if (d[j][0] > db.max || db.max == null) db.max = d[j][0];
-                }              
-                else {
-                    if (d[j][1] < db.min || db.min == null) db.min = d[j][1];
-                    if (d[j][1] > db.max || db.max == null) db.max = d[j][1];
-                }              
-            }
-        }
         this.renderer = new this.renderer();
         this.renderer.init.call(this, this.rendererOptions);
         
     
     $.jqplot.LinearAxisRenderer.prototype.init = function(options){
         $.extend(true, this, options);
+        var db = this._dataBounds;
+        // Go through all the series attached to this axis and find
+        // the min/max bounds for this axis.
+        for (var i=0; i<this._series.length; i++) {
+            var s = this._series[i];
+            var d = s.data;
+            
+            for (var j=0; j<d.length; j++) { 
+                if (this.name == 'xaxis' || this.name == 'x2axis') {
+                    if (d[j][0] < db.min || db.min == null) db.min = d[j][0];
+                    if (d[j][0] > db.max || db.max == null) db.max = d[j][0];
+                }              
+                else {
+                    if (d[j][1] < db.min || db.min == null) db.min = d[j][1];
+                    if (d[j][1] > db.max || db.max == null) db.max = d[j][1];
+                }              
+            }
+        }
     };
     
 
     $.jqplot.LinearAxisRenderer.prototype.createTicks = function() {
         // we're are operating on an axis here
         var ticks = this._ticks;
+        var userTicks = this.ticks;
         var name = this.name;
         // databounds were set on axis initialization.
         var db = this._dataBounds;
         var pos1, pos2;
         var tt, i;
         
-        //////////////////////////
-        //////////////////////////
-        // fix me
-        //////////////////////////
         // if we already have ticks, use them.
         // ticks must be in order of increasing value.
-        if (ticks.length) {
-            // for (i=0; i<ticks.length; i++){
-            //     var t = ticks[i];
-            //     if (!t.label) t.label = t.value.toString();
-            //     // set iitial css positioning styles for the ticks.
-            //     var pox = i*15+'px';
-            //     switch (name) {
-            //         case 'xaxis':
-            //             t._styles = {position:'absolute', top:'0px', left:pox, paddingTop:'10px'};
-            //             break;
-            //         case 'x2axis':
-            //             t._styles = {position:'absolute', bottom:'0px', left:pox, paddingBottom:'10px'};
-            //             break;
-            //         case 'yaxis':
-            //             t._styles = {position:'absolute', right:'0px', top:pox, paddingRight:'10px'};
-            //             break;
-            //         case 'y2axis':
-            //             t._styles = {position:'absolute', left:'0px', top:pox, paddingLeft:'10px'};
-            //             break;
-            //     }
-            // }
-            // axis.numberTicks = ticks.length;
-            // axis.min = ticks[0].value;
-            // axis.max = ticks[axis.numberTicks-1].value;
-            // axis.tickInterval = (axis.max - axis.min) / (axis.numberTicks - 1);
+        
+        if (userTicks.length) {
+            // ticks could be 1D or 2D array of [val, val, ,,,] or [[val, label], [val, label], ...] or mixed
+            for (i=0; i<userTicks.length; i++){
+                var ut = userTicks[i];
+                var t = new this.tickRenderer(this.tickOptions);
+                if (ut.constructor == Array) {
+                    t.value = ut[0];
+                    t.label = ut[1];
+                    if (!this.showTicks) {
+                        t.showLabel = false;
+                        t.showMark = false;
+                    }
+                    else if (!this.showTickMarks) t.showMark = false;
+                    t.setTick(ut[0], this.name);
+                    this._ticks.push(t);
+                }
+                
+                else {
+                    t.value = ut;
+                    if (!this.showTicks) {
+                        t.showLabel = false;
+                        t.showMark = false;
+                    }
+                    else if (!this.showTickMarks) t.showMark = false;
+                    t.setTick(ut, this.name);
+                    this._ticks.push(t);
+                }
+            }
+            this.numberTicks = userTicks.length;
+            this.min = this._ticks[0].value;
+            this.max = this._ticks[this.numberTicks-1].value;
+            this.tickInterval = (this.max - this.min) / (this.numberTicks - 1);
         }
         
         // we don't have any ticks yet, let's make some!
                     }
                 }
             }
-        }   
-        log(this); 
+        }
     };
 
     // class: $.jqplot.AxisTickRenderer
         this._styles = {};
         // prop: formatter
         // A class of a formatter for the tick text.  sprintf by default.
-        this.formatter = $.jqplot.sprintf;
+        this.formatter = $.jqplot.DefaultTickFormatter;
         // prop: formatString
         // string passed to the formatter.
-        this.formatString = '%.1f'
+        this.formatString = '';
         // prop: fontFamily
         // css spec for the font-family css attribute.
         this.fontFamily='';
                 this.target.append(this.axes[name].draw());
                 this.axes[name].set();
             }
-            
             if (this.axes.yaxis.show) this._gridOffsets.left = this.axes.yaxis.getWidth();
             if (this.axes.y2axis.show) this._gridOffsets.right = this.axes.y2axis.getWidth();
             if (this.title.show && this.axes.x2axis.show) this._gridOffsets.top = this.title.getHeight() + this.axes.x2axis.getHeight();
     // string sprintf(string format[mixed arg1[, mixed arg2[,...]]])
     // The placeholders in the format string are marked by "%" and are followed by one or more of these elements, in this order:
     // 
-    // An optional "+" sign that forces to preceed the result with a plus or minus sign on numeric values. By default, only the "-" sign is used on negative numbers.
-    // An optional padding specifier that says what character to use for padding (if specified). Possible values are 0 or any other character precedeed by a '. The default is to pad with spaces.
-    // An optional "-" sign, that causes sprintf to left-align the result of this placeholder. The default is to right-align the result.
-    // An optional number, that says how many characters the result should have. If the value to be returned is shorter than this number, the result will be padded.
-    // An optional precision modifier, consisting of a "." (dot) followed by a number, that says how many digits should be displayed for floating point numbers. When used on a string, it causes the result to be truncated.
+    // An optional "+" sign that forces to preceed the result with a plus or minus 
+    //   sign on numeric values. By default, only the "-" sign is used on negative numbers.
+    // An optional padding specifier that says what character to use for padding (if specified). 
+    //   Possible values are 0 or any other character precedeed by a '. The default is to pad with spaces.
+    // An optional "-" sign, that causes sprintf to left-align the result of this placeholder. 
+    //   The default is to right-align the result.
+    // An optional number, that says how many characters the result should have. If the value 
+    //   to be returned is shorter than this number, the result will be padded.
+    // An optional precision modifier, consisting of a "." (dot) followed by a number, that says 
+    //   how many digits should be displayed for floating point numbers. When used on a string, 
+    //   it causes the result to be truncated.
+    // 
     // A type specifier that can be any of:
     // % - print a literal "%" character
     // b - print an integer as a binary number
     // e - print a float as scientific notation
     // u - print an integer as an unsigned decimal number
     // f - print a float as is
+    // p - print a float of given significant digits instead of precision
+    // P - print a float of given significant digits instead of precision without padding out trailing zeros.
     // o - print an integer as an octal number
     // s - print a string as is
     // x - print an integer as a hexadecimal number (lower-case)
 
     function str_repeat(i, m) { for (var o = []; m > 0; o[--m] = i); return(o.join('')); }
 
+    function calcSigDigits(num) {
+        var count = 0;
+        var parts = String(num).split('.');
+        var part, i, p;
+
+        if (parts.length == 2) {
+            part = parts[1];
+            for (i=part.length-1; i>-1; i--) {
+                p = part[i];
+                if (count == 0) {
+                    if (p != '0') count++;
+                }
+                else count++;
+            }
+            part = parts[0];
+            for (var i=part.length-1; i>-1; i--) {
+                p = part[i];
+                if (count == 0) {
+                    if (p != '0') count++;
+                }
+                else if (part.length == 1) {
+                    if (p != '0') count++;
+                }
+                else count++;
+            }
+        }
+
+        else if (parts.length == 1) {
+            part = parts[0];
+            for (var i=part.length-1; i>-1; i--) {
+                p = part[i];
+                if (count == 0) {
+                    if (p != '0') count++;
+                }
+                else count++;
+            }
+        }
+        
+        if (count<1) count = 1;
+
+        return count;
+    };
+
     $.jqplot.sprintf = function() {
       var i = 0, a, f = arguments[i++], o = [], m, p, c, x;
       while (f) {
         if (m = /^[^\x25]+/.exec(f)) o.push(m[0]);
         else if (m = /^\x25{2}/.exec(f)) o.push('%');
-        else if (m = /^\x25(?:(\d+)\$)?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(f)) {
+        else if (m = /^\x25(?:(\d+)\$)?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fopPsuxX])/.exec(f)) {
           if (((a = arguments[m[1] || i++]) == null) || (a == undefined)) throw("Too few arguments.");
           if (/[^s]/.test(m[7]) && (typeof(a) != 'number'))
             throw("Expecting number but found " + typeof(a));
             case 'd': a = parseInt(a); break;
             case 'e': a = m[6] ? a.toExponential(m[6]) : a.toExponential(); break;
             case 'f': a = m[6] ? parseFloat(a).toFixed(m[6]) : parseFloat(a); break;
+            case 'p': a = m[6] ? parseFloat(a).toPrecision(m[6]) : parseFloat(a); break;
+            case 'P':
+                var np = m[6];
+                if (np) {
+                    var b = parseFloat(a).toPrecision(np);
+                    var sig = calcSigDigits(b);
+                    if (sig < np) np = sig;
+                    a = parseFloat(a).toPrecision(np);
+                }
+                else a = parseFloat(a);
+                break;
             case 'o': a = a.toString(8); break;
             case 's': a = ((a = String(a)) && m[6] ? a.substring(0, m[6]) : a); break;
             case 'u': a = Math.abs(a); break;
         f = f.substring(m[0].length);
       }
       return o.join('');
+    };
+    
+    $.jqplot.DefaultTickFormatter = function (format, val) {
+        if (typeof val == 'number') {
+            if (!format) format = '%.6P';
+            return $.jqplot.sprintf(format, val);
+        }
+        else return String(val);
     }
     
 })(jQuery);