canvas-calendar-chart
Version:
Calendar chart using an HTML canvas
715 lines (584 loc) • 32 kB
JavaScript
(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]);