UNPKG

canvas-calendar-chart

Version:

Calendar chart using an HTML canvas

715 lines (584 loc) 32 kB
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Calendar = undefined; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /** * Created by Ivo Zeba on 5/6/17. * * Calendar component */ var _defaultConfigurations = require("./defaultConfigurations"); var _CanvasHelper = require("./CanvasHelper"); var _JsHelper = require("./JsHelper"); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Calendar = exports.Calendar = function () { /** * Default constructor * * @param config */ function Calendar(config) { _classCallCheck(this, Calendar); Calendar.validateConfig(config); this.setConfig(config); this.initializeCanvas(); this.prepareData(); this.renderChart(); } _createClass(Calendar, [{ key: "setConfig", /** * Merges the provided configuration with the default configuration set * * @param config */ value: function setConfig(config) { var configCopy = JSON.parse(JSON.stringify(config)); this.config = JSON.parse(JSON.stringify(_defaultConfigurations.defaultConfig)); this.config.id = configCopy.id; for (var propertyName in configCopy) { if (configCopy.hasOwnProperty(propertyName) && _defaultConfigurations.defaultConfig.hasOwnProperty(propertyName)) { if (propertyName !== "events") Object.assign(this.config[propertyName], configCopy[propertyName]);else { if (config["events"].hasOwnProperty("onClick")) this.config["events"].onClick = config["events"].onClick; if (config["events"].hasOwnProperty("onHover")) { if (config["events"]["onHover"].hasOwnProperty("date")) this.config["events"]["onHover"].date = config["events"]["onHover"].date; if (config["events"]["onHover"].hasOwnProperty("value")) this.config["events"]["onHover"].value = config["events"]["onHover"].value; } } } } } /** * Initializes the canvases and sets member variables */ }, { key: "initializeCanvas", value: function initializeCanvas() { var element = document.getElementById(this.config.id); this.canvas = document.createElement("canvas"); this.canvas.id = this.config.id + "Canvas"; this.context = this.canvas.getContext("2d"); this.canvas.width = this.config.chart.width; this.canvas.height = this.config.chart.height; element.appendChild(this.canvas); this.toolTip = document.createElement("canvas"); this.toolTip.id = this.config.id + "ToolTip"; this.toolTip.width = 100; this.toolTip.style = "background-color:white; border:1px solid blue; position:absolute; display: none;"; this.toolTipContext = this.toolTip.getContext("2d"); element.appendChild(this.toolTip); this.canvas.addEventListener("mousemove", this.onHover.bind(this), false); if (this.config.events.onClick !== null) this.canvas.addEventListener("click", this.onClick.bind(this), false); } }, { key: "destroy", /** * Destroys the canvas */ value: function destroy() { var element = document.getElementById(this.config.id); element.removeChild(this.canvas); this.canvas = null; this.context = null; element.removeChild(this.toolTip); this.toolTip = null; this.toolTipContext = null; } /** * Callback which is called when a mouse move occurs on the canvas * * @param event */ }, { key: "onHover", value: function onHover(event) { var point = this.getPointFromCoordinates(event); if (point !== null) { this.toolTipContext.clearRect(0, 0, this.toolTip.width, this.toolTip.height); this.toolTip.style = "position:absolute; display: inline; background-color: #6a6a6a; padding: 5px 0; border-radius: 6px;"; this.toolTip.style.left = event.clientX - this.canvas.getBoundingClientRect().left + "px"; if (point.value !== null && point.value !== undefined && point.value !== "N/A") { this.toolTip.style.top = event.clientY - 20 - this.canvas.getBoundingClientRect().top + "px"; this.toolTip.height = 40; this.toolTipContext.fillStyle = "#fff"; var tooltipValueText = void 0; if (this.config.events.onHover.value === null) tooltipValueText = "Value: " + point.value.toFixed(2);else tooltipValueText = this.config.events.onHover.value(point, this, event); this.toolTipContext.fillText(tooltipValueText, 7, 30); } else { this.toolTip.style.top = event.clientY - 10 - this.canvas.getBoundingClientRect().top + "px"; this.toolTip.height = 25; } this.toolTipContext.fillStyle = "#fff"; var tooltipDateText = void 0; if (this.config.events.onHover.date === null) tooltipDateText = "Date: " + _JsHelper.JsHelper.getDateString(point.date, this.config.text.months);else tooltipDateText = this.config.events.onHover.date(point, this, event); this.toolTipContext.fillText(tooltipDateText, 7, 15); } else { this.toolTipContext.clearRect(0, 0, this.toolTip.width, this.toolTip.height); this.toolTip.style = "background-color:white; position:absolute; display: none;"; } } /** * Callback which is called when a mouse click occurs on the canvas * * @param event */ }, { key: "onClick", value: function onClick(event) { var point = this.getPointFromCoordinates(event); if (point !== null) this.config.events.onClick(point, this, event); } /** * Returns the data point being displayed for the x,y coordinate. If no data point exists null is returned. * * @param event * @return {*} */ }, { key: "getPointFromCoordinates", value: function getPointFromCoordinates(event) { var x = event.clientX - this.canvas.getBoundingClientRect().left; var y = event.clientY - this.canvas.getBoundingClientRect().top; for (var i = 0; i < this.config.data.length; i++) { var point = this.config.data[i]; var sideLength = this.config.style.squareSideLength; if (point.x <= x && point.y <= y && point.x + sideLength > x && point.y + sideLength > y) { return point; } } return null; } /** * Clears the canvas and renders the chart */ }, { key: "renderChart", value: function renderChart() { this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); this.drawMonths(); this.drawWeekDays(); this.drawChart(); } /** * Helper function which returns a font string for the canvas * * @return {string} */ }, { key: "getFontString", value: function getFontString() { return "" + this.config.text.fontSize + " " + this.config.text.font; } }, { key: "getYOffset", /** * Returns the y-margin based on whether weekends are to be skipped. This makes sure that the months are not * covered up by the Sunday boxes. * * @return {number} */ value: function getYOffset() { return this.config.chart.skipWeekend ? 0 : this.config.style.yMargin; } }, { key: "drawWeekDays", /** * Renders the individual weekday names onto the canvas */ value: function drawWeekDays() { var xMargin = 5; var yMargin = this.getYOffset() + 12; for (var i = 0; i < this.config.text.weekDays.length; i++) { if (this.config.chart.skipWeekend && i !== 0 && i !== 6) { this.context.font = this.getFontString(); this.context.fillText(this.config.text.weekDays[i], xMargin, yMargin + this.config.style.squareSideLength * i); } else if (!this.config.chart.skipWeekend) { this.context.font = this.getFontString(); this.context.fillText(this.config.text.weekDays[i], xMargin, yMargin + this.config.style.squareSideLength * i); } } } }, { key: "drawMonths", /** * Renders the individual month names onto the canvas */ value: function drawMonths() { var numberOfMonths = _JsHelper.JsHelper.getNumberOfMonthsBetweenDates(this.config.chart.startDate, this.config.chart.endDate); var xMargin = 60; var yMargin = 12; var monthCounter = this.config.chart.startDate.getMonth(); for (var i = 0; i <= numberOfMonths; i++) { this.context.font = this.getFontString(); this.context.fillText(this.config.text.months[monthCounter], xMargin + 75 * i, yMargin); monthCounter++; if (monthCounter >= this.config.text.months.length) monthCounter = 0; } } }, { key: "valueToColor", /** * Converts a value to a color. Special cases are taken into consideration if a value is "N/A" or null/undefined * * @param value between 0 and 1 * @return {*} */ value: function valueToColor(value) { if (value === null || value === undefined) return this.config.color.missingValue;else if (value === "N/A") return this.config.color.missingDate; for (var i = 0; i < this.config.color.range.length; i++) { if (this.config.color.range.length - 1 === i) return this.config.color.range[i];else if (value >= i * (1 / this.config.color.range.length) && value <= (i + 1) * (1 / this.config.color.range.length)) return this.config.color.range[i]; } } }, { key: "drawMonthBorder", /** * Draws borders around the months * * @param date * @param xStart X position of the top left corner of the square on the canvas * @param yStart Y position of the top left corner of the square on the canvas */ value: function drawMonthBorder(date, xStart, yStart) { var color = this.config.color.monthOutline; // Draw a border if the square is at the top or if it is the first day in the month if (this.config.chart.skipWeekend && date.getDay() === 1 || date.getDay() === 0 || date.getDate() === 1) _CanvasHelper.CanvasHelper.drawLine(this.context, xStart, yStart, xStart + this.config.style.squareSideLength, yStart, color); // Draw a border to the left if it is one of the first days of the month if (date.getDate() <= 7) _CanvasHelper.CanvasHelper.drawLine(this.context, xStart, yStart, xStart, yStart + this.config.style.squareSideLength, color); var lastDay = _JsHelper.JsHelper.getDaysInMonth(date.getMonth(), date.getYear()); // Draw a border if the square is at the bottom if (this.config.chart.skipWeekend && date.getDay() === 5 || date.getDay() === 6 || date.getDate() === lastDay) _CanvasHelper.CanvasHelper.drawLine(this.context, xStart, yStart + this.config.style.squareSideLength, xStart + this.config.style.squareSideLength, yStart + this.config.style.squareSideLength, color); // Draw a border to the right if it is one of the last days of the month if (date.getDate() > lastDay - 7) _CanvasHelper.CanvasHelper.drawLine(this.context, xStart + this.config.style.squareSideLength, yStart, xStart + this.config.style.squareSideLength, yStart + this.config.style.squareSideLength, color); } }, { key: "prepareData", /** * Prepares the data for rendering */ value: function prepareData() { this.config.chart.startDate = new Date(this.config.chart.startDate); this.config.chart.endDate = new Date(this.config.chart.endDate); var normalizer = 0; var min = void 0; if (this.config.data.length > 0) { min = _JsHelper.JsHelper.minObjectValue(this.config.data, "value"); var max = _JsHelper.JsHelper.maxObjectValue(this.config.data, "value"); normalizer = max - min; _JsHelper.JsHelper.sortObjectArrayByDate(this.config.data); } var date = this.config.chart.startDate; for (var i = 0; i <= _JsHelper.JsHelper.getNumberOfDaysBetweenDates(this.config.chart.startDate, this.config.chart.endDate); i++) { if (normalizer !== 0 && i < this.config.data.length && date.getTime() === new Date(this.config.data[i].date).getTime()) { if (this.config.data[i].value !== null && this.config.data[i].value !== undefined) this.config.data[i].color = this.valueToColor((this.config.data[i].value - min) / normalizer);else { this.config.data[i].color = this.valueToColor(null); } } else { this.config.data.splice(i, 0, { date: date, value: "N/A", color: this.valueToColor("N/A") }); } date = new Date(date.getTime() + this.config.constants.dayInMs); } } }, { key: "drawChart", /** * Renders the chart */ value: function drawChart() { var column = 0; for (var i = 0; i < this.config.data.length; i++) { var date = new Date(this.config.data[i].date); if (this.config.chart.skipWeekend && date.getDay() === 6) { if (i + 2 < this.config.data.length) { i = i + 2; date = new Date(this.config.data[i].date); } else { return; } } this.config.data[i].x = this.config.style.xMargin + this.config.style.squareSideLength * column; this.config.data[i].y = this.getYOffset() + this.config.style.squareSideLength * date.getDay(); _CanvasHelper.CanvasHelper.drawSquare(this.context, this.config.data[i].x, this.config.data[i].y, this.config.style.squareSideLength, this.config.color.dateOutline, this.config.data[i].color); this.drawMonthBorder(date, this.config.data[i].x, this.config.data[i].y); if (this.config.chart.skipWeekend && date.getDay() === 5 || date.getDay() === 6) column++; } } }, { key: "getCanvasHelper", value: function getCanvasHelper() { return _CanvasHelper.CanvasHelper; } }, { key: "updateChart", value: function updateChart(data) { this.config.data = data; this.prepareData(); this.drawChart(); } }], [{ key: "validateConfig", /** * Validates whether the minimum values have been provided to the configuration * * @param config */ value: function validateConfig(config) { if (config.id === null || config.id === undefined || config.id === "") throw new Error("canvas-calendar-chart: Please provide an id in the Calendar configuration object"); if (config.chart.startDate === null || config.chart.startDate === undefined || config.chart.startDate === "") throw new Error("canvas-calendar-chart: Please provide a start date in the Calendar configuration object"); if (config.chart.endDate === null || config.chart.endDate === undefined || config.chart.endDate === "") throw new Error("canvas-calendar-chart: Please provide an end date in the Calendar configuration object"); } }]); return Calendar; }(); },{"./CanvasHelper":3,"./JsHelper":4,"./defaultConfigurations":5}],2:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CalendarFactory = undefined; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /** * Created by Ivo Zeba on 5/14/17. * * Factory used to create calendar instances */ var _Calendar = require("./Calendar"); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var CalendarFactory = exports.CalendarFactory = function () { function CalendarFactory() { _classCallCheck(this, CalendarFactory); } _createClass(CalendarFactory, null, [{ key: "createChart", value: function createChart(config) { return new _Calendar.Calendar(config); } }]); return CalendarFactory; }(); // Make CalendarFactory available in the global scope window.CalendarFactory = CalendarFactory; },{"./Calendar":1}],3:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * Created by Ivo Zeba on 5/14/17. * * A collection of helper methods for drawing on the canvas */ var CanvasHelper = exports.CanvasHelper = function () { function CanvasHelper() { _classCallCheck(this, CanvasHelper); } _createClass(CanvasHelper, null, [{ key: "drawLine", /** * Draws a line between two points * * @param context Canvas context * @param startX X-coordinate of first point * @param startY Y-coordinate of first point * @param endX X-coordinate of second point * @param endY Y-coordinate of second point * @param color Hex value of the color of the line */ value: function drawLine(context, startX, startY, endX, endY, color) { context.beginPath(); context.strokeStyle = color; context.moveTo(startX, startY); context.lineTo(endX, endY); context.stroke(); context.closePath(); } }, { key: "drawSquare", /** * Draws and fills square * * @param context Canvas context * @param startX X-coordinate of first point * @param startY Y-coordinate of first point * @param squareSideLength * @param strokeColor Color of the edges * @param fillColor Color of the square */ value: function drawSquare(context, startX, startY, squareSideLength, strokeColor, fillColor) { context.beginPath(); context.clearRect(startX, startY, squareSideLength, squareSideLength); context.rect(startX, startY, squareSideLength, squareSideLength); context.strokeStyle = strokeColor; context.fillStyle = fillColor; context.fill(); context.stroke(); context.closePath(); } }]); return CanvasHelper; }(); },{}],4:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * Created by Ivo Zeba on 5/14/17. * * Common javascript helper functions */ var JsHelper = exports.JsHelper = function () { function JsHelper() { _classCallCheck(this, JsHelper); } _createClass(JsHelper, null, [{ key: "getDaysInMonth", /** * Returns the number of days within a month * * @param month * @param year * @return {number} */ value: function getDaysInMonth(month, year) { return new Date(year, month + 1, 0).getDate(); } }, { key: "minObjectValue", /** * Returns the minimum value of a specified attribute in an object array * * @param data Object array * @param attribute Attribute of the object for which the minimum will be calculated for * @return {*} */ value: function minObjectValue(data, attribute) { return Math.min.apply(Math, data.map(function (object) { return object[attribute]; })); } /** * Returns the maximum value of a specified attribute in an object array * * @param data Object array * @param attribute Attribute of the object for which the maximum will be calculated for * @return {*} */ }, { key: "maxObjectValue", value: function maxObjectValue(data, attribute) { return Math.max.apply(Math, data.map(function (object) { return object[attribute]; })); } /** * In place sort of an object array by date * * @param data Object array with an object that has the attribute date defined */ }, { key: "sortObjectArrayByDate", value: function sortObjectArrayByDate(data) { data.sort(function (a, b) { var dateA = new Date(a.date); var dateB = new Date(b.date); if (dateA.getTime() < dateB.getTime()) return -1; if (dateA.getTime() > dateB.getTime()) return 1; return 0; }); } /** * Returns the number of days between two dates * * @param date1 * @param date2 * @return {number} */ }, { key: "getNumberOfDaysBetweenDates", value: function getNumberOfDaysBetweenDates(date1, date2) { var endDate = new Date(date2); var startDate = new Date(date1); return Math.abs(endDate.getTime() - startDate.getTime()) / 86400000; } /** * Returns the number of months between two dates * * @param date1 * @param date2 * @return {number} */ }, { key: "getNumberOfMonthsBetweenDates", value: function getNumberOfMonthsBetweenDates(date1, date2) { var endDate = new Date(date2); var startDate = new Date(date1); return endDate.getMonth() - startDate.getMonth() + 12 * (endDate.getFullYear() - startDate.getFullYear()); } /** * Returns a string formatted date * * @param date * @param months * @return {string} */ }, { key: "getDateString", value: function getDateString(date, months) { var jsDate = new Date(date); return months[jsDate.getMonth()] + ". " + jsDate.getDate() + " " + jsDate.getFullYear(); } }]); return JsHelper; }(); },{}],5:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /** * Created by Ivo Zeba on 5/14/17. * * Default configuration object */ var defaultConfig = exports.defaultConfig = { color: { missingValue: "#000000", missingDate: "#FFFFFF", dateOutline: "#f1f1f1", monthOutline: "#000000", range: ["#D7191C", "#FDAE61", "#FFFFBF", "#A6D96A", "#1A9641"] }, constants: { dayInMs: 86400000 }, chart: { width: 1500, height: 140, skipWeekend: false }, data: [], events: { onClick: null, onHover: { date: null, value: null } }, style: { squareSideLength: 17, xMargin: 30, yMargin: 17 }, text: { font: "Sans-serif", fontSize: 10, months: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], weekDays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] } }; },{}]},{},[2]);