Преглед изворни кода

HUE-342. Add draggable HueChart.Box selections and refactor date handling in HueChart

Add draggability to selection in HueChart.Box
Add lineWidth option to HueChart.Line
Add showOnEnter to tip creation, and hide tip on creation
Separate tick display into x and y options
Add subspan and truncate members to Date
Refactor date handling and tick creation
Make Protovis drag and resize work in IE
Review responses.
Marcus McLaughlin пре 15 година
родитељ
комит
97716ff80f

+ 200 - 97
desktop/core/static/js/Source/CCS/HueChart.Box.js

@@ -20,7 +20,6 @@ requires:
 - More/DynamicTips
 
 provides: [ HueChart.Box ]
-
 ...
 */
 (function() {
@@ -41,18 +40,23 @@ HueChart.Box = new Class({
                 dates: {x: false, y: false}, //is the data in an axis a date ? 
                 dateSpans:{x: 'day'}, //if an axis is a date, what time span should the tick marks for that axis be shown in
                 positionIndicator: false, //should the position indicator be shown ?
-                showTicks: false, //should tick marks be shown ?
+                ticks: {x: false, y: false}, //should tick marks be shown ?
                 showLabels: false, //should axis labels be shown ?
                 tickColor: "#555", //the color of the tick marks
                 dateFormat: "%b %d", //the format that should be used when displaying dates
                 verticalTickSpacing: 35, //the distance between vertical ticks
                 xTickHeight: 10, //the height of the xTick marks.
                 labels:{x:"X", y: "Y"}, //the labels that should be used for each axis
-                selectColor: "rgba(255, 128, 128, .4)", //the color that should be used to fill selections in this chart
+                selectBarColor: "rgba(0, 0, 0, .2)", //the color that should be used to fill selections in this chart
+                selectBarBorderColor: "rgba(0, 0, 0, 1)", //the color that should be used as a border for selections in this chart
                 selectedIndicatorColor: "black", //color that should be used to show the position of the selected index, when using the position indicator
                 highlightedIndicatorColor: "rgba(255, 255, 255, .5)",
                 yType: 'string', //the type of value that is being graphed on the y-axis,
-                showPointValue: false //show the value at the point when moused over
+                showPointValue: false, //show the value at the point when moused over
+                selectable: false, //make the chart selectable
+                //initialSelectValue: {left: 0, right: 0}, //the initial chart selection, must be same type as x values 
+                draggable: false, //make the chart selection draggable,
+                fireSelectOnDrag: true //fires the select event on completion of a drag
                 /*
                 onPointMouseOut: function that should be run when the mouse is moved out of the chart
                 onPointMouseOver: function that should be run when the mouse is moved over a datapoint, takes the dataPoint and index as arguments
@@ -73,7 +77,7 @@ HueChart.Box = new Class({
                         this.getData(true).prepareDates(this.options.xProperty);
                         //Set dateProperty to the initial xProperty, this will hold a date object which will be used for rendering dates as strings 
                         this.dateProperty = this.options.xProperty;
-                        this.options.xProperty = 'seconds_from_first';
+                        this.options.xProperty = 'ms_from_first';
                 } else {
                         //Otherwise sort by the x property.
                         this.getData(true).sortByProperty(this.options.xProperty);
@@ -83,10 +87,18 @@ HueChart.Box = new Class({
                         this.tip = new DynamicTips(this.element, {
                                 className: 'huechart tip-wrap',
                                 title: $lambda("Title"),
-                                text: $lambda("Text")
+                                text: $lambda("Text"),
+                                showOnEnter: false
                         });
+                        this.tip.hide();
                         this.addEvent('seriesMouseOver', this.updatePointValue);
                 }
+                //Initialize dragState and selectState
+                this.dragState = {x: 0, y: 0};
+                this.selectState = {x: 0, dx: 0};
+                //Set this.draggable and this.selectable to reflect whether or not the chart has these capabilities, based on the existence of events and/or options.
+                this.draggable = this.options.draggable || this.hasEvent('drag') || this.hasEvent('dragStart') || this.hasEvent('dragEnd');
+                this.selectable = this.options.selectable || this.draggable || this.hasEvent('spanSelect') || this.hasEvent('select') || this.hasEvent('selectStart') || this.hasEvent('selectEnd');
                 //When the setupChart event is fired, the full ProtoVis visualization is being set up, in preparation for render.
                 //The addGraph function is responsible for adding the actual representation of the data, be that a group of lines, or a group of area graphs.
                 this.addEvent('setupChart', function(vis) {
@@ -95,7 +107,7 @@ HueChart.Box = new Class({
                         //Add representation of the data.
                         this.addGraph(vis);
                         //Add tick marks (rules on side and bottom) if enabled
-                        if (this.options.showTicks) this.setTicks(vis);
+                        if (this.options.ticks.x || this.options.ticks.y) this.setTicks(vis);
                         //Add axis labels if enabled
                         if (this.options.showLabels) this.setLabels(vis);
                         //Add position indicator if enabled
@@ -103,9 +115,9 @@ HueChart.Box = new Class({
                         //Create panel for capture of events
                         this.eventPanel = vis.add(pv.Panel).fillStyle("rgba(0,0,0,.001)");
                         //If there's a mouse event, add the functionality to capture these events.
-                        //if (this.hasEvent('pointMouseOut') && this.hasEvent('pointMouseOver') || this.hasEvent('pointClick')) this.addMouseEvents(vis);
-                        this.addMouseEvents(vis);
-                        if (this.hasEvent('spanSelect')) this.makeSelectable(vis);
+                        if (this.hasEvent('pointMouseOut') && this.hasEvent('pointMouseOver') || this.hasEvent('pointClick') || this.options.showPointValue) this.addMouseEvents(vis);
+                        //If there is a selection or drag event of any sort make the graph selectable.
+                        if (this.selectable) this.makeSelectable(vis);
                 }.bind(this));
         },
         
@@ -128,59 +140,63 @@ HueChart.Box = new Class({
         
         //Draw the X and Y tick marks.
         setTicks:function(vis) {
-                //Add X-Ticks.
-                //Create tick array.
-                var xTicks = (this.options.dates.x ? this.getData(true).createTickArray(7, this.options.dateSpans.x, this.dateProperty) : this.xScale.ticks(7));
-                //Function will return the correct xValue dependent on whether or not x is a date
-                var getXValue = getXValueFn(this.options.dates.x ? this.options.xProperty : null);
-                //Create rules (lines intended to denote scale)
-                vis.add(pv.Rule)
-                        //Use the tick array as data.
-                        .data(xTicks)
-                        //The bottom of the rule should be at the bottomPadding - the height of the rule.  
-                        .bottom(this.options.bottomPadding - this.options.xTickHeight)
-                        //The left of the rule should be at the data object's xProperty value scaled to pixels.
-                        .left(function(d) { return this.xScale(getXValue(d)); }.bind(this))
-                        //Set the height of the rule to the xTickHeight
-                        .height(this.options.xTickHeight)
-                        .strokeStyle(this.options.tickColor)
-                        //Add label to bottom of each rule
-                        .anchor("bottom").add(pv.Label)
-                                .text(function(d) {
-                                        //If the option is a date, format the date property field.
-                                        //Otherwise, simply show it.
-                                        if(this.options.dates.x) {
-                                                return d[this.dateProperty].format(this.options.dateFormat);
-                                        } else {
-                                                return getXValue(d);
-                                        }
-                                }.bind(this));
-               
-                //Add Y-Ticks
-                //Calculate number of yTicks to show.
-                //Approximate goal of 35 pixels between yTicks.
-                var yTickCount = (this.height - (this.options.bottomPadding + this.options.topPadding))/this.options.verticalTickSpacing;
-                //In some box-style charts, there is a need to have a different scale for yTicks and for y values.
-                //If there is a scale defined for yTicks, use it, otherwise use the standard yScale.
-                var tickScale = this.yScaleTicks || this.yScale;
-                //Create rules
-                vis.add(pv.Rule)
-                        //Always show at least two ticks.
-                        //tickScale.ticks returns an array of values which are evenly spaced to be used as tick marks.
-                        .data(tickScale.ticks(yTickCount > 1 ? yTickCount : 2))
-                        //The left side of the rule should be at leftPadding pixels.
-                        .left(this.options.leftPadding)
-                        //The bottom of the rule should be at the tickScale.ticks value scaled to pixels.
-                        .bottom(function(d) {return tickScale(d);}.bind(this))
-                        //The width of the rule should be the width minus the hoizontal padding.
-                        .width(this.width - this.options.leftPadding - this.options.rightPadding + 1)
-                        .strokeStyle(this.options.tickColor)
-                        //Add label to the left which shows the number of bytes.
-                        .anchor("left").add(pv.Label)
-                                .text(function(d) { 
-                                        if(this.options.yType == 'bytes') return d.convertFileSize(); 
-                                        return d;
-                                }.bind(this));
+                if (this.options.ticks.x) {
+                        //Add X-Ticks.
+                        //Create tick array.
+                        var xTicks = (this.options.dates.x ? this.getData(true).createTickArray(7, this.options.dateSpans.x, this.dateProperty) : this.xScale.ticks(7));
+                        //Function will return the correct xValue dependent on whether or not x is a date
+                        var getXValue = getXValueFn(this.options.dates.x ? this.options.xProperty : null);
+                        //Create rules (lines intended to denote scale)
+                        vis.add(pv.Rule)
+                                //Use the tick array as data.
+                                .data(xTicks)
+                                //The bottom of the rule should be at the bottomPadding - the height of the rule.  
+                                .bottom(this.options.bottomPadding - this.options.xTickHeight)
+                                //The left of the rule should be at the data object's xProperty value scaled to pixels.
+                                .left(function(d) { return this.xScale(getXValue(d)); }.bind(this))
+                                //Set the height of the rule to the xTickHeight
+                                .height(this.options.xTickHeight)
+                                .strokeStyle(this.options.tickColor)
+                                //Add label to bottom of each rule
+                                .anchor("bottom").add(pv.Label)
+                                        .text(function(d) {
+                                                //If the option is a date, format the date property field.
+                                                //Otherwise, simply show it.
+                                                if(this.options.dates.x) {
+                                                        return d[this.dateProperty].format(this.options.dateFormat);
+                                                } else {
+                                                        return getXValue(d);
+                                                }
+                                        }.bind(this));
+                }
+                
+                if (this.options.ticks.y) {      
+                        //Add Y-Ticks
+                        //Calculate number of yTicks to show.
+                        //Approximate goal of 35 pixels between yTicks.
+                        var yTickCount = (this.height - (this.options.bottomPadding + this.options.topPadding))/this.options.verticalTickSpacing;
+                        //In some box-style charts, there is a need to have a different scale for yTicks and for y values.
+                        //If there is a scale defined for yTicks, use it, otherwise use the standard yScale.
+                        var tickScale = this.yScaleTicks || this.yScale;
+                        //Create rules
+                        vis.add(pv.Rule)
+                                //Always show at least two ticks.
+                                //tickScale.ticks returns an array of values which are evenly spaced to be used as tick marks.
+                                .data(tickScale.ticks(yTickCount > 1 ? yTickCount : 2))
+                                //The left side of the rule should be at leftPadding pixels.
+                                .left(this.options.leftPadding)
+                                //The bottom of the rule should be at the tickScale.ticks value scaled to pixels.
+                                .bottom(function(d) {return tickScale(d);}.bind(this))
+                                //The width of the rule should be the width minus the hoizontal padding.
+                                .width(this.width - this.options.leftPadding - this.options.rightPadding + 1)
+                                .strokeStyle(this.options.tickColor)
+                                //Add label to the left which shows the number of bytes.
+                                .anchor("left").add(pv.Label)
+                                        .text(function(d) { 
+                                                if(this.options.yType == 'bytes') return d.convertFileSize(); 
+                                                return d;
+                                        }.bind(this));
+                }
         },
         
         //Add X and Y axis labels.
@@ -201,7 +217,7 @@ HueChart.Box = new Class({
 
         //Add bars which indicate the positions which are currently selected and/or highlighted on the box graph.
         setPositionIndicators: function(vis) {
-                //Put selected_index in scope.
+                //Put selected_index and highlighted_index in scope.
                 get_selected_index = this.getSelectedIndex.bind(this);
                 get_highlighted_index = this.getHighlightedIndex.bind(this);
                 var selectedColor = this.options.selectedIndicatorColor;
@@ -223,14 +239,16 @@ HueChart.Box = new Class({
                         });
         },
 
-        getDataIndexFromX: function(x) {
-                //Convert the passedin in xValue into its corresponding data value on the xScale. 
-                var mx = this.xScale.invert(x);
-                //Search the data for the index of the element at this data value.
-                var i = pv.search(this.getData(true).getObjects().map(function(d){ return d[this.options.xProperty]; }.bind(this)), Math.round(mx));
-                //Adjust for ProtoVis search
-                i = i < 0 ? (-i - 2) : i;
-                return (i >= 0 && i < this.getData(true).getLength() ? i : null);
+        getDataIndexFromPoint: function(axis, x) {
+                if(axis == 'x') {
+                        //Convert the passedin in xValue into its corresponding data value on the xScale. 
+                        var mx = this.xScale.invert(x);
+                        //Search the data for the index of the element at this data value.
+                        var i = pv.search(this.getData(true).getObjects().map(function(d){ return d[this.options.xProperty]; }.bind(this)), Math.round(mx));
+                        //Adjust for ProtoVis search
+                        i = i < 0 ? (-i - 2) : i;
+                        return (i >= 0 && i < this.getData(true).getLength() ? i : null);
+                }
         },
 
         getYRange: function(y, inversionScale) {
@@ -262,7 +280,7 @@ HueChart.Box = new Class({
         addMouseEvents: function(vis) {
                 //Function that controls the search for data points and fires mouse positioning events. 
                 var mousePositionEvent = function(eventName, position) {
-                        var dataIndex = this.getDataIndexFromX(position.x);
+                        var dataIndex = this.getDataIndexFromPoint('x', position.x);
                         if(dataIndex != null) {
                                 var dataPoint = this.getData(true).getObjects()[dataIndex];
                                 this.fireEvent('point' + eventName.capitalize(), [ dataPoint, dataIndex ]);
@@ -296,37 +314,121 @@ HueChart.Box = new Class({
                         .event("click", clickFn);
 
         },
-
+        
+        //Given points on an axis, return an array of data objects for each point
+        getObjectsForPoints: function(axis /*points*/) {
+                var argArray = $A(arguments).slice(1);
+                return argArray.map(function(point) {
+                        var toGraph = this.adjustToGraph(axis, point);
+                        var index = this.getDataIndexFromPoint(axis, toGraph);
+                        return { index: index, data: this.getData(true).getObjects()[index] };
+                }.bind(this));
+                
+        },
+        
+        //Make selection in graph draggable
+        makeSelectionDraggable: function() {
+                //Attach the ProtoVis drag behavior to the select bar and attach events for drag occurrences
+                this.selectBar = this.selectBar 
+                //Set cursor to be a mouse pointer
+                .cursor("move")
+                .event("mousedown", pv.Behavior.drag())
+                        .event("drag", function() {
+                                this.fireEvent('drag');
+                        }.bind(this))
+                        .event("dragstart", function() {
+                                this.fireEvent('dragStart');
+                        }.bind(this))
+                        .event("dragend", function() {
+                                if (this.options.fireSelectOnDrag) {
+                                        //Get objects for edge points
+                                        var leftPoint = this.dragState.x;
+                                        var rightPoint = this.dragState.x + this.selectWidth;
+                                        var objectArray = this.getObjectsForPoints('x', leftPoint, rightPoint);
+                                        this.fireEvent('spanSelect', objectArray);
+                                }
+                                this.fireEvent('dragEnd');
+                        }.bind(this));
+        },
+        
+        //Make graph selectable
         makeSelectable: function(){
-                this.selectState = {x: 0, dx: 0};
+                //Create select bar
+                this.selectBar = this.eventPanel.add(pv.Bar);
+                
+                //If there is a need for draggability, make the selection draggable.
+                if (this.draggable) this.makeSelectionDraggable();
+                
+                //Set the basic settings for the selectBar 
+                this.selectBar = this.selectBar
+                                //Initialize dragState as selectBar's data.
+                                .data([this.dragState])
+                                //Set fillStyle to the selectBarColor
+                                .fillStyle(this.options.selectBarColor)
+                                //Set height so that it only covers the chart
+                                .height(this.height - (this.options.bottomPadding + this.options.topPadding))
+                                //Set top to start at top padding
+                                .top(this.options.topPadding)
+                                //Set line width to 1 pixel
+                                .lineWidth(1)
+                                //Set the left value to the dragState's x value.
+                                .left(function(d) {
+                                        return d.x;
+                                });
+                
+                //Initialize selection to nothing
+                this.selectRange(0, 0);
+                //If the initialSelectValue has been set, select that range.
+                if(this.options.initialSelectValue) {
+                        //If date, convert to date
+                        if (this.options.dates.x) {
+                                startValue = this.data.getMsFromFirst(this.data.parseDate(this.options.initialSelectValue.start));
+                                endValue = this.data.getMsFromFirst(this.data.parseDate(this.options.initialSelectValue.end));
+                        }
+                        //Convert to points on xScale
+                        startX = this.xScale(startValue);
+                        endX = this.xScale(endValue);
+                        this.selectRange(startX, endX);
+                        this.selectBar.strokeStyle(this.options.selectBarBorderColor);
+                }
+                //Initialize eventPanel's select events.
                 this.eventPanel
                         .data([this.selectState])
-                        .event("mousedown", pv.Behavior.select())
-                        .event("mouseup", function() {
+                        .event("mousedown", pv.Behavior.select());
+                         
+                //If d.dx has a value greater than 0...meaning we're in the middle of a
+                //drag, adjust the width value to the graph.
+                //Otherwise give it a width of 0.
+                this.eventPanel
+                        .event("selectstart", function() {
+                                this.selectBar.width(0);
+                                this.selectBar.strokeStyle(this.options.selectBarBorderColor);
+                                this.fireEvent('selectStart');
+                        }.bind(this))
+                        .event("select", function() {
+                                this.selectRange(this.adjustToGraph('x', this.selectState.x), this.adjustToGraph('x', this.selectState.x + this.selectState.dx));
+                                this.fireEvent('select');
+                        }.bind(this))
+                        .event("selectend", function() {
                                 if (this.selectState.dx > 2) {
-                                        //Get edges of selected area.
-                                         var leftEdge = this.adjustToGraph('x', this.selectState.x);
-                                         var rightEdge = this.adjustToGraph('x', this.selectState.x + this.selectState.dx);
-                                         //Get corresponding indexes for edges of selected area.
-                                         var leftIndex = this.getDataIndexFromX(leftEdge);
-                                         var rightIndex = this.getDataIndexFromX(rightEdge);
-                                         var leftObj = { index: leftIndex, data: this.getData(true).getObjects()[leftIndex] };
-                                         var rightObj = { index: rightIndex, data: this.getData(true).getObjects()[rightIndex] };
-                                         this.fireEvent('spanSelect', [leftObj, rightObj]);
+                                        //Get objects for edge points
+                                        //left - this.selectState.x
+                                        //right - this.selectState.dx
+                                        var objectArray = this.getObjectsForPoints('x', this.selectState.x, this.selectState.x + this.selectState.dx);
+                                        this.fireEvent('spanSelect', objectArray);
                                 }
+                                this.fireEvent('selectEnd');
                         }.bind(this));
-                
-                //Add a bar to display the selected state.
-                this.eventPanel.add(pv.Bar)
-                        .left(function(d) { return this.adjustToGraph('x', this.selectState.x); }.bind(this))
-                        //If d.dx has a value greater than 0...meaning we're in the middle of a
-                        //drag, adjust the width value to the graph.
-                        //Otherwise give it a width of 0. 
-                        .width(function(d) { return this.selectState.dx > 0 ? this.adjustToGraph('x', this.selectState.x + this.selectState.dx) - this.selectState.x : 0; 
-                        }.bind(this))
-                        .fillStyle(this.options.selectColor);
         },
         
+        //Selects a pixel range in the graph.
+        selectRange: function(leftValue, rightValue) {
+                this.dragState.x = this.adjustToGraph('x', leftValue);
+                this.selectWidth = rightValue - leftValue;
+                this.selectBar
+                        .width(rightValue - leftValue);
+        },
+
         //Adjusts a point to the graph.  Ie...if you give it a point that's greater than or less than
         //points in the graph, it will reset it to points within the graph.
         //This is easily accomplished using the range of the graphing scales.
@@ -349,7 +451,8 @@ HueChart.Box = new Class({
                 if (point > high) return high;
                 return point;
         },
-
+        
+        //Updates the display of the currently visible tip
         updatePointValue: function(series) {
                 if (series != null) {
                         var tipColor = new Element('div', {'class': 'tip-series-color'});

+ 5 - 1
desktop/core/static/js/Source/CCS/HueChart.Line.js

@@ -22,6 +22,10 @@ HueChart.Line = new Class({
 
         Extends: HueChart.Box,
 
+        options: {
+                lineWidth: 3 //Width of the lines in the chart
+        },
+
         initialize: function(element, options) {
                 this.parent(element, options);
                 this.render();
@@ -53,7 +57,7 @@ HueChart.Line = new Class({
                                         }.bind(this);
                                 }.bind(this)(itemIndex))
                                 //Make the line's width 3 pixels.
-                                .lineWidth(3);
+                                .lineWidth(this.options.lineWidth);
                 }
         }
 });

+ 93 - 39
desktop/core/static/js/Source/CCS/HueChart.js

@@ -26,7 +26,35 @@ provides: [ HueChart ]
 //
 
 (function() {
-var colorManager = GroupValueManager; 
+var colorManager = GroupValueManager;
+
+Date.extend({
+        spans: ['ms', 'second', 'minute', 'hour', 'day'],
+        subspan: function(span) {
+                return this.spans[this.spans.indexOf(span) - 1];
+        }
+});
+
+Date.implement({
+        truncateTo: function(target) {
+                //Set value of spans below truncation target to 0
+                var subspan = Date.subspan(target);
+                var subspanIndex = Date.spans.indexOf(subspan);
+                var toZero = Date.spans.slice(0, subspanIndex + 1);
+                var truncated = 0;
+                toZero.each(function(zeroee) {
+                        //Zeroees stores the plural version of zeroee, used in get and set
+                        var zeroees = zeroee;
+                        if (zeroee != 'ms') {
+                                zeroees += 's';
+                        }
+                        truncated += Date.units[zeroee]() * this.get(zeroees);
+                        this.set(zeroees, 0);
+                }.bind(this));       
+                return truncated;
+        }
+});
+
 HueChart = new Class({
 
         Implements: [Events, Options],
@@ -180,31 +208,39 @@ HueChart.Data = new Class({
         },
         
         //Function to sort by dates and convert date strings into date objects.
-        //Also creates seconds_from_first, a useful integer value for graphing.
+        //Also creates ms_from_first, a useful integer value for graphing.
         //Accepts date formats parsable in Native/Date.js, or integral ms values.
         prepareDates: function(dateProperty) {
-                //Convert date string to date object.
+                //Convert date string in each object to date object
                 this.dataObjects.each(function(d) {
-                        //Check if dateProperty value is parsable as string.  
-                        //Parse as string if it is, otherwise parse as ms value.
-                        var parseResult = Date.parse(d[dateProperty]);
-                        if (parseResult.isValid()) {
-                                d[dateProperty] = parseResult;
-                        } else {
-                                parseResult = Date.parse(d[dateProperty].toInt());
-                                if (parseResult.isValid()) d[dateProperty] = parseResult;
-                        } 
-                });
+                        d[dateProperty] = this.parseDate(d[dateProperty]);
+                }.bind(this));
                 //Sort data by date property.
                 this.sortByProperty(dateProperty);
                 //Store first date for comparison.
                 var firstDate = this.dataObjects[0][dateProperty];
-                //Create seconds from first, for comparison sake.
+                this.getMsFromFirst = function(dateObject) {
+                        return firstDate.diff(dateObject, 'ms'); 
+                };
+                //Create ms from first, for comparison sake.
                 this.dataObjects.each(function(d) {
-                        d.seconds_from_first = firstDate.diff(d[dateProperty], 'second');
-                });
+                        d.ms_from_first = this.getMsFromFirst(d[dateProperty]);
+                }.bind(this));
         },
         
+        //Parse date -- accepts date formats parsable in Native/Date.js, or integral ms values.
+        parseDate: function(d) {
+                //Check if dateProperty value is parsable as string.
+                //Parse as string if it is, otherwise parse as ms value.
+                var parseResult = Date.parse(d);
+                if (parseResult.isValid()) {
+                        return parseResult;
+                } else {
+                        parseResult = Date.parse(d.toInt());
+                        if (parseResult.isValid()) return parseResult;
+                }
+        },
+
         //Sort data by some property within it.
         sortByProperty: function(property) {
                 this.dataObjects.sort(function(a, b) {
@@ -317,46 +353,63 @@ HueChart.Data = new Class({
         //Currently requires the dates in the data object to be exact days.
 
         createTickArray: function(numTicks, timespan, dateProperty) {
-                //Get the largest number of seconds.
-                var mostSeconds = this.dataObjects.getLast().seconds_from_first;
-                //Get the smallest number of seconds.
-                var leastSeconds = this.dataObjects[0].seconds_from_first;
-                //Get the total number of seconds spanned by the array.
-                var totalSeconds = mostSeconds - leastSeconds;
-                //Get the number of seconds from Date.js
-                var secondsInSpan = Date.units[timespan]()/1000;
+                //Get the largest number of ms.
+                var mostMs = this.dataObjects.getLast().ms_from_first;
+                //Get the smallest number of ms.
+                var leastMs = this.dataObjects[0].ms_from_first;
+                //Get the total number of ms spanned by the array.
+                var totalMs = mostMs - leastMs;
+                //Get the number of ms from Date.js
+                var msInSpan = Date.units[timespan]();
                 var xTicks = [];
-                //If the number of ticks that would be generated for the timespan is less than the number of ticks requested, use the current data array.
-                if (totalSeconds/secondsInSpan <= numTicks) {
+                //The number of spans contained in the total span of ms 
+                var spansContained = totalMs/msInSpan;
+                //If the number of ticks that would be generated for the timespan is less than the number of ticks requested and the number of dataObjects is less than the number of ticks, use the current data array.
+                if (spansContained <= numTicks && this.dataObjects.length < numTicks) {
                         xTicks = this.dataObjects;
                 } else {
                 //Generate ticks.
                         //Calculate the size of the increment between ticks to produce the number of ticks. 
                         //Find number of timespans in an increment.
-                        var spansInIncrement = parseInt((totalSeconds/secondsInSpan)/numTicks, 10);
-                        //Find secondsToIncrement each value by
-                        var secondsInIncrement = spansInIncrement * secondsInSpan; 
+                        var spansInIncrement = Math.ceil(spansContained/numTicks);
+                        //Find msToIncrement each value by
+                        var msInIncrement = spansInIncrement * msInSpan; 
                         var firstDate = this.dataObjects[0][dateProperty];
-                        var firstSeconds = this.dataObjects[0].seconds_from_first;
+                        var firstMs = this.dataObjects[0].ms_from_first;
                         //Add ticks to the xTicks array.
                         //Sample date - firstDate cloned and incremented by the increment times the iteration number.
-                        //Seconds_from_first - dateIncrement multiplied by the iteration number.
-                        for (var i = 0; i <= numTicks; i++) {
+                        //Ms_from_first - dateIncrement multiplied by the iteration number.
+                        for (var i = 0; i <= Math.min(numTicks, spansContained); i++) {
                                 var tickObject = {};
-                                tickObject[dateProperty] = firstDate.clone().increment('second', secondsInIncrement * i);
-                                tickObject.seconds_from_first = firstSeconds + (secondsInIncrement * i);
+                                tickObject[dateProperty] = firstDate.clone().increment('ms', msInIncrement * i);
+                                tickObject.ms_from_first = firstMs + (msInIncrement * i);
                                 xTicks.push(tickObject);
                         }
+                        //Ensure tick is exactly on timespan
+                        xTicks.each(function(tick) {
+                                //Truncate the date to timespan and get the amount of the subspan which was truncated
+                                var msToTruncate = tick[dateProperty].truncateTo(timespan);
+                                //Adjust ms from first by subtracting the number of ms in the truncated subspans
+                                tick.ms_from_first -= msToTruncate; 
+                        });
+                        var lastDataPoint = this.dataObjects.getLast();
+                        //Filter xTicks to only contain numbers on chart
+                        xTicks = xTicks.filter(function(tick) {
+                                if(tick.ms_from_first >= 0 && lastDataPoint.ms_from_first >= tick.ms_from_first) return true; 
+                        });
                         //Center tick marks.
                         var lastTick = xTicks.getLast();
-                        var lastDataPoint = this.dataObjects.getLast();
+                        var firstDataPoint = this.dataObjects[0];
+                        var firstTick = xTicks[0];
                         //Get the number of spans between the lastDataPoint and the lastTick
-                        var spansAtEnd = (lastDataPoint.seconds_from_first - lastTick.seconds_from_first)/secondsInSpan;
+                        var spansAtEnd = (lastDataPoint.ms_from_first - lastTick.ms_from_first)/msInSpan;
+                        //Get the number of spans between the firstDataPoint and the firstTick
+                        var spansAtBeginning = (firstTick.ms_from_first - firstDataPoint.ms_from_first)/msInSpan; 
                         //Get number of spans to move ticks forward to result in evenly spaced, centered ticks.
-                        var centerAdjustment = parseInt(spansAtEnd/2, 10);
+                        var centerAdjustment = parseInt((spansAtEnd - spansAtBeginning)/2, 10);
                         xTicks.each(function(tick) {
-                                tick[dateProperty].increment('second', secondsInSpan * centerAdjustment);
-                                tick.seconds_from_first += secondsInSpan * centerAdjustment;
+                                tick[dateProperty].increment('ms', msInSpan * centerAdjustment);
+                                tick.ms_from_first += msInSpan * centerAdjustment;
                         });
                 }
                 return xTicks;
@@ -388,4 +441,5 @@ HueChart.buildData = function(table) {
         });
         return data;
 };
+
 })();

+ 4 - 4
ext/thirdparty/js/protovis/protovis-d3.2.js

@@ -14862,8 +14862,8 @@ pv.Behavior.drag = function() {
     scene = null;
   }
 
-  pv.listen(window, "mousemove", mousemove);
-  pv.listen(window, "mouseup", mouseup);
+  pv.listen(window.document.documentElement, "mousemove", mousemove);
+  pv.listen(window.document.documentElement, "mouseup", mouseup);
   return mousedown;
 };
 /**
@@ -15223,8 +15223,8 @@ pv.Behavior.resize = function(side) {
     scene = null;
   }
 
-  pv.listen(window, "mousemove", mousemove);
-  pv.listen(window, "mouseup", mouseup);
+  pv.listen(window.document.documentElement, "mousemove", mousemove);
+  pv.listen(window.document.documentElement, "mouseup", mouseup);
   return mousedown;
 };
 /**