UNPKG

@nivo/calendar

Version:
1 lines 101 kB
{"version":3,"file":"nivo-calendar.mjs","sources":["../src/CalendarYearLegends.tsx","../src/CalendarMonthPath.tsx","../src/CalendarMonthLegends.tsx","../src/CalendarDay.tsx","../src/CalendarTooltip.tsx","../src/props.ts","../src/compute/calendar.ts","../src/hooks.ts","../src/Calendar.tsx","../src/compute/timeRange.ts","../src/TimeRangeDay.tsx","../src/TimeRange.tsx","../src/ResponsiveTimeRange.tsx","../src/ResponsiveCalendar.tsx","../src/CalendarCanvas.tsx","../src/ResponsiveCalendarCanvas.tsx"],"sourcesContent":["import { memo } from 'react'\nimport { Text } from '@nivo/text'\nimport { CalendarYearLegendsProps } from './types'\n\nexport const CalendarYearLegends = memo(({ years, legend, theme }: CalendarYearLegendsProps) => {\n return (\n <>\n {years.map(year => {\n return (\n <Text\n key={year.year}\n transform={`translate(${year.x},${year.y}) rotate(${year.rotation})`}\n textAnchor=\"middle\"\n style={theme.labels.text}\n >\n {legend(year.year)}\n </Text>\n )\n })}\n </>\n )\n})\n","import { CalendarMonthPathProps } from './types'\nimport { memo } from 'react'\n\nexport const CalendarMonthPath = memo(\n ({ path, borderWidth, borderColor }: CalendarMonthPathProps) => {\n return (\n <path\n d={path}\n style={{\n fill: 'none',\n strokeWidth: borderWidth,\n stroke: borderColor,\n pointerEvents: 'none',\n }}\n />\n )\n }\n)\n","import { memo } from 'react'\nimport { Text } from '@nivo/text'\nimport { CalendarMonthLegendsProps } from './types'\n\nexport const CalendarMonthLegends = memo(({ months, legend, theme }: CalendarMonthLegendsProps) => {\n return (\n <>\n {months.map(month => {\n return (\n <Text\n key={`${month.date.toString()}.legend`}\n transform={`translate(${month.x},${month.y}) rotate(${month.rotation})`}\n textAnchor=\"middle\"\n style={theme.labels.text}\n >\n {legend(month.year, month.month, month.date)}\n </Text>\n )\n })}\n </>\n )\n})\n","import { CalendarDayProps } from './types'\nimport { useTooltip } from '@nivo/tooltip'\nimport { memo, useCallback } from 'react'\nimport * as React from 'react'\n\nexport const CalendarDay = memo(\n ({\n data,\n x,\n y,\n size,\n color,\n borderWidth,\n borderColor,\n isInteractive,\n tooltip,\n onMouseEnter,\n onMouseMove,\n onMouseLeave,\n onClick,\n formatValue,\n }: CalendarDayProps) => {\n const { showTooltipFromEvent, hideTooltip } = useTooltip()\n\n const handleMouseEnter = useCallback(\n (event: React.MouseEvent<SVGRectElement>) => {\n if (!('value' in data)) {\n return\n }\n\n const formatedData = {\n ...data,\n value: formatValue(data.value),\n data: { ...data.data },\n }\n showTooltipFromEvent(React.createElement(tooltip, { ...formatedData }), event)\n onMouseEnter?.(data, event)\n },\n [showTooltipFromEvent, tooltip, data, onMouseEnter, formatValue]\n )\n const handleMouseMove = useCallback(\n (event: React.MouseEvent<SVGRectElement>) => {\n if (!('value' in data)) {\n return\n }\n\n const formatedData = {\n ...data,\n value: formatValue(data.value),\n data: { ...data.data },\n }\n showTooltipFromEvent(React.createElement(tooltip, { ...formatedData }), event)\n onMouseMove?.(data, event)\n },\n [showTooltipFromEvent, tooltip, data, onMouseMove, formatValue]\n )\n const handleMouseLeave = useCallback(\n (event: React.MouseEvent<SVGRectElement>) => {\n if (!('value' in data)) {\n return\n }\n\n hideTooltip()\n onMouseLeave?.(data, event)\n },\n [hideTooltip, data, onMouseLeave]\n )\n const handleClick = useCallback(\n (event: React.MouseEvent<SVGRectElement>) => onClick?.(data, event),\n [data, onClick]\n )\n\n return (\n <rect\n x={x}\n y={y}\n width={size}\n height={size}\n style={{\n fill: color,\n strokeWidth: borderWidth,\n stroke: borderColor,\n }}\n onMouseEnter={isInteractive ? handleMouseEnter : undefined}\n onMouseMove={isInteractive ? handleMouseMove : undefined}\n onMouseLeave={isInteractive ? handleMouseLeave : undefined}\n onClick={isInteractive ? handleClick : undefined}\n />\n )\n }\n)\n","import { BasicTooltip } from '@nivo/tooltip'\nimport { CalendarTooltipProps } from './types'\nimport { memo } from 'react'\n\nexport const CalendarTooltip = memo(({ value, day, color }: CalendarTooltipProps) => {\n if (value === undefined || isNaN(Number(value))) return null\n return <BasicTooltip id={day} value={value} color={color} enableChip={true} />\n})\n","import { timeFormat } from 'd3-time-format'\nimport { CalendarLegendProps } from './types'\nimport { CalendarTooltip } from './CalendarTooltip'\n\nconst monthLabelFormat = timeFormat('%b')\n\nconst commonDefaultProps = {\n colors: ['#61cdbb', '#97e3d5', '#e8c1a0', '#f47560'] as string[],\n\n align: 'center',\n direction: 'horizontal',\n emptyColor: '#fff',\n\n minValue: 0,\n maxValue: 'auto',\n\n yearSpacing: 30,\n yearLegend: (year: number) => year,\n yearLegendPosition: 'before',\n yearLegendOffset: 10,\n\n monthBorderWidth: 2,\n monthBorderColor: '#000',\n monthSpacing: 0,\n monthLegend: (_year: number, _month: number, date: Date) => monthLabelFormat(date),\n monthLegendPosition: 'before',\n monthLegendOffset: 10,\n\n daySpacing: 0,\n dayBorderWidth: 1,\n dayBorderColor: '#000',\n\n isInteractive: true,\n\n legends: [] as CalendarLegendProps[],\n tooltip: CalendarTooltip,\n} as const\n\nexport const calendarDefaultProps = {\n ...commonDefaultProps,\n role: 'img',\n} as const\n\nexport const calendarCanvasDefaultProps = {\n ...commonDefaultProps,\n pixelRatio: typeof window !== 'undefined' ? (window.devicePixelRatio ?? 1) : 1,\n} as const\n\nexport const timeRangeDefaultProps = {\n ...calendarDefaultProps,\n dayBorderColor: '#fff',\n dayRadius: 0,\n square: true,\n weekdayLegendOffset: 75,\n firstWeekday: 'sunday',\n weekdays: [\n 'Sunday',\n 'Monday',\n 'Tuesday',\n 'Wednesday',\n 'Thursday',\n 'Friday',\n 'Saturday',\n ] as string[],\n} as const\n","import isDate from 'lodash/isDate.js'\nimport memoize from 'lodash/memoize.js'\nimport range from 'lodash/range.js'\nimport { alignBox } from '@nivo/core'\nimport { timeFormat } from 'd3-time-format'\nimport { timeDays, timeWeek, timeWeeks, timeMonths, timeYear } from 'd3-time'\nimport { ScaleQuantize } from 'd3-scale'\nimport { BBox, CalendarSvgProps, ColorScale, Datum, Year } from '../types'\n\n/**\n * Compute min/max values.\n */\nexport const computeDomain = (\n data: CalendarSvgProps['data'],\n minSpec: NonNullable<CalendarSvgProps['minValue']>,\n maxSpec: NonNullable<CalendarSvgProps['maxValue']>\n) => {\n const allValues = data.map(d => d.value)\n const minValue = minSpec === 'auto' ? Math.min(...allValues) : minSpec\n const maxValue = maxSpec === 'auto' ? Math.max(...allValues) : maxSpec\n\n return [minValue, maxValue] as const\n}\n\n/**\n * Compute day cell size according to current context.\n */\nconst computeCellSize = ({\n width,\n height,\n direction,\n yearRange,\n yearSpacing,\n monthSpacing,\n daySpacing,\n maxWeeks,\n}: Pick<\n Required<CalendarSvgProps>,\n 'direction' | 'width' | 'height' | 'yearSpacing' | 'monthSpacing' | 'daySpacing'\n> & {\n maxWeeks: number\n yearRange: number[]\n}) => {\n let hCellSize\n let vCellSize\n\n if (direction === 'horizontal') {\n hCellSize = (width - monthSpacing * 12 - daySpacing * maxWeeks) / maxWeeks\n vCellSize =\n (height - (yearRange.length - 1) * yearSpacing - yearRange.length * (8 * daySpacing)) /\n (yearRange.length * 7)\n } else {\n hCellSize =\n (width - (yearRange.length - 1) * yearSpacing - yearRange.length * (8 * daySpacing)) /\n (yearRange.length * 7)\n vCellSize = (height - monthSpacing * 12 - daySpacing * maxWeeks) / maxWeeks\n }\n\n return Math.min(hCellSize, vCellSize)\n}\n\n/**\n * Computes month path and bounding box.\n */\nconst monthPathAndBBox = ({\n date,\n cellSize,\n yearIndex,\n yearSpacing,\n monthSpacing,\n daySpacing,\n direction,\n originX,\n originY,\n}: Record<'cellSize' | 'originX' | 'originY' | 'yearIndex', number> &\n Pick<\n Required<CalendarSvgProps>,\n 'direction' | 'yearSpacing' | 'monthSpacing' | 'daySpacing'\n > & {\n date: Date\n }) => {\n // first day of next month\n const t1 = new Date(date.getFullYear(), date.getMonth() + 1, 0)\n\n // ranges\n const firstWeek = timeWeek.count(timeYear(date), date)\n const lastWeek = timeWeek.count(timeYear(t1), t1)\n const firstDay = date.getDay()\n const lastDay = t1.getDay()\n\n // offset according to year index and month\n let xO = originX\n let yO = originY\n const yearOffset = yearIndex * (7 * (cellSize + daySpacing) + yearSpacing)\n const monthOffset = date.getMonth() * monthSpacing\n if (direction === 'horizontal') {\n yO += yearOffset\n xO += monthOffset\n } else {\n yO += monthOffset\n xO += yearOffset\n }\n\n let path\n const bbox = { x: xO, y: yO, width: 0, height: 0 }\n if (direction === 'horizontal') {\n path = [\n `M${xO + (firstWeek + 1) * (cellSize + daySpacing)},${\n yO + firstDay * (cellSize + daySpacing)\n }`,\n `H${xO + firstWeek * (cellSize + daySpacing)}V${yO + 7 * (cellSize + daySpacing)}`,\n `H${xO + lastWeek * (cellSize + daySpacing)}V${\n yO + (lastDay + 1) * (cellSize + daySpacing)\n }`,\n `H${xO + (lastWeek + 1) * (cellSize + daySpacing)}V${yO}`,\n `H${xO + (firstWeek + 1) * (cellSize + daySpacing)}Z`,\n ].join('')\n\n bbox.x = xO + firstWeek * (cellSize + daySpacing)\n bbox.width = xO + (lastWeek + 1) * (cellSize + daySpacing) - bbox.x\n bbox.height = 7 * (cellSize + daySpacing)\n } else {\n path = [\n `M${xO + firstDay * (cellSize + daySpacing)},${\n yO + (firstWeek + 1) * (cellSize + daySpacing)\n }`,\n `H${xO}V${yO + (lastWeek + 1) * (cellSize + daySpacing)}`,\n `H${xO + (lastDay + 1) * (cellSize + daySpacing)}V${\n yO + lastWeek * (cellSize + daySpacing)\n }`,\n `H${xO + 7 * (cellSize + daySpacing)}V${yO + firstWeek * (cellSize + daySpacing)}`,\n `H${xO + firstDay * (cellSize + daySpacing)}Z`,\n ].join('')\n\n bbox.y = yO + firstWeek * (cellSize + daySpacing)\n bbox.width = 7 * (cellSize + daySpacing)\n bbox.height = yO + (lastWeek + 1) * (cellSize + daySpacing) - bbox.y\n }\n\n return { path, bbox }\n}\n\n/**\n * Creates a memoized version of monthPathAndBBox function.\n */\nconst memoMonthPathAndBBox = memoize(\n monthPathAndBBox,\n ({\n date,\n cellSize,\n yearIndex,\n yearSpacing,\n monthSpacing,\n daySpacing,\n direction,\n originX,\n originY,\n }) => {\n return `${date.toString()}.${cellSize}.${yearIndex}.${yearSpacing}.${monthSpacing}.${daySpacing}.${direction}.${originX}.${originY}`\n }\n)\n\n/**\n * Returns a function to Compute day cell position for horizontal layout.\n */\nconst cellPositionHorizontal = (\n cellSize: number,\n yearSpacing: number,\n monthSpacing: number,\n daySpacing: number\n) => {\n return (originX: number, originY: number, d: Date, yearIndex: number) => {\n const weekOfYear = timeWeek.count(timeYear(d), d)\n\n return {\n x:\n originX +\n weekOfYear * (cellSize + daySpacing) +\n daySpacing / 2 +\n d.getMonth() * monthSpacing,\n y:\n originY +\n d.getDay() * (cellSize + daySpacing) +\n daySpacing / 2 +\n yearIndex * (yearSpacing + 7 * (cellSize + daySpacing)),\n }\n }\n}\n\n/**\n * Returns a function to Compute day cell position for vertical layout.\n */\nconst cellPositionVertical = (\n cellSize: number,\n yearSpacing: number,\n monthSpacing: number,\n daySpacing: number\n) => {\n return (originX: number, originY: number, d: Date, yearIndex: number) => {\n const weekOfYear = timeWeek.count(timeYear(d), d)\n\n return {\n x:\n originX +\n d.getDay() * (cellSize + daySpacing) +\n daySpacing / 2 +\n yearIndex * (yearSpacing + 7 * (cellSize + daySpacing)),\n y:\n originY +\n weekOfYear * (cellSize + daySpacing) +\n daySpacing / 2 +\n d.getMonth() * monthSpacing,\n }\n }\n}\n\n// used for days range and data matching\nconst dayFormat = timeFormat('%Y-%m-%d')\n\n/**\n * Compute base layout, without caring about the current data.\n */\nexport const computeLayout = ({\n width,\n height,\n from,\n to,\n direction,\n yearSpacing,\n monthSpacing,\n daySpacing,\n align,\n}: Pick<\n Required<CalendarSvgProps>,\n | 'align'\n | 'direction'\n | 'from'\n | 'to'\n | 'width'\n | 'height'\n | 'yearSpacing'\n | 'monthSpacing'\n | 'daySpacing'\n>) => {\n const fromDate = isDate(from) ? from : new Date(from)\n const toDate = isDate(to) ? to : new Date(to)\n\n const yearRange = range(fromDate.getFullYear(), toDate.getFullYear() + 1)\n const maxWeeks =\n Math.max(\n ...yearRange.map(\n year => timeWeeks(new Date(year, 0, 1), new Date(year + 1, 0, 1)).length\n )\n ) + 1\n\n const cellSize = computeCellSize({\n width,\n height,\n direction,\n yearRange,\n yearSpacing,\n monthSpacing,\n daySpacing,\n maxWeeks,\n })\n\n const monthsSize = cellSize * maxWeeks + daySpacing * maxWeeks + monthSpacing * 12\n const yearsSize =\n (cellSize + daySpacing) * 7 * yearRange.length + yearSpacing * (yearRange.length - 1)\n\n const calendarWidth = direction === 'horizontal' ? monthsSize : yearsSize\n const calendarHeight = direction === 'horizontal' ? yearsSize : monthsSize\n const [originX, originY] = alignBox(\n {\n x: 0,\n y: 0,\n width: calendarWidth,\n height: calendarHeight,\n },\n {\n x: 0,\n y: 0,\n width,\n height,\n },\n align\n )\n\n let cellPosition: ReturnType<typeof cellPositionHorizontal>\n if (direction === 'horizontal') {\n cellPosition = cellPositionHorizontal(cellSize, yearSpacing, monthSpacing, daySpacing)\n } else {\n cellPosition = cellPositionVertical(cellSize, yearSpacing, monthSpacing, daySpacing)\n }\n\n const years: Array<{\n year: number\n bbox: BBox\n }> = []\n\n let months: Array<{\n path: string\n bbox: {\n x: number\n y: number\n width: number\n height: number\n }\n date: Date\n year: number\n month: number\n }> = []\n\n let days: Array<Omit<Datum, 'color' | 'data' | 'value'>> = []\n\n yearRange.forEach((year, i) => {\n const yearStart = new Date(year, 0, 1)\n const yearEnd = new Date(year + 1, 0, 1)\n\n days = days.concat(\n timeDays(yearStart, yearEnd).map(dayDate => {\n return {\n date: dayDate,\n day: dayFormat(dayDate),\n size: cellSize,\n ...cellPosition(originX, originY, dayDate, i),\n }\n })\n )\n\n const yearMonths = timeMonths(yearStart, yearEnd).map(monthDate => ({\n date: monthDate,\n year: monthDate.getFullYear(),\n month: monthDate.getMonth(),\n ...memoMonthPathAndBBox({\n originX,\n originY,\n date: monthDate,\n direction,\n yearIndex: i,\n yearSpacing,\n monthSpacing,\n daySpacing,\n cellSize,\n }),\n }))\n\n months = months.concat(yearMonths)\n\n years.push({\n year,\n bbox: {\n x: yearMonths[0].bbox.x,\n y: yearMonths[0].bbox.y,\n width: yearMonths[11].bbox.x - yearMonths[0].bbox.x + yearMonths[11].bbox.width,\n height: yearMonths[11].bbox.y - yearMonths[0].bbox.y + yearMonths[11].bbox.height,\n },\n })\n })\n\n return { years, months, days, cellSize, calendarWidth, calendarHeight, originX, originY }\n}\n\n/**\n * Bind current data to computed day cells.\n */\nexport const bindDaysData = ({\n days,\n data,\n colorScale,\n emptyColor,\n}: Pick<Required<CalendarSvgProps>, 'data' | 'emptyColor'> & {\n colorScale: ScaleQuantize<string> | ColorScale\n days: Array<Omit<Datum, 'color' | 'data' | 'value'>>\n}) => {\n return days.map(day => {\n const dayData = data.find(item => item.day === day.day)\n\n if (!dayData) {\n return { ...day, color: emptyColor }\n }\n\n return {\n ...day,\n color: colorScale(dayData.value),\n data: dayData,\n value: dayData.value,\n }\n })\n}\n\nexport const computeYearLegendPositions = ({\n years,\n direction,\n position,\n offset,\n}: Pick<Required<CalendarSvgProps>, 'direction'> & {\n offset: number\n position: 'before' | 'after'\n years: Year[]\n}) => {\n return years.map(year => {\n let x = 0\n let y = 0\n let rotation = 0\n if (direction === 'horizontal' && position === 'before') {\n x = year.bbox.x - offset\n y = year.bbox.y + year.bbox.height / 2\n rotation = -90\n } else if (direction === 'horizontal' && position === 'after') {\n x = year.bbox.x + year.bbox.width + offset\n y = year.bbox.y + year.bbox.height / 2\n rotation = -90\n } else if (direction === 'vertical' && position === 'before') {\n x = year.bbox.x + year.bbox.width / 2\n y = year.bbox.y - offset\n } else {\n x = year.bbox.x + year.bbox.width / 2\n y = year.bbox.y + year.bbox.height + offset\n }\n\n return {\n ...year,\n x,\n y,\n rotation,\n }\n })\n}\n\nexport const computeMonthLegendPositions = <Month extends { bbox: BBox }>({\n months,\n direction,\n position,\n offset,\n}: Pick<Required<CalendarSvgProps>, 'direction'> & {\n offset: number\n position: 'before' | 'after'\n months: Month[]\n}) => {\n return months.map(month => {\n let x = 0\n let y = 0\n let rotation = 0\n if (direction === 'horizontal' && position === 'before') {\n x = month.bbox.x + month.bbox.width / 2\n y = month.bbox.y - offset\n } else if (direction === 'horizontal' && position === 'after') {\n x = month.bbox.x + month.bbox.width / 2\n y = month.bbox.y + month.bbox.height + offset\n } else if (direction === 'vertical' && position === 'before') {\n x = month.bbox.x - offset\n y = month.bbox.y + month.bbox.height / 2\n rotation = -90\n } else {\n x = month.bbox.x + month.bbox.width + offset\n y = month.bbox.y + month.bbox.height / 2\n rotation = -90\n }\n\n return {\n ...month,\n x,\n y,\n rotation,\n }\n })\n}\n","import { useMemo } from 'react'\nimport { ScaleQuantize, scaleQuantize } from 'd3-scale'\nimport {\n computeDomain,\n computeYearLegendPositions,\n computeMonthLegendPositions,\n bindDaysData,\n computeLayout,\n} from './compute/calendar'\nimport { BBox, CalendarSvgProps, ColorScale, Year } from './types'\n\nexport const useCalendarLayout = ({\n width,\n height,\n from,\n to,\n direction,\n yearSpacing,\n monthSpacing,\n daySpacing,\n align,\n}: Pick<\n Required<CalendarSvgProps>,\n | 'width'\n | 'height'\n | 'from'\n | 'to'\n | 'direction'\n | 'yearSpacing'\n | 'monthSpacing'\n | 'daySpacing'\n | 'align'\n>) =>\n useMemo(\n () =>\n computeLayout({\n width,\n height,\n from,\n to,\n direction,\n yearSpacing,\n monthSpacing,\n daySpacing,\n align,\n }),\n [width, height, from, to, direction, yearSpacing, monthSpacing, daySpacing, align]\n )\n\nexport const useColorScale = ({\n data,\n minValue,\n maxValue,\n colors,\n colorScale,\n}: Pick<Required<CalendarSvgProps>, 'data' | 'minValue' | 'maxValue' | 'colors'> &\n Pick<CalendarSvgProps, 'colorScale'>) =>\n useMemo(() => {\n if (colorScale) return colorScale\n const domain = computeDomain(data, minValue, maxValue)\n const defaultColorScale = scaleQuantize<string>().domain(domain).range(colors)\n return defaultColorScale\n }, [data, minValue, maxValue, colors, colorScale])\n\nexport const useYearLegends = ({\n years,\n direction,\n yearLegendPosition,\n yearLegendOffset,\n}: {\n years: Year[]\n direction: 'horizontal' | 'vertical'\n yearLegendPosition: 'before' | 'after'\n yearLegendOffset: number\n}) =>\n useMemo(\n () =>\n computeYearLegendPositions({\n years,\n direction,\n position: yearLegendPosition,\n offset: yearLegendOffset,\n }),\n [years, direction, yearLegendPosition, yearLegendOffset]\n )\n\nexport const useMonthLegends = <Month extends { bbox: BBox }>({\n months,\n direction,\n monthLegendPosition,\n monthLegendOffset,\n}: {\n months: Month[]\n direction: 'horizontal' | 'vertical'\n monthLegendPosition: 'before' | 'after'\n monthLegendOffset: number\n}) =>\n useMemo(\n () =>\n computeMonthLegendPositions({\n months,\n direction,\n position: monthLegendPosition,\n offset: monthLegendOffset,\n }),\n [months, direction, monthLegendPosition, monthLegendOffset]\n )\n\nexport const useDays = ({\n days,\n data,\n colorScale,\n emptyColor,\n}: Pick<Required<CalendarSvgProps>, 'data' | 'emptyColor'> &\n Pick<Parameters<typeof bindDaysData>[0], 'days'> & {\n colorScale: ScaleQuantize<string> | ColorScale\n }) =>\n useMemo(\n () =>\n bindDaysData({\n days,\n data,\n colorScale,\n emptyColor,\n }),\n [days, data, colorScale, emptyColor]\n )\n","import { forwardRef, Ref } from 'react'\nimport { Container, SvgWrapper, useDimensions, useValueFormatter } from '@nivo/core'\nimport { useTheme } from '@nivo/theming'\nimport { BoxLegendSvg } from '@nivo/legends'\nimport { CalendarSvgProps } from './types'\nimport { CalendarYearLegends } from './CalendarYearLegends'\nimport { CalendarMonthPath } from './CalendarMonthPath'\nimport { CalendarMonthLegends } from './CalendarMonthLegends'\nimport { CalendarDay } from './CalendarDay'\nimport { calendarDefaultProps } from './props'\nimport { useMonthLegends, useYearLegends, useCalendarLayout, useDays, useColorScale } from './hooks'\n\nconst InnerCalendar = ({\n margin: partialMargin,\n width,\n height,\n align = calendarDefaultProps.align,\n colors = calendarDefaultProps.colors,\n colorScale,\n data,\n direction = calendarDefaultProps.direction,\n emptyColor = calendarDefaultProps.emptyColor,\n from,\n to,\n minValue = calendarDefaultProps.minValue,\n maxValue = calendarDefaultProps.maxValue,\n valueFormat,\n legendFormat,\n yearLegend = calendarDefaultProps.yearLegend,\n yearLegendOffset = calendarDefaultProps.yearLegendOffset,\n yearLegendPosition = calendarDefaultProps.yearLegendPosition,\n yearSpacing = calendarDefaultProps.yearSpacing,\n monthBorderColor = calendarDefaultProps.monthBorderColor,\n monthBorderWidth = calendarDefaultProps.monthBorderWidth,\n monthLegend = calendarDefaultProps.monthLegend,\n monthLegendOffset = calendarDefaultProps.monthLegendOffset,\n monthLegendPosition = calendarDefaultProps.monthLegendPosition,\n monthSpacing = calendarDefaultProps.monthSpacing,\n dayBorderColor = calendarDefaultProps.dayBorderColor,\n dayBorderWidth = calendarDefaultProps.dayBorderWidth,\n daySpacing = calendarDefaultProps.daySpacing,\n isInteractive = calendarDefaultProps.isInteractive,\n tooltip = calendarDefaultProps.tooltip,\n onClick,\n onMouseEnter,\n onMouseLeave,\n onMouseMove,\n legends = calendarDefaultProps.legends,\n role = calendarDefaultProps.role,\n forwardedRef,\n}: CalendarSvgProps & {\n forwardedRef: Ref<SVGSVGElement>\n}) => {\n const theme = useTheme()\n const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions(\n width,\n height,\n partialMargin\n )\n const { months, years, ...rest } = useCalendarLayout({\n width: innerWidth,\n height: innerHeight,\n from,\n to,\n direction,\n yearSpacing,\n monthSpacing,\n daySpacing,\n align,\n })\n const colorScaleFn = useColorScale({ data, minValue, maxValue, colors, colorScale })\n const monthLegends = useMonthLegends({\n months,\n direction,\n monthLegendPosition,\n monthLegendOffset,\n })\n const yearLegends = useYearLegends({ years, direction, yearLegendPosition, yearLegendOffset })\n const days = useDays({ days: rest.days, data, colorScale: colorScaleFn, emptyColor })\n const formatLegend = useValueFormatter(legendFormat)\n const formatValue = useValueFormatter(valueFormat)\n\n return (\n <SvgWrapper\n width={outerWidth}\n height={outerHeight}\n margin={margin}\n role={role}\n ref={forwardedRef}\n >\n {days.map(d => (\n <CalendarDay\n key={d.date.toString()}\n data={d}\n x={d.x}\n y={d.y}\n size={d.size}\n color={d.color}\n borderWidth={dayBorderWidth}\n borderColor={dayBorderColor}\n onMouseEnter={onMouseEnter}\n onMouseLeave={onMouseLeave}\n onMouseMove={onMouseMove}\n isInteractive={isInteractive}\n tooltip={tooltip}\n onClick={onClick}\n formatValue={formatValue}\n />\n ))}\n {months.map(m => (\n <CalendarMonthPath\n key={m.date.toString()}\n path={m.path}\n borderWidth={monthBorderWidth}\n borderColor={monthBorderColor}\n />\n ))}\n <CalendarMonthLegends months={monthLegends} legend={monthLegend} theme={theme} />\n <CalendarYearLegends years={yearLegends} legend={yearLegend} theme={theme} />\n {legends.map((legend, i) => {\n const legendData = colorScaleFn.ticks(legend.itemCount).map(value => ({\n id: value,\n label: formatLegend(value),\n color: colorScaleFn(value),\n }))\n\n return (\n <BoxLegendSvg\n key={i}\n {...legend}\n containerWidth={width}\n containerHeight={height}\n data={legendData}\n />\n )\n })}\n </SvgWrapper>\n )\n}\n\nexport const Calendar = forwardRef(\n (\n {\n isInteractive = calendarDefaultProps.isInteractive,\n renderWrapper,\n theme,\n ...props\n }: CalendarSvgProps,\n ref: Ref<SVGSVGElement>\n ) => (\n <Container {...{ isInteractive, renderWrapper, theme }}>\n <InnerCalendar isInteractive={isInteractive} {...props} forwardedRef={ref} />\n </Container>\n )\n)\n","import {\n timeDays,\n timeDay,\n timeMonday,\n timeTuesday,\n timeWednesday,\n timeThursday,\n timeFriday,\n timeSaturday,\n timeSunday,\n} from 'd3-time'\nimport { timeFormat } from 'd3-time-format'\nimport { DateOrString, Weekday } from '../types'\nimport isDate from 'lodash/isDate.js'\n\n// Interfaces\ninterface ComputeBaseProps {\n direction: 'horizontal' | 'vertical'\n}\n\ninterface ComputeBaseSpaceProps {\n daySpacing: number\n offset: number\n}\n\ninterface ComputeBaseDimensionProps {\n cellWidth: number\n cellHeight: number\n}\n\ninterface ComputeCellSize extends ComputeBaseProps, ComputeBaseSpaceProps {\n totalDays: number\n width: number\n height: number\n square: boolean\n}\n\ninterface ComputeCellPositions\n extends ComputeBaseProps,\n ComputeBaseSpaceProps,\n ComputeBaseDimensionProps {\n from?: DateOrString\n to?: DateOrString\n data: {\n date: Date\n day: string\n value: number\n }[]\n colorScale: (value: number) => string\n emptyColor: string\n firstWeekday: Weekday\n}\n\ninterface ComputeWeekdays\n extends Omit<ComputeBaseProps, 'daysInRange'>,\n Omit<ComputeBaseSpaceProps, 'offset'>,\n ComputeBaseDimensionProps {\n ticks?: number[]\n arrayOfWeekdays?: string[]\n firstWeekday: Weekday\n}\n\ninterface Day {\n coordinates: {\n x: number\n y: number\n }\n firstWeek: number\n month: number\n year: number\n date: Date\n color: string\n day: string\n value?: number\n}\n\ninterface Month {\n date: Date\n bbox: {\n x: number\n y: number\n width: number\n height: number\n }\n firstWeek: number\n month: number\n year: number\n}\n\ninterface ComputeMonths\n extends ComputeBaseProps,\n Omit<ComputeBaseSpaceProps, 'offset'>,\n ComputeBaseDimensionProps {\n days: Day[]\n}\n\ninterface ComputeTotalDays {\n from?: DateOrString\n to?: DateOrString\n data: {\n date: Date\n day: string\n value: number\n }[]\n}\n\n// used for days range and data matching\nconst dayFormat = timeFormat('%Y-%m-%d')\n\n/**\n * Compute day cell size according to\n * current context.\n */\nexport const computeCellSize = ({\n direction,\n daySpacing,\n offset,\n square,\n totalDays,\n width,\n height,\n}: ComputeCellSize) => {\n const daysInRange = 7\n let rows\n let columns\n let widthRest = width\n let heightRest = height\n if (direction === 'horizontal') {\n widthRest -= offset\n rows = daysInRange\n columns = Math.ceil(totalDays / daysInRange)\n } else {\n heightRest -= offset\n columns = daysInRange\n rows = Math.ceil(totalDays / daysInRange)\n }\n // + 1 since we have to apply spacing to the rigth and left\n const cellHeight = (heightRest - daySpacing * (rows + 1)) / rows\n const cellWidth = (widthRest - daySpacing * (columns + 1)) / columns\n // do we want square?\n const size = Math.min(cellHeight, cellWidth)\n return {\n columns,\n rows,\n cellHeight: square ? size : cellHeight,\n cellWidth: square ? size : cellWidth,\n }\n}\n\nexport const ARRAY_OF_WEEKDAYS = [\n 'Sunday',\n 'Monday',\n 'Tuesday',\n 'Wednesday',\n 'Thursday',\n 'Friday',\n 'Saturday',\n]\n\nexport function getFirstWeekdayIndex(weekday: Weekday) {\n return ARRAY_OF_WEEKDAYS.findIndex(item => item.toLowerCase() === weekday)\n}\n\nexport const getDayIndex = (date: Date, firstWeekday: Weekday) => {\n const days = [0, 1, 2, 3, 4, 5, 6]\n const day = date.getDay()\n const offsetDay = day - getFirstWeekdayIndex(firstWeekday)\n const [dayIndex] = days.slice(offsetDay)\n return dayIndex\n}\n\nconst getTimeInterval = (firstWeekday: Weekday) => {\n return [\n timeSunday,\n timeMonday,\n timeTuesday,\n timeWednesday,\n timeThursday,\n timeFriday,\n timeSaturday,\n ][getFirstWeekdayIndex(firstWeekday)]\n}\n\nfunction shiftArray<T>(arr: T[], x: number): T[] {\n if (!arr.length || !x) return arr\n\n x = x % arr.length\n return arr.slice(x, arr.length).concat(arr.slice(0, x))\n}\n\nfunction computeGrid({\n startDate,\n date,\n direction,\n firstWeekday,\n}: {\n startDate: Date\n date: Date\n direction: 'horizontal' | 'vertical'\n firstWeekday: Weekday\n}) {\n const timeInterval = getTimeInterval(firstWeekday)\n const firstWeek = timeInterval.count(startDate, date)\n const month = date.getMonth()\n const year = date.getFullYear()\n\n let currentColumn = 0\n let currentRow = 0\n if (direction === 'horizontal') {\n currentColumn = firstWeek\n currentRow = getDayIndex(date, firstWeekday)\n } else {\n currentColumn = getDayIndex(date, firstWeekday)\n currentRow = firstWeek\n }\n\n return { currentColumn, year, currentRow, firstWeek, month, date }\n}\n\nexport const computeCellPositions = ({\n direction,\n colorScale,\n emptyColor,\n from,\n to,\n data,\n cellWidth,\n cellHeight,\n daySpacing,\n offset,\n firstWeekday,\n}: ComputeCellPositions) => {\n let x = daySpacing\n let y = daySpacing\n\n if (direction === 'horizontal') {\n x += offset\n } else {\n y += offset\n }\n\n // we need to determine whether we need to add days to move to correct position\n const start = from ? from : data[0].date\n const end = to ? to : data[data.length - 1].date\n const startDate = isDate(start) ? start : new Date(start)\n const endDate = isDate(end) ? end : new Date(end)\n const dateRange = timeDays(startDate, endDate).map(dayDate => {\n return {\n date: dayDate,\n day: dayFormat(dayDate),\n }\n })\n\n const dataWithCellPosition = dateRange.map(day => {\n const dayData = data.find(item => item.day === day.day)\n\n const { currentColumn, currentRow, firstWeek, year, month, date } = computeGrid({\n startDate,\n date: day.date,\n direction,\n firstWeekday,\n })\n\n const coordinates = {\n x: x + daySpacing * currentColumn + cellWidth * currentColumn,\n y: y + daySpacing * currentRow + cellHeight * currentRow,\n }\n\n if (!dayData) {\n return {\n ...day,\n coordinates,\n firstWeek,\n month,\n year,\n date,\n color: emptyColor,\n width: cellWidth,\n height: cellHeight,\n }\n }\n\n return {\n ...dayData,\n coordinates,\n firstWeek,\n month,\n year,\n date,\n color: colorScale(dayData.value),\n width: cellWidth,\n height: cellHeight,\n }\n })\n\n return dataWithCellPosition\n}\n\nexport const computeWeekdays = ({\n cellHeight,\n cellWidth,\n direction,\n daySpacing,\n ticks = [1, 3, 5],\n firstWeekday,\n arrayOfWeekdays = ARRAY_OF_WEEKDAYS,\n}: ComputeWeekdays) => {\n const sizes = {\n width: cellWidth + daySpacing,\n height: cellHeight + daySpacing,\n }\n const shiftedWeekdays = shiftArray(arrayOfWeekdays, getFirstWeekdayIndex(firstWeekday))\n return ticks.map(day => ({\n value: shiftedWeekdays[day],\n rotation: direction === 'horizontal' ? 0 : -90,\n y: direction === 'horizontal' ? sizes.height * (day + 1) - sizes.height / 3 : 0,\n x: direction === 'horizontal' ? 0 : sizes.width * (day + 1) - sizes.width / 3,\n }))\n}\n\nexport const computeMonthLegends = ({\n direction,\n daySpacing,\n days,\n cellHeight,\n cellWidth,\n}: ComputeMonths) => {\n const accumulator: {\n months: Record<string, Month>\n weeks: Day[]\n } = {\n months: {},\n weeks: [],\n }\n\n return days.reduce((acc, day) => {\n if (acc.weeks.length === day.firstWeek || (!acc.weeks.length && day.firstWeek === 1)) {\n acc.weeks.push(day)\n\n const key = `${day.year}-${day.month}`\n\n if (!Object.keys(acc.months).includes(key)) {\n const bbox = { x: 0, y: 0, width: 0, height: 0 }\n\n if (direction === 'horizontal') {\n bbox.x = day.coordinates.x - daySpacing\n bbox.height = cellHeight + daySpacing\n bbox.width = cellWidth + daySpacing * 2\n } else {\n bbox.y = day.coordinates.y - daySpacing\n bbox.height = cellHeight + daySpacing * 2\n bbox.width = cellWidth + daySpacing * 2\n }\n\n acc.months[key] = {\n date: day.date,\n bbox,\n firstWeek: day.firstWeek,\n month: 0,\n year: 0,\n }\n } else {\n // enhance width/height\n if (direction === 'horizontal') {\n acc.months[key].bbox.width =\n (day.firstWeek - acc.months[key].firstWeek) * (cellWidth + daySpacing)\n } else {\n acc.months[key].bbox.height =\n (day.firstWeek - acc.months[key].firstWeek) * (cellHeight + daySpacing)\n }\n }\n }\n return acc\n }, accumulator)\n}\n\nexport const computeTotalDays = ({ from, to, data }: ComputeTotalDays) => {\n let startDate\n let endDate\n if (from) {\n startDate = isDate(from) ? from : new Date(from)\n } else {\n startDate = data[0].date\n }\n\n if (from && to) {\n endDate = isDate(to) ? to : new Date(to)\n } else {\n endDate = data[data.length - 1].date\n }\n\n return startDate.getDay() + timeDay.count(startDate, endDate)\n}\n","import { createElement, memo, useCallback, MouseEvent } from 'react'\nimport { useTooltip } from '@nivo/tooltip'\nimport { TimeRangeDayProps } from './types'\n\nexport const TimeRangeDay = memo(\n ({\n data,\n x,\n ry = 5,\n rx = 5,\n y,\n width,\n height,\n color,\n borderWidth,\n borderColor,\n isInteractive,\n tooltip,\n onMouseEnter,\n onMouseMove,\n onMouseLeave,\n onClick,\n formatValue,\n }: TimeRangeDayProps) => {\n const { showTooltipFromEvent, hideTooltip } = useTooltip()\n\n const handleMouseEnter = useCallback(\n (event: MouseEvent<SVGRectElement>) => {\n if (!('value' in data)) {\n return\n }\n\n const formatedData = {\n ...data,\n value: formatValue(data.value),\n }\n showTooltipFromEvent(createElement(tooltip, { ...formatedData }), event)\n onMouseEnter?.(data, event)\n },\n [showTooltipFromEvent, tooltip, data, onMouseEnter, formatValue]\n )\n const handleMouseMove = useCallback(\n (event: MouseEvent<SVGRectElement>) => {\n if (!('value' in data)) {\n return\n }\n\n const formatedData = {\n ...data,\n value: formatValue(data.value),\n }\n showTooltipFromEvent(createElement(tooltip, { ...formatedData }), event)\n onMouseMove?.(data, event)\n },\n [showTooltipFromEvent, tooltip, data, onMouseMove, formatValue]\n )\n const handleMouseLeave = useCallback(\n (event: MouseEvent<SVGRectElement>) => {\n if (!('value' in data)) {\n return\n }\n\n hideTooltip()\n onMouseLeave?.(data, event)\n },\n [hideTooltip, data, onMouseLeave]\n )\n const handleClick = useCallback(\n (event: MouseEvent<SVGRectElement>) => onClick?.(data, event),\n [data, onClick]\n )\n\n return (\n <rect\n x={x}\n y={y}\n rx={rx}\n ry={ry}\n width={width}\n height={height}\n style={{\n fill: color,\n strokeWidth: borderWidth,\n stroke: borderColor,\n }}\n onMouseEnter={isInteractive ? handleMouseEnter : undefined}\n onMouseMove={isInteractive ? handleMouseMove : undefined}\n onMouseLeave={isInteractive ? handleMouseLeave : undefined}\n onClick={isInteractive ? handleClick : undefined}\n />\n )\n }\n)\n","import { useMemo, forwardRef, Ref } from 'react'\nimport { Container, SvgWrapper, useValueFormatter, useDimensions } from '@nivo/core'\nimport { useTheme } from '@nivo/theming'\nimport { BoxLegendSvg } from '@nivo/legends'\nimport { Text } from '@nivo/text'\nimport {\n computeWeekdays,\n computeCellSize,\n computeCellPositions,\n computeMonthLegends,\n computeTotalDays,\n} from './compute/timeRange'\nimport { useMonthLegends, useColorScale } from './hooks'\nimport { TimeRangeDay } from './TimeRangeDay'\nimport { CalendarMonthLegends } from './CalendarMonthLegends'\nimport { TimeRangeSvgProps } from './types'\nimport { timeRangeDefaultProps } from './props'\n\nconst InnerTimeRange = ({\n margin: partialMargin,\n width,\n height,\n square = timeRangeDefaultProps.square,\n colors = timeRangeDefaultProps.colors,\n colorScale,\n emptyColor = timeRangeDefaultProps.emptyColor,\n from,\n to,\n data: _data,\n direction = timeRangeDefaultProps.direction,\n minValue = timeRangeDefaultProps.minValue,\n maxValue = timeRangeDefaultProps.maxValue,\n valueFormat,\n legendFormat,\n monthLegend = timeRangeDefaultProps.monthLegend,\n monthLegendOffset = timeRangeDefaultProps.monthLegendOffset,\n monthLegendPosition = timeRangeDefaultProps.monthLegendPosition,\n weekdayLegendOffset = timeRangeDefaultProps.weekdayLegendOffset,\n weekdayTicks,\n weekdays = timeRangeDefaultProps.weekdays,\n dayBorderColor = timeRangeDefaultProps.dayBorderColor,\n dayBorderWidth = timeRangeDefaultProps.dayBorderWidth,\n daySpacing = timeRangeDefaultProps.daySpacing,\n dayRadius = timeRangeDefaultProps.dayRadius,\n isInteractive = timeRangeDefaultProps.isInteractive,\n tooltip = timeRangeDefaultProps.tooltip,\n onClick,\n onMouseEnter,\n onMouseLeave,\n onMouseMove,\n legends = timeRangeDefaultProps.legends,\n role = timeRangeDefaultProps.role,\n firstWeekday = timeRangeDefaultProps.firstWeekday,\n forwardedRef,\n}: TimeRangeSvgProps & {\n forwardedRef: Ref<SVGSVGElement>\n}) => {\n const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions(\n width,\n height,\n partialMargin\n )\n\n const data = useMemo(\n () =>\n _data\n .map(data => ({ ...data, date: new Date(`${data.day}T00:00:00`) }))\n .sort((left, right) => left.day.localeCompare(right.day)),\n [_data]\n )\n\n const theme = useTheme()\n const colorScaleFn = useColorScale({ data, minValue, maxValue, colors, colorScale })\n\n const totalDays = computeTotalDays({\n from,\n to,\n data,\n })\n\n const { cellHeight, cellWidth } = computeCellSize({\n square,\n offset: weekdayLegendOffset,\n totalDays: totalDays,\n width: innerWidth,\n height: innerHeight,\n daySpacing,\n direction,\n })\n\n const days = computeCellPositions({\n offset: weekdayLegendOffset,\n colorScale: colorScaleFn,\n emptyColor,\n cellHeight,\n cellWidth,\n from,\n to,\n data,\n direction,\n daySpacing,\n firstWeekday,\n })\n\n // map the days and reduce the month\n const months = Object.values(\n computeMonthLegends({\n daySpacing,\n direction,\n cellHeight,\n cellWidth,\n days,\n }).months\n )\n\n const weekdayLegends = computeWeekdays({\n direction,\n cellHeight,\n cellWidth,\n daySpacing,\n ticks: weekdayTicks,\n firstWeekday,\n arrayOfWeekdays: weekdays,\n })\n\n const monthLegends = useMonthLegends({\n months,\n direction,\n monthLegendPosition,\n monthLegendOffset,\n })\n\n const formatValue = useValueFormatter(valueFormat)\n const formatLegend = useValueFormatter(legendFormat)\n\n return (\n <SvgWrapper\n width={outerWidth}\n height={outerHeight}\n margin={margin}\n role={role}\n ref={forwardedRef}\n >\n {weekdayLegends.map(legend => (\n <Text\n key={`${legend.value}-${legend.x}-${legend.y}`}\n transform={`translate(${legend.x},${legend.y}) rotate(${legend.rotation})`}\n textAnchor=\"left\"\n style={theme.labels.text}\n >\n {legend.value}\n </Text>\n ))}\n {days.map(d => {\n return (\n <TimeRangeDay\n key={d.date.toString()}\n data={d}\n x={d.coordinates.x}\n rx={dayRadius}\n y={d.coordinates.y}\n ry={dayRadius}\n width={cellWidth}\n height={cellHeight}\n color={d.color}\n borderWidth={dayBorderWidth}\n borderColor={dayBorderColor}\n onMouseEnter={onMouseEnter}\n onMouseLeave={onMouseLeave}\n onMouseMove={onMouseMove}\n isInteractive={isInteractive}\n tooltip={tooltip}\n onClick={onClick}\n formatValue={formatValue}\n />\n )\n })}\n <CalendarMonthLegends months={monthLegends} legend={monthLegend} theme={theme} />\n\n {legends.map((legend, i) => {\n const legendData = colorScaleFn.ticks(legend.itemCount).map(value => ({\n id: value,\n label: formatLegend(value),\n color: colorScaleFn(value),\n }))\n\n return (\n <BoxLegendSvg\n key={i}\n {...legend}\n containerWidth={width}\n containerHeight={height}\n data={legendData}\n />\n )\n })}\n </SvgWrapper>\n )\n}\n\nexport const TimeRange = forwardRef(\n (\n {\n isInt