You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3487 lines
93 KiB

  1. /*! cal-heatmap v3.6.2 (Mon Oct 10 2016 01:36:20)
  2. * ---------------------------------------------
  3. * Cal-Heatmap is a javascript module to create calendar heatmap to visualize time series data
  4. * https://github.com/wa0x6e/cal-heatmap
  5. * Licensed under the MIT license
  6. * Copyright 2014 Wan Qi Chen
  7. */
  8. var d3 = typeof require === "function" ? require("d3") : window.d3;
  9. var CalHeatMap = function() {
  10. "use strict";
  11. var self = this;
  12. this.allowedDataType = ["json", "csv", "tsv", "txt"];
  13. // Default settings
  14. this.options = {
  15. // selector string of the container to append the graph to
  16. // Accept any string value accepted by document.querySelector or CSS3
  17. // or an Element object
  18. itemSelector: "#cal-heatmap",
  19. // Whether to paint the calendar on init()
  20. // Used by testsuite to reduce testing time
  21. paintOnLoad: true,
  22. // ================================================
  23. // DOMAIN
  24. // ================================================
  25. // Number of domain to display on the graph
  26. range: 12,
  27. // Size of each cell, in pixel
  28. cellSize: 10,
  29. // Padding between each cell, in pixel
  30. cellPadding: 2,
  31. // For rounded subdomain rectangles, in pixels
  32. cellRadius: 0,
  33. domainGutter: 2,
  34. domainMargin: [0, 0, 0, 0],
  35. domain: "hour",
  36. subDomain: "min",
  37. // Number of columns to split the subDomains to
  38. // If not null, will takes precedence over rowLimit
  39. colLimit: null,
  40. // Number of rows to split the subDomains to
  41. // Will be ignored if colLimit is not null
  42. rowLimit: null,
  43. // First day of the week is Monday
  44. // 0 to start the week on Sunday
  45. weekStartOnMonday: true,
  46. // Start date of the graph
  47. // @default now
  48. start: new Date(),
  49. minDate: null,
  50. maxDate: null,
  51. // ================================================
  52. // DATA
  53. // ================================================
  54. // Data source
  55. // URL, where to fetch the original datas
  56. data: "",
  57. // Data type
  58. // Default: json
  59. dataType: this.allowedDataType[0],
  60. // Payload sent when using POST http method
  61. // Leave to null (default) for GET request
  62. // Expect a string, formatted like "a=b;c=d"
  63. dataPostPayload: null,
  64. // Whether to consider missing date:value from the datasource
  65. // as equal to 0, or just leave them as missing
  66. considerMissingDataAsZero: false,
  67. // Load remote data on calendar creation
  68. // When false, the calendar will be left empty
  69. loadOnInit: true,
  70. // Calendar orientation
  71. // false: display domains side by side
  72. // true : display domains one under the other
  73. verticalOrientation: false,
  74. // Domain dynamic width/height
  75. // The width on a domain depends on the number of
  76. domainDynamicDimension: true,
  77. // Domain Label properties
  78. label: {
  79. // valid: top, right, bottom, left
  80. position: "bottom",
  81. // Valid: left, center, right
  82. // Also valid are the direct svg values: start, middle, end
  83. align: "center",
  84. // By default, there is no margin/padding around the label
  85. offset: {
  86. x: 0,
  87. y: 0
  88. },
  89. rotate: null,
  90. // Used only on vertical orientation
  91. width: 100,
  92. // Used only on horizontal orientation
  93. height: null
  94. },
  95. // ================================================
  96. // LEGEND
  97. // ================================================
  98. // Threshold for the legend
  99. legend: [10, 20, 30, 40],
  100. // Whether to display the legend
  101. displayLegend: true,
  102. legendCellSize: 10,
  103. legendCellPadding: 2,
  104. legendMargin: [0, 0, 0, 0],
  105. // Legend vertical position
  106. // top: place legend above calendar
  107. // bottom: place legend below the calendar
  108. legendVerticalPosition: "bottom",
  109. // Legend horizontal position
  110. // accepted values: left, center, right
  111. legendHorizontalPosition: "left",
  112. // Legend rotation
  113. // accepted values: horizontal, vertical
  114. legendOrientation: "horizontal",
  115. // Objects holding all the heatmap different colors
  116. // null to disable, and use the default css styles
  117. //
  118. // Examples:
  119. // legendColors: {
  120. // min: "green",
  121. // max: "red",
  122. // empty: "#ffffff",
  123. // base: "grey",
  124. // overflow: "red"
  125. // }
  126. legendColors: null,
  127. // ================================================
  128. // HIGHLIGHT
  129. // ================================================
  130. // List of dates to highlight
  131. // Valid values:
  132. // - []: don't highlight anything
  133. // - "now": highlight the current date
  134. // - an array of Date objects: highlight the specified dates
  135. highlight: [],
  136. // ================================================
  137. // TEXT FORMATTING / i18n
  138. // ================================================
  139. // Name of the items to represent in the calendar
  140. itemName: ["item", "items"],
  141. // Formatting of the domain label
  142. // @default: null, will use the formatting according to domain type
  143. // Accept a string used as specifier by d3.time.format()
  144. // or a function
  145. //
  146. // Refer to https://github.com/mbostock/d3/wiki/Time-Formatting
  147. // for accepted date formatting used by d3.time.format()
  148. domainLabelFormat: null,
  149. // Formatting of the title displayed when hovering a subDomain cell
  150. subDomainTitleFormat: {
  151. empty: "{date}",
  152. filled: "{count} {name} {connector} {date}"
  153. },
  154. // Formatting of the {date} used in subDomainTitleFormat
  155. // @default: null, will use the formatting according to subDomain type
  156. // Accept a string used as specifier by d3.time.format()
  157. // or a function
  158. //
  159. // Refer to https://github.com/mbostock/d3/wiki/Time-Formatting
  160. // for accepted date formatting used by d3.time.format()
  161. subDomainDateFormat: null,
  162. // Formatting of the text inside each subDomain cell
  163. // @default: null, no text
  164. // Accept a string used as specifier by d3.time.format()
  165. // or a function
  166. //
  167. // Refer to https://github.com/mbostock/d3/wiki/Time-Formatting
  168. // for accepted date formatting used by d3.time.format()
  169. subDomainTextFormat: null,
  170. // Formatting of the title displayed when hovering a legend cell
  171. legendTitleFormat: {
  172. lower: "less than {min} {name}",
  173. inner: "between {down} and {up} {name}",
  174. upper: "more than {max} {name}"
  175. },
  176. // Animation duration, in ms
  177. animationDuration: 500,
  178. nextSelector: false,
  179. previousSelector: false,
  180. itemNamespace: "cal-heatmap",
  181. tooltip: false,
  182. // ================================================
  183. // EVENTS CALLBACK
  184. // ================================================
  185. // Callback when clicking on a time block
  186. onClick: null,
  187. // Callback after painting the empty calendar
  188. // Can be used to trigger an API call, once the calendar is ready to be filled
  189. afterLoad: null,
  190. // Callback after loading the next domain in the calendar
  191. afterLoadNextDomain: null,
  192. // Callback after loading the previous domain in the calendar
  193. afterLoadPreviousDomain: null,
  194. // Callback after finishing all actions on the calendar
  195. onComplete: null,
  196. // Callback after fetching the datas, but before applying them to the calendar
  197. // Used mainly to convert the datas if they're not formatted like expected
  198. // Takes the fetched "data" object as argument, must return a json object
  199. // formatted like {timestamp:count, timestamp2:count2},
  200. afterLoadData: function(data) { return data; },
  201. // Callback triggered after calling next().
  202. // The `status` argument is equal to true if there is no
  203. // more next domain to load
  204. //
  205. // This callback is also executed once, after calling previous(),
  206. // only when the max domain is reached
  207. onMaxDomainReached: null,
  208. // Callback triggered after calling previous().
  209. // The `status` argument is equal to true if there is no
  210. // more previous domain to load
  211. //
  212. // This callback is also executed once, after calling next(),
  213. // only when the min domain is reached
  214. onMinDomainReached: null
  215. };
  216. this._domainType = {
  217. "min": {
  218. name: "minute",
  219. level: 10,
  220. maxItemNumber: 60,
  221. defaultRowNumber: 10,
  222. defaultColumnNumber: 6,
  223. row: function(d) { return self.getSubDomainRowNumber(d); },
  224. column: function(d) { return self.getSubDomainColumnNumber(d); },
  225. position: {
  226. x: function(d) { return Math.floor(d.getMinutes() / self._domainType.min.row(d)); },
  227. y: function(d) { return d.getMinutes() % self._domainType.min.row(d); }
  228. },
  229. format: {
  230. date: "%H:%M, %A %B %-e, %Y",
  231. legend: "",
  232. connector: "at"
  233. },
  234. extractUnit: function(d) {
  235. return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes()).getTime();
  236. }
  237. },
  238. "hour": {
  239. name: "hour",
  240. level: 20,
  241. maxItemNumber: function(d) {
  242. switch(self.options.domain) {
  243. case "day":
  244. return 24;
  245. case "week":
  246. return 24 * 7;
  247. case "month":
  248. return 24 * (self.options.domainDynamicDimension ? self.getDayCountInMonth(d): 31);
  249. }
  250. },
  251. defaultRowNumber: 6,
  252. defaultColumnNumber: function(d) {
  253. switch(self.options.domain) {
  254. case "day":
  255. return 4;
  256. case "week":
  257. return 28;
  258. case "month":
  259. return self.options.domainDynamicDimension ? self.getDayCountInMonth(d): 31;
  260. }
  261. },
  262. row: function(d) { return self.getSubDomainRowNumber(d); },
  263. column: function(d) { return self.getSubDomainColumnNumber(d); },
  264. position: {
  265. x: function(d) {
  266. if (self.options.domain === "month") {
  267. if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
  268. return Math.floor((d.getHours() + (d.getDate()-1)*24) / self._domainType.hour.row(d));
  269. }
  270. return Math.floor(d.getHours() / self._domainType.hour.row(d)) + (d.getDate()-1)*4;
  271. } else if (self.options.domain === "week") {
  272. if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
  273. return Math.floor((d.getHours() + self.getWeekDay(d)*24) / self._domainType.hour.row(d));
  274. }
  275. return Math.floor(d.getHours() / self._domainType.hour.row(d)) + self.getWeekDay(d)*4;
  276. }
  277. return Math.floor(d.getHours() / self._domainType.hour.row(d));
  278. },
  279. y: function(d) {
  280. var p = d.getHours();
  281. if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
  282. switch(self.options.domain) {
  283. case "month":
  284. p += (d.getDate()-1) * 24;
  285. break;
  286. case "week":
  287. p += self.getWeekDay(d) * 24;
  288. break;
  289. }
  290. }
  291. return Math.floor(p % self._domainType.hour.row(d));
  292. }
  293. },
  294. format: {
  295. date: "%Hh, %A %B %-e, %Y",
  296. legend: "%H:00",
  297. connector: "at"
  298. },
  299. extractUnit: function(d) {
  300. return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()).getTime();
  301. }
  302. },
  303. "day": {
  304. name: "day",
  305. level: 30,
  306. maxItemNumber: function(d) {
  307. switch(self.options.domain) {
  308. case "week":
  309. return 7;
  310. case "month":
  311. return self.options.domainDynamicDimension ? self.getDayCountInMonth(d) : 31;
  312. case "year":
  313. return self.options.domainDynamicDimension ? self.getDayCountInYear(d) : 366;
  314. }
  315. },
  316. defaultColumnNumber: function(d) {
  317. d = new Date(d);
  318. switch(self.options.domain) {
  319. case "week":
  320. return 1;
  321. case "month":
  322. return (self.options.domainDynamicDimension && !self.options.verticalOrientation) ? (self.getWeekNumber(new Date(d.getFullYear(), d.getMonth()+1, 0)) - self.getWeekNumber(d) + 1): 6;
  323. case "year":
  324. return (self.options.domainDynamicDimension ? (self.getWeekNumber(new Date(d.getFullYear(), 11, 31)) - self.getWeekNumber(new Date(d.getFullYear(), 0)) + 1): 54);
  325. }
  326. },
  327. defaultRowNumber: 7,
  328. row: function(d) { return self.getSubDomainRowNumber(d); },
  329. column: function(d) { return self.getSubDomainColumnNumber(d); },
  330. position: {
  331. x: function(d) {
  332. switch(self.options.domain) {
  333. case "week":
  334. return Math.floor(self.getWeekDay(d) / self._domainType.day.row(d));
  335. case "month":
  336. if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
  337. return Math.floor((d.getDate() - 1)/ self._domainType.day.row(d));
  338. }
  339. return self.getWeekNumber(d) - self.getWeekNumber(new Date(d.getFullYear(), d.getMonth()));
  340. case "year":
  341. if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
  342. return Math.floor((self.getDayOfYear(d) - 1) / self._domainType.day.row(d));
  343. }
  344. return self.getWeekNumber(d);
  345. }
  346. },
  347. y: function(d) {
  348. var p = self.getWeekDay(d);
  349. if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
  350. switch(self.options.domain) {
  351. case "year":
  352. p = self.getDayOfYear(d) - 1;
  353. break;
  354. case "week":
  355. p = self.getWeekDay(d);
  356. break;
  357. case "month":
  358. p = d.getDate() - 1;
  359. break;
  360. }
  361. }
  362. return Math.floor(p % self._domainType.day.row(d));
  363. }
  364. },
  365. format: {
  366. date: "%A %B %-e, %Y",
  367. legend: "%e %b",
  368. connector: "on"
  369. },
  370. extractUnit: function(d) {
  371. return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
  372. }
  373. },
  374. "week": {
  375. name: "week",
  376. level: 40,
  377. maxItemNumber: 54,
  378. defaultColumnNumber: function(d) {
  379. d = new Date(d);
  380. switch(self.options.domain) {
  381. case "year":
  382. return self._domainType.week.maxItemNumber;
  383. case "month":
  384. return self.options.domainDynamicDimension ? self.getWeekNumber(new Date(d.getFullYear(), d.getMonth()+1, 0)) - self.getWeekNumber(d) : 5;
  385. }
  386. },
  387. defaultRowNumber: 1,
  388. row: function(d) { return self.getSubDomainRowNumber(d); },
  389. column: function(d) { return self.getSubDomainColumnNumber(d); },
  390. position: {
  391. x: function(d) {
  392. switch(self.options.domain) {
  393. case "year":
  394. return Math.floor(self.getWeekNumber(d) / self._domainType.week.row(d));
  395. case "month":
  396. return Math.floor(self.getMonthWeekNumber(d) / self._domainType.week.row(d));
  397. }
  398. },
  399. y: function(d) {
  400. return self.getWeekNumber(d) % self._domainType.week.row(d);
  401. }
  402. },
  403. format: {
  404. date: "%B Week #%W",
  405. legend: "%B Week #%W",
  406. connector: "in"
  407. },
  408. extractUnit: function(d) {
  409. var dt = new Date(d.getFullYear(), d.getMonth(), d.getDate());
  410. // According to ISO-8601, week number computation are based on week starting on Monday
  411. var weekDay = dt.getDay() - (self.options.weekStartOnMonday ? 1 : 0);
  412. if (weekDay < 0) {
  413. weekDay = 6;
  414. }
  415. dt.setDate(dt.getDate() - weekDay);
  416. return dt.getTime();
  417. }
  418. },
  419. "month": {
  420. name: "month",
  421. level: 50,
  422. maxItemNumber: 12,
  423. defaultColumnNumber: 12,
  424. defaultRowNumber: 1,
  425. row: function() { return self.getSubDomainRowNumber(); },
  426. column: function() { return self.getSubDomainColumnNumber(); },
  427. position: {
  428. x: function(d) { return Math.floor(d.getMonth() / self._domainType.month.row(d)); },
  429. y: function(d) { return d.getMonth() % self._domainType.month.row(d); }
  430. },
  431. format: {
  432. date: "%B %Y",
  433. legend: "%B",
  434. connector: "in"
  435. },
  436. extractUnit: function(d) {
  437. return new Date(d.getFullYear(), d.getMonth()).getTime();
  438. }
  439. },
  440. "year": {
  441. name: "year",
  442. level: 60,
  443. row: function() { return self.options.rowLimit || 1; },
  444. column: function() { return self.options.colLimit || 1; },
  445. position: {
  446. x: function() { return 1; },
  447. y: function() { return 1; }
  448. },
  449. format: {
  450. date: "%Y",
  451. legend: "%Y",
  452. connector: "in"
  453. },
  454. extractUnit: function(d) {
  455. return new Date(d.getFullYear()).getTime();
  456. }
  457. }
  458. };
  459. for (var type in this._domainType) {
  460. if (this._domainType.hasOwnProperty(type)) {
  461. var d = this._domainType[type];
  462. this._domainType["x_" + type] = {
  463. name: "x_" + type,
  464. level: d.type,
  465. maxItemNumber: d.maxItemNumber,
  466. defaultRowNumber: d.defaultRowNumber,
  467. defaultColumnNumber: d.defaultColumnNumber,
  468. row: d.column,
  469. column: d.row,
  470. position: {
  471. x: d.position.y,
  472. y: d.position.x
  473. },
  474. format: d.format,
  475. extractUnit: d.extractUnit
  476. };
  477. }
  478. }
  479. // Record the address of the last inserted domain when browsing
  480. this.lastInsertedSvg = null;
  481. this._completed = false;
  482. // Record all the valid domains
  483. // Each domain value is a timestamp in milliseconds
  484. this._domains = d3.map();
  485. this.graphDim = {
  486. width: 0,
  487. height: 0
  488. };
  489. this.legendDim = {
  490. width: 0,
  491. height: 0
  492. };
  493. this.NAVIGATE_LEFT = 1;
  494. this.NAVIGATE_RIGHT = 2;
  495. // Various update mode when using the update() API
  496. this.RESET_ALL_ON_UPDATE = 0;
  497. this.RESET_SINGLE_ON_UPDATE = 1;
  498. this.APPEND_ON_UPDATE = 2;
  499. this.DEFAULT_LEGEND_MARGIN = 10;
  500. this.root = null;
  501. this.tooltip = null;
  502. this._maxDomainReached = false;
  503. this._minDomainReached = false;
  504. this.domainPosition = new DomainPosition();
  505. this.Legend = null;
  506. this.legendScale = null;
  507. // List of domains that are skipped because of DST
  508. // All times belonging to these domains should be re-assigned to the previous domain
  509. this.DSTDomain = [];
  510. /**
  511. * Display the graph for the first time
  512. * @return bool True if the calendar is created
  513. */
  514. this._init = function() {
  515. self.getDomain(self.options.start).map(function(d) { return d.getTime(); }).map(function(d) {
  516. self._domains.set(d, self.getSubDomain(d).map(function(d) { return {t: self._domainType[self.options.subDomain].extractUnit(d), v: null}; }));
  517. });
  518. self.root = d3.select(self.options.itemSelector).append("svg").attr("class", "cal-heatmap-container");
  519. self.tooltip = d3.select(self.options.itemSelector)
  520. .attr("style", function() {
  521. var current = d3.select(self.options.itemSelector).attr("style");
  522. return (current !== null ? current : "") + "position:relative;";
  523. })
  524. .append("div")
  525. .attr("class", "ch-tooltip")
  526. ;
  527. self.root.attr("x", 0).attr("y", 0).append("svg").attr("class", "graph");
  528. self.Legend = new Legend(self);
  529. if (self.options.paintOnLoad) {
  530. _initCalendar();
  531. }
  532. return true;
  533. };
  534. function _initCalendar() {
  535. self.verticalDomainLabel = (self.options.label.position === "top" || self.options.label.position === "bottom");
  536. self.domainVerticalLabelHeight = self.options.label.height === null ? Math.max(25, self.options.cellSize*2): self.options.label.height;
  537. self.domainHorizontalLabelWidth = 0;
  538. if (self.options.domainLabelFormat === "" && self.options.label.height === null) {
  539. self.domainVerticalLabelHeight = 0;
  540. }
  541. if (!self.verticalDomainLabel) {
  542. self.domainVerticalLabelHeight = 0;
  543. self.domainHorizontalLabelWidth = self.options.label.width;
  544. }
  545. self.paint();
  546. // =========================================================================//
  547. // ATTACHING DOMAIN NAVIGATION EVENT //
  548. // =========================================================================//
  549. if (self.options.nextSelector !== false) {
  550. d3.select(self.options.nextSelector).on("click." + self.options.itemNamespace, function() {
  551. d3.event.preventDefault();
  552. return self.loadNextDomain(1);
  553. });
  554. }
  555. if (self.options.previousSelector !== false) {
  556. d3.select(self.options.previousSelector).on("click." + self.options.itemNamespace, function() {
  557. d3.event.preventDefault();
  558. return self.loadPreviousDomain(1);
  559. });
  560. }
  561. self.Legend.redraw(self.graphDim.width - self.options.domainGutter - self.options.cellPadding);
  562. self.afterLoad();
  563. var domains = self.getDomainKeys();
  564. // Fill the graph with some datas
  565. if (self.options.loadOnInit) {
  566. self.getDatas(
  567. self.options.data,
  568. new Date(domains[0]),
  569. self.getSubDomain(domains[domains.length-1]).pop(),
  570. function() {
  571. self.fill();
  572. self.onComplete();
  573. }
  574. );
  575. } else {
  576. self.onComplete();
  577. }
  578. self.checkIfMinDomainIsReached(domains[0]);
  579. self.checkIfMaxDomainIsReached(self.getNextDomain().getTime());
  580. }
  581. // Return the width of the domain block, without the domain gutter
  582. // @param int d Domain start timestamp
  583. function w(d, outer) {
  584. var width = self.options.cellSize*self._domainType[self.options.subDomain].column(d) + self.options.cellPadding*self._domainType[self.options.subDomain].column(d);
  585. if (arguments.length === 2 && outer === true) {
  586. return width += self.domainHorizontalLabelWidth + self.options.domainGutter + self.options.domainMargin[1] + self.options.domainMargin[3];
  587. }
  588. return width;
  589. }
  590. // Return the height of the domain block, without the domain gutter
  591. function h(d, outer) {
  592. var height = self.options.cellSize*self._domainType[self.options.subDomain].row(d) + self.options.cellPadding*self._domainType[self.options.subDomain].row(d);
  593. if (arguments.length === 2 && outer === true) {
  594. height += self.options.domainGutter + self.domainVerticalLabelHeight + self.options.domainMargin[0] + self.options.domainMargin[2];
  595. }
  596. return height;
  597. }
  598. /**
  599. *
  600. *
  601. * @param int navigationDir
  602. */
  603. this.paint = function(navigationDir) {
  604. var options = self.options;
  605. if (arguments.length === 0) {
  606. navigationDir = false;
  607. }
  608. // Painting all the domains
  609. var domainSvg = self.root.select(".graph")
  610. .selectAll(".graph-domain")
  611. .data(
  612. function() {
  613. var data = self.getDomainKeys();
  614. return navigationDir === self.NAVIGATE_LEFT ? data.reverse(): data;
  615. },
  616. function(d) { return d; }
  617. )
  618. ;
  619. var enteringDomainDim = 0;
  620. var exitingDomainDim = 0;
  621. // =========================================================================//
  622. // PAINTING DOMAIN //
  623. // =========================================================================//
  624. var svg = domainSvg
  625. .enter()
  626. .append("svg")
  627. .attr("width", function(d) {
  628. return w(d, true);
  629. })
  630. .attr("height", function(d) {
  631. return h(d, true);
  632. })
  633. .attr("x", function(d) {
  634. if (options.verticalOrientation) {
  635. self.graphDim.width = Math.max(self.graphDim.width, w(d, true));
  636. return 0;
  637. } else {
  638. return getDomainPosition(d, self.graphDim, "width", w(d, true));
  639. }
  640. })
  641. .attr("y", function(d) {
  642. if (options.verticalOrientation) {
  643. return getDomainPosition(d, self.graphDim, "height", h(d, true));
  644. } else {
  645. self.graphDim.height = Math.max(self.graphDim.height, h(d, true));
  646. return 0;
  647. }
  648. })
  649. .attr("class", function(d) {
  650. var classname = "graph-domain";
  651. var date = new Date(d);
  652. switch(options.domain) {
  653. case "hour":
  654. classname += " h_" + date.getHours();
  655. /* falls through */
  656. case "day":
  657. classname += " d_" + date.getDate() + " dy_" + date.getDay();
  658. /* falls through */
  659. case "week":
  660. classname += " w_" + self.getWeekNumber(date);
  661. /* falls through */
  662. case "month":
  663. classname += " m_" + (date.getMonth() + 1);
  664. /* falls through */
  665. case "year":
  666. classname += " y_" + date.getFullYear();
  667. }
  668. return classname;
  669. })
  670. ;
  671. self.lastInsertedSvg = svg;
  672. function getDomainPosition(domainIndex, graphDim, axis, domainDim) {
  673. var tmp = 0;
  674. switch(navigationDir) {
  675. case false:
  676. tmp = graphDim[axis];
  677. graphDim[axis] += domainDim;
  678. self.domainPosition.setPosition(domainIndex, tmp);
  679. return tmp;
  680. case self.NAVIGATE_RIGHT:
  681. self.domainPosition.setPosition(domainIndex, graphDim[axis]);
  682. enteringDomainDim = domainDim;
  683. exitingDomainDim = self.domainPosition.getPositionFromIndex(1);
  684. self.domainPosition.shiftRightBy(exitingDomainDim);
  685. return graphDim[axis];
  686. case self.NAVIGATE_LEFT:
  687. tmp = -domainDim;
  688. enteringDomainDim = -tmp;
  689. exitingDomainDim = graphDim[axis] - self.domainPosition.getLast();
  690. self.domainPosition.setPosition(domainIndex, tmp);
  691. self.domainPosition.shiftLeftBy(enteringDomainDim);
  692. return tmp;
  693. }
  694. }
  695. svg.append("rect")
  696. .attr("width", function(d) { return w(d, true) - options.domainGutter - options.cellPadding; })
  697. .attr("height", function(d) { return h(d, true) - options.domainGutter - options.cellPadding; })
  698. .attr("class", "domain-background")
  699. ;
  700. // =========================================================================//
  701. // PAINTING SUBDOMAINS //
  702. // =========================================================================//
  703. var subDomainSvgGroup = svg.append("svg")
  704. .attr("x", function() {
  705. if (options.label.position === "left") {
  706. return self.domainHorizontalLabelWidth + options.domainMargin[3];
  707. } else {
  708. return options.domainMargin[3];
  709. }
  710. })
  711. .attr("y", function() {
  712. if (options.label.position === "top") {
  713. return self.domainVerticalLabelHeight + options.domainMargin[0];
  714. } else {
  715. return options.domainMargin[0];
  716. }
  717. })
  718. .attr("class", "graph-subdomain-group")
  719. ;
  720. var rect = subDomainSvgGroup
  721. .selectAll("g")
  722. .data(function(d) { return self._domains.get(d); })
  723. .enter()
  724. .append("g")
  725. ;
  726. rect
  727. .append("rect")
  728. .attr("class", function(d) {
  729. return "graph-rect" + self.getHighlightClassName(d.t) + (options.onClick !== null ? " hover_cursor": "");
  730. })
  731. .attr("width", options.cellSize)
  732. .attr("height", options.cellSize)
  733. .attr("x", function(d) { return self.positionSubDomainX(d.t); })
  734. .attr("y", function(d) { return self.positionSubDomainY(d.t); })
  735. .on("click", function(d) {
  736. if (options.onClick !== null) {
  737. return self.onClick(new Date(d.t), d.v);
  738. }
  739. })
  740. .call(function(selection) {
  741. if (options.cellRadius > 0) {
  742. selection
  743. .attr("rx", options.cellRadius)
  744. .attr("ry", options.cellRadius)
  745. ;
  746. }
  747. if (self.legendScale !== null && options.legendColors !== null && options.legendColors.hasOwnProperty("base")) {
  748. selection.attr("fill", options.legendColors.base);
  749. }
  750. if (options.tooltip) {
  751. selection.on("mouseover", function(d) {
  752. var domainNode = this.parentNode.parentNode;
  753. self.tooltip
  754. .html(self.getSubDomainTitle(d))
  755. .attr("style", "display: block;")
  756. ;
  757. var tooltipPositionX = self.positionSubDomainX(d.t) - self.tooltip[0][0].offsetWidth/2 + options.cellSize/2;
  758. var tooltipPositionY = self.positionSubDomainY(d.t) - self.tooltip[0][0].offsetHeight - options.cellSize/2;
  759. // Offset by the domain position
  760. tooltipPositionX += parseInt(domainNode.getAttribute("x"), 10);
  761. tooltipPositionY += parseInt(domainNode.getAttribute("y"), 10);
  762. // Offset by the calendar position (when legend is left/top)
  763. tooltipPositionX += parseInt(self.root.select(".graph").attr("x"), 10);
  764. tooltipPositionY += parseInt(self.root.select(".graph").attr("y"), 10);
  765. // Offset by the inside domain position (when label is left/top)
  766. tooltipPositionX += parseInt(domainNode.parentNode.getAttribute("x"), 10);
  767. tooltipPositionY += parseInt(domainNode.parentNode.getAttribute("y"), 10);
  768. self.tooltip.attr("style",
  769. "display: block; " +
  770. "left: " + tooltipPositionX + "px; " +
  771. "top: " + tooltipPositionY + "px;")
  772. ;
  773. });
  774. selection.on("mouseout", function() {
  775. self.tooltip
  776. .attr("style", "display:none")
  777. .html("");
  778. });
  779. }
  780. })
  781. ;
  782. // Appending a title to each subdomain
  783. if (!options.tooltip) {
  784. rect.append("title").text(function(d){ return self.formatDate(new Date(d.t), options.subDomainDateFormat); });
  785. }
  786. // =========================================================================//
  787. // PAINTING LABEL //
  788. // =========================================================================//
  789. if (options.domainLabelFormat !== "") {
  790. svg.append("text")
  791. .attr("class", "graph-label")
  792. .attr("y", function(d) {
  793. var y = options.domainMargin[0];
  794. switch(options.label.position) {
  795. case "top":
  796. y += self.domainVerticalLabelHeight/2;
  797. break;
  798. case "bottom":
  799. y += h(d) + self.domainVerticalLabelHeight/2;
  800. }
  801. return y + options.label.offset.y *
  802. (
  803. ((options.label.rotate === "right" && options.label.position === "right") ||
  804. (options.label.rotate === "left" && options.label.position === "left")) ?
  805. -1: 1
  806. );
  807. })
  808. .attr("x", function(d){
  809. var x = options.domainMargin[3];
  810. switch(options.label.position) {
  811. case "right":
  812. x += w(d);
  813. break;
  814. case "bottom":
  815. case "top":
  816. x += w(d)/2;
  817. }
  818. if (options.label.align === "right") {
  819. return x + self.domainHorizontalLabelWidth - options.label.offset.x *
  820. (options.label.rotate === "right" ? -1: 1);
  821. }
  822. return x + options.label.offset.x;
  823. })
  824. .attr("text-anchor", function() {
  825. switch(options.label.align) {
  826. case "start":
  827. case "left":
  828. return "start";
  829. case "end":
  830. case "right":
  831. return "end";
  832. default:
  833. return "middle";
  834. }
  835. })
  836. .attr("dominant-baseline", function() { return self.verticalDomainLabel ? "middle": "top"; })
  837. .text(function(d) { return self.formatDate(new Date(d), options.domainLabelFormat); })
  838. .call(domainRotate)
  839. ;
  840. }
  841. function domainRotate(selection) {
  842. switch (options.label.rotate) {
  843. case "right":
  844. selection
  845. .attr("transform", function(d) {
  846. var s = "rotate(90), ";
  847. switch(options.label.position) {
  848. case "right":
  849. s += "translate(-" + w(d) + " , -" + w(d) + ")";
  850. break;
  851. case "left":
  852. s += "translate(0, -" + self.domainHorizontalLabelWidth + ")";
  853. break;
  854. }
  855. return s;
  856. });
  857. break;
  858. case "left":
  859. selection
  860. .attr("transform", function(d) {
  861. var s = "rotate(270), ";
  862. switch(options.label.position) {
  863. case "right":
  864. s += "translate(-" + (w(d) + self.domainHorizontalLabelWidth) + " , " + w(d) + ")";
  865. break;
  866. case "left":
  867. s += "translate(-" + (self.domainHorizontalLabelWidth) + " , " + self.domainHorizontalLabelWidth + ")";
  868. break;
  869. }
  870. return s;
  871. });
  872. break;
  873. }
  874. }
  875. // =========================================================================//
  876. // PAINTING DOMAIN SUBDOMAIN CONTENT //
  877. // =========================================================================//
  878. if (options.subDomainTextFormat !== null) {
  879. rect
  880. .append("text")
  881. .attr("class", function(d) { return "subdomain-text" + self.getHighlightClassName(d.t); })
  882. .attr("x", function(d) { return self.positionSubDomainX(d.t) + options.cellSize/2; })
  883. .attr("y", function(d) { return self.positionSubDomainY(d.t) + options.cellSize/2; })
  884. .attr("text-anchor", "middle")
  885. .attr("dominant-baseline", "central")
  886. .text(function(d){
  887. return self.formatDate(new Date(d.t), options.subDomainTextFormat);
  888. })
  889. ;
  890. }
  891. // =========================================================================//
  892. // ANIMATION //
  893. // =========================================================================//
  894. if (navigationDir !== false) {
  895. domainSvg.transition().duration(options.animationDuration)
  896. .attr("x", function(d){
  897. return options.verticalOrientation ? 0: self.domainPosition.getPosition(d);
  898. })
  899. .attr("y", function(d){
  900. return options.verticalOrientation? self.domainPosition.getPosition(d): 0;
  901. })
  902. ;
  903. }
  904. var tempWidth = self.graphDim.width;
  905. var tempHeight = self.graphDim.height;
  906. if (options.verticalOrientation) {
  907. self.graphDim.height += enteringDomainDim - exitingDomainDim;
  908. } else {
  909. self.graphDim.width += enteringDomainDim - exitingDomainDim;
  910. }
  911. // At the time of exit, domainsWidth and domainsHeight already automatically shifted
  912. domainSvg.exit().transition().duration(options.animationDuration)
  913. .attr("x", function(d){
  914. if (options.verticalOrientation) {
  915. return 0;
  916. } else {
  917. switch(navigationDir) {
  918. case self.NAVIGATE_LEFT:
  919. return Math.min(self.graphDim.width, tempWidth);
  920. case self.NAVIGATE_RIGHT:
  921. return -w(d, true);
  922. }
  923. }
  924. })
  925. .attr("y", function(d){
  926. if (options.verticalOrientation) {
  927. switch(navigationDir) {
  928. case self.NAVIGATE_LEFT:
  929. return Math.min(self.graphDim.height, tempHeight);
  930. case self.NAVIGATE_RIGHT:
  931. return -h(d, true);
  932. }
  933. } else {
  934. return 0;
  935. }
  936. })
  937. .remove()
  938. ;
  939. // Resize the root container
  940. self.resize();
  941. };
  942. };
  943. CalHeatMap.prototype = {
  944. /**
  945. * Validate and merge user settings with default settings
  946. *
  947. * @param {object} settings User settings
  948. * @return {bool} False if settings contains error
  949. */
  950. /* jshint maxstatements:false */
  951. init: function(settings) {
  952. "use strict";
  953. var parent = this;
  954. var options = parent.options = mergeRecursive(parent.options, settings);
  955. // Fatal errors
  956. // Stop script execution on error
  957. validateDomainType();
  958. validateSelector(options.itemSelector, false, "itemSelector");
  959. if (parent.allowedDataType.indexOf(options.dataType) === -1) {
  960. throw new Error("The data type '" + options.dataType + "' is not valid data type");
  961. }
  962. if (d3.select(options.itemSelector)[0][0] === null) {
  963. throw new Error("The node '" + options.itemSelector + "' specified in itemSelector does not exists");
  964. }
  965. try {
  966. validateSelector(options.nextSelector, true, "nextSelector");
  967. validateSelector(options.previousSelector, true, "previousSelector");
  968. } catch(error) {
  969. console.log(error.message);
  970. return false;
  971. }
  972. // If other settings contains error, will fallback to default
  973. if (!settings.hasOwnProperty("subDomain")) {
  974. this.options.subDomain = getOptimalSubDomain(settings.domain);
  975. }
  976. if (typeof options.itemNamespace !== "string" || options.itemNamespace === "") {
  977. console.log("itemNamespace can not be empty, falling back to cal-heatmap");
  978. options.itemNamespace = "cal-heatmap";
  979. }
  980. // Don't touch these settings
  981. var s = ["data", "onComplete", "onClick", "afterLoad", "afterLoadData", "afterLoadPreviousDomain", "afterLoadNextDomain"];
  982. for (var k in s) {
  983. if (settings.hasOwnProperty(s[k])) {
  984. options[s[k]] = settings[s[k]];
  985. }
  986. }
  987. options.subDomainDateFormat = (typeof options.subDomainDateFormat === "string" || typeof options.subDomainDateFormat === "function" ? options.subDomainDateFormat : this._domainType[options.subDomain].format.date);
  988. options.domainLabelFormat = (typeof options.domainLabelFormat === "string" || typeof options.domainLabelFormat === "function" ? options.domainLabelFormat : this._domainType[options.domain].format.legend);
  989. options.subDomainTextFormat = ((typeof options.subDomainTextFormat === "string" && options.subDomainTextFormat !== "") || typeof options.subDomainTextFormat === "function" ? options.subDomainTextFormat : null);
  990. options.domainMargin = expandMarginSetting(options.domainMargin);
  991. options.legendMargin = expandMarginSetting(options.legendMargin);
  992. options.highlight = parent.expandDateSetting(options.highlight);
  993. options.itemName = expandItemName(options.itemName);
  994. options.colLimit = parseColLimit(options.colLimit);
  995. options.rowLimit = parseRowLimit(options.rowLimit);
  996. if (!settings.hasOwnProperty("legendMargin")) {
  997. autoAddLegendMargin();
  998. }
  999. autoAlignLabel();
  1000. /**
  1001. * Validate that a queryString is valid
  1002. *
  1003. * @param {Element|string|bool} selector The queryString to test
  1004. * @param {bool} canBeFalse Whether false is an accepted and valid value
  1005. * @param {string} name Name of the tested selector
  1006. * @throws {Error} If the selector is not valid
  1007. * @return {bool} True if the selector is a valid queryString
  1008. */
  1009. function validateSelector(selector, canBeFalse, name) {
  1010. if (((canBeFalse && selector === false) || selector instanceof Element || typeof selector === "string") && selector !== "") {
  1011. return true;
  1012. }
  1013. throw new Error("The " + name + " is not valid");
  1014. }
  1015. /**
  1016. * Return the optimal subDomain for the specified domain
  1017. *
  1018. * @param {string} domain a domain name
  1019. * @return {string} the subDomain name
  1020. */
  1021. function getOptimalSubDomain(domain) {
  1022. switch(domain) {
  1023. case "year":
  1024. return "month";
  1025. case "month":
  1026. return "day";
  1027. case "week":
  1028. return "day";
  1029. case "day":
  1030. return "hour";
  1031. default:
  1032. return "min";
  1033. }
  1034. }
  1035. /**
  1036. * Ensure that the domain and subdomain are valid
  1037. *
  1038. * @throw {Error} when domain or subdomain are not valid
  1039. * @return {bool} True if domain and subdomain are valid and compatible
  1040. */
  1041. function validateDomainType() {
  1042. if (!parent._domainType.hasOwnProperty(options.domain) || options.domain === "min" || options.domain.substring(0, 2) === "x_") {
  1043. throw new Error("The domain '" + options.domain + "' is not valid");
  1044. }
  1045. if (!parent._domainType.hasOwnProperty(options.subDomain) || options.subDomain === "year") {
  1046. throw new Error("The subDomain '" + options.subDomain + "' is not valid");
  1047. }
  1048. if (parent._domainType[options.domain].level <= parent._domainType[options.subDomain].level) {
  1049. throw new Error("'" + options.subDomain + "' is not a valid subDomain to '" + options.domain + "'");
  1050. }
  1051. return true;
  1052. }
  1053. /**
  1054. * Fine-tune the label alignement depending on its position
  1055. *
  1056. * @return void
  1057. */
  1058. function autoAlignLabel() {
  1059. // Auto-align label, depending on it's position
  1060. if (!settings.hasOwnProperty("label") || (settings.hasOwnProperty("label") && !settings.label.hasOwnProperty("align"))) {
  1061. switch(options.label.position) {
  1062. case "left":
  1063. options.label.align = "right";
  1064. break;
  1065. case "right":
  1066. options.label.align = "left";
  1067. break;
  1068. default:
  1069. options.label.align = "center";
  1070. }
  1071. if (options.label.rotate === "left") {
  1072. options.label.align = "right";
  1073. } else if (options.label.rotate === "right") {
  1074. options.label.align = "left";
  1075. }
  1076. }
  1077. if (!settings.hasOwnProperty("label") || (settings.hasOwnProperty("label") && !settings.label.hasOwnProperty("offset"))) {
  1078. if (options.label.position === "left" || options.label.position === "right") {
  1079. options.label.offset = {
  1080. x: 10,
  1081. y: 15
  1082. };
  1083. }
  1084. }
  1085. }
  1086. /**
  1087. * If not specified, add some margin around the legend depending on its position
  1088. *
  1089. * @return void
  1090. */
  1091. function autoAddLegendMargin() {
  1092. switch(options.legendVerticalPosition) {
  1093. case "top":
  1094. options.legendMargin[2] = parent.DEFAULT_LEGEND_MARGIN;
  1095. break;
  1096. case "bottom":
  1097. options.legendMargin[0] = parent.DEFAULT_LEGEND_MARGIN;
  1098. break;
  1099. case "middle":
  1100. case "center":
  1101. options.legendMargin[options.legendHorizontalPosition === "right" ? 3 : 1] = parent.DEFAULT_LEGEND_MARGIN;
  1102. }
  1103. }
  1104. /**
  1105. * Expand a number of an array of numbers to an usable 4 values array
  1106. *
  1107. * @param {integer|array} value
  1108. * @return {array} array
  1109. */
  1110. function expandMarginSetting(value) {
  1111. if (typeof value === "number") {
  1112. value = [value];
  1113. }
  1114. if (!Array.isArray(value)) {
  1115. console.log("Margin only takes an integer or an array of integers");
  1116. value = [0];
  1117. }
  1118. switch(value.length) {
  1119. case 1:
  1120. return [value[0], value[0], value[0], value[0]];
  1121. case 2:
  1122. return [value[0], value[1], value[0], value[1]];
  1123. case 3:
  1124. return [value[0], value[1], value[2], value[1]];
  1125. case 4:
  1126. return value;
  1127. default:
  1128. return value.slice(0, 4);
  1129. }
  1130. }
  1131. /**
  1132. * Convert a string to an array like [singular-form, plural-form]
  1133. *
  1134. * @param {string|array} value Date to convert
  1135. * @return {array} An array like [singular-form, plural-form]
  1136. */
  1137. function expandItemName(value) {
  1138. if (typeof value === "string") {
  1139. return [value, value + (value !== "" ? "s" : "")];
  1140. }
  1141. if (Array.isArray(value)) {
  1142. if (value.length === 1) {
  1143. return [value[0], value[0] + "s"];
  1144. } else if (value.length > 2) {
  1145. return value.slice(0, 2);
  1146. }
  1147. return value;
  1148. }
  1149. return ["item", "items"];
  1150. }
  1151. function parseColLimit(value) {
  1152. return value > 0 ? value : null;
  1153. }
  1154. function parseRowLimit(value) {
  1155. if (value > 0 && options.colLimit > 0) {
  1156. console.log("colLimit and rowLimit are mutually exclusive, rowLimit will be ignored");
  1157. return null;
  1158. }
  1159. return value > 0 ? value : null;
  1160. }
  1161. return this._init();
  1162. },
  1163. /**
  1164. * Convert a keyword or an array of keyword/date to an array of date objects
  1165. *
  1166. * @param {string|array|Date} value Data to convert
  1167. * @return {array} An array of Dates
  1168. */
  1169. expandDateSetting: function(value) {
  1170. "use strict";
  1171. if (!Array.isArray(value)) {
  1172. value = [value];
  1173. }
  1174. return value.map(function(data) {
  1175. if (data === "now") {
  1176. return new Date();
  1177. }
  1178. if (data instanceof Date) {
  1179. return data;
  1180. }
  1181. return false;
  1182. }).filter(function(d) { return d !== false; });
  1183. },
  1184. /**
  1185. * Fill the calendar by coloring the cells
  1186. *
  1187. * @param array svg An array of html node to apply the transformation to (optional)
  1188. * It's used to limit the painting to only a subset of the calendar
  1189. * @return void
  1190. */
  1191. fill: function(svg) {
  1192. "use strict";
  1193. var parent = this;
  1194. var options = parent.options;
  1195. if (arguments.length === 0) {
  1196. svg = parent.root.selectAll(".graph-domain");
  1197. }
  1198. var rect = svg
  1199. .selectAll("svg").selectAll("g")
  1200. .data(function(d) { return parent._domains.get(d); })
  1201. ;
  1202. /**
  1203. * Colorize the cell via a style attribute if enabled
  1204. */
  1205. function addStyle(element) {
  1206. if (parent.legendScale === null) {
  1207. return false;
  1208. }
  1209. element.attr("fill", function(d) {
  1210. if (d.v === null && (options.hasOwnProperty("considerMissingDataAsZero") && !options.considerMissingDataAsZero)) {
  1211. if (options.legendColors.hasOwnProperty("base")) {
  1212. return options.legendColors.base;
  1213. }
  1214. }
  1215. if (options.legendColors !== null && options.legendColors.hasOwnProperty("empty") &&
  1216. (d.v === 0 || (d.v === null && options.hasOwnProperty("considerMissingDataAsZero") && options.considerMissingDataAsZero))
  1217. ) {
  1218. return options.legendColors.empty;
  1219. }
  1220. if (d.v < 0 && options.legend[0] > 0 && options.legendColors !== null && options.legendColors.hasOwnProperty("overflow")) {
  1221. return options.legendColors.overflow;
  1222. }
  1223. return parent.legendScale(Math.min(d.v, options.legend[options.legend.length-1]));
  1224. });
  1225. }
  1226. rect.transition().duration(options.animationDuration).select("rect")
  1227. .attr("class", function(d) {
  1228. var htmlClass = parent.getHighlightClassName(d.t).trim().split(" ");
  1229. var pastDate = parent.dateIsLessThan(d.t, new Date());
  1230. var sameDate = parent.dateIsEqual(d.t, new Date());
  1231. if (parent.legendScale === null ||
  1232. (d.v === null && (options.hasOwnProperty("considerMissingDataAsZero") && !options.considerMissingDataAsZero) &&!options.legendColors.hasOwnProperty("base"))
  1233. ) {
  1234. htmlClass.push("graph-rect");
  1235. }
  1236. if (sameDate) {
  1237. htmlClass.push("now");
  1238. } else if (!pastDate) {
  1239. htmlClass.push("future");
  1240. }
  1241. if (d.v !== null) {
  1242. htmlClass.push(parent.Legend.getClass(d.v, (parent.legendScale === null)));
  1243. } else if (options.considerMissingDataAsZero && pastDate) {
  1244. htmlClass.push(parent.Legend.getClass(0, (parent.legendScale === null)));
  1245. }
  1246. if (options.onClick !== null) {
  1247. htmlClass.push("hover_cursor");
  1248. }
  1249. return htmlClass.join(" ");
  1250. })
  1251. .call(addStyle)
  1252. ;
  1253. rect.transition().duration(options.animationDuration).select("title")
  1254. .text(function(d) { return parent.getSubDomainTitle(d); })
  1255. ;
  1256. function formatSubDomainText(element) {
  1257. if (typeof options.subDomainTextFormat === "function") {
  1258. element.text(function(d) { return options.subDomainTextFormat(d.t, d.v); });
  1259. }
  1260. }
  1261. /**
  1262. * Change the subDomainText class if necessary
  1263. * Also change the text, e.g when text is representing the value
  1264. * instead of the date
  1265. */
  1266. rect.transition().duration(options.animationDuration).select("text")
  1267. .attr("class", function(d) { return "subdomain-text" + parent.getHighlightClassName(d.t); })
  1268. .call(formatSubDomainText)
  1269. ;
  1270. },
  1271. // =========================================================================//
  1272. // EVENTS CALLBACK //
  1273. // =========================================================================//
  1274. /**
  1275. * Helper method for triggering event callback
  1276. *
  1277. * @param string eventName Name of the event to trigger
  1278. * @param array successArgs List of argument to pass to the callback
  1279. * @param boolean skip Whether to skip the event triggering
  1280. * @return mixed True when the triggering was skipped, false on error, else the callback function
  1281. */
  1282. triggerEvent: function(eventName, successArgs, skip) {
  1283. "use strict";
  1284. if ((arguments.length === 3 && skip) || this.options[eventName] === null) {
  1285. return true;
  1286. }
  1287. if (typeof this.options[eventName] === "function") {
  1288. if (typeof successArgs === "function") {
  1289. successArgs = successArgs();
  1290. }
  1291. return this.options[eventName].apply(this, successArgs);
  1292. } else {
  1293. console.log("Provided callback for " + eventName + " is not a function.");
  1294. return false;
  1295. }
  1296. },
  1297. /**
  1298. * Event triggered on a mouse click on a subDomain cell
  1299. *
  1300. * @param Date d Date of the subdomain block
  1301. * @param int itemNb Number of items in that date
  1302. */
  1303. onClick: function(d, itemNb) {
  1304. "use strict";
  1305. return this.triggerEvent("onClick", [d, itemNb]);
  1306. },
  1307. /**
  1308. * Event triggered after drawing the calendar, byt before filling it with data
  1309. */
  1310. afterLoad: function() {
  1311. "use strict";
  1312. return this.triggerEvent("afterLoad");
  1313. },
  1314. /**
  1315. * Event triggered after completing drawing and filling the calendar
  1316. */
  1317. onComplete: function() {
  1318. "use strict";
  1319. var response = this.triggerEvent("onComplete", [], this._completed);
  1320. this._completed = true;
  1321. return response;
  1322. },
  1323. /**
  1324. * Event triggered after shifting the calendar one domain back
  1325. *
  1326. * @param Date start Domain start date
  1327. * @param Date end Domain end date
  1328. */
  1329. afterLoadPreviousDomain: function(start) {
  1330. "use strict";
  1331. var parent = this;
  1332. return this.triggerEvent("afterLoadPreviousDomain", function() {
  1333. var subDomain = parent.getSubDomain(start);
  1334. return [subDomain.shift(), subDomain.pop()];
  1335. });
  1336. },
  1337. /**
  1338. * Event triggered after shifting the calendar one domain above
  1339. *
  1340. * @param Date start Domain start date
  1341. * @param Date end Domain end date
  1342. */
  1343. afterLoadNextDomain: function(start) {
  1344. "use strict";
  1345. var parent = this;
  1346. return this.triggerEvent("afterLoadNextDomain", function() {
  1347. var subDomain = parent.getSubDomain(start);
  1348. return [subDomain.shift(), subDomain.pop()];
  1349. });
  1350. },
  1351. /**
  1352. * Event triggered after loading the leftmost domain allowed by minDate
  1353. *
  1354. * @param boolean reached True if the leftmost domain was reached
  1355. */
  1356. onMinDomainReached: function(reached) {
  1357. "use strict";
  1358. this._minDomainReached = reached;
  1359. return this.triggerEvent("onMinDomainReached", [reached]);
  1360. },
  1361. /**
  1362. * Event triggered after loading the rightmost domain allowed by maxDate
  1363. *
  1364. * @param boolean reached True if the rightmost domain was reached
  1365. */
  1366. onMaxDomainReached: function(reached) {
  1367. "use strict";
  1368. this._maxDomainReached = reached;
  1369. return this.triggerEvent("onMaxDomainReached", [reached]);
  1370. },
  1371. checkIfMinDomainIsReached: function(date, upperBound) {
  1372. "use strict";
  1373. if (this.minDomainIsReached(date)) {
  1374. this.onMinDomainReached(true);
  1375. }
  1376. if (arguments.length === 2) {
  1377. if (this._maxDomainReached && !this.maxDomainIsReached(upperBound)) {
  1378. this.onMaxDomainReached(false);
  1379. }
  1380. }
  1381. },
  1382. checkIfMaxDomainIsReached: function(date, lowerBound) {
  1383. "use strict";
  1384. if (this.maxDomainIsReached(date)) {
  1385. this.onMaxDomainReached(true);
  1386. }
  1387. if (arguments.length === 2) {
  1388. if (this._minDomainReached && !this.minDomainIsReached(lowerBound)) {
  1389. this.onMinDomainReached(false);
  1390. }
  1391. }
  1392. },
  1393. // =========================================================================//
  1394. // FORMATTER //
  1395. // =========================================================================//
  1396. formatNumber: d3.format(",g"),
  1397. formatDate: function(d, format) {
  1398. "use strict";
  1399. if (arguments.length < 2) {
  1400. format = "title";
  1401. }
  1402. if (typeof format === "function") {
  1403. return format(d);
  1404. } else {
  1405. var f = d3.time.format(format);
  1406. return f(d);
  1407. }
  1408. },
  1409. getSubDomainTitle: function(d) {
  1410. "use strict";
  1411. if (d.v === null && !this.options.considerMissingDataAsZero) {
  1412. return (this.options.subDomainTitleFormat.empty).format({
  1413. date: this.formatDate(new Date(d.t), this.options.subDomainDateFormat)
  1414. });
  1415. } else {
  1416. var value = d.v;
  1417. // Consider null as 0
  1418. if (value === null && this.options.considerMissingDataAsZero) {
  1419. value = 0;
  1420. }
  1421. return (this.options.subDomainTitleFormat.filled).format({
  1422. count: this.formatNumber(value),
  1423. name: this.options.itemName[(value !== 1 ? 1: 0)],
  1424. connector: this._domainType[this.options.subDomain].format.connector,
  1425. date: this.formatDate(new Date(d.t), this.options.subDomainDateFormat)
  1426. });
  1427. }
  1428. },
  1429. // =========================================================================//
  1430. // DOMAIN NAVIGATION //
  1431. // =========================================================================//
  1432. /**
  1433. * Shift the calendar one domain forward
  1434. *
  1435. * The new domain is loaded only if it's not beyond maxDate
  1436. *
  1437. * @param int n Number of domains to load
  1438. * @return bool True if the next domain was loaded, else false
  1439. */
  1440. loadNextDomain: function(n) {
  1441. "use strict";
  1442. if (this._maxDomainReached || n === 0) {
  1443. return false;
  1444. }
  1445. var bound = this.loadNewDomains(this.NAVIGATE_RIGHT, this.getDomain(this.getNextDomain(), n));
  1446. this.afterLoadNextDomain(bound.end);
  1447. this.checkIfMaxDomainIsReached(this.getNextDomain().getTime(), bound.start);
  1448. return true;
  1449. },
  1450. /**
  1451. * Shift the calendar one domain backward
  1452. *
  1453. * The previous domain is loaded only if it's not beyond the minDate
  1454. *
  1455. * @param int n Number of domains to load
  1456. * @return bool True if the previous domain was loaded, else false
  1457. */
  1458. loadPreviousDomain: function(n) {
  1459. "use strict";
  1460. if (this._minDomainReached || n === 0) {
  1461. return false;
  1462. }
  1463. var bound = this.loadNewDomains(this.NAVIGATE_LEFT, this.getDomain(this.getDomainKeys()[0], -n).reverse());
  1464. this.afterLoadPreviousDomain(bound.start);
  1465. this.checkIfMinDomainIsReached(bound.start, bound.end);
  1466. return true;
  1467. },
  1468. loadNewDomains: function(direction, newDomains) {
  1469. "use strict";
  1470. var parent = this;
  1471. var backward = direction === this.NAVIGATE_LEFT;
  1472. var i = -1;
  1473. var total = newDomains.length;
  1474. var domains = this.getDomainKeys();
  1475. function buildSubDomain(d) {
  1476. return {t: parent._domainType[parent.options.subDomain].extractUnit(d), v: null};
  1477. }
  1478. // Remove out of bound domains from list of new domains to prepend
  1479. while (++i < total) {
  1480. if (backward && this.minDomainIsReached(newDomains[i])) {
  1481. newDomains = newDomains.slice(0, i+1);
  1482. break;
  1483. }
  1484. if (!backward && this.maxDomainIsReached(newDomains[i])) {
  1485. newDomains = newDomains.slice(0, i);
  1486. break;
  1487. }
  1488. }
  1489. newDomains = newDomains.slice(-this.options.range);
  1490. for (i = 0, total = newDomains.length; i < total; i++) {
  1491. this._domains.set(
  1492. newDomains[i].getTime(),
  1493. this.getSubDomain(newDomains[i]).map(buildSubDomain)
  1494. );
  1495. this._domains.remove(backward ? domains.pop() : domains.shift());
  1496. }
  1497. domains = this.getDomainKeys();
  1498. if (backward) {
  1499. newDomains = newDomains.reverse();
  1500. }
  1501. this.paint(direction);
  1502. this.getDatas(
  1503. this.options.data,
  1504. newDomains[0],
  1505. this.getSubDomain(newDomains[newDomains.length-1]).pop(),
  1506. function() {
  1507. parent.fill(parent.lastInsertedSvg);
  1508. }
  1509. );
  1510. return {
  1511. start: newDomains[backward ? 0 : 1],
  1512. end: domains[domains.length-1]
  1513. };
  1514. },
  1515. /**
  1516. * Return whether a date is inside the scope determined by maxDate
  1517. *
  1518. * @param int datetimestamp The timestamp in ms to test
  1519. * @return bool True if the specified date correspond to the calendar upper bound
  1520. */
  1521. maxDomainIsReached: function(datetimestamp) {
  1522. "use strict";
  1523. return (this.options.maxDate !== null && (this.options.maxDate.getTime() < datetimestamp));
  1524. },
  1525. /**
  1526. * Return whether a date is inside the scope determined by minDate
  1527. *
  1528. * @param int datetimestamp The timestamp in ms to test
  1529. * @return bool True if the specified date correspond to the calendar lower bound
  1530. */
  1531. minDomainIsReached: function (datetimestamp) {
  1532. "use strict";
  1533. return (this.options.minDate !== null && (this.options.minDate.getTime() >= datetimestamp));
  1534. },
  1535. /**
  1536. * Return the list of the calendar's domain timestamp
  1537. *
  1538. * @return Array a sorted array of timestamp
  1539. */
  1540. getDomainKeys: function() {
  1541. "use strict";
  1542. return this._domains.keys()
  1543. .map(function(d) { return parseInt(d, 10); })
  1544. .sort(function(a,b) { return a-b; });
  1545. },
  1546. // =========================================================================//
  1547. // POSITIONNING //
  1548. // =========================================================================//
  1549. positionSubDomainX: function(d) {
  1550. "use strict";
  1551. var index = this._domainType[this.options.subDomain].position.x(new Date(d));
  1552. return index * this.options.cellSize + index * this.options.cellPadding;
  1553. },
  1554. positionSubDomainY: function(d) {
  1555. "use strict";
  1556. var index = this._domainType[this.options.subDomain].position.y(new Date(d));
  1557. return index * this.options.cellSize + index * this.options.cellPadding;
  1558. },
  1559. getSubDomainColumnNumber: function(d) {
  1560. "use strict";
  1561. if (this.options.rowLimit > 0) {
  1562. var i = this._domainType[this.options.subDomain].maxItemNumber;
  1563. if (typeof i === "function") {
  1564. i = i(d);
  1565. }
  1566. return Math.ceil(i / this.options.rowLimit);
  1567. }
  1568. var j = this._domainType[this.options.subDomain].defaultColumnNumber;
  1569. if (typeof j === "function") {
  1570. j = j(d);
  1571. }
  1572. return this.options.colLimit || j;
  1573. },
  1574. getSubDomainRowNumber: function(d) {
  1575. "use strict";
  1576. if (this.options.colLimit > 0) {
  1577. var i = this._domainType[this.options.subDomain].maxItemNumber;
  1578. if (typeof i === "function") {
  1579. i = i(d);
  1580. }
  1581. return Math.ceil(i / this.options.colLimit);
  1582. }
  1583. var j = this._domainType[this.options.subDomain].defaultRowNumber;
  1584. if (typeof j === "function") {
  1585. j = j(d);
  1586. }
  1587. return this.options.rowLimit || j;
  1588. },
  1589. /**
  1590. * Return a classname if the specified date should be highlighted
  1591. *
  1592. * @param timestamp date Date of the current subDomain
  1593. * @return String the highlight class
  1594. */
  1595. getHighlightClassName: function(d) {
  1596. "use strict";
  1597. d = new Date(d);
  1598. if (this.options.highlight.length > 0) {
  1599. for (var i in this.options.highlight) {
  1600. if (this.dateIsEqual(this.options.highlight[i], d)) {
  1601. return this.isNow(this.options.highlight[i]) ? " highlight-now": " highlight";
  1602. }
  1603. }
  1604. }
  1605. return "";
  1606. },
  1607. /**
  1608. * Return whether the specified date is now,
  1609. * according to the type of subdomain
  1610. *
  1611. * @param Date d The date to compare
  1612. * @return bool True if the date correspond to a subdomain cell
  1613. */
  1614. isNow: function(d) {
  1615. "use strict";
  1616. return this.dateIsEqual(d, new Date());
  1617. },
  1618. /**
  1619. * Return whether 2 dates are equals
  1620. * This function is subdomain-aware,
  1621. * and dates comparison are dependent of the subdomain
  1622. *
  1623. * @param Date dateA First date to compare
  1624. * @param Date dateB Secon date to compare
  1625. * @return bool true if the 2 dates are equals
  1626. */
  1627. /* jshint maxcomplexity: false */
  1628. dateIsEqual: function(dateA, dateB) {
  1629. "use strict";
  1630. if(!(dateA instanceof Date)) {
  1631. dateA = new Date(dateA);
  1632. }
  1633. if (!(dateB instanceof Date)) {
  1634. dateB = new Date(dateB);
  1635. }
  1636. switch(this.options.subDomain) {
  1637. case "x_min":
  1638. case "min":
  1639. return dateA.getFullYear() === dateB.getFullYear() &&
  1640. dateA.getMonth() === dateB.getMonth() &&
  1641. dateA.getDate() === dateB.getDate() &&
  1642. dateA.getHours() === dateB.getHours() &&
  1643. dateA.getMinutes() === dateB.getMinutes();
  1644. case "x_hour":
  1645. case "hour":
  1646. return dateA.getFullYear() === dateB.getFullYear() &&
  1647. dateA.getMonth() === dateB.getMonth() &&
  1648. dateA.getDate() === dateB.getDate() &&
  1649. dateA.getHours() === dateB.getHours();
  1650. case "x_day":
  1651. case "day":
  1652. return dateA.getFullYear() === dateB.getFullYear() &&
  1653. dateA.getMonth() === dateB.getMonth() &&
  1654. dateA.getDate() === dateB.getDate();
  1655. case "x_week":
  1656. case "week":
  1657. return dateA.getFullYear() === dateB.getFullYear() &&
  1658. this.getWeekNumber(dateA) === this.getWeekNumber(dateB);
  1659. case "x_month":
  1660. case "month":
  1661. return dateA.getFullYear() === dateB.getFullYear() &&
  1662. dateA.getMonth() === dateB.getMonth();
  1663. default:
  1664. return false;
  1665. }
  1666. },
  1667. /**
  1668. * Returns wether or not dateA is less than or equal to dateB. This function is subdomain aware.
  1669. * Performs automatic conversion of values.
  1670. * @param dateA may be a number or a Date
  1671. * @param dateB may be a number or a Date
  1672. * @returns {boolean}
  1673. */
  1674. dateIsLessThan: function(dateA, dateB) {
  1675. "use strict";
  1676. if(!(dateA instanceof Date)) {
  1677. dateA = new Date(dateA);
  1678. }
  1679. if (!(dateB instanceof Date)) {
  1680. dateB = new Date(dateB);
  1681. }
  1682. function normalizedMillis(date, subdomain) {
  1683. switch(subdomain) {
  1684. case "x_min":
  1685. case "min":
  1686. return new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes()).getTime();
  1687. case "x_hour":
  1688. case "hour":
  1689. return new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours()).getTime();
  1690. case "x_day":
  1691. case "day":
  1692. return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
  1693. case "x_week":
  1694. case "week":
  1695. case "x_month":
  1696. case "month":
  1697. return new Date(date.getFullYear(), date.getMonth()).getTime();
  1698. default:
  1699. return date.getTime();
  1700. }
  1701. }
  1702. return normalizedMillis(dateA, this.options.subDomain) < normalizedMillis(dateB, this.options.subDomain);
  1703. },
  1704. // =========================================================================//
  1705. // DATE COMPUTATION //
  1706. // =========================================================================//
  1707. /**
  1708. * Return the day of the year for the date
  1709. * @param Date
  1710. * @return int Day of the year [1,366]
  1711. */
  1712. getDayOfYear: d3.time.format("%j"),
  1713. /**
  1714. * Return the week number of the year
  1715. * Monday as the first day of the week
  1716. * @return int Week number [0-53]
  1717. */
  1718. getWeekNumber: function(d) {
  1719. "use strict";
  1720. var f = this.options.weekStartOnMonday === true ? d3.time.format("%W"): d3.time.format("%U");
  1721. return f(d);
  1722. },
  1723. /**
  1724. * Return the week number, relative to its month
  1725. *
  1726. * @param int|Date d Date or timestamp in milliseconds
  1727. * @return int Week number, relative to the month [0-5]
  1728. */
  1729. getMonthWeekNumber: function (d) {
  1730. "use strict";
  1731. if (typeof d === "number") {
  1732. d = new Date(d);
  1733. }
  1734. var monthFirstWeekNumber = this.getWeekNumber(new Date(d.getFullYear(), d.getMonth()));
  1735. return this.getWeekNumber(d) - monthFirstWeekNumber - 1;
  1736. },
  1737. /**
  1738. * Return the number of weeks in the dates' year
  1739. *
  1740. * @param int|Date d Date or timestamp in milliseconds
  1741. * @return int Number of weeks in the date's year
  1742. */
  1743. getWeekNumberInYear: function(d) {
  1744. "use strict";
  1745. if (typeof d === "number") {
  1746. d = new Date(d);
  1747. }
  1748. },
  1749. /**
  1750. * Return the number of days in the date's month
  1751. *
  1752. * @param int|Date d Date or timestamp in milliseconds
  1753. * @return int Number of days in the date's month
  1754. */
  1755. getDayCountInMonth: function(d) {
  1756. "use strict";
  1757. return this.getEndOfMonth(d).getDate();
  1758. },
  1759. /**
  1760. * Return the number of days in the date's year
  1761. *
  1762. * @param int|Date d Date or timestamp in milliseconds
  1763. * @return int Number of days in the date's year
  1764. */
  1765. getDayCountInYear: function(d) {
  1766. "use strict";
  1767. if (typeof d === "number") {
  1768. d = new Date(d);
  1769. }
  1770. return (new Date(d.getFullYear(), 1, 29).getMonth() === 1) ? 366 : 365;
  1771. },
  1772. /**
  1773. * Get the weekday from a date
  1774. *
  1775. * Return the week day number (0-6) of a date,
  1776. * depending on whether the week start on monday or sunday
  1777. *
  1778. * @param Date d
  1779. * @return int The week day number (0-6)
  1780. */
  1781. getWeekDay: function(d) {
  1782. "use strict";
  1783. if (this.options.weekStartOnMonday === false) {
  1784. return d.getDay();
  1785. }
  1786. return d.getDay() === 0 ? 6 : (d.getDay()-1);
  1787. },
  1788. /**
  1789. * Get the last day of the month
  1790. * @param Date|int d Date or timestamp in milliseconds
  1791. * @return Date Last day of the month
  1792. */
  1793. getEndOfMonth: function(d) {
  1794. "use strict";
  1795. if (typeof d === "number") {
  1796. d = new Date(d);
  1797. }
  1798. return new Date(d.getFullYear(), d.getMonth()+1, 0);
  1799. },
  1800. /**
  1801. *
  1802. * @param Date date
  1803. * @param int count
  1804. * @param string step
  1805. * @return Date
  1806. */
  1807. jumpDate: function(date, count, step) {
  1808. "use strict";
  1809. var d = new Date(date);
  1810. switch(step) {
  1811. case "hour":
  1812. d.setHours(d.getHours() + count);
  1813. break;
  1814. case "day":
  1815. d.setHours(d.getHours() + count * 24);
  1816. break;
  1817. case "week":
  1818. d.setHours(d.getHours() + count * 24 * 7);
  1819. break;
  1820. case "month":
  1821. d.setMonth(d.getMonth() + count);
  1822. break;
  1823. case "year":
  1824. d.setFullYear(d.getFullYear() + count);
  1825. }
  1826. return new Date(d);
  1827. },
  1828. // =========================================================================//
  1829. // DOMAIN COMPUTATION //
  1830. // =========================================================================//
  1831. /**
  1832. * Return all the minutes between 2 dates
  1833. *
  1834. * @param Date d date A date
  1835. * @param int|date range Number of minutes in the range, or a stop date
  1836. * @return array An array of minutes
  1837. */
  1838. getMinuteDomain: function (d, range) {
  1839. "use strict";
  1840. var start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours());
  1841. var stop = null;
  1842. if (range instanceof Date) {
  1843. stop = new Date(range.getFullYear(), range.getMonth(), range.getDate(), range.getHours());
  1844. } else {
  1845. stop = new Date(+start + range * 1000 * 60);
  1846. }
  1847. return d3.time.minutes(Math.min(start, stop), Math.max(start, stop));
  1848. },
  1849. /**
  1850. * Return all the hours between 2 dates
  1851. *
  1852. * @param Date d A date
  1853. * @param int|date range Number of hours in the range, or a stop date
  1854. * @return array An array of hours
  1855. */
  1856. getHourDomain: function (d, range) {
  1857. "use strict";
  1858. var start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours());
  1859. var stop = null;
  1860. if (range instanceof Date) {
  1861. stop = new Date(range.getFullYear(), range.getMonth(), range.getDate(), range.getHours());
  1862. } else {
  1863. stop = new Date(start);
  1864. stop.setHours(stop.getHours() + range);
  1865. }
  1866. var domains = d3.time.hours(Math.min(start, stop), Math.max(start, stop));
  1867. // Passing from DST to standard time
  1868. // If there are 25 hours, let's compress the duplicate hours
  1869. var i = 0;
  1870. var total = domains.length;
  1871. for(i = 0; i < total; i++) {
  1872. if (i > 0 && (domains[i].getHours() === domains[i-1].getHours())) {
  1873. this.DSTDomain.push(domains[i].getTime());
  1874. domains.splice(i, 1);
  1875. break;
  1876. }
  1877. }
  1878. // d3.time.hours is returning more hours than needed when changing
  1879. // from DST to standard time, because there is really 2 hours between
  1880. // 1am and 2am!
  1881. if (typeof range === "number" && domains.length > Math.abs(range)) {
  1882. domains.splice(domains.length-1, 1);
  1883. }
  1884. return domains;
  1885. },
  1886. /**
  1887. * Return all the days between 2 dates
  1888. *
  1889. * @param Date d A date
  1890. * @param int|date range Number of days in the range, or a stop date
  1891. * @return array An array of weeks
  1892. */
  1893. getDayDomain: function (d, range) {
  1894. "use strict";
  1895. var start = new Date(d.getFullYear(), d.getMonth(), d.getDate());
  1896. var stop = null;
  1897. if (range instanceof Date) {
  1898. stop = new Date(range.getFullYear(), range.getMonth(), range.getDate());
  1899. } else {
  1900. stop = new Date(start);
  1901. stop = new Date(stop.setDate(stop.getDate() + parseInt(range, 10)));
  1902. }
  1903. return d3.time.days(Math.min(start, stop), Math.max(start, stop));
  1904. },
  1905. /**
  1906. * Return all the weeks between 2 dates
  1907. *
  1908. * @param Date d A date
  1909. * @param int|date range Number of minutes in the range, or a stop date
  1910. * @return array An array of weeks
  1911. */
  1912. getWeekDomain: function (d, range) {
  1913. "use strict";
  1914. var weekStart;
  1915. if (this.options.weekStartOnMonday === false) {
  1916. weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate() - d.getDay());
  1917. } else {
  1918. if (d.getDay() === 1) {
  1919. weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
  1920. } else if (d.getDay() === 0) {
  1921. weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
  1922. weekStart.setDate(weekStart.getDate() - 6);
  1923. } else {
  1924. weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()-d.getDay()+1);
  1925. }
  1926. }
  1927. var endDate = new Date(weekStart);
  1928. var stop = range;
  1929. if (typeof range !== "object") {
  1930. stop = new Date(endDate.setDate(endDate.getDate() + range * 7));
  1931. }
  1932. return (this.options.weekStartOnMonday === true) ?
  1933. d3.time.mondays(Math.min(weekStart, stop), Math.max(weekStart, stop)):
  1934. d3.time.sundays(Math.min(weekStart, stop), Math.max(weekStart, stop))
  1935. ;
  1936. },
  1937. /**
  1938. * Return all the months between 2 dates
  1939. *
  1940. * @param Date d A date
  1941. * @param int|date range Number of months in the range, or a stop date
  1942. * @return array An array of months
  1943. */
  1944. getMonthDomain: function (d, range) {
  1945. "use strict";
  1946. var start = new Date(d.getFullYear(), d.getMonth());
  1947. var stop = null;
  1948. if (range instanceof Date) {
  1949. stop = new Date(range.getFullYear(), range.getMonth());
  1950. } else {
  1951. stop = new Date(start);
  1952. stop = stop.setMonth(stop.getMonth()+range);
  1953. }
  1954. return d3.time.months(Math.min(start, stop), Math.max(start, stop));
  1955. },
  1956. /**
  1957. * Return all the years between 2 dates
  1958. *
  1959. * @param Date d date A date
  1960. * @param int|date range Number of minutes in the range, or a stop date
  1961. * @return array An array of hours
  1962. */
  1963. getYearDomain: function(d, range){
  1964. "use strict";
  1965. var start = new Date(d.getFullYear(), 0);
  1966. var stop = null;
  1967. if (range instanceof Date) {
  1968. stop = new Date(range.getFullYear(), 0);
  1969. } else {
  1970. stop = new Date(d.getFullYear()+range, 0);
  1971. }
  1972. return d3.time.years(Math.min(start, stop), Math.max(start, stop));
  1973. },
  1974. /**
  1975. * Get an array of domain start dates
  1976. *
  1977. * @param int|Date date A random date included in the wanted domain
  1978. * @param int|Date range Number of dates to get, or a stop date
  1979. * @return Array of dates
  1980. */
  1981. getDomain: function(date, range) {
  1982. "use strict";
  1983. if (typeof date === "number") {
  1984. date = new Date(date);
  1985. }
  1986. if (arguments.length < 2) {
  1987. range = this.options.range;
  1988. }
  1989. switch(this.options.domain) {
  1990. case "hour" :
  1991. var domains = this.getHourDomain(date, range);
  1992. // Case where an hour is missing, when passing from standard time to DST
  1993. // Missing hour is perfectly acceptabl in subDomain, but not in domains
  1994. if (typeof range === "number" && domains.length < range) {
  1995. if (range > 0) {
  1996. domains.push(this.getHourDomain(domains[domains.length-1], 2)[1]);
  1997. } else {
  1998. domains.shift(this.getHourDomain(domains[0], -2)[0]);
  1999. }
  2000. }
  2001. return domains;
  2002. case "day" :
  2003. return this.getDayDomain(date, range);
  2004. case "week" :
  2005. return this.getWeekDomain(date, range);
  2006. case "month":
  2007. return this.getMonthDomain(date, range);
  2008. case "year" :
  2009. return this.getYearDomain(date, range);
  2010. }
  2011. },
  2012. /* jshint maxcomplexity: false */
  2013. getSubDomain: function(date) {
  2014. "use strict";
  2015. if (typeof date === "number") {
  2016. date = new Date(date);
  2017. }
  2018. var parent = this;
  2019. /**
  2020. * @return int
  2021. */
  2022. var computeDaySubDomainSize = function(date, domain) {
  2023. switch(domain) {
  2024. case "year":
  2025. return parent.getDayCountInYear(date);
  2026. case "month":
  2027. return parent.getDayCountInMonth(date);
  2028. case "week":
  2029. return 7;
  2030. }
  2031. };
  2032. /**
  2033. * @return int
  2034. */
  2035. var computeMinSubDomainSize = function(date, domain) {
  2036. switch (domain) {
  2037. case "hour":
  2038. return 60;
  2039. case "day":
  2040. return 60 * 24;
  2041. case "week":
  2042. return 60 * 24 * 7;
  2043. }
  2044. };
  2045. /**
  2046. * @return int
  2047. */
  2048. var computeHourSubDomainSize = function(date, domain) {
  2049. switch(domain) {
  2050. case "day":
  2051. return 24;
  2052. case "week":
  2053. return 168;
  2054. case "month":
  2055. return parent.getDayCountInMonth(date) * 24;
  2056. }
  2057. };
  2058. /**
  2059. * @return int
  2060. */
  2061. var computeWeekSubDomainSize = function(date, domain) {
  2062. if (domain === "month") {
  2063. var endOfMonth = new Date(date.getFullYear(), date.getMonth()+1, 0);
  2064. var endWeekNb = parent.getWeekNumber(endOfMonth);
  2065. var startWeekNb = parent.getWeekNumber(new Date(date.getFullYear(), date.getMonth()));
  2066. if (startWeekNb > endWeekNb) {
  2067. startWeekNb = 0;
  2068. endWeekNb++;
  2069. }
  2070. return endWeekNb - startWeekNb + 1;
  2071. } else if (domain === "year") {
  2072. return parent.getWeekNumber(new Date(date.getFullYear(), 11, 31));
  2073. }
  2074. };
  2075. switch(this.options.subDomain) {
  2076. case "x_min":
  2077. case "min" :
  2078. return this.getMinuteDomain(date, computeMinSubDomainSize(date, this.options.domain));
  2079. case "x_hour":
  2080. case "hour" :
  2081. return this.getHourDomain(date, computeHourSubDomainSize(date, this.options.domain));
  2082. case "x_day":
  2083. case "day" :
  2084. return this.getDayDomain(date, computeDaySubDomainSize(date, this.options.domain));
  2085. case "x_week":
  2086. case "week" :
  2087. return this.getWeekDomain(date, computeWeekSubDomainSize(date, this.options.domain));
  2088. case "x_month":
  2089. case "month":
  2090. return this.getMonthDomain(date, 12);
  2091. }
  2092. },
  2093. /**
  2094. * Get the n-th next domain after the calendar newest (rightmost) domain
  2095. * @param int n
  2096. * @return Date The start date of the wanted domain
  2097. */
  2098. getNextDomain: function(n) {
  2099. "use strict";
  2100. if (arguments.length === 0) {
  2101. n = 1;
  2102. }
  2103. return this.getDomain(this.jumpDate(this.getDomainKeys().pop(), n, this.options.domain), 1)[0];
  2104. },
  2105. /**
  2106. * Get the n-th domain before the calendar oldest (leftmost) domain
  2107. * @param int n
  2108. * @return Date The start date of the wanted domain
  2109. */
  2110. getPreviousDomain: function(n) {
  2111. "use strict";
  2112. if (arguments.length === 0) {
  2113. n = 1;
  2114. }
  2115. return this.getDomain(this.jumpDate(this.getDomainKeys().shift(), -n, this.options.domain), 1)[0];
  2116. },
  2117. // =========================================================================//
  2118. // DATAS //
  2119. // =========================================================================//
  2120. /**
  2121. * Fetch and interpret data from the datasource
  2122. *
  2123. * @param string|object source
  2124. * @param Date startDate
  2125. * @param Date endDate
  2126. * @param function callback
  2127. * @param function|boolean afterLoad function used to convert the data into a json object. Use true to use the afterLoad callback
  2128. * @param updateMode
  2129. *
  2130. * @return mixed
  2131. * - True if there are no data to load
  2132. * - False if data are loaded asynchronously
  2133. */
  2134. getDatas: function(source, startDate, endDate, callback, afterLoad, updateMode) {
  2135. "use strict";
  2136. var self = this;
  2137. if (arguments.length < 5) {
  2138. afterLoad = true;
  2139. }
  2140. if (arguments.length < 6) {
  2141. updateMode = this.APPEND_ON_UPDATE;
  2142. }
  2143. var _callback = function(data) {
  2144. if (afterLoad !== false) {
  2145. if (typeof afterLoad === "function") {
  2146. data = afterLoad(data);
  2147. } else if (typeof (self.options.afterLoadData) === "function") {
  2148. data = self.options.afterLoadData(data);
  2149. } else {
  2150. console.log("Provided callback for afterLoadData is not a function.");
  2151. }
  2152. } else if (self.options.dataType === "csv" || self.options.dataType === "tsv") {
  2153. data = this.interpretCSV(data);
  2154. }
  2155. self.parseDatas(data, updateMode, startDate, endDate);
  2156. if (typeof callback === "function") {
  2157. callback();
  2158. }
  2159. };
  2160. switch(typeof source) {
  2161. case "string":
  2162. if (source === "") {
  2163. _callback({});
  2164. return true;
  2165. } else {
  2166. var url = this.parseURI(source, startDate, endDate);
  2167. var requestType = "GET";
  2168. if (self.options.dataPostPayload !== null ) {
  2169. requestType = "POST";
  2170. }
  2171. var payload = null;
  2172. if (self.options.dataPostPayload !== null) {
  2173. payload = this.parseURI(self.options.dataPostPayload, startDate, endDate);
  2174. }
  2175. switch(this.options.dataType) {
  2176. case "json":
  2177. d3.json(url, _callback).send(requestType, payload);
  2178. break;
  2179. case "csv":
  2180. d3.csv(url, _callback).send(requestType, payload);
  2181. break;
  2182. case "tsv":
  2183. d3.tsv(url, _callback).send(requestType, payload);
  2184. break;
  2185. case "txt":
  2186. d3.text(url, "text/plain", _callback).send(requestType, payload);
  2187. break;
  2188. }
  2189. }
  2190. return false;
  2191. case "object":
  2192. if (source === Object(source)) {
  2193. _callback(source);
  2194. return false;
  2195. }
  2196. /* falls through */
  2197. default:
  2198. _callback({});
  2199. return true;
  2200. }
  2201. },
  2202. /**
  2203. * Populate the calendar internal data
  2204. *
  2205. * @param object data
  2206. * @param constant updateMode
  2207. * @param Date startDate
  2208. * @param Date endDate
  2209. *
  2210. * @return void
  2211. */
  2212. parseDatas: function(data, updateMode, startDate, endDate) {
  2213. "use strict";
  2214. if (updateMode === this.RESET_ALL_ON_UPDATE) {
  2215. this._domains.forEach(function(key, value) {
  2216. value.forEach(function(element, index, array) {
  2217. array[index].v = null;
  2218. });
  2219. });
  2220. }
  2221. var temp = {};
  2222. var extractTime = function(d) { return d.t; };
  2223. /*jshint forin:false */
  2224. for (var d in data) {
  2225. var date = new Date(d*1000);
  2226. var domainUnit = this.getDomain(date)[0].getTime();
  2227. // The current data belongs to a domain that was compressed
  2228. // Compress the data for the two duplicate hours into the same hour
  2229. if (this.DSTDomain.indexOf(domainUnit) >= 0) {
  2230. // Re-assign all data to the first or the second duplicate hours
  2231. // depending on which is visible
  2232. if (this._domains.has(domainUnit - 3600 * 1000)) {
  2233. domainUnit -= 3600 * 1000;
  2234. }
  2235. }
  2236. // Skip if data is not relevant to current domain
  2237. if (isNaN(d) || !data.hasOwnProperty(d) || !this._domains.has(domainUnit) || !(domainUnit >= +startDate && domainUnit < +endDate)) {
  2238. continue;
  2239. }
  2240. var subDomainsData = this._domains.get(domainUnit);
  2241. if (!temp.hasOwnProperty(domainUnit)) {
  2242. temp[domainUnit] = subDomainsData.map(extractTime);
  2243. }
  2244. var index = temp[domainUnit].indexOf(this._domainType[this.options.subDomain].extractUnit(date));
  2245. if (updateMode === this.RESET_SINGLE_ON_UPDATE) {
  2246. subDomainsData[index].v = data[d];
  2247. } else {
  2248. if (!isNaN(subDomainsData[index].v)) {
  2249. subDomainsData[index].v += data[d];
  2250. } else {
  2251. subDomainsData[index].v = data[d];
  2252. }
  2253. }
  2254. }
  2255. },
  2256. parseURI: function(str, startDate, endDate) {
  2257. "use strict";
  2258. // Use a timestamp in seconds
  2259. str = str.replace(/\{\{t:start\}\}/g, startDate.getTime()/1000);
  2260. str = str.replace(/\{\{t:end\}\}/g, endDate.getTime()/1000);
  2261. // Use a string date, following the ISO-8601
  2262. str = str.replace(/\{\{d:start\}\}/g, startDate.toISOString());
  2263. str = str.replace(/\{\{d:end\}\}/g, endDate.toISOString());
  2264. return str;
  2265. },
  2266. interpretCSV: function(data) {
  2267. "use strict";
  2268. var d = {};
  2269. var keys = Object.keys(data[0]);
  2270. var i, total;
  2271. for (i = 0, total = data.length; i < total; i++) {
  2272. d[data[i][keys[0]]] = +data[i][keys[1]];
  2273. }
  2274. return d;
  2275. },
  2276. /**
  2277. * Handle the calendar layout and dimension
  2278. *
  2279. * Expand and shrink the container depending on its children dimension
  2280. * Also rearrange the children position depending on their dimension,
  2281. * and the legend position
  2282. *
  2283. * @return void
  2284. */
  2285. resize: function() {
  2286. "use strict";
  2287. var parent = this;
  2288. var options = parent.options;
  2289. var legendWidth = options.displayLegend ? (parent.Legend.getDim("width") + options.legendMargin[1] + options.legendMargin[3]) : 0;
  2290. var legendHeight = options.displayLegend ? (parent.Legend.getDim("height") + options.legendMargin[0] + options.legendMargin[2]) : 0;
  2291. var graphWidth = parent.graphDim.width - options.domainGutter - options.cellPadding;
  2292. var graphHeight = parent.graphDim.height - options.domainGutter - options.cellPadding;
  2293. this.root.transition().duration(options.animationDuration)
  2294. .attr("width", function() {
  2295. if (options.legendVerticalPosition === "middle" || options.legendVerticalPosition === "center") {
  2296. return graphWidth + legendWidth;
  2297. }
  2298. return Math.max(graphWidth, legendWidth);
  2299. })
  2300. .attr("height", function() {
  2301. if (options.legendVerticalPosition === "middle" || options.legendVerticalPosition === "center") {
  2302. return Math.max(graphHeight, legendHeight);
  2303. }
  2304. return graphHeight + legendHeight;
  2305. })
  2306. ;
  2307. this.root.select(".graph").transition().duration(options.animationDuration)
  2308. .attr("y", function() {
  2309. if (options.legendVerticalPosition === "top") {
  2310. return legendHeight;
  2311. }
  2312. return 0;
  2313. })
  2314. .attr("x", function() {
  2315. if (
  2316. (options.legendVerticalPosition === "middle" || options.legendVerticalPosition === "center") &&
  2317. options.legendHorizontalPosition === "left") {
  2318. return legendWidth;
  2319. }
  2320. return 0;
  2321. })
  2322. ;
  2323. },
  2324. // =========================================================================//
  2325. // PUBLIC API //
  2326. // =========================================================================//
  2327. /**
  2328. * Shift the calendar forward
  2329. */
  2330. next: function(n) {
  2331. "use strict";
  2332. if (arguments.length === 0) {
  2333. n = 1;
  2334. }
  2335. return this.loadNextDomain(n);
  2336. },
  2337. /**
  2338. * Shift the calendar backward
  2339. */
  2340. previous: function(n) {
  2341. "use strict";
  2342. if (arguments.length === 0) {
  2343. n = 1;
  2344. }
  2345. return this.loadPreviousDomain(n);
  2346. },
  2347. /**
  2348. * Jump directly to a specific date
  2349. *
  2350. * JumpTo will scroll the calendar until the wanted domain with the specified
  2351. * date is visible. Unless you set reset to true, the wanted domain
  2352. * will not necessarily be the first (leftmost) domain of the calendar.
  2353. *
  2354. * @param Date date Jump to the domain containing that date
  2355. * @param bool reset Whether the wanted domain should be the first domain of the calendar
  2356. * @param bool True of the calendar was scrolled
  2357. */
  2358. jumpTo: function(date, reset) {
  2359. "use strict";
  2360. if (arguments.length < 2) {
  2361. reset = false;
  2362. }
  2363. var domains = this.getDomainKeys();
  2364. var firstDomain = domains[0];
  2365. var lastDomain = domains[domains.length-1];
  2366. if (date < firstDomain) {
  2367. return this.loadPreviousDomain(this.getDomain(firstDomain, date).length);
  2368. } else {
  2369. if (reset) {
  2370. return this.loadNextDomain(this.getDomain(firstDomain, date).length);
  2371. }
  2372. if (date > lastDomain) {
  2373. return this.loadNextDomain(this.getDomain(lastDomain, date).length);
  2374. }
  2375. }
  2376. return false;
  2377. },
  2378. /**
  2379. * Navigate back to the start date
  2380. *
  2381. * @since 3.3.8
  2382. * @return void
  2383. */
  2384. rewind: function() {
  2385. "use strict";
  2386. this.jumpTo(this.options.start, true);
  2387. },
  2388. /**
  2389. * Update the calendar with new data
  2390. *
  2391. * @param object|string dataSource The calendar's datasource, same type as this.options.data
  2392. * @param boolean|function afterLoad Whether to execute afterLoad() on the data. Pass directly a function
  2393. * if you don't want to use the afterLoad() callback
  2394. */
  2395. update: function(dataSource, afterLoad, updateMode) {
  2396. "use strict";
  2397. if (arguments.length < 2) {
  2398. afterLoad = true;
  2399. }
  2400. if (arguments.length < 3) {
  2401. updateMode = this.RESET_ALL_ON_UPDATE;
  2402. }
  2403. var domains = this.getDomainKeys();
  2404. var self = this;
  2405. this.getDatas(
  2406. dataSource,
  2407. new Date(domains[0]),
  2408. this.getSubDomain(domains[domains.length-1]).pop(),
  2409. function() {
  2410. self.fill();
  2411. },
  2412. afterLoad,
  2413. updateMode
  2414. );
  2415. },
  2416. /**
  2417. * Set the legend
  2418. *
  2419. * @param array legend an array of integer, representing the different threshold value
  2420. * @param array colorRange an array of 2 hex colors, for the minimum and maximum colors
  2421. */
  2422. setLegend: function() {
  2423. "use strict";
  2424. var oldLegend = this.options.legend.slice(0);
  2425. if (arguments.length >= 1 && Array.isArray(arguments[0])) {
  2426. this.options.legend = arguments[0];
  2427. }
  2428. if (arguments.length >= 2) {
  2429. if (Array.isArray(arguments[1]) && arguments[1].length >= 2) {
  2430. this.options.legendColors = [arguments[1][0], arguments[1][1]];
  2431. } else {
  2432. this.options.legendColors = arguments[1];
  2433. }
  2434. }
  2435. if ((arguments.length > 0 && !arrayEquals(oldLegend, this.options.legend)) || arguments.length >= 2) {
  2436. this.Legend.buildColors();
  2437. this.fill();
  2438. }
  2439. this.Legend.redraw(this.graphDim.width - this.options.domainGutter - this.options.cellPadding);
  2440. },
  2441. /**
  2442. * Remove the legend
  2443. *
  2444. * @return bool False if there is no legend to remove
  2445. */
  2446. removeLegend: function() {
  2447. "use strict";
  2448. if (!this.options.displayLegend) {
  2449. return false;
  2450. }
  2451. this.options.displayLegend = false;
  2452. this.Legend.remove();
  2453. return true;
  2454. },
  2455. /**
  2456. * Display the legend
  2457. *
  2458. * @return bool False if the legend was already displayed
  2459. */
  2460. showLegend: function() {
  2461. "use strict";
  2462. if (this.options.displayLegend) {
  2463. return false;
  2464. }
  2465. this.options.displayLegend = true;
  2466. this.Legend.redraw(this.graphDim.width - this.options.domainGutter - this.options.cellPadding);
  2467. return true;
  2468. },
  2469. /**
  2470. * Highlight dates
  2471. *
  2472. * Add a highlight class to a set of dates
  2473. *
  2474. * @since 3.3.5
  2475. * @param array Array of dates to highlight
  2476. * @return bool True if dates were highlighted
  2477. */
  2478. highlight: function(args) {
  2479. "use strict";
  2480. if ((this.options.highlight = this.expandDateSetting(args)).length > 0) {
  2481. this.fill();
  2482. return true;
  2483. }
  2484. return false;
  2485. },
  2486. /**
  2487. * Destroy the calendar
  2488. *
  2489. * Usage: cal = cal.destroy();
  2490. *
  2491. * @since 3.3.6
  2492. * @param function A callback function to trigger after destroying the calendar
  2493. * @return null
  2494. */
  2495. destroy: function(callback) {
  2496. "use strict";
  2497. this.root.transition().duration(this.options.animationDuration)
  2498. .attr("width", 0)
  2499. .attr("height", 0)
  2500. .remove()
  2501. .each("end", function() {
  2502. if (typeof callback === "function") {
  2503. callback();
  2504. } else if (typeof callback !== "undefined") {
  2505. console.log("Provided callback for destroy() is not a function.");
  2506. }
  2507. })
  2508. ;
  2509. return null;
  2510. },
  2511. getSVG: function() {
  2512. "use strict";
  2513. var styles = {
  2514. ".cal-heatmap-container": {},
  2515. ".graph": {},
  2516. ".graph-rect": {},
  2517. "rect.highlight": {},
  2518. "rect.now": {},
  2519. "rect.highlight-now": {},
  2520. "text.highlight": {},
  2521. "text.now": {},
  2522. "text.highlight-now": {},
  2523. ".domain-background": {},
  2524. ".graph-label": {},
  2525. ".subdomain-text": {},
  2526. ".q0": {},
  2527. ".qi": {}
  2528. };
  2529. for (var j = 1, total = this.options.legend.length+1; j <= total; j++) {
  2530. styles[".q" + j] = {};
  2531. }
  2532. var root = this.root;
  2533. var whitelistStyles = [
  2534. // SVG specific properties
  2535. "stroke", "stroke-width", "stroke-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-miterlimit",
  2536. "fill", "fill-opacity", "fill-rule",
  2537. "marker", "marker-start", "marker-mid", "marker-end",
  2538. "alignement-baseline", "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal", "glyph-orientation-vertical", "kerning", "text-anchor",
  2539. "shape-rendering",
  2540. // Text Specific properties
  2541. "text-transform", "font-family", "font", "font-size", "font-weight"
  2542. ];
  2543. var filterStyles = function(attribute, property, value) {
  2544. if (whitelistStyles.indexOf(property) !== -1) {
  2545. styles[attribute][property] = value;
  2546. }
  2547. };
  2548. var getElement = function(e) {
  2549. return root.select(e)[0][0];
  2550. };
  2551. /* jshint forin:false */
  2552. for (var element in styles) {
  2553. if (!styles.hasOwnProperty(element)) {
  2554. continue;
  2555. }
  2556. var dom = getElement(element);
  2557. if (dom === null) {
  2558. continue;
  2559. }
  2560. // The DOM Level 2 CSS way
  2561. /* jshint maxdepth: false */
  2562. if ("getComputedStyle" in window) {
  2563. var cs = getComputedStyle(dom, null);
  2564. if (cs.length !== 0) {
  2565. for (var i = 0; i < cs.length; i++) {
  2566. filterStyles(element, cs.item(i), cs.getPropertyValue(cs.item(i)));
  2567. }
  2568. // Opera workaround. Opera doesn"t support `item`/`length`
  2569. // on CSSStyleDeclaration.
  2570. } else {
  2571. for (var k in cs) {
  2572. if (cs.hasOwnProperty(k)) {
  2573. filterStyles(element, k, cs[k]);
  2574. }
  2575. }
  2576. }
  2577. // The IE way
  2578. } else if ("currentStyle" in dom) {
  2579. var css = dom.currentStyle;
  2580. for (var p in css) {
  2581. filterStyles(element, p, css[p]);
  2582. }
  2583. }
  2584. }
  2585. var string = "<svg xmlns=\"http://www.w3.org/2000/svg\" "+
  2586. "xmlns:xlink=\"http://www.w3.org/1999/xlink\"><style type=\"text/css\"><![CDATA[ ";
  2587. for (var style in styles) {
  2588. string += style + " {\n";
  2589. for (var l in styles[style]) {
  2590. string += "\t" + l + ":" + styles[style][l] + ";\n";
  2591. }
  2592. string += "}\n";
  2593. }
  2594. string += "]]></style>";
  2595. string += new XMLSerializer().serializeToString(this.root[0][0]);
  2596. string += "</svg>";
  2597. return string;
  2598. }
  2599. };
  2600. // =========================================================================//
  2601. // DOMAIN POSITION COMPUTATION //
  2602. // =========================================================================//
  2603. /**
  2604. * Compute the position of a domain, relative to the calendar
  2605. */
  2606. var DomainPosition = function() {
  2607. "use strict";
  2608. this.positions = d3.map();
  2609. };
  2610. DomainPosition.prototype.getPosition = function(d) {
  2611. "use strict";
  2612. return this.positions.get(d);
  2613. };
  2614. DomainPosition.prototype.getPositionFromIndex = function(i) {
  2615. "use strict";
  2616. var domains = this.getKeys();
  2617. return this.positions.get(domains[i]);
  2618. };
  2619. DomainPosition.prototype.getLast = function() {
  2620. "use strict";
  2621. var domains = this.getKeys();
  2622. return this.positions.get(domains[domains.length-1]);
  2623. };
  2624. DomainPosition.prototype.setPosition = function(d, dim) {
  2625. "use strict";
  2626. this.positions.set(d, dim);
  2627. };
  2628. DomainPosition.prototype.shiftRightBy = function(exitingDomainDim) {
  2629. "use strict";
  2630. this.positions.forEach(function(key, value) {
  2631. this.set(key, value - exitingDomainDim);
  2632. });
  2633. var domains = this.getKeys();
  2634. this.positions.remove(domains[0]);
  2635. };
  2636. DomainPosition.prototype.shiftLeftBy = function(enteringDomainDim) {
  2637. "use strict";
  2638. this.positions.forEach(function(key, value) {
  2639. this.set(key, value + enteringDomainDim);
  2640. });
  2641. var domains = this.getKeys();
  2642. this.positions.remove(domains[domains.length-1]);
  2643. };
  2644. DomainPosition.prototype.getKeys = function() {
  2645. "use strict";
  2646. return this.positions.keys().sort(function(a, b) {
  2647. return parseInt(a, 10) - parseInt(b, 10);
  2648. });
  2649. };
  2650. // =========================================================================//
  2651. // LEGEND //
  2652. // =========================================================================//
  2653. var Legend = function(calendar) {
  2654. "use strict";
  2655. this.calendar = calendar;
  2656. this.computeDim();
  2657. if (calendar.options.legendColors !== null) {
  2658. this.buildColors();
  2659. }
  2660. };
  2661. Legend.prototype.computeDim = function() {
  2662. "use strict";
  2663. var options = this.calendar.options; // Shorter accessor for variable name mangling when minifying
  2664. this.dim = {
  2665. width:
  2666. options.legendCellSize * (options.legend.length+1) +
  2667. options.legendCellPadding * (options.legend.length),
  2668. height:
  2669. options.legendCellSize
  2670. };
  2671. };
  2672. Legend.prototype.remove = function() {
  2673. "use strict";
  2674. this.calendar.root.select(".graph-legend").remove();
  2675. this.calendar.resize();
  2676. };
  2677. Legend.prototype.redraw = function(width) {
  2678. "use strict";
  2679. if (!this.calendar.options.displayLegend) {
  2680. return false;
  2681. }
  2682. var parent = this;
  2683. var calendar = this.calendar;
  2684. var legend = calendar.root;
  2685. var legendItem;
  2686. var options = calendar.options; // Shorter accessor for variable name mangling when minifying
  2687. this.computeDim();
  2688. var _legend = options.legend.slice(0);
  2689. _legend.push(_legend[_legend.length-1]+1);
  2690. var legendElement = calendar.root.select(".graph-legend");
  2691. if (legendElement[0][0] !== null) {
  2692. legend = legendElement;
  2693. legendItem = legend
  2694. .select("g")
  2695. .selectAll("rect").data(_legend)
  2696. ;
  2697. } else {
  2698. // Creating the new legend DOM if it doesn't already exist
  2699. legend = options.legendVerticalPosition === "top" ? legend.insert("svg", ".graph") : legend.append("svg");
  2700. legend
  2701. .attr("x", getLegendXPosition())
  2702. .attr("y", getLegendYPosition())
  2703. ;
  2704. legendItem = legend
  2705. .attr("class", "graph-legend")
  2706. .attr("height", parent.getDim("height"))
  2707. .attr("width", parent.getDim("width"))
  2708. .append("g")
  2709. .selectAll().data(_legend)
  2710. ;
  2711. }
  2712. legendItem
  2713. .enter()
  2714. .append("rect")
  2715. .call(legendCellLayout)
  2716. .attr("class", function(d){ return calendar.Legend.getClass(d, (calendar.legendScale === null)); })
  2717. .attr("fill-opacity", 0)
  2718. .call(function(selection) {
  2719. if (calendar.legendScale !== null && options.legendColors !== null && options.legendColors.hasOwnProperty("base")) {
  2720. selection.attr("fill", options.legendColors.base);
  2721. }
  2722. })
  2723. .append("title")
  2724. ;
  2725. legendItem.exit().transition().duration(options.animationDuration)
  2726. .attr("fill-opacity", 0)
  2727. .remove();
  2728. legendItem.transition().delay(function(d, i) { return options.animationDuration * i/10; })
  2729. .call(legendCellLayout)
  2730. .attr("fill-opacity", 1)
  2731. .call(function(element) {
  2732. element.attr("fill", function(d, i) {
  2733. if (calendar.legendScale === null) {
  2734. return "";
  2735. }
  2736. if (i === 0) {
  2737. return calendar.legendScale(d - 1);
  2738. }
  2739. return calendar.legendScale(options.legend[i-1]);
  2740. });
  2741. element.attr("class", function(d) { return calendar.Legend.getClass(d, (calendar.legendScale === null)); });
  2742. })
  2743. ;
  2744. function legendCellLayout(selection) {
  2745. selection
  2746. .attr("width", options.legendCellSize)
  2747. .attr("height", options.legendCellSize)
  2748. .attr("x", function(d, i) {
  2749. return i * (options.legendCellSize + options.legendCellPadding);
  2750. })
  2751. ;
  2752. }
  2753. legendItem.select("title").text(function(d, i) {
  2754. if (i === 0) {
  2755. return (options.legendTitleFormat.lower).format({
  2756. min: options.legend[i],
  2757. name: options.itemName[1]
  2758. });
  2759. } else if (i === _legend.length-1) {
  2760. return (options.legendTitleFormat.upper).format({
  2761. max: options.legend[i-1],
  2762. name: options.itemName[1]
  2763. });
  2764. } else {
  2765. return (options.legendTitleFormat.inner).format({
  2766. down: options.legend[i-1],
  2767. up: options.legend[i],
  2768. name: options.itemName[1]
  2769. });
  2770. }
  2771. })
  2772. ;
  2773. legend.transition().duration(options.animationDuration)
  2774. .attr("x", getLegendXPosition())
  2775. .attr("y", getLegendYPosition())
  2776. .attr("width", parent.getDim("width"))
  2777. .attr("height", parent.getDim("height"))
  2778. ;
  2779. legend.select("g").transition().duration(options.animationDuration)
  2780. .attr("transform", function() {
  2781. if (options.legendOrientation === "vertical") {
  2782. return "rotate(90 " + options.legendCellSize/2 + " " + options.legendCellSize/2 + ")";
  2783. }
  2784. return "";
  2785. })
  2786. ;
  2787. function getLegendXPosition() {
  2788. switch(options.legendHorizontalPosition) {
  2789. case "right":
  2790. if (options.legendVerticalPosition === "center" || options.legendVerticalPosition === "middle") {
  2791. return width + options.legendMargin[3];
  2792. }
  2793. return width - parent.getDim("width") - options.legendMargin[1];
  2794. case "middle":
  2795. case "center":
  2796. return Math.round(width/2 - parent.getDim("width")/2);
  2797. default:
  2798. return options.legendMargin[3];
  2799. }
  2800. }
  2801. function getLegendYPosition() {
  2802. if (options.legendVerticalPosition === "bottom") {
  2803. return calendar.graphDim.height + options.legendMargin[0] - options.domainGutter - options.cellPadding;
  2804. }
  2805. return options.legendMargin[0];
  2806. }
  2807. calendar.resize();
  2808. };
  2809. /**
  2810. * Return the dimension of the legend
  2811. *
  2812. * Takes into account rotation
  2813. *
  2814. * @param string axis Width or height
  2815. * @return int height or width in pixels
  2816. */
  2817. Legend.prototype.getDim = function(axis) {
  2818. "use strict";
  2819. var isHorizontal = (this.calendar.options.legendOrientation === "horizontal");
  2820. switch(axis) {
  2821. case "width":
  2822. return this.dim[isHorizontal ? "width": "height"];
  2823. case "height":
  2824. return this.dim[isHorizontal ? "height": "width"];
  2825. }
  2826. };
  2827. Legend.prototype.buildColors = function() {
  2828. "use strict";
  2829. var options = this.calendar.options; // Shorter accessor for variable name mangling when minifying
  2830. if (options.legendColors === null) {
  2831. this.calendar.legendScale = null;
  2832. return false;
  2833. }
  2834. var _colorRange = [];
  2835. if (Array.isArray(options.legendColors)) {
  2836. _colorRange = options.legendColors;
  2837. } else if (options.legendColors.hasOwnProperty("min") && options.legendColors.hasOwnProperty("max")) {
  2838. _colorRange = [options.legendColors.min, options.legendColors.max];
  2839. } else {
  2840. options.legendColors = null;
  2841. return false;
  2842. }
  2843. var _legend = options.legend.slice(0);
  2844. if (_legend[0] > 0) {
  2845. _legend.unshift(0);
  2846. } else if (_legend[0] < 0) {
  2847. // Let's guess the leftmost value, it we have to add one
  2848. _legend.unshift(_legend[0] - (_legend[_legend.length-1] - _legend[0])/_legend.length);
  2849. }
  2850. var colorScale = d3.scale.linear()
  2851. .range(_colorRange)
  2852. .interpolate(d3.interpolateHcl)
  2853. .domain([d3.min(_legend), d3.max(_legend)])
  2854. ;
  2855. var legendColors = _legend.map(function(element) { return colorScale(element); });
  2856. this.calendar.legendScale = d3.scale.threshold().domain(options.legend).range(legendColors);
  2857. return true;
  2858. };
  2859. /**
  2860. * Return the classname on the legend for the specified value
  2861. *
  2862. * @param integer n Value associated to a date
  2863. * @param bool withCssClass Whether to display the css class used to style the cell.
  2864. * Disabling will allow styling directly via html fill attribute
  2865. *
  2866. * @return string Classname according to the legend
  2867. */
  2868. Legend.prototype.getClass = function(n, withCssClass) {
  2869. "use strict";
  2870. if (n === null || isNaN(n)) {
  2871. return "";
  2872. }
  2873. var index = [this.calendar.options.legend.length + 1];
  2874. for (var i = 0, total = this.calendar.options.legend.length-1; i <= total; i++) {
  2875. if (this.calendar.options.legend[0] > 0 && n < 0) {
  2876. index = ["1", "i"];
  2877. break;
  2878. }
  2879. if (n <= this.calendar.options.legend[i]) {
  2880. index = [i+1];
  2881. break;
  2882. }
  2883. }
  2884. if (n === 0) {
  2885. index.push(0);
  2886. }
  2887. index.unshift("");
  2888. return (index.join(" r") + (withCssClass ? index.join(" q"): "")).trim();
  2889. };
  2890. /**
  2891. * Sprintf like function
  2892. * @source http://stackoverflow.com/a/4795914/805649
  2893. * @return String
  2894. */
  2895. String.prototype.format = function () {
  2896. "use strict";
  2897. var formatted = this;
  2898. for (var prop in arguments[0]) {
  2899. if (arguments[0].hasOwnProperty(prop)) {
  2900. var regexp = new RegExp("\\{" + prop + "\\}", "gi");
  2901. formatted = formatted.replace(regexp, arguments[0][prop]);
  2902. }
  2903. }
  2904. return formatted;
  2905. };
  2906. /**
  2907. * #source http://stackoverflow.com/a/383245/805649
  2908. */
  2909. function mergeRecursive(obj1, obj2) {
  2910. "use strict";
  2911. /*jshint forin:false */
  2912. for (var p in obj2) {
  2913. try {
  2914. // Property in destination object set; update its value.
  2915. if (obj2[p].constructor === Object) {
  2916. obj1[p] = mergeRecursive(obj1[p], obj2[p]);
  2917. } else {
  2918. obj1[p] = obj2[p];
  2919. }
  2920. } catch(e) {
  2921. // Property in destination object not set; create it and set its value.
  2922. obj1[p] = obj2[p];
  2923. }
  2924. }
  2925. return obj1;
  2926. }
  2927. /**
  2928. * Check if 2 arrays are equals
  2929. *
  2930. * @link http://stackoverflow.com/a/14853974/805649
  2931. * @param array array the array to compare to
  2932. * @return bool true of the 2 arrays are equals
  2933. */
  2934. function arrayEquals(arrayA, arrayB) {
  2935. "use strict";
  2936. // if the other array is a falsy value, return
  2937. if (!arrayB || !arrayA) {
  2938. return false;
  2939. }
  2940. // compare lengths - can save a lot of time
  2941. if (arrayA.length !== arrayB.length) {
  2942. return false;
  2943. }
  2944. for (var i = 0; i < arrayA.length; i++) {
  2945. // Check if we have nested arrays
  2946. if (arrayA[i] instanceof Array && arrayB[i] instanceof Array) {
  2947. // recurse into the nested arrays
  2948. if (!arrayEquals(arrayA[i], arrayB[i])) {
  2949. return false;
  2950. }
  2951. }
  2952. else if (arrayA[i] !== arrayB[i]) {
  2953. // Warning - two different object instances will never be equal: {x:20} != {x:20}
  2954. return false;
  2955. }
  2956. }
  2957. return true;
  2958. }
  2959. /**
  2960. * AMD Loader
  2961. */
  2962. if (typeof define === "function" && define.amd) {
  2963. define(["d3"], function() {
  2964. "use strict";
  2965. return CalHeatMap;
  2966. });
  2967. } else if (typeof module === "object" && module.exports) {
  2968. module.exports = CalHeatMap;
  2969. } else {
  2970. window.CalHeatMap = CalHeatMap;
  2971. }