Explorar o código

HUE-286. Adding notion of 'currentData' and selection handling to HueChart

   Refactoring event handling
   Adding select handling
   Adding getData method and notion of "currentData"
   Improving tick generation.
Marcus McLaughlin %!s(int64=15) %!d(string=hai) anos
pai
achega
cf54d60c1c

+ 84 - 23
desktop/core/static/js/Source/CCS/HueChart.Box.js

@@ -46,6 +46,7 @@ HueChart.Box = new Class({
                 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
                 onPointClick: function that should be run when the mouse is clicked on a datapoint, takes the dataPoint and index as arguments
+                onSpanSelect: function that should be run when a segment of the chart is selected.  Takes a left object and a right object as arguments, each of which contains the corresponding index and data object. 
                 */
         },
         
@@ -79,8 +80,11 @@ HueChart.Box = new Class({
                         if (this.options.labels) this.setLabels(vis);
                         //Add position indicator if enabled
                         if (this.options.positionIndicator) this.setPositionIndicator(vis);
-                        //If there's an event, add the event bar to capture these events.
-                        if (this.$events.pointMouseOut && this.$events.pointMouseOver || this.$events.pointClick) this.addEventBar(vis);
+                        //Create panel for capture of events
+                        this.eventPanel = vis.add(pv.Panel);
+                        //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);
+                        if (this.hasEvent('spanSelect')) this.makeSelectable(vis);
                 }.bind(this));
         },
         
@@ -187,25 +191,26 @@ HueChart.Box = new Class({
                                 else return null;
                         });
         },
-        
-        //Add bar detecting mouse events.
-        addEventBar: function(vis) {
+
+        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.xField]; }.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);
+        },
+         
+        //Add handlers to detect mouse events.
+        addMouseEvents: function(vis) {
                 //Create functions which handle the graph specific aspects of the event and call the event arguments.
                 var outVisFn = function() {
                         this.fireEvent('pointMouseOut');
                         return vis;
                 }.bind(this);
-                var getDataIndexFromMouse = function() {
-                        //Convert the mouse's xValue into its corresponding data value on the xScale. 
-                        var mx = this.xScale.invert(vis.mouse().x);
-                        //Search the data for the index of the element at this data value.
-                        var i = pv.search(this.data.getObjects().map(function(d){ return d[this.options.xField]; }.bind(this)), Math.round(mx));
-                        //Adjust for ProtoVis search
-                        i = i < 0 ? (-i - 2) : i;
-                        return (i >= 0 && i < this.data.getLength() ? i : null);
-                }.bind(this);
                 var moveVisFn = function() {
-                        var dataIndex = getDataIndexFromMouse();
+                        var dataIndex = this.getDataIndexFromX(vis.mouse().x);
                         //Fire pointMouseOver if the item exists
                         if(dataIndex) {
                                 this.fireEvent('pointMouseOver', [this.getData(true).getObjects()[dataIndex], dataIndex]);
@@ -213,21 +218,77 @@ HueChart.Box = new Class({
                         return vis;
                 }.bind(this);
                 var clickFn = function() {
-                        var dataIndex = getDataIndexFromMouse();
-                        //Fire pointClick if the item exists
-                        if(dataIndex != null) {
-                                this.fireEvent('pointClick', [ this.data.getObjects()[dataIndex], dataIndex ]);
+                        //Only click if the movement is clearly not a drag.
+                        if (this.selectState && this.selectState.dx < 2) {
+                                var dataIndex = this.getDataIndexFromX(vis.mouse().x);
+                                //Fire pointClick if the item exists
+                                if(dataIndex != null) {
                                         this.fireEvent('pointClick', [ this.getData(true).getObjects()[dataIndex], dataIndex ]);
+                                }
+                                return vis;
                         }
-                        return vis;
                 }.bind(this);
-                vis.add(pv.Bar)
-                        .fillStyle("rgba(0,0,0,.001)")
+
+                this.eventPanel
+                        .events("all")
                         .event("mouseout", outVisFn)
                         .event("mousemove", moveVisFn)
                         .event("click", clickFn);
-        }
+
+        },
+
+        makeSelectable: function(){
+                this.selectState = {x: 0, dx: 0};
+                this.eventPanel
+                        .data([this.selectState])
+                        .event("mousedown", pv.Behavior.select())
+                        .event("mouseup", 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]);
+                                }
+                        }.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("rgba(255, 128, 128, .4)");
+        },
         
