Merge branch 'chart-refactoring-part-2' into 'master'

Chart refactoring part 2

See merge request bloodyhealth/drip!255
This commit is contained in:
Julia Friesel
2019-11-22 12:00:49 +00:00
7 changed files with 309 additions and 252 deletions
+28
View File
@@ -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
+19 -51
View File
@@ -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 { makeColumnInfo } from '../helpers/chart'
import config from '../../config'
import AppLoadingView from '../app-loading'
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, [])
}
+39
View File
@@ -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
+22 -201
View File
@@ -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
}
+4
View File
@@ -25,6 +25,10 @@ const orangeColor = '#bc6642'
const mintColor = '#6ca299'
const styles = {
container: {
flexDirection: 'row',
flex: 1,
},
curve: {
stroke: colorTemperature,
strokeWidth: lineWidth,
+64
View File
@@ -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
+133
View File
@@ -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, [])
}