Ver código fonte

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 15 anos atrás
pai
commit
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;
         }