diff --git a/components/chart/chart-line.js b/components/chart/chart-line.js new file mode 100644 index 0000000000000000000000000000000000000000..95300ad3bcb3dce3c59eb308656763f831cc0ff3 --- /dev/null +++ b/components/chart/chart-line.js @@ -0,0 +1,28 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import { Shape } from 'react-native/Libraries/ART/ReactNativeART' + +import styles from './styles' + +const ChartLine = ({ path, isNfpLine = false }) => { + const strokeStyle = + isNfpLine ? styles.nfpLine.stroke : styles.column.stroke.color + const strokeWidth = + isNfpLine ? styles.nfpLine.strokeWidth : styles.column.stroke.width + + return ( + <Shape + stroke={strokeStyle} + strokeWidth={strokeWidth} + d={path} + /> + ) +} + +ChartLine.propTypes = { + path: PropTypes.object, + isNfpLine: PropTypes.bool, +} + +export default ChartLine diff --git a/components/chart/chart.js b/components/chart/chart.js index 8f8a3f5d37018b81edc4ccfa9d9c0c1b58f06ce0..95985522d030396da9e0240cbdbdfaff6561d79e 100644 --- a/components/chart/chart.js +++ b/components/chart/chart.js @@ -1,20 +1,20 @@ import React, { Component } from 'react' import { View, FlatList, ActivityIndicator } from 'react-native' -import { LocalDate } from 'js-joda' +import AppLoadingView from '../app-loading' import YAxis from './y-axis' import nfpLines from './nfp-lines' import DayColumn from './day-column' import HorizontalGrid from './horizontal-grid' -import { getCycleDaysSortedByDate, getAmountOfCycleDays } from '../../db' -import styles from './styles' +import { getCycleDaysSortedByDate } from '../../db' +import nothingChanged from '../../db/db-unchanged' import { scaleObservable } from '../../local-storage' -import config from '../../config' +import { makeColumnInfo } from '../helpers/chart' -import AppLoadingView from '../app-loading' +import config from '../../config' -import nothingChanged from '../../db/db-unchanged' +import styles from './styles' export default class CycleChart extends Component { constructor(props) { @@ -32,7 +32,6 @@ export default class CycleChart extends Component { navigate={this.props.navigate} symptomHeight={this.symptomHeight} columnHeight={this.columnHeight} - chartHeight={this.state.chartHeight} symptomRowSymptoms={this.symptomRowSymptoms} chartSymptoms={this.chartSymptoms} getFhmAndLtlInfo={this.getFhmAndLtlInfo} @@ -72,7 +71,7 @@ export default class CycleChart extends Component { this.chartSymptoms.push('temperature') } - const columnData = this.makeColumnInfo() + const columnData = makeColumnInfo() this.setState({ columns: columnData, chartHeight: height @@ -104,43 +103,29 @@ export default class CycleChart extends Component { 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() { const { chartHeight, chartLoaded } = this.state return ( <View onLayout={this.onLayout} - style={{ flexDirection: 'row', flex: 1 }} + style={styles.container} > {!chartLoaded && <AppLoadingView />} {chartHeight && chartLoaded && ( - <YAxis - height={this.columnHeight} - symptomsToDisplay={this.symptomRowSymptoms} - symptomsSectionHeight={this.symptomRowHeight} - /> + <React.Fragment> + <YAxis + height={this.columnHeight} + symptomsToDisplay={this.symptomRowSymptoms} + symptomsSectionHeight={this.symptomRowHeight} + /> + <HorizontalGrid + height={this.columnHeight} + startPosition={this.symptomRowHeight} + /> + </React.Fragment> )} - {chartHeight && chartLoaded && ( - <HorizontalGrid - height={this.columnHeight} - startPosition={this.symptomRowHeight} - />) - } - {chartHeight && <FlatList horizontal={true} @@ -171,20 +156,3 @@ function LoadingMoreView(props) { </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, []) -} diff --git a/components/chart/cycle-day-label.js b/components/chart/cycle-day-label.js new file mode 100644 index 0000000000000000000000000000000000000000..38a0d2e50b24620c34446ebd1205e9a1ee4309c6 --- /dev/null +++ b/components/chart/cycle-day-label.js @@ -0,0 +1,39 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import { Text, View } from 'react-native' + +import moment from 'moment' +import { LocalDate } from 'js-joda' + +import styles from './styles' +import cycleModule from '../../lib/cycle' + +const CycleDayLabel = ({ height, date }) => { + const { label } = styles.column + const dayDate = LocalDate.parse(date) + const cycleDayNumber = cycleModule().getCycleDayNumber(date) + + const isFirstDayOfMonth = dayDate.dayOfMonth() === 1 + const dateFormatting = isFirstDayOfMonth ? 'MMM' : 'Do' + const shortDate = moment(date, "YYYY-MM-DD").format(dateFormatting) + const boldDateLabel = isFirstDayOfMonth ? {fontWeight: 'bold'} : {} + + return ( + <View style={{ height }}> + <Text style={label.number}> + {cycleDayNumber ? cycleDayNumber : ' '} + </Text> + <Text style={[label.date, boldDateLabel]}> + {shortDate} + </Text> + </View> + ) +} + +CycleDayLabel.propTypes = { + height: PropTypes.number, + date: PropTypes.string, +} + +export default CycleDayLabel diff --git a/components/chart/day-column.js b/components/chart/day-column.js index 1597697fc14e5d042f0a2dfb9ea27b4199907277..633f6364c9dc280bab9ff023a25fdb2acb1326b4 100644 --- a/components/chart/day-column.js +++ b/components/chart/day-column.js @@ -1,30 +1,20 @@ import React, { Component } from 'react' -import { - Text, View, TouchableOpacity -} from 'react-native' -import { - Surface, - Group as G, - Path, - Shape -} from 'react-native/Libraries/ART/ReactNativeART' +import { TouchableOpacity } from 'react-native' import { connect } from 'react-redux' import { setDate } from '../../slices/date' -import { LocalDate } from 'js-joda' -import moment from 'moment' -import styles from './styles' -import config from '../../config' -import cycleModule from '../../lib/cycle' import { getCycleDay } from '../../db' -import DotAndLine from './dot-and-line' import SymptomCell from './symptom-cell' +import TemperatureColumn from './temperature-column' +import CycleDayLabel from './cycle-day-label' -import { normalizeToScale } from '../helpers/chart' - -const label = styles.column.label +import { + symptomColorMethods, + getTemperatureProps, + isSymptomDataComplete +} from '../helpers/chart' class DayColumn extends Component { constructor(props) { @@ -40,14 +30,13 @@ class DayColumn extends Component { if (symptomData && symptom === 'temperature') { symptomDataToDisplay[symptom] = - this.getTemperatureProps(symptomData, columnHeight, dateString) + getTemperatureProps(symptomData, columnHeight, dateString) } else { if (symptomData && ! symptomData.exclude) { // if symptomColorMethods entry doesn't exist for given symptom, // use 'default' const getSymptomColorIndex = - this.symptomColorMethods[symptom] || - this.symptomColorMethods['default'] + symptomColorMethods[symptom] || symptomColorMethods['default'] symptomDataToDisplay[symptom] = getSymptomColorIndex(symptomData) } @@ -64,75 +53,6 @@ class DayColumn extends Component { ) } - getTemperatureProps = (symptomData, columnHeight, dateString) => { - const extractedData = {} - const { value, exclude } = symptomData - const neighborTemperatureGraphPoints = - getInfoForNeighborColumns(dateString, columnHeight) - - for (const key in neighborTemperatureGraphPoints) { - extractedData[key] = neighborTemperatureGraphPoints[key] - } - return Object.assign({ - value, - y: normalizeToScale(value, columnHeight), - temperatureExclude: exclude, - }, extractedData) - } - - symptomColorMethods = { - 'mucus': (symptomData) => { - const { feeling, texture } = symptomData - const colorIndex = feeling + texture - return colorIndex - }, - 'cervix': (symptomData) => { - const { opening, firmness } = symptomData - const isDataComplete = opening !== null && firmness !== null - const isClosedAndHard = - isDataComplete && - (opening === 0 && firmness === 0) - const colorIndex = isClosedAndHard ? 0 : 2 - return colorIndex - }, - 'sex': (symptomData) => { - const { solo, partner } = symptomData - const colorIndex = (solo !== null && partner !== null) ? - (solo + 2 * partner - 1) : 0 - return colorIndex - }, - 'bleeding': (symptomData) => { - const { value } = symptomData - const colorIndex = value - return colorIndex - }, - 'default': () => { // desire, pain, mood, note - const colorIndex = 0 - return colorIndex - } - } - - isSymptomDataComplete = (symptom) => { - const { dateString } = this.props - const cycleDayData = getCycleDay(dateString) - const symptomData = cycleDayData[symptom] - - const dataCompletenessCheck = { - 'cervix': () => { - const { opening, firmness } = symptomData - return (opening !== null) && (firmness !== null) - }, - 'mucus': () => { - const { feeling, texture } = symptomData - return (feeling !== null) && (texture !== null) - }, - 'default': () => { - return true - } - } - return (dataCompletenessCheck[symptom] || dataCompletenessCheck['default'])() - } - onDaySelect = (date) => { this.props.setDate(date) this.props.navigate('CycleDay') @@ -143,89 +63,11 @@ class DayColumn extends Component { } render() { - const columnElements = [] const { dateString, symptomRowSymptoms, - chartHeight, columnHeight, xAxisHeight } = this.props - if(this.fhmAndLtl.drawLtlAt) { - const ltlLine = (<Shape - stroke={styles.nfpLine.stroke} - strokeWidth={styles.nfpLine.strokeWidth} - d={new Path() - .moveTo(0, this.fhmAndLtl.drawLtlAt) - .lineTo(config.columnWidth, this.fhmAndLtl.drawLtlAt) - } - key='ltl' - />) - columnElements.push(ltlLine) - } - - if (this.fhmAndLtl.drawFhmLine) { - const x = styles.nfpLine.strokeWidth / 2 - const fhmLine = (<Shape - fill="red" - stroke={styles.nfpLine.stroke} - strokeWidth={styles.nfpLine.strokeWidth} - d={new Path().moveTo(x, x).lineTo(x, columnHeight)} - key='fhm' - />) - columnElements.push(fhmLine) - } - - if (this.data && this.data.temperature && this.data.temperature.y) { - const { temperatureExclude, - y, - rightY, - leftY, - rightTemperatureExclude, - leftTemperatureExclude - } = this.data.temperature - - columnElements.push( - <DotAndLine - y={y} - exclude={temperatureExclude} - rightY={rightY} - rightTemperatureExclude={rightTemperatureExclude} - leftY={leftY} - leftTemperatureExclude={leftTemperatureExclude} - key='dotandline' - /> - ) - } - - const cycleDayNumber = cycleModule().getCycleDayNumber(dateString) - const dayDate = LocalDate.parse(dateString) - const shortDate = dayDate.dayOfMonth() === 1 ? - moment(dateString, "YYYY-MM-DD").format('MMM') - : - moment(dateString, "YYYY-MM-DD").format('Do') - const boldDateLabel = dayDate.dayOfMonth() === 1 ? {fontWeight: 'bold'} : {} - - const cycleDayLabel = ( - <Text style = {label.number}> - {cycleDayNumber ? cycleDayNumber : ' '} - </Text>) - const dateLabel = ( - <Text style = {[label.date, boldDateLabel]}> - {shortDate} - </Text> - ) - - const column = ( - <G> - <Shape - stroke={styles.column.stroke.color} - strokeWidth={styles.column.stroke.width} - d={new Path().lineTo(0, chartHeight)} - /> - { columnElements } - </G> - ) - return ( <TouchableOpacity onPress={() => this.onDaySelect(dateString)} @@ -240,21 +82,25 @@ class DayColumn extends Component { symptom={symptom} symptomValue={hasSymptomData && this.data[symptom]} isSymptomDataComplete={ - hasSymptomData && this.isSymptomDataComplete(symptom) + hasSymptomData && isSymptomDataComplete(symptom, dateString) } height={this.props.symptomHeight} />) } )} - <Surface width={config.columnWidth} height={columnHeight}> - {column} - </Surface> + <TemperatureColumn + horizontalLinePosition={this.fhmAndLtl.drawLtlAt} + isVerticalLine={this.fhmAndLtl.drawFhmLine} + data={this.data && this.data.temperature} + columnHeight={columnHeight} + /> + + <CycleDayLabel + height={xAxisHeight} + date={dateString} + /> - <View style={{height: xAxisHeight}}> - {cycleDayLabel} - {dateLabel} - </View> </TouchableOpacity> ) } @@ -270,28 +116,3 @@ export default connect( null, mapDispatchToProps, )(DayColumn) - - -function getInfoForNeighborColumns(dateString, columnHeight) { - const ret = { - rightY: null, - rightTemperatureExclude: null, - leftY: null, - leftTemperatureExclude: null - } - const target = LocalDate.parse(dateString) - const dayBefore = target.minusDays(1).toString() - const dayAfter = target.plusDays(1).toString() - const cycleDayBefore = getCycleDay(dayBefore) - const cycleDayAfter = getCycleDay(dayAfter) - if (cycleDayAfter && cycleDayAfter.temperature) { - ret.rightY = normalizeToScale(cycleDayAfter.temperature.value, columnHeight) - ret.rightTemperatureExclude = cycleDayAfter.temperature.exclude - } - if (cycleDayBefore && cycleDayBefore.temperature) { - ret.leftY = normalizeToScale(cycleDayBefore.temperature.value, columnHeight) - ret.leftTemperatureExclude = cycleDayBefore.temperature.exclude - } - - return ret -} diff --git a/components/chart/styles.js b/components/chart/styles.js index dc3a69be97429e7cdf93b590ca0c01a28003e234..5580af01c3ec70e3c9a4f360393391fd98dadfad 100644 --- a/components/chart/styles.js +++ b/components/chart/styles.js @@ -25,6 +25,10 @@ const orangeColor = '#bc6642' const mintColor = '#6ca299' const styles = { + container: { + flexDirection: 'row', + flex: 1, + }, curve: { stroke: colorTemperature, strokeWidth: lineWidth, diff --git a/components/chart/temperature-column.js b/components/chart/temperature-column.js new file mode 100644 index 0000000000000000000000000000000000000000..fab3d976970198ee2e66c9af2d15a6000c4c32bf --- /dev/null +++ b/components/chart/temperature-column.js @@ -0,0 +1,64 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import { Surface , Path } from 'react-native/Libraries/ART/ReactNativeART' + +import ChartLine from './chart-line' +import DotAndLine from './dot-and-line' + +import styles from './styles' +import config from '../../config' + +const TemperatureColumn = ({ + horizontalLinePosition, + isVerticalLine, + data, + columnHeight +}) => { + + const x = styles.nfpLine.strokeWidth / 2 + + return ( + <Surface width={config.columnWidth} height={columnHeight}> + + <ChartLine + path={new Path().lineTo(0, columnHeight)} + /> + + {horizontalLinePosition && <ChartLine + path={new Path() + .moveTo(0, horizontalLinePosition) + .lineTo(config.columnWidth, horizontalLinePosition) + } + isNfpLine={true} + key='ltl' + />} + + {isVerticalLine && <ChartLine + path={new Path().moveTo(x, x).lineTo(x, columnHeight)} + isNfpLine={true} + key='fhm' + />} + + {data && data.y && <DotAndLine + y={data.y} + exclude={data.temperatureExclude} + rightY={data.rightY} + rightTemperatureExclude={data.rightTemperatureExclude} + leftY={data.leftY} + leftTemperatureExclude={data.leftTemperatureExclude} + key='dotandline' + />} + + </Surface> + ) +} + +TemperatureColumn.propTypes = { + horizontalLinePosition: PropTypes.number, + isVerticalLine: PropTypes.bool, + data: PropTypes.object, + columnHeight: PropTypes.number, +} + +export default TemperatureColumn diff --git a/components/helpers/chart.js b/components/helpers/chart.js index 24a563cb1ab291bd0606a9b1ff813293f2d12fde..d59a55a63086cf320ea1d9f1f33f3845528b9f0c 100644 --- a/components/helpers/chart.js +++ b/components/helpers/chart.js @@ -1,6 +1,12 @@ +import { LocalDate } from 'js-joda' + import { scaleObservable, unitObservable } from '../../local-storage' +import { getCycleDay, getAmountOfCycleDays } from '../../db' + import config from '../../config' +//YAxis helpers + export function normalizeToScale(temp, columnHeight) { const scale = scaleObservable.value const valueRelativeToScale = (scale.max - temp) / (scale.max - scale.min) @@ -65,3 +71,130 @@ export function getTickList(columnHeight) { } }) } + +//DayColumn helpers + +export function isSymptomDataComplete(symptom, dateString) { + const cycleDayData = getCycleDay(dateString) + const symptomData = cycleDayData[symptom] + + const dataCompletenessCheck = { + 'cervix': () => { + const { opening, firmness } = symptomData + return (opening !== null) && (firmness !== null) + }, + 'mucus': () => { + const { feeling, texture } = symptomData + return (feeling !== null) && (texture !== null) + }, + 'default': () => { + return true + } + } + return (dataCompletenessCheck[symptom] || dataCompletenessCheck['default'])() +} + +function getInfoForNeighborColumns(dateString, columnHeight) { + const ret = { + rightY: null, + rightTemperatureExclude: null, + leftY: null, + leftTemperatureExclude: null + } + const target = LocalDate.parse(dateString) + const dayBefore = target.minusDays(1).toString() + const dayAfter = target.plusDays(1).toString() + const cycleDayBefore = getCycleDay(dayBefore) + const cycleDayAfter = getCycleDay(dayAfter) + + if (cycleDayAfter && cycleDayAfter.temperature) { + ret.rightY = normalizeToScale(cycleDayAfter.temperature.value, columnHeight) + ret.rightTemperatureExclude = cycleDayAfter.temperature.exclude + } + if (cycleDayBefore && cycleDayBefore.temperature) { + ret.leftY = normalizeToScale(cycleDayBefore.temperature.value, columnHeight) + ret.leftTemperatureExclude = cycleDayBefore.temperature.exclude + } + + return ret +} + +export function getTemperatureProps(symptomData, columnHeight, dateString) { + const extractedData = {} + const { value, exclude } = symptomData + const neighborTemperatureGraphPoints = + getInfoForNeighborColumns(dateString, columnHeight) + + for (const key in neighborTemperatureGraphPoints) { + extractedData[key] = neighborTemperatureGraphPoints[key] + } + return Object.assign({ + value, + y: normalizeToScale(value, columnHeight), + temperatureExclude: exclude, + }, extractedData) +} + +export const symptomColorMethods = { + 'mucus': (symptomData) => { + const { feeling, texture } = symptomData + const colorIndex = feeling + texture + return colorIndex + }, + 'cervix': (symptomData) => { + const { opening, firmness } = symptomData + const isDataComplete = opening !== null && firmness !== null + const isClosedAndHard = + isDataComplete && + (opening === 0 && firmness === 0) + const colorIndex = isClosedAndHard ? 0 : 2 + return colorIndex + }, + 'sex': (symptomData) => { + const { solo, partner } = symptomData + const colorIndex = (solo !== null && partner !== null) ? + (solo + 2 * partner - 1) : 0 + return colorIndex + }, + 'bleeding': (symptomData) => { + const { value } = symptomData + const colorIndex = value + return colorIndex + }, + 'default': () => { // desire, pain, mood, note + const colorIndex = 0 + return colorIndex + } +} + +// Chart helpers + +export function 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()) +} + +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, []) +} \ No newline at end of file