block by timelyportfolio 5200272

fork of https://gist.github.com/benjchristensen/2657838 with zoomooz.js and intro.js functionality

Full Screen

#Quick Fork to add Zoom and Intro Zoom functionality from zoomooz.js Intro functionality from intro.js

#Original Readme.md below


Proof of concept line graph implemented using d3.js and some jQuery that builds on previous examples.

The top graph is 24 hours of data in 2 minute increments. I have it rolling every 2 seconds to simulate live updating. In real-life it would only update every 2 minutes to match the data granularity.

See it running at http://bl.ocks.org/2657838

Features:

Missing:

I don’t normally work in javascript, so if it isn’t quite right, I’d appreciate suggestions on where to improve it.

index.html

<html>
	<head>
		<title>Interactive Line Graph</title>
		<script src="//d3js.org/d3.v3.js"></script>
		<!-- 
			using JQuery for element dimensions
			This is a small aspect of this example so it can be removed fairly easily if needed.
		-->
		<script src="//code.jquery.com/jquery-1.7.2.min.js"></script>
		<script src="sample_data.js"></script>
		<script src="line-graph.js"></script>
	        <script src="jquery.zoomooz.js"></script>
	        <script src="intro.js"></script>
	
	        <link href="introjs.min.css" rel="stylesheet">
		<link rel="stylesheet" href="style.css" type="text/css">
		<style>
			body {
				font-family: "Helvetica Neue", Helvetica;
			}
					
			p {
				clear:both;
				top: 20px;
			}		
					
			div.aGraph {
				margin-bottom: 30px;
			}
		</style>
	</head>
	<body>
	<div id="graph1" class="aGraph" style="position:relative;width:100%;height:400px" data-step="1" data-intro="This view offers a realtime look at the data and continously updates."></div>
	
	<div id="graph2" class="aGraph zoomTarget" data-targetsize="0.8" style="float:left;position:relative;width:49%;height:200px" data-step="2" data-intro="A look in to the future since this data is a couple of hours ahead of above.  Click to zoom."></div>
	<div id="graph3" class="aGraph zoomTarget" data-targetsize="0.8" style="float:left;position:relative;width:49%;height:200px" data-step="3" data-intro="5 days for comparison purposes.  Click on it to zoom in"></div>

	<script>
		/* 
		 * If running inside bl.ocks.org we want to resize the iframe to fit both graphs
		 */
		 if(parent.document.getElementsByTagName("iframe")[0]) {
			 parent.document.getElementsByTagName("iframe")[0].setAttribute('style', 'height: 650px !important');
		 }
	
		 /*
		 * Note how the 'data' object is added to here before rendering to provide decoration information.
		 * <p>
		 * This is purposefully done here instead of in data.js as an example of how data would come from a server
		 * and then have presentation information injected into it (rather than as separate arguments in another object)
		 * and passed into LineGraph.
		 *
		 * Also, CSS can be used to style colors etc, but this is also doable via the 'data' object so that the styling
		 * of different data points can be done in code which is often more natural for display names, legends, line colors etc
		 */
		 // add presentation logic for 'data' object using optional data arguments
		 data["displayNames"] = ["2xx","3xx","4xx","5xx"];
		 data["colors"] = ["green","orange","red","darkred"];
		 data["scale"] = "pow";
		 
		 // add presentation logic for 'data' object using optional data arguments
		 data2["displayNames"] = ["2xx","3xx","4xx","5xx"];
		 data2["colors"] = ["green","orange","red","darkred"];
		 data2["scale"] = "linear";
		 
		 // add presentation logic for 'data' object using optional data arguments
		 data3["displayNames"] = ["Data1", "Data2"];
		 data3["axis"] = ["left", "right"];
		 data3["colors"] = ["#2863bc","#c8801c"];
		 data3["rounding"] = [2, 0];

		 // create graph now that we've added presentation config
		var l1 = new LineGraph({containerId: 'graph1', data: data});
		var l2 = new LineGraph({containerId: 'graph2', data: data2});
		var l3 = new LineGraph({containerId: 'graph3', data: data3});
		
	setInterval(function() {
		/*
		* The following will simulate live updating of the data (see dataA, dataB, dataC etc in data.js which are real examples)
		* This is being simulated so this example functions standalone without a backend server which generates data such as data.js contains.
		*/
		// for each data series ...
		var newData = [];
		data.values.forEach(function(dataSeries, index) {
			// take the first value and move it to the end
			// and capture the value we're moving so we can send it to the graph as an update
			var v = dataSeries.shift();
			dataSeries.push(v);
			// put this value in newData as an array with 1 value
			newData[index] = [v];
		})
		
		// we will reuse dataA each time
		dataA.values = newData;
		// increment time 1 step
		dataA.start = dataA.start + dataA.step;
		dataA.end = dataA.end + dataA.step; 
					
		l1.slideData(dataA);
	}, 2000);

	introJs().start();
	</script>



	</body>
