Browse Source

HUE-8178 [charts] Modified selection & filtering timeline behavior

jdesjean 7 years ago
parent
commit
85b8501b91

+ 9 - 0
desktop/core/src/desktop/static/desktop/css/nv.d3.css

@@ -69,3 +69,12 @@ svg:not(.leaflet-zoom-animated) {
 .nvtooltip p {
   font-size: 12px;
 }
+.nvtooltip b {
+  width: 100px;
+  display: inline-block;
+  text-align: right;
+  direction: rtl;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  vertical-align: middle;
+}

+ 145 - 290
desktop/core/src/desktop/static/desktop/js/ko.charts.js

@@ -118,7 +118,7 @@
             _d3.selectAll(".nv-slice").on("click",
                 function (d, i) {
                   if (typeof _options.onClick != "undefined") {
-                    chartsUpdatingState();
+                    huePubSub.publish('charts.state', { updating: true });
                     _options.onClick(d);
                   }
                 });
@@ -149,7 +149,7 @@
               }
             });
           }
-        chartsNormalState();
+        huePubSub.publish('charts.state');
       }
       else if (_data.length > 0) {
         ko.bindingHandlers.pieChart.init(element, valueAccessor);
@@ -198,24 +198,8 @@
         if (_chart.multibar){
           _chart.multibar.stacked(typeof _options.stacked != "undefined" ? _options.stacked : false);
         }
-
-        var _isDiscrete = false;
-        for (var j = 0; j < _datum.length; j++) {
-          for (var i = 0; i < _datum[j].values.length; i++) {
-            if (isNaN(_datum[j].values[i].x * 1)) {
-              _isDiscrete = true;
-              break;
-            }
-          }
-        }
-
-        var _isPivot = _options.isPivot != null ? _options.isPivot : false;
-
-        if ((_isDiscrete && $(element).data('chart_type') !== 'discrete_bar') || (!_isDiscrete && $(element).data('chart_type') === 'discrete_bar') || (_isPivot && !$(element).data('chart_pivot')) || (!_isPivot && $(element).data('chart_pivot'))){
-          ko.bindingHandlers.barChart.init(element, valueAccessor);
-        }
-        else {
-          window.setTimeout(function () {
+        window.setTimeout(function () {
+          handleSelection(_chart, _options, _datum);
           var _d3 = d3v3.select($(element).find("svg")[0]);
           _d3.datum(_datum)
             .transition().duration(150)
@@ -224,46 +208,8 @@
                 _options.onComplete();
               }
             }).call(_chart);
-
-          if (_chart.selectBars) {
-            var _field = (typeof _options.field == "function") ? _options.field() : _options.field;
-            $.each(_options.fqs(), function (cnt, item) {
-              if (item.id() == _options.datum.widget_id) {
-                if (item.field() == _field) {
-                  if (item.properties) {
-                    _chart.selectBars({
-                      singleValues: $.map(item.filter(), function (it) {
-                        return it.value();
-                      }),
-                      rangeValues: $.map(item.properties(), function (it) {
-                        return {from: it.from(), to: it.to()};
-                      })
-                    });
-                  }
-                  else {
-                    _chart.selectBars($.map(item.filter(), function (it) {
-                      return it.value();
-                    }));
-                  }
-                }
-                if (Array.isArray(item.field())) {
-                  _chart.selectBars({
-                    field: item.field(),
-                    selected: $.map(item.filter(), function (it) {
-                      return {values: it.value()};
-                    })
-                  });
-                }
-              }
-            });
-          }
-          chartsNormalState();
+          huePubSub.publish('charts.state');
         }, 0);
-
-        }
-      }
-      else if (_datum.length > 0) {
-        ko.bindingHandlers.barChart.init(element, valueAccessor);
       }
     }
   };
@@ -308,16 +254,7 @@
           if (_chart.multibar) {
             _chart.multibar.stacked(typeof _options.stacked != "undefined" ? _options.stacked : false);
           }
-          var enableSelection = true;
-          if (typeof _options.enableSelection !== 'undefined') {
-            enableSelection = _options.enableSelection;
-          }
-          if (_datum.length > 0 && _datum[0].values.length > 10 && enableSelection) {
-            _chart.enableSelection();
-          }
-          else {
-            _chart.disableSelection();
-          }
+          handleSelection(_chart, _options, _datum);
           var _d3 = d3v3.select($(element).find("svg")[0]);
           _d3.datum(_datum)
             .transition().duration(150)
@@ -327,34 +264,11 @@
               }
             }).call(_chart);
           _d3.selectAll("g.nv-x.nv-axis g text").each(function (d) {
-            insertLinebreaks(d, this);
+            insertLinebreaks(_chart, d, this);
           });
-          if (_chart.selectBars) {
-            var _field = (typeof _options.field == "function") ? _options.field() : _options.field;
-            $.each(_options.fqs(), function (cnt, item) {
-              if (item.id() == _options.datum.widget_id) {
-                if (item.field() == _field) {
-                  _chart.selectBars($.map(item.filter(), function (it) {
-                    return it.value();
-                  }));
-                }
-                if (Array.isArray(item.field())) {
-                  _chart.selectBars({
-                    field: item.field(),
-                    selected: $.map(item.filter(), function (it) {
-                      return {values: it.value()};
-                    })
-                  });
-                }
-              }
-            });
-          }
-          chartsNormalState();
+          huePubSub.publish('charts.state');
         }, 0);
       }
-      else if (_datum.length > 0) {
-        ko.bindingHandlers.timelineChart.init(element, valueAccessor);
-      }
     }
   };
 
@@ -375,7 +289,7 @@
             _chart.xAxis.tickFormat(function(d) { return d3v3.time.format("%Y-%m-%d %H:%M:%S")(new Date(d)); })
             _chart.onChartUpdate(function () {
               _d3.selectAll("g.nv-x.nv-axis g text").each(function (d){
-                insertLinebreaks(d, this);
+                insertLinebreaks(_chart, d, this);
               });
             });
           }
@@ -386,7 +300,7 @@
                 _options.onComplete();
               }
             }).call(_chart);
-          chartsNormalState();
+          huePubSub.publish('charts.state');
         }, 0);
       }
       else if (_datum.length > 0) {
@@ -579,7 +493,7 @@
           legendData: _legend,
           onClick: function (data) {
             if (typeof options.onClick != "undefined") {
-              chartsUpdatingState();
+              huePubSub.publish('charts.state', { updating: true });
               options.onClick(data);
             }
           },
@@ -656,7 +570,7 @@
         }, 200);
       });
 
-      chartsNormalState();
+      huePubSub.publish('charts.state');
     },
     init: function (element, valueAccessor) {
       ko.bindingHandlers.mapChart.render(element, valueAccessor);
@@ -732,11 +646,11 @@
     }
   };
 
-  var insertLinebreaks = function (d, ref) {
+  var insertLinebreaks = function (_chart, d, ref) {
     var _el = d3v3.select(ref);
     var _mom = moment(d);
     if (_mom != null && _mom.isValid()) {
-      var _words = _mom.format("HH:mm:ss YYYY-MM-DD").split(" ");
+      var _words = _chart.xAxis.tickFormat()(d).split(" ");
       _el.text("");
       for (var i = 0; i < _words.length; i++) {
         var tspan = _el.append("tspan").text(_words[i]);
@@ -758,44 +672,40 @@
       $(element).find("svg").empty();
     }
 
-    var _hideSelection = options.hideSelection != null ? options.hideSelection : false;
-
     if ($(element).is(":visible")) {
       nv.addGraph(function () {
         var _chart = nv.models.lineWithBrushChart();
         $(element).data("chart", _chart);
         _chart.transitionDuration(0);
-        var enableSelection = true;
-        if (typeof options.enableSelection !== 'undefined') {
-          enableSelection = options.enableSelection;
-        }
-        if (_datum.length > 0 && _datum[0].values.length > 10 && enableSelection) {
-          _chart.enableSelection();
-        }
-        if (_hideSelection) {
-          _chart.hideSelection();
-        }
+        _chart.convert = function (d) {
+          return isTimeline ? new Date(moment(values1[0].obj.from).valueOf()) : parseFloat(d);
+        };
         if (options.showControls != null) {
           _chart.showControls(false);
         }
         _chart.onSelectRange(function (from, to) {
-          chartsUpdatingState();
-          options.onSelectRange($.isNumeric(from) ? new Date(moment(from).valueOf()) : from, $.isNumeric(to) ? new Date(moment(to).valueOf()) : to);
+          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.
         });
         _chart.xAxis.showMaxMin(false);
         if (isTimeline){
-          _chart.xAxis.tickFormat(function(d) { return d3v3.time.format("%Y-%m-%d %H:%M:%S")(new Date(d)); })
+          _chart.xAxis.tickFormat(function(d) {
+            return moment(d).utc().format("YYYY-MM-DD HH:mm:ss");
+          });
           _chart.onChartUpdate(function () {
             _d3.selectAll("g.nv-x.nv-axis g text").each(function (d){
-              insertLinebreaks(d, this);
+              insertLinebreaks(_chart, d, this);
             });
           });
         }
 
         _chart.yAxis
             .tickFormat(d3v3.format(",0f"));
-
+        handleSelection(_chart, options, _datum);
         var _d3 = ($(element).find("svg").length > 0) ? d3v3.select($(element).find("svg")[0]) : d3v3.select($(element)[0]).insert("svg", ":first-child");
+        if ($(element).find("svg").length < 2) {
+          addLegend(element);
+        }
         _d3.datum(_datum)
             .transition().duration(150)
             .each("end", function () {
@@ -804,7 +714,7 @@
               }
               if (isTimeline) {
                 _d3.selectAll("g.nv-x.nv-axis g text").each(function (d){
-                  insertLinebreaks(d, this);
+                  insertLinebreaks(_chart, d, this);
                 });
               }
             }).call(_chart);
@@ -820,25 +730,14 @@
         $(element).on("forceUpdate", function () {
           _chart.update();
         });
+        _chart.lines.dispatch.on('elementClick', function(d){
+          if (typeof options.onClick != "undefined") {
+            huePubSub.publish('charts.state', { updating: true });
+            options.onClick(d.point);
+          }
+        });
 
         return _chart;
-      }, function () {
-        if ($(element).find("svg").length > 0) {
-          _d3 = d3v3.select($(element).find("svg")[0]);
-          if ($(element).find("svg").length < 2) {
-            addLegend(element);
-          }
-        } else {
-          _d3 = d3v3.select($(element)[0]).append("svg");
-          addLegend(element);
-        }
-        _d3.selectAll(".nv-line").on("click",
-            function (d, i) {
-              if (typeof options.onClick != "undefined") {
-                chartsUpdatingState();
-                options.onClick(d);
-              }
-            });
       });
     }
   }
@@ -854,13 +753,81 @@
         .style("height", "100%")
       .append("svg");
   }
