diff --git a/.storybook/config.js b/.storybook/config.js index 75c980d9d0..7650a9c362 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -50,6 +50,7 @@ function loadStories() { require('../packages/bullet/stories/bullet.stories') require('../packages/bump/stories/bump.stories') require('../packages/calendar/stories/calendar.stories') + require('../packages/calendar/stories/calendarCanvas.stories') require('../packages/chord/stories/chord.stories') require('../packages/circle-packing/stories/bubble.stories') require('../packages/circle-packing/stories/bubbleHtml.stories') diff --git a/packages/calendar/index.d.ts b/packages/calendar/index.d.ts index 3ffee6513d..ff98455f86 100644 --- a/packages/calendar/index.d.ts +++ b/packages/calendar/index.d.ts @@ -1,3 +1,11 @@ +/* + * 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 * as React from 'react' import { Dimensions, Theme, Box, BoxAlign } from '@nivo/core' import { LegendProps } from '@nivo/legends' @@ -16,12 +24,18 @@ declare module '@nivo/calendar' { data: CalendarDatum[] } + type ValueFormatter = ( + datum: Omit + ) => string | number + export type CalendarDirection = 'horizontal' | 'vertical' export type CalendarLegend = LegendProps & { itemCount: number } + export type CalendarMouseHandler = (data: CalendarDayData, event: React.MouseEvent) => void + export interface CalendarDayData { date: Date day: string @@ -32,18 +46,25 @@ declare module '@nivo/calendar' { y: number } + export interface ColorScale { + (value: number | { valueOf(): number }): Range + ticks(count?: number): number[] + } + export type CalendarCommonProps = Partial<{ minValue: 'auto' | number maxValue: 'auto' | number direction: CalendarDirection colors: string[] + colorScale: ColorScale margin: Box align: BoxAlign yearLegend: (year: number) => string | number yearSpacing: number yearLegendOffset: number + yearLegendPosition: 'before' | 'after' monthLegend: (year: number, month: number, date: Date) => string | number monthSpacing: number @@ -59,9 +80,16 @@ declare module '@nivo/calendar' { isInteractive: boolean - tooltipFormat: (value: number) => string | number + onClick?: CalendarMouseHandler + onMouseMove?: CalendarMouseHandler + onMouseLeave?: CalendarMouseHandler + onMouseEnter?: CalendarMouseHandler + tooltip: React.StatelessComponent + valueFormat?: string | ValueFormatter + legendFormat?: string | ValueFormatter + legends: CalendarLegend[] theme: Theme diff --git a/packages/calendar/package.json b/packages/calendar/package.json index 04943a9501..3c9c5c3caf 100644 --- a/packages/calendar/package.json +++ b/packages/calendar/package.json @@ -31,8 +31,7 @@ "d3-time-format": "^2.1.3", "lodash.isdate": "^4.0.1", "lodash.memoize": "^4.1.2", - "lodash.range": "^3.2.0", - "recompose": "^0.30.0" + "lodash.range": "^3.2.0" }, "peerDependencies": { "prop-types": ">= 15.5.10 < 16.0.0", @@ -41,4 +40,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/packages/calendar/src/Calendar.js b/packages/calendar/src/Calendar.js index 44291c7ade..cdec35f72a 100644 --- a/packages/calendar/src/Calendar.js +++ b/packages/calendar/src/Calendar.js @@ -7,111 +7,144 @@ * file that was distributed with this source code. */ import React from 'react' -import { Container, SvgWrapper } from '@nivo/core' +import { SvgWrapper, useTheme, useDimensions, withContainer, useValueFormatter } from '@nivo/core' import { BoxLegendSvg } from '@nivo/legends' -import { setDisplayName } from 'recompose' -import { CalendarPropTypes } from './props' -import enhance from './enhance' +import { CalendarPropTypes, CalendarDefaultProps } from './props' import CalendarYearLegends from './CalendarYearLegends' import CalendarMonthPath from './CalendarMonthPath' import CalendarMonthLegends from './CalendarMonthLegends' +import { useMonthLegends, useYearLegends, useCalendarLayout, useDays, useColorScale } from './hooks' import CalendarDay from './CalendarDay' const Calendar = ({ - colorScale, - - margin, + margin: partialMargin, width, height, - outerWidth, - outerHeight, - yearLegends, + align, + colors, + colorScale, + data, + direction, + emptyColor, + from, + to, + minValue, + maxValue, + valueFormat, + legendFormat, + yearLegend, + yearLegendOffset, + yearLegendPosition, + yearSpacing, - monthLegends, - monthLegend, - monthBorderWidth, monthBorderColor, + monthBorderWidth, + monthLegend, + monthLegendOffset, + monthLegendPosition, + monthSpacing, - daySpacing, - dayBorderWidth, dayBorderColor, - - theme, + dayBorderWidth, + daySpacing, isInteractive, - tooltipFormat, tooltip, onClick, + onMouseEnter, + onMouseLeave, + onMouseMove, legends, - - months, - days, }) => { + const theme = useTheme() + const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions( + width, + height, + partialMargin + ) + const { months, years, ...rest } = useCalendarLayout({ + width: innerWidth, + height: innerHeight, + from, + to, + direction, + yearSpacing, + monthSpacing, + daySpacing, + align, + }) + const colorScaleFn = useColorScale({ data, minValue, maxValue, colors, colorScale }) + const monthLegends = useMonthLegends({ + months, + direction, + monthLegendPosition, + monthLegendOffset, + }) + const yearLegends = useYearLegends({ years, direction, yearLegendPosition, yearLegendOffset }) + const days = useDays({ days: rest.days, data, colorScale: colorScaleFn, emptyColor }) + const formatLegend = useValueFormatter(legendFormat) + const formatValue = useValueFormatter(valueFormat) + return ( - - {({ showTooltip, hideTooltip }) => ( - - {days.map(d => ( - - ))} - {months.map(m => ( - - ))} - + {days.map(d => ( + + ))} + {months.map(m => ( + + ))} + + + {legends.map((legend, i) => { + const legendData = colorScaleFn.ticks(legend.itemCount).map(value => ({ + id: value, + label: formatLegend(value), + color: colorScaleFn(value), + })) + + return ( + - - {legends.map((legend, i) => { - const legendData = colorScale.ticks(legend.itemCount).map(value => ({ - id: value, - label: value, - color: colorScale(value), - })) - - return ( - - ) - })} - - )} - + ) + })} + ) } Calendar.displayName = 'Calendar' +Calendar.defaultProps = CalendarDefaultProps Calendar.propTypes = CalendarPropTypes -export default setDisplayName('Calendar')(enhance(Calendar)) +export default withContainer(Calendar) diff --git a/packages/calendar/src/CalendarCanvas.js b/packages/calendar/src/CalendarCanvas.js index f55c0fcf47..ec84329101 100644 --- a/packages/calendar/src/CalendarCanvas.js +++ b/packages/calendar/src/CalendarCanvas.js @@ -6,23 +6,31 @@ * 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 } from '@nivo/core' +import React, { memo, useRef, useState, useEffect, useCallback } from 'react' +import { + isCursorInRect, + getRelativeCursor, + degreesToRadians, + useDimensions, + useTheme, + withContainer, + useValueFormatter, +} from '@nivo/core' import { renderLegendToCanvas } from '@nivo/legends' -import { BasicTooltip } from '@nivo/tooltip' -import enhance from './enhance' -import { CalendarCanvasPropTypes } from './props' +import { CalendarCanvasPropTypes, CalendarCanvasDefaultProps } from './props' +import { useCalendarLayout, useColorScale, useMonthLegends, useYearLegends, useDays } from './hooks' +import { useTooltip } from '@nivo/tooltip' -const findDayUnderCursor = (days, size, spacing, margin, x, y) => { +const findDayUnderCursor = (event, canvasEl, days, size, dayBorderWidth, margin) => { + const [x, y] = getRelativeCursor(canvasEl, event) 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, + day.x + margin.left - dayBorderWidth / 2, + day.y + margin.top - dayBorderWidth / 2, + size + dayBorderWidth, + size + dayBorderWidth, x, y ) @@ -30,201 +38,260 @@ const findDayUnderCursor = (days, size, spacing, margin, 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, +const CalendarCanvas = memo( + ({ + margin: partialMargin, + width, + height, + pixelRatio, + + align, + colors, + colorScale, + data, + direction, + emptyColor, + from, + to, + minValue, + maxValue, + valueFormat, + legendFormat, + + yearLegend, + yearLegendOffset, + yearLegendPosition, + yearSpacing, + + monthLegend, + monthLegendOffset, + monthLegendPosition, + monthSpacing, + + dayBorderColor, + dayBorderWidth, + daySpacing, + + isInteractive, + tooltip, + onClick, + onMouseEnter, + onMouseLeave, + onMouseMove, + + legends, + }) => { + const canvasEl = useRef(null) + const { innerWidth, innerHeight, outerWidth, outerHeight, margin } = useDimensions( width, height, - outerWidth, - outerHeight, + partialMargin + ) + const { months, years, ...rest } = useCalendarLayout({ + width: innerWidth, + height: innerHeight, + from, + to, + direction, + yearSpacing, + monthSpacing, + daySpacing, + align, + }) + const colorScaleFn = useColorScale({ data, minValue, maxValue, colors, colorScale }) + const monthLegends = useMonthLegends({ + months, + direction, + monthLegendPosition, + monthLegendOffset, + }) + const yearLegends = useYearLegends({ + years, + direction, + yearLegendPosition, + yearLegendOffset, + }) + const days = useDays({ days: rest.days, data, colorScale: colorScaleFn, emptyColor }) + const [currentDay, setCurrentDay] = useState(null) + const theme = useTheme() + const formatValue = useValueFormatter(valueFormat) + const formatLegend = useValueFormatter(legendFormat) - colorScale, + const { showTooltipFromEvent, hideTooltip } = useTooltip() - yearLegends, - yearLegend, + useEffect(() => { + canvasEl.current.width = outerWidth * pixelRatio + canvasEl.current.height = outerHeight * pixelRatio - monthLegends, - monthLegend, + const ctx = canvasEl.current.getContext('2d') - days, - dayBorderWidth, - dayBorderColor, + ctx.scale(pixelRatio, pixelRatio) - legends, + ctx.fillStyle = theme.background + ctx.fillRect(0, 0, outerWidth, outerHeight) + ctx.translate(margin.left, margin.top) - 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() - } - }) + days.forEach(day => { + ctx.fillStyle = day.color + if (dayBorderWidth > 0) { + ctx.strokeStyle = dayBorderColor + ctx.lineWidth = dayBorderWidth + } - 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() - }) + ctx.beginPath() + ctx.rect(day.x, day.y, day.size, day.size) + ctx.fill() - 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() - }) + if (dayBorderWidth > 0) { + ctx.stroke() + } + }) - 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, + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillStyle = theme.labels.text.fill + ctx.font = `${theme.labels.text.fontSize}px ${theme.labels.text.fontFamily}` + + monthLegends.forEach(month => { + ctx.save() + ctx.translate(month.x, month.y) + ctx.rotate(degreesToRadians(month.rotation)) + ctx.fillText(monthLegend(month.year, month.month, month.date), 0, 0) + ctx.restore() + }) + + yearLegends.forEach(year => { + ctx.save() + ctx.translate(year.x, year.y) + ctx.rotate(degreesToRadians(year.rotation)) + ctx.fillText(yearLegend(year.year), 0, 0) + ctx.restore() }) - }) - } - handleMouseHover = (showTooltip, hideTooltip) => event => { - const { - isInteractive, + legends.forEach(legend => { + const legendData = colorScaleFn.ticks(legend.itemCount).map(value => ({ + id: value, + label: formatLegend(value), + color: colorScaleFn(value), + })) + + renderLegendToCanvas(ctx, { + ...legend, + data: legendData, + containerWidth: innerWidth, + containerHeight: innerHeight, + theme, + }) + }) + }, [ + canvasEl, + innerHeight, + innerWidth, + outerWidth, + outerHeight, + pixelRatio, 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( - { + const data = findDayUnderCursor( + event, + canvasEl.current, + days, + days[0].size, + dayBorderWidth, + margin + ) + + if (data) { + setCurrentDay(data) + const formatedData = { + ...data, + value: formatValue(data.value), + data: { ...data.data }, } - />, - 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) - } + showTooltipFromEvent(React.createElement(tooltip, { ...formatedData }), event) + !currentDay && onMouseEnter && onMouseEnter(data, event) + onMouseMove && onMouseMove(data, event) + currentDay && + currentDay.id !== data.id && + onMouseLeave && + onMouseLeave(data, event) + } else { + hideTooltip() + onMouseLeave && onMouseLeave(data, event) + } + }, + [ + canvasEl, + margin, + days, + setCurrentDay, + formatValue, + daySpacing, + showTooltipFromEvent, + hideTooltip, + onMouseEnter, + onMouseMove, + onMouseLeave, + ] + ) - render() { - const { outerWidth, outerHeight, pixelRatio, isInteractive, theme } = this.props + const handleMouseLeave = useCallback(() => { + setCurrentDay(null) + hideTooltip() + }, [setCurrentDay, hideTooltip]) + + const handleClick = useCallback( + event => { + if (!onClick) return + + const data = findDayUnderCursor( + event, + canvasEl.current, + days, + days[0].size, + daySpacing, + margin + ) + + data && onClick(data, event) + }, + [canvasEl, daySpacing, margin, setCurrentDay, days, onClick] + ) return ( - - {({ showTooltip, hideTooltip }) => ( - { - 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} - /> - )} - + ) } -} +) CalendarCanvas.displayName = 'CalendarCanvas' +CalendarCanvas.defaultProps = CalendarCanvasDefaultProps +CalendarCanvas.propTypes = CalendarCanvasPropTypes -export default setDisplayName(CalendarCanvas.displayName)(enhance(CalendarCanvas)) +export default withContainer(CalendarCanvas) diff --git a/packages/calendar/src/CalendarDay.js b/packages/calendar/src/CalendarDay.js index c528abf848..f91379db9d 100644 --- a/packages/calendar/src/CalendarDay.js +++ b/packages/calendar/src/CalendarDay.js @@ -6,56 +6,92 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React, { memo } from 'react' +import React, { memo, useCallback } from 'react' import PropTypes from 'prop-types' -import { compose, withPropsOnChange, pure } from 'recompose' -import { noop } from '@nivo/core' -import { BasicTooltip } from '@nivo/tooltip' +import { useTooltip } from '@nivo/tooltip' const CalendarDay = memo( ({ + data, x, y, size, - spacing, color, borderWidth, borderColor, + isInteractive, + tooltip, + onMouseEnter, + onMouseMove, + onMouseLeave, onClick, - showTooltip, - hideTooltip, + 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 ( - <> - - - + ) } ) +CalendarDay.displayName = 'CalendarDay' CalendarDay.propTypes = { - onClick: PropTypes.func.isRequired, + onClick: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, + onMouseMove: PropTypes.func, data: PropTypes.object.isRequired, x: PropTypes.number.isRequired, y: PropTypes.number.isRequired, @@ -64,50 +100,14 @@ CalendarDay.propTypes = { color: PropTypes.string.isRequired, borderWidth: PropTypes.number.isRequired, borderColor: PropTypes.string.isRequired, + isInteractive: PropTypes.bool.isRequired, + formatValue: PropTypes.func, - tooltipFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - tooltip: PropTypes.func, - showTooltip: PropTypes.func.isRequired, - hideTooltip: PropTypes.func.isRequired, + tooltip: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired, theme: PropTypes.shape({ tooltip: PropTypes.shape({}).isRequired, }).isRequired, } -CalendarDay.displayName = 'CalendarDay' - -const enhance = compose( - withPropsOnChange(['data', 'onClick'], ({ data, onClick }) => ({ - onClick: event => onClick(data, event), - })), - withPropsOnChange( - ['data', 'color', 'showTooltip', 'tooltipFormat', 'tooltip', 'theme'], - ({ data, color, showTooltip, tooltipFormat, tooltip, theme }) => { - if (data.value === undefined) return { showTooltip: noop } - - return { - showTooltip: event => - showTooltip( - , - event - ), - } - } - ), - pure -) - -export default enhance(CalendarDay) +export default CalendarDay diff --git a/packages/calendar/src/CalendarMonthPath.js b/packages/calendar/src/CalendarMonthPath.js index ad894d5634..95c60b0ce2 100644 --- a/packages/calendar/src/CalendarMonthPath.js +++ b/packages/calendar/src/CalendarMonthPath.js @@ -6,21 +6,22 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React from 'react' -import pure from 'recompose/pure' +import React, { memo } from 'react' import PropTypes from 'prop-types' -const CalendarMonthPath = ({ path, borderWidth, borderColor }) => ( - -) +const CalendarMonthPath = memo(({ path, borderWidth, borderColor }) => { + return ( + + ) +}) CalendarMonthPath.propTypes = { path: PropTypes.string.isRequired, @@ -28,4 +29,6 @@ CalendarMonthPath.propTypes = { borderColor: PropTypes.string.isRequired, } -export default pure(CalendarMonthPath) +CalendarMonthPath.displayName = 'CalendarMonthPath' + +export default CalendarMonthPath diff --git a/packages/calendar/src/CalendarTooltip.js b/packages/calendar/src/CalendarTooltip.js new file mode 100644 index 0000000000..b9f50a55cd --- /dev/null +++ b/packages/calendar/src/CalendarTooltip.js @@ -0,0 +1,25 @@ +/* + * 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 } from 'react' +import PropTypes from 'prop-types' +import { BasicTooltip } from '@nivo/tooltip' + +const CalendarTooltip = memo(({ value, day, color }) => { + if (value === undefined || isNaN(value)) return null + return +}) + +CalendarTooltip.displayName = 'CalendarTooltip' +CalendarTooltip.propTypes = { + value: PropTypes.object.isRequired, + day: PropTypes.object.isRequired, + color: PropTypes.object.isRequired, +} + +export default CalendarTooltip diff --git a/packages/calendar/src/compute.js b/packages/calendar/src/compute.js index fb26eb1752..9732496b4e 100644 --- a/packages/calendar/src/compute.js +++ b/packages/calendar/src/compute.js @@ -374,16 +374,18 @@ export const computeLayout = ({ */ export const bindDaysData = ({ days, data, colorScale, emptyColor }) => { return days.map(day => { - day.color = emptyColor - data.forEach(dayData => { - if (dayData.day === day.day) { - day.value = dayData.value - day.color = colorScale(dayData.value) - day.data = dayData - } - }) + const dayData = data.find(item => item.day === day.day) + + if (!dayData) { + return { ...day, color: emptyColor } + } - return day + return { + ...day, + color: colorScale(dayData.value), + data: dayData, + value: dayData.value, + } }) } diff --git a/packages/calendar/src/enhance.js b/packages/calendar/src/enhance.js deleted file mode 100644 index 1b5e1fdff9..0000000000 --- a/packages/calendar/src/enhance.js +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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 { compose, defaultProps, withPropsOnChange, pure } from 'recompose' -import { scaleQuantize } from 'd3-scale' -import { withTheme, withDimensions } from '@nivo/core' -import { CalendarDefaultProps, CalendarCanvasDefaultProps } from './props' -import { - computeDomain, - computeLayout, - bindDaysData, - computeYearLegendPositions, - computeMonthLegendPositions, -} from './compute' - -const commonEnhancers = [ - withTheme(), - withDimensions(), - withPropsOnChange( - ['data', 'minValue', 'maxValue', 'colors', 'colorScale'], - ({ data, minValue, maxValue, colors, colorScale }) => { - if (colorScale) return { colorScale } - const domain = computeDomain(data, minValue, maxValue) - - const defaultColorScale = scaleQuantize().domain(domain).range(colors) - - return { colorScale: defaultColorScale } - } - ), - withPropsOnChange( - [ - 'width', - 'height', - 'from', - 'to', - 'direction', - 'yearSpacing', - 'monthSpacing', - 'daySpacing', - 'align', - ], - ({ width, height, from, to, direction, yearSpacing, monthSpacing, daySpacing, align }) => { - return computeLayout({ - width, - height, - from, - to, - direction, - yearSpacing, - monthSpacing, - daySpacing, - align, - }) - } - ), - withPropsOnChange( - ['years', 'direction', 'yearLegendPosition', 'yearLegendOffset'], - ({ years, direction, yearLegendPosition, yearLegendOffset }) => { - return { - yearLegends: computeYearLegendPositions({ - years, - direction, - position: yearLegendPosition, - offset: yearLegendOffset, - }), - } - } - ), - withPropsOnChange( - ['months', 'direction', 'monthLegendPosition', 'monthLegendOffset'], - ({ months, direction, monthLegendPosition, monthLegendOffset }) => { - return { - monthLegends: computeMonthLegendPositions({ - months, - direction, - position: monthLegendPosition, - offset: monthLegendOffset, - }), - } - } - ), - withPropsOnChange( - ['days', 'data', 'colorScale', 'emptyColor'], - ({ days, data, colorScale, emptyColor }) => { - return { - days: bindDaysData({ - days, - data, - colorScale, - emptyColor, - }), - } - } - ), -] - -export default Component => { - switch (Component.displayName) { - case 'Calendar': - return compose(...[defaultProps(CalendarDefaultProps), ...commonEnhancers, pure])( - Component - ) - - case 'CalendarCanvas': - return compose(...[defaultProps(CalendarCanvasDefaultProps), ...commonEnhancers, pure])( - Component - ) - } - - return Component -} diff --git a/packages/calendar/src/hooks.js b/packages/calendar/src/hooks.js new file mode 100644 index 0000000000..18648fe95b --- /dev/null +++ b/packages/calendar/src/hooks.js @@ -0,0 +1,88 @@ +/* + * 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 { useMemo } from 'react' +import { scaleQuantize } from 'd3-scale' +import { + computeDomain, + computeYearLegendPositions, + computeMonthLegendPositions, + bindDaysData, + computeLayout, +} from './compute' + +export const useCalendarLayout = ({ + width, + height, + from, + to, + direction, + yearSpacing, + monthSpacing, + daySpacing, + align, +}) => + useMemo( + () => + computeLayout({ + width, + height, + from, + to, + direction, + yearSpacing, + monthSpacing, + daySpacing, + align, + }), + [width, height, from, to, direction, yearSpacing, monthSpacing, daySpacing, align] + ) + +export const useColorScale = ({ data, minValue, maxValue, colors, colorScale }) => + useMemo(() => { + if (colorScale) return colorScale + const domain = computeDomain(data, minValue, maxValue) + const defaultColorScale = scaleQuantize().domain(domain).range(colors) + return defaultColorScale + }, [data, minValue, maxValue, colors, colorScale]) + +export const useYearLegends = ({ years, direction, yearLegendPosition, yearLegendOffset }) => + useMemo( + () => + computeYearLegendPositions({ + years, + direction, + position: yearLegendPosition, + offset: yearLegendOffset, + }), + [years, direction, yearLegendPosition, yearLegendOffset] + ) + +export const useMonthLegends = ({ months, direction, monthLegendPosition, monthLegendOffset }) => + useMemo( + () => + computeMonthLegendPositions({ + months, + direction, + position: monthLegendPosition, + offset: monthLegendOffset, + }), + [months, direction, monthLegendPosition, monthLegendOffset] + ) + +export const useDays = ({ days, data, colorScale, emptyColor }) => + useMemo( + () => + bindDaysData({ + days, + data, + colorScale, + emptyColor, + }), + [days, data, colorScale, emptyColor] + ) diff --git a/packages/calendar/src/index.js b/packages/calendar/src/index.js index fe5bd5f824..3348563ca2 100644 --- a/packages/calendar/src/index.js +++ b/packages/calendar/src/index.js @@ -11,3 +11,5 @@ export { default as ResponsiveCalendar } from './ResponsiveCalendar' export { default as CalendarCanvas } from './CalendarCanvas' export { default as ResponsiveCalendarCanvas } from './ResponsiveCalendarCanvas' export * from './props' +export * from './compute' +export * from './hooks' diff --git a/packages/calendar/src/props.js b/packages/calendar/src/props.js index 3fac3cacfa..3ecb584b2c 100644 --- a/packages/calendar/src/props.js +++ b/packages/calendar/src/props.js @@ -8,8 +8,9 @@ */ import PropTypes from 'prop-types' import { timeFormat } from 'd3-time-format' -import { noop, boxAlignments } from '@nivo/core' +import { boxAlignments } from '@nivo/core' import { LegendPropShape } from '@nivo/legends' +import CalendarTooltip from './CalendarTooltip' const monthLabelFormat = timeFormat('%b') @@ -24,16 +25,12 @@ const commonPropTypes = { ).isRequired, align: PropTypes.oneOf(boxAlignments).isRequired, - originX: PropTypes.number.isRequired, - originY: PropTypes.number.isRequired, - calendarWidth: PropTypes.number.isRequired, - calendarHeight: PropTypes.number.isRequired, minValue: PropTypes.oneOfType([PropTypes.oneOf(['auto']), PropTypes.number]).isRequired, maxValue: PropTypes.oneOfType([PropTypes.oneOf(['auto']), PropTypes.number]).isRequired, colors: PropTypes.arrayOf(PropTypes.string).isRequired, - colorScale: PropTypes.func.isRequired, + colorScale: PropTypes.func, direction: PropTypes.oneOf(['horizontal', 'vertical']), emptyColor: PropTypes.string.isRequired, @@ -55,10 +52,13 @@ const commonPropTypes = { dayBorderColor: PropTypes.string.isRequired, isInteractive: PropTypes.bool, - onClick: PropTypes.func.isRequired, - tooltipFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - tooltip: PropTypes.func, - + onClick: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, + onMouseMove: PropTypes.func, + tooltip: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired, + valueFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + legendFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), legends: PropTypes.arrayOf( PropTypes.shape({ ...LegendPropShape, @@ -102,9 +102,9 @@ const commonDefaultProps = { dayBorderColor: '#000', isInteractive: true, - onClick: noop, legends: [], + tooltip: CalendarTooltip, } export const CalendarDefaultProps = commonDefaultProps diff --git a/packages/calendar/stories/calendar.stories.js b/packages/calendar/stories/calendar.stories.js index 0005c31f8f..57f008c460 100644 --- a/packages/calendar/stories/calendar.stories.js +++ b/packages/calendar/stories/calendar.stories.js @@ -51,22 +51,39 @@ stories.add('vertical calendar', () => ( )) -stories.add('custom tooltip', () => ( +const CustomTooltip = data => { + if (data.value === undefined) return null + return ( + + {data.day} : {data.value} + + ) +} + +stories.add('custom tooltip', () => ) + +stories.add('month spacing', () => ) + +const formater = value => value / 10 + 'M' + +stories.add('custom legend formater', () => ( ( - - {day}: {value} - - )} - theme={{ - tooltip: { - container: { - background: '#333', - }, - }, - }} {...commonProps} + margin={{ top: 40, right: 40, bottom: 40, left: 40 }} + valueFormat=".2f" + legendFormat={formater} + height={460} + legends={[ + { + anchor: 'top', + direction: 'row', + translateY: 36, + itemCount: 4, + itemWidth: 42, + itemHeight: 36, + itemsSpacing: 14, + itemDirection: 'right-to-left', + }, + ]} /> )) - -stories.add('month spacing', () => ) diff --git a/packages/calendar/stories/calendarCanvas.stories.js b/packages/calendar/stories/calendarCanvas.stories.js new file mode 100644 index 0000000000..4f503a503c --- /dev/null +++ b/packages/calendar/stories/calendarCanvas.stories.js @@ -0,0 +1,90 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { generateDayCounts } from '@nivo/generators' +import { CalendarCanvas } from '../src' +import { scaleQuantize } from 'd3-scale' + +const from = new Date(2013, 0, 1) +const to = new Date(2019, 11, 31) +const data = generateDayCounts(from, to) + +const commonProps = { + width: 1500, + height: 1560, + margin: { + top: 50, + right: 10, + bottom: 10, + left: 50, + }, + from: from.toISOString(), + to: to.toISOString(), + direction: 'horizontal', + data, +} + +const stories = storiesOf('CalendarCanvas', module) + +const CustomTooltip = data => { + if (data.value === undefined) return null + return ( + + {data.day} : {data.value} + + ) +} + +stories.add('custom tooltip', () => ) + +stories.add('spacing', () => ( + +)) + +const createColorScale = (data, minValue, maxValue) => { + const computeDomain = (data, minSpec, maxSpec) => { + const allValues = data.map(d => d.value) + const minValue = minSpec === 'auto' ? Math.min(...allValues) : minSpec + const maxValue = maxSpec === 'auto' ? Math.max(...allValues) : maxSpec + return [minValue, maxValue] + } + const colors = ['#a1cfff', '#468df3', '#a053f0', '#9629f0', '#8428d8'] + const domain = computeDomain(data, minValue, maxValue) + const defaultColorScale = scaleQuantize().domain(domain).range(colors) + const colorScale = value => { + return defaultColorScale(value) + '33' //adding alpha channel + } + colorScale.ticks = count => { + return defaultColorScale.ticks(count) + } + return colorScale +} + +const customColorScale = createColorScale(data, 'auto', 'auto') + +stories.add('custom color space function', () => ( + +)) + +const formater = value => value.toFixed(1) + ' hours' + +stories.add('custom value and legend formaters', () => ( + +))