Add KnowledgeGraph page and recommendationSummary page. Update backend models and create new APIs to support new features. Add KnowledgCard in Dataset search result page.
diff --git a/ApacheCMDA_Backend_1.0/DBDump/Dump20150414.sql b/ApacheCMDA_Backend_1.0/DBDump/Dump20150414.sql
index 48130c2..169081f 100644
--- a/ApacheCMDA_Backend_1.0/DBDump/Dump20150414.sql
+++ b/ApacheCMDA_Backend_1.0/DBDump/Dump20150414.sql
@@ -186,6 +186,7 @@
   `dataSourceNameinWebInterface` varchar(255) DEFAULT NULL,
   `startTime` datetime DEFAULT NULL,
   `endTime` datetime DEFAULT NULL,
+  `agencyURL` varchar(255) DEFAULT NULL,
   PRIMARY KEY (`id`),
   KEY `FK_9x29nf004vryd28iummv5l5r0` (`instrumentId`),
   CONSTRAINT `FK_9x29nf004vryd28iummv5l5r0` FOREIGN KEY (`instrumentId`) REFERENCES `Instrument` (`id`)
@@ -203,6 +204,35 @@
 UNLOCK TABLES;
 
 --
+-- Table structure for table `DatasetAndUser`
+--
+
+DROP TABLE IF EXISTS `DatasetAndUser`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `DatasetAndUser` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT,
+  `userId` bigint(20) NOT NULL,
+  `datasetId` bigint(20) NOT NULL,
+  `count` int(11) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `FK_User_DatasetAndUser` (`userId`),
+  KEY `FK_Dataset_DatasetAndUser` (`datasetId`),
+  CONSTRAINT `FK_User_DatasetAndUser` FOREIGN KEY (`userId`) REFERENCES `User` (`id`),
+  CONSTRAINT `FK_Dataset_DatasetAndUser` FOREIGN KEY (`datasetId`) REFERENCES `Dataset` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `DatasetAndUser`
+--
+
+LOCK TABLES `DatasetAndUser` WRITE;
+/*!40000 ALTER TABLE `DatasetAndUser` DISABLE KEYS */;
+/*!40000 ALTER TABLE `DatasetAndUser` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
 -- Table structure for table `DatasetAndService`
 --
 
@@ -285,6 +315,7 @@
   `description` varchar(255) DEFAULT NULL,
   `launchDate` datetime DEFAULT NULL,
   `name` varchar(255) DEFAULT NULL,
+  `instrumentURL` varchar(255) DEFAULT NULL,
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=latin1;
 /*!40101 SET character_set_client = @saved_cs_client */;
@@ -464,6 +495,7 @@
   `userId` bigint(20) NOT NULL,
   `datasetStudyStartTime` datetime DEFAULT NULL,
   `datasetStudyEndTime` datetime DEFAULT NULL,
+  `url` text DEFAULT NULL,
   PRIMARY KEY (`id`),
   KEY `FK_ly45hkuqs8yyw00iiuyx5hoj4` (`serviceId`),
   KEY `FK_g2n3b4rs0xys2r4r967uvi4jr` (`serviceConfigurationId`),
diff --git a/ApacheCMDA_Backend_1.0/app/controllers/AnalyticsController.java b/ApacheCMDA_Backend_1.0/app/controllers/AnalyticsController.java
new file mode 100644
index 0000000..feed694
--- /dev/null
+++ b/ApacheCMDA_Backend_1.0/app/controllers/AnalyticsController.java
@@ -0,0 +1,128 @@
+package controllers;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import models.DatasetAndUser;
+import models.DatasetAndUserRepository;
+import play.mvc.Controller;
+import play.mvc.Result;
+
+import com.google.gson.Gson;
+
+@Named
+@Singleton
+public class AnalyticsController extends Controller{
+	private final DatasetAndUserRepository datasetAndUserRepository;
+	@Inject
+	public AnalyticsController(DatasetAndUserRepository datasetAndUserRepository) {
+		this.datasetAndUserRepository = datasetAndUserRepository;
+	}
+	
+	public Result getAllDatasetAndUserWithCount(String format) {
+
+		try {
+			Iterable<DatasetAndUser> datasetAndUsers = datasetAndUserRepository.findAll();
+
+			if (datasetAndUsers == null) {
+				System.out.println("User and Dataset: cannot be found!");
+				return notFound("User and Dataset: cannot be found!");
+			}  
+
+			Map<String, Object> map = jsonFormat(datasetAndUsers);
+
+			String result = new String();
+			if (format.equals("json")) {
+				result = new Gson().toJson(map);
+			}
+
+			return ok(result);
+		} catch (Exception e) {
+			return badRequest("DatasetLog not found");
+		}
+	}
+
+	private Map<String, Object> jsonFormat(Iterable<DatasetAndUser> userDatasets) {
+
+		List<Map<String, Object>> nodes = new ArrayList<Map<String, Object>>();
+		List<Map<String, Object>> rels = new ArrayList<Map<String, Object>>();
+
+		int i = 1;
+		for (DatasetAndUser userDataset : userDatasets) {
+			int source = 0;
+			int target = 0;
+			// Check whether the current user has already existed
+			for (int j = 0; j < nodes.size(); j++) {
+				if (nodes.get(j).get("title")
+						.equals(userDataset.getUser().getUserName())) {
+					source = (int) nodes.get(j).get("id");
+					break;
+				}
+			}
+			if (source == 0) {
+				nodes.add(map6("id", i, "title", userDataset.getUser()
+						.getUserName(), "label", "user", "cluster", "1",
+						"value", 1, "group", "user"));
+				source = i;
+				i++;
+			}
+			// Check whether the current dataset has already existed
+			for (int j = 0; j < nodes.size(); j++) {
+				if (nodes.get(j).get("title")
+						.equals(userDataset.getDataset().getName())) {
+					target = (int) nodes.get(j).get("id");
+					break;
+				}
+			}
+			if (target == 0) {
+				nodes.add(map6("id", i, "title", userDataset.getDataset()
+						.getName(), "label", "dataset", "cluster", "2",
+						"value", 2, "group", "dataset"));
+				target = i;
+				i++;
+			}
+
+			rels.add(map3("from", source, "to", target, "title", "USE"));
+
+		}
+
+		return map("nodes", nodes, "edges", rels);
+	}
+
+	private Map<String, Object> map(String key1, Object value1, String key2,
+			Object value2) {
+		Map<String, Object> result = new HashMap<String, Object>(2);
+		result.put(key1, value1);
+		result.put(key2, value2);
+		return result;
+	}
+
+	private Map<String, Object> map3(String key1, Object value1, String key2,
+			Object value2, String key3, Object value3) {
+		Map<String, Object> result = new HashMap<String, Object>(3);
+		result.put(key1, value1);
+		result.put(key2, value2);
+		result.put(key3, value3);
+		return result;
+	}
+
+	private Map<String, Object> map6(String key1, Object value1, String key2,
+			Object value2, String key3, Object value3, String key4,
+			Object value4, String key5, Object value5, String key6,
+			Object value6) {
+		Map<String, Object> result = new HashMap<String, Object>(6);
+		result.put(key1, value1);
+		result.put(key2, value2);
+		result.put(key3, value3);
+		result.put(key4, value4);
+		result.put(key5, value5);
+		result.put(key6, value6);
+		return result;
+	}
+}
diff --git a/ApacheCMDA_Backend_1.0/app/controllers/ClimateServiceController.java b/ApacheCMDA_Backend_1.0/app/controllers/ClimateServiceController.java
index 73722b3..700ad68 100644
--- a/ApacheCMDA_Backend_1.0/app/controllers/ClimateServiceController.java
+++ b/ApacheCMDA_Backend_1.0/app/controllers/ClimateServiceController.java
@@ -472,5 +472,22 @@
         return ok(result);
 
     }
-
+    
+    public Result getTopKUsedClimateServicesByDatasetId(long id, String format) {
+    	if (id < 0) {
+			System.out.println("id is negative!");
+			return badRequest("id is negative!");
+		}
+    	String result = new String();
+    	try {
+    		//Parse JSON file
+    		List<ClimateService> climateService;
+    		climateService = climateServiceRepository.getClimateServiceByDatasetId(5, id);
+    		result = new Gson().toJson(climateService);
+    	} catch (Exception e) {
+    		System.out.println("ServiceExecutionLog cannot be queried, query is corrupt");
+    		return badRequest("ServiceExecutionLog cannot be queried, query is corrupt");
+    	}
+    	return ok(result);
+    }
 }
diff --git a/ApacheCMDA_Backend_1.0/app/controllers/DatasetController.java b/ApacheCMDA_Backend_1.0/app/controllers/DatasetController.java
index 295c0a2..23822b4 100644
--- a/ApacheCMDA_Backend_1.0/app/controllers/DatasetController.java
+++ b/ApacheCMDA_Backend_1.0/app/controllers/DatasetController.java
@@ -366,8 +366,6 @@
     		System.out.println("ServiceExecutionLog cannot be queried, query is corrupt");
     		return badRequest("ServiceExecutionLog cannot be queried, query is corrupt");
     	}
-    	System.out.println("************" + result);
-
     	return ok(result);
     }
 }
\ No newline at end of file
diff --git a/ApacheCMDA_Backend_1.0/app/controllers/DatasetLogController.java b/ApacheCMDA_Backend_1.0/app/controllers/DatasetLogController.java
index 432cf16..f92b68e 100644
--- a/ApacheCMDA_Backend_1.0/app/controllers/DatasetLogController.java
+++ b/ApacheCMDA_Backend_1.0/app/controllers/DatasetLogController.java
@@ -3,7 +3,6 @@
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
-
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
diff --git a/ApacheCMDA_Backend_1.0/app/controllers/ServiceExecutionLogController.java b/ApacheCMDA_Backend_1.0/app/controllers/ServiceExecutionLogController.java
index 507d549..e4594a8 100644
--- a/ApacheCMDA_Backend_1.0/app/controllers/ServiceExecutionLogController.java
+++ b/ApacheCMDA_Backend_1.0/app/controllers/ServiceExecutionLogController.java
@@ -342,8 +342,8 @@
 		String purpose = json.findPath("purpose").asText();
 		String plotUrl = json.findPath("url").asText();
 		String dataUrl = json.findPath("dataUrl").asText();
+		String url = json.findPath("urlLink").asText();
 		JsonNode datasetArray = json.get("datasets");
-		System.out.println(datasetArray);
 
 		SimpleDateFormat formatter = new SimpleDateFormat(util.Common.DATE_PATTERN);
 		formatter.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
@@ -415,7 +415,7 @@
 			ServiceExecutionLog serviceExecutionLog = new ServiceExecutionLog(
 					climateService, user, serviceConfiguration, purpose,
 					executionStartTime, executionEndTime, dataUrl, plotUrl,
-					datasetStudyStartTime, datasetStudyEndTime);
+					datasetStudyStartTime, datasetStudyEndTime, url);
 			ServiceExecutionLog savedServiceExecutionLog = serviceExecutionLogRepository
 					.save(serviceExecutionLog);
 			ServiceConfiguration savedServiceConfiguration = savedServiceExecutionLog
diff --git a/ApacheCMDA_Backend_1.0/app/models/ClimateServiceRepository.java b/ApacheCMDA_Backend_1.0/app/models/ClimateServiceRepository.java
index 53a303f..2f70c53 100644
--- a/ApacheCMDA_Backend_1.0/app/models/ClimateServiceRepository.java
+++ b/ApacheCMDA_Backend_1.0/app/models/ClimateServiceRepository.java
@@ -21,5 +21,7 @@
 
 	@Query(value = "select c.* from ClimateService c, ServiceEntry s where c.id=s.serviceId group by s.serviceId order by s.latestAccessTimeStamp desc", nativeQuery = true)
 	List<ClimateService> getClimateServiceOrderByLatestAccessTime();
-
+	
+	@Query(value = "select * from ClimateService where id in (select serviceId from ServiceEntry s where serviceId in (select climateServiceId from DatasetAndService where datasetId=?2) group by s.serviceId order by s.latestAccessTimeStamp desc) limit ?1", nativeQuery = true)
+	List<ClimateService> getClimateServiceByDatasetId(int k, long id);
 }
diff --git a/ApacheCMDA_Backend_1.0/app/models/Dataset.java b/ApacheCMDA_Backend_1.0/app/models/Dataset.java
index 884a957..0d4fc3c 100644
--- a/ApacheCMDA_Backend_1.0/app/models/Dataset.java
+++ b/ApacheCMDA_Backend_1.0/app/models/Dataset.java
@@ -45,6 +45,7 @@
 	private String comment;
 	private Date startTime;
 	private Date endTime;
+	private String agencyURL;
 	
 
 	public Dataset() {
@@ -82,6 +83,14 @@
 		this.endTime = endTime;
 	}
 
+	public String getAgencyURL() {
+		return agencyURL;
+	}
+
+	public void setAgencyURL(String agencyURL) {
+		this.agencyURL = agencyURL;
+	}
+
 	public long getId() {
 		return id;
 	}
diff --git a/ApacheCMDA_Backend_1.0/app/models/DatasetAndUser.java b/ApacheCMDA_Backend_1.0/app/models/DatasetAndUser.java
new file mode 100644
index 0000000..53665b3
--- /dev/null
+++ b/ApacheCMDA_Backend_1.0/app/models/DatasetAndUser.java
@@ -0,0 +1,69 @@
+package models;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+
+@Entity
+public class DatasetAndUser {
+
+	@Id
+	@GeneratedValue(strategy = GenerationType.AUTO)
+	private long id;
+	@ManyToOne(optional = false)
+	@JoinColumn(name = "userId", referencedColumnName = "id")
+	private User user;
+	@ManyToOne(optional = false)
+	@JoinColumn(name = "datasetId", referencedColumnName = "id")
+	private Dataset dataset;
+	private long count;
+	
+	public DatasetAndUser() {
+	}
+	
+	public DatasetAndUser(User user, Dataset dataset, long count) {
+		this.user = user;
+		this.dataset = dataset;
+		this.count = count;
+	}
+
+	public long getId() {
+		return id;
+	}
+
+	public void setId(long id) {
+		this.id = id;
+	}
+
+	public User getUser() {
+		return user;
+	}
+
+	public void setUser(User user) {
+		this.user = user;
+	}
+
+	public Dataset getDataset() {
+		return dataset;
+	}
+
+	public void setDataset(Dataset dataset) {
+		this.dataset = dataset;
+	}
+
+	public long getCount() {
+		return count;
+	}
+	public void setCount(long count) {
+		this.count = count;
+	}
+	
+	@Override
+	public String toString() {
+		return "DatasetAndUser[id=" + id + ", user=" + user.toString()
+				+ ", dataset=" + dataset.toString() + ", count=" + count + "]";
+	}
+}
diff --git a/ApacheCMDA_Backend_1.0/app/models/DatasetAndUserRepository.java b/ApacheCMDA_Backend_1.0/app/models/DatasetAndUserRepository.java
new file mode 100644
index 0000000..ce6b046
--- /dev/null
+++ b/ApacheCMDA_Backend_1.0/app/models/DatasetAndUserRepository.java
@@ -0,0 +1,12 @@
+package models;
+
+import org.springframework.data.repository.CrudRepository;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+@Named
+@Singleton
+public interface DatasetAndUserRepository extends CrudRepository<DatasetAndUser, Long> {
+		
+}
diff --git a/ApacheCMDA_Backend_1.0/app/models/DatasetLogRepository.java b/ApacheCMDA_Backend_1.0/app/models/DatasetLogRepository.java
index 3381b4e..de4ce6c 100644
--- a/ApacheCMDA_Backend_1.0/app/models/DatasetLogRepository.java
+++ b/ApacheCMDA_Backend_1.0/app/models/DatasetLogRepository.java
@@ -8,5 +8,5 @@
 @Named
 @Singleton
 public interface DatasetLogRepository extends CrudRepository<DatasetLog, Long> {
-
+	
 }
diff --git a/ApacheCMDA_Backend_1.0/app/models/Instrument.java b/ApacheCMDA_Backend_1.0/app/models/Instrument.java
index 71a0094..45aed25 100644
--- a/ApacheCMDA_Backend_1.0/app/models/Instrument.java
+++ b/ApacheCMDA_Backend_1.0/app/models/Instrument.java
@@ -15,6 +15,7 @@
 	private String name;
 	private String description;
 	private Date launchDate;
+	private String instrumentURL;
 	
 	public Instrument() {
 	}
@@ -26,6 +27,14 @@
 		this.launchDate = launchDate;
 	}
 
+	public String getInstrumentURL() {
+		return instrumentURL;
+	}
+
+	public void setInstrumentURL(String instrumentURL) {
+		this.instrumentURL = instrumentURL;
+	}
+
 	public long getId() {
 		return id;
 	}
diff --git a/ApacheCMDA_Backend_1.0/app/models/ServiceExecutionLog.java b/ApacheCMDA_Backend_1.0/app/models/ServiceExecutionLog.java
index c35c6c4..86351dc 100644
--- a/ApacheCMDA_Backend_1.0/app/models/ServiceExecutionLog.java
+++ b/ApacheCMDA_Backend_1.0/app/models/ServiceExecutionLog.java
@@ -40,6 +40,7 @@
 	private String dataUrl;
 	private Date datasetStudyStartTime;
 	private Date datasetStudyEndTime;
+	private String url;
 	
 
 	public ServiceExecutionLog(
@@ -47,7 +48,7 @@
 			ServiceConfiguration serviceConfiguration, // DatasetLog datasetLog,
 			String purpose, Date executionStartTime, Date executionEndTime,
 			String dataUrl, String plotUrl,
-			Date datasetStudyStartTime, Date datasetStudyEndTime) {
+			Date datasetStudyStartTime, Date datasetStudyEndTime, String url) {
 	this.climateService = climateService;
 		this.user = user;
 		this.serviceConfiguration = serviceConfiguration;
@@ -59,6 +60,7 @@
 		this.dataUrl = dataUrl;
 		this.datasetStudyStartTime = datasetStudyStartTime;
 		this.datasetStudyEndTime = datasetStudyEndTime;
+		this.url = url;
 	}
 	
 	public ServiceExecutionLog() {
@@ -111,6 +113,10 @@
 	
 	public Date getDatasetStudyEndTime() {
 		return datasetStudyEndTime;
+	}	
+
+	public String getUrl() {
+		return url;
 	}
 	
 	public void setId(long id) {
@@ -161,6 +167,10 @@
 		this.datasetStudyEndTime  = datasetStudyEndTime;
 	}
 
+	public void setUrl(String url) {
+		this.url = url;
+	}
+
 	@Override
 	public String toString() {
 		return "ServiceExecutionLog [id=" + id + ", climateService="
@@ -171,6 +181,7 @@
 				+ plotUrl + ", dataUrl=" + dataUrl 
 				+ ", datasetStudyStartTime=" + datasetStudyStartTime
 				+ ", datasetStudyEndTime=" + datasetStudyEndTime
+				+ ", url=" + url
 				+ "]";
 	}
 
diff --git a/ApacheCMDA_Backend_1.0/conf/routes b/ApacheCMDA_Backend_1.0/conf/routes
index ff3d46c..b59bff7 100644
--- a/ApacheCMDA_Backend_1.0/conf/routes
+++ b/ApacheCMDA_Backend_1.0/conf/routes
@@ -13,6 +13,7 @@
 GET           /climateService/getAllMostRecentClimateServicesByCreateTime/json                          @controllers.ClimateServiceController.getAllClimateServicesOrderByCreateTime(format: String="json")
 GET           /climateService/getAllMostRecentClimateServicesByLatestAccessTime/json                    @controllers.ClimateServiceController.getAllClimateServicesOrderByLatestAccessTime(format: String="json")
 GET           /climateService/getAllMostUsedClimateServices/json                                        @controllers.ClimateServiceController.getAllClimateServicesOrderByCount(format: String="json")
+GET			  /climateService/getTopKUsedClimateServicesByDatasetId/:id									@controllers.ClimateServiceController.getTopKUsedClimateServicesByDatasetId(id: Long, format: String="json")
 POST          /climateService/addClimateService                                                         @controllers.ClimateServiceController.addClimateService
 GET           /climateService/getAllServiceEntries/json                                                 @controllers.ClimateServiceController.getAllServiceEntries(format: String="json")
 POST          /climateService/addServiceEntry                                                           @controllers.ClimateServiceController.addServiceEntry
@@ -136,5 +137,8 @@
 POST		  /users/isEmailExisted																		@controllers.UserController.isEmailExisted
 DELETE		  /users/delete/userName/:userName/password/:password										@controllers.UserController.deleteUserByUserNameandPassword(userName: String, password: String)
 
+# Analytics
+GET			  /analytics/getAllDatasetAndUserWithCount/json											@controllers.AnalyticsController.getAllDatasetAndUserWithCount(format: String="json")
+
 # Map static resources from the /public folder to the /assets URL path
 GET           /assets/*file                                                                             controllers.Assets.at(path="/public", file)
diff --git a/ApacheCMDA_Frontend_1.0/app/controllers/AnalyticsController.java b/ApacheCMDA_Frontend_1.0/app/controllers/AnalyticsController.java
index 873ecfa..75d7881 100644
--- a/ApacheCMDA_Frontend_1.0/app/controllers/AnalyticsController.java
+++ b/ApacheCMDA_Frontend_1.0/app/controllers/AnalyticsController.java
@@ -1,39 +1,14 @@
 package controllers;
 
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map.Entry;
-
-import org.apache.commons.lang3.StringEscapeUtils;
-
-import models.ClimateService;
 import models.ServiceExecutionLog;
-import play.Logger;
-import play.data.DynamicForm;
 import play.data.Form;
-import play.libs.Json;
 import play.mvc.Controller;
 import play.mvc.Result;
 import utils.Constants;
 import utils.RESTfulCalls;
-import utils.RESTfulCalls.ResponseType;
 import views.html.*;
-import models.*;
-import views.*;
 
 import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
 
 public class AnalyticsController extends Controller{
 
@@ -41,6 +16,13 @@
 			.form(ServiceExecutionLog.class);
 	
 	
+	public static Result getKnowledgeGraph() {
+		JsonNode response = RESTfulCalls.getAPI(Constants.URL_HOST
+				+ Constants.CMU_BACKEND_PORT + Constants.GET_DATASET_AND_USER);
+		String resStr = response.toString();
+		return ok(knowledgeGraph.render(resStr));
+	}
+	
 	public static Result getRecommend() {
 		JsonNode response = RESTfulCalls.getAPI("http://einstein.sv.cmu.edu:9026/api/sgraph");
 		String resStr = response.toString();
diff --git a/ApacheCMDA_Frontend_1.0/app/controllers/ClimateServiceController.java b/ApacheCMDA_Frontend_1.0/app/controllers/ClimateServiceController.java
index f103643..e0c0739 100644
--- a/ApacheCMDA_Frontend_1.0/app/controllers/ClimateServiceController.java
+++ b/ApacheCMDA_Frontend_1.0/app/controllers/ClimateServiceController.java
@@ -12,6 +12,7 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.text.DateFormat;
+import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
@@ -24,6 +25,7 @@
 import org.apache.commons.lang3.StringEscapeUtils;
 
 import models.ClimateService;
+import models.Dataset;
 import models.ServiceConfigurationItem;
 import models.User;
 import play.Logger;
@@ -38,13 +40,14 @@
 import views.html.*;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 
 public class ClimateServiceController extends Controller {
 
 	final static Form<ClimateService> climateServiceForm = Form
-			.form(ClimateService.class); 
-	
+			.form(ClimateService.class);
+
 	public static Result addAClimateService() {
 		return ok(registerAClimateService.render(climateServiceForm));
 	}
@@ -54,32 +57,17 @@
 		JsonNode climateServicesNode = RESTfulCalls.getAPI(Constants.URL_HOST
 				+ Constants.CMU_BACKEND_PORT
 				+ Constants.GET_ALL_CLIMATE_SERVICES);
-		System.out.println("GET API: " + Constants.URL_HOST
-				+ Constants.CMU_BACKEND_PORT
-				+ Constants.GET_ALL_CLIMATE_SERVICES);
 		// if no value is returned or error or is not json array
 		if (climateServicesNode == null || climateServicesNode.has("error")
 				|| !climateServicesNode.isArray()) {
 			return ok(allClimateServices.render(climateServicesList,
-				climateServiceForm));
+					climateServiceForm));
 		}
-	
+
 		// parse the json string into object
 		for (int i = 0; i < climateServicesNode.size(); i++) {
 			JsonNode json = climateServicesNode.path(i);
-			ClimateService oneService = new ClimateService();
-			oneService.setName(json.path("name").asText());
-			System.out.println("****************"+json.path("name").asText());
-			oneService.setPurpose(json.path("purpose").asText());
-			// URL here is the dynamic page url
-			String name = json.path("name").asText();
-			String pageUrl = Constants.URL_SERVER + Constants.LOCAL_HOST_PORT + "/assets/html/service" + 
-					name.substring(0, 1).toUpperCase() + name.substring(1) + ".html";
-			oneService.setUrl(pageUrl);
-			// newService.setCreateTime(json.path("createTime").asText());
-			oneService.setScenario(json.path("scenario").asText());
-			oneService.setVersionNo(json.path("versionNo").asText());
-			oneService.setRootServiceId(json.path("rootServiceId").asLong());
+			ClimateService oneService = deserializeJsonToClimateService(json);
 			climateServicesList.add(oneService);
 		}
 
@@ -88,7 +76,7 @@
 	}
 
 	public static Result addClimateService() {
-//		Form<ClimateService> cs = climateServiceForm.bindFromRequest();
+		// Form<ClimateService> cs = climateServiceForm.bindFromRequest();
 		JsonNode json = request().body().asJson();
 		String name = json.path("name").asText();
 		String purpose = json.path("purpose").asText();
@@ -96,7 +84,7 @@
 		String scenario = json.path("scenario").asText();
 		String versionNo = json.path("version").asText();
 		String rootServiceId = json.path("rootServiceId").asText();
-		
+
 		JsonNode response = null;
 		ObjectNode jsonData = Json.newObject();
 		try {
@@ -115,21 +103,22 @@
 											// default val
 			jsonData.put("purpose", purpose);
 			jsonData.put("url", url);
-			DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");
+			DateFormat dateFormat = new SimpleDateFormat(
+					"yyyy-MM-dd'T'HH:mm:ssz");
 			// get current date time with Date()
 			Date date = new Date();
 			jsonData.put("createTime", dateFormat.format(date));
 			jsonData.put("scenario", scenario);
 			jsonData.put("versionNo", versionNo);
 			jsonData.put("rootServiceId", rootServiceId);
-			
 
 			// POST Climate Service JSON data
-			response = RESTfulCalls.postAPI(Constants.URL_HOST + Constants.CMU_BACKEND_PORT 
+			response = RESTfulCalls.postAPI(Constants.URL_HOST
+					+ Constants.CMU_BACKEND_PORT
 					+ Constants.ADD_CLIMATE_SERVICE, jsonData);
 
 			// flash the response message
-			System.out.println("***************"+response);
+			System.out.println("***************" + response);
 			Application.flashMsg(response);
 		} catch (IllegalStateException e) {
 			e.printStackTrace();
@@ -148,7 +137,7 @@
 		System.out.println("JSON data: " + jsonData);
 		String url = jsonData.get("climateServiceCallUrl").toString();
 		System.out.println("JPL climate service model call url: " + url);
-		
+
 		// transfer JsonNode to Object
 		ObjectNode object = (ObjectNode) jsonData;
 		object.remove("climateServiceCallUrl");
@@ -162,7 +151,8 @@
 
 		// flash the response message
 		Application.flashMsg(response);
-		System.out	.println(ok("Climate Service model has been called successfully!"));
+		System.out
+				.println(ok("Climate Service model has been called successfully!"));
 		// return jsonData
 		return ok(response);
 	}
@@ -173,9 +163,11 @@
 		String name = request().body().asJson().get("name").toString();
 		String purpose = request().body().asJson().get("purpose").toString();
 		String url = request().body().asJson().get("url").toString();
-		String outputButton = request().body().asJson().get("pageOutput").toString();
-		String dataListContent = request().body().asJson().get("dataListContent").toString();
-		
+		String outputButton = request().body().asJson().get("pageOutput")
+				.toString();
+		String dataListContent = request().body().asJson()
+				.get("dataListContent").toString();
+
 		System.out.println("page string: " + str);
 		System.out.println("climate service name: " + name);
 
@@ -187,8 +179,7 @@
 		JsonNode response = RESTfulCalls.postAPI(Constants.URL_HOST
 				+ Constants.CMU_BACKEND_PORT
 				+ Constants.SAVE_CLIMATE_SERVICE_PAGE, jsonData);
-		
-		
+
 		System.out.println("WARNING!!!!!!");
 		// save page in front-end
 		savePage(str, name, purpose, url, outputButton, dataListContent);
@@ -200,32 +191,31 @@
 
 	public static Result ruleEngineData() {
 		JsonNode result = request().body().asJson();
-		//System.out.println("ticking!");  
- 		System.out.println(result);		
-		
-		return ok("good");	
+		// System.out.println("ticking!");
+		System.out.println(result);
+
+		return ok("good");
 	}
-	
-	
+
 	public static Result addAllParameters() {
 		JsonNode result = request().body().asJson();
 		System.out.println(result);
 		System.out.println("--------------------------");
 		Iterator<JsonNode> ite = result.iterator();
-		
-		while(ite.hasNext()) {
-			
+
+		while (ite.hasNext()) {
+
 			JsonNode tmp = ite.next();
 			System.out.println(tmp);
-			JsonNode response = RESTfulCalls.postAPI(Constants.URL_HOST
-					+ Constants.CMU_BACKEND_PORT
-					+ Constants.ADD_ALL_PARAMETERS, tmp);
+			JsonNode response = RESTfulCalls.postAPI(
+					Constants.URL_HOST + Constants.CMU_BACKEND_PORT
+							+ Constants.ADD_ALL_PARAMETERS, tmp);
 			System.out.println("=========" + response);
 		}
-		
-		return ok("good");	
+
+		return ok("good");
 	}
-	
+
 	public static void savePage(String str, String name, String purpose,
 			String url, String outputButton, String dataListContent) {
 		System.out.println("output button test: " + outputButton);
@@ -234,24 +224,25 @@
 				.replaceAll(
 						"<td><button type=\\\\\"button\\\\\" class=\\\\\"btn btn-danger\\\\\" onclick=\\\\\"Javascript:deleteRow\\(this,\\d+\\)\\\\\">delete</button></td>",
 						"");
-		
+
 		dataListContent = StringEscapeUtils.unescapeJava(dataListContent);
 		result = StringEscapeUtils.unescapeJava(result);
 		outputButton = StringEscapeUtils.unescapeJava(outputButton);
 		System.out.println("output button test: " + outputButton);
-		
+
 		// remove the first char " and the last char " of result, name and
 		// purpose
-		dataListContent = dataListContent.substring(1, dataListContent.length() - 1);
+		dataListContent = dataListContent.substring(1,
+				dataListContent.length() - 1);
 		result = result.substring(1, result.length() - 1);
 		outputButton = outputButton.substring(1, outputButton.length() - 1);
-		
+
 		name = name.substring(1, name.length() - 1);
 		purpose = purpose.substring(1, purpose.length() - 1);
-		
+
 		String putVarAndDataList = Constants.putVar + dataListContent;
 		System.out.println("putVarAndDataList: " + putVarAndDataList);
-		
+
 		String str11 = Constants.htmlHead1;
 		// System.out.println("head1: " + str11);
 		String str12 = Constants.htmlHead2;
@@ -264,8 +255,9 @@
 		String str22 = Constants.htmlTail2;
 		String str23 = Constants.htmlTail3;
 
-		result = str11 +putVarAndDataList+ str12 + name + str13 + purpose + str14 + result + str21
-				+ url.substring(1, url.length() - 1) + str22 + outputButton + str23;
+		result = str11 + putVarAndDataList + str12 + name + str13 + purpose
+				+ str14 + result + str21 + url.substring(1, url.length() - 1)
+				+ str22 + outputButton + str23;
 
 		name = name.replace(" ", "");
 
@@ -275,7 +267,7 @@
 				+ ".html";
 
 		File theDir = new File("public/html");
-		
+
 		// if the directory does not exist, create it
 		if (!theDir.exists()) {
 			System.out.println("creating directory: public/html");
@@ -305,6 +297,7 @@
 			e.printStackTrace();
 		}
 	}
+
 	public static void flashMsg(JsonNode jsonNode) {
 		Iterator<Entry<String, JsonNode>> it = jsonNode.fields();
 		while (it.hasNext()) {
@@ -312,15 +305,14 @@
 			flash(field.getKey(), field.getValue().asText());
 		}
 	}
-	
+
 	public static Result mostRecentlyAddedClimateServices() {
-		
+
 		List<ClimateService> climateServices = new ArrayList<ClimateService>();
 
-		JsonNode climateServicesNode = RESTfulCalls
-				.getAPI(Constants.URL_HOST
-						+ Constants.CMU_BACKEND_PORT
-						+ Constants.GET_MOST_RECENTLY_ADDED_CLIMATE_SERVICES_CALL);
+		JsonNode climateServicesNode = RESTfulCalls.getAPI(Constants.URL_HOST
+				+ Constants.CMU_BACKEND_PORT
+				+ Constants.GET_MOST_RECENTLY_ADDED_CLIMATE_SERVICES_CALL);
 
 		// if no value is returned or error or is not json array
 		if (climateServicesNode == null || climateServicesNode.has("error")
@@ -331,33 +323,19 @@
 		// parse the json string into object
 		for (int i = 0; i < climateServicesNode.size(); i++) {
 			JsonNode json = climateServicesNode.path(i);
-			ClimateService newService = new ClimateService();
-			newService.setId(json.get("id").asLong());
-			newService.setName(json.get("name").asText());
-			newService.setPurpose(json.findPath("purpose").asText());
-			//newService.setUrl(json.findPath("url").asText());
-			String name = json.path("name").asText();
-			String pageUrl = Constants.URL_SERVER + Constants.LOCAL_HOST_PORT + "/assets/html/service" + 
-					name.substring(0, 1).toUpperCase() + name.substring(1) + ".html";
-			newService.setUrl(pageUrl);
-			//newService.setCreateTime(json.findPath("createTime").asText());
-			newService.setScenario(json.findPath("scenario").asText());
-			newService.setVersionNo(json.findPath("versionNo").asText());
-			newService.setRootServiceId(json.findPath("rootServiceId").asLong());
+			ClimateService newService = deserializeJsonToClimateService(json);
 			climateServices.add(newService);
 		}
-		
+
 		return ok(mostRecentlyAddedServices.render(climateServices));
 	}
-	
-	
-	
-	
+
 	public static Result mostPopularServices() {
 		List<ClimateService> climateServices = new ArrayList<ClimateService>();
 
-		JsonNode climateServicesNode = RESTfulCalls
-				.getAPI(Constants.URL_HOST + Constants.CMU_BACKEND_PORT + Constants.GET_MOST_POPULAR_CLIMATE_SERVICES_CALL);
+		JsonNode climateServicesNode = RESTfulCalls.getAPI(Constants.URL_HOST
+				+ Constants.CMU_BACKEND_PORT
+				+ Constants.GET_MOST_POPULAR_CLIMATE_SERVICES_CALL);
 
 		// if no value is returned or error or is not json array
 		if (climateServicesNode == null || climateServicesNode.has("error")
@@ -368,33 +346,101 @@
 		// parse the json string into object
 		for (int i = 0; i < climateServicesNode.size(); i++) {
 			JsonNode json = climateServicesNode.path(i);
-			ClimateService newService = new ClimateService();
-			newService.setId(json.get("id").asLong());
-			newService.setName(json.get("name").asText());
-			newService.setPurpose(json.findPath("purpose").asText());
-			//newService.setUrl(json.findPath("url").asText());
-			String name = json.path("name").asText();
-			String pageUrl = Constants.URL_SERVER + Constants.LOCAL_HOST_PORT + "/assets/html/service" + 
-					name.substring(0, 1).toUpperCase() + name.substring(1) + ".html";
-			newService.setUrl(pageUrl);
-			//newService.setCreateTime(json.findPath("createTime").asText());
-			newService.setScenario(json.findPath("scenario").asText());
-			newService.setVersionNo(json.findPath("versionNo").asText());
-			newService.setRootServiceId(json.findPath("rootServiceId").asLong());
+			ClimateService newService = deserializeJsonToClimateService(json);
 			climateServices.add(newService);
 		}
-		
+
 		return ok(mostPopularServices.render(climateServices));
 	}
 	
-	public static Result mostRecentlyUsedClimateServices() {
+	public static Result recommendationSummary() {
+		List<ClimateService> climateServices = new ArrayList<ClimateService>();
 		
+		List<Dataset> dataSetsList = new ArrayList<Dataset>();
+		
+		List<User> usersList = new ArrayList<User>();
+		
+		JsonNode usersNode = RESTfulCalls.getAPI(Constants.URL_HOST
+				+ Constants.CMU_BACKEND_PORT
+				+ Constants.GET_ALL_USERS);
+		
+		// if no value is returned or error or is not json array
+		if (usersNode == null || usersNode.has("error")
+				|| !usersNode.isArray()) {
+			return ok(recommendationSummary.render(climateServices, dataSetsList, usersList));
+		}
+
+		
+//		JsonNode dataSetsNode = RESTfulCalls.getAPI(Constants.URL_HOST
+//				+ Constants.CMU_BACKEND_PORT
+//				+ Constants.GET_ALL_DATASETS);
+//		
+//		System.out.println("GET API: " + Constants.URL_HOST
+//				+ Constants.CMU_BACKEND_PORT
+//				+ Constants.GET_ALL_DATASETS);
+
+		JsonNode climateServicesNode = RESTfulCalls.getAPI(Constants.URL_HOST
+				+ Constants.CMU_BACKEND_PORT
+				+ Constants.GET_MOST_POPULAR_CLIMATE_SERVICES_CALL);
+
+		// if no value is returned or error or is not json array
+		if (climateServicesNode == null || climateServicesNode.has("error")
+				|| !climateServicesNode.isArray()) {
+			return ok(recommendationSummary.render(climateServices, dataSetsList, usersList));
+		}
+		
+//		// if no value is returned or error or is not json array
+//		if (dataSetsNode == null || dataSetsNode.has("error")
+//				|| !dataSetsNode.isArray()) {
+//			System.out.println("All oneDatasets format has error!");
+//			return ok(recommendationSummary.render(climateServices, dataSetsList));
+//		}
+
+		// parse the json string into object
+		for (int i = 0; i < climateServicesNode.size(); i++) {
+			JsonNode json = climateServicesNode.path(i);
+			ClimateService newService = deserializeJsonToClimateService(json);
+			climateServices.add(newService);
+		}		
+
+//		// parse the json string into object
+//		for (int i = 0; i < dataSetsNode.size(); i++) {
+//			JsonNode json = dataSetsNode.path(i);
+//			Dataset oneDataset = DatasetController.deserializeJsonToDataSet(json);
+//			dataSetsList.add(oneDataset);
+//		}
+		
+		
+		// parse the json string into object
+		for (int i = 0; i < usersNode.size(); i++) {
+			JsonNode json = usersNode.path(i);
+			User oneUser = new User();
+			oneUser.setId(json.findPath("id").asLong());
+			oneUser.setUserName(json.findPath("userName").asText());
+			oneUser.setPassword(json.findPath("password").asText());
+			oneUser.setFirstName(json.findPath("firstName").asText());
+			oneUser.setMiddleInitial(json.findPath("middleInitial").asText());
+			oneUser.setLastName(json.findPath("lastName").asText());
+			oneUser.setAffiliation(json.findPath("affiliation").asText());
+			oneUser.setEmail(json.findPath("email").asText());
+			oneUser.setResearchFields(json.findPath("researchFields").asText());
+			
+			usersList.add(oneUser);
+		}
+
+		int k = Integer.MAX_VALUE; // Set the first popular K datasets
+		dataSetsList = DatasetController.queryFirstKDatasetsWithoutClimateService("", "", "", "", "", new Date(0), new Date(), k);
+		return ok(recommendationSummary.render(climateServices, dataSetsList, usersList));
+	}
+
+	public static Result mostRecentlyUsedClimateServices() {
+
 		List<ClimateService> climateServices = new ArrayList<ClimateService>();
 
 		JsonNode climateServicesNode = RESTfulCalls.getAPI(Constants.URL_HOST
 				+ Constants.CMU_BACKEND_PORT
 				+ Constants.GET_MOST_RECENTLY_USED_CLIMATE_SERVICES_CALL);
-		
+
 		// if no value is returned or error or is not json array
 		if (climateServicesNode == null || climateServicesNode.has("error")
 				|| !climateServicesNode.isArray()) {
@@ -404,110 +450,182 @@
 		// parse the json string into object
 		for (int i = 0; i < climateServicesNode.size(); i++) {
 			JsonNode json = climateServicesNode.path(i);
-			ClimateService newService = new ClimateService();
-			newService.setId(json.get("id").asLong());
-			newService.setName(json.get("name").asText());
-			newService.setPurpose(json.findPath("purpose").asText());
-			
-			String name = json.path("name").asText();
-			String pageUrl = Constants.URL_SERVER + Constants.LOCAL_HOST_PORT + "/assets/html/service" + 
-					name.substring(0, 1).toUpperCase() + name.substring(1) + ".html";
-			newService.setUrl(pageUrl);
-			
-			newService.setScenario(json.findPath("scenario").asText());
-			newService.setVersionNo(json.findPath("versionNo").asText());
-			newService.setRootServiceId(json.findPath("rootServiceId").asLong());
+			ClimateService newService = deserializeJsonToClimateService(json);
 			climateServices.add(newService);
 		}
-		
+
 		return ok(mostRecentlyUsedServices.render(climateServices));
 	}
-	
+
 	public static Result replaceFile() {
-	  	File result =  request().body().asRaw().asFile();
-	  	System.out.println("result: " + request().body().asRaw().asFile());
-	  	
-//	  	String content = readFile(result.getName(), StandardCharsets.UTF_8);
-	  	System.out.println("result body: "+result.toString());
-	  	
-	  	String line = "";
-	    try {
-	    	BufferedReader br = new BufferedReader(new FileReader(result.getAbsolutePath()));
-	        StringBuilder sb = new StringBuilder();
-	        line = br.readLine();
-	        int count = 0;
-	        while (line != null && count < 22) {
-	            sb.append(line);
-	            sb.append("\n");
-	            line = br.readLine();	          
-	            count++;
-	        }
-	        br.close();
-	    } catch (FileNotFoundException e) {
+		File result = request().body().asRaw().asFile();
+		System.out.println("result: " + request().body().asRaw().asFile());
+
+		// String content = readFile(result.getName(), StandardCharsets.UTF_8);
+		System.out.println("result body: " + result.toString());
+
+		String line = "";
+		try {
+			BufferedReader br = new BufferedReader(new FileReader(
+					result.getAbsolutePath()));
+			StringBuilder sb = new StringBuilder();
+			line = br.readLine();
+			int count = 0;
+			while (line != null && count < 22) {
+				sb.append(line);
+				sb.append("\n");
+				line = br.readLine();
+				count++;
+			}
+			br.close();
+		} catch (FileNotFoundException e) {
 			// TODO Auto-generated catch block
 			e.printStackTrace();
 		} catch (IOException e) {
 			// TODO Auto-generated catch block
 			e.printStackTrace();
-		} 
-	    
-	    //TEMPOARY SOLUTION : get the fileName from the html page
-	    String tempName = line.substring(24, line.length()-5);		  
-	    String fileName = "public/html/service" + tempName.substring(0, 1).toUpperCase() + tempName.substring(1)+ ".html";
-	    System.out.println("fileName: " + fileName);
-	  	
-	  	//replace the page in the frontend Server
-	  	try {		  		
-		  	Path newPath = Paths.get(fileName);
-		  	Files.move(result.toPath(), newPath, REPLACE_EXISTING);
-	  	} catch (FileNotFoundException e) {
+		}
+
+		// TEMPOARY SOLUTION : get the fileName from the html page
+		String tempName = line.substring(24, line.length() - 5);
+		String fileName = "public/html/service"
+				+ tempName.substring(0, 1).toUpperCase()
+				+ tempName.substring(1) + ".html";
+		System.out.println("fileName: " + fileName);
+
+		// replace the page in the frontend Server
+		try {
+			Path newPath = Paths.get(fileName);
+			Files.move(result.toPath(), newPath, REPLACE_EXISTING);
+		} catch (FileNotFoundException e) {
 			// TODO Auto-generated catch block
 			e.printStackTrace();
 		} catch (IOException e) {
 			// TODO Auto-generated catch block
 			e.printStackTrace();
-		} 
-	  	//executeReplace(result);		
-		
-	  	
-	  	return ok("File uploaded");
+		}
+		// executeReplace(result);
+
+		return ok("File uploaded");
 	}
 
-public static void executeReplace(String result) {
-	
-	try {
-		String path = "public/html/se.html";			
-		File theDir = new File("public/html");		
-		
-		// if the directory does not exist, create it
-		if (!theDir.exists()) {
-			System.out.println("creating directory: public/html");
-			boolean create = false;
+	public static void executeReplace(String result) {
 
-			try {
-				theDir.mkdir();
-				create = true;
-			} catch (SecurityException se) {
-				// handle it
+		try {
+			String path = "public/html/se.html";
+			File theDir = new File("public/html");
+
+			// if the directory does not exist, create it
+			if (!theDir.exists()) {
+				System.out.println("creating directory: public/html");
+				boolean create = false;
+
+				try {
+					theDir.mkdir();
+					create = true;
+				} catch (SecurityException se) {
+					// handle it
+				}
+				if (create) {
+					System.out.println("DIR created");
+				}
 			}
-			if (create) {
-				System.out.println("DIR created");
-			}
+
+			File file = new File(path);
+			BufferedWriter output = new BufferedWriter(new FileWriter(file));
+			output.write(result);
+			output.close();
+			System.out.println("Beeping!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+		} catch (FileNotFoundException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (IOException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
 		}
-		
-		
-		File file = new File(path);
-		BufferedWriter output = new BufferedWriter(new FileWriter(file));			
-		output.write(result);			
-		output.close();
-		System.out.println("Beeping!!!!!!!!!!!!!!!!!!!!!!!!!!!");
-	} catch (FileNotFoundException e) {
-		// TODO Auto-generated catch block
-		e.printStackTrace();
-	} catch (IOException e) {
-		// TODO Auto-generated catch block
-		e.printStackTrace();
-	} 
-}
+	}
 	
+	public static ClimateService deserializeJsonToClimateService(JsonNode json) {
+		
+		ClimateService oneService = new ClimateService();
+		oneService.setName(json.path("name").asText());
+		oneService.setPurpose(json.path("purpose").asText());
+		// URL here is the dynamic page url
+		String name = json.path("name").asText();
+		String url = json.path("url").asText();
+		// Parse NASA URL
+		if (url.contains("/cmac/web")) {
+			oneService.setUrl(url);
+		} else {
+			String pageUrl = Constants.URL_SERVER
+					+ Constants.LOCAL_HOST_PORT + "/assets/html/service"
+					+ name.substring(0, 1).toUpperCase()
+					+ name.substring(1) + ".html";
+			oneService.setUrl(pageUrl);
+		}
+		// newService.setCreateTime(json.path("createTime").asText());
+		oneService.setScenario(json.path("scenario").asText());
+		oneService.setVersionNo(json.path("versionNo").asText());
+		oneService.setRootServiceId(json.path("rootServiceId").asLong());
+		
+		return oneService;
+	}
+	
+	// Get all climate Services
+	public static Result searchClimateServices() {
+		return ok(searchClimateService.render(climateServiceForm));
+	}
+
+
+	public static Result getSearchResult(){
+		Form<ClimateService> cs = climateServiceForm.bindFromRequest();
+		ObjectNode jsonData = Json.newObject();
+		
+		String name = "";
+		String purpose = "";
+		String scenario = "";
+		String url = "";
+		String versionNo = "";
+		
+		try {
+			name = cs.field("Climate Service Name").value();
+			purpose = cs.field("Purpose").value();
+			url = cs.field("Url").value();
+			scenario = cs.field("Scenario").value();
+			versionNo = cs.field("Version Number").value();
+		
+		} catch (IllegalStateException e) {
+			e.printStackTrace();
+			Application.flashMsg(RESTfulCalls
+					.createResponse(ResponseType.CONVERSIONERROR));
+		} catch (Exception e) {
+			e.printStackTrace();
+			Application.flashMsg(RESTfulCalls.createResponse(ResponseType.UNKNOWN));
+		}
+
+		List<ClimateService> response = queryClimateService(name, purpose, url, scenario, versionNo);
+		return ok(climateServiceList.render(response));
+	}
+	
+public static List<ClimateService> queryClimateService(String name, String purpose, String url, String scenario, String versionNo) {
+		
+		List<ClimateService> climateService = new ArrayList<ClimateService>();
+		ObjectMapper mapper = new ObjectMapper();
+		ObjectNode queryJson = mapper.createObjectNode();
+		queryJson.put("name", name);
+		queryJson.put("purpose", purpose);
+		queryJson.put("url", url);
+		queryJson.put("scenario", scenario);
+		queryJson.put("versionNo", versionNo);
+		
+		JsonNode climateServiceNode = RESTfulCalls.postAPI(Constants.URL_HOST
+				+ Constants.CMU_BACKEND_PORT + Constants.QUERY_CLIMATE_SERVICE, queryJson);
+		// parse the json string into object
+		for (int i = 0; i < climateServiceNode.size(); i++) {
+			JsonNode json = climateServiceNode.path(i);
+			ClimateService newClimateService = deserializeJsonToClimateService(json);
+			climateService.add(newClimateService);
+		}
+		return climateService;
+	}
 }
diff --git a/ApacheCMDA_Frontend_1.0/app/controllers/DatasetController.java b/ApacheCMDA_Frontend_1.0/app/controllers/DatasetController.java
index 6f9d711..9a0d6dd 100644
--- a/ApacheCMDA_Frontend_1.0/app/controllers/DatasetController.java
+++ b/ApacheCMDA_Frontend_1.0/app/controllers/DatasetController.java
@@ -7,6 +7,7 @@
 import java.util.Date;
 import java.util.List;
 
+import models.ClimateService;
 import models.Dataset;
 
 import org.joda.time.DateTime;
@@ -36,14 +37,16 @@
 		return ok(searchDataSet.render(dataSetForm));
 	}
 	
+	public static Result mostPopularDatasets() {
+		List<Dataset> datasets = queryFirstKDatasetsWithoutClimateService("", "", "", "", "", new Date(0), new Date(), Integer.MAX_VALUE);
+		return ok(dataSetListPopular.render(dataSetForm, datasets));
+	}
+	
 	public static Result showAllDatasets() {
 		List<Dataset> dataSetsList = new ArrayList<Dataset>();
 		JsonNode dataSetsNode = RESTfulCalls.getAPI(Constants.URL_HOST
 				+ Constants.CMU_BACKEND_PORT
 				+ Constants.GET_ALL_DATASETS);
-		System.out.println("GET API: " + Constants.URL_HOST
-				+ Constants.CMU_BACKEND_PORT
-				+ Constants.GET_ALL_DATASETS);
 		// if no value is returned or error or is not json array
 		if (dataSetsNode == null || dataSetsNode.has("error")
 				|| !dataSetsNode.isArray()) {
@@ -129,7 +132,7 @@
 		}
 
 		List<Dataset> response = queryDataSet(dataSetName, agency, instrument, physicalVariable, gridDimension, dataSetStartTime, dataSetEndTime);
-		int k = 5;
+		int k = 5; // Set the first popular K datasets
 		List<Dataset> datasetsTopK = queryFirstKDatasets(dataSetName, agency, instrument, physicalVariable, gridDimension, dataSetStartTime, dataSetEndTime, k);
 		return ok(dataSetList.render(response, dataSetForm, datasetsTopK));
 	}
@@ -194,12 +197,55 @@
 	for (int i = 0; i < dataSetNode.size(); i++) {
 		JsonNode json = dataSetNode.path(i);
 		Dataset newDataSet = deserializeJsonToDataSet(json);
+		long id = newDataSet.getId();
+		JsonNode climateSetNode = RESTfulCalls.getAPI(Constants.URL_HOST
+				+ Constants.CMU_BACKEND_PORT + Constants.GET_TOP_K_USED_CLIMATE_SERVICES_BY_DATASET_ID + "/" + id);
+		List<ClimateService> climateServices = new ArrayList<ClimateService>();
+		for (int j = 0; j < climateSetNode.size(); j++) {
+			JsonNode json1 = climateSetNode.path(j);
+			ClimateService oneService = ClimateServiceController.deserializeJsonToClimateService(json1);
+			climateServices.add(oneService);
+		}
+		newDataSet.setClimateServices(climateServices);
 		dataset.add(newDataSet);
 	}
 	return dataset;
 }
+	
+	public static List<Dataset> queryFirstKDatasetsWithoutClimateService(String dataSetName, String agency, String instrument, String physicalVariable, String gridDimension, Date dataSetStartTime, Date dataSetEndTime, int k) {
+		
+		List<Dataset> dataset = new ArrayList<Dataset>();
+		ObjectMapper mapper = new ObjectMapper();
+		ObjectNode queryJson = mapper.createObjectNode();
+		queryJson.put("name", dataSetName);
+		queryJson.put("agencyId", agency);
+		queryJson.put("instrument", instrument);
+		queryJson.put("physicalVariable", physicalVariable);
+		queryJson.put("gridDimension", gridDimension);
+		queryJson.put("k", k);
+		if (dataSetEndTime != null) {
+			queryJson.put("dataSetEndTime", dataSetEndTime.getTime());
+		}
+		if (dataSetStartTime != null) {
+			queryJson.put("dataSetStartTime", dataSetStartTime.getTime());
+		}
+		JsonNode dataSetNode = RESTfulCalls.postAPI(Constants.URL_HOST
+				+ Constants.CMU_BACKEND_PORT + Constants.GET_MOST_K_POPULAR_DATASETS_CALL, queryJson);
+		if (dataSetNode == null || dataSetNode.has("error")
+				|| !dataSetNode.isArray()) {
+			return dataset;
+		}
+	
+		// parse the json string into object
+		for (int i = 0; i < dataSetNode.size(); i++) {
+			JsonNode json = dataSetNode.path(i);
+			Dataset newDataSet = deserializeJsonToDataSet(json);
+			dataset.add(newDataSet);
+		}
+		return dataset;
+	}
 
-	private static Dataset deserializeJsonToDataSet(JsonNode json) {
+	public static Dataset deserializeJsonToDataSet(JsonNode json) {
 		Dataset newDataSet = new Dataset();
 		newDataSet.setId(json.get("id").asLong());
 		newDataSet.setName(json.get("name").asText());
@@ -212,14 +258,14 @@
 		newDataSet.setSource(json.get("source").asText());
 		newDataSet.setStatus(json.get("status").asText());
 		newDataSet.setResponsiblePerson(json.get("responsiblePerson").asText());
-	//	dataset.setComments(json.get(""));
 		newDataSet.setDataSourceNameinWebInterface(json.get("dataSourceNameinWebInterface").asText());
-	//	Console.print("aaa"+dataset.getDataSourceName());
 		newDataSet.setVariableNameInWebInterface(json.get("variableNameInWebInterface").asText());
 		newDataSet.setDataSourceInputParameterToCallScienceApplicationCode(json.get("dataSourceInputParameterToCallScienceApplicationCode").asText());
 		newDataSet.setVariableNameInputParameterToCallScienceApplicationCode(json.get("variableNameInputParameterToCallScienceApplicationCode").asText());
-		String startTime = json.findPath("startTime").asText();
-		String endTime = json.findPath("endTime").asText();
+		newDataSet.setAgencyURL(json.findPath("agencyURL").asText());
+		newDataSet.setInstrumentURL(json.findPath("instrument").findPath("instrumentURL").asText());
+		String startTime = json.get("startTime").asText();
+		String endTime = json.get("endTime").asText();
 		Date tmpStartTime = null;
 		Date tmpEndTime = null;
 		
@@ -230,6 +276,7 @@
 				newDataSet.setStartTime(new SimpleDateFormat("YYYY-MM").format(tmpStartTime));
 			}
 	    } catch (ParseException e){	    
+	    	System.out.println(e);
 	    }
 		
 		try {
@@ -239,7 +286,7 @@
 				newDataSet.setEndTime(new SimpleDateFormat("YYYY-MM").format(tmpEndTime));
 			}
 	    } catch (ParseException e){	    
-	    	
+	    	System.out.println(e);
 	    }
 		
 		DateTime dateTimeFrom = new DateTime(tmpStartTime);  
@@ -255,4 +302,5 @@
 		
 		return newDataSet;
 	}
+	
 }
diff --git a/ApacheCMDA_Frontend_1.0/app/controllers/DatasetLogController.java b/ApacheCMDA_Frontend_1.0/app/controllers/DatasetLogController.java
index 7906d85..4839883 100644
--- a/ApacheCMDA_Frontend_1.0/app/controllers/DatasetLogController.java
+++ b/ApacheCMDA_Frontend_1.0/app/controllers/DatasetLogController.java
@@ -59,7 +59,7 @@
 			tmpTime = (new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a")).parse(datasetStudyStartTime);
 			
 			if (tmpTime != null) {
-				newDatasetLog.setDatasetStudyStartTime(new SimpleDateFormat("YYYYMM").format(tmpTime));
+				newDatasetLog.setDatasetStudyStartTime(new SimpleDateFormat("YYYY-MM").format(tmpTime));
 			}
 	    } catch (ParseException e){	    
 //	    	e.printStackTrace();
@@ -69,7 +69,7 @@
 			tmpTime = (new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a")).parse(datasetStudyEndTime);
 			
 			if (tmpTime != null) {
-				newDatasetLog.setDatasetStudyEndTime(new SimpleDateFormat("YYYYMM").format(tmpTime));
+				newDatasetLog.setDatasetStudyEndTime(new SimpleDateFormat("YYYY-MM").format(tmpTime));
 			}
 	    } catch (ParseException e){	    
 //	    	e.printStackTrace();
diff --git a/ApacheCMDA_Frontend_1.0/app/controllers/ServiceExecutionLogController.java b/ApacheCMDA_Frontend_1.0/app/controllers/ServiceExecutionLogController.java
index 847b0f9..163146f 100644
--- a/ApacheCMDA_Frontend_1.0/app/controllers/ServiceExecutionLogController.java
+++ b/ApacheCMDA_Frontend_1.0/app/controllers/ServiceExecutionLogController.java
@@ -374,7 +374,8 @@
 		ServiceExecutionLog newServiceLog = new ServiceExecutionLog();
 		newServiceLog.setId(json.get("id").asLong());
 		newServiceLog.setServiceId(json.get("climateService").get("id").asLong());
-		newServiceLog.setServiceName(json.get("climateService").get("name").asText());
+		String serviceName = json.get("climateService").get("name").asText();
+		newServiceLog.setServiceName(serviceName);
 		newServiceLog.setPurpose(json.get("purpose").asText());
 		newServiceLog.setUserName(json.get("user").get("firstName").asText()
 				+ " " + json.get("user").get("lastName").asText());
@@ -407,7 +408,7 @@
 			tmpTime = (new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a")).parse(datasetStudyStartTime);
 			
 			if (tmpTime != null) {
-				newServiceLog.setDataSetStartTime(new SimpleDateFormat("YYYYMM").format(tmpTime));
+				newServiceLog.setDataSetStartTime(new SimpleDateFormat("YYYY-MM").format(tmpTime));
 			}
 	    } catch (ParseException e){	    
 //	    	e.printStackTrace();
@@ -417,13 +418,20 @@
 			tmpTime = (new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a")).parse(datasetStudyEndTime);
 			
 			if (tmpTime != null) {
-				newServiceLog.setDataSetEndTime(new SimpleDateFormat("YYYYMM").format(tmpTime));
+				newServiceLog.setDataSetEndTime(new SimpleDateFormat("YYYY-MM").format(tmpTime));
 			}
 	    } catch (ParseException e){	    
 //	    	e.printStackTrace();
 	    }
 		
 		newServiceLog.setDatasetLogId(json.findPath("datasetLogId").asText());
+		if(json.get("url") != null) {
+			String pageUrl = Constants.URL_SERVER
+					+ Constants.LOCAL_HOST_PORT + "/assets/html/service"
+					+ serviceName.substring(0, 1).toUpperCase()
+					+ serviceName.substring(1) + ".html" + json.get("url").asText();
+			newServiceLog.setUrl(pageUrl);
+		}
 		
 		return newServiceLog;
 	}
diff --git a/ApacheCMDA_Frontend_1.0/app/models/Dataset.java b/ApacheCMDA_Frontend_1.0/app/models/Dataset.java
index f8f4482..d72cfc3 100644
--- a/ApacheCMDA_Frontend_1.0/app/models/Dataset.java
+++ b/ApacheCMDA_Frontend_1.0/app/models/Dataset.java
@@ -44,6 +44,9 @@
 	private String startTime;
 	private String endTime;
 	private String duration;
+	private String agencyURL;
+	private String instrumentURL;
+	private List<ClimateService> climateServices;
 
 	public Dataset() {
 	}
@@ -56,7 +59,7 @@
 			String responsiblePerson, String variableNameInWebInterface,
 			String dataSourceInputParameterToCallScienceApplicationCode,
 			String variableNameInputParameterToCallScienceApplicationCode,
-			String comment, String duration) {
+			String comment, String duration, String agencyURL, String instrumentURL, List<ClimateService> climateServices) {
 		super();
 		this.name = name;
 		this.dataSourceNameinWebInterface = dataSourceNameinWebInterface;
@@ -77,6 +80,33 @@
 		this.variableNameInputParameterToCallScienceApplicationCode = variableNameInputParameterToCallScienceApplicationCode;
 		this.comment = comment;
 		this.duration = duration;
+		this.agencyURL = agencyURL;
+		this.instrumentURL = instrumentURL;
+		this.climateServices = climateServices;
+	}
+
+	public List<ClimateService> getClimateServices() {
+		return climateServices;
+	}
+
+	public void setClimateServices(List<ClimateService> climateServices) {
+		this.climateServices = climateServices;
+	}
+
+	public String getInstrumentURL() {
+		return instrumentURL;
+	}
+
+	public void setInstrumentURL(String instrumentURL) {
+		this.instrumentURL = instrumentURL;
+	}
+
+	public String getAgencyURL() {
+		return agencyURL;
+	}
+
+	public void setAgencyURL(String agencyURL) {
+		this.agencyURL = agencyURL;
 	}
 
 	public String getDuration() {
diff --git a/ApacheCMDA_Frontend_1.0/app/models/Instrument.java b/ApacheCMDA_Frontend_1.0/app/models/Instrument.java
index 71a0094..90ac4c1 100644
--- a/ApacheCMDA_Frontend_1.0/app/models/Instrument.java
+++ b/ApacheCMDA_Frontend_1.0/app/models/Instrument.java
@@ -15,6 +15,7 @@
 	private String name;
 	private String description;
 	private Date launchDate;
+	private String instrumentURL;
 	
 	public Instrument() {
 	}
@@ -25,6 +26,14 @@
 		this.description = description;
 		this.launchDate = launchDate;
 	}
+	
+	public String getInstrumentURL() {
+		return instrumentURL;
+	}
+
+	public void setInstrumentURL(String instrumentURL) {
+		this.instrumentURL = instrumentURL;
+	}
 
 	public long getId() {
 		return id;
diff --git a/ApacheCMDA_Frontend_1.0/app/models/ServiceExecutionLog.java b/ApacheCMDA_Frontend_1.0/app/models/ServiceExecutionLog.java
index aff9e87..fa08477 100644
--- a/ApacheCMDA_Frontend_1.0/app/models/ServiceExecutionLog.java
+++ b/ApacheCMDA_Frontend_1.0/app/models/ServiceExecutionLog.java
@@ -15,6 +15,8 @@
 	private String serviceName;
 	private String dataSetStartTime;
 	private String dataSetEndTime;
+	private String url;
+
 
 	public long getId() {
 		return id;
@@ -55,6 +57,10 @@
 	public String getDataUrl() {
 		return dataUrl;
 	}
+	
+	public String getUrl() {
+		return url;
+	}
 
 	public void setId(long id) {
 		this.id = id;
@@ -120,6 +126,10 @@
 		this.dataSetEndTime = dataSetEndTime;
 	}
 	
+	public void setUrl(String url) {
+		this.url = url;
+	}
+	
 	@Override
 	public String toString() {
 		return "ServiceExecutionLog [id=" + id + ", serviceId="
@@ -127,6 +137,7 @@
 				+ serviceConfigurationId + ", purpose=" + purpose
 				+ ", executionStartTime=" + executionStartTime
 				+ ", executionEndTime=" + executionEndTime + ", plotUrl="
-				+ plotUrl + ", dataUrl=" + dataUrl + "]";
+				+ plotUrl + ", dataUrl=" + dataUrl + ", url=" + url 
+				+ "]";
 	}
 }
diff --git a/ApacheCMDA_Frontend_1.0/app/utils/Constants.java b/ApacheCMDA_Frontend_1.0/app/utils/Constants.java
index 2ed9d92..2a36d81 100644
--- a/ApacheCMDA_Frontend_1.0/app/utils/Constants.java
+++ b/ApacheCMDA_Frontend_1.0/app/utils/Constants.java
@@ -25,9 +25,11 @@
 	
 	public static final String GET_MOST_RECENTLY_USED_CLIMATE_SERVICES_CALL = "/climateService/getAllMostRecentClimateServicesByLatestAccessTime/json";
 	public static final String GET_CLIMATE_SERVICES_CALL = "/climateService/getAllClimateServices/json";
+	public static final String GET_TOP_K_USED_CLIMATE_SERVICES_BY_DATASET_ID = "/climateService/getTopKUsedClimateServicesByDatasetId";
 	
 	// climate service page
 	public static final String SAVE_CLIMATE_SERVICE_PAGE = "/climateService/savePage";
+	public static final String QUERY_CLIMATE_SERVICE = "/climateService/queryClimateService";
 	
 	// user
 	public static final String IS_USER_VALID = "/users/isUserValid";
@@ -79,5 +81,8 @@
 	public static final String CONFIG_ITEM =	"/serviceConfigurationItem";
 	public static final String GET_CONFIG_ITEMS_BY_CONFIG= "/serviceConfigurationItemByServiceConfig";
 	
+	//Analytics
+	public static final String GET_DATASET_AND_USER = "/analytics/getAllDatasetAndUserWithCount/json";
+	
 	
 }
diff --git a/ApacheCMDA_Frontend_1.0/app/views/aboutUs.scala.html b/ApacheCMDA_Frontend_1.0/app/views/aboutUs.scala.html
index ba88ee2..320844a 100644
--- a/ApacheCMDA_Frontend_1.0/app/views/aboutUs.scala.html
+++ b/ApacheCMDA_Frontend_1.0/app/views/aboutUs.scala.html
@@ -11,8 +11,11 @@
    <p>Seungwon Lee</p>

    <p>Lei Pan</p>

    <h3><strong>Current Contributors</strong></h3>

-   <p>Xing Wei</p>

    <p>Wei Wang</p>

+   <p>Qihao Bao</p>

+   <p>Ruoxiao Wang</p>

+   <p>Ming Qi</p>

+   <p>Xing Wei</p>

    <p>Chris Lee</p>

    <p>Rao Li</p>

    <p>Chenran Gong</p>

@@ -21,7 +24,6 @@
    <p>Yichen Liu</p>

    <p>Edward Huang</p>

    <p>Zhiyu Lin</p>

-   <p>Ming Qi</p>

    <p>Jian Jiao</p>

    <p>Juanchen Li</p>

    <h3><strong>Former Contributors</strong></h3>

diff --git a/ApacheCMDA_Frontend_1.0/app/views/allClimateServices.scala.html b/ApacheCMDA_Frontend_1.0/app/views/allClimateServices.scala.html
index 547e8d1..6dabf7f 100644
--- a/ApacheCMDA_Frontend_1.0/app/views/allClimateServices.scala.html
+++ b/ApacheCMDA_Frontend_1.0/app/views/allClimateServices.scala.html
@@ -11,10 +11,10 @@
 		<th style = "vertical-align: middle;" class="col-md-4">Purpose</th>
 		<th style = "vertical-align: middle;" class="col-md-4">URL</th>
 		<th style = "vertical-align: middle;" class="col-md-3">Scenario</th>
-		<th style = "vertical-align: middle;" class="col-md-3">Version_No</th>
+		<th style = "vertical-align: middle;" class="col-md-3">Version No</th>
 		<th style = "vertical-align: middle;" class="col-md-3">Root Service_Id</th>
 		 @if(true){
-			<th style = "vertical-align: middle;" class="col-md-3">Operation</th> 
+			<th style = "vertical-align: middle;" class="col-md-2">Operation</th> 
 	     } 
 	     <th style = "vertical-align: middle;" class="col-md-2">Operation</th>
 	</tr>
@@ -27,14 +27,21 @@
 		<td><span class="@climateService.getName() editable" data-name='purpose'>
 				@{
 					if (climateService.getPurpose().length > 5 ) {
-						climateService.getPurpose().substring(0,5);
+						climateService.getPurpose().substring(0,5) + "...";
 					}else {
 						climateService.getPurpose();
 					}
-				} ... </span></td>
+				}  </span></td>
 		
 		<td><span class="@climateService.getName() editable" id = "url" data-name='url'>
-				<a href = "@climateService.getUrl()">@climateService.getUrl()</a></span></td>
+				<a href = "@climateService.getUrl()">
+				@{
+					if (climateService.getUrl().length > 5 ) {
+						"..." + climateService.getUrl().substring(31);
+					}else {
+						climateService.getUrl();
+					}
+				}</a></span></td>
 				
 		<td><span class="@climateService.getName() editable" data-name='scenario'>
 				@climateService.getScenario() </span></td>
diff --git a/ApacheCMDA_Frontend_1.0/app/views/allDatasets.scala.html b/ApacheCMDA_Frontend_1.0/app/views/allDatasets.scala.html
index 8976c6b..50649d4 100644
--- a/ApacheCMDA_Frontend_1.0/app/views/allDatasets.scala.html
+++ b/ApacheCMDA_Frontend_1.0/app/views/allDatasets.scala.html
@@ -12,7 +12,7 @@
 		-->
 		<th style = "vertical-align: top;" class="col-md-2">Dataset Name</th>
 		<th style = "vertical-align: top;" class="col-md-1">Agency</th>
-		<th style = "vertical-align: top;" class="col-md-1">Instrument</th>
+		<th style = "vertical-align: top;" class="col-md-1">Instrument/Model Experiment</th>
 		<th style = "vertical-align: top;" class="col-md-2">Physical variable</th>
 		<th style = "vertical-align: top;" class="col-md-1">Variable short name</th>
 		<th style = "vertical-align: top;" class="col-md-1">Units</th>
diff --git a/ApacheCMDA_Frontend_1.0/app/views/climateServiceList.scala.html b/ApacheCMDA_Frontend_1.0/app/views/climateServiceList.scala.html
new file mode 100644
index 0000000..f871090
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/app/views/climateServiceList.scala.html
@@ -0,0 +1,62 @@
+@(climateServices: List[ClimateService])
+@import helper._
+
+@main("All Climate Services") {
+
+    <h1>@climateServices.size() Climate Services</h1>
+		<table class="table table-striped table-bordered table-condensed ex2 tablesorter" id = "csTable">
+	<thead>
+	<tr class="text-center">
+		<th style = "vertical-align: middle;" class="col-md-2">Climate Service Name</th>
+		<th style = "vertical-align: middle;" class="col-md-4">Purpose</th>
+		<th style = "vertical-align: middle;" class="col-md-4">URL</th>
+		<th style = "vertical-align: middle;" class="col-md-3">Scenario</th>
+		<th style = "vertical-align: middle;" class="col-md-3">Version No</th>
+		<th style = "vertical-align: middle;" class="col-md-3">Root Service_Id</th>
+		 @if(true){
+			<th style = "vertical-align: middle;" class="col-md-2">Operation</th> 
+	     } 
+	     <th style = "vertical-align: middle;" class="col-md-2">Operation</th>
+	</tr>
+	</thead>
+	<tbody>
+	@for(climateService <- climateServices) {
+	<tr>
+		<td><a href = "@climateService.getUrl()">@climateService.getName()</a></td>
+
+		<td><span class="@climateService.getName() editable" data-name='purpose'>
+				@{
+					if (climateService.getPurpose().length > 5 ) {
+						climateService.getPurpose().substring(0,5) + "...";
+					}else {
+						climateService.getPurpose();
+					}
+				}  </span></td>
+		
+		<td><span class="@climateService.getName() editable" id = "url" data-name='url'>
+				<a href = "@climateService.getUrl()">
+				@{
+					if (climateService.getUrl().length > 5 ) {
+						"..." + climateService.getUrl().substring(31);
+					}else {
+						climateService.getUrl();
+					}
+				}</a></span></td>
+				
+		<td><span class="@climateService.getName() editable" data-name='scenario'>
+				@climateService.getScenario() </span></td>
+				
+		<td><span class="@climateService.getName() editable" data-name='versionNo'>
+				@climateService.getVersionNo() </span></td>
+				
+		<td><span class="@climateService.getName() editable" data-name='rootServiceId'>
+				@climateService.getRootServiceId() </span></td>
+				
+		<td><input type="file" class="btn btn-info" id ="upload @climateService.getName()" ></button></td>
+		<td><button type="button" class="btn btn-danger" id ="doReplace"  onclick="Javascript:replaceFile('upload '+'@climateService.getName()')" >Execute</button></td>
+	
+	</tr>
+	}
+	</tbody>
+    </table>
+}
diff --git a/ApacheCMDA_Frontend_1.0/app/views/dataSetList.scala.html b/ApacheCMDA_Frontend_1.0/app/views/dataSetList.scala.html
index 83aeb18..078c6c8 100644
--- a/ApacheCMDA_Frontend_1.0/app/views/dataSetList.scala.html
+++ b/ApacheCMDA_Frontend_1.0/app/views/dataSetList.scala.html
@@ -72,15 +72,15 @@
 								<div class="well col-lg-offset-4 col-lg-4 col-sm-offset-3 col-sm-6">
 									<div class="text-center">
 										<img class="card-img-top" style=" width:60%;"
-											src='@routes.Assets.at("images/data.png")' >
+											src='@routes.Assets.at("images/giphy.gif")' >
 									</div>
 									<div class="card-block text-center">
 										<h4 class="card-title">@dataSet.getName()</h4>
 										<p class="card-text text-muted">A data set (or dataset) is a collection of data.</p>
 									</div>
 									<ul class="list-group list-group-flush">
-										<li class="list-group-item"><strong>Agency:</strong> <code>@dataSet.getAgencyId()</code></li>
-										<li class="list-group-item"><strong>Instrument:</strong> <code>@dataSet.getInstrument()</code></li>
+										<li class="list-group-item"><strong>Agency:</strong> <a href="@dataSet.getAgencyURL()" target="_blank"><code><u>@dataSet.getAgencyId()</u></code></a></li>
+										<li class="list-group-item"><strong>Instrument/Model Experiment:</strong> <a href="@dataSet.getInstrumentURL()" target="_blank"><code><u>@dataSet.getInstrument()</u></code></a></li>
 										<li class="list-group-item"><strong>Units:</strong> <code>@dataSet.getUnits()</code></li>
 										<li class="list-group-item"><strong>Start Time:</strong> <code>@dataSet.getStartTime()</code></li>
 										<li class="list-group-item"><strong>End Time:</strong> <code>@dataSet.getEndTime()</code></li>
@@ -102,15 +102,20 @@
 									</div>
 									
 									<div class="row">
+									@for(climateService <- dataSet.getClimateServices()) {
 									  <div class="col-md-6">
 									    <div class="thumbnail">
+									    <a href="@climateService.getUrl()">
 									      <img src='@routes.Assets.at("images/github.png")' >
+								      	</a>
 									      <div class="caption">
-									        <h5>NASA</h5>				        
-									        <p><a href="#" class="btn btn-primary" role="button">Go</a> <a href="#" class="btn btn-default" role="button">Ignore</a></p>
+									        <h5>@climateService.getName()</h5>				        
+									        
 									      </div>				      
 									    </div>
 									  </div>
+									  }
+									  <!--  
 									  <div class="col-md-6">
 									    <div class="thumbnail">
 									      <img src='@routes.Assets.at("images/bug.png")' >
@@ -120,6 +125,7 @@
 									      </div>				      
 									    </div>
 									  </div>
+									  -->
 									</div>
 									
 								</div>
@@ -162,7 +168,7 @@
 		-->
 		<th style = "vertical-align: top;" class="col-md-2">Dataset Name</th>
 		<th style = "vertical-align: top;" class="col-md-1">Agency</th>
-		<th style = "vertical-align: top;" class="col-md-1">Instrument</th>
+		<th style = "vertical-align: top;" class="col-md-1">Instrument/Model Experiment</th>
 		<th style = "vertical-align: top;" class="col-md-2">Physical variable</th>
 		<th style = "vertical-align: top;" class="col-md-1">Variable short name</th>
 		<th style = "vertical-align: top;" class="col-md-1">Units</th>
diff --git a/ApacheCMDA_Frontend_1.0/app/views/dataSetListPopular.scala.html b/ApacheCMDA_Frontend_1.0/app/views/dataSetListPopular.scala.html
new file mode 100644
index 0000000..9df2a3d
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/app/views/dataSetListPopular.scala.html
@@ -0,0 +1,68 @@
+@(dataSetForm: play.data.Form[Dataset], dataSets: List[Dataset])
+
+@import helper._
+@import java.math.BigInteger;var k=1;var n = 0;
+
+@scripts = {
+	<script src='@routes.Assets.at("javascripts/edit_button.js")'></script>
+	<script type="text/javascript">
+	$(document).ready(function(){
+		//alert($("#url").text());
+	});
+	</script>
+}
+
+@main("Dataset List", scripts) { 
+	 
+    <h1>Datasets List</h1>
+    <h2>@dataSets.size() Datasets Found</h2>
+<div style="overflow-y:scroll">
+		<table class="table table-striped table-bordered table-condensed tablesorter" id ="myTable">
+		<thead>
+	<tr >
+		<!-- 
+		<th style = "vertical-align: top;" class="col-md-1 header">Id</th>
+		-->
+		<th style = "vertical-align: top;" class="col-md-2">Dataset Name</th>
+		<th style = "vertical-align: top;" class="col-md-1">Agency</th>
+		<th style = "vertical-align: top;" class="col-md-1">Instrument/Model Experiment</th>
+		<th style = "vertical-align: top;" class="col-md-2">Physical variable</th>
+		<th style = "vertical-align: top;" class="col-md-1">Variable short name</th>
+		<th style = "vertical-align: top;" class="col-md-1">Units</th>
+		<th style = "vertical-align: top;" class="col-md-1">Grid Dimension</th>
+		
+		<th style = "vertical-align: top;" class="col-md-2">Variable Name in Web Interface</th>
+		<th style = "vertical-align: top;" class="col-md-1">Data Source Input Parameter</th>
+		
+		<th style = "vertical-align: top;" class="col-md-1">Dataset Start Time</th>
+		<th style = "vertical-align: top;"class="col-md-1">Dataset End Time</th>
+		<th style = "vertical-align: top;"class="col-md-1">Duration</th>
+
+	</tr>
+	</thead>
+	<tbody>
+@for(dataSet <- dataSets){
+							<tr>
+								<!-- 
+								<td><font size="2">@dataSet.getId()</font></td>
+								-->
+								<td><font size="2">@dataSet.getName()</font></td>
+								<td><font size="2">@dataSet.getAgencyId()</font></td>
+								<td><font size="2">@dataSet.getInstrument()</font></td>
+								<td><font size="2">@dataSet.getPhysicalVariable()</font></td>
+								<td><font size="2">@dataSet.getCMIP5VarName()</font></td>
+								<td><font size="2">@dataSet.getUnits()</font></td>
+								<td><font size="2">@dataSet.getGridDimension()</font></td>
+								<td><font size="2">@dataSet.getVariableNameInWebInterface()</font></td>
+								<td><font size="2">@dataSet.getDataSourceInputParameterToCallScienceApplicationCode()</font></td>
+								<td><font size="2">@dataSet.getStartTime()</font></td>
+								<td><font size="2">@dataSet.getEndTime()</font></td>
+								<td><font size="2">@dataSet.getDuration()</font></td>
+							</tr>
+							}
+
+	
+	</tbody>
+    </table>
+    </div>
+}
diff --git a/ApacheCMDA_Frontend_1.0/app/views/header.scala.html b/ApacheCMDA_Frontend_1.0/app/views/header.scala.html
index c67371d..3446fdd 100644
--- a/ApacheCMDA_Frontend_1.0/app/views/header.scala.html
+++ b/ApacheCMDA_Frontend_1.0/app/views/header.scala.html
@@ -10,8 +10,8 @@
 				</button>
 				<a class="navbar-brand" style="padding-bottom: 0;" href="/">
 					<div>
-					<img src='@routes.Assets.at("images/NASA_JPL_logo.png")' style="height: 30px; width: 180px;">&nbsp&nbsp
-					<img src='@routes.Assets.at("images/logo.png")' style="height: 15px; width: 200px;">
+					<img src='@routes.Assets.at("images/NASA_JPL_logo.png")' style="height: 24px; width: 144px;">
+					<img src='@routes.Assets.at("images/logo.png")' style="height: 12px; width: 160px;">
 					</div>
 				</a>
 			</div>
@@ -19,6 +19,14 @@
 				<ul class="nav navbar-nav navbar-right">
 					
 					<li class="dropdown">
+						<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="text-danger">Recommendation</span><b class="caret"></b></a>
+						<ul class="dropdown-menu">
+							<li><a href="@routes.ClimateServiceController.recommendationSummary()">Summary</a></li>
+							
+						</ul>				
+					</li>
+					
+					<li class="dropdown">
 						<a href="#" class="dropdown-toggle" data-toggle="dropdown">Web Service<b class="caret"></b></a>
 						<ul class="dropdown-menu">
 							<li><a href="@routes.ClimateServiceController.mostRecentlyAddedClimateServices()">Most Recently Added</a></li>
@@ -28,14 +36,14 @@
 							@if(true) {
 								<li><a href="@routes.ClimateServiceController.addAClimateService()">Register A Service</a></li>
 							}
-							<li><a>Search Service</a></li>
+							<li><a href="@routes.ClimateServiceController.searchClimateServices()">Search Service</a></li>
 						</ul>				
 					</li>
 					
 					<li class="dropdown">
 						<a href="#" class="dropdown-toggle" data-toggle="dropdown">Dataset<b class="caret"></b></a>
 						<ul class="dropdown-menu">
-							<li><a>Most Popular</a></li>
+							<li><a href="@routes.DatasetController.mostPopularDatasets()">Most Popular</a></li>
 							<li><a href="@routes.DatasetController.showAllDatasets()">Dataset List</a></li>
 							@if(true) {
 								<li><a href="#">Register A Dataset</a></li>
@@ -50,7 +58,7 @@
 							<li><a href="@routes.ServiceExecutionLogController.getServiceLog">Service Execution Log</a></li>
 							<li><a href="@routes.ServiceExecutionLogController.searchServiceLog">Search Service Log</a></li>
 							<li><a href="@routes.DatasetLogController.getAllDatasetLogs">Dataset Log</a></li>
-							<li><a href="@routes.AnalyticsController.getRecommend">Semantic Service Analytics</a></li>
+							<li><a href="@routes.AnalyticsController.getKnowledgeGraph">Knowledge Graph</a></li>
 							<li><a href="@routes.AnalyticsController.getDatasetRecommend">Semantic Dataset Analytics</a></li>
 							<li><a href="@routes.AnalyticsController.getScientistRecommend">User Analytics</a></li>
 							<li><a href="@routes.AnalyticsController.getLogGraph">Service Execution Log Analytics</a></li>
diff --git a/ApacheCMDA_Frontend_1.0/app/views/knowledgeGraph.scala.html b/ApacheCMDA_Frontend_1.0/app/views/knowledgeGraph.scala.html
new file mode 100644
index 0000000..fe6a223
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/app/views/knowledgeGraph.scala.html
@@ -0,0 +1,320 @@
+@(jsonData: String)
+@import helper._
+
+@scripts = {
+	<link rel="stylesheet" href='@routes.Assets.at("stylesheets/vis.css")'>
+	<style type="text/css">
+	        html, body {
+	            font: 10pt arial;
+	          }
+	        #mynetwork {
+	            width: 600px;
+	            height: 600px;
+	            border: 1px solid lightgray;
+	        }
+	        #loadingBar {
+	            position:absolute;
+	            
+	            width: 610px;
+	            height: 610px;
+	            background-color:rgba(200,200,200,0.8);
+	            -webkit-transition: all 0.5s ease;
+	            -moz-transition: all 0.5s ease;
+	            -ms-transition: all 0.5s ease;
+	            -o-transition: all 0.5s ease;
+	            transition: all 0.5s ease;
+	            opacity:1;
+	        }
+	        #wrapper {
+	            position:relative;
+	            width:900px;
+	            height:900px;
+	        }
+	
+	        #text {
+	            position:absolute;
+	            top:0px;
+	            left:530px;
+	            width:30px;
+	            height:50px;
+	            margin:auto auto auto auto;
+	            font-size:22px;
+	            color: #000000;
+	        }
+	
+	
+	        div.outerBorder {
+	            position:relative;
+	            top:300px;
+	            width:600px;
+	            height:44px;
+	            margin:auto auto auto auto;
+	            border:8px solid rgba(0,0,0,0.1);
+	            background: rgb(252,252,252); /* Old browsers */
+	            background: -moz-linear-gradient(top,  rgba(252,252,252,1) 0%, rgba(237,237,237,1) 100%); /* FF3.6+ */
+	            background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(252,252,252,1)), color-stop(100%,rgba(237,237,237,1))); /* Chrome,Safari4+ */
+	            background: -webkit-linear-gradient(top,  rgba(252,252,252,1) 0%,rgba(237,237,237,1) 100%); /* Chrome10+,Safari5.1+ */
+	            background: -o-linear-gradient(top,  rgba(252,252,252,1) 0%,rgba(237,237,237,1) 100%); /* Opera 11.10+ */
+	            background: -ms-linear-gradient(top,  rgba(252,252,252,1) 0%,rgba(237,237,237,1) 100%); /* IE10+ */
+	            background: linear-gradient(to bottom,  rgba(252,252,252,1) 0%,rgba(237,237,237,1) 100%); /* W3C */
+	            filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fcfcfc', endColorstr='#ededed',GradientType=0 ); /* IE6-9 */
+	            border-radius:72px;
+	            box-shadow: 0px 0px 10px rgba(0,0,0,0.2);
+	        }
+	
+	        #border {
+	            position:absolute;
+	            top:2px;
+	            left:2px;
+	            width:500px;
+	            height:23px;
+	            margin:auto auto auto auto;
+	            box-shadow: 0px 0px 4px rgba(0,0,0,0.2);
+	            border-radius:10px;
+	        }
+	
+	        #bar {
+	            position:absolute;
+	            
+	            width:10px;
+	            height:20px;
+	            margin:auto auto auto auto;
+	            border-radius:11px;
+	            border:2px solid rgba(30,30,30,0.05);
+	            background: rgb(153, 255, 255); /* Old browsers */
+	            box-shadow: 2px 2px 4px rgba(0,0,0,0.4);
+	        }
+	
+	        #config {            
+	            
+	            width: 400px;
+	            height: 600px;
+	
+	        }
+	        #config input {
+	            display: inline-block;
+	        }
+	        #config input.vis-configuration.vis-config-rangeinput {
+	            height: 15px;
+	        }
+	        #config button, input, select, textarea {
+	            line-height: 100%;
+	        }
+	</style>
+	
+	<script type="text/javascript" src='@routes.Assets.at("javascripts/exampleUtil.js")'></script>
+	<script type="text/javascript" src='@routes.Assets.at("javascripts/vis.js")'></script>
+	<script type="text/javascript">
+	$(document).ready(function() {  
+	    $("#panel").hide(); 
+	    $("#config").hide();
+	    
+	    draw();
+	  })
+
+	    var nodes = null;
+	    var edges = null;
+	    var network = null;
+
+	    var visConfig = 1;
+
+	    function hideConfig() {
+	        if (visConfig == 0) {
+	            $("#config").hide();
+	            visConfig =1;
+	        }else {
+	            $("#config").show();
+	            visConfig =0;
+	        }
+	    }
+
+	    function draw() {
+	    	console.log("draw");
+	      // create people.
+	      // value corresponds with the age of the person
+	      var jsonString = $('#jsonData').text();
+	      var test = jsonString;
+	      
+	      //test = JSON.stringify(test);
+	      console.log("test: " + test);
+	      test = JSON.parse(test);
+	      var nodes = test.nodes;
+	      console.log("nodes: " + nodes);
+	      var edges = test.edges;
+	      console.log("edges: " + edges); 
+
+	      // Instantiate our network object.
+	      var container = document.getElementById('mynetwork');
+	      var data = {
+	        nodes: nodes,
+	        edges: edges
+	      };
+	      var options = {
+   		  nodes: {
+   			    color: {
+   			      highlight: {
+   			        border: "red",
+   			        background: "pink"
+   			      }
+   			    },
+   			    scaling: {
+   			      min: 12
+   			    },
+   			    shadow: {
+   			      enabled: true,
+   			      size: 8
+   			    },
+	   			 font: {
+	   		      color: "rgba(52,52,52,1)",
+	   		      size: 10,
+	   		      strokeWidth: 4
+	   		    },
+   			    shape: "dot",
+   			    shapeProperties: {
+   			      borderRadius: 5
+   			    }
+   			  },
+   			  edges: {
+   			    arrows: {
+   			      to: {
+   			        enabled: true,
+   			        scaleFactor: 0.4
+   			      }
+   			    },
+ 			  color: {
+ 			      highlight: "rgba(244,1,0,1)",
+ 			      hover: "rgba(0,0,0,1)",
+ 			      inherit: false
+ 			    },
+		      smooth: {
+ 			      type: "continuous",
+ 			      forceDirection: "vertical"
+ 			    }
+   			  },
+   			  interaction: {
+   			    hover: true,
+   			    multiselect: true
+   			  },
+	         groups: {
+	          dataset: {
+	            color:"#A9E2F3"
+	          },
+	          user: {
+	        	color:"#F781F3"
+	          }
+	        },
+	        
+	        physics: {    
+	          barnesHut: {
+	        	  centralGravity: 1.95,	              
+	        	  springLength: 195,
+	              springConstant: 0.185,
+	        	  damping: 0.25,
+	              avoidOverlap: 0.2
+   			    },
+   			 	maxVelocity: 10,
+   			    minVelocity: 0.75,
+	          stabilization: {
+	            enabled:true,
+	            iterations:200,
+	            updateInterval:1
+	          }
+	        },
+	        configure: {
+	          filter:function (option, path) {
+	            if (path.indexOf('physics') !== -1 || option === 'physics') {
+	              return true;
+	            }
+	            if (path.indexOf('smooth') !== -1 || option === 'smooth') {
+	              return true;
+	            }
+	            return true;
+	          },
+	          container: document.getElementById('config')
+	        }
+	      };
+	      network = new vis.Network(container, data, options);
+
+	      network.on("stabilizationProgress", function(params) {
+	                var maxWidth = 496;
+	                var minWidth = 20;
+	                var widthFactor = params.iterations/params.total;
+	                var width = Math.max(minWidth,maxWidth * widthFactor);
+
+	                document.getElementById('bar').style.width = width + 'px';
+	                document.getElementById('text').innerHTML = Math.round(widthFactor*100) + '%';
+	            });
+	            network.once("stabilizationIterationsDone", function() {
+	                document.getElementById('text').innerHTML = '100%';
+	                document.getElementById('bar').style.width = '496px';
+	                document.getElementById('loadingBar').style.opacity = 0;
+	                // really clean the dom element
+	                setTimeout(function () {document.getElementById('loadingBar').style.display = 'none';}, 500);
+	            });
+
+	      //network.focusOnNode(19);
+	      network.on('select', function(properties) {
+	        var select_node = $.grep(data.nodes, function(e){
+	          return e["id"] == properties.nodes[0];
+	        })[0];
+
+	        $("#nodeName").text(select_node["title"]);
+	        //alert(select_node["title"]);
+	        $("#id").text(select_node["id"]);
+	        $("#cluster").text(select_node["cluster"]);
+	        $("#label").text(select_node["label"]);
+
+	        $("#panel").show();
+	      });
+	    }
+	    
+	    
+	</script>
+	<script type="text/javascript" src='@routes.Assets.at("javascripts/googleAnalytics.js")'></script>
+}
+
+@main("Knowledge Graph", scripts){
+	<div id="jsonData" style="display: none;">@jsonData</div>
+	<div id="knowledgeGraph">
+	
+	   <div id="loadingBar" class="col-lg-3">
+	    <div class="outerBorder" >
+	        <div id="text">0%</div>
+	        <div id="border">
+	            <div id="bar"></div>
+	        </div>
+	    </div>	    
+	   </div>
+	   
+	  <div id="mynetwork" class="col-lg-3"></div>
+	  <div class = "col-lg-offset-7">
+	        <button type="button" onclick="hideConfig()" class="btn btn-default btn-lg "><span class="glyphicon glyphicon-star" aria-hidden="true">
+	        </span> Show Advanced Settings </button>
+	  </div>
+	  <div id="config" class="col-lg-offset-7"></div>  
+	</div>
+	
+	<br>
+	
+	<div id="panel" class="col-lg-offset-7">  
+	  <div class="well col-lg-5">
+	    <div class="text-center">
+	      <img class="card-img-top" style=" width:60%;" src="/assets/images/data.png">
+	    </div>
+	    <div class="card-block text-center">
+	      <h4 class="card-title" id="nodeName">card</h4>
+	      <p class="card-text text-muted" >A data set (or dataset) is a collection of data.</p>
+	    </div>
+	    <ul class="list-group list-group-flush">
+	      <li class="list-group-item"><strong>Id:</strong><code id="id"></code></li>
+	      <li class="list-group-item"><strong>Cluster:</strong><code id="cluster"></code></li>
+	      <li class="list-group-item"><strong>Label:</strong><code id="label"></code></li>
+	    </ul>
+	    <div class="card-block">
+	      <a href="#" class="card-link col-lg-offset-0 col-lg-6">Card link</a> 
+	      <a href="#" class="card-link col-lg-6">Another link</a>
+	    </div>    
+	  </div>  
+	</div>
+  
+}
diff --git a/ApacheCMDA_Frontend_1.0/app/views/main.scala.html b/ApacheCMDA_Frontend_1.0/app/views/main.scala.html
index 37a5caa..a41f414 100644
--- a/ApacheCMDA_Frontend_1.0/app/views/main.scala.html
+++ b/ApacheCMDA_Frontend_1.0/app/views/main.scala.html
@@ -32,8 +32,6 @@
 	href='@routes.Assets.at("stylesheets/livefitler.css")'>
 <link rel="stylesheet"
     href='@routes.Assets.at("stylesheets/custom_recommend.css")'>
-<link rel="stylesheet"
-	href="https://cdnjs.cloudflare.com/ajax/libs/vis/3.11.0/vis.min.css">
 <link rel="stylesheet" 
 	href="http://code.jquery.com/ui/1.11.3/themes/smoothness/jquery-ui.css">
 
diff --git a/ApacheCMDA_Frontend_1.0/app/views/recommendationSummary.scala.html b/ApacheCMDA_Frontend_1.0/app/views/recommendationSummary.scala.html
new file mode 100644
index 0000000..7cd4d3f
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/app/views/recommendationSummary.scala.html
@@ -0,0 +1,171 @@
+@(climateServices: List[ClimateService], dataSets: List[Dataset], users: List[User])
+
+@import helper._
+
+@scripts = {
+	<script type="text/javascript">
+	$(document).ready(function(){
+		$('#myTabs a').click(function (e) {
+			  e.preventDefault()
+			  $(this).tab('show')
+		})
+	});
+	</script>
+}
+
+@main("Climate Services", scripts) {
+	 
+    <h1>Recommendation</h1>
+    
+    <div>
+	  <!-- Nav tabs -->
+	  <ul class="nav nav-tabs" role="tablist">
+	    <li role="presentation" class="active"><a href="#mostPopService" aria-controls="home" role="tab" data-toggle="tab">Most Popular Service</a></li>
+	    <li role="presentation"><a href="#mostPopDataset" aria-controls="profile" role="tab" data-toggle="tab">Most Popular Dataset</a></li>
+	    <li role="presentation"><a href="#mostActUsers" aria-controls="messages" role="tab" data-toggle="tab">Most Active User</a></li>
+	    <li role="presentation"><a href="#settings" aria-controls="settings" role="tab" data-toggle="tab">Settings</a></li>
+	  </ul>	
+	  <!-- Tab panes -->
+	  <div class="tab-content">
+	    <div role="tabpanel" class="tab-pane active" id="mostPopService">
+				<table class="table table-striped table-bordered table-condensed ex2">
+					<tr>
+						<td class="col-md-2">Climate Service Name</td>
+						<td class="col-md-2">Purpose</td>
+						<td class="col-md-4">Url</td>
+						<td class="col-md-2">Scenario</td>
+						<td class="col-md-2">Version</td>
+						<td class="col-md-2">Root_Service</td>						  
+					</tr>
+					
+					@for(climateService <- climateServices){
+					<tr>
+						<td><a href = "@climateService.getUrl()">@climateService.getName()</a></td>
+				
+						<td><span class="@climateService.getName() editable" data-name='purpose'>
+								@climateService.getPurpose() </span></td>
+						
+						<td><span class="@climateService.getName() editable" id = "url" data-name='url'>
+								<a href = "@climateService.getUrl()">
+								@climateService.getUrl()</a> </span></td>
+								
+						<td><span class="@climateService.getName() editable" data-name='scenario'>
+								@climateService.getScenario() </span></td>
+								
+						<td><span class="@climateService.getName() editable" data-name='versionNo'>
+								@climateService.getVersionNo() </span></td>
+								
+						<td><span class="@climateService.getName() editable" data-name='rootServiceId'>
+								@climateService.getRootServiceId() </span></td>						
+					</tr>
+					}
+			    </table>
+    	</div>
+    	
+	    <div role="tabpanel" class="tab-pane" id="mostPopDataset">
+				<table class="table table-striped table-bordered table-condensed ex2 tablesorter" >
+					<thead>
+					<tr>
+						<!--  
+						<th style = "vertical-align: top;" class="col-md-1 header">Id</th>
+						-->
+						<th style = "vertical-align: top;" class="col-md-2">Dataset Name</th>
+						<th style = "vertical-align: top;" class="col-md-1">Agency</th>
+						<th style = "vertical-align: top;" class="col-md-1">Instrument/Model Experiment</th>
+						<th style = "vertical-align: top;" class="col-md-2">Physical variable</th>
+						<th style = "vertical-align: top;" class="col-md-1">Variable short name</th>
+						<th style = "vertical-align: top;" class="col-md-1">Units</th>
+						<th style = "vertical-align: top;" class="col-md-1">Grid Dimension</th>
+						<!--  
+						<th style = "vertical-align: top;" class="col-md-3">Source</th> 
+						<th style = "vertical-align: top;" class="col-md-3">Status</th>
+						<th style = "vertical-align: top;" class="col-md-4">Responsible Person</th>
+						<th style = "vertical-align: top;" class="col-md-4">Data Source Name in Web Interface</th> 
+						-->
+						<th style = "vertical-align: top;" class="col-md-2">Variable Name in Web Interface</th>
+						<th style = "vertical-align: top;" class="col-md-1">Data Source Input Parameter</th>
+						<!-- <th style = "vertical-align: top;" class="col-md-3">Variable Name Input Parameter</th> -->
+						<th style = "vertical-align: top;" class="col-md-1">Dataset Start Time</th>
+						<th style = "vertical-align: top;"class="col-md-1">Dataset End Time</th>
+						<th style = "vertical-align: top;"class="col-md-1">Duration</th>
+					</tr>
+					</thead>
+					<tbody>
+					@for(dataSet <- dataSets){
+					<tr>
+						<!--  
+						<td><font size="2">@dataSet.getId()</font></td> 
+						-->
+						<td><font size="2">@dataSet.getName()</font></td>
+						<td><font size="2">@dataSet.getAgencyId()</font></td>
+						<td><font size="2">@dataSet.getInstrument()</font></td>
+						<td><font size="2">@dataSet.getPhysicalVariable()</font></td>
+						<td><font size="2">@dataSet.getCMIP5VarName()</font></td>
+						<td><font size="2">@dataSet.getUnits()</font></td>
+						<td><font size="2">@dataSet.getGridDimension()</font></td>
+						<!--
+						<td><font size="2">@dataSet.getSource()</font></td>
+						<td><font size="2">@dataSet.getStatus()</font></td>
+						<td><font size="2">@dataSet.getResponsiblePerson()</font></td>
+						<td><font size="2">@dataSet.getDataSourceNameinWebInterface()</font></td>
+						-->
+						<td><font size="2">@dataSet.getVariableNameInWebInterface()</font></td>
+						<td><font size="2">@dataSet.getDataSourceInputParameterToCallScienceApplicationCode()</font></td>
+						<!--<td><font size="2">@dataSet.getVariableNameInputParameterToCallScienceApplicationCode()</font></td> -->
+						<td><font size="2">@dataSet.getStartTime()</font></td>
+						<td><font size="2">@dataSet.getEndTime()</font></td>
+						<td><font size="2">@dataSet.getDuration()</font></td>						
+					</tr>					
+					}
+					</tbody>
+				    </table>
+		
+		</div>
+	    <div role="tabpanel" class="tab-pane" id="mostActUsers">
+
+			<table class="table table-striped table-bordered table-condensed ex2">
+				<tr>
+					<td class="col-md-1">Id</td>
+					<td class="col-md-2">User Name</td>
+					<td class="col-md-2">First Name</td>
+					<td class="col-md-2">Middle Name</td>
+					<td class="col-md-2">Last Name</td>
+					<td class="col-md-2">Afflication</td>
+					<td class="col-md-4">Email</td>
+					<td class="col-md-2">Research Area</td>
+					
+				</tr>
+				@for(user <- users){
+				<tr>
+					<td>@user.getId()</td>
+					
+					<td><span class="@user.getId() editable" data-name='username'>@user.getUserName()</span></td>
+							
+					<td><span class="@user.getId() editable" data-name='firstName'>@user.getFirstName()</span></td>
+							
+					<td><span class="@user.getId() editable" data-name='middleInitial'>@user.getMiddleInitial()</span></td>
+							
+					<td><span class="@user.getId() editable" data-name='lastName'>@user.getLastName()</span></td>
+					
+					<td><span class="@user.getId() editable" data-name='affiliation'>@user.getAffiliation()</span></td>
+							
+					<td><span class="@user.getId() editable" data-name='email'>@user.getEmail()</span></td>
+							
+					<td><span class="@user.getId() editable" data-name='researchFields'>@user.getResearchFields()</span></td>
+						
+				</tr>
+				}
+		    </table>
+		
+		
+		
+		</div>
+	    <div role="tabpanel" class="tab-pane" id="settings">...</div>
+	  </div>
+	</div>
+    
+	
+ 
+    
+     
+}
diff --git a/ApacheCMDA_Frontend_1.0/app/views/searchClimateService.scala.html b/ApacheCMDA_Frontend_1.0/app/views/searchClimateService.scala.html
new file mode 100644
index 0000000..84ae278
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/app/views/searchClimateService.scala.html
@@ -0,0 +1,117 @@
+@(climateServiceForm: play.data.Form[ClimateService])
+
+@import helper._
+
+@scripts = {
+	<script src='@routes.Assets.at("javascripts/edit_button.js")'></script>
+	<link rel="stylesheet" href="//code.jquery.com/ui/1.11.3/themes/smoothness/jquery-ui.css">
+	<script src="//code.jquery.com/jquery-1.10.2.js"></script>
+  	<script src="//code.jquery.com/ui/1.11.3/jquery-ui.js"></script>
+	<script type="text/javascript">
+		$(document)
+				.ready(
+						function() {
+							$("#preview")
+									.click(
+											function() {
+												var target = document
+														.getElementById("show");
+												if (target.style.display == "none") {
+													target.style.display = "block";
+													$("#preview").text("Hide");
+													var climateServiceName = $(
+															"#climateServiceName")
+															.val();
+													var purpose = $("#purpose")
+															.val();
+													var scenario = $(
+															"#scenario")
+															.val();
+													var url = $(
+															"#url")
+															.val();
+													var versionNo = $(
+															"#versionNo")
+															.val();
+												
+													if (climateServiceName != "") {
+														$("#content")
+																.append(
+																		" Climate Service Name = "
+																				+ climateServiceName);
+													}
+													if (purpose != ""
+															&& purpose != null) {
+														$("#content")
+																.append(
+																		" Purpose = "
+																				+ purpose);
+													}
+												
+													if (scenario != ""
+															&& scenario != null) {
+														$("#content")
+																.append(
+																		" Scenario = "
+																				+ scenario);
+													}
+													if (url != ""
+															&& url != null) {
+														$("#content")
+																.append(
+																		" URL = "
+																				+ url);
+													}
+													if (versionNo != ""
+															&& versionNo != null) {
+														$("#content")
+																.append(
+																		" Version Number = "
+																				+ versionNo);
+													}
+
+												} else {
+													$("#content").val('');
+													target.style.display = "none";
+													$("#preview").text(
+															"Preview");
+												}
+											});
+						});
+	</script>
+}
+
+@main("Search Climate Service", scripts){ 
+	 
+    <h1 style="margin-left:420px">Search Climate Service</h1>
+    @helper.form(routes.ClimateServiceController.getSearchResult()) {
+    	<div class="ui-widget col-sm-offset-3 col-sm-7">
+    	<div class = "form-group">
+    		@inputText(climateServiceForm("Climate Service Name"), 'class -> "form-control", 'id -> "climateServiceName", '_label -> Messages("Climate Service Name"), 'placeholder -> "twoDimSlice3D", 'size->70) 
+    	</div>
+    	<div class = "form-group">
+    		@inputText(climateServiceForm("Purpose"), 'class -> "form-control", 'id -> "purpose", '_label -> Messages("Purpose"), 'placeholder -> "service purpose", 'size->70) 
+    	</div>
+    	
+       	<div class = "form-group">
+       		@inputText(climateServiceForm("Scenario"), 'class -> "form-control", 'id -> "scenario", '_label -> Messages("Scenario"), 'placeholder -> "1", 'size->70)
+	    </div>
+	    <div class = "form-group">
+	    	@inputText(climateServiceForm("Url"), 'class -> "form-control", 'id -> "url", '_label -> Messages("Url"), 'placeholder -> "http://", 'size->70)
+	    	
+	    </div>
+	    <div class = "form-group">
+       		@inputText(climateServiceForm("Version Number"), 'class -> "form-control", 'id -> "versionNo", '_label -> Messages("Version Number"), 'placeholder -> "1", 'size->70)
+	    
+	    	<div id="show" style="display: none;">
+	    		<textarea style="width: 640px" rows="4" id="content"></textarea>
+	    	</div>
+	    </div>
+	    <div align="center">
+	    	<button id="preview" type="button" class="btn btn-info"> Preview</button>
+	    	<input class="btn" type="submit" value="Search">
+	    </div>
+	    </div>
+	    
+	    }
+	 }
\ No newline at end of file
diff --git a/ApacheCMDA_Frontend_1.0/app/views/serviceLog.scala.html b/ApacheCMDA_Frontend_1.0/app/views/serviceLog.scala.html
index b5d99f2..1ef867e 100644
--- a/ApacheCMDA_Frontend_1.0/app/views/serviceLog.scala.html
+++ b/ApacheCMDA_Frontend_1.0/app/views/serviceLog.scala.html
@@ -57,12 +57,19 @@
 	<td><font size="2">@serviceLog.getDataSetStartTime</font></td>
 	<td><font size="2">@serviceLog.getDataSetEndTime</font></td>
 	<td>
-	@form(routes.ServiceExecutionLogController.getConfigurationByConfId()){
-		<input
-			name="logId" class="hidden" type="hidden"
-			value="@serviceLog.getId">
-		<input
-			type="submit"  value="see details">
+	@if(serviceLog.getUrl() != null){
+		<form method="post" action="@serviceLog.getUrl">
+			<input type="submit"  value="see details">
+		</form>
+	
+	}else{
+		@form(routes.ServiceExecutionLogController.getConfigurationByConfId()){
+			<input
+				name="logId" class="hidden" type="hidden"
+				value="@serviceLog.getId">
+			<input
+				type="submit"  value="see details">
+		}
 	}
 	</td>
 	<!--
diff --git a/ApacheCMDA_Frontend_1.0/conf/routes b/ApacheCMDA_Frontend_1.0/conf/routes
index 35927f7..47558c9 100644
--- a/ApacheCMDA_Frontend_1.0/conf/routes
+++ b/ApacheCMDA_Frontend_1.0/conf/routes
@@ -12,6 +12,9 @@
 GET		/createSuccess				controllers.Application.createSuccess()
 POST	/isEmailExisted				controllers.Application.isEmailExisted()
 
+#recommendation overview
+GET		/climateService/recommendationSummary					controllers.ClimateServiceController.recommendationSummary()
+
 # Climate Service
 GET		/climateService/allServices									controllers.ClimateServiceController.showAllClimateServices()
 POST		/climateService/add											controllers.ClimateServiceController.addClimateService()
@@ -24,6 +27,8 @@
 GET     	/climateService/mostPopularServices						controllers.ClimateServiceController.mostPopularServices()
 GET		/climateService/mostRecentlyUsedClimateServices			controllers.ClimateServiceController.mostRecentlyUsedClimateServices()
 GET		/climateService/mostRecentlyAddedClimateServices		controllers.ClimateServiceController.mostRecentlyAddedClimateServices()
+GET		/climateService/searchClimateService 					 		controllers.ClimateServiceController.searchClimateServices()
+GET 	/climateService/getSearchResult				    controllers.ClimateServiceController.getSearchResult()
 
 #replace page
 POST 	/climateService/replaceFile								controllers.ClimateServiceController.replaceFile()
@@ -39,6 +44,7 @@
 GET     /dataSet/allDatasets                        controllers.DatasetController.showAllDatasets()
 GET		/dataSet/searchDataSet 					 	controllers.DatasetController.searchDataset()
 GET 	/dataSet/getSearchResult				    controllers.DatasetController.getSearchResult()
+GET     /dataSet/mostPopularDatasets				controllers.DatasetController.mostPopularDatasets()
 
 # Dataset Log
 GET		/datasetLog/allDatasetLogs					controllers.DatasetLogController.getAllDatasetLogs()
@@ -49,6 +55,7 @@
 GET     /profileRecommend                           controllers.AnalyticsController.getScientistRecommend()
 GET     /serviceLogGraph                            controllers.AnalyticsController.getLogGraph()
 GET 	/searchAndGenerateWorkflow					controllers.AnalyticsController.getSearchAndGenerateWorkflow()
+GET     /serviceKnowledgeGraph                      controllers.AnalyticsController.getKnowledgeGraph()
 
 # Users
 GET		/scientist/allUsers							controllers.UsersController.getAllUsers()
@@ -56,6 +63,7 @@
 
 # Map static resources from the /public folder to the /assets URL path
 GET     /assets/*file               controllers.Assets.at(path="/public", file)
+POST    /assets/*file               controllers.Assets.at(path="/public", file)
 
 
 # About
diff --git a/ApacheCMDA_Frontend_1.0/public/html/js2/.DS_Store b/ApacheCMDA_Frontend_1.0/public/html/js2/.DS_Store
new file mode 100644
index 0000000..5008ddf
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/html/js2/.DS_Store
Binary files differ
diff --git a/ApacheCMDA_Frontend_1.0/public/html/js2/common.css b/ApacheCMDA_Frontend_1.0/public/html/js2/common.css
new file mode 100644
index 0000000..477fb80
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/html/js2/common.css
@@ -0,0 +1,29 @@
+  .center1 {text-align: center;}
+  .right1 {text-align: right;}
+  /*.left1 {text-align: left; margin-left: 50px;}*/
+  .left1 {text-align: left;}
+  .middle1 {vertical-align: middle;}
+  .textwrapper {
+    border:1px solid #999999;
+    margin:5px 0;
+    padding:3px;
+  }
+  textarea { width:100%; }
+
+  .subtitle1 {color: DarkBlue; font-weight: bold; }
+
+  /*.color-head {background-color: Aqua;} */
+  .color-head {background-color: #C2EBFF  ;} /* A6C0CE  *A6C0CE  ABD8C0 */
+  .color4 {background-color: #B8B8B8;}  /*Azure EBFFF5  */
+  .color3 {background-color: #C8C8C8;}  /* Beige C2F0F0 */
+  /* .color3 {border-color: Azure; border-style: solid; } */ /* Beige C2F0F0 */
+  .color2 {background-color: #E8E8E8;}   /* YellowGreen B2B2E0 */
+  .color1 {background-color: #D8D8D8;}   /* SpringGreen*/
+  .color0 {background-color: #F8F8F8;}   /* Turquoise*/
+
+
+@media screen and (max-width: 768px) {
+.right1 {text-align: left;}
+}
+
+
diff --git a/ApacheCMDA_Frontend_1.0/public/html/js2/common.js b/ApacheCMDA_Frontend_1.0/public/html/js2/common.js
new file mode 100644
index 0000000..625de5f
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/html/js2/common.js
@@ -0,0 +1,548 @@
+// The following is for the code navigation purpose.
+//
+// enable_download data button
+// disable_pres1__
+// enable_pres1__(ID)
+// put_data__
+// data_block_str__
+// change_datan_
+// put_var__
+// is3D__
+// select_var__
+// time_range__
+// time_range2__
+// time_range3__
+// monthList__
+// fillMonth__
+// reset_months__
+// no_month_check
+// select_all_months__
+// select_months__
+// getMonthStr__
+// parse_pres__
+// get_querystring__
+
+var naValue = "-999999";
+
+// disable download data button
+function disable_download_button()
+{
+  var x=document.getElementById("download_data");
+  x.disabled=true;
+}
+
+// enable_download data button
+function enable_download_button()
+{
+  var x=document.getElementById("download_data");
+  x.disabled=false;
+}
+
+// disable pressure level box for 2D var
+// disable_pres1__
+function disable_pres1(ID)
+{
+  // if isPressure1 is defined, there is no pressure widget 
+  try {
+    var x;
+    x=document.getElementById("pres"+ID);
+    x.value = "N/A";
+    x.disabled=true;
+    var y=document.getElementById("pressureLabel"+ID);
+    y.innerHTML = "pressure:";
+  }
+  catch(err) {}
+}
+
+// enable pressure level box for 3D var
+// enable_pres1__(ID)
+function enable_pres1(ID)
+{
+  var var_string = $("#var"+ID).val();
+  var oceanStr = "";
+
+  try { 
+    var rangeStr0 = eval("rangeStr"+ID); 
+  } catch(err) { 
+    var rangeStr0 = "";
+  }
+
+  try {
+    var y=document.getElementById("pressureLabel"+ID);
+  //alert(y.value);
+    y.innerHTML = "atmospheric pressure " + rangeStr0 + "(hPa):";
+  } catch(err) {}
+
+  if (varList[var_string][1]==="ocean") {
+    oceanStr = "o";
+    try {
+    y.innerHTML = "ocean pressure " + rangeStr0 + "(dbar):";
+    } catch(err) {}
+  } 
+
+  // there can be no pressure widget, so there is a error catch here.
+  try {
+    var pressDf0;
+    try { 
+      pressDf0 = eval("pressDf"+ID+oceanStr); 
+    } catch(err) {
+      pressDf0 = "500";
+    }
+
+
+    var x=document.getElementById("pres"+ID);
+    x.value = pressDf0;
+    x.disabled=false;
+  } catch(err) {}
+/*  try {
+    if ( eval("typeof pressDf"+ID) !== 'undefined') var pressDf0 = eval("pressDf"+ID);
+    else var pressDf0 = "500";
+
+    var x=document.getElementById("pres"+ID);
+    x.value = pressDf0;
+    x.disabled=false;
+  } catch(err) {}
+*/
+}
+
+// put_data__
+function put_data(ID){
+  var list1=document.getElementById("data"+ID);
+
+  for(var key in dataList) {
+    if (key.slice(0,5)==="group") {
+      var og = document.createElement("OPTGROUP");
+      og.setAttribute('label', dataList[key][0]);
+      list1.add(og);
+
+    } else {
+      var toAdd = true;
+
+      // whether the dataset has only 2D or only 3D variables
+      if   ( (typeof isOnly2d !== 'undefined') 
+          || (typeof isOnly3d !== 'undefined') ) {
+        var dims = "";
+        var varList2 = dataList[key][1];  
+        for(var i=0; i<varList2.length; i++)   
+          dims += String(varList[ varList2[i] ][2]);
+
+        if (typeof isOnly2d !== 'undefined') 
+          if (dims.indexOf('2')==-1) toAdd = false;
+        if (typeof isOnly3d !== 'undefined') 
+          if (dims.indexOf('3')==-1) toAdd = false;
+      }
+  
+      // add to the option group
+      if (toAdd) og.appendChild(new Option(key,key));
+    }
+  }
+}
+
+// change_datan_
+function change_datan(numTB){
+  var nVar=Number( document.getElementById("nVar").value );
+  //alert(nVar);
+
+  for (var i=0; i<nVar; i++) {
+    var str1 = data_block_str(String(i+2), numTB, "Source Variable "+String(i+1), "", 500);
+    //alert(str1);
+    document.getElementById("datan"+(i+1)).innerHTML = str1;
+    put_data(i+2);
+    put_var(i+2);
+    select_var(i+2);
+  }
+  for (var i=nVar; i<10; i++) {
+    document.getElementById("datan"+(i+1)).innerHTML = "";
+  }
+}
+
+// data_block_str__
+function data_block_str(ID, numTB, dataTitle, isRange, pressDf){
+var temp1= '';
+temp1 += '<div class="row ">\n'
+temp1 += '<div class="col-sm-12 center1 subtitle1">\n';
+temp1 += dataTitle + '\n';
+temp1 += '</div>\n';
+temp1 += '</div> <!-- row --> \n';
+
+temp1 += '<div class="row">\n';
+temp1 += ' <div class="col-sm-4 right1">\n';
+temp1 += '   source:' + '\n';
+temp1 += '  </div> <!-- col-sm-6 -->\n';
+temp1 += '  <div class="col-sm-8 left1">\n';
+temp1 += '    <select name="data' + ID + '", id="data' + ID;
+temp1 += '" onchange="put_var(' + ID + '); select_var(' + ID + ');time_range' + numTB + '()"></select>\n';
+temp1 += '  </div> <!-- col-sm-6 level2-->\n';
+temp1 += '</div> <!-- row -->\n';
+
+temp1 += '<div class="row">\n';
+temp1 += '  <div class="col-sm-4 right1">\n';
+temp1 += '    variable name:\n';
+temp1 += '  </div> <!-- col-sm-6 level2-->\n';
+temp1 += '  <div class="col-sm-8 left1">\n';
+temp1 += '    <select name="var' + ID +'", id="var' + ID;
+temp1 += '" onchange="select_var(' + ID + '); select_var(' + ID + '); time_range' + numTB + '()"> </select>\n';
+temp1 += '  </div> <!-- col-sm-6 level2-->\n';
+temp1 += '</div> <!-- row -->\n';
+
+temp1 += '<div class="row">\n';
+temp1 += '  <div class="col-sm-4 right1" id="pressureLabel' + ID + '">\n';
+//temp1 += '    pressure ' + isRange + '(atmosphere hPa) <br> or (ocean dbar):\n';
+temp1 += '    pressure ' + isRange +':\n';
+temp1 += '  </div> <!-- col-sm-6 level2-->\n';
+temp1 += '  <div class="col-sm-8 left1">\n';
+temp1 += '    <input id="pres' + ID + '" value="' + pressDf + '" alt="pressure"/>\n';
+temp1 += '  </div> <!-- col-sm-6 level2-->\n';
+temp1 += '</div> <!-- row -->\n';
+// alert(temp1);
+return temp1;
+}
+
+// put_var__
+function put_var(ID) {
+  var list1=document.getElementById("var"+ID);
+  for (var i=list1.length-1; i>=0; i--) {
+  list1.remove(i);
+  }
+
+  data_string =  document.getElementById("data"+ID).value;
+  var varList2 = dataList[data_string][1];  
+
+  if (typeof isOnly2d !== 'undefined') {
+    // list only 2D variables
+    for (var i=0; i<varList2.length; i++) {
+      var k = varList2[i];
+      if (varList[k][2]==2) list1.add(new Option(varList[k][0],k));
+    }
+
+  } else if (typeof isOnly3d !== 'undefined') {
+    // list only 3D variables
+    for (var i=0; i<varList2.length; i++) {
+      var k = varList2[i];
+      if (varList[k][2]==3) list1.add(new Option(varList[k][0],k));
+    }
+
+  } else {
+    // list all 2D/3D variables
+    for (var i=0; i<varList2.length; i++) {
+      var k = varList2[i];
+      list1.add(new Option(varList[k][0],k));
+    }
+  }
+
+  var nVar = list1.options.length;
+  if (nVar==0) {
+    alert(data_string + " has no suitable variable.");
+    document.getElementById("data"+ID).options[0].selected = true; 
+    put_var(ID);
+  }
+}
+
+// is3D__
+function is3D(ID)
+{
+  var var_string = $("#var"+ID).val();
+  return varList[var_string][2]==3;
+}
+
+// select_var__
+function select_var(ID)
+{
+  // if there isOnly2d is defined, there is no pressure widget.
+  try {
+    //var var_string = $("#var"+ID).val();
+    //alert(is3D(ID));
+    if (is3D(ID)) {
+      enable_pres1(ID);
+    } else {
+      disable_pres1(ID);
+    }
+  }
+  catch(err) {}
+}
+
+
+// time_range__
+// this is identical to time_range1()
+function time_range() {
+  var var_string1 = $("#var"+1).val();
+  var data_string1 = $("#data"+1).val();
+
+  var sTime = dataList[data_string1][2][var_string1][0].toString();
+  var eTime = dataList[data_string1][2][var_string1][1].toString();
+
+  $("#startYear").html("start year-month: (earliest:" + sTime.slice(0,4) + "-" + sTime.slice(4,6) + ")");
+  $("#endYear").html("end year-month: (latest:" + eTime.slice(0,4) + "-" + eTime.slice(4,6) + ")");
+}
+
+function time_range1() {
+  var var_string1 = $("#var"+1).val();
+  var data_string1 = $("#data"+1).val();
+
+  var sTime = dataList[data_string1][2][var_string1][0].toString();
+  var eTime = dataList[data_string1][2][var_string1][1].toString();
+
+  $("#startYear").html("start year-month: (earliest:" + sTime.slice(0,4) + "-" + sTime.slice(4,6) + ")");
+  $("#endYear").html("end year-month: (latest:" + eTime.slice(0,4) + "-" + eTime.slice(4,6) + ")");
+}
+
+// time_range2__
+function time_range2() {
+  var var_string1 = $("#var"+1).val();
+  var var_string2 = $("#var"+2).val();
+  var data_string1 = $("#data"+1).val();
+  var data_string2 = $("#data"+2).val();
+  //alert(data_string1);
+  //alert(data_string2);
+  
+
+  var sTime = Math.max( Number(dataList[data_string1][2][var_string1][0]),
+                        Number(dataList[data_string2][2][var_string2][0]) ).toString();
+  var eTime = Math.min( Number(dataList[data_string1][2][var_string1][1]),
+                        Number(dataList[data_string2][2][var_string2][1]) ).toString();
+
+  //sTime = sTime.toString();
+  //eTime = eTime.toString();
+
+  $("#startYear").html("start year-month: (earliest:" + sTime.slice(0,4) + "-" + sTime.slice(4,6) + ")");
+  $("#endYear").html("end year-month: (latest:" + eTime.slice(0,4) + "-" + eTime.slice(4,6) + ")");
+}
+
+// time_range3__
+function time_range3() {
+  var var_string1 = $("#var"+1).val();
+  var var_string2 = $("#var"+2).val();
+  var var_string3 = $("#var"+3).val();
+  var data_string1 = $("#data"+1).val();
+  var data_string2 = $("#data"+2).val();
+  var data_string3 = $("#data"+3).val();
+
+  var sTime = Math.max( 
+        Number(dataList[data_string1][2][var_string1][0]),
+        Number(dataList[data_string2][2][var_string2][0]),
+        Number(dataList[data_string3][2][var_string3][0]) 
+        ).toString();
+  var eTime = Math.min(
+        Number(dataList[data_string1][2][var_string1][1]),
+        Number(dataList[data_string2][2][var_string2][1]),
+        Number(dataList[data_string3][2][var_string3][1]) 
+        ).toString();
+
+  $("#startYear").html("start year-month: (earliest:" + sTime.slice(0,4) + "-" + sTime.slice(4,6) + ")");
+  $("#endYear").html("end year-month: (latest:" + eTime.slice(0,4) + "-" + eTime.slice(4,6) + ")");
+}
+
+// monthList__
+var monthList = [
+"Jan",
+"Feb",
+"Mar",
+"Apr",
+"May",
+"Jun",
+"Jul",
+"Aug",
+"Sep",
+"Oct",
+"Nov",
+"Dec",
+];
+
+// fillMonth__
+function fillMonth() { 
+  var temp1 = 'select months:\
+<select name="months" id="months" onchange="select_months()">\
+<option id="all">select all</option>\
+<option id="none">select none</option>\
+<option id="summer">Summer:Jun-Jul-Aug</option>\
+<option id="autum">Autumn:Sep-Oct-Nov</option>\
+<option id="winter">Winter:Dec-Jan-Feb</option>\
+<option id="spring">Spring:Mar-Apr-May</option> </select>';
+  $("#monthSelect0").html(temp1); 
+
+  temp1 = '<form role="form">'; 
+  for (var i=0; i<monthList.length; i++) {
+    temp1 +=
+        '<label"><input type="checkbox" id="' + monthList[i] + '" value="' + monthList[i] + '" checked/></label>' 
+          +  monthList[i] + " ";
+  }
+  temp1 += '</form>';
+  $("#monthSelect").html(temp1); 
+}
+
+// unselect all months in the checkboxes
+// reset_months__
+function reset_months()
+{
+  for (var i=0; i<monthList.length; i++) {
+    document.getElementById(monthList[i]).checked = false;
+  }
+}
+
+// see if no month is selected
+// no_month_check
+function no_month_check()
+{
+  var nonChecked = true;
+  for (var i=0; i<monthList.length; i++) {
+    if (document.getElementById(monthList[i]).checked == true) {
+      nonChecked = false;
+    }
+  }
+  return nonChecked;
+}
+
+// select all months in the checkboxes
+// select_all_months__
+function select_all_months()
+{
+  for (var i=0; i<monthList.length; i++) {
+    document.getElementById(monthList[i]).checked = true;
+  }
+}
+
+// select checkboxes based on "months" dropdown
+// select_months__
+function select_months()
+{
+  var s1=document.getElementById("months");
+  // alert(s1.selectedIndex);
+  // alert(s1.options[s1.selectedIndex].value);
+
+  // disable the download button because of this change
+  disable_download_button();
+
+  // "select none"
+  if (s1.selectedIndex == 1) {
+    reset_months();
+  }
+  // "select all"
+  if (s1.selectedIndex == 0) {
+    select_all_months();
+  }
+  // "summer"
+  if (s1.selectedIndex == 2) {
+    reset_months();
+    document.getElementById('Jun').checked = true;
+    document.getElementById('Jul').checked = true;
+    document.getElementById('Aug').checked = true;
+  }
+  // "autumn"
+  if (s1.selectedIndex == 3) {
+    reset_months();
+    document.getElementById('Sep').checked = true;
+    document.getElementById('Oct').checked = true;
+    document.getElementById('Nov').checked = true;
+  }
+  // "winter"
+  if (s1.selectedIndex == 4) {
+    reset_months();
+    document.getElementById('Dec').checked = true;
+    document.getElementById('Jan').checked = true;
+    document.getElementById('Feb').checked = true;
+  }
+  // "spring"
+  if (s1.selectedIndex == 5) {
+    reset_months();
+    document.getElementById('Mar').checked = true;
+    document.getElementById('Apr').checked = true;
+    document.getElementById('May').checked = true;
+  }
+
+}
+
+// getMonthStr__
+function getMonthStr() {
+        // get months checked by client
+        var month_str = "";
+        for (var i=0; i<monthList.length; i++) {
+          var mm = document.getElementById(monthList[i]);
+          if (mm.checked == true) {
+            month_str += ","+(i+1);
+          }
+        }
+        month_str = month_str.substr(1);
+        return month_str;
+}
+
+// parse_pres__
+function parse_pres(pres10) {
+  var pres1 = "";
+
+  if (pres10=="") {pres1 = naValue; }
+  else {
+    if (!(isNaN(Number(pres10)))) { 
+      pres1 = pres10; 
+    } else {
+      var checkNan = 0;
+      var pres2 = [];
+      var temp1=pres10.split(",");
+      //for (var i in temp1) {
+      for (var i=0; i<temp1.length; i++) {
+        if (isNaN(Number(temp1[i]))) {
+          checkNan = 1; 
+        } else {
+          pres2.push(Number(temp1[i]));
+        }
+      }
+      if (pres2.length>0) { pres1 = pres2.join(); }
+      else { pres1 = naValue; }
+    }
+  }
+  return pres1;
+}
+
+// get_querystring__
+function get_querystring() {
+  var queries = {};
+  $.each(document.location.search.substr(1).split('&'),function(c,q){
+    var i = q.split('=');
+    queries[i[0].toString()] = i[1].toString();
+  });
+  return queries;
+}
+
+
+function showUrl(inputs) {
+  var v1q, v2, key0, key1;
+  try {
+    var temp1 = "";
+    //for (var i=0; i<inputs.length; i++) {
+    for (key0 in inputs) {
+      if (!inputs.hasOwnProperty(key0)) { continue; }
+      if ( key0 == "Image" || key0 == "data_url" ) { continue; }
+      key1 = inputs[key0];
+
+      try {
+        v1q = $("#"+key1).val();
+
+        if ( key0.slice(0,5) == "model" ) {
+          v1q = v1q.replace("/", "_");
+        }
+
+        if ( key0 == "startT" || key0 == "endT" ) {
+          v1q = v1q.replace("-", "");
+        }
+
+        if ( key0 == "purpose" ) {
+          v1q = escape(v1q);
+        }
+
+/*
+        if ( key0.slice(0,4) == "pres" ) {
+          if ( Number(v1q) == NaN ) {
+            v1q = "-999999";
+          }
+        }
+*/
+        temp1 += key0 + "=" + v1q + "&";
+      } catch(err) {}
+    }
+    temp1 = temp1.slice(0,-1);
+  } catch(err) { 
+    var temp1 = document.location.search.substr(1);
+  }
+
+  document.getElementById("actionUrl").innerHTML = document.location.href.split('?')[0] + "?" + temp1;
+}
+
diff --git a/ApacheCMDA_Frontend_1.0/public/html/js2/dataList2.js b/ApacheCMDA_Frontend_1.0/public/html/js2/dataList2.js
new file mode 100644
index 0000000..8cf0903
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/html/js2/dataList2.js
@@ -0,0 +1,42 @@
+// modelName: [category, listOfVar],
+var groupList={
+"group1":         ["Model: Historical"],
+"group2":         ["Model: AMIP"],
+"group3":         ["Observation"],
+"group4":         ["Reanalysis"],
+};
+
+var dataList={
+"group1":         ["Model: Historical"],
+"GFDL/ESM2G":     ["Model: Historical",      ["pr", "clt", "ts", "tos", "uas", "vas", "sfcWind", "zos", "lai", "rlds", "rlus", "rldscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "ta", "hus", "cli", "clw", "wap", "hur", ], {'uas': [199101, 200512, '.'], 'rlutcs': [199601, 200512, '.'], 'rsutcs': [199101, 200512, '.'], 'rldscs': [199601, 200512, '.'], 'pr': [199601, 200512, '.'], 'rlut': [199601, 200512, '.'], 'cli': [199101, 200512, 'regridded'], 'rlus': [199601, 200512, '.'], 'tos': [199601, 200512, 'regridded'], 'ts': [199601, 200512, '.'], 'zos': [199601, 200512, 'regridded'], 'clt': [199601, 200512, '.'], 'vas': [199101, 200512, '.'], 'clw': [199101, 200512, 'regridded'], 'ta': [199101, 200512, '.'], 'lai': [199601, 200512, '.'], 'rsdt': [199101, 200512, '.'], 'hur': [198601, 200512, '.'], 'hus': [199101, 200512, '.'], 'sfcWind': [199101, 200512, '.'], 'rlds': [199601, 200512, '.'], 'wap': [198601, 200512, '.'], 'rsut': [199101, 200512, '.']} ],       
+"GISS/E2-H":      ["Model: Historical",      ["pr", "clt", "ts", "tos", "uas", "vas", "sfcWind", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", "wap", "hur", ], {'hur': [195101, 200512, '.'], 'rsuscs': [199501, 200512, '.'], 'rsdscs': [199501, 200512, '.'], 'rlutcs': [199501, 200512, '.'], 'rsutcs': [199501, 200512, '.'], 'rldscs': [199501, 200512, '.'], 'pr': [199501, 200512, '.'], 'rlut': [199501, 200512, '.'], 'cli': [195101, 200512, 'regridded'], 'rlus': [199501, 200512, '.'], 'rlds': [199501, 200512, '.'], 'ts': [199501, 200512, '.'], 'clt': [199501, 200512, '.'], 'vas': [199501, 200512, '.'], 'clw': [195101, 200512, 'regridded'], 'uas': [199501, 200512, '.'], 'wap': [195101, 200512, '.'], 'rsdt': [199501, 200512, '.'], 'rsds': [199501, 200512, '.'], 'prw': [199501, 200512, '.'], 'sfcWind': [199501, 200512, '.'], 'tos': [199501, 200512, '.'], 'rsus': [199501, 200512, '.'], 'rsut': [199501, 200512, '.']} ],      
+"GISS/E2-R":      ["Model: Historical",      ["pr", "clt", "ts", "tos", "uas", "vas", "sfcWind", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", "wap", "hur", ], {'hur': [195101, 200512, '.'], 'rsuscs': [199501, 200012, '.'], 'uas': [199501, 200512, '.'], 'rlutcs': [199501, 200012, '.'], 'rsutcs': [199501, 200012, '.'], 'rldscs': [199501, 200012, '.'], 'pr': [199501, 200512, '.'], 'rlut': [199501, 200012, '.'], 'cli': [200101, 200512, 'regridded'], 'rlus': [199501, 200012, '.'], 'tos': [199501, 200512, '.'], 'ts': [199501, 200512, '.'], 'clt': [199501, 200512, '.'], 'vas': [199501, 200512, '.'], 'clw': [199501, 200512, 'regridded'], 'rsdscs': [199501, 200012, '.'], 'wap': [195101, 200512, '.'], 'rsdt': [199501, 200012, '.'], 'rsds': [199501, 200012, '.'], 'sfcWind': [199501, 200512, '.'], 'rsus': [199501, 200012, '.'], 'rsut': [199501, 200012, '.']} ],      
+"NCAR/CAM5":      ["Model: Historical",      ["pr", "clt", "ts", "tos", "sfcWind", "zos", "lai", "rlds", "rsds", "rlus", "rsus", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", "wap", "hur", ], {'hur': [195001, 200512, '.'], 'rsuscs': [199501, 200512, '.'], 'rsdscs': [199501, 200512, '.'], 'rlutcs': [199501, 200512, '.'], 'rsutcs': [199501, 200512, '.'], 'pr': [199501, 200512, '.'], 'rlut': [199501, 200512, '.'], 'cli': [197501, 200512, 'regridded'], 'rlus': [199501, 200512, '.'], 'tos': [199501, 200512, 'regridded'], 'ts': [199501, 200512, '.'], 'zos': [199501, 200512, 'regridded'], 'clt': [199501, 200512, '.'], 'clw': [197501, 200512, 'regridded'], 'wap': [195001, 200512, '.'], 'lai': [199501, 200512, '.'], 'rsdt': [199501, 200512, '.'], 'rsds': [199501, 200512, '.'], 'sfcWind': [199501, 200512, '.'], 'rlds': [199501, 200512, '.'], 'rsus': [199501, 200512, '.'], 'rsut': [199501, 200512, '.']} ],      
+"NCC/NORESM":     ["Model: Historical",      ["pr", "clt", "ts", "tos", "uas", "vas", "zos", "lai", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "ta", "hus", "cli", "clw", "wap", "hur", ], {'hur': [195001, 200512, '.'], 'rsuscs': [199501, 200512, '.'], 'rsdscs': [199501, 200512, '.'], 'rlutcs': [199501, 200512, '.'], 'rsutcs': [199501, 200512, '.'], 'rldscs': [199501, 200512, '.'], 'pr': [199501, 200512, '.'], 'rlut': [199501, 200512, '.'], 'cli': [195001, 200512, 'regridded'], 'rlus': [199501, 200512, '.'], 'tos': [199501, 200512, 'regridded'], 'ts': [199501, 200512, '.'], 'zos': [199501, 200512, 'regridded'], 'clt': [199501, 200512, '.'], 'vas': [199501, 200512, '.'], 'clw': [195001, 200512, 'regridded'], 'uas': [199501, 200512, '.'], 'wap': [195001, 200512, '.'], 'lai': [199501, 200512, '.'], 'rsdt': [199501, 200512, '.'], 'rsds': [199501, 200512, '.'], 'hus': [199501, 200512, '.'], 'rlds': [199501, 200512, '.'], 'rsus': [199501, 200512, '.'], 'ta': [199501, 200512, '.'], 'rsut': [199501, 200512, '.']} ],       
+"UKMO/HadGEM2-ES":["Model: Historical",      ["pr", "clt", "ts", "uas", "vas", "sfcWind", "zos", "lai", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", "wap", "hur", ], {'rsds': [199501, 200511, '.'], 'rsuscs': [199501, 200511, '.'], 'rsdscs': [199501, 200511, '.'], 'rlutcs': [199501, 200511, '.'], 'rsutcs': [199501, 200511, '.'], 'rldscs': [199501, 200511, '.'], 'pr': [199501, 200511, '.'], 'rlut': [199501, 200511, '.'], 'cli': [198412, 200511, 'regridded'], 'rlus': [199501, 200511, '.'], 'rlds': [199501, 200511, '.'], 'ts': [199501, 200511, '.'], 'zos': [195912, 200512, '.'], 'clt': [199501, 200511, '.'], 'vas': [199501, 200511, '.'], 'clw': [198412, 200511, 'regridded'], 'uas': [199501, 200511, '.'], 'wap': [198412, 200511, '.'], 'lai': [199501, 200511, '.'], 'rsdt': [199501, 200511, '.'], 'hur': [198412, 200511, '.'], 'sfcWind': [199501, 200511, '.'], 'rsus': [199501, 200511, '.'], 'rsut': [199501, 200511, '.']} ],            
+//                [ 
+"group2":         ["Model: AMIP"],
+"CCCMA/CANAM4":   ["Model: AMIP",            ["pr", "clt", "ts", "tas", "hurs", "uas", "vas", "sfcWind", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "ta", "hus", "cli", "clw", "wap", "hur", ], {'va': [195001, 200912, '.'], 'ci': [195001, 200912, '.'], 'sci': [195001, 200912, '.'], 'rsds': [195001, 200912, '.'], 'prc': [195001, 200912, '.'], 'cl': [195001, 200912, '.'], 'rsuscs': [195001, 200912, '.'], 'uas': [195001, 200912, '.'], 'huss': [195001, 200912, '.'], 'hfss': [195001, 200912, '.'], 'rlutcs': [195001, 200912, '.'], 'evspsbl': [195001, 200912, '.'], 'prsn': [195001, 200912, '.'], 'rldscs': [195001, 200912, '.'], 'ccb': [195001, 200912, '.'], 'pr': [195001, 200912, '.'], 'ps': [195001, 200912, '.'], 'cli': [195001, 200912, 'regridded'], 'rlus': [195001, 200912, '.'], 'rlds': [195001, 200912, '.'], 'tas': [195001, 200912, '.'], 'ts': [195001, 200912, '.'], 'prw': [195001, 200912, '.'], 'clt': [195001, 200912, '.'], 'vas': [195001, 200912, '.'], 'clw': [195001, 200912, 'regridded'], 'rsdscs': [195001, 200912, '.'], 'wap': [195001, 200912, '.'], 'zg': [195001, 200912, '.'], 'tasmin': [195001, 200912, '.'], 'psl': [195001, 200912, '.'], 'rlut': [195001, 200912, '.'], 'hurs': [195001, 200912, '.'], 'rsdt': [195001, 200912, '.'], 'hur': [195001, 200912, '.'], 'hus': [195001, 200912, '.'], 'sbl': [195001, 200912, '.'], 'clivi': [195001, 200912, '.'], 'mc': [195001, 200912, '.'], 'rtmt': [195001, 200912, '.'], 'cct': [195001, 200912, '.'], 'rsutcs': [195001, 200912, '.'], 'sfcWind': [195001, 200912, '.'], 'tauv': [195001, 200912, '.'], 'clwvi': [195001, 200912, '.'], 'tauu': [195001, 200912, '.'], 'tasmax': [195001, 200912, '.'], 'rsus': [195001, 200912, '.'], 'ta': [195001, 200912, '.'], 'ua': [195001, 200912, '.'], 'rsut': [195001, 200912, '.'], 'hfls': [195001, 200912, '.']} ],         
+"CSIRO/MK3.6":    ["Model: AMIP",            ["pr", "clt", "ts", "uas", "vas", "sfcWind", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", "wap", "hur", ], {'rsds': [197901, 200912, '.'], 'rsuscs': [197901, 200912, '.'], 'uas': [197901, 200912, '.'], 'rlutcs': [197901, 200912, '.'], 'rsutcs': [197901, 200912, '.'], 'rldscs': [197901, 200912, '.'], 'pr': [199501, 200912, '.'], 'rlut': [197901, 200912, '.'], 'cli': [197901, 200912, 'regridded'], 'rlus': [197901, 200912, '.'], 'rlds': [197901, 200912, '.'], 'ts': [199501, 200912, '.'], 'clt': [199501, 200912, '.'], 'vas': [197901, 200912, '.'], 'clw': [199901, 200912, 'regridded'], 'rsdscs': [197901, 200912, '.'], 'wap': [199001, 200512, '.'], 'rsdt': [197901, 200912, '.'], 'hur': [199001, 200512, '.'], 'sfcWind': [197901, 200912, '.'], 'rsus': [197901, 200912, '.'], 'rsut': [197901, 200912, '.']} ],        
+"GFDL/CM3":       ["Model: AMIP",            ["pr", "clt", "ts", "uas", "vas", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "ta", "hus", "cli", "clw", "wap", "hur", ], {'hur': [198901, 200812, '.'], 'rsuscs': [199401, 200812, '.'], 'rsdscs': [199401, 200812, '.'], 'rlutcs': [199401, 200812, '.'], 'rsutcs': [199401, 200812, '.'], 'rldscs': [199401, 200812, '.'], 'pr': [199401, 200812, '.'], 'rlut': [199401, 200812, '.'], 'cli': [197901, 200812, 'regridded'], 'rlus': [199401, 200812, '.'], 'rlds': [199401, 200812, '.'], 'ts': [199401, 200812, '.'], 'clt': [199401, 200812, '.'], 'vas': [199401, 200812, '.'], 'clw': [197901, 200812, 'regridded'], 'uas': [199401, 200812, '.'], 'wap': [198901, 200812, '.'], 'rsdt': [199401, 200812, '.'], 'rsds': [199401, 200812, '.'], 'hus': [198901, 200812, '.'], 'rsus': [199401, 200812, '.'], 'ta': [198901, 200812, '.'], 'rsut': [199401, 200812, '.']} ],     
+"IPSL/CM5A-LR":   ["Model: AMIP",            ["pr", "clt", "ts", "lai", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", ], {'pr': [199501, 200912, '.'], 'rlut': [199501, 200912, '.'], 'rsus': [199501, 200912, '.'], 'cli': [197901, 200912, 'regridded'], 'rlus': [199501, 200912, '.'], 'rsdt': [199501, 200912, '.'], 'rlds': [199501, 200912, '.'], 'rsdscs': [199501, 200912, '.'], 'ts': [199501, 200912, '.'], 'rlutcs': [199501, 200912, '.'], 'rsutcs': [199501, 200912, '.'], 'rsds': [199501, 200912, '.'], 'rsuscs': [199501, 200912, '.'], 'clt': [199501, 200912, '.'], 'rldscs': [199501, 200912, '.'], 'clw': [197901, 200912, 'regridded'], 'rsut': [199501, 200912, '.'], 'lai': [199501, 200912, '.']} ],         
+"MIROC/MIROC5":   ["Model: AMIP",            ["pr", "clt", "ts", "uas", "vas", "sfcWind", "lai", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", "wap", "hur", ], {'hur': [192001, 200912, '.'], 'rsuscs': [197901, 200812, '.'], 'rsdscs': [197901, 200812, '.'], 'rlutcs': [197901, 200812, '.'], 'rsutcs': [197901, 200812, '.'], 'rldscs': [197901, 200812, '.'], 'pr': [197901, 200812, '.'], 'rlut': [197901, 200812, '.'], 'cli': [197901, 199812, 'regridded'], 'rlus': [197901, 200812, '.'], 'rlds': [197901, 200812, '.'], 'ts': [197901, 200812, '.'], 'clt': [197901, 200812, '.'], 'vas': [197901, 200812, '.'], 'clw': [197901, 200812, 'regridded'], 'uas': [197901, 200812, '.'], 'wap': [192001, 200912, '.'], 'lai': [197901, 200812, '.'], 'rsdt': [197901, 200812, '.'], 'rsds': [197901, 200812, '.'], 'sfcWind': [197901, 200812, '.'], 'rsus': [197901, 200812, '.'], 'rsut': [197901, 200812, '.']} ],         
+"UKMO/HadGEM2-A": ["Model: AMIP",            ["pr", "clt", "ts", "hurs", "uas", "vas", "sfcWind", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", "wap", "hur", ], {'hur': [197809, 200812, '.'], 'rsuscs': [199501, 200812, '.'], 'rsdscs': [199501, 200812, '.'], 'rlutcs': [199501, 200812, '.'], 'rsutcs': [199501, 200812, '.'], 'rldscs': [199501, 200812, '.'], 'pr': [199501, 200812, '.'], 'rlut': [199501, 200812, '.'], 'cli': [197809, 200811, 'regridded'], 'rlus': [200812, 200812, '.'], 'rlds': [199501, 200812, '.'], 'ts': [199501, 200812, '.'], 'clt': [199501, 200812, '.'], 'vas': [199501, 200812, '.'], 'clw': [197809, 200811, 'regridded'], 'uas': [199501, 200812, '.'], 'wap': [197809, 200812, '.'], 'hurs': [197809, 200812, '.'], 'rsdt': [199501, 200812, '.'], 'rsds': [199501, 200812, '.'], 'sfcWind': [199501, 200812, '.'], 'rsus': [199501, 200812, '.'], 'rsut': [199501, 200812, '.']} ],           
+//                [ 
+"group3":         ["Observation"],
+"NASA/GRACE":     ["Observation", ["zl", "zo", ], {'zl': [200302, 201112, '.'], 'zo': [200302, 201112, '.']} ],            
+"NASA/MODIS":     ["Observation", ["clt", "lai", ], {'cltNobs': [200003, 201109, '.'], 'clt': [200003, 201109, '.'], 'lai': [200002, 200912, '.'], 'cltStddev': [200003, 201109, '.']} ],
+"NASA/AMSRE":     ["Observation", ["tos", ], {'tosNobs': [200206, 201012, '.'], 'tos': [200206, 201012, '.'], 'tosStderr': [200206, 201012, '.']} ],
+"NASA/TRMM":      ["Observation", ["pr", ], {'pr': [199801, 201312, '.']} ],
+"NASA/GPCP":      ["Observation", ["pr", ], {'pr': [197901, 201106, '.']} ],
+"NASA/QuikSCAT":  ["Observation", ["uas", "vas", "sfcWind", ], {'uasNobs': [199908, 200910, '.'], 'vasStderr': [199908, 200910, '.'], 'uas': [199908, 200910, '.'], 'uasStderr': [199908, 200910, '.'], 'vas': [199908, 200910, '.'], 'sfcWind': [199908, 200910, '.'], 'sfcWindStderr': [199908, 200910, '.'], 'sfcWindNobs': [199908, 200910, '.'], 'vasNobs': [199908, 200910, '.']} ],
+"NASA/AVISO":     ["Observation", ["zos", ], {'zos': [199210, 201012, '.']} ],
+"NOAA/NODC":      ["Observation", ["ohc700", "ohc2000", ], {'ohc2000': [200501, 201212, '.'], 'ohc700': [195501, 201212, '.']} ],
+"NASA/CERES":     ["Observation", ["rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", ], {'rlut': [200003, 201206, '.'], 'rlus': [200003, 201002, '.'], 'rlutcs': [200003, 201206, '.'], 'rsuscs': [200003, 201002, '.'], 'rsdscs': [200003, 201002, '.'], 'rsdt': [200003, 201206, '.'], 'rsutcs': [200003, 201206, '.'], 'rsds': [200003, 201002, '.'], 'rlds': [200003, 201002, '.'], 'rldscs': [200003, 201002, '.'], 'rsus': [200003, 201002, '.'], 'rsut': [200003, 201206, '.']} ],
+"NASA/AIRS":      ["Observation", ["tas", "ta", "hus", ], {'hus': [200209, 201105, '.'], 'tas': [200209, 201112, '.'], 'ta': [200209, 201105, '.']} ],
+"NASA/MLS":       ["Observation", ["ta", "hus", ], {'hus': [200408, 201012, '.'], 'ta': [200408, 201012, '.']} ],
+"ARGO/ARGO":      ["Observation", ["ot", "os", ], {'ot': [200101, 201305, '.'], 'os': [200101, 201305, '.']} ],           
+//
+"group4":         ["Reanalysis"],
+"ECMWF/interim":  ["Reanalysis",  ["clt", "tos", "uas", "vas", "sfcWind", "wap", "hur", ], {'tos': [197901, 201402, '.'], 'uas': [199501, 201412, '.'], 'sfcWind': [199501, 201412, '.'], 'hur': [197901, 201402, '.'], 'clt': [197901, 201404, '.'], 'vas': [199501, 201412, '.'], 'wap': [197901, 201402, '.']} ], 
+};
diff --git a/ApacheCMDA_Frontend_1.0/public/html/js2/varList.js b/ApacheCMDA_Frontend_1.0/public/html/js2/varList.js
new file mode 100644
index 0000000..51cb5ad
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/html/js2/varList.js
@@ -0,0 +1,40 @@
+// shortName: [longName, groupName, dimension, units],
+var varList = {
+"pr":       ["Precipitation Flux",                                "", 2, "kg  m-2 s-1"],    
+"clt":      ["Total Cloud Fraction",                              "", 2, "%"],      
+"ts":       ["Surface Temperature",                               "", 2, "K"],     
+"lst_day":  ["Daytime Land Surface Temperature",                  "", 2, "K"],   
+"lst_night":["Nighttime Land Surface Temperature",                "", 2, "K"],   
+"tas":      ["Near-Surface Air Temperature",                      "", 2, "K"],   
+"hurs":     ["Near-Surface Relative Humidity",                    "", 2, "%"],   
+"tos":      ["Sea Surface Temperature",                           "", 2, "K"],         
+"uas":      ["Eastward Near-Surface Wind",                        "", 2, "m s-1"],            
+"vas":      ["Northward Near-Surface Wind",                       "", 2, "m s-1"],             
+"sfcWind":  ["Near-Surface Wind Speed",                           "", 2, "m s-1"],         
+"zos":      ["Sea Surface Height",                                "", 2, "m"],    
+"lai":      ["Leaf Area Index",                                   "", 2, "1"], 
+"zl":       ["Equivalent Water Height Over Land",                 "", 2, "cm"],                   
+"zo":       ["Equivalent Water Height Over Ocean",                "", 2, "cm"],                    
+"ohc700":   ["Ocean Heat Content Anomaly within 700 m Depth",     "", 2, "1e18 joules"],                
+"ohc2000":  ["Ocean Heat Content Anomaly within 2000 m Depth",    "", 2, "1e18 joules"],                
+"rlds":     ["Surface Downwelling Longwave Radiation",            "", 2, "W m-2"],                        
+"rsds":     ["Surface Downwelling Shortwave Radiation",           "", 2, "W m-2"],                         
+"rlus":     ["Surface Upwelling Longwave Radiation",              "", 2, "W m-2"],                      
+"rsus":     ["Surface Upwelling Shortwave Radiation",             "", 2, "W m-2"],                       
+"rldscs":   ["Surface Downwelling Clear-Sky Longwave Radiation",  "", 2, "W m-2"],             
+"rsdscs":   ["Surface Downwelling Clear-Sky Shortwave Radiation", "", 2, "W m-2"],                   
+"rsuscs":   ["Surface Upwelling Clear-Sky Shortwave Radiation",   "", 2, "W m-2"],                
+"rsdt":     ["TOA Incident Shortwave Radiation",                  "", 2, "W m-2"],                  
+"rlut":     ["TOA Outgoing Longwave Radiation",                   "", 2, "W m-2"],                 
+"rsut":     ["TOA Outgoing Shortwave Radiation",                  "", 2, "W m-2"],                  
+"rlutcs":   ["TOA Outgoing Clear-Sky Longwave Radiation",         "", 2, "W m-2"],       
+"rsutcs":   ["TOA Outgoing Clear-Sky Shortwave Radiation",        "", 2, "W m-2"],              
+"ta":       ["Air Temperature",                                   "", 3, "K"], 
+"hus":      ["Specific Humidity",                                 "", 3, "1"],   
+"cli":      ["Cloud Ice Water Content",                           "", 3, "1"],         
+"clw":      ["Cloud Liquid Water Content",                        "", 3, "1"],            
+"ot":       ["Ocean Temperature",                            "ocean", 3, "K"],   
+"os":       ["Ocean Salinity",                               "ocean", 3, "psu"],
+"wap":      ["Vertical Wind Velocity",                            "", 3, "m s-1"],        
+"hur":      ["Relative Humidity",                                 "", 3, "%"],   
+};
diff --git a/ApacheCMDA_Frontend_1.0/public/html/serviceScatterPlot2Vars.html b/ApacheCMDA_Frontend_1.0/public/html/serviceScatterPlot2Vars.html
new file mode 100644
index 0000000..6e96fd2
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/html/serviceScatterPlot2Vars.html
@@ -0,0 +1,672 @@
+<!DOCTYPE html>
+
+
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+
+  <!-- for Bootstrap -->
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
+  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
+  <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
+
+  <!-- still needed? -->
+<!--
+  <script src="js/jquery.flot.min.js"></script>
+  <script src="js/json2.js"></script>
+  <script src="js/xmisc.js"></script>
+-->
+
+  <!-- cmac related -->
+  <link rel="stylesheet" href="js2/common.css">
+  <script src='js2/dataList2.js'></script>
+  <script src='js2/varList.js'></script>
+  <script src='js2/common.js'></script>
+
+  <title>Scatter and Histogram Plot</title>
+
+  <script>
+
+    var Response = null;
+    var variable1 = "";
+    var variable2 = "";
+
+    var naValue = "-999999";
+    var defaultDataIndex = 0; // not used yet
+
+    
+    var isPressure1 = true;
+    
+
+    
+    var pressDf1 = "500";
+    
+    
+
+    
+
+    
+
+    
+      var inputs = {
+'model1':'data1',
+'var1':'var1',
+'pres1':'pres1',
+'model2':'data2',
+'var2':'var2',
+'pres2':'pres2',
+'lon1':'lon0',
+'lon2':'lon1',
+'lat1':'lat0',
+'lat2':'lat1',
+'nSample':'nSample',
+'startT':'t0',
+'endT':'t1',
+'purpose':'purpose',
+'Image':'Image',
+'data_url':'data_url',
+};
+    
+
+    // called on load or reload
+    window.onload = function() {
+      
+      put_data(1);
+      put_data(2);
+      put_var(1);
+      put_var(2);
+      select_var(1);
+      select_var(2);
+      time_range2();
+      //fillMonth();
+
+      disable_download_button();
+
+      
+
+      // parse querystring
+      
+      queries = get_querystring();
+      //for (var key in queries) { if (queries.hasOwnProperty(key)) { alert(key + " -> " + queries[key]); } }
+
+      var key0, key1, v1q;
+
+      for (key0 in queries) {
+        if (!queries.hasOwnProperty(key0)) { continue; }
+        if (!inputs.hasOwnProperty(key0)) { continue; }
+        key1 = inputs[key0];
+        v1q = queries[key0];
+        v1q = v1q.trim();
+
+        if ( key0.slice(0,5) == "model" ) {
+          v1q = v1q.replace("_", "/");
+        }
+
+        if ( key0 == "startT" || key0 == "endT" ) {
+          v1q = v1q.slice(0,4) + "-" + v1q.slice(4,6);
+        }
+
+        if ( key0 == "purpose" ) {
+          v1q = unescape(v1q);
+        }
+
+        try {
+          if ( key1 == "Image" ) {
+            $("#"+key1).html( "<img src='" + v1q + "' width='820'/>" );
+          } else if ( key1 == "data_url" ) {
+            $("#"+key1).val( v1q );
+            enable_download_button();
+          } else {
+            $("#"+key1).val(v1q);
+            $("#"+key1).change();
+          }
+        } catch(err) {}
+      }
+
+/*
+      key1 = "var1";
+      if (queries.hasOwnProperty(key1)) {
+        v1q = queries[key1];
+        v1q = v1q.trim();
+        $("#var1").val(v1q);
+        $("#var1").change();
+      }
+         
+      key1 = "pres1";
+      if (queries.hasOwnProperty(key1)) {
+        v1q = queries[key1];
+        v1q = v1q.trim();
+        $("#pres1").val(v1q);
+      }
+*/
+       
+    }
+
+    $(document).ready(function(){
+      
+
+      $("a").click(function(event){
+        alert("As you can see, the link no longer took you to jquery.com");
+        event.preventDefault();
+      });
+
+
+      $("#download_data").click(function(event) {
+        var durl = $("#data_url").val();
+        // alert(durl);
+        window.location.assign(durl);
+      });
+
+
+      $("#action1").click(function(event) {
+        showUrl(inputs);
+        Response = null;
+
+        // no data to download yet
+        disable_download_button();
+
+        $("#Response").html("Calculating ...");
+        $("#data_url").html("Calculating ...");
+        $("#Image").html("");
+
+        // sample url: http://cmacws.jpl.nasa.gov:8090/svc/scatterPlot2V?model1=ukmo_hadgem2-a&var1=ts&pres1=200&model2=ukmo_hadgem2-a&var2=clt&pres2=200&start_time=199001&end_time=199512&lon1=0&lon2=100&lat1=-29&lat2=29
+        // form url string
+        // var url = "http://cmacws.jpl.nasa.gov:8090/svc/scatterPlot2V?";
+        var url = "http://" + window.location.hostname + ":9002/svc/newScatterPlot2V?";
+        // alert("url: " + url);
+
+        var d1 = $("#data1").val();
+        var model1 = d1.replace("/", "_");
+        model1 = model1.toLowerCase();
+
+        var arglist = "";
+        arglist = arglist.concat("model1=");
+        arglist = arglist.concat(model1);
+
+        // alert("arglist: " + arglist);
+
+        var variable1 = $("#var1").val();
+        arglist = arglist.concat("&var1=");
+        arglist = arglist.concat(variable1);
+
+        // alert("arglist: " + arglist);
+
+        var pres1 = $("#pres1").val();
+        arglist = arglist.concat("&pres1=");
+        arglist = arglist.concat(pres1);
+
+        var d2 = $("#data2").val();
+        var model2 = d2.replace("/", "_");
+        model2 = model2.toLowerCase();
+
+        arglist = arglist.concat("&model2=");
+        arglist = arglist.concat(model2);
+
+        // alert("arglist: " + arglist);
+
+        var variable2 = $("#var2").val();
+        arglist = arglist.concat("&var2=");
+        arglist = arglist.concat(variable2);
+
+        // alert("arglist: " + arglist);
+
+        var pres2 = $("#pres2").val();
+        arglist = arglist.concat("&pres2=");
+        arglist = arglist.concat(pres2);
+
+        var t0 = $("#t0").val();
+        var t1 = $("#t1").val();
+
+        t0 = t0.replace("-", "");
+        t1 = t1.replace("-", "");
+
+        arglist = arglist.concat("&start_time=");
+        arglist = arglist.concat(t0);
+
+        arglist = arglist.concat("&end_time=");
+        arglist = arglist.concat(t1);
+
+        // alert("arglist: " + arglist);
+
+        var lon0 = $("#lon0").val();
+        var lon1 = $("#lon1").val();
+        var lat0 = $("#lat0").val();
+        var lat1 = $("#lat1").val();
+
+        arglist = arglist.concat("&lon1=");
+        arglist = arglist.concat(lon0);
+
+        arglist = arglist.concat("&lon2=");
+        arglist = arglist.concat(lon1);
+
+        arglist = arglist.concat("&lat1=");
+        arglist = arglist.concat(lat0);
+
+        arglist = arglist.concat("&lat2=");
+        arglist = arglist.concat(lat1);
+
+        var nSample = $("#nSample").val();
+        arglist = arglist.concat("&nSample=");
+        arglist = arglist.concat(nSample);
+
+        var purpose = $("#purpose").val();
+        arglist = arglist.concat("&purpose=");
+        arglist = arglist.concat(purpose);
+
+        // alert("arglist: " + arglist);
+
+        // url = url + encodeURIComponent(arglist);
+        url = url + encodeURI(arglist);
+        // url = url + arglist;
+        // alert("url: " + url);
+
+        var urlTimeBounds = "http://" + window.location.hostname + ":9002/svc/two_time_bounds?";
+        var arglistTB = "";
+        arglistTB = arglistTB.concat("serviceType=");
+        arglistTB = arglistTB.concat("2");
+        arglistTB = arglistTB.concat("&source1=");
+        arglistTB = arglistTB.concat(d1);
+        arglistTB = arglistTB.concat("&var1=");
+        arglistTB = arglistTB.concat(variable1);
+        arglistTB = arglistTB.concat("&source2=");
+        arglistTB = arglistTB.concat(d2);
+        arglistTB = arglistTB.concat("&var2=");
+        arglistTB = arglistTB.concat(variable2);
+        urlTimeBounds = urlTimeBounds + encodeURI(arglistTB);
+        // alert("urlTimeBounds: " + urlTimeBounds);
+
+        $.ajax({
+            type: "GET",
+            url: urlTimeBounds,
+            dataType: "json",
+            data: null,
+            success: function(data, textStatus, xhr) {
+                Response = data;
+                // alert("data: " + data);
+                if (data.success == false) {
+                    // alert(data.error);
+                    Response = null;
+                    var text = JSON.stringify(data, null, 4);
+                    text = "Error in backend: <br>" + text; 
+                    $("#Response").html(text);
+                    $("#data_url").html(text);
+                    return;
+                }
+                var text = JSON.stringify(data, null, 4);
+                // alert("text: " + text);
+                // $("#Response").html("<pre>"+text+"</pre>");
+                // $("#Response").html(text);
+
+                var tb1 = data.time_bounds1;
+                var bds1 = String(tb1).split(",");
+                // alert("tb1: " + tb1);
+                // alert("bds1: " + bds1);
+                var lowerT1 = parseInt(bds1[0]);
+                // alert("inside ajax, lowerT1: " + lowerT1);
+                var upperT1 = parseInt(bds1[1]);
+                // alert("upperT1: " + upperT1);
+
+                var tb2 = data.time_bounds2;
+                var bds2 = String(tb2).split(",");
+                // alert("tb2: " + tb2);
+                // alert("bds2: " + bds2);
+                var lowerT2 = parseInt(bds2[0]);
+                // alert("inside ajax, lowerT2: " + lowerT2);
+                var upperT2 = parseInt(bds2[1]);
+                // alert("upperT2: " + upperT2);
+
+                var t0I = parseInt(t0);
+                var t1I = parseInt(t1);
+                // alert("t0: " + t0I);
+                // alert("t1: " + t1I);
+
+                var lowerT, upperT;
+                // compute the intersection of the two data bounds
+                if (lowerT1 == 0  || upperT1 == 0){ // no data-1
+                  alert("We do not have data for the data-1 source and variable configuration.");
+                  return;
+                }
+                else if (lowerT2 == 0  || upperT2 == 0){ // no data-2
+                  alert("We do not have data for the data-2 source and variable configuration.");
+                  return;
+                }
+                else if (lowerT2 > upperT1 || lowerT1 > upperT2) { // no intersection
+                  alert("The two data sets/vars do not have a common time range.");
+                  return;
+                }
+                else { // compute intersection
+                  if (lowerT1 > lowerT2) { // pick bigger lower time bound
+                    lowerT = lowerT1;
+                  }
+                  else {
+                    lowerT = lowerT2;
+                  }
+
+                  if (upperT1 > upperT2) { // pick smaller upper time bound
+                    upperT = upperT2;
+                  }
+                  else {
+                    upperT = upperT1;
+                  }
+                }
+                // alert("lowerT: " + lowerT);
+                // alert("upperT: " + upperT);
+
+                if (t0I < lowerT && t1I < lowerT ||
+                    t0I > upperT && t1I > upperT) {
+                  alert("We do not have data that span your time range. Try the range inside ["+lowerT+", "+upperT+"].");
+                  return;
+                }
+
+                if (t0I < lowerT && t1I <= upperT) {
+                  alert("Your start year-month is out of bound. It has to be in or later than " + lowerT +
+                        ". We will use the range ["+lowerT+", "+t1I+"] for you.");
+                }
+
+                if (t1I > upperT && t0I >= lowerT) {
+                  alert("Your end year-month is out of bound. It has to be in or earlier than " + upperT +
+                        ". We will use the range ["+t0I+", "+upperT+"] for you.");
+                }
+
+                if (t0I < lowerT && t1I > upperT ) {
+                  alert("Both of your start and end year-months are out of bounds. They have to be in or earlier than " + upperT +
+                        ", and in or later than " + lowerT + ". We will use the range ["+lowerT+", "+upperT+"] for you.");
+                }
+            },
+            error: function(xhr, textStatus, errorThrown) {
+		$("#Response").html("error!");
+		$("#data_url").html("error!");
+                // alert("xhr.status: "+xhr.status);
+                // alert("error status: "+textStatus);
+            },
+            complete: function(xhr, textStatus) {
+                //alert("complete status: "+textStatus);
+            },
+        });
+
+
+        $.ajax({
+            type: "GET",
+            url: url,
+            dataType: "json",
+            data: null,
+            success: function(data, textStatus, xhr) {
+                Response = data;
+                // alert("data: " + data);
+                if (data.success == false) {
+                    // alert(data.error);
+                    Response = null;
+                    var text = JSON.stringify(data, null, 4);
+
+                    if (text.indexOf("No Data") != -1) {
+                      $("#Image").html("No Data");
+                      $("#Response").html("No Data");
+                      $("#data_url").html("No Data");
+                      return;
+                    }
+
+                    text = "Error in backend: <br>" + text; 
+                    // $("#Response").html("<span style='color:red'>" + text + "</span>");
+                    $("#Response").html(text);
+                    $("#data_url").html(text);
+
+                    return;
+                }
+                var text = JSON.stringify(data, null, 4);
+                // alert(text);
+                // $("#Response").html("<pre>"+text+"</pre>");
+                $("#Response").html(text);
+
+                var html = "<img src='"+data.url+"' width='820'/>";
+                // alert(html);
+                $("#Image").html(html);
+
+                // post dataUrl to textarea and enable download button
+                $("#data_url").html(data.dataUrl);
+                enable_download_button();
+            },
+            error: function(xhr, textStatus, errorThrown) {
+                $("#Response").html("error!");
+                $("#data_url").html("error!");
+                // alert("xhr.status: "+xhr.status);
+                // alert("error status: "+textStatus);
+            },
+            complete: function(xhr, textStatus) {
+                //alert("complete status: "+textStatus);
+            },
+        });
+
+      });
+      
+    });
+
+
+
+  </script>
+
+</head>
+
+<body>
+<div class="container-fluid">
+<div class="row center1">
+<div class="col-sm-8 col-sm-offset-2 col-xs-12 color-head">
+<h3>Service: Scatter and Histogram Plot of Two Variables</h3>
+This service generates a scatter plot between two specified variables and the histograms of the two variables, and calculates the correlation of the two variables. The two variables can be either a two-dimensional variable or a slice of a three-dimensional variable at a specific pressure level. The number of samples used for this analysis should be specified.
+</div> <!-- col-sm -->
+<div class="col-sm-offset-2">
+</div> <!-- col-sm -->
+</div> <!-- row center1 -->
+
+
+
+<div class="color0">
+<div class="row ">
+<div class="col-sm-12 center1 subtitle1">
+Variable 1
+</div>
+</div> <!-- row --> 
+
+<div class="row">
+ <div class="col-sm-4 right1">
+   model:
+  </div> <!-- col-sm-6 -->
+  <div class="col-sm-8 left1">
+    <select name="data1"  id="data1" onchange="put_var(1); select_var(1); time_range2()"></select>;
+  </div> <!-- col-sm-6 level2-->
+</div> <!-- row -->
+
+<div class="row">
+ <div class="col-sm-4 right1">
+   variable:
+  </div> <!-- col-sm-6 -->
+  <div class="col-sm-8 left1">
+    <select name="var1"  id="var1" onchange="select_var(1); time_range2()"></select>;
+  </div> <!-- col-sm-6 level2-->
+</div> <!-- row -->
+
+
+<div class="row">
+  <div class="col-sm-4 right1" id="pressureLabel1" >
+    pressure:
+  </div> <!-- col-sm-6 level2-->
+  <div class="col-sm-8 left1">
+    <input id="pres1" value="500"  alt="pressure" size=7>
+  </div> <!-- col-sm-6 level2-->
+</div> <!-- row -->
+ 
+
+</div> <!-- color0 -->
+ 
+
+ 
+
+
+
+<div class="color1">
+<div class="row ">
+<div class="col-sm-12 center1 subtitle1">
+Variable 2
+</div>
+</div> <!-- row --> 
+
+<div class="row">
+ <div class="col-sm-4 right1">
+   model:
+  </div> <!-- col-sm-6 -->
+  <div class="col-sm-8 left1">
+    <select name="data2"  id="data2" onchange="put_var(2); select_var(2); time_range2()"></select>;
+  </div> <!-- col-sm-6 level2-->
+</div> <!-- row -->
+
+<div class="row">
+ <div class="col-sm-4 right1">
+   variable:
+  </div> <!-- col-sm-6 -->
+  <div class="col-sm-8 left1">
+    <select name="var2"  id="var2" onchange="select_var(2); time_range2()"></select>;
+  </div> <!-- col-sm-6 level2-->
+</div> <!-- row -->
+
+
+<div class="row">
+  <div class="col-sm-4 right1" id="pressureLabel2" >
+    pressure:
+  </div> <!-- col-sm-6 level2-->
+  <div class="col-sm-8 left1">
+    <input id="pres2" value="500"  alt="pressure" size=7>
+  </div> <!-- col-sm-6 level2-->
+</div> <!-- row -->
+ 
+
+</div> <!-- color1 -->
+ 
+
+
+<div class="color3">
+<div class="row center1 subtitle1" >
+Data Subsetting
+</div> <!-- row -->
+
+
+<div class="row">
+  <div class="col-sm-4 right1">
+    <div id=startYear>start year-month:</div>
+  </div>
+  <div class="col-sm-2 left1">
+    <input id="t0" value="2004-01" alt="start" size=7>
+  </div>
+
+  <div class="col-sm-4 right1">
+    <div id=endYear>end year-month:</div>
+  </div>
+  <div class="col-sm-2 left1">
+    <input id="t1" value="2004-12" alt="end" size=7>
+  </div>
+</div> <!-- row -->
+
+
+<div class="row">
+  <div class="col-sm-4 right1">
+    start lon (deg):
+  </div>
+  <div class="col-sm-2 left1">
+    <input id="lon0" value="0" size=7>
+  </div>
+  <div class="col-sm-4 right1">
+    end lon (deg):
+  </div>
+  <div class="col-sm-2 left1">
+    <input id="lon1" value="360" size=7>
+  </div>
+</div> <!-- row -->
+
+
+<div class="row">
+  <div class="col-sm-4 right1">
+    start lat (deg):
+  </div>
+  <div class="col-sm-2 left1">
+    <input id="lat0" value="-90" size=7>
+  </div>
+  <div class="col-sm-4 right1">
+    end lat (deg):
+  </div>
+  <div class="col-sm-2 left1">
+    <input id="lat1" value="90" size=7>
+  </div>
+</div> <!-- row -->
+
+<div class="row">
+  <div class="col-sm-4 right1">
+    number of samples:
+  </div>
+  <div class="col-sm-8 left1">
+    <input id="nSample" value="500" size=7>  
+  </div>
+</div>
+
+</div> <!-- color3 -->
+
+
+
+
+
+
+<div class="color2">
+<div class="row">
+  <div class="col-sm-4 right1">
+    Execution purpose:
+  </div> <!-- col -->
+  <div class="col-sm-8 left1">
+    <form>
+    <textarea name="purpose" id="purpose" rows="4" cols="50"> </textarea>
+    </form>
+  </div> <!-- col -->
+</div> <!-- row -->
+</div> <!-- color2 -->
+
+<div class="color4">
+<div class="row">
+  <div class="col-sm-4 center1">
+    <input id="action1" type="submit" value="Get Plot" style="height:28px"/>
+  </div>
+  <div class="col-sm-4 center1">
+    <button id="showUrl1" onclick="showUrl(inputs)">Show Service URL</button>
+  </div>
+  <div class="col-sm-4 center1">
+    <form>
+      <input id="download_data" type="button" value="Download Data" style="height:28px"/>
+    </form>
+  </div>
+</div> <!-- row -->
+
+</div> <!-- color4 -->
+
+<div class="row" >
+  <div class="col-sm-12 center1">
+    <textarea readonly id="actionUrl" >Service URL Here</textarea>
+  </div>
+</div> <!-- row -->
+ 			
+<div class="row" center1>
+  <div class="col-sm-12 center1">
+    <div id="Image">Image Here</div>
+  </div>
+</div> <!-- row -->
+
+<div class="row" >
+  <div class="col-sm-12 center1">
+    <textarea id="data_url" cols="150" rows="1">Data URL Here</textarea>
+  </div>
+</div> <!-- row -->
+
+<div class="row" center1>
+  <div class="col-sm-12 center1">
+    <textarea id="Response" cols="150" rows="6">Service Response Text Here</textarea>
+  </div>
+</div> <!-- row -->
+
+</div> <!-- container -->
+</body>
+</html>
diff --git a/ApacheCMDA_Frontend_1.0/public/images/giphy.gif b/ApacheCMDA_Frontend_1.0/public/images/giphy.gif
new file mode 100644
index 0000000..f5dabf5
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/images/giphy.gif
Binary files differ
diff --git a/ApacheCMDA_Frontend_1.0/public/javascripts/exampleUtil.js b/ApacheCMDA_Frontend_1.0/public/javascripts/exampleUtil.js
new file mode 100644
index 0000000..aeeb2df
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/javascripts/exampleUtil.js
@@ -0,0 +1,126 @@
+/**
+ * Created by Alex on 5/20/2015.
+ */
+
+function loadJSON(path, success, error) {
+  var xhr = new XMLHttpRequest();
+  xhr.onreadystatechange = function () {
+    if (xhr.readyState === 4) {
+      if (xhr.status === 200) {
+        success(JSON.parse(xhr.responseText));
+      }
+      else {
+        error(xhr);
+      }
+    }
+  };
+  xhr.open('GET', path, true);
+  xhr.send();
+}
+
+
+function getScaleFreeNetwork(nodeCount) {
+  var nodes = [];
+  var edges = [];
+  var connectionCount = [];
+
+  // randomly create some nodes and edges
+  for (var i = 0; i < nodeCount; i++) {
+    nodes.push({
+      id: i,
+      label: String(i)
+    });
+
+    connectionCount[i] = 0;
+
+    // create edges in a scale-free-network way
+    if (i == 1) {
+      var from = i;
+      var to = 0;
+      edges.push({
+        from: from,
+        to: to
+      });
+      connectionCount[from]++;
+      connectionCount[to]++;
+    }
+    else if (i > 1) {
+      var conn = edges.length * 2;
+      var rand = Math.floor(Math.random() * conn);
+      var cum = 0;
+      var j = 0;
+      while (j < connectionCount.length && cum < rand) {
+        cum += connectionCount[j];
+        j++;
+      }
+
+
+      var from = i;
+      var to = j;
+      edges.push({
+        from: from,
+        to: to
+      });
+      connectionCount[from]++;
+      connectionCount[to]++;
+    }
+  }
+
+  return {nodes:nodes, edges:edges};
+}
+
+var randomSeed = 764; // Math.round(Math.random()*1000);
+function seededRandom() {
+  var x = Math.sin(randomSeed++) * 10000;
+  return x - Math.floor(x);
+}
+
+function getScaleFreeNetworkSeeded(nodeCount) {
+  var nodes = [];
+  var edges = [];
+  var connectionCount = [];
+
+  // randomly create some nodes and edges
+  for (var i = 0; i < nodeCount; i++) {
+    nodes.push({
+      id: i,
+      label: String(i)
+    });
+
+    connectionCount[i] = 0;
+
+    // create edges in a scale-free-network way
+    if (i == 1) {
+      var from = i;
+      var to = 0;
+      edges.push({
+        from: from,
+        to: to
+      });
+      connectionCount[from]++;
+      connectionCount[to]++;
+    }
+    else if (i > 1) {
+      var conn = edges.length * 2;
+      var rand = Math.floor(seededRandom() * conn);
+      var cum = 0;
+      var j = 0;
+      while (j < connectionCount.length && cum < rand) {
+        cum += connectionCount[j];
+        j++;
+      }
+
+
+      var from = i;
+      var to = j;
+      edges.push({
+        from: from,
+        to: to
+      });
+      connectionCount[from]++;
+      connectionCount[to]++;
+    }
+  }
+
+  return {nodes:nodes, edges:edges};
+}
\ No newline at end of file
diff --git a/ApacheCMDA_Frontend_1.0/public/javascripts/googleAnalytics.js b/ApacheCMDA_Frontend_1.0/public/javascripts/googleAnalytics.js
new file mode 100644
index 0000000..3bdfc85
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/javascripts/googleAnalytics.js
@@ -0,0 +1,12 @@
+(function (i, s, o, g, r, a, m) {
+  i['GoogleAnalyticsObject'] = r;
+  i[r] = i[r] || function () {
+      (i[r].q = i[r].q || []).push(arguments)
+    }, i[r].l = 1 * new Date();
+  a = s.createElement(o), m = s.getElementsByTagName(o)[0];
+  a.async = 1;
+  a.src = g;
+  m.parentNode.insertBefore(a, m)
+})(window, document, 'script', 'http://www.google-analytics.com/analytics.js', 'ga');
+ga('create', 'UA-61231638-1', 'auto');
+ga('send', 'pageview');
\ No newline at end of file
diff --git a/ApacheCMDA_Frontend_1.0/public/javascripts/vis.js b/ApacheCMDA_Frontend_1.0/public/javascripts/vis.js
new file mode 100644
index 0000000..3388fc4
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/javascripts/vis.js
@@ -0,0 +1,43387 @@
+/**
+ * vis.js
+ * https://github.com/almende/vis
+ *
+ * A dynamic, browser-based visualization library.
+ *
+ * @version 4.9.0
+ * @date    2015-10-01
+ *
+ * @license
+ * Copyright (C) 2011-2015 Almende B.V, http://almende.com
+ *
+ * Vis.js is dual licensed under both
+ *
+ * * The Apache 2.0 License
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * and
+ *
+ * * The MIT License
+ *   http://opensource.org/licenses/MIT
+ *
+ * Vis.js may be distributed under either license.
+ */
+
+"use strict";
+
+(function webpackUniversalModuleDefinition(root, factory) {
+	if(typeof exports === 'object' && typeof module === 'object')
+		module.exports = factory();
+	else if(typeof define === 'function' && define.amd)
+		define([], factory);
+	else if(typeof exports === 'object')
+		exports["vis"] = factory();
+	else
+		root["vis"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ 	// The module cache
+/******/ 	var installedModules = {};
+
+/******/ 	// The require function
+/******/ 	function __webpack_require__(moduleId) {
+
+/******/ 		// Check if module is in cache
+/******/ 		if(installedModules[moduleId])
+/******/ 			return installedModules[moduleId].exports;
+
+/******/ 		// Create a new module (and put it into the cache)
+/******/ 		var module = installedModules[moduleId] = {
+/******/ 			exports: {},
+/******/ 			id: moduleId,
+/******/ 			loaded: false
+/******/ 		};
+
+/******/ 		// Execute the module function
+/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ 		// Flag the module as loaded
+/******/ 		module.loaded = true;
+
+/******/ 		// Return the exports of the module
+/******/ 		return module.exports;
+/******/ 	}
+
+
+/******/ 	// expose the modules object (__webpack_modules__)
+/******/ 	__webpack_require__.m = modules;
+
+/******/ 	// expose the module cache
+/******/ 	__webpack_require__.c = installedModules;
+
+/******/ 	// __webpack_public_path__
+/******/ 	__webpack_require__.p = "";
+
+/******/ 	// Load entry module and return exports
+/******/ 	return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+  // utils
+  'use strict';
+
+  exports.util = __webpack_require__(1);
+  exports.DOMutil = __webpack_require__(7);
+
+  // data
+  exports.DataSet = __webpack_require__(8);
+  exports.DataView = __webpack_require__(10);
+  exports.Queue = __webpack_require__(9);
+
+  // Graph3d
+  exports.Graph3d = __webpack_require__(11);
+  exports.graph3d = {
+    Camera: __webpack_require__(15),
+    Filter: __webpack_require__(16),
+    Point2d: __webpack_require__(14),
+    Point3d: __webpack_require__(13),
+    Slider: __webpack_require__(17),
+    StepNumber: __webpack_require__(18)
+  };
+
+  // Timeline
+  exports.Timeline = __webpack_require__(19);
+  exports.Graph2d = __webpack_require__(49);
+  exports.timeline = {
+    DateUtil: __webpack_require__(27),
+    DataStep: __webpack_require__(52),
+    Range: __webpack_require__(24),
+    stack: __webpack_require__(32),
+    TimeStep: __webpack_require__(30),
+
+    components: {
+      items: {
+        Item: __webpack_require__(34),
+        BackgroundItem: __webpack_require__(38),
+        BoxItem: __webpack_require__(36),
+        PointItem: __webpack_require__(37),
+        RangeItem: __webpack_require__(33)
+      },
+
+      Component: __webpack_require__(26),
+      CurrentTime: __webpack_require__(44),
+      CustomTime: __webpack_require__(42),
+      DataAxis: __webpack_require__(51),
+      GraphGroup: __webpack_require__(53),
+      Group: __webpack_require__(31),
+      BackgroundGroup: __webpack_require__(35),
+      ItemSet: __webpack_require__(29),
+      Legend: __webpack_require__(57),
+      LineGraph: __webpack_require__(50),
+      TimeAxis: __webpack_require__(39)
+    }
+  };
+
+  // Network
+  exports.Network = __webpack_require__(59);
+  exports.network = {
+    Images: __webpack_require__(117),
+    dotparser: __webpack_require__(115),
+    gephiParser: __webpack_require__(116),
+    allOptions: __webpack_require__(111)
+  };
+  exports.network.convertDot = function (input) {
+    return exports.network.dotparser.DOTToGraph(input);
+  };
+  exports.network.convertGephi = function (input, options) {
+    return exports.network.gephiParser.parseGephi(input, options);
+  };
+
+  // bundled external libraries
+  exports.moment = __webpack_require__(2);
+  exports.Hammer = __webpack_require__(20);
+  exports.keycharm = __webpack_require__(41);
+
+/***/ },
+/* 1 */
+/***/ function(module, exports, __webpack_require__) {
+
+  // utility functions
+
+  // first check if moment.js is already loaded in the browser window, if so,
+  // use this instance. Else, load via commonjs.
+
+  'use strict';
+
+  var moment = __webpack_require__(2);
+  var uuid = __webpack_require__(6);
+
+  /**
+   * Test whether given object is a number
+   * @param {*} object
+   * @return {Boolean} isNumber
+   */
+  exports.isNumber = function (object) {
+    return object instanceof Number || typeof object == 'number';
+  };
+
+  /**
+   * Remove everything in the DOM object
+   * @param DOMobject
+   */
+  exports.recursiveDOMDelete = function (DOMobject) {
+    if (DOMobject) {
+      while (DOMobject.hasChildNodes() === true) {
+        exports.recursiveDOMDelete(DOMobject.firstChild);
+        DOMobject.removeChild(DOMobject.firstChild);
+      }
+    }
+  };
+
+  /**
+   * this function gives you a range between 0 and 1 based on the min and max values in the set, the total sum of all values and the current value.
+   *
+   * @param min
+   * @param max
+   * @param total
+   * @param value
+   * @returns {number}
+   */
+  exports.giveRange = function (min, max, total, value) {
+    if (max == min) {
+      return 0.5;
+    } else {
+      var scale = 1 / (max - min);
+      return Math.max(0, (value - min) * scale);
+    }
+  };
+
+  /**
+   * Test whether given object is a string
+   * @param {*} object
+   * @return {Boolean} isString
+   */
+  exports.isString = function (object) {
+    return object instanceof String || typeof object == 'string';
+  };
+
+  /**
+   * Test whether given object is a Date, or a String containing a Date
+   * @param {Date | String} object
+   * @return {Boolean} isDate
+   */
+  exports.isDate = function (object) {
+    if (object instanceof Date) {
+      return true;
+    } else if (exports.isString(object)) {
+      // test whether this string contains a date
+      var match = ASPDateRegex.exec(object);
+      if (match) {
+        return true;
+      } else if (!isNaN(Date.parse(object))) {
+        return true;
+      }
+    }
+
+    return false;
+  };
+
+  /**
+   * Create a semi UUID
+   * source: http://stackoverflow.com/a/105074/1262753
+   * @return {String} uuid
+   */
+  exports.randomUUID = function () {
+    return uuid.v4();
+  };
+
+  /**
+   * assign all keys of an object that are not nested objects to a certain value (used for color objects).
+   * @param obj
+   * @param value
+   */
+  exports.assignAllKeys = function (obj, value) {
+    for (var prop in obj) {
+      if (obj.hasOwnProperty(prop)) {
+        if (typeof obj[prop] !== 'object') {
+          obj[prop] = value;
+        }
+      }
+    }
+  };
+
+  /**
+   * Fill an object with a possibly partially defined other object. Only copies values if the a object has an object requiring values.
+   * That means an object is not created on a property if only the b object has it.
+   * @param obj
+   * @param value
+   */
+  exports.fillIfDefined = function (a, b) {
+    var allowDeletion = arguments.length <= 2 || arguments[2] === undefined ? false : arguments[2];
+
+    for (var prop in a) {
+      if (b[prop] !== undefined) {
+        if (typeof b[prop] !== 'object') {
+          if ((b[prop] === undefined || b[prop] === null) && a[prop] !== undefined && allowDeletion === true) {
+            delete a[prop];
+          } else {
+            a[prop] = b[prop];
+          }
+        } else {
+          if (typeof a[prop] === 'object') {
+            exports.fillIfDefined(a[prop], b[prop], allowDeletion);
+          }
+        }
+      }
+    }
+  };
+
+  /**
+   * Extend object a with the properties of object b or a series of objects
+   * Only properties with defined values are copied
+   * @param {Object} a
+   * @param {... Object} b
+   * @return {Object} a
+   */
+  exports.protoExtend = function (a, b) {
+    for (var i = 1; i < arguments.length; i++) {
+      var other = arguments[i];
+      for (var prop in other) {
+        a[prop] = other[prop];
+      }
+    }
+    return a;
+  };
+
+  /**
+   * Extend object a with the properties of object b or a series of objects
+   * Only properties with defined values are copied
+   * @param {Object} a
+   * @param {... Object} b
+   * @return {Object} a
+   */
+  exports.extend = function (a, b) {
+    for (var i = 1; i < arguments.length; i++) {
+      var other = arguments[i];
+      for (var prop in other) {
+        if (other.hasOwnProperty(prop)) {
+          a[prop] = other[prop];
+        }
+      }
+    }
+    return a;
+  };
+
+  /**
+   * Extend object a with selected properties of object b or a series of objects
+   * Only properties with defined values are copied
+   * @param {Array.<String>} props
+   * @param {Object} a
+   * @param {Object} b
+   * @return {Object} a
+   */
+  exports.selectiveExtend = function (props, a, b) {
+    if (!Array.isArray(props)) {
+      throw new Error('Array with property names expected as first argument');
+    }
+
+    for (var i = 2; i < arguments.length; i++) {
+      var other = arguments[i];
+
+      for (var p = 0; p < props.length; p++) {
+        var prop = props[p];
+        if (other.hasOwnProperty(prop)) {
+          a[prop] = other[prop];
+        }
+      }
+    }
+    return a;
+  };
+
+  /**
+   * Extend object a with selected properties of object b or a series of objects
+   * Only properties with defined values are copied
+   * @param {Array.<String>} props
+   * @param {Object} a
+   * @param {Object} b
+   * @return {Object} a
+   */
+  exports.selectiveDeepExtend = function (props, a, b) {
+    var allowDeletion = arguments.length <= 3 || arguments[3] === undefined ? false : arguments[3];
+
+    // TODO: add support for Arrays to deepExtend
+    if (Array.isArray(b)) {
+      throw new TypeError('Arrays are not supported by deepExtend');
+    }
+    for (var i = 2; i < arguments.length; i++) {
+      var other = arguments[i];
+      for (var p = 0; p < props.length; p++) {
+        var prop = props[p];
+        if (other.hasOwnProperty(prop)) {
+          if (b[prop] && b[prop].constructor === Object) {
+            if (a[prop] === undefined) {
+              a[prop] = {};
+            }
+            if (a[prop].constructor === Object) {
+              exports.deepExtend(a[prop], b[prop], false, allowDeletion);
+            } else {
+              if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) {
+                delete a[prop];
+              } else {
+                a[prop] = b[prop];
+              }
+            }
+          } else if (Array.isArray(b[prop])) {
+            throw new TypeError('Arrays are not supported by deepExtend');
+          } else {
+            if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) {
+              delete a[prop];
+            } else {
+              a[prop] = b[prop];
+            }
+          }
+        }
+      }
+    }
+    return a;
+  };
+
+  /**
+   * Extend object a with selected properties of object b or a series of objects
+   * Only properties with defined values are copied
+   * @param {Array.<String>} props
+   * @param {Object} a
+   * @param {Object} b
+   * @return {Object} a
+   */
+  exports.selectiveNotDeepExtend = function (props, a, b) {
+    var allowDeletion = arguments.length <= 3 || arguments[3] === undefined ? false : arguments[3];
+
+    // TODO: add support for Arrays to deepExtend
+    if (Array.isArray(b)) {
+      throw new TypeError('Arrays are not supported by deepExtend');
+    }
+    for (var prop in b) {
+      if (b.hasOwnProperty(prop)) {
+        if (props.indexOf(prop) == -1) {
+          if (b[prop] && b[prop].constructor === Object) {
+            if (a[prop] === undefined) {
+              a[prop] = {};
+            }
+            if (a[prop].constructor === Object) {
+              exports.deepExtend(a[prop], b[prop]);
+            } else {
+              if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) {
+                delete a[prop];
+              } else {
+                a[prop] = b[prop];
+              }
+            }
+          } else if (Array.isArray(b[prop])) {
+            a[prop] = [];
+            for (var i = 0; i < b[prop].length; i++) {
+              a[prop].push(b[prop][i]);
+            }
+          } else {
+            if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) {
+              delete a[prop];
+            } else {
+              a[prop] = b[prop];
+            }
+          }
+        }
+      }
+    }
+    return a;
+  };
+
+  /**
+   * Deep extend an object a with the properties of object b
+   * @param {Object} a
+   * @param {Object} b
+   * @param [Boolean] protoExtend --> optional parameter. If true, the prototype values will also be extended.
+   *                                  (ie. the options objects that inherit from others will also get the inherited options)
+   * @param [Boolean] global      --> optional parameter. If true, the values of fields that are null will not deleted
+   * @returns {Object}
+   */
+  exports.deepExtend = function (a, b, protoExtend, allowDeletion) {
+    for (var prop in b) {
+      if (b.hasOwnProperty(prop) || protoExtend === true) {
+        if (b[prop] && b[prop].constructor === Object) {
+          if (a[prop] === undefined) {
+            a[prop] = {};
+          }
+          if (a[prop].constructor === Object) {
+            exports.deepExtend(a[prop], b[prop], protoExtend);
+          } else {
+            if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) {
+              delete a[prop];
+            } else {
+              a[prop] = b[prop];
+            }
+          }
+        } else if (Array.isArray(b[prop])) {
+          a[prop] = [];
+          for (var i = 0; i < b[prop].length; i++) {
+            a[prop].push(b[prop][i]);
+          }
+        } else {
+          if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) {
+            delete a[prop];
+          } else {
+            a[prop] = b[prop];
+          }
+        }
+      }
+    }
+    return a;
+  };
+
+  /**
+   * Test whether all elements in two arrays are equal.
+   * @param {Array} a
+   * @param {Array} b
+   * @return {boolean} Returns true if both arrays have the same length and same
+   *                   elements.
+   */
+  exports.equalArray = function (a, b) {
+    if (a.length != b.length) return false;
+
+    for (var i = 0, len = a.length; i < len; i++) {
+      if (a[i] != b[i]) return false;
+    }
+
+    return true;
+  };
+
+  /**
+   * Convert an object to another type
+   * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
+   * @param {String | undefined} type   Name of the type. Available types:
+   *                                    'Boolean', 'Number', 'String',
+   *                                    'Date', 'Moment', ISODate', 'ASPDate'.
+   * @return {*} object
+   * @throws Error
+   */
+  exports.convert = function (object, type) {
+    var match;
+
+    if (object === undefined) {
+      return undefined;
+    }
+    if (object === null) {
+      return null;
+    }
+
+    if (!type) {
+      return object;
+    }
+    if (!(typeof type === 'string') && !(type instanceof String)) {
+      throw new Error('Type must be a string');
+    }
+
+    //noinspection FallthroughInSwitchStatementJS
+    switch (type) {
+      case 'boolean':
+      case 'Boolean':
+        return Boolean(object);
+
+      case 'number':
+      case 'Number':
+        return Number(object.valueOf());
+
+      case 'string':
+      case 'String':
+        return String(object);
+
+      case 'Date':
+        if (exports.isNumber(object)) {
+          return new Date(object);
+        }
+        if (object instanceof Date) {
+          return new Date(object.valueOf());
+        } else if (moment.isMoment(object)) {
+          return new Date(object.valueOf());
+        }
+        if (exports.isString(object)) {
+          match = ASPDateRegex.exec(object);
+          if (match) {
+            // object is an ASP date
+            return new Date(Number(match[1])); // parse number
+          } else {
+            return moment(object).toDate(); // parse string
+          }
+        } else {
+          throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date');
+        }
+
+      case 'Moment':
+        if (exports.isNumber(object)) {
+          return moment(object);
+        }
+        if (object instanceof Date) {
+          return moment(object.valueOf());
+        } else if (moment.isMoment(object)) {
+          return moment(object);
+        }
+        if (exports.isString(object)) {
+          match = ASPDateRegex.exec(object);
+          if (match) {
+            // object is an ASP date
+            return moment(Number(match[1])); // parse number
+          } else {
+            return moment(object); // parse string
+          }
+        } else {
+          throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date');
+        }
+
+      case 'ISODate':
+        if (exports.isNumber(object)) {
+          return new Date(object);
+        } else if (object instanceof Date) {
+          return object.toISOString();
+        } else if (moment.isMoment(object)) {
+          return object.toDate().toISOString();
+        } else if (exports.isString(object)) {
+          match = ASPDateRegex.exec(object);
+          if (match) {
+            // object is an ASP date
+            return new Date(Number(match[1])).toISOString(); // parse number
+          } else {
+            return new Date(object).toISOString(); // parse string
+          }
+        } else {
+          throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ISODate');
+        }
+
+      case 'ASPDate':
+        if (exports.isNumber(object)) {
+          return '/Date(' + object + ')/';
+        } else if (object instanceof Date) {
+          return '/Date(' + object.valueOf() + ')/';
+        } else if (exports.isString(object)) {
+          match = ASPDateRegex.exec(object);
+          var value;
+          if (match) {
+            // object is an ASP date
+            value = new Date(Number(match[1])).valueOf(); // parse number
+          } else {
+            value = new Date(object).valueOf(); // parse string
+          }
+          return '/Date(' + value + ')/';
+        } else {
+          throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ASPDate');
+        }
+
+      default:
+        throw new Error('Unknown type "' + type + '"');
+    }
+  };
+
+  // parse ASP.Net Date pattern,
+  // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
+  // code from http://momentjs.com/
+  var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
+
+  /**
+   * Get the type of an object, for example exports.getType([]) returns 'Array'
+   * @param {*} object
+   * @return {String} type
+   */
+  exports.getType = function (object) {
+    var type = typeof object;
+
+    if (type == 'object') {
+      if (object === null) {
+        return 'null';
+      }
+      if (object instanceof Boolean) {
+        return 'Boolean';
+      }
+      if (object instanceof Number) {
+        return 'Number';
+      }
+      if (object instanceof String) {
+        return 'String';
+      }
+      if (Array.isArray(object)) {
+        return 'Array';
+      }
+      if (object instanceof Date) {
+        return 'Date';
+      }
+      return 'Object';
+    } else if (type == 'number') {
+      return 'Number';
+    } else if (type == 'boolean') {
+      return 'Boolean';
+    } else if (type == 'string') {
+      return 'String';
+    } else if (type === undefined) {
+      return 'undefined';
+    }
+
+    return type;
+  };
+
+  /**
+   * Used to extend an array and copy it. This is used to propagate paths recursively.
+   *
+   * @param arr
+   * @param newValue
+   * @returns {Array}
+   */
+  exports.copyAndExtendArray = function (arr, newValue) {
+    var newArr = [];
+    for (var i = 0; i < arr.length; i++) {
+      newArr.push(arr[i]);
+    }
+    newArr.push(newValue);
+    return newArr;
+  };
+
+  /**
+   * Used to extend an array and copy it. This is used to propagate paths recursively.
+   *
+   * @param arr
+   * @param newValue
+   * @returns {Array}
+   */
+  exports.copyArray = function (arr) {
+    var newArr = [];
+    for (var i = 0; i < arr.length; i++) {
+      newArr.push(arr[i]);
+    }
+    return newArr;
+  };
+
+  /**
+   * Retrieve the absolute left value of a DOM element
+   * @param {Element} elem        A dom element, for example a div
+   * @return {number} left        The absolute left position of this element
+   *                              in the browser page.
+   */
+  exports.getAbsoluteLeft = function (elem) {
+    return elem.getBoundingClientRect().left;
+  };
+
+  /**
+   * Retrieve the absolute top value of a DOM element
+   * @param {Element} elem        A dom element, for example a div
+   * @return {number} top        The absolute top position of this element
+   *                              in the browser page.
+   */
+  exports.getAbsoluteTop = function (elem) {
+    return elem.getBoundingClientRect().top;
+  };
+
+  /**
+   * add a className to the given elements style
+   * @param {Element} elem
+   * @param {String} className
+   */
+  exports.addClassName = function (elem, className) {
+    var classes = elem.className.split(' ');
+    if (classes.indexOf(className) == -1) {
+      classes.push(className); // add the class to the array
+      elem.className = classes.join(' ');
+    }
+  };
+
+  /**
+   * add a className to the given elements style
+   * @param {Element} elem
+   * @param {String} className
+   */
+  exports.removeClassName = function (elem, className) {
+    var classes = elem.className.split(' ');
+    var index = classes.indexOf(className);
+    if (index != -1) {
+      classes.splice(index, 1); // remove the class from the array
+      elem.className = classes.join(' ');
+    }
+  };
+
+  /**
+   * For each method for both arrays and objects.
+   * In case of an array, the built-in Array.forEach() is applied.
+   * In case of an Object, the method loops over all properties of the object.
+   * @param {Object | Array} object   An Object or Array
+   * @param {function} callback       Callback method, called for each item in
+   *                                  the object or array with three parameters:
+   *                                  callback(value, index, object)
+   */
+  exports.forEach = function (object, callback) {
+    var i, len;
+    if (Array.isArray(object)) {
+      // array
+      for (i = 0, len = object.length; i < len; i++) {
+        callback(object[i], i, object);
+      }
+    } else {
+      // object
+      for (i in object) {
+        if (object.hasOwnProperty(i)) {
+          callback(object[i], i, object);
+        }
+      }
+    }
+  };
+
+  /**
+   * Convert an object into an array: all objects properties are put into the
+   * array. The resulting array is unordered.
+   * @param {Object} object
+   * @param {Array} array
+   */
+  exports.toArray = function (object) {
+    var array = [];
+
+    for (var prop in object) {
+      if (object.hasOwnProperty(prop)) array.push(object[prop]);
+    }
+
+    return array;
+  };
+
+  /**
+   * Update a property in an object
+   * @param {Object} object
+   * @param {String} key
+   * @param {*} value
+   * @return {Boolean} changed
+   */
+  exports.updateProperty = function (object, key, value) {
+    if (object[key] !== value) {
+      object[key] = value;
+      return true;
+    } else {
+      return false;
+    }
+  };
+
+  /**
+   * Throttle the given function to be only executed once every `wait` milliseconds
+   * @param {function} fn
+   * @param {number} wait    Time in milliseconds
+   * @returns {function} Returns the throttled function
+   */
+  exports.throttle = function (fn, wait) {
+    var timeout = null;
+    var needExecution = false;
+
+    return function throttled() {
+      if (!timeout) {
+        needExecution = false;
+        fn();
+
+        timeout = setTimeout(function () {
+          timeout = null;
+          if (needExecution) {
+            throttled();
+          }
+        }, wait);
+      } else {
+        needExecution = true;
+      }
+    };
+  };
+
+  /**
+   * Add and event listener. Works for all browsers
+   * @param {Element}     element    An html element
+   * @param {string}      action     The action, for example "click",
+   *                                 without the prefix "on"
+   * @param {function}    listener   The callback function to be executed
+   * @param {boolean}     [useCapture]
+   */
+  exports.addEventListener = function (element, action, listener, useCapture) {
+    if (element.addEventListener) {
+      if (useCapture === undefined) useCapture = false;
+
+      if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
+        action = 'DOMMouseScroll'; // For Firefox
+      }
+
+      element.addEventListener(action, listener, useCapture);
+    } else {
+      element.attachEvent('on' + action, listener); // IE browsers
+    }
+  };
+
+  /**
+   * Remove an event listener from an element
+   * @param {Element}     element         An html dom element
+   * @param {string}      action          The name of the event, for example "mousedown"
+   * @param {function}    listener        The listener function
+   * @param {boolean}     [useCapture]
+   */
+  exports.removeEventListener = function (element, action, listener, useCapture) {
+    if (element.removeEventListener) {
+      // non-IE browsers
+      if (useCapture === undefined) useCapture = false;
+
+      if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
+        action = 'DOMMouseScroll'; // For Firefox
+      }
+
+      element.removeEventListener(action, listener, useCapture);
+    } else {
+      // IE browsers
+      element.detachEvent('on' + action, listener);
+    }
+  };
+
+  /**
+   * Cancels the event if it is cancelable, without stopping further propagation of the event.
+   */
+  exports.preventDefault = function (event) {
+    if (!event) event = window.event;
+
+    if (event.preventDefault) {
+      event.preventDefault(); // non-IE browsers
+    } else {
+      event.returnValue = false; // IE browsers
+    }
+  };
+
+  /**
+   * Get HTML element which is the target of the event
+   * @param {Event} event
+   * @return {Element} target element
+   */
+  exports.getTarget = function (event) {
+    // code from http://www.quirksmode.org/js/events_properties.html
+    if (!event) {
+      event = window.event;
+    }
+
+    var target;
+
+    if (event.target) {
+      target = event.target;
+    } else if (event.srcElement) {
+      target = event.srcElement;
+    }
+
+    if (target.nodeType != undefined && target.nodeType == 3) {
+      // defeat Safari bug
+      target = target.parentNode;
+    }
+
+    return target;
+  };
+
+  /**
+   * Check if given element contains given parent somewhere in the DOM tree
+   * @param {Element} element
+   * @param {Element} parent
+   */
+  exports.hasParent = function (element, parent) {
+    var e = element;
+
+    while (e) {
+      if (e === parent) {
+        return true;
+      }
+      e = e.parentNode;
+    }
+
+    return false;
+  };
+
+  exports.option = {};
+
+  /**
+   * Convert a value into a boolean
+   * @param {Boolean | function | undefined} value
+   * @param {Boolean} [defaultValue]
+   * @returns {Boolean} bool
+   */
+  exports.option.asBoolean = function (value, defaultValue) {
+    if (typeof value == 'function') {
+      value = value();
+    }
+
+    if (value != null) {
+      return value != false;
+    }
+
+    return defaultValue || null;
+  };
+
+  /**
+   * Convert a value into a number
+   * @param {Boolean | function | undefined} value
+   * @param {Number} [defaultValue]
+   * @returns {Number} number
+   */
+  exports.option.asNumber = function (value, defaultValue) {
+    if (typeof value == 'function') {
+      value = value();
+    }
+
+    if (value != null) {
+      return Number(value) || defaultValue || null;
+    }
+
+    return defaultValue || null;
+  };
+
+  /**
+   * Convert a value into a string
+   * @param {String | function | undefined} value
+   * @param {String} [defaultValue]
+   * @returns {String} str
+   */
+  exports.option.asString = function (value, defaultValue) {
+    if (typeof value == 'function') {
+      value = value();
+    }
+
+    if (value != null) {
+      return String(value);
+    }
+
+    return defaultValue || null;
+  };
+
+  /**
+   * Convert a size or location into a string with pixels or a percentage
+   * @param {String | Number | function | undefined} value
+   * @param {String} [defaultValue]
+   * @returns {String} size
+   */
+  exports.option.asSize = function (value, defaultValue) {
+    if (typeof value == 'function') {
+      value = value();
+    }
+
+    if (exports.isString(value)) {
+      return value;
+    } else if (exports.isNumber(value)) {
+      return value + 'px';
+    } else {
+      return defaultValue || null;
+    }
+  };
+
+  /**
+   * Convert a value into a DOM element
+   * @param {HTMLElement | function | undefined} value
+   * @param {HTMLElement} [defaultValue]
+   * @returns {HTMLElement | null} dom
+   */
+  exports.option.asElement = function (value, defaultValue) {
+    if (typeof value == 'function') {
+      value = value();
+    }
+
+    return value || defaultValue || null;
+  };
+
+  /**
+   * http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
+   *
+   * @param {String} hex
+   * @returns {{r: *, g: *, b: *}} | 255 range
+   */
+  exports.hexToRGB = function (hex) {
+    // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
+    var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
+    hex = hex.replace(shorthandRegex, function (m, r, g, b) {
+      return r + r + g + g + b + b;
+    });
+    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+    return result ? {
+      r: parseInt(result[1], 16),
+      g: parseInt(result[2], 16),
+      b: parseInt(result[3], 16)
+    } : null;
+  };
+
+  /**
+   * This function takes color in hex format or rgb() or rgba() format and overrides the opacity. Returns rgba() string.
+   * @param color
+   * @param opacity
+   * @returns {*}
+   */
+  exports.overrideOpacity = function (color, opacity) {
+    if (color.indexOf('rgba') != -1) {
+      return color;
+    } else if (color.indexOf('rgb') != -1) {
+      var rgb = color.substr(color.indexOf('(') + 1).replace(')', '').split(',');
+      return 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',' + opacity + ')';
+    } else {
+      var rgb = exports.hexToRGB(color);
+      if (rgb == null) {
+        return color;
+      } else {
+        return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + opacity + ')';
+      }
+    }
+  };
+
+  /**
+   *
+   * @param red     0 -- 255
+   * @param green   0 -- 255
+   * @param blue    0 -- 255
+   * @returns {string}
+   * @constructor
+   */
+  exports.RGBToHex = function (red, green, blue) {
+    return '#' + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1);
+  };
+
+  /**
+   * Parse a color property into an object with border, background, and
+   * highlight colors
+   * @param {Object | String} color
+   * @return {Object} colorObject
+   */
+  exports.parseColor = function (color) {
+    var c;
+    if (exports.isString(color) === true) {
+      if (exports.isValidRGB(color) === true) {
+        var rgb = color.substr(4).substr(0, color.length - 5).split(',').map(function (value) {
+          return parseInt(value);
+        });
+        color = exports.RGBToHex(rgb[0], rgb[1], rgb[2]);
+      }
+      if (exports.isValidHex(color) === true) {
+        var hsv = exports.hexToHSV(color);
+        var lighterColorHSV = { h: hsv.h, s: hsv.s * 0.8, v: Math.min(1, hsv.v * 1.02) };
+        var darkerColorHSV = { h: hsv.h, s: Math.min(1, hsv.s * 1.25), v: hsv.v * 0.8 };
+        var darkerColorHex = exports.HSVToHex(darkerColorHSV.h, darkerColorHSV.s, darkerColorHSV.v);
+        var lighterColorHex = exports.HSVToHex(lighterColorHSV.h, lighterColorHSV.s, lighterColorHSV.v);
+        c = {
+          background: color,
+          border: darkerColorHex,
+          highlight: {
+            background: lighterColorHex,
+            border: darkerColorHex
+          },
+          hover: {
+            background: lighterColorHex,
+            border: darkerColorHex
+          }
+        };
+      } else {
+        c = {
+          background: color,
+          border: color,
+          highlight: {
+            background: color,
+            border: color
+          },
+          hover: {
+            background: color,
+            border: color
+          }
+        };
+      }
+    } else {
+      c = {};
+      c.background = color.background || undefined;
+      c.border = color.border || undefined;
+
+      if (exports.isString(color.highlight)) {
+        c.highlight = {
+          border: color.highlight,
+          background: color.highlight
+        };
+      } else {
+        c.highlight = {};
+        c.highlight.background = color.highlight && color.highlight.background || undefined;
+        c.highlight.border = color.highlight && color.highlight.border || undefined;
+      }
+
+      if (exports.isString(color.hover)) {
+        c.hover = {
+          border: color.hover,
+          background: color.hover
+        };
+      } else {
+        c.hover = {};
+        c.hover.background = color.hover && color.hover.background || undefined;
+        c.hover.border = color.hover && color.hover.border || undefined;
+      }
+    }
+
+    return c;
+  };
+
+  /**
+   * http://www.javascripter.net/faq/rgb2hsv.htm
+   *
+   * @param red
+   * @param green
+   * @param blue
+   * @returns {*}
+   * @constructor
+   */
+  exports.RGBToHSV = function (red, green, blue) {
+    red = red / 255;green = green / 255;blue = blue / 255;
+    var minRGB = Math.min(red, Math.min(green, blue));
+    var maxRGB = Math.max(red, Math.max(green, blue));
+
+    // Black-gray-white
+    if (minRGB == maxRGB) {
+      return { h: 0, s: 0, v: minRGB };
+    }
+
+    // Colors other than black-gray-white:
+    var d = red == minRGB ? green - blue : blue == minRGB ? red - green : blue - red;
+    var h = red == minRGB ? 3 : blue == minRGB ? 1 : 5;
+    var hue = 60 * (h - d / (maxRGB - minRGB)) / 360;
+    var saturation = (maxRGB - minRGB) / maxRGB;
+    var value = maxRGB;
+    return { h: hue, s: saturation, v: value };
+  };
+
+  var cssUtil = {
+    // split a string with css styles into an object with key/values
+    split: function split(cssText) {
+      var styles = {};
+
+      cssText.split(';').forEach(function (style) {
+        if (style.trim() != '') {
+          var parts = style.split(':');
+          var key = parts[0].trim();
+          var value = parts[1].trim();
+          styles[key] = value;
+        }
+      });
+
+      return styles;
+    },
+
+    // build a css text string from an object with key/values
+    join: function join(styles) {
+      return Object.keys(styles).map(function (key) {
+        return key + ': ' + styles[key];
+      }).join('; ');
+    }
+  };
+
+  /**
+   * Append a string with css styles to an element
+   * @param {Element} element
+   * @param {String} cssText
+   */
+  exports.addCssText = function (element, cssText) {
+    var currentStyles = cssUtil.split(element.style.cssText);
+    var newStyles = cssUtil.split(cssText);
+    var styles = exports.extend(currentStyles, newStyles);
+
+    element.style.cssText = cssUtil.join(styles);
+  };
+
+  /**
+   * Remove a string with css styles from an element
+   * @param {Element} element
+   * @param {String} cssText
+   */
+  exports.removeCssText = function (element, cssText) {
+    var styles = cssUtil.split(element.style.cssText);
+    var removeStyles = cssUtil.split(cssText);
+
+    for (var key in removeStyles) {
+      if (removeStyles.hasOwnProperty(key)) {
+        delete styles[key];
+      }
+    }
+
+    element.style.cssText = cssUtil.join(styles);
+  };
+
+  /**
+   * https://gist.github.com/mjijackson/5311256
+   * @param h
+   * @param s
+   * @param v
+   * @returns {{r: number, g: number, b: number}}
+   * @constructor
+   */
+  exports.HSVToRGB = function (h, s, v) {
+    var r, g, b;
+
+    var i = Math.floor(h * 6);
+    var f = h * 6 - i;
+    var p = v * (1 - s);
+    var q = v * (1 - f * s);
+    var t = v * (1 - (1 - f) * s);
+
+    switch (i % 6) {
+      case 0:
+        r = v, g = t, b = p;break;
+      case 1:
+        r = q, g = v, b = p;break;
+      case 2:
+        r = p, g = v, b = t;break;
+      case 3:
+        r = p, g = q, b = v;break;
+      case 4:
+        r = t, g = p, b = v;break;
+      case 5:
+        r = v, g = p, b = q;break;
+    }
+
+    return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) };
+  };
+
+  exports.HSVToHex = function (h, s, v) {
+    var rgb = exports.HSVToRGB(h, s, v);
+    return exports.RGBToHex(rgb.r, rgb.g, rgb.b);
+  };
+
+  exports.hexToHSV = function (hex) {
+    var rgb = exports.hexToRGB(hex);
+    return exports.RGBToHSV(rgb.r, rgb.g, rgb.b);
+  };
+
+  exports.isValidHex = function (hex) {
+    var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
+    return isOk;
+  };
+
+  exports.isValidRGB = function (rgb) {
+    rgb = rgb.replace(' ', '');
+    var isOk = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/i.test(rgb);
+    return isOk;
+  };
+  exports.isValidRGBA = function (rgba) {
+    rgba = rgba.replace(' ', '');
+    var isOk = /rgba\((\d{1,3}),(\d{1,3}),(\d{1,3}),(.{1,3})\)/i.test(rgba);
+    return isOk;
+  };
+
+  /**
+   * This recursively redirects the prototype of JSON objects to the referenceObject
+   * This is used for default options.
+   *
+   * @param referenceObject
+   * @returns {*}
+   */
+  exports.selectiveBridgeObject = function (fields, referenceObject) {
+    if (typeof referenceObject == 'object') {
+      var objectTo = Object.create(referenceObject);
+      for (var i = 0; i < fields.length; i++) {
+        if (referenceObject.hasOwnProperty(fields[i])) {
+          if (typeof referenceObject[fields[i]] == 'object') {
+            objectTo[fields[i]] = exports.bridgeObject(referenceObject[fields[i]]);
+          }
+        }
+      }
+      return objectTo;
+    } else {
+      return null;
+    }
+  };
+
+  /**
+   * This recursively redirects the prototype of JSON objects to the referenceObject
+   * This is used for default options.
+   *
+   * @param referenceObject
+   * @returns {*}
+   */
+  exports.bridgeObject = function (referenceObject) {
+    if (typeof referenceObject == 'object') {
+      var objectTo = Object.create(referenceObject);
+      for (var i in referenceObject) {
+        if (referenceObject.hasOwnProperty(i)) {
+          if (typeof referenceObject[i] == 'object') {
+            objectTo[i] = exports.bridgeObject(referenceObject[i]);
+          }
+        }
+      }
+      return objectTo;
+    } else {
+      return null;
+    }
+  };
+
+  /**
+   * this is used to set the options of subobjects in the options object. A requirement of these subobjects
+   * is that they have an 'enabled' element which is optional for the user but mandatory for the program.
+   *
+   * @param [object] mergeTarget | this is either this.options or the options used for the groups.
+   * @param [object] options     | options
+   * @param [String] option      | this is the option key in the options argument
+   * @private
+   */
+  exports.mergeOptions = function (mergeTarget, options, option) {
+    var allowDeletion = arguments.length <= 3 || arguments[3] === undefined ? false : arguments[3];
+    var globalOptions = arguments.length <= 4 || arguments[4] === undefined ? {} : arguments[4];
+
+    if (options[option] === null) {
+      mergeTarget[option] = Object.create(globalOptions[option]);
+    } else {
+      if (options[option] !== undefined) {
+        if (typeof options[option] === 'boolean') {
+          mergeTarget[option].enabled = options[option];
+        } else {
+          if (options[option].enabled === undefined) {
+            mergeTarget[option].enabled = true;
+          }
+          for (var prop in options[option]) {
+            if (options[option].hasOwnProperty(prop)) {
+              mergeTarget[option][prop] = options[option][prop];
+            }
+          }
+        }
+      }
+    }
+  };
+
+  /**
+   * This function does a binary search for a visible item in a sorted list. If we find a visible item, the code that uses
+   * this function will then iterate in both directions over this sorted list to find all visible items.
+   *
+   * @param {Item[]} orderedItems       | Items ordered by start
+   * @param {function} searchFunction   | -1 is lower, 0 is found, 1 is higher
+   * @param {String} field
+   * @param {String} field2
+   * @returns {number}
+   * @private
+   */
+  exports.binarySearchCustom = function (orderedItems, searchFunction, field, field2) {
+    var maxIterations = 10000;
+    var iteration = 0;
+    var low = 0;
+    var high = orderedItems.length - 1;
+
+    while (low <= high && iteration < maxIterations) {
+      var middle = Math.floor((low + high) / 2);
+
+      var item = orderedItems[middle];
+      var value = field2 === undefined ? item[field] : item[field][field2];
+
+      var searchResult = searchFunction(value);
+      if (searchResult == 0) {
+        // jihaa, found a visible item!
+        return middle;
+      } else if (searchResult == -1) {
+        // it is too small --> increase low
+        low = middle + 1;
+      } else {
+        // it is too big --> decrease high
+        high = middle - 1;
+      }
+
+      iteration++;
+    }
+
+    return -1;
+  };
+
+  /**
+   * This function does a binary search for a specific value in a sorted array. If it does not exist but is in between of
+   * two values, we return either the one before or the one after, depending on user input
+   * If it is found, we return the index, else -1.
+   *
+   * @param {Array} orderedItems
+   * @param {{start: number, end: number}} target
+   * @param {String} field
+   * @param {String} sidePreference   'before' or 'after'
+   * @returns {number}
+   * @private
+   */
+  exports.binarySearchValue = function (orderedItems, target, field, sidePreference) {
+    var maxIterations = 10000;
+    var iteration = 0;
+    var low = 0;
+    var high = orderedItems.length - 1;
+    var prevValue, value, nextValue, middle;
+
+    while (low <= high && iteration < maxIterations) {
+      // get a new guess
+      middle = Math.floor(0.5 * (high + low));
+      prevValue = orderedItems[Math.max(0, middle - 1)][field];
+      value = orderedItems[middle][field];
+      nextValue = orderedItems[Math.min(orderedItems.length - 1, middle + 1)][field];
+
+      if (value == target) {
+        // we found the target
+        return middle;
+      } else if (prevValue < target && value > target) {
+        // target is in between of the previous and the current
+        return sidePreference == 'before' ? Math.max(0, middle - 1) : middle;
+      } else if (value < target && nextValue > target) {
+        // target is in between of the current and the next
+        return sidePreference == 'before' ? middle : Math.min(orderedItems.length - 1, middle + 1);
+      } else {
+        // didnt find the target, we need to change our boundaries.
+        if (value < target) {
+          // it is too small --> increase low
+          low = middle + 1;
+        } else {
+          // it is too big --> decrease high
+          high = middle - 1;
+        }
+      }
+      iteration++;
+    }
+
+    // didnt find anything. Return -1.
+    return -1;
+  };
+
+  /*
+   * Easing Functions - inspired from http://gizma.com/easing/
+   * only considering the t value for the range [0, 1] => [0, 1]
+   * https://gist.github.com/gre/1650294
+   */
+  exports.easingFunctions = {
+    // no easing, no acceleration
+    linear: function linear(t) {
+      return t;
+    },
+    // accelerating from zero velocity
+    easeInQuad: function easeInQuad(t) {
+      return t * t;
+    },
+    // decelerating to zero velocity
+    easeOutQuad: function easeOutQuad(t) {
+      return t * (2 - t);
+    },
+    // acceleration until halfway, then deceleration
+    easeInOutQuad: function easeInOutQuad(t) {
+      return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
+    },
+    // accelerating from zero velocity
+    easeInCubic: function easeInCubic(t) {
+      return t * t * t;
+    },
+    // decelerating to zero velocity
+    easeOutCubic: function easeOutCubic(t) {
+      return --t * t * t + 1;
+    },
+    // acceleration until halfway, then deceleration
+    easeInOutCubic: function easeInOutCubic(t) {
+      return t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
+    },
+    // accelerating from zero velocity
+    easeInQuart: function easeInQuart(t) {
+      return t * t * t * t;
+    },
+    // decelerating to zero velocity
+    easeOutQuart: function easeOutQuart(t) {
+      return 1 - --t * t * t * t;
+    },
+    // acceleration until halfway, then deceleration
+    easeInOutQuart: function easeInOutQuart(t) {
+      return t < .5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t;
+    },
+    // accelerating from zero velocity
+    easeInQuint: function easeInQuint(t) {
+      return t * t * t * t * t;
+    },
+    // decelerating to zero velocity
+    easeOutQuint: function easeOutQuint(t) {
+      return 1 + --t * t * t * t * t;
+    },
+    // acceleration until halfway, then deceleration
+    easeInOutQuint: function easeInOutQuint(t) {
+      return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t;
+    }
+  };
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+  // first check if moment.js is already loaded in the browser window, if so,
+  // use this instance. Else, load via commonjs.
+  'use strict';
+
+  module.exports = typeof window !== 'undefined' && window['moment'] || __webpack_require__(3);
+
+/***/ },
+/* 3 */
+/***/ function(module, exports, __webpack_require__) {
+
+  /* WEBPACK VAR INJECTION */(function(module) {//! moment.js
+  //! version : 2.10.6
+  //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+  //! license : MIT
+  //! momentjs.com
+
+  (function (global, factory) {
+       true ? module.exports = factory() :
+      typeof define === 'function' && define.amd ? define(factory) :
+      global.moment = factory()
+  }(this, function () { 'use strict';
+
+      var hookCallback;
+
+      function utils_hooks__hooks () {
+          return hookCallback.apply(null, arguments);
+      }
+
+      // This is done to register the method called with moment()
+      // without creating circular dependencies.
+      function setHookCallback (callback) {
+          hookCallback = callback;
+      }
+
+      function isArray(input) {
+          return Object.prototype.toString.call(input) === '[object Array]';
+      }
+
+      function isDate(input) {
+          return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
+      }
+
+      function map(arr, fn) {
+          var res = [], i;
+          for (i = 0; i < arr.length; ++i) {
+              res.push(fn(arr[i], i));
+          }
+          return res;
+      }
+
+      function hasOwnProp(a, b) {
+          return Object.prototype.hasOwnProperty.call(a, b);
+      }
+
+      function extend(a, b) {
+          for (var i in b) {
+              if (hasOwnProp(b, i)) {
+                  a[i] = b[i];
+              }
+          }
+
+          if (hasOwnProp(b, 'toString')) {
+              a.toString = b.toString;
+          }
+
+          if (hasOwnProp(b, 'valueOf')) {
+              a.valueOf = b.valueOf;
+          }
+
+          return a;
+      }
+
+      function create_utc__createUTC (input, format, locale, strict) {
+          return createLocalOrUTC(input, format, locale, strict, true).utc();
+      }
+
+      function defaultParsingFlags() {
+          // We need to deep clone this object.
+          return {
+              empty           : false,
+              unusedTokens    : [],
+              unusedInput     : [],
+              overflow        : -2,
+              charsLeftOver   : 0,
+              nullInput       : false,
+              invalidMonth    : null,
+              invalidFormat   : false,
+              userInvalidated : false,
+              iso             : false
+          };
+      }
+
+      function getParsingFlags(m) {
+          if (m._pf == null) {
+              m._pf = defaultParsingFlags();
+          }
+          return m._pf;
+      }
+
+      function valid__isValid(m) {
+          if (m._isValid == null) {
+              var flags = getParsingFlags(m);
+              m._isValid = !isNaN(m._d.getTime()) &&
+                  flags.overflow < 0 &&
+                  !flags.empty &&
+                  !flags.invalidMonth &&
+                  !flags.invalidWeekday &&
+                  !flags.nullInput &&
+                  !flags.invalidFormat &&
+                  !flags.userInvalidated;
+
+              if (m._strict) {
+                  m._isValid = m._isValid &&
+                      flags.charsLeftOver === 0 &&
+                      flags.unusedTokens.length === 0 &&
+                      flags.bigHour === undefined;
+              }
+          }
+          return m._isValid;
+      }
+
+      function valid__createInvalid (flags) {
+          var m = create_utc__createUTC(NaN);
+          if (flags != null) {
+              extend(getParsingFlags(m), flags);
+          }
+          else {
+              getParsingFlags(m).userInvalidated = true;
+          }
+
+          return m;
+      }
+
+      var momentProperties = utils_hooks__hooks.momentProperties = [];
+
+      function copyConfig(to, from) {
+          var i, prop, val;
+
+          if (typeof from._isAMomentObject !== 'undefined') {
+              to._isAMomentObject = from._isAMomentObject;
+          }
+          if (typeof from._i !== 'undefined') {
+              to._i = from._i;
+          }
+          if (typeof from._f !== 'undefined') {
+              to._f = from._f;
+          }
+          if (typeof from._l !== 'undefined') {
+              to._l = from._l;
+          }
+          if (typeof from._strict !== 'undefined') {
+              to._strict = from._strict;
+          }
+          if (typeof from._tzm !== 'undefined') {
+              to._tzm = from._tzm;
+          }
+          if (typeof from._isUTC !== 'undefined') {
+              to._isUTC = from._isUTC;
+          }
+          if (typeof from._offset !== 'undefined') {
+              to._offset = from._offset;
+          }
+          if (typeof from._pf !== 'undefined') {
+              to._pf = getParsingFlags(from);
+          }
+          if (typeof from._locale !== 'undefined') {
+              to._locale = from._locale;
+          }
+
+          if (momentProperties.length > 0) {
+              for (i in momentProperties) {
+                  prop = momentProperties[i];
+                  val = from[prop];
+                  if (typeof val !== 'undefined') {
+                      to[prop] = val;
+                  }
+              }
+          }
+
+          return to;
+      }
+
+      var updateInProgress = false;
+
+      // Moment prototype object
+      function Moment(config) {
+          copyConfig(this, config);
+          this._d = new Date(config._d != null ? config._d.getTime() : NaN);
+          // Prevent infinite loop in case updateOffset creates new moment
+          // objects.
+          if (updateInProgress === false) {
+              updateInProgress = true;
+              utils_hooks__hooks.updateOffset(this);
+              updateInProgress = false;
+          }
+      }
+
+      function isMoment (obj) {
+          return obj instanceof Moment || (obj != null && obj._isAMomentObject != null);
+      }
+
+      function absFloor (number) {
+          if (number < 0) {
+              return Math.ceil(number);
+          } else {
+              return Math.floor(number);
+          }
+      }
+
+      function toInt(argumentForCoercion) {
+          var coercedNumber = +argumentForCoercion,
+              value = 0;
+
+          if (coercedNumber !== 0 && isFinite(coercedNumber)) {
+              value = absFloor(coercedNumber);
+          }
+
+          return value;
+      }
+
+      function compareArrays(array1, array2, dontConvert) {
+          var len = Math.min(array1.length, array2.length),
+              lengthDiff = Math.abs(array1.length - array2.length),
+              diffs = 0,
+              i;
+          for (i = 0; i < len; i++) {
+              if ((dontConvert && array1[i] !== array2[i]) ||
+                  (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
+                  diffs++;
+              }
+          }
+          return diffs + lengthDiff;
+      }
+
+      function Locale() {
+      }
+
+      var locales = {};
+      var globalLocale;
+
+      function normalizeLocale(key) {
+          return key ? key.toLowerCase().replace('_', '-') : key;
+      }
+
+      // pick the locale from the array
+      // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
+      // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
+      function chooseLocale(names) {
+          var i = 0, j, next, locale, split;
+
+          while (i < names.length) {
+              split = normalizeLocale(names[i]).split('-');
+              j = split.length;
+              next = normalizeLocale(names[i + 1]);
+              next = next ? next.split('-') : null;
+              while (j > 0) {
+                  locale = loadLocale(split.slice(0, j).join('-'));
+                  if (locale) {
+                      return locale;
+                  }
+                  if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
+                      //the next array item is better than a shallower substring of this one
+                      break;
+                  }
+                  j--;
+              }
+              i++;
+          }
+          return null;
+      }
+
+      function loadLocale(name) {
+          var oldLocale = null;
+          // TODO: Find a better way to register and load all the locales in Node
+          if (!locales[name] && typeof module !== 'undefined' &&
+                  module && module.exports) {
+              try {
+                  oldLocale = globalLocale._abbr;
+                  !(function webpackMissingModule() { var e = new Error("Cannot find module \"./locale\""); e.code = 'MODULE_NOT_FOUND'; throw e; }());
+                  // because defineLocale currently also sets the global locale, we
+                  // want to undo that for lazy loaded locales
+                  locale_locales__getSetGlobalLocale(oldLocale);
+              } catch (e) { }
+          }
+          return locales[name];
+      }
+
+      // This function will load locale and then set the global locale.  If
+      // no arguments are passed in, it will simply return the current global
+      // locale key.
+      function locale_locales__getSetGlobalLocale (key, values) {
+          var data;
+          if (key) {
+              if (typeof values === 'undefined') {
+                  data = locale_locales__getLocale(key);
+              }
+              else {
+                  data = defineLocale(key, values);
+              }
+
+              if (data) {
+                  // moment.duration._locale = moment._locale = data;
+                  globalLocale = data;
+              }
+          }
+
+          return globalLocale._abbr;
+      }
+
+      function defineLocale (name, values) {
+          if (values !== null) {
+              values.abbr = name;
+              locales[name] = locales[name] || new Locale();
+              locales[name].set(values);
+
+              // backwards compat for now: also set the locale
+              locale_locales__getSetGlobalLocale(name);
+
+              return locales[name];
+          } else {
+              // useful for testing
+              delete locales[name];
+              return null;
+          }
+      }
+
+      // returns locale data
+      function locale_locales__getLocale (key) {
+          var locale;
+
+          if (key && key._locale && key._locale._abbr) {
+              key = key._locale._abbr;
+          }
+
+          if (!key) {
+              return globalLocale;
+          }
+
+          if (!isArray(key)) {
+              //short-circuit everything else
+              locale = loadLocale(key);
+              if (locale) {
+                  return locale;
+              }
+              key = [key];
+          }
+
+          return chooseLocale(key);
+      }
+
+      var aliases = {};
+
+      function addUnitAlias (unit, shorthand) {
+          var lowerCase = unit.toLowerCase();
+          aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit;
+      }
+
+      function normalizeUnits(units) {
+          return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined;
+      }
+
+      function normalizeObjectUnits(inputObject) {
+          var normalizedInput = {},
+              normalizedProp,
+              prop;
+
+          for (prop in inputObject) {
+              if (hasOwnProp(inputObject, prop)) {
+                  normalizedProp = normalizeUnits(prop);
+                  if (normalizedProp) {
+                      normalizedInput[normalizedProp] = inputObject[prop];
+                  }
+              }
+          }
+
+          return normalizedInput;
+      }
+
+      function makeGetSet (unit, keepTime) {
+          return function (value) {
+              if (value != null) {
+                  get_set__set(this, unit, value);
+                  utils_hooks__hooks.updateOffset(this, keepTime);
+                  return this;
+              } else {
+                  return get_set__get(this, unit);
+              }
+          };
+      }
+
+      function get_set__get (mom, unit) {
+          return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
+      }
+
+      function get_set__set (mom, unit, value) {
+          return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
+      }
+
+      // MOMENTS
+
+      function getSet (units, value) {
+          var unit;
+          if (typeof units === 'object') {
+              for (unit in units) {
+                  this.set(unit, units[unit]);
+              }
+          } else {
+              units = normalizeUnits(units);
+              if (typeof this[units] === 'function') {
+                  return this[units](value);
+              }
+          }
+          return this;
+      }
+
+      function zeroFill(number, targetLength, forceSign) {
+          var absNumber = '' + Math.abs(number),
+              zerosToFill = targetLength - absNumber.length,
+              sign = number >= 0;
+          return (sign ? (forceSign ? '+' : '') : '-') +
+              Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + absNumber;
+      }
+
+      var formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g;
+
+      var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g;
+
+      var formatFunctions = {};
+
+      var formatTokenFunctions = {};
+
+      // token:    'M'
+      // padded:   ['MM', 2]
+      // ordinal:  'Mo'
+      // callback: function () { this.month() + 1 }
+      function addFormatToken (token, padded, ordinal, callback) {
+          var func = callback;
+          if (typeof callback === 'string') {
+              func = function () {
+                  return this[callback]();
+              };
+          }
+          if (token) {
+              formatTokenFunctions[token] = func;
+          }
+          if (padded) {
+              formatTokenFunctions[padded[0]] = function () {
+                  return zeroFill(func.apply(this, arguments), padded[1], padded[2]);
+              };
+          }
+          if (ordinal) {
+              formatTokenFunctions[ordinal] = function () {
+                  return this.localeData().ordinal(func.apply(this, arguments), token);
+              };
+          }
+      }
+
+      function removeFormattingTokens(input) {
+          if (input.match(/\[[\s\S]/)) {
+              return input.replace(/^\[|\]$/g, '');
+          }
+          return input.replace(/\\/g, '');
+      }
+
+      function makeFormatFunction(format) {
+          var array = format.match(formattingTokens), i, length;
+
+          for (i = 0, length = array.length; i < length; i++) {
+              if (formatTokenFunctions[array[i]]) {
+                  array[i] = formatTokenFunctions[array[i]];
+              } else {
+                  array[i] = removeFormattingTokens(array[i]);
+              }
+          }
+
+          return function (mom) {
+              var output = '';
+              for (i = 0; i < length; i++) {
+                  output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
+              }
+              return output;
+          };
+      }
+
+      // format date using native date object
+      function formatMoment(m, format) {
+          if (!m.isValid()) {
+              return m.localeData().invalidDate();
+          }
+
+          format = expandFormat(format, m.localeData());
+          formatFunctions[format] = formatFunctions[format] || makeFormatFunction(format);
+
+          return formatFunctions[format](m);
+      }
+
+      function expandFormat(format, locale) {
+          var i = 5;
+
+          function replaceLongDateFormatTokens(input) {
+              return locale.longDateFormat(input) || input;
+          }
+
+          localFormattingTokens.lastIndex = 0;
+          while (i >= 0 && localFormattingTokens.test(format)) {
+              format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
+              localFormattingTokens.lastIndex = 0;
+              i -= 1;
+          }
+
+          return format;
+      }
+
+      var match1         = /\d/;            //       0 - 9
+      var match2         = /\d\d/;          //      00 - 99
+      var match3         = /\d{3}/;         //     000 - 999
+      var match4         = /\d{4}/;         //    0000 - 9999
+      var match6         = /[+-]?\d{6}/;    // -999999 - 999999
+      var match1to2      = /\d\d?/;         //       0 - 99
+      var match1to3      = /\d{1,3}/;       //       0 - 999
+      var match1to4      = /\d{1,4}/;       //       0 - 9999
+      var match1to6      = /[+-]?\d{1,6}/;  // -999999 - 999999
+
+      var matchUnsigned  = /\d+/;           //       0 - inf
+      var matchSigned    = /[+-]?\d+/;      //    -inf - inf
+
+      var matchOffset    = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z
+
+      var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123
+
+      // any word (or two) characters or numbers including two/three word month in arabic.
+      var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i;
+
+      var regexes = {};
+
+      function isFunction (sth) {
+          // https://github.com/moment/moment/issues/2325
+          return typeof sth === 'function' &&
+              Object.prototype.toString.call(sth) === '[object Function]';
+      }
+
+
+      function addRegexToken (token, regex, strictRegex) {
+          regexes[token] = isFunction(regex) ? regex : function (isStrict) {
+              return (isStrict && strictRegex) ? strictRegex : regex;
+          };
+      }
+
+      function getParseRegexForToken (token, config) {
+          if (!hasOwnProp(regexes, token)) {
+              return new RegExp(unescapeFormat(token));
+          }
+
+          return regexes[token](config._strict, config._locale);
+      }
+
+      // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+      function unescapeFormat(s) {
+          return s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
+              return p1 || p2 || p3 || p4;
+          }).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+      }
+
+      var tokens = {};
+
+      function addParseToken (token, callback) {
+          var i, func = callback;
+          if (typeof token === 'string') {
+              token = [token];
+          }
+          if (typeof callback === 'number') {
+              func = function (input, array) {
+                  array[callback] = toInt(input);
+              };
+          }
+          for (i = 0; i < token.length; i++) {
+              tokens[token[i]] = func;
+          }
+      }
+
+      function addWeekParseToken (token, callback) {
+          addParseToken(token, function (input, array, config, token) {
+              config._w = config._w || {};
+              callback(input, config._w, config, token);
+          });
+      }
+
+      function addTimeToArrayFromToken(token, input, config) {
+          if (input != null && hasOwnProp(tokens, token)) {
+              tokens[token](input, config._a, config, token);
+          }
+      }
+
+      var YEAR = 0;
+      var MONTH = 1;
+      var DATE = 2;
+      var HOUR = 3;
+      var MINUTE = 4;
+      var SECOND = 5;
+      var MILLISECOND = 6;
+
+      function daysInMonth(year, month) {
+          return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
+      }
+
+      // FORMATTING
+
+      addFormatToken('M', ['MM', 2], 'Mo', function () {
+          return this.month() + 1;
+      });
+
+      addFormatToken('MMM', 0, 0, function (format) {
+          return this.localeData().monthsShort(this, format);
+      });
+
+      addFormatToken('MMMM', 0, 0, function (format) {
+          return this.localeData().months(this, format);
+      });
+
+      // ALIASES
+
+      addUnitAlias('month', 'M');
+
+      // PARSING
+
+      addRegexToken('M',    match1to2);
+      addRegexToken('MM',   match1to2, match2);
+      addRegexToken('MMM',  matchWord);
+      addRegexToken('MMMM', matchWord);
+
+      addParseToken(['M', 'MM'], function (input, array) {
+          array[MONTH] = toInt(input) - 1;
+      });
+
+      addParseToken(['MMM', 'MMMM'], function (input, array, config, token) {
+          var month = config._locale.monthsParse(input, token, config._strict);
+          // if we didn't find a month name, mark the date as invalid.
+          if (month != null) {
+              array[MONTH] = month;
+          } else {
+              getParsingFlags(config).invalidMonth = input;
+          }
+      });
+
+      // LOCALES
+
+      var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_');
+      function localeMonths (m) {
+          return this._months[m.month()];
+      }
+
+      var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_');
+      function localeMonthsShort (m) {
+          return this._monthsShort[m.month()];
+      }
+
+      function localeMonthsParse (monthName, format, strict) {
+          var i, mom, regex;
+
+          if (!this._monthsParse) {
+              this._monthsParse = [];
+              this._longMonthsParse = [];
+              this._shortMonthsParse = [];
+          }
+
+          for (i = 0; i < 12; i++) {
+              // make the regex if we don't have it already
+              mom = create_utc__createUTC([2000, i]);
+              if (strict && !this._longMonthsParse[i]) {
+                  this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i');
+                  this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i');
+              }
+              if (!strict && !this._monthsParse[i]) {
+                  regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
+                  this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
+              }
+              // test the regex
+              if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) {
+                  return i;
+              } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) {
+                  return i;
+              } else if (!strict && this._monthsParse[i].test(monthName)) {
+                  return i;
+              }
+          }
+      }
+
+      // MOMENTS
+
+      function setMonth (mom, value) {
+          var dayOfMonth;
+
+          // TODO: Move this out of here!
+          if (typeof value === 'string') {
+              value = mom.localeData().monthsParse(value);
+              // TODO: Another silent failure?
+              if (typeof value !== 'number') {
+                  return mom;
+              }
+          }
+
+          dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value));
+          mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
+          return mom;
+      }
+
+      function getSetMonth (value) {
+          if (value != null) {
+              setMonth(this, value);
+              utils_hooks__hooks.updateOffset(this, true);
+              return this;
+          } else {
+              return get_set__get(this, 'Month');
+          }
+      }
+
+      function getDaysInMonth () {
+          return daysInMonth(this.year(), this.month());
+      }
+
+      function checkOverflow (m) {
+          var overflow;
+          var a = m._a;
+
+          if (a && getParsingFlags(m).overflow === -2) {
+              overflow =
+                  a[MONTH]       < 0 || a[MONTH]       > 11  ? MONTH :
+                  a[DATE]        < 1 || a[DATE]        > daysInMonth(a[YEAR], a[MONTH]) ? DATE :
+                  a[HOUR]        < 0 || a[HOUR]        > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR :
+                  a[MINUTE]      < 0 || a[MINUTE]      > 59  ? MINUTE :
+                  a[SECOND]      < 0 || a[SECOND]      > 59  ? SECOND :
+                  a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND :
+                  -1;
+
+              if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
+                  overflow = DATE;
+              }
+
+              getParsingFlags(m).overflow = overflow;
+          }
+
+          return m;
+      }
+
+      function warn(msg) {
+          if (utils_hooks__hooks.suppressDeprecationWarnings === false && typeof console !== 'undefined' && console.warn) {
+              console.warn('Deprecation warning: ' + msg);
+          }
+      }
+
+      function deprecate(msg, fn) {
+          var firstTime = true;
+
+          return extend(function () {
+              if (firstTime) {
+                  warn(msg + '\n' + (new Error()).stack);
+                  firstTime = false;
+              }
+              return fn.apply(this, arguments);
+          }, fn);
+      }
+
+      var deprecations = {};
+
+      function deprecateSimple(name, msg) {
+          if (!deprecations[name]) {
+              warn(msg);
+              deprecations[name] = true;
+          }
+      }
+
+      utils_hooks__hooks.suppressDeprecationWarnings = false;
+
+      var from_string__isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/;
+
+      var isoDates = [
+          ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
+          ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
+          ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
+          ['GGGG-[W]WW', /\d{4}-W\d{2}/],
+          ['YYYY-DDD', /\d{4}-\d{3}/]
+      ];
+
+      // iso time formats and regexes
+      var isoTimes = [
+          ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
+          ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
+          ['HH:mm', /(T| )\d\d:\d\d/],
+          ['HH', /(T| )\d\d/]
+      ];
+
+      var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i;
+
+      // date from iso format
+      function configFromISO(config) {
+          var i, l,
+              string = config._i,
+              match = from_string__isoRegex.exec(string);
+
+          if (match) {
+              getParsingFlags(config).iso = true;
+              for (i = 0, l = isoDates.length; i < l; i++) {
+                  if (isoDates[i][1].exec(string)) {
+                      config._f = isoDates[i][0];
+                      break;
+                  }
+              }
+              for (i = 0, l = isoTimes.length; i < l; i++) {
+                  if (isoTimes[i][1].exec(string)) {
+                      // match[6] should be 'T' or space
+                      config._f += (match[6] || ' ') + isoTimes[i][0];
+                      break;
+                  }
+              }
+              if (string.match(matchOffset)) {
+                  config._f += 'Z';
+              }
+              configFromStringAndFormat(config);
+          } else {
+              config._isValid = false;
+          }
+      }
+
+      // date from iso format or fallback
+      function configFromString(config) {
+          var matched = aspNetJsonRegex.exec(config._i);
+
+          if (matched !== null) {
+              config._d = new Date(+matched[1]);
+              return;
+          }
+
+          configFromISO(config);
+          if (config._isValid === false) {
+              delete config._isValid;
+              utils_hooks__hooks.createFromInputFallback(config);
+          }
+      }
+
+      utils_hooks__hooks.createFromInputFallback = deprecate(
+          'moment construction falls back to js Date. This is ' +
+          'discouraged and will be removed in upcoming major ' +
+          'release. Please refer to ' +
+          'https://github.com/moment/moment/issues/1407 for more info.',
+          function (config) {
+              config._d = new Date(config._i + (config._useUTC ? ' UTC' : ''));
+          }
+      );
+
+      function createDate (y, m, d, h, M, s, ms) {
+          //can't just apply() to create a date:
+          //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
+          var date = new Date(y, m, d, h, M, s, ms);
+
+          //the date constructor doesn't accept years < 1970
+          if (y < 1970) {
+              date.setFullYear(y);
+          }
+          return date;
+      }
+
+      function createUTCDate (y) {
+          var date = new Date(Date.UTC.apply(null, arguments));
+          if (y < 1970) {
+              date.setUTCFullYear(y);
+          }
+          return date;
+      }
+
+      addFormatToken(0, ['YY', 2], 0, function () {
+          return this.year() % 100;
+      });
+
+      addFormatToken(0, ['YYYY',   4],       0, 'year');
+      addFormatToken(0, ['YYYYY',  5],       0, 'year');
+      addFormatToken(0, ['YYYYYY', 6, true], 0, 'year');
+
+      // ALIASES
+
+      addUnitAlias('year', 'y');
+
+      // PARSING
+
+      addRegexToken('Y',      matchSigned);
+      addRegexToken('YY',     match1to2, match2);
+      addRegexToken('YYYY',   match1to4, match4);
+      addRegexToken('YYYYY',  match1to6, match6);
+      addRegexToken('YYYYYY', match1to6, match6);
+
+      addParseToken(['YYYYY', 'YYYYYY'], YEAR);
+      addParseToken('YYYY', function (input, array) {
+          array[YEAR] = input.length === 2 ? utils_hooks__hooks.parseTwoDigitYear(input) : toInt(input);
+      });
+      addParseToken('YY', function (input, array) {
+          array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input);
+      });
+
+      // HELPERS
+
+      function daysInYear(year) {
+          return isLeapYear(year) ? 366 : 365;
+      }
+
+      function isLeapYear(year) {
+          return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+      }
+
+      // HOOKS
+
+      utils_hooks__hooks.parseTwoDigitYear = function (input) {
+          return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
+      };
+
+      // MOMENTS
+
+      var getSetYear = makeGetSet('FullYear', false);
+
+      function getIsLeapYear () {
+          return isLeapYear(this.year());
+      }
+
+      addFormatToken('w', ['ww', 2], 'wo', 'week');
+      addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek');
+
+      // ALIASES
+
+      addUnitAlias('week', 'w');
+      addUnitAlias('isoWeek', 'W');
+
+      // PARSING
+
+      addRegexToken('w',  match1to2);
+      addRegexToken('ww', match1to2, match2);
+      addRegexToken('W',  match1to2);
+      addRegexToken('WW', match1to2, match2);
+
+      addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) {
+          week[token.substr(0, 1)] = toInt(input);
+      });
+
+      // HELPERS
+
+      // firstDayOfWeek       0 = sun, 6 = sat
+      //                      the day of the week that starts the week
+      //                      (usually sunday or monday)
+      // firstDayOfWeekOfYear 0 = sun, 6 = sat
+      //                      the first week is the week that contains the first
+      //                      of this day of the week
+      //                      (eg. ISO weeks use thursday (4))
+      function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
+          var end = firstDayOfWeekOfYear - firstDayOfWeek,
+              daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
+              adjustedMoment;
+
+
+          if (daysToDayOfWeek > end) {
+              daysToDayOfWeek -= 7;
+          }
+
+          if (daysToDayOfWeek < end - 7) {
+              daysToDayOfWeek += 7;
+          }
+
+          adjustedMoment = local__createLocal(mom).add(daysToDayOfWeek, 'd');
+          return {
+              week: Math.ceil(adjustedMoment.dayOfYear() / 7),
+              year: adjustedMoment.year()
+          };
+      }
+
+      // LOCALES
+
+      function localeWeek (mom) {
+          return weekOfYear(mom, this._week.dow, this._week.doy).week;
+      }
+
+      var defaultLocaleWeek = {
+          dow : 0, // Sunday is the first day of the week.
+          doy : 6  // The week that contains Jan 1st is the first week of the year.
+      };
+
+      function localeFirstDayOfWeek () {
+          return this._week.dow;
+      }
+
+      function localeFirstDayOfYear () {
+          return this._week.doy;
+      }
+
+      // MOMENTS
+
+      function getSetWeek (input) {
+          var week = this.localeData().week(this);
+          return input == null ? week : this.add((input - week) * 7, 'd');
+      }
+
+      function getSetISOWeek (input) {
+          var week = weekOfYear(this, 1, 4).week;
+          return input == null ? week : this.add((input - week) * 7, 'd');
+      }
+
+      addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear');
+
+      // ALIASES
+
+      addUnitAlias('dayOfYear', 'DDD');
+
+      // PARSING
+
+      addRegexToken('DDD',  match1to3);
+      addRegexToken('DDDD', match3);
+      addParseToken(['DDD', 'DDDD'], function (input, array, config) {
+          config._dayOfYear = toInt(input);
+      });
+
+      // HELPERS
+
+      //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
+      function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
+          var week1Jan = 6 + firstDayOfWeek - firstDayOfWeekOfYear, janX = createUTCDate(year, 0, 1 + week1Jan), d = janX.getUTCDay(), dayOfYear;
+          if (d < firstDayOfWeek) {
+              d += 7;
+          }
+
+          weekday = weekday != null ? 1 * weekday : firstDayOfWeek;
+
+          dayOfYear = 1 + week1Jan + 7 * (week - 1) - d + weekday;
+
+          return {
+              year: dayOfYear > 0 ? year : year - 1,
+              dayOfYear: dayOfYear > 0 ?  dayOfYear : daysInYear(year - 1) + dayOfYear
+          };
+      }
+
+      // MOMENTS
+
+      function getSetDayOfYear (input) {
+          var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1;
+          return input == null ? dayOfYear : this.add((input - dayOfYear), 'd');
+      }
+
+      // Pick the first defined of two or three arguments.
+      function defaults(a, b, c) {
+          if (a != null) {
+              return a;
+          }
+          if (b != null) {
+              return b;
+          }
+          return c;
+      }
+
+      function currentDateArray(config) {
+          var now = new Date();
+          if (config._useUTC) {
+              return [now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()];
+          }
+          return [now.getFullYear(), now.getMonth(), now.getDate()];
+      }
+
+      // convert an array to a date.
+      // the array should mirror the parameters below
+      // note: all values past the year are optional and will default to the lowest possible value.
+      // [year, month, day , hour, minute, second, millisecond]
+      function configFromArray (config) {
+          var i, date, input = [], currentDate, yearToUse;
+
+          if (config._d) {
+              return;
+          }
+
+          currentDate = currentDateArray(config);
+
+          //compute day of the year from weeks and weekdays
+          if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
+              dayOfYearFromWeekInfo(config);
+          }
+
+          //if the day of the year is set, figure out what it is
+          if (config._dayOfYear) {
+              yearToUse = defaults(config._a[YEAR], currentDate[YEAR]);
+
+              if (config._dayOfYear > daysInYear(yearToUse)) {
+                  getParsingFlags(config)._overflowDayOfYear = true;
+              }
+
+              date = createUTCDate(yearToUse, 0, config._dayOfYear);
+              config._a[MONTH] = date.getUTCMonth();
+              config._a[DATE] = date.getUTCDate();
+          }
+
+          // Default to current date.
+          // * if no year, month, day of month are given, default to today
+          // * if day of month is given, default month and year
+          // * if month is given, default only year
+          // * if year is given, don't default anything
+          for (i = 0; i < 3 && config._a[i] == null; ++i) {
+              config._a[i] = input[i] = currentDate[i];
+          }
+
+          // Zero out whatever was not defaulted, including time
+          for (; i < 7; i++) {
+              config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
+          }
+
+          // Check for 24:00:00.000
+          if (config._a[HOUR] === 24 &&
+                  config._a[MINUTE] === 0 &&
+                  config._a[SECOND] === 0 &&
+                  config._a[MILLISECOND] === 0) {
+              config._nextDay = true;
+              config._a[HOUR] = 0;
+          }
+
+          config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input);
+          // Apply timezone offset from input. The actual utcOffset can be changed
+          // with parseZone.
+          if (config._tzm != null) {
+              config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm);
+          }
+
+          if (config._nextDay) {
+              config._a[HOUR] = 24;
+          }
+      }
+
+      function dayOfYearFromWeekInfo(config) {
+          var w, weekYear, week, weekday, dow, doy, temp;
+
+          w = config._w;
+          if (w.GG != null || w.W != null || w.E != null) {
+              dow = 1;
+              doy = 4;
+
+              // TODO: We need to take the current isoWeekYear, but that depends on
+              // how we interpret now (local, utc, fixed offset). So create
+              // a now version of current config (take local/utc/offset flags, and
+              // create now).
+              weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(local__createLocal(), 1, 4).year);
+              week = defaults(w.W, 1);
+              weekday = defaults(w.E, 1);
+          } else {
+              dow = config._locale._week.dow;
+              doy = config._locale._week.doy;
+
+              weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year);
+              week = defaults(w.w, 1);
+
+              if (w.d != null) {
+                  // weekday -- low day numbers are considered next week
+                  weekday = w.d;
+                  if (weekday < dow) {
+                      ++week;
+                  }
+              } else if (w.e != null) {
+                  // local weekday -- counting starts from begining of week
+                  weekday = w.e + dow;
+              } else {
+                  // default to begining of week
+                  weekday = dow;
+              }
+          }
+          temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow);
+
+          config._a[YEAR] = temp.year;
+          config._dayOfYear = temp.dayOfYear;
+      }
+
+      utils_hooks__hooks.ISO_8601 = function () {};
+
+      // date from string and format string
+      function configFromStringAndFormat(config) {
+          // TODO: Move this to another part of the creation flow to prevent circular deps
+          if (config._f === utils_hooks__hooks.ISO_8601) {
+              configFromISO(config);
+              return;
+          }
+
+          config._a = [];
+          getParsingFlags(config).empty = true;
+
+          // This array is used to make a Date, either with `new Date` or `Date.UTC`
+          var string = '' + config._i,
+              i, parsedInput, tokens, token, skipped,
+              stringLength = string.length,
+              totalParsedInputLength = 0;
+
+          tokens = expandFormat(config._f, config._locale).match(formattingTokens) || [];
+
+          for (i = 0; i < tokens.length; i++) {
+              token = tokens[i];
+              parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
+              if (parsedInput) {
+                  skipped = string.substr(0, string.indexOf(parsedInput));
+                  if (skipped.length > 0) {
+                      getParsingFlags(config).unusedInput.push(skipped);
+                  }
+                  string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
+                  totalParsedInputLength += parsedInput.length;
+              }
+              // don't parse if it's not a known token
+              if (formatTokenFunctions[token]) {
+                  if (parsedInput) {
+                      getParsingFlags(config).empty = false;
+                  }
+                  else {
+                      getParsingFlags(config).unusedTokens.push(token);
+                  }
+                  addTimeToArrayFromToken(token, parsedInput, config);
+              }
+              else if (config._strict && !parsedInput) {
+                  getParsingFlags(config).unusedTokens.push(token);
+              }
+          }
+
+          // add remaining unparsed input length to the string
+          getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength;
+          if (string.length > 0) {
+              getParsingFlags(config).unusedInput.push(string);
+          }
+
+          // clear _12h flag if hour is <= 12
+          if (getParsingFlags(config).bigHour === true &&
+                  config._a[HOUR] <= 12 &&
+                  config._a[HOUR] > 0) {
+              getParsingFlags(config).bigHour = undefined;
+          }
+          // handle meridiem
+          config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem);
+
+          configFromArray(config);
+          checkOverflow(config);
+      }
+
+
+      function meridiemFixWrap (locale, hour, meridiem) {
+          var isPm;
+
+          if (meridiem == null) {
+              // nothing to do
+              return hour;
+          }
+          if (locale.meridiemHour != null) {
+              return locale.meridiemHour(hour, meridiem);
+          } else if (locale.isPM != null) {
+              // Fallback
+              isPm = locale.isPM(meridiem);
+              if (isPm && hour < 12) {
+                  hour += 12;
+              }
+              if (!isPm && hour === 12) {
+                  hour = 0;
+              }
+              return hour;
+          } else {
+              // this is not supposed to happen
+              return hour;
+          }
+      }
+
+      function configFromStringAndArray(config) {
+          var tempConfig,
+              bestMoment,
+
+              scoreToBeat,
+              i,
+              currentScore;
+
+          if (config._f.length === 0) {
+              getParsingFlags(config).invalidFormat = true;
+              config._d = new Date(NaN);
+              return;
+          }
+
+          for (i = 0; i < config._f.length; i++) {
+              currentScore = 0;
+              tempConfig = copyConfig({}, config);
+              if (config._useUTC != null) {
+                  tempConfig._useUTC = config._useUTC;
+              }
+              tempConfig._f = config._f[i];
+              configFromStringAndFormat(tempConfig);
+
+              if (!valid__isValid(tempConfig)) {
+                  continue;
+              }
+
+              // if there is any input that was not parsed add a penalty for that format
+              currentScore += getParsingFlags(tempConfig).charsLeftOver;
+
+              //or tokens
+              currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10;
+
+              getParsingFlags(tempConfig).score = currentScore;
+
+              if (scoreToBeat == null || currentScore < scoreToBeat) {
+                  scoreToBeat = currentScore;
+                  bestMoment = tempConfig;
+              }
+          }
+
+          extend(config, bestMoment || tempConfig);
+      }
+
+      function configFromObject(config) {
+          if (config._d) {
+              return;
+          }
+
+          var i = normalizeObjectUnits(config._i);
+          config._a = [i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond];
+
+          configFromArray(config);
+      }
+
+      function createFromConfig (config) {
+          var res = new Moment(checkOverflow(prepareConfig(config)));
+          if (res._nextDay) {
+              // Adding is smart enough around DST
+              res.add(1, 'd');
+              res._nextDay = undefined;
+          }
+
+          return res;
+      }
+
+      function prepareConfig (config) {
+          var input = config._i,
+              format = config._f;
+
+          config._locale = config._locale || locale_locales__getLocale(config._l);
+
+          if (input === null || (format === undefined && input === '')) {
+              return valid__createInvalid({nullInput: true});
+          }
+
+          if (typeof input === 'string') {
+              config._i = input = config._locale.preparse(input);
+          }
+
+          if (isMoment(input)) {
+              return new Moment(checkOverflow(input));
+          } else if (isArray(format)) {
+              configFromStringAndArray(config);
+          } else if (format) {
+              configFromStringAndFormat(config);
+          } else if (isDate(input)) {
+              config._d = input;
+          } else {
+              configFromInput(config);
+          }
+
+          return config;
+      }
+
+      function configFromInput(config) {
+          var input = config._i;
+          if (input === undefined) {
+              config._d = new Date();
+          } else if (isDate(input)) {
+              config._d = new Date(+input);
+          } else if (typeof input === 'string') {
+              configFromString(config);
+          } else if (isArray(input)) {
+              config._a = map(input.slice(0), function (obj) {
+                  return parseInt(obj, 10);
+              });
+              configFromArray(config);
+          } else if (typeof(input) === 'object') {
+              configFromObject(config);
+          } else if (typeof(input) === 'number') {
+              // from milliseconds
+              config._d = new Date(input);
+          } else {
+              utils_hooks__hooks.createFromInputFallback(config);
+          }
+      }
+
+      function createLocalOrUTC (input, format, locale, strict, isUTC) {
+          var c = {};
+
+          if (typeof(locale) === 'boolean') {
+              strict = locale;
+              locale = undefined;
+          }
+          // object construction must be done this way.
+          // https://github.com/moment/moment/issues/1423
+          c._isAMomentObject = true;
+          c._useUTC = c._isUTC = isUTC;
+          c._l = locale;
+          c._i = input;
+          c._f = format;
+          c._strict = strict;
+
+          return createFromConfig(c);
+      }
+
+      function local__createLocal (input, format, locale, strict) {
+          return createLocalOrUTC(input, format, locale, strict, false);
+      }
+
+      var prototypeMin = deprecate(
+           'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548',
+           function () {
+               var other = local__createLocal.apply(null, arguments);
+               return other < this ? this : other;
+           }
+       );
+
+      var prototypeMax = deprecate(
+          'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548',
+          function () {
+              var other = local__createLocal.apply(null, arguments);
+              return other > this ? this : other;
+          }
+      );
+
+      // Pick a moment m from moments so that m[fn](other) is true for all
+      // other. This relies on the function fn to be transitive.
+      //
+      // moments should either be an array of moment objects or an array, whose
+      // first element is an array of moment objects.
+      function pickBy(fn, moments) {
+          var res, i;
+          if (moments.length === 1 && isArray(moments[0])) {
+              moments = moments[0];
+          }
+          if (!moments.length) {
+              return local__createLocal();
+          }
+          res = moments[0];
+          for (i = 1; i < moments.length; ++i) {
+              if (!moments[i].isValid() || moments[i][fn](res)) {
+                  res = moments[i];
+              }
+          }
+          return res;
+      }
+
+      // TODO: Use [].sort instead?
+      function min () {
+          var args = [].slice.call(arguments, 0);
+
+          return pickBy('isBefore', args);
+      }
+
+      function max () {
+          var args = [].slice.call(arguments, 0);
+
+          return pickBy('isAfter', args);
+      }
+
+      function Duration (duration) {
+          var normalizedInput = normalizeObjectUnits(duration),
+              years = normalizedInput.year || 0,
+              quarters = normalizedInput.quarter || 0,
+              months = normalizedInput.month || 0,
+              weeks = normalizedInput.week || 0,
+              days = normalizedInput.day || 0,
+              hours = normalizedInput.hour || 0,
+              minutes = normalizedInput.minute || 0,
+              seconds = normalizedInput.second || 0,
+              milliseconds = normalizedInput.millisecond || 0;
+
+          // representation for dateAddRemove
+          this._milliseconds = +milliseconds +
+              seconds * 1e3 + // 1000
+              minutes * 6e4 + // 1000 * 60
+              hours * 36e5; // 1000 * 60 * 60
+          // Because of dateAddRemove treats 24 hours as different from a
+          // day when working around DST, we need to store them separately
+          this._days = +days +
+              weeks * 7;
+          // It is impossible translate months into days without knowing
+          // which months you are are talking about, so we have to store
+          // it separately.
+          this._months = +months +
+              quarters * 3 +
+              years * 12;
+
+          this._data = {};
+
+          this._locale = locale_locales__getLocale();
+
+          this._bubble();
+      }
+
+      function isDuration (obj) {
+          return obj instanceof Duration;
+      }
+
+      function offset (token, separator) {
+          addFormatToken(token, 0, 0, function () {
+              var offset = this.utcOffset();
+              var sign = '+';
+              if (offset < 0) {
+                  offset = -offset;
+                  sign = '-';
+              }
+              return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2);
+          });
+      }
+
+      offset('Z', ':');
+      offset('ZZ', '');
+
+      // PARSING
+
+      addRegexToken('Z',  matchOffset);
+      addRegexToken('ZZ', matchOffset);
+      addParseToken(['Z', 'ZZ'], function (input, array, config) {
+          config._useUTC = true;
+          config._tzm = offsetFromString(input);
+      });
+
+      // HELPERS
+
+      // timezone chunker
+      // '+10:00' > ['10',  '00']
+      // '-1530'  > ['-15', '30']
+      var chunkOffset = /([\+\-]|\d\d)/gi;
+
+      function offsetFromString(string) {
+          var matches = ((string || '').match(matchOffset) || []);
+          var chunk   = matches[matches.length - 1] || [];
+          var parts   = (chunk + '').match(chunkOffset) || ['-', 0, 0];
+          var minutes = +(parts[1] * 60) + toInt(parts[2]);
+
+          return parts[0] === '+' ? minutes : -minutes;
+      }
+
+      // Return a moment from input, that is local/utc/zone equivalent to model.
+      function cloneWithOffset(input, model) {
+          var res, diff;
+          if (model._isUTC) {
+              res = model.clone();
+              diff = (isMoment(input) || isDate(input) ? +input : +local__createLocal(input)) - (+res);
+              // Use low-level api, because this fn is low-level api.
+              res._d.setTime(+res._d + diff);
+              utils_hooks__hooks.updateOffset(res, false);
+              return res;
+          } else {
+              return local__createLocal(input).local();
+          }
+      }
+
+      function getDateOffset (m) {
+          // On Firefox.24 Date#getTimezoneOffset returns a floating point.
+          // https://github.com/moment/moment/pull/1871
+          return -Math.round(m._d.getTimezoneOffset() / 15) * 15;
+      }
+
+      // HOOKS
+
+      // This function will be called whenever a moment is mutated.
+      // It is intended to keep the offset in sync with the timezone.
+      utils_hooks__hooks.updateOffset = function () {};
+
+      // MOMENTS
+
+      // keepLocalTime = true means only change the timezone, without
+      // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]-->
+      // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset
+      // +0200, so we adjust the time as needed, to be valid.
+      //
+      // Keeping the time actually adds/subtracts (one hour)
+      // from the actual represented time. That is why we call updateOffset
+      // a second time. In case it wants us to change the offset again
+      // _changeInProgress == true case, then we have to adjust, because
+      // there is no such time in the given timezone.
+      function getSetOffset (input, keepLocalTime) {
+          var offset = this._offset || 0,
+              localAdjust;
+          if (input != null) {
+              if (typeof input === 'string') {
+                  input = offsetFromString(input);
+              }
+              if (Math.abs(input) < 16) {
+                  input = input * 60;
+              }
+              if (!this._isUTC && keepLocalTime) {
+                  localAdjust = getDateOffset(this);
+              }
+              this._offset = input;
+              this._isUTC = true;
+              if (localAdjust != null) {
+                  this.add(localAdjust, 'm');
+              }
+              if (offset !== input) {
+                  if (!keepLocalTime || this._changeInProgress) {
+                      add_subtract__addSubtract(this, create__createDuration(input - offset, 'm'), 1, false);
+                  } else if (!this._changeInProgress) {
+                      this._changeInProgress = true;
+                      utils_hooks__hooks.updateOffset(this, true);
+                      this._changeInProgress = null;
+                  }
+              }
+              return this;
+          } else {
+              return this._isUTC ? offset : getDateOffset(this);
+          }
+      }
+
+      function getSetZone (input, keepLocalTime) {
+          if (input != null) {
+              if (typeof input !== 'string') {
+                  input = -input;
+              }
+
+              this.utcOffset(input, keepLocalTime);
+
+              return this;
+          } else {
+              return -this.utcOffset();
+          }
+      }
+
+      function setOffsetToUTC (keepLocalTime) {
+          return this.utcOffset(0, keepLocalTime);
+      }
+
+      function setOffsetToLocal (keepLocalTime) {
+          if (this._isUTC) {
+              this.utcOffset(0, keepLocalTime);
+              this._isUTC = false;
+
+              if (keepLocalTime) {
+                  this.subtract(getDateOffset(this), 'm');
+              }
+          }
+          return this;
+      }
+
+      function setOffsetToParsedOffset () {
+          if (this._tzm) {
+              this.utcOffset(this._tzm);
+          } else if (typeof this._i === 'string') {
+              this.utcOffset(offsetFromString(this._i));
+          }
+          return this;
+      }
+
+      function hasAlignedHourOffset (input) {
+          input = input ? local__createLocal(input).utcOffset() : 0;
+
+          return (this.utcOffset() - input) % 60 === 0;
+      }
+
+      function isDaylightSavingTime () {
+          return (
+              this.utcOffset() > this.clone().month(0).utcOffset() ||
+              this.utcOffset() > this.clone().month(5).utcOffset()
+          );
+      }
+
+      function isDaylightSavingTimeShifted () {
+          if (typeof this._isDSTShifted !== 'undefined') {
+              return this._isDSTShifted;
+          }
+
+          var c = {};
+
+          copyConfig(c, this);
+          c = prepareConfig(c);
+
+          if (c._a) {
+              var other = c._isUTC ? create_utc__createUTC(c._a) : local__createLocal(c._a);
+              this._isDSTShifted = this.isValid() &&
+                  compareArrays(c._a, other.toArray()) > 0;
+          } else {
+              this._isDSTShifted = false;
+          }
+
+          return this._isDSTShifted;
+      }
+
+      function isLocal () {
+          return !this._isUTC;
+      }
+
+      function isUtcOffset () {
+          return this._isUTC;
+      }
+
+      function isUtc () {
+          return this._isUTC && this._offset === 0;
+      }
+
+      var aspNetRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/;
+
+      // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
+      // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
+      var create__isoRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/;
+
+      function create__createDuration (input, key) {
+          var duration = input,
+              // matching against regexp is expensive, do it on demand
+              match = null,
+              sign,
+              ret,
+              diffRes;
+
+          if (isDuration(input)) {
+              duration = {
+                  ms : input._milliseconds,
+                  d  : input._days,
+                  M  : input._months
+              };
+          } else if (typeof input === 'number') {
+              duration = {};
+              if (key) {
+                  duration[key] = input;
+              } else {
+                  duration.milliseconds = input;
+              }
+          } else if (!!(match = aspNetRegex.exec(input))) {
+              sign = (match[1] === '-') ? -1 : 1;
+              duration = {
+                  y  : 0,
+                  d  : toInt(match[DATE])        * sign,
+                  h  : toInt(match[HOUR])        * sign,
+                  m  : toInt(match[MINUTE])      * sign,
+                  s  : toInt(match[SECOND])      * sign,
+                  ms : toInt(match[MILLISECOND]) * sign
+              };
+          } else if (!!(match = create__isoRegex.exec(input))) {
+              sign = (match[1] === '-') ? -1 : 1;
+              duration = {
+                  y : parseIso(match[2], sign),
+                  M : parseIso(match[3], sign),
+                  d : parseIso(match[4], sign),
+                  h : parseIso(match[5], sign),
+                  m : parseIso(match[6], sign),
+                  s : parseIso(match[7], sign),
+                  w : parseIso(match[8], sign)
+              };
+          } else if (duration == null) {// checks for null or undefined
+              duration = {};
+          } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) {
+              diffRes = momentsDifference(local__createLocal(duration.from), local__createLocal(duration.to));
+
+              duration = {};
+              duration.ms = diffRes.milliseconds;
+              duration.M = diffRes.months;
+          }
+
+          ret = new Duration(duration);
+
+          if (isDuration(input) && hasOwnProp(input, '_locale')) {
+              ret._locale = input._locale;
+          }
+
+          return ret;
+      }
+
+      create__createDuration.fn = Duration.prototype;
+
+      function parseIso (inp, sign) {
+          // We'd normally use ~~inp for this, but unfortunately it also
+          // converts floats to ints.
+          // inp may be undefined, so careful calling replace on it.
+          var res = inp && parseFloat(inp.replace(',', '.'));
+          // apply sign while we're at it
+          return (isNaN(res) ? 0 : res) * sign;
+      }
+
+      function positiveMomentsDifference(base, other) {
+          var res = {milliseconds: 0, months: 0};
+
+          res.months = other.month() - base.month() +
+              (other.year() - base.year()) * 12;
+          if (base.clone().add(res.months, 'M').isAfter(other)) {
+              --res.months;
+          }
+
+          res.milliseconds = +other - +(base.clone().add(res.months, 'M'));
+
+          return res;
+      }
+
+      function momentsDifference(base, other) {
+          var res;
+          other = cloneWithOffset(other, base);
+          if (base.isBefore(other)) {
+              res = positiveMomentsDifference(base, other);
+          } else {
+              res = positiveMomentsDifference(other, base);
+              res.milliseconds = -res.milliseconds;
+              res.months = -res.months;
+          }
+
+          return res;
+      }
+
+      function createAdder(direction, name) {
+          return function (val, period) {
+              var dur, tmp;
+              //invert the arguments, but complain about it
+              if (period !== null && !isNaN(+period)) {
+                  deprecateSimple(name, 'moment().' + name  + '(period, number) is deprecated. Please use moment().' + name + '(number, period).');
+                  tmp = val; val = period; period = tmp;
+              }
+
+              val = typeof val === 'string' ? +val : val;
+              dur = create__createDuration(val, period);
+              add_subtract__addSubtract(this, dur, direction);
+              return this;
+          };
+      }
+
+      function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) {
+          var milliseconds = duration._milliseconds,
+              days = duration._days,
+              months = duration._months;
+          updateOffset = updateOffset == null ? true : updateOffset;
+
+          if (milliseconds) {
+              mom._d.setTime(+mom._d + milliseconds * isAdding);
+          }
+          if (days) {
+              get_set__set(mom, 'Date', get_set__get(mom, 'Date') + days * isAdding);
+          }
+          if (months) {
+              setMonth(mom, get_set__get(mom, 'Month') + months * isAdding);
+          }
+          if (updateOffset) {
+              utils_hooks__hooks.updateOffset(mom, days || months);
+          }
+      }
+
+      var add_subtract__add      = createAdder(1, 'add');
+      var add_subtract__subtract = createAdder(-1, 'subtract');
+
+      function moment_calendar__calendar (time, formats) {
+          // We want to compare the start of today, vs this.
+          // Getting start-of-today depends on whether we're local/utc/offset or not.
+          var now = time || local__createLocal(),
+              sod = cloneWithOffset(now, this).startOf('day'),
+              diff = this.diff(sod, 'days', true),
+              format = diff < -6 ? 'sameElse' :
+                  diff < -1 ? 'lastWeek' :
+                  diff < 0 ? 'lastDay' :
+                  diff < 1 ? 'sameDay' :
+                  diff < 2 ? 'nextDay' :
+                  diff < 7 ? 'nextWeek' : 'sameElse';
+          return this.format(formats && formats[format] || this.localeData().calendar(format, this, local__createLocal(now)));
+      }
+
+      function clone () {
+          return new Moment(this);
+      }
+
+      function isAfter (input, units) {
+          var inputMs;
+          units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond');
+          if (units === 'millisecond') {
+              input = isMoment(input) ? input : local__createLocal(input);
+              return +this > +input;
+          } else {
+              inputMs = isMoment(input) ? +input : +local__createLocal(input);
+              return inputMs < +this.clone().startOf(units);
+          }
+      }
+
+      function isBefore (input, units) {
+          var inputMs;
+          units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond');
+          if (units === 'millisecond') {
+              input = isMoment(input) ? input : local__createLocal(input);
+              return +this < +input;
+          } else {
+              inputMs = isMoment(input) ? +input : +local__createLocal(input);
+              return +this.clone().endOf(units) < inputMs;
+          }
+      }
+
+      function isBetween (from, to, units) {
+          return this.isAfter(from, units) && this.isBefore(to, units);
+      }
+
+      function isSame (input, units) {
+          var inputMs;
+          units = normalizeUnits(units || 'millisecond');
+          if (units === 'millisecond') {
+              input = isMoment(input) ? input : local__createLocal(input);
+              return +this === +input;
+          } else {
+              inputMs = +local__createLocal(input);
+              return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units));
+          }
+      }
+
+      function diff (input, units, asFloat) {
+          var that = cloneWithOffset(input, this),
+              zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4,
+              delta, output;
+
+          units = normalizeUnits(units);
+
+          if (units === 'year' || units === 'month' || units === 'quarter') {
+              output = monthDiff(this, that);
+              if (units === 'quarter') {
+                  output = output / 3;
+              } else if (units === 'year') {
+                  output = output / 12;
+              }
+          } else {
+              delta = this - that;
+              output = units === 'second' ? delta / 1e3 : // 1000
+                  units === 'minute' ? delta / 6e4 : // 1000 * 60
+                  units === 'hour' ? delta / 36e5 : // 1000 * 60 * 60
+                  units === 'day' ? (delta - zoneDelta) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
+                  units === 'week' ? (delta - zoneDelta) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
+                  delta;
+          }
+          return asFloat ? output : absFloor(output);
+      }
+
+      function monthDiff (a, b) {
+          // difference in months
+          var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()),
+              // b is in (anchor - 1 month, anchor + 1 month)
+              anchor = a.clone().add(wholeMonthDiff, 'months'),
+              anchor2, adjust;
+
+          if (b - anchor < 0) {
+              anchor2 = a.clone().add(wholeMonthDiff - 1, 'months');
+              // linear across the month
+              adjust = (b - anchor) / (anchor - anchor2);
+          } else {
+              anchor2 = a.clone().add(wholeMonthDiff + 1, 'months');
+              // linear across the month
+              adjust = (b - anchor) / (anchor2 - anchor);
+          }
+
+          return -(wholeMonthDiff + adjust);
+      }
+
+      utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ';
+
+      function toString () {
+          return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ');
+      }
+
+      function moment_format__toISOString () {
+          var m = this.clone().utc();
+          if (0 < m.year() && m.year() <= 9999) {
+              if ('function' === typeof Date.prototype.toISOString) {
+                  // native implementation is ~50x faster, use it when we can
+                  return this.toDate().toISOString();
+              } else {
+                  return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+              }
+          } else {
+              return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+          }
+      }
+
+      function format (inputString) {
+          var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat);
+          return this.localeData().postformat(output);
+      }
+
+      function from (time, withoutSuffix) {
+          if (!this.isValid()) {
+              return this.localeData().invalidDate();
+          }
+          return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix);
+      }
+
+      function fromNow (withoutSuffix) {
+          return this.from(local__createLocal(), withoutSuffix);
+      }
+
+      function to (time, withoutSuffix) {
+          if (!this.isValid()) {
+              return this.localeData().invalidDate();
+          }
+          return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix);
+      }
+
+      function toNow (withoutSuffix) {
+          return this.to(local__createLocal(), withoutSuffix);
+      }
+
+      function locale (key) {
+          var newLocaleData;
+
+          if (key === undefined) {
+              return this._locale._abbr;
+          } else {
+              newLocaleData = locale_locales__getLocale(key);
+              if (newLocaleData != null) {
+                  this._locale = newLocaleData;
+              }
+              return this;
+          }
+      }
+
+      var lang = deprecate(
+          'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.',
+          function (key) {
+              if (key === undefined) {
+                  return this.localeData();
+              } else {
+                  return this.locale(key);
+              }
+          }
+      );
+
+      function localeData () {
+          return this._locale;
+      }
+
+      function startOf (units) {
+          units = normalizeUnits(units);
+          // the following switch intentionally omits break keywords
+          // to utilize falling through the cases.
+          switch (units) {
+          case 'year':
+              this.month(0);
+              /* falls through */
+          case 'quarter':
+          case 'month':
+              this.date(1);
+              /* falls through */
+          case 'week':
+          case 'isoWeek':
+          case 'day':
+              this.hours(0);
+              /* falls through */
+          case 'hour':
+              this.minutes(0);
+              /* falls through */
+          case 'minute':
+              this.seconds(0);
+              /* falls through */
+          case 'second':
+              this.milliseconds(0);
+          }
+
+          // weeks are a special case
+          if (units === 'week') {
+              this.weekday(0);
+          }
+          if (units === 'isoWeek') {
+              this.isoWeekday(1);
+          }
+
+          // quarters are also special
+          if (units === 'quarter') {
+              this.month(Math.floor(this.month() / 3) * 3);
+          }
+
+          return this;
+      }
+
+      function endOf (units) {
+          units = normalizeUnits(units);
+          if (units === undefined || units === 'millisecond') {
+              return this;
+          }
+          return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms');
+      }
+
+      function to_type__valueOf () {
+          return +this._d - ((this._offset || 0) * 60000);
+      }
+
+      function unix () {
+          return Math.floor(+this / 1000);
+      }
+
+      function toDate () {
+          return this._offset ? new Date(+this) : this._d;
+      }
+
+      function toArray () {
+          var m = this;
+          return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()];
+      }
+
+      function toObject () {
+          var m = this;
+          return {
+              years: m.year(),
+              months: m.month(),
+              date: m.date(),
+              hours: m.hours(),
+              minutes: m.minutes(),
+              seconds: m.seconds(),
+              milliseconds: m.milliseconds()
+          };
+      }
+
+      function moment_valid__isValid () {
+          return valid__isValid(this);
+      }
+
+      function parsingFlags () {
+          return extend({}, getParsingFlags(this));
+      }
+
+      function invalidAt () {
+          return getParsingFlags(this).overflow;
+      }
+
+      addFormatToken(0, ['gg', 2], 0, function () {
+          return this.weekYear() % 100;
+      });
+
+      addFormatToken(0, ['GG', 2], 0, function () {
+          return this.isoWeekYear() % 100;
+      });
+
+      function addWeekYearFormatToken (token, getter) {
+          addFormatToken(0, [token, token.length], 0, getter);
+      }
+
+      addWeekYearFormatToken('gggg',     'weekYear');
+      addWeekYearFormatToken('ggggg',    'weekYear');
+      addWeekYearFormatToken('GGGG',  'isoWeekYear');
+      addWeekYearFormatToken('GGGGG', 'isoWeekYear');
+
+      // ALIASES
+
+      addUnitAlias('weekYear', 'gg');
+      addUnitAlias('isoWeekYear', 'GG');
+
+      // PARSING
+
+      addRegexToken('G',      matchSigned);
+      addRegexToken('g',      matchSigned);
+      addRegexToken('GG',     match1to2, match2);
+      addRegexToken('gg',     match1to2, match2);
+      addRegexToken('GGGG',   match1to4, match4);
+      addRegexToken('gggg',   match1to4, match4);
+      addRegexToken('GGGGG',  match1to6, match6);
+      addRegexToken('ggggg',  match1to6, match6);
+
+      addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) {
+          week[token.substr(0, 2)] = toInt(input);
+      });
+
+      addWeekParseToken(['gg', 'GG'], function (input, week, config, token) {
+          week[token] = utils_hooks__hooks.parseTwoDigitYear(input);
+      });
+
+      // HELPERS
+
+      function weeksInYear(year, dow, doy) {
+          return weekOfYear(local__createLocal([year, 11, 31 + dow - doy]), dow, doy).week;
+      }
+
+      // MOMENTS
+
+      function getSetWeekYear (input) {
+          var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year;
+          return input == null ? year : this.add((input - year), 'y');
+      }
+
+      function getSetISOWeekYear (input) {
+          var year = weekOfYear(this, 1, 4).year;
+          return input == null ? year : this.add((input - year), 'y');
+      }
+
+      function getISOWeeksInYear () {
+          return weeksInYear(this.year(), 1, 4);
+      }
+
+      function getWeeksInYear () {
+          var weekInfo = this.localeData()._week;
+          return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
+      }
+
+      addFormatToken('Q', 0, 0, 'quarter');
+
+      // ALIASES
+
+      addUnitAlias('quarter', 'Q');
+
+      // PARSING
+
+      addRegexToken('Q', match1);
+      addParseToken('Q', function (input, array) {
+          array[MONTH] = (toInt(input) - 1) * 3;
+      });
+
+      // MOMENTS
+
+      function getSetQuarter (input) {
+          return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
+      }
+
+      addFormatToken('D', ['DD', 2], 'Do', 'date');
+
+      // ALIASES
+
+      addUnitAlias('date', 'D');
+
+      // PARSING
+
+      addRegexToken('D',  match1to2);
+      addRegexToken('DD', match1to2, match2);
+      addRegexToken('Do', function (isStrict, locale) {
+          return isStrict ? locale._ordinalParse : locale._ordinalParseLenient;
+      });
+
+      addParseToken(['D', 'DD'], DATE);
+      addParseToken('Do', function (input, array) {
+          array[DATE] = toInt(input.match(match1to2)[0], 10);
+      });
+
+      // MOMENTS
+
+      var getSetDayOfMonth = makeGetSet('Date', true);
+
+      addFormatToken('d', 0, 'do', 'day');
+
+      addFormatToken('dd', 0, 0, function (format) {
+          return this.localeData().weekdaysMin(this, format);
+      });
+
+      addFormatToken('ddd', 0, 0, function (format) {
+          return this.localeData().weekdaysShort(this, format);
+      });
+
+      addFormatToken('dddd', 0, 0, function (format) {
+          return this.localeData().weekdays(this, format);
+      });
+
+      addFormatToken('e', 0, 0, 'weekday');
+      addFormatToken('E', 0, 0, 'isoWeekday');
+
+      // ALIASES
+
+      addUnitAlias('day', 'd');
+      addUnitAlias('weekday', 'e');
+      addUnitAlias('isoWeekday', 'E');
+
+      // PARSING
+
+      addRegexToken('d',    match1to2);
+      addRegexToken('e',    match1to2);
+      addRegexToken('E',    match1to2);
+      addRegexToken('dd',   matchWord);
+      addRegexToken('ddd',  matchWord);
+      addRegexToken('dddd', matchWord);
+
+      addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config) {
+          var weekday = config._locale.weekdaysParse(input);
+          // if we didn't get a weekday name, mark the date as invalid
+          if (weekday != null) {
+              week.d = weekday;
+          } else {
+              getParsingFlags(config).invalidWeekday = input;
+          }
+      });
+
+      addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) {
+          week[token] = toInt(input);
+      });
+
+      // HELPERS
+
+      function parseWeekday(input, locale) {
+          if (typeof input !== 'string') {
+              return input;
+          }
+
+          if (!isNaN(input)) {
+              return parseInt(input, 10);
+          }
+
+          input = locale.weekdaysParse(input);
+          if (typeof input === 'number') {
+              return input;
+          }
+
+          return null;
+      }
+
+      // LOCALES
+
+      var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_');
+      function localeWeekdays (m) {
+          return this._weekdays[m.day()];
+      }
+
+      var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_');
+      function localeWeekdaysShort (m) {
+          return this._weekdaysShort[m.day()];
+      }
+
+      var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_');
+      function localeWeekdaysMin (m) {
+          return this._weekdaysMin[m.day()];
+      }
+
+      function localeWeekdaysParse (weekdayName) {
+          var i, mom, regex;
+
+          this._weekdaysParse = this._weekdaysParse || [];
+
+          for (i = 0; i < 7; i++) {
+              // make the regex if we don't have it already
+              if (!this._weekdaysParse[i]) {
+                  mom = local__createLocal([2000, 1]).day(i);
+                  regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
+                  this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
+              }
+              // test the regex
+              if (this._weekdaysParse[i].test(weekdayName)) {
+                  return i;
+              }
+          }
+      }
+
+      // MOMENTS
+
+      function getSetDayOfWeek (input) {
+          var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
+          if (input != null) {
+              input = parseWeekday(input, this.localeData());
+              return this.add(input - day, 'd');
+          } else {
+              return day;
+          }
+      }
+
+      function getSetLocaleDayOfWeek (input) {
+          var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7;
+          return input == null ? weekday : this.add(input - weekday, 'd');
+      }
+
+      function getSetISODayOfWeek (input) {
+          // behaves the same as moment#day except
+          // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
+          // as a setter, sunday should belong to the previous week.
+          return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
+      }
+
+      addFormatToken('H', ['HH', 2], 0, 'hour');
+      addFormatToken('h', ['hh', 2], 0, function () {
+          return this.hours() % 12 || 12;
+      });
+
+      function meridiem (token, lowercase) {
+          addFormatToken(token, 0, 0, function () {
+              return this.localeData().meridiem(this.hours(), this.minutes(), lowercase);
+          });
+      }
+
+      meridiem('a', true);
+      meridiem('A', false);
+
+      // ALIASES
+
+      addUnitAlias('hour', 'h');
+
+      // PARSING
+
+      function matchMeridiem (isStrict, locale) {
+          return locale._meridiemParse;
+      }
+
+      addRegexToken('a',  matchMeridiem);
+      addRegexToken('A',  matchMeridiem);
+      addRegexToken('H',  match1to2);
+      addRegexToken('h',  match1to2);
+      addRegexToken('HH', match1to2, match2);
+      addRegexToken('hh', match1to2, match2);
+
+      addParseToken(['H', 'HH'], HOUR);
+      addParseToken(['a', 'A'], function (input, array, config) {
+          config._isPm = config._locale.isPM(input);
+          config._meridiem = input;
+      });
+      addParseToken(['h', 'hh'], function (input, array, config) {
+          array[HOUR] = toInt(input);
+          getParsingFlags(config).bigHour = true;
+      });
+
+      // LOCALES
+
+      function localeIsPM (input) {
+          // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
+          // Using charAt should be more compatible.
+          return ((input + '').toLowerCase().charAt(0) === 'p');
+      }
+
+      var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i;
+      function localeMeridiem (hours, minutes, isLower) {
+          if (hours > 11) {
+              return isLower ? 'pm' : 'PM';
+          } else {
+              return isLower ? 'am' : 'AM';
+          }
+      }
+
+
+      // MOMENTS
+
+      // Setting the hour should keep the time, because the user explicitly
+      // specified which hour he wants. So trying to maintain the same hour (in
+      // a new timezone) makes sense. Adding/subtracting hours does not follow
+      // this rule.
+      var getSetHour = makeGetSet('Hours', true);
+
+      addFormatToken('m', ['mm', 2], 0, 'minute');
+
+      // ALIASES
+
+      addUnitAlias('minute', 'm');
+
+      // PARSING
+
+      addRegexToken('m',  match1to2);
+      addRegexToken('mm', match1to2, match2);
+      addParseToken(['m', 'mm'], MINUTE);
+
+      // MOMENTS
+
+      var getSetMinute = makeGetSet('Minutes', false);
+
+      addFormatToken('s', ['ss', 2], 0, 'second');
+
+      // ALIASES
+
+      addUnitAlias('second', 's');
+
+      // PARSING
+
+      addRegexToken('s',  match1to2);
+      addRegexToken('ss', match1to2, match2);
+      addParseToken(['s', 'ss'], SECOND);
+
+      // MOMENTS
+
+      var getSetSecond = makeGetSet('Seconds', false);
+
+      addFormatToken('S', 0, 0, function () {
+          return ~~(this.millisecond() / 100);
+      });
+
+      addFormatToken(0, ['SS', 2], 0, function () {
+          return ~~(this.millisecond() / 10);
+      });
+
+      addFormatToken(0, ['SSS', 3], 0, 'millisecond');
+      addFormatToken(0, ['SSSS', 4], 0, function () {
+          return this.millisecond() * 10;
+      });
+      addFormatToken(0, ['SSSSS', 5], 0, function () {
+          return this.millisecond() * 100;
+      });
+      addFormatToken(0, ['SSSSSS', 6], 0, function () {
+          return this.millisecond() * 1000;
+      });
+      addFormatToken(0, ['SSSSSSS', 7], 0, function () {
+          return this.millisecond() * 10000;
+      });
+      addFormatToken(0, ['SSSSSSSS', 8], 0, function () {
+          return this.millisecond() * 100000;
+      });
+      addFormatToken(0, ['SSSSSSSSS', 9], 0, function () {
+          return this.millisecond() * 1000000;
+      });
+
+
+      // ALIASES
+
+      addUnitAlias('millisecond', 'ms');
+
+      // PARSING
+
+      addRegexToken('S',    match1to3, match1);
+      addRegexToken('SS',   match1to3, match2);
+      addRegexToken('SSS',  match1to3, match3);
+
+      var token;
+      for (token = 'SSSS'; token.length <= 9; token += 'S') {
+          addRegexToken(token, matchUnsigned);
+      }
+
+      function parseMs(input, array) {
+          array[MILLISECOND] = toInt(('0.' + input) * 1000);
+      }
+
+      for (token = 'S'; token.length <= 9; token += 'S') {
+          addParseToken(token, parseMs);
+      }
+      // MOMENTS
+
+      var getSetMillisecond = makeGetSet('Milliseconds', false);
+
+      addFormatToken('z',  0, 0, 'zoneAbbr');
+      addFormatToken('zz', 0, 0, 'zoneName');
+
+      // MOMENTS
+
+      function getZoneAbbr () {
+          return this._isUTC ? 'UTC' : '';
+      }
+
+      function getZoneName () {
+          return this._isUTC ? 'Coordinated Universal Time' : '';
+      }
+
+      var momentPrototype__proto = Moment.prototype;
+
+      momentPrototype__proto.add          = add_subtract__add;
+      momentPrototype__proto.calendar     = moment_calendar__calendar;
+      momentPrototype__proto.clone        = clone;
+      momentPrototype__proto.diff         = diff;
+      momentPrototype__proto.endOf        = endOf;
+      momentPrototype__proto.format       = format;
+      momentPrototype__proto.from         = from;
+      momentPrototype__proto.fromNow      = fromNow;
+      momentPrototype__proto.to           = to;
+      momentPrototype__proto.toNow        = toNow;
+      momentPrototype__proto.get          = getSet;
+      momentPrototype__proto.invalidAt    = invalidAt;
+      momentPrototype__proto.isAfter      = isAfter;
+      momentPrototype__proto.isBefore     = isBefore;
+      momentPrototype__proto.isBetween    = isBetween;
+      momentPrototype__proto.isSame       = isSame;
+      momentPrototype__proto.isValid      = moment_valid__isValid;
+      momentPrototype__proto.lang         = lang;
+      momentPrototype__proto.locale       = locale;
+      momentPrototype__proto.localeData   = localeData;
+      momentPrototype__proto.max          = prototypeMax;
+      momentPrototype__proto.min          = prototypeMin;
+      momentPrototype__proto.parsingFlags = parsingFlags;
+      momentPrototype__proto.set          = getSet;
+      momentPrototype__proto.startOf      = startOf;
+      momentPrototype__proto.subtract     = add_subtract__subtract;
+      momentPrototype__proto.toArray      = toArray;
+      momentPrototype__proto.toObject     = toObject;
+      momentPrototype__proto.toDate       = toDate;
+      momentPrototype__proto.toISOString  = moment_format__toISOString;
+      momentPrototype__proto.toJSON       = moment_format__toISOString;
+      momentPrototype__proto.toString     = toString;
+      momentPrototype__proto.unix         = unix;
+      momentPrototype__proto.valueOf      = to_type__valueOf;
+
+      // Year
+      momentPrototype__proto.year       = getSetYear;
+      momentPrototype__proto.isLeapYear = getIsLeapYear;
+
+      // Week Year
+      momentPrototype__proto.weekYear    = getSetWeekYear;
+      momentPrototype__proto.isoWeekYear = getSetISOWeekYear;
+
+      // Quarter
+      momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter;
+
+      // Month
+      momentPrototype__proto.month       = getSetMonth;
+      momentPrototype__proto.daysInMonth = getDaysInMonth;
+
+      // Week
+      momentPrototype__proto.week           = momentPrototype__proto.weeks        = getSetWeek;
+      momentPrototype__proto.isoWeek        = momentPrototype__proto.isoWeeks     = getSetISOWeek;
+      momentPrototype__proto.weeksInYear    = getWeeksInYear;
+      momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear;
+
+      // Day
+      momentPrototype__proto.date       = getSetDayOfMonth;
+      momentPrototype__proto.day        = momentPrototype__proto.days             = getSetDayOfWeek;
+      momentPrototype__proto.weekday    = getSetLocaleDayOfWeek;
+      momentPrototype__proto.isoWeekday = getSetISODayOfWeek;
+      momentPrototype__proto.dayOfYear  = getSetDayOfYear;
+
+      // Hour
+      momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour;
+
+      // Minute
+      momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute;
+
+      // Second
+      momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond;
+
+      // Millisecond
+      momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond;
+
+      // Offset
+      momentPrototype__proto.utcOffset            = getSetOffset;
+      momentPrototype__proto.utc                  = setOffsetToUTC;
+      momentPrototype__proto.local                = setOffsetToLocal;
+      momentPrototype__proto.parseZone            = setOffsetToParsedOffset;
+      momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset;
+      momentPrototype__proto.isDST                = isDaylightSavingTime;
+      momentPrototype__proto.isDSTShifted         = isDaylightSavingTimeShifted;
+      momentPrototype__proto.isLocal              = isLocal;
+      momentPrototype__proto.isUtcOffset          = isUtcOffset;
+      momentPrototype__proto.isUtc                = isUtc;
+      momentPrototype__proto.isUTC                = isUtc;
+
+      // Timezone
+      momentPrototype__proto.zoneAbbr = getZoneAbbr;
+      momentPrototype__proto.zoneName = getZoneName;
+
+      // Deprecations
+      momentPrototype__proto.dates  = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth);
+      momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth);
+      momentPrototype__proto.years  = deprecate('years accessor is deprecated. Use year instead', getSetYear);
+      momentPrototype__proto.zone   = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone);
+
+      var momentPrototype = momentPrototype__proto;
+
+      function moment__createUnix (input) {
+          return local__createLocal(input * 1000);
+      }
+
+      function moment__createInZone () {
+          return local__createLocal.apply(null, arguments).parseZone();
+      }
+
+      var defaultCalendar = {
+          sameDay : '[Today at] LT',
+          nextDay : '[Tomorrow at] LT',
+          nextWeek : 'dddd [at] LT',
+          lastDay : '[Yesterday at] LT',
+          lastWeek : '[Last] dddd [at] LT',
+          sameElse : 'L'
+      };
+
+      function locale_calendar__calendar (key, mom, now) {
+          var output = this._calendar[key];
+          return typeof output === 'function' ? output.call(mom, now) : output;
+      }
+
+      var defaultLongDateFormat = {
+          LTS  : 'h:mm:ss A',
+          LT   : 'h:mm A',
+          L    : 'MM/DD/YYYY',
+          LL   : 'MMMM D, YYYY',
+          LLL  : 'MMMM D, YYYY h:mm A',
+          LLLL : 'dddd, MMMM D, YYYY h:mm A'
+      };
+
+      function longDateFormat (key) {
+          var format = this._longDateFormat[key],
+              formatUpper = this._longDateFormat[key.toUpperCase()];
+
+          if (format || !formatUpper) {
+              return format;
+          }
+
+          this._longDateFormat[key] = formatUpper.replace(/MMMM|MM|DD|dddd/g, function (val) {
+              return val.slice(1);
+          });
+
+          return this._longDateFormat[key];
+      }
+
+      var defaultInvalidDate = 'Invalid date';
+
+      function invalidDate () {
+          return this._invalidDate;
+      }
+
+      var defaultOrdinal = '%d';
+      var defaultOrdinalParse = /\d{1,2}/;
+
+      function ordinal (number) {
+          return this._ordinal.replace('%d', number);
+      }
+
+      function preParsePostFormat (string) {
+          return string;
+      }
+
+      var defaultRelativeTime = {
+          future : 'in %s',
+          past   : '%s ago',
+          s  : 'a few seconds',
+          m  : 'a minute',
+          mm : '%d minutes',
+          h  : 'an hour',
+          hh : '%d hours',
+          d  : 'a day',
+          dd : '%d days',
+          M  : 'a month',
+          MM : '%d months',
+          y  : 'a year',
+          yy : '%d years'
+      };
+
+      function relative__relativeTime (number, withoutSuffix, string, isFuture) {
+          var output = this._relativeTime[string];
+          return (typeof output === 'function') ?
+              output(number, withoutSuffix, string, isFuture) :
+              output.replace(/%d/i, number);
+      }
+
+      function pastFuture (diff, output) {
+          var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
+          return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
+      }
+
+      function locale_set__set (config) {
+          var prop, i;
+          for (i in config) {
+              prop = config[i];
+              if (typeof prop === 'function') {
+                  this[i] = prop;
+              } else {
+                  this['_' + i] = prop;
+              }
+          }
+          // Lenient ordinal parsing accepts just a number in addition to
+          // number + (possibly) stuff coming from _ordinalParseLenient.
+          this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source);
+      }
+
+      var prototype__proto = Locale.prototype;
+
+      prototype__proto._calendar       = defaultCalendar;
+      prototype__proto.calendar        = locale_calendar__calendar;
+      prototype__proto._longDateFormat = defaultLongDateFormat;
+      prototype__proto.longDateFormat  = longDateFormat;
+      prototype__proto._invalidDate    = defaultInvalidDate;
+      prototype__proto.invalidDate     = invalidDate;
+      prototype__proto._ordinal        = defaultOrdinal;
+      prototype__proto.ordinal         = ordinal;
+      prototype__proto._ordinalParse   = defaultOrdinalParse;
+      prototype__proto.preparse        = preParsePostFormat;
+      prototype__proto.postformat      = preParsePostFormat;
+      prototype__proto._relativeTime   = defaultRelativeTime;
+      prototype__proto.relativeTime    = relative__relativeTime;
+      prototype__proto.pastFuture      = pastFuture;
+      prototype__proto.set             = locale_set__set;
+
+      // Month
+      prototype__proto.months       =        localeMonths;
+      prototype__proto._months      = defaultLocaleMonths;
+      prototype__proto.monthsShort  =        localeMonthsShort;
+      prototype__proto._monthsShort = defaultLocaleMonthsShort;
+      prototype__proto.monthsParse  =        localeMonthsParse;
+
+      // Week
+      prototype__proto.week = localeWeek;
+      prototype__proto._week = defaultLocaleWeek;
+      prototype__proto.firstDayOfYear = localeFirstDayOfYear;
+      prototype__proto.firstDayOfWeek = localeFirstDayOfWeek;
+
+      // Day of Week
+      prototype__proto.weekdays       =        localeWeekdays;
+      prototype__proto._weekdays      = defaultLocaleWeekdays;
+      prototype__proto.weekdaysMin    =        localeWeekdaysMin;
+      prototype__proto._weekdaysMin   = defaultLocaleWeekdaysMin;
+      prototype__proto.weekdaysShort  =        localeWeekdaysShort;
+      prototype__proto._weekdaysShort = defaultLocaleWeekdaysShort;
+      prototype__proto.weekdaysParse  =        localeWeekdaysParse;
+
+      // Hours
+      prototype__proto.isPM = localeIsPM;
+      prototype__proto._meridiemParse = defaultLocaleMeridiemParse;
+      prototype__proto.meridiem = localeMeridiem;
+
+      function lists__get (format, index, field, setter) {
+          var locale = locale_locales__getLocale();
+          var utc = create_utc__createUTC().set(setter, index);
+          return locale[field](utc, format);
+      }
+
+      function list (format, index, field, count, setter) {
+          if (typeof format === 'number') {
+              index = format;
+              format = undefined;
+          }
+
+          format = format || '';
+
+          if (index != null) {
+              return lists__get(format, index, field, setter);
+          }
+
+          var i;
+          var out = [];
+          for (i = 0; i < count; i++) {
+              out[i] = lists__get(format, i, field, setter);
+          }
+          return out;
+      }
+
+      function lists__listMonths (format, index) {
+          return list(format, index, 'months', 12, 'month');
+      }
+
+      function lists__listMonthsShort (format, index) {
+          return list(format, index, 'monthsShort', 12, 'month');
+      }
+
+      function lists__listWeekdays (format, index) {
+          return list(format, index, 'weekdays', 7, 'day');
+      }
+
+      function lists__listWeekdaysShort (format, index) {
+          return list(format, index, 'weekdaysShort', 7, 'day');
+      }
+
+      function lists__listWeekdaysMin (format, index) {
+          return list(format, index, 'weekdaysMin', 7, 'day');
+      }
+
+      locale_locales__getSetGlobalLocale('en', {
+          ordinalParse: /\d{1,2}(th|st|nd|rd)/,
+          ordinal : function (number) {
+              var b = number % 10,
+                  output = (toInt(number % 100 / 10) === 1) ? 'th' :
+                  (b === 1) ? 'st' :
+                  (b === 2) ? 'nd' :
+                  (b === 3) ? 'rd' : 'th';
+              return number + output;
+          }
+      });
+
+      // Side effect imports
+      utils_hooks__hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', locale_locales__getSetGlobalLocale);
+      utils_hooks__hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', locale_locales__getLocale);
+
+      var mathAbs = Math.abs;
+
+      function duration_abs__abs () {
+          var data           = this._data;
+
+          this._milliseconds = mathAbs(this._milliseconds);
+          this._days         = mathAbs(this._days);
+          this._months       = mathAbs(this._months);
+
+          data.milliseconds  = mathAbs(data.milliseconds);
+          data.seconds       = mathAbs(data.seconds);
+          data.minutes       = mathAbs(data.minutes);
+          data.hours         = mathAbs(data.hours);
+          data.months        = mathAbs(data.months);
+          data.years         = mathAbs(data.years);
+
+          return this;
+      }
+
+      function duration_add_subtract__addSubtract (duration, input, value, direction) {
+          var other = create__createDuration(input, value);
+
+          duration._milliseconds += direction * other._milliseconds;
+          duration._days         += direction * other._days;
+          duration._months       += direction * other._months;
+
+          return duration._bubble();
+      }
+
+      // supports only 2.0-style add(1, 's') or add(duration)
+      function duration_add_subtract__add (input, value) {
+          return duration_add_subtract__addSubtract(this, input, value, 1);
+      }
+
+      // supports only 2.0-style subtract(1, 's') or subtract(duration)
+      function duration_add_subtract__subtract (input, value) {
+          return duration_add_subtract__addSubtract(this, input, value, -1);
+      }
+
+      function absCeil (number) {
+          if (number < 0) {
+              return Math.floor(number);
+          } else {
+              return Math.ceil(number);
+          }
+      }
+
+      function bubble () {
+          var milliseconds = this._milliseconds;
+          var days         = this._days;
+          var months       = this._months;
+          var data         = this._data;
+          var seconds, minutes, hours, years, monthsFromDays;
+
+          // if we have a mix of positive and negative values, bubble down first
+          // check: https://github.com/moment/moment/issues/2166
+          if (!((milliseconds >= 0 && days >= 0 && months >= 0) ||
+                  (milliseconds <= 0 && days <= 0 && months <= 0))) {
+              milliseconds += absCeil(monthsToDays(months) + days) * 864e5;
+              days = 0;
+              months = 0;
+          }
+
+          // The following code bubbles up values, see the tests for
+          // examples of what that means.
+          data.milliseconds = milliseconds % 1000;
+
+          seconds           = absFloor(milliseconds / 1000);
+          data.seconds      = seconds % 60;
+
+          minutes           = absFloor(seconds / 60);
+          data.minutes      = minutes % 60;
+
+          hours             = absFloor(minutes / 60);
+          data.hours        = hours % 24;
+
+          days += absFloor(hours / 24);
+
+          // convert days to months
+          monthsFromDays = absFloor(daysToMonths(days));
+          months += monthsFromDays;
+          days -= absCeil(monthsToDays(monthsFromDays));
+
+          // 12 months -> 1 year
+          years = absFloor(months / 12);
+          months %= 12;
+
+          data.days   = days;
+          data.months = months;
+          data.years  = years;
+
+          return this;
+      }
+
+      function daysToMonths (days) {
+          // 400 years have 146097 days (taking into account leap year rules)
+          // 400 years have 12 months === 4800
+          return days * 4800 / 146097;
+      }
+
+      function monthsToDays (months) {
+          // the reverse of daysToMonths
+          return months * 146097 / 4800;
+      }
+
+      function as (units) {
+          var days;
+          var months;
+          var milliseconds = this._milliseconds;
+
+          units = normalizeUnits(units);
+
+          if (units === 'month' || units === 'year') {
+              days   = this._days   + milliseconds / 864e5;
+              months = this._months + daysToMonths(days);
+              return units === 'month' ? months : months / 12;
+          } else {
+              // handle milliseconds separately because of floating point math errors (issue #1867)
+              days = this._days + Math.round(monthsToDays(this._months));
+              switch (units) {
+                  case 'week'   : return days / 7     + milliseconds / 6048e5;
+                  case 'day'    : return days         + milliseconds / 864e5;
+                  case 'hour'   : return days * 24    + milliseconds / 36e5;
+                  case 'minute' : return days * 1440  + milliseconds / 6e4;
+                  case 'second' : return days * 86400 + milliseconds / 1000;
+                  // Math.floor prevents floating point math errors here
+                  case 'millisecond': return Math.floor(days * 864e5) + milliseconds;
+                  default: throw new Error('Unknown unit ' + units);
+              }
+          }
+      }
+
+      // TODO: Use this.as('ms')?
+      function duration_as__valueOf () {
+          return (
+              this._milliseconds +
+              this._days * 864e5 +
+              (this._months % 12) * 2592e6 +
+              toInt(this._months / 12) * 31536e6
+          );
+      }
+
+      function makeAs (alias) {
+          return function () {
+              return this.as(alias);
+          };
+      }
+
+      var asMilliseconds = makeAs('ms');
+      var asSeconds      = makeAs('s');
+      var asMinutes      = makeAs('m');
+      var asHours        = makeAs('h');
+      var asDays         = makeAs('d');
+      var asWeeks        = makeAs('w');
+      var asMonths       = makeAs('M');
+      var asYears        = makeAs('y');
+
+      function duration_get__get (units) {
+          units = normalizeUnits(units);
+          return this[units + 's']();
+      }
+
+      function makeGetter(name) {
+          return function () {
+              return this._data[name];
+          };
+      }
+
+      var milliseconds = makeGetter('milliseconds');
+      var seconds      = makeGetter('seconds');
+      var minutes      = makeGetter('minutes');
+      var hours        = makeGetter('hours');
+      var days         = makeGetter('days');
+      var months       = makeGetter('months');
+      var years        = makeGetter('years');
+
+      function weeks () {
+          return absFloor(this.days() / 7);
+      }
+
+      var round = Math.round;
+      var thresholds = {
+          s: 45,  // seconds to minute
+          m: 45,  // minutes to hour
+          h: 22,  // hours to day
+          d: 26,  // days to month
+          M: 11   // months to year
+      };
+
+      // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
+      function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) {
+          return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
+      }
+
+      function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) {
+          var duration = create__createDuration(posNegDuration).abs();
+          var seconds  = round(duration.as('s'));
+          var minutes  = round(duration.as('m'));
+          var hours    = round(duration.as('h'));
+          var days     = round(duration.as('d'));
+          var months   = round(duration.as('M'));
+          var years    = round(duration.as('y'));
+
+          var a = seconds < thresholds.s && ['s', seconds]  ||
+                  minutes === 1          && ['m']           ||
+                  minutes < thresholds.m && ['mm', minutes] ||
+                  hours   === 1          && ['h']           ||
+                  hours   < thresholds.h && ['hh', hours]   ||
+                  days    === 1          && ['d']           ||
+                  days    < thresholds.d && ['dd', days]    ||
+                  months  === 1          && ['M']           ||
+                  months  < thresholds.M && ['MM', months]  ||
+                  years   === 1          && ['y']           || ['yy', years];
+
+          a[2] = withoutSuffix;
+          a[3] = +posNegDuration > 0;
+          a[4] = locale;
+          return substituteTimeAgo.apply(null, a);
+      }
+
+      // This function allows you to set a threshold for relative time strings
+      function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) {
+          if (thresholds[threshold] === undefined) {
+              return false;
+          }
+          if (limit === undefined) {
+              return thresholds[threshold];
+          }
+          thresholds[threshold] = limit;
+          return true;
+      }
+
+      function humanize (withSuffix) {
+          var locale = this.localeData();
+          var output = duration_humanize__relativeTime(this, !withSuffix, locale);
+
+          if (withSuffix) {
+              output = locale.pastFuture(+this, output);
+          }
+
+          return locale.postformat(output);
+      }
+
+      var iso_string__abs = Math.abs;
+
+      function iso_string__toISOString() {
+          // for ISO strings we do not use the normal bubbling rules:
+          //  * milliseconds bubble up until they become hours
+          //  * days do not bubble at all
+          //  * months bubble up until they become years
+          // This is because there is no context-free conversion between hours and days
+          // (think of clock changes)
+          // and also not between days and months (28-31 days per month)
+          var seconds = iso_string__abs(this._milliseconds) / 1000;
+          var days         = iso_string__abs(this._days);
+          var months       = iso_string__abs(this._months);
+          var minutes, hours, years;
+
+          // 3600 seconds -> 60 minutes -> 1 hour
+          minutes           = absFloor(seconds / 60);
+          hours             = absFloor(minutes / 60);
+          seconds %= 60;
+          minutes %= 60;
+
+          // 12 months -> 1 year
+          years  = absFloor(months / 12);
+          months %= 12;
+
+
+          // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
+          var Y = years;
+          var M = months;
+          var D = days;
+          var h = hours;
+          var m = minutes;
+          var s = seconds;
+          var total = this.asSeconds();
+
+          if (!total) {
+              // this is the same as C#'s (Noda) and python (isodate)...
+              // but not other JS (goog.date)
+              return 'P0D';
+          }
+
+          return (total < 0 ? '-' : '') +
+              'P' +
+              (Y ? Y + 'Y' : '') +
+              (M ? M + 'M' : '') +
+              (D ? D + 'D' : '') +
+              ((h || m || s) ? 'T' : '') +
+              (h ? h + 'H' : '') +
+              (m ? m + 'M' : '') +
+              (s ? s + 'S' : '');
+      }
+
+      var duration_prototype__proto = Duration.prototype;
+
+      duration_prototype__proto.abs            = duration_abs__abs;
+      duration_prototype__proto.add            = duration_add_subtract__add;
+      duration_prototype__proto.subtract       = duration_add_subtract__subtract;
+      duration_prototype__proto.as             = as;
+      duration_prototype__proto.asMilliseconds = asMilliseconds;
+      duration_prototype__proto.asSeconds      = asSeconds;
+      duration_prototype__proto.asMinutes      = asMinutes;
+      duration_prototype__proto.asHours        = asHours;
+      duration_prototype__proto.asDays         = asDays;
+      duration_prototype__proto.asWeeks        = asWeeks;
+      duration_prototype__proto.asMonths       = asMonths;
+      duration_prototype__proto.asYears        = asYears;
+      duration_prototype__proto.valueOf        = duration_as__valueOf;
+      duration_prototype__proto._bubble        = bubble;
+      duration_prototype__proto.get            = duration_get__get;
+      duration_prototype__proto.milliseconds   = milliseconds;
+      duration_prototype__proto.seconds        = seconds;
+      duration_prototype__proto.minutes        = minutes;
+      duration_prototype__proto.hours          = hours;
+      duration_prototype__proto.days           = days;
+      duration_prototype__proto.weeks          = weeks;
+      duration_prototype__proto.months         = months;
+      duration_prototype__proto.years          = years;
+      duration_prototype__proto.humanize       = humanize;
+      duration_prototype__proto.toISOString    = iso_string__toISOString;
+      duration_prototype__proto.toString       = iso_string__toISOString;
+      duration_prototype__proto.toJSON         = iso_string__toISOString;
+      duration_prototype__proto.locale         = locale;
+      duration_prototype__proto.localeData     = localeData;
+
+      // Deprecations
+      duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString);
+      duration_prototype__proto.lang = lang;
+
+      // Side effect imports
+
+      addFormatToken('X', 0, 0, 'unix');
+      addFormatToken('x', 0, 0, 'valueOf');
+
+      // PARSING
+
+      addRegexToken('x', matchSigned);
+      addRegexToken('X', matchTimestamp);
+      addParseToken('X', function (input, array, config) {
+          config._d = new Date(parseFloat(input, 10) * 1000);
+      });
+      addParseToken('x', function (input, array, config) {
+          config._d = new Date(toInt(input));
+      });
+
+      // Side effect imports
+
+
+      utils_hooks__hooks.version = '2.10.6';
+
+      setHookCallback(local__createLocal);
+
+      utils_hooks__hooks.fn                    = momentPrototype;
+      utils_hooks__hooks.min                   = min;
+      utils_hooks__hooks.max                   = max;
+      utils_hooks__hooks.utc                   = create_utc__createUTC;
+      utils_hooks__hooks.unix                  = moment__createUnix;
+      utils_hooks__hooks.months                = lists__listMonths;
+      utils_hooks__hooks.isDate                = isDate;
+      utils_hooks__hooks.locale                = locale_locales__getSetGlobalLocale;
+      utils_hooks__hooks.invalid               = valid__createInvalid;
+      utils_hooks__hooks.duration              = create__createDuration;
+      utils_hooks__hooks.isMoment              = isMoment;
+      utils_hooks__hooks.weekdays              = lists__listWeekdays;
+      utils_hooks__hooks.parseZone             = moment__createInZone;
+      utils_hooks__hooks.localeData            = locale_locales__getLocale;
+      utils_hooks__hooks.isDuration            = isDuration;
+      utils_hooks__hooks.monthsShort           = lists__listMonthsShort;
+      utils_hooks__hooks.weekdaysMin           = lists__listWeekdaysMin;
+      utils_hooks__hooks.defineLocale          = defineLocale;
+      utils_hooks__hooks.weekdaysShort         = lists__listWeekdaysShort;
+      utils_hooks__hooks.normalizeUnits        = normalizeUnits;
+      utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold;
+
+      var _moment = utils_hooks__hooks;
+
+      return _moment;
+
+  }));
+  /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)(module)))
+
+/***/ },
+/* 4 */
+/***/ function(module, exports) {
+
+  module.exports = function(module) {
+  	if(!module.webpackPolyfill) {
+  		module.deprecate = function() {};
+  		module.paths = [];
+  		// module.parent = undefined by default
+  		module.children = [];
+  		module.webpackPolyfill = 1;
+  	}
+  	return module;
+  }
+
+
+/***/ },
+/* 5 */
+/***/ function(module, exports) {
+
+  function webpackContext(req) {
+  	throw new Error("Cannot find module '" + req + "'.");
+  }
+  webpackContext.keys = function() { return []; };
+  webpackContext.resolve = webpackContext;
+  module.exports = webpackContext;
+  webpackContext.id = 5;
+
+
+/***/ },
+/* 6 */
+/***/ function(module, exports) {
+
+  /* WEBPACK VAR INJECTION */(function(global) {'use strict';
+
+  var _rng;
+
+  var globalVar = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : null;
+
+  if (globalVar && globalVar.crypto && crypto.getRandomValues) {
+    // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
+    // Moderately fast, high quality
+    var _rnds8 = new Uint8Array(16);
+    _rng = function whatwgRNG() {
+      crypto.getRandomValues(_rnds8);
+      return _rnds8;
+    };
+  }
+
+  if (!_rng) {
+    // Math.random()-based (RNG)
+    //
+    // If all else fails, use Math.random().  It's fast, but is of unspecified
+    // quality.
+    var _rnds = new Array(16);
+    _rng = function () {
+      for (var i = 0, r; i < 16; i++) {
+        if ((i & 0x03) === 0) r = Math.random() * 0x100000000;
+        _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
+      }
+
+      return _rnds;
+    };
+  }
+
+  //     uuid.js
+  //
+  //     Copyright (c) 2010-2012 Robert Kieffer
+  //     MIT License - http://opensource.org/licenses/mit-license.php
+
+  // Unique ID creation requires a high quality random # generator.  We feature
+  // detect to determine the best RNG source, normalizing to a function that
+  // returns 128-bits of randomness, since that's what's usually required
+
+  //var _rng = require('./rng');
+
+  // Maps for number <-> hex string conversion
+  var _byteToHex = [];
+  var _hexToByte = {};
+  for (var i = 0; i < 256; i++) {
+    _byteToHex[i] = (i + 0x100).toString(16).substr(1);
+    _hexToByte[_byteToHex[i]] = i;
+  }
+
+  // **`parse()` - Parse a UUID into it's component bytes**
+  function parse(s, buf, offset) {
+    var i = buf && offset || 0,
+        ii = 0;
+
+    buf = buf || [];
+    s.toLowerCase().replace(/[0-9a-f]{2}/g, function (oct) {
+      if (ii < 16) {
+        // Don't overflow!
+        buf[i + ii++] = _hexToByte[oct];
+      }
+    });
+
+    // Zero out remaining bytes if string was short
+    while (ii < 16) {
+      buf[i + ii++] = 0;
+    }
+
+    return buf;
+  }
+
+  // **`unparse()` - Convert UUID byte array (ala parse()) into a string**
+  function unparse(buf, offset) {
+    var i = offset || 0,
+        bth = _byteToHex;
+    return bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]];
+  }
+
+  // **`v1()` - Generate time-based UUID**
+  //
+  // Inspired by https://github.com/LiosK/UUID.js
+  // and http://docs.python.org/library/uuid.html
+
+  // random #'s we need to init node and clockseq
+  var _seedBytes = _rng();
+
+  // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
+  var _nodeId = [_seedBytes[0] | 0x01, _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]];
+
+  // Per 4.2.2, randomize (14 bit) clockseq
+  var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
+
+  // Previous uuid creation time
+  var _lastMSecs = 0,
+      _lastNSecs = 0;
+
+  // See https://github.com/broofa/node-uuid for API details
+  function v1(options, buf, offset) {
+    var i = buf && offset || 0;
+    var b = buf || [];
+
+    options = options || {};
+
+    var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq;
+
+    // UUID timestamps are 100 nano-second units since the Gregorian epoch,
+    // (1582-10-15 00:00).  JSNumbers aren't precise enough for this, so
+    // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
+    // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
+    var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime();
+
+    // Per 4.2.1.2, use count of uuid's generated during the current clock
+    // cycle to simulate higher resolution clock
+    var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1;
+
+    // Time since last uuid creation (in msecs)
+    var dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000;
+
+    // Per 4.2.1.2, Bump clockseq on clock regression
+    if (dt < 0 && options.clockseq === undefined) {
+      clockseq = clockseq + 1 & 0x3fff;
+    }
+
+    // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
+    // time interval
+    if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) {
+      nsecs = 0;
+    }
+
+    // Per 4.2.1.2 Throw error if too many uuids are requested
+    if (nsecs >= 10000) {
+      throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
+    }
+
+    _lastMSecs = msecs;
+    _lastNSecs = nsecs;
+    _clockseq = clockseq;
+
+    // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
+    msecs += 12219292800000;
+
+    // `time_low`
+    var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
+    b[i++] = tl >>> 24 & 0xff;
+    b[i++] = tl >>> 16 & 0xff;
+    b[i++] = tl >>> 8 & 0xff;
+    b[i++] = tl & 0xff;
+
+    // `time_mid`
+    var tmh = msecs / 0x100000000 * 10000 & 0xfffffff;
+    b[i++] = tmh >>> 8 & 0xff;
+    b[i++] = tmh & 0xff;
+
+    // `time_high_and_version`
+    b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
+    b[i++] = tmh >>> 16 & 0xff;
+
+    // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
+    b[i++] = clockseq >>> 8 | 0x80;
+
+    // `clock_seq_low`
+    b[i++] = clockseq & 0xff;
+
+    // `node`
+    var node = options.node || _nodeId;
+    for (var n = 0; n < 6; n++) {
+      b[i + n] = node[n];
+    }
+
+    return buf ? buf : unparse(b);
+  }
+
+  // **`v4()` - Generate random UUID**
+
+  // See https://github.com/broofa/node-uuid for API details
+  function v4(options, buf, offset) {
+    // Deprecated - 'format' argument, as supported in v1.2
+    var i = buf && offset || 0;
+
+    if (typeof options == 'string') {
+      buf = options == 'binary' ? new Array(16) : null;
+      options = null;
+    }
+    options = options || {};
+
+    var rnds = options.random || (options.rng || _rng)();
+
+    // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
+    rnds[6] = rnds[6] & 0x0f | 0x40;
+    rnds[8] = rnds[8] & 0x3f | 0x80;
+
+    // Copy bytes to buffer, if provided
+    if (buf) {
+      for (var ii = 0; ii < 16; ii++) {
+        buf[i + ii] = rnds[ii];
+      }
+    }
+
+    return buf || unparse(rnds);
+  }
+
+  // Export public API
+  var uuid = v4;
+  uuid.v1 = v1;
+  uuid.v4 = v4;
+  uuid.parse = parse;
+  uuid.unparse = unparse;
+
+  module.exports = uuid;
+  /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ },
+/* 7 */
+/***/ function(module, exports) {
+
+  // DOM utility methods
+
+  /**
+   * this prepares the JSON container for allocating SVG elements
+   * @param JSONcontainer
+   * @private
+   */
+  'use strict';
+
+  exports.prepareElements = function (JSONcontainer) {
+    // cleanup the redundant svgElements;
+    for (var elementType in JSONcontainer) {
+      if (JSONcontainer.hasOwnProperty(elementType)) {
+        JSONcontainer[elementType].redundant = JSONcontainer[elementType].used;
+        JSONcontainer[elementType].used = [];
+      }
+    }
+  };
+
+  /**
+   * this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from
+   * which to remove the redundant elements.
+   *
+   * @param JSONcontainer
+   * @private
+   */
+  exports.cleanupElements = function (JSONcontainer) {
+    // cleanup the redundant svgElements;
+    for (var elementType in JSONcontainer) {
+      if (JSONcontainer.hasOwnProperty(elementType)) {
+        if (JSONcontainer[elementType].redundant) {
+          for (var i = 0; i < JSONcontainer[elementType].redundant.length; i++) {
+            JSONcontainer[elementType].redundant[i].parentNode.removeChild(JSONcontainer[elementType].redundant[i]);
+          }
+          JSONcontainer[elementType].redundant = [];
+        }
+      }
+    }
+  };
+
+  /**
+   * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
+   * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
+   *
+   * @param elementType
+   * @param JSONcontainer
+   * @param svgContainer
+   * @returns {*}
+   * @private
+   */
+  exports.getSVGElement = function (elementType, JSONcontainer, svgContainer) {
+    var element;
+    // allocate SVG element, if it doesnt yet exist, create one.
+    if (JSONcontainer.hasOwnProperty(elementType)) {
+      // this element has been created before
+      // check if there is an redundant element
+      if (JSONcontainer[elementType].redundant.length > 0) {
+        element = JSONcontainer[elementType].redundant[0];
+        JSONcontainer[elementType].redundant.shift();
+      } else {
+        // create a new element and add it to the SVG
+        element = document.createElementNS('http://www.w3.org/2000/svg', elementType);
+        svgContainer.appendChild(element);
+      }
+    } else {
+      // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.
+      element = document.createElementNS('http://www.w3.org/2000/svg', elementType);
+      JSONcontainer[elementType] = { used: [], redundant: [] };
+      svgContainer.appendChild(element);
+    }
+    JSONcontainer[elementType].used.push(element);
+    return element;
+  };
+
+  /**
+   * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
+   * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
+   *
+   * @param elementType
+   * @param JSONcontainer
+   * @param DOMContainer
+   * @returns {*}
+   * @private
+   */
+  exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, insertBefore) {
+    var element;
+    // allocate DOM element, if it doesnt yet exist, create one.
+    if (JSONcontainer.hasOwnProperty(elementType)) {
+      // this element has been created before
+      // check if there is an redundant element
+      if (JSONcontainer[elementType].redundant.length > 0) {
+        element = JSONcontainer[elementType].redundant[0];
+        JSONcontainer[elementType].redundant.shift();
+      } else {
+        // create a new element and add it to the SVG
+        element = document.createElement(elementType);
+        if (insertBefore !== undefined) {
+          DOMContainer.insertBefore(element, insertBefore);
+        } else {
+          DOMContainer.appendChild(element);
+        }
+      }
+    } else {
+      // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.
+      element = document.createElement(elementType);
+      JSONcontainer[elementType] = { used: [], redundant: [] };
+      if (insertBefore !== undefined) {
+        DOMContainer.insertBefore(element, insertBefore);
+      } else {
+        DOMContainer.appendChild(element);
+      }
+    }
+    JSONcontainer[elementType].used.push(element);
+    return element;
+  };
+
+  /**
+   * Draw a point object. This is a separate function because it can also be called by the legend.
+   * The reason the JSONcontainer and the target SVG svgContainer have to be supplied is so the legend can use these functions
+   * as well.
+   *
+   * @param x
+   * @param y
+   * @param groupTemplate: A template containing the necessary information to draw the datapoint e.g., {style: 'circle', size: 5, className: 'className' }
+   * @param JSONcontainer
+   * @param svgContainer
+   * @param labelObj
+   * @returns {*}
+   */
+  exports.drawPoint = function (x, y, groupTemplate, JSONcontainer, svgContainer, labelObj) {
+    var point;
+    if (groupTemplate.style == 'circle') {
+      point = exports.getSVGElement('circle', JSONcontainer, svgContainer);
+      point.setAttributeNS(null, 'cx', x);
+      point.setAttributeNS(null, 'cy', y);
+      point.setAttributeNS(null, 'r', 0.5 * groupTemplate.size);
+    } else {
+      point = exports.getSVGElement('rect', JSONcontainer, svgContainer);
+      point.setAttributeNS(null, 'x', x - 0.5 * groupTemplate.size);
+      point.setAttributeNS(null, 'y', y - 0.5 * groupTemplate.size);
+      point.setAttributeNS(null, 'width', groupTemplate.size);
+      point.setAttributeNS(null, 'height', groupTemplate.size);
+    }
+
+    if (groupTemplate.styles !== undefined) {
+      point.setAttributeNS(null, 'style', groupTemplate.styles);
+    }
+    point.setAttributeNS(null, 'class', groupTemplate.className + ' vis-point');
+    //handle label
+
+    if (labelObj) {
+      var label = exports.getSVGElement('text', JSONcontainer, svgContainer);
+      if (labelObj.xOffset) {
+        x = x + labelObj.xOffset;
+      }
+
+      if (labelObj.yOffset) {
+        y = y + labelObj.yOffset;
+      }
+      if (labelObj.content) {
+        label.textContent = labelObj.content;
+      }
+
+      if (labelObj.className) {
+        label.setAttributeNS(null, 'class', labelObj.className + ' vis-label');
+      }
+      label.setAttributeNS(null, 'x', x);
+      label.setAttributeNS(null, 'y', y);
+    }
+
+    return point;
+  };
+
+  /**
+   * draw a bar SVG element centered on the X coordinate
+   *
+   * @param x
+   * @param y
+   * @param className
+   */
+  exports.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer, style) {
+    if (height != 0) {
+      if (height < 0) {
+        height *= -1;
+        y -= height;
+      }
+      var rect = exports.getSVGElement('rect', JSONcontainer, svgContainer);
+      rect.setAttributeNS(null, 'x', x - 0.5 * width);
+      rect.setAttributeNS(null, 'y', y);
+      rect.setAttributeNS(null, 'width', width);
+      rect.setAttributeNS(null, 'height', height);
+      rect.setAttributeNS(null, 'class', className);
+      if (style) {
+        rect.setAttributeNS(null, 'style', style);
+      }
+    }
+  };
+
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
+
+  'use strict';
+
+  var util = __webpack_require__(1);
+  var Queue = __webpack_require__(9);
+
+  /**
+   * DataSet
+   *
+   * Usage:
+   *     var dataSet = new DataSet({
+   *         fieldId: '_id',
+   *         type: {
+   *             // ...
+   *         }
+   *     });
+   *
+   *     dataSet.add(item);
+   *     dataSet.add(data);
+   *     dataSet.update(item);
+   *     dataSet.update(data);
+   *     dataSet.remove(id);
+   *     dataSet.remove(ids);
+   *     var data = dataSet.get();
+   *     var data = dataSet.get(id);
+   *     var data = dataSet.get(ids);
+   *     var data = dataSet.get(ids, options, data);
+   *     dataSet.clear();
+   *
+   * A data set can:
+   * - add/remove/update data
+   * - gives triggers upon changes in the data
+   * - can  import/export data in various data formats
+   *
+   * @param {Array} [data]    Optional array with initial data
+   * @param {Object} [options]   Available options:
+   *                             {String} fieldId Field name of the id in the
+   *                                              items, 'id' by default.
+   *                             {Object.<String, String} type
+   *                                              A map with field names as key,
+   *                                              and the field type as value.
+   *                             {Object} queue   Queue changes to the DataSet,
+   *                                              flush them all at once.
+   *                                              Queue options:
+   *                                              - {number} delay  Delay in ms, null by default
+   *                                              - {number} max    Maximum number of entries in the queue, Infinity by default
+   * @constructor DataSet
+   */
+  // TODO: add a DataSet constructor DataSet(data, options)
+  function DataSet(data, options) {
+    // correctly read optional arguments
+    if (data && !Array.isArray(data)) {
+      options = data;
+      data = null;
+    }
+
+    this._options = options || {};
+    this._data = {}; // map with data indexed by id
+    this.length = 0; // number of items in the DataSet
+    this._fieldId = this._options.fieldId || 'id'; // name of the field containing id
+    this._type = {}; // internal field types (NOTE: this can differ from this._options.type)
+
+    // all variants of a Date are internally stored as Date, so we can convert
+    // from everything to everything (also from ISODate to Number for example)
+    if (this._options.type) {
+      for (var field in this._options.type) {
+        if (this._options.type.hasOwnProperty(field)) {
+          var value = this._options.type[field];
+          if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
+            this._type[field] = 'Date';
+          } else {
+            this._type[field] = value;
+          }
+        }
+      }
+    }
+
+    // TODO: deprecated since version 1.1.1 (or 2.0.0?)
+    if (this._options.convert) {
+      throw new Error('Option "convert" is deprecated. Use "type" instead.');
+    }
+
+    this._subscribers = {}; // event subscribers
+
+    // add initial data when provided
+    if (data) {
+      this.add(data);
+    }
+
+    this.setOptions(options);
+  }
+
+  /**
+   * @param {Object} [options]   Available options:
+   *                             {Object} queue   Queue changes to the DataSet,
+   *                                              flush them all at once.
+   *                                              Queue options:
+   *                                              - {number} delay  Delay in ms, null by default
+   *                                              - {number} max    Maximum number of entries in the queue, Infinity by default
+   * @param options
+   */
+  DataSet.prototype.setOptions = function (options) {
+    if (options && options.queue !== undefined) {
+      if (options.queue === false) {
+        // delete queue if loaded
+        if (this._queue) {
+          this._queue.destroy();
+          delete this._queue;
+        }
+      } else {
+        // create queue and update its options
+        if (!this._queue) {
+          this._queue = Queue.extend(this, {
+            replace: ['add', 'update', 'remove']
+          });
+        }
+
+        if (typeof options.queue === 'object') {
+          this._queue.setOptions(options.queue);
+        }
+      }
+    }
+  };
+
+  /**
+   * Subscribe to an event, add an event listener
+   * @param {String} event        Event name. Available events: 'put', 'update',
+   *                              'remove'
+   * @param {function} callback   Callback method. Called with three parameters:
+   *                                  {String} event
+   *                                  {Object | null} params
+   *                                  {String | Number} senderId
+   */
+  DataSet.prototype.on = function (event, callback) {
+    var subscribers = this._subscribers[event];
+    if (!subscribers) {
+      subscribers = [];
+      this._subscribers[event] = subscribers;
+    }
+
+    subscribers.push({
+      callback: callback
+    });
+  };
+
+  // TODO: remove this deprecated function some day (replaced with `on` since version 0.5, deprecated since v4.0)
+  DataSet.prototype.subscribe = function () {
+    throw new Error('DataSet.subscribe is deprecated. Use DataSet.on instead.');
+  };
+
+  /**
+   * Unsubscribe from an event, remove an event listener
+   * @param {String} event
+   * @param {function} callback
+   */
+  DataSet.prototype.off = function (event, callback) {
+    var subscribers = this._subscribers[event];
+    if (subscribers) {
+      this._subscribers[event] = subscribers.filter(function (listener) {
+        return listener.callback != callback;
+      });
+    }
+  };
+
+  // TODO: remove this deprecated function some day (replaced with `on` since version 0.5, deprecated since v4.0)
+  DataSet.prototype.unsubscribe = function () {
+    throw new Error('DataSet.unsubscribe is deprecated. Use DataSet.off instead.');
+  };
+
+  /**
+   * Trigger an event
+   * @param {String} event
+   * @param {Object | null} params
+   * @param {String} [senderId]       Optional id of the sender.
+   * @private
+   */
+  DataSet.prototype._trigger = function (event, params, senderId) {
+    if (event == '*') {
+      throw new Error('Cannot trigger event *');
+    }
+
+    var subscribers = [];
+    if (event in this._subscribers) {
+      subscribers = subscribers.concat(this._subscribers[event]);
+    }
+    if ('*' in this._subscribers) {
+      subscribers = subscribers.concat(this._subscribers['*']);
+    }
+
+    for (var i = 0; i < subscribers.length; i++) {
+      var subscriber = subscribers[i];
+      if (subscriber.callback) {
+        subscriber.callback(event, params, senderId || null);
+      }
+    }
+  };
+
+  /**
+   * Add data.
+   * Adding an item will fail when there already is an item with the same id.
+   * @param {Object | Array} data
+   * @param {String} [senderId] Optional sender id
+   * @return {Array} addedIds      Array with the ids of the added items
+   */
+  DataSet.prototype.add = function (data, senderId) {
+    var addedIds = [],
+        id,
+        me = this;
+
+    if (Array.isArray(data)) {
+      // Array
+      for (var i = 0, len = data.length; i < len; i++) {
+        id = me._addItem(data[i]);
+        addedIds.push(id);
+      }
+    } else if (data instanceof Object) {
+      // Single item
+      id = me._addItem(data);
+      addedIds.push(id);
+    } else {
+      throw new Error('Unknown dataType');
+    }
+
+    if (addedIds.length) {
+      this._trigger('add', { items: addedIds }, senderId);
+    }
+
+    return addedIds;
+  };
+
+  /**
+   * Update existing items. When an item does not exist, it will be created
+   * @param {Object | Array} data
+   * @param {String} [senderId] Optional sender id
+   * @return {Array} updatedIds     The ids of the added or updated items
+   */
+  DataSet.prototype.update = function (data, senderId) {
+    var addedIds = [];
+    var updatedIds = [];
+    var updatedData = [];
+    var me = this;
+    var fieldId = me._fieldId;
+
+    var addOrUpdate = function addOrUpdate(item) {
+      var id = item[fieldId];
+      if (me._data[id]) {
+        // update item
+        id = me._updateItem(item);
+        updatedIds.push(id);
+        updatedData.push(item);
+      } else {
+        // add new item
+        id = me._addItem(item);
+        addedIds.push(id);
+      }
+    };
+
+    if (Array.isArray(data)) {
+      // Array
+      for (var i = 0, len = data.length; i < len; i++) {
+        addOrUpdate(data[i]);
+      }
+    } else if (data instanceof Object) {
+      // Single item
+      addOrUpdate(data);
+    } else {
+      throw new Error('Unknown dataType');
+    }
+
+    if (addedIds.length) {
+      this._trigger('add', { items: addedIds }, senderId);
+    }
+    if (updatedIds.length) {
+      this._trigger('update', { items: updatedIds, data: updatedData }, senderId);
+    }
+
+    return addedIds.concat(updatedIds);
+  };
+
+  /**
+   * Get a data item or multiple items.
+   *
+   * Usage:
+   *
+   *     get()
+   *     get(options: Object)
+   *
+   *     get(id: Number | String)
+   *     get(id: Number | String, options: Object)
+   *
+   *     get(ids: Number[] | String[])
+   *     get(ids: Number[] | String[], options: Object)
+   *
+   * Where:
+   *
+   * {Number | String} id         The id of an item
+   * {Number[] | String{}} ids    An array with ids of items
+   * {Object} options             An Object with options. Available options:
+   * {String} [returnType]        Type of data to be returned.
+   *                              Can be 'Array' (default) or 'Object'.
+   * {Object.<String, String>} [type]
+   * {String[]} [fields]          field names to be returned
+   * {function} [filter]          filter items
+   * {String | function} [order]  Order the items by a field name or custom sort function.
+   * @throws Error
+   */
+  DataSet.prototype.get = function (args) {
+    var me = this;
+
+    // parse the arguments
+    var id, ids, options;
+    var firstType = util.getType(arguments[0]);
+    if (firstType == 'String' || firstType == 'Number') {
+      // get(id [, options])
+      id = arguments[0];
+      options = arguments[1];
+    } else if (firstType == 'Array') {
+      // get(ids [, options])
+      ids = arguments[0];
+      options = arguments[1];
+    } else {
+      // get([, options])
+      options = arguments[0];
+    }
+
+    // determine the return type
+    var returnType;
+    if (options && options.returnType) {
+      var allowedValues = ['Array', 'Object'];
+      returnType = allowedValues.indexOf(options.returnType) == -1 ? 'Array' : options.returnType;
+    } else {
+      returnType = 'Array';
+    }
+
+    // build options
+    var type = options && options.type || this._options.type;
+    var filter = options && options.filter;
+    var items = [],
+        item,
+        itemId,
+        i,
+        len;
+
+    // convert items
+    if (id != undefined) {
+      // return a single item
+      item = me._getItem(id, type);
+      if (filter && !filter(item)) {
+        item = null;
+      }
+    } else if (ids != undefined) {
+      // return a subset of items
+      for (i = 0, len = ids.length; i < len; i++) {
+        item = me._getItem(ids[i], type);
+        if (!filter || filter(item)) {
+          items.push(item);
+        }
+      }
+    } else {
+      // return all items
+      for (itemId in this._data) {
+        if (this._data.hasOwnProperty(itemId)) {
+          item = me._getItem(itemId, type);
+          if (!filter || filter(item)) {
+            items.push(item);
+          }
+        }
+      }
+    }
+
+    // order the results
+    if (options && options.order && id == undefined) {
+      this._sort(items, options.order);
+    }
+
+    // filter fields of the items
+    if (options && options.fields) {
+      var fields = options.fields;
+      if (id != undefined) {
+        item = this._filterFields(item, fields);
+      } else {
+        for (i = 0, len = items.length; i < len; i++) {
+          items[i] = this._filterFields(items[i], fields);
+        }
+      }
+    }
+
+    // return the results
+    if (returnType == 'Object') {
+      var result = {};
+      for (i = 0; i < items.length; i++) {
+        result[items[i].id] = items[i];
+      }
+      return result;
+    } else {
+      if (id != undefined) {
+        // a single item
+        return item;
+      } else {
+        // just return our array
+        return items;
+      }
+    }
+  };
+
+  /**
+   * Get ids of all items or from a filtered set of items.
+   * @param {Object} [options]    An Object with options. Available options:
+   *                              {function} [filter] filter items
+   *                              {String | function} [order] Order the items by
+   *                                  a field name or custom sort function.
+   * @return {Array} ids
+   */
+  DataSet.prototype.getIds = function (options) {
+    var data = this._data,
+        filter = options && options.filter,
+        order = options && options.order,
+        type = options && options.type || this._options.type,
+        i,
+        len,
+        id,
+        item,
+        items,
+        ids = [];
+
+    if (filter) {
+      // get filtered items
+      if (order) {
+        // create ordered list
+        items = [];
+        for (id in data) {
+          if (data.hasOwnProperty(id)) {
+            item = this._getItem(id, type);
+            if (filter(item)) {
+              items.push(item);
+            }
+          }
+        }
+
+        this._sort(items, order);
+
+        for (i = 0, len = items.length; i < len; i++) {
+          ids[i] = items[i][this._fieldId];
+        }
+      } else {
+        // create unordered list
+        for (id in data) {
+          if (data.hasOwnProperty(id)) {
+            item = this._getItem(id, type);
+            if (filter(item)) {
+              ids.push(item[this._fieldId]);
+            }
+          }
+        }
+      }
+    } else {
+      // get all items
+      if (order) {
+        // create an ordered list
+        items = [];
+        for (id in data) {
+          if (data.hasOwnProperty(id)) {
+            items.push(data[id]);
+          }
+        }
+
+        this._sort(items, order);
+
+        for (i = 0, len = items.length; i < len; i++) {
+          ids[i] = items[i][this._fieldId];
+        }
+      } else {
+        // create unordered list
+        for (id in data) {
+          if (data.hasOwnProperty(id)) {
+            item = data[id];
+            ids.push(item[this._fieldId]);
+          }
+        }
+      }
+    }
+
+    return ids;
+  };
+
+  /**
+   * Returns the DataSet itself. Is overwritten for example by the DataView,
+   * which returns the DataSet it is connected to instead.
+   */
+  DataSet.prototype.getDataSet = function () {
+    return this;
+  };
+
+  /**
+   * Execute a callback function for every item in the dataset.
+   * @param {function} callback
+   * @param {Object} [options]    Available options:
+   *                              {Object.<String, String>} [type]
+   *                              {String[]} [fields] filter fields
+   *                              {function} [filter] filter items
+   *                              {String | function} [order] Order the items by
+   *                                  a field name or custom sort function.
+   */
+  DataSet.prototype.forEach = function (callback, options) {
+    var filter = options && options.filter,
+        type = options && options.type || this._options.type,
+        data = this._data,
+        item,
+        id;
+
+    if (options && options.order) {
+      // execute forEach on ordered list
+      var items = this.get(options);
+
+      for (var i = 0, len = items.length; i < len; i++) {
+        item = items[i];
+        id = item[this._fieldId];
+        callback(item, id);
+      }
+    } else {
+      // unordered
+      for (id in data) {
+        if (data.hasOwnProperty(id)) {
+          item = this._getItem(id, type);
+          if (!filter || filter(item)) {
+            callback(item, id);
+          }
+        }
+      }
+    }
+  };
+
+  /**
+   * Map every item in the dataset.
+   * @param {function} callback
+   * @param {Object} [options]    Available options:
+   *                              {Object.<String, String>} [type]
+   *                              {String[]} [fields] filter fields
+   *                              {function} [filter] filter items
+   *                              {String | function} [order] Order the items by
+   *                                  a field name or custom sort function.
+   * @return {Object[]} mappedItems
+   */
+  DataSet.prototype.map = function (callback, options) {
+    var filter = options && options.filter,
+        type = options && options.type || this._options.type,
+        mappedItems = [],
+        data = this._data,
+        item;
+
+    // convert and filter items
+    for (var id in data) {
+      if (data.hasOwnProperty(id)) {
+        item = this._getItem(id, type);
+        if (!filter || filter(item)) {
+          mappedItems.push(callback(item, id));
+        }
+      }
+    }
+
+    // order items
+    if (options && options.order) {
+      this._sort(mappedItems, options.order);
+    }
+
+    return mappedItems;
+  };
+
+  /**
+   * Filter the fields of an item
+   * @param {Object | null} item
+   * @param {String[]} fields     Field names
+   * @return {Object | null} filteredItem or null if no item is provided
+   * @private
+   */
+  DataSet.prototype._filterFields = function (item, fields) {
+    if (!item) {
+      // item is null
+      return item;
+    }
+
+    var filteredItem = {};
+
+    if (Array.isArray(fields)) {
+      for (var field in item) {
+        if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) {
+          filteredItem[field] = item[field];
+        }
+      }
+    } else {
+      for (var field in item) {
+        if (item.hasOwnProperty(field) && fields.hasOwnProperty(field)) {
+          filteredItem[fields[field]] = item[field];
+        }
+      }
+    }
+
+    return filteredItem;
+  };
+
+  /**
+   * Sort the provided array with items
+   * @param {Object[]} items
+   * @param {String | function} order      A field name or custom sort function.
+   * @private
+   */
+  DataSet.prototype._sort = function (items, order) {
+    if (util.isString(order)) {
+      // order by provided field name
+      var name = order; // field name
+      items.sort(function (a, b) {
+        var av = a[name];
+        var bv = b[name];
+        return av > bv ? 1 : av < bv ? -1 : 0;
+      });
+    } else if (typeof order === 'function') {
+      // order by sort function
+      items.sort(order);
+    }
+    // TODO: extend order by an Object {field:String, direction:String}
+    //       where direction can be 'asc' or 'desc'
+    else {
+      throw new TypeError('Order must be a function or a string');
+    }
+  };
+
+  /**
+   * Remove an object by pointer or by id
+   * @param {String | Number | Object | Array} id Object or id, or an array with
+   *                                              objects or ids to be removed
+   * @param {String} [senderId] Optional sender id
+   * @return {Array} removedIds
+   */
+  DataSet.prototype.remove = function (id, senderId) {
+    var removedIds = [],
+        i,
+        len,
+        removedId;
+
+    if (Array.isArray(id)) {
+      for (i = 0, len = id.length; i < len; i++) {
+        removedId = this._remove(id[i]);
+        if (removedId != null) {
+          removedIds.push(removedId);
+        }
+      }
+    } else {
+      removedId = this._remove(id);
+      if (removedId != null) {
+        removedIds.push(removedId);
+      }
+    }
+
+    if (removedIds.length) {
+      this._trigger('remove', { items: removedIds }, senderId);
+    }
+
+    return removedIds;
+  };
+
+  /**
+   * Remove an item by its id
+   * @param {Number | String | Object} id   id or item
+   * @returns {Number | String | null} id
+   * @private
+   */
+  DataSet.prototype._remove = function (id) {
+    if (util.isNumber(id) || util.isString(id)) {
+      if (this._data[id]) {
+        delete this._data[id];
+        this.length--;
+        return id;
+      }
+    } else if (id instanceof Object) {
+      var itemId = id[this._fieldId];
+      if (itemId && this._data[itemId]) {
+        delete this._data[itemId];
+        this.length--;
+        return itemId;
+      }
+    }
+    return null;
+  };
+
+  /**
+   * Clear the data
+   * @param {String} [senderId] Optional sender id
+   * @return {Array} removedIds    The ids of all removed items
+   */
+  DataSet.prototype.clear = function (senderId) {
+    var ids = Object.keys(this._data);
+
+    this._data = {};
+    this.length = 0;
+
+    this._trigger('remove', { items: ids }, senderId);
+
+    return ids;
+  };
+
+  /**
+   * Find the item with maximum value of a specified field
+   * @param {String} field
+   * @return {Object | null} item  Item containing max value, or null if no items
+   */
+  DataSet.prototype.max = function (field) {
+    var data = this._data,
+        max = null,
+        maxField = null;
+
+    for (var id in data) {
+      if (data.hasOwnProperty(id)) {
+        var item = data[id];
+        var itemField = item[field];
+        if (itemField != null && (!max || itemField > maxField)) {
+          max = item;
+          maxField = itemField;
+        }
+      }
+    }
+
+    return max;
+  };
+
+  /**
+   * Find the item with minimum value of a specified field
+   * @param {String} field
+   * @return {Object | null} item  Item containing max value, or null if no items
+   */
+  DataSet.prototype.min = function (field) {
+    var data = this._data,
+        min = null,
+        minField = null;
+
+    for (var id in data) {
+      if (data.hasOwnProperty(id)) {
+        var item = data[id];
+        var itemField = item[field];
+        if (itemField != null && (!min || itemField < minField)) {
+          min = item;
+          minField = itemField;
+        }
+      }
+    }
+
+    return min;
+  };
+
+  /**
+   * Find all distinct values of a specified field
+   * @param {String} field
+   * @return {Array} values  Array containing all distinct values. If data items
+   *                         do not contain the specified field are ignored.
+   *                         The returned array is unordered.
+   */
+  DataSet.prototype.distinct = function (field) {
+    var data = this._data;
+    var values = [];
+    var fieldType = this._options.type && this._options.type[field] || null;
+    var count = 0;
+    var i;
+
+    for (var prop in data) {
+      if (data.hasOwnProperty(prop)) {
+        var item = data[prop];
+        var value = item[field];
+        var exists = false;
+        for (i = 0; i < count; i++) {
+          if (values[i] == value) {
+            exists = true;
+            break;
+          }
+        }
+        if (!exists && value !== undefined) {
+          values[count] = value;
+          count++;
+        }
+      }
+    }
+
+    if (fieldType) {
+      for (i = 0; i < values.length; i++) {
+        values[i] = util.convert(values[i], fieldType);
+      }
+    }
+
+    return values;
+  };
+
+  /**
+   * Add a single item. Will fail when an item with the same id already exists.
+   * @param {Object} item
+   * @return {String} id
+   * @private
+   */
+  DataSet.prototype._addItem = function (item) {
+    var id = item[this._fieldId];
+
+    if (id != undefined) {
+      // check whether this id is already taken
+      if (this._data[id]) {
+        // item already exists
+        throw new Error('Cannot add item: item with id ' + id + ' already exists');
+      }
+    } else {
+      // generate an id
+      id = util.randomUUID();
+      item[this._fieldId] = id;
+    }
+
+    var d = {};
+    for (var field in item) {
+      if (item.hasOwnProperty(field)) {
+        var fieldType = this._type[field]; // type may be undefined
+        d[field] = util.convert(item[field], fieldType);
+      }
+    }
+    this._data[id] = d;
+    this.length++;
+
+    return id;
+  };
+
+  /**
+   * Get an item. Fields can be converted to a specific type
+   * @param {String} id
+   * @param {Object.<String, String>} [types]  field types to convert
+   * @return {Object | null} item
+   * @private
+   */
+  DataSet.prototype._getItem = function (id, types) {
+    var field, value;
+
+    // get the item from the dataset
+    var raw = this._data[id];
+    if (!raw) {
+      return null;
+    }
+
+    // convert the items field types
+    var converted = {};
+    if (types) {
+      for (field in raw) {
+        if (raw.hasOwnProperty(field)) {
+          value = raw[field];
+          converted[field] = util.convert(value, types[field]);
+        }
+      }
+    } else {
+      // no field types specified, no converting needed
+      for (field in raw) {
+        if (raw.hasOwnProperty(field)) {
+          value = raw[field];
+          converted[field] = value;
+        }
+      }
+    }
+    return converted;
+  };
+
+  /**
+   * Update a single item: merge with existing item.
+   * Will fail when the item has no id, or when there does not exist an item
+   * with the same id.
+   * @param {Object} item
+   * @return {String} id
+   * @private
+   */
+  DataSet.prototype._updateItem = function (item) {
+    var id = item[this._fieldId];
+    if (id == undefined) {
+      throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
+    }
+    var d = this._data[id];
+    if (!d) {
+      // item doesn't exist
+      throw new Error('Cannot update item: no item with id ' + id + ' found');
+    }
+
+    // merge with current item
+    for (var field in item) {
+      if (item.hasOwnProperty(field)) {
+        var fieldType = this._type[field]; // type may be undefined
+        d[field] = util.convert(item[field], fieldType);
+      }
+    }
+
+    return id;
+  };
+
+  module.exports = DataSet;
+
+/***/ },
+/* 9 */
+/***/ function(module, exports) {
+
+  /**
+   * A queue
+   * @param {Object} options
+   *            Available options:
+   *            - delay: number    When provided, the queue will be flushed
+   *                               automatically after an inactivity of this delay
+   *                               in milliseconds.
+   *                               Default value is null.
+   *            - max: number      When the queue exceeds the given maximum number
+   *                               of entries, the queue is flushed automatically.
+   *                               Default value of max is Infinity.
+   * @constructor
+   */
+  'use strict';
+
+  function Queue(options) {
+    // options
+    this.delay = null;
+    this.max = Infinity;
+
+    // properties
+    this._queue = [];
+    this._timeout = null;
+    this._extended = null;
+
+    this.setOptions(options);
+  }
+
+  /**
+   * Update the configuration of the queue
+   * @param {Object} options
+   *            Available options:
+   *            - delay: number    When provided, the queue will be flushed
+   *                               automatically after an inactivity of this delay
+   *                               in milliseconds.
+   *                               Default value is null.
+   *            - max: number      When the queue exceeds the given maximum number
+   *                               of entries, the queue is flushed automatically.
+   *                               Default value of max is Infinity.
+   * @param options
+   */
+  Queue.prototype.setOptions = function (options) {
+    if (options && typeof options.delay !== 'undefined') {
+      this.delay = options.delay;
+    }
+    if (options && typeof options.max !== 'undefined') {
+      this.max = options.max;
+    }
+
+    this._flushIfNeeded();
+  };
+
+  /**
+   * Extend an object with queuing functionality.
+   * The object will be extended with a function flush, and the methods provided
+   * in options.replace will be replaced with queued ones.
+   * @param {Object} object
+   * @param {Object} options
+   *            Available options:
+   *            - replace: Array.<string>
+   *                               A list with method names of the methods
+   *                               on the object to be replaced with queued ones.
+   *            - delay: number    When provided, the queue will be flushed
+   *                               automatically after an inactivity of this delay
+   *                               in milliseconds.
+   *                               Default value is null.
+   *            - max: number      When the queue exceeds the given maximum number
+   *                               of entries, the queue is flushed automatically.
+   *                               Default value of max is Infinity.
+   * @return {Queue} Returns the created queue
+   */
+  Queue.extend = function (object, options) {
+    var queue = new Queue(options);
+
+    if (object.flush !== undefined) {
+      throw new Error('Target object already has a property flush');
+    }
+    object.flush = function () {
+      queue.flush();
+    };
+
+    var methods = [{
+      name: 'flush',
+      original: undefined
+    }];
+
+    if (options && options.replace) {
+      for (var i = 0; i < options.replace.length; i++) {
+        var name = options.replace[i];
+        methods.push({
+          name: name,
+          original: object[name]
+        });
+        queue.replace(object, name);
+      }
+    }
+
+    queue._extended = {
+      object: object,
+      methods: methods
+    };
+
+    return queue;
+  };
+
+  /**
+   * Destroy the queue. The queue will first flush all queued actions, and in
+   * case it has extended an object, will restore the original object.
+   */
+  Queue.prototype.destroy = function () {
+    this.flush();
+
+    if (this._extended) {
+      var object = this._extended.object;
+      var methods = this._extended.methods;
+      for (var i = 0; i < methods.length; i++) {
+        var method = methods[i];
+        if (method.original) {
+          object[method.name] = method.original;
+        } else {
+          delete object[method.name];
+        }
+      }
+      this._extended = null;
+    }
+  };
+
+  /**
+   * Replace a method on an object with a queued version
+   * @param {Object} object   Object having the method
+   * @param {string} method   The method name
+   */
+  Queue.prototype.replace = function (object, method) {
+    var me = this;
+    var original = object[method];
+    if (!original) {
+      throw new Error('Method ' + method + ' undefined');
+    }
+
+    object[method] = function () {
+      // create an Array with the arguments
+      var args = [];
+      for (var i = 0; i < arguments.length; i++) {
+        args[i] = arguments[i];
+      }
+
+      // add this call to the queue
+      me.queue({
+        args: args,
+        fn: original,
+        context: this
+      });
+    };
+  };
+
+  /**
+   * Queue a call
+   * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry
+   */
+  Queue.prototype.queue = function (entry) {
+    if (typeof entry === 'function') {
+      this._queue.push({ fn: entry });
+    } else {
+      this._queue.push(entry);
+    }
+
+    this._flushIfNeeded();
+  };
+
+  /**
+   * Check whether the queue needs to be flushed
+   * @private
+   */
+  Queue.prototype._flushIfNeeded = function () {
+    // flush when the maximum is exceeded.
+    if (this._queue.length > this.max) {
+      this.flush();
+    }
+
+    // flush after a period of inactivity when a delay is configured
+    clearTimeout(this._timeout);
+    if (this.queue.length > 0 && typeof this.delay === 'number') {
+      var me = this;
+      this._timeout = setTimeout(function () {
+        me.flush();
+      }, this.delay);
+    }
+  };
+
+  /**
+   * Flush all queued calls
+   */
+  Queue.prototype.flush = function () {
+    while (this._queue.length > 0) {
+      var entry = this._queue.shift();
+      entry.fn.apply(entry.context || entry.fn, entry.args || []);
+    }
+  };
+
+  module.exports = Queue;
+
+/***/ },
+/* 10 */
+/***/ function(module, exports, __webpack_require__) {
+
+  'use strict';
+
+  var util = __webpack_require__(1);
+  var DataSet = __webpack_require__(8);
+
+  /**
+   * DataView
+   *
+   * a dataview offers a filtered view on a dataset or an other dataview.
+   *
+   * @param {DataSet | DataView} data
+   * @param {Object} [options]   Available options: see method get
+   *
+   * @constructor DataView
+   */
+  function DataView(data, options) {
+    this._data = null;
+    this._ids = {}; // ids of the items currently in memory (just contains a boolean true)
+    this.length = 0; // number of items in the DataView
+    this._options = options || {};
+    this._fieldId = 'id'; // name of the field containing id
+    this._subscribers = {}; // event subscribers
+
+    var me = this;
+    this.listener = function () {
+      me._onEvent.apply(me, arguments);
+    };
+
+    this.setData(data);
+  }
+
+  // TODO: implement a function .config() to dynamically update things like configured filter
+  // and trigger changes accordingly
+
+  /**
+   * Set a data source for the view
+   * @param {DataSet | DataView} data
+   */
+  DataView.prototype.setData = function (data) {
+    var ids, i, len;
+
+    if (this._data) {
+      // unsubscribe from current dataset
+      if (this._data.off) {
+        this._data.off('*', this.listener);
+      }
+
+      // trigger a remove of all items in memory
+      ids = [];
+      for (var id in this._ids) {
+        if (this._ids.hasOwnProperty(id)) {
+          ids.push(id);
+        }
+      }
+      this._ids = {};
+      this.length = 0;
+      this._trigger('remove', { items: ids });
+    }
+
+    this._data = data;
+
+    if (this._data) {
+      // update fieldId
+      this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || 'id';
+
+      // trigger an add of all added items
+      ids = this._data.getIds({ filter: this._options && this._options.filter });
+      for (i = 0, len = ids.length; i < len; i++) {
+        id = ids[i];
+        this._ids[id] = true;
+      }
+      this.length = ids.length;
+      this._trigger('add', { items: ids });
+
+      // subscribe to new dataset
+      if (this._data.on) {
+        this._data.on('*', this.listener);
+      }
+    }
+  };
+
+  /**
+   * Refresh the DataView. Useful when the DataView has a filter function
+   * containing a variable parameter.
+   */
+  DataView.prototype.refresh = function () {
+    var id;
+    var ids = this._data.getIds({ filter: this._options && this._options.filter });
+    var newIds = {};
+    var added = [];
+    var removed = [];
+
+    // check for additions
+    for (var i = 0; i < ids.length; i++) {
+      id = ids[i];
+      newIds[id] = true;
+      if (!this._ids[id]) {
+        added.push(id);
+        this._ids[id] = true;
+        this.length++;
+      }
+    }
+
+    // check for removals
+    for (id in this._ids) {
+      if (this._ids.hasOwnProperty(id)) {
+        if (!newIds[id]) {
+          removed.push(id);
+          delete this._ids[id];
+          this.length--;
+        }
+      }
+    }
+
+    // trigger events
+    if (added.length) {
+      this._trigger('add', { items: added });
+    }
+    if (removed.length) {
+      this._trigger('remove', { items: removed });
+    }
+  };
+
+  /**
+   * Get data from the data view
+   *
+   * Usage:
+   *
+   *     get()
+   *     get(options: Object)
+   *     get(options: Object, data: Array | DataTable)
+   *
+   *     get(id: Number)
+   *     get(id: Number, options: Object)
+   *     get(id: Number, options: Object, data: Array | DataTable)
+   *
+   *     get(ids: Number[])
+   *     get(ids: Number[], options: Object)
+   *     get(ids: Number[], options: Object, data: Array | DataTable)
+   *
+   * Where:
+   *
+   * {Number | String} id         The id of an item
+   * {Number[] | String{}} ids    An array with ids of items
+   * {Object} options             An Object with options. Available options:
+   *                              {String} [type] Type of data to be returned. Can
+   *                                              be 'DataTable' or 'Array' (default)
+   *                              {Object.<String, String>} [convert]
+   *                              {String[]} [fields] field names to be returned
+   *                              {function} [filter] filter items
+   *                              {String | function} [order] Order the items by
+   *                                  a field name or custom sort function.
+   * {Array | DataTable} [data]   If provided, items will be appended to this
+   *                              array or table. Required in case of Google
+   *                              DataTable.
+   * @param args
+   */
+  DataView.prototype.get = function (args) {
+    var me = this;
+
+    // parse the arguments
+    var ids, options, data;
+    var firstType = util.getType(arguments[0]);
+    if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
+      // get(id(s) [, options] [, data])
+      ids = arguments[0]; // can be a single id or an array with ids
+      options = arguments[1];
+      data = arguments[2];
+    } else {
+      // get([, options] [, data])
+      options = arguments[0];
+      data = arguments[1];
+    }
+
+    // extend the options with the default options and provided options
+    var viewOptions = util.extend({}, this._options, options);
+
+    // create a combined filter method when needed
+    if (this._options.filter && options && options.filter) {
+      viewOptions.filter = function (item) {
+        return me._options.filter(item) && options.filter(item);
+      };
+    }
+
+    // build up the call to the linked data set
+    var getArguments = [];
+    if (ids != undefined) {
+      getArguments.push(ids);
+    }
+    getArguments.push(viewOptions);
+    getArguments.push(data);
+
+    return this._data && this._data.get.apply(this._data, getArguments);
+  };
+
+  /**
+   * Get ids of all items or from a filtered set of items.
+   * @param {Object} [options]    An Object with options. Available options:
+   *                              {function} [filter] filter items
+   *                              {String | function} [order] Order the items by
+   *                                  a field name or custom sort function.
+   * @return {Array} ids
+   */
+  DataView.prototype.getIds = function (options) {
+    var ids;
+
+    if (this._data) {
+      var defaultFilter = this._options.filter;
+      var filter;
+
+      if (options && options.filter) {
+        if (defaultFilter) {
+          filter = function (item) {
+            return defaultFilter(item) && options.filter(item);
+          };
+        } else {
+          filter = options.filter;
+        }
+      } else {
+        filter = defaultFilter;
+      }
+
+      ids = this._data.getIds({
+        filter: filter,
+        order: options && options.order
+      });
+    } else {
+      ids = [];
+    }
+
+    return ids;
+  };
+
+  /**
+   * Get the DataSet to which this DataView is connected. In case there is a chain
+   * of multiple DataViews, the root DataSet of this chain is returned.
+   * @return {DataSet} dataSet
+   */
+  DataView.prototype.getDataSet = function () {
+    var dataSet = this;
+    while (dataSet instanceof DataView) {
+      dataSet = dataSet._data;
+    }
+    return dataSet || null;
+  };
+
+  /**
+   * Event listener. Will propagate all events from the connected data set to
+   * the subscribers of the DataView, but will filter the items and only trigger
+   * when there are changes in the filtered data set.
+   * @param {String} event
+   * @param {Object | null} params
+   * @param {String} senderId
+   * @private
+   */
+  DataView.prototype._onEvent = function (event, params, senderId) {
+    var i, len, id, item;
+    var ids = params && params.items;
+    var data = this._data;
+    var updatedData = [];
+    var added = [];
+    var updated = [];
+    var removed = [];
+
+    if (ids && data) {
+      switch (event) {
+        case 'add':
+          // filter the ids of the added items
+          for (i = 0, len = ids.length; i < len; i++) {
+            id = ids[i];
+            item = this.get(id);
+            if (item) {
+              this._ids[id] = true;
+              added.push(id);
+            }
+          }
+
+          break;
+
+        case 'update':
+          // determine the event from the views viewpoint: an updated
+          // item can be added, updated, or removed from this view.
+          for (i = 0, len = ids.length; i < len; i++) {
+            id = ids[i];
+            item = this.get(id);
+
+            if (item) {
+              if (this._ids[id]) {
+                updated.push(id);
+                updatedData.push(params.data[i]);
+              } else {
+                this._ids[id] = true;
+                added.push(id);
+              }
+            } else {
+              if (this._ids[id]) {
+                delete this._ids[id];
+                removed.push(id);
+              } else {}
+            }
+          }
+
+          break;
+
+        case 'remove':
+          // filter the ids of the removed items
+          for (i = 0, len = ids.length; i < len; i++) {
+            id = ids[i];
+            if (this._ids[id]) {
+              delete this._ids[id];
+              removed.push(id);
+            }
+          }
+
+          break;
+      }
+
+      this.length += added.length - removed.length;
+
+      if (added.length) {
+        this._trigger('add', { items: added }, senderId);
+      }
+      if (updated.length) {
+        this._trigger('update', { items: updated, data: updatedData }, senderId);
+      }
+      if (removed.length) {
+        this._trigger('remove', { items: removed }, senderId);
+      }
+    }
+  };
+
+  // copy subscription functionality from DataSet
+  DataView.prototype.on = DataSet.prototype.on;
+  DataView.prototype.off = DataSet.prototype.off;
+  DataView.prototype._trigger = DataSet.prototype._trigger;
+
+  // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
+  DataView.prototype.subscribe = DataView.prototype.on;
+  DataView.prototype.unsubscribe = DataView.prototype.off;
+
+  module.exports = DataView;
+
+  // nothing interesting for me :-(
+
+/***/ },
+/* 11 */
+/***/ function(module, exports, __webpack_require__) {
+
+  'use strict';
+
+  var Emitter = __webpack_require__(12);
+  var DataSet = __webpack_require__(8);
+  var DataView = __webpack_require__(10);
+  var util = __webpack_require__(1);
+  var Point3d = __webpack_require__(13);
+  var Point2d = __webpack_require__(14);
+  var Camera = __webpack_require__(15);
+  var Filter = __webpack_require__(16);
+  var Slider = __webpack_require__(17);
+  var StepNumber = __webpack_require__(18);
+
+  /**
+   * @constructor Graph3d
+   * Graph3d displays data in 3d.
+   *
+   * Graph3d is developed in javascript as a Google Visualization Chart.
+   *
+   * @param {Element} container   The DOM element in which the Graph3d will
+   *                              be created. Normally a div element.
+   * @param {DataSet | DataView | Array} [data]
+   * @param {Object} [options]
+   */
+  function Graph3d(container, data, options) {
+    if (!(this instanceof Graph3d)) {
+      throw new SyntaxError('Constructor must be called with the new operator');
+    }
+
+    // create variables and set default values
+    this.containerElement = container;
+    this.width = '400px';
+    this.height = '400px';
+    this.margin = 10; // px
+    this.defaultXCenter = '55%';
+    this.defaultYCenter = '50%';
+
+    this.xLabel = 'x';
+    this.yLabel = 'y';
+    this.zLabel = 'z';
+
+    var passValueFn = function passValueFn(v) {
+      return v;
+    };
+    this.xValueLabel = passValueFn;
+    this.yValueLabel = passValueFn;
+    this.zValueLabel = passValueFn;
+
+    this.filterLabel = 'time';
+    this.legendLabel = 'value';
+
+    this.style = Graph3d.STYLE.DOT;
+    this.showPerspective = true;
+    this.showGrid = true;
+    this.keepAspectRatio = true;
+    this.showShadow = false;
+    this.showGrayBottom = false; // TODO: this does not work correctly
+    this.showTooltip = false;
+    this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube'
+
+    this.animationInterval = 1000; // milliseconds
+    this.animationPreload = false;
+
+    this.camera = new Camera();
+    this.camera.setArmRotation(1.0, 0.5);
+    this.camera.setArmLength(1.7);
+    this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window?
+
+    this.dataTable = null; // The original data table
+    this.dataPoints = null; // The table with point objects
+
+    // the column indexes
+    this.colX = undefined;
+    this.colY = undefined;
+    this.colZ = undefined;
+    this.colValue = undefined;
+    this.colFilter = undefined;
+
+    this.xMin = 0;
+    this.xStep = undefined; // auto by default
+    this.xMax = 1;
+    this.yMin = 0;
+    this.yStep = undefined; // auto by default
+    this.yMax = 1;
+    this.zMin = 0;
+    this.zStep = undefined; // auto by default
+    this.zMax = 1;
+    this.valueMin = 0;
+    this.valueMax = 1;
+    this.xBarWidth = 1;
+    this.yBarWidth = 1;
+    // TODO: customize axis range
+
+    // colors
+    this.axisColor = '#4D4D4D';
+    this.gridColor = '#D3D3D3';
+    this.dataColor = {
+      fill: '#7DC1FF',
+      stroke: '#3267D2',
+      strokeWidth: 1 // px
+    };
+
+    // create a frame and canvas
+    this.create();
+
+    // apply options (also when undefined)
+    this.setOptions(options);
+
+    // apply data
+    if (data) {
+      this.setData(data);
+    }
+  }
+
+  // Extend Graph3d with an Emitter mixin
+  Emitter(Graph3d.prototype);
+
+  /**
+   * Calculate the scaling values, dependent on the range in x, y, and z direction
+   */
+  Graph3d.prototype._setScale = function () {
+    this.scale = new Point3d(1 / (this.xMax - this.xMin), 1 / (this.yMax - this.yMin), 1 / (this.zMax - this.zMin));
+
+    // keep aspect ration between x and y scale if desired
+    if (this.keepAspectRatio) {
+      if (this.scale.x < this.scale.y) {
+        //noinspection JSSuspiciousNameCombination
+        this.scale.y = this.scale.x;
+      } else {
+        //noinspection JSSuspiciousNameCombination
+        this.scale.x = this.scale.y;
+      }
+    }
+
+    // scale the vertical axis
+    this.scale.z *= this.verticalRatio;
+    // TODO: can this be automated? verticalRatio?
+
+    // determine scale for (optional) value
+    this.scale.value = 1 / (this.valueMax - this.valueMin);
+
+    // position the camera arm
+    var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x;
+    var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y;
+    var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z;
+    this.camera.setArmLocation(xCenter, yCenter, zCenter);
+  };
+
+  /**
+   * Convert a 3D location to a 2D location on screen
+   * http://en.wikipedia.org/wiki/3D_projection
+   * @param {Point3d} point3d   A 3D point with parameters x, y, z
+   * @return {Point2d} point2d  A 2D point with parameters x, y
+   */
+  Graph3d.prototype._convert3Dto2D = function (point3d) {
+    var translation = this._convertPointToTranslation(point3d);
+    return this._convertTranslationToScreen(translation);
+  };
+
+  /**
+   * Convert a 3D location its translation seen from the camera
+   * http://en.wikipedia.org/wiki/3D_projection
+   * @param {Point3d} point3d    A 3D point with parameters x, y, z
+   * @return {Point3d} translation A 3D point with parameters x, y, z This is
+   *                   the translation of the point, seen from the
+   *                   camera
+   */
+  Graph3d.prototype._convertPointToTranslation = function (point3d) {
+    var ax = point3d.x * this.scale.x,
+        ay = point3d.y * this.scale.y,
+        az = point3d.z * this.scale.z,
+        cx = this.camera.getCameraLocation().x,
+        cy = this.camera.getCameraLocation().y,
+        cz = this.camera.getCameraLocation().z,
+
+    // calculate angles
+    sinTx = Math.sin(this.camera.getCameraRotation().x),
+        cosTx = Math.cos(this.camera.getCameraRotation().x),
+        sinTy = Math.sin(this.camera.getCameraRotation().y),
+        cosTy = Math.cos(this.camera.getCameraRotation().y),
+        sinTz = Math.sin(this.camera.getCameraRotation().z),
+        cosTz = Math.cos(this.camera.getCameraRotation().z),
+
+    // calculate translation
+    dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz),
+        dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax - cx)),
+        dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax - cx));
+
+    return new Point3d(dx, dy, dz);
+  };
+
+  /**
+   * Convert a translation point to a point on the screen
+   * @param {Point3d} translation   A 3D point with parameters x, y, z This is
+   *                    the translation of the point, seen from the
+   *                    camera
+   * @return {Point2d} point2d    A 2D point with parameters x, y
+   */
+  Graph3d.prototype._convertTranslationToScreen = function (translation) {
+    var ex = this.eye.x,
+        ey = this.eye.y,
+        ez = this.eye.z,
+        dx = translation.x,
+        dy = translation.y,
+        dz = translation.z;
+
+    // calculate position on screen from translation
+    var bx;
+    var by;
+    if (this.showPerspective) {
+      bx = (dx - ex) * (ez / dz);
+      by = (dy - ey) * (ez / dz);
+    } else {
+      bx = dx * -(ez / this.camera.getArmLength());
+      by = dy * -(ez / this.camera.getArmLength());
+    }
+
+    // shift and scale the point to the center of the screen
+    // use the width of the graph to scale both horizontally and vertically.
+    return new Point2d(this.xcenter + bx * this.frame.canvas.clientWidth, this.ycenter - by * this.frame.canvas.clientWidth);
+  };
+
+  /**
+   * Set the background styling for the graph
+   * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor
+   */
+  Graph3d.prototype._setBackgroundColor = function (backgroundColor) {
+    var fill = 'white';
+    var stroke = 'gray';
+    var strokeWidth = 1;
+
+    if (typeof backgroundColor === 'string') {
+      fill = backgroundColor;
+      stroke = 'none';
+      strokeWidth = 0;
+    } else if (typeof backgroundColor === 'object') {
+      if (backgroundColor.fill !== undefined) fill = backgroundColor.fill;
+      if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke;
+      if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth;
+    } else if (backgroundColor === undefined) {} else {
+      throw 'Unsupported type of backgroundColor';
+    }
+
+    this.frame.style.backgroundColor = fill;
+    this.frame.style.borderColor = stroke;
+    this.frame.style.borderWidth = strokeWidth + 'px';
+    this.frame.style.borderStyle = 'solid';
+  };
+
+  /// enumerate the available styles
+  Graph3d.STYLE = {
+    BAR: 0,
+    BARCOLOR: 1,
+    BARSIZE: 2,
+    DOT: 3,
+    DOTLINE: 4,
+    DOTCOLOR: 5,
+    DOTSIZE: 6,
+    GRID: 7,
+    LINE: 8,
+    SURFACE: 9
+  };
+
+  /**
+   * Retrieve the style index from given styleName
+   * @param {string} styleName  Style name such as 'dot', 'grid', 'dot-line'
+   * @return {Number} styleNumber Enumeration value representing the style, or -1
+   *                when not found
+   */
+  Graph3d.prototype._getStyleNumber = function (styleName) {
+    switch (styleName) {
+      case 'dot':
+        return Graph3d.STYLE.DOT;
+      case 'dot-line':
+        return Graph3d.STYLE.DOTLINE;
+      case 'dot-color':
+        return Graph3d.STYLE.DOTCOLOR;
+      case 'dot-size':
+        return Graph3d.STYLE.DOTSIZE;
+      case 'line':
+        return Graph3d.STYLE.LINE;
+      case 'grid':
+        return Graph3d.STYLE.GRID;
+      case 'surface':
+        return Graph3d.STYLE.SURFACE;
+      case 'bar':
+        return Graph3d.STYLE.BAR;
+      case 'bar-color':
+        return Graph3d.STYLE.BARCOLOR;
+      case 'bar-size':
+        return Graph3d.STYLE.BARSIZE;
+    }
+
+    return -1;
+  };
+
+  /**
+   * Determine the indexes of the data columns, based on the given style and data
+   * @param {DataSet} data
+   * @param {Number}  style
+   */
+  Graph3d.prototype._determineColumnIndexes = function (data, style) {
+    if (this.style === Graph3d.STYLE.DOT || this.style === Graph3d.STYLE.DOTLINE || this.style === Graph3d.STYLE.LINE || this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE || this.style === Graph3d.STYLE.BAR) {
+      // 3 columns expected, and optionally a 4th with filter values
+      this.colX = 0;
+      this.colY = 1;
+      this.colZ = 2;
+      this.colValue = undefined;
+
+      if (data.getNumberOfColumns() > 3) {
+        this.colFilter = 3;
+      }
+    } else if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) {
+      // 4 columns expected, and optionally a 5th with filter values
+      this.colX = 0;
+      this.colY = 1;
+      this.colZ = 2;
+      this.colValue = 3;
+
+      if (data.getNumberOfColumns() > 4) {
+        this.colFilter = 4;
+      }
+    } else {
+      throw 'Unknown style "' + this.style + '"';
+    }
+  };
+
+  Graph3d.prototype.getNumberOfRows = function (data) {
+    return data.length;
+  };
+
+  Graph3d.prototype.getNumberOfColumns = function (data) {
+    var counter = 0;
+    for (var column in data[0]) {
+      if (data[0].hasOwnProperty(column)) {
+        counter++;
+      }
+    }
+    return counter;
+  };
+
+  Graph3d.prototype.getDistinctValues = function (data, column) {
+    var distinctValues = [];
+    for (var i = 0; i < data.length; i++) {
+      if (distinctValues.indexOf(data[i][column]) == -1) {
+        distinctValues.push(data[i][column]);
+      }
+    }
+    return distinctValues;
+  };
+
+  Graph3d.prototype.getColumnRange = function (data, column) {
+    var minMax = { min: data[0][column], max: data[0][column] };
+    for (var i = 0; i < data.length; i++) {
+      if (minMax.min > data[i][column]) {
+        minMax.min = data[i][column];
+      }
+      if (minMax.max < data[i][column]) {
+        minMax.max = data[i][column];
+      }
+    }
+    return minMax;
+  };
+
+  /**
+   * Initialize the data from the data table. Calculate minimum and maximum values
+   * and column index values
+   * @param {Array | DataSet | DataView} rawData   The data containing the items for the Graph.
+   * @param {Number}     style   Style Number
+   */
+  Graph3d.prototype._dataInitialize = function (rawData, style) {
+    var me = this;
+
+    // unsubscribe from the dataTable
+    if (this.dataSet) {
+      this.dataSet.off('*', this._onChange);
+    }
+
+    if (rawData === undefined) return;
+
+    if (Array.isArray(rawData)) {
+      rawData = new DataSet(rawData);
+    }
+
+    var data;
+    if (rawData instanceof DataSet || rawData instanceof DataView) {
+      data = rawData.get();
+    } else {
+      throw new Error('Array, DataSet, or DataView expected');
+    }
+
+    if (data.length == 0) return;
+
+    this.dataSet = rawData;
+    this.dataTable = data;
+
+    // subscribe to changes in the dataset
+    this._onChange = function () {
+      me.setData(me.dataSet);
+    };
+    this.dataSet.on('*', this._onChange);
+
+    // _determineColumnIndexes
+    // getNumberOfRows (points)
+    // getNumberOfColumns (x,y,z,v,t,t1,t2...)
+    // getDistinctValues (unique values?)
+    // getColumnRange
+
+    // determine the location of x,y,z,value,filter columns
+    this.colX = 'x';
+    this.colY = 'y';
+    this.colZ = 'z';
+    this.colValue = 'style';
+    this.colFilter = 'filter';
+
+    // check if a filter column is provided
+    if (data[0].hasOwnProperty('filter')) {
+      if (this.dataFilter === undefined) {
+        this.dataFilter = new Filter(rawData, this.colFilter, this);
+        this.dataFilter.setOnLoadCallback(function () {
+          me.redraw();
+        });
+      }
+    }
+
+    var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE;
+
+    // determine barWidth from data
+    if (withBars) {
+      if (this.defaultXBarWidth !== undefined) {
+        this.xBarWidth = this.defaultXBarWidth;
+      } else {
+        var dataX = this.getDistinctValues(data, this.colX);
+        this.xBarWidth = dataX[1] - dataX[0] || 1;
+      }
+
+      if (this.defaultYBarWidth !== undefined) {
+        this.yBarWidth = this.defaultYBarWidth;
+      } else {
+        var dataY = this.getDistinctValues(data, this.colY);
+        this.yBarWidth = dataY[1] - dataY[0] || 1;
+      }
+    }
+
+    // calculate minimums and maximums
+    var xRange = this.getColumnRange(data, this.colX);
+    if (withBars) {
+      xRange.min -= this.xBarWidth / 2;
+      xRange.max += this.xBarWidth / 2;
+    }
+    this.xMin = this.defaultXMin !== undefined ? this.defaultXMin : xRange.min;
+    this.xMax = this.defaultXMax !== undefined ? this.defaultXMax : xRange.max;
+    if (this.xMax <= this.xMin) this.xMax = this.xMin + 1;
+    this.xStep = this.defaultXStep !== undefined ? this.defaultXStep : (this.xMax - this.xMin) / 5;
+
+    var yRange = this.getColumnRange(data, this.colY);
+    if (withBars) {
+      yRange.min -= this.yBarWidth / 2;
+      yRange.max += this.yBarWidth / 2;
+    }
+    this.yMin = this.defaultYMin !== undefined ? this.defaultYMin : yRange.min;
+    this.yMax = this.defaultYMax !== undefined ? this.defaultYMax : yRange.max;
+    if (this.yMax <= this.yMin) this.yMax = this.yMin + 1;
+    this.yStep = this.defaultYStep !== undefined ? this.defaultYStep : (this.yMax - this.yMin) / 5;
+
+    var zRange = this.getColumnRange(data, this.colZ);
+    this.zMin = this.defaultZMin !== undefined ? this.defaultZMin : zRange.min;
+    this.zMax = this.defaultZMax !== undefined ? this.defaultZMax : zRange.max;
+    if (this.zMax <= this.zMin) this.zMax = this.zMin + 1;
+    this.zStep = this.defaultZStep !== undefined ? this.defaultZStep : (this.zMax - this.zMin) / 5;
+
+    if (this.colValue !== undefined) {
+      var valueRange = this.getColumnRange(data, this.colValue);
+      this.valueMin = this.defaultValueMin !== undefined ? this.defaultValueMin : valueRange.min;
+      this.valueMax = this.defaultValueMax !== undefined ? this.defaultValueMax : valueRange.max;
+      if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1;
+    }
+
+    // set the scale dependent on the ranges.
+    this._setScale();
+  };
+
+  /**
+   * Filter the data based on the current filter
+   * @param {Array} data
+   * @return {Array} dataPoints   Array with point objects which can be drawn on screen
+   */
+  Graph3d.prototype._getDataPoints = function (data) {
+    // TODO: store the created matrix dataPoints in the filters instead of reloading each time
+    var x, y, i, z, obj, point;
+
+    var dataPoints = [];
+
+    if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) {
+      // copy all values from the google data table to a matrix
+      // the provided values are supposed to form a grid of (x,y) positions
+
+      // create two lists with all present x and y values
+      var dataX = [];
+      var dataY = [];
+      for (i = 0; i < this.getNumberOfRows(data); i++) {
+        x = data[i][this.colX] || 0;
+        y = data[i][this.colY] || 0;
+
+        if (dataX.indexOf(x) === -1) {
+          dataX.push(x);
+        }
+        if (dataY.indexOf(y) === -1) {
+          dataY.push(y);
+        }
+      }
+
+      var sortNumber = function sortNumber(a, b) {
+        return a - b;
+      };
+      dataX.sort(sortNumber);
+      dataY.sort(sortNumber);
+
+      // create a grid, a 2d matrix, with all values.
+      var dataMatrix = []; // temporary data matrix
+      for (i = 0; i < data.length; i++) {
+        x = data[i][this.colX] || 0;
+        y = data[i][this.colY] || 0;
+        z = data[i][this.colZ] || 0;
+
+        var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer
+        var yIndex = dataY.indexOf(y);
+
+        if (dataMatrix[xIndex] === undefined) {
+          dataMatrix[xIndex] = [];
+        }
+
+        var point3d = new Point3d();
+        point3d.x = x;
+        point3d.y = y;
+        point3d.z = z;
+
+        obj = {};
+        obj.point = point3d;
+        obj.trans = undefined;
+        obj.screen = undefined;
+        obj.bottom = new Point3d(x, y, this.zMin);
+
+        dataMatrix[xIndex][yIndex] = obj;
+
+        dataPoints.push(obj);
+      }
+
+      // fill in the pointers to the neighbors.
+      for (x = 0; x < dataMatrix.length; x++) {
+        for (y = 0; y < dataMatrix[x].length; y++) {
+          if (dataMatrix[x][y]) {
+            dataMatrix[x][y].pointRight = x < dataMatrix.length - 1 ? dataMatrix[x + 1][y] : undefined;
+            dataMatrix[x][y].pointTop = y < dataMatrix[x].length - 1 ? dataMatrix[x][y + 1] : undefined;
+            dataMatrix[x][y].pointCross = x < dataMatrix.length - 1 && y < dataMatrix[x].length - 1 ? dataMatrix[x + 1][y + 1] : undefined;
+          }
+        }
+      }
+    } else {
+      // 'dot', 'dot-line', etc.
+      // copy all values from the google data table to a list with Point3d objects
+      for (i = 0; i < data.length; i++) {
+        point = new Point3d();
+        point.x = data[i][this.colX] || 0;
+        point.y = data[i][this.colY] || 0;
+        point.z = data[i][this.colZ] || 0;
+
+        if (this.colValue !== undefined) {
+          point.value = data[i][this.colValue] || 0;
+        }
+
+        obj = {};
+        obj.point = point;
+        obj.bottom = new Point3d(point.x, point.y, this.zMin);
+        obj.trans = undefined;
+        obj.screen = undefined;
+
+        dataPoints.push(obj);
+      }
+    }
+
+    return dataPoints;
+  };
+
+  /**
+   * Create the main frame for the Graph3d.
+   * This function is executed once when a Graph3d object is created. The frame
+   * contains a canvas, and this canvas contains all objects like the axis and
+   * nodes.
+   */
+  Graph3d.prototype.create = function () {
+    // remove all elements from the container element.
+    while (this.containerElement.hasChildNodes()) {
+      this.containerElement.removeChild(this.containerElement.firstChild);
+    }
+
+    this.frame = document.createElement('div');
+    this.frame.style.position = 'relative';
+    this.frame.style.overflow = 'hidden';
+
+    // create the graph canvas (HTML canvas element)
+    this.frame.canvas = document.createElement('canvas');
+    this.frame.canvas.style.position = 'relative';
+    this.frame.appendChild(this.frame.canvas);
+    //if (!this.frame.canvas.getContext) {
+    {
+      var noCanvas = document.createElement('DIV');
+      noCanvas.style.color = 'red';
+      noCanvas.style.fontWeight = 'bold';
+      noCanvas.style.padding = '10px';
+      noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
+      this.frame.canvas.appendChild(noCanvas);
+    }
+
+    this.frame.filter = document.createElement('div');
+    this.frame.filter.style.position = 'absolute';
+    this.frame.filter.style.bottom = '0px';
+    this.frame.filter.style.left = '0px';
+    this.frame.filter.style.width = '100%';
+    this.frame.appendChild(this.frame.filter);
+
+    // add event listeners to handle moving and zooming the contents
+    var me = this;
+    var onmousedown = function onmousedown(event) {
+      me._onMouseDown(event);
+    };
+    var ontouchstart = function ontouchstart(event) {
+      me._onTouchStart(event);
+    };
+    var onmousewheel = function onmousewheel(event) {
+      me._onWheel(event);
+    };
+    var ontooltip = function ontooltip(event) {
+      me._onTooltip(event);
+    };
+    // TODO: these events are never cleaned up... can give a 'memory leakage'
+
+    util.addEventListener(this.frame.canvas, 'keydown', onkeydown);
+    util.addEventListener(this.frame.canvas, 'mousedown', onmousedown);
+    util.addEventListener(this.frame.canvas, 'touchstart', ontouchstart);
+    util.addEventListener(this.frame.canvas, 'mousewheel', onmousewheel);
+    util.addEventListener(this.frame.canvas, 'mousemove', ontooltip);
+
+    // add the new graph to the container element
+    this.containerElement.appendChild(this.frame);
+  };
+
+  /**
+   * Set a new size for the graph
+   * @param {string} width   Width in pixels or percentage (for example '800px'
+   *             or '50%')
+   * @param {string} height  Height in pixels or percentage  (for example '400px'
+   *             or '30%')
+   */
+  Graph3d.prototype.setSize = function (width, height) {
+    this.frame.style.width = width;
+    this.frame.style.height = height;
+
+    this._resizeCanvas();
+  };
+
+  /**
+   * Resize the canvas to the current size of the frame
+   */
+  Graph3d.prototype._resizeCanvas = function () {
+    this.frame.canvas.style.width = '100%';
+    this.frame.canvas.style.height = '100%';
+
+    this.frame.canvas.width = this.frame.canvas.clientWidth;
+    this.frame.canvas.height = this.frame.canvas.clientHeight;
+
+    // adjust with for margin
+    this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + 'px';
+  };
+
+  /**
+   * Start animation
+   */
+  Graph3d.prototype.animationStart = function () {
+    if (!this.frame.filter || !this.frame.filter.slider) throw 'No animation available';
+
+    this.frame.filter.slider.play();
+  };
+
+  /**
+   * Stop animation
+   */
+  Graph3d.prototype.animationStop = function () {
+    if (!this.frame.filter || !this.frame.filter.slider) return;
+
+    this.frame.filter.slider.stop();
+  };
+
+  /**
+   * Resize the center position based on the current values in this.defaultXCenter
+   * and this.defaultYCenter (which are strings with a percentage or a value
+   * in pixels). The center positions are the variables this.xCenter
+   * and this.yCenter
+   */
+  Graph3d.prototype._resizeCenter = function () {
+    // calculate the horizontal center position
+    if (this.defaultXCenter.charAt(this.defaultXCenter.length - 1) === '%') {
+      this.xcenter = parseFloat(this.defaultXCenter) / 100 * this.frame.canvas.clientWidth;
+    } else {
+      this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px
+    }
+
+    // calculate the vertical center position
+    if (this.defaultYCenter.charAt(this.defaultYCenter.length - 1) === '%') {
+      this.ycenter = parseFloat(this.defaultYCenter) / 100 * (this.frame.canvas.clientHeight - this.frame.filter.clientHeight);
+    } else {
+      this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px
+    }
+  };
+
+  /**
+   * Set the rotation and distance of the camera
+   * @param {Object} pos   An object with the camera position. The object
+   *             contains three parameters:
+   *             - horizontal {Number}
+   *             The horizontal rotation, between 0 and 2*PI.
+   *             Optional, can be left undefined.
+   *             - vertical {Number}
+   *             The vertical rotation, between 0 and 0.5*PI
+   *             if vertical=0.5*PI, the graph is shown from the
+   *             top. Optional, can be left undefined.
+   *             - distance {Number}
+   *             The (normalized) distance of the camera to the
+   *             center of the graph, a value between 0.71 and 5.0.
+   *             Optional, can be left undefined.
+   */
+  Graph3d.prototype.setCameraPosition = function (pos) {
+    if (pos === undefined) {
+      return;
+    }
+
+    if (pos.horizontal !== undefined && pos.vertical !== undefined) {
+      this.camera.setArmRotation(pos.horizontal, pos.vertical);
+    }
+
+    if (pos.distance !== undefined) {
+      this.camera.setArmLength(pos.distance);
+    }
+
+    this.redraw();
+  };
+
+  /**
+   * Retrieve the current camera rotation
+   * @return {object}   An object with parameters horizontal, vertical, and
+   *          distance
+   */
+  Graph3d.prototype.getCameraPosition = function () {
+    var pos = this.camera.getArmRotation();
+    pos.distance = this.camera.getArmLength();
+    return pos;
+  };
+
+  /**
+   * Load data into the 3D Graph
+   */
+  Graph3d.prototype._readData = function (data) {
+    // read the data
+    this._dataInitialize(data, this.style);
+
+    if (this.dataFilter) {
+      // apply filtering
+      this.dataPoints = this.dataFilter._getDataPoints();
+    } else {
+      // no filtering. load all data
+      this.dataPoints = this._getDataPoints(this.dataTable);
+    }
+
+    // draw the filter
+    this._redrawFilter();
+  };
+
+  /**
+   * Replace the dataset of the Graph3d
+   * @param {Array | DataSet | DataView} data
+   */
+  Graph3d.prototype.setData = function (data) {
+    this._readData(data);
+    this.redraw();
+
+    // start animation when option is true
+    if (this.animationAutoStart && this.dataFilter) {
+      this.animationStart();
+    }
+  };
+
+  /**
+   * Update the options. Options will be merged with current options
+   * @param {Object} options
+   */
+  Graph3d.prototype.setOptions = function (options) {
+    var cameraPosition = undefined;
+
+    this.animationStop();
+
+    if (options !== undefined) {
+      // retrieve parameter values
+      if (options.width !== undefined) this.width = options.width;
+      if (options.height !== undefined) this.height = options.height;
+
+      if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter;
+      if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter;
+
+      if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel;
+      if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel;
+      if (options.xLabel !== undefined) this.xLabel = options.xLabel;
+      if (options.yLabel !== undefined) this.yLabel = options.yLabel;
+      if (options.zLabel !== undefined) this.zLabel = options.zLabel;
+
+      if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel;
+      if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel;
+      if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel;
+
+      if (options.style !== undefined) {
+        var styleNumber = this._getStyleNumber(options.style);
+        if (styleNumber !== -1) {
+          this.style = styleNumber;
+        }
+      }
+      if (options.showGrid !== undefined) this.showGrid = options.showGrid;
+      if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective;
+      if (options.showShadow !== undefined) this.showShadow = options.showShadow;
+      if (options.tooltip !== undefined) this.showTooltip = options.tooltip;
+      if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls;
+      if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio;
+      if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio;
+
+      if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval;
+      if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload;
+      if (options.animationAutoStart !== undefined) this.animationAutoStart = options.animationAutoStart;
+
+      if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth;
+      if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth;
+
+      if (options.xMin !== undefined) this.defaultXMin = options.xMin;
+      if (options.xStep !== undefined) this.defaultXStep = options.xStep;
+      if (options.xMax !== undefined) this.defaultXMax = options.xMax;
+      if (options.yMin !== undefined) this.defaultYMin = options.yMin;
+      if (options.yStep !== undefined) this.defaultYStep = options.yStep;
+      if (options.yMax !== undefined) this.defaultYMax = options.yMax;
+      if (options.zMin !== undefined) this.defaultZMin = options.zMin;
+      if (options.zStep !== undefined) this.defaultZStep = options.zStep;
+      if (options.zMax !== undefined) this.defaultZMax = options.zMax;
+      if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin;
+      if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax;
+      if (options.backgroundColor !== undefined) this._setBackgroundColor(options.backgroundColor);
+
+      if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition;
+
+      if (cameraPosition !== undefined) {
+        this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical);
+        this.camera.setArmLength(cameraPosition.distance);
+      }
+
+      // colors
+      if (options.axisColor !== undefined) this.axisColor = options.axisColor;
+      if (options.gridColor !== undefined) this.gridColor = options.gridColor;
+      if (options.dataColor) {
+        if (typeof options.dataColor === 'string') {
+          this.dataColor.fill = options.dataColor;
+          this.dataColor.stroke = options.dataColor;
+        } else {
+          if (options.dataColor.fill) {
+            this.dataColor.fill = options.dataColor.fill;
+          }
+          if (options.dataColor.stroke) {
+            this.dataColor.stroke = options.dataColor.stroke;
+          }
+          if (options.dataColor.strokeWidth !== undefined) {
+            this.dataColor.strokeWidth = options.dataColor.strokeWidth;
+          }
+        }
+      }
+    }
+
+    this.setSize(this.width, this.height);
+
+    // re-load the data
+    if (this.dataTable) {
+      this.setData(this.dataTable);
+    }
+
+    // start animation when option is true
+    if (this.animationAutoStart && this.dataFilter) {
+      this.animationStart();
+    }
+  };
+
+  /**
+   * Redraw the Graph.
+   */
+  Graph3d.prototype.redraw = function () {
+    if (this.dataPoints === undefined) {
+      throw 'Error: graph data not initialized';
+    }
+
+    this._resizeCanvas();
+    this._resizeCenter();
+    this._redrawSlider();
+    this._redrawClear();
+    this._redrawAxis();
+
+    if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) {
+      this._redrawDataGrid();
+    } else if (this.style === Graph3d.STYLE.LINE) {
+      this._redrawDataLine();
+    } else if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) {
+      this._redrawDataBar();
+    } else {
+      // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE
+      this._redrawDataDot();
+    }
+
+    this._redrawInfo();
+    this._redrawLegend();
+  };
+
+  /**
+   * Clear the canvas before redrawing
+   */
+  Graph3d.prototype._redrawClear = function () {
+    var canvas = this.frame.canvas;
+    var ctx = canvas.getContext('2d');
+
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+  };
+
+  /**
+   * Redraw the legend showing the colors
+   */
+  Graph3d.prototype._redrawLegend = function () {
+    var y;
+
+    if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) {
+
+      var dotSize = this.frame.clientWidth * 0.02;
+
+      var widthMin, widthMax;
+      if (this.style === Graph3d.STYLE.DOTSIZE) {
+        widthMin = dotSize / 2; // px
+        widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function
+      } else {
+        widthMin = 20; // px
+        widthMax = 20; // px
+      }
+
+      var height = Math.max(this.frame.clientHeight * 0.25, 100);
+      var top = this.margin;
+      var right = this.frame.clientWidth - this.margin;
+      var left = right - widthMax;
+      var bottom = top + height;
+    }
+
+    var canvas = this.frame.canvas;
+    var ctx = canvas.getContext('2d');
+    ctx.lineWidth = 1;
+    ctx.font = '14px arial'; // TODO: put in options
+
+    if (this.style === Graph3d.STYLE.DOTCOLOR) {
+      // draw the color bar
+      var ymin = 0;
+      var ymax = height; // Todo: make height customizable
+      for (y = ymin; y < ymax; y++) {
+        var f = (y - ymin) / (ymax - ymin);
+
+        //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function
+        var hue = f * 240;
+        var color = this._hsv2rgb(hue, 1, 1);
+
+        ctx.strokeStyle = color;
+        ctx.beginPath();
+        ctx.moveTo(left, top + y);
+        ctx.lineTo(right, top + y);
+        ctx.stroke();
+      }
+
+      ctx.strokeStyle = this.axisColor;
+      ctx.strokeRect(left, top, widthMax, height);
+    }
+
+    if (this.style === Graph3d.STYLE.DOTSIZE) {
+      // draw border around color bar
+      ctx.strokeStyle = this.axisColor;
+      ctx.fillStyle = this.dataColor.fill;
+      ctx.beginPath();
+      ctx.moveTo(left, top);
+      ctx.lineTo(right, top);
+      ctx.lineTo(right - widthMax + widthMin, bottom);
+      ctx.lineTo(left, bottom);
+      ctx.closePath();
+      ctx.fill();
+      ctx.stroke();
+    }
+
+    if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) {
+      // print values along the color bar
+      var gridLineLen = 5; // px
+      var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax - this.valueMin) / 5, true);
+      step.start();
+      if (step.getCurrent() < this.valueMin) {
+        step.next();
+      }
+      while (!step.end()) {
+        y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height;
+
+        ctx.beginPath();
+        ctx.moveTo(left - gridLineLen, y);
+        ctx.lineTo(left, y);
+        ctx.stroke();
+
+        ctx.textAlign = 'right';
+        ctx.textBaseline = 'middle';
+        ctx.fillStyle = this.axisColor;
+        ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y);
+
+        step.next();
+      }
+
+      ctx.textAlign = 'right';
+      ctx.textBaseline = 'top';
+      var label = this.legendLabel;
+      ctx.fillText(label, right, bottom + this.margin);
+    }
+  };
+
+  /**
+   * Redraw the filter
+   */
+  Graph3d.prototype._redrawFilter = function () {
+    this.frame.filter.innerHTML = '';
+
+    if (this.dataFilter) {
+      var options = {
+        'visible': this.showAnimationControls
+      };
+      var slider = new Slider(this.frame.filter, options);
+      this.frame.filter.slider = slider;
+
+      // TODO: css here is not nice here...
+      this.frame.filter.style.padding = '10px';
+      //this.frame.filter.style.backgroundColor = '#EFEFEF';
+
+      slider.setValues(this.dataFilter.values);
+      slider.setPlayInterval(this.animationInterval);
+
+      // create an event handler
+      var me = this;
+      var onchange = function onchange() {
+        var index = slider.getIndex();
+
+        me.dataFilter.selectValue(index);
+        me.dataPoints = me.dataFilter._getDataPoints();
+
+        me.redraw();
+      };
+      slider.setOnChangeCallback(onchange);
+    } else {
+      this.frame.filter.slider = undefined;
+    }
+  };
+
+  /**
+   * Redraw the slider
+   */
+  Graph3d.prototype._redrawSlider = function () {
+    if (this.frame.filter.slider !== undefined) {
+      this.frame.filter.slider.redraw();
+    }
+  };
+
+  /**
+   * Redraw common information
+   */
+  Graph3d.prototype._redrawInfo = function () {
+    if (this.dataFilter) {
+      var canvas = this.frame.canvas;
+      var ctx = canvas.getContext('2d');
+
+      ctx.font = '14px arial'; // TODO: put in options
+      ctx.lineStyle = 'gray';
+      ctx.fillStyle = 'gray';
+      ctx.textAlign = 'left';
+      ctx.textBaseline = 'top';
+
+      var x = this.margin;
+      var y = this.margin;
+      ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y);
+    }
+  };
+
+  /**
+   * Redraw the axis
+   */
+  Graph3d.prototype._redrawAxis = function () {
+    var canvas = this.frame.canvas,
+        ctx = canvas.getContext('2d'),
+        from,
+        to,
+        step,
+        prettyStep,
+        text,
+        xText,
+        yText,
+        zText,
+        offset,
+        xOffset,
+        yOffset,
+        xMin2d,
+        xMax2d;
+
+    // TODO: get the actual rendered style of the containerElement
+    //ctx.font = this.containerElement.style.font;
+    ctx.font = 24 / this.camera.getArmLength() + 'px arial';
+
+    // calculate the length for the short grid lines
+    var gridLenX = 0.025 / this.scale.x;
+    var gridLenY = 0.025 / this.scale.y;
+    var textMargin = 5 / this.camera.getArmLength(); // px
+    var armAngle = this.camera.getArmRotation().horizontal;
+
+    // draw x-grid lines
+    ctx.lineWidth = 1;
+    prettyStep = this.defaultXStep === undefined;
+    step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep);
+    step.start();
+    if (step.getCurrent() < this.xMin) {
+      step.next();
+    }
+    while (!step.end()) {
+      var x = step.getCurrent();
+
+      if (this.showGrid) {
+        from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
+        to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
+        ctx.strokeStyle = this.gridColor;
+        ctx.beginPath();
+        ctx.moveTo(from.x, from.y);
+        ctx.lineTo(to.x, to.y);
+        ctx.stroke();
+      } else {
+        from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
+        to = this._convert3Dto2D(new Point3d(x, this.yMin + gridLenX, this.zMin));
+        ctx.strokeStyle = this.axisColor;
+        ctx.beginPath();
+        ctx.moveTo(from.x, from.y);
+        ctx.lineTo(to.x, to.y);
+        ctx.stroke();
+
+        from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
+        to = this._convert3Dto2D(new Point3d(x, this.yMax - gridLenX, this.zMin));
+        ctx.strokeStyle = this.axisColor;
+        ctx.beginPath();
+        ctx.moveTo(from.x, from.y);
+        ctx.lineTo(to.x, to.y);
+        ctx.stroke();
+      }
+
+      yText = Math.cos(armAngle) > 0 ? this.yMin : this.yMax;
+      text = this._convert3Dto2D(new Point3d(x, yText, this.zMin));
+      if (Math.cos(armAngle * 2) > 0) {
+        ctx.textAlign = 'center';
+        ctx.textBaseline = 'top';
+        text.y += textMargin;
+      } else if (Math.sin(armAngle * 2) < 0) {
+        ctx.textAlign = 'right';
+        ctx.textBaseline = 'middle';
+      } else {
+        ctx.textAlign = 'left';
+        ctx.textBaseline = 'middle';
+      }
+      ctx.fillStyle = this.axisColor;
+      ctx.fillText('  ' + this.xValueLabel(step.getCurrent()) + '  ', text.x, text.y);
+
+      step.next();
+    }
+
+    // draw y-grid lines
+    ctx.lineWidth = 1;
+    prettyStep = this.defaultYStep === undefined;
+    step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep);
+    step.start();
+    if (step.getCurrent() < this.yMin) {
+      step.next();
+    }
+    while (!step.end()) {
+      if (this.showGrid) {
+        from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
+        to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
+        ctx.strokeStyle = this.gridColor;
+        ctx.beginPath();
+        ctx.moveTo(from.x, from.y);
+        ctx.lineTo(to.x, to.y);
+        ctx.stroke();
+      } else {
+        from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
+        to = this._convert3Dto2D(new Point3d(this.xMin + gridLenY, step.getCurrent(), this.zMin));
+        ctx.strokeStyle = this.axisColor;
+        ctx.beginPath();
+        ctx.moveTo(from.x, from.y);
+        ctx.lineTo(to.x, to.y);
+        ctx.stroke();
+
+        from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
+        to = this._convert3Dto2D(new Point3d(this.xMax - gridLenY, step.getCurrent(), this.zMin));
+        ctx.strokeStyle = this.axisColor;
+        ctx.beginPath();
+        ctx.moveTo(from.x, from.y);
+        ctx.lineTo(to.x, to.y);
+        ctx.stroke();
+      }
+
+      xText = Math.sin(armAngle) > 0 ? this.xMin : this.xMax;
+      text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin));
+      if (Math.cos(armAngle * 2) < 0) {
+        ctx.textAlign = 'center';
+        ctx.textBaseline = 'top';
+        text.y += textMargin;
+      } else if (Math.sin(armAngle * 2) > 0) {
+        ctx.textAlign = 'right';
+        ctx.textBaseline = 'middle';
+      } else {
+        ctx.textAlign = 'left';
+        ctx.textBaseline = 'middle';
+      }
+      ctx.fillStyle = this.axisColor;
+      ctx.fillText('  ' + this.yValueLabel(step.getCurrent()) + '  ', text.x, text.y);
+
+      step.next();
+    }
+
+    // draw z-grid lines and axis
+    ctx.lineWidth = 1;
+    prettyStep = this.defaultZStep === undefined;
+    step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep);
+    step.start();
+    if (step.getCurrent() < this.zMin) {
+      step.next();
+    }
+    xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax;
+    yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax;
+    while (!step.end()) {
+      // TODO: make z-grid lines really 3d?
+      from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent()));
+      ctx.strokeStyle = this.axisColor;
+      ctx.beginPath();
+      ctx.moveTo(from.x, from.y);
+      ctx.lineTo(from.x - textMargin, from.y);
+      ctx.stroke();
+
+      ctx.textAlign = 'right';
+      ctx.textBaseline = 'middle';
+      ctx.fillStyle = this.axisColor;
+      ctx.fillText(this.zValueLabel(step.getCurrent()) + ' ', from.x - 5, from.y);
+
+      step.next();
+    }
+    ctx.lineWidth = 1;
+    from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
+    to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax));
+    ctx.strokeStyle = this.axisColor;
+    ctx.beginPath();
+    ctx.moveTo(from.x, from.y);
+    ctx.lineTo(to.x, to.y);
+    ctx.stroke();
+
+    // draw x-axis
+    ctx.lineWidth = 1;
+    // line at yMin
+    xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
+    xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
+    ctx.strokeStyle = this.axisColor;
+    ctx.beginPath();
+    ctx.moveTo(xMin2d.x, xMin2d.y);
+    ctx.lineTo(xMax2d.x, xMax2d.y);
+    ctx.stroke();
+    // line at ymax
+    xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
+    xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
+    ctx.strokeStyle = this.axisColor;
+    ctx.beginPath();
+    ctx.moveTo(xMin2d.x, xMin2d.y);
+    ctx.lineTo(xMax2d.x, xMax2d.y);
+    ctx.stroke();
+
+    // draw y-axis
+    ctx.lineWidth = 1;
+    // line at xMin
+    from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
+    to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
+    ctx.strokeStyle = this.axisColor;
+    ctx.beginPath();
+    ctx.moveTo(from.x, from.y);
+    ctx.lineTo(to.x, to.y);
+    ctx.stroke();
+    // line at xMax
+    from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
+    to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
+    ctx.strokeStyle = this.axisColor;
+    ctx.beginPath();
+    ctx.moveTo(from.x, from.y);
+    ctx.lineTo(to.x, to.y);
+    ctx.stroke();
+
+    // draw x-label
+    var xLabel = this.xLabel;
+    if (xLabel.length > 0) {
+      yOffset = 0.1 / this.scale.y;
+      xText = (this.xMin + this.xMax) / 2;
+      yText = Math.cos(armAngle) > 0 ? this.yMin - yOffset : this.yMax + yOffset;
+      text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
+      if (Math.cos(armAngle * 2) > 0) {
+        ctx.textAlign = 'center';
+        ctx.textBaseline = 'top';
+      } else if (Math.sin(armAngle * 2) < 0) {
+        ctx.textAlign = 'right';
+        ctx.textBaseline = 'middle';
+      } else {
+        ctx.textAlign = 'left';
+        ctx.textBaseline = 'middle';
+      }
+      ctx.fillStyle = this.axisColor;
+      ctx.fillText(xLabel, text.x, text.y);
+    }
+
+    // draw y-label
+    var yLabel = this.yLabel;
+    if (yLabel.length > 0) {
+      xOffset = 0.1 / this.scale.x;
+      xText = Math.sin(armAngle) > 0 ? this.xMin - xOffset : this.xMax + xOffset;
+      yText = (this.yMin + this.yMax) / 2;
+      text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
+      if (Math.cos(armAngle * 2) < 0) {
+        ctx.textAlign = 'center';
+        ctx.textBaseline = 'top';
+      } else if (Math.sin(armAngle * 2) > 0) {
+        ctx.textAlign = 'right';
+        ctx.textBaseline = 'middle';
+      } else {
+        ctx.textAlign = 'left';
+        ctx.textBaseline = 'middle';
+      }
+      ctx.fillStyle = this.axisColor;
+      ctx.fillText(yLabel, text.x, text.y);
+    }
+
+    // draw z-label
+    var zLabel = this.zLabel;
+    if (zLabel.length > 0) {
+      offset = 30; // pixels.  // TODO: relate to the max width of the values on the z axis?
+      xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax;
+      yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax;
+      zText = (this.zMin + this.zMax) / 2;
+      text = this._convert3Dto2D(new Point3d(xText, yText, zText));
+      ctx.textAlign = 'right';
+      ctx.textBaseline = 'middle';
+      ctx.fillStyle = this.axisColor;
+      ctx.fillText(zLabel, text.x - offset, text.y);
+    }
+  };
+
+  /**
+   * Calculate the color based on the given value.
+   * @param {Number} H   Hue, a value be between 0 and 360
+   * @param {Number} S   Saturation, a value between 0 and 1
+   * @param {Number} V   Value, a value between 0 and 1
+   */
+  Graph3d.prototype._hsv2rgb = function (H, S, V) {
+    var R, G, B, C, Hi, X;
+
+    C = V * S;
+    Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5
+    X = C * (1 - Math.abs(H / 60 % 2 - 1));
+
+    switch (Hi) {
+      case 0:
+        R = C;G = X;B = 0;break;
+      case 1:
+        R = X;G = C;B = 0;break;
+      case 2:
+        R = 0;G = C;B = X;break;
+      case 3:
+        R = 0;G = X;B = C;break;
+      case 4:
+        R = X;G = 0;B = C;break;
+      case 5:
+        R = C;G = 0;B = X;break;
+
+      default:
+        R = 0;G = 0;B = 0;break;
+    }
+
+    return 'RGB(' + parseInt(R * 255) + ',' + parseInt(G * 255) + ',' + parseInt(B * 255) + ')';
+  };
+
+  /**
+   * Draw all datapoints as a grid
+   * This function can be used when the style is 'grid'
+   */
+  Graph3d.prototype._redrawDataGrid = function () {
+    var canvas = this.frame.canvas,
+        ctx = canvas.getContext('2d'),
+        point,
+        right,
+        top,
+        cross,
+        i,
+        topSideVisible,
+        fillStyle,
+        strokeStyle,
+        lineWidth,
+        h,
+        s,
+        v,
+        zAvg;
+
+    ctx.lineJoin = 'round';
+    ctx.lineCap = 'round';
+
+    if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception?
+
+    // calculate the translations and screen position of all points
+    for (i = 0; i < this.dataPoints.length; i++) {
+      var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+      var screen = this._convertTranslationToScreen(trans);
+
+      this.dataPoints[i].trans = trans;
+      this.dataPoints[i].screen = screen;
+
+      // calculate the translation of the point at the bottom (needed for sorting)
+      var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
+      this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
+    }
+
+    // sort the points on depth of their (x,y) position (not on z)
+    var sortDepth = function sortDepth(a, b) {
+      return b.dist - a.dist;
+    };
+    this.dataPoints.sort(sortDepth);
+
+    if (this.style === Graph3d.STYLE.SURFACE) {
+      for (i = 0; i < this.dataPoints.length; i++) {
+        point = this.dataPoints[i];
+        right = this.dataPoints[i].pointRight;
+        top = this.dataPoints[i].pointTop;
+        cross = this.dataPoints[i].pointCross;
+
+        if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) {
+
+          if (this.showGrayBottom || this.showShadow) {
+            // calculate the cross product of the two vectors from center
+            // to left and right, in order to know whether we are looking at the
+            // bottom or at the top side. We can also use the cross product
+            // for calculating light intensity
+            var aDiff = Point3d.subtract(cross.trans, point.trans);
+            var bDiff = Point3d.subtract(top.trans, right.trans);
+            var crossproduct = Point3d.crossProduct(aDiff, bDiff);
+            var len = crossproduct.length();
+            // FIXME: there is a bug with determining the surface side (shadow or colored)
+
+            topSideVisible = crossproduct.z > 0;
+          } else {
+            topSideVisible = true;
+          }
+
+          if (topSideVisible) {
+            // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+            zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4;
+            h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+            s = 1; // saturation
+
+            if (this.showShadow) {
+              v = Math.min(1 + crossproduct.x / len / 2, 1); // value. TODO: scale
+              fillStyle = this._hsv2rgb(h, s, v);
+              strokeStyle = fillStyle;
+            } else {
+              v = 1;
+              fillStyle = this._hsv2rgb(h, s, v);
+              strokeStyle = this.axisColor; // TODO: should be customizable
+            }
+          } else {
+            fillStyle = 'gray';
+            strokeStyle = this.axisColor;
+          }
+
+          ctx.lineWidth = this._getStrokeWidth(point);
+          ctx.fillStyle = fillStyle;
+          ctx.strokeStyle = strokeStyle;
+          ctx.beginPath();
+          ctx.moveTo(point.screen.x, point.screen.y);
+          ctx.lineTo(right.screen.x, right.screen.y);
+          ctx.lineTo(cross.screen.x, cross.screen.y);
+          ctx.lineTo(top.screen.x, top.screen.y);
+          ctx.closePath();
+          ctx.fill();
+          ctx.stroke(); // TODO: only draw stroke when strokeWidth > 0
+        }
+      }
+    } else {
+      // grid style
+      for (i = 0; i < this.dataPoints.length; i++) {
+        point = this.dataPoints[i];
+        right = this.dataPoints[i].pointRight;
+        top = this.dataPoints[i].pointTop;
+
+        if (point !== undefined && right !== undefined) {
+          // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+          zAvg = (point.point.z + right.point.z) / 2;
+          h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+
+          ctx.lineWidth = this._getStrokeWidth(point) * 2;
+          ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
+          ctx.beginPath();
+          ctx.moveTo(point.screen.x, point.screen.y);
+          ctx.lineTo(right.screen.x, right.screen.y);
+          ctx.stroke();
+        }
+
+        if (point !== undefined && top !== undefined) {
+          // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+          zAvg = (point.point.z + top.point.z) / 2;
+          h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+
+          ctx.lineWidth = this._getStrokeWidth(point) * 2;
+          ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
+          ctx.beginPath();
+          ctx.moveTo(point.screen.x, point.screen.y);
+          ctx.lineTo(top.screen.x, top.screen.y);
+          ctx.stroke();
+        }
+      }
+    }
+  };
+
+  Graph3d.prototype._getStrokeWidth = function (point) {
+    if (point !== undefined) {
+      if (this.showPerspective) {
+        return 1 / -point.trans.z * this.dataColor.strokeWidth;
+      } else {
+        return -(this.eye.z / this.camera.getArmLength()) * this.dataColor.strokeWidth;
+      }
+    }
+
+    return this.dataColor.strokeWidth;
+  };
+
+  /**
+   * Draw all datapoints as dots.
+   * This function can be used when the style is 'dot' or 'dot-line'
+   */
+  Graph3d.prototype._redrawDataDot = function () {
+    var canvas = this.frame.canvas;
+    var ctx = canvas.getContext('2d');
+    var i;
+
+    if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception?
+
+    // calculate the translations of all points
+    for (i = 0; i < this.dataPoints.length; i++) {
+      var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+      var screen = this._convertTranslationToScreen(trans);
+      this.dataPoints[i].trans = trans;
+      this.dataPoints[i].screen = screen;
+
+      // calculate the distance from the point at the bottom to the camera
+      var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
+      this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
+    }
+
+    // order the translated points by depth
+    var sortDepth = function sortDepth(a, b) {
+      return b.dist - a.dist;
+    };
+    this.dataPoints.sort(sortDepth);
+
+    // draw the datapoints as colored circles
+    var dotSize = this.frame.clientWidth * 0.02; // px
+    for (i = 0; i < this.dataPoints.length; i++) {
+      var point = this.dataPoints[i];
+
+      if (this.style === Graph3d.STYLE.DOTLINE) {
+        // draw a vertical line from the bottom to the graph value
+        //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin));
+        var from = this._convert3Dto2D(point.bottom);
+        ctx.lineWidth = 1;
+        ctx.strokeStyle = this.gridColor;
+        ctx.beginPath();
+        ctx.moveTo(from.x, from.y);
+        ctx.lineTo(point.screen.x, point.screen.y);
+        ctx.stroke();
+      }
+
+      // calculate radius for the circle
+      var size;
+      if (this.style === Graph3d.STYLE.DOTSIZE) {
+        size = dotSize / 2 + 2 * dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin);
+      } else {
+        size = dotSize;
+      }
+
+      var radius;
+      if (this.showPerspective) {
+        radius = size / -point.trans.z;
+      } else {
+        radius = size * -(this.eye.z / this.camera.getArmLength());
+      }
+      if (radius < 0) {
+        radius = 0;
+      }
+
+      var hue, color, borderColor;
+      if (this.style === Graph3d.STYLE.DOTCOLOR) {
+        // calculate the color based on the value
+        hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
+        color = this._hsv2rgb(hue, 1, 1);
+        borderColor = this._hsv2rgb(hue, 1, 0.8);
+      } else if (this.style === Graph3d.STYLE.DOTSIZE) {
+        color = this.dataColor.fill;
+        borderColor = this.dataColor.stroke;
+      } else {
+        // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+        hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+        color = this._hsv2rgb(hue, 1, 1);
+        borderColor = this._hsv2rgb(hue, 1, 0.8);
+      }
+
+      // draw the circle
+      ctx.lineWidth = this._getStrokeWidth(point);
+      ctx.strokeStyle = borderColor;
+      ctx.fillStyle = color;
+      ctx.beginPath();
+      ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI * 2, true);
+      ctx.fill();
+      ctx.stroke();
+    }
+  };
+
+  /**
+   * Draw all datapoints as bars.
+   * This function can be used when the style is 'bar', 'bar-color', or 'bar-size'
+   */
+  Graph3d.prototype._redrawDataBar = function () {
+    var canvas = this.frame.canvas;
+    var ctx = canvas.getContext('2d');
+    var i, j, surface, corners;
+
+    if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception?
+
+    // calculate the translations of all points
+    for (i = 0; i < this.dataPoints.length; i++) {
+      var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+      var screen = this._convertTranslationToScreen(trans);
+      this.dataPoints[i].trans = trans;
+      this.dataPoints[i].screen = screen;
+
+      // calculate the distance from the point at the bottom to the camera
+      var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
+      this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
+    }
+
+    // order the translated points by depth
+    var sortDepth = function sortDepth(a, b) {
+      return b.dist - a.dist;
+    };
+    this.dataPoints.sort(sortDepth);
+
+    ctx.lineJoin = 'round';
+    ctx.lineCap = 'round';
+
+    // draw the datapoints as bars
+    var xWidth = this.xBarWidth / 2;
+    var yWidth = this.yBarWidth / 2;
+    for (i = 0; i < this.dataPoints.length; i++) {
+      var point = this.dataPoints[i];
+
+      // determine color
+      var hue, color, borderColor;
+      if (this.style === Graph3d.STYLE.BARCOLOR) {
+        // calculate the color based on the value
+        hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
+        color = this._hsv2rgb(hue, 1, 1);
+        borderColor = this._hsv2rgb(hue, 1, 0.8);
+      } else if (this.style === Graph3d.STYLE.BARSIZE) {
+        color = this.dataColor.fill;
+        borderColor = this.dataColor.stroke;
+      } else {
+        // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+        hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+        color = this._hsv2rgb(hue, 1, 1);
+        borderColor = this._hsv2rgb(hue, 1, 0.8);
+      }
+
+      // calculate size for the bar
+      if (this.style === Graph3d.STYLE.BARSIZE) {
+        xWidth = this.xBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
+        yWidth = this.yBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
+      }
+
+      // calculate all corner points
+      var me = this;
+      var point3d = point.point;
+      var top = [{ point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z) }, { point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z) }, { point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z) }, { point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z) }];
+      var bottom = [{ point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin) }, { point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin) }, { point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin) }, { point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin) }];
+
+      // calculate screen location of the points
+      top.forEach(function (obj) {
+        obj.screen = me._convert3Dto2D(obj.point);
+      });
+      bottom.forEach(function (obj) {
+        obj.screen = me._convert3Dto2D(obj.point);
+      });
+
+      // create five sides, calculate both corner points and center points
+      var surfaces = [{ corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point) }, { corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point) }, { corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point) }, { corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point) }, { corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point) }];
+      point.surfaces = surfaces;
+
+      // calculate the distance of each of the surface centers to the camera
+      for (j = 0; j < surfaces.length; j++) {
+        surface = surfaces[j];
+        var transCenter = this._convertPointToTranslation(surface.center);
+        surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z;
+        // TODO: this dept calculation doesn't work 100% of the cases due to perspective,
+        //     but the current solution is fast/simple and works in 99.9% of all cases
+        //     the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9})
+      }
+
+      // order the surfaces by their (translated) depth
+      surfaces.sort(function (a, b) {
+        var diff = b.dist - a.dist;
+        if (diff) return diff;
+
+        // if equal depth, sort the top surface last
+        if (a.corners === top) return 1;
+        if (b.corners === top) return -1;
+
+        // both are equal
+        return 0;
+      });
+
+      // draw the ordered surfaces
+      ctx.lineWidth = this._getStrokeWidth(point);
+      ctx.strokeStyle = borderColor;
+      ctx.fillStyle = color;
+      // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside
+      for (j = 2; j < surfaces.length; j++) {
+        surface = surfaces[j];
+        corners = surface.corners;
+        ctx.beginPath();
+        ctx.moveTo(corners[3].screen.x, corners[3].screen.y);
+        ctx.lineTo(corners[0].screen.x, corners[0].screen.y);
+        ctx.lineTo(corners[1].screen.x, corners[1].screen.y);
+        ctx.lineTo(corners[2].screen.x, corners[2].screen.y);
+        ctx.lineTo(corners[3].screen.x, corners[3].screen.y);
+        ctx.fill();
+        ctx.stroke();
+      }
+    }
+  };
+
+  /**
+   * Draw a line through all datapoints.
+   * This function can be used when the style is 'line'
+   */
+  Graph3d.prototype._redrawDataLine = function () {
+    var canvas = this.frame.canvas,
+        ctx = canvas.getContext('2d'),
+        point,
+        i;
+
+    if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception?
+
+    // calculate the translations of all points
+    for (i = 0; i < this.dataPoints.length; i++) {
+      var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+      var screen = this._convertTranslationToScreen(trans);
+
+      this.dataPoints[i].trans = trans;
+      this.dataPoints[i].screen = screen;
+    }
+
+    // start the line
+    if (this.dataPoints.length > 0) {
+      point = this.dataPoints[0];
+
+      ctx.lineWidth = this._getStrokeWidth(point);
+      ctx.lineJoin = 'round';
+      ctx.lineCap = 'round';
+      ctx.strokeStyle = this.dataColor.stroke;
+      ctx.beginPath();
+      ctx.moveTo(point.screen.x, point.screen.y);
+
+      // draw the datapoints as colored circles
+      for (i = 1; i < this.dataPoints.length; i++) {
+        point = this.dataPoints[i];
+        ctx.lineTo(point.screen.x, point.screen.y);
+      }
+
+      // finish the line
+      ctx.stroke();
+    }
+  };
+
+  /**
+   * Start a moving operation inside the provided parent element
+   * @param {Event}     event     The event that occurred (required for
+   *                  retrieving the  mouse position)
+   */
+  Graph3d.prototype._onMouseDown = function (event) {
+    event = event || window.event;
+
+    // check if mouse is still down (may be up when focus is lost for example
+    // in an iframe)
+    if (this.leftButtonDown) {
+      this._onMouseUp(event);
+    }
+
+    // only react on left mouse button down
+    this.leftButtonDown = event.which ? event.which === 1 : event.button === 1;
+    if (!this.leftButtonDown && !this.touchDown) return;
+
+    // get mouse position (different code for IE and all other browsers)
+    this.startMouseX = getMouseX(event);
+    this.startMouseY = getMouseY(event);
+
+    this.startStart = new Date(this.start);
+    this.startEnd = new Date(this.end);
+    this.startArmRotation = this.camera.getArmRotation();
+
+    this.frame.style.cursor = 'move';
+
+    // add event listeners to handle moving the contents
+    // we store the function onmousemove and onmouseup in the graph, so we can
+    // remove the eventlisteners lateron in the function mouseUp()
+    var me = this;
+    this.onmousemove = function (event) {
+      me._onMouseMove(event);
+    };
+    this.onmouseup = function (event) {
+      me._onMouseUp(event);
+    };
+    util.addEventListener(document, 'mousemove', me.onmousemove);
+    util.addEventListener(document, 'mouseup', me.onmouseup);
+    util.preventDefault(event);
+  };
+
+  /**
+   * Perform moving operating.
+   * This function activated from within the funcion Graph.mouseDown().
+   * @param {Event}   event  Well, eehh, the event
+   */
+  Graph3d.prototype._onMouseMove = function (event) {
+    event = event || window.event;
+
+    // calculate change in mouse position
+    var diffX = parseFloat(getMouseX(event)) - this.startMouseX;
+    var diffY = parseFloat(getMouseY(event)) - this.startMouseY;
+
+    var horizontalNew = this.startArmRotation.horizontal + diffX / 200;
+    var verticalNew = this.startArmRotation.vertical + diffY / 200;
+
+    var snapAngle = 4; // degrees
+    var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI);
+
+    // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc...
+    // the -0.001 is to take care that the vertical axis is always drawn at the left front corner
+    if (Math.abs(Math.sin(horizontalNew)) < snapValue) {
+      horizontalNew = Math.round(horizontalNew / Math.PI) * Math.PI - 0.001;
+    }
+    if (Math.abs(Math.cos(horizontalNew)) < snapValue) {
+      horizontalNew = (Math.round(horizontalNew / Math.PI - 0.5) + 0.5) * Math.PI - 0.001;
+    }
+
+    // snap vertically to nice angles
+    if (Math.abs(Math.sin(verticalNew)) < snapValue) {
+      verticalNew = Math.round(verticalNew / Math.PI) * Math.PI;
+    }
+    if (Math.abs(Math.cos(verticalNew)) < snapValue) {
+      verticalNew = (Math.round(verticalNew / Math.PI - 0.5) + 0.5) * Math.PI;
+    }
+
+    this.camera.setArmRotation(horizontalNew, verticalNew);
+    this.redraw();
+
+    // fire a cameraPositionChange event
+    var parameters = this.getCameraPosition();
+    this.emit('cameraPositionChange', parameters);
+
+    util.preventDefault(event);
+  };
+
+  /**
+   * Stop moving operating.
+   * This function activated from within the funcion Graph.mouseDown().
+   * @param {event}  event   The event
+   */
+  Graph3d.prototype._onMouseUp = function (event) {
+    this.frame.style.cursor = 'auto';
+    this.leftButtonDown = false;
+
+    // remove event listeners here
+    util.removeEventListener(document, 'mousemove', this.onmousemove);
+    util.removeEventListener(document, 'mouseup', this.onmouseup);
+    util.preventDefault(event);
+  };
+
+  /**
+   * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point
+   * @param {Event}  event   A mouse move event
+   */
+  Graph3d.prototype._onTooltip = function (event) {
+    var delay = 300; // ms
+    var boundingRect = this.frame.getBoundingClientRect();
+    var mouseX = getMouseX(event) - boundingRect.left;
+    var mouseY = getMouseY(event) - boundingRect.top;
+
+    if (!this.showTooltip) {
+      return;
+    }
+
+    if (this.tooltipTimeout) {
+      clearTimeout(this.tooltipTimeout);
+    }
+
+    // (delayed) display of a tooltip only if no mouse button is down
+    if (this.leftButtonDown) {
+      this._hideTooltip();
+      return;
+    }
+
+    if (this.tooltip && this.tooltip.dataPoint) {
+      // tooltip is currently visible
+      var dataPoint = this._dataPointFromXY(mouseX, mouseY);
+      if (dataPoint !== this.tooltip.dataPoint) {
+        // datapoint changed
+        if (dataPoint) {
+          this._showTooltip(dataPoint);
+        } else {
+          this._hideTooltip();
+        }
+      }
+    } else {
+      // tooltip is currently not visible
+      var me = this;
+      this.tooltipTimeout = setTimeout(function () {
+        me.tooltipTimeout = null;
+
+        // show a tooltip if we have a data point
+        var dataPoint = me._dataPointFromXY(mouseX, mouseY);
+        if (dataPoint) {
+          me._showTooltip(dataPoint);
+        }
+      }, delay);
+    }
+  };
+
+  /**
+   * Event handler for touchstart event on mobile devices
+   */
+  Graph3d.prototype._onTouchStart = function (event) {
+    this.touchDown = true;
+
+    var me = this;
+    this.ontouchmove = function (event) {
+      me._onTouchMove(event);
+    };
+    this.ontouchend = function (event) {
+      me._onTouchEnd(event);
+    };
+    util.addEventListener(document, 'touchmove', me.ontouchmove);
+    util.addEventListener(document, 'touchend', me.ontouchend);
+
+    this._onMouseDown(event);
+  };
+
+  /**
+   * Event handler for touchmove event on mobile devices
+   */
+  Graph3d.prototype._onTouchMove = function (event) {
+    this._onMouseMove(event);
+  };
+
+  /**
+   * Event handler for touchend event on mobile devices
+   */
+  Graph3d.prototype._onTouchEnd = function (event) {
+    this.touchDown = false;
+
+    util.removeEventListener(document, 'touchmove', this.ontouchmove);
+    util.removeEventListener(document, 'touchend', this.ontouchend);
+
+    this._onMouseUp(event);
+  };
+
+  /**
+   * Event handler for mouse wheel event, used to zoom the graph
+   * Code from http://adomas.org/javascript-mouse-wheel/
+   * @param {event}  event   The event
+   */
+  Graph3d.prototype._onWheel = function (event) {
+    if (!event) /* For IE. */
+      event = window.event;
+
+    // retrieve delta
+    var delta = 0;
+    if (event.wheelDelta) {
+      /* IE/Opera. */
+      delta = event.wheelDelta / 120;
+    } else if (event.detail) {
+      /* Mozilla case. */
+      // In Mozilla, sign of delta is different than in IE.
+      // Also, delta is multiple of 3.
+      delta = -event.detail / 3;
+    }
+
+    // If delta is nonzero, handle it.
+    // Basically, delta is now positive if wheel was scrolled up,
+    // and negative, if wheel was scrolled down.
+    if (delta) {
+      var oldLength = this.camera.getArmLength();
+      var newLength = oldLength * (1 - delta / 10);
+
+      this.camera.setArmLength(newLength);
+      this.redraw();
+
+      this._hideTooltip();
+    }
+
+    // fire a cameraPositionChange event
+    var parameters = this.getCameraPosition();
+    this.emit('cameraPositionChange', parameters);
+
+    // Prevent default actions caused by mouse wheel.
+    // That might be ugly, but we handle scrolls somehow
+    // anyway, so don't bother here..
+    util.preventDefault(event);
+  };
+
+  /**
+   * Test whether a point lies inside given 2D triangle
+   * @param {Point2d} point
+   * @param {Point2d[]} triangle
+   * @return {boolean} Returns true if given point lies inside or on the edge of the triangle
+   * @private
+   */
+  Graph3d.prototype._insideTriangle = function (point, triangle) {
+    var a = triangle[0],
+        b = triangle[1],
+        c = triangle[2];
+
+    function sign(x) {
+      return x > 0 ? 1 : x < 0 ? -1 : 0;
+    }
+
+    var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x));
+    var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x));
+    var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x));
+
+    // each of the three signs must be either equal to each other or zero
+    return (as == 0 || bs == 0 || as == bs) && (bs == 0 || cs == 0 || bs == cs) && (as == 0 || cs == 0 || as == cs);
+  };
+
+  /**
+   * Find a data point close to given screen position (x, y)
+   * @param {Number} x
+   * @param {Number} y
+   * @return {Object | null} The closest data point or null if not close to any data point
+   * @private
+   */
+  Graph3d.prototype._dataPointFromXY = function (x, y) {
+    var i,
+        distMax = 100,
+        // px
+    dataPoint = null,
+        closestDataPoint = null,
+        closestDist = null,
+        center = new Point2d(x, y);
+
+    if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) {
+      // the data points are ordered from far away to closest
+      for (i = this.dataPoints.length - 1; i >= 0; i--) {
+        dataPoint = this.dataPoints[i];
+        var surfaces = dataPoint.surfaces;
+        if (surfaces) {
+          for (var s = surfaces.length - 1; s >= 0; s--) {
+            // split each surface in two triangles, and see if the center point is inside one of these
+            var surface = surfaces[s];
+            var corners = surface.corners;
+            var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen];
+            var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen];
+            if (this._insideTriangle(center, triangle1) || this._insideTriangle(center, triangle2)) {
+              // return immediately at the first hit
+              return dataPoint;
+            }
+          }
+        }
+      }
+    } else {
+      // find the closest data point, using distance to the center of the point on 2d screen
+      for (i = 0; i < this.dataPoints.length; i++) {
+        dataPoint = this.dataPoints[i];
+        var point = dataPoint.screen;
+        if (point) {
+          var distX = Math.abs(x - point.x);
+          var distY = Math.abs(y - point.y);
+          var dist = Math.sqrt(distX * distX + distY * distY);
+
+          if ((closestDist === null || dist < closestDist) && dist < distMax) {
+            closestDist = dist;
+            closestDataPoint = dataPoint;
+          }
+        }
+      }
+    }
+
+    return closestDataPoint;
+  };
+
+  /**
+   * Display a tooltip for given data point
+   * @param {Object} dataPoint
+   * @private
+   */
+  Graph3d.prototype._showTooltip = function (dataPoint) {
+    var content, line, dot;
+
+    if (!this.tooltip) {
+      content = document.createElement('div');
+      content.style.position = 'absolute';
+      content.style.padding = '10px';
+      content.style.border = '1px solid #4d4d4d';
+      content.style.color = '#1a1a1a';
+      content.style.background = 'rgba(255,255,255,0.7)';
+      content.style.borderRadius = '2px';
+      content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)';
+
+      line = document.createElement('div');
+      line.style.position = 'absolute';
+      line.style.height = '40px';
+      line.style.width = '0';
+      line.style.borderLeft = '1px solid #4d4d4d';
+
+      dot = document.createElement('div');
+      dot.style.position = 'absolute';
+      dot.style.height = '0';
+      dot.style.width = '0';
+      dot.style.border = '5px solid #4d4d4d';
+      dot.style.borderRadius = '5px';
+
+      this.tooltip = {
+        dataPoint: null,
+        dom: {
+          content: content,
+          line: line,
+          dot: dot
+        }
+      };
+    } else {
+      content = this.tooltip.dom.content;
+      line = this.tooltip.dom.line;
+      dot = this.tooltip.dom.dot;
+    }
+
+    this._hideTooltip();
+
+    this.tooltip.dataPoint = dataPoint;
+    if (typeof this.showTooltip === 'function') {
+      content.innerHTML = this.showTooltip(dataPoint.point);
+    } else {
+      content.innerHTML = '<table>' + '<tr><td>x:</td><td>' + dataPoint.point.x + '</td></tr>' + '<tr><td>y:</td><td>' + dataPoint.point.y + '</td></tr>' + '<tr><td>z:</td><td>' + dataPoint.point.z + '</td></tr>' + '</table>';
+    }
+
+    content.style.left = '0';
+    content.style.top = '0';
+    this.frame.appendChild(content);
+    this.frame.appendChild(line);
+    this.frame.appendChild(dot);
+
+    // calculate sizes
+    var contentWidth = content.offsetWidth;
+    var contentHeight = content.offsetHeight;
+    var lineHeight = line.offsetHeight;
+    var dotWidth = dot.offsetWidth;
+    var dotHeight = dot.offsetHeight;
+
+    var left = dataPoint.screen.x - contentWidth / 2;
+    left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth);
+
+    line.style.left = dataPoint.screen.x + 'px';
+    line.style.top = dataPoint.screen.y - lineHeight + 'px';
+    content.style.left = left + 'px';
+    content.style.top = dataPoint.screen.y - lineHeight - contentHeight + 'px';
+    dot.style.left = dataPoint.screen.x - dotWidth / 2 + 'px';
+    dot.style.top = dataPoint.screen.y - dotHeight / 2 + 'px';
+  };
+
+  /**
+   * Hide the tooltip when displayed
+   * @private
+   */
+  Graph3d.prototype._hideTooltip = function () {
+    if (this.tooltip) {
+      this.tooltip.dataPoint = null;
+
+      for (var prop in this.tooltip.dom) {
+        if (this.tooltip.dom.hasOwnProperty(prop)) {
+          var elem = this.tooltip.dom[prop];
+          if (elem && elem.parentNode) {
+            elem.parentNode.removeChild(elem);
+          }
+        }
+      }
+    }
+  };
+
+  /**--------------------------------------------------------------------------**/
+
+  /**
+   * Get the horizontal mouse position from a mouse event
+   * @param {Event} event
+   * @return {Number} mouse x
+   */
+  function getMouseX(event) {
+    if ('clientX' in event) return event.clientX;
+    return event.targetTouches[0] && event.targetTouches[0].clientX || 0;
+  }
+
+  /**
+   * Get the vertical mouse position from a mouse event
+   * @param {Event} event
+   * @return {Number} mouse y
+   */
+  function getMouseY(event) {
+    if ('clientY' in event) return event.clientY;
+    return event.targetTouches[0] && event.targetTouches[0].clientY || 0;
+  }
+
+  module.exports = Graph3d;
+
+  // use use defaults
+
+/***/ },
+/* 12 */
+/***/ function(module, exports) {
+
+  
+  /**
+   * Expose `Emitter`.
+   */
+
+  module.exports = Emitter;
+
+  /**
+   * Initialize a new `Emitter`.
+   *
+   * @api public
+   */
+
+  function Emitter(obj) {
+    if (obj) return mixin(obj);
+  };
+
+  /**
+   * Mixin the emitter properties.
+   *
+   * @param {Object} obj
+   * @return {Object}
+   * @api private
+   */
+
+  function mixin(obj) {
+    for (var key in Emitter.prototype) {
+      obj[key] = Emitter.prototype[key];
+    }
+    return obj;
+  }
+
+  /**
+   * Listen on the given `event` with `fn`.
+   *
+   * @param {String} event
+   * @param {Function} fn
+   * @return {Emitter}
+   * @api public
+   */
+
+  Emitter.prototype.on =
+  Emitter.prototype.addEventListener = function(event, fn){
+    this._callbacks = this._callbacks || {};
+    (this._callbacks[event] = this._callbacks[event] || [])
+      .push(fn);
+    return this;
+  };
+
+  /**
+   * Adds an `event` listener that will be invoked a single
+   * time then automatically removed.
+   *
+   * @param {String} event
+   * @param {Function} fn
+   * @return {Emitter}
+   * @api public
+   */
+
+  Emitter.prototype.once = function(event, fn){
+    var self = this;
+    this._callbacks = this._callbacks || {};
+
+    function on() {
+      self.off(event, on);
+      fn.apply(this, arguments);
+    }
+
+    on.fn = fn;
+    this.on(event, on);
+    return this;
+  };
+
+  /**
+   * Remove the given callback for `event` or all
+   * registered callbacks.
+   *
+   * @param {String} event
+   * @param {Function} fn
+   * @return {Emitter}
+   * @api public
+   */
+
+  Emitter.prototype.off =
+  Emitter.prototype.removeListener =
+  Emitter.prototype.removeAllListeners =
+  Emitter.prototype.removeEventListener = function(event, fn){
+    this._callbacks = this._callbacks || {};
+
+    // all
+    if (0 == arguments.length) {
+      this._callbacks = {};
+      return this;
+    }
+
+    // specific event
+    var callbacks = this._callbacks[event];
+    if (!callbacks) return this;
+
+    // remove all handlers
+    if (1 == arguments.length) {
+      delete this._callbacks[event];
+      return this;
+    }
+
+    // remove specific handler
+    var cb;
+    for (var i = 0; i < callbacks.length; i++) {
+      cb = callbacks[i];
+      if (cb === fn || cb.fn === fn) {
+        callbacks.splice(i, 1);
+        break;
+      }
+    }
+    return this;
+  };
+
+  /**
+   * Emit `event` with the given args.
+   *
+   * @param {String} event
+   * @param {Mixed} ...
+   * @return {Emitter}
+   */
+
+  Emitter.prototype.emit = function(event){
+    this._callbacks = this._callbacks || {};
+    var args = [].slice.call(arguments, 1)
+      , callbacks = this._callbacks[event];
+
+    if (callbacks) {
+      callbacks = callbacks.slice(0);
+      for (var i = 0, len = callbacks.length; i < len; ++i) {
+        callbacks[i].apply(this, args);
+      }
+    }
+
+    return this;
+  };
+
+  /**
+   * Return array of callbacks for `event`.
+   *
+   * @param {String} event
+   * @return {Array}
+   * @api public
+   */
+
+  Emitter.prototype.listeners = function(event){
+    this._callbacks = this._callbacks || {};
+    return this._callbacks[event] || [];
+  };
+
+  /**
+   * Check if this emitter has `event` handlers.
+   *
+   * @param {String} event
+   * @return {Boolean}
+   * @api public
+   */
+
+  Emitter.prototype.hasListeners = function(event){
+    return !! this.listeners(event).length;
+  };
+
+
+/***/ },
+/* 13 */
+/***/ function(module, exports) {
+
+  /**
+   * @prototype Point3d
+   * @param {Number} [x]
+   * @param {Number} [y]
+   * @param {Number} [z]
+   */
+  "use strict";
+
+  function Point3d(x, y, z) {
+    this.x = x !== undefined ? x : 0;
+    this.y = y !== undefined ? y : 0;
+    this.z = z !== undefined ? z : 0;
+  };
+
+  /**
+   * Subtract the two provided points, returns a-b
+   * @param {Point3d} a
+   * @param {Point3d} b
+   * @return {Point3d} a-b
+   */
+  Point3d.subtract = function (a, b) {
+    var sub = new Point3d();
+    sub.x = a.x - b.x;
+    sub.y = a.y - b.y;
+    sub.z = a.z - b.z;
+    return sub;
+  };
+
+  /**
+   * Add the two provided points, returns a+b
+   * @param {Point3d} a
+   * @param {Point3d} b
+   * @return {Point3d} a+b
+   */
+  Point3d.add = function (a, b) {
+    var sum = new Point3d();
+    sum.x = a.x + b.x;
+    sum.y = a.y + b.y;
+    sum.z = a.z + b.z;
+    return sum;
+  };
+
+  /**
+   * Calculate the average of two 3d points
+   * @param {Point3d} a
+   * @param {Point3d} b
+   * @return {Point3d} The average, (a+b)/2
+   */
+  Point3d.avg = function (a, b) {
+    return new Point3d((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2);
+  };
+
+  /**
+   * Calculate the cross product of the two provided points, returns axb
+   * Documentation: http://en.wikipedia.org/wiki/Cross_product
+   * @param {Point3d} a
+   * @param {Point3d} b
+   * @return {Point3d} cross product axb
+   */
+  Point3d.crossProduct = function (a, b) {
+    var crossproduct = new Point3d();
+
+    crossproduct.x = a.y * b.z - a.z * b.y;
+    crossproduct.y = a.z * b.x - a.x * b.z;
+    crossproduct.z = a.x * b.y - a.y * b.x;
+
+    return crossproduct;
+  };
+
+  /**
+   * Rtrieve the length of the vector (or the distance from this point to the origin
+   * @return {Number}  length
+   */
+  Point3d.prototype.length = function () {
+    return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
+  };
+
+  module.exports = Point3d;
+
+/***/ },
+/* 14 */
+/***/ function(module, exports) {
+
+  /**
+   * @prototype Point2d
+   * @param {Number} [x]
+   * @param {Number} [y]
+   */
+  "use strict";
+
+  function Point2d(x, y) {
+    this.x = x !== undefined ? x : 0;
+    this.y = y !== undefined ? y : 0;
+  }
+
+  module.exports = Point2d;
+
+/***/ },
+/* 15 */
+/***/ function(module, exports, __webpack_require__) {
+
+  'use strict';
+
+  var Point3d = __webpack_require__(13);
+
+  /**
+   * @class Camera
+   * The camera is mounted on a (virtual) camera arm. The camera arm can rotate
+   * The camera is always looking in the direction of the origin of the arm.
+   * This way, the camera always rotates around one fixed point, the location
+   * of the camera arm.
+   *
+   * Documentation:
+   *   http://en.wikipedia.org/wiki/3D_projection
+   */
+  function Camera() {
+    this.armLocation = new Point3d();
+    this.armRotation = {};
+    this.armRotation.horizontal = 0;
+    this.armRotation.vertical = 0;
+    this.armLength = 1.7;
+
+    this.cameraLocation = new Point3d();
+    this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0);
+
+    this.calculateCameraOrientation();
+  }
+
+  /**
+   * Set the location (origin) of the arm
+   * @param {Number} x  Normalized value of x
+   * @param {Number} y  Normalized value of y
+   * @param {Number} z  Normalized value of z
+   */
+  Camera.prototype.setArmLocation = function (x, y, z) {
+    this.armLocation.x = x;
+    this.armLocation.y = y;
+    this.armLocation.z = z;
+
+    this.calculateCameraOrientation();
+  };
+
+  /**
+   * Set the rotation of the camera arm
+   * @param {Number} horizontal   The horizontal rotation, between 0 and 2*PI.
+   *                Optional, can be left undefined.
+   * @param {Number} vertical   The vertical rotation, between 0 and 0.5*PI
+   *                if vertical=0.5*PI, the graph is shown from the
+   *                top. Optional, can be left undefined.
+   */
+  Camera.prototype.setArmRotation = function (horizontal, vertical) {
+    if (horizontal !== undefined) {
+      this.armRotation.horizontal = horizontal;
+    }
+
+    if (vertical !== undefined) {
+      this.armRotation.vertical = vertical;
+      if (this.armRotation.vertical < 0) this.armRotation.vertical = 0;
+      if (this.armRotation.vertical > 0.5 * Math.PI) this.armRotation.vertical = 0.5 * Math.PI;
+    }
+
+    if (horizontal !== undefined || vertical !== undefined) {
+      this.calculateCameraOrientation();
+    }
+  };
+
+  /**
+   * Retrieve the current arm rotation
+   * @return {object}   An object with parameters horizontal and vertical
+   */
+  Camera.prototype.getArmRotation = function () {
+    var rot = {};
+    rot.horizontal = this.armRotation.horizontal;
+    rot.vertical = this.armRotation.vertical;
+
+    return rot;
+  };
+
+  /**
+   * Set the (normalized) length of the camera arm.
+   * @param {Number} length A length between 0.71 and 5.0
+   */
+  Camera.prototype.setArmLength = function (length) {
+    if (length === undefined) return;
+
+    this.armLength = length;
+
+    // Radius must be larger than the corner of the graph,
+    // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the
+    // graph
+    if (this.armLength < 0.71) this.armLength = 0.71;
+    if (this.armLength > 5.0) this.armLength = 5.0;
+
+    this.calculateCameraOrientation();
+  };
+
+  /**
+   * Retrieve the arm length
+   * @return {Number} length
+   */
+  Camera.prototype.getArmLength = function () {
+    return this.armLength;
+  };
+
+  /**
+   * Retrieve the camera location
+   * @return {Point3d} cameraLocation
+   */
+  Camera.prototype.getCameraLocation = function () {
+    return this.cameraLocation;
+  };
+
+  /**
+   * Retrieve the camera rotation
+   * @return {Point3d} cameraRotation
+   */
+  Camera.prototype.getCameraRotation = function () {
+    return this.cameraRotation;
+  };
+
+  /**
+   * Calculate the location and rotation of the camera based on the
+   * position and orientation of the camera arm
+   */
+  Camera.prototype.calculateCameraOrientation = function () {
+    // calculate location of the camera
+    this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
+    this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
+    this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical);
+
+    // calculate rotation of the camera
+    this.cameraRotation.x = Math.PI / 2 - this.armRotation.vertical;
+    this.cameraRotation.y = 0;
+    this.cameraRotation.z = -this.armRotation.horizontal;
+  };
+
+  module.exports = Camera;
+
+/***/ },
+/* 16 */
+/***/ function(module, exports, __webpack_require__) {
+
+  'use strict';
+
+  var DataView = __webpack_require__(10);
+
+  /**
+   * @class Filter
+   *
+   * @param {DataSet} data The google data table
+   * @param {Number}  column             The index of the column to be filtered
+   * @param {Graph} graph           The graph
+   */
+  function Filter(data, column, graph) {
+    this.data = data;
+    this.column = column;
+    this.graph = graph; // the parent graph
+
+    this.index = undefined;
+    this.value = undefined;
+
+    // read all distinct values and select the first one
+    this.values = graph.getDistinctValues(data.get(), this.column);
+
+    // sort both numeric and string values correctly
+    this.values.sort(function (a, b) {
+      return a > b ? 1 : a < b ? -1 : 0;
+    });
+
+    if (this.values.length > 0) {
+      this.selectValue(0);
+    }
+
+    // create an array with the filtered datapoints. this will be loaded afterwards
+    this.dataPoints = [];
+
+    this.loaded = false;
+    this.onLoadCallback = undefined;
+
+    if (graph.animationPreload) {
+      this.loaded = false;
+      this.loadInBackground();
+    } else {
+      this.loaded = true;
+    }
+  };
+
+  /**
+   * Return the label
+   * @return {string} label
+   */
+  Filter.prototype.isLoaded = function () {
+    return this.loaded;
+  };
+
+  /**
+   * Return the loaded progress
+   * @return {Number} percentage between 0 and 100
+   */
+  Filter.prototype.getLoadedProgress = function () {
+    var len = this.values.length;
+
+    var i = 0;
+    while (this.dataPoints[i]) {
+      i++;
+    }
+
+    return Math.round(i / len * 100);
+  };
+
+  /**
+   * Return the label
+   * @return {string} label
+   */
+  Filter.prototype.getLabel = function () {
+    return this.graph.filterLabel;
+  };
+
+  /**
+   * Return the columnIndex of the filter
+   * @return {Number} columnIndex
+   */
+  Filter.prototype.getColumn = function () {
+    return this.column;
+  };
+
+  /**
+   * Return the currently selected value. Returns undefined if there is no selection
+   * @return {*} value
+   */
+  Filter.prototype.getSelectedValue = function () {
+    if (this.index === undefined) return undefined;
+
+    return this.values[this.index];
+  };
+
+  /**
+   * Retrieve all values of the filter
+   * @return {Array} values
+   */
+  Filter.prototype.getValues = function () {
+    return this.values;
+  };
+
+  /**
+   * Retrieve one value of the filter
+   * @param {Number}  index
+   * @return {*} value
+   */
+  Filter.prototype.getValue = function (index) {
+    if (index >= this.values.length) throw 'Error: index out of range';
+
+    return this.values[index];
+  };
+
+  /**
+   * Retrieve the (filtered) dataPoints for the currently selected filter index
+   * @param {Number} [index] (optional)
+   * @return {Array} dataPoints
+   */
+  Filter.prototype._getDataPoints = function (index) {
+    if (index === undefined) index = this.index;
+
+    if (index === undefined) return [];
+
+    var dataPoints;
+    if (this.dataPoints[index]) {
+      dataPoints = this.dataPoints[index];
+    } else {
+      var f = {};
+      f.column = this.column;
+      f.value = this.values[index];
+
+      var dataView = new DataView(this.data, { filter: function filter(item) {
+          return item[f.column] == f.value;
+        } }).get();
+      dataPoints = this.graph._getDataPoints(dataView);
+
+      this.dataPoints[index] = dataPoints;
+    }
+
+    return dataPoints;
+  };
+
+  /**
+   * Set a callback function when the filter is fully loaded.
+   */
+  Filter.prototype.setOnLoadCallback = function (callback) {
+    this.onLoadCallback = callback;
+  };
+
+  /**
+   * Add a value to the list with available values for this filter
+   * No double entries will be created.
+   * @param {Number} index
+   */
+  Filter.prototype.selectValue = function (index) {
+    if (index >= this.values.length) throw 'Error: index out of range';
+
+    this.index = index;
+    this.value = this.values[index];
+  };
+
+  /**
+   * Load all filtered rows in the background one by one
+   * Start this method without providing an index!
+   */
+  Filter.prototype.loadInBackground = function (index) {
+    if (index === undefined) index = 0;
+
+    var frame = this.graph.frame;
+
+    if (index < this.values.length) {
+      var dataPointsTemp = this._getDataPoints(index);
+      //this.graph.redrawInfo(); // TODO: not neat
+
+      // create a progress box
+      if (frame.progress === undefined) {
+        frame.progress = document.createElement('DIV');
+        frame.progress.style.position = 'absolute';
+        frame.progress.style.color = 'gray';
+        frame.appendChild(frame.progress);
+      }
+      var progress = this.getLoadedProgress();
+      frame.progress.innerHTML = 'Loading animation... ' + progress + '%';
+      // TODO: this is no nice solution...
+      frame.progress.style.bottom = 60 + 'px'; // TODO: use height of slider
+      frame.progress.style.left = 10 + 'px';
+
+      var me = this;
+      setTimeout(function () {
+        me.loadInBackground(index + 1);
+      }, 10);
+      this.loaded = false;
+    } else {
+      this.loaded = true;
+
+      // remove the progress box
+      if (frame.progress !== undefined) {
+        frame.removeChild(frame.progress);
+        frame.progress = undefined;
+      }
+
+      if (this.onLoadCallback) this.onLoadCallback();
+    }
+  };
+
+  module.exports = Filter;
+
+/***/ },
+/* 17 */
+/***/ function(module, exports, __webpack_require__) {
+
+  'use strict';
+
+  var util = __webpack_require__(1);
+
+  /**
+   * @constructor Slider
+   *
+   * An html slider control with start/stop/prev/next buttons
+   * @param {Element} container  The element where the slider will be created
+   * @param {Object} options   Available options:
+   *                 {boolean} visible   If true (default) the
+   *                           slider is visible.
+   */
+  function Slider(container, options) {
+    if (container === undefined) {
+      throw 'Error: No container element defined';
+    }
+    this.container = container;
+    this.visible = options && options.visible != undefined ? options.visible : true;
+
+    if (this.visible) {
+      this.frame = document.createElement('DIV');
+      //this.frame.style.backgroundColor = '#E5E5E5';
+      this.frame.style.width = '100%';
+      this.frame.style.position = 'relative';
+      this.container.appendChild(this.frame);
+
+      this.frame.prev = document.createElement('INPUT');
+      this.frame.prev.type = 'BUTTON';
+      this.frame.prev.value = 'Prev';
+      this.frame.appendChild(this.frame.prev);
+
+      this.frame.play = document.createElement('INPUT');
+      this.frame.play.type = 'BUTTON';
+      this.frame.play.value = 'Play';
+      this.frame.appendChild(this.frame.play);
+
+      this.frame.next = document.createElement('INPUT');
+      this.frame.next.type = 'BUTTON';
+      this.frame.next.value = 'Next';
+      this.frame.appendChild(this.frame.next);
+
+      this.frame.bar = document.createElement('INPUT');
+      this.frame.bar.type = 'BUTTON';
+      this.frame.bar.style.position = 'absolute';
+      this.frame.bar.style.border = '1px solid red';
+      this.frame.bar.style.width = '100px';
+      this.frame.bar.style.height = '6px';
+      this.frame.bar.style.borderRadius = '2px';
+      this.frame.bar.style.MozBorderRadius = '2px';
+      this.frame.bar.style.border = '1px solid #7F7F7F';
+      this.frame.bar.style.backgroundColor = '#E5E5E5';
+      this.frame.appendChild(this.frame.bar);
+
+      this.frame.slide = document.createElement('INPUT');
+      this.frame.slide.type = 'BUTTON';
+      this.frame.slide.style.margin = '0px';
+      this.frame.slide.value = ' ';
+      this.frame.slide.style.position = 'relative';
+      this.frame.slide.style.left = '-100px';
+      this.frame.appendChild(this.frame.slide);
+
+      // create events
+      var me = this;
+      this.frame.slide.onmousedown = function (event) {
+        me._onMouseDown(event);
+      };
+      this.frame.prev.onclick = function (event) {
+        me.prev(event);
+      };
+      this.frame.play.onclick = function (event) {
+        me.togglePlay(event);
+      };
+      this.frame.next.onclick = function (event) {
+        me.next(event);
+      };
+    }
+
+    this.onChangeCallback = undefined;
+
+    this.values = [];
+    this.index = undefined;
+
+    this.playTimeout = undefined;
+    this.playInterval = 1000; // milliseconds
+    this.playLoop = true;
+  }
+
+  /**
+   * Select the previous index
+   */
+  Slider.prototype.prev = function () {
+    var index = this.getIndex();
+    if (index > 0) {
+      index--;
+      this.setIndex(index);
+    }
+  };
+
+  /**
+   * Select the next index
+   */
+  Slider.prototype.next = function () {
+    var index = this.getIndex();
+    if (index < this.values.length - 1) {
+      index++;
+      this.setIndex(index);
+    }
+  };
+
+  /**
+   * Select the next index
+   */
+  Slider.prototype.playNext = function () {
+    var start = new Date();
+
+    var index = this.getIndex();
+    if (index < this.values.length - 1) {
+      index++;
+      this.setIndex(index);
+    } else if (this.playLoop) {
+      // jump to the start
+      index = 0;
+      this.setIndex(index);
+    }
+
+    var end = new Date();
+    var diff = end - start;
+
+    // calculate how much time it to to set the index and to execute the callback
+    // function.
+    var interval = Math.max(this.playInterval - diff, 0);
+    // document.title = diff // TODO: cleanup
+
+    var me = this;
+    this.playTimeout = setTimeout(function () {
+      me.playNext();
+    }, interval);
+  };
+
+  /**
+   * Toggle start or stop playing
+   */
+  Slider.prototype.togglePlay = function () {
+    if (this.playTimeout === undefined) {
+      this.play();
+    } else {
+      this.stop();
+    }
+  };
+
+  /**
+   * Start playing
+   */
+  Slider.prototype.play = function () {
+    // Test whether already playing
+    if (this.playTimeout) return;
+
+    this.playNext();
+
+    if (this.frame) {
+      this.frame.play.value = 'Stop';
+    }
+  };
+
+  /**
+   * Stop playing
+   */
+  Slider.prototype.stop = function () {
+    clearInterval(this.playTimeout);
+    this.playTimeout = undefined;
+
+    if (this.frame) {
+      this.frame.play.value = 'Play';
+    }
+  };
+
+  /**
+   * Set a callback function which will be triggered when the value of the
+   * slider bar has changed.
+   */
+  Slider.prototype.setOnChangeCallback = function (callback) {
+    this.onChangeCallback = callback;
+  };
+
+  /**
+   * Set the interval for playing the list
+   * @param {Number} interval   The interval in milliseconds
+   */
+  Slider.prototype.setPlayInterval = function (interval) {
+    this.playInterval = interval;
+  };
+
+  /**
+   * Retrieve the current play interval
+   * @return {Number} interval   The interval in milliseconds
+   */
+  Slider.prototype.getPlayInterval = function (interval) {
+    return this.playInterval;
+  };
+
+  /**
+   * Set looping on or off
+   * @pararm {boolean} doLoop  If true, the slider will jump to the start when
+   *               the end is passed, and will jump to the end
+   *               when the start is passed.
+   */
+  Slider.prototype.setPlayLoop = function (doLoop) {
+    this.playLoop = doLoop;
+  };
+
+  /**
+   * Execute the onchange callback function
+   */
+  Slider.prototype.onChange = function () {
+    if (this.onChangeCallback !== undefined) {
+      this.onChangeCallback();
+    }
+  };
+
+  /**
+   * redraw the slider on the correct place
+   */
+  Slider.prototype.redraw = function () {
+    if (this.frame) {
+      // resize the bar
+      this.frame.bar.style.top = this.frame.clientHeight / 2 - this.frame.bar.offsetHeight / 2 + 'px';
+      this.frame.bar.style.width = this.frame.clientWidth - this.frame.prev.clientWidth - this.frame.play.clientWidth - this.frame.next.clientWidth - 30 + 'px';
+
+      // position the slider button
+      var left = this.indexToLeft(this.index);
+      this.frame.slide.style.left = left + 'px';
+    }
+  };
+
+  /**
+   * Set the list with values for the slider
+   * @param {Array} values   A javascript array with values (any type)
+   */
+  Slider.prototype.setValues = function (values) {
+    this.values = values;
+
+    if (this.values.length > 0) this.setIndex(0);else this.index = undefined;
+  };
+
+  /**
+   * Select a value by its index
+   * @param {Number} index
+   */
+  Slider.prototype.setIndex = function (index) {
+    if (index < this.values.length) {
+      this.index = index;
+
+      this.redraw();
+      this.onChange();
+    } else {
+      throw 'Error: index out of range';
+    }
+  };
+
+  /**
+   * retrieve the index of the currently selected vaue
+   * @return {Number} index
+   */
+  Slider.prototype.getIndex = function () {
+    return this.index;
+  };
+
+  /**
+   * retrieve the currently selected value
+   * @return {*} value
+   */
+  Slider.prototype.get = function () {
+    return this.values[this.index];
+  };
+
+  Slider.prototype._onMouseDown = function (event) {
+    // only react on left mouse button down
+    var leftButtonDown = event.which ? event.which === 1 : event.button === 1;
+    if (!leftButtonDown) return;
+
+    this.startClientX = event.clientX;
+    this.startSlideX = parseFloat(this.frame.slide.style.left);
+
+    this.frame.style.cursor = 'move';
+
+    // add event listeners to handle moving the contents
+    // we store the function onmousemove and onmouseup in the graph, so we can
+    // remove the eventlisteners lateron in the function mouseUp()
+    var me = this;
+    this.onmousemove = function (event) {
+      me._onMouseMove(event);
+    };
+    this.onmouseup = function (event) {
+      me._onMouseUp(event);
+    };
+    util.addEventListener(document, 'mousemove', this.onmousemove);
+    util.addEventListener(document, 'mouseup', this.onmouseup);
+    util.preventDefault(event);
+  };
+
+  Slider.prototype.leftToIndex = function (left) {
+    var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10;
+    var x = left - 3;
+
+    var index = Math.round(x / width * (this.values.length - 1));
+    if (index < 0) index = 0;
+    if (index > this.values.length - 1) index = this.values.length - 1;
+
+    return index;
+  };
+
+  Slider.prototype.indexToLeft = function (index) {
+    var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10;
+
+    var x = index / (this.values.length - 1) * width;
+    var left = x + 3;
+
+    return left;
+  };
+
+  Slider.prototype._onMouseMove = function (event) {
+    var diff = event.clientX - this.startClientX;
+    var x = this.startSlideX + diff;
+
+    var index = this.leftToIndex(x);
+
+    this.setIndex(index);
+
+    util.preventDefault();
+  };
+
+  Slider.prototype._onMouseUp = function (event) {
+    this.frame.style.cursor = 'auto';
+
+    // remove event listeners
+    util.removeEventListener(document, 'mousemove', this.onmousemove);
+    util.removeEventListener(document, 'mouseup', this.onmouseup);
+
+    util.preventDefault();
+  };
+
+  module.exports = Slider;
+
+/***/ },
+/* 18 */
+/***/ function(module, exports) {
+
+  /**
+   * @prototype StepNumber
+   * The class StepNumber is an iterator for Numbers. You provide a start and end
+   * value, and a best step size. StepNumber itself rounds to fixed values and
+   * a finds the step that best fits the provided step.
+   *
+   * If prettyStep is true, the step size is chosen as close as possible to the
+   * provided step, but being a round value like 1, 2, 5, 10, 20, 50, ....
+   *
+   * Example usage:
+   *   var step = new StepNumber(0, 10, 2.5, true);
+   *   step.start();
+   *   while (!step.end()) {
+   *   alert(step.getCurrent());
+   *   step.next();
+   *   }
+   *
+   * Version: 1.0
+   *
+   * @param {Number} start     The start value
+   * @param {Number} end     The end value
+   * @param {Number} step    Optional. Step size. Must be a positive value.
+   * @param {boolean} prettyStep Optional. If true, the step size is rounded
+   *               To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
+   */
+  "use strict";
+
+  function StepNumber(start, end, step, prettyStep) {
+    // set default values
+    this._start = 0;
+    this._end = 0;
+    this._step = 1;
+    this.prettyStep = true;
+    this.precision = 5;
+
+    this._current = 0;
+    this.setRange(start, end, step, prettyStep);
+  };
+
+  /**
+   * Set a new range: start, end and step.
+   *
+   * @param {Number} start     The start value
+   * @param {Number} end     The end value
+   * @param {Number} step    Optional. Step size. Must be a positive value.
+   * @param {boolean} prettyStep Optional. If true, the step size is rounded
+   *               To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
+   */
+  StepNumber.prototype.setRange = function (start, end, step, prettyStep) {
+    this._start = start ? start : 0;
+    this._end = end ? end : 0;
+
+    this.setStep(step, prettyStep);
+  };
+
+  /**
+   * Set a new step size
+   * @param {Number} step    New step size. Must be a positive value
+   * @param {boolean} prettyStep Optional. If true, the provided step is rounded
+   *               to a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
+   */
+  StepNumber.prototype.setStep = function (step, prettyStep) {
+    if (step === undefined || step <= 0) return;
+
+    if (prettyStep !== undefined) this.prettyStep = prettyStep;
+
+    if (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step;
+  };
+
+  /**
+   * Calculate a nice step size, closest to the desired step size.
+   * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an
+   * integer Number. For example 1, 2, 5, 10, 20, 50, etc...
+   * @param {Number}  step  Desired step size
+   * @return {Number}     Nice step size
+   */
+  StepNumber.calculatePrettyStep = function (step) {
+    var log10 = function log10(x) {
+      return Math.log(x) / Math.LN10;
+    };
+
+    // try three steps (multiple of 1, 2, or 5
+    var step1 = Math.pow(10, Math.round(log10(step))),
+        step2 = 2 * Math.pow(10, Math.round(log10(step / 2))),
+        step5 = 5 * Math.pow(10, Math.round(log10(step / 5)));
+
+    // choose the best step (closest to minimum step)
+    var prettyStep = step1;
+    if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2;
+    if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5;
+
+    // for safety
+    if (prettyStep <= 0) {
+      prettyStep = 1;
+    }
+
+    return prettyStep;
+  };
+
+  /**
+   * returns the current value of the step
+   * @return {Number} current value
+   */
+  StepNumber.prototype.getCurrent = function () {
+    return parseFloat(this._current.toPrecision(this.precision));
+  };
+
+  /**
+   * returns the current step size
+   * @return {Number} current step size
+   */
+  StepNumber.prototype.getStep = function () {
+    return this._step;
+  };
+
+  /**
+   * Set the current value to the largest value smaller than start, which
+   * is a multiple of the step size
+   */
+  StepNumber.prototype.start = function () {
+    this._current = this._start - this._start % this._step;
+  };
+
+  /**
+   * Do a step, add the step size to the current value
+   */
+  StepNumber.prototype.next = function () {
+    this._current += this._step;
+  };
+
+  /**
+   * Returns true whether the end is reached
+   * @return {boolean}  True if the current value has passed the end value.
+   */
+  StepNumber.prototype.end = function () {
+    return this._current > this._end;
+  };
+
+  module.exports = StepNumber;
+
+/***/ },
+/* 19 */
+/***/ function(module, exports, __webpack_require__) {
+
+  'use strict';
+
+  var Emitter = __webpack_require__(12);
+  var Hammer = __webpack_require__(20);
+  var moment = __webpack_require__(2);
+  var util = __webpack_require__(1);
+  var DataSet = __webpack_require__(8);
+  var DataView = __webpack_require__(10);
+  var Range = __webpack_require__(24);
+  var Core = __webpack_require__(28);
+  var TimeAxis = __webpack_require__(39);
+  var CurrentTime = __webpack_require__(44);
+  var CustomTime = __webpack_require__(42);
+  var ItemSet = __webpack_require__(29);
+
+  var Configurator = __webpack_require__(45);
+  var Validator = __webpack_require__(47)['default'];
+  var printStyle = __webpack_require__(47).printStyle;
+  var allOptions = __webpack_require__(48).allOptions;
+  var configureOptions = __webpack_require__(48).configureOptions;
+
+  /**
+   * Create a timeline visualization
+   * @param {HTMLElement} container
+   * @param {vis.DataSet | vis.DataView | Array} [items]
+   * @param {vis.DataSet | vis.DataView | Array} [groups]
+   * @param {Object} [options]  See Timeline.setOptions for the available options.
+   * @constructor
+   * @extends Core
+   */
+  function Timeline(container, items, groups, options) {
+    if (!(this instanceof Timeline)) {
+      throw new SyntaxError('Constructor must be called with the new operator');
+    }
+
+    // if the third element is options, the forth is groups (optionally);
+    if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) {
+      var forthArgument = options;
+      options = groups;
+      groups = forthArgument;
+    }
+
+    var me = this;
+    this.defaultOptions = {
+      start: null,
+      end: null,
+
+      autoResize: true,
+      throttleRedraw: 0, // ms
+
+      orientation: {
+        axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both'
+        item: 'bottom' // not relevant
+      },
+
+      moment: moment,
+
+      width: null,
+      height: null,
+      maxHeight: null,
+      minHeight: null
+    };
+    this.options = util.deepExtend({}, this.defaultOptions);
+
+    // Create the DOM, props, and emitter
+    this._create(container);
+
+    // all components listed here will be repainted automatically
+    this.components = [];
+
+    this.body = {
+      dom: this.dom,
+      domProps: this.props,
+      emitter: {
+        on: this.on.bind(this),
+        off: this.off.bind(this),
+        emit: this.emit.bind(this)
+      },
+      hiddenDates: [],
+      util: {
+        getScale: function getScale() {
+          return me.timeAxis.step.scale;
+        },
+        getStep: function getStep() {
+          return me.timeAxis.step.step;
+        },
+
+        toScreen: me._toScreen.bind(me),
+        toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
+        toTime: me._toTime.bind(me),
+        toGlobalTime: me._toGlobalTime.bind(me)
+      }
+    };
+
+    // range
+    this.range = new Range(this.body);
+    this.components.push(this.range);
+    this.body.range = this.range;
+
+    // time axis
+    this.timeAxis = new TimeAxis(this.body);
+    this.timeAxis2 = null; // used in case of orientation option 'both'
+    this.components.push(this.timeAxis);
+
+    // current time bar
+    this.currentTime = new CurrentTime(this.body);
+    this.components.push(this.currentTime);
+
+    // item set
+    this.itemSet = new ItemSet(this.body);
+    this.components.push(this.itemSet);
+
+    this.itemsData = null; // DataSet
+    this.groupsData = null; // DataSet
+
+    this.on('tap', function (event) {
+      me.emit('click', me.getEventProperties(event));
+    });
+    this.on('doubletap', function (event) {
+      me.emit('doubleClick', me.getEventProperties(event));
+    });
+    this.dom.root.oncontextmenu = function (event) {
+      me.emit('contextmenu', me.getEventProperties(event));
+    };
+
+    // apply options
+    if (options) {
+      this.setOptions(options);
+    }
+
+    // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
+    if (groups) {
+      this.setGroups(groups);
+    }
+
+    // create itemset
+    if (items) {
+      this.setItems(items);
+    } else {
+      this._redraw();
+    }
+  }
+
+  // Extend the functionality from Core
+  Timeline.prototype = new Core();
+
+  /**
+   * Load a configurator
+   * @return {Object}
+   * @private
+   */
+  Timeline.prototype._createConfigurator = function () {
+    return new Configurator(this, this.dom.container, configureOptions);
+  };
+
+  /**
+   * Force a redraw. The size of all items will be recalculated.
+   * Can be useful to manually redraw when option autoResize=false and the window
+   * has been resized, or when the items CSS has been changed.
+   *
+   * Note: this function will be overridden on construction with a trottled version
+   */
+  Timeline.prototype.redraw = function () {
+    this.itemSet && this.itemSet.markDirty({ refreshItems: true });
+    this._redraw();
+  };
+
+  Timeline.prototype.setOptions = function (options) {
+    // validate options
+    var errorFound = Validator.validate(options, allOptions);
+    if (errorFound === true) {
+      console.log('%cErrors have been found in the supplied options object.', printStyle);
+    }
+
+    Core.prototype.setOptions.call(this, options);
+
+    if ('type' in options) {
+      if (options.type !== this.options.type) {
+        this.options.type = options.type;
+
+        // force recreation of all items
+        var itemsData = this.itemsData;
+        if (itemsData) {
+          var selection = this.getSelection();
+          this.setItems(null); // remove all
+          this.setItems(itemsData); // add all
+          this.setSelection(selection); // restore selection
+        }
+      }
+    }
+  };
+
+  /**
+   * Set items
+   * @param {vis.DataSet | Array | null} items
+   */
+  Timeline.prototype.setItems = function (items) {
+    var initialLoad = this.itemsData == null;
+
+    // convert to type DataSet when needed
+    var newDataSet;
+    if (!items) {
+      newDataSet = null;
+    } else if (items instanceof DataSet || items instanceof DataView) {
+      newDataSet = items;
+    } else {
+      // turn an array into a dataset
+      newDataSet = new DataSet(items, {
+        type: {
+          start: 'Date',
+          end: 'Date'
+        }
+      });
+    }
+
+    // set items
+    this.itemsData = newDataSet;
+    this.itemSet && this.itemSet.setItems(newDataSet);
+
+    if (initialLoad) {
+      if (this.options.start != undefined || this.options.end != undefined) {
+        if (this.options.start == undefined || this.options.end == undefined) {
+          var range = this.getItemRange();
+        }
+
+        var start = this.options.start != undefined ? this.options.start : range.min;
+        var end = this.options.end != undefined ? this.options.end : range.max;
+
+        this.setWindow(start, end, { animation: false });
+      } else {
+        this.fit({ animation: false });
+      }
+    }
+  };
+
+  /**
+   * Set groups
+   * @param {vis.DataSet | Array} groups
+   */
+  Timeline.prototype.setGroups = function (groups) {
+    // convert to type DataSet when needed
+    var newDataSet;
+    if (!groups) {
+      newDataSet = null;
+    } else if (groups instanceof DataSet || groups instanceof DataView) {
+      newDataSet = groups;
+    } else {
+      // turn an array into a dataset
+      newDataSet = new DataSet(groups);
+    }
+
+    this.groupsData = newDataSet;
+    this.itemSet.setGroups(newDataSet);
+  };
+
+  /**
+   * Set both items and groups in one go
+   * @param {{items: Array | vis.DataSet, groups: Array | vis.DataSet}} data
+   */
+  Timeline.prototype.setData = function (data) {
+    if (data && data.groups) {
+      this.setGroups(data.groups);
+    }
+
+    if (data && data.items) {
+      this.setItems(data.items);
+    }
+  };
+
+  /**
+   * Set selected items by their id. Replaces the current selection
+   * Unknown id's are silently ignored.
+   * @param {string[] | string} [ids]  An array with zero or more id's of the items to be
+   *                                selected. If ids is an empty array, all items will be
+   *                                unselected.
+   * @param {Object} [options]      Available options:
+   *                                `focus: boolean`
+   *                                    If true, focus will be set to the selected item(s)
+   *                                `animation: boolean | {duration: number, easingFunction: string}`
+   *                                    If true (default), the range is animated
+   *                                    smoothly to the new window. An object can be
+   *                                    provided to specify duration and easing function.
+   *                                    Default duration is 500 ms, and default easing
+   *                                    function is 'easeInOutQuad'.
+   *                                    Only applicable when option focus is true.
+   */
+  Timeline.prototype.setSelection = function (ids, options) {
+    this.itemSet && this.itemSet.setSelection(ids);
+
+    if (options && options.focus) {
+      this.focus(ids, options);
+    }
+  };
+
+  /**
+   * Get the selected items by their id
+   * @return {Array} ids  The ids of the selected items
+   */
+  Timeline.prototype.getSelection = function () {
+    return this.itemSet && this.itemSet.getSelection() || [];
+  };
+
+  /**
+   * Adjust the visible window such that the selected item (or multiple items)
+   * are centered on screen.
+   * @param {String | String[]} id     An item id or array with item ids
+   * @param {Object} [options]      Available options:
+   *                                `animation: boolean | {duration: number, easingFunction: string}`
+   *                                    If true (default), the range is animated
+   *                                    smoothly to the new window. An object can be
+   *                                    provided to specify duration and easing function.
+   *                                    Default duration is 500 ms, and default easing
+   *                                    function is 'easeInOutQuad'.
+   */
+  Timeline.prototype.focus = function (id, options) {
+    if (!this.itemsData || id == undefined) return;
+
+    var ids = Array.isArray(id) ? id : [id];
+
+    // get the specified item(s)
+    var itemsData = this.itemsData.getDataSet().get(ids, {
+      type: {
+        start: 'Date',
+        end: 'Date'
+      }
+    });
+
+    // calculate minimum start and maximum end of specified items
+    var start = null;
+    var end = null;
+    itemsData.forEach(function (itemData) {
+      var s = itemData.start.valueOf();
+      var e = 'end' in itemData ? itemData.end.valueOf() : itemData.start.valueOf();
+
+      if (start === null || s < start) {
+        start = s;
+      }
+
+      if (end === null || e > end) {
+        end = e;
+      }
+    });
+
+    if (start !== null && end !== null) {
+      // calculate the new middle and interval for the window
+      var middle = (start + end) / 2;
+      var interval = Math.max(this.range.end - this.range.start, (end - start) * 1.1);
+
+      var animation = options && options.animation !== undefined ? options.animation : true;
+      this.range.setRange(middle - interval / 2, middle + interval / 2, animation);
+    }
+  };
+
+  /**
+   * Set Timeline window such that it fits all items
+   * @param {Object} [options]  Available options:
+   *                                `animation: boolean | {duration: number, easingFunction: string}`
+   *                                    If true (default), the range is animated
+   *                                    smoothly to the new window. An object can be
+   *                                    provided to specify duration and easing function.
+   *                                    Default duration is 500 ms, and default easing
+   *                                    function is 'easeInOutQuad'.
+   */
+  Timeline.prototype.fit = function (options) {
+    var animation = options && options.animation !== undefined ? options.animation : true;
+    var range = this.getItemRange();
+    this.range.setRange(range.min, range.max, animation);
+  };
+
+  /**
+   * Determine the range of the items, taking into account their actual width
+   * and a margin of 10 pixels on both sides.
+   * @return {{min: Date | null, max: Date | null}}
+   */
+  Timeline.prototype.getItemRange = function () {
+    var _this = this;
+
+    // get a rough approximation for the range based on the items start and end dates
+    var range = this.getDataRange();
+    var min = range.min;
+    var max = range.max;
+    var minItem = null;
+    var maxItem = null;
+
+    if (min != null && max != null) {
+      var interval;
+      var factor;
+      var lhs;
+      var rhs;
+      var delta;
+
+      (function () {
+        var getStart = function getStart(item) {
+          return util.convert(item.data.start, 'Date').valueOf();
+        };
+
+        var getEnd = function getEnd(item) {
+          var end = item.data.end != undefined ? item.data.end : item.data.start;
+          return util.convert(end, 'Date').valueOf();
+        };
+
+        interval = max - min;
+        // ms
+        if (interval <= 0) {
+          interval = 10;
+        }
+        factor = interval / _this.props.center.width;
+
+        // calculate the date of the left side and right side of the items given
+        util.forEach(_this.itemSet.items, (function (item) {
+          item.show();
+
+          var start = getStart(item);
+          var end = getEnd(item);
+
+          var left = new Date(start - (item.getWidthLeft() + 10) * factor);
+          var right = new Date(end + (item.getWidthRight() + 10) * factor);
+
+          if (left < min) {
+            min = left;
+            minItem = item;
+          }
+          if (right > max) {
+            max = right;
+            maxItem = item;
+          }
+        }).bind(_this));
+
+        if (minItem && maxItem) {
+          lhs = minItem.getWidthLeft() + 10;
+          rhs = maxItem.getWidthRight() + 10;
+          delta = _this.props.center.width - lhs - rhs;
+          // px
+
+          if (delta > 0) {
+            min = getStart(minItem) - lhs * interval / delta; // ms
+            max = getEnd(maxItem) + rhs * interval / delta; // ms
+          }
+        }
+      })();
+    }
+
+    return {
+      min: min != null ? new Date(min) : null,
+      max: max != null ? new Date(max) : null
+    };
+  };
+
+  /**
+   * Calculate the data range of the items start and end dates
+   * @returns {{min: Date | null, max: Date | null}}
+   */
+  Timeline.prototype.getDataRange = function () {
+    var min = null;
+    var max = null;
+
+    var dataset = this.itemsData && this.itemsData.getDataSet();
+    if (dataset) {
+      dataset.forEach(function (item) {
+        var start = util.convert(item.start, 'Date').valueOf();
+        var end = util.convert(item.end != undefined ? item.end : item.start, 'Date').valueOf();
+        if (min === null || start < min) {
+          min = start;
+        }
+        if (max === null || end > max) {
+          max = start;
+        }
+      });
+    }
+
+    return {
+      min: min != null ? new Date(min) : null,
+      max: max != null ? new Date(max) : null
+    };
+  };
+
+  /**
+   * Generate Timeline related information from an event
+   * @param {Event} event
+   * @return {Object} An object with related information, like on which area
+   *                  The event happened, whether clicked on an item, etc.
+   */
+  Timeline.prototype.getEventProperties = function (event) {
+    var clientX = event.center ? event.center.x : event.clientX;
+    var clientY = event.center ? event.center.y : event.clientY;
+    var x = clientX - util.getAbsoluteLeft(this.dom.centerContainer);
+    var y = clientY - util.getAbsoluteTop(this.dom.centerContainer);
+
+    var item = this.itemSet.itemFromTarget(event);
+    var group = this.itemSet.groupFromTarget(event);
+    var customTime = CustomTime.customTimeFromTarget(event);
+
+    var snap = this.itemSet.options.snap || null;
+    var scale = this.body.util.getScale();
+    var step = this.body.util.getStep();
+    var time = this._toTime(x);
+    var snappedTime = snap ? snap(time, scale, step) : time;
+
+    var element = util.getTarget(event);
+    var what = null;
+    if (item != null) {
+      what = 'item';
+    } else if (customTime != null) {
+      what = 'custom-time';
+    } else if (util.hasParent(element, this.timeAxis.dom.foreground)) {
+      what = 'axis';
+    } else if (this.timeAxis2 && util.hasParent(element, this.timeAxis2.dom.foreground)) {
+      what = 'axis';
+    } else if (util.hasParent(element, this.itemSet.dom.labelSet)) {
+      what = 'group-label';
+    } else if (util.hasParent(element, this.currentTime.bar)) {
+      what = 'current-time';
+    } else if (util.hasParent(element, this.dom.center)) {
+      what = 'background';
+    }
+
+    return {
+      event: event,
+      item: item ? item.id : null,
+      group: group ? group.groupId : null,
+      what: what,
+      pageX: event.srcEvent ? event.srcEvent.pageX : event.pageX,
+      pageY: event.srcEvent ? event.srcEvent.pageY : event.pageY,
+      x: x,
+      y: y,
+      time: time,
+      snappedTime: snappedTime
+    };
+  };
+
+  module.exports = Timeline;
+
+/***/ },
+/* 20 */
+/***/ function(module, exports, __webpack_require__) {
+
+  // Only load hammer.js when in a browser environment
+  // (loading hammer.js in a node.js environment gives errors)
+  'use strict';
+
+  if (typeof window !== 'undefined') {
+    var propagating = __webpack_require__(21);
+    var Hammer = window['Hammer'] || __webpack_require__(22);
+    module.exports = propagating(Hammer, {
+      preventDefault: 'mouse'
+    });
+  } else {
+    module.exports = function () {
+      throw Error('hammer.js is only available in a browser, not in node.js.');
+    };
+  }
+
+/***/ },
+/* 21 */
+/***/ function(module, exports, __webpack_require__) {
+
+  var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict';
+
+  (function (factory) {
+    if (true) {
+      // AMD. Register as an anonymous module.
+      !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+    } else if (typeof exports === 'object') {
+      // Node. Does not work with strict CommonJS, but
+      // only CommonJS-like environments that support module.exports,
+      // like Node.
+      module.exports = factory();
+    } else {
+      // Browser globals (root is window)
+      window.propagating = factory();
+    }
+  }(function () {
+    var _firstTarget = null; // singleton, will contain the target element where the touch event started
+    var _processing = false; // singleton, true when a touch event is being handled
+
+    /**
+     * Extend an Hammer.js instance with event propagation.
+     *
+     * Features:
+     * - Events emitted by hammer will propagate in order from child to parent
+     *   elements.
+     * - Events are extended with a function `event.stopPropagation()` to stop
+     *   propagation to parent elements.
+     * - An option `preventDefault` to stop all default browser behavior.
+     *
+     * Usage:
+     *   var hammer = propagatingHammer(new Hammer(element));
+     *   var hammer = propagatingHammer(new Hammer(element), {preventDefault: true});
+     *
+     * @param {Hammer.Manager} hammer   An hammer instance.
+     * @param {Object} [options]        Available options:
+     *                                  - `preventDefault: true | 'mouse' | 'touch' | 'pen'`.
+     *                                    Enforce preventing the default browser behavior.
+     *                                    Cannot be set to `false`.
+     * @return {Hammer.Manager} Returns the same hammer instance with extended
+     *                          functionality
+     */
+    return function propagating(hammer, options) {
+      var _options = options || {
+        preventDefault: false
+      };
+
+      if (hammer.Manager) {
+        // This looks like the Hammer constructor.
+        // Overload the constructors with our own.
+        var Hammer = hammer;
+
+        var PropagatingHammer = function(element, options) {
+          var o = Object.create(_options);
+          if (options) Hammer.extend(o, options);
+          return propagating(new Hammer(element, o), o);
+        };
+        Hammer.extend(PropagatingHammer, Hammer);
+
+        PropagatingHammer.Manager = function (element, options) {
+          var o = Object.create(_options);
+          if (options) Hammer.extend(o, options);
+          return propagating(new Hammer.Manager(element, o), o);
+        };
+
+        return PropagatingHammer;
+      }
+
+      // create a wrapper object which will override the functions
+      // `on`, `off`, `destroy`, and `emit` of the hammer instance
+      var wrapper = Object.create(hammer);
+
+      // attach to DOM element
+      var element = hammer.element;
+      element.hammer = wrapper;
+
+      // register an event to catch the start of a gesture and store the
+      // target in a singleton
+      hammer.on('hammer.input', function (event) {
+        if (_options.preventDefault === true || (_options.preventDefault === event.pointerType)) {
+          event.preventDefault();
+        }
+        if (event.isFirst) {
+          _firstTarget = event.target;
+        }
+      });
+
+      /** @type {Object.<String, Array.<function>>} */
+      wrapper._handlers = {};
+
+      /**
+       * Register a handler for one or multiple events
+       * @param {String} events    A space separated string with events
+       * @param {function} handler A callback function, called as handler(event)
+       * @returns {Hammer.Manager} Returns the hammer instance
+       */
+      wrapper.on = function (events, han