Search code examples
twitter-bootstrap-3flot

Bootstrap popover on flot graph


I am trying to add popover on my graph. it is not working.

var datasets = [{
  "label": "Amend Existing Report",
  "data": [{
    "0": 1446422400000,
    "1": 0
  }, {
    "0": 1447027200000,
    "1": 0
  }, {
    "0": 1447632000000,
    "1": 0
  }, {
    "0": 1448236800000,
    "1": 0
  }, {
    "0": 1448841600000,
    "1": 0
  }, {
    "0": 1449446400000,
    "1": 1
  }, {
    "0": 1450051200000,
    "1": 0
  }, {
    "0": 1450656000000,
    "1": 0
  }, {
    "0": 1451260800000,
    "1": 0
  }, {
    "0": 1451865600000,
    "1": 0
  }, {
    "0": 1452470400000,
    "1": 0
  }, {
    "0": 1453075200000,
    "1": 1
  }, {
    "0": 1453680000000,
    "1": 1
  }, {
    "0": 1454284800000,
    "1": 0
  }],
  "idx": 0
}, {
  "label": "Investigate Report Problem",
  "data": [{
    "0": 1446422400000,
    "1": 1
  }, {
    "0": 1447027200000,
    "1": 0
  }, {
    "0": 1447632000000,
    "1": 2
  }, {
    "0": 1448236800000,
    "1": 4
  }, {
    "0": 1448841600000,
    "1": 0
  }, {
    "0": 1449446400000,
    "1": 1
  }, {
    "0": 1450051200000,
    "1": 0
  }, {
    "0": 1450656000000,
    "1": 2
  }, {
    "0": 1451260800000,
    "1": 0
  }, {
    "0": 1451865600000,
    "1": 0
  }, {
    "0": 1452470400000,
    "1": 0
  }, {
    "0": 1453075200000,
    "1": 5
  }, {
    "0": 1453680000000,
    "1": 0
  }, {
    "0": 1454284800000,
    "1": 0
  }],
  "idx": 1
}, {
  "label": "New Request (One Off Report)",
  "data": [{
    "0": 1446422400000,
    "1": 0
  }, {
    "0": 1447027200000,
    "1": 0
  }, {
    "0": 1447632000000,
    "1": 1
  }, {
    "0": 1448236800000,
    "1": 0
  }, {
    "0": 1448841600000,
    "1": 0
  }, {
    "0": 1449446400000,
    "1": 0
  }, {
    "0": 1450051200000,
    "1": 0
  }, {
    "0": 1450656000000,
    "1": 0
  }, {
    "0": 1451260800000,
    "1": 0
  }, {
    "0": 1451865600000,
    "1": 0
  }, {
    "0": 1452470400000,
    "1": 0
  }, {
    "0": 1453075200000,
    "1": 0
  }, {
    "0": 1453680000000,
    "1": 1
  }, {
    "0": 1454284800000,
    "1": 0
  }],
  "idx": 2
}, {
  "label": "New Request (Regular Report)",
  "data": [{
    "0": 1446422400000,
    "1": 4
  }, {
    "0": 1447027200000,
    "1": 0
  }, {
    "0": 1447632000000,
    "1": 2
  }, {
    "0": 1448236800000,
    "1": 2
  }, {
    "0": 1448841600000,
    "1": 0
  }, {
    "0": 1449446400000,
    "1": 1
  }, {
    "0": 1450051200000,
    "1": 0
  }, {
    "0": 1450656000000,
    "1": 0
  }, {
    "0": 1451260800000,
    "1": 1
  }, {
    "0": 1451865600000,
    "1": 1
  }, {
    "0": 1452470400000,
    "1": 0
  }, {
    "0": 1453075200000,
    "1": 3
  }, {
    "0": 1453680000000,
    "1": 2
  }, {
    "0": 1454284800000,
    "1": 0
  }],
  "idx": 3
}, {
  "label": "Other",
  "data": [{
    "0": 1446422400000,
    "1": 0
  }, {
    "0": 1447027200000,
    "1": 0
  }, {
    "0": 1447632000000,
    "1": 2
  }, {
    "0": 1448236800000,
    "1": 4
  }, {
    "0": 1448841600000,
    "1": 2
  }, {
    "0": 1449446400000,
    "1": 0
  }, {
    "0": 1450051200000,
    "1": 2
  }, {
    "0": 1450656000000,
    "1": 0
  }, {
    "0": 1451260800000,
    "1": 0
  }, {
    "0": 1451865600000,
    "1": 0
  }, {
    "0": 1452470400000,
    "1": 3
  }, {
    "0": 1453075200000,
    "1": 0
  }, {
    "0": 1453680000000,
    "1": 3
  }, {
    "0": 1454284800000,
    "1": 0
  }],
  "idx": 4
}, {
  "label": "Special Events",
  "data": [{
    "0": 1446422400000,
    "1": 0
  }, {
    "0": 1447027200000,
    "1": 0
  }, {
    "0": 1447632000000,
    "1": 0
  }, {
    "0": 1448236800000,
    "1": 1
  }, {
    "0": 1448841600000,
    "1": 0
  }, {
    "0": 1449446400000,
    "1": 3
  }, {
    "0": 1450051200000,
    "1": 1
  }, {
    "0": 1450656000000,
    "1": 0
  }, {
    "0": 1451260800000,
    "1": 0
  }, {
    "0": 1451865600000,
    "1": 0
  }, {
    "0": 1452470400000,
    "1": 0
  }, {
    "0": 1453075200000,
    "1": 0
  }, {
    "0": 1453680000000,
    "1": 0
  }, {
    "0": 1454284800000,
    "1": 0
  }],
  "idx": 5
}];

