{"version":3,"file":"calendar.mjs","names":[],"sources":["../../src/utils/calendar.ts"],"sourcesContent":["import type { Day as WeekDay } from 'date-fns';\nimport {\n  differenceInCalendarDays,\n  eachDayOfInterval,\n  endOfYear,\n  formatISO,\n  getDay,\n  getMonth,\n  nextDay,\n  parseISO,\n  startOfYear,\n  subWeeks,\n} from 'date-fns';\n\nimport type { Activity, Week } from '@/types/charts';\nimport { isOnSeverSide } from '@/utils/index';\n\nexport const DEFAULT_MONTH_LABELS = [\n  'Jan',\n  'Feb',\n  'Mar',\n  'Apr',\n  'May',\n  'Jun',\n  'Jul',\n  'Aug',\n  'Sep',\n  'Oct',\n  'Nov',\n  'Dec',\n];\n\nexport const DEFAULT_LABELS = {\n  legend: {\n    less: 'Less',\n    more: 'More',\n  },\n  months: DEFAULT_MONTH_LABELS,\n  tooltip: '{{count}} activities on {{date}}',\n  totalCount: '{{count}} activities in {{year}}',\n  weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],\n};\n\ninterface MonthLabel {\n  label: string;\n  weekIndex: number;\n}\n\nconst fillHoles = (activities: Array<Activity>): Array<Activity> => {\n  if (activities.length === 0) {\n    return [];\n  }\n\n  const calendar = new Map<string, Activity>(activities.map((a) => [a.date, a]));\n  const firstActivity = activities[0] as Activity;\n  const lastActivity = activities.at(-1) as Activity;\n\n  return eachDayOfInterval({\n    end: parseISO(lastActivity.date),\n    start: parseISO(firstActivity.date),\n  }).map((day) => {\n    const date = formatISO(day, { representation: 'date' });\n\n    if (calendar.has(date)) {\n      return calendar.get(date) as Activity;\n    }\n\n    return {\n      count: 0,\n      date,\n      level: 0,\n    };\n  });\n};\n\nconst calcTextDimensions = (text: string, fontSize: number) => {\n  if (isOnSeverSide) return;\n\n  if (fontSize < 1) {\n    throw new RangeError('fontSize must be positive');\n  }\n\n  if (text.length === 0) {\n    return { height: 0, width: 0 };\n  }\n\n  const namespace = 'http://www.w3.org/2000/svg';\n  const svg = document.createElementNS(namespace, 'svg');\n\n  svg.style.position = 'absolute';\n  svg.style.visibility = 'hidden';\n  svg.style.fontFamily = window.getComputedStyle(document.body).fontFamily;\n  svg.style.fontSize = `${fontSize}px`;\n\n  const textNode = document.createElementNS(namespace, 'text');\n  textNode.textContent = text;\n\n  svg.append(textNode);\n  document.body.append(svg);\n  const boundingBox = textNode.getBBox();\n\n  svg.remove();\n\n  return { height: boundingBox.height, width: boundingBox.width };\n};\n\nexport const groupByWeeks = (\n  activities: Array<Activity>,\n  weekStart: WeekDay = 0, // 0 = Sunday\n): Array<Week> => {\n  if (activities.length === 0) {\n    return [];\n  }\n\n  const normalizedActivities = fillHoles(activities);\n\n  // Determine the first date of the calendar. If the first date is not the\n  // set start weekday, the selected weekday one week earlier is used.\n  const firstActivity = normalizedActivities[0] as Activity;\n  const firstDate = parseISO(firstActivity.date);\n  const firstCalendarDate =\n    getDay(firstDate) === weekStart ? firstDate : subWeeks(nextDay(firstDate, weekStart), 1);\n\n  // To correctly group activities by week, it is necessary to left-pad the list\n  // because the first date might not be set start weekday.\n  const paddedActivities = [\n    ...(Array.from({ length: differenceInCalendarDays(firstDate, firstCalendarDate) }).fill(\n      null,\n    ) as Array<Activity>),\n    ...normalizedActivities,\n  ];\n\n  const numberOfWeeks = Math.ceil(paddedActivities.length / 7);\n\n  // Finally, group activities by week\n  return Array.from({ length: numberOfWeeks })\n    .fill(null)\n    .map((_, weekIndex) => paddedActivities.slice(weekIndex * 7, weekIndex * 7 + 7));\n};\n\nexport const getMonthLabels = (\n  weeks: Array<Week>,\n  monthNames: Array<string> = DEFAULT_MONTH_LABELS,\n): Array<MonthLabel> => {\n  return weeks\n    .reduce<Array<MonthLabel>>((labels, week, weekIndex) => {\n      const firstActivity = week.filter(Boolean).find((activity) => activity !== undefined);\n\n      if (!firstActivity) {\n        throw new Error(`Unexpected error: Week ${weekIndex + 1} is empty: [${week}].`);\n      }\n\n      const month = monthNames[getMonth(parseISO(firstActivity.date))];\n\n      if (!month) {\n        const monthName = new Date(firstActivity.date).toLocaleString('en-US', { month: 'short' });\n        throw new Error(`Unexpected error: undefined month label for ${monthName}.`);\n      }\n\n      const prevLabel = labels.at(-1);\n\n      if (weekIndex === 0 || !prevLabel || prevLabel.label !== month) {\n        return [...labels, { label: month, weekIndex }];\n      }\n\n      return labels;\n    }, [])\n    .filter(({ weekIndex }, index, labels) => {\n      // Labels should only be shown if there is \"enough\" space (data).\n      // This is a naive implementation that does not take the block size,\n      // font size, etc. into account.\n      const minWeeks = 3;\n\n      // Skip the first month label if there is not enough space to the next one.\n      if (index === 0) {\n        return labels[1] && labels[1].weekIndex - weekIndex >= minWeeks;\n      }\n\n      // Skip the last month label if there is not enough data in that month\n      // to avoid overflowing the calendar on the right.\n      if (index === labels.length - 1) {\n        return weeks.slice(weekIndex).length >= minWeeks;\n      }\n\n      return true;\n    });\n};\n\nexport const generateEmptyData = (): Array<Activity> => {\n  const year = new Date().getFullYear();\n  const days = eachDayOfInterval({\n    end: new Date(year, 11, 31),\n    start: new Date(year, 0, 1),\n  });\n\n  return days.map((date) => ({\n    count: 0,\n    date: formatISO(date, { representation: 'date' }),\n    level: 0,\n  }));\n};\n\nexport const generateTestData = (args: {\n  interval?: { end: Date; start: Date };\n  maxLevel?: number;\n}): Array<Activity> => {\n  const maxCount = 20;\n  const maxLevel = args.maxLevel ? Math.max(1, args.maxLevel) : 4;\n  const now = new Date();\n\n  const days = eachDayOfInterval(\n    args.interval ?? {\n      end: endOfYear(now),\n      start: startOfYear(now),\n    },\n  );\n\n  return days.map((date) => {\n    // The random activity count is shifted by up to 80% towards zero.\n    const c = Math.round(Math.random() * maxCount - Math.random() * (0.8 * maxCount));\n    const count = Math.max(0, c);\n    const level = Math.ceil((count / maxCount) * maxLevel);\n\n    return {\n      count,\n      date: formatISO(date, { representation: 'date' }),\n      level,\n    };\n  });\n};\n\nexport const maxWeekdayLabelLength = (\n  firstWeek: Week,\n  weekStart: number,\n  labels: string[],\n  fontSize: number,\n): number => {\n  if (isOnSeverSide) return 0;\n  if (labels.length !== 7) {\n    throw new Error('Exactly 7 labels, one for each weekday must be passed.');\n  }\n\n  return firstWeek.reduce((maxLength, _, index) => {\n    if (index % 2 !== 0) {\n      const dayIndex = (index + weekStart) % 7;\n      const label = labels[dayIndex] as string;\n      // @ts-ignore\n      const curLength = Math.ceil(calcTextDimensions(label, fontSize).width);\n\n      return Math.max(maxLength, curLength);\n    }\n\n    return maxLength;\n  }, 0);\n};\n"],"mappings":"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"}