+        //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.
+        adjustToGraph: function(axis, point){
+                var scale;
+                switch(axis){
+                        case 'x': scale = this.xScale;
+                                    break;
+                        case 'y': scale = this.yScale;
+                                    break;
+                        //Return if axis is not x or y.
+                        default: return;
+                }
+                //scale.range() returns an array of two values.
+                //The first is the low end of the range, while the second is the highest.
+                var low = scale.range()[0];
+                var high = scale.range()[1];
+                //Return low or high is the value is outside their interval.  Otherwise, return point.
+                if (point < low) return low;
+                if (point > high) return high;
+                return point;
+        }
 });
 
 

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

@@ -44,19 +44,19 @@ HueChart.Circle = new Class({
                 this.radius = this.options.radius || this.width/2;
                 this.addEvent('setupChart', function(vis) {
                         this.addGraph(vis);
-                        this.addEventBar(vis);
+                        if(this.hasEvent('wedgeOut') && this.hasEvent('wedgeOver') || this.hasEvent('wedgeClick')) this.addEventBar(vis);
                 });
         },
 
         addGraph: function(vis) {
-                var valueSum = this.data.getSeriesSum(this.options.graphField);
+                var valueSum = this.getData(false).getSeriesSum(this.options.graphField);
                 //Put selected index, color array, and hue chart in scope
                 var get_selected_index = this.getSelectedIndex.bind(this);
                 var colorArray = this.options.colorArray;
                 var hueChart = this;
                 vis.add(pv.Wedge)
                         //Data is the array of contained within the HueChart.Data object.
-                        .data(this.data.getObjects())
+                        .data(this.getData(false).getObjects())
                         //Bottom of the wedge space is the radius
                         .bottom(this.radius)
                         //Left of the wedge space is the radius
@@ -80,9 +80,9 @@ HueChart.Circle = new Class({
                 //Base vector is vector pointing straight up.
                 var baseVector = {x: 0, y: this.radius};
                 //Shortcut to data array
-                var dataArray = this.data.getObjects();
+                var dataArray = this.getData(false).getObjects();
                 //Calculate sum of graph values
-                var valueSum = this.data.getSeriesSum(this.options.graphField);
+                var valueSum = this.getData(false).getSeriesSum(this.options.graphField);
                 //Shortcut to graphField
                 var graphField = this.options.graphField;
                 //Add an invisible bar to catch events

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

@@ -35,7 +35,7 @@ HueChart.Line = new Class({
                         //Add a line to the visualization, connecting points produced from the data object.
                         vis.add(pv.Line)
                                 //Using as data, this.data.getObjects.
-                                .data(this.data.getObjects())
+                                .data(this.getData(true).getObjects())
                                 //Closures used because the function argument isn't executed until the render phase.
                                 //Color the line, based on a color in the colarArray.
                                 .strokeStyle(function(itemIndex) {

+ 61 - 8
desktop/core/static/js/Source/CCS/HueChart.js

@@ -127,15 +127,41 @@ HueChart = new Class({
         //Sets selected data index
         setSelectedIndex: function(index) {
                 this.selected_index = index;
-        } 
+        },
+
+        //Set the currentData, which is the data currently being displayed, assuming it is different from the base data.  Should be a subset of the main data object. 
+        setCurrentData: function(data) {
+                this.currentData = new HueChart.Data(data);
+        },
+        
+        //Return a data object.
+        //current- (boolean) if true and there is a currentData object, return the currentData object.
+        //Otherwise, return the base data object.
+        getData: function(current) {
+                if (current && this.currentData) return this.currentData;
+                return this.data;
+        },
+        
+        //Delete this.currentData to reset data to the base data.
+        resetData: function() {
+                delete this.currentData;
+        },
+
+        //Check that there is an event handler registered for an event
+        hasEvent: function(name) {
+                return this.$events[name] && this.$events[name].length;
+        }
 });
 
 //Wrapper for data object to be charted.
 //Adds various functions on the data.
 HueChart.Data = new Class({
         
-        initialize: function(dataArray) {
-                this.dataObjects = dataArray;
+        initialize: function(data) {
+                //Check if it's a HC.Data object, and return the data if it is. 
+                if(data._getExtreme) return data;
+                //Otherwise, set the dataObjects property to data.
+                this.dataObjects = data;
         },
         
         //Function to sort by dates and convert date strings into date objects.
@@ -247,11 +273,19 @@ HueChart.Data = new Class({
         getLength: function() {
                 return this.dataObjects.length;
         },
+
+        //Return a HueChart.Data object containing the dataObjects from the first index to the last index.
+        //Optional argument flatArray - boolean.  If true, will return a flatArray object. 
+        getRange: function(first, last, flatArray) {
+                var sliced = this.dataObjects.slice(first, last);
+                if (flatArray) return sliced;
+                return new HueChart.Data(sliced);
+        },
         
         //Need to figure out an answer for this.  This is very date specific.  
         //Possibly need to genericize a bit more.
         /* Get Ticks --
-                take this data array and give me back an array of data objects to use as ticks.
+                take this data array and give me back an array of data objects to use as ticks (lines representing scale).
                 this array should contain no more than num_ticks values.
                 the increment in which the values should be shown is timespan
         */
@@ -260,6 +294,10 @@ HueChart.Data = new Class({
         createTickArray: function(numTicks, timespan) {
                 //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;
                 var secondsInSpan;
                 //Choose the timespan -- currently only day
                 switch (timespan) {
@@ -269,22 +307,37 @@ HueChart.Data = new Class({
                 }
                 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 (mostSeconds/secondsInSpan <= numTicks) {
+                if (totalSeconds/secondsInSpan <= numTicks) {
                         xTicks = this.dataObjects;
                 } else {
                 //Generate ticks.
                         //Calculate the size of the increment between ticks to produce the number of ticks. 
-                        var dateIncrement = mostSeconds/numTicks;
+                        //Find number of timespans in an increment.
+                        var spansInIncrement = parseInt((totalSeconds/secondsInSpan)/numTicks, 10);
+                        //Find secondsToIncrement each value by
+                        var secondsInIncrement = spansInIncrement * secondsInSpan; 
                         var firstDate = this.dataObjects[0].sample_date;
+                        var firstSeconds = this.dataObjects[0].seconds_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++) {
                                 xTicks.push({
-                                        sample_date: firstDate.clone().increment('second', dateIncrement * i),
-                                        seconds_from_first: dateIncrement * i
+                                        sample_date: firstDate.clone().increment('second', secondsInIncrement * i),
+                                        seconds_from_first: firstSeconds + (secondsInIncrement * i)
                                 });
                         }
+                        //Center tick marks.
+                        var lastTick = xTicks.getLast();
+                        var lastDataPoint = this.dataObjects.getLast();
+                        //Get the number of spans between the lastDataPoint and the lastTick
+                        var spansAtEnd = (lastDataPoint.seconds_from_first - lastTick.seconds_from_first)/secondsInSpan;
+                        //Get number of spans to move ticks forward to result in evenly spaced, centered ticks.
+                        var centerAdjustment = parseInt(spansAtEnd/2, 10);
+                        xTicks.each(function(tick) {
+                                tick.sample_date.increment('second', secondsInSpan * centerAdjustment);
+                                tick.seconds_from_first += secondsInSpan * centerAdjustment;
+                        });
                 }
                 return xTicks;
         }