var ticks = [];
for (var i = 0; i < datasets[0].data.length; i++) {
  ticks.push(datasets[0].data[i][0]);
}

var options = {
  "legend": {
    "position": "ne",
    "noColumns": 6
  },
  "yaxis": {
    "min": 0
  },
  "xaxis": {
    "mode": "time",
    "timeformat": "%d %b",
    //    "tickSize": [7, "day"],
    ticks: ticks,
    "min": 1446163200000,
    "max": 1454544000000 // 1454284800000
  },
  "grid": {
    "clickable": true,
    "hoverable": true
  },
  "series": {
    "stack": true,
    "bars": {
      "show": true,
      "barWidth": 181440000.00000003,
      align: 'center'
    }
  }
};

$.plot($('#CAGraph'), datasets, options);




$("#CAGraph").bind("plothover",function(event, pos, item) {

	if (item) {
//console.log(item);
			var epoch = new Date(item.datapoint[0]);
			var percent = item.datapoint[1].toFixed(0);
			$('#tooltip').attr("data-original-title", item.series.label);
			$('#tooltip').attr("data-content", (percent) + "<br>Total: " + item.datapoint[1]);
			$("#tooltip").popover("show");
			
			$("#tooltip").popover({
				html: true,
				title : function() {
			          return $(".popover-title").html();
				},
				content : function() {
			          return  $(".popover-content").html();
				}
				
			});
			$(".popover").css({
				top : item.pageY,
				left : item.pageX + 10
			});
			$(".popover.right>.arrow").css({
				top : "20%",
			});

		} else {
			$('#tooltip').attr("title","");
			$('#tooltip').attr("data-content", "");
			
			$("#tooltip").popover("hide");
		}	
  
  });
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://rawgit.com/flot/flot/master/jquery.flot.js"></script>
<script src="https://rawgit.com/Codicode/flotanimator/master/jquery.flot.animator.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flot/0.8.3/jquery.flot.time.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flot/0.8.3/jquery.flot.stack.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flot.tooltip/0.8.5/jquery.flot.tooltip.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.2/moment.min.js"></script>

<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha256-7s5uDGW3AHqw6xtJmNNtr+OBRJUlgkNJEo78P4b0yRw= sha512-nNo+yCHEyn0smMxSswnf/OnX6/KwJuZTlNZBjauKhTK0c+zT+q5JOCx0UFhXQ6rJR9jg6Es8gPuD2uZcYDLqSw==" crossorigin="anonymous">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha256-KXn5puMvxCw+dAYznun+drMdG1IFl3agK0p/pqT9KAo= sha512-2e8qq0ETcfWRI4HJBzQiA3UoyFk6tbNyG+qSaIBZLyW9Xf3sWZHN/lxe9fTh1U45DpPf07yj94KsUHHWe4Yk1A==" crossorigin="anonymous"></script>



<div id="choices_CAGraph"></div>
<div id="CAGraph" style="width:910px;height:400px"></div>
<div id=tooltip class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>


Solution

  • OK, any solution has to take into account the async behavior of Bootstrap's popover functionality as Raidri correctly stated a couple of comments ago. Plus it has to take into account the fact that the plothover event is going to be firing a lot faster than the async popover show/hide calls finish. In other words, you are going to have to pay special attention to the state of the system.

    This led me to the understanding that creating the popover object over and over again as part of the hover event handler was a no-no. It has to be created once and then just shown and hidden.

    I note also that in your latest code you again ignored my previous point that the title and content properties are either strings or functions that return a string. You are returning jQuery objects from yours -- wrong.

    First up I created a new jQuery function. This would help me maintain the state needed in a closure, including the popover object.

    $.fn.popoverTooltip = function (selector, popoverSelector) {
      // the rest of the code forming a nice closure
    }
    $.plot('#CAGraph', datasets, options);
    $("#tooltip").popoverTooltip("#CAGraph", ".popover");
    

    As part of this enclosed code, I created some local variables at the top:

    var barIdShown = null;
    var chart = $(selector);
    var tooltip = $(this);
    
    var popoverProcessor = function () {
      // mysterious code maintaining state
    }();
    

    And then a new object called popoverProcessor (code to be shown in a moment) that was going to do most of the actual work, and maintain state.

    After that code, I create the actual popover and bound some event handlers. First: I need to know when the popover hide/show functionality actually finishes so I added handlers to the relevant BS popover events. Second: I bound a handler to the plothover event to process showing or hiding the tooltip.

    //create popover
    tooltip.popover({
      html: true,
      title : popoverProcessor.getTitle,
      content : popoverProcessor.getContent
    });
    
    // bind events to know when shown or hidden
    tooltip.on("hidden.bs.popover", popoverProcessor.hideDone);
    tooltip.on("shown.bs.popover", popoverProcessor.showDone);
    
    // bind hover event to chart
    chart.bind("plothover", function(event, pos, item) {
      var thisBarId;
      if (item) {
        thisBarId = seriesIndex * 10000 + dataIndex;
        if (thisBarId !== barIdShown) {
          if (barIdShown) {
            popoverProcessor.hide();
          }
          popoverProcessor.setItem(item);
          popoverProcessor.show();
          barIdShown = thisBarId;
        }
      }
      else {
        if (barIdShown) {
          popoverProcessor.hide();
          barIdShown = null;
        }
      }
    });
    

    Notice first that I use functions inside my popoverProcessor to return the title and content of the tooltip. Then in order to know if the cursor hovers over another bar segment without moving outside the bar, I create a special "bar identifier". (If it changes I hide the popover, before re-showing it.) It's all nicely "synchronous" inside this handler, note; the async part is handled inside this mysterious popoverProcessor object.

    var popoverProcessor = function () {
      var item = null;
      var state = "hidden";
      var taskQueue = [];
      var showPopover = function () {
        tooltip.popover("show");
        $(popoverSelector).css({
          top : item.pageY,
          left : item.pageX + 10
        });
        $(".popover.right > .arrow").css({
          top : "20%",
        });      
        state = "showing";
      };
      var hidePopover = function () {
        tooltip.popover("hide");
        state = "hiding";
      };
      var processNextTask = function () {
        var task;
        if (taskQueue.length > 0) {
          task = taskQueue.shift();
          if (task === "show") {
            showPopover();
          }
          else {
            hidePopover();
          }      
        }
      };
    
      return {
        setItem: function (newItem) { item = newItem; },
        getTitle: function () { 
          if (item) {
            return item.series.label; 
          }
          return "unknown item";
        },
        getContent: function () { 
          var percent;
          if (item) {
            percent = item.datapoint[1].toFixed(0);
            return percent.toString() + "<br />Total: " + item.datapoint[1];
          }
          return "unknown item";
        },
        hideDone: function () {
          state = "hidden";
          processNextTask();
        },
        showDone: function () {
          state = "shown";
          processNextTask();
        },
        hide: function () {
          if (state === "shown") {
            hidePopover();
          }
          else {
            taskQueue.push("hide");
          }
        },
        show: function () {
          if (state === "hidden") {
            showPopover();
          }
          else {
            taskQueue.push("show");
          }
        }
      };
    }();
    

    The public object has a set of methods. You can set the item being processed, you can get the title and content for the popover, you can signal that the popover has shown (or has been hidden) and you can ask for the popover to be shown or hidden.

    The processor maintains the current state of the popover object. These are: "hidden", "showing", "shown", and "hiding". If you call hide() and the state is "shown", the code immediately calls the internal function hidePopover to start hiding the popover, otherwise an item is added to a task queue to indicate that the popover should be hidden when possible. A similar thing happens if you call show().

    The fun stuff happens in the event handlers showDone() and hideDone(). This is where the next task is popped off the task queue and is processed. Using this task queue, I am maintaining the sequence of hide/show calls in the Bootstrap async environment, ensuring that a new display state change is only initiated when the previous one has completed.

    Note also that it's when .popover("show") is called that the title and content for the tooltip are actually calculated via the functions provided.

    No doubt this code could be refactored a bit to be made simpler, but I'm done.