Browse Source

HUE-8178 [charts] Bugfixes for timeline selection & refine intervals

jdesjean 7 năm trước cách đây
mục cha
commit
2b45a5081a

+ 1 - 1
apps/search/src/search/models.py

@@ -304,7 +304,7 @@ class Collection(models.Model):
       if facet['widgetType'] == 'map-widget' and facet['type'] == 'field':
         facet['type'] = 'pivot'
         properties['facets'] = []
-        properties['facets_form'] = {'field': '', 'mincount': 1, 'limit': 5}
+        properties['facets_form'] = {'field': '', 'mincount': 0, 'limit': 5}
 
     return json.dumps(props)
 

+ 126 - 67
desktop/core/src/desktop/static/desktop/js/ko.charts.js

@@ -15,7 +15,20 @@
 // limitations under the License.
 
 (function () {
-
+  var MS = 1,
+  SECOND_MS = 1000 * MS,
+  MINUTE_MS = SECOND_MS * 60,
+  HOUR_MS = MINUTE_MS * 60,
+  DAY_MS = HOUR_MS * 24,
+  WEEK_MS = DAY_MS * 7,
+  MONTH_MS = DAY_MS * 30.5,
+  YEAR_MS = DAY_MS * 365;
+  TIME_INTERVALS = [{ms: SECOND_MS * 1, coeff: 1, unit: 'SECONDS'}, {ms: SECOND_MS * 2, coeff: 2, unit: 'SECONDS'}, {ms: SECOND_MS * 5, coeff: 5, unit: 'SECONDS'}, {ms: SECOND_MS * 10, coeff: 10, unit: 'SECONDS'}, {ms: SECOND_MS * 15, coeff: 15, unit: 'SECONDS'}, {ms: SECOND_MS * 30, coeff: 30, unit: 'SECONDS'},
+  {ms: MINUTE_MS * 1, coeff: 1, unit: 'MINUTES'}, {ms: MINUTE_MS * 2, coeff: 2, unit: 'MINUTES'}, {ms: MINUTE_MS * 5, coeff: 5, unit: 'MINUTES'}, {ms: MINUTE_MS * 10, coeff: 10, unit: 'MINUTES'}, {ms: MINUTE_MS * 15, coeff: 15, unit: 'MINUTES'}, {ms: MINUTE_MS * 30, coeff: 30, unit: 'MINUTES'},
+  {ms: HOUR_MS * 1, coeff: 1, unit: 'HOURS'}, {ms: HOUR_MS * 2, coeff: 2, unit: 'HOURS'}, {ms: HOUR_MS * 4, coeff: 4, unit: 'HOURS'}, {ms: HOUR_MS * 6, coeff:6, unit: 'HOURS'}, {ms: HOUR_MS * 8, coeff: 8, unit: 'HOURS'}, {ms: HOUR_MS * 12, coeff: 12, unit: 'HOURS'},
+  {ms: DAY_MS * 1, coeff: 1, unit: 'DAYS'}, {ms: DAY_MS * 2, coeff: 2, unit: 'MONTHS'}, {ms: WEEK_MS * 1, coeff: 7, unit: 'DAYS'}, {ms: WEEK_MS * 2, coeff: 14, unit: 'DAYS'},
+  {ms: MONTH_MS * 1, coeff: 1, unit: 'MONTHS'}, {ms: MONTH_MS * 2, coeff: 2, unit: 'MONTHS'}, {ms: MONTH_MS * 3, coeff: 3, unit: 'MONTHS'}, {ms: MONTH_MS * 6, coeff: 6, unit: 'MONTHS'},
+  {ms: YEAR_MS * 1, coeff: 1, unit: 'YEARS'}];
   ko.HUE_CHARTS = {
     TYPES: {
       COUNTER: "counter",
@@ -201,13 +214,17 @@
         }
        if (numeric(_datum)) {
           _chart.xAxis.showMaxMin(false).tickFormat(d3v3.format(",0f"));
-          _chart.multibar.barColor(null);
+          if (_chart.multibar) {
+            _chart.multibar.barColor(null);
+          }
         } else {
           _chart.xAxis.tickFormat(function(s){ return s; });
-          if (!_isPivot) {
-            _chart.multibar.barColor(nv.utils.defaultColor());
-          } else {
-            _chart.multibar.barColor(null);
+          if (_chart.multibar) {
+            if (!_isPivot) {
+              _chart.multibar.barColor(nv.utils.defaultColor());
+            } else {
+              _chart.multibar.barColor(null);
+            }
           }
         }
         window.setTimeout(function () {
@@ -225,7 +242,21 @@
       }
     }
   };
-
+  function getInterval(domainMs, maxSlots) {
+    var biggestInterval = TIME_INTERVALS[TIME_INTERVALS.length-1];
+    var biggestIntervalIsTooSmall = domainMs / biggestInterval.ms > maxSlots;
+    if (biggestIntervalIsTooSmall) {
+      var coeff = Math.ceil(domainMs / maxSlots);
+      return "+" + coeff + "YEARS";
+    }
+    for (var i = TIME_INTERVALS.length - 2; i >= 0; i--) {
+      var slots = domainMs / TIME_INTERVALS[i].ms;
+      if (slots > maxSlots) {
+        return "+" + TIME_INTERVALS[i + 1].coeff + TIME_INTERVALS[i + 1].unit;
+      }
+    }
+    return "+" + TIME_INTERVALS[0].coeff + TIME_INTERVALS[0].unit;
+  }
   ko.bindingHandlers.timelineChart = {
     init: function (element, valueAccessor) {
       if (valueAccessor().type && valueAccessor().type() == "line"){
@@ -673,28 +704,14 @@
     }
   };
   function multi(xAxis) {
-    var previous = new Date(9999,11,31);
+    var previous = null;
     var minDiff = 5.1;
-    var s = 1000,
-    m = s * 60,
-    h = m * 60,
-    day = h * 24,
-    mn = day * 30.5,
-    y = day * 365;
     return d3v3.time.format.utc.multi([
-      ["%L %Y-%m-%dT%H:%M:%S", function(d) {
-        var domain = xAxis.domain();
-        var domainDiff = domain[domain.length - 1] - domain[0];
-        var result = previous >= d && domainDiff < s * minDiff;
-        if (result) {
-          previous = d;
-        }
-        return result;
-      }],
       ["%S %Y-%m-%dT%H:%M", function(d) {
         var domain = xAxis.domain();
-        var domainDiff = domain[domain.length - 1] - domain[0];
-        var result = previous >= d && domainDiff < m * minDiff;
+        var domainDiff = Math.abs(domain[domain.length - 1] - domain[0]);
+        var isFirst = d == domain[0];
+        var result = isFirst && domainDiff < MINUTE_MS * minDiff;
         if (result) {
           previous = d;
         }
@@ -702,8 +719,9 @@
       }],
       ["%H:%M %Y-%m-%d", function(d) {
         var domain = xAxis.domain();
-        var domainDiff = domain[domain.length - 1] - domain[0];
-        var result = previous >= d && domainDiff < h * minDiff;
+        var domainDiff = Math.abs(domain[domain.length - 1] - domain[0]);
+        var isFirst = d == domain[0];
+        var result = isFirst && domainDiff < HOUR_MS * minDiff;
         if (result) {
           previous = d;
         }
@@ -711,8 +729,9 @@
       }],
       ["%H:%M %Y-%m-%d", function(d) {
         var domain = xAxis.domain();
-        var domainDiff = domain[domain.length - 1] - domain[0];
-        var result = previous >= d && domainDiff < day * minDiff;
+        var domainDiff = Math.abs(domain[domain.length - 1] - domain[0]);
+        var isFirst = d == domain[0];
+        var result = isFirst && domainDiff < DAY_MS * minDiff;
         if (result) {
           previous = d;
         }
@@ -720,8 +739,9 @@
       }],
       ["%d %Y-%m", function(d) {
         var domain = xAxis.domain();
-        var domainDiff = domain[domain.length - 1] - domain[0];
-        var result = previous >= d && domainDiff < mn * minDiff;
+        var domainDiff = Math.abs(domain[domain.length - 1] - domain[0]);
+        var isFirst = d == domain[0];
+        var result = isFirst && domainDiff < MONTH_MS * minDiff;
         if (result) {
           previous = d;
         }
@@ -729,8 +749,9 @@
       }],
       ["%m %Y", function(d) {
         var domain = xAxis.domain();
-        var domainDiff = domain[domain.length - 1] - domain[0];
-        var result = previous >= d && domainDiff < y * minDiff;
+        var domainDiff = Math.abs(domain[domain.length - 1] - domain[0]);
+        var isFirst = d == domain[0];
+        var result = isFirst && domainDiff < YEAR_MS * minDiff;
         if (result) {
           previous = d;
         }
@@ -738,91 +759,81 @@
       }],
       ["%Y", function(d) {
         var test = xAxis;
-        var result = previous > d;
-        if (result) {
-          previous = d;
-        }
-        return result;
-      }],
-      ["%L %H:%M:%S", function(d) {
-        var result = moment(previous).utc().seconds() !== moment(d).utc().seconds() && d - previous < s;
-        if (result) {
-          previous = d;
-        }
-        return result;
-      }],
-      ["%L", function(d) {
-        var result = moment(previous).utc().milliseconds() !== moment(d).utc().milliseconds();
+        var domain = xAxis.domain();
+        var isFirst = d == domain[0];
+        var result = isFirst;
         if (result) {
           previous = d;
         }
         return result;
       }],
       ["%S %H:%M", function(d) {
-        var result = moment(previous).utc().minutes() !== moment(d).utc().minutes() && d - previous < m;
+        var previousDiff = Math.abs(d - previous);
+        var result = moment(previous).utc().minutes() !== moment(d).utc().minutes() && previousDiff < MINUTE_MS;
         if (result) {
           previous = d;
         }
         return result;
       }],
       ["%S", function(d) {
-        var result = moment(previous).utc().seconds() !== moment(d).utc().seconds();
+        var previousDiff = Math.abs(d - previous);
+        var result = moment(previous).utc().seconds() !== moment(d).utc().seconds() && previousDiff < MINUTE_MS;
         if (result) {
           previous = d;
         }
         return result;
       }],
       ["%H:%M %Y-%m-%d", function(d) {
-        var result = moment(previous).utc().date() !== moment(d).utc().date() && d - previous < h;
+        var previousDiff = Math.abs(d - previous);
+        var result = moment(previous).utc().date() !== moment(d).utc().date() && previousDiff < WEEK_MS;
         if (result) {
           previous = d;
         }
         return result;
       }],
       ["%H:%M", function(d) {
-        var result = moment(previous).utc().minutes() !== moment(d).utc().minutes();
-        if (result) {
-          previous = d;
-        }
-        return result;
-      }],
-      ["%H:%M %Y-%m-%d", function(d) {
-        var result = moment(previous).utc().date() !== moment(d).utc().date() && d - previous < day;
+        var previousDiff = Math.abs(d - previous);
+        var result = moment(previous).utc().minutes() !== moment(d).utc().minutes() && previousDiff < WEEK_MS;
         if (result) {
           previous = d;
         }
         return result;
       }],
       ["%H:%M", function(d) {
-        var result = moment(previous).utc().hours() !== moment(d).utc().hours();
+        var previousDiff = Math.abs(d - previous);
+        var result = moment(previous).utc().hours() !== moment(d).utc().hours() && previousDiff < WEEK_MS;
         if (result) {
           previous = d;
         }
         return result;
       }],
       ["%d %Y-%m", function(d) {
-        var result = moment(previous).utc().months() !== moment(d).utc().months() && d - previous < mn;
+        var previousDiff = Math.abs(d - previous);
+        var result = moment(previous).utc().months() !== moment(d).utc().months() && previousDiff < MONTH_MS;
         if (result) {
           previous = d;
         }
         return result;
       }],
       ["%d", function(d) {
-        var result = moment(previous).utc().date() !== moment(d).utc().date();
+        var previousDiff = Math.abs(d - previous);
+        var result = moment(previous).utc().date() !== moment(d).utc().date() && previousDiff < MONTH_MS;
         if (result) {
           previous = d;
         }
         return result;
       }],
       ["%m %Y", function(d) {
-        var result = moment(previous).utc().years() !== moment(d).utc().years() && d - previous < y;
+        var previousDiff = Math.abs(d - previous);
+        var result = moment(previous).utc().years() !== moment(d).utc().years() && previousDiff < YEAR_MS;
         if (result) {
           previous = d;
         }
         return result;
       }],
       ["%m", function(d) {
-        var result = moment(previous).utc().months() !== moment(d).utc().months();
+        var previousDiff = Math.abs(d - previous);
+        var result = moment(previous).utc().months() !== moment(d).utc().months() && previousDiff < YEAR_MS;
         if (result) {
           previous = d;
         }
@@ -860,6 +871,16 @@
           huePubSub.publish('charts.state', { updating: true });
           options.onSelectRange($.isNumeric(from) && isTimeline ? new Date(moment(from).valueOf()) : parseInt(from), $.isNumeric(to) && isTimeline ? new Date(moment(to).valueOf()) : parseInt(to)); // FIXME when using pdouble we should not parseInt.
         });
+        if (options.selectedSerie) {
+          _chart.onLegendChange(function (state) {
+            var selectedSerie = options.selectedSerie();
+            var _datum = d3v3.select($(element).find("svg")[0]).datum();
+            for (var i = 0; i < state.disabled.length; i++) {
+              selectedSerie[_datum[i].key] = !state.disabled[i];
+            }
+            options.selectedSerie(selectedSerie);
+          });
+        }
         _chart.xAxis.showMaxMin(false);
         if (isTimeline){
           _chart.xScale(d3v3.time.scale.utc());
@@ -922,8 +943,10 @@
   }
 
   function addLegend(element) {
-    d3v3.select($(element)[0])
-      .append("div")
+    var $el = d3v3.select($(element)[0]);
+    var $div = $el.select('div');
+    if (!$div.size()) {
+      $el.append("div")
         .style("position", "absolute")
         .style("overflow", "auto")
         .style("top", "20px")
@@ -931,6 +954,9 @@
         .style("width", "175px")
         .style("height", "calc(100% - 20px)")
       .append("svg");
+    } else {
+      $div.append("svg");
+    }
   }
   function numeric(_datum) {
     for (var j = 0; j < _datum.length; j++) {
@@ -943,6 +969,25 @@
     return true;
   }
   function handleSelection(_chart, _options, _datum) {
+    var i;
+    var serieEnabled = {};
+    if (_options.selectedSerie) {
+      var selectedSerie = _options.selectedSerie();
+      var enabledCount = 0;
+      for (i = 0; i < _datum.length; i++) {
+        if (!selectedSerie[_datum[i].key]) {
+          _datum[i].disabled = true;
+        } else {
+          enabledCount++;
+        }
+      }
+      if (enabledCount === 0) {
+        for (i = 0; i < Math.min(5, _datum.length); i++) {
+          _datum[i].disabled = false;
+          selectedSerie[_datum[i].key] = true;
+        }
+      }
+    }
     var _isPivot = _options.isPivot != null ? _options.isPivot : false;
     var _hideSelection = typeof _options.hideSelection !== 'undefined' ? typeof _options.hideSelection === 'function' ? _options.hideSelection() : _options.hideSelection : false;
     var _enableSelection = typeof _options.enableSelection !== 'undefined' ? typeof _options.enableSelection === 'function' ? _options.enableSelection() : _options.enableSelection : true;
@@ -1035,6 +1080,16 @@
         }
       });
       _chart.onStateChange(options.onStateChange);
+      if (options.selectedSerie) {
+        _chart.onLegendChange(function (state) {
+          var selectedSerie = options.selectedSerie();
+          var _datum = d3v3.select($(element).find("svg")[0]).datum();
+          for (var i = 0; i < state.disabled.length; i++) {
+            selectedSerie[_datum[i].key] = !state.disabled[i];
+          }
+          options.selectedSerie(selectedSerie);
+        });
+      }
       _chart.multibar.hideable(true);
       _chart.multibar.stacked(typeof options.stacked != "undefined" ? options.stacked : false);
       if (isTimeline) {
@@ -1060,10 +1115,11 @@
       else {
         if (numeric(_datum)) {
           _chart.xAxis.showMaxMin(false).tickFormat(d3v3.format(",0f"));
+          _chart.staggerLabels(false);
         } else if (!_isPivot) {
           _chart.multibar.barColor(nv.utils.defaultColor());
+          _chart.staggerLabels(true);
         }
-        _chart.staggerLabels(true);
       }
       if ($(element).width() < 300 && typeof _chart.showLegend != "undefined") {
         _chart.showLegend(false);
@@ -1089,6 +1145,9 @@
               insertLinebreaks(_chart, d, this);
             });
           }
+          if (options.slot && _chart.recommendedTicks) {
+            options.slot(_chart.recommendedTicks());
+          }
         }).call(_chart);
 
 

+ 10 - 0
desktop/core/src/desktop/static/desktop/js/nv.d3.lineWithBrushChart.js

@@ -73,6 +73,7 @@ nv.models.lineWithBrushChart = function() {
     , selectionHidden = false
     , onSelectRange = null
     , onStateChange = null
+    , onLegendChange = null
     , onChartUpdate = null
     ;
 
@@ -399,6 +400,9 @@ nv.models.lineWithBrushChart = function() {
       legend.dispatch.on('stateChange', function(newState) {
           state = newState;
           dispatch.stateChange(state);
+          if (onLegendChange) {
+            onLegendChange(state);
+          }
           chart.update();
       });
 
@@ -853,6 +857,12 @@ nv.models.lineWithBrushChart = function() {
     return chart;
   };
 
+  chart.onLegendChange = function(_) {
+    if (!arguments.length) return onLegendChange;
+    onLegendChange = _;
+    return chart;
+  };
+
   chart.brush = function(_) {
     if (!arguments.length) return brush;
     brush = _;

+ 37 - 19
desktop/core/src/desktop/static/desktop/js/nv.d3.multiBarWithBrushChart.js

@@ -49,6 +49,7 @@ nv.models.multiBarWithBrushChart = function() {
     , rotateLabels = 0
     , tooltips = true
     , tooltip = null
+    , minTickWidth = 60
     , tooltipSingle = function(value) {
       return '<h3>' + hueUtils.htmlEncode(value.key) + '</h3>' +
         '<p>' + hueUtils.htmlEncode(value.y) + ' on ' + hueUtils.htmlEncode(value.x) + '</p>';
@@ -76,6 +77,7 @@ nv.models.multiBarWithBrushChart = function() {
     , stackedHidden = false
     , onSelectRange = null
     , onStateChange = null
+    , onLegendChange = null
     , onChartUpdate = null
     , selectBars = null
     ;
@@ -140,6 +142,9 @@ nv.models.multiBarWithBrushChart = function() {
           availableHeight = (height || parseInt(container.style('height')) || 400)
                              - margin.top - margin.bottom;
 
+      chart.recommendedTicks = function() {
+        return Math.floor(availableWidth / minTickWidth);
+      };
       chart.update = function() {
         container
             .transition()
@@ -392,9 +397,12 @@ nv.models.multiBarWithBrushChart = function() {
       // Setup Axes
 
       if (showXAxis) {
+          function tickSkip () {
+            return Math.ceil(minTickWidth / xAxis.rangeBand());
+          }
           xAxis
             .scale(x)
-            .ticks( availableChartWidth / 100 )
+            .tickValues(x.domain().filter(function(d, i) { return reduceXTicks && !(i % tickSkip()); }))
             .tickSize(-availableHeight, 0);
 
           g.select('.nv-x.nv-axis')
@@ -437,15 +445,6 @@ nv.models.multiBarWithBrushChart = function() {
                     return getTranslate(0, (i === 0 || totalInBetweenTicks % 2 !== 0) ? staggerDown : staggerUp);
                 });
           }
-
-          if (reduceXTicks)
-            xTicks
-              .filter(function(d,i) {
-                  return i % Math.ceil(data[0].values.length / (availableChartWidth / 100)) !== 0;
-                })
-              .selectAll('text, line')
-              .style('opacity', 0);
-
           if(rotateLabels)
             xTicks
               .selectAll('.tick text')
@@ -479,6 +478,9 @@ nv.models.multiBarWithBrushChart = function() {
       legend.dispatch.on('stateChange', function(newState) {
         state = newState;
         dispatch.stateChange(state);
+        if (onLegendChange != null) {
+          onLegendChange(state);
+        }
         chart.update();
       });
 
@@ -562,13 +564,17 @@ nv.models.multiBarWithBrushChart = function() {
         _l = x.domain()[_l] != undefined ? _l : 0;
         var _from = x.domain()[_l];
 
-        for(_j=0; extent[1] > (_leftEdges[_j] + _width) * 1.01; _j++) {}
-        var _to = x.domain()[_j + 1] != undefined ? x.domain()[_j + 1]: new Date(9999,11,31)
+        for(_j=0; extent[1] > (_leftEdges[_j] + _width); _j++) {}
+        var _to = x.domain()[_j + 1] != undefined ? x.domain()[_j + 1]: filteredData[0].values[filteredData[0].values.length - 1].x_end;
         var range  = [x.range()[_l], x.range()[_j + 1] != undefined ? x.range()[_j + 1] : x.range()[_j] + _width];
         brush.extent(chart.brushExtent = range);
         g.select('.nv-x.nv-brush').call(brush);
         if (onSelectRange != null){
-          onSelectRange(_from, _to);
+          if (_from > _to) {
+            onSelectRange(_to, _from);
+          } else {
+            onSelectRange(_from, _to);
+          }
         }
       }
       function getElByMouse (coords, filterSeries) {
@@ -610,15 +616,17 @@ nv.models.multiBarWithBrushChart = function() {
         }
         var _l, _j;
         var isDescending = _leftEdges[0] < _leftEdges[1];
+        if (!isDescending) {
+          selection = [Math.max(selection[0], selection[1]), Math.min(selection[0], selection[1])]
+        }
         if (isDescending) {
-          for(_l= 0; selection[0] > _leftEdges[_l]; _l++) {}
+          for(_l= 0; selection[0] >= _leftEdges[_l]; _l++) {}
         } else {
           for(_l= _leftEdges.length - 1; selection[0] > _leftEdges[_l]; _l--) {}
         }
 
-        _l = x.range()[_l] != undefined ? _l : 0;
+        _l = x.range()[_l + (isDescending ? -1 : 0)] != undefined ? _l + (isDescending ? -1 : 0) : isDescending ? _leftEdges.length - 1 : 0;
         var _fromRange = x.range()[_l] != undefined ? x.range()[_l] : 0;
-        var _from = x.domain()[_l] != undefined ? x.domain()[_l] : 0;
 
         if (isDescending) {
           for(_j = 0; selection[1] > _leftEdges[_j]; _j++) {}
@@ -626,10 +634,8 @@ nv.models.multiBarWithBrushChart = function() {
           for(_j = _leftEdges.length - 1; selection[1] > _leftEdges[_j]; _j--) {}
         }
         var _toRange = x.range()[_j] != undefined ? x.range()[_j] : x.range()[_leftEdges.length - 1] + _width;
-        var _to = x.domain()[_j] != undefined ? x.domain()[_j]: new Date(9999,11,31); // TODO: Fix for non time data
         return {
-          range: [_fromRange, _toRange],
-          domain: [_from, _to]
+          range: [_fromRange, _fromRange === _toRange ? _fromRange + x.rangeBand() : _toRange]
         };
       }
       function onMouseMove () {
@@ -929,12 +935,24 @@ nv.models.multiBarWithBrushChart = function() {
     return chart;
   };
 
+  chart.onLegendChange = function(_) {
+    if (!arguments.length) return onLegendChange;
+    onLegendChange = _;
+    return chart;
+  };
+
   chart.onChartUpdate = function(_) {
     if (!arguments.length) return onChartUpdate;
     onChartUpdate = _;
     return chart;
   };
 
+  chart.minTickWidth = function() {
+    if (!arguments.length) return minTickWidth;
+    minTickWidth = _;
+    return chart;
+  };
+
   chart.selectBars = function(args) {
     if (!arguments.length) return selectBars;
     if (args && args.rangeValues) {

+ 7 - 5
desktop/libs/dashboard/src/dashboard/api.py

@@ -362,9 +362,10 @@ def new_facet(request):
     facet_label = request.POST.get('label')
     facet_field = request.POST.get('field')
     widget_type = request.POST.get('widget_type')
+    window_size = request.POST.get('window_size')
 
     result['message'] = ''
-    result['facet'] = _create_facet(collection, request.user, facet_id, facet_label, facet_field, widget_type)
+    result['facet'] = _create_facet(collection, request.user, facet_id, facet_label, facet_field, widget_type, window_size)
     result['status'] = 0
   except Exception, e:
     result['message'] = force_unicode(e)
@@ -372,15 +373,16 @@ def new_facet(request):
   return JsonResponse(result)
 
 
-def _create_facet(collection, user, facet_id, facet_label, facet_field, widget_type):
+def _create_facet(collection, user, facet_id, facet_label, facet_field, widget_type, window_size):
   properties = {
     'sort': 'desc',
     'canRange': False,
     'stacked': False,
     'limit': 10,
-    'mincount': 1,
+    'mincount': 0,
     'missing': False,
     'isDate': False,
+    'slot': 0,
     'aggregate': {'function': 'unique', 'formula': '', 'plain_formula': '', 'percentile': 50}
   }
 
@@ -391,11 +393,11 @@ def _create_facet(collection, user, facet_id, facet_label, facet_field, widget_t
     properties['uuid'] = facet_field
     properties['engine'] = 'impala'
     properties['statement'] = 'select * from web_logs limit 50'
-    properties['facets'] = [{'canRange': False, 'field': 'blank', 'limit': 10, 'mincount': 1, 'sort': 'desc', 'aggregate': {'function': 'count'}, 'isDate': False}]
+    properties['facets'] = [{'canRange': False, 'field': 'blank', 'limit': 10, 'mincount': 0, 'sort': 'desc', 'aggregate': {'function': 'count'}, 'isDate': False}]
     facet_type = 'statement'
   else:
     api = get_engine(user, collection)
-    range_properties = _new_range_facet(api, collection, facet_field, widget_type)
+    range_properties = _new_range_facet(api, collection, facet_field, widget_type, window_size)
 
     if range_properties:
       facet_type = 'range'

+ 98 - 52
desktop/libs/dashboard/src/dashboard/facet_builder.py

@@ -22,6 +22,7 @@ import urllib
 import re
 
 from datetime import datetime, timedelta
+from math import ceil
 from math import log
 from time import mktime
 
@@ -30,30 +31,102 @@ from django.utils.translation import ugettext as _
 
 LOG = logging.getLogger(__name__)
 
+MS = 1
+SECOND_MS = 1000 * MS
+MINUTE_MS = SECOND_MS * 60
+HOUR_MS = MINUTE_MS * 60
+DAY_MS = HOUR_MS * 24
+WEEK_MS = DAY_MS * 7
+MONTH_MS = DAY_MS * 30.5
+YEAR_MS = DAY_MS * 365
+TIME_INTERVALS = [
+  {'ms': SECOND_MS * 1, 'coeff': '1', 'unit': 'SECONDS'},
+  {'ms': SECOND_MS * 2, 'coeff': '2', 'unit': 'SECONDS'},
+  {'ms': SECOND_MS * 5, 'coeff': '5', 'unit': 'SECONDS'},
+  {'ms': SECOND_MS * 10, 'coeff': '10', 'unit': 'SECONDS'},
+  {'ms': SECOND_MS * 15, 'coeff': '15', 'unit': 'SECONDS'},
+  {'ms': SECOND_MS * 30, 'coeff': '30', 'unit': 'SECONDS'},
+  {'ms': MINUTE_MS * 1, 'coeff': '1', 'unit': 'MINUTES'},
+  {'ms': MINUTE_MS * 2, 'coeff': '2', 'unit': 'MINUTES'},
+  {'ms': MINUTE_MS * 5, 'coeff': '5', 'unit': 'MINUTES'},
+  {'ms': MINUTE_MS * 10, 'coeff': '10', 'unit': 'MINUTES'},
+  {'ms': MINUTE_MS * 15, 'coeff': '15', 'unit': 'MINUTES'},
+  {'ms': MINUTE_MS * 30, 'coeff': '30', 'unit': 'MINUTES'},
+  {'ms': HOUR_MS * 1, 'coeff': '1', 'unit': 'HOURS'},
+  {'ms': HOUR_MS * 2, 'coeff': '2', 'unit': 'HOURS'},
+  {'ms': HOUR_MS * 4, 'coeff': '4', 'unit': 'HOURS'},
+  {'ms': HOUR_MS * 6, 'coeff': '6', 'unit': 'HOURS'},
+  {'ms': HOUR_MS * 8, 'coeff': '8', 'unit': 'HOURS'},
+  {'ms': HOUR_MS * 12, 'coeff': '12', 'unit': 'HOURS'},
+  {'ms': DAY_MS * 1, 'coeff': '1', 'unit': 'DAYS'},
+  {'ms': DAY_MS * 2, 'coeff': '2', 'unit': 'MONTHS'},
+  {'ms': WEEK_MS * 1, 'coeff': '7', 'unit': 'DAYS'},
+  {'ms': WEEK_MS * 2, 'coeff': '14', 'unit': 'DAYS'},
+  {'ms': MONTH_MS * 1, 'coeff': '1', 'unit': 'MONTHS'},
+  {'ms': MONTH_MS * 2, 'coeff': '2', 'unit': 'MONTHS'},
+  {'ms': MONTH_MS * 3, 'coeff': '3', 'unit': 'MONTHS'},
+  {'ms': MONTH_MS * 6, 'coeff': '6', 'unit': 'MONTHS'},
+  {'ms': YEAR_MS * 1, 'coeff': '1', 'unit': 'YEARS'}];
+TIME_INTERVALS_MS = {
+  'SECONDS': SECOND_MS,
+  'MINUTES': MINUTE_MS,
+  'HOURS': HOUR_MS,
+  'DAYS': DAY_MS,
+  'WEEKS': WEEK_MS,
+  'MONTHS': MONTH_MS,
+  'YEARS': YEAR_MS
+}
 
 def utf_quoter(what):
   return urllib.quote(unicode(what).encode('utf-8'), safe='~@#$&()*!+=:;,.?/\'')
 
 
-def _guess_range_facet(widget_type, solr_api, collection, facet_field, properties, start=None, end=None, gap=None):
+def _guess_range_facet(widget_type, solr_api, collection, facet_field, properties, start=None, end=None, gap=None, window_size=None, slot = 0):
   try:
     stats_json = solr_api.stats(collection['name'], [facet_field])
     stat_facet = stats_json['stats']['stats_fields'][facet_field]
 
-    _compute_range_facet(widget_type, stat_facet, properties, start, end, gap)
+    _compute_range_facet(widget_type, stat_facet, properties, start, end, gap, window_size = window_size, SLOTS = slot)
   except Exception, e:
     print e
     LOG.info('Stats not supported on all the fields, like text: %s' % e)
 
 
-def _compute_range_facet(widget_type, stat_facet, properties, start=None, end=None, gap=None):
-    if widget_type == 'pie-widget' or widget_type == 'pie2-widget':
-      SLOTS = 5
-    elif widget_type == 'facet-widget' or widget_type == 'text-facet-widget':
-      SLOTS = 10
-    else:
-      SLOTS = 100
-      
+def _get_interval(domain_ms, SLOTS):
+  biggest_interval = TIME_INTERVALS[len(TIME_INTERVALS) - 1]
+  biggest_interval_is_too_small = domain_ms / biggest_interval['ms'] > SLOTS
+  if biggest_interval_is_too_small:
+    coeff = ceil(domain_ms / SLOTS)
+    return '+' + coeff + 'YEARS'
+
+  for i in range(len(TIME_INTERVALS) - 2, 0, -1):
+    slots = domain_ms / TIME_INTERVALS[i]['ms']
+    if slots > SLOTS:
+      return '+' + TIME_INTERVALS[i + 1]['coeff'] + TIME_INTERVALS[i + 1]['unit']
+
+  return '+' + TIME_INTERVALS[0]['coeff'] + TIME_INTERVALS[0]['unit'];
+
+def _get_interval_duration(text):
+  regex = re.search('.*-(\d*)(.*)', text)
+
+  if regex:
+    groups = regex.groups()
+    if TIME_INTERVALS_MS[groups[1]]:
+      return TIME_INTERVALS_MS[groups[1]] * int(groups[0])
+  return 0
+
+def _compute_range_facet(widget_type, stat_facet, properties, start=None, end=None, gap=None, SLOTS=0, window_size=None):
+    if SLOTS == 0:
+      if widget_type == 'pie-widget' or widget_type == 'pie2-widget':
+        SLOTS = 5
+      elif widget_type == 'facet-widget' or widget_type == 'text-facet-widget' or widget_type == 'histogram-widget' or widget_type == 'bar-widget' or widget_type == 'bucket-widget' or widget_type == 'timeline-widget':
+        if window_size:
+          SLOTS = int(window_size) / 75 # Value is determined as the thinnest space required to display a timestamp on x axis
+        else:
+          SLOTS = 10
+      else:
+        SLOTS = 100
+
     is_date = widget_type == 'timeline-widget'
 
     if isinstance(stat_facet['min'], numbers.Number):
@@ -93,7 +166,6 @@ def _compute_range_facet(widget_type, stat_facet, properties, start=None, end=No
       except Exception, e:
         LOG.error('Bad date: %s' % e)
         start_ts = datetime.strptime('1970-01-01T00:00:00Z', '%Y-%m-%dT%H:%M:%SZ')
-      start_ts, _ = _round_date_range(start_ts)
       start = start_ts.strftime('%Y-%m-%dT%H:%M:%SZ')
       stats_min = min(stats_min, start)
       if end is None:
@@ -105,46 +177,19 @@ def _compute_range_facet(widget_type, stat_facet, properties, start=None, end=No
       except Exception, e:
         LOG.error('Bad date: %s' % e)
         end_ts = datetime.strptime('2050-01-01T00:00:00Z', '%Y-%m-%dT%H:%M:%SZ')
-      _, end_ts = _round_date_range(end_ts)
       end = end_ts.strftime('%Y-%m-%dT%H:%M:%SZ')
       stats_max = max(stats_max, end)
-      difference = (
-          mktime(end_ts.timetuple()) -
-          mktime(start_ts.timetuple())
-      ) / SLOTS
-
-      if difference < 2:
-        gap = '+1SECONDS'
-      elif difference < 5:
-        gap = '+5SECONDS'
-      elif difference < 30:
-        gap = '+30SECONDS'
-      elif difference < 100:
-        gap = '+1MINUTES'
-      elif difference < 60 * 5:
-        gap = '+5MINUTES'
-      elif difference < 60 * 10:
-        gap = '+10MINUTES'
-      elif difference < 60 * 30:
-        gap = '+30MINUTES'
-      elif difference < 3600:
-        gap = '+1HOURS'
-      elif difference < 3600 * 3:
-        gap = '+3HOURS'
-      elif difference < 3600 * 6:
-        gap = '+6HOURS'
-      elif difference < 3600 * 12:
-        gap = '+12HOURS'
-      elif difference < 3600 * 24:
-        gap = '+1DAYS'
-      elif difference < 3600 * 24 * 7:
-        gap = '+7DAYS'
-      elif difference < 3600 * 24 * 40:
-        gap = '+1MONTHS'
-      elif difference < 3600 * 24 * 40 * 12:
-        gap = '+1YEARS'
-      else:
-        gap = '+10YEARS'
+      domain_ms = (mktime(end_ts.timetuple()) - mktime(start_ts.timetuple())) * 1000
+
+      gap = _get_interval(domain_ms, SLOTS)
+    elif stat_facet['max'] == 'NOW':
+      is_date = True
+      domain_ms = _get_interval_duration(stat_facet['min'])
+      start = stat_facet['min']
+      end = stat_facet['max']
+      stats_min = start
+      stats_max = end
+      gap = _get_interval(domain_ms, SLOTS)
 
     properties.update({
       'min': stats_min,
@@ -152,6 +197,7 @@ def _compute_range_facet(widget_type, stat_facet, properties, start=None, end=No
       'start': start,
       'end': end,
       'gap': gap,
+      'slot': SLOTS,
       'canRange': True,
       'isDate': is_date,
     })
@@ -188,13 +234,13 @@ def _round_thousand_range(n):
 
 def _guess_gap(solr_api, collection, facet, start=None, end=None):
   properties = {}
-  _guess_range_facet(facet['widgetType'], solr_api, collection, facet['field'], properties, start=start, end=end)
+  _guess_range_facet(facet['widgetType'], solr_api, collection, facet['field'], properties, start=start, end=end, slot = facet.get('properties', facet)['slot'])
   return properties
 
 
-def _new_range_facet(solr_api, collection, facet_field, widget_type):
+def _new_range_facet(solr_api, collection, facet_field, widget_type, window_size):
   properties = {}
-  _guess_range_facet(widget_type, solr_api, collection, facet_field, properties)
+  _guess_range_facet(widget_type, solr_api, collection, facet_field, properties, window_size = window_size)
   return properties
 
 

+ 12 - 3
desktop/libs/dashboard/src/dashboard/models.py

@@ -42,7 +42,7 @@ LOG = logging.getLogger(__name__)
 
 NESTED_FACET_FORM = {
     'field': '',
-    'mincount': 1,
+    'mincount': 0,
     'limit': 5,
     'sort': 'desc',
     'canRange': False,
@@ -144,6 +144,8 @@ class Collection2(object):
         properties['domain'] = {'blockParent': [], 'blockChildren': []}
       if 'missing' not in properties:
         properties['missing'] = False
+      if 'slot' not in properties:
+        properties['slot'] = 0
 
       if properties.get('facets'):
         for facet_facet in properties['facets']:
@@ -163,7 +165,7 @@ class Collection2(object):
       if facet['widgetType'] == 'map-widget' and facet['type'] == 'field':
         facet['type'] = 'pivot'
         properties['facets'] = []
-        properties['facets_form'] = {'field': '', 'mincount': 1, 'limit': 5}
+        properties['facets_form'] = {'field': '', 'mincount': 0, 'limit': 5}
 
       if 'compare' not in properties:
         properties['compare'] = COMPARE_FACET
@@ -370,14 +372,21 @@ def range_pair(field, cat, fq_filter, iterable, end, collection_facet):
   next(to, None)
   counts = iterable[1::2]
   total_counts = counts.pop(0) if collection_facet['properties']['sort'] == 'asc' else 0
+  isDate = collection_facet['properties']['isDate']
 
   for element in a:
     next(to, None)
     to_value = next(to, end)
     count = next(a)
 
+    if collection_facet['properties']['sort'] == 'asc':
+      from_value = to_value
+      to_value = element
+    else:
+      from_value = element
+
     pairs.append({
-        'field': field, 'from': element, 'value': count, 'to': to_value, 'selected': element in selected_values,
+        'field': field, 'from': from_value if isDate else int(element), 'value': count, 'to': to_value if isDate else int(to_value), 'selected': element in selected_values,
         'exclude': all([f['exclude'] for f in fq_filter if f['value'] == element]),
         'is_single_unit_gap': is_single_unit_gap,
         'total_counts': total_counts,

+ 5 - 3
desktop/libs/dashboard/src/dashboard/static/dashboard/js/search.ko.js

@@ -1137,6 +1137,7 @@ var Collection = function (vm, collection) {
         "id": facet_json.widget_id,
         "label": facet_json.name,
         "field": facet_json.name,
+        'window_size': $(window).width() - 600, // TODO: Find a better way to get facet width.
         "widget_type": facet_json.widgetType
       }, function (data) {
         if (data.status == 0) {
@@ -1167,7 +1168,7 @@ var Collection = function (vm, collection) {
       });
       facet.properties.facets_form.field = null;
       facet.properties.facets_form.limit = 5;
-      facet.properties.facets_form.mincount = 1;
+      facet.properties.facets_form.mincount = 0;
       facet.properties.facets_form.aggregate = 'count';
     } else {
       if (typeof facet.properties.facets_form.field != 'undefined') {
@@ -1179,7 +1180,7 @@ var Collection = function (vm, collection) {
         });
         facet.properties.facets_form.field(null);
         facet.properties.facets_form.limit(5);
-        facet.properties.facets_form.mincount(1);
+        facet.properties.facets_form.mincount(0);
         facet.properties.facets_form.aggregate ? facet.properties.facets_form.aggregate('count') : '';
       }
     }
@@ -1217,7 +1218,7 @@ var Collection = function (vm, collection) {
 
     facet.properties.facets_form.field(null);
     facet.properties.facets_form.limit(5);
-    facet.properties.facets_form.mincount(1);
+    facet.properties.facets_form.mincount(0);
     facet.properties.facets_form.sort('desc');
 
     facet.properties.facets_form.aggregate.formula('');
@@ -2555,6 +2556,7 @@ var SearchViewModel = function (collection_json, query_json, initial_json, has_g
         facet.hideStacked = ko.computed(function () {
           return !facet.extraSeries() || !facet.extraSeries().length;
         });
+        facet.selectedSerie = ko.observable({});
         facet.resultHash(_hash);
         facet.filterHash(_filterHash);
         facet.has_data(true);

+ 64 - 40
desktop/libs/dashboard/src/dashboard/templates/common_search.mako

@@ -1245,12 +1245,6 @@ ${ dashboard.layout_skeleton(suffix='search') }
                        optionsValue: 'value',
                        value: properties.timelineChartType">
         </select>&nbsp;
-        <span class="facet-field-label">${ _('Interval') }</span>
-        <select class="input-small" data-bind="options: $root.intervalOptions,
-                       optionsText: 'label',
-                       optionsValue: 'value',
-                       value: properties.gap">
-        </select>&nbsp;
       </span>
       <span class="facet-field-label">${ _('Zoom') }</span>
       <a href="javascript:void(0)" data-bind="click: $root.collection.rangeZoomOut"><i class="fa fa-search-minus"></i> ${ _('reset') }</a>
@@ -1264,7 +1258,9 @@ ${ dashboard.layout_skeleton(suffix='search') }
         type: $root.collection.getFacetById($parent.id()).properties.timelineChartType,
         hideSelection: true,
         hideStacked: hideStacked,
+        selectedSerie: selectedSerie,
         fqs: $root.query.fqs,
+        slot: $root.collection.getFacetById($parent.id()).properties.slot,
         onSelectRange: function(from, to){ $root.collection.selectTimelineFacet({from: from, to: to, cat: field, widget_id: $parent.id()}) },
         onStateChange: function(state){ $root.collection.getFacetById($parent.id()).properties.stacked(state.stacked); $root.collection.getFacetById($parent.id()).properties.enableSelection(state.selectionEnabled); },
         onClick: function(d){ $root.query.selectRangeFacet({count: d.obj.value, widget_id: $parent.id(), from: d.obj.from, to: d.obj.to, cat: d.obj.field}) },
@@ -1301,12 +1297,6 @@ ${ dashboard.layout_skeleton(suffix='search') }
                        optionsValue: 'value',
                        value: properties.timelineChartType">
         </select>&nbsp;
-        <span class="facet-field-label">${ _('Interval') }</span>
-        <select class="input-small" data-bind="options: $root.intervalOptions,
-                       optionsText: 'label',
-                       optionsValue: 'value',
-                       value: properties.facets()[0].gap">
-        </select>&nbsp;
       </span>
     </div>
 
@@ -1344,6 +1334,7 @@ ${ dashboard.layout_skeleton(suffix='search') }
       enableSelection: true,
       hideSelection: true,
       hideStacked: hideStacked,
+      slot: $root.collection.getFacetById($parent.id()).properties.slot,
       transformer: ($data.type == 'range-up' ? barChartRangeUpDataTransformer : barChartDataTransformer),
       onStateChange: function(state){ $root.collection.getFacetById($parent.id()).properties.stacked(state.stacked); },
       onClick: function(d) {
@@ -1610,12 +1601,13 @@ ${ dashboard.layout_skeleton(suffix='search') }
           <!-- ko with: $parent -->
 
           <!-- ko if: dimension() == 1 -->
-            <div data-bind="barChart: {datum: {counts: counts(), widget_id: $parent.id(), label: label()}, stacked: $root.collection.getFacetById($parent.id()).properties.stacked(), field: field, label: label(),
+            <div data-bind="barChart: {datum: {counts: counts(), extraSeries: extraSeries(), widget_id: $parent.id(), label: label()}, stacked: $root.collection.getFacetById($parent.id()).properties.stacked(), field: field, label: label(),
               fqs: $root.query.fqs,
               enableSelection: true,
               hideSelection: true,
               hideStacked: hideStacked,
-              transformer: ($data.type == 'range-up' ? barChartRangeUpDataTransformer : barChartDataTransformer),
+              slot: $root.collection.getFacetById($parent.id()).properties.slot,
+              transformer: barChartDataTransformer2,
               onStateChange: function(state){ $root.collection.getFacetById($parent.id()).properties.stacked(state.stacked); },
               onClick: function(d) {
                 if (d.obj.field != undefined) {
@@ -1642,7 +1634,9 @@ ${ dashboard.layout_skeleton(suffix='search') }
               enableSelection: true,
               hideSelection: true,
               hideStacked: hideStacked,
+              slot: $root.collection.getFacetById($parent.id()).properties.slot,
               transformer: pivotChartDataTransformer,
+              onSelectRange: function(from, to){ $root.collection.selectTimelineFacet2({from: from, to: to, cat: field, widget_id: $parent.id()}) },
               onStateChange: function(state){ $root.collection.getFacetById($parent.id()).properties.stacked(state.stacked); },
               onClick: function(d) {
                 $root.query.togglePivotFacet({facet: d.obj, widget_id: id()});
@@ -1660,6 +1654,8 @@ ${ dashboard.layout_skeleton(suffix='search') }
             enableSelection: true,
             hideSelection: true,
             hideStacked: hideStacked,
+            selectedSerie: selectedSerie,
+            slot: $root.collection.getFacetById($parent.id()).properties.facets()[0].slot,
             onSelectRange: function(from, to){ $root.collection.selectTimelineFacet2({from: from, to: to, cat: field, widget_id: $parent.id()}) },
             onStateChange: function(state){ $root.collection.getFacetById($parent.id()).properties.stacked(state.stacked); },
             onClick: function(d){ $root.query.selectRangeFacet({count: d.obj.value, widget_id: $parent.id(), from: d.obj.from, to: d.obj.to, cat: d.obj.field}) },
@@ -1691,6 +1687,8 @@ ${ dashboard.layout_skeleton(suffix='search') }
             enableSelection: true,
             hideSelection: true,
             hideStacked: hideStacked,
+            selectedSerie: selectedSerie,
+            slot: $root.collection.getFacetById($parent.id()).properties.slot,
             onSelectRange: function(from, to){ $root.collection.selectTimelineFacet2({from: from, to: to, cat: field, widget_id: $parent.id()}) },
             onStateChange: function(state){ $root.collection.getFacetById($parent.id()).properties.stacked(state.stacked); },
             onClick: function(d){ $root.query.selectRangeFacet({count: d.obj.value, widget_id: $parent.id(), from: d.obj.from, to: d.obj.to, cat: d.obj.field}) },
@@ -1807,17 +1805,6 @@ ${ dashboard.layout_skeleton(suffix='search') }
         </select>
       </div>
       <!-- /ko -->
-      <!-- ko if: properties.isDate -->
-        <div class="inline-block" style="padding-bottom: 10px; padding-right: 20px">
-          <span class="facet-field-label">${ _('Interval') }</span>
-          <select class="input-small" data-bind="options: $root.intervalOptions,
-                         optionsText: 'label',
-                         optionsValue: 'value',
-                         value: properties.facets()[0].gap">
-          </select>
-        </div>
-      <!-- /ko -->
-
       </div>
       <div class="clearfix"></div>
     </div>
@@ -2076,7 +2063,9 @@ ${ dashboard.layout_skeleton(suffix='search') }
           enableSelection: true,
           hideSelection: true,
           fqs: $root.query.fqs,
+          slot: $root.collection.getFacetById($parent.id()).properties.slot,
           transformer: pivotChartDataTransformer,
+          onSelectRange: function(from, to){ $root.collection.selectTimelineFacet2({from: from, to: to, cat: field, widget_id: $parent.id()}) },
           onStateChange: function(state){ $root.collection.getFacetById($parent.id()).properties.stacked(state.stacked); },
           onClick: function(d) {
             $root.query.togglePivotFacet({facet: d.obj, widget_id: id()});
@@ -3404,6 +3393,9 @@ function _barChartDataTransformer(rawDatum, isUp) {
     key: rawDatum.label,
     values: _data
   });
+  _data.sort(function (a, b) {
+    return a.x - b.x;
+  });
 
   return _datum;
 }
@@ -3412,6 +3404,10 @@ function barChartDataTransformer(rawDatum) {
   return _barChartDataTransformer(rawDatum, false);
 }
 
+function barChartDataTransformer2(rawDatum) {
+  return _timelineChartDataTransformer(rawDatum, false);
+}
+
 function barChartRangeUpDataTransformer(rawDatum) {
   return _barChartDataTransformer(rawDatum, true);
 }
@@ -3495,6 +3491,11 @@ function pivotChartDataTransformer(rawDatum) {
     });
   });
 
+  _categories.forEach(function (category) {
+    category.values.sort(function (a, b) {
+      return a.x - b.x;
+    });
+  });
   return _categories;
 }
 
@@ -3528,24 +3529,42 @@ function lineChartDataTransformer(rawDatum) {
   return _datum;
 }
 
-function timelineChartDataTransformer(rawDatum) {
+function timelineChartDataTransformer (rawDatum) {
+  return _timelineChartDataTransformer(rawDatum, true);
+}
+
+function _timelineChartDataTransformer(rawDatum, isDate) {
   var _datum = [];
   var _data = [];
 
+  function getValue (value) {
+    return isDate ? new Date(moment(value).valueOf()) : value
+  }
+  function getNumericValue(value) {
+    return value && value.getTime ? value.getTime() : value;
+  }
+
   $(rawDatum.counts).each(function (cnt, item) {
+    item.widget_id = rawDatum.widget_id;
     _data.push({
       series: 0,
-      x: new Date(moment(item.from ? item.from : item.value).valueOf()), // When started from a non timeline widget
-      x_end: item.to && new Date(moment(item.to).valueOf()),
-      y: item.from ? item.value : item.count,
+      x: getValue(item.from ? item.from : item.value), // When started from a non timeline widget
+      x_end: item.to && getValue(item.to),
+      y: item.from !== undefined ? item.value : item.count,
       obj: item
     });
   });
 
-  _datum.push({
-    key: rawDatum.label,
-    values: _data
-  });
+  if (_data.length) {
+    _datum.push({
+      key: rawDatum.label,
+      values: _data
+    });
+
+    _data.sort(function (a, b) {
+      return a.x - b.x;
+    });
+  }
 
   // In Solr, all series might not have values on all data point. If a value is 0 or if it's been filtered by the limit option, solr does not return a value.
   // Unfortunately, this causes the following issues in the chart:
@@ -3556,7 +3575,7 @@ function timelineChartDataTransformer(rawDatum) {
   //Preprocess to obtain all the x values.
   var values = rawDatum.extraSeries.reduce(function (values, serie) {
     serie.counts.reduce(function (values, item) {
-      var x = new Date(moment(item.from ? item.from : item.value).valueOf()).getTime();
+      var x = getNumericValue(getValue(item.from ? item.from : item.value));
       if (!values[x]) {
         values[x] = {};
       }
@@ -3568,7 +3587,10 @@ function timelineChartDataTransformer(rawDatum) {
 
 
   // If multi query
-  var keys = Object.keys(values).sort();
+  var keys = Object.keys(values);
+  if (isDate) {
+    keys.sort();
+  }
   $(rawDatum.extraSeries).each(function (cnt, serie) {
     if (cnt == 0) {
       _datum = [];
@@ -3578,22 +3600,24 @@ function timelineChartDataTransformer(rawDatum) {
     $(keys).each(function (cnt, key) {
       if (values[key][serie.label]) {
         var item = values[key][serie.label];
+        item.widget_id = rawDatum.widget_id;
         _data.push({
           series: cnt + 1,
-          x: new Date(moment(item.from ? item.from : item.value).valueOf()), // When started from a non timeline widget
-          x_end: item.to && new Date(moment(item.to).valueOf()),
-          y: item.from ? item.value : item.count,
+          x: getValue(item.from ? item.from : item.value), // When started from a non timeline widget
+          x_end: item.to && getValue(item.to),
+          y: item.from !== undefined ? item.value : item.count,
           obj: item
         });
       } else {
         var keys = Object.keys(values[key]);
         var item = keys[0] && values[key][keys[0]];
+        item.widget_id = rawDatum.widget_id;
         var copy = JSON.parse(JSON.stringify(item));
         copy.value = 0;
         _data.push({
           series: cnt + 1,
-          x: new Date(moment(item.from ? item.from : item.value).valueOf()),
-          x_end: item.to && new Date(moment(item.to).valueOf()),
+          x: getValue(item.from ? item.from : item.value),
+          x_end: item.to && getValue(item.to),
           y: copy.value,
           obj: copy
         });

+ 29 - 7
desktop/libs/libsolr/src/libsolr/api.py

@@ -124,7 +124,7 @@ class SolrApi(object):
               'mincount': int(facet['properties']['mincount'])
           }
 
-          if timeFilter and timeFilter['time_field'] == facet['field'] and (facet['id'] not in timeFilter['time_filter_overrides'] or facet['widgetType'] != 'histogram-widget'):
+          if facet['properties']['canRange'] or timeFilter and timeFilter['time_field'] == facet['field'] and (facet['id'] not in timeFilter['time_filter_overrides'] or facet['widgetType'] != 'histogram-widget'):
             keys.update(self._get_time_filter_query(timeFilter, facet))
 
           params += (
@@ -145,7 +145,7 @@ class SolrApi(object):
         elif facet['type'] == 'nested':
           _f = {}
           if facet['properties']['facets']:
-            self._n_facet_dimension(facet, _f, facet['properties']['facets'], 1, timeFilter)
+            self._n_facet_dimension(facet, _f, facet['properties']['facets'], 1, timeFilter, can_range = facet['properties']['canRange'])
 
           if facet['properties'].get('domain'):
             if facet['properties']['domain'].get('blockParent') or facet['properties']['domain'].get('blockChildren'):
@@ -273,7 +273,7 @@ class SolrApi(object):
     return self._get_json(response)
 
 
-  def _n_facet_dimension(self, widget, _f, facets, dim, timeFilter):
+  def _n_facet_dimension(self, widget, _f, facets, dim, timeFilter, can_range=None):
     facet = facets[0]
     f_name = 'dim_%02d:%s' % (dim, facet['field'])
 
@@ -296,13 +296,14 @@ class SolrApi(object):
           'type': 'terms',
           'field': '%(field)s' % facet,
           'limit': int(facet.get('limit', 10)),
-          'mincount': int(facet['mincount']),
           'numBuckets': True,
           'allBuckets': True,
           'sort': sort,
           'missing': facet.get('missing', False)
           #'prefix': '' # Forbidden on numeric fields
       }
+      if int(facet['mincount']):
+        _f[f_name]['mincount'] = int(facet['mincount']) # Forbidden on n > 0 field if mincount = 0
 
       if 'start' in facet and not facet.get('type') == 'field':
         _f[f_name].update({
@@ -313,7 +314,7 @@ class SolrApi(object):
         })
 
         # Only on dim 1 currently
-        if timeFilter and timeFilter['time_field'] == facet['field'] and (widget['id'] not in timeFilter['time_filter_overrides']): # or facet['widgetType'] != 'bucket-widget'):
+        if can_range or (timeFilter and timeFilter['time_field'] == facet['field'] and (widget['id'] not in timeFilter['time_filter_overrides'])): # or facet['widgetType'] != 'bucket-widget'):
           facet['widgetType'] = widget['widgetType']
           _f[f_name].update(self._get_time_filter_query(timeFilter, facet))
 
@@ -937,10 +938,19 @@ class SolrApi(object):
     return props
 
   def _get_time_filter_query(self, timeFilter, facet):
-    if 'fixed' in timeFilter:
+    properties = facet.get('properties', facet)
+    if not timeFilter:
+      props = {}
+      stat_facet = {'min': properties['start'], 'max': properties['end']}
+      _compute_range_facet(facet['widgetType'], stat_facet, props, stat_facet['min'], stat_facet['max'],
+                           SLOTS=properties['slot'])
+      return {
+        'gap': '%(gap)s' % props,  # add a 'auto'
+      }
+    elif 'fixed' in timeFilter or properties['slot'] != 0:
       props = {}
       stat_facet = {'min': timeFilter['from'], 'max': timeFilter['to']}
-      _compute_range_facet(facet['widgetType'], stat_facet, props, stat_facet['min'], stat_facet['max'])
+      _compute_range_facet(facet['widgetType'], stat_facet, props, stat_facet['min'], stat_facet['max'], SLOTS = properties['slot'])
       gap = props['gap']
       unit = re.split('\d+', gap)[1]
       return {
@@ -1074,6 +1084,7 @@ GAPS = {
         'bucket-widget': {'coeff': '+3', 'unit': 'SECONDS'}, # ~100 slots
         'bar-widget': {'coeff': '+3', 'unit': 'SECONDS'}, # ~100 slots
         'facet-widget': {'coeff': '+1', 'unit': 'MINUTES'}, # ~10 slots
+        'pie-widget': {'coeff': '+1', 'unit': 'MINUTES'} # ~10 slots
     },
     '30MINUTES': {
         'histogram-widget': {'coeff': '+20', 'unit': 'SECONDS'},
@@ -1081,6 +1092,7 @@ GAPS = {
         'bucket-widget': {'coeff': '+20', 'unit': 'SECONDS'},
         'bar-widget': {'coeff': '+20', 'unit': 'SECONDS'},
         'facet-widget': {'coeff': '+5', 'unit': 'MINUTES'},
+        'pie-widget': {'coeff': '+5', 'unit': 'MINUTES'},
     },
     '1HOURS': {
         'histogram-widget': {'coeff': '+30', 'unit': 'SECONDS'},
@@ -1088,6 +1100,7 @@ GAPS = {
         'bucket-widget': {'coeff': '+30', 'unit': 'SECONDS'},
         'bar-widget': {'coeff': '+30', 'unit': 'SECONDS'},
         'facet-widget': {'coeff': '+10', 'unit': 'MINUTES'},
+        'pie-widget': {'coeff': '+10', 'unit': 'MINUTES'}
     },
     '12HOURS': {
         'histogram-widget': {'coeff': '+7', 'unit': 'MINUTES'},
@@ -1095,6 +1108,7 @@ GAPS = {
         'bucket-widget': {'coeff': '+7', 'unit': 'MINUTES'},
         'bar-widget': {'coeff': '+7', 'unit': 'MINUTES'},
         'facet-widget': {'coeff': '+1', 'unit': 'HOURS'},
+        'pie-widget': {'coeff': '+1', 'unit': 'HOURS'}
     },
     '1DAYS': {
         'histogram-widget': {'coeff': '+15', 'unit': 'MINUTES'},
@@ -1102,6 +1116,7 @@ GAPS = {
         'bucket-widget': {'coeff': '+15', 'unit': 'MINUTES'},
         'bar-widget': {'coeff': '+15', 'unit': 'MINUTES'},
         'facet-widget': {'coeff': '+3', 'unit': 'HOURS'},
+        'pie-widget': {'coeff': '+3', 'unit': 'HOURS'}
     },
     '2DAYS': {
         'histogram-widget': {'coeff': '+30', 'unit': 'MINUTES'},
@@ -1109,6 +1124,7 @@ GAPS = {
         'bucket-widget': {'coeff': '+30', 'unit': 'MINUTES'},
         'bar-widget': {'coeff': '+30', 'unit': 'MINUTES'},
         'facet-widget': {'coeff': '+6', 'unit': 'HOURS'},
+        'pie-widget': {'coeff': '+6', 'unit': 'HOURS'}
     },
     '7DAYS': {
         'histogram-widget': {'coeff': '+3', 'unit': 'HOURS'},
@@ -1116,6 +1132,7 @@ GAPS = {
         'bucket-widget': {'coeff': '+3', 'unit': 'HOURS'},
         'bar-widget': {'coeff': '+3', 'unit': 'HOURS'},
         'facet-widget': {'coeff': '+1', 'unit': 'DAYS'},
+        'pie-widget': {'coeff': '+1', 'unit': 'DAYS'}
     },
     '1MONTHS': {
         'histogram-widget': {'coeff': '+12', 'unit': 'HOURS'},
@@ -1123,6 +1140,7 @@ GAPS = {
         'bucket-widget': {'coeff': '+12', 'unit': 'HOURS'},
         'bar-widget': {'coeff': '+12', 'unit': 'HOURS'},
         'facet-widget': {'coeff': '+5', 'unit': 'DAYS'},
+        'pie-widget': {'coeff': '+5', 'unit': 'DAYS'}
     },
     '3MONTHS': {
         'histogram-widget': {'coeff': '+1', 'unit': 'DAYS'},
@@ -1130,6 +1148,7 @@ GAPS = {
         'bucket-widget': {'coeff': '+1', 'unit': 'DAYS'},
         'bar-widget': {'coeff': '+1', 'unit': 'DAYS'},
         'facet-widget': {'coeff': '+30', 'unit': 'DAYS'},
+        'pie-widget': {'coeff': '+30', 'unit': 'DAYS'}
     },
     '1YEARS': {
         'histogram-widget': {'coeff': '+3', 'unit': 'DAYS'},
@@ -1137,6 +1156,7 @@ GAPS = {
         'bucket-widget': {'coeff': '+3', 'unit': 'DAYS'},
         'bar-widget': {'coeff': '+3', 'unit': 'DAYS'},
         'facet-widget': {'coeff': '+12', 'unit': 'MONTHS'},
+        'pie-widget': {'coeff': '+12', 'unit': 'MONTHS'}
     },
     '2YEARS': {
         'histogram-widget': {'coeff': '+7', 'unit': 'DAYS'},
@@ -1144,6 +1164,7 @@ GAPS = {
         'bucket-widget': {'coeff': '+7', 'unit': 'DAYS'},
         'bar-widget': {'coeff': '+7', 'unit': 'DAYS'},
         'facet-widget': {'coeff': '+3', 'unit': 'MONTHS'},
+        'pie-widget': {'coeff': '+3', 'unit': 'MONTHS'}
     },
     '10YEARS': {
         'histogram-widget': {'coeff': '+1', 'unit': 'MONTHS'},
@@ -1151,5 +1172,6 @@ GAPS = {
         'bucket-widget': {'coeff': '+1', 'unit': 'MONTHS'},
         'bar-widget': {'coeff': '+1', 'unit': 'MONTHS'},
         'facet-widget': {'coeff': '+1', 'unit': 'YEARS'},
+        'pie-widget': {'coeff': '+1', 'unit': 'YEARS'}
     }
 }