+  function handleSelection(_chart, _options, _datum) {
+    function numeric() {
+      for (var j = 0; j < _datum.length; j++) {
+        for (var i = 0; i < _datum[j].values.length; i++) {
+          if (isNaN(_datum[j].values[i].x * 1)) {
+            return false;
+          }
+        }
+      }
+      return 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;
+    _enableSelection = _enableSelection && numeric();
+    var _hideStacked = _options.hideStacked !== null ? typeof _options.hideStacked === 'function' ? _options.hideStacked() : _options.hideStacked : false;
+    var fHideSelection = _isPivot || _hideSelection ? _chart.hideSelection : _chart.showSelection;
+    if (fHideSelection) {
+      fHideSelection.call(_chart);
+    }
+    var fEnableSelection = _enableSelection ? _chart.enableSelection : _chart.disableSelection;
+    if (fEnableSelection) {
+      fEnableSelection.call(_chart);
+    }
+    var fHideStacked = _hideStacked ? _chart.hideStacked : _chart.showStacked;
+    if (fHideStacked) {
+      fHideStacked.call(_chart);
+    }
+    if (_chart.selectBars) {
+      var _field = (typeof _options.field == "function") ? _options.field() : _options.field;
+      var bHasSelection = false;
+      $.each(_options.fqs(), function (cnt, item) {
+        if (item.id() == _options.datum.widget_id) {
+          if (item.field() == _field) {
+            if (item.properties) {
+              bHasSelection = true;
+              _chart.selectBars({
+                singleValues: $.map(item.filter(), function (it) {
+                  return it.value();
+                }),
+                rangeValues: $.map(item.properties(), function (it) {
+                  return {from: it.from(), to: it.to()};
+                })
+              });
+            }
+            else {
+              bHasSelection = true;
+              _chart.selectBars($.map(item.filter(), function (it) {
+                return it.value();
+              }));
+            }
+          }
+          if (Array.isArray(item.field())) {
+            bHasSelection = true;
+            _chart.selectBars({
+              field: item.field(),
+              selected: $.map(item.filter(), function (it) {
+                return {values: it.value()};
+              })
+            });
+          }
+        }
+      });
+      if (!bHasSelection) {
+        _chart.selectBars({field: '', selected:[]});
+      }
+    }
+  }
 
   function barChartBuilder(element, options, isTimeline) {
     var _datum = options.transformer(options.datum);
     $(element).height(300);
 
     var _isPivot = options.isPivot != null ? options.isPivot : false;
-    var _hideSelection = options.hideSelection != null ? options.hideSelection : false;
+    var _hideSelection = typeof options.hideSelection !== 'undefined' ? typeof options.hideSelection === 'function' ? options.hideSelection() : options.hideSelection : false;
 
     if ($(element).find("svg").length > 0 && (_datum.length == 0 || _datum[0].values.length == 0)) {
       $(element).find("svg").remove();
@@ -873,125 +840,58 @@
 
     nv.addGraph(function () {
       var _chart;
-      if (isTimeline) {
-        if ($(element).find("svg").length > 0 && $(element).find(".nv-discreteBarWithAxes").length > 0) {
-          $(element).find("svg").empty();
-        }
-        _chart = nv.models.multiBarWithBrushChart();
-        if (_datum.length > 0) $(element).data('chart_type', 'multibar_brush');
-        var enableSelection = true;
-        if (typeof options.enableSelection !== 'undefined') {
-          enableSelection = options.enableSelection;
-        }
-        if (_datum.length > 0 && _datum[0].values.length > 10 && enableSelection) {
-          _chart.enableSelection();
-        }
-        else {
-          _chart.disableSelection();
-        }
-        if (_hideSelection) {
-          _chart.hideSelection();
+      if ($(element).find("svg").length > 0 && $(element).find(".nv-discreteBarWithAxes").length > 0) {
+        $(element).find("svg").empty();
+      }
+      _chart = nv.models.multiBarWithBrushChart();
+      if (_datum.length > 0) $(element).data('chart_type', 'multibar_brush');
+      if (!_isPivot && !_hideSelection) {
+        _chart.xAxis.showMaxMin(false).tickFormat(d3v3.format(",0f"));
+      }
+      _chart.onSelectRange(function (from, to) {
+        huePubSub.publish('charts.state', { updating: true });
+        options.onSelectRange(from, to);
+      });
+      _chart.multibar.dispatch.on('elementClick', function(d){
+        if (typeof options.onClick != "undefined") {
+          huePubSub.publish('charts.state', { updating: true });
+          options.onClick(d.point);
         }
-        _chart.onSelectRange(function (from, to) {
-          chartsUpdatingState();
-          options.onSelectRange(from, to);
-        });
+      });
+      _chart.onStateChange(options.onStateChange);
+      _chart.multibar.hideable(true);
+      _chart.multibar.stacked(typeof options.stacked != "undefined" ? options.stacked : false);
+      if (isTimeline) {
+        _chart.convert = function (d) {
+          return isTimeline ? new Date(moment(values1[0].obj.from).valueOf()) : parseFloat(d);
+        };
         _chart.staggerLabels(false);
-        _chart.xAxis.tickFormat(d3v3.time.format("%Y-%m-%d %H:%M:%S"));
-        _chart.multibar.hideable(true);
+        _chart.xAxis.tickFormat(function(d) {
+          return moment(d).utc().format("YYYY-MM-DD HH:mm:ss");
+        });
         _chart.multibar.stacked(typeof options.stacked != "undefined" ? options.stacked : false);
-        _chart.onStateChange(options.onStateChange);
         _chart.onChartUpdate(function () {
           _d3.selectAll("g.nv-x.nv-axis g text").each(function (d) {
-            insertLinebreaks(d, this);
+            insertLinebreaks(_chart, d, this);
           });
         });
       }
       else {
-        var _isDiscrete = false;
-        for (var j = 0; j < _datum.length; j++) {
-          for (var i = 0; i < _datum[j].values.length; i++) {
-            if (isNaN(_datum[j].values[i].x * 1)) {
-              _isDiscrete = true;
-              break;
-            }
-          }
-        }
-        if (_isDiscrete && !_isPivot) {
-          if ($(element).find("svg").length > 0 && $(element).find(".nv-multiBarWithLegend").length > 0) {
-            $(element).find("svg").remove();
-          }
-          if (_datum.length > 0) $(element).data('chart_type', 'discrete_bar');
-          $(element).data('chart_pivot', false);
-          _chart = nv.models.growingDiscreteBarChart()
-            .x(function (d) {
-              return d.x
-            })
-            .y(function (d) {
-              return d.y
-            })
-            .staggerLabels(true)
-            .tooltipContent(function (key, x, y) {
-              return '<h3>' + key + '</h3><p>' + y + ' on ' + hueUtils.htmlEncode(x) + '</p>'
-            });
-        }
-        else if (_isDiscrete && _isPivot) {
-          if ($(element).find("svg").length > 0 && $(element).find(".nv-discreteBarWithAxes").length > 0) {
-            $(element).find("svg").remove();
-          }
-          if (_datum.length > 0) $(element).data('chart_type', 'discrete_bar');
-          $(element).data('chart_pivot', true);
-          _chart = nv.models.growingMultiBarChart()
-            .x(function (d) {
-              return d.x
-            })
-            .y(function (d) {
-              return d.y
-            })
-            .tooltipContent(function (key, x, y) {
-              return '<h3>' + key + '</h3><p>' + y + ' on ' + hueUtils.htmlEncode(x) + '</p>'
-            });
-        }
-        else {
-          if ($(element).find("svg").length > 0 && $(element).find(".nv-discreteBarWithAxes").length > 0) {
-            $(element).find("svg").remove();
-          }
-          if (_datum.length > 0) $(element).data('chart_type', 'multibar_brush');
-          _chart = nv.models.multiBarWithBrushChart();
-          _chart.tooltipContent(function (key, x, y) {
-            return '<h3>' + hueUtils.htmlEncode(key) + '</h3><p>' + y + ' on ' + hueUtils.htmlEncode(x) + '</p>'
-          });
-          if (_datum.length > 0 && _datum[0].values.length > 10) {
-            _chart.enableSelection();
-          }
-
-          if (_isPivot || _hideSelection) {
-            _chart.hideSelection();
-          }
-          else {
-            _chart.xAxis.showMaxMin(false).tickFormat(d3v3.format(",0f"));
-          }
-          _chart.staggerLabels(true);
-          _chart.multibar.hideable(true);
-          _chart.multibar.stacked(typeof options.stacked != "undefined" ? options.stacked : false);
-          _chart.onStateChange(options.onStateChange);
-          _chart.onSelectRange(function (from, to) {
-            chartsUpdatingState();
-            options.onSelectRange(from, to);
-          });
-        }
+        _chart.staggerLabels(true);
       }
       if ($(element).width() < 300 && typeof _chart.showLegend != "undefined") {
         _chart.showLegend(false);
       }
       _chart.transitionDuration(0);
 
-      _chart.yAxis
-        .tickFormat(d3v3.format("s"));
+      _chart.yAxis.tickFormat(d3v3.format("s"));
 
       $(element).data("chart", _chart);
-
+      handleSelection(_chart, options, _datum);
       var _d3 = ($(element).find("svg").length > 0) ? d3v3.select($(element).find("svg")[0]) : d3v3.select($(element)[0]).insert("svg",":first-child");
+      if ($(element).find("svg").length < 2) {
+        addLegend(element);
+      }
       _d3.datum(_datum)
         .transition().duration(150)
         .each("end", function () {
@@ -1000,31 +900,11 @@
           }
           if (isTimeline) {
             _d3.selectAll("g.nv-x.nv-axis g text").each(function (d) {
-              insertLinebreaks(d, this);
+              insertLinebreaks(_chart, d, this);
             });
           }
         }).call(_chart);
 
-      if (_chart.selectBars && options.fqs) {
-        var _field = (typeof options.field == "function") ? options.field() : options.field;
-        $.each(options.fqs(), function (cnt, item) {
-          if (item.id() == options.datum.widget_id) {
-            if (item.field() == _field) {
-              _chart.selectBars($.map(item.filter(), function (it) {
-                return it.value();
-              }));
-            }
-            if (Array.isArray(item.field())) {
-              _chart.selectBars({
-                field: item.field(),
-                selected: $.map(item.filter(), function (it) {
-                  return {values: it.value()};
-                })
-              });
-            }
-          }
-        });
-      }
 
       if (!options.skipWindowResize) {
         var _resizeTimeout = -1;
@@ -1041,30 +921,12 @@
       });
 
       return _chart;
-    }, function () {
-      var _d3;
-      if ($(element).find("svg").length > 0) {
-        _d3 = d3v3.select($(element).find("svg")[0]);
-        if ($(element).find("svg").length < 2) {
-          addLegend(element);
-        }
-      } else {
-        _d3 = d3v3.select($(element)[0]).append("svg");
-        addLegend(element);
-      }
-      _d3.selectAll(".nv-bar").on("click",
-        function (d, i) {
-          if (typeof options.onClick != "undefined") {
-            chartsUpdatingState();
-            options.onClick(d);
-          }
-        });
     });
   }
 
   ko.bindingHandlers.partitionChart = {
     render: function (element, valueAccessor) {
-      chartsNormalState();
+      huePubSub.publish('charts.state');
       var MIN_HEIGHT_FOR_TOOLTIP = 24;
 
       var _options = valueAccessor();
@@ -1139,7 +1001,7 @@
         g.on("click", click)
           .on("dblclick", function (d, i) {
             if (typeof _options.onClick != "undefined" && d.depth > 0) {
-              chartsUpdatingState();
+              huePubSub.publish('charts.state', { updating: true });
               _options.onClick(d);
             }
           });
@@ -1147,7 +1009,7 @@
       else {
         g.on("click", function (d, i) {
           if (typeof _options.onClick != "undefined" && d.depth > 0) {
-            chartsUpdatingState();
+            huePubSub.publish('charts.state', { updating: true });
             _options.onClick(d);
           }
         });
@@ -1251,17 +1113,10 @@
     }
   };
 
-  function chartsUpdatingState() {
-    $('.nvd3').parents('svg').css('opacity', '0.5');
-  }
-
-  window.chartsUpdatingState = chartsUpdatingState;
-
-  function chartsNormalState() {
-    $('.nvd3').parents('svg').css('opacity', '1');
-  }
-
-  window.chartsNormalState = chartsNormalState;
+  huePubSub.subscribe('charts.state', function(state) {
+    var opacity = state && state.updating ? '0.5' : '1';
+    $('.nvd3').parents('svg').css('opacity', opacity);
+  });
 
   var tipBuilder = function () {
     var direction = d3_tip_direction,

+ 0 - 3
desktop/core/src/desktop/static/desktop/js/ko.charts.plotly.js

@@ -170,7 +170,4 @@
     }
   };
 
-  window.chartsUpdatingState = function(){};
-  window.chartsNormalState = function(){};
-
 })();

+ 1 - 1
desktop/core/src/desktop/static/desktop/js/nv.d3.growingDiscreteBarChart.js

@@ -376,7 +376,7 @@ nv.models.growingDiscreteBarChart = function() {
 
   chart.selectBars = function(args) {
     if (!arguments.length) return selectBars;
-    selectBars(args);
+    if (selectBars) selectBars(args);
     return chart;
   };
 

+ 1 - 1
desktop/core/src/desktop/static/desktop/js/nv.d3.growingMultiBarChart.js

@@ -552,7 +552,7 @@ nv.models.growingMultiBarChart = function() {
 
   chart.selectBars = function(args) {
     if (!arguments.length) return selectBars;
-    selectBars(args);
+    if (selectBars) selectBars(args);
     return chart;
   };
 

+ 238 - 33
desktop/core/src/desktop/static/desktop/js/nv.d3.lineWithBrushChart.js

@@ -46,12 +46,19 @@ nv.models.lineWithBrushChart = function() {
     , rightAlignYAxis = false
     , useInteractiveGuideline = false
     , tooltips = true
-    , tooltip = function(key, x, y, e, graph) {
-        return '<h3>' + key + '</h3>' +
-               '<p>' +  y + ' at ' + x + '</p>'
-      }
+    , tooltipSingle = function(value) {
+      return '<h3>' + hueUtils.htmlEncode(value.key) + '</h3>' +
+        '<p>' + hueUtils.htmlEncode(value.y) + ' on ' + hueUtils.htmlEncode(value.x) + '</p>';
+    }
+    , tooltipMultiple = function (values) {
+      return values.map(function (value) {
+          return '<p><b>' + hueUtils.htmlEncode(value.key) + '</b>: ' +  hueUtils.htmlEncode(value.y) + '</p>';
+        }).join("") + '<h3>' + hueUtils.htmlEncode(values[0] && values[0].x) + '</h3>';
+    }
     , x
     , y
+    , getX = function(d) { return d.x } // accessor to get the x value
+    , getY = function(d) { return d.y } // accessor to get the y value
     , state = {}
     , defaultState = null
     , noData = 'No Data Available.'
@@ -86,11 +93,15 @@ nv.models.lineWithBrushChart = function() {
   //------------------------------------------------------------
 
   var showTooltip = function(e, offsetElement) {
+    var values = (e.list || [e]).map(function (e) {
+      var x = xAxis.tickFormat()(lines.x()(e.point, e.pointIndex)),
+      y = yAxis.tickFormat()(lines.y()(e.point, e.pointIndex));
+      return {x: x, y: y, key: e.series.key};
+    });
+
     var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ),
         top = e.pos[1] + ( offsetElement.offsetTop || 0),
-        x = xAxis.tickFormat()(lines.x()(e.point, e.pointIndex)),
-        y = yAxis.tickFormat()(lines.y()(e.point, e.pointIndex)),
-        content = tooltip(e.series.key, x, y, e, chart);
+        content = values.length > 1 && tooltipMultiple(values) || tooltipSingle(values[0]);
 
     nv.tooltip.show([left, top], content, null, null, offsetElement);
   };
@@ -111,7 +122,8 @@ nv.models.lineWithBrushChart = function() {
 
 
       chart.update = function() {
-        container.transition().duration(transitionDuration).each("end", onChartUpdate).call(chart)
+        container.transition().duration(transitionDuration).each("end", onChartUpdate).call(chart);
+        filteredData = data.filter(function(series) { return !series.disabled; });
         if (selectionEnabled){
           enableBrush();
         }
@@ -150,7 +162,7 @@ nv.models.lineWithBrushChart = function() {
         noDataText
           .attr('x', margin.left + availableWidth / 2)
           .attr('y', margin.top + availableHeight / 2)
-          .text(function(d) { return d });
+          .text(function(d) { return d; });
 
         return chart;
       } else {
@@ -252,17 +264,17 @@ nv.models.lineWithBrushChart = function() {
         wrap.select(".nv-interactive").call(interactiveLayer);
       }
 
-
+      var filteredData = data.filter(function(series) { return !series.disabled; });
       lines
         .width(availableChartWidth)
         .height(availableHeight)
         .color(data.map(function(d,i) {
           return d.color || color(d, i);
-        }).filter(function(d,i) { return !data[i].disabled }));
+        }).filter(function(d,i) { return !data[i].disabled; }));
 
 
       var linesWrap = g.select('.nv-linesWrap')
-          .datum(data.filter(function(d) { return !d.disabled }))
+          .datum(data.filter(function(d) { return !d.disabled; }));
 
       linesWrap.transition().call(lines);
 
@@ -270,23 +282,45 @@ nv.models.lineWithBrushChart = function() {
 
       //------------------------------------------------------------
       // Setup Brush
+      var overlay;
       if (selectionEnabled){
         enableBrush();
+        overlay = g.select('rect');
+        overlay.style('display', 'none');
+      } else {
+        disableBrush();
+        overlay = g.select('rect');
+        overlay
+        .style('display', 'inherit')
+        .attr('height', availableHeight)
+        .attr('width', availableWidth)
+        .on('mousemove', onMouseMove)
+        .on('mouseout', onMouseOut)
+        .on('click', onClick);
       }
 
 
       function enableBrush() {
+        if (!g) { // Can happen if the state change before the charts has been created.
+          return;
+        }
+        brush
+          .x(x)
+          .on('brush', onBrush)
+          .on('brushend', onBrushEnd);
+        if (chart.brushDomain) {
+          var brushExtent = [fGetNumericValue(chart.brushDomain[0]), fGetNumericValue(chart.brushDomain[1])];
+          brush.extent(brushExtent);
+        } else {
+          brush.clear();
+        }
+        var gBrush;
         if (g.selectAll('.nv-brush')[0].length == 0) {
-          gEnter.append('g').attr('class', 'nv-brushBackground');
-          gEnter.append('g').attr('class', 'nv-x nv-brush');
-          brush
-              .x(x)
-              .on('brush', onBrush)
-              .on('brushend', onBrushEnd)
-
-          if (brushExtent) brush.extent(brushExtent);
+          g.append('g').attr('class', 'nv-brushBackground');
+          g.append('g').attr('class', 'nv-x nv-brush');
+
           var brushBG = g.select('.nv-brushBackground').selectAll('g')
-              .data([brushExtent || brush.extent()])
+              .data([chart.brushExtent || brush.extent()])
           var brushBGenter = brushBG.enter()
               .append('g');
 
@@ -302,14 +336,16 @@ nv.models.lineWithBrushChart = function() {
               .attr('y', 0)
               .attr('height', availableHeight);
 
-          var gBrush = g.select('.nv-x.nv-brush')
-              .call(brush);
-          gBrush.selectAll('rect')
-              .attr('height', availableHeight);
+          gBrush = g.select('.nv-x.nv-brush').call(brush);
         }
         else {
           g.selectAll('.nv-brush').attr('display', 'inline');
+          gBrush = g.select('.nv-x.nv-brush').call(brush);
         }
+        gBrush.selectAll('rect')
+          .attr('height', availableHeight)
+          .on('mousemove', onMouseMove)
+          .on('mouseout', onMouseOut);
       }
 
       function disableBrush() {
@@ -400,7 +436,7 @@ nv.models.lineWithBrushChart = function() {
             var yValue = chart.yScale().invert(e.mouseY);
             var domainExtent = Math.abs(chart.yScale().domain()[0] - chart.yScale().domain()[1]);
             var threshold = 0.03 * domainExtent;
-            var indexToHighlight = nv.nearestValueIndex(allData.map(function(d){return d.value}),yValue,threshold);
+            var indexToHighlight = nv.nearestValueIndex(allData.map(function(d){return d.value;}),yValue,threshold);
             if (indexToHighlight !== null)
               allData[indexToHighlight].highlight = true;
           }
@@ -450,21 +486,172 @@ nv.models.lineWithBrushChart = function() {
       //============================================================
 
       function onBrush(){
-        brushExtent = brush.empty() ? null : brush.extent();
+        chart.brushExtent = brush.empty() ? null : brush.extent();
         extent = brush.empty() ? x.domain() : brush.extent();
 
         dispatch.brush({extent: extent, brush: brush});
       }
+      function fGetNumericValue (o) {
+        return o instanceof Date ? o.getTime() : o;
+      }
 
       function onBrushEnd(){
-        brushExtent = brush.empty() ? null : brush.extent();
-        extent = brush.empty() ? x.domain() : brush.extent();
+        var closest = function () {
+          if (!data[0].values.length) {
+            return [];
+          }
+          var xDate = x.invert(d3v3.mouse(this)[0]);
+
+          var distances = {};
+          var min = Number.MAX_VALUE;
+          var next = -Number.MAX_VALUE;
+          var diff;
+          var i, j;
+          for (j = 0; j < data.length; j++) {
+            for (i = 0; i < data[j].values.length; i++) {
+              diff = xDate - fGetNumericValue(data[j].values[i].x);
+              if (diff >= 0 && diff < min) {
+                min = diff;
+              } else if (diff < 0 && diff > next) {
+                next = diff;
+              }
+              if (!distances[diff]) {
+                distances[diff] = [];
+              }
+              distances[diff].push(data[j].values[i]);
+            }
+          }
+          if (distances[min][0].x_end) {
+            return [distances[min][0].x, distances[min][0].x_end];
+          } else if (distances[min] !== undefined && distances[next] !== undefined) {
+            var _from = distances[min][0].x < distances[next][0].x ? distances[min][0].x : distances[next][0].x;
+            var _to = distances[min][0].x < distances[next][0].x ? distances[next][0].x : distances[min][0].x;
+            return [_from, _to];
+          } else {
+            return [];
+          }
+        };
+
+        chart.brushExtent = extent = brush.empty() ? closest.call(this) : brush.extent();
 
-        if (onSelectRange != null){
-          onSelectRange(parseInt(extent[0]), parseInt(extent[1])); // Only int values currently
+        if (onSelectRange) {
+          onSelectRange(fGetNumericValue(extent[0]), fGetNumericValue(extent[1]));
         }
       }
 
+      function getElByMouse () {
+        var point = x.invert(d3v3.mouse(this)[0]);
+        if (!filteredData.length || !filteredData[0].values.length) {
+          return null;
+        }
+        var min = Math.abs(filteredData[0].values[0].x - point);
+        var distances = {};
+
+        for (var j = 0; j < filteredData.length; j++) {
+          for (var i = 0; i < filteredData[j].values.length; i++) {
+            var diff = Math.abs(fGetNumericValue(filteredData[j].values[i].x) - point);
+            if (!distances[diff]) {
+              distances[diff] = [];
+            }
+            distances[diff].push(filteredData[j].values[i]);
+            if (diff < min) {
+              min = diff;
+            }
+          }
+        }
+        return distances[min];
+      }
+      function onMouseMove () {
+        var el = getElByMouse.call(this);
+        // If we're mousing over a circle that doesn't have class hover, set class and dispatch mouseover
+        var target = container.selectAll('g.nv-wrap.nv-lineChart circle:not(.hover)').filter(function(rect) {
+          return el && el.some(function(d) {
+            return fGetNumericValue(getX(rect)) === fGetNumericValue(getX(d)) && d.series === rect.series;
+          });
+        });
+        // If there's rectangle with the hover class that are not the target, remove the class and dispatch mouseout
+        var others = container.selectAll('g.nv-wrap.nv-lineChart circle.hover').filter(function(rect) {
+          return !el || !el.some(function(d) {
+            return fGetNumericValue(getX(rect)) === fGetNumericValue(getX(d)) && d.series === rect.series;
+          });
+        });
+        if (others.size()) {
+          others.classed('hover', false)
+          .each(function (d, i) {
+            lines.dispatch.elementMouseout({
+              value: getY(d),
+              point: d,
+              series: filteredData[d.series],
+              pointIndex: i,
+              seriesIndex: d.series,
+              e: d3v3.event
+            });
+          });
+        }
+        if (target.size()) {
+          var e = d3v3.event; // Keep reference to event for setTimeout
+          setTimeout(function(){ // Delayed to conteract conflict with elementMouseout.
+            target.classed('hover', true);
+            var max, maxi;
+            target.each(function (d, i) {
+              if (isNaN(max) || max < getY(d)) {
+                max = getY(d);
+                maxi = i;
+              }
+            });
+            d3v3.select(target[0][maxi])
+            .each(function(d, i){
+              lines.dispatch.elementMouseover({
+                value: getY(d),
+                point: d,
+                series: data[d.series],
+                pos: [x(getX(d)), y(getY(d))],
+                pointIndex: -1,
+                seriesIndex: d.series,
+                list: el.map(function (d) {
+                  return {
+                    value: getY(d),
+                    point: d,
+                    series: data[d.series],
+                    pos: [x(getX(d)), y(getY(d))],
+                    pointIndex: -1,
+                    seriesIndex: d.series,
+                  };
+                }),
+                e: e,
+              });
+            });
+          });
+        }
+      }
+      function onMouseOut () {
+        var others = container.selectAll('g.nv-wrap.nv-lineChart circle.hover');
+        if (others.size()) {
+           others.classed('hover', false)
+          .each(function (d) {
+            lines.dispatch.elementMouseout({
+              value: getY(d),
+              point: d,
+              series: data[d.series],
+              pointIndex: d.index,
+              seriesIndex: d.series,
+              e: d3v3.event
+            });
+          });
+        }
+      }
+      function onClick () {
+        var el = getElByMouse.call(this);
+        var d = el[0];
+        lines.dispatch.elementClick({
+          value: getY(d),
+          point: d,
+          series: data[d.series],
+          pointIndex: d.index,
+          seriesIndex: d.series,
+          e: d3v3.event
+        });
+      }
     });
 
     return chart;
@@ -623,11 +810,19 @@ nv.models.lineWithBrushChart = function() {
     return chart;
   };
 
+  chart.isSelectionEnabled = function() {
+    return selectionEnabled;
+  };
+
   chart.hideSelection = function() {
     selectionHidden = true;
-    selectionEnabled = false;
     return chart;
-  }
+  };
+
+  chart.showSelection = function() {
+    selectionHidden = false;
+    return chart;
+  };
 
   chart.onSelectRange = function(_) {
     if (!arguments.length) return onSelectRange;
@@ -653,6 +848,16 @@ nv.models.lineWithBrushChart = function() {
     return chart;
   };
 
+  chart.selectBars = function(args) {
+    if (!arguments.length) return selectBars;
+    if (args && args.rangeValues) {
+      chart.brushDomain = [args.rangeValues[0].from, args.rangeValues[0].to];
+    } else {
+      chart.brushDomain = null;
+    }
+    return chart;
+  };
+
   //============================================================
 
 

+ 265 - 49
desktop/core/src/desktop/static/desktop/js/nv.d3.multiBarWithBrushChart.js

@@ -48,12 +48,19 @@ nv.models.multiBarWithBrushChart = function() {
     , staggerLabels = false
     , rotateLabels = 0
     , tooltips = true
-    , tooltip = function(key, x, y, e, graph) {
-        return '<h3>' + key + '</h3>' +
-               '<p>' +  y + ' on ' + x + '</p>'
-      }
+    , tooltipSingle = function(value) {
+      return '<h3>' + hueUtils.htmlEncode(value.key) + '</h3>' +
+        '<p>' + hueUtils.htmlEncode(value.y) + ' on ' + hueUtils.htmlEncode(value.x) + '</p>';
+    }
+    , tooltipMultiple = function (values) {
+      return values.map(function (value) {
+          return '<p><b>' + hueUtils.htmlEncode(value.key) + '</b>: ' +  hueUtils.htmlEncode(value.y) + '</p>';
+        }).join("") + '<h3>' + hueUtils.htmlEncode(values[0] && values[0].x) + '</h3>';
+    }
     , x //can be accessed via chart.xScale()
     , y //can be accessed via chart.yScale()
+    , getX = function(d) { return d.x } // accessor to get the x value
+    , getY = function(d) { return d.y } // accessor to get the y value
     , state = { stacked: false, selectionEnabled: false }
     , defaultState = null
     , noData = "No Data Available."
@@ -65,6 +72,7 @@ nv.models.multiBarWithBrushChart = function() {
     , brushExtent = null
     , selectionEnabled = false
     , selectionHidden = false
+    , stackedHidden = false
     , onSelectRange = null
     , onStateChange = null
     , onChartUpdate = null
@@ -79,7 +87,7 @@ nv.models.multiBarWithBrushChart = function() {
     .tickPadding(7)
     .highlightZero(true)
     .showMaxMin(false)
-    .tickFormat(function(d) { return d })
+    .tickFormat(function(d) { return d; })
     ;
   yAxis
     .orient((rightAlignYAxis) ? 'right' : 'left')
@@ -95,11 +103,15 @@ nv.models.multiBarWithBrushChart = function() {
   //------------------------------------------------------------
 
   var showTooltip = function(e, offsetElement) {
-    var left = ($.browser.msie && $.browser.version.indexOf("9.") > -1) ? e.e.clientX : e.e.layerX,
+    var values = (e.list || [e]).map(function (e) {
+      var x = xAxis.tickFormat()(multibar.x()(e.point, e.pointIndex)),
+      y = yAxis.tickFormat()(multibar.y()(e.point, e.pointIndex));
+      return {x: x, y: y, key: e.series.key};
+    });
+
+    var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ),
         top = e.pos[1] + ( offsetElement.offsetTop || 0),
-        x = xAxis.tickFormat()(multibar.x()(e.point, e.pointIndex)),
-        y = yAxis.tickFormat()(multibar.y()(e.point, e.pointIndex)),
-        content = tooltip(e.point.seriesKey, x, y, e, chart);
+        content = values.length > 1 && tooltipMultiple(values) || tooltipSingle(values[0]);
 
     nv.tooltip.show([left, top], content, e.value < 0 ? 'n' : 's', null, offsetElement);
   };
@@ -124,6 +136,7 @@ nv.models.multiBarWithBrushChart = function() {
             .duration(transitionDuration)
             .each("end", onChartUpdate)
             .call(chart);
+        filteredData = data.filter(function(series) { return !series.disabled; });
         if (selectionEnabled){
           enableBrush();
         }
@@ -190,6 +203,7 @@ nv.models.multiBarWithBrushChart = function() {
       var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-multiBarWithLegend').append('g');
       var g = wrap.select('g');
 
+      gEnter.append("rect").style("opacity",0);
       gEnter.append('g').attr('class', 'nv-x nv-axis');
       gEnter.append('g').attr('class', 'nv-y nv-axis');
       gEnter.append('g').attr('class', 'nv-barsWrap');
@@ -237,13 +251,14 @@ nv.models.multiBarWithBrushChart = function() {
       // Controls
 
       if (showControls) {
-        var controlsData = [
-          { key: LABELS.GROUPED, disabled: multibar.stacked() },
-          { key: LABELS.STACKED, disabled: !multibar.stacked() }
-        ];
+        var controlsData = [];
+        if (!stackedHidden) {
+          controlsData.push({ key: LABELS.GROUPED, disabled: multibar.stacked() });
+          controlsData.push({ key: LABELS.STACKED, disabled: !multibar.stacked() });
+        }
 
         if (! selectionHidden) {
-          controlsData.push({ key: LABELS.SELECT, disabled: !selectionEnabled, checkbox: true });
+          controlsData.push({ key: LABELS.SELECT, disabled: !selectionEnabled, checkbox: selectionEnabled });
         }
 
         controls.width(controlWidth()).color(['#444', '#444', '#444']);
@@ -265,20 +280,20 @@ nv.models.multiBarWithBrushChart = function() {
 
       //------------------------------------------------------------
       // Main Chart Component(s)
-
+      var filteredData = data.filter(function(series) { return !series.disabled; });
       multibar
-        .disabled(data.map(function(series) { return series.disabled }))
+        .disabled(data.map(function(series) { return series.disabled; }))
         .width(availableChartWidth)
         .height(availableHeight)
         .color(data.map(function(d,i) {
           return d.color || color(d, i);
-        }).filter(function(d,i) { return !data[i].disabled }))
+        }).filter(function(d,i) { return !data[i].disabled; }));
 
       selectBars = multibar.selectBars;
 
-      var dataBars = data.filter(function(d) { return !d.disabled && d.bar });
+      var dataBars = data.filter(function(d) { return !d.disabled && d.bar; });
       var barsWrap = g.select('.nv-barsWrap')
-          .datum(data.filter(function(d) { return !d.disabled }))
+          .datum(data.filter(function(d) { return !d.disabled; }));
 
       barsWrap.transition().call(multibar);
 
@@ -286,24 +301,47 @@ nv.models.multiBarWithBrushChart = function() {
 
       //------------------------------------------------------------
       // Setup Brush
+      var overlay;
       if (selectionEnabled){
         enableBrush();
+        overlay = g.select('rect');
+        overlay.style('display', 'none');
+      } else {
+        disableBrush();
+        overlay = g.select('rect');
+        overlay
+        .style('display', 'inherit')
+        .attr('height', availableHeight)
+        .attr('width', availableWidth)
+        .on('mousemove', onMouseMove)
+        .on('mouseout', onMouseOut)
+        .on('click', onClick);
       }
 
 
       function enableBrush() {
+        if (!g) { // Can happen if the state change before the charts has been created.
+          return;
+        }
+        brush
+          .x(x)
+          .on('brush', onBrush)
+          .on('brushstart', onBrushStart)
+          .on('brushend', onBrushEnd);
+        if (chart.brushDomain) {
+          var brushExtent = fromSelection(chart.brushDomain).range;
+          brush.extent(brushExtent);
+        } else {
+          brush.clear();
+        }
+        var gBrush;
+
         if (g.selectAll('.nv-brush')[0].length == 0) {
-          gEnter.append('g').attr('class', 'nv-brushBackground');
-          gEnter.append('g').attr('class', 'nv-x nv-brush');
-          brush
-              .x(x)
-              .on('brush', onBrush)
-              .on('brushstart', onBrushStart)
-              .on('brushend', onBrushEnd)
-
-          if (brushExtent) brush.extent(brushExtent);
+          g.append('g').attr('class', 'nv-brushBackground');
+          g.append('g').attr('class', 'nv-x nv-brush');
+
           var brushBG = g.select('.nv-brushBackground').selectAll('g')
-              .data([brushExtent || brush.extent()])
+              .data([chart.brushExtent || brush.extent()])
           var brushBGenter = brushBG.enter()
               .append('g');
 
@@ -319,14 +357,17 @@ nv.models.multiBarWithBrushChart = function() {
               .attr('y', 0)
               .attr('height', availableHeight);
 
-          var gBrush = g.select('.nv-x.nv-brush')
+          gBrush = g.select('.nv-x.nv-brush')
               .call(brush);
-          gBrush.selectAll('rect')
-              .attr('height', availableHeight);
         }
         else {
           g.selectAll('.nv-brush').attr('display', 'inline');
+          gBrush = g.select('.nv-x.nv-brush').call(brush);
         }
+        gBrush.selectAll('rect')
+          .attr('height', availableHeight)
+          .on('mousemove', onMouseMove)
+          .on('mouseout', onMouseOut);
       }
 
       function disableBrush() {
@@ -486,10 +527,13 @@ nv.models.multiBarWithBrushChart = function() {
       });
 
       //============================================================
+      function fGetNumericValue(o) {
+        return o instanceof Date ? o.getTime() : o;
+      };
 
 
       function onBrush(){
-        brushExtent = brush.empty() ? null : brush.extent();
+        chart.brushExtent = brush.empty() ? null : brush.extent();
         extent = brush.empty() ? x.domain() : brush.extent();
 
         dispatch.brush({extent: extent, brush: brush});
@@ -500,23 +544,174 @@ nv.models.multiBarWithBrushChart = function() {
       }
 
       function onBrushEnd(){
-        brushExtent = brush.empty() ? null : brush.extent();
-        extent = brush.empty() ? x.domain() : brush.extent();
-
+        extent = brush.empty() ? [d3v3.mouse(this)[0], d3v3.mouse(this)[0]] : brush.extent();
+        var _leftEdges = x.range();
+        var _width = x.rangeBand() + multibar.groupSpacing() * x.rangeBand();
+        var _l, _j;
+        for(_l=0; extent[0] > (_leftEdges[_l] + _width); _l++) {}
+        _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)
+        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){
-          var _leftEdges = x.range();
-          var _width = x.rangeBand();
-          var _j;
-          for(_j=0; extent[0] > (_leftEdges[_j] + _width); _j++) {}
-          var _from = typeof x.domain()[_j] != "undefined" ? x.domain()[_j] : x.domain()[0];
-
-          for(_j=0; extent[1] > (_leftEdges[_j] + _width); _j++) {}
-          var _to = typeof x.domain()[_j] != "undefined" ? x.domain()[_j] : x.domain()[x.domain().length-1];
-
           onSelectRange(_from, _to);
         }
+      }
+      function getElByMouse (coords, filterSeries) {
+        var extent = coords || d3v3.mouse(this)[0];
+        var _l, _j;
+        var _width = x.rangeBand();
+        var _leftEdges = x.range();
+        for(_l=0; extent >= _leftEdges[_l]; _l++) {}
+        _l = Math.max(_l - 1, 0);
+        var value = fGetNumericValue(x.domain()[_l]);
+        var values = [];
+        if (!filterSeries || multibar.stacked()) {
+          var j;
+          for (j = 0; j < filteredData.length; j++) {
+            for (i = 0; i < filteredData[j].values.length; i++) {
+              if (fGetNumericValue(getX(filteredData[j].values[i])) == value) {
+                values.push(filteredData[j].values[i]);
+              }
+            }
+          }
+        } else {
+          var serieIndex = Math.floor(Math.min(extent - _leftEdges[_l], _width - 0.001) / (_width / filteredData.length)); // Math.min(extent - _leftEdges[_l], _width - 0.001) to handle the padding at the end. Would it make sense to remove the padding?
+          var i;
+          if (serieIndex < 0) return null;
+          for (i = 0; i < filteredData[serieIndex].values.length; i++) {
+            if (fGetNumericValue(getX(filteredData[serieIndex].values[i])) == value) {
+              values.push(filteredData[serieIndex].values[i]);
+              break;
+            }
+          }
+        }
+        return values;
+      }
+      function fromSelection (selection) {
+        var _width = x.rangeBand() + multibar.groupSpacing() * x.rangeBand();
+        var _leftEdges = x.domain();
+        if (!_leftEdges.length) {
+          return null;
+        }
+        var _l, _j;
+        var isDescending = _leftEdges[0] < _leftEdges[1];
+        if (isDescending) {
+          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;
+        var _fromRange = x.range()[_l] != undefined ? x.range()[_l] : 0;
+        var _from = x.domain()[_l] != undefined ? x.domain()[_l] : 0;
 
-        gEnter.select(".nv-brush").select(".extent").style("display", "none");
+        if (isDescending) {
+          for(_j = 0; selection[1] > _leftEdges[_j]; _j++) {}
+        } else {
+          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]
+        };
+      }
+      function onMouseMove () {
+        var el = getElByMouse.call(this);
+        // If we're mousing over a rectangle that doesn't have class hover, set class and dispatch mouseover
+        var target = container.selectAll('g.nv-wrap.nv-multiBarWithLegend rect:not(.hover)').filter(function(rect) {
+          return el && el.some(function(d) {
+            return fGetNumericValue(getX(rect)) === fGetNumericValue(getX(d)) && d.series === rect.series;
+          });
+        });
+        // If there's rectangle with the hover class that are not the target, remove the class and dispatch mouseout
+        var others = container.selectAll('g.nv-wrap.nv-multiBarWithLegend rect.hover').filter(function(rect) {
+          return !el || !el.some(function(d) {
+            return fGetNumericValue(getX(rect)) === fGetNumericValue(getX(d)) && d.series === rect.series;
+          });
+        })
+        if (others.size()) {
+          others.classed('hover', false)
+          .each(function (d, i) {
+            multibar.dispatch.elementMouseout({
+              value: getY(d,i),
+              point: d,
+              series: data[d.series],
+              pointIndex: i,
+              seriesIndex: d.series,
+              e: d3v3.event
+            });
+          });
+        }
+        if (target.size()) {
+          var e = d3v3.event; // Keep reference to event for setTimeout
+          setTimeout(function(){ // Delayed to conteract conflict with elementMouseout.
+            target.classed('hover', true);
+            var max, maxi;
+            target.each(function (d, i) {
+              if (isNaN(max) || max < getY(d)) {
+                max = getY(d);
+                maxi = i;
+              }
+            });
+            d3v3.select(target[0][maxi])
+            .each(function(d, i){
+              multibar.dispatch.elementMouseover({
+                value: getY(d),
+                point: d,
+                series: filteredData[d.series],
+                pos: [x(getX(d)) + (x.rangeBand() * (multibar.stacked() ? data.length / 2 : d.series + .5) / data.length), y(getY(d) + (multibar.stacked() ? d.y0 : 0))],  // TODO: Figure out why the value appears to be shifted
+                pointIndex: i,
+                seriesIndex: d.series,
+                list: el.map(function (d) {
+                  return {
+                    value: getY(d),
+                    point: d,
+                    series: data[d.series],
+                    pos: [x(getX(d)) + (x.rangeBand() * (multibar.stacked() ? data.length / 2 : d.series + .5) / data.length), y(getY(d) + (multibar.stacked() ? d.y0 : 0))],  // TODO: Figure out why the value appears to be shifted
+                    pointIndex: -1,
+                    seriesIndex: d.series,
+                  };
+                }),
+                e: e
+              });
+            });
+          },0);
+        }
+      }
+      function onMouseOut () {
+        var others = container.selectAll('g.nv-wrap.nv-multiBarWithLegend rect.hover')
+        if (others.size()) {
+           others.classed('hover', false)
+          .each(function (d, i) {
+            multibar.dispatch.elementMouseout({
+              value: getY(d,i),
+              point: d,
+              series: data[d.series],
+              pointIndex: i,
+              seriesIndex: d.series,
+              e: d3v3.event
+            });
+          });
+        }
+      }
+      function onClick () {
+        var el = getElByMouse.call(this);
+        var d = el[0];
+        multibar.dispatch.elementClick({
+          value: getY(d),
+          point: d,
+          series: data[d.series],
+          pointIndex: d.index,
+          seriesIndex: d.series,
+          e: d3v3.event
+        });
       }
     });
 
@@ -651,7 +846,7 @@ nv.models.multiBarWithBrushChart = function() {
 
   chart.tooltipContent = function(_) {
     if (!arguments.length) return tooltip;
-    tooltip = _;
+    tooltipSingle = _;
     return chart;
   };
 
@@ -689,11 +884,27 @@ nv.models.multiBarWithBrushChart = function() {
     return chart;
   };
 
+  chart.isSelectionEnabled = function() {
+    return selectionEnabled;
+  };
+
   chart.hideSelection = function() {
     selectionHidden = true;
-    selectionEnabled = false;
     return chart;
-  }
+  };
+
+  chart.showSelection = function() {
+    selectionHidden = false;
+    return chart;
+  };
+
+  chart.showStacked = function() {
+    stackedHidden = false;
+  };
+
+  chart.hideStacked = function() {
+    stackedHidden = true;
+  };
 
   chart.onSelectRange = function(_) {
     if (!arguments.length) return onSelectRange;
@@ -715,6 +926,11 @@ nv.models.multiBarWithBrushChart = function() {
 
   chart.selectBars = function(args) {
     if (!arguments.length) return selectBars;
+    if (args && args.rangeValues) {
+      chart.brushDomain = [args.rangeValues[0].from, args.rangeValues[0].to];
+    } else {
+      chart.brushDomain = null;
+    }
     if (selectBars) {
       selectBars(args);
     }

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

@@ -18,6 +18,8 @@
 from __future__ import division
 
 import collections
+import datetime
+import dateutil
 import itertools
 import json
 import logging
@@ -585,7 +587,14 @@ def augment_solr_response(response, collection, query):
             _augment_stats_2d(name, facet, counts, selected_values, agg_keys, rows)
 
             counts = [_v for _f in counts for _v in (_f['val'], _f[column])]
-            counts = range_pair2(facet['field'], name, selected_values.get(facet['id'], []), counts, 1, collection_facet['properties']['facets'][0], collection_facet=collection_facet)
+            counts = range_pair2(
+                                 facet['field'],
+                                 name,
+                                 selected_values.get(facet['id'], []),
+                                 counts,
+                                 datetime.datetime(datetime.MAXYEAR, 12, 31, 23, 59, 59, 0, dateutil.tz.tzoffset('Z', 0)) if facet['properties'].get('isDate') else 1,
+                                 collection_facet['properties']['facets'][0],
+                                 collection_facet=collection_facet)
           else:
             # Dimension 1 with counts and 2 with analytics
             agg_keys = [key for key, value in counts[0].items() if key.lower().startswith('agg_') or key.lower().startswith('dim_')] if counts else []
@@ -607,7 +616,13 @@ def augment_solr_response(response, collection, query):
                   _series[legend].append(cell)
 
             for _name, val in _series.iteritems():
-              _c = range_pair2(facet['field'], _name, selected_values.get(facet['id'], []), val, 1, collection_facet['properties']['facets'][0])
+              _c = range_pair2(
+                               facet['field'],
+                               _name,
+                               selected_values.get(facet['id'], []),
+                               val,
+                               datetime.datetime(datetime.MAXYEAR, 12, 31, 23, 59, 59, 0, dateutil.tz.tzoffset('Z', 0)) if facet['properties'].get('isDate') else 1,
+                               collection_facet['properties']['facets'][0])
               extraSeries.append({'counts': _c, 'label': _name})
             counts = []
         elif collection_facet['properties'].get('isOldPivot'):
@@ -664,7 +679,8 @@ def augment_solr_response(response, collection, query):
           'dimension': dimension,
           'response': {'response': {'start': 0, 'numFound': num_bucket}}, # Todo * nested buckets + offsets
           'docs': [dict(zip(cols, row)) for row in rows],
-          'fieldsAttributes': [Collection2._make_gridlayout_header_field({'name': col, 'type': 'aggr' if '(' in col else 'string'}) for col in cols]
+          'fieldsAttributes': [Collection2._make_gridlayout_header_field({'name': col, 'type': 'aggr' if '(' in col else 'string'}) for col in cols],
+          'multiselect': collection_facet['properties']['facets'][0].get('multiselect', True)
         }
 
         normalized_facets.append(facet)

+ 99 - 53
desktop/libs/dashboard/src/dashboard/static/dashboard/js/search.ko.js

@@ -650,6 +650,10 @@ var Collection = function (vm, collection) {
 
   self.template = ko.mapping.fromJS(collection.template);
 
+  self.template.chartSettings.hideStacked = ko.computed(function () {
+    return self.template.chartSettings.chartYMulti().length <= 1;
+  });
+
   for (var setting in self.template.chartSettings) {
     self.template.chartSettings[setting].subscribe(function () {
       huePubSub.publish('gridChartForceUpdate');
@@ -900,12 +904,57 @@ var Collection = function (vm, collection) {
 
     if (facet.properties.facets) { // Sub facet
       $.each(facet.properties.facets(), function (index, nestedFacet) {
-        self._addObservablesToNestedFacet(facet, nestedFacet, vm);
+        self._addObservablesToNestedFacet(facet, nestedFacet, vm, index);
       });
     }
+
+    facet.canReset = ko.computed(function () {
+      var _fq = (function() {
+        var _fq, fqs = vm.query && vm.query.fqs();
+        for (var i = 0; fqs && i < fqs.length; i++) {
+          var fq = fqs[i];
+          if (fq.id() == facet.id()) {
+            _fq = fq;
+            break;
+          }
+        }
+        return _fq;
+      })();
+      return _fq && _fq.filter().length;
+    });
   }
 
-  self._addObservablesToNestedFacet = function(facet, nestedFacet, vm) {
+  self.template.getMeta = function (extraCheck) {
+    var iterable = self.template.fieldsAttributes();
+    if (self.template.fields().length > 0) {
+      iterable = self.template.fields();
+    }
+    return $.map(iterable, function (field) {
+      if (typeof field !== 'undefined' && field.name() != '' && extraCheck(field.type())) {
+        return field;
+      }
+    }).sort(function (a, b) {
+      return a.name().toLowerCase().localeCompare(b.name().toLowerCase());
+    });
+  }
+
+  self.template.cleanedMeta = ko.computed(function () {
+    return self.template.getMeta(alwaysTrue);
+  });
+
+  self.template.cleanedNumericMeta = ko.computed(function () {
+    return self.template.getMeta(isNumericColumn);
+  });
+
+  self.template.cleanedStringMeta = ko.computed(function () {
+    return self.template.getMeta(isStringColumn);
+  });
+
+  self.template.cleanedDateTimeMeta = ko.computed(function () {
+    return self.template.getMeta(isDateTimeColumn);
+  });
+
+  self._addObservablesToNestedFacet = function(facet, nestedFacet, vm, index) {
     nestedFacet.limit.subscribe(function () {
       vm.search();
     });
@@ -927,7 +976,17 @@ var Collection = function (vm, collection) {
       });
 
       nestedFacet.aggregate.facetFieldsNames = ko.computed(function () {
-        return self._getCompatibleMetricFields(nestedFacet);
+        if (index != 0) {
+          return self._getCompatibleMetricFields(nestedFacet);
+        }
+        var template = self.template;
+        if (facet.properties.canRange() && facet.properties.isDate()) {
+          return template.cleanedDateTimeMeta();
+        } else if (facet.properties.canRange()) {
+          return template.cleanedNumericMeta();
+        } else {
+          return template.cleanedStringMeta();
+        }
       }).extend({ trackArrayChanges: true });
     }
 
@@ -1260,20 +1319,6 @@ var Collection = function (vm, collection) {
     });
   });
 
-  self.template.getMeta = function (extraCheck) {
-    var iterable = self.template.fieldsAttributes();
-    if (self.template.fields().length > 0) {
-      iterable = self.template.fields();
-    }
-    return $.map(iterable, function (field) {
-      if (typeof field !== 'undefined' && field.name() != '' && extraCheck(field.type())) {
-        return field;
-      }
-    }).sort(function (a, b) {
-      return a.name().toLowerCase().localeCompare(b.name().toLowerCase());
-    });
-  }
-
   function alwaysTrue() {
     return true;
   }
@@ -1290,25 +1335,10 @@ var Collection = function (vm, collection) {
     return !isNumericColumn(type) && !isDateTimeColumn(type);
   }
 
-  self.template.cleanedMeta = ko.computed(function () {
-    return self.template.getMeta(alwaysTrue);
-  });
-
-  self.template.cleanedNumericMeta = ko.computed(function () {
-    return self.template.getMeta(isNumericColumn);
-  });
-
-  self.template.cleanedStringMeta = ko.computed(function () {
-    return self.template.getMeta(isStringColumn);
-  });
-
-  self.template.cleanedDateTimeMeta = ko.computed(function () {
-    return self.template.getMeta(isDateTimeColumn);
-  });
-
   self.template.hasDataForChart = ko.computed(function () {
     var hasData = false;
-    if (self.template.chartSettings.chartType() == ko.HUE_CHARTS.TYPES.BARCHART || self.template.chartSettings.chartType() == ko.HUE_CHARTS.TYPES.LINECHART) {
+
+    if ([ko.HUE_CHARTS.TYPES.BARCHART, ko.HUE_CHARTS.TYPES.LINECHART, ko.HUE_CHARTS.TYPES.TIMELINECHART].indexOf(self.template.chartSettings.chartType()) >= 0) {
       hasData = typeof self.template.chartSettings.chartX() != "undefined" && self.template.chartSettings.chartX() != null && self.template.chartSettings.chartYMulti().length > 0;
     }
     else {
@@ -1641,8 +1671,6 @@ var Collection = function (vm, collection) {
     var nestedFacet = facet.properties.facets()[0];
 
     if (nestedFacet.isDate()) {
-      nestedFacet.start(moment(data.from).utc().format("YYYY-MM-DD[T]HH:mm:ss[Z]"));
-      nestedFacet.end(moment(data.to).utc().format("YYYY-MM-DD[T]HH:mm:ss[Z]"));
       nestedFacet.min(moment(data.from).utc().format("YYYY-MM-DD[T]HH:mm:ss[Z]"));
       nestedFacet.max(moment(data.to).utc().format("YYYY-MM-DD[T]HH:mm:ss[Z]"));
     }
@@ -1928,6 +1956,7 @@ var SearchViewModel = function (collection_json, query_json, initial_json, has_g
           id: facet_id,
           has_data: false,
           resultHash: '',
+          filterHash: '',
           counts: [],
           label: '',
           field: '',
@@ -2351,6 +2380,7 @@ var SearchViewModel = function (collection_json, query_json, initial_json, has_g
             query: ko.mapping.toJSON(self.query),
             layout: ko.mapping.toJSON(self.columns)
           }, function (data) {
+            huePubSub.publish('charts.state');
             data = JSON.bigdataParse(data);
             try {
               if (self.collection.engine() == 'solr') {
@@ -2435,8 +2465,19 @@ var SearchViewModel = function (collection_json, query_json, initial_json, has_g
     self._make_result_facet = function (new_facet) {
       var facet = self.getFacetFromQuery(new_facet.id);
       var _hash = ko.mapping.toJSON(new_facet);
-
-      if (!facet.has_data() || facet.resultHash() != _hash) {
+      var _fq = (function() {
+        var _fq, fqs = self.query.fqs();
+        for (var i = 0; i < fqs.length; i++) {
+          var fq = fqs[i];
+          if (fq.id() == new_facet.id) {
+            _fq = fq;
+            break;
+          }
+        }
+        return _fq;
+      })();
+      var _filterHash = _fq ? ko.mapping.toJSON(_fq) : '';
+      if (!facet.has_data() || facet.resultHash() != _hash || facet.filterHash() != _filterHash) {
         if (facet.countsSelectedSubscription) {
           facet.countsSelectedSubscription.dispose();
         }
@@ -2455,19 +2496,7 @@ var SearchViewModel = function (collection_json, query_json, initial_json, has_g
         $.each(facet.counts(), function (index, item) {
           item.text = item.value + ' (' + item.count + ')';
         });
-        var countsSelected = (function() {
-          var _fq;
-          var fqs = self.query.fqs();
-          for (var i = 0; i < fqs.length; i++) {
-            var fq = fqs[i];
-            if (fq.id() == new_facet.id) {
-              _fq = fq;
-              break;
-            }
-          }
-          return _fq && _fq.filter()[0].value();
-        })();
-
+        var countsSelected = _fq && _fq.filter()[0].value() || "";
         if (!facet.countsFiltered) {
           facet.countsSelected = ko.observable(countsSelected);
           facet.countsFiltered = ko.pureComputed(function() {
@@ -2481,8 +2510,17 @@ var SearchViewModel = function (collection_json, query_json, initial_json, has_g
           });
         }
         setTimeout(function () { // Delay the execution, because setting facet.counts() above sets the value of countsSelected to ""
-          facet.countsSelected(countsSelected);
           facet.countsSelectedSubscription = facet.countsSelected.subscribe(function (value) {
+            var bIsSingleSelect = self.collection.facets()
+            .filter(function (facet) { return facet.id() == new_facet.id; })
+            .reduce(function (isSingle, facet) {
+              if (!facet.properties.facets) {
+                return isSingle;
+              }
+              var dimension = facet.properties.facets()[0];
+              return isSingle || (dimension.multiselect && !dimension.multiselect());
+            }, false);
+            if (!bIsSingleSelect) return;
             var counts = facet.counts();
             for (var i = 0; i < counts.length; i++) {
               if (counts[i].value == value && value !== '') {
@@ -2492,8 +2530,8 @@ var SearchViewModel = function (collection_json, query_json, initial_json, has_g
             }
             self.query.toggleFacetClear({ widget_id: new_facet.id });
           });
+          facet.countsSelected(countsSelected);
         },1);
-
         if (typeof new_facet.docs != 'undefined') {
           var _docs = [];
 
@@ -2515,7 +2553,11 @@ var SearchViewModel = function (collection_json, query_json, initial_json, has_g
         facet.field(new_facet.field);
         facet.dimension(new_facet.dimension);
         facet.extraSeries(typeof new_facet.extraSeries != 'undefined' ? new_facet.extraSeries : []);
+        facet.hideStacked = ko.computed(function () {
+          return !facet.extraSeries() || !facet.extraSeries().length;
+        });
         facet.resultHash(_hash);
+        facet.filterHash(_filterHash);
         facet.has_data(true);
       }
     }
@@ -2846,6 +2888,10 @@ var SearchViewModel = function (collection_json, query_json, initial_json, has_g
 
     ko.mapping.fromJS(c.template, self.collection.template);
 
+    self.collection.template.chartSettings.hideStacked = ko.computed(function () {
+      return self.collection.template.chartSettings.chartYMulti().length <= 1;
+    });
+
     for (var setting in self.collection.template.chartSettings) {
       self.collection.template.chartSettings[setting].subscribe(function () {
         huePubSub.publish('gridChartForceUpdate');

+ 99 - 93
desktop/libs/dashboard/src/dashboard/templates/common_search.mako

@@ -325,15 +325,6 @@ from dashboard.conf import USE_GRIDSTER, USE_NEW_ADD_METHOD, HAS_REPORT_ENABLED,
                        <i class="fa fa-bar-chart"></i>
          </a>
     </div>
-    <div data-bind="visible: !$root.collection.supportAnalytics(),
-                    css: { 'draggable-widget': true, 'disabled': !availableDraggableNumbers() },
-                    draggable: {data: draggableLine(), isEnabled: availableDraggableNumbers,
-                    options: getDraggableOptions({ data: draggableLine() }) }"
-         title="${_('Line Chart')}" rel="tooltip" data-placement="top">
-         <a data-bind="style: { cursor: $root.availableDraggableNumbers() ? 'move' : 'default' }">
-                       <i class="hcha hcha-line-chart"></i>
-         </a>
-    </div>
     <div data-bind="visible: !$root.collection.supportAnalytics(), css: { 'draggable-widget': true, 'disabled': false },
                     draggable: {data: draggableTree(), isEnabled: true,
                     options: getDraggableOptions({ data: draggableTree() }) }"
@@ -369,14 +360,6 @@ from dashboard.conf import USE_GRIDSTER, USE_NEW_ADD_METHOD, HAS_REPORT_ENABLED,
                        <i class="hcha hcha-timeline-chart"></i>
          </a>
     </div>
-    <div data-bind="visible: $root.collection.supportAnalytics(), css: { 'draggable-widget': true, 'disabled': ! availableTimeline() },
-                    draggable: {data: draggableTimeline(), isEnabled: availableTimeline,
-                    options: getDraggableOptions({ data: draggableTimeline() }) }"
-         title="${_('Timeline')}" rel="tooltip" data-placement="top">
-         <a data-bind="style: { cursor: $root.availableTimeline() ? 'move' : 'default' }">
-                       <i class="fa fa-line-chart"></i>
-         </a>
-    </div>
     <div data-bind="visible: !$root.collection.supportAnalytics(), css: { 'draggable-widget': true, 'disabled': ! availableDraggableMap() },
                     draggable: {data: draggableMap(), isEnabled: availableDraggableMap,
                     options: getDraggableOptions({ data: draggableMap() }) }"
@@ -385,6 +368,14 @@ from dashboard.conf import USE_GRIDSTER, USE_NEW_ADD_METHOD, HAS_REPORT_ENABLED,
                        <i class="hcha hcha-map-chart"></i>
          </a>
     </div>
+    <div data-bind="visible: $root.collection.supportAnalytics(), css: { 'draggable-widget': true, 'disabled': ! availableTimeline() },
+                    draggable: {data: draggableTimeline(), isEnabled: availableTimeline,
+                    options: getDraggableOptions({ data: draggableTimeline() }) }"
+         title="${_('Timeline')}" rel="tooltip" data-placement="top">
+         <a data-bind="style: { cursor: $root.availableTimeline() ? 'move' : 'default' }">
+                       <i class="fa fa-line-chart"></i>
+         </a>
+    </div>
     <div data-bind="visible: $root.collection.supportAnalytics(), css: { 'draggable-widget': true, 'disabled': ! availableDraggableMap() },
                     draggable: {data: draggableGradienMap(), isEnabled: availableDraggableMap,
                     options: getDraggableOptions({ data: draggableGradienMap() }) }"
@@ -894,7 +885,7 @@ ${ dashboard.layout_skeleton(suffix='search') }
     <li data-bind="visible: chartType() == ko.HUE_CHARTS.TYPES.PIECHART" class="nav-header">${_('legend')}</li>
   </ul>
   <div data-bind="visible: chartType() != ''">
-    <select data-bind="options: (chartType() == ko.HUE_CHARTS.TYPES.BARCHART || chartType() == ko.HUE_CHARTS.TYPES.PIECHART) ? $root.collection.template.cleanedMeta : $root.collection.template.cleanedNumericMeta, value: chartX, optionsText: 'name', optionsValue: 'name', optionsCaption: '${_ko('Choose a column...')}', select2: { width: '100%', placeholder: '${ _ko("Choose a column...") }', update: chartX}" class="input-medium"></select>
+    <select data-bind="options: (chartType() == ko.HUE_CHARTS.TYPES.BARCHART || chartType() == ko.HUE_CHARTS.TYPES.PIECHART) ? $root.collection.template.cleanedMeta : chartType() == ko.HUE_CHARTS.TYPES.TIMELINECHART ? $root.collection.template.cleanedDateTimeMeta : $root.collection.template.cleanedNumericMeta, value: chartX, optionsText: 'name', optionsValue: 'name', optionsCaption: '${_ko('Choose a column...')}', select2: { width: '100%', placeholder: '${ _ko("Choose a column...") }', update: chartX}" class="input-medium"></select>
   </div>
 
   <ul class="nav nav-list" style="border: none; background-color: #FFF" data-bind="visible: chartType() != ''">
@@ -903,7 +894,7 @@ ${ dashboard.layout_skeleton(suffix='search') }
     <li data-bind="visible: chartType() == ko.HUE_CHARTS.TYPES.PIECHART" class="nav-header">${_('value')}</li>
   </ul>
 
-  <div style="overflow-y: auto; max-height: 220px" data-bind="visible: chartType() != '' && (chartType() == ko.HUE_CHARTS.TYPES.BARCHART || chartType() == ko.HUE_CHARTS.TYPES.LINECHART)">
+  <div style="overflow-y: auto; max-height: 220px" data-bind="visible: chartType() != '' && ([ko.HUE_CHARTS.TYPES.TIMELINECHART, ko.HUE_CHARTS.TYPES.BARCHART, ko.HUE_CHARTS.TYPES.LINECHART].indexOf(chartType()) >= 0 )">
     <ul class="unstyled" data-bind="foreach: $root.collection.template.cleanedNumericMeta">
       <li><input type="checkbox" data-bind="checkedValue: name, checked: $parent.chartYMulti" /> <span data-bind="text: $data.name"></span></li>
     </ul>
@@ -1265,14 +1256,16 @@ ${ dashboard.layout_skeleton(suffix='search') }
     </div>
 
     <!-- ko if: $root.collection.getFacetById($parent.id()) -->
-      <div data-bind="timelineChart: {datum: {counts: counts(), extraSeries: extraSeries(), widget_id: $parent.id(), label: label()}, stacked: $root.collection.getFacetById($parent.id()).properties.stacked(), enableSelection: $root.collection.getFacetById($parent.id()).properties.enableSelection(), field: field, label: label(), transformer: timelineChartDataTransformer,
+      <div style="position: relative;">
+      <div data-bind="timelineChart: {datum: {counts: counts(), extraSeries: extraSeries(), widget_id: $parent.id(), label: label()}, stacked: $root.collection.getFacetById($parent.id()).properties.stacked(), enableSelection: true, field: field, label: label(), transformer: timelineChartDataTransformer,
         type: $root.collection.getFacetById($parent.id()).properties.timelineChartType,
         fqs: $root.query.fqs,
         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}) },
-        onComplete: function(){ $root.getWidgetById($parent.id()).isLoading(false); }}" />
+        onComplete: function(){ $root.getWidgetById($parent.id()).isLoading(false); }}"/>
       <div class="clearfix"></div>
+      </div>
     <!-- /ko -->
   </div>
   <!-- /ko -->
@@ -1293,6 +1286,9 @@ ${ dashboard.layout_skeleton(suffix='search') }
     </div>
 
     <div style="padding-bottom: 10px; text-align: right; padding-right: 20px">
+      <div class="inline-block" style="padding-bottom: 10px; padding-right: 20px" data-bind="visible: canReset">
+        <a href="javascript:void(0)" data-bind="click: $root.collection.rangeZoomOut"></i> ${ _('reset') }</a>
+      </div>
       <span data-bind="with: $root.collection.getFacetById($parent.id())">
         <span class="facet-field-label">${ _('Chart Type') }</span>
         <select class="input-small" data-bind="options: $root.timelineChartTypes,
@@ -1307,8 +1303,6 @@ ${ dashboard.layout_skeleton(suffix='search') }
                        value: properties.facets()[0].gap">
         </select>&nbsp;
       </span>
-      <span class="facet-field-label">${ _('Zoom') }</span>
-      <a href="javascript:void(0)" data-bind="click: $root.collection.rangeZoomOut2"><i class="fa fa-search-minus"></i> ${ _('reset') }</a>
     </div>
 
     <span data-bind="template: { name: 'data-grid' }"></span>
@@ -1317,7 +1311,6 @@ ${ dashboard.layout_skeleton(suffix='search') }
   <!-- /ko -->
 </script>
 
-
 <script type="text/html" id="bar-widget">
   <div class="widget-spinner" data-bind="visible: isLoading()">
     <i class="fa fa-spinner fa-spin"></i>
@@ -1340,6 +1333,7 @@ ${ dashboard.layout_skeleton(suffix='search') }
     </div>
 
     <!-- ko if: $root.collection.getFacetById($parent.id()) -->
+    <div style="position: relative;">
     <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(),
       fqs: $root.query.fqs,
       transformer: ($data.type == 'range-up' ? barChartRangeUpDataTransformer : barChartDataTransformer),
@@ -1359,6 +1353,7 @@ ${ dashboard.layout_skeleton(suffix='search') }
       onComplete: function(){ searchViewModel.getWidgetById($parent.id()).isLoading(false); },
       type: $root.collection.getFacetById($parent.id()).properties.timelineChartType }"
     />
+    </div>
     <div class="clearfix"></div>
     <!-- /ko -->
   </div>
@@ -1609,6 +1604,9 @@ ${ dashboard.layout_skeleton(suffix='search') }
           <!-- 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(),
               fqs: $root.query.fqs,
+              enableSelection: true,
+              hideSelection: true,
+              hideStacked: hideStacked,
               transformer: ($data.type == 'range-up' ? barChartRangeUpDataTransformer : barChartDataTransformer),
               onStateChange: function(state){ $root.collection.getFacetById($parent.id()).properties.stacked(state.stacked); },
               onClick: function(d) {
@@ -1633,6 +1631,9 @@ ${ dashboard.layout_skeleton(suffix='search') }
             <div data-bind="barChart: {datum: {counts: counts(), widget_id: $parent.id(), label: label()}, stacked: $root.collection.getFacetById($parent.id()).properties.stacked(),
               isPivot: true,
               fqs: $root.query.fqs,
+              enableSelection: true,
+              hideSelection: true,
+              hideStacked: hideStacked,
               transformer: pivotChartDataTransformer,
               onStateChange: function(state){ $root.collection.getFacetById($parent.id()).properties.stacked(state.stacked); },
               onClick: function(d) {
@@ -1648,6 +1649,9 @@ ${ dashboard.layout_skeleton(suffix='search') }
           <div data-bind="timelineChart: {datum: {counts: counts(), extraSeries: extraSeries(), widget_id: $parent.id(), label: label()}, stacked: $root.collection.getFacetById($parent.id()).properties.stacked(), field: field, label: label(), transformer: timelineChartDataTransformer,
             type: $root.collection.getFacetById($parent.id()).properties.timelineChartType,
             fqs: $root.query.fqs,
+            enableSelection: true,
+            hideSelection: true,
+            hideStacked: hideStacked,
             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}) },
@@ -1676,6 +1680,9 @@ ${ dashboard.layout_skeleton(suffix='search') }
           <div data-bind="timelineChart: {datum: {counts: counts(), extraSeries: extraSeries(), widget_id: $parent.id(), label: label()}, stacked: $root.collection.getFacetById($parent.id()).properties.stacked(), field: field, label: label(), transformer: timelineChartDataTransformer,
             type: $root.collection.getFacetById($parent.id()).properties.timelineChartType,
             fqs: $root.query.fqs,
+            enableSelection: true,
+            hideSelection: true,
+            hideStacked: hideStacked,
             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}) },
@@ -1718,7 +1725,7 @@ ${ dashboard.layout_skeleton(suffix='search') }
           <div data-bind="attr:{'id': 'pieChart_'+id()}, pieChart: {data: {counts: $root.results(), sorting: $root.collection.template.chartSettings.chartSorting(), snippet: $data}, fqs: ko.observableArray([]),
                 transformer: pieChartDataTransformerGrid, maxWidth: 350, parentSelector: '.chart-container' }, visible: $root.collection.template.chartSettings.chartType() == ko.HUE_CHARTS.TYPES.PIECHART" class="chart"></div>
 
-          <div data-bind="attr:{'id': 'barChart_'+id()}, barChart: {datum: {counts: $root.results(), sorting: $root.collection.template.chartSettings.chartSorting(), snippet: $data}, fqs: ko.observableArray([]), hideSelection: true,
+          <div data-bind="attr:{'id': 'barChart_'+id()}, barChart: {datum: {counts: $root.results(), sorting: $root.collection.template.chartSettings.chartSorting(), snippet: $data}, fqs: ko.observableArray([]), hideSelection: true, enableSelection: false, hideStacked: $root.collection.template.chartSettings.hideStacked,
                 transformer: multiSerieDataTransformerGrid, stacked: false, showLegend: true, type: $root.collection.template.chartSettings.chartSelectorType},  stacked: true, showLegend: true, visible: $root.collection.template.chartSettings.chartType() == ko.HUE_CHARTS.TYPES.BARCHART" class="chart"></div>
 
           <div data-bind="attr:{'id': 'lineChart_'+id()}, lineChart: {datum: {counts: $root.results(), sorting: $root.collection.template.chartSettings.chartSorting(), snippet: $data},
@@ -1726,6 +1733,9 @@ ${ dashboard.layout_skeleton(suffix='search') }
 
           <div data-bind="attr: {'id': 'leafletMapChart_'+id()}, leafletMapChart: {datum: {counts: $root.results(), sorting: $root.collection.template.chartSettings.chartSorting(), snippet: $data},
                 transformer: leafletMapChartDataTransformerGrid, showControls: false, height: 380, visible: $root.collection.template.chartSettings.chartType() == ko.HUE_CHARTS.TYPES.MAP, forceRedraw: true}" class="chart"></div>
+
+          <div data-bind="attr:{'id': 'timelineChart_'+id()}, timelineChart: {datum: {counts: $root.results(), sorting: $root.collection.template.chartSettings.chartSorting(), snippet: $data}, fqs: ko.observableArray([]), hideSelection: true, enableSelection: false, hideStacked: $root.collection.template.chartSettings.hideStacked,
+                transformer: multiSerieDataTransformerGrid, showControls: false }, visible: $root.collection.template.chartSettings.chartType() == ko.HUE_CHARTS.TYPES.TIMELINECHART" class="chart"></div>
           <div class="clearfix"></div>
         <!-- /ko -->
 
@@ -1775,28 +1785,11 @@ ${ dashboard.layout_skeleton(suffix='search') }
     <!-- ko with: $root.collection.getFacetById($parent.id()) -->
     <div>
       <span data-bind="template: { name: 'facet-toggle2' }"></span>
-
       <div class="pull-right">
-
-      <!-- ko if: properties.isDate -->
-        <div class="inline-block" style="padding-bottom: 10px; padding-right: 20px">
-          <span class="facet-field-label">${ _('Chart Type') }</span>
-          <select class="input-small" data-bind="options: $root.timelineChartTypes,
-                       optionsText: 'label',
-                       optionsValue: 'value',
-                       value: properties.timelineChartType">
-          </select>
-        </div>
-        <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.gap">
-          </select>
-        </div>
-      <!-- /ko -->
-      <!-- ko if: ['bar', 'line'].indexOf(properties.timelineChartType()) >= 0 -->
+      <div class="inline-block" style="padding-bottom: 10px; padding-right: 20px" data-bind="visible: canReset">
+        <a href="javascript:void(0)" data-bind="click: $root.collection.rangeZoomOut"></i> ${ _('reset') }</a>
+      </div>
+      <!-- ko if: properties.canRange -->
       <div class="inline-block" style="padding-bottom: 10px; padding-right: 20px">
         <span class="facet-field-label">${ _('Chart Type') }</span>
         <select class="input-small" data-bind="options: $root.timelineChartTypes,
@@ -1805,14 +1798,18 @@ ${ dashboard.layout_skeleton(suffix='search') }
                      value: properties.timelineChartType">
         </select>
       </div>
-       <!-- /ko -->
-
-      <!-- ko if: properties.canRange -->
+      <!-- /ko -->
+      <!-- ko if: properties.isDate -->
         <div class="inline-block" style="padding-bottom: 10px; padding-right: 20px">
-          <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>
+          <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>
@@ -1824,36 +1821,6 @@ ${ dashboard.layout_skeleton(suffix='search') }
   <!-- /ko -->
 </script>
 
-
-<script type="text/html" id="line-widget">
-  <div class="widget-spinner" data-bind="visible: isLoading()">
-    <i class="fa fa-spinner fa-spin"></i>
-  </div>
-
-  <!-- ko if: $root.getFacetFromQuery(id()).has_data() -->
-  <div class="row-fluid" data-bind="with: $root.getFacetFromQuery(id())">
-    <div data-bind="visible: $root.isEditing, with: $root.collection.getFacetById($parent.id())" style="margin-bottom: 20px">
-      <span data-bind="template: { name: 'facet-toggle' }">
-      </span>
-    </div>
-    <div class="clearfix"></div>
-    <div style="padding-bottom: 10px; text-align: right; padding-right: 20px" data-bind="visible: counts.length > 0">
-      <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>
-    </div>
-
-    <div data-bind="lineChart: {datum: {counts: counts(), widget_id: $parent.id(), label: label()}, field: field, label: label(),
-      transformer: lineChartDataTransformer,
-      onClick: function(d){ searchViewModel.query.selectRangeFacet({count: d.obj.value, widget_id: d.obj.widget_id, from: d.obj.from, to: d.obj.to, cat: d.obj.field}) },
-      onSelectRange: function(from, to){ searchViewModel.collection.selectTimelineFacet({from: from, to: to, cat: field, widget_id: $parent.id()}) },
-      onComplete: function(){ searchViewModel.getWidgetById($parent.id()).isLoading(false); }}"
-    />
-    <div class="clearfix"></div>
-  </div>
-  <!-- /ko -->
-</script>
-
-
 <script type="text/html" id="pie-widget">
   <!-- ko if: $root.getFacetFromQuery(id()).has_data() -->
   <div class="row-fluid" data-bind="with: $root.getFacetFromQuery(id())">
@@ -2315,7 +2282,7 @@ ${ dashboard.layout_skeleton(suffix='search') }
     <!-- ko if: $data.type() == 'field' -->
     <div class="filter-box">
       <div class="title">
-        <a href="javascript:void(0)" class="pull-right" data-bind="click: function() { chartsUpdatingState(); $root.query.removeFilter($data); $root.search(); }">
+        <a href="javascript:void(0)" class="pull-right" data-bind="click: function() { huePubSub.publish('charts.state', { updating: true }); $root.query.removeFilter($data); $root.search(); }">
           <i class="fa fa-times"></i>
         </a>
         <span data-bind="text: $data.field"></span>
@@ -2338,7 +2305,7 @@ ${ dashboard.layout_skeleton(suffix='search') }
     <!-- ko if: $data.type() == 'range' || $data.type() == 'range-up' -->
     <div class="filter-box">
       <div class="title">
-        <a href="javascript:void(0)" class="pull-right" data-bind="click: function(){ chartsUpdatingState(); $root.query.removeFilter($data); $root.search() }">
+        <a href="javascript:void(0)" class="pull-right" data-bind="click: function(){ huePubSub.publish('charts.state', { updating: true }); $root.query.removeFilter($data); $root.search() }">
           <i class="fa fa-times"></i>
         </a>
         <span data-bind="text: $data.field"></span>
@@ -2377,7 +2344,7 @@ ${ dashboard.layout_skeleton(suffix='search') }
     <!-- ko if: $data.type() == 'map' -->
     <div class="filter-box">
       <div class="title">
-        <a href="javascript:void(0)" class="pull-right" data-bind="click: function(){ chartsUpdatingState(); $root.query.removeFilter($data); $root.search() }">
+        <a href="javascript:void(0)" class="pull-right" data-bind="click: function(){ huePubSub.publish('charts.state', { updating: true }); $root.query.removeFilter($data); $root.search() }">
           <i class="fa fa-times"></i>
         </a>
         <span data-bind="text: $data.lat"></span>, <span data-bind="text: $data.lon"></span>
@@ -3527,6 +3494,7 @@ function timelineChartDataTransformer(rawDatum) {
     _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,
       obj: item
     });
@@ -3537,23 +3505,61 @@ function timelineChartDataTransformer(rawDatum) {
     values: _data
   });
 
+  // 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:
+  // 1) The x axis can be unsorted
+  // 2) The stacked option does not render correctly.
+  // To fix this we pad series that don't have values with zeros
+
+  //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();
+      if (!values[x]) {
+        values[x] = {};
+      }
+      values[x][serie.label] = item;
+      return values;
+    }, values);
+    return values;
+  }, {});
+
+
   // If multi query
-  $(rawDatum.extraSeries).each(function (cnt, item) {
+  var keys = Object.keys(values).sort();
+  $(rawDatum.extraSeries).each(function (cnt, serie) {
     if (cnt == 0) {
       _datum = [];
     }
     var _data = [];
-    $(item.counts).each(function (cnt, item) {
-      _data.push({
-        series: cnt + 1,
-        x: new Date(moment(item.from ? item.from : item.value).valueOf()), // When started from a non timeline widget
-        y: item.from ? item.value : item.count,
-        obj: item
-      });
+
+    $(keys).each(function (cnt, key) {
+      if (values[key][serie.label]) {
+        var item = values[key][serie.label];
+        _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,
+          obj: item
+        });
+      } else {
+        var keys = Object.keys(values[key]);
+        var item = keys[0] && values[key][keys[0]];
+        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()),
+          y: copy.value,
+          obj: copy
+        });
+      }
     });
 
     _datum.push({
-      key: item.label,
+      key: serie.label,
       values: _data
     });
   });

+ 3 - 0
desktop/libs/notebook/src/notebook/static/notebook/js/notebook.ko.js

@@ -1022,6 +1022,9 @@ var EditorViewModel = (function() {
     self.chartMapType = ko.observable(typeof snippet.chartMapType != "undefined" && snippet.chartMapType != null ? snippet.chartMapType : 'marker');
     self.chartMapLabel = ko.observable(typeof snippet.chartMapLabel != "undefined" && snippet.chartMapLabel != null ? snippet.chartMapLabel : null);
     self.chartMapHeat = ko.observable(typeof snippet.chartMapHeat != "undefined" && snippet.chartMapHeat != null ? snippet.chartMapHeat : null);
+    self.hideStacked = ko.computed(function() {
+      return self.chartYMulti().length <= 1;
+    });
 
     self.hasDataForChart = ko.computed(function () {
       if (self.chartType() == ko.HUE_CHARTS.TYPES.BARCHART || self.chartType() == ko.HUE_CHARTS.TYPES.LINECHART || self.chartType() == ko.HUE_CHARTS.TYPES.TIMELINECHART) {

+ 3 - 3
desktop/libs/notebook/src/notebook/templates/editor_components.mako

@@ -1519,7 +1519,7 @@ ${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
                 <!-- /ko -->
 
                 <!-- ko if: chartType() == ko.HUE_CHARTS.TYPES.BARCHART -->
-                <div data-bind="attr:{'id': 'barChart_'+id()}, barChart: {skipWindowResize: true, datum: {counts: result.data, sorting: chartSorting(), snippet: $data, limit: chartLimit()}, fqs: ko.observableArray([]), hideSelection: true,
+                <div data-bind="attr:{'id': 'barChart_'+id()}, barChart: {skipWindowResize: true, datum: {counts: result.data, sorting: chartSorting(), snippet: $data, limit: chartLimit()}, fqs: ko.observableArray([]), hideSelection: true, enableSelection: false, hideStacked: hideStacked,
                       transformer: editorMultiSerieDataTransformer, stacked: false, showLegend: true, isPivot: typeof chartXPivot() !== 'undefined', type: chartTimelineType},  stacked: true, showLegend: true, visible: chartType() == ko.HUE_CHARTS.TYPES.BARCHART" class="chart"></div>
                 <!-- /ko -->
 
@@ -1529,8 +1529,8 @@ ${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
                 <!-- /ko -->
 
                 <!-- ko if: chartType() == ko.HUE_CHARTS.TYPES.TIMELINECHART -->
-                <div data-bind="attr:{'id': 'timelineChart_'+id()}, timelineChart: {type: chartTimelineType, skipWindowResize: true, datum: {counts: result.data, sorting: chartSorting(), snippet: $data, limit: chartLimit()}, fqs: ko.observableArray([]), hideSelection: true,
-                      transformer: editorTimelineChartDataTransformer, stacked: false, showLegend: true}, hideSelection: true, visible: chartType() == ko.HUE_CHARTS.TYPES.TIMELINECHART" class="chart"></div>
+                <div data-bind="attr:{'id': 'timelineChart_'+id()}, timelineChart: {type: chartTimelineType, skipWindowResize: true, datum: {counts: result.data, sorting: chartSorting(), snippet: $data, limit: chartLimit()}, fqs: ko.observableArray([]), hideSelection: true, enableSelection: false, hideStacked: hideStacked,
+                      transformer: editorTimelineChartDataTransformer, stacked: false, showLegend: true}, visible: chartType() == ko.HUE_CHARTS.TYPES.TIMELINECHART" class="chart"></div>
                 <!-- /ko -->
 
                 <!-- ko if: chartType() == ko.HUE_CHARTS.TYPES.MAP -->