react-native-chart-kit
Version:
If you're looking to **build a website or a cross-platform mobile app** – we will be happy to help you! Send a note to clients@ui1.io and we will be in touch with you shortly.
317 lines (281 loc) • 9.36 kB
JavaScript
import React from 'react'
import PropTypes from 'prop-types'
import { View } from 'react-native'
import {
Svg,
G,
Text,
Rect
} from 'react-native-svg'
import _ from 'lodash'
import AbstractChart from "../abstract-chart"
import { DAYS_IN_WEEK, MILLISECONDS_IN_ONE_DAY, MONTH_LABELS } from './constants'
import { shiftDate, getBeginningTimeForDate, convertToDate } from './dateHelpers'
const SQUARE_SIZE = 20
const MONTH_LABEL_GUTTER_SIZE = 8
const paddingLeft = 32
class ContributionGraph extends AbstractChart {
constructor(props) {
super(props)
this.state = {
valueCache: this.getValueCache(props.values)
}
}
componentWillReceiveProps(nextProps) {
this.setState({
valueCache: this.getValueCache(nextProps.values)
})
}
getSquareSizeWithGutter() {
return (this.props.squareSize || SQUARE_SIZE) + this.props.gutterSize
}
getMonthLabelSize() {
let {squareSize = SQUARE_SIZE} = this.props
if (!this.props.showMonthLabels) {
return 0
} if (this.props.horizontal) {
return squareSize + MONTH_LABEL_GUTTER_SIZE
}
return 2 * (squareSize + MONTH_LABEL_GUTTER_SIZE)
}
getStartDate() {
return shiftDate(this.getEndDate(), -this.props.numDays + 1) // +1 because endDate is inclusive
}
getEndDate() {
return getBeginningTimeForDate(convertToDate(this.props.endDate))
}
getStartDateWithEmptyDays() {
return shiftDate(this.getStartDate(), -this.getNumEmptyDaysAtStart())
}
getNumEmptyDaysAtStart() {
return this.getStartDate().getDay()
}
getNumEmptyDaysAtEnd() {
return (DAYS_IN_WEEK - 1) - this.getEndDate().getDay()
}
getWeekCount() {
const numDaysRoundedToWeek = this.props.numDays + this.getNumEmptyDaysAtStart() + this.getNumEmptyDaysAtEnd()
return Math.ceil(numDaysRoundedToWeek / DAYS_IN_WEEK)
}
getWeekWidth() {
return DAYS_IN_WEEK * this.getSquareSizeWithGutter()
}
getWidth() {
return (this.getWeekCount() * this.getSquareSizeWithGutter()) - this.props.gutterSize
}
getHeight() {
return this.getWeekWidth() + (this.getMonthLabelSize() - this.props.gutterSize)
}
getValueCache(values) {
return values.reduce((memo, value) => {
const date = convertToDate(value.date)
const index = Math.floor((date - this.getStartDateWithEmptyDays()) / MILLISECONDS_IN_ONE_DAY)
memo[index] = {
value,
title: this.props.titleForValue ? this.props.titleForValue(value) : null,
tooltipDataAttrs: this.getTooltipDataAttrsForValue(value)
}
return memo
}, {})
}
getValueForIndex(index) {
if (this.state.valueCache[index]) {
return this.state.valueCache[index].value
}
return null
}
getClassNameForIndex(index) {
if (this.state.valueCache[index]) {
if (this.state.valueCache[index].value) {
const count = this.state.valueCache[index].value.count
if (count) {
const opacity = ((count * 0.15 > 1) ? 1 : count * 0.15) + 0.15
return this.props.chartConfig.color(opacity)
}
}
}
return this.props.chartConfig.color(0.15)
}
getTitleForIndex(index) {
if (this.state.valueCache[index]) {
return this.state.valueCache[index].title
}
return this.props.titleForValue ? this.props.titleForValue(null) : null
}
getTooltipDataAttrsForIndex(index) {
if (this.state.valueCache[index]) {
return this.state.valueCache[index].tooltipDataAttrs
}
return this.getTooltipDataAttrsForValue({ date: null, count: null })
}
getTooltipDataAttrsForValue(value) {
const { tooltipDataAttrs } = this.props
if (typeof tooltipDataAttrs === 'function') {
return tooltipDataAttrs(value)
}
return tooltipDataAttrs
}
getTransformForWeek(weekIndex) {
if (this.props.horizontal) {
return [weekIndex * this.getSquareSizeWithGutter(), 50]
}
return [10, weekIndex * this.getSquareSizeWithGutter()]
}
getTransformForMonthLabels() {
if (this.props.horizontal) {
return null
}
return `${this.getWeekWidth() + MONTH_LABEL_GUTTER_SIZE}, 0`
}
getTransformForAllWeeks() {
if (this.props.horizontal) {
return `0, ${this.getMonthLabelSize() - 100}`
}
return null
}
getViewBox() {
if (this.props.horizontal) {
return `${this.getWidth()} ${this.getHeight()}`
}
return `${this.getHeight()} ${this.getWidth()}`
}
getSquareCoordinates(dayIndex) {
if (this.props.horizontal) {
return [0, dayIndex * this.getSquareSizeWithGutter()]
}
return [dayIndex * this.getSquareSizeWithGutter(), 0]
}
getMonthLabelCoordinates(weekIndex) {
if (this.props.horizontal) {
return [
weekIndex * this.getSquareSizeWithGutter(),
this.getMonthLabelSize() - MONTH_LABEL_GUTTER_SIZE
]
}
const verticalOffset = -2
return [
0,
((weekIndex + 1) * this.getSquareSizeWithGutter()) + verticalOffset
]
}
handleClick(value) {
if (this.props.onClick) {
this.props.onClick(value)
}
}
renderSquare(dayIndex, index) {
const indexOutOfRange = index < this.getNumEmptyDaysAtStart() || index >= this.getNumEmptyDaysAtStart() + this.props.numDays
if (indexOutOfRange && !this.props.showOutOfRangeDays) {
return null
}
const [x, y] = this.getSquareCoordinates(dayIndex)
const { squareSize = SQUARE_SIZE } = this.props
return (
<Rect
key={index}
width={squareSize}
height={squareSize}
x={x + paddingLeft}
y={y}
title={this.getTitleForIndex(index)}
fill={this.getClassNameForIndex(index)}
{...this.getTooltipDataAttrsForIndex(index)}
/>
)
}
renderWeek(weekIndex) {
const [x, y] = this.getTransformForWeek(weekIndex)
return (
<G key={weekIndex} x={x} y={y}>
{_.range(DAYS_IN_WEEK).map(dayIndex => this.renderSquare(dayIndex, (weekIndex * DAYS_IN_WEEK) + dayIndex))}
</G>
)
}
renderAllWeeks() {
return _.range(this.getWeekCount()).map(weekIndex => this.renderWeek(weekIndex))
}
renderMonthLabels() {
if (!this.props.showMonthLabels) {
return null
}
const weekRange = _.range(this.getWeekCount() - 1) // don't render for last week, because label will be cut off
return weekRange.map(weekIndex => {
const endOfWeek = shiftDate(this.getStartDateWithEmptyDays(), (weekIndex + 1) * DAYS_IN_WEEK)
const [x, y] = this.getMonthLabelCoordinates(weekIndex)
return (endOfWeek.getDate() >= 1 && endOfWeek.getDate() <= DAYS_IN_WEEK) ? (
<Text
key={weekIndex}
fontSize={12}
x={x + paddingLeft}
y={y + 8}
fill={this.props.chartConfig.color(0.5)}
>
{MONTH_LABELS[endOfWeek.getMonth()]}
</Text>
) : null
})
}
render() {
const { style = {} } = this.props
let { borderRadius = 0 } = style
if (!borderRadius && this.props.chartConfig.style) {
const stupidXo = this.props.chartConfig.style.borderRadius
borderRadius = stupidXo
}
return (
<View style={style}>
<Svg
height={this.props.height}
width={this.props.width}
>
{this.renderDefs({
width: this.props.width,
height: this.props.height,
...this.props.chartConfig
})}
<Rect
width="100%"
height={this.props.height}
rx={borderRadius}
ry={borderRadius}
fill="url(#backgroundGradient)"/>
<G>
{this.renderMonthLabels()}
</G>
<G>
{this.renderAllWeeks()}
</G>
</Svg>
</View>
)
}
}
ContributionGraph.ViewPropTypes = {
values: PropTypes.arrayOf( // array of objects with date and arbitrary metadata
PropTypes.shape({
date: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date)]).isRequired
}).isRequired
).isRequired,
numDays: PropTypes.number, // number of days back from endDate to show
endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date)]), // end of date range
gutterSize: PropTypes.number, // size of space between squares
squareSize: PropTypes.number, // size of squares
horizontal: PropTypes.bool, // whether to orient horizontally or vertically
showMonthLabels: PropTypes.bool, // whether to show month labels
showOutOfRangeDays: PropTypes.bool, // whether to render squares for extra days in week after endDate, and before start date
tooltipDataAttrs: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), // data attributes to add to square for setting 3rd party tooltips, e.g. { 'data-toggle': 'tooltip' } for bootstrap tooltips
titleForValue: PropTypes.func, // function which returns title text for value
classForValue: PropTypes.func, // function which returns html class for value
onClick: PropTypes.func // callback function when a square is clicked
}
ContributionGraph.defaultProps = {
numDays: 200,
endDate: new Date(),
gutterSize: 1,
squareSize: SQUARE_SIZE,
horizontal: true,
showMonthLabels: true,
showOutOfRangeDays: false,
classForValue: value => (value ? 'black' : '#8cc665')
}
export default ContributionGraph