</html>

intro.js

/**
 * Intro.js v0.1.0
 * https://github.com/usablica/intro.js
 * MIT licensed
 *
 * Copyright (C) 2013 usabli.ca - A weekend project by Afshin Mehrabani (@afshinmeh)
 */

(function () {

  //Default config/variables
  var VERSION = "0.1.0";

  /**
   * IntroJs main class
   *
   * @class IntroJs
   */
  function IntroJs(obj) {
    this._targetElement = obj;
  }

  /**
   * Initiate a new introduction/guide from an element in the page
   *
   * @api private
   * @method _introForElement
   * @param {Object} targetElm
   * @returns {Boolean} Success or not?
   */
  function _introForElement(targetElm) {
    var allIntroSteps = targetElm.querySelectorAll("*[data-intro]"),
        introItems = [],
        self = this;

    //if there's no element to intro
    if(allIntroSteps.length < 1) {
      return false;
    }

    for (var i = 0, elmsLength = allIntroSteps.length; i < elmsLength; i++) {
      var currentElement = allIntroSteps[i];
      introItems.push({
        element: currentElement,
        intro: currentElement.getAttribute("data-intro"),
        step: parseInt(currentElement.getAttribute("data-step"), 10),
        position: currentElement.getAttribute("data-position") || 'bottom'
      });
    }

    //Ok, sort all items with given steps
    introItems.sort(function (a, b) {
      return a.step - b.step;
    });

    //set it to the introJs object
    self._introItems = introItems;

    //add overlay layer to the page
    if(_addOverlayLayer.call(self, targetElm)) {
      //then, start the show
      _nextStep.call(self);

      var skipButton = targetElm.querySelector(".introjs-skipbutton"),
          nextStepButton = targetElm.querySelector(".introjs-nextbutton");

      window.onkeydown = function(e) {
        if (e.keyCode == 27) {
          //escape key pressed, exit the intro
          _exitIntro.call(self, targetElm);
        } else if(e.keyCode == 37) {
          //left arrow
          _previousStep.call(self);
        } else if (e.keyCode == 39 || e.keyCode == 13) {
          //right arrow or enter
          _nextStep.call(self);
        }
      };
    }
    return false;
  }

  /**
   * Go to next step on intro
   *
   * @api private
   * @method _nextStep
   */
  function _nextStep() {
    if (typeof(this._currentStep) === 'undefined') {
      this._currentStep = 0;
    } else {
      ++this._currentStep;
    }

    if((this._introItems.length) <= this._currentStep) {
      //end of the intro
      //check if any callback is defined
      if (this._introCompleteCallback != undefined) {
        this._introCompleteCallback.call(this);
      }
      _exitIntro.call(this, this._targetElement);
      return;
    }

    _showElement.call(this, this._introItems[this._currentStep].element);
  }

  /**
   * Go to previous step on intro
   *
   * @api private
   * @method _nextStep
   */
  function _previousStep() {
    if (this._currentStep == 0) {
      return false;
    }

    _showElement.call(this, this._introItems[--this._currentStep].element);
  }

  /**
   * Exit from intro
   *
   * @api private
   * @method _exitIntro
   * @param {Object} targetElement
   */
  function _exitIntro(targetElement) {
    //remove overlay layer from the page
    var overlayLayer = targetElement.querySelector(".introjs-overlay");
    //for fade-out animation
    overlayLayer.style.opacity = 0;
    setTimeout(function () {
      if (overlayLayer.parentNode) {
        overlayLayer.parentNode.removeChild(overlayLayer);
      }
    }, 500);
    //remove all helper layers
    var helperLayer = targetElement.querySelector(".introjs-helperLayer");
    if (helperLayer) {
      helperLayer.parentNode.removeChild(helperLayer);
    }
    //remove `introjs-showElement` class from the element
    var showElement = document.querySelector(".introjs-showElement");
    if (showElement) {
      showElement.className = showElement.className.replace(/introjs-[a-zA-Z]+/g, '').trim();
    }
    //clean listeners
    targetElement.onkeydown = null;
    //set the step to zero
    this._currentStep = undefined;
    //check if any callback is defined
    if (this._introExitCallback != undefined) {
      this._introExitCallback.call(this);
    }
  }

  /**
   * Render tooltip box in the page
   *
   * @api private
   * @method _placeTooltip
   * @param {Object} targetElement
   * @param {Object} tooltipLayer
   * @param {Object} arrowLayer
   */
  function _placeTooltip(targetElement, tooltipLayer, arrowLayer) {
    var tooltipLayerPosition = _getOffset(tooltipLayer);
    //reset the old style
    tooltipLayer.style.top = null;
    tooltipLayer.style.right = null;
    tooltipLayer.style.bottom = null;
    tooltipLayer.style.left = null;
    switch (targetElement.getAttribute('data-position')) {
      case 'top':
        tooltipLayer.style.left = "15px";
        tooltipLayer.style.top = "-" + (tooltipLayerPosition.height + 10) + "px";
        arrowLayer.className = 'introjs-arrow bottom';
        break;
      case 'right':
        console.log(tooltipLayerPosition);
        tooltipLayer.style.right = "-" + (tooltipLayerPosition.width + 10) + "px";
        arrowLayer.className = 'introjs-arrow left';
        break;
      case 'left':
        tooltipLayer.style.top = "15px";
        tooltipLayer.style.left = "-" + (tooltipLayerPosition.width + 10) + "px";
        arrowLayer.className = 'introjs-arrow right';
        break;
      case 'bottom':
      default:
        tooltipLayer.style.bottom = "-" + (tooltipLayerPosition.height + 10) + "px";
        arrowLayer.className = 'introjs-arrow top';
        break;
    }
  }

  /**
   * Show an element on the page
   *
   * @api private
   * @method _showElement
   * @param {Object} targetElement
   */
  function _showElement(targetElement) {

    var self = this,
        oldHelperLayer = document.querySelector(".introjs-helperLayer"),
        elementPosition = _getOffset(targetElement);

    if(oldHelperLayer != null) {
      var oldHelperNumberLayer = oldHelperLayer.querySelector(".introjs-helperNumberLayer"),
          oldtooltipLayer = oldHelperLayer.querySelector(".introjs-tooltiptext"),
          oldArrowLayer = oldHelperLayer.querySelector(".introjs-arrow"),
          oldtooltipContainer = oldHelperLayer.querySelector(".introjs-tooltip")

      //set new position to helper layer
      oldHelperLayer.setAttribute("style", "width: " + (elementPosition.width + 10)  + "px; " +
                                           "height:" + (elementPosition.height + 10) + "px; " +
                                           "top:"    + (elementPosition.top - 5)     + "px;" +
                                           "left: "  + (elementPosition.left - 5)    + "px;");
      //set current step to the label
      oldHelperNumberLayer.innerHTML = targetElement.getAttribute("data-step");
      //set current tooltip text
      oldtooltipLayer.innerHTML = targetElement.getAttribute("data-intro");
      var oldShowElement = document.querySelector(".introjs-showElement");
      oldShowElement.className = oldShowElement.className.replace(/introjs-[a-zA-Z]+/g, '').trim();
      _placeTooltip(targetElement, oldtooltipContainer, oldArrowLayer);
    } else {
      var helperLayer = document.createElement("div"),
          helperNumberLayer = document.createElement("span"),
          arrowLayer = document.createElement("div"),
          tooltipLayer = document.createElement("div");

      helperLayer.className = "introjs-helperLayer";
      helperLayer.setAttribute("style", "width: " + (elementPosition.width + 10)  + "px; " +
                                        "height:" + (elementPosition.height + 10) + "px; " +
                                        "top:"    + (elementPosition.top - 5)     + "px;" +
                                        "left: "  + (elementPosition.left - 5)    + "px;");

      //add helper layer to target element
      this._targetElement.appendChild(helperLayer);

      helperNumberLayer.className = "introjs-helperNumberLayer";
      arrowLayer.className = 'introjs-arrow';
      tooltipLayer.className = "introjs-tooltip";

      helperNumberLayer.innerHTML = targetElement.getAttribute("data-step");
      tooltipLayer.innerHTML = "<div class='introjs-tooltiptext'>" + targetElement.getAttribute("data-intro") + "</div><div class='introjs-tooltipbuttons'></div>";
      helperLayer.appendChild(helperNumberLayer);
      tooltipLayer.appendChild(arrowLayer);
      helperLayer.appendChild(tooltipLayer);

      var skipTooltipButton = document.createElement("a");
      skipTooltipButton.className = "introjs-skipbutton";
      skipTooltipButton.href = "javascript:void(0);";
      skipTooltipButton.innerHTML = "Skip";

      var nextTooltipButton = document.createElement("a");

      nextTooltipButton.onclick = function() {
        _nextStep.call(self);
      };

      nextTooltipButton.className = "introjs-nextbutton";
      nextTooltipButton.href = "javascript:void(0);";
      nextTooltipButton.innerHTML = "Next &rarr;";

      skipTooltipButton.onclick = function() {
        _exitIntro.call(self, self._targetElement);
      };

      var tooltipButtonsLayer = tooltipLayer.querySelector('.introjs-tooltipbuttons');
      tooltipButtonsLayer.appendChild(skipTooltipButton);
      tooltipButtonsLayer.appendChild(nextTooltipButton);

      //set proper position
      _placeTooltip(targetElement, tooltipLayer, arrowLayer);
    }

    //add target element position style
    targetElement.className += " introjs-showElement";

    //Thanks to JavaScript Kit: http://www.javascriptkit.com/dhtmltutors/dhtmlcascade4.shtml
    var currentElementPosition = "";
    if (targetElement.currentStyle) { //IE
      currentElementPosition = targetElement.currentStyle["position"];
    } else if (document.defaultView && document.defaultView.getComputedStyle) { //Firefox
      currentElementPosition = document.defaultView.getComputedStyle(targetElement, null).getPropertyValue("position");
    }

    //I don't know is this necessary or not, but I clear the position for better comparing
    currentElementPosition = currentElementPosition.toLowerCase();
    if (currentElementPosition != "absolute" && currentElementPosition != "relative") {
      //change to new intro item
      targetElement.className += " introjs-relativePosition";
    }

    //scroll the page to the element position
    if (typeof(targetElement.scrollIntoViewIfNeeded) === "function") {
      //awesome method guys: https://bugzilla.mozilla.org/show_bug.cgi?id=403510
      //but I think this method has some problems with IE < 7.0, I should find a proper failover way
      targetElement.scrollIntoViewIfNeeded();
    }
  }

  /**
   * Add overlay layer to the page
   *
   * @api private
   * @method _addOverlayLayer
   * @param {Object} targetElm
   */
  function _addOverlayLayer(targetElm) {
    var overlayLayer = document.createElement("div"),
        styleText = "",
        self = this;

    //set css class name
    overlayLayer.className = "introjs-overlay";

    //check if the target element is body, we should calculate the size of overlay layer in a better way
    if (targetElm.tagName.toLowerCase() == "body") {
      styleText += "top: 0;bottom: 0; left: 0;right: 0;position: fixed;";
      overlayLayer.setAttribute("style", styleText);
    } else {
      //set overlay layer position
      var elementPosition = _getOffset(targetElm);
      if(elementPosition) {
        styleText += "width: " + elementPosition.width + "px; height:" + elementPosition.height + "px; top:" + elementPosition.top + "px;left: " + elementPosition.left + "px;";
        overlayLayer.setAttribute("style", styleText);
      }
    }

    targetElm.appendChild(overlayLayer);

    overlayLayer.onclick = function() {
      _exitIntro.call(self, targetElm);
    };

    setTimeout(function() {
      styleText += "opacity: .5;";
      overlayLayer.setAttribute("style", styleText);
    }, 10);
    return true;
  }

  /**
   * Get an element position on the page
   * Thanks to `meouw`: http://stackoverflow.com/a/442474/375966
   *
   * @api private
   * @method _getOffset
   * @param {Object} element
   * @returns Element's position info
   */
  function _getOffset(element) {
    var elementPosition = {};

    //set width
    elementPosition.width = element.offsetWidth;

    //set height
    elementPosition.height = element.offsetHeight;

    //calculate element top and left
    var _x = 0;
    var _y = 0;
    while(element && !isNaN(element.offsetLeft) && !isNaN(element.offsetTop)) {
      _x += element.offsetLeft;
      _y += element.offsetTop;
      element = element.offsetParent;
    }
    //set top
    elementPosition.top = _y;
    //set left
    elementPosition.left = _x;

    return elementPosition;
  }

  var introJs = function (targetElm) {
    if (typeof (targetElm) === "object") {
      //Ok, create a new instance
      return new IntroJs(targetElm);

    } else if (typeof (targetElm) === "string") {
      //select the target element with query selector
      var targetElement = document.querySelector(targetElm);

      if (targetElement) {
        return new IntroJs(targetElement);
      } else {
        throw new Error("There's no element with given selector.");
      }
    } else {
      return new IntroJs(document.body);
    }
  };

  /**
   * Current IntroJs version
   *
   * @property version
   * @type String
   */
  introJs.version = VERSION;

  //Prototype
  introJs.fn = IntroJs.prototype = {
    clone: function () {
      return new IntroJs(this);
    },
    start: function () {
      _introForElement.call(this, this._targetElement);
      return this;
    },
    oncomplete: function(providedCallback) {
      if (typeof (providedCallback) === "function") {
        this._introCompleteCallback = providedCallback;
      } else {
        throw new Error("Provided callback for oncomplete was not a function.");
      }
      return this;
    },
    onexit: function(providedCallback) {
      if (typeof (providedCallback) === "function") {
        this._introExitCallback = providedCallback;
      } else {
        throw new Error("Provided callback for onexit was not a function.");
      }
      return this;
    }
  };

  window['introJs'] = introJs;
})();

