Skip to content

Commit

Permalink
feat(calendar): add TimeRange component and storybook (#1503)
Browse files Browse the repository at this point in the history
  • Loading branch information
scherler authored May 29, 2021
1 parent fadeb0e commit 8245fbd
Show file tree
Hide file tree
Showing 9 changed files with 840 additions and 6 deletions.
69 changes: 63 additions & 6 deletions packages/calendar/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
}

Expand All @@ -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'

Expand Down Expand Up @@ -102,8 +102,65 @@ declare module '@nivo/calendar' {
role: string
}>

export class Calendar extends React.Component<CalendarSvgProps & Dimensions> {}
export class ResponsiveCalendar extends React.Component<CalendarSvgProps> {}
export class CalendarCanvas extends React.Component<CalendarSvgProps & Dimensions> {}
export class ResponsiveCalendarCanvas extends React.Component<CalendarSvgProps> {}
export class Calendar extends React.Component<CalendarSvgProps & Dimensions> { }
export class ResponsiveCalendar extends React.Component<CalendarSvgProps> { }
export class CalendarCanvas extends React.Component<CalendarSvgProps & Dimensions> { }
export class ResponsiveCalendarCanvas extends React.Component<CalendarSvgProps> { }

export type TimeRangeCommonProps = Partial<{
minValue: 'auto' | number
maxValue: 'auto' | number
direction: CalendarDirection
colors: string[]
colorScale: ColorScale
margin: Box
square?: boolean
daySpacing: number
dayRadius: number
dayBorderWidth: number
dayBorderColor: string
emptyColor: string
isInteractive: boolean
onClick?: CalendarMouseHandler
onMouseMove?: CalendarMouseHandler
onMouseLeave?: CalendarMouseHandler
onMouseEnter?: CalendarMouseHandler
tooltip: React.FunctionComponent<CalendarDayData>
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: string
date: Date
value: number
}

export interface TimeRangeData {
from: Date
to: Date
data: TimeRangeDatum[]
}
export type TimeRangeProps = TimeRangeData &
TimeRangeCommonProps &
Partial<{
onClick: (datum: CalendarDayData, event: React.MouseEvent<SVGRectElement>) => void
role: string
}> &
Dimensions

export type TimeRangeSvgProps = TimeRangeData &
TimeRangeCommonProps &
Partial<{
onClick: (datum: CalendarDayData, event: React.MouseEvent<SVGRectElement>) => void
role: string
}>

export class ResponsiveTimeRange extends React.Component<TimeRangeSvgProps> { }
}
11 changes: 11 additions & 0 deletions packages/calendar/src/ResponsiveTimeRange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react'
import { ResponsiveWrapper } from '@nivo/core'
import TimeRange from './TimeRange'

const ResponsiveTimeRange = props => (
<ResponsiveWrapper>
{({ width, height }) => <TimeRange width={width} height={height} {...props} />}
</ResponsiveWrapper>
)

export default ResponsiveTimeRange
181 changes: 181 additions & 0 deletions packages/calendar/src/TimeRange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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,

square,
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({
square,
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 (
<SvgWrapper
width={outerWidth}
height={outerHeight}
margin={margin}
role={role}
theme={theme}
>
{weekdayLegends.map(legend => (
<text
key={legend.value}
transform={`translate(${legend.x},${legend.y}) rotate(${legend.rotation})`}
textAnchor="left"
style={theme.labels.text}
>
{legend.value}
</text>
))}
{days.map(d => {
return (
<TimeRangeDay
key={d.day.toString()}
data={d}
x={d.coordinates.x}
rx={dayRadius}
y={d.coordinates.y}
ry={dayRadius}
spacing={daySpacing}
width={cellWidth}
height={cellHeight}
color={d.color}
borderWidth={dayBorderWidth}
borderColor={dayBorderColor}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseMove={onMouseMove}
isInteractive={isInteractive}
tooltip={tooltip}
theme={theme}
onClick={onClick}
formatValue={formatValue}
/>
)
})}
<CalendarMonthLegends months={monthLegends} legend={monthLegend} theme={theme} />

{legends?.map((legend, i) => {
const legendData = colorScaleFn.ticks(legend.itemCount).map(value => ({
id: value,
label: formatLegend(value),
color: colorScaleFn(value),
}))

return (
<BoxLegendSvg
key={i}
{...legend}
containerWidth={width}
containerHeight={height}
data={legendData}
theme={theme}
/>
)
})}
</SvgWrapper>
)
}

export default withContainer(TimeRange)
Loading

0 comments on commit 8245fbd

Please sign in to comment.