diff --git a/components/chart/chart-legend.js b/components/chart/chart-legend.js new file mode 100644 index 0000000..df6f8a6 --- /dev/null +++ b/components/chart/chart-legend.js @@ -0,0 +1,27 @@ +import React from 'react' +import { View } from 'react-native' + +import AppText from '../app-text' +import DripHomeIcon from '../../assets/drip-home-icons' + +import styles from './styles' +import { cycleDayColor } from '../../styles' + +import { shared as labels } from '../../i18n/en/labels' + +const ChartLegend = () => { + return ( + + + + {labels.date.toLowerCase()} + + + ) +} + +export default ChartLegend diff --git a/components/chart/chart.js b/components/chart/chart.js index 4106938..8f8a3f5 100644 --- a/components/chart/chart.js +++ b/components/chart/chart.js @@ -1,31 +1,20 @@ import React, { Component } from 'react' import { View, FlatList, ActivityIndicator } from 'react-native' import { LocalDate } from 'js-joda' -import { makeYAxisLabels, makeHorizontalGrid } from './y-axis' + +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 { 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: , - mucus: , - cervix: , - desire: , - sex: , - pain: , - mood: , - note: -} +import AppLoadingView from '../app-loading' + +import nothingChanged from '../../db/db-unchanged' export default class CycleChart extends Component { constructor(props) { @@ -129,49 +118,30 @@ export default class CycleChart extends Component { } render() { + const { chartHeight, chartLoaded } = this.state return ( - {!this.state.chartLoaded && } + {!chartLoaded && } - {this.state.chartHeight && this.state.chartLoaded && - - - {this.symptomRowSymptoms.map(symptomName => { - return - {symptomIcons[symptomName]} - - })} - - - {makeYAxisLabels(this.columnHeight)} - - - - - {labels.date.toLowerCase()} - - - } + {chartHeight && chartLoaded && ( + + )} - - {this.state.chartHeight && this.state.chartLoaded && - makeHorizontalGrid(this.columnHeight, this.symptomRowHeight) + {chartHeight && chartLoaded && ( + ) } - {this.state.chartHeight && + {chartHeight && { - - const { symptomHeight } = this.props - const shouldDrawSymptom = this.data.hasOwnProperty(symptom) - const styleParent = [styles.symptomRow, {height: symptomHeight}] - - if (shouldDrawSymptom) { - const styleSymptom = styles.iconShades[symptom] - const symptomData = this.data[symptom] - - const dataIsComplete = this.isSymptomDataComplete(symptom) - const isMucusOrCervix = (symptom === 'mucus') || (symptom === 'cervix') - - const backgroundColor = (isMucusOrCervix && !dataIsComplete) ? - 'white' : styleSymptom[symptomData] - const borderWidth = (isMucusOrCervix && !dataIsComplete) ? 2 : 0 - const borderColor = styleSymptom[0] - const styleChild = [styles.symptomIcon, { - backgroundColor, - borderColor, - borderWidth - }] - - return ( - - - - ) - } else { - return ( - - ) - } - } - render() { const columnElements = [] const { dateString, @@ -263,9 +231,21 @@ class DayColumn extends Component { onPress={() => this.onDaySelect(dateString)} activeOpacity={1} > - - {symptomRowSymptoms.map(symptom => this.drawSymptom(symptom))} - + + { symptomRowSymptoms.map(symptom => { + const hasSymptomData = this.data.hasOwnProperty(symptom) + return ( + ) + } + )} {column} diff --git a/components/chart/horizontal-grid.js b/components/chart/horizontal-grid.js new file mode 100644 index 0000000..e7cf7cc --- /dev/null +++ b/components/chart/horizontal-grid.js @@ -0,0 +1,26 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { View } from 'react-native' + +import { getTickPositions } from '../helpers/chart' + +import styles from './styles' + +const HorizontalGrid = ({ height, startPosition }) => { + return getTickPositions(height).map(tick => { + return ( + + ) + }) +} + +HorizontalGrid.propTypes = { + height: PropTypes.number, + startPosition: PropTypes.number, +} + +export default HorizontalGrid diff --git a/components/chart/nfp-lines.js b/components/chart/nfp-lines.js index cf73c52..853dde5 100644 --- a/components/chart/nfp-lines.js +++ b/components/chart/nfp-lines.js @@ -1,5 +1,5 @@ import { getCycleStatusForDay } from '../../lib/sympto-adapter' -import { normalizeToScale } from './y-axis' +import { normalizeToScale } from '../helpers/chart' export default function () { const cycle = { diff --git a/components/chart/styles.js b/components/chart/styles.js index d2a95d7..dc3a69b 100644 --- a/components/chart/styles.js +++ b/components/chart/styles.js @@ -11,6 +11,19 @@ const gridLineWidthVertical = 0.6 const gridLineWidthHorizontal = 0.3 const numberLabelFontSize = 13 +const redColor = '#c3000d' +const violetColor = '#7689a9' +const shadesOfViolet = ['#e3e7ed', '#c8cfdc', '#acb8cb', '#91a0ba', violetColor] // light to dark +const yellowColor = '#dbb40c' +const shadesOfYellow = ['#f0e19d', '#e9d26d', '#e2c33c', yellowColor] // light to dark +const magentaColor = '#6f2565' +const shadesOfMagenta = ['#a87ca2', '#8b5083', magentaColor] // light to dark +const pinkColor = '#9e346c' +const shadesOfPink = ['#c485a6', '#b15c89', pinkColor] // light to dark +const lightGreenColor = '#bccd67' +const orangeColor = '#bc6642' +const mintColor = '#6ca299' + const styles = { curve: { stroke: colorTemperature, @@ -48,39 +61,44 @@ const styles = { width: gridLineWidthVertical, } }, - symptomIcon: { + symptomDot: { width: 12, height: 12, borderRadius: 50, }, - iconShades: { - 'bleeding': shadesOfRed, - 'mucus': [ - '#e3e7ed', - '#c8cfdc', - '#acb8cb', - '#91a0ba', - '#7689a9' - ], - 'cervix': [ - '#f0e19d', - '#e9d26d', - '#e2c33c', - '#dbb40c', - ], - 'sex': [ - '#a87ca2', - '#8b5083', - '#6f2565', - ], - 'desire': [ - '#c485a6', - '#b15c89', - '#9e346c', - ], - 'pain': ['#bccd67'], - 'mood': ['#bc6642'], - 'note': ['#6ca299'] + iconColors: { + 'bleeding': { + color: redColor, + shades: shadesOfRed, + }, + 'mucus': { + color: violetColor, + shades: shadesOfViolet, + }, + 'cervix': { + color: yellowColor, + shades: shadesOfYellow, + }, + 'sex': { + color: magentaColor, + shades: shadesOfMagenta, + }, + 'desire': { + color: pinkColor, + shades: shadesOfPink, + }, + 'pain': { + color: lightGreenColor, + shades: [lightGreenColor], + }, + 'mood': { + color: orangeColor, + shades: [orangeColor], + }, + 'note': { + color: mintColor, + shades: [mintColor], + }, }, yAxis: { width: 27, @@ -109,6 +127,18 @@ const styles = { fontWeight: '100', } }, + symptomIcon: { + alignItems: 'center', + justifyContent: 'center', + }, + chartLegend: { + alignItems: 'center', + justifyContent: 'center', + }, + boldTick: { + fontWeight: 'bold', + fontSize: 11, + }, horizontalGrid: { position:'absolute', borderStyle: 'solid', diff --git a/components/chart/symptom-cell.js b/components/chart/symptom-cell.js new file mode 100644 index 0000000..fa76042 --- /dev/null +++ b/components/chart/symptom-cell.js @@ -0,0 +1,52 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { View } from 'react-native' + +import styles from './styles' + +const SymptomCell = ({ + height, + symptom, + symptomValue, + isSymptomDataComplete +}) => { + + const shouldDrawDot = symptomValue !== false + const styleParent = [styles.symptomRow, { height }] + let styleChild + + if (shouldDrawDot) { + const styleSymptom = styles.iconColors[symptom] + const symptomColor = styleSymptom.shades[symptomValue] + + const isMucusOrCervix = (symptom === 'mucus') || (symptom === 'cervix') + + const backgroundColor = (isMucusOrCervix && !isSymptomDataComplete) ? + 'white' : symptomColor + const borderWidth = (isMucusOrCervix && !isSymptomDataComplete) ? 2 : 0 + const borderColor = symptomColor + styleChild = [styles.symptomDot, { + backgroundColor, + borderColor, + borderWidth + }] + } + + return ( + + {shouldDrawDot && } + + ) +} + +SymptomCell.propTypes = { + height: PropTypes.number, + symptom: PropTypes.string, + symptomValue: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.number, + ]), + isSymptomDataComplete: PropTypes.bool, +} + +export default SymptomCell diff --git a/components/chart/symptom-icon.js b/components/chart/symptom-icon.js new file mode 100644 index 0000000..3236c3b --- /dev/null +++ b/components/chart/symptom-icon.js @@ -0,0 +1,26 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { View } from 'react-native' + +import DripIcon from '../../assets/drip-icons' + +import styles from './styles' + +const SymptomIcon = ({ symptom, height }) => { + return ( + + + + ) +} + +SymptomIcon.propTypes = { + height: PropTypes.number, + symptom: PropTypes.string, +} + +export default SymptomIcon diff --git a/components/chart/tick-list.js b/components/chart/tick-list.js new file mode 100644 index 0000000..16fba0a --- /dev/null +++ b/components/chart/tick-list.js @@ -0,0 +1,34 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { View } from 'react-native' + +import Tick from './tick' + +import { getTickList } from '../helpers/chart' + +import styles from './styles' + +const TickList = ({ height }) => { + return ( + { + getTickList(height) + .map(({ label, position, isBold, shouldShowLabel}) => { + return ( + + ) + }) + } + ) +} + +TickList.propTypes = { + height: PropTypes.number, +} + +export default TickList diff --git a/components/chart/tick.js b/components/chart/tick.js new file mode 100644 index 0000000..173cfbe --- /dev/null +++ b/components/chart/tick.js @@ -0,0 +1,29 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import AppText from '../app-text' + +import styles from './styles' + +const Tick = ({ yPosition, isBold, shouldShowLabel, label }) => { + // 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 + const topPosition = yPosition - 8 + const style = [ + styles.yAxisLabels.tempScale, + {top: topPosition}, + isBold && styles.boldTick + ] + + return {shouldShowLabel && label} +} + +Tick.propTypes = { + yPosition: PropTypes.number, + isBold: PropTypes.bool, + shouldShowLabel: PropTypes.bool, + label: PropTypes.string, +} + +export default Tick diff --git a/components/chart/y-axis.js b/components/chart/y-axis.js index 1f33736..acd3ccf 100644 --- a/components/chart/y-axis.js +++ b/components/chart/y-axis.js @@ -1,75 +1,37 @@ import React from 'react' +import PropTypes from 'prop-types' import { View } from 'react-native' -import config from '../../config' + +import SymptomIcon from './symptom-icon' +import TickList from './tick-list' +import ChartLegend from './chart-legend' + import styles from './styles' -import { scaleObservable, unitObservable } from '../../local-storage' -import AppText from '../app-text' -export function makeYAxisLabels(columnHeight) { - const units = unitObservable.value - const scaleMax = scaleObservable.value.max - const style = styles.yAxisLabels.tempScale - - return getTickPositions(columnHeight).map((y, i) => { - const tick = scaleMax - i * units - const tickLabel = tick * 10 % 10 ? tick.toString() : tick.toString() + '.0' - let showTick - let tickBold - if (units === 0.1) { - showTick = (tick * 10 % 2) ? false : true - tickBold = tick * 10 % 5 ? {} : {fontWeight: 'bold', fontSize: 11} - } else { - showTick = (tick * 10 % 5) ? false : true - tickBold = tick * 10 % 10 ? {} : {fontWeight: 'bold', fontSize: 11} - } - // 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 - return ( - - {showTick && tickLabel} - - ) - }) +const YAxis = ({ height, symptomsToDisplay, symptomsSectionHeight }) => { + const symptomIconHeight = symptomsSectionHeight / symptomsToDisplay.length + return ( + + + {symptomsToDisplay.map(symptom => ( + + ) + )} + + + + + ) } -export function makeHorizontalGrid(columnHeight, symptomRowHeight) { - return getTickPositions(columnHeight).map(tick => { - return ( - - ) - }) +YAxis.propTypes = { + height: PropTypes.number, + symptomsToDisplay: PropTypes.array, + symptomsSectionHeight: PropTypes.number, } -function getTickPositions(columnHeight) { - const units = unitObservable.value - const scaleMin = scaleObservable.value.min - const scaleMax = scaleObservable.value.max - const numberOfTicks = (scaleMax - scaleMin) * (1 / units) + 1 - const tickDistance = 1 / (numberOfTicks - 1) - const tickPositions = [] - for (let i = 0; i < numberOfTicks; i++) { - const position = getAbsoluteValue(tickDistance * i, columnHeight) - tickPositions.push(position) - } - return tickPositions -} - -export function normalizeToScale(temp, columnHeight) { - const scale = scaleObservable.value - const valueRelativeToScale = (scale.max - temp) / (scale.max - scale.min) - return getAbsoluteValue(valueRelativeToScale, columnHeight) -} - -function getAbsoluteValue(relative, columnHeight) { - // we add some height to have some breathing room - const verticalPadding = columnHeight * config.temperatureScale.verticalPadding - const scaleHeight = columnHeight - 2 * verticalPadding - return scaleHeight * relative + verticalPadding -} \ No newline at end of file +export default YAxis diff --git a/components/helpers/chart.js b/components/helpers/chart.js new file mode 100644 index 0000000..24a563c --- /dev/null +++ b/components/helpers/chart.js @@ -0,0 +1,67 @@ +import { scaleObservable, unitObservable } from '../../local-storage' +import config from '../../config' + +export function normalizeToScale(temp, columnHeight) { + const scale = scaleObservable.value + const valueRelativeToScale = (scale.max - temp) / (scale.max - scale.min) + return getAbsoluteValue(valueRelativeToScale, columnHeight) +} + +function getAbsoluteValue(relative, columnHeight) { + // we add some height to have some breathing room + const verticalPadding = columnHeight * config.temperatureScale.verticalPadding + const scaleHeight = columnHeight - 2 * verticalPadding + return scaleHeight * relative + verticalPadding +} + +export function getTickPositions(columnHeight) { + const units = unitObservable.value + const scaleMin = scaleObservable.value.min + const scaleMax = scaleObservable.value.max + const numberOfTicks = (scaleMax - scaleMin) * (1 / units) + 1 + const tickDistance = 1 / (numberOfTicks - 1) + const tickPositions = [] + for (let i = 0; i < numberOfTicks; i++) { + const position = getAbsoluteValue(tickDistance * i, columnHeight) + tickPositions.push(position) + } + return tickPositions +} + +export function getTickList(columnHeight) { + + const units = unitObservable.value + const scaleMax = scaleObservable.value.max + + return getTickPositions(columnHeight).map((tickPosition, i) => { + + const tick = scaleMax - i * units + let isBold, label, shouldShowLabel + + if (Number.isInteger(tick)) { + isBold = true + label = tick.toString() + '.0' + } else { + isBold = false + label = tick.toString() + } + + // when temp range <= 3, units === 0.1 we show temp values with step 0.2 + // when temp range > 3, units === 0.5 we show temp values with step 0.5 + + if (units === 0.1) { + // show label with step 0.2 + shouldShowLabel = !(tick * 10 % 2) + } else { + // show label with step 0.5 + shouldShowLabel = !(tick * 10 % 5) + } + + return { + position: tickPosition, + label, + isBold, + shouldShowLabel, + } + }) +}