import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; // constants // types // import { Data } from "../types/Data"; // import { MonthLabel } from "../types/MonthLabel"; // import { WeekdayLabel } from "../types/WeekdayLabel"; type Data = { date: string; activities: any[]; }; // constants // types // import { Data } from "../types/Data"; // import { MonthLabel } from "../types/MonthLabel"; // import { WeekdayLabel } from "../types/WeekdayLabel"; type MonthKey = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; // constants // types // import { Data } from "../types/Data"; // import { MonthLabel } from "../types/MonthLabel"; // import { WeekdayLabel } from "../types/WeekdayLabel"; type MonthLabel = Record; // constants // types // import { Data } from "../types/Data"; // import { MonthLabel } from "../types/MonthLabel"; // import { WeekdayLabel } from "../types/WeekdayLabel"; type WeekdayKey = 0 | 1 | 2 | 3 | 4 | 5 | 6; // constants // types // import { Data } from "../types/Data"; // import { MonthLabel } from "../types/MonthLabel"; // import { WeekdayLabel } from "../types/WeekdayLabel"; type WeekdayLabel = Record; // components // constants // types // import { Data } from "../types/Data"; // import { MonthLabel } from "../types/MonthLabel"; // import { WeekdayLabel } from "../types/WeekdayLabel"; // components type DaysProps = { // General props data: Data[]; daysToRender?: number; // Event Handler clickHandler?: Function; // General props - summary showSummary?: boolean; summaryText?: string; // General props - levels showLevels?: boolean; levelColorMode?: "light" | "dark"; levelColors?: string[]; levelLabelLess?: string; levelLabelMore?: string; // General props - tooltip showTooltip?: boolean; tooltipBgColor?: string; tooltipTextColor?: string; tooltipText?: string; // 'day' specific props weekStart?: WeekdayKey; showWeekdayLabels?: boolean; weekdayLabel?: WeekdayLabel; showMonthLabels?: boolean; monthLabel?: MonthLabel; }; // Util import CONSTANTS from "../constants/constants"; // Util const getSafeWeekStart = (weekStart) => Math.max(Math.min(weekStart ?? 0, 6), 0); // Day Block // Day Block const getDayDiffToStartOfCalendar = (weekStart) => { const todayDt = new Date(); const oneYearAgoDt = new Date(); oneYearAgoDt.setDate(todayDt.getDate() - 364); const oneYearAgoDayOfWeek = oneYearAgoDt.getDay(); const dayDiffToStartOfCalendar = oneYearAgoDayOfWeek - weekStart; return dayDiffToStartOfCalendar; }; const getContainerWidth = (weekStart, daysToRender, showWeekdayLabels) => { const daysContainerWidth = getDaysContainerWidth( getSafeWeekStart(weekStart), daysToRender ); return daysContainerWidth + (showWeekdayLabels ?? true ? 42 : 0) + "px"; }; const getDaysContainerWidth = (weekStart, daysToRender) => { const _weekStart = getSafeWeekStart(weekStart); const todayDayOfWeek = new Date().getDay(); let numOfDaysInLastColumn = 0; if (!_weekStart) { numOfDaysInLastColumn = todayDayOfWeek + 1; } else { numOfDaysInLastColumn = _weekStart > todayDayOfWeek ? todayDayOfWeek + 7 + 1 - _weekStart : todayDayOfWeek + 1 - _weekStart; } let numOfDaysInRemainingColumns = 0; if (!daysToRender && daysToRender !== 0) { const dayDiffToStartOfCalendar = getDayDiffToStartOfCalendar(_weekStart); numOfDaysInRemainingColumns = 365 + dayDiffToStartOfCalendar - numOfDaysInLastColumn; } else { numOfDaysInRemainingColumns = daysToRender - numOfDaysInLastColumn; } return Math.ceil(numOfDaysInRemainingColumns / 7 + 1) * 14; }; const getDayLevel = ({ numOfLevels, activitiesPerc }) => { for (let i = 1; i <= numOfLevels; i++) { if (activitiesPerc <= i / numOfLevels) { return i; } } }; const getDays = ({ levelColors, data, weekStart, daysToRender }) => { const _weekStart = getSafeWeekStart(weekStart); // 1. Init helper variables const numOfLevels = levelColors?.length ? levelColors.length - 1 : 4; let maxNumOfContributions = 0; const dataMap = new Map(); (data || []).forEach((day) => { maxNumOfContributions = Math.max( maxNumOfContributions, day?.activities?.length || 0 ); dataMap.set(day.date, day.activities); }); // 3. Merge the activities to days array return new Array( daysToRender ? daysToRender : 365 + getDayDiffToStartOfCalendar(_weekStart) ) .fill(0) .map((_, index) => { const dt = new Date(); dt.setDate(dt.getDate() - index); const year = dt.getFullYear(); const month = dt.getMonth() + 1; const day = dt.getDate(); const id = year + "-" + (month + "").padStart(2, "0") + "-" + (day + "").padStart(2, "0"); const activities = dataMap.get(id) || []; const activitiesPerc = activities.length / maxNumOfContributions; const level = activities.length === 0 ? 0 : getDayLevel({ numOfLevels, activitiesPerc, }); return { id, year, month, day, dayOfWeek: dt.getDay(), dayDiffFromToday: index, activities, level, }; }); }; const getDayColor = ({ level, levelColors, levelColorMode }) => { const _levelColors = getLevelColors({ levelColors, levelColorMode, }); return _levelColors[level]; }; const getDayTop = (arg) => { const { dayOfWeek = 0, weekStart = 0 } = arg; const _weekStart = getSafeWeekStart(weekStart); if (dayOfWeek - _weekStart < 0) { return (dayOfWeek - _weekStart + 7) * 14; } return (dayOfWeek - _weekStart) * 14; }; const getDayRight = (arg) => { const { dayDiffFromToday = 0, dayOfWeek = 0, weekStart = 0 } = arg; const todayDayOfWeek = new Date().getDay(); return ( (Math.floor(dayDiffFromToday / 7) + (dayOfWeek > todayDayOfWeek ? 1 : 0) + (dayOfWeek < getSafeWeekStart(weekStart) ? 1 : 0) + (weekStart > todayDayOfWeek ? -1 : 0)) * 14 ); }; // Week Block // Week Block const getWeekdayLabels = (weekdayLabel?: WeekdayLabel, weekStart = 0) => { const _weekStart = getSafeWeekStart(weekStart); let _weekdayLabels: string[] = []; for (let i = 0; i < 7; i++) { let weekStartId = _weekStart + i >= 7 ? _weekStart + i - 7 : _weekStart + i; if (weekdayLabel) { // If `weekdayLabel` is available // => only render `weekdayLabel` value _weekdayLabels.push(weekdayLabel[weekStartId] || ""); } else if ([1, 3, 5].includes(i)) { // Otherwise, render only the 2nd, 4th, 6th value _weekdayLabels.push(CONSTANTS.WEEK_MAP[weekStartId]); } else { _weekdayLabels.push(""); } } return _weekdayLabels; }; // Month Block // Month Block const getMonthRight = (arg) => { // day's right position - width + day's width return getDayRight(arg) - 36 + 12; }; const getMonthLabel = ({ monthLabel, dt, CONSTANTS }) => { return monthLabel?.[dt.month] ?? CONSTANTS.MONTH_MAP[dt.month]; }; // Summary Block // Summary Block const getSummary = ({ summaryText, levelColors, data, weekStart, daysToRender, }) => { const days = getDays({ levelColors, data, weekStart, daysToRender, }); const count = days.reduce((acc, cur) => { return acc + cur.activities.length; }, 0); if (summaryText) { return summaryText.replaceAll("{{count}}", count); } return `${count} activities in this period`; }; // Level Block 1000 -> // Level Block 1000 -> const getLevelColors = ({ levelColors, levelColorMode }) => { return ( levelColors ?? (levelColorMode === "dark" ? CONSTANTS.LEVEL_COLOR.dark : CONSTANTS.LEVEL_COLOR.light) ); }; @Component({ selector: "days, Days", template: `
{{getSummary({ summaryText: summaryText, levelColors: levelColors, data: data, weekStart: weekStart, daysToRender: daysToRender })}}
{{levelLabelLess ?? 'Less'}}
{{levelLabelMore ?? 'More'}}
`, styles: [ ` :host { display: contents; } `, ], }) export default class Days { getContainerWidth = getContainerWidth; getDaysContainerWidth = getDaysContainerWidth; getDays = getDays; getDayColor = getDayColor; getDayTop = getDayTop; getDayRight = getDayRight; getWeekdayLabels = getWeekdayLabels; getMonthRight = getMonthRight; getMonthLabel = getMonthLabel; getSummary = getSummary; getLevelColors = getLevelColors; CONSTANTS = CONSTANTS; @Input() weekStart!: DaysProps["weekStart"]; @Input() daysToRender!: DaysProps["daysToRender"]; @Input() showWeekdayLabels!: DaysProps["showWeekdayLabels"]; @Input() showMonthLabels!: DaysProps["showMonthLabels"]; @Input() weekdayLabel!: DaysProps["weekdayLabel"]; @Input() data!: DaysProps["data"]; @Input() levelColors!: DaysProps["levelColors"]; @Input() monthLabel!: DaysProps["monthLabel"]; @Input() clickHandler!: DaysProps["clickHandler"]; @Input() levelColorMode!: DaysProps["levelColorMode"]; @Input() showTooltip!: DaysProps["showTooltip"]; @Input() tooltipBgColor!: DaysProps["tooltipBgColor"]; @Input() tooltipTextColor!: DaysProps["tooltipTextColor"]; @Input() tooltipText!: DaysProps["tooltipText"]; @Input() showSummary!: DaysProps["showSummary"]; @Input() summaryText!: DaysProps["summaryText"]; @Input() showLevels!: DaysProps["showLevels"]; @Input() levelLabelLess!: DaysProps["levelLabelLess"]; @Input() levelLabelMore!: DaysProps["levelLabelMore"]; trackByDayOfWeek0(index, dayOfWeek) { return index; } trackByDt1(index, dt) { return index; } trackByLevelLabel2(index, levelLabel) { return index; } useObjectWrapper(...args: any[]) { let obj = {}; args.forEach((arg) => { obj = { ...obj, ...arg }; }); return obj; } } @NgModule({ declarations: [Days], imports: [CommonModule, WeekModule, MonthModule, DayModule], exports: [Days], }) export class DaysModule {}