UNPKG

canvas-calendar-chart

Version:

Calendar chart using an HTML canvas

377 lines (317 loc) 14.5 kB
/** * Created by Ivo Zeba on 5/6/17. * * Calendar component */ import { defaultConfig } from "./defaultConfigurations"; import { CanvasHelper } from "./CanvasHelper"; import { JsHelper } from "./JsHelper"; export class Calendar { /** * Default constructor * * @param config */ constructor(config) { Calendar.validateConfig(config); this.setConfig(config); this.initializeCanvas(); this.prepareData(); this.renderChart(); }; /** * Validates whether the minimum values have been provided to the configuration * * @param config */ static 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"); } /** * Merges the provided configuration with the default configuration set * * @param config */ setConfig(config) { let configCopy = JSON.parse(JSON.stringify(config)); this.config = JSON.parse(JSON.stringify(defaultConfig)); this.config.id = configCopy.id; for(let propertyName in configCopy) { if (configCopy.hasOwnProperty(propertyName) && 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 */ initializeCanvas() { let 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); }; /** * Destroys the canvas */ destroy() { let 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 */ onHover(event) { let 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"; let tooltipValueText; 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"; let tooltipDateText; if(this.config.events.onHover.date === null) tooltipDateText = "Date: " + 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 */ onClick(event) { let 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 {*} */ getPointFromCoordinates(event) { let x = event.clientX - this.canvas.getBoundingClientRect().left; let y = event.clientY - this.canvas.getBoundingClientRect().top; for(let i = 0; i < this.config.data.length; i++) { let point = this.config.data[i]; let 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 */ 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} */ getFontString() { return "" + this.config.text.fontSize + " " + this.config.text.font; }; /** * 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} */ getYOffset() { return this.config.chart.skipWeekend ? 0 : this.config.style.yMargin; }; /** * Renders the individual weekday names onto the canvas */ drawWeekDays() { let xMargin = 5; let yMargin = this.getYOffset() + 12; for(let 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); } } }; /** * Renders the individual month names onto the canvas */ drawMonths() { let numberOfMonths = JsHelper.getNumberOfMonthsBetweenDates(this.config.chart.startDate, this.config.chart.endDate); let xMargin = 60; let yMargin = 12; let monthCounter = this.config.chart.startDate.getMonth(); for(let 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; } }; /** * 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 {*} */ valueToColor(value) { if(value === null || value === undefined) return this.config.color.missingValue; else if(value === "N/A") return this.config.color.missingDate; for(let 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]; } }; /** * 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 */ drawMonthBorder(date, xStart, yStart) { let 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.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.drawLine(this.context, xStart, yStart, xStart, yStart+this.config.style.squareSideLength, color); let lastDay = 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.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.drawLine(this.context, xStart+this.config.style.squareSideLength, yStart, xStart+this.config.style.squareSideLength, yStart+this.config.style.squareSideLength, color); }; /** * Prepares the data for rendering */ prepareData() { this.config.chart.startDate = new Date(this.config.chart.startDate); this.config.chart.endDate = new Date(this.config.chart.endDate); let normalizer = 0; let min; if(this.config.data.length > 0) { min = JsHelper.minObjectValue(this.config.data, "value"); let max = JsHelper.maxObjectValue(this.config.data, "value"); normalizer = max - min; JsHelper.sortObjectArrayByDate(this.config.data); } let date = this.config.chart.startDate; for(let i = 0; i <= 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); } }; /** * Renders the chart */ drawChart() { let column = 0; for (let i = 0; i < this.config.data.length; i++) { let 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.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++; } }; getCanvasHelper() { return CanvasHelper; } updateChart(data) { this.config.data = data; this.prepareData(); this.drawChart(); } }