From d3fa1475e1c8fd5f7dba9b8390b9e7b472124c4d Mon Sep 17 00:00:00 2001 From: Thorsten Scherler Date: Mon, 3 May 2021 18:31:54 +0200 Subject: [PATCH 1/8] [timeRange] add timeRange component and storybook --- packages/calendar/src/TimeRange.js | 179 ++++++++++++ packages/calendar/src/TimeRangeDay.js | 119 ++++++++ packages/calendar/src/compute-timeRange.ts | 259 ++++++++++++++++++ packages/calendar/src/index.js | 1 + .../calendar/stories/generateDayCounts.ts | 48 ++++ .../calendar/stories/timeRange.stories.js | 89 ++++++ 6 files changed, 695 insertions(+) create mode 100644 packages/calendar/src/TimeRange.js create mode 100644 packages/calendar/src/TimeRangeDay.js create mode 100644 packages/calendar/src/compute-timeRange.ts create mode 100644 packages/calendar/stories/generateDayCounts.ts create mode 100644 packages/calendar/stories/timeRange.stories.js diff --git a/packages/calendar/src/TimeRange.js b/packages/calendar/src/TimeRange.js new file mode 100644 index 000000000..4e4d1aa1c --- /dev/null +++ b/packages/calendar/src/TimeRange.js @@ -0,0 +1,179 @@ +import React from 'react' +import { timeFormat } from 'd3-time-format' + +import { SvgWrapper, withContainer, useValueFormatter, useTheme, useDimensions } from '@nivo/core' +import { BoxLegendSvg } from '@nivo/legends' + +import { + Direction, + computeWeekdays, + computeCellSize, + computeCellPositions, + computeMonthLegends, +} from './compute-timeRange' + +import { useMonthLegends, useColorScale } from './hooks' +import TimeRangeDay from './TimeRangeDay' +import CalendarTooltip from './CalendarTooltip' +import CalendarMonthLegends from './CalendarMonthLegends' + +const monthLabelFormat = timeFormat('%b') + +const TimeRange = ({ + margin: partialMargin, + width, + height, + + colors = ['#61cdbb', '#97e3d5', '#e8c1a0', '#f47560'], + colorScale, + data, + direction = Direction.HORIZONTAL, + minValue = 0, + maxValue = 'auto', + valueFormat, + legendFormat, + role, + tooltip = CalendarTooltip, + onClick, + onMouseEnter, + onMouseLeave, + onMouseMove, + isInteractive = true, + legends = [], + dayBorderColor = '#fff', + dayBorderWidth = 0, + dayRadius = 0, + daySpacing, + daysInRange, + weekdayLegendsOffset, + monthLegend = (_year, _month, date) => monthLabelFormat(date), + monthLegendOffset = 0, + monthLegendPosition = 'before', +}) => { + const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions( + width, + height, + partialMargin + ) + + const theme = useTheme() + const colorScaleFn = useColorScale({ data, minValue, maxValue, colors, colorScale }) + + const { cellHeight, cellWidth } = computeCellSize({ + offset: weekdayLegendsOffset, + totalDays: data.length + data[0].date.getDay(), + width: innerWidth, + height: innerHeight, + daySpacing, + direction, + }) + + const days = computeCellPositions({ + offset: weekdayLegendsOffset, + daysInRange, + colorScale: colorScaleFn, + cellHeight, + cellWidth, + data, + direction, + daySpacing, + }) + + // map the days and reduce the month + const months = Object.values( + computeMonthLegends({ + daySpacing, + direction, + cellHeight, + cellWidth, + days, + daysInRange, + }).months + ) + + const weekdayLegends = computeWeekdays({ + direction, + cellHeight, + cellWidth, + daySpacing, + }) + + const monthLegends = useMonthLegends({ + months, + direction, + monthLegendPosition, + monthLegendOffset, + }) + + const formatValue = useValueFormatter(valueFormat) + const formatLegend = useValueFormatter(legendFormat) + + return ( + + {weekdayLegends.map(legend => ( + + {legend.value} + + ))} + {days.map(d => { + return ( + + ) + })} + + + {legends?.map((legend, i) => { + const legendData = colorScaleFn.ticks(legend.itemCount).map(value => ({ + id: value, + label: formatLegend(value), + color: colorScaleFn(value), + })) + + return ( + + ) + })} + + ) +} + +export default withContainer(TimeRange) diff --git a/packages/calendar/src/TimeRangeDay.js b/packages/calendar/src/TimeRangeDay.js new file mode 100644 index 000000000..dbb4dcacc --- /dev/null +++ b/packages/calendar/src/TimeRangeDay.js @@ -0,0 +1,119 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaƫl Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import React, { memo, useCallback } from 'react' +import PropTypes from 'prop-types' +import { useTooltip } from '@nivo/tooltip' +import CalendarTooltip from './CalendarTooltip' + +const TimeRangeDay = memo( + ({ + data, + x, + ry = 5, + rx = 5, + y, + width, + height, + color, + borderWidth, + borderColor, + isInteractive, + tooltip = CalendarTooltip, + onMouseEnter, + onMouseMove, + onMouseLeave, + onClick, + formatValue, + }) => { + const { showTooltipFromEvent, hideTooltip } = useTooltip() + + const handleMouseEnter = useCallback( + event => { + const formatedData = { + ...data, + value: formatValue(data.value), + data: { ...data.data }, + } + showTooltipFromEvent(React.createElement(tooltip, { ...formatedData }), event) + onMouseEnter && onMouseEnter(data, event) + }, + [showTooltipFromEvent, tooltip, data, onMouseEnter, formatValue] + ) + const handleMouseMove = useCallback( + event => { + const formatedData = { + ...data, + value: formatValue(data.value), + data: { ...data.data }, + } + showTooltipFromEvent(React.createElement(tooltip, { ...formatedData }), event) + onMouseMove && onMouseMove(data, event) + }, + [showTooltipFromEvent, tooltip, data, onMouseMove, formatValue] + ) + const handleMouseLeave = useCallback( + event => { + hideTooltip() + onMouseLeave && onMouseLeave(data, event) + }, + [isInteractive, hideTooltip, data, onMouseLeave] + ) + const handleClick = useCallback(event => onClick && onClick(data, event), [ + isInteractive, + data, + onClick, + ]) + return ( + + ) + } +) + +TimeRangeDay.displayName = 'TimeRangeDay' +TimeRangeDay.propTypes = { + onClick: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, + onMouseMove: PropTypes.func, + data: PropTypes.object.isRequired, + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + spacing: PropTypes.number.isRequired, + color: PropTypes.string.isRequired, + borderWidth: PropTypes.number.isRequired, + borderColor: PropTypes.string.isRequired, + isInteractive: PropTypes.bool.isRequired, + formatValue: PropTypes.func, + + tooltip: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired, + + theme: PropTypes.shape({ + tooltip: PropTypes.shape({}).isRequired, + }).isRequired, +} + +export default TimeRangeDay diff --git a/packages/calendar/src/compute-timeRange.ts b/packages/calendar/src/compute-timeRange.ts new file mode 100644 index 000000000..732b1681e --- /dev/null +++ b/packages/calendar/src/compute-timeRange.ts @@ -0,0 +1,259 @@ +import { timeWeek } from 'd3-time' + +export enum Direction { + VERTICAL = 'vertical', + HORIZONTAL = 'horizontal', +} + +export const defaultProps = { + daysInRange: 7, + direction: Direction.HORIZONTAL, + daySpacing: 10, + offset: 65, +} + +// Interfaces +export interface ComputeBaseProps { + daysInRange?: number + direction?: Direction +} + +export interface ComputeBaseSpaceProps { + daySpacing?: number + offset?: number +} + +export interface ComputeBaseDimensionProps { + cellWidth: number + cellHeight: number +} + +export interface ComputeCellSize extends ComputeBaseProps, ComputeBaseSpaceProps { + totalDays: number + width: number + height: number +} + +export interface ComputeCellPositions + extends ComputeBaseProps, + ComputeBaseSpaceProps, + ComputeBaseDimensionProps { + data: { + date: Date + day: string + value: number + }[] + colorScale: (value: number) => string +} + +export interface ComputeWeekdays + extends ComputeBaseProps, + ComputeBaseSpaceProps, + ComputeBaseDimensionProps { + ticks: number[] + arrayOfWeekdays: string[] +} + +export interface ComputeMonths + extends ComputeBaseProps, + ComputeBaseSpaceProps, + ComputeBaseDimensionProps { + days: { + coordinates: { + x: number + y: number + } + firstWeek: number + month: number + year: number + date: Date + color: string + day: string + value: number + }[] +} + +/** + * Compute day cell size according to + * current context. + */ +export const computeCellSize = ({ + direction = defaultProps.direction, + daysInRange = defaultProps.daysInRange, + daySpacing = defaultProps.daySpacing, + offset = defaultProps.offset, + totalDays, + width, + height, +}: ComputeCellSize) => { + let rows + let columns + let widthRest = width + let heightRest = height + if (direction === Direction.HORIZONTAL) { + widthRest -= offset + rows = daysInRange + columns = Math.ceil(totalDays / daysInRange) + } else { + heightRest -= offset + columns = daysInRange + rows = Math.ceil(totalDays / daysInRange) + } + // + 1 since we have to apply spacing to the rigth and left + return { + columns, + rows, + cellHeight: (heightRest - daySpacing * (rows + 1)) / rows, + cellWidth: (widthRest - daySpacing * (columns + 1)) / columns, + } +} + +function computeGrid({ + startDate, + date, + direction, +}: { + startDate: Date + date: Date + direction: Direction +}) { + const firstWeek = timeWeek.count(startDate, date) + const month = date.getMonth() + const year = date.getFullYear() + + let currentColumn = 0 + let currentRow = 0 + if (direction === Direction.HORIZONTAL) { + currentColumn = firstWeek + currentRow = date.getDay() + } else { + currentColumn = date.getDay() + currentRow = firstWeek + } + + return { currentColumn, year, currentRow, firstWeek, month, date } +} + +export const computeCellPositions = ({ + direction = defaultProps.direction, + colorScale, + data, + cellWidth, + cellHeight, + daySpacing = defaultProps.daySpacing, + offset = defaultProps.offset, +}: ComputeCellPositions) => { + let x = daySpacing + let y = daySpacing + + if (direction === Direction.HORIZONTAL) { + x += offset + } else { + y += offset + } + + // we need to determine whether we need to add days to move to correct position + const startDate = data[0].date + const dataWithCellPosition = data.map(dateValue => { + const { currentColumn, currentRow, firstWeek, year, month, date } = computeGrid({ + startDate, + date: dateValue.date, + direction, + }) + + const coordinates = { + x: x + daySpacing * currentColumn + cellWidth * currentColumn, + y: y + daySpacing * currentRow + cellHeight * currentRow, + } + + return { + ...dateValue, + coordinates, + firstWeek, + month, + year, + date, + color: colorScale(dateValue.value), + } + }) + + return dataWithCellPosition +} + +export const computeWeekdays = ({ + cellHeight, + cellWidth, + direction = defaultProps.direction, + daySpacing = defaultProps.daySpacing, + ticks = [1, 3, 5], + arrayOfWeekdays = [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + ], +}: ComputeWeekdays) => { + const sizes = { + width: cellWidth + daySpacing, + height: cellHeight + daySpacing, + } + return ticks.map(day => ({ + value: arrayOfWeekdays[day], + rotation: direction === Direction.HORIZONTAL ? 0 : -90, + y: direction === Direction.HORIZONTAL ? sizes.height * (day + 1) - daySpacing / 2 : 0, + x: direction === Direction.HORIZONTAL ? 0 : sizes.width * (day + 1) - daySpacing / 2, + })) +} + +export const computeMonthLegends = ({ + direction = defaultProps.direction, + daySpacing = defaultProps.daySpacing, + daysInRange = defaultProps.daysInRange, + days, + cellHeight, + cellWidth, +}: ComputeMonths) => { + return days.reduce( + (acc, day) => { + if (acc.weeks.length === day.firstWeek) { + acc.weeks.push(day) + if (!Object.keys(acc.months).includes(`${day.year}-${day.month}`)) { + const bbox = { x: 0, y: 0, width: 0, height: 0 } + if (direction === Direction.HORIZONTAL) { + bbox.x = day.coordinates.x - daySpacing + bbox.height = daysInRange * cellHeight + daySpacing + bbox.width = cellWidth + daySpacing * 2 + } else { + bbox.y = day.coordinates.y - daySpacing + bbox.height = cellHeight + daySpacing * 2 + bbox.width = daysInRange * cellWidth + daySpacing * 2 + } + acc.months[`${day.year}-${day.month}`] = { + date: day.date, + bbox, + firstWeek: day.firstWeek, + } + } else { + // enhance width/height + if (direction === Direction.HORIZONTAL) { + acc.months[`${day.year}-${day.month}`].bbox.width = + (day.firstWeek - acc.months[`${day.year}-${day.month}`].firstWeek) * + (cellWidth + daySpacing) + } else { + acc.months[`${day.year}-${day.month}`].bbox.height = + (day.firstWeek - acc.months[`${day.year}-${day.month}`].firstWeek) * + (cellHeight + daySpacing) + } + } + } + return acc + }, + { + months: {}, + weeks: [], + } + ) +} diff --git a/packages/calendar/src/index.js b/packages/calendar/src/index.js index 3348563ca..5e57d132d 100644 --- a/packages/calendar/src/index.js +++ b/packages/calendar/src/index.js @@ -7,6 +7,7 @@ * file that was distributed with this source code. */ export { default as Calendar } from './Calendar' +export { default as TimeRange } from './TimeRange' export { default as ResponsiveCalendar } from './ResponsiveCalendar' export { default as CalendarCanvas } from './CalendarCanvas' export { default as ResponsiveCalendarCanvas } from './ResponsiveCalendarCanvas' diff --git a/packages/calendar/stories/generateDayCounts.ts b/packages/calendar/stories/generateDayCounts.ts new file mode 100644 index 000000000..ceaadd6ed --- /dev/null +++ b/packages/calendar/stories/generateDayCounts.ts @@ -0,0 +1,48 @@ +import shuffle from 'lodash/shuffle' +import { timeDays } from 'd3-time' +import { timeFormat } from 'd3-time-format' +/* + * generating some random data + * + * Copied from + * https://github.com/plouc/nivo/blob/master/packages/generators/src/index.js#L105 + * since that package does not have typedefs I just copied it over + * */ +export const generateDayCounts = ( + { from, to, maxSize = 0.9 }: + { from: Date, to: Date, maxSize?: number } +) => { + const days = timeDays(from, to) + + const size = + Math.round(days.length * (maxSize * 0.4)) + + Math.round(Math.random() * (days.length * (maxSize * 0.6))) + + const dayFormat = timeFormat('%Y-%m-%d') + + return shuffle(days) + .slice(0, size) + .map(day => { + return { + day: dayFormat(day), + value: Math.round(Math.random() * 400), + } + }) +} + +export const generateOrderedDayCounts = ( + { from, to }: + { from: Date, to: Date } +) => { + const days = timeDays(from, to) + const dayFormat = timeFormat('%Y-%m-%d') + + return days + .map(day => { + return { + value: Math.round(Math.random() * 400), + date: day, + day: dayFormat(day), + } + }) +} diff --git a/packages/calendar/stories/timeRange.stories.js b/packages/calendar/stories/timeRange.stories.js new file mode 100644 index 000000000..ae098db15 --- /dev/null +++ b/packages/calendar/stories/timeRange.stories.js @@ -0,0 +1,89 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { withKnobs, number, date } from '@storybook/addon-knobs' + +import { TimeRange } from '../src' +import { generateOrderedDayCounts } from "./generateDayCounts"; + + +const formater = value => value / 10 + 'M' + +const stories = storiesOf('TimeRange', module) + +stories.addDecorator(withKnobs) + +stories.add('TimeRange horizontal', () => { + const from = new Date(date('from', new Date(2020, 6, 27))) + const to = new Date(date('to', new Date(2021, 0, 7))) + return value, + margin: { + top: number('margin-top',40), + right: number('margin-right',40), + bottom: number('margin-bottom',40), + left: number('margin-left',40), + }, + from, + to, + data: generateOrderedDayCounts({ + from, + to, + }), + daySpacing: number('daySpacing',10) + }} + height={number('height', 250)} + width={number('width', 655)} + legendFormat={formater} + legends={[ + { + anchor: 'bottom', + direction: 'row', + itemCount: 4, + itemWidth: 42, + itemHeight: 36, + itemsSpacing: 14, + translateY: -30, + }, + ]} + /> +}) + +stories.add('TimeRange vertical', () => { + const from = new Date(date('from', new Date(2020, 6, 27))) + const to = new Date(date('to', new Date(2021, 0, 7))) + + return value, + margin: { + top: number('margin-top',40), + right: number('margin-right',40), + bottom: number('margin-bottom',40), + left: number('margin-left',40), + }, + data: generateOrderedDayCounts({ + from, + to, + }), + daySpacing: number('daySpacing',10) + }} + weekdayLegendsOffset={0} + height={number('height', 900)} + width={number('width', 250)} + direction="vertical" + legendFormat={formater} + legends={[ + { + anchor: 'bottom', + direction: 'row', + itemCount: 4, + itemWidth: 42, + itemHeight: 36, + itemsSpacing: 14, + }, + ]} + /> +}) \ No newline at end of file From 2e68dd8efaae527b53b24ea16e89d8cb3176dd93 Mon Sep 17 00:00:00 2001 From: Thorsten Scherler Date: Mon, 3 May 2021 19:18:18 +0200 Subject: [PATCH 2/8] [timeRange] fix interface --- packages/calendar/index.d.ts | 60 +++++++++++++++++-- packages/calendar/src/compute-timeRange.ts | 4 +- .../calendar/stories/timeRange.stories.js | 2 - 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/packages/calendar/index.d.ts b/packages/calendar/index.d.ts index c04550b0d..dcb27db88 100644 --- a/packages/calendar/index.d.ts +++ b/packages/calendar/index.d.ts @@ -10,7 +10,7 @@ import * as React from 'react' import { Dimensions, Theme, Box, BoxAlign } from '@nivo/core' import { LegendProps } from '@nivo/legends' -declare module '@nivo/calendar' { +declare module '@scherler/nivo-calendar' { export type DateOrString = string | Date export interface CalendarDatum { @@ -66,10 +66,10 @@ declare module '@nivo/calendar' { yearLegendOffset: number yearLegendPosition: 'before' | 'after' - monthLegend: (year: number, month: number, date: Date) => string | number monthSpacing: number monthBorderWidth: number monthBorderColor: string + monthLegend: (year: number, month: number, date: Date) => string | number monthLegendOffset: number monthLegendPosition: 'before' | 'after' @@ -102,8 +102,56 @@ declare module '@nivo/calendar' { role: string }> - export class Calendar extends React.Component {} - export class ResponsiveCalendar extends React.Component {} - export class CalendarCanvas extends React.Component {} - export class ResponsiveCalendarCanvas extends React.Component {} + export class Calendar extends React.Component { } + export class ResponsiveCalendar extends React.Component { } + export class CalendarCanvas extends React.Component { } + export class ResponsiveCalendarCanvas extends React.Component { } + + export type TimeRangeCommonProps = Partial<{ + minValue: 'auto' | number + maxValue: 'auto' | number + direction: CalendarDirection + colors: string[] + colorScale: ColorScale + margin: Box + align: BoxAlign + daySpacing: number + dayRadius: number + dayBorderWidth: number + dayBorderColor: string + emptyColor: string + isInteractive: boolean + onClick?: CalendarMouseHandler + onMouseMove?: CalendarMouseHandler + onMouseLeave?: CalendarMouseHandler + onMouseEnter?: CalendarMouseHandler + tooltip: React.FunctionComponent + valueFormat?: string | ValueFormatter + legendFormat?: string | ValueFormatter + legends: CalendarLegend[] + theme: Theme + weekdayLegendsOffset: number + monthLegend: (year: number, month: number, date: Date) => string | number + monthLegendOffset: number + monthLegendPosition: 'before' | 'after' + }> + + export interface TimeRangeDatum { + day: Date + value: number + } + + export interface TimeRangeData { + from: Date + to: Date + data: TimeRangeDatum[] + } + export type TimeRangeProps = TimeRangeData & + TimeRangeCommonProps & + Partial<{ + onClick: (datum: CalendarDayData, event: React.MouseEvent) => void + role: string + }> & + Dimensions + } diff --git a/packages/calendar/src/compute-timeRange.ts b/packages/calendar/src/compute-timeRange.ts index 732b1681e..e1896c03e 100644 --- a/packages/calendar/src/compute-timeRange.ts +++ b/packages/calendar/src/compute-timeRange.ts @@ -50,8 +50,8 @@ export interface ComputeWeekdays extends ComputeBaseProps, ComputeBaseSpaceProps, ComputeBaseDimensionProps { - ticks: number[] - arrayOfWeekdays: string[] + ticks?: number[] + arrayOfWeekdays?: string[] } export interface ComputeMonths diff --git a/packages/calendar/stories/timeRange.stories.js b/packages/calendar/stories/timeRange.stories.js index ae098db15..10c54cf3a 100644 --- a/packages/calendar/stories/timeRange.stories.js +++ b/packages/calendar/stories/timeRange.stories.js @@ -25,8 +25,6 @@ stories.add('TimeRange horizontal', () => { bottom: number('margin-bottom',40), left: number('margin-left',40), }, - from, - to, data: generateOrderedDayCounts({ from, to, From d6cbf95c10e3e8c60cbbac827cf9bda5e771459e Mon Sep 17 00:00:00 2001 From: Thorsten Scherler Date: Mon, 3 May 2021 20:03:36 +0200 Subject: [PATCH 3/8] [timeRange] fix types --- packages/calendar/src/compute-timeRange.ts | 111 ++++++++++++--------- packages/calendar/tsconfig.json | 8 ++ 2 files changed, 70 insertions(+), 49 deletions(-) create mode 100644 packages/calendar/tsconfig.json diff --git a/packages/calendar/src/compute-timeRange.ts b/packages/calendar/src/compute-timeRange.ts index e1896c03e..f581aec40 100644 --- a/packages/calendar/src/compute-timeRange.ts +++ b/packages/calendar/src/compute-timeRange.ts @@ -54,23 +54,35 @@ export interface ComputeWeekdays arrayOfWeekdays?: string[] } +export interface Day { + coordinates: { + x: number + y: number + } + firstWeek: number + month: number + year: number + date: Date + color: string + day: string + value: number +} + +export interface Month { + date: Date + bbox: { + x: number + y: number + width: number + height: number + } + firstWeek: number +} export interface ComputeMonths extends ComputeBaseProps, ComputeBaseSpaceProps, ComputeBaseDimensionProps { - days: { - coordinates: { - x: number - y: number - } - firstWeek: number - month: number - year: number - date: Date - color: string - day: string - value: number - }[] + days: Day[] } /** @@ -216,44 +228,45 @@ export const computeMonthLegends = ({ cellHeight, cellWidth, }: ComputeMonths) => { - return days.reduce( - (acc, day) => { - if (acc.weeks.length === day.firstWeek) { - acc.weeks.push(day) - if (!Object.keys(acc.months).includes(`${day.year}-${day.month}`)) { - const bbox = { x: 0, y: 0, width: 0, height: 0 } - if (direction === Direction.HORIZONTAL) { - bbox.x = day.coordinates.x - daySpacing - bbox.height = daysInRange * cellHeight + daySpacing - bbox.width = cellWidth + daySpacing * 2 - } else { - bbox.y = day.coordinates.y - daySpacing - bbox.height = cellHeight + daySpacing * 2 - bbox.width = daysInRange * cellWidth + daySpacing * 2 - } - acc.months[`${day.year}-${day.month}`] = { - date: day.date, - bbox, - firstWeek: day.firstWeek, - } + const accumulator: { + months: { [key: string]: Month } + weeks: Day[] + } = { + months: {}, + weeks: [], + } + return days.reduce((acc, day) => { + if (acc.weeks.length === day.firstWeek) { + acc.weeks.push(day) + if (!Object.keys(acc.months).includes(`${day.year}-${day.month}`)) { + const bbox = { x: 0, y: 0, width: 0, height: 0 } + if (direction === Direction.HORIZONTAL) { + bbox.x = day.coordinates.x - daySpacing + bbox.height = daysInRange * cellHeight + daySpacing + bbox.width = cellWidth + daySpacing * 2 + } else { + bbox.y = day.coordinates.y - daySpacing + bbox.height = cellHeight + daySpacing * 2 + bbox.width = daysInRange * cellWidth + daySpacing * 2 + } + acc.months[`${day.year}-${day.month}`] = { + date: day.date, + bbox, + firstWeek: day.firstWeek, + } + } else { + // enhance width/height + if (direction === Direction.HORIZONTAL) { + acc.months[`${day.year}-${day.month}`].bbox.width = + (day.firstWeek - acc.months[`${day.year}-${day.month}`].firstWeek) * + (cellWidth + daySpacing) } else { - // enhance width/height - if (direction === Direction.HORIZONTAL) { - acc.months[`${day.year}-${day.month}`].bbox.width = - (day.firstWeek - acc.months[`${day.year}-${day.month}`].firstWeek) * - (cellWidth + daySpacing) - } else { - acc.months[`${day.year}-${day.month}`].bbox.height = - (day.firstWeek - acc.months[`${day.year}-${day.month}`].firstWeek) * - (cellHeight + daySpacing) - } + acc.months[`${day.year}-${day.month}`].bbox.height = + (day.firstWeek - acc.months[`${day.year}-${day.month}`].firstWeek) * + (cellHeight + daySpacing) } } - return acc - }, - { - months: {}, - weeks: [], } - ) + return acc + }, accumulator) } diff --git a/packages/calendar/tsconfig.json b/packages/calendar/tsconfig.json new file mode 100644 index 000000000..855b4b2b7 --- /dev/null +++ b/packages/calendar/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.types.json", + "compilerOptions": { + "outDir": "./dist/types", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} From bc3c6247fa592f6d121d62ce31b6cdf64fb91c39 Mon Sep 17 00:00:00 2001 From: Thorsten Scherler Date: Mon, 3 May 2021 20:47:21 +0200 Subject: [PATCH 4/8] fix declaration --- packages/calendar/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/calendar/index.d.ts b/packages/calendar/index.d.ts index dcb27db88..46c3d493e 100644 --- a/packages/calendar/index.d.ts +++ b/packages/calendar/index.d.ts @@ -10,7 +10,7 @@ import * as React from 'react' import { Dimensions, Theme, Box, BoxAlign } from '@nivo/core' import { LegendProps } from '@nivo/legends' -declare module '@scherler/nivo-calendar' { +declare module '@nivo/calendar' { export type DateOrString = string | Date export interface CalendarDatum { From 3a58b60c0cf30777f9142ff0a4f8fae41bdf456b Mon Sep 17 00:00:00 2001 From: Thorsten Scherler Date: Mon, 3 May 2021 23:34:44 +0200 Subject: [PATCH 5/8] [timeRange] add responsive --- packages/calendar/src/ResponsiveTimeRange.js | 11 +++ packages/calendar/src/index.js | 1 + .../calendar/stories/timeRange.stories.js | 75 ++++++++++++++----- 3 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 packages/calendar/src/ResponsiveTimeRange.js diff --git a/packages/calendar/src/ResponsiveTimeRange.js b/packages/calendar/src/ResponsiveTimeRange.js new file mode 100644 index 000000000..83ab3e9b5 --- /dev/null +++ b/packages/calendar/src/ResponsiveTimeRange.js @@ -0,0 +1,11 @@ +import React from 'react' +import { ResponsiveWrapper } from '@nivo/core' +import TimeRange from './TimeRange' + +const ResponsiveTimeRange = props => ( + + {({ width, height }) => } + +) + +export default ResponsiveTimeRange diff --git a/packages/calendar/src/index.js b/packages/calendar/src/index.js index 5e57d132d..71c73e9ae 100644 --- a/packages/calendar/src/index.js +++ b/packages/calendar/src/index.js @@ -8,6 +8,7 @@ */ export { default as Calendar } from './Calendar' export { default as TimeRange } from './TimeRange' +export { default as ResponsiveTimeRange } from './ResponsiveTimeRange' export { default as ResponsiveCalendar } from './ResponsiveCalendar' export { default as CalendarCanvas } from './CalendarCanvas' export { default as ResponsiveCalendarCanvas } from './ResponsiveCalendarCanvas' diff --git a/packages/calendar/stories/timeRange.stories.js b/packages/calendar/stories/timeRange.stories.js index 10c54cf3a..7158f8dc4 100644 --- a/packages/calendar/stories/timeRange.stories.js +++ b/packages/calendar/stories/timeRange.stories.js @@ -2,7 +2,7 @@ import React from 'react' import { storiesOf } from '@storybook/react' import { withKnobs, number, date } from '@storybook/addon-knobs' -import { TimeRange } from '../src' +import { TimeRange, ResponsiveTimeRange } from '../src' import { generateOrderedDayCounts } from "./generateDayCounts"; @@ -13,23 +13,23 @@ const stories = storiesOf('TimeRange', module) stories.addDecorator(withKnobs) stories.add('TimeRange horizontal', () => { - const from = new Date(date('from', new Date(2020, 6, 27))) - const to = new Date(date('to', new Date(2021, 0, 7))) + const from = new Date(date('from', new Date(2020, 6, 27))) + const to = new Date(date('to', new Date(2021, 0, 7))) return value, margin: { - top: number('margin-top',40), - right: number('margin-right',40), - bottom: number('margin-bottom',40), - left: number('margin-left',40), + top: number('margin-top', 40), + right: number('margin-right', 40), + bottom: number('margin-bottom', 40), + left: number('margin-left', 40), }, data: generateOrderedDayCounts({ from, to, }), - daySpacing: number('daySpacing',10) + daySpacing: number('daySpacing', 10) }} height={number('height', 250)} width={number('width', 655)} @@ -47,26 +47,67 @@ stories.add('TimeRange horizontal', () => { ]} /> }) +stories.add('responsive', () => { + const from = new Date(date('from', new Date(2020, 6, 27))) + const to = new Date(date('to', new Date(2021, 0, 7))) + return ( +
+ value, + margin: { + top: number('margin-top', 40), + right: number('margin-right', 40), + bottom: number('margin-bottom', 40), + left: number('margin-left', 40), + }, + data: generateOrderedDayCounts({ + from, + to, + }), + daySpacing: number('daySpacing', 10) + }} + /> +
+ ) + +}) stories.add('TimeRange vertical', () => { - const from = new Date(date('from', new Date(2020, 6, 27))) - const to = new Date(date('to', new Date(2021, 0, 7))) - + const from = new Date(date('from', new Date(2020, 6, 27))) + const to = new Date(date('to', new Date(2021, 0, 7))) + return value, margin: { - top: number('margin-top',40), - right: number('margin-right',40), - bottom: number('margin-bottom',40), - left: number('margin-left',40), + top: number('margin-top', 40), + right: number('margin-right', 40), + bottom: number('margin-bottom', 40), + left: number('margin-left', 40), }, data: generateOrderedDayCounts({ from, to, }), - daySpacing: number('daySpacing',10) + daySpacing: number('daySpacing', 10) }} weekdayLegendsOffset={0} height={number('height', 900)} From fc28251d15134d7ee104e96e396fa69091584f0f Mon Sep 17 00:00:00 2001 From: Thorsten Scherler Date: Wed, 5 May 2021 09:56:30 +0200 Subject: [PATCH 6/8] [timeRange] fix types --- packages/calendar/index.d.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/calendar/index.d.ts b/packages/calendar/index.d.ts index 46c3d493e..31e172eaa 100644 --- a/packages/calendar/index.d.ts +++ b/packages/calendar/index.d.ts @@ -47,7 +47,7 @@ declare module '@nivo/calendar' { } export interface ColorScale { - (value: number | { valueOf(): number }): Range + (value: number | { valueOf(): number }): any[] ticks(count?: number): number[] } @@ -137,7 +137,8 @@ declare module '@nivo/calendar' { }> export interface TimeRangeDatum { - day: Date + day: string + date: Date value: number } @@ -154,4 +155,12 @@ declare module '@nivo/calendar' { }> & Dimensions + export type TimeRangeSvgProps = TimeRangeData & + TimeRangeCommonProps & + Partial<{ + onClick: (datum: CalendarDayData, event: React.MouseEvent) => void + role: string + }> + + export class ResponsiveTimeRange extends React.Component { } } From 27463da586d26e59e4cfad00007211f9f6470734 Mon Sep 17 00:00:00 2001 From: Thorsten Scherler Date: Fri, 7 May 2021 09:33:25 +0200 Subject: [PATCH 7/8] [timeRange] add square feature --- packages/calendar/index.d.ts | 2 +- packages/calendar/src/TimeRange.js | 2 ++ packages/calendar/src/compute-timeRange.ts | 23 ++++++++++++------- .../calendar/stories/timeRange.stories.js | 3 ++- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/calendar/index.d.ts b/packages/calendar/index.d.ts index 31e172eaa..2e193044f 100644 --- a/packages/calendar/index.d.ts +++ b/packages/calendar/index.d.ts @@ -114,7 +114,7 @@ declare module '@nivo/calendar' { colors: string[] colorScale: ColorScale margin: Box - align: BoxAlign + square?: boolean daySpacing: number dayRadius: number dayBorderWidth: number diff --git a/packages/calendar/src/TimeRange.js b/packages/calendar/src/TimeRange.js index 4e4d1aa1c..fac041501 100644 --- a/packages/calendar/src/TimeRange.js +++ b/packages/calendar/src/TimeRange.js @@ -24,6 +24,7 @@ const TimeRange = ({ width, height, + square, colors = ['#61cdbb', '#97e3d5', '#e8c1a0', '#f47560'], colorScale, data, @@ -60,6 +61,7 @@ const TimeRange = ({ const colorScaleFn = useColorScale({ data, minValue, maxValue, colors, colorScale }) const { cellHeight, cellWidth } = computeCellSize({ + square, offset: weekdayLegendsOffset, totalDays: data.length + data[0].date.getDay(), width: innerWidth, diff --git a/packages/calendar/src/compute-timeRange.ts b/packages/calendar/src/compute-timeRange.ts index f581aec40..aef6e22d1 100644 --- a/packages/calendar/src/compute-timeRange.ts +++ b/packages/calendar/src/compute-timeRange.ts @@ -10,6 +10,7 @@ export const defaultProps = { direction: Direction.HORIZONTAL, daySpacing: 10, offset: 65, + square: true, } // Interfaces @@ -32,12 +33,13 @@ export interface ComputeCellSize extends ComputeBaseProps, ComputeBaseSpaceProps totalDays: number width: number height: number + square?: boolean } export interface ComputeCellPositions extends ComputeBaseProps, - ComputeBaseSpaceProps, - ComputeBaseDimensionProps { + ComputeBaseSpaceProps, + ComputeBaseDimensionProps { data: { date: Date day: string @@ -48,8 +50,8 @@ export interface ComputeCellPositions export interface ComputeWeekdays extends ComputeBaseProps, - ComputeBaseSpaceProps, - ComputeBaseDimensionProps { + ComputeBaseSpaceProps, + ComputeBaseDimensionProps { ticks?: number[] arrayOfWeekdays?: string[] } @@ -80,8 +82,8 @@ export interface Month { } export interface ComputeMonths extends ComputeBaseProps, - ComputeBaseSpaceProps, - ComputeBaseDimensionProps { + ComputeBaseSpaceProps, + ComputeBaseDimensionProps { days: Day[] } @@ -94,6 +96,7 @@ export const computeCellSize = ({ daysInRange = defaultProps.daysInRange, daySpacing = defaultProps.daySpacing, offset = defaultProps.offset, + square = defaultProps.square, totalDays, width, height, @@ -112,11 +115,15 @@ export const computeCellSize = ({ rows = Math.ceil(totalDays / daysInRange) } // + 1 since we have to apply spacing to the rigth and left + const cellHeight = (heightRest - daySpacing * (rows + 1)) / rows + const cellWidth = (widthRest - daySpacing * (columns + 1)) / columns + // do we want square? + const size = Math.min(cellHeight, cellWidth) return { columns, rows, - cellHeight: (heightRest - daySpacing * (rows + 1)) / rows, - cellWidth: (widthRest - daySpacing * (columns + 1)) / columns, + cellHeight: square ? size : cellHeight, + cellWidth: square ? size : cellWidth, } } diff --git a/packages/calendar/stories/timeRange.stories.js b/packages/calendar/stories/timeRange.stories.js index 7158f8dc4..0bc24688a 100644 --- a/packages/calendar/stories/timeRange.stories.js +++ b/packages/calendar/stories/timeRange.stories.js @@ -1,6 +1,6 @@ import React from 'react' import { storiesOf } from '@storybook/react' -import { withKnobs, number, date } from '@storybook/addon-knobs' +import { withKnobs, number, date, boolean } from '@storybook/addon-knobs' import { TimeRange, ResponsiveTimeRange } from '../src' import { generateOrderedDayCounts } from "./generateDayCounts"; @@ -17,6 +17,7 @@ stories.add('TimeRange horizontal', () => { const to = new Date(date('to', new Date(2021, 0, 7))) return value, margin: { From 2740308b037829b2dcc8ddb61a71b9d929127ab1 Mon Sep 17 00:00:00 2001 From: Thorsten Scherler Date: Fri, 7 May 2021 10:31:14 +0200 Subject: [PATCH 8/8] eslint - formating changes and fix offences --- packages/calendar/src/compute-timeRange.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/calendar/src/compute-timeRange.ts b/packages/calendar/src/compute-timeRange.ts index aef6e22d1..811b6cb89 100644 --- a/packages/calendar/src/compute-timeRange.ts +++ b/packages/calendar/src/compute-timeRange.ts @@ -38,8 +38,8 @@ export interface ComputeCellSize extends ComputeBaseProps, ComputeBaseSpaceProps export interface ComputeCellPositions extends ComputeBaseProps, - ComputeBaseSpaceProps, - ComputeBaseDimensionProps { + ComputeBaseSpaceProps, + ComputeBaseDimensionProps { data: { date: Date day: string @@ -50,8 +50,8 @@ export interface ComputeCellPositions export interface ComputeWeekdays extends ComputeBaseProps, - ComputeBaseSpaceProps, - ComputeBaseDimensionProps { + ComputeBaseSpaceProps, + ComputeBaseDimensionProps { ticks?: number[] arrayOfWeekdays?: string[] } @@ -82,8 +82,8 @@ export interface Month { } export interface ComputeMonths extends ComputeBaseProps, - ComputeBaseSpaceProps, - ComputeBaseDimensionProps { + ComputeBaseSpaceProps, + ComputeBaseDimensionProps { days: Day[] }