#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.
<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 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 →";
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;
})();
/*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}
/*
* 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;
}