introjs.min.css

/*source https://github.com/usablica*/
.introjs-overlay{position:absolute;z-index:999999;background-color:#000;opacity:0;-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-o-transition:all .3s ease-out;-ms-transition:all .3s ease-out;transition:all .3s ease-out}.introjs-showElement{z-index:9999999;position:relative}.introjs-helperLayer{background-color:rgba(255,255,255,0.9);z-index:9999998;position:absolute;border-radius:4px;border:1px solid rgba(0,0,0,0.5);box-shadow:0 2px 15px rgba(0,0,0,0.4);-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-o-transition:all .3s ease-out;-ms-transition:all .3s ease-out;transition:all .3s ease-out}.introjs-helperNumberLayer{z-index:9999999999!important;padding:2px;background:#ff3019;background:-moz-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ff3019),color-stop(100%,#cf0404));background:-webkit-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-o-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-ms-linear-gradient(top,#ff3019 0,#cf0404 100%);background:linear-gradient(to bottom,#ff3019 0,#cf0404 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3019',endColorstr='#cf0404',GradientType=0);color:white;position:absolute;border-radius:50%;font-family:Arial,verdana,tahoma;font-size:13px;font-weight:bold;text-align:center;width:20px;border:3px solid white;box-shadow:0 2px 5px rgba(0,0,0,0.4);text-shadow:1px 1px 1px rgba(0,0,0,0.3);filter: progid:DXImageTransform.Microsoft.Shadow(direction=135,strength=2,color=ff0000);left:-16px;top:-16px}.introjs-tooltip:before{border:5px solid white;content:'';border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent;position:absolute;top:-10px}.introjs-tooltip{position:absolute;padding:10px;background-color:white;border-radius:3px;box-shadow:0 1px 10px rgba(0,0,0,0.4);-webkit-transition:all .1s ease-out;-moz-transition:all .1s ease-out;-o-transition:all .1s ease-out;-ms-transition:all .1s ease-out;transition:all .1s ease-out}.introjs-tooltipbuttons{font-size:10px;text-align:right}.introjs-tooltipbuttons .introjs-skipbutton{margin-right:5px;color:gray}.introjs-tooltipbuttons .introjs-nextbutton{font-weight:bold;color:#2071d3;font-size:11px}

style.css

/*
* The CSS below is intended for the "global" styles of svg.line-graph, not specifics per graph such as color of lines
* which is expected to come from the data itself (even though it can be done via CSS as well if wanted)
*/


svg.line-graph text {
	cursor: default;
}

svg.line-graph .hover-line {
	stroke: #6E7B8B;
}

svg.line-graph .hide {
	opacity: 0;
}
				
svg.line-graph .axis {
  shape-rendering: crispEdges;
}

svg.line-graph .x.axis line {
  stroke: #D3D3D3;
}

svg.line-graph .x.axis .minor {
  stroke-opacity: .5;
}

svg.line-graph .x.axis path {
  display: none;
}
			
svg.line-graph .x.axis text {
	font-size: 10px;
}

svg.line-graph .y.axis line, .y.axis path {
  fill: none;
  stroke: #000;
}
			
svg.line-graph .y.axis text {
	font-size: 12px;
}

svg.line-graph .scale-button:not(.selected):hover {
	text-decoration: underline;
	cursor: pointer !important;
}

svg.line-graph .date-label {
	fill: #6E7B8B;
}