Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/syui/hugo-theme-wave.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'static/bower_components/cal-heatmap/cal-heatmap.js')
-rw-r--r--static/bower_components/cal-heatmap/cal-heatmap.js3484
1 files changed, 3484 insertions, 0 deletions
diff --git a/static/bower_components/cal-heatmap/cal-heatmap.js b/static/bower_components/cal-heatmap/cal-heatmap.js
new file mode 100644
index 0000000..0ce105e
--- /dev/null
+++ b/static/bower_components/cal-heatmap/cal-heatmap.js
@@ -0,0 +1,3484 @@
+/*! cal-heatmap v3.6.0 (Sun Apr 24 2016 19:19:35)
+ * ---------------------------------------------
+ * Cal-Heatmap is a javascript module to create calendar heatmap to visualize time series data
+ * https://github.com/wa0x6e/cal-heatmap
+ * Licensed under the MIT license
+ * Copyright 2014 Wan Qi Chen
+ */
+
+var d3 = typeof require === "function" ? require("d3") : window.d3;
+
+var CalHeatMap = function() {
+ "use strict";
+
+ var self = this;
+
+ this.allowedDataType = ["json", "csv", "tsv", "txt"];
+
+ // Default settings
+ this.options = {
+ // selector string of the container to append the graph to
+ // Accept any string value accepted by document.querySelector or CSS3
+ // or an Element object
+ itemSelector: "#cal-heatmap",
+
+ // Whether to paint the calendar on init()
+ // Used by testsuite to reduce testing time
+ paintOnLoad: true,
+
+ // ================================================
+ // DOMAIN
+ // ================================================
+
+ // Number of domain to display on the graph
+ range: 12,
+
+ // Size of each cell, in pixel
+ cellSize: 10,
+
+ // Padding between each cell, in pixel
+ cellPadding: 2,
+
+ // For rounded subdomain rectangles, in pixels
+ cellRadius: 0,
+
+ domainGutter: 2,
+
+ domainMargin: [0, 0, 0, 0],
+
+ domain: "hour",
+
+ subDomain: "min",
+
+ // Number of columns to split the subDomains to
+ // If not null, will takes precedence over rowLimit
+ colLimit: null,
+
+ // Number of rows to split the subDomains to
+ // Will be ignored if colLimit is not null
+ rowLimit: null,
+
+ // First day of the week is Monday
+ // 0 to start the week on Sunday
+ weekStartOnMonday: true,
+
+ // Start date of the graph
+ // @default now
+ start: new Date(),
+
+ minDate: null,
+
+ maxDate: null,
+
+ // ================================================
+ // DATA
+ // ================================================
+
+ // Data source
+ // URL, where to fetch the original datas
+ data: "",
+
+ // Data type
+ // Default: json
+ dataType: this.allowedDataType[0],
+
+ // Payload sent when using POST http method
+ // Leave to null (default) for GET request
+ // Expect a string, formatted like "a=b;c=d"
+ dataPostPayload: null,
+
+ // Whether to consider missing date:value from the datasource
+ // as equal to 0, or just leave them as missing
+ considerMissingDataAsZero: false,
+
+ // Load remote data on calendar creation
+ // When false, the calendar will be left empty
+ loadOnInit: true,
+
+ // Calendar orientation
+ // false: display domains side by side
+ // true : display domains one under the other
+ verticalOrientation: false,
+
+ // Domain dynamic width/height
+ // The width on a domain depends on the number of
+ domainDynamicDimension: true,
+
+ // Domain Label properties
+ label: {
+ // valid: top, right, bottom, left
+ position: "bottom",
+
+ // Valid: left, center, right
+ // Also valid are the direct svg values: start, middle, end
+ align: "center",
+
+ // By default, there is no margin/padding around the label
+ offset: {
+ x: 0,
+ y: 0
+ },
+
+ rotate: null,
+
+ // Used only on vertical orientation
+ width: 100,
+
+ // Used only on horizontal orientation
+ height: null
+ },
+
+ // ================================================
+ // LEGEND
+ // ================================================
+
+ // Threshold for the legend
+ legend: [10, 20, 30, 40],
+
+ // Whether to display the legend
+ displayLegend: true,
+
+ legendCellSize: 10,
+
+ legendCellPadding: 2,
+
+ legendMargin: [0, 0, 0, 0],
+
+ // Legend vertical position
+ // top: place legend above calendar
+ // bottom: place legend below the calendar
+ legendVerticalPosition: "bottom",
+
+ // Legend horizontal position
+ // accepted values: left, center, right
+ legendHorizontalPosition: "left",
+
+ // Legend rotation
+ // accepted values: horizontal, vertical
+ legendOrientation: "horizontal",
+
+ // Objects holding all the heatmap different colors
+ // null to disable, and use the default css styles
+ //
+ // Examples:
+ // legendColors: {
+ // min: "green",
+ // max: "red",
+ // empty: "#ffffff",
+ // base: "grey",
+ // overflow: "red"
+ // }
+ legendColors: null,
+
+ // ================================================
+ // HIGHLIGHT
+ // ================================================
+
+ // List of dates to highlight
+ // Valid values:
+ // - []: don't highlight anything
+ // - "now": highlight the current date
+ // - an array of Date objects: highlight the specified dates
+ highlight: [],
+
+ // ================================================
+ // TEXT FORMATTING / i18n
+ // ================================================
+
+ // Name of the items to represent in the calendar
+ itemName: ["item", "items"],
+
+ // Formatting of the domain label
+ // @default: null, will use the formatting according to domain type
+ // Accept a string used as specifier by d3.time.format()
+ // or a function
+ //
+ // Refer to https://github.com/mbostock/d3/wiki/Time-Formatting
+ // for accepted date formatting used by d3.time.format()
+ domainLabelFormat: null,
+
+ // Formatting of the title displayed when hovering a subDomain cell
+ subDomainTitleFormat: {
+ empty: "{date}",
+ filled: "{count} {name} {connector} {date}"
+ },
+
+ // Formatting of the {date} used in subDomainTitleFormat
+ // @default: null, will use the formatting according to subDomain type
+ // Accept a string used as specifier by d3.time.format()
+ // or a function
+ //
+ // Refer to https://github.com/mbostock/d3/wiki/Time-Formatting
+ // for accepted date formatting used by d3.time.format()
+ subDomainDateFormat: null,
+
+ // Formatting of the text inside each subDomain cell
+ // @default: null, no text
+ // Accept a string used as specifier by d3.time.format()
+ // or a function
+ //
+ // Refer to https://github.com/mbostock/d3/wiki/Time-Formatting
+ // for accepted date formatting used by d3.time.format()
+ subDomainTextFormat: null,
+
+ // Formatting of the title displayed when hovering a legend cell
+ legendTitleFormat: {
+ lower: "less than {min} {name}",
+ inner: "between {down} and {up} {name}",
+ upper: "more than {max} {name}"
+ },
+
+ // Animation duration, in ms
+ animationDuration: 500,
+
+ nextSelector: false,
+
+ previousSelector: false,
+
+ itemNamespace: "cal-heatmap",
+
+ tooltip: false,
+
+ // ================================================
+ // EVENTS CALLBACK
+ // ================================================
+
+ // Callback when clicking on a time block
+ onClick: null,
+
+ // Callback after painting the empty calendar
+ // Can be used to trigger an API call, once the calendar is ready to be filled
+ afterLoad: null,
+
+ // Callback after loading the next domain in the calendar
+ afterLoadNextDomain: null,
+
+ // Callback after loading the previous domain in the calendar
+ afterLoadPreviousDomain: null,
+
+ // Callback after finishing all actions on the calendar
+ onComplete: null,
+
+ // Callback after fetching the datas, but before applying them to the calendar
+ // Used mainly to convert the datas if they're not formatted like expected
+ // Takes the fetched "data" object as argument, must return a json object
+ // formatted like {timestamp:count, timestamp2:count2},
+ afterLoadData: function(data) { return data; },
+
+ // Callback triggered after calling next().
+ // The `status` argument is equal to true if there is no
+ // more next domain to load
+ //
+ // This callback is also executed once, after calling previous(),
+ // only when the max domain is reached
+ onMaxDomainReached: null,
+
+ // Callback triggered after calling previous().
+ // The `status` argument is equal to true if there is no
+ // more previous domain to load
+ //
+ // This callback is also executed once, after calling next(),
+ // only when the min domain is reached
+ onMinDomainReached: null
+ };
+
+ this._domainType = {
+ "min": {
+ name: "minute",
+ level: 10,
+ maxItemNumber: 60,
+ defaultRowNumber: 10,
+ defaultColumnNumber: 6,
+ row: function(d) { return self.getSubDomainRowNumber(d); },
+ column: function(d) { return self.getSubDomainColumnNumber(d); },
+ position: {
+ x: function(d) { return Math.floor(d.getMinutes() / self._domainType.min.row(d)); },
+ y: function(d) { return d.getMinutes() % self._domainType.min.row(d); }
+ },
+ format: {
+ date: "%H:%M, %A %B %-e, %Y",
+ legend: "",
+ connector: "at"
+ },
+ extractUnit: function(d) {
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes()).getTime();
+ }
+ },
+ "hour": {
+ name: "hour",
+ level: 20,
+ maxItemNumber: function(d) {
+ switch(self.options.domain) {
+ case "day":
+ return 24;
+ case "week":
+ return 24 * 7;
+ case "month":
+ return 24 * (self.options.domainDynamicDimension ? self.getDayCountInMonth(d): 31);
+ }
+ },
+ defaultRowNumber: 6,
+ defaultColumnNumber: function(d) {
+ switch(self.options.domain) {
+ case "day":
+ return 4;
+ case "week":
+ return 28;
+ case "month":
+ return self.options.domainDynamicDimension ? self.getDayCountInMonth(d): 31;
+ }
+ },
+ row: function(d) { return self.getSubDomainRowNumber(d); },
+ column: function(d) { return self.getSubDomainColumnNumber(d); },
+ position: {
+ x: function(d) {
+ if (self.options.domain === "month") {
+ if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
+ return Math.floor((d.getHours() + (d.getDate()-1)*24) / self._domainType.hour.row(d));
+ }
+ return Math.floor(d.getHours() / self._domainType.hour.row(d)) + (d.getDate()-1)*4;
+ } else if (self.options.domain === "week") {
+ if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
+ return Math.floor((d.getHours() + self.getWeekDay(d)*24) / self._domainType.hour.row(d));
+ }
+ return Math.floor(d.getHours() / self._domainType.hour.row(d)) + self.getWeekDay(d)*4;
+ }
+ return Math.floor(d.getHours() / self._domainType.hour.row(d));
+ },
+ y: function(d) {
+ var p = d.getHours();
+ if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
+ switch(self.options.domain) {
+ case "month":
+ p += (d.getDate()-1) * 24;
+ break;
+ case "week":
+ p += self.getWeekDay(d) * 24;
+ break;
+ }
+ }
+ return Math.floor(p % self._domainType.hour.row(d));
+ }
+ },
+ format: {
+ date: "%Hh, %A %B %-e, %Y",
+ legend: "%H:00",
+ connector: "at"
+ },
+ extractUnit: function(d) {
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()).getTime();
+ }
+ },
+ "day": {
+ name: "day",
+ level: 30,
+ maxItemNumber: function(d) {
+ switch(self.options.domain) {
+ case "week":
+ return 7;
+ case "month":
+ return self.options.domainDynamicDimension ? self.getDayCountInMonth(d) : 31;
+ case "year":
+ return self.options.domainDynamicDimension ? self.getDayCountInYear(d) : 366;
+ }
+ },
+ defaultColumnNumber: function(d) {
+ d = new Date(d);
+ switch(self.options.domain) {
+ case "week":
+ return 1;
+ case "month":
+ return (self.options.domainDynamicDimension && !self.options.verticalOrientation) ? (self.getWeekNumber(new Date(d.getFullYear(), d.getMonth()+1, 0)) - self.getWeekNumber(d) + 1): 6;
+ case "year":
+ return (self.options.domainDynamicDimension ? (self.getWeekNumber(new Date(d.getFullYear(), 11, 31)) - self.getWeekNumber(new Date(d.getFullYear(), 0)) + 1): 54);
+ }
+ },
+ defaultRowNumber: 7,
+ row: function(d) { return self.getSubDomainRowNumber(d); },
+ column: function(d) { return self.getSubDomainColumnNumber(d); },
+ position: {
+ x: function(d) {
+ switch(self.options.domain) {
+ case "week":
+ return Math.floor(self.getWeekDay(d) / self._domainType.day.row(d));
+ case "month":
+ if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
+ return Math.floor((d.getDate() - 1)/ self._domainType.day.row(d));
+ }
+ return self.getWeekNumber(d) - self.getWeekNumber(new Date(d.getFullYear(), d.getMonth()));
+ case "year":
+ if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
+ return Math.floor((self.getDayOfYear(d) - 1) / self._domainType.day.row(d));
+ }
+ return self.getWeekNumber(d);
+ }
+ },
+ y: function(d) {
+ var p = self.getWeekDay(d);
+ if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
+ switch(self.options.domain) {
+ case "year":
+ p = self.getDayOfYear(d) - 1;
+ break;
+ case "week":
+ p = self.getWeekDay(d);
+ break;
+ case "month":
+ p = d.getDate() - 1;
+ break;
+ }
+ }
+ return Math.floor(p % self._domainType.day.row(d));
+ }
+ },
+ format: {
+ date: "%A %B %-e, %Y",
+ legend: "%e %b",
+ connector: "on"
+ },
+ extractUnit: function(d) {
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
+ }
+ },
+ "week": {
+ name: "week",
+ level: 40,
+ maxItemNumber: 54,
+ defaultColumnNumber: function(d) {
+ d = new Date(d);
+ switch(self.options.domain) {
+ case "year":
+ return self._domainType.week.maxItemNumber;
+ case "month":
+ return self.options.domainDynamicDimension ? self.getWeekNumber(new Date(d.getFullYear(), d.getMonth()+1, 0)) - self.getWeekNumber(d) : 5;
+ }
+ },
+ defaultRowNumber: 1,
+ row: function(d) { return self.getSubDomainRowNumber(d); },
+ column: function(d) { return self.getSubDomainColumnNumber(d); },
+ position: {
+ x: function(d) {
+ switch(self.options.domain) {
+ case "year":
+ return Math.floor(self.getWeekNumber(d) / self._domainType.week.row(d));
+ case "month":
+ return Math.floor(self.getMonthWeekNumber(d) / self._domainType.week.row(d));
+ }
+ },
+ y: function(d) {
+ return self.getWeekNumber(d) % self._domainType.week.row(d);
+ }
+ },
+ format: {
+ date: "%B Week #%W",
+ legend: "%B Week #%W",
+ connector: "in"
+ },
+ extractUnit: function(d) {
+ var dt = new Date(d.getFullYear(), d.getMonth(), d.getDate());
+ // According to ISO-8601, week number computation are based on week starting on Monday
+ var weekDay = dt.getDay()-1;
+ if (weekDay < 0) {
+ weekDay = 6;
+ }
+ dt.setDate(dt.getDate() - weekDay);
+ return dt.getTime();
+ }
+ },
+ "month": {
+ name: "month",
+ level: 50,
+ maxItemNumber: 12,
+ defaultColumnNumber: 12,
+ defaultRowNumber: 1,
+ row: function() { return self.getSubDomainRowNumber(); },
+ column: function() { return self.getSubDomainColumnNumber(); },
+ position: {
+ x: function(d) { return Math.floor(d.getMonth() / self._domainType.month.row(d)); },
+ y: function(d) { return d.getMonth() % self._domainType.month.row(d); }
+ },
+ format: {
+ date: "%B %Y",
+ legend: "%B",
+ connector: "in"
+ },
+ extractUnit: function(d) {
+ return new Date(d.getFullYear(), d.getMonth()).getTime();
+ }
+ },
+ "year": {
+ name: "year",
+ level: 60,
+ row: function() { return self.options.rowLimit || 1; },
+ column: function() { return self.options.colLimit || 1; },
+ position: {
+ x: function() { return 1; },
+ y: function() { return 1; }
+ },
+ format: {
+ date: "%Y",
+ legend: "%Y",
+ connector: "in"
+ },
+ extractUnit: function(d) {
+ return new Date(d.getFullYear()).getTime();
+ }
+ }
+ };
+
+ for (var type in this._domainType) {
+ if (this._domainType.hasOwnProperty(type)) {
+ var d = this._domainType[type];
+ this._domainType["x_" + type] = {
+ name: "x_" + type,
+ level: d.type,
+ maxItemNumber: d.maxItemNumber,
+ defaultRowNumber: d.defaultRowNumber,
+ defaultColumnNumber: d.defaultColumnNumber,
+ row: d.column,
+ column: d.row,
+ position: {
+ x: d.position.y,
+ y: d.position.x
+ },
+ format: d.format,
+ extractUnit: d.extractUnit
+ };
+ }
+ }
+
+ // Record the address of the last inserted domain when browsing
+ this.lastInsertedSvg = null;
+
+ this._completed = false;
+
+ // Record all the valid domains
+ // Each domain value is a timestamp in milliseconds
+ this._domains = d3.map();
+
+ this.graphDim = {
+ width: 0,
+ height: 0
+ };
+
+ this.legendDim = {
+ width: 0,
+ height: 0
+ };
+
+ this.NAVIGATE_LEFT = 1;
+ this.NAVIGATE_RIGHT = 2;
+
+ // Various update mode when using the update() API
+ this.RESET_ALL_ON_UPDATE = 0;
+ this.RESET_SINGLE_ON_UPDATE = 1;
+ this.APPEND_ON_UPDATE = 2;
+
+ this.DEFAULT_LEGEND_MARGIN = 10;
+
+ this.root = null;
+ this.tooltip = null;
+
+ this._maxDomainReached = false;
+ this._minDomainReached = false;
+
+ this.domainPosition = new DomainPosition();
+ this.Legend = null;
+ this.legendScale = null;
+
+ // List of domains that are skipped because of DST
+ // All times belonging to these domains should be re-assigned to the previous domain
+ this.DSTDomain = [];
+
+ /**
+ * Display the graph for the first time
+ * @return bool True if the calendar is created
+ */
+ this._init = function() {
+
+ self.getDomain(self.options.start).map(function(d) { return d.getTime(); }).map(function(d) {
+ self._domains.set(d, self.getSubDomain(d).map(function(d) { return {t: self._domainType[self.options.subDomain].extractUnit(d), v: null}; }));
+ });
+
+ self.root = d3.select(self.options.itemSelector).append("svg").attr("class", "cal-heatmap-container");
+
+ self.tooltip = d3.select(self.options.itemSelector)
+ .attr("style", function() {
+ var current = d3.select(self.options.itemSelector).attr("style");
+ return (current !== null ? current : "") + "position:relative;";
+ })
+ .append("div")
+ .attr("class", "ch-tooltip")
+ ;
+
+ self.root.attr("x", 0).attr("y", 0).append("svg").attr("class", "graph");
+
+ self.Legend = new Legend(self);
+
+ if (self.options.paintOnLoad) {
+ _initCalendar();
+ }
+
+ return true;
+ };
+
+ function _initCalendar() {
+ self.verticalDomainLabel = (self.options.label.position === "top" || self.options.label.position === "bottom");
+
+ self.domainVerticalLabelHeight = self.options.label.height === null ? Math.max(25, self.options.cellSize*2): self.options.label.height;
+ self.domainHorizontalLabelWidth = 0;
+
+ if (self.options.domainLabelFormat === "" && self.options.label.height === null) {
+ self.domainVerticalLabelHeight = 0;
+ }
+
+ if (!self.verticalDomainLabel) {
+ self.domainVerticalLabelHeight = 0;
+ self.domainHorizontalLabelWidth = self.options.label.width;
+ }
+
+ self.paint();
+
+ // =========================================================================//
+ // ATTACHING DOMAIN NAVIGATION EVENT //
+ // =========================================================================//
+ if (self.options.nextSelector !== false) {
+ d3.select(self.options.nextSelector).on("click." + self.options.itemNamespace, function() {
+ d3.event.preventDefault();
+ return self.loadNextDomain(1);
+ });
+ }
+
+ if (self.options.previousSelector !== false) {
+ d3.select(self.options.previousSelector).on("click." + self.options.itemNamespace, function() {
+ d3.event.preventDefault();
+ return self.loadPreviousDomain(1);
+ });
+ }
+
+ self.Legend.redraw(self.graphDim.width - self.options.domainGutter - self.options.cellPadding);
+ self.afterLoad();
+
+ var domains = self.getDomainKeys();
+
+ // Fill the graph with some datas
+ if (self.options.loadOnInit) {
+ self.getDatas(
+ self.options.data,
+ new Date(domains[0]),
+ self.getSubDomain(domains[domains.length-1]).pop(),
+ function() {
+ self.fill();
+ self.onComplete();
+ }
+ );
+ } else {
+ self.onComplete();
+ }
+
+ self.checkIfMinDomainIsReached(domains[0]);
+ self.checkIfMaxDomainIsReached(self.getNextDomain().getTime());
+ }
+
+ // Return the width of the domain block, without the domain gutter
+ // @param int d Domain start timestamp
+ function w(d, outer) {
+ var width = self.options.cellSize*self._domainType[self.options.subDomain].column(d) + self.options.cellPadding*self._domainType[self.options.subDomain].column(d);
+ if (arguments.length === 2 && outer === true) {
+ return width += self.domainHorizontalLabelWidth + self.options.domainGutter + self.options.domainMargin[1] + self.options.domainMargin[3];
+ }
+ return width;
+ }
+
+ // Return the height of the domain block, without the domain gutter
+ function h(d, outer) {
+ var height = self.options.cellSize*self._domainType[self.options.subDomain].row(d) + self.options.cellPadding*self._domainType[self.options.subDomain].row(d);
+ if (arguments.length === 2 && outer === true) {
+ height += self.options.domainGutter + self.domainVerticalLabelHeight + self.options.domainMargin[0] + self.options.domainMargin[2];
+ }
+ return height;
+ }
+
+ /**
+ *
+ *
+ * @param int navigationDir
+ */
+ this.paint = function(navigationDir) {
+
+ var options = self.options;
+
+ if (arguments.length === 0) {
+ navigationDir = false;
+ }
+
+ // Painting all the domains
+ var domainSvg = self.root.select(".graph")
+ .selectAll(".graph-domain")
+ .data(
+ function() {
+ var data = self.getDomainKeys();
+ return navigationDir === self.NAVIGATE_LEFT ? data.reverse(): data;
+ },
+ function(d) { return d; }
+ )
+ ;
+
+ var enteringDomainDim = 0;
+ var exitingDomainDim = 0;
+
+ // =========================================================================//
+ // PAINTING DOMAIN //
+ // =========================================================================//
+
+ var svg = domainSvg
+ .enter()
+ .append("svg")
+ .attr("width", function(d) {
+ return w(d, true);
+ })
+ .attr("height", function(d) {
+ return h(d, true);
+ })
+ .attr("x", function(d) {
+ if (options.verticalOrientation) {
+ self.graphDim.width = Math.max(self.graphDim.width, w(d, true));
+ return 0;
+ } else {
+ return getDomainPosition(d, self.graphDim, "width", w(d, true));
+ }
+ })
+ .attr("y", function(d) {
+ if (options.verticalOrientation) {
+ return getDomainPosition(d, self.graphDim, "height", h(d, true));
+ } else {
+ self.graphDim.height = Math.max(self.graphDim.height, h(d, true));
+ return 0;
+ }
+ })
+ .attr("class", function(d) {
+ var classname = "graph-domain";
+ var date = new Date(d);
+ switch(options.domain) {
+ case "hour":
+ classname += " h_" + date.getHours();
+ /* falls through */
+ case "day":
+ classname += " d_" + date.getDate() + " dy_" + date.getDay();
+ /* falls through */
+ case "week":
+ classname += " w_" + self.getWeekNumber(date);
+ /* falls through */
+ case "month":
+ classname += " m_" + (date.getMonth() + 1);
+ /* falls through */
+ case "year":
+ classname += " y_" + date.getFullYear();
+ }
+ return classname;
+ })
+ ;
+
+ self.lastInsertedSvg = svg;
+
+ function getDomainPosition(domainIndex, graphDim, axis, domainDim) {
+ var tmp = 0;
+ switch(navigationDir) {
+ case false:
+ tmp = graphDim[axis];
+
+ graphDim[axis] += domainDim;
+ self.domainPosition.setPosition(domainIndex, tmp);
+ return tmp;
+
+ case self.NAVIGATE_RIGHT:
+ self.domainPosition.setPosition(domainIndex, graphDim[axis]);
+
+ enteringDomainDim = domainDim;
+ exitingDomainDim = self.domainPosition.getPositionFromIndex(1);
+
+ self.domainPosition.shiftRightBy(exitingDomainDim);
+ return graphDim[axis];
+
+ case self.NAVIGATE_LEFT:
+ tmp = -domainDim;
+
+ enteringDomainDim = -tmp;
+ exitingDomainDim = graphDim[axis] - self.domainPosition.getLast();
+
+ self.domainPosition.setPosition(domainIndex, tmp);
+ self.domainPosition.shiftLeftBy(enteringDomainDim);
+ return tmp;
+ }
+ }
+
+ svg.append("rect")
+ .attr("width", function(d) { return w(d, true) - options.domainGutter - options.cellPadding; })
+ .attr("height", function(d) { return h(d, true) - options.domainGutter - options.cellPadding; })
+ .attr("class", "domain-background")
+ ;
+
+ // =========================================================================//
+ // PAINTING SUBDOMAINS //
+ // =========================================================================//
+ var subDomainSvgGroup = svg.append("svg")
+ .attr("x", function() {
+ if (options.label.position === "left") {
+ return self.domainHorizontalLabelWidth + options.domainMargin[3];
+ } else {
+ return options.domainMargin[3];
+ }
+ })
+ .attr("y", function() {
+ if (options.label.position === "top") {
+ return self.domainVerticalLabelHeight + options.domainMargin[0];
+ } else {
+ return options.domainMargin[0];
+ }
+ })
+ .attr("class", "graph-subdomain-group")
+ ;
+
+ var rect = subDomainSvgGroup
+ .selectAll("g")
+ .data(function(d) { return self._domains.get(d); })
+ .enter()
+ .append("g")
+ ;
+
+ rect
+ .append("rect")
+ .attr("class", function(d) {
+ return "graph-rect" + self.getHighlightClassName(d.t) + (options.onClick !== null ? " hover_cursor": "");
+ })
+ .attr("width", options.cellSize)
+ .attr("height", options.cellSize)
+ .attr("x", function(d) { return self.positionSubDomainX(d.t); })
+ .attr("y", function(d) { return self.positionSubDomainY(d.t); })
+ .on("click", function(d) {
+ if (options.onClick !== null) {
+ return self.onClick(new Date(d.t), d.v);
+ }
+ })
+ .call(function(selection) {
+ if (options.cellRadius > 0) {
+ selection
+ .attr("rx", options.cellRadius)
+ .attr("ry", options.cellRadius)
+ ;
+ }
+
+ if (self.legendScale !== null && options.legendColors !== null && options.legendColors.hasOwnProperty("base")) {
+ selection.attr("fill", options.legendColors.base);
+ }
+
+ if (options.tooltip) {
+ selection.on("mouseover", function(d) {
+ var domainNode = this.parentNode.parentNode;
+
+ self.tooltip
+ .html(self.getSubDomainTitle(d))
+ .attr("style", "display: block;")
+ ;
+
+ var tooltipPositionX = self.positionSubDomainX(d.t) - self.tooltip[0][0].offsetWidth/2 + options.cellSize/2;
+ var tooltipPositionY = self.positionSubDomainY(d.t) - self.tooltip[0][0].offsetHeight - options.cellSize/2;
+
+ // Offset by the domain position
+ tooltipPositionX += parseInt(domainNode.getAttribute("x"), 10);
+ tooltipPositionY += parseInt(domainNode.getAttribute("y"), 10);
+
+ // Offset by the calendar position (when legend is left/top)
+ tooltipPositionX += parseInt(self.root.select(".graph").attr("x"), 10);
+ tooltipPositionY += parseInt(self.root.select(".graph").attr("y"), 10);
+
+ // Offset by the inside domain position (when label is left/top)
+ tooltipPositionX += parseInt(domainNode.parentNode.getAttribute("x"), 10);
+ tooltipPositionY += parseInt(domainNode.parentNode.getAttribute("y"), 10);
+
+ self.tooltip.attr("style",
+ "display: block; " +
+ "left: " + tooltipPositionX + "px; " +
+ "top: " + tooltipPositionY + "px;")
+ ;
+ });
+
+ selection.on("mouseout", function() {
+ self.tooltip
+ .attr("style", "display:none")
+ .html("");
+ });
+ }
+ })
+ ;
+
+ // Appending a title to each subdomain
+ if (!options.tooltip) {
+ rect.append("title").text(function(d){ return self.formatDate(new Date(d.t), options.subDomainDateFormat); });
+ }
+
+ // =========================================================================//
+ // PAINTING LABEL //
+ // =========================================================================//
+ if (options.domainLabelFormat !== "") {
+ svg.append("text")
+ .attr("class", "graph-label")
+ .attr("y", function(d) {
+ var y = options.domainMargin[0];
+ switch(options.label.position) {
+ case "top":
+ y += self.domainVerticalLabelHeight/2;
+ break;
+ case "bottom":
+ y += h(d) + self.domainVerticalLabelHeight/2;
+ }
+
+ return y + options.label.offset.y *
+ (
+ ((options.label.rotate === "right" && options.label.position === "right") ||
+ (options.label.rotate === "left" && options.label.position === "left")) ?
+ -1: 1
+ );
+ })
+ .attr("x", function(d){
+ var x = options.domainMargin[3];
+ switch(options.label.position) {
+ case "right":
+ x += w(d);
+ break;
+ case "bottom":
+ case "top":
+ x += w(d)/2;
+ }
+
+ if (options.label.align === "right") {
+ return x + self.domainHorizontalLabelWidth - options.label.offset.x *
+ (options.label.rotate === "right" ? -1: 1);
+ }
+ return x + options.label.offset.x;
+
+ })
+ .attr("text-anchor", function() {
+ switch(options.label.align) {
+ case "start":
+ case "left":
+ return "start";
+ case "end":
+ case "right":
+ return "end";
+ default:
+ return "middle";
+ }
+ })
+ .attr("dominant-baseline", function() { return self.verticalDomainLabel ? "middle": "top"; })
+ .text(function(d) { return self.formatDate(new Date(d), options.domainLabelFormat); })
+ .call(domainRotate)
+ ;
+ }
+
+ function domainRotate(selection) {
+ switch (options.label.rotate) {
+ case "right":
+ selection
+ .attr("transform", function(d) {
+ var s = "rotate(90), ";
+ switch(options.label.position) {
+ case "right":
+ s += "translate(-" + w(d) + " , -" + w(d) + ")";
+ break;
+ case "left":
+ s += "translate(0, -" + self.domainHorizontalLabelWidth + ")";
+ break;
+ }
+
+ return s;
+ });
+ break;
+ case "left":
+ selection
+ .attr("transform", function(d) {
+ var s = "rotate(270), ";
+ switch(options.label.position) {
+ case "right":
+ s += "translate(-" + (w(d) + self.domainHorizontalLabelWidth) + " , " + w(d) + ")";
+ break;
+ case "left":
+ s += "translate(-" + (self.domainHorizontalLabelWidth) + " , " + self.domainHorizontalLabelWidth + ")";
+ break;
+ }
+
+ return s;
+ });
+ break;
+ }
+ }
+
+ // =========================================================================//
+ // PAINTING DOMAIN SUBDOMAIN CONTENT //
+ // =========================================================================//
+ if (options.subDomainTextFormat !== null) {
+ rect
+ .append("text")
+ .attr("class", function(d) { return "subdomain-text" + self.getHighlightClassName(d.t); })
+ .attr("x", function(d) { return self.positionSubDomainX(d.t) + options.cellSize/2; })
+ .attr("y", function(d) { return self.positionSubDomainY(d.t) + options.cellSize/2; })
+ .attr("text-anchor", "middle")
+ .attr("dominant-baseline", "central")
+ .text(function(d){
+ return self.formatDate(new Date(d.t), options.subDomainTextFormat);
+ })
+ ;
+ }
+
+ // =========================================================================//
+ // ANIMATION //
+ // =========================================================================//
+
+ if (navigationDir !== false) {
+ domainSvg.transition().duration(options.animationDuration)
+ .attr("x", function(d){
+ return options.verticalOrientation ? 0: self.domainPosition.getPosition(d);
+ })
+ .attr("y", function(d){
+ return options.verticalOrientation? self.domainPosition.getPosition(d): 0;
+ })
+ ;
+ }
+
+ var tempWidth = self.graphDim.width;
+ var tempHeight = self.graphDim.height;
+
+ if (options.verticalOrientation) {
+ self.graphDim.height += enteringDomainDim - exitingDomainDim;
+ } else {
+ self.graphDim.width += enteringDomainDim - exitingDomainDim;
+ }
+
+ // At the time of exit, domainsWidth and domainsHeight already automatically shifted
+ domainSvg.exit().transition().duration(options.animationDuration)
+ .attr("x", function(d){
+ if (options.verticalOrientation) {
+ return 0;
+ } else {
+ switch(navigationDir) {
+ case self.NAVIGATE_LEFT:
+ return Math.min(self.graphDim.width, tempWidth);
+ case self.NAVIGATE_RIGHT:
+ return -w(d, true);
+ }
+ }
+ })
+ .attr("y", function(d){
+ if (options.verticalOrientation) {
+ switch(navigationDir) {
+ case self.NAVIGATE_LEFT:
+ return Math.min(self.graphDim.height, tempHeight);
+ case self.NAVIGATE_RIGHT:
+ return -h(d, true);
+ }
+ } else {
+ return 0;
+ }
+ })
+ .remove()
+ ;
+
+ // Resize the root container
+ self.resize();
+ };
+};
+
+CalHeatMap.prototype = {
+
+ /**
+ * Validate and merge user settings with default settings
+ *
+ * @param {object} settings User settings
+ * @return {bool} False if settings contains error
+ */
+ /* jshint maxstatements:false */
+ init: function(settings) {
+ "use strict";
+
+ var parent = this;
+
+ var options = parent.options = mergeRecursive(parent.options, settings);
+
+ // Fatal errors
+ // Stop script execution on error
+ validateDomainType();
+ validateSelector(options.itemSelector, false, "itemSelector");
+
+ if (parent.allowedDataType.indexOf(options.dataType) === -1) {
+ throw new Error("The data type '" + options.dataType + "' is not valid data type");
+ }
+
+ if (d3.select(options.itemSelector)[0][0] === null) {
+ throw new Error("The node '" + options.itemSelector + "' specified in itemSelector does not exists");
+ }
+
+ try {
+ validateSelector(options.nextSelector, true, "nextSelector");
+ validateSelector(options.previousSelector, true, "previousSelector");
+ } catch(error) {
+ console.log(error.message);
+ return false;
+ }
+
+ // If other settings contains error, will fallback to default
+
+ if (!settings.hasOwnProperty("subDomain")) {
+ this.options.subDomain = getOptimalSubDomain(settings.domain);
+ }
+
+ if (typeof options.itemNamespace !== "string" || options.itemNamespace === "") {
+ console.log("itemNamespace can not be empty, falling back to cal-heatmap");
+ options.itemNamespace = "cal-heatmap";
+ }
+
+ // Don't touch these settings
+ var s = ["data", "onComplete", "onClick", "afterLoad", "afterLoadData", "afterLoadPreviousDomain", "afterLoadNextDomain"];
+
+ for (var k in s) {
+ if (settings.hasOwnProperty(s[k])) {
+ options[s[k]] = settings[s[k]];
+ }
+ }
+
+ options.subDomainDateFormat = (typeof options.subDomainDateFormat === "string" || typeof options.subDomainDateFormat === "function" ? options.subDomainDateFormat : this._domainType[options.subDomain].format.date);
+ options.domainLabelFormat = (typeof options.domainLabelFormat === "string" || typeof options.domainLabelFormat === "function" ? options.domainLabelFormat : this._domainType[options.domain].format.legend);
+ options.subDomainTextFormat = ((typeof options.subDomainTextFormat === "string" && options.subDomainTextFormat !== "") || typeof options.subDomainTextFormat === "function" ? options.subDomainTextFormat : null);
+ options.domainMargin = expandMarginSetting(options.domainMargin);
+ options.legendMargin = expandMarginSetting(options.legendMargin);
+ options.highlight = parent.expandDateSetting(options.highlight);
+ options.itemName = expandItemName(options.itemName);
+ options.colLimit = parseColLimit(options.colLimit);
+ options.rowLimit = parseRowLimit(options.rowLimit);
+ if (!settings.hasOwnProperty("legendMargin")) {
+ autoAddLegendMargin();
+ }
+ autoAlignLabel();
+
+ /**
+ * Validate that a queryString is valid
+ *
+ * @param {Element|string|bool} selector The queryString to test
+ * @param {bool} canBeFalse Whether false is an accepted and valid value
+ * @param {string} name Name of the tested selector
+ * @throws {Error} If the selector is not valid
+ * @return {bool} True if the selector is a valid queryString
+ */
+ function validateSelector(selector, canBeFalse, name) {
+ if (((canBeFalse && selector === false) || selector instanceof Element || typeof selector === "string") && selector !== "") {
+ return true;
+ }
+ throw new Error("The " + name + " is not valid");
+ }
+
+ /**
+ * Return the optimal subDomain for the specified domain
+ *
+ * @param {string} domain a domain name
+ * @return {string} the subDomain name
+ */
+ function getOptimalSubDomain(domain) {
+ switch(domain) {
+ case "year":
+ return "month";
+ case "month":
+ return "day";
+ case "week":
+ return "day";
+ case "day":
+ return "hour";
+ default:
+ return "min";
+ }
+ }
+
+ /**
+ * Ensure that the domain and subdomain are valid
+ *
+ * @throw {Error} when domain or subdomain are not valid
+ * @return {bool} True if domain and subdomain are valid and compatible
+ */
+ function validateDomainType() {
+ if (!parent._domainType.hasOwnProperty(options.domain) || options.domain === "min" || options.domain.substring(0, 2) === "x_") {
+ throw new Error("The domain '" + options.domain + "' is not valid");
+ }
+
+ if (!parent._domainType.hasOwnProperty(options.subDomain) || options.subDomain === "year") {
+ throw new Error("The subDomain '" + options.subDomain + "' is not valid");
+ }
+
+ if (parent._domainType[options.domain].level <= parent._domainType[options.subDomain].level) {
+ throw new Error("'" + options.subDomain + "' is not a valid subDomain to '" + options.domain + "'");
+ }
+
+ return true;
+ }
+
+ /**
+ * Fine-tune the label alignement depending on its position
+ *
+ * @return void
+ */
+ function autoAlignLabel() {
+ // Auto-align label, depending on it's position
+ if (!settings.hasOwnProperty("label") || (settings.hasOwnProperty("label") && !settings.label.hasOwnProperty("align"))) {
+ switch(options.label.position) {
+ case "left":
+ options.label.align = "right";
+ break;
+ case "right":
+ options.label.align = "left";
+ break;
+ default:
+ options.label.align = "center";
+ }
+
+ if (options.label.rotate === "left") {
+ options.label.align = "right";
+ } else if (options.label.rotate === "right") {
+ options.label.align = "left";
+ }
+ }
+
+ if (!settings.hasOwnProperty("label") || (settings.hasOwnProperty("label") && !settings.label.hasOwnProperty("offset"))) {
+ if (options.label.position === "left" || options.label.position === "right") {
+ options.label.offset = {
+ x: 10,
+ y: 15
+ };
+ }
+ }
+ }
+
+ /**
+ * If not specified, add some margin around the legend depending on its position
+ *
+ * @return void
+ */
+ function autoAddLegendMargin() {
+ switch(options.legendVerticalPosition) {
+ case "top":
+ options.legendMargin[2] = parent.DEFAULT_LEGEND_MARGIN;
+ break;
+ case "bottom":
+ options.legendMargin[0] = parent.DEFAULT_LEGEND_MARGIN;
+ break;
+ case "middle":
+ case "center":
+ options.legendMargin[options.legendHorizontalPosition === "right" ? 3 : 1] = parent.DEFAULT_LEGEND_MARGIN;
+ }
+ }
+
+ /**
+ * Expand a number of an array of numbers to an usable 4 values array
+ *
+ * @param {integer|array} value
+ * @return {array} array
+ */
+ function expandMarginSetting(value) {
+ if (typeof value === "number") {
+ value = [value];
+ }
+
+ if (!Array.isArray(value)) {
+ console.log("Margin only takes an integer or an array of integers");
+ value = [0];
+ }
+
+ switch(value.length) {
+ case 1:
+ return [value[0], value[0], value[0], value[0]];
+ case 2:
+ return [value[0], value[1], value[0], value[1]];
+ case 3:
+ return [value[0], value[1], value[2], value[1]];
+ case 4:
+ return value;
+ default:
+ return value.slice(0, 4);
+ }
+ }
+
+ /**
+ * Convert a string to an array like [singular-form, plural-form]
+ *
+ * @param {string|array} value Date to convert
+ * @return {array} An array like [singular-form, plural-form]
+ */
+ function expandItemName(value) {
+ if (typeof value === "string") {
+ return [value, value + (value !== "" ? "s" : "")];
+ }
+
+ if (Array.isArray(value)) {
+ if (value.length === 1) {
+ return [value[0], value[0] + "s"];
+ } else if (value.length > 2) {
+ return value.slice(0, 2);
+ }
+
+ return value;
+ }
+
+ return ["item", "items"];
+ }
+
+ function parseColLimit(value) {
+ return value > 0 ? value : null;
+ }
+
+ function parseRowLimit(value) {
+ if (value > 0 && options.colLimit > 0) {
+ console.log("colLimit and rowLimit are mutually exclusive, rowLimit will be ignored");
+ return null;
+ }
+ return value > 0 ? value : null;
+ }
+
+ return this._init();
+
+ },
+
+ /**
+ * Convert a keyword or an array of keyword/date to an array of date objects
+ *
+ * @param {string|array|Date} value Data to convert
+ * @return {array} An array of Dates
+ */
+ expandDateSetting: function(value) {
+ "use strict";
+
+ if (!Array.isArray(value)) {
+ value = [value];
+ }
+
+ return value.map(function(data) {
+ if (data === "now") {
+ return new Date();
+ }
+ if (data instanceof Date) {
+ return data;
+ }
+ return false;
+ }).filter(function(d) { return d !== false; });
+ },
+
+ /**
+ * Fill the calendar by coloring the cells
+ *
+ * @param array svg An array of html node to apply the transformation to (optional)
+ * It's used to limit the painting to only a subset of the calendar
+ * @return void
+ */
+ fill: function(svg) {
+ "use strict";
+
+ var parent = this;
+ var options = parent.options;
+
+ if (arguments.length === 0) {
+ svg = parent.root.selectAll(".graph-domain");
+ }
+
+ var rect = svg
+ .selectAll("svg").selectAll("g")
+ .data(function(d) { return parent._domains.get(d); })
+ ;
+
+ /**
+ * Colorize the cell via a style attribute if enabled
+ */
+ function addStyle(element) {
+ if (parent.legendScale === null) {
+ return false;
+ }
+
+ element.attr("fill", function(d) {
+ if (d.v === null && (options.hasOwnProperty("considerMissingDataAsZero") && !options.considerMissingDataAsZero)) {
+ if (options.legendColors.hasOwnProperty("base")) {
+ return options.legendColors.base;
+ }
+ }
+
+ if (options.legendColors !== null && options.legendColors.hasOwnProperty("empty") &&
+ (d.v === 0 || (d.v === null && options.hasOwnProperty("considerMissingDataAsZero") && options.considerMissingDataAsZero))
+ ) {
+ return options.legendColors.empty;
+ }
+
+ if (d.v < 0 && options.legend[0] > 0 && options.legendColors !== null && options.legendColors.hasOwnProperty("overflow")) {
+ return options.legendColors.overflow;
+ }
+
+ return parent.legendScale(Math.min(d.v, options.legend[options.legend.length-1]));
+ });
+ }
+
+ rect.transition().duration(options.animationDuration).select("rect")
+ .attr("class", function(d) {
+
+ var htmlClass = parent.getHighlightClassName(d.t).trim().split(" ");
+ var pastDate = parent.dateIsLessThan(d.t, new Date());
+ var sameDate = parent.dateIsEqual(d.t, new Date());
+
+ if (parent.legendScale === null ||
+ (d.v === null && (options.hasOwnProperty("considerMissingDataAsZero") && !options.considerMissingDataAsZero) &&!options.legendColors.hasOwnProperty("base"))
+ ) {
+ htmlClass.push("graph-rect");
+ }
+
+ if (sameDate) {
+ htmlClass.push("now");
+ } else if (!pastDate) {
+ htmlClass.push("future");
+ }
+
+ if (d.v !== null) {
+ htmlClass.push(parent.Legend.getClass(d.v, (parent.legendScale === null)));
+ } else if (options.considerMissingDataAsZero && pastDate) {
+ htmlClass.push(parent.Legend.getClass(0, (parent.legendScale === null)));
+ }
+
+ if (options.onClick !== null) {
+ htmlClass.push("hover_cursor");
+ }
+
+ return htmlClass.join(" ");
+ })
+ .call(addStyle)
+ ;
+
+ rect.transition().duration(options.animationDuration).select("title")
+ .text(function(d) { return parent.getSubDomainTitle(d); })
+ ;
+
+ function formatSubDomainText(element) {
+ if (typeof options.subDomainTextFormat === "function") {
+ element.text(function(d) { return options.subDomainTextFormat(d.t, d.v); });
+ }
+ }
+
+ /**
+ * Change the subDomainText class if necessary
+ * Also change the text, e.g when text is representing the value
+ * instead of the date
+ */
+ rect.transition().duration(options.animationDuration).select("text")
+ .attr("class", function(d) { return "subdomain-text" + parent.getHighlightClassName(d.t); })
+ .call(formatSubDomainText)
+ ;
+ },
+
+ // =========================================================================//
+ // EVENTS CALLBACK //
+ // =========================================================================//
+
+ /**
+ * Helper method for triggering event callback
+ *
+ * @param string eventName Name of the event to trigger
+ * @param array successArgs List of argument to pass to the callback
+ * @param boolean skip Whether to skip the event triggering
+ * @return mixed True when the triggering was skipped, false on error, else the callback function
+ */
+ triggerEvent: function(eventName, successArgs, skip) {
+ "use strict";
+
+ if ((arguments.length === 3 && skip) || this.options[eventName] === null) {
+ return true;
+ }
+
+ if (typeof this.options[eventName] === "function") {
+ if (typeof successArgs === "function") {
+ successArgs = successArgs();
+ }
+ return this.options[eventName].apply(this, successArgs);
+ } else {
+ console.log("Provided callback for " + eventName + " is not a function.");
+ return false;
+ }
+ },
+
+ /**
+ * Event triggered on a mouse click on a subDomain cell
+ *
+ * @param Date d Date of the subdomain block
+ * @param int itemNb Number of items in that date
+ */
+ onClick: function(d, itemNb) {
+ "use strict";
+
+ return this.triggerEvent("onClick", [d, itemNb]);
+ },
+
+ /**
+ * Event triggered after drawing the calendar, byt before filling it with data
+ */
+ afterLoad: function() {
+ "use strict";
+
+ return this.triggerEvent("afterLoad");
+ },
+
+ /**
+ * Event triggered after completing drawing and filling the calendar
+ */
+ onComplete: function() {
+ "use strict";
+
+ var response = this.triggerEvent("onComplete", [], this._completed);
+ this._completed = true;
+ return response;
+ },
+
+ /**
+ * Event triggered after shifting the calendar one domain back
+ *
+ * @param Date start Domain start date
+ * @param Date end Domain end date
+ */
+ afterLoadPreviousDomain: function(start) {
+ "use strict";
+
+ var parent = this;
+ return this.triggerEvent("afterLoadPreviousDomain", function() {
+ var subDomain = parent.getSubDomain(start);
+ return [subDomain.shift(), subDomain.pop()];
+ });
+ },
+
+ /**
+ * Event triggered after shifting the calendar one domain above
+ *
+ * @param Date start Domain start date
+ * @param Date end Domain end date
+ */
+ afterLoadNextDomain: function(start) {
+ "use strict";
+
+ var parent = this;
+ return this.triggerEvent("afterLoadNextDomain", function() {
+ var subDomain = parent.getSubDomain(start);
+ return [subDomain.shift(), subDomain.pop()];
+ });
+ },
+
+ /**
+ * Event triggered after loading the leftmost domain allowed by minDate
+ *
+ * @param boolean reached True if the leftmost domain was reached
+ */
+ onMinDomainReached: function(reached) {
+ "use strict";
+
+ this._minDomainReached = reached;
+ return this.triggerEvent("onMinDomainReached", [reached]);
+ },
+
+ /**
+ * Event triggered after loading the rightmost domain allowed by maxDate
+ *
+ * @param boolean reached True if the rightmost domain was reached
+ */
+ onMaxDomainReached: function(reached) {
+ "use strict";
+
+ this._maxDomainReached = reached;
+ return this.triggerEvent("onMaxDomainReached", [reached]);
+ },
+
+ checkIfMinDomainIsReached: function(date, upperBound) {
+ "use strict";
+
+ if (this.minDomainIsReached(date)) {
+ this.onMinDomainReached(true);
+ }
+
+ if (arguments.length === 2) {
+ if (this._maxDomainReached && !this.maxDomainIsReached(upperBound)) {
+ this.onMaxDomainReached(false);
+ }
+ }
+ },
+
+ checkIfMaxDomainIsReached: function(date, lowerBound) {
+ "use strict";
+
+ if (this.maxDomainIsReached(date)) {
+ this.onMaxDomainReached(true);
+ }
+
+ if (arguments.length === 2) {
+ if (this._minDomainReached && !this.minDomainIsReached(lowerBound)) {
+ this.onMinDomainReached(false);
+ }
+ }
+ },
+
+ // =========================================================================//
+ // FORMATTER //
+ // =========================================================================//
+
+ formatNumber: d3.format(",g"),
+
+ formatDate: function(d, format) {
+ "use strict";
+
+ if (arguments.length < 2) {
+ format = "title";
+ }
+
+ if (typeof format === "function") {
+ return format(d);
+ } else {
+ var f = d3.time.format(format);
+ return f(d);
+ }
+ },
+
+ getSubDomainTitle: function(d) {
+ "use strict";
+
+ if (d.v === null && !this.options.considerMissingDataAsZero) {
+ return (this.options.subDomainTitleFormat.empty).format({
+ date: this.formatDate(new Date(d.t), this.options.subDomainDateFormat)
+ });
+ } else {
+ var value = d.v;
+ // Consider null as 0
+ if (value === null && this.options.considerMissingDataAsZero) {
+ value = 0;
+ }
+
+ return (this.options.subDomainTitleFormat.filled).format({
+ count: this.formatNumber(value),
+ name: this.options.itemName[(value !== 1 ? 1: 0)],
+ connector: this._domainType[this.options.subDomain].format.connector,
+ date: this.formatDate(new Date(d.t), this.options.subDomainDateFormat)
+ });
+ }
+ },
+
+ // =========================================================================//
+ // DOMAIN NAVIGATION //
+ // =========================================================================//
+
+ /**
+ * Shift the calendar one domain forward
+ *
+ * The new domain is loaded only if it's not beyond maxDate
+ *
+ * @param int n Number of domains to load
+ * @return bool True if the next domain was loaded, else false
+ */
+ loadNextDomain: function(n) {
+ "use strict";
+
+ if (this._maxDomainReached || n === 0) {
+ return false;
+ }
+
+ var bound = this.loadNewDomains(this.NAVIGATE_RIGHT, this.getDomain(this.getNextDomain(), n));
+
+ this.afterLoadNextDomain(bound.end);
+ this.checkIfMaxDomainIsReached(this.getNextDomain().getTime(), bound.start);
+
+ return true;
+ },
+
+ /**
+ * Shift the calendar one domain backward
+ *
+ * The previous domain is loaded only if it's not beyond the minDate
+ *
+ * @param int n Number of domains to load
+ * @return bool True if the previous domain was loaded, else false
+ */
+ loadPreviousDomain: function(n) {
+ "use strict";
+
+ if (this._minDomainReached || n === 0) {
+ return false;
+ }
+
+ var bound = this.loadNewDomains(this.NAVIGATE_LEFT, this.getDomain(this.getDomainKeys()[0], -n).reverse());
+
+ this.afterLoadPreviousDomain(bound.start);
+ this.checkIfMinDomainIsReached(bound.start, bound.end);
+
+ return true;
+ },
+
+ loadNewDomains: function(direction, newDomains) {
+ "use strict";
+
+ var parent = this;
+ var backward = direction === this.NAVIGATE_LEFT;
+ var i = -1;
+ var total = newDomains.length;
+ var domains = this.getDomainKeys();
+
+ function buildSubDomain(d) {
+ return {t: parent._domainType[parent.options.subDomain].extractUnit(d), v: null};
+ }
+
+ // Remove out of bound domains from list of new domains to prepend
+ while (++i < total) {
+ if (backward && this.minDomainIsReached(newDomains[i])) {
+ newDomains = newDomains.slice(0, i+1);
+ break;
+ }
+ if (!backward && this.maxDomainIsReached(newDomains[i])) {
+ newDomains = newDomains.slice(0, i);
+ break;
+ }
+ }
+
+ newDomains = newDomains.slice(-this.options.range);
+
+ for (i = 0, total = newDomains.length; i < total; i++) {
+ this._domains.set(
+ newDomains[i].getTime(),
+ this.getSubDomain(newDomains[i]).map(buildSubDomain)
+ );
+
+ this._domains.remove(backward ? domains.pop() : domains.shift());
+ }
+
+ domains = this.getDomainKeys();
+
+ if (backward) {
+ newDomains = newDomains.reverse();
+ }
+
+ this.paint(direction);
+
+ this.getDatas(
+ this.options.data,
+ newDomains[0],
+ this.getSubDomain(newDomains[newDomains.length-1]).pop(),
+ function() {
+ parent.fill(parent.lastInsertedSvg);
+ }
+ );
+
+ return {
+ start: newDomains[backward ? 0 : 1],
+ end: domains[domains.length-1]
+ };
+ },
+
+ /**
+ * Return whether a date is inside the scope determined by maxDate
+ *
+ * @param int datetimestamp The timestamp in ms to test
+ * @return bool True if the specified date correspond to the calendar upper bound
+ */
+ maxDomainIsReached: function(datetimestamp) {
+ "use strict";
+
+ return (this.options.maxDate !== null && (this.options.maxDate.getTime() < datetimestamp));
+ },
+
+ /**
+ * Return whether a date is inside the scope determined by minDate
+ *
+ * @param int datetimestamp The timestamp in ms to test
+ * @return bool True if the specified date correspond to the calendar lower bound
+ */
+ minDomainIsReached: function (datetimestamp) {
+ "use strict";
+
+ return (this.options.minDate !== null && (this.options.minDate.getTime() >= datetimestamp));
+ },
+
+ /**
+ * Return the list of the calendar's domain timestamp
+ *
+ * @return Array a sorted array of timestamp
+ */
+ getDomainKeys: function() {
+ "use strict";
+
+ return this._domains.keys()
+ .map(function(d) { return parseInt(d, 10); })
+ .sort(function(a,b) { return a-b; });
+ },
+
+ // =========================================================================//
+ // POSITIONNING //
+ // =========================================================================//
+
+ positionSubDomainX: function(d) {
+ "use strict";
+
+ var index = this._domainType[this.options.subDomain].position.x(new Date(d));
+ return index * this.options.cellSize + index * this.options.cellPadding;
+ },
+
+ positionSubDomainY: function(d) {
+ "use strict";
+
+ var index = this._domainType[this.options.subDomain].position.y(new Date(d));
+ return index * this.options.cellSize + index * this.options.cellPadding;
+ },
+
+ getSubDomainColumnNumber: function(d) {
+ "use strict";
+
+ if (this.options.rowLimit > 0) {
+ var i = this._domainType[this.options.subDomain].maxItemNumber;
+ if (typeof i === "function") {
+ i = i(d);
+ }
+ return Math.ceil(i / this.options.rowLimit);
+ }
+
+ var j = this._domainType[this.options.subDomain].defaultColumnNumber;
+ if (typeof j === "function") {
+ j = j(d);
+
+ }
+ return this.options.colLimit || j;
+ },
+
+ getSubDomainRowNumber: function(d) {
+ "use strict";
+
+ if (this.options.colLimit > 0) {
+ var i = this._domainType[this.options.subDomain].maxItemNumber;
+ if (typeof i === "function") {
+ i = i(d);
+ }
+ return Math.ceil(i / this.options.colLimit);
+ }
+
+ var j = this._domainType[this.options.subDomain].defaultRowNumber;
+ if (typeof j === "function") {
+ j = j(d);
+
+ }
+ return this.options.rowLimit || j;
+ },
+
+ /**
+ * Return a classname if the specified date should be highlighted
+ *
+ * @param timestamp date Date of the current subDomain
+ * @return String the highlight class
+ */
+ getHighlightClassName: function(d) {
+ "use strict";
+
+ d = new Date(d);
+
+ if (this.options.highlight.length > 0) {
+ for (var i in this.options.highlight) {
+ if (this.dateIsEqual(this.options.highlight[i], d)) {
+ return this.isNow(this.options.highlight[i]) ? " highlight-now": " highlight";
+ }
+ }
+ }
+ return "";
+ },
+
+ /**
+ * Return whether the specified date is now,
+ * according to the type of subdomain
+ *
+ * @param Date d The date to compare
+ * @return bool True if the date correspond to a subdomain cell
+ */
+ isNow: function(d) {
+ "use strict";
+
+ return this.dateIsEqual(d, new Date());
+ },
+
+ /**
+ * Return whether 2 dates are equals
+ * This function is subdomain-aware,
+ * and dates comparison are dependent of the subdomain
+ *
+ * @param Date dateA First date to compare
+ * @param Date dateB Secon date to compare
+ * @return bool true if the 2 dates are equals
+ */
+ /* jshint maxcomplexity: false */
+ dateIsEqual: function(dateA, dateB) {
+ "use strict";
+
+ if(!(dateA instanceof Date)) {
+ dateA = new Date(dateA);
+ }
+
+ if (!(dateB instanceof Date)) {
+ dateB = new Date(dateB);
+ }
+
+ switch(this.options.subDomain) {
+ case "x_min":
+ case "min":
+ return dateA.getFullYear() === dateB.getFullYear() &&
+ dateA.getMonth() === dateB.getMonth() &&
+ dateA.getDate() === dateB.getDate() &&
+ dateA.getHours() === dateB.getHours() &&
+ dateA.getMinutes() === dateB.getMinutes();
+ case "x_hour":
+ case "hour":
+ return dateA.getFullYear() === dateB.getFullYear() &&
+ dateA.getMonth() === dateB.getMonth() &&
+ dateA.getDate() === dateB.getDate() &&
+ dateA.getHours() === dateB.getHours();
+ case "x_day":
+ case "day":
+ return dateA.getFullYear() === dateB.getFullYear() &&
+ dateA.getMonth() === dateB.getMonth() &&
+ dateA.getDate() === dateB.getDate();
+ case "x_week":
+ case "week":
+ return dateA.getFullYear() === dateB.getFullYear() &&
+ this.getWeekNumber(dateA) === this.getWeekNumber(dateB);
+ case "x_month":
+ case "month":
+ return dateA.getFullYear() === dateB.getFullYear() &&
+ dateA.getMonth() === dateB.getMonth();
+ default:
+ return false;
+ }
+ },
+
+
+ /**
+ * Returns wether or not dateA is less than or equal to dateB. This function is subdomain aware.
+ * Performs automatic conversion of values.
+ * @param dateA may be a number or a Date
+ * @param dateB may be a number or a Date
+ * @returns {boolean}
+ */
+ dateIsLessThan: function(dateA, dateB) {
+ "use strict";
+
+ if(!(dateA instanceof Date)) {
+ dateA = new Date(dateA);
+ }
+
+ if (!(dateB instanceof Date)) {
+ dateB = new Date(dateB);
+ }
+
+
+ function normalizedMillis(date, subdomain) {
+ switch(subdomain) {
+ case "x_min":
+ case "min":
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes()).getTime();
+ case "x_hour":
+ case "hour":
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours()).getTime();
+ case "x_day":
+ case "day":
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
+ case "x_week":
+ case "week":
+ case "x_month":
+ case "month":
+ return new Date(date.getFullYear(), date.getMonth()).getTime();
+ default:
+ return date.getTime();
+ }
+ }
+
+ return normalizedMillis(dateA, this.options.subDomain) < normalizedMillis(dateB, this.options.subDomain);
+ },
+
+
+ // =========================================================================//
+ // DATE COMPUTATION //
+ // =========================================================================//
+
+ /**
+ * Return the day of the year for the date
+ * @param Date
+ * @return int Day of the year [1,366]
+ */
+ getDayOfYear: d3.time.format("%j"),
+
+ /**
+ * Return the week number of the year
+ * Monday as the first day of the week
+ * @return int Week number [0-53]
+ */
+ getWeekNumber: function(d) {
+ "use strict";
+
+ var f = this.options.weekStartOnMonday === true ? d3.time.format("%W"): d3.time.format("%U");
+ return f(d);
+ },
+
+ /**
+ * Return the week number, relative to its month
+ *
+ * @param int|Date d Date or timestamp in milliseconds
+ * @return int Week number, relative to the month [0-5]
+ */
+ getMonthWeekNumber: function (d) {
+ "use strict";
+
+ if (typeof d === "number") {
+ d = new Date(d);
+ }
+
+ var monthFirstWeekNumber = this.getWeekNumber(new Date(d.getFullYear(), d.getMonth()));
+ return this.getWeekNumber(d) - monthFirstWeekNumber - 1;
+ },
+
+ /**
+ * Return the number of weeks in the dates' year
+ *
+ * @param int|Date d Date or timestamp in milliseconds
+ * @return int Number of weeks in the date's year
+ */
+ getWeekNumberInYear: function(d) {
+ "use strict";
+
+ if (typeof d === "number") {
+ d = new Date(d);
+ }
+ },
+
+ /**
+ * Return the number of days in the date's month
+ *
+ * @param int|Date d Date or timestamp in milliseconds
+ * @return int Number of days in the date's month
+ */
+ getDayCountInMonth: function(d) {
+ "use strict";
+
+ return this.getEndOfMonth(d).getDate();
+ },
+
+ /**
+ * Return the number of days in the date's year
+ *
+ * @param int|Date d Date or timestamp in milliseconds
+ * @return int Number of days in the date's year
+ */
+ getDayCountInYear: function(d) {
+ "use strict";
+
+ if (typeof d === "number") {
+ d = new Date(d);
+ }
+ return (new Date(d.getFullYear(), 1, 29).getMonth() === 1) ? 366 : 365;
+ },
+
+ /**
+ * Get the weekday from a date
+ *
+ * Return the week day number (0-6) of a date,
+ * depending on whether the week start on monday or sunday
+ *
+ * @param Date d
+ * @return int The week day number (0-6)
+ */
+ getWeekDay: function(d) {
+ "use strict";
+
+ if (this.options.weekStartOnMonday === false) {
+ return d.getDay();
+ }
+ return d.getDay() === 0 ? 6 : (d.getDay()-1);
+ },
+
+ /**
+ * Get the last day of the month
+ * @param Date|int d Date or timestamp in milliseconds
+ * @return Date Last day of the month
+ */
+ getEndOfMonth: function(d) {
+ "use strict";
+
+ if (typeof d === "number") {
+ d = new Date(d);
+ }
+ return new Date(d.getFullYear(), d.getMonth()+1, 0);
+ },
+
+ /**
+ *
+ * @param Date date
+ * @param int count
+ * @param string step
+ * @return Date
+ */
+ jumpDate: function(date, count, step) {
+ "use strict";
+
+ var d = new Date(date);
+ switch(step) {
+ case "hour":
+ d.setHours(d.getHours() + count);
+ break;
+ case "day":
+ d.setHours(d.getHours() + count * 24);
+ break;
+ case "week":
+ d.setHours(d.getHours() + count * 24 * 7);
+ break;
+ case "month":
+ d.setMonth(d.getMonth() + count);
+ break;
+ case "year":
+ d.setFullYear(d.getFullYear() + count);
+ }
+
+ return new Date(d);
+ },
+
+ // =========================================================================//
+ // DOMAIN COMPUTATION //
+ // =========================================================================//
+
+ /**
+ * Return all the minutes between 2 dates
+ *
+ * @param Date d date A date
+ * @param int|date range Number of minutes in the range, or a stop date
+ * @return array An array of minutes
+ */
+ getMinuteDomain: function (d, range) {
+ "use strict";
+
+ var start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours());
+ var stop = null;
+ if (range instanceof Date) {
+ stop = new Date(range.getFullYear(), range.getMonth(), range.getDate(), range.getHours());
+ } else {
+ stop = new Date(+start + range * 1000 * 60);
+ }
+ return d3.time.minutes(Math.min(start, stop), Math.max(start, stop));
+ },
+
+ /**
+ * Return all the hours between 2 dates
+ *
+ * @param Date d A date
+ * @param int|date range Number of hours in the range, or a stop date
+ * @return array An array of hours
+ */
+ getHourDomain: function (d, range) {
+ "use strict";
+
+ var start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours());
+ var stop = null;
+ if (range instanceof Date) {
+ stop = new Date(range.getFullYear(), range.getMonth(), range.getDate(), range.getHours());
+ } else {
+ stop = new Date(start);
+ stop.setHours(stop.getHours() + range);
+ }
+
+ var domains = d3.time.hours(Math.min(start, stop), Math.max(start, stop));
+
+ // Passing from DST to standard time
+ // If there are 25 hours, let's compress the duplicate hours
+ var i = 0;
+ var total = domains.length;
+ for(i = 0; i < total; i++) {
+ if (i > 0 && (domains[i].getHours() === domains[i-1].getHours())) {
+ this.DSTDomain.push(domains[i].getTime());
+ domains.splice(i, 1);
+ break;
+ }
+ }
+
+ // d3.time.hours is returning more hours than needed when changing
+ // from DST to standard time, because there is really 2 hours between
+ // 1am and 2am!
+ if (typeof range === "number" && domains.length > Math.abs(range)) {
+ domains.splice(domains.length-1, 1);
+ }
+
+ return domains;
+ },
+
+ /**
+ * Return all the days between 2 dates
+ *
+ * @param Date d A date
+ * @param int|date range Number of days in the range, or a stop date
+ * @return array An array of weeks
+ */
+ getDayDomain: function (d, range) {
+ "use strict";
+
+ var start = new Date(d.getFullYear(), d.getMonth(), d.getDate());
+ var stop = null;
+ if (range instanceof Date) {
+ stop = new Date(range.getFullYear(), range.getMonth(), range.getDate());
+ } else {
+ stop = new Date(start);
+ stop = new Date(stop.setDate(stop.getDate() + parseInt(range, 10)));
+ }
+
+ return d3.time.days(Math.min(start, stop), Math.max(start, stop));
+ },
+
+ /**
+ * Return all the weeks between 2 dates
+ *
+ * @param Date d A date
+ * @param int|date range Number of minutes in the range, or a stop date
+ * @return array An array of weeks
+ */
+ getWeekDomain: function (d, range) {
+ "use strict";
+
+ var weekStart;
+
+ if (this.options.weekStartOnMonday === false) {
+ weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate() - d.getDay());
+ } else {
+ if (d.getDay() === 1) {
+ weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
+ } else if (d.getDay() === 0) {
+ weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
+ weekStart.setDate(weekStart.getDate() - 6);
+ } else {
+ weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()-d.getDay()+1);
+ }
+ }
+
+ var endDate = new Date(weekStart);
+
+ var stop = range;
+ if (typeof range !== "object") {
+ stop = new Date(endDate.setDate(endDate.getDate() + range * 7));
+ }
+
+ return (this.options.weekStartOnMonday === true) ?
+ d3.time.mondays(Math.min(weekStart, stop), Math.max(weekStart, stop)):
+ d3.time.sundays(Math.min(weekStart, stop), Math.max(weekStart, stop))
+ ;
+ },
+
+ /**
+ * Return all the months between 2 dates
+ *
+ * @param Date d A date
+ * @param int|date range Number of months in the range, or a stop date
+ * @return array An array of months
+ */
+ getMonthDomain: function (d, range) {
+ "use strict";
+
+ var start = new Date(d.getFullYear(), d.getMonth());
+ var stop = null;
+ if (range instanceof Date) {
+ stop = new Date(range.getFullYear(), range.getMonth());
+ } else {
+ stop = new Date(start);
+ stop = stop.setMonth(stop.getMonth()+range);
+ }
+
+ return d3.time.months(Math.min(start, stop), Math.max(start, stop));
+ },
+
+ /**
+ * Return all the years between 2 dates
+ *
+ * @param Date d date A date
+ * @param int|date range Number of minutes in the range, or a stop date
+ * @return array An array of hours
+ */
+ getYearDomain: function(d, range){
+ "use strict";
+
+ var start = new Date(d.getFullYear(), 0);
+ var stop = null;
+ if (range instanceof Date) {
+ stop = new Date(range.getFullYear(), 0);
+ } else {
+ stop = new Date(d.getFullYear()+range, 0);
+ }
+
+ return d3.time.years(Math.min(start, stop), Math.max(start, stop));
+ },
+
+ /**
+ * Get an array of domain start dates
+ *
+ * @param int|Date date A random date included in the wanted domain
+ * @param int|Date range Number of dates to get, or a stop date
+ * @return Array of dates
+ */
+ getDomain: function(date, range) {
+ "use strict";
+
+ if (typeof date === "number") {
+ date = new Date(date);
+ }
+
+ if (arguments.length < 2) {
+ range = this.options.range;
+ }
+
+ switch(this.options.domain) {
+ case "hour" :
+ var domains = this.getHourDomain(date, range);
+
+ // Case where an hour is missing, when passing from standard time to DST
+ // Missing hour is perfectly acceptabl in subDomain, but not in domains
+ if (typeof range === "number" && domains.length < range) {
+ if (range > 0) {
+ domains.push(this.getHourDomain(domains[domains.length-1], 2)[1]);
+ } else {
+ domains.shift(this.getHourDomain(domains[0], -2)[0]);
+ }
+ }
+ return domains;
+ case "day" :
+ return this.getDayDomain(date, range);
+ case "week" :
+ return this.getWeekDomain(date, range);
+ case "month":
+ return this.getMonthDomain(date, range);
+ case "year" :
+ return this.getYearDomain(date, range);
+ }
+ },
+
+ /* jshint maxcomplexity: false */
+ getSubDomain: function(date) {
+ "use strict";
+
+ if (typeof date === "number") {
+ date = new Date(date);
+ }
+
+ var parent = this;
+
+ /**
+ * @return int
+ */
+ var computeDaySubDomainSize = function(date, domain) {
+ switch(domain) {
+ case "year":
+ return parent.getDayCountInYear(date);
+ case "month":
+ return parent.getDayCountInMonth(date);
+ case "week":
+ return 7;
+ }
+ };
+
+ /**
+ * @return int
+ */
+ var computeMinSubDomainSize = function(date, domain) {
+ switch (domain) {
+ case "hour":
+ return 60;
+ case "day":
+ return 60 * 24;
+ case "week":
+ return 60 * 24 * 7;
+ }
+ };
+
+ /**
+ * @return int
+ */
+ var computeHourSubDomainSize = function(date, domain) {
+ switch(domain) {
+ case "day":
+ return 24;
+ case "week":
+ return 168;
+ case "month":
+ return parent.getDayCountInMonth(date) * 24;
+ }
+ };
+
+ /**
+ * @return int
+ */
+ var computeWeekSubDomainSize = function(date, domain) {
+ if (domain === "month") {
+ var endOfMonth = new Date(date.getFullYear(), date.getMonth()+1, 0);
+ var endWeekNb = parent.getWeekNumber(endOfMonth);
+ var startWeekNb = parent.getWeekNumber(new Date(date.getFullYear(), date.getMonth()));
+
+ if (startWeekNb > endWeekNb) {
+ startWeekNb = 0;
+ endWeekNb++;
+ }
+
+ return endWeekNb - startWeekNb + 1;
+ } else if (domain === "year") {
+ return parent.getWeekNumber(new Date(date.getFullYear(), 11, 31));
+ }
+ };
+
+ switch(this.options.subDomain) {
+ case "x_min":
+ case "min" :
+ return this.getMinuteDomain(date, computeMinSubDomainSize(date, this.options.domain));
+ case "x_hour":
+ case "hour" :
+ return this.getHourDomain(date, computeHourSubDomainSize(date, this.options.domain));
+ case "x_day":
+ case "day" :
+ return this.getDayDomain(date, computeDaySubDomainSize(date, this.options.domain));
+ case "x_week":
+ case "week" :
+ return this.getWeekDomain(date, computeWeekSubDomainSize(date, this.options.domain));
+ case "x_month":
+ case "month":
+ return this.getMonthDomain(date, 12);
+ }
+ },
+
+ /**
+ * Get the n-th next domain after the calendar newest (rightmost) domain
+ * @param int n
+ * @return Date The start date of the wanted domain
+ */
+ getNextDomain: function(n) {
+ "use strict";
+
+ if (arguments.length === 0) {
+ n = 1;
+ }
+ return this.getDomain(this.jumpDate(this.getDomainKeys().pop(), n, this.options.domain), 1)[0];
+ },
+
+ /**
+ * Get the n-th domain before the calendar oldest (leftmost) domain
+ * @param int n
+ * @return Date The start date of the wanted domain
+ */
+ getPreviousDomain: function(n) {
+ "use strict";
+
+ if (arguments.length === 0) {
+ n = 1;
+ }
+ return this.getDomain(this.jumpDate(this.getDomainKeys().shift(), -n, this.options.domain), 1)[0];
+ },
+
+
+ // =========================================================================//
+ // DATAS //
+ // =========================================================================//
+
+ /**
+ * Fetch and interpret data from the datasource
+ *
+ * @param string|object source
+ * @param Date startDate
+ * @param Date endDate
+ * @param function callback
+ * @param function|boolean afterLoad function used to convert the data into a json object. Use true to use the afterLoad callback
+ * @param updateMode
+ *
+ * @return mixed
+ * - True if there are no data to load
+ * - False if data are loaded asynchronously
+ */
+ getDatas: function(source, startDate, endDate, callback, afterLoad, updateMode) {
+ "use strict";
+
+ var self = this;
+ if (arguments.length < 5) {
+ afterLoad = true;
+ }
+ if (arguments.length < 6) {
+ updateMode = this.APPEND_ON_UPDATE;
+ }
+ var _callback = function(data) {
+ if (afterLoad !== false) {
+ if (typeof afterLoad === "function") {
+ data = afterLoad(data);
+ } else if (typeof (self.options.afterLoadData) === "function") {
+ data = self.options.afterLoadData(data);
+ } else {
+ console.log("Provided callback for afterLoadData is not a function.");
+ }
+ } else if (self.options.dataType === "csv" || self.options.dataType === "tsv") {
+ data = this.interpretCSV(data);
+ }
+ self.parseDatas(data, updateMode, startDate, endDate);
+ if (typeof callback === "function") {
+ callback();
+ }
+ };
+
+ switch(typeof source) {
+ case "string":
+ if (source === "") {
+ _callback({});
+ return true;
+ } else {
+ var url = this.parseURI(source, startDate, endDate);
+ var requestType = "GET";
+ if (self.options.dataPostPayload !== null ) {
+ requestType = "POST";
+ }
+ var payload = null;
+ if (self.options.dataPostPayload !== null) {
+ payload = this.parseURI(self.options.dataPostPayload, startDate, endDate);
+ }
+
+ switch(this.options.dataType) {
+ case "json":
+ d3.json(url, _callback).send(requestType, payload);
+ break;
+ case "csv":
+ d3.csv(url, _callback).send(requestType, payload);
+ break;
+ case "tsv":
+ d3.tsv(url, _callback).send(requestType, payload);
+ break;
+ case "txt":
+ d3.text(url, "text/plain", _callback).send(requestType, payload);
+ break;
+ }
+ }
+ return false;
+ case "object":
+ if (source === Object(source)) {
+ _callback(source);
+ return false;
+ }
+ /* falls through */
+ default:
+ _callback({});
+ return true;
+ }
+ },
+
+ /**
+ * Populate the calendar internal data
+ *
+ * @param object data
+ * @param constant updateMode
+ * @param Date startDate
+ * @param Date endDate
+ *
+ * @return void
+ */
+ parseDatas: function(data, updateMode, startDate, endDate) {
+ "use strict";
+
+ if (updateMode === this.RESET_ALL_ON_UPDATE) {
+ this._domains.forEach(function(key, value) {
+ value.forEach(function(element, index, array) {
+ array[index].v = null;
+ });
+ });
+ }
+
+ var temp = {};
+
+ var extractTime = function(d) { return d.t; };
+
+ /*jshint forin:false */
+ for (var d in data) {
+ var date = new Date(d*1000);
+ var domainUnit = this.getDomain(date)[0].getTime();
+
+ // The current data belongs to a domain that was compressed
+ // Compress the data for the two duplicate hours into the same hour
+ if (this.DSTDomain.indexOf(domainUnit) >= 0) {
+
+ // Re-assign all data to the first or the second duplicate hours
+ // depending on which is visible
+ if (this._domains.has(domainUnit - 3600 * 1000)) {
+ domainUnit -= 3600 * 1000;
+ }
+ }
+
+ // Skip if data is not relevant to current domain
+ if (isNaN(d) || !data.hasOwnProperty(d) || !this._domains.has(domainUnit) || !(domainUnit >= +startDate && domainUnit < +endDate)) {
+ continue;
+ }
+
+ var subDomainsData = this._domains.get(domainUnit);
+
+ if (!temp.hasOwnProperty(domainUnit)) {
+ temp[domainUnit] = subDomainsData.map(extractTime);
+ }
+
+ var index = temp[domainUnit].indexOf(this._domainType[this.options.subDomain].extractUnit(date));
+
+ if (updateMode === this.RESET_SINGLE_ON_UPDATE) {
+ subDomainsData[index].v = data[d];
+ } else {
+ if (!isNaN(subDomainsData[index].v)) {
+ subDomainsData[index].v += data[d];
+ } else {
+ subDomainsData[index].v = data[d];
+ }
+ }
+ }
+ },
+
+ parseURI: function(str, startDate, endDate) {
+ "use strict";
+
+ // Use a timestamp in seconds
+ str = str.replace(/\{\{t:start\}\}/g, startDate.getTime()/1000);
+ str = str.replace(/\{\{t:end\}\}/g, endDate.getTime()/1000);
+
+ // Use a string date, following the ISO-8601
+ str = str.replace(/\{\{d:start\}\}/g, startDate.toISOString());
+ str = str.replace(/\{\{d:end\}\}/g, endDate.toISOString());
+
+ return str;
+ },
+
+ interpretCSV: function(data) {
+ "use strict";
+
+ var d = {};
+ var keys = Object.keys(data[0]);
+ var i, total;
+ for (i = 0, total = data.length; i < total; i++) {
+ d[data[i][keys[0]]] = +data[i][keys[1]];
+ }
+ return d;
+ },
+
+ /**
+ * Handle the calendar layout and dimension
+ *
+ * Expand and shrink the container depending on its children dimension
+ * Also rearrange the children position depending on their dimension,
+ * and the legend position
+ *
+ * @return void
+ */
+ resize: function() {
+ "use strict";
+
+ var parent = this;
+ var options = parent.options;
+ var legendWidth = options.displayLegend ? (parent.Legend.getDim("width") + options.legendMargin[1] + options.legendMargin[3]) : 0;
+ var legendHeight = options.displayLegend ? (parent.Legend.getDim("height") + options.legendMargin[0] + options.legendMargin[2]) : 0;
+
+ var graphWidth = parent.graphDim.width - options.domainGutter - options.cellPadding;
+ var graphHeight = parent.graphDim.height - options.domainGutter - options.cellPadding;
+
+ this.root.transition().duration(options.animationDuration)
+ .attr("width", function() {
+ if (options.legendVerticalPosition === "middle" || options.legendVerticalPosition === "center") {
+ return graphWidth + legendWidth;
+ }
+ return Math.max(graphWidth, legendWidth);
+ })
+ .attr("height", function() {
+ if (options.legendVerticalPosition === "middle" || options.legendVerticalPosition === "center") {
+ return Math.max(graphHeight, legendHeight);
+ }
+ return graphHeight + legendHeight;
+ })
+ ;
+
+ this.root.select(".graph").transition().duration(options.animationDuration)
+ .attr("y", function() {
+ if (options.legendVerticalPosition === "top") {
+ return legendHeight;
+ }
+ return 0;
+ })
+ .attr("x", function() {
+ if (
+ (options.legendVerticalPosition === "middle" || options.legendVerticalPosition === "center") &&
+ options.legendHorizontalPosition === "left") {
+ return legendWidth;
+ }
+ return 0;
+
+ })
+ ;
+ },
+
+ // =========================================================================//
+ // PUBLIC API //
+ // =========================================================================//
+
+ /**
+ * Shift the calendar forward
+ */
+ next: function(n) {
+ "use strict";
+
+ if (arguments.length === 0) {
+ n = 1;
+ }
+ return this.loadNextDomain(n);
+ },
+
+ /**
+ * Shift the calendar backward
+ */
+ previous: function(n) {
+ "use strict";
+
+ if (arguments.length === 0) {
+ n = 1;
+ }
+ return this.loadPreviousDomain(n);
+ },
+
+ /**
+ * Jump directly to a specific date
+ *
+ * JumpTo will scroll the calendar until the wanted domain with the specified
+ * date is visible. Unless you set reset to true, the wanted domain
+ * will not necessarily be the first (leftmost) domain of the calendar.
+ *
+ * @param Date date Jump to the domain containing that date
+ * @param bool reset Whether the wanted domain should be the first domain of the calendar
+ * @param bool True of the calendar was scrolled
+ */
+ jumpTo: function(date, reset) {
+ "use strict";
+
+ if (arguments.length < 2) {
+ reset = false;
+ }
+ var domains = this.getDomainKeys();
+ var firstDomain = domains[0];
+ var lastDomain = domains[domains.length-1];
+
+ if (date < firstDomain) {
+ return this.loadPreviousDomain(this.getDomain(firstDomain, date).length);
+ } else {
+ if (reset) {
+ return this.loadNextDomain(this.getDomain(firstDomain, date).length);
+ }
+
+ if (date > lastDomain) {
+ return this.loadNextDomain(this.getDomain(lastDomain, date).length);
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Navigate back to the start date
+ *
+ * @since 3.3.8
+ * @return void
+ */
+ rewind: function() {
+ "use strict";
+
+ this.jumpTo(this.options.start, true);
+ },
+
+ /**
+ * Update the calendar with new data
+ *
+ * @param object|string dataSource The calendar's datasource, same type as this.options.data
+ * @param boolean|function afterLoad Whether to execute afterLoad() on the data. Pass directly a function
+ * if you don't want to use the afterLoad() callback
+ */
+ update: function(dataSource, afterLoad, updateMode) {
+ "use strict";
+
+ if (arguments.length < 2) {
+ afterLoad = true;
+ }
+ if (arguments.length < 3) {
+ updateMode = this.RESET_ALL_ON_UPDATE;
+ }
+
+ var domains = this.getDomainKeys();
+ var self = this;
+ this.getDatas(
+ dataSource,
+ new Date(domains[0]),
+ this.getSubDomain(domains[domains.length-1]).pop(),
+ function() {
+ self.fill();
+ },
+ afterLoad,
+ updateMode
+ );
+ },
+
+ /**
+ * Set the legend
+ *
+ * @param array legend an array of integer, representing the different threshold value
+ * @param array colorRange an array of 2 hex colors, for the minimum and maximum colors
+ */
+ setLegend: function() {
+ "use strict";
+
+ var oldLegend = this.options.legend.slice(0);
+ if (arguments.length >= 1 && Array.isArray(arguments[0])) {
+ this.options.legend = arguments[0];
+ }
+ if (arguments.length >= 2) {
+ if (Array.isArray(arguments[1]) && arguments[1].length >= 2) {
+ this.options.legendColors = [arguments[1][0], arguments[1][1]];
+ } else {
+ this.options.legendColors = arguments[1];
+ }
+ }
+
+ if ((arguments.length > 0 && !arrayEquals(oldLegend, this.options.legend)) || arguments.length >= 2) {
+ this.Legend.buildColors();
+ this.fill();
+ }
+
+ this.Legend.redraw(this.graphDim.width - this.options.domainGutter - this.options.cellPadding);
+ },
+
+ /**
+ * Remove the legend
+ *
+ * @return bool False if there is no legend to remove
+ */
+ removeLegend: function() {
+ "use strict";
+
+ if (!this.options.displayLegend) {
+ return false;
+ }
+ this.options.displayLegend = false;
+ this.Legend.remove();
+ return true;
+ },
+
+ /**
+ * Display the legend
+ *
+ * @return bool False if the legend was already displayed
+ */
+ showLegend: function() {
+ "use strict";
+
+ if (this.options.displayLegend) {
+ return false;
+ }
+ this.options.displayLegend = true;
+ this.Legend.redraw(this.graphDim.width - this.options.domainGutter - this.options.cellPadding);
+ return true;
+ },
+
+ /**
+ * Highlight dates
+ *
+ * Add a highlight class to a set of dates
+ *
+ * @since 3.3.5
+ * @param array Array of dates to highlight
+ * @return bool True if dates were highlighted
+ */
+ highlight: function(args) {
+ "use strict";
+
+ if ((this.options.highlight = this.expandDateSetting(args)).length > 0) {
+ this.fill();
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Destroy the calendar
+ *
+ * Usage: cal = cal.destroy();
+ *
+ * @since 3.3.6
+ * @param function A callback function to trigger after destroying the calendar
+ * @return null
+ */
+ destroy: function(callback) {
+ "use strict";
+
+ this.root.transition().duration(this.options.animationDuration)
+ .attr("width", 0)
+ .attr("height", 0)
+ .remove()
+ .each("end", function() {
+ if (typeof callback === "function") {
+ callback();
+ } else if (typeof callback !== "undefined") {
+ console.log("Provided callback for destroy() is not a function.");
+ }
+ })
+ ;
+
+ return null;
+ },
+
+ getSVG: function() {
+ "use strict";
+
+ var styles = {
+ ".cal-heatmap-container": {},
+ ".graph": {},
+ ".graph-rect": {},
+ "rect.highlight": {},
+ "rect.now": {},
+ "rect.highlight-now": {},
+ "text.highlight": {},
+ "text.now": {},
+ "text.highlight-now": {},
+ ".domain-background": {},
+ ".graph-label": {},
+ ".subdomain-text": {},
+ ".q0": {},
+ ".qi": {}
+ };
+
+ for (var j = 1, total = this.options.legend.length+1; j <= total; j++) {
+ styles[".q" + j] = {};
+ }
+
+ var root = this.root;
+
+ var whitelistStyles = [
+ // SVG specific properties
+ "stroke", "stroke-width", "stroke-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-miterlimit",
+ "fill", "fill-opacity", "fill-rule",
+ "marker", "marker-start", "marker-mid", "marker-end",
+ "alignement-baseline", "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal", "glyph-orientation-vertical", "kerning", "text-anchor",
+ "shape-rendering",
+
+ // Text Specific properties
+ "text-transform", "font-family", "font", "font-size", "font-weight"
+ ];
+
+ var filterStyles = function(attribute, property, value) {
+ if (whitelistStyles.indexOf(property) !== -1) {
+ styles[attribute][property] = value;
+ }
+ };
+
+ var getElement = function(e) {
+ return root.select(e)[0][0];
+ };
+
+ /* jshint forin:false */
+ for (var element in styles) {
+ if (!styles.hasOwnProperty(element)) {
+ continue;
+ }
+
+ var dom = getElement(element);
+
+ if (dom === null) {
+ continue;
+ }
+
+ // The DOM Level 2 CSS way
+ /* jshint maxdepth: false */
+ if ("getComputedStyle" in window) {
+ var cs = getComputedStyle(dom, null);
+ if (cs.length !== 0) {
+ for (var i = 0; i < cs.length; i++) {
+ filterStyles(element, cs.item(i), cs.getPropertyValue(cs.item(i)));
+ }
+
+ // Opera workaround. Opera doesn"t support `item`/`length`
+ // on CSSStyleDeclaration.
+ } else {
+ for (var k in cs) {
+ if (cs.hasOwnProperty(k)) {
+ filterStyles(element, k, cs[k]);
+ }
+ }
+ }
+
+ // The IE way
+ } else if ("currentStyle" in dom) {
+ var css = dom.currentStyle;
+ for (var p in css) {
+ filterStyles(element, p, css[p]);
+ }
+ }
+ }
+
+ var string = "<svg xmlns=\"http://www.w3.org/2000/svg\" "+
+ "xmlns:xlink=\"http://www.w3.org/1999/xlink\"><style type=\"text/css\"><![CDATA[ ";
+
+ for (var style in styles) {
+ string += style + " {\n";
+ for (var l in styles[style]) {
+ string += "\t" + l + ":" + styles[style][l] + ";\n";
+ }
+ string += "}\n";
+ }
+
+ string += "]]></style>";
+ string += new XMLSerializer().serializeToString(this.root[0][0]);
+ string += "</svg>";
+
+ return string;
+ }
+};
+
+// =========================================================================//
+// DOMAIN POSITION COMPUTATION //
+// =========================================================================//
+
+/**
+ * Compute the position of a domain, relative to the calendar
+ */
+var DomainPosition = function() {
+ "use strict";
+
+ this.positions = d3.map();
+};
+
+DomainPosition.prototype.getPosition = function(d) {
+ "use strict";
+
+ return this.positions.get(d);
+};
+
+DomainPosition.prototype.getPositionFromIndex = function(i) {
+ "use strict";
+
+ var domains = this.getKeys();
+ return this.positions.get(domains[i]);
+};
+
+DomainPosition.prototype.getLast = function() {
+ "use strict";
+
+ var domains = this.getKeys();
+ return this.positions.get(domains[domains.length-1]);
+};
+
+DomainPosition.prototype.setPosition = function(d, dim) {
+ "use strict";
+
+ this.positions.set(d, dim);
+};
+
+DomainPosition.prototype.shiftRightBy = function(exitingDomainDim) {
+ "use strict";
+
+ this.positions.forEach(function(key, value) {
+ this.set(key, value - exitingDomainDim);
+ });
+
+ var domains = this.getKeys();
+ this.positions.remove(domains[0]);
+};
+
+DomainPosition.prototype.shiftLeftBy = function(enteringDomainDim) {
+ "use strict";
+
+ this.positions.forEach(function(key, value) {
+ this.set(key, value + enteringDomainDim);
+ });
+
+ var domains = this.getKeys();
+ this.positions.remove(domains[domains.length-1]);
+};
+
+DomainPosition.prototype.getKeys = function() {
+ "use strict";
+
+ return this.positions.keys().sort(function(a, b) {
+ return parseInt(a, 10) - parseInt(b, 10);
+ });
+};
+
+// =========================================================================//
+// LEGEND //
+// =========================================================================//
+
+var Legend = function(calendar) {
+ "use strict";
+
+ this.calendar = calendar;
+ this.computeDim();
+
+ if (calendar.options.legendColors !== null) {
+ this.buildColors();
+ }
+};
+
+Legend.prototype.computeDim = function() {
+ "use strict";
+
+ var options = this.calendar.options; // Shorter accessor for variable name mangling when minifying
+ this.dim = {
+ width:
+ options.legendCellSize * (options.legend.length+1) +
+ options.legendCellPadding * (options.legend.length),
+ height:
+ options.legendCellSize
+ };
+};
+
+Legend.prototype.remove = function() {
+ "use strict";
+
+ this.calendar.root.select(".graph-legend").remove();
+ this.calendar.resize();
+};
+
+Legend.prototype.redraw = function(width) {
+ "use strict";
+
+ if (!this.calendar.options.displayLegend) {
+ return false;
+ }
+
+ var parent = this;
+ var calendar = this.calendar;
+ var legend = calendar.root;
+ var legendItem;
+ var options = calendar.options; // Shorter accessor for variable name mangling when minifying
+
+ this.computeDim();
+
+ var _legend = options.legend.slice(0);
+ _legend.push(_legend[_legend.length-1]+1);
+
+ var legendElement = calendar.root.select(".graph-legend");
+ if (legendElement[0][0] !== null) {
+ legend = legendElement;
+ legendItem = legend
+ .select("g")
+ .selectAll("rect").data(_legend)
+ ;
+ } else {
+ // Creating the new legend DOM if it doesn't already exist
+ legend = options.legendVerticalPosition === "top" ? legend.insert("svg", ".graph") : legend.append("svg");
+
+ legend
+ .attr("x", getLegendXPosition())
+ .attr("y", getLegendYPosition())
+ ;
+
+ legendItem = legend
+ .attr("class", "graph-legend")
+ .attr("height", parent.getDim("height"))
+ .attr("width", parent.getDim("width"))
+ .append("g")
+ .selectAll().data(_legend)
+ ;
+ }
+
+ legendItem
+ .enter()
+ .append("rect")
+ .call(legendCellLayout)
+ .attr("class", function(d){ return calendar.Legend.getClass(d, (calendar.legendScale === null)); })
+ .attr("fill-opacity", 0)
+ .call(function(selection) {
+ if (calendar.legendScale !== null && options.legendColors !== null && options.legendColors.hasOwnProperty("base")) {
+ selection.attr("fill", options.legendColors.base);
+ }
+ })
+ .append("title")
+ ;
+
+ legendItem.exit().transition().duration(options.animationDuration)
+ .attr("fill-opacity", 0)
+ .remove();
+
+ legendItem.transition().delay(function(d, i) { return options.animationDuration * i/10; })
+ .call(legendCellLayout)
+ .attr("fill-opacity", 1)
+ .call(function(element) {
+ element.attr("fill", function(d, i) {
+ if (calendar.legendScale === null) {
+ return "";
+ }
+
+ if (i === 0) {
+ return calendar.legendScale(d - 1);
+ }
+ return calendar.legendScale(options.legend[i-1]);
+ });
+
+ element.attr("class", function(d) { return calendar.Legend.getClass(d, (calendar.legendScale === null)); });
+ })
+ ;
+
+ function legendCellLayout(selection) {
+ selection
+ .attr("width", options.legendCellSize)
+ .attr("height", options.legendCellSize)
+ .attr("x", function(d, i) {
+ return i * (options.legendCellSize + options.legendCellPadding);
+ })
+ ;
+ }
+
+ legendItem.select("title").text(function(d, i) {
+ if (i === 0) {
+ return (options.legendTitleFormat.lower).format({
+ min: options.legend[i],
+ name: options.itemName[1]
+ });
+ } else if (i === _legend.length-1) {
+ return (options.legendTitleFormat.upper).format({
+ max: options.legend[i-1],
+ name: options.itemName[1]
+ });
+ } else {
+ return (options.legendTitleFormat.inner).format({
+ down: options.legend[i-1],
+ up: options.legend[i],
+ name: options.itemName[1]
+ });
+ }
+ })
+ ;
+
+ legend.transition().duration(options.animationDuration)
+ .attr("x", getLegendXPosition())
+ .attr("y", getLegendYPosition())
+ .attr("width", parent.getDim("width"))
+ .attr("height", parent.getDim("height"))
+ ;
+
+ legend.select("g").transition().duration(options.animationDuration)
+ .attr("transform", function() {
+ if (options.legendOrientation === "vertical") {
+ return "rotate(90 " + options.legendCellSize/2 + " " + options.legendCellSize/2 + ")";
+ }
+ return "";
+ })
+ ;
+
+ function getLegendXPosition() {
+ switch(options.legendHorizontalPosition) {
+ case "right":
+ if (options.legendVerticalPosition === "center" || options.legendVerticalPosition === "middle") {
+ return width + options.legendMargin[3];
+ }
+ return width - parent.getDim("width") - options.legendMargin[1];
+ case "middle":
+ case "center":
+ return Math.round(width/2 - parent.getDim("width")/2);
+ default:
+ return options.legendMargin[3];
+ }
+ }
+
+ function getLegendYPosition() {
+ if (options.legendVerticalPosition === "bottom") {
+ return calendar.graphDim.height + options.legendMargin[0] - options.domainGutter - options.cellPadding;
+ }
+ return options.legendMargin[0];
+ }
+
+ calendar.resize();
+};
+
+/**
+ * Return the dimension of the legend
+ *
+ * Takes into account rotation
+ *
+ * @param string axis Width or height
+ * @return int height or width in pixels
+ */
+Legend.prototype.getDim = function(axis) {
+ "use strict";
+
+ var isHorizontal = (this.calendar.options.legendOrientation === "horizontal");
+
+ switch(axis) {
+ case "width":
+ return this.dim[isHorizontal ? "width": "height"];
+ case "height":
+ return this.dim[isHorizontal ? "height": "width"];
+ }
+};
+
+Legend.prototype.buildColors = function() {
+ "use strict";
+
+ var options = this.calendar.options; // Shorter accessor for variable name mangling when minifying
+
+ if (options.legendColors === null) {
+ this.calendar.legendScale = null;
+ return false;
+ }
+
+ var _colorRange = [];
+
+ if (Array.isArray(options.legendColors)) {
+ _colorRange = options.legendColors;
+ } else if (options.legendColors.hasOwnProperty("min") && options.legendColors.hasOwnProperty("max")) {
+ _colorRange = [options.legendColors.min, options.legendColors.max];
+ } else {
+ options.legendColors = null;
+ return false;
+ }
+
+ var _legend = options.legend.slice(0);
+
+ if (_legend[0] > 0) {
+ _legend.unshift(0);
+ } else if (_legend[0] < 0) {
+ // Let's guess the leftmost value, it we have to add one
+ _legend.unshift(_legend[0] - (_legend[_legend.length-1] - _legend[0])/_legend.length);
+ }
+
+ var colorScale = d3.scale.linear()
+ .range(_colorRange)
+ .interpolate(d3.interpolateHcl)
+ .domain([d3.min(_legend), d3.max(_legend)])
+ ;
+
+ var legendColors = _legend.map(function(element) { return colorScale(element); });
+ this.calendar.legendScale = d3.scale.threshold().domain(options.legend).range(legendColors);
+
+ return true;
+};
+
+/**
+ * Return the classname on the legend for the specified value
+ *
+ * @param integer n Value associated to a date
+ * @param bool withCssClass Whether to display the css class used to style the cell.
+ * Disabling will allow styling directly via html fill attribute
+ *
+ * @return string Classname according to the legend
+ */
+Legend.prototype.getClass = function(n, withCssClass) {
+ "use strict";
+
+ if (n === null || isNaN(n)) {
+ return "";
+ }
+
+ var index = [this.calendar.options.legend.length + 1];
+
+ for (var i = 0, total = this.calendar.options.legend.length-1; i <= total; i++) {
+
+ if (this.calendar.options.legend[0] > 0 && n < 0) {
+ index = ["1", "i"];
+ break;
+ }
+
+ if (n <= this.calendar.options.legend[i]) {
+ index = [i+1];
+ break;
+ }
+ }
+
+ if (n === 0) {
+ index.push(0);
+ }
+
+ index.unshift("");
+ return (index.join(" r") + (withCssClass ? index.join(" q"): "")).trim();
+};
+
+/**
+ * Sprintf like function
+ * @source http://stackoverflow.com/a/4795914/805649
+ * @return String
+ */
+String.prototype.format = function () {
+ "use strict";
+
+ var formatted = this;
+ for (var prop in arguments[0]) {
+ if (arguments[0].hasOwnProperty(prop)) {
+ var regexp = new RegExp("\\{" + prop + "\\}", "gi");
+ formatted = formatted.replace(regexp, arguments[0][prop]);
+ }
+ }
+ return formatted;
+};
+
+/**
+ * #source http://stackoverflow.com/a/383245/805649
+ */
+function mergeRecursive(obj1, obj2) {
+ "use strict";
+
+ /*jshint forin:false */
+ for (var p in obj2) {
+ try {
+ // Property in destination object set; update its value.
+ if (obj2[p].constructor === Object) {
+ obj1[p] = mergeRecursive(obj1[p], obj2[p]);
+ } else {
+ obj1[p] = obj2[p];
+ }
+ } catch(e) {
+ // Property in destination object not set; create it and set its value.
+ obj1[p] = obj2[p];
+ }
+ }
+
+ return obj1;
+}
+
+/**
+ * Check if 2 arrays are equals
+ *
+ * @link http://stackoverflow.com/a/14853974/805649
+ * @param array array the array to compare to
+ * @return bool true of the 2 arrays are equals
+ */
+function arrayEquals(arrayA, arrayB) {
+ "use strict";
+
+ // if the other array is a falsy value, return
+ if (!arrayB || !arrayA) {
+ return false;
+ }
+
+ // compare lengths - can save a lot of time
+ if (arrayA.length !== arrayB.length) {
+ return false;
+ }
+
+ for (var i = 0; i < arrayA.length; i++) {
+ // Check if we have nested arrays
+ if (arrayA[i] instanceof Array && arrayB[i] instanceof Array) {
+ // recurse into the nested arrays
+ if (!arrayEquals(arrayA[i], arrayB[i])) {
+ return false;
+ }
+ }
+ else if (arrayA[i] !== arrayB[i]) {
+ // Warning - two different object instances will never be equal: {x:20} != {x:20}
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * AMD Loader
+ */
+if (typeof define === "function" && define.amd) {
+ define(["d3"], function() {
+ "use strict";
+
+ return CalHeatMap;
+ });
+} else if (typeof module === "object" && module.exports) {
+ module.exports = CalHeatMap;
+} else {
+ window.CalHeatMap = CalHeatMap;
+}