Skip to content

Commit

Permalink
feat(calendar): add CalendarCanvas component
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphaël Benitte authored and Raphaël Benitte committed Mar 22, 2019
1 parent bf8797a commit 96f8ac2
Show file tree
Hide file tree
Showing 18 changed files with 590 additions and 119 deletions.
1 change: 0 additions & 1 deletion packages/calendar/package.json
Expand Up @@ -29,7 +29,6 @@
"d3-time": "^1.0.10",
"d3-time-format": "^2.1.3",
"lodash": "^4.17.4",
"react-spring": "^8.0.18",
"recompose": "^0.30.0"
},
"peerDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/calendar/src/Calendar.js
Expand Up @@ -111,6 +111,7 @@ const Calendar = ({
)
}

Calendar.displayName = 'Calendar'
Calendar.propTypes = CalendarPropTypes

export default setDisplayName('Calendar')(enhance(Calendar))
235 changes: 235 additions & 0 deletions packages/calendar/src/CalendarCanvas.js
@@ -0,0 +1,235 @@
/*
* 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, { Component } from 'react'
import setDisplayName from 'recompose/setDisplayName'
import {
isCursorInRect,
getRelativeCursor,
Container,
degreesToRadians,
BasicTooltip,
} from '@nivo/core'
import { renderLegendToCanvas } from '@nivo/legends'
import enhance from './enhance'
import { CalendarCanvasPropTypes } from './props'

const findDayUnderCursor = (days, size, spacing, margin, x, y) => {
return days.find(day => {
return (
day.value !== undefined &&
isCursorInRect(
day.x + margin.left - spacing / 2,
day.y + margin.top - spacing / 2,
size + spacing,
size + spacing,
x,
y
)
)
})
}

class CalendarCanvas extends Component {
static propTypes = CalendarCanvasPropTypes

componentDidMount() {
this.ctx = this.surface.getContext('2d')
this.draw(this.props)
}

shouldComponentUpdate(props) {
if (
this.props.outerWidth !== props.outerWidth ||
this.props.outerHeight !== props.outerHeight ||
this.props.isInteractive !== props.isInteractive ||
this.props.theme !== props.theme
) {
return true
} else {
this.draw(props)
return false
}
}

componentDidUpdate() {
this.ctx = this.surface.getContext('2d')
this.draw(this.props)
}

draw(props) {
const {
pixelRatio,

margin,
width,
height,
outerWidth,
outerHeight,

colorScale,

yearLegends,
yearLegend,

monthLegends,
monthLegend,

days,
dayBorderWidth,
dayBorderColor,

legends,

theme,
} = props

this.surface.width = outerWidth * pixelRatio
this.surface.height = outerHeight * pixelRatio

this.ctx.scale(pixelRatio, pixelRatio)
this.ctx.fillStyle = theme.background
this.ctx.fillRect(0, 0, outerWidth, outerHeight)
this.ctx.translate(margin.left, margin.top)

days.forEach(day => {
this.ctx.fillStyle = day.color
if (dayBorderWidth > 0) {
this.ctx.strokeStyle = dayBorderColor
this.ctx.lineWidth = dayBorderWidth
}

this.ctx.beginPath()
this.ctx.rect(day.x, day.y, day.size, day.size)
this.ctx.fill()

if (dayBorderWidth > 0) {
this.ctx.stroke()
}
})

this.ctx.textAlign = 'center'
this.ctx.textBaseline = 'middle'
this.ctx.fillStyle = theme.labels.text.fill
this.ctx.font = `${theme.labels.text.fontSize}px ${theme.labels.text.fontFamily}`

monthLegends.forEach(month => {
this.ctx.save()
this.ctx.translate(month.x, month.y)
this.ctx.rotate(degreesToRadians(month.rotation))
this.ctx.fillText(monthLegend(month.year, month.month, month.date), 0, 0)
this.ctx.restore()
})

yearLegends.forEach(year => {
this.ctx.save()
this.ctx.translate(year.x, year.y)
this.ctx.rotate(degreesToRadians(year.rotation))
this.ctx.fillText(yearLegend(year.year), 0, 0)
this.ctx.restore()
})

legends.forEach(legend => {
const legendData = colorScale.ticks(legend.itemCount).map(value => ({
id: value,
label: value,
color: colorScale(value),
}))

renderLegendToCanvas(this.ctx, {
...legend,
data: legendData,
containerWidth: width,
containerHeight: height,
theme,
})
})
}

handleMouseHover = (showTooltip, hideTooltip) => event => {
const {
isInteractive,
margin,
theme,
days,
daySpacing,
tooltipFormat,
tooltip,
} = this.props

if (!isInteractive || !days || days.length === 0) return

const [x, y] = getRelativeCursor(this.surface, event)
const currentDay = findDayUnderCursor(days, days[0].size, daySpacing, margin, x, y)

if (currentDay !== undefined) {
showTooltip(
<BasicTooltip
id={`${currentDay.day}`}
value={currentDay.value}
enableChip={true}
color={currentDay.color}
theme={theme}
format={tooltipFormat}
renderContent={
typeof tooltip === 'function' ? tooltip.bind(null, currentDay) : null
}
/>,
event
)
} else {
hideTooltip()
}
}

handleMouseLeave = hideTooltip => () => {
if (this.props.isInteractive !== true) return

hideTooltip()
}

handleClick = event => {
const { isInteractive, margin, onClick, days, daySpacing } = this.props

if (!isInteractive || !days || days.length === 0) return

const [x, y] = getRelativeCursor(this.surface, event)
const currentDay = findDayUnderCursor(days, days[0].size, daySpacing, margin, x, y)
if (currentDay !== undefined) onClick(currentDay, event)
}

render() {
const { outerWidth, outerHeight, pixelRatio, isInteractive, theme } = this.props

return (
<Container isInteractive={isInteractive} theme={theme}>
{({ showTooltip, hideTooltip }) => (
<canvas
ref={surface => {
this.surface = surface
}}
width={outerWidth * pixelRatio}
height={outerHeight * pixelRatio}
style={{
width: outerWidth,
height: outerHeight,
}}
onMouseEnter={this.handleMouseHover(showTooltip, hideTooltip)}
onMouseMove={this.handleMouseHover(showTooltip, hideTooltip)}
onMouseLeave={this.handleMouseLeave(hideTooltip)}
onClick={this.handleClick}
/>
)}
</Container>
)
}
}

CalendarCanvas.displayName = 'CalendarCanvas'

export default setDisplayName(CalendarCanvas.displayName)(enhance(CalendarCanvas))
7 changes: 2 additions & 5 deletions packages/calendar/src/CalendarDay.js
Expand Up @@ -8,11 +8,8 @@
*/
import React from 'react'
import PropTypes from 'prop-types'
import compose from 'recompose/compose'
import withPropsOnChange from 'recompose/withPropsOnChange'
import pure from 'recompose/pure'
import { noop } from '@nivo/core'
import { BasicTooltip } from '@nivo/core'
import { compose, withPropsOnChange, pure } from 'recompose'
import { BasicTooltip, noop } from '@nivo/core'

const CalendarDay = ({
x,
Expand Down
2 changes: 1 addition & 1 deletion packages/calendar/src/CalendarYearLegends.js
Expand Up @@ -34,6 +34,6 @@ CalendarYearLegends.propTypes = {
theme: PropTypes.object.isRequired,
}

CalendarYearLegends.setDisplayName = 'CalendarYearLegends'
CalendarYearLegends.displayName = 'CalendarYearLegends'

export default CalendarYearLegends
19 changes: 19 additions & 0 deletions packages/calendar/src/ResponsiveCalendarCanvas.js
@@ -0,0 +1,19 @@
/*
* 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 from 'react'
import { ResponsiveWrapper } from '@nivo/core'
import CalendarCanvas from './CalendarCanvas'

const ResponsiveCalendarCanvas = props => (
<ResponsiveWrapper>
{({ width, height }) => <CalendarCanvas width={width} height={height} {...props} />}
</ResponsiveWrapper>
)

export default ResponsiveCalendarCanvas
19 changes: 8 additions & 11 deletions packages/calendar/src/computeCalendar.js
Expand Up @@ -10,7 +10,6 @@ import memoize from 'lodash/memoize'
import isDate from 'lodash/isDate'
import range from 'lodash/range'
import max from 'lodash/max'
import assign from 'lodash/assign'
import { timeFormat } from 'd3-time-format'
import { timeDays, timeWeek, timeWeeks, timeMonths, timeYear } from 'd3-time'

Expand Down Expand Up @@ -233,16 +232,14 @@ export const computeLayout = ({ width, height, from, to, direction, yearSpacing,
const yearEnd = new Date(year + 1, 0, 1)

days = days.concat(
timeDays(yearStart, yearEnd).map(dayDate =>
assign(
{
date: dayDate,
day: dayFormat(dayDate),
size: cellSize,
},
cellPosition(dayDate, i)
)
)
timeDays(yearStart, yearEnd).map(dayDate => {
return {
date: dayDate,
day: dayFormat(dayDate),
size: cellSize,
...cellPosition(dayDate, i),
}
})
)

const yearMonths = timeMonths(yearStart, yearEnd).map(monthDate => ({
Expand Down

0 comments on commit 96f8ac2

Please sign in to comment.