Skip to content
Snippets Groups Projects
Commit 0879d4ae authored by Julia Friesel's avatar Julia Friesel
Browse files

Merge branch '82-chart-improvements' into 'master'

Resolve "chart improvements"

Closes #82

See merge request bloodyhealth/drip!52
parents 2883aaa8 af8c2c14
No related branches found
No related tags found
No related merge requests found
Showing
with 215 additions and 282 deletions
......@@ -77,6 +77,7 @@ project.ext.react = [
]
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
/**
* Set this to true to create two separate APKs instead of one:
......@@ -137,11 +138,11 @@ android {
}
dependencies {
compile project(':react-native-vector-icons')
compile project(':react-native-fs')
compile project(':react-native-document-picker')
compile project(':react-native-share')
compile project(':realm')
compile project(':react-native-svg')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
compile "com.facebook.react:react-native:+" // From node_modules
......
File added
File added
File added
File added
File added
File added
File added
File added
File added
File added
File added
File added
File added
File added
......@@ -3,12 +3,12 @@ package com.drip;
import android.app.Application;
import com.facebook.react.ReactApplication;
import com.oblador.vectoricons.VectorIconsPackage;
import com.rnfs.RNFSPackage;
import com.reactnativedocumentpicker.ReactNativeDocumentPicker;
import cl.json.RNSharePackage;
import cl.json.ShareApplication;
import io.realm.react.RealmReactPackage;
import com.horcrux.svg.SvgPackage;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
......@@ -29,11 +29,11 @@ public class MainApplication extends Application implements ReactApplication, Sh
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new VectorIconsPackage(),
new RNFSPackage(),
new ReactNativeDocumentPicker(),
new RNSharePackage(),
new RealmReactPackage(),
new SvgPackage()
new RealmReactPackage()
);
}
......
rootProject.name = 'drip'
include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android')
include ':react-native-document-picker'
......@@ -7,7 +9,5 @@ include ':react-native-share'
project(':react-native-share').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-share/android')
include ':realm'
project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android')
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':app'
import React, { Component } from 'react'
import { Text as ReactNativeText, View, FlatList, ScrollView } from 'react-native'
import { View, FlatList } from 'react-native'
import range from 'date-range'
import Svg,{
G,
Rect,
Text,
Circle,
Line,
Path
} from 'react-native-svg'
import { LocalDate } from 'js-joda'
import { getCycleDay, getOrCreateCycleDay, cycleDaysSortedByDate } from '../../db'
import cycleModule from '../../lib/cycle'
import { yAxis, normalizeToScale, horizontalGrid } from './y-axis'
import setUpFertilityStatusFunc from './nfp-lines'
import DayColumn from './day-column'
import { getCycleDay, cycleDaysSortedByDate, getAmountOfCycleDays } from '../../db'
import styles from './styles'
import config from './config'
import { getCycleStatusForDay } from '../../lib/sympto-adapter'
const getCycleDayNumber = cycleModule().getCycleDayNumber
const yAxis = makeYAxis(config)
const yAxisView = <View {...styles.yAxis}>{yAxis.labels}</View>
export default class CycleChart extends Component {
constructor(props) {
super(props)
this.state = {
columns: makeColumnInfo(config.xAxisRangeInDays)
columns: makeColumnInfo(setUpFertilityStatusFunc()),
}
this.renderColumn = ({item, index}) => {
return (
<DayColumn
{...item}
index={index}
navigate={this.props.navigation.navigate}
/>
)
}
this.reCalculateChartInfo = (function(Chart) {
return function() {
Chart.setState({columns: makeColumnInfo(config.xAxisRangeInDays)})
Chart.setState({columns: makeColumnInfo(setUpFertilityStatusFunc())})
}
})(this)
......@@ -40,159 +39,37 @@ export default class CycleChart extends Component {
cycleDaysSortedByDate.removeListener(this.reCalculateChartInfo)
}
passDateToDayView(dateString) {
const cycleDay = getOrCreateCycleDay(dateString)
this.props.navigation.navigate('cycleDay', { cycleDay })
}
placeHorizontalGrid() {
return yAxis.tickPositions.map(tick => {
return (
<Line
x1={0}
y1={tick}
x2={config.columnWidth}
y2={tick}
{...styles.horizontalGrid}
key={tick}
/>
)
})
}
makeDayColumn({ dateString, cycleDay, y }, index) {
const cycleDayNumber = getCycleDayNumber(dateString)
const label = styles.column.label
const dateLabel = dateString.split('-').slice(1).join('-')
const getFhmAndLtlInfo = setUpFertilityStatusFunc()
const nfpLineInfo = getFhmAndLtlInfo(dateString, cycleDay)
return (
<G onPress={() => this.passDateToDayView(dateString)}>
<Rect {...styles.column.rect} />
{nfpLineInfo.drawFhmLine ?
<Line
x1={0 + styles.nfpLine.strokeWidth / 2}
y1="20"
x2={0 + styles.nfpLine.strokeWidth / 2}
y2={config.chartHeight - 20}
{...styles.nfpLine}
/> : null}
{this.placeHorizontalGrid()}
<Text {...label.number} y={config.cycleDayNumberRowY}>
{cycleDayNumber}
</Text>
<Text {...label.date} y={config.dateRowY}>
{dateLabel}
</Text>
{cycleDay && cycleDay.bleeding ?
<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}
{y ?
this.drawDotAndLines(y, cycleDay.temperature.exclude, index)
: null
}
{cycleDay && cycleDay.mucus ?
<Circle
{...styles.mucusIcon}
fill={styles.mucusIconShades[cycleDay.mucus.value]}
/> : null}
{y ?
this.drawDotAndLines(y, cycleDay.temperature.exclude, index)
: null}
</G>
)
}
drawDotAndLines(currY, exclude, index) {
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 (<G>
{lineToRight}
{lineToLeft}
<Circle
cx={config.columnMiddle}
cy={currY}
{...dotStyle}
/>
</G>)
}
render() {
return (
<ScrollView contentContainerStyle={{flexDirection: 'row'}}>
<View {...styles.yAxis}>{yAxis.labels}</View>
<FlatList
<View style={{ flexDirection: 'row', marginTop: 50 }}>
{yAxisView}
{horizontalGrid}
{<FlatList
horizontal={true}
inverted={true}
showsHorizontalScrollIndicator={false}
data={this.state.columns}
renderItem={({ item, index }) => {
return (
<Svg width={config.columnWidth} height={config.chartHeight}>
{this.makeDayColumn(item, index)}
</Svg>
)
}}
renderItem={this.renderColumn}
keyExtractor={item => item.dateString}
initialNumToRender={15}
maxToRenderPerBatch={5}
>
</FlatList>
</ScrollView>
</FlatList>}
</View>
)
}
}
function makeColumnInfo(n) {
const xAxisDates = getPreviousDays(n).map(jsDate => {
function makeColumnInfo(getFhmAndLtlInfo) {
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 xAxisDates = getTodayAndPreviousDays(amountOfCycleDays).map(jsDate => {
return LocalDate.of(
jsDate.getFullYear(),
jsDate.getMonth() + 1,
......@@ -200,18 +77,29 @@ function makeColumnInfo(n) {
).toString()
})
return xAxisDates.map(dateString => {
const columns = 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,
...getFhmAndLtlInfo(dateString, symptoms.temperature)
}
})
return columns.map((col, i) => {
const info = getInfoForNeighborColumns(i, columns)
return Object.assign(col, info)
})
}
function getPreviousDays(n) {
function getTodayAndPreviousDays(n) {
const today = new Date()
today.setHours(0)
today.setMinutes(0)
......@@ -222,114 +110,22 @@ function getPreviousDays(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 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(
<ReactNativeText
style={{...style}}
key={i}>
{scaleMax - i * 0.5}
</ReactNativeText>
)
tickPositions.push(y)
}
return {labels, tickPositions}
}
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 getInfoForNeighborColumns(index, cols) {
const ret = {
rightY: null,
rightTemperatureExclude: null,
leftY: null,
leftTemperatureExclude: null
}
function isInTempMeasuringPhase(cycleDay, dateString) {
return (
cycleDay && cycleDay.temperature
|| precededByAnotherTempValue(dateString)
)
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
}
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
if (left && left.y) {
ret.leftY = left.y
ret.leftTemperatureExclude = left.temperatureExclude
}
return ret
}
\ No newline at end of file
const config = {
chartHeight: 350,
columnWidth: 30,
columnWidth: 25,
temperatureScale: {
low: 33,
high: 40
},
xAxisRangeInDays: 40
low: 35,
high: 38,
units: 0.1
}
}
const margin = 3
......
import React, { Component } from 'react'
import {
Text, View, TouchableOpacity
} 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 DotAndLine from './dot-and-line'
const getCycleDayNumber = cycleModule().getCycleDayNumber
const label = styles.column.label
export default class DayColumn extends Component {
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() {
const {
dateString,
y,
temperatureExclude,
bleeding,
mucus,
drawFhmLine,
drawLtlAt,
rightY,
rightTemperatureExclude,
leftY,
leftTemperatureExclude
} = this.props
const columnElements = []
if (typeof bleeding === 'number') {
columnElements.push(
<Icon
name='drop'
position='absolute'
size={18}
color='#900'
style={{ marginTop: 10, marginLeft: 3 }}
key='bleeding'
/>
)
}
if (typeof mucus === 'number') {
const mucusIcon = (
<View
position='absolute'
top = {40}
left = {config.columnMiddle - styles.mucusIcon.width / 2}
{...styles.mucusIcon}
backgroundColor={styles.mucusIconShades[mucus]}
key='mucus'
/>
)
columnElements.push(mucusIcon)
}
if(drawFhmLine) {
const fhmLine = (<View
position = 'absolute'
top={100}
width={styles.nfpLine.strokeWidth}
height={200}
{...styles.nfpLine}
key='fhm'
/>)
columnElements.push(fhmLine)
}
if(drawLtlAt) {
const ltlLine = (<View
position = 'absolute'
width={'100%'}
top={drawLtlAt}
{...styles.nfpLine}
key='ltl'
/>)
columnElements.push(ltlLine)
}
if (y) {
columnElements.push(
<DotAndLine
y={y}
exclude={temperatureExclude}
rightY={rightY}
rightTemperatureExclude={rightTemperatureExclude}
leftY={leftY}
leftTemperatureExclude={leftTemperatureExclude}
key='dotandline'
/>
)
}
const cycleDayNumber = getCycleDayNumber(dateString)
const shortDate = dateString.split('-').slice(1).join('-')
const cycleDayLabel = (
<Text style={label.number} y={config.cycleDayNumberRowY}>
{cycleDayNumber}
</Text>)
const dateLabel = (
<Text style = {label.date} y={config.dateRowY}>
{shortDate}
</Text>
)
columnElements.push(
<View position='absolute' bottom={0} key='date'>
{cycleDayLabel}
{dateLabel}
</View>
)
return React.createElement(
TouchableOpacity,
{
style: styles.column.rect,
key: this.props.index.toString(),
onPress: () => {
this.passDateToDayView(dateString)
},
activeOpacity: 1
},
columnElements
)
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment