diff --git a/components/chart/chart.js b/components/chart/chart.js index 449a120662303a4a72457d206e63e153275bf0dd..9393407bb2e66d1cd19e11ff1898a85e7b88e61d 100644 --- a/components/chart/chart.js +++ b/components/chart/chart.js @@ -1,17 +1,29 @@ import React, { Component } from 'react' -import { Text, View, FlatList, ScrollView } from 'react-native' +import { View, FlatList } from 'react-native' import range from 'date-range' import { LocalDate } from 'js-joda' -import Icon from 'react-native-vector-icons/Entypo' -import { getCycleDay, getOrCreateCycleDay, cycleDaysSortedByDate } from '../../db' -import cycleModule from '../../lib/cycle' +import { yAxis, normalizeToScale } from './y-axis' +import DayColumn from './day-column' +import { getCycleDay, cycleDaysSortedByDate } from '../../db' import styles from './styles' import config from './config' -import { getCycleStatusForDay } from '../../lib/sympto-adapter' -const getCycleDayNumber = cycleModule().getCycleDayNumber +const yAxisView = <View {...styles.yAxis}>{yAxis.labels}</View> -const yAxis = makeYAxis(config) +function getInfoForNeighborColumns(index, cols) { + const ret = {} + const right = index > 0 ? cols[index - 1] : undefined + const left = index < cols.length - 1 ? cols[index + 1] : undefined + if (right && right.y) { + ret.rightY = right.y + ret.rightTemperatureExclude = right.temperatureExclude + } + if (left && left.y) { + ret.leftY = left.y + ret.leftTemperatureExclude = left.temperatureExclude + } + return ret +} export default class CycleChart extends Component { constructor(props) { @@ -19,6 +31,16 @@ export default class CycleChart extends Component { this.state = { columns: makeColumnInfo(config.xAxisRangeInDays) } + this.renderColumn = ({item, index}) => { + return ( + <DayColumn + item={item} + index={index} + navigate={this.props.navigation.navigate} + {...getInfoForNeighborColumns(index, this.state.columns)} + /> + ) + } this.reCalculateChartInfo = (function(Chart) { return function() { @@ -33,186 +55,22 @@ export default class CycleChart extends Component { cycleDaysSortedByDate.removeListener(this.reCalculateChartInfo) } - passDateToDayView(dateString) { - const cycleDay = getOrCreateCycleDay(dateString) - this.props.navigation.navigate('cycleDay', { cycleDay }) - } - - placeHorizontalGrid() { - } - - makeDayColumn({ dateString, cycleDay, y }, index) { - const cycleDayNumber = getCycleDayNumber(dateString) - const label = styles.column.label - const dateText = dateString.split('-').slice(1).join('-') - const getFhmAndLtlInfo = setUpFertilityStatusFunc() - const nfpLineInfo = getFhmAndLtlInfo(dateString, cycleDay) - - const horizontalGrid = yAxis.labels.map((_, i) => { - return React.createElement( - View, - { - style: Object.assign( - {}, - styles.horizontalGrid, - { marginTop: yAxis.tickDistance } - ), - key: i.toString() - } - ) - }) - //TODO move these so they are visible - const cycleDayLabel = ( - <Text {...label.number} y={config.cycleDayNumberRowY}> - {cycleDayNumber} - </Text>) - const dateLabel = ( - <Text {...label.date} y={config.dateRowY}> - {dateText} - </Text> - ) - const columnElements = [] - if (cycleDay && cycleDay.bleeding) { - console.log('ever?') - columnElements.push( - <Icon - name='drop' - position='absolute' - top = {10} - left = {20} - size={30} - color='#900' - style={{ marginTop: 20 }} - /> - ) - } - columnElements.push(...[horizontalGrid, cycleDayLabel, dateLabel]) - // {nfpLineInfo.drawFhmLine ? - // <Line - // x1={0 + styles.nfpLine.strokeWidth / 2} - // y1="20" - // x2={0 + styles.nfpLine.strokeWidth / 2} - // y2={config.chartHeight - 20} - // {...styles.nfpLine} - // /> : null} - // />) - - // onPress: () => this.passDateToDayView(dateString), - - // <Path {...styles.bleedingIcon} - // d="M15 3 - // Q16.5 6.8 25 18 - // A12.8 12.8 0 1 1 5 18 - // Q13.5 6.8 15 3z" /> - // : null} - - // {nfpLineInfo.drawLtlAt ? - // <Line - // x1="0" - // y1={nfpLineInfo.drawLtlAt} - // x2={config.columnWidth} - // y2={nfpLineInfo.drawLtlAt} - // {...styles.nfpLine} - // /> : null} - - if (y) { - columnElements.push(this.drawDotAndLines(y, cycleDay.temperature.exclude, index)) - } - // {cycleDay && cycleDay.mucus ? - // <Circle - // {...styles.mucusIcon} - // fill={styles.mucusIconShades[cycleDay.mucus.value]} - // /> : null} - - // {y ? - // this.drawDotAndLines(y, cycleDay.temperature.exclude, index) - // : null} */} - - return React.createElement( - View, - { - style: styles.column.rect, - key: index.toString() - }, - columnElements - ) - } - - drawDotAndLines(currY, exclude, index) { - /* <View - width='150%' - borderStyle = 'solid' - borderColor = 'red' - borderWidth = {1} - position = 'absolute' - top={200} - style={{ - transform: [{rotateZ: '30deg'}] - }} - /> - ) */ - let lineToRight - let lineToLeft - const cols = this.state.columns - - function makeLine(otherColY, x, excludeLine) { - const middleY = ((otherColY - currY) / 2) + currY - const target = [x, middleY] - const lineStyle = excludeLine ? styles.curveExcluded : styles.curve - - return <Line - x1={config.columnMiddle} - y1={currY} - x2={target[0]} - y2={target[1]} - {...lineStyle} - /> - } - - const thereIsADotToTheRight = index > 0 && cols[index - 1].y - const thereIsADotToTheLeft = index < cols.length - 1 && cols[index + 1].y - - /* if (thereIsADotToTheRight) { - const otherDot = cols[index - 1] - const excludedLine = otherDot.cycleDay.temperature.exclude || exclude - lineToRight = makeLine(otherDot.y, config.columnWidth, excludedLine) - } - if (thereIsADotToTheLeft) { - const otherDot = cols[index + 1] - const excludedLine = otherDot.cycleDay.temperature.exclude || exclude - lineToLeft = makeLine(otherDot.y, 0, excludedLine) - } */ - - const dotStyle = exclude ? styles.curveDotsExcluded : styles.curveDots - return [ - /* {lineToRight} - {lineToLeft} */ - <View - position='absolute' - left={config.columnMiddle - dotStyle.width / 2} - top={currY - dotStyle.width / 2} - style = {dotStyle} - key='0' - /> - ] - } - render() { return ( - <ScrollView contentContainerStyle={{flexDirection: 'row'}}> - <View {...styles.yAxis}>{yAxis.labels}</View> + <View style={{ flexDirection: 'row' }}> + {yAxisView} <FlatList horizontal={true} inverted={true} showsHorizontalScrollIndicator={false} data={this.state.columns} - renderItem={({ item, index }) => { - return this.makeDayColumn(item, index) - }} + renderItem={this.renderColumn} keyExtractor={item => item.dateString} - initialNumToRender={20} - /> - </ScrollView> + initialNumToRender={15} + maxToRenderPerBatch={5} + > + </FlatList> + </View> ) } } @@ -228,11 +86,16 @@ function makeColumnInfo(n) { return xAxisDates.map(dateString => { const cycleDay = getCycleDay(dateString) - const temp = cycleDay && cycleDay.temperature && cycleDay.temperature.value + const symptoms = ['temperature', 'mucus', 'bleeding'].reduce((acc, symptom) => { + acc[symptom] = cycleDay && cycleDay[symptom] && cycleDay[symptom].value + acc[`${symptom}Exclude`] = cycleDay && cycleDay[symptom] && cycleDay[symptom].exclude + return acc + }, {}) + return { dateString, - cycleDay, - y: temp ? normalizeToScale(temp) : null + y: symptoms.temperature ? normalizeToScale(symptoms.temperature) : null, + ...symptoms } }) } @@ -246,114 +109,4 @@ function getPreviousDays(n) { const earlierDate = new Date(today - (range.DAY * n)) return range(earlierDate, today).reverse() -} - -function normalizeToScale(temp) { - const scale = config.temperatureScale - const valueRelativeToScale = (scale.high - temp) / (scale.high - scale.low) - const scaleHeight = config.chartHeight - return scaleHeight * valueRelativeToScale -} - -function makeYAxis() { - const scaleMin = config.temperatureScale.low - const scaleMax = config.temperatureScale.high - const numberOfTicks = (scaleMax - scaleMin) * 2 - const tickDistance = config.chartHeight / numberOfTicks - - const labels = [] - // for style reasons, we don't want the first and last tick - for (let i = 1; i < numberOfTicks - 1; i++) { - const y = tickDistance * i - const style = styles.yAxisLabel - // this eyeballing is sadly necessary because RN does not - // support percentage values for transforms, which we'd need - // to reliably place the label vertically centered to the grid - style.top = y - 8 - labels.push( - <Text - style={{...style}} - key={i}> - {scaleMax - i * 0.5} - </Text> - ) - } - - return {labels, tickDistance} -} - -function setUpFertilityStatusFunc() { - let cycleStatus - let cycleStartDate - let noMoreCycles = false - - function updateCurrentCycle(dateString) { - cycleStatus = getCycleStatusForDay(dateString) - if(!cycleStatus) { - noMoreCycles = true - return - } - if (cycleStatus.phases.preOvulatory) { - cycleStartDate = cycleStatus.phases.preOvulatory.start.date - } else { - cycleStartDate = cycleStatus.phases.periOvulatory.start.date - } - } - - function dateIsInPeriOrPostPhase(dateString) { - return ( - dateString >= cycleStatus.phases.periOvulatory.start.date - ) - } - - function precededByAnotherTempValue(dateString) { - return ( - // we are only interested in days that have a preceding - // temp - Object.keys(cycleStatus.phases).some(phaseName => { - return cycleStatus.phases[phaseName].cycleDays.some(day => { - return day.temperature && day.date < dateString - }) - }) - // and also a following temp, so we don't draw the line - // longer than necessary - && - cycleStatus.phases.postOvulatory.cycleDays.some(day => { - return day.temperature && day.date > dateString - }) - ) - } - - function isInTempMeasuringPhase(cycleDay, dateString) { - return ( - cycleDay && cycleDay.temperature - || precededByAnotherTempValue(dateString) - ) - } - - return function(dateString, cycleDay) { - const ret = {} - if (!cycleStatus && !noMoreCycles) updateCurrentCycle(dateString) - if (noMoreCycles) return ret - - if (dateString < cycleStartDate) updateCurrentCycle(dateString) - if (noMoreCycles) return ret - - const tempShift = cycleStatus.temperatureShift - - if (tempShift) { - if (tempShift.firstHighMeasurementDay.date === dateString) { - ret.drawFhmLine = true - } - - if ( - dateIsInPeriOrPostPhase(dateString) && - isInTempMeasuringPhase(cycleDay, dateString) - ) { - ret.drawLtlAt = normalizeToScale(tempShift.ltl) - } - } - - return ret - } } \ No newline at end of file diff --git a/components/chart/config.js b/components/chart/config.js index d36413adafc45a7f20374b0790cc71203913f853..e9d5f95e3060a007af9146a04d5ba0b0f7b0cad7 100644 --- a/components/chart/config.js +++ b/components/chart/config.js @@ -2,10 +2,10 @@ const config = { chartHeight: 350, columnWidth: 30, temperatureScale: { - low: 33, + low: 35, high: 40 }, - xAxisRangeInDays: 30 + xAxisRangeInDays: 50 } const margin = 3 diff --git a/components/chart/day-column.js b/components/chart/day-column.js new file mode 100644 index 0000000000000000000000000000000000000000..cdcb015c4af335d5a124502800cc1ddf2403395d --- /dev/null +++ b/components/chart/day-column.js @@ -0,0 +1,175 @@ +import React, { Component } from 'react' +import { + Text, View +} from 'react-native' +import Icon from 'react-native-vector-icons/Entypo' +import styles from './styles' +import config from './config' +import { getOrCreateCycleDay } from '../../db' +import cycleModule from '../../lib/cycle' +import setUpFertilityStatusFunc from './nfp-lines' +import { horizontalGrid } from './y-axis' +import slowlog from 'react-native-slowlog' + +const getCycleDayNumber = cycleModule().getCycleDayNumber +const label = styles.column.label +const getFhmAndLtlInfo = setUpFertilityStatusFunc() + +export default class DayColumn extends Component { + constructor(props) { + super(props) + slowlog(this, /.*/, {threshold: 30}) + } + makeDayColumn(data, index) { + const { + dateString, + y, + temperature, + temperatureExclude, + bleeding, + mucus + } = data + const cycleDayNumber = getCycleDayNumber(dateString) + const shortDate = dateString.split('-').slice(1).join('-') + const nfpLineInfo = getFhmAndLtlInfo(dateString, temperature) + + //TODO move these so they are visible + const cycleDayLabel = ( + <Text {...label.number} y={config.cycleDayNumberRowY}> + {cycleDayNumber} + </Text>) + const dateLabel = ( + <Text {...label.date} y={config.dateRowY}> + {shortDate} + </Text> + ) + const columnElements = [] + if (bleeding) { + columnElements.push( + <Icon + name='drop' + position='absolute' + top = {10} + left = {20} + size={30} + color='#900' + style={{ marginTop: 20 }} + /> + ) + } + columnElements.push(...[horizontalGrid, cycleDayLabel, dateLabel]) + // {nfpLineInfo.drawFhmLine ? + // <Line + // x1={0 + styles.nfpLine.strokeWidth / 2} + // y1="20" + // x2={0 + styles.nfpLine.strokeWidth / 2} + // y2={config.chartHeight - 20} + // {...styles.nfpLine} + // /> : null} + // />) + + // onPress: () => this.passDateToDayView(dateString), + + // <Path {...styles.bleedingIcon} + // d="M15 3 + // Q16.5 6.8 25 18 + // A12.8 12.8 0 1 1 5 18 + // Q13.5 6.8 15 3z" /> + // : null} + + // {nfpLineInfo.drawLtlAt ? + // <Line + // x1="0" + // y1={nfpLineInfo.drawLtlAt} + // x2={config.columnWidth} + // y2={nfpLineInfo.drawLtlAt} + // {...styles.nfpLine} + // /> : null} + + if (y) { + columnElements.push(this.drawDotAndLines(y, temperatureExclude, index)) + } + // {cycleDay && cycleDay.mucus ? + // <Circle + // {...styles.mucusIcon} + // fill={styles.mucusIconShades[cycleDay.mucus.value]} + // /> : null} + + // {y ? + // this.drawDotAndLines(y, cycleDay.temperature.exclude, index) + // : null} */} + + return React.createElement( + View, + { + style: styles.column.rect, + key: index.toString() + }, + columnElements + ) + } + + drawDotAndLines(currY, exclude) { + /* <View + width='150%' + borderStyle = 'solid' + borderColor = 'red' + borderWidth = {1} + position = 'absolute' + top={200} + style={{ + transform: [{rotateZ: '30deg'}] + }} + /> + ) */ + let lineToRight + let lineToLeft + + /* function makeLine(otherColY, x, excludeLine) { + const middleY = ((otherColY - currY) / 2) + currY + const target = [x, middleY] + const lineStyle = excludeLine ? styles.curveExcluded : styles.curve + + return <Line + x1={config.columnMiddle} + y1={currY} + x2={target[0]} + y2={target[1]} + {...lineStyle} + /> + } */ + + /* if (this.props.rightY) { + const excludedLine = this.props.rightTemperatureExclude || exclude + lineToRight = makeLine(this.props.rightY, config.columnWidth, excludedLine) + } + if (this.props.leftY) { + const excludedLine = this.props.leftTemperatureExclude || exclude + lineToLeft = makeLine(this.props.leftY, 0, excludedLine) + } */ + + const dotStyle = exclude ? styles.curveDotsExcluded : styles.curveDots +/* return (<G> + {lineToRight} + {lineToLeft} + <Circle + cx={config.columnMiddle} + cy={currY} + {...dotStyle} + /> + </G>) */ + } + + passDateToDayView(dateString) { + const cycleDay = getOrCreateCycleDay(dateString) + this.props.navigate('cycleDay', { cycleDay }) + } + + shouldComponentUpdate(newProps) { + return Object.keys(newProps).some(key => newProps[key] != this.props[key]) + } + + render() { + return this.makeDayColumn(this.props.item, this.props.index) + } +} \ No newline at end of file diff --git a/components/chart/nfp-lines.js b/components/chart/nfp-lines.js new file mode 100644 index 0000000000000000000000000000000000000000..e5265acdbe46ed3683ff5c5829568bf5638bc0c4 --- /dev/null +++ b/components/chart/nfp-lines.js @@ -0,0 +1,77 @@ +import { getCycleStatusForDay } from '../../lib/sympto-adapter' +import { normalizeToScale } from './y-axis' + +export default function () { + const cycle = { + status: null + } + + function updateCurrentCycle(dateString) { + cycle.status = getCycleStatusForDay(dateString) + if(!cycle.status) { + cycle.noMoreCycles = true + return + } + if (cycle.status.phases.preOvulatory) { + cycle.startDate = cycle.status.phases.preOvulatory.start.date + } else { + cycle.startDate = cycle.status.phases.periOvulatory.start.date + } + } + + function dateIsInPeriOrPostPhase(dateString) { + return ( + dateString >= cycle.status.phases.periOvulatory.start.date + ) + } + + function precededByAnotherTempValue(dateString) { + return ( + // we are only interested in days that have a preceding + // temp + Object.keys(cycle.status.phases).some(phaseName => { + return cycle.status.phases[phaseName].cycleDays.some(day => { + return day.temperature && day.date < dateString + }) + }) + // and also a following temp, so we don't draw the line + // longer than necessary + && + cycle.status.phases.postOvulatory.cycleDays.some(day => { + return day.temperature && day.date > dateString + }) + ) + } + + function isInTempMeasuringPhase(temperature, dateString) { + return ( + temperature || precededByAnotherTempValue(dateString) + ) + } + + return function(dateString, temperature) { + const ret = {} + if (!cycle.status && !cycle.noMoreCycles) updateCurrentCycle(dateString) + if (cycle.noMoreCycles) return ret + + if (dateString < cycle.startDate) updateCurrentCycle(dateString) + if (cycle.noMoreCycles) return ret + + const tempShift = cycle.status.temperatureShift + + if (tempShift) { + if (tempShift.firstHighMeasurementDay.date === dateString) { + ret.drawFhmLine = true + } + + if ( + dateIsInPeriOrPostPhase(dateString) && + isInTempMeasuringPhase(temperature, dateString) + ) { + ret.drawLtlAt = normalizeToScale(tempShift.ltl) + } + } + + return ret + } +} \ No newline at end of file diff --git a/components/chart/y-axis.js b/components/chart/y-axis.js new file mode 100644 index 0000000000000000000000000000000000000000..e90dcac9af64c6236fb1553477240dd870a14b3c --- /dev/null +++ b/components/chart/y-axis.js @@ -0,0 +1,54 @@ +import React from 'react' +import { Text, View } from 'react-native' +import config from './config' +import styles from './styles' + +function makeYAxis() { + const scaleMin = config.temperatureScale.low + const scaleMax = config.temperatureScale.high + const numberOfTicks = (scaleMax - scaleMin) * 2 + const tickDistance = config.chartHeight / numberOfTicks + + const tickPositions = [] + const labels = [] + // for style reasons, we don't want the first and last tick + for (let i = 1; i < numberOfTicks - 1; i++) { + const y = tickDistance * i + const style = styles.yAxisLabel + // this eyeballing is sadly necessary because RN does not + // support percentage values for transforms, which we'd need + // to reliably place the label vertically centered to the grid + style.top = y - 8 + labels.push( + <Text + style={{...style}} + key={i}> + {scaleMax - i * 0.5} + </Text> + ) + tickPositions.push(y) + } + + return {labels, tickPositions} +} + +export const yAxis = makeYAxis() + +export const horizontalGrid = yAxis.tickPositions.map(tick => { + return ( + <View + position='absolute' + width='100%' + top={tick} + {...styles.horizontalGrid} + key={tick} + /> + ) +}) + +export function normalizeToScale(temp) { + const scale = config.temperatureScale + const valueRelativeToScale = (scale.high - temp) / (scale.high - scale.low) + const scaleHeight = config.chartHeight + return scaleHeight * valueRelativeToScale +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 86dcf0073f06b33f99e247ebd19f22b8e339005e..b0cdbd301e71bc399aacc11e595c7e9856d894d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6426,6 +6426,11 @@ "resolved": "https://registry.npmjs.org/react-native-simple-radio-button/-/react-native-simple-radio-button-2.7.2.tgz", "integrity": "sha512-BdlllHsC/gYJtxPJ2tshDWN8CzmlGg1G9uB+Lu4FRGvGkwhvMtJ/uNShMbvxu134xosH/feri6HQgLGlIT202Q==" }, + "react-native-slowlog": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/react-native-slowlog/-/react-native-slowlog-1.0.2.tgz", + "integrity": "sha1-VSCXnj751Sc0ldQx/zvjTwLjXIk=" + }, "react-native-tab-view": { "version": "0.0.77", "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-0.0.77.tgz", diff --git a/package.json b/package.json index 58f26381f17f6a93962d6b0ef88a1ae8fd9050e0..cf36e5014cd675cbfb518858df7ddfb6db215c5d 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "react-native-share": "^1.1.0", "react-native-simple-radio-button": "^2.7.1", "react-native-vector-icons": "^5.0.0", + "react-native-slowlog": "^1.0.2", "react-navigation": "^2.0.4", "realm": "^2.7.1", "uuid": "^3.2.1"