import React, { Component } from 'react' import { View, FlatList, ActivityIndicator } from 'react-native' import { LocalDate } from 'js-joda' import { makeYAxisLabels, makeHorizontalGrid } from './y-axis' import nfpLines from './nfp-lines' import DayColumn from './day-column' import { getCycleDaysSortedByDate, getAmountOfCycleDays } from '../../db' import styles from './styles' import { cycleDayColor } from '../../styles' import { scaleObservable } from '../../local-storage' import config from '../../config' import AppText from '../app-text' import AppLoadingView from '../app-loading' import { shared as labels } from '../../i18n/en/labels' import DripIcon from '../../assets/drip-icons' import DripHomeIcon from '../../assets/drip-home-icons' import nothingChanged from '../../db/db-unchanged' const symptomIcons = { bleeding: <DripIcon size={16} name='drip-icon-bleeding' color={styles.iconShades.bleeding[3]}/>, mucus: <DripIcon size={16} name='drip-icon-mucus' color={styles.iconShades.mucus[4]}/>, cervix: <DripIcon size={16} name='drip-icon-cervix' color={styles.iconShades.cervix[3]}/>, desire: <DripIcon size={16} name='drip-icon-desire' color={styles.iconShades.desire[2]}/>, sex: <DripIcon size={16} name='drip-icon-sex' color={styles.iconShades.sex[2]}/>, pain: <DripIcon size={16} name='drip-icon-pain' color={styles.iconShades.pain[0]}/>, mood: <DripIcon size={16} name='drip-icon-mood' color={styles.iconShades.mood[0]}/>, note: <DripIcon size={16} name='drip-icon-note' color={styles.iconShades.note[0]}/> } export default class CycleChart extends Component { constructor(props) { super(props) this.state = {} this.cycleDaysSortedByDate = getCycleDaysSortedByDate() this.getFhmAndLtlInfo = nfpLines() } renderColumn = ({ item, index }) => { return ( <DayColumn dateString={item} index={index} navigate={this.props.navigate} symptomHeight={this.symptomHeight} columnHeight={this.columnHeight} chartHeight={this.state.chartHeight} symptomRowSymptoms={this.symptomRowSymptoms} chartSymptoms={this.chartSymptoms} getFhmAndLtlInfo={this.getFhmAndLtlInfo} /> ) } onLayout = ({ nativeEvent }) => { if (this.state.chartHeight) return const height = nativeEvent.layout.height const reCalculateChartInfo = () => { // how many symptoms need to be displayed on the chart's upper symptom row? this.symptomRowSymptoms = [ 'bleeding', 'mucus', 'cervix', 'sex', 'desire', 'pain', 'mood', 'note' ].filter((symptomName) => { return this.cycleDaysSortedByDate.some(cycleDay => { return cycleDay[symptomName] }) }) this.xAxisHeight = height * config.xAxisHeightPercentage const remainingHeight = height - this.xAxisHeight this.symptomHeight = config.symptomHeightPercentage * remainingHeight this.symptomRowHeight = this.symptomRowSymptoms.length * this.symptomHeight this.columnHeight = remainingHeight - this.symptomRowHeight this.chartSymptoms = [...this.symptomRowSymptoms] if (this.cycleDaysSortedByDate.some(day => day.temperature)) { this.chartSymptoms.push('temperature') } const columnData = this.makeColumnInfo() this.setState({ columns: columnData, chartHeight: height }) } reCalculateChartInfo() this.updateListeners(reCalculateChartInfo) } updateListeners(dataUpdateHandler) { // remove existing listeners if(this.handleDbChange) { this.cycleDaysSortedByDate.removeListener(this.handleDbChange) } if (this.removeObvListener) this.removeObvListener() this.handleDbChange = (_, changes) => { if (nothingChanged(changes)) return dataUpdateHandler() } this.cycleDaysSortedByDate.addListener(this.handleDbChange) this.removeObvListener = scaleObservable(dataUpdateHandler, false) } componentWillUnmount() { this.cycleDaysSortedByDate.removeListener(this.handleDbChange) this.removeObvListener() } makeColumnInfo() { let amountOfCycleDays = getAmountOfCycleDays() // if there's not much data yet, we want to show at least 30 days on the chart if (amountOfCycleDays < 30) { amountOfCycleDays = 30 } else { // we don't want the chart to end abruptly before the first data day amountOfCycleDays += 5 } const localDates = getTodayAndPreviousDays(amountOfCycleDays) return localDates.map(localDate => localDate.toString()) } render() { return ( <View onLayout={this.onLayout} style={{ flexDirection: 'row', flex: 1 }} > {!this.state.chartLoaded && <AppLoadingView />} {this.state.chartHeight && this.state.chartLoaded && <View> <View style={[styles.yAxis, {height: this.symptomRowHeight}]}> {this.symptomRowSymptoms.map(symptomName => { return <View style={{ alignItems: 'center', justifyContent: 'center' }} key={symptomName} width={styles.yAxis.width} height={this.symptomRowHeight / this.symptomRowSymptoms.length} > {symptomIcons[symptomName]} </View> })} </View> <View style={[styles.yAxis, {height: this.columnHeight}]}> {makeYAxisLabels(this.columnHeight)} </View> <View style={[styles.yAxis, { alignItems: 'center', justifyContent: 'center' }]}> <DripHomeIcon name="circle" size={styles.yAxis.width - 7} color={cycleDayColor} /> <AppText style={[styles.yAxisLabels.dateLabel]}> {labels.date.toLowerCase()} </AppText> </View> </View>} {this.state.chartHeight && this.state.chartLoaded && makeHorizontalGrid(this.columnHeight, this.symptomRowHeight) } {this.state.chartHeight && <FlatList horizontal={true} inverted={true} showsHorizontalScrollIndicator={false} data={this.state.columns} renderItem={this.renderColumn} keyExtractor={item => item} initialNumToRender={15} windowSize={30} onLayout={() => this.setState({chartLoaded: true})} onEndReached={() => this.setState({end: true})} ListFooterComponent={<LoadingMoreView end={this.state.end}/>} updateCellsBatchingPeriod={800} /> } </View> ) } } function LoadingMoreView(props) { return ( <View style={styles.loadingMore}> {!props.end && <ActivityIndicator size={'large'} color={'white'}/> } </View> ) } function getTodayAndPreviousDays(n) { const today = LocalDate.now() const targetDate = today.minusDays(n) function getDaysInRange(currDate, range) { if (currDate.isBefore(targetDate)) { return range } else { range.push(currDate) const next = currDate.minusDays(1) return getDaysInRange(next, range) } } return getDaysInRange(today, []) }