From b9385e6e0deca7b5a1822bbbdb799764304db971 Mon Sep 17 00:00:00 2001 From: ctyrrellnrel Date: Tue, 11 Jul 2023 16:19:24 -0800 Subject: [PATCH 001/164] Initial commit, added chart - replaced normal details view of 'My distance' with chart - next steps - create button to switch between chart and details view in 'my distance' card - Figure out why all data is showing up unlabled - Allow the axis to switch easily - right now, setting isHorizontal doesn't completely switch data, only thedirection of the bars - would be nice to flip data axis and bar axis simultaneously --- www/js/components/BarChart.tsx | 2 +- www/templates/main-metrics.html | 20 +++----------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 6da3d2a2b..9d835f918 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -20,7 +20,7 @@ Chart.register( ); const BarChart = ({ chartData, axisTitle, lineAnnotations=null, isHorizontal=false }) => { - + const { colors } = useTheme(); const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); diff --git a/www/templates/main-metrics.html b/www/templates/main-metrics.html index 40e731d91..e85f50795 100644 --- a/www/templates/main-metrics.html +++ b/www/templates/main-metrics.html @@ -133,23 +133,9 @@

{{'main-metrics.calories'}}

{{'main-metrics.distance'}}

-
{{'main-metrics.no-summary-data'}}
-
-
- -
- -
-
-
- {{ summaryData.defaultSummary.distance[dIndex + i].key }} -
-
- {{ formatDistance(summaryData.defaultSummary.distance[dIndex + i].values).slice(-1)[0] }} -
-
-
-
+ + +
From 26fe4f9fae365bab7345aa7224eb594c486cfba3 Mon Sep 17 00:00:00 2001 From: ctyrrellnrel Date: Tue, 11 Jul 2023 17:56:01 -0800 Subject: [PATCH 002/164] Added two new components - one (MetricDetails) is going to be a copy of the original metric details view - the next (MetricsCard) is going to be a container to have both the BarChart component, and the MetricDetails component - Will be a react component, rather than function, and will hold onto the state of whether or not the barchart or metricdetails will be showing - next steps - add a button to switch between the two views --- www/js/components/MetricDetails.tsx | 87 +++++++++++++++++++++++++++ www/js/components/MetricsCard.tsx | 91 +++++++++++++++++++++++++++++ www/templates/main-metrics.html | 4 +- 3 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 www/js/components/MetricDetails.tsx create mode 100644 www/js/components/MetricsCard.tsx diff --git a/www/js/components/MetricDetails.tsx b/www/js/components/MetricDetails.tsx new file mode 100644 index 000000000..20107efea --- /dev/null +++ b/www/js/components/MetricDetails.tsx @@ -0,0 +1,87 @@ + +import React, { useRef, useState } from 'react'; +import { array, string, bool } from 'prop-types'; +import { angularize } from '../angular-react-helper'; +import { View } from 'react-native'; +import { useTheme } from 'react-native-paper'; + +const MetricsDetails = ({ chartData}) => { + + const { colors } = useTheme(); + const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); + + const defaultPalette = [ + '#c95465', // red oklch(60% 0.15 14) + '#4a71b1', // blue oklch(55% 0.11 260) + '#d2824e', // orange oklch(68% 0.12 52) + '#856b5d', // brown oklch(55% 0.04 50) + '#59894f', // green oklch(58% 0.1 140) + '#e0cc55', // yellow oklch(84% 0.14 100) + '#b273ac', // purple oklch(64% 0.11 330) + '#f09da6', // pink oklch(78% 0.1 12) + '#b3aca8', // grey oklch(75% 0.01 55) + '#80afad', // teal oklch(72% 0.05 192) + ] + + + + return ( + + + + ) +} + +MetricsDetails.propTypes = { + chartData: array, + axisTitle: string, + lineAnnotations: array, + isHorizontal: bool, +}; + +angularize(MetricsDetails, 'MetricsDetails', 'emission.main.metricsdetails'); +export default MetricsDetails; + +// const sampleAnnotations = [ +// { value: 35, label: 'Target1' }, +// { value: 65, label: 'Target2' }, +// ]; + +// const sampleChartData = [ +// { +// label: 'Primary', +// records: [ +// { x: moment('2023-06-20'), y: 20 }, +// { x: moment('2023-06-21'), y: 30 }, +// { x: moment('2023-06-23'), y: 80 }, +// { x: moment('2023-06-24'), y: 40 }, +// ], +// }, +// { +// label: 'Secondary', +// records: [ +// { x: moment('2023-06-21'), y: 10 }, +// { x: moment('2023-06-22'), y: 50 }, +// { x: moment('2023-06-23'), y: 30 }, +// { x: moment('2023-06-25'), y: 40 }, +// ], +// }, +// { +// label: 'Tertiary', +// records: [ +// { x: moment('2023-06-20'), y: 30 }, +// { x: moment('2023-06-22'), y: 40 }, +// { x: moment('2023-06-24'), y: 10 }, +// { x: moment('2023-06-25'), y: 60 }, +// ], +// }, +// { +// label: 'Quaternary', +// records: [ +// { x: moment('2023-06-22'), y: 10 }, +// { x: moment('2023-06-23'), y: 20 }, +// { x: moment('2023-06-24'), y: 30 }, +// { x: moment('2023-06-25'), y: 40 }, +// ], +// }, +// ]; diff --git a/www/js/components/MetricsCard.tsx b/www/js/components/MetricsCard.tsx new file mode 100644 index 000000000..66d77cb68 --- /dev/null +++ b/www/js/components/MetricsCard.tsx @@ -0,0 +1,91 @@ + +import React, { useRef, useState } from 'react'; +import { array, string, bool } from 'prop-types'; +import { angularize } from '../angular-react-helper'; +import { View } from 'react-native'; +import { useTheme , Card, CardProps} from 'react-native-paper'; +import BarChart from './BarChart'; +import MetricsDetails from './MetricDetails'; + +const MetricsCard = ({chartData, axisTitle}) => { + + const { colors } = useTheme(); + const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); + + const defaultPalette = [ + '#c95465', // red oklch(60% 0.15 14) + '#4a71b1', // blue oklch(55% 0.11 260) + '#d2824e', // orange oklch(68% 0.12 52) + '#856b5d', // brown oklch(55% 0.04 50) + '#59894f', // green oklch(58% 0.1 140) + '#e0cc55', // yellow oklch(84% 0.14 100) + '#b273ac', // purple oklch(64% 0.11 330) + '#f09da6', // pink oklch(78% 0.1 12) + '#b3aca8', // grey oklch(75% 0.01 55) + '#80afad', // teal oklch(72% 0.05 192) + ] + const details : bool = false; + return ( + + + {details ? ( + + ) : ( + + ) + } + + + ) +} + +MetricsCard.propTypes = { + chartData: array, +}; + +angularize(MetricsCard, 'MetricsCard', 'emission.main.metricscard'); +export default MetricsCard; + +// const sampleAnnotations = [ +// { value: 35, label: 'Target1' }, +// { value: 65, label: 'Target2' }, +// ]; + +// const sampleChartData = [ +// { +// label: 'Primary', +// records: [ +// { x: moment('2023-06-20'), y: 20 }, +// { x: moment('2023-06-21'), y: 30 }, +// { x: moment('2023-06-23'), y: 80 }, +// { x: moment('2023-06-24'), y: 40 }, +// ], +// }, +// { +// label: 'Secondary', +// records: [ +// { x: moment('2023-06-21'), y: 10 }, +// { x: moment('2023-06-22'), y: 50 }, +// { x: moment('2023-06-23'), y: 30 }, +// { x: moment('2023-06-25'), y: 40 }, +// ], +// }, +// { +// label: 'Tertiary', +// records: [ +// { x: moment('2023-06-20'), y: 30 }, +// { x: moment('2023-06-22'), y: 40 }, +// { x: moment('2023-06-24'), y: 10 }, +// { x: moment('2023-06-25'), y: 60 }, +// ], +// }, +// { +// label: 'Quaternary', +// records: [ +// { x: moment('2023-06-22'), y: 10 }, +// { x: moment('2023-06-23'), y: 20 }, +// { x: moment('2023-06-24'), y: 30 }, +// { x: moment('2023-06-25'), y: 40 }, +// ], +// }, +// ]; diff --git a/www/templates/main-metrics.html b/www/templates/main-metrics.html index e85f50795..ce96a91ff 100644 --- a/www/templates/main-metrics.html +++ b/www/templates/main-metrics.html @@ -133,9 +133,7 @@

{{'main-metrics.calories'}}

{{'main-metrics.distance'}}

- - - +
From c05e0718fc14de7a187ea78f9ac16b604de86b08 Mon Sep 17 00:00:00 2001 From: ctyrrellnrel Date: Tue, 11 Jul 2023 18:26:19 -0800 Subject: [PATCH 003/164] Added view state to MetricsCard - allows the card to keep track of whether or not it has a BarChart or MetricDetails child component - next step - Add button --- www/js/components/MetricsCard.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/www/js/components/MetricsCard.tsx b/www/js/components/MetricsCard.tsx index 66d77cb68..b0edc6ad5 100644 --- a/www/js/components/MetricsCard.tsx +++ b/www/js/components/MetricsCard.tsx @@ -1,5 +1,5 @@ -import React, { useRef, useState } from 'react'; +import React, { useRef, state, useState } from 'react'; import { array, string, bool } from 'prop-types'; import { angularize } from '../angular-react-helper'; import { View } from 'react-native'; @@ -7,9 +7,11 @@ import { useTheme , Card, CardProps} from 'react-native-paper'; import BarChart from './BarChart'; import MetricsDetails from './MetricDetails'; + const MetricsCard = ({chartData, axisTitle}) => { - + const { colors } = useTheme(); + const [state, setState] = useState({detailsView : false}) const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); const defaultPalette = [ @@ -24,21 +26,23 @@ const MetricsCard = ({chartData, axisTitle}) => { '#b3aca8', // grey oklch(75% 0.01 55) '#80afad', // teal oklch(72% 0.05 192) ] - const details : bool = false; return ( - {details ? ( + + {state.detailsView ? ( ) : ( ) } + ) } + MetricsCard.propTypes = { chartData: array, }; From 011b858d1f488ebd367d30c233bf0999bafebdbd Mon Sep 17 00:00:00 2001 From: ctyrrellnrel Date: Tue, 11 Jul 2023 19:02:13 -0800 Subject: [PATCH 004/164] Added button to switch between components - next step - format and style button --- www/js/components/MetricsCard.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/www/js/components/MetricsCard.tsx b/www/js/components/MetricsCard.tsx index b0edc6ad5..35514b03f 100644 --- a/www/js/components/MetricsCard.tsx +++ b/www/js/components/MetricsCard.tsx @@ -3,7 +3,7 @@ import React, { useRef, state, useState } from 'react'; import { array, string, bool } from 'prop-types'; import { angularize } from '../angular-react-helper'; import { View } from 'react-native'; -import { useTheme , Card, CardProps} from 'react-native-paper'; +import { useTheme , Card, IconButton} from 'react-native-paper'; import BarChart from './BarChart'; import MetricsDetails from './MetricDetails'; @@ -28,15 +28,22 @@ const MetricsCard = ({chartData, axisTitle}) => { ] return ( - - + + {state.detailsView ? ( + + setState({detailsView : false})}/> + ) : ( + + + setState({detailsView : true})}/> + ) } - + ) From e36ba873cc44b35aae68716183c0adc7f4d84055 Mon Sep 17 00:00:00 2001 From: ctyrrellnrel Date: Wed, 12 Jul 2023 02:16:51 -0800 Subject: [PATCH 005/164] Added button and styling to MetricsCard - button allows switching between the BarChart and the MetricDetails card - button icon switches depending on the card represented --- www/js/components/BarChart.tsx | 2 +- www/js/components/MetricsCard.tsx | 46 ++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 9d835f918..764719ad6 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -26,7 +26,7 @@ const BarChart = ({ chartData, axisTitle, lineAnnotations=null, isHorizontal=fal const barChartRef = useRef(null); - const defaultPalette = [ + const defaultPalette = [ '#c95465', // red oklch(60% 0.15 14) '#4a71b1', // blue oklch(55% 0.11 260) '#d2824e', // orange oklch(68% 0.12 52) diff --git a/www/js/components/MetricsCard.tsx b/www/js/components/MetricsCard.tsx index 35514b03f..e14df49ec 100644 --- a/www/js/components/MetricsCard.tsx +++ b/www/js/components/MetricsCard.tsx @@ -3,9 +3,10 @@ import React, { useRef, state, useState } from 'react'; import { array, string, bool } from 'prop-types'; import { angularize } from '../angular-react-helper'; import { View } from 'react-native'; -import { useTheme , Card, IconButton} from 'react-native-paper'; +import { useTheme , Card, IconButton, Surface, SurfaceProps} from 'react-native-paper'; import BarChart from './BarChart'; import MetricsDetails from './MetricDetails'; +import { StyleSheet } from 'react-native'; const MetricsCard = ({chartData, axisTitle}) => { @@ -24,23 +25,37 @@ const MetricsCard = ({chartData, axisTitle}) => { '#b273ac', // purple oklch(64% 0.11 330) '#f09da6', // pink oklch(78% 0.1 12) '#b3aca8', // grey oklch(75% 0.01 55) - '#80afad', // teal oklch(72% 0.05 192) + '#80afad', // teal oklch(72% 0.05 192) ] return ( - - + + + setState({detailsView : false})}/> + ): + ( + ()=> setState({detailsView : true})}/> + )} + /> + + {state.detailsView ? ( - - setState({detailsView : false})}/> - - + <> + + + + + ) : ( - - - setState({detailsView : true})}/> - - + <> + + + + ) } @@ -57,6 +72,11 @@ MetricsCard.propTypes = { angularize(MetricsCard, 'MetricsCard', 'emission.main.metricscard'); export default MetricsCard; +const cardStyles = StyleSheet.create({ + cardLabel: { + color: '#c95465' + } +}); // const sampleAnnotations = [ // { value: 35, label: 'Target1' }, // { value: 65, label: 'Target2' }, From 4440e1db3e5ef0a10cf64e70c292a884216c4cd7 Mon Sep 17 00:00:00 2001 From: ctyrrellnrel Date: Thu, 13 Jul 2023 02:52:19 -0800 Subject: [PATCH 006/164] Removing unnecessary code - removed children property from card - removed leftover code from copying from BarChart.tsx to MetricsCard and MetricDetails - Removed unecessary import statements --- www/js/components/MetricDetails.tsx | 54 ++------------------- www/js/components/MetricsCard.tsx | 75 ++--------------------------- 2 files changed, 9 insertions(+), 120 deletions(-) diff --git a/www/js/components/MetricDetails.tsx b/www/js/components/MetricDetails.tsx index 20107efea..6a195875f 100644 --- a/www/js/components/MetricDetails.tsx +++ b/www/js/components/MetricDetails.tsx @@ -3,7 +3,7 @@ import React, { useRef, useState } from 'react'; import { array, string, bool } from 'prop-types'; import { angularize } from '../angular-react-helper'; import { View } from 'react-native'; -import { useTheme } from 'react-native-paper'; +import { useTheme, Card } from 'react-native-paper'; const MetricsDetails = ({ chartData}) => { @@ -27,61 +27,17 @@ const MetricsDetails = ({ chartData}) => { return ( - +
+ 25 + miles +
) } MetricsDetails.propTypes = { chartData: array, - axisTitle: string, - lineAnnotations: array, - isHorizontal: bool, }; angularize(MetricsDetails, 'MetricsDetails', 'emission.main.metricsdetails'); export default MetricsDetails; - -// const sampleAnnotations = [ -// { value: 35, label: 'Target1' }, -// { value: 65, label: 'Target2' }, -// ]; - -// const sampleChartData = [ -// { -// label: 'Primary', -// records: [ -// { x: moment('2023-06-20'), y: 20 }, -// { x: moment('2023-06-21'), y: 30 }, -// { x: moment('2023-06-23'), y: 80 }, -// { x: moment('2023-06-24'), y: 40 }, -// ], -// }, -// { -// label: 'Secondary', -// records: [ -// { x: moment('2023-06-21'), y: 10 }, -// { x: moment('2023-06-22'), y: 50 }, -// { x: moment('2023-06-23'), y: 30 }, -// { x: moment('2023-06-25'), y: 40 }, -// ], -// }, -// { -// label: 'Tertiary', -// records: [ -// { x: moment('2023-06-20'), y: 30 }, -// { x: moment('2023-06-22'), y: 40 }, -// { x: moment('2023-06-24'), y: 10 }, -// { x: moment('2023-06-25'), y: 60 }, -// ], -// }, -// { -// label: 'Quaternary', -// records: [ -// { x: moment('2023-06-22'), y: 10 }, -// { x: moment('2023-06-23'), y: 20 }, -// { x: moment('2023-06-24'), y: 30 }, -// { x: moment('2023-06-25'), y: 40 }, -// ], -// }, -// ]; diff --git a/www/js/components/MetricsCard.tsx b/www/js/components/MetricsCard.tsx index e14df49ec..ddd792235 100644 --- a/www/js/components/MetricsCard.tsx +++ b/www/js/components/MetricsCard.tsx @@ -1,9 +1,8 @@ -import React, { useRef, state, useState } from 'react'; -import { array, string, bool } from 'prop-types'; +import React, { useState } from 'react'; +import { array, } from 'prop-types'; import { angularize } from '../angular-react-helper'; -import { View } from 'react-native'; -import { useTheme , Card, IconButton, Surface, SurfaceProps} from 'react-native-paper'; +import { Card, IconButton, Surface} from 'react-native-paper'; import BarChart from './BarChart'; import MetricsDetails from './MetricDetails'; import { StyleSheet } from 'react-native'; @@ -11,24 +10,9 @@ import { StyleSheet } from 'react-native'; const MetricsCard = ({chartData, axisTitle}) => { - const { colors } = useTheme(); const [state, setState] = useState({detailsView : false}) - const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); - - const defaultPalette = [ - '#c95465', // red oklch(60% 0.15 14) - '#4a71b1', // blue oklch(55% 0.11 260) - '#d2824e', // orange oklch(68% 0.12 52) - '#856b5d', // brown oklch(55% 0.04 50) - '#59894f', // green oklch(58% 0.1 140) - '#e0cc55', // yellow oklch(84% 0.14 100) - '#b273ac', // purple oklch(64% 0.11 330) - '#f09da6', // pink oklch(78% 0.1 12) - '#b3aca8', // grey oklch(75% 0.01 55) - '#80afad', // teal oklch(72% 0.05 192) - ] return ( - + { /> - {state.detailsView ? ( <> @@ -64,59 +47,9 @@ const MetricsCard = ({chartData, axisTitle}) => { ) } - MetricsCard.propTypes = { chartData: array, }; angularize(MetricsCard, 'MetricsCard', 'emission.main.metricscard'); export default MetricsCard; - -const cardStyles = StyleSheet.create({ - cardLabel: { - color: '#c95465' - } -}); -// const sampleAnnotations = [ -// { value: 35, label: 'Target1' }, -// { value: 65, label: 'Target2' }, -// ]; - -// const sampleChartData = [ -// { -// label: 'Primary', -// records: [ -// { x: moment('2023-06-20'), y: 20 }, -// { x: moment('2023-06-21'), y: 30 }, -// { x: moment('2023-06-23'), y: 80 }, -// { x: moment('2023-06-24'), y: 40 }, -// ], -// }, -// { -// label: 'Secondary', -// records: [ -// { x: moment('2023-06-21'), y: 10 }, -// { x: moment('2023-06-22'), y: 50 }, -// { x: moment('2023-06-23'), y: 30 }, -// { x: moment('2023-06-25'), y: 40 }, -// ], -// }, -// { -// label: 'Tertiary', -// records: [ -// { x: moment('2023-06-20'), y: 30 }, -// { x: moment('2023-06-22'), y: 40 }, -// { x: moment('2023-06-24'), y: 10 }, -// { x: moment('2023-06-25'), y: 60 }, -// ], -// }, -// { -// label: 'Quaternary', -// records: [ -// { x: moment('2023-06-22'), y: 10 }, -// { x: moment('2023-06-23'), y: 20 }, -// { x: moment('2023-06-24'), y: 30 }, -// { x: moment('2023-06-25'), y: 40 }, -// ], -// }, -// ]; From 5ca9754784a85c52f7ceeebc67cecad043afba08 Mon Sep 17 00:00:00 2001 From: ctyrrellnrel Date: Tue, 18 Jul 2023 16:57:19 -0800 Subject: [PATCH 007/164] Added MetricsCard module to metrics.js -- also switched main-metrics html so that only the MetricsCard element is held in the ion-slide element, without any extra divs or headers --- www/js/metrics.js | 3 ++- www/templates/main-metrics.html | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/www/js/metrics.js b/www/js/metrics.js index ef1b26891..1e65b5bdd 100644 --- a/www/js/metrics.js +++ b/www/js/metrics.js @@ -3,7 +3,7 @@ import angular from 'angular'; import 'nvd3'; import BarChart from './components/BarChart'; - +import MetricsCard from './components/MetricsCard'; angular.module('emission.main.metrics',['emission.services', 'ionic-datepicker', 'emission.config.imperial', @@ -12,6 +12,7 @@ angular.module('emission.main.metrics',['emission.services', 'emission.stats.clientstats', 'emission.plugin.kvstore', 'emission.plugin.logger', + MetricsCard.module, BarChart.module]) .controller('MetricsCtrl', function($scope, $ionicActionSheet, $ionicLoading, diff --git a/www/templates/main-metrics.html b/www/templates/main-metrics.html index ce96a91ff..925f390f7 100644 --- a/www/templates/main-metrics.html +++ b/www/templates/main-metrics.html @@ -131,10 +131,8 @@

{{'main-metrics.calories'}}

-
-

{{'main-metrics.distance'}}

- -
+ +
From cb4c65c35b97b1bdcf1abd9d0b258f122169e349 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 21 Aug 2023 16:48:32 -0400 Subject: [PATCH 008/164] remove unneeded code from old PR --- www/js/components/BarChart.tsx | 4 ++-- www/js/components/MetricDetails.tsx | 32 +++-------------------------- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 764719ad6..6da3d2a2b 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -20,13 +20,13 @@ Chart.register( ); const BarChart = ({ chartData, axisTitle, lineAnnotations=null, isHorizontal=false }) => { - + const { colors } = useTheme(); const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); const barChartRef = useRef(null); - const defaultPalette = [ + const defaultPalette = [ '#c95465', // red oklch(60% 0.15 14) '#4a71b1', // blue oklch(55% 0.11 260) '#d2824e', // orange oklch(68% 0.12 52) diff --git a/www/js/components/MetricDetails.tsx b/www/js/components/MetricDetails.tsx index 6a195875f..9bde70fff 100644 --- a/www/js/components/MetricDetails.tsx +++ b/www/js/components/MetricDetails.tsx @@ -1,43 +1,17 @@ -import React, { useRef, useState } from 'react'; -import { array, string, bool } from 'prop-types'; -import { angularize } from '../angular-react-helper'; +import React from 'react'; import { View } from 'react-native'; -import { useTheme, Card } from 'react-native-paper'; +import { useTheme } from 'react-native-paper'; const MetricsDetails = ({ chartData}) => { const { colors } = useTheme(); - const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); - - const defaultPalette = [ - '#c95465', // red oklch(60% 0.15 14) - '#4a71b1', // blue oklch(55% 0.11 260) - '#d2824e', // orange oklch(68% 0.12 52) - '#856b5d', // brown oklch(55% 0.04 50) - '#59894f', // green oklch(58% 0.1 140) - '#e0cc55', // yellow oklch(84% 0.14 100) - '#b273ac', // purple oklch(64% 0.11 330) - '#f09da6', // pink oklch(78% 0.1 12) - '#b3aca8', // grey oklch(75% 0.01 55) - '#80afad', // teal oklch(72% 0.05 192) - ] - - return ( -
- 25 - miles -
+
) } -MetricsDetails.propTypes = { - chartData: array, -}; - -angularize(MetricsDetails, 'MetricsDetails', 'emission.main.metricsdetails'); export default MetricsDetails; From ef8ce3ce3ea3ff0e81ec6a07f4dca5d60b49f246 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 21 Aug 2023 17:08:58 -0400 Subject: [PATCH 009/164] move MetricDetails and MetricsCard to js/metrics --- www/js/metrics.js | 2 +- www/js/{components => metrics}/MetricDetails.tsx | 0 www/js/{components => metrics}/MetricsCard.tsx | 4 +--- 3 files changed, 2 insertions(+), 4 deletions(-) rename www/js/{components => metrics}/MetricDetails.tsx (100%) rename www/js/{components => metrics}/MetricsCard.tsx (95%) diff --git a/www/js/metrics.js b/www/js/metrics.js index 1e65b5bdd..21a158116 100644 --- a/www/js/metrics.js +++ b/www/js/metrics.js @@ -3,7 +3,7 @@ import angular from 'angular'; import 'nvd3'; import BarChart from './components/BarChart'; -import MetricsCard from './components/MetricsCard'; +import MetricsCard from './metrics/MetricsCard'; angular.module('emission.main.metrics',['emission.services', 'ionic-datepicker', 'emission.config.imperial', diff --git a/www/js/components/MetricDetails.tsx b/www/js/metrics/MetricDetails.tsx similarity index 100% rename from www/js/components/MetricDetails.tsx rename to www/js/metrics/MetricDetails.tsx diff --git a/www/js/components/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx similarity index 95% rename from www/js/components/MetricsCard.tsx rename to www/js/metrics/MetricsCard.tsx index ddd792235..e3019e59c 100644 --- a/www/js/components/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -3,10 +3,8 @@ import React, { useState } from 'react'; import { array, } from 'prop-types'; import { angularize } from '../angular-react-helper'; import { Card, IconButton, Surface} from 'react-native-paper'; -import BarChart from './BarChart'; +import BarChart from '../components/BarChart'; import MetricsDetails from './MetricDetails'; -import { StyleSheet } from 'react-native'; - const MetricsCard = ({chartData, axisTitle}) => { From 1bd3b6213fa5c0d0cef7579f096fccd844d8c46f Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 22 Aug 2023 12:16:50 -0400 Subject: [PATCH 010/164] use new MetricsTab instead of main-metrics The new dashboard will have the MetricsTab component as its entry point - main-metrics.html will no longer be rendered --- www/js/main.js | 8 ++++++-- www/js/metrics.js | 2 -- www/js/metrics/MetricsCard.tsx | 5 ----- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/www/js/main.js b/www/js/main.js index 652d46054..bf1c7d876 100644 --- a/www/js/main.js +++ b/www/js/main.js @@ -2,13 +2,16 @@ import angular from 'angular'; +import MetricsTab from './metrics/MetricsTab'; + angular.module('emission.main', ['emission.main.diary', 'emission.main.control', 'emission.main.metrics', 'emission.main.recent', 'emission.config.dynamic', 'emission.services', - 'emission.services.upload']) + 'emission.services.upload', + MetricsTab.module]) .config(function($stateProvider, $ionicConfigProvider, $urlRouterProvider) { $stateProvider @@ -24,7 +27,8 @@ angular.module('emission.main', ['emission.main.diary', url: '/metrics', views: { 'main-metrics': { - templateUrl: 'templates/main-metrics.html', + // templateUrl: 'templates/main-metrics.html', + template: "", controller: 'MetricsCtrl' } } diff --git a/www/js/metrics.js b/www/js/metrics.js index 21a158116..9b6522fd7 100644 --- a/www/js/metrics.js +++ b/www/js/metrics.js @@ -3,7 +3,6 @@ import angular from 'angular'; import 'nvd3'; import BarChart from './components/BarChart'; -import MetricsCard from './metrics/MetricsCard'; angular.module('emission.main.metrics',['emission.services', 'ionic-datepicker', 'emission.config.imperial', @@ -12,7 +11,6 @@ angular.module('emission.main.metrics',['emission.services', 'emission.stats.clientstats', 'emission.plugin.kvstore', 'emission.plugin.logger', - MetricsCard.module, BarChart.module]) .controller('MetricsCtrl', function($scope, $ionicActionSheet, $ionicLoading, diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index e3019e59c..ba2fc49e0 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -45,9 +45,4 @@ const MetricsCard = ({chartData, axisTitle}) => { ) } -MetricsCard.propTypes = { - chartData: array, -}; - -angularize(MetricsCard, 'MetricsCard', 'emission.main.metricscard'); export default MetricsCard; From c24da1807c2bb15e552cd6f2f781b27f98fa7c5e Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 22 Aug 2023 12:22:19 -0400 Subject: [PATCH 011/164] new MetricsTab Replacing main-metrics, this component represents the entire Dashboard tab and will get metrics data from the server, store it as state, and pass it down to child components like MetricsCard. There is also a date picker at the top (TODO) and a refresh button. The logic for fetching metrics has been modernized into modern JS (async/await) and uses Luxon instead of Moment. I tested to make sure the timestamps of the resulting queries are the same using Luxon as they were with Moment. --- www/js/metrics/MetricsTab.tsx | 109 ++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 www/js/metrics/MetricsTab.tsx diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx new file mode 100644 index 000000000..f53ed9e2f --- /dev/null +++ b/www/js/metrics/MetricsTab.tsx @@ -0,0 +1,109 @@ +import React, { useEffect, useState, useMemo } from "react"; +import { angularize, getAngularService } from "../angular-react-helper"; +import { View, ScrollView, useWindowDimensions } from "react-native"; +import { Appbar } from "react-native-paper"; +import NavBarButton from "../components/NavBarButton"; +import { useTranslation } from "react-i18next"; +import { DateTime } from "luxon"; +import { UserMetrics } from "./metricsTypes"; +import MetricsCard from "./MetricsCard"; +import { useImperialConfig } from "../config/useImperialConfig"; + +export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; + +const MetricsTab = () => { + + const { t } = useTranslation(); + const { distanceSuffix, speedSuffix } = useImperialConfig(); + const CommHelper = getAngularService('CommHelper'); + + const [userMetrics, setUserMetrics] = useState(null); + // const [aggMetrics, setAggMetrics] = useState({}); + + const tsRange = useMemo(() => { + const now = DateTime.utc().startOf('day'); + const start = now.minus({ days: 14 }); + const end = now.minus({ days: 1 }); + return { + start_time: start.toSeconds(), + end_time: end.toSeconds() + } + }, []) + + useEffect(() => { + fetchMetricsFromServer(); + }, []); + + async function fetchMetricsFromServer() { + const serverResponse = await CommHelper.getMetrics('timestamp', { + freq: 'D', + ...tsRange, + metric_list: METRIC_LIST, + is_return_aggregate: false + }); + console.debug("Got metrics = ", serverResponse); + const userMetrics = {}; + METRIC_LIST.forEach((metricName, i) => { + userMetrics[metricName] = serverResponse['user_metrics'][i]; + }); + setUserMetrics(userMetrics as UserMetrics); + } + + function refresh() { + // TODO + } + + const { width: windowWidth } = useWindowDimensions(); + const cardWidth = windowWidth * .85; + + return (<> + + + {}}> + Date + + refresh()} /> + + + + + + + + + + ); +} + +const cardMargin = 8; +const s = { + scroll: { + scrollSnapType: 'x mandatory', + }, + card: (cardWidth) => ({ + margin: cardMargin, + width: cardWidth, + scrollSnapAlign: 'center', + scrollSnapStop: 'always', + }), +}; + +angularize(MetricsTab, 'MetricsTab', 'emission.main.metricstab'); +export default MetricsTab; From 747385575265797019529d353a6b9a1380665861 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 22 Aug 2023 12:36:18 -0400 Subject: [PATCH 012/164] add typings for metric data This typing is based on the data we receive from the server. For each metric, we receive an array containing "days" of metric data. Each day has a few props (ts, fmt_time, nUsers, local_dt) plus any number of properties with the `label_` prefix that represent the measurment for a particular label. Having this be type-safe from the start should make it much easier to realized the desired visualizations! --- www/js/metrics/metricsTypes.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 www/js/metrics/metricsTypes.ts diff --git a/www/js/metrics/metricsTypes.ts b/www/js/metrics/metricsTypes.ts new file mode 100644 index 000000000..ee8b3e3f9 --- /dev/null +++ b/www/js/metrics/metricsTypes.ts @@ -0,0 +1,14 @@ +import { METRIC_LIST } from "./MetricsTab" + +type MetricName = typeof METRIC_LIST[number]; +type LabelProps = {[k in `label_${string}`]?: number}; // label_, where could be anything +export type DayOfMetricData = LabelProps & { + ts: number, + fmt_time: string, + nUsers: number, + local_dt: {[k: string]: any}, // TODO type datetime obj +} + +export type UserMetrics = { + [key in MetricName]: DayOfMetricData[] +} From 8753162514ffdec084deb5472103c990dbf5aa7c Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 22 Aug 2023 12:49:24 -0400 Subject: [PATCH 013/164] reformat MetricsCard to work with new MetricsTab MetricsCard will now accept 'metricDataDays' as a prop and derive the chart data from this. We can employ `useMemo` for this so that iff metricDataDays changes, chartData updates. Prior to this, MetricsCard had an IconButton on the right side allowing users toggle between 'details' and 'graph' view. To be clearer to the user, I have changed this to a SegmentedButtons. This way, users see both their options side by side and can select the desired one. A few colors were added in appTheme.ts - surfaceDisabled and onSurfaceDisabled. These are colors that already exist in the React Native Paper default theme, but we are overriding for our own purposes. In the SegmentedButtons on MetricsCard, the surfaceDisabled color is used for the button that is not checked --- www/js/appTheme.ts | 2 + www/js/metrics/MetricsCard.tsx | 101 ++++++++++++++++++++------------ www/js/metrics/metricsHelper.ts | 14 +++++ 3 files changed, 81 insertions(+), 36 deletions(-) create mode 100644 www/js/metrics/metricsHelper.ts diff --git a/www/js/appTheme.ts b/www/js/appTheme.ts index 90862a2b3..ed5e19a97 100644 --- a/www/js/appTheme.ts +++ b/www/js/appTheme.ts @@ -15,6 +15,8 @@ const AppTheme = { background: '#edf1f6', // lch(95% 3 250) - background of label screen, other screens still have this as CSS .pane surface: '#fafdff', // lch(99% 30 250) surfaceVariant: '#e0f0ff', // lch(94% 50 250) - background of DataTable + surfaceDisabled: '#c7e0f7', // lch(88% 15 250) + onSurfaceDisabled: '#52606d', // lch(40% 10 250) elevation: { level0: 'transparent', level1: '#fafdff', // lch(99% 30 250) diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index ba2fc49e0..b9729020f 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -1,46 +1,75 @@ -import React, { useState } from 'react'; -import { array, } from 'prop-types'; -import { angularize } from '../angular-react-helper'; -import { Card, IconButton, Surface} from 'react-native-paper'; +import React, { useMemo, useState } from 'react'; +import { Card, CardProps, SegmentedButtons, useTheme} from 'react-native-paper'; import BarChart from '../components/BarChart'; import MetricsDetails from './MetricDetails'; +import { DayOfMetricData } from './metricsTypes'; +import { getUniqueLabelsForDays } from './metricsHelper'; -const MetricsCard = ({chartData, axisTitle}) => { +type Props = CardProps & { + cardTitle: string, + metricDataDays: DayOfMetricData[], + axisUnits: string, +} +const MetricsCard = ({cardTitle, metricDataDays, axisUnits, ...rest}: Props) => { + + const { colors } = useTheme(); - const [state, setState] = useState({detailsView : false}) - return ( - - - setState({detailsView : false})}/> - ): - ( - ()=> setState({detailsView : true})}/> - )} - /> - - - {state.detailsView ? ( - <> - - - - + const [viewMode, setViewMode] = useState<'details'|'graph'>('details'); + const chartData = useMemo(() => { + if (!metricDataDays) return []; + const uniqueLabels = getUniqueLabelsForDays(metricDataDays); - ) : ( - <> - - - - - ) + // for each label, format data for chart, with a record for each day with that label + return uniqueLabels.map((label, i) => { + const daysWithThisLabel = metricDataDays.filter(e => e[`label_${label}`]); + return { + label: label, + records: daysWithThisLabel.map(e => ({ + x: e[`label_${label}`], + y: e.ts * 1000, // time (as milliseconds) will go on Y axis because it will be a horizontal chart + })) } - - + }); + }, [metricDataDays]); + + return ( + + + setViewMode(v)} + density='medium' + buttons={[{ + icon: 'abacus', value: 'details', + uncheckedColor: colors.onSurfaceDisabled, + style: { + minWidth: 0, + backgroundColor: viewMode == 'details' ? colors.elevation.level2 : colors.surfaceDisabled + }, + showSelectedCheck: true + }, { + icon: 'chart-bar', + uncheckedColor: colors.onSurfaceDisabled, + value: 'graph', + style: { + minWidth: 0, + backgroundColor: viewMode == 'graph' ? colors.elevation.level2 : colors.surfaceDisabled + }, + showSelectedCheck: true + }]} /> + } + style={{backgroundColor: colors.primary, paddingHorizontal: 8, minHeight: 60}} /> + + {viewMode=='details' ? + + : + + } + ) } diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts new file mode 100644 index 000000000..04492c65e --- /dev/null +++ b/www/js/metrics/metricsHelper.ts @@ -0,0 +1,14 @@ +import { DayOfMetricData } from "./metricsTypes"; + +export function getUniqueLabelsForDays(metricDataDays: DayOfMetricData[]) { + const uniqueLabels: string[] = []; + metricDataDays.forEach(e => { + Object.keys(e).forEach(k => { + if (k.startsWith('label_')) { + const label = k.substring(6); // remove 'label_' prefix leaving just the mode label + if (!uniqueLabels.includes(label)) uniqueLabels.push(label); + } + }); + }); + return uniqueLabels; +} From 12d17ce2b0f4f0590f93b5a0098685649c7340d6 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 22 Aug 2023 12:53:14 -0400 Subject: [PATCH 014/164] fill out MetricDetails With the new MetricsTab, MetricsDetails will receive 'metricDataDays', sum up the totals for each label, and display these in a 2-column. The display is the same as the old dashboard, but now the logic to sum up the values is here in this component instead of jumbled in with everything else. --- www/js/metrics/MetricDetails.tsx | 34 +++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/www/js/metrics/MetricDetails.tsx b/www/js/metrics/MetricDetails.tsx index 9bde70fff..3f2e2c795 100644 --- a/www/js/metrics/MetricDetails.tsx +++ b/www/js/metrics/MetricDetails.tsx @@ -1,17 +1,37 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { View } from 'react-native'; -import { useTheme } from 'react-native-paper'; +import { Text } from 'react-native-paper'; +import { DayOfMetricData } from './metricsTypes'; +import { getUniqueLabelsForDays } from './metricsHelper'; -const MetricsDetails = ({ chartData}) => { +type Props = { metricDataDays: DayOfMetricData[], style: any }; +const MetricsDetails = ({ metricDataDays, style }: Props) => { - const { colors } = useTheme(); + const metricValues = useMemo(() => { + if (!metricDataDays) return []; + const uniqueLabels = getUniqueLabelsForDays(metricDataDays); - return ( - + // for each label, sum up cumulative values across all days + const vals = {}; + uniqueLabels.forEach(label => { + vals[label] = metricDataDays.reduce((acc, day) => ( + acc + (day[`label_${label}`] || 0) + ), 0); + }); + return vals; + }, [metricDataDays]); + return ( + + { Object.keys(metricValues).map((label, i) => + + {label} + {metricValues[label]} + + )} - ) + ); } export default MetricsDetails; From 52854c069bc8dcc4fcbad4c7bbb86318ca32c78d Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 13:09:13 -0400 Subject: [PATCH 015/164] implement datepicker + agg metrics on new dashboard MetricsDateSelect will allow users to select a date range. It appears as a NavBarButton, like the label screen date picker, and also uses a DatePickerModal. However, this one works differently because users can select a range of days instead of just one. The selected range is stored in LabelTab as `dateRange` - once this range is loaded, all the displayed metrics will update to reflect the new data. `dateRange` is an array of two Luxon DateTime objects, which works well to bridge between the JS Date objects (used by RN Paper Dates) and Unix timestamps (used by e-mission-server / for any network calls) --- www/js/metrics/MetricsDateSelect.tsx | 71 ++++++++++++++++++++++++++++ www/js/metrics/MetricsTab.tsx | 59 +++++++++++++---------- 2 files changed, 106 insertions(+), 24 deletions(-) create mode 100644 www/js/metrics/MetricsDateSelect.tsx diff --git a/www/js/metrics/MetricsDateSelect.tsx b/www/js/metrics/MetricsDateSelect.tsx new file mode 100644 index 000000000..0757d0826 --- /dev/null +++ b/www/js/metrics/MetricsDateSelect.tsx @@ -0,0 +1,71 @@ +/* This button launches a modal to select a date range, which determines what time period + for which metrics should be displayed. + The button itself is a NavBarButton, which shows the currently selected date range, + a calendar icon, and launches the modal when clicked. + The modal is a DatePickerModal from react-native-paper-dates, which shows a calendar + and allows the user to select a date. +*/ + +import React, { useState, useCallback, useMemo } from "react"; +import { Text, StyleSheet } from "react-native"; +import { DatePickerModal } from "react-native-paper-dates"; +import { Divider, useTheme } from "react-native-paper"; +import i18next from "i18next"; +import { useTranslation } from "react-i18next"; +import NavBarButton from "../components/NavBarButton"; +import { DateTime } from "luxon"; + +type Props = { + dateRange: [DateTime, DateTime], + setDateRange: (dateRange: [DateTime, DateTime]) => void, +} +const MetricsDateSelect = ({ dateRange, setDateRange }: Props) => { + + const { t } = useTranslation(); + const { colors } = useTheme(); + const [open, setOpen] = useState(false); + const todayDate = useMemo(() => new Date(), []); + const dateRangeAsJSDate = useMemo(() => + [ dateRange[0].toJSDate(), dateRange[1].toJSDate() ], + [dateRange]); + + const onDismiss = useCallback(() => { + setOpen(false); + }, [setOpen]); + + const onChoose = useCallback(({ startDate, endDate }) => { + setOpen(false); + setDateRange([ + DateTime.fromJSDate(startDate).startOf('day'), + DateTime.fromJSDate(endDate).startOf('day') + ]); + }, [setOpen, setDateRange]); + + return (<> + setOpen(true)}> + {dateRange[0] && (<> + {dateRange[0].toLocaleString()} + + )} + {dateRange[1]?.toLocaleString() || t('diary.today')} + + + ); +}; + +export const s = StyleSheet.create({ + divider: { + width: '3ch', + marginHorizontal: 'auto', + } +}); + +export default MetricsDateSelect; diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index f53ed9e2f..873ee5f02 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -8,45 +8,58 @@ import { DateTime } from "luxon"; import { UserMetrics } from "./metricsTypes"; import MetricsCard from "./MetricsCard"; import { useImperialConfig } from "../config/useImperialConfig"; +import MetricsDateSelect from "./MetricsDateSelect"; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; +async function fetchMetricsFromServer(type: 'user'|'aggregate', dateRange: [DateTime, DateTime]) { + const CommHelper = getAngularService('CommHelper'); + const query = { + freq: 'D', + start_time: dateRange[0].toSeconds(), + end_time: dateRange[1].toSeconds(), + metric_list: METRIC_LIST, + is_return_aggregate: (type == 'aggregate'), + } + if (type == 'user') + return CommHelper.getMetrics('timestamp', query); + return CommHelper.getAggregateData("result/metrics/timestamp", query); +} + const MetricsTab = () => { const { t } = useTranslation(); const { distanceSuffix, speedSuffix } = useImperialConfig(); const CommHelper = getAngularService('CommHelper'); - const [userMetrics, setUserMetrics] = useState(null); - // const [aggMetrics, setAggMetrics] = useState({}); - - const tsRange = useMemo(() => { + const [dateRange, setDateRange] = useState<[DateTime, DateTime]>(() => { const now = DateTime.utc().startOf('day'); const start = now.minus({ days: 14 }); const end = now.minus({ days: 1 }); - return { - start_time: start.toSeconds(), - end_time: end.toSeconds() - } - }, []) + return [start, end]; + }); + const [metricsPopulation, setMetricsPopulation] = useState<'user'|'aggregate'>('user'); + const [aggMetrics, setAggMetrics] = useState(null); + const [userMetrics, setUserMetrics] = useState(null); useEffect(() => { - fetchMetricsFromServer(); - }, []); + loadMetricsForPopulation('user', dateRange); + loadMetricsForPopulation('aggregate', dateRange); + }, [dateRange]); - async function fetchMetricsFromServer() { - const serverResponse = await CommHelper.getMetrics('timestamp', { - freq: 'D', - ...tsRange, - metric_list: METRIC_LIST, - is_return_aggregate: false - }); + async function loadMetricsForPopulation(population: 'user'|'aggregate', dateRange: [DateTime, DateTime]) { + const serverResponse = await fetchMetricsFromServer(population, dateRange); console.debug("Got metrics = ", serverResponse); - const userMetrics = {}; + const metrics = {}; + const dataKey = (population == 'user') ? 'user_metrics' : 'aggregate_metrics'; METRIC_LIST.forEach((metricName, i) => { - userMetrics[metricName] = serverResponse['user_metrics'][i]; + metrics[metricName] = serverResponse[dataKey][i]; }); - setUserMetrics(userMetrics as UserMetrics); + if (population == 'user') { + setUserMetrics(metrics as UserMetrics); + } else { + setAggMetrics(metrics as UserMetrics); + } } function refresh() { @@ -59,9 +72,7 @@ const MetricsTab = () => { return (<> - {}}> - Date - + refresh()} /> From 5ebced849bd8206344208a870245a87e2153fbe1 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 13:44:54 -0400 Subject: [PATCH 016/164] adjust padding in MetricsCard The chart does not need so much padding - we want to be able to show as much detail as we can. MetricDetails should show with 2 columns, but could use some padding in between. So let's remove all the padding from the card content since its children will have padding anyway. --- www/js/components/BarChart.tsx | 2 +- www/js/metrics/MetricDetails.tsx | 4 ++-- www/js/metrics/MetricsCard.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 6da3d2a2b..a294230d7 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -65,7 +65,7 @@ const BarChart = ({ chartData, axisTitle, lineAnnotations=null, isHorizontal=fal } return ( - + ({ diff --git a/www/js/metrics/MetricDetails.tsx b/www/js/metrics/MetricDetails.tsx index 3f2e2c795..aea330c7c 100644 --- a/www/js/metrics/MetricDetails.tsx +++ b/www/js/metrics/MetricDetails.tsx @@ -23,9 +23,9 @@ const MetricsDetails = ({ metricDataDays, style }: Props) => { }, [metricDataDays]); return ( - + { Object.keys(metricValues).map((label, i) => - + {label} {metricValues[label]} diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index b9729020f..a962e4af1 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -63,7 +63,7 @@ const MetricsCard = ({cardTitle, metricDataDays, axisUnits, ...rest}: Props) => }]} /> } style={{backgroundColor: colors.primary, paddingHorizontal: 8, minHeight: 60}} /> - + {viewMode=='details' ? : From f32609eb1d2b3d9f593ceac22c28aba23c2768bc Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 14:05:09 -0400 Subject: [PATCH 017/164] MetricsCard fix typing of Props --- www/js/metrics/MetricsCard.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index a962e4af1..5422d9c80 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -1,17 +1,18 @@ import React, { useMemo, useState } from 'react'; -import { Card, CardProps, SegmentedButtons, useTheme} from 'react-native-paper'; +import { Card, SegmentedButtons, useTheme} from 'react-native-paper'; import BarChart from '../components/BarChart'; import MetricsDetails from './MetricDetails'; import { DayOfMetricData } from './metricsTypes'; import { getUniqueLabelsForDays } from './metricsHelper'; -type Props = CardProps & { +type Props = { + style: any, cardTitle: string, metricDataDays: DayOfMetricData[], axisUnits: string, } -const MetricsCard = ({cardTitle, metricDataDays, axisUnits, ...rest}: Props) => { +const MetricsCard = ({cardTitle, metricDataDays, axisUnits, style}: Props) => { const { colors } = useTheme(); @@ -34,7 +35,7 @@ const MetricsCard = ({cardTitle, metricDataDays, axisUnits, ...rest}: Props) => }, [metricDataDays]); return ( - + Date: Wed, 23 Aug 2023 14:52:32 -0400 Subject: [PATCH 018/164] add ActiveMinutesCard to new dashboard The "active minutes" should display the cumulative duration for modes that are considered "active" - currenty this is just 'walk' and 'bike'; I am not sure if anything else will count. This should include a graph later on, but right now it just lists out the minutes for each mode. --- www/js/metrics/ActiveMinutesCard.tsx | 47 ++++++++++++++++++++++++++++ www/js/metrics/MetricsTab.tsx | 12 +++++++ 2 files changed, 59 insertions(+) create mode 100644 www/js/metrics/ActiveMinutesCard.tsx diff --git a/www/js/metrics/ActiveMinutesCard.tsx b/www/js/metrics/ActiveMinutesCard.tsx new file mode 100644 index 000000000..98031b9b9 --- /dev/null +++ b/www/js/metrics/ActiveMinutesCard.tsx @@ -0,0 +1,47 @@ + +import React, { useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { Card, Text, useTheme} from 'react-native-paper'; +import { MetricsData } from './metricsTypes'; +import { cardStyles } from './MetricsTab'; +import { useImperialConfig } from '../config/useImperialConfig'; + +const ACTIVE_MODES = ['walk', 'bike']; + +type Props = { userMetrics: MetricsData } +const ActiveMinutesCard = ({ userMetrics }: Props) => { + + const { colors } = useTheme(); + + // number of seconds for each of [walk, bike] + const activeModesDurations = useMemo(() => { + if (!userMetrics?.duration) return []; + return ACTIVE_MODES.map(mode => { + const sum = userMetrics.duration.reduce((acc, day) => ( + acc + (day[`label_${mode}`] || 0) + ), 0); + return sum; + }); + }, [userMetrics?.duration]); + + return ( + + + + { activeModesDurations.map((mode, i) => ( + + {ACTIVE_MODES[i]} + {Math.round(activeModesDurations[i] / 60) + ' minutes'}{/* TODO i18n*/} + + ))} + + + ) +} + +export default ActiveMinutesCard; diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 873ee5f02..555c90e65 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -9,6 +9,7 @@ import { UserMetrics } from "./metricsTypes"; import MetricsCard from "./MetricsCard"; import { useImperialConfig } from "../config/useImperialConfig"; import MetricsDateSelect from "./MetricsDateSelect"; +import ActiveMinutesCard from "./ActiveMinutesCard"; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; @@ -76,6 +77,7 @@ const MetricsTab = () => { refresh()} /> + ({ + backgroundColor: colors.primary, paddingHorizontal: 8, minHeight: 60 + }), + titleText: (colors) => ({ + color: colors.onPrimary, + fontWeight: '500', + textAlign: 'center' + }), +} angularize(MetricsTab, 'MetricsTab', 'emission.main.metricstab'); export default MetricsTab; From f93ba1e8b3e7650629b6569613fc2cae1dbf23cf Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 14:53:35 -0400 Subject: [PATCH 019/164] rename UserMetrics type to MetricsData The same type will be used for both user metrics and aggregate metrics, so its naming should be more generic. --- www/js/metrics/MetricsTab.tsx | 11 +++++------ www/js/metrics/metricsTypes.ts | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 555c90e65..96a8eb9cf 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -5,7 +5,7 @@ import { Appbar } from "react-native-paper"; import NavBarButton from "../components/NavBarButton"; import { useTranslation } from "react-i18next"; import { DateTime } from "luxon"; -import { UserMetrics } from "./metricsTypes"; +import { MetricsData } from "./metricsTypes"; import MetricsCard from "./MetricsCard"; import { useImperialConfig } from "../config/useImperialConfig"; import MetricsDateSelect from "./MetricsDateSelect"; @@ -39,9 +39,8 @@ const MetricsTab = () => { const end = now.minus({ days: 1 }); return [start, end]; }); - const [metricsPopulation, setMetricsPopulation] = useState<'user'|'aggregate'>('user'); - const [aggMetrics, setAggMetrics] = useState(null); - const [userMetrics, setUserMetrics] = useState(null); + const [aggMetrics, setAggMetrics] = useState(null); + const [userMetrics, setUserMetrics] = useState(null); useEffect(() => { loadMetricsForPopulation('user', dateRange); @@ -57,9 +56,9 @@ const MetricsTab = () => { metrics[metricName] = serverResponse[dataKey][i]; }); if (population == 'user') { - setUserMetrics(metrics as UserMetrics); + setUserMetrics(metrics as MetricsData); } else { - setAggMetrics(metrics as UserMetrics); + setAggMetrics(metrics as MetricsData); } } diff --git a/www/js/metrics/metricsTypes.ts b/www/js/metrics/metricsTypes.ts index ee8b3e3f9..d51c98b3a 100644 --- a/www/js/metrics/metricsTypes.ts +++ b/www/js/metrics/metricsTypes.ts @@ -9,6 +9,6 @@ export type DayOfMetricData = LabelProps & { local_dt: {[k: string]: any}, // TODO type datetime obj } -export type UserMetrics = { +export type MetricsData = { [key in MetricName]: DayOfMetricData[] } From 602691809e92938d2756708c361fe741ac901e8a Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 14:54:53 -0400 Subject: [PATCH 020/164] add @types/react to devDependencies This adds type definitions for the React hooks and makes some of the annoying typing errors go away. I don't know why the React package doesn't have types built-in, but this is how you get them. --- package.cordovabuild.json | 1 + package.serve.json | 1 + 2 files changed, 2 insertions(+) diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 72c8304aa..c508cb004 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -23,6 +23,7 @@ "@babel/preset-typescript": "^7.21.4", "@ionic/cli": "6.20.8", "@types/luxon": "^3.3.0", + "@types/react": "^18.2.20", "babel-loader": "^9.1.2", "babel-plugin-angularjs-annotate": "^0.10.0", "babel-plugin-optional-require": "^0.3.1", diff --git a/package.serve.json b/package.serve.json index 88dc636a8..b2e3b96c8 100644 --- a/package.serve.json +++ b/package.serve.json @@ -22,6 +22,7 @@ "@babel/preset-typescript": "^7.21.4", "@ionic/cli": "6.20.8", "@types/luxon": "^3.3.0", + "@types/react": "^18.2.20", "babel-loader": "^9.1.2", "babel-plugin-optional-require": "^0.3.1", "concurrently": "^8.0.1", From eb60ad59418aef59ab9c6d3ed7e131c725cef99f Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 18:40:29 -0400 Subject: [PATCH 021/164] refactor & memoize seconds to minutes calculation --- www/js/metrics/ActiveMinutesCard.tsx | 7 ++++--- www/js/metrics/metricsHelper.ts | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/www/js/metrics/ActiveMinutesCard.tsx b/www/js/metrics/ActiveMinutesCard.tsx index 98031b9b9..3b16251e5 100644 --- a/www/js/metrics/ActiveMinutesCard.tsx +++ b/www/js/metrics/ActiveMinutesCard.tsx @@ -5,6 +5,7 @@ import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; import { useImperialConfig } from '../config/useImperialConfig'; +import { secondsToMinutes } from './metricsHelper'; const ACTIVE_MODES = ['walk', 'bike']; @@ -13,14 +14,14 @@ const ActiveMinutesCard = ({ userMetrics }: Props) => { const { colors } = useTheme(); - // number of seconds for each of [walk, bike] + // number of minutes for each of [walk, bike] const activeModesDurations = useMemo(() => { if (!userMetrics?.duration) return []; return ACTIVE_MODES.map(mode => { const sum = userMetrics.duration.reduce((acc, day) => ( acc + (day[`label_${mode}`] || 0) ), 0); - return sum; + return secondsToMinutes(sum); }); }, [userMetrics?.duration]); @@ -36,7 +37,7 @@ const ActiveMinutesCard = ({ userMetrics }: Props) => { { activeModesDurations.map((mode, i) => ( {ACTIVE_MODES[i]} - {Math.round(activeModesDurations[i] / 60) + ' minutes'}{/* TODO i18n*/} + {`${activeModesDurations[i]} minutes`}{/* TODO i18n*/} ))} diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index 04492c65e..385988842 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -12,3 +12,6 @@ export function getUniqueLabelsForDays(metricDataDays: DayOfMetricData[]) { }); return uniqueLabels; } + +export const secondsToMinutes = (seconds: number) => + Math.round(seconds / 60); From 18eafef49dd89639965aa1cf13278cf943f15cad Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 18:43:51 -0400 Subject: [PATCH 022/164] unify MetricDetails and MetricsCard It's not really necessary for MetricDetails to be its own component - it is actually simpler to just include these few lines of code in MetricsCard. Then MetricsCard will handle the memoized computation either way, whether it's `chartData` or `metricSumValues` that is needed (depending on whether 'graph' or 'details' is active) --- www/js/metrics/MetricsCard.tsx | 40 ++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index 5422d9c80..67197110e 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -1,8 +1,8 @@ import React, { useMemo, useState } from 'react'; -import { Card, SegmentedButtons, useTheme} from 'react-native-paper'; +import { View } from 'react-native'; +import { Card, SegmentedButtons, Text, useTheme } from 'react-native-paper'; import BarChart from '../components/BarChart'; -import MetricsDetails from './MetricDetails'; import { DayOfMetricData } from './metricsTypes'; import { getUniqueLabelsForDays } from './metricsHelper'; @@ -14,14 +14,14 @@ type Props = { } const MetricsCard = ({cardTitle, metricDataDays, axisUnits, style}: Props) => { - const { colors } = useTheme(); - + const { colors } = useTheme(); const [viewMode, setViewMode] = useState<'details'|'graph'>('details'); + + // for each label, format data for chart, with a record for each day with that label const chartData = useMemo(() => { - if (!metricDataDays) return []; + if (!metricDataDays || viewMode != 'graph') return []; const uniqueLabels = getUniqueLabelsForDays(metricDataDays); - // for each label, format data for chart, with a record for each day with that label return uniqueLabels.map((label, i) => { const daysWithThisLabel = metricDataDays.filter(e => e[`label_${label}`]); return { @@ -32,6 +32,21 @@ const MetricsCard = ({cardTitle, metricDataDays, axisUnits, style}: Props) => { })) } }); + }, [metricDataDays, viewMode]); + + // for each label, sum up cumulative values across all days + const metricSumValues = useMemo(() => { + if (!metricDataDays || viewMode != 'details') return []; + const uniqueLabels = getUniqueLabelsForDays(metricDataDays); + + // for each label, sum up cumulative values across all days + const vals = {}; + uniqueLabels.forEach(label => { + vals[label] = metricDataDays.reduce((acc, day) => ( + acc + (day[`label_${label}`] || 0) + ), 0); + }); + return vals; }, [metricDataDays]); return ( @@ -65,9 +80,16 @@ const MetricsCard = ({cardTitle, metricDataDays, axisUnits, style}: Props) => { } style={{backgroundColor: colors.primary, paddingHorizontal: 8, minHeight: 60}} /> - {viewMode=='details' ? - - : + {viewMode=='details' && + + { Object.keys(metricSumValues).map((label, i) => + + {label} + {metricSumValues[label]} + + )} + + } {viewMode=='graph' && } From db062569d76ac9e80688d70c1d9d42e9dda39150 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 18:46:27 -0400 Subject: [PATCH 023/164] delete MetricDetails.tsx It was incorporated into MetricsCard and is no longer a standalone component --- www/js/metrics/MetricDetails.tsx | 37 -------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 www/js/metrics/MetricDetails.tsx diff --git a/www/js/metrics/MetricDetails.tsx b/www/js/metrics/MetricDetails.tsx deleted file mode 100644 index aea330c7c..000000000 --- a/www/js/metrics/MetricDetails.tsx +++ /dev/null @@ -1,37 +0,0 @@ - -import React, { useMemo } from 'react'; -import { View } from 'react-native'; -import { Text } from 'react-native-paper'; -import { DayOfMetricData } from './metricsTypes'; -import { getUniqueLabelsForDays } from './metricsHelper'; - -type Props = { metricDataDays: DayOfMetricData[], style: any }; -const MetricsDetails = ({ metricDataDays, style }: Props) => { - - const metricValues = useMemo(() => { - if (!metricDataDays) return []; - const uniqueLabels = getUniqueLabelsForDays(metricDataDays); - - // for each label, sum up cumulative values across all days - const vals = {}; - uniqueLabels.forEach(label => { - vals[label] = metricDataDays.reduce((acc, day) => ( - acc + (day[`label_${label}`] || 0) - ), 0); - }); - return vals; - }, [metricDataDays]); - - return ( - - { Object.keys(metricValues).map((label, i) => - - {label} - {metricValues[label]} - - )} - - ); -} - -export default MetricsDetails; From cf4f0e6631bd8ea360fa40ee2619c497a3a6d10c Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 20:13:11 -0400 Subject: [PATCH 024/164] generalize format functions in useImperialConfig It is useful to have the format function (which handles rounding to 2 or 3 sig figs) generic enough to work for any unit. We can also have it use Intl.NumberFormat to work better with other languages (other languages might use commas and periods differently, or have a different decimal marking altogether) --- www/js/config/useImperialConfig.ts | 55 +++++++++++++++++------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/www/js/config/useImperialConfig.ts b/www/js/config/useImperialConfig.ts index 3ddb287a2..855c853d9 100644 --- a/www/js/config/useImperialConfig.ts +++ b/www/js/config/useImperialConfig.ts @@ -1,33 +1,38 @@ import React, { useEffect, useState } from "react"; import useAppConfig from "../useAppConfig"; +import i18next from "i18next"; const KM_TO_MILES = 0.621371; -/* formatting distances for display: - - if distance >= 100, round to the nearest integer - e.g. "105 mi", "167 km" - - if 1 <= distance < 100, round to 3 significant digits - e.g. "7.02 mi", "11.3 km" - - if distance < 1, round to 2 significant digits +const MPS_TO_KMPH = 3.6; + +// it might make sense to move this to a more general location in the codebase +/* formatting units for display: + - if value >= 100, round to the nearest integer + e.g. "105 mi", "119 kmph" + - if 1 <= value < 100, round to 3 significant digits + e.g. "7.02 km", "11.3 mph" + - if value < 1, round to 2 significant digits e.g. "0.47 mi", "0.75 km" */ -const formatDistance = (dist: number) => { - if (dist < 1) - return dist.toPrecision(2); - if (dist < 100) - return dist.toPrecision(3); - return Math.round(dist).toString(); +export const formatForDisplay = (value: number): string => { + let opts: Intl.NumberFormatOptions = {}; + if (value < 100) + opts.maximumSignificantDigits = 3; + else if (value < 1) + opts.maximumFractionDigits = 2; + return Intl.NumberFormat(i18next.language, opts).format(value); } -const getFormattedDistanceInKm = (distInMeters: string) => - formatDistance(Number.parseFloat(distInMeters) / 1000); - -const getFormattedDistanceInMiles = (distInMeters: string) => - formatDistance((Number.parseFloat(distInMeters) / 1000) * KM_TO_MILES); - -const getKmph = (metersPerSec) => - (metersPerSec * 3.6).toFixed(2); +const convertDistance = (distMeters: number, imperial: boolean): number => { + if (imperial) + return (distMeters / 1000) * KM_TO_MILES; + return distMeters / 1000; +} -const getMph = (metersPerSecond) => - (KM_TO_MILES * Number.parseFloat(getKmph(metersPerSecond))).toFixed(2); +const convertSpeed = (speedMetersPerSec: number, imperial: boolean): number => { + if (imperial) + return speedMetersPerSec * MPS_TO_KMPH * KM_TO_MILES; + return speedMetersPerSec * MPS_TO_KMPH; +} export function useImperialConfig() { const { appConfig, loading } = useAppConfig(); @@ -41,7 +46,9 @@ export function useImperialConfig() { return { distanceSuffix: useImperial ? "mi" : "km", speedSuffix: useImperial ? "mph" : "kmph", - getFormattedDistance: useImperial ? getFormattedDistanceInMiles : getFormattedDistanceInKm, - getFormattedSpeed: useImperial ? getMph : getKmph, + getFormattedDistance: useImperial ? (d) => formatForDisplay(convertDistance(d, true)) + : (d) => formatForDisplay(convertDistance(d, false)), + getFormattedSpeed: useImperial ? (s) => formatForDisplay(convertSpeed(s, true)) + : (s) => formatForDisplay(convertSpeed(s, false)), } } From 964da4b161153ea170bd301e1e320f476e0d7140 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 20:41:22 -0400 Subject: [PATCH 025/164] fix formatForDisplay --- www/js/config/useImperialConfig.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/www/js/config/useImperialConfig.ts b/www/js/config/useImperialConfig.ts index 855c853d9..58af79551 100644 --- a/www/js/config/useImperialConfig.ts +++ b/www/js/config/useImperialConfig.ts @@ -11,13 +11,15 @@ const MPS_TO_KMPH = 3.6; e.g. "105 mi", "119 kmph" - if 1 <= value < 100, round to 3 significant digits e.g. "7.02 km", "11.3 mph" - - if value < 1, round to 2 significant digits - e.g. "0.47 mi", "0.75 km" */ + - if value < 1, round to 2 decimal places + e.g. "0.07 mi", "0.75 km" */ export const formatForDisplay = (value: number): string => { let opts: Intl.NumberFormatOptions = {}; - if (value < 100) + if (value >= 100) + opts.maximumFractionDigits = 0; + else if (value >= 1) opts.maximumSignificantDigits = 3; - else if (value < 1) + else opts.maximumFractionDigits = 2; return Intl.NumberFormat(i18next.language, opts).format(value); } From c417879fd658c037afb9c41e74f0d4db8147ef96 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 21:09:45 -0400 Subject: [PATCH 026/164] show units correctly on MetricsCards Using a prop passed to MetricsCard called unitFormatFn, we will format the raw numerical values as human-readable strings in the correct unit of measurement. --- www/js/metrics/MetricsCard.tsx | 9 ++++++--- www/js/metrics/MetricsTab.tsx | 15 ++++++++++----- www/js/metrics/metricsHelper.ts | 6 +++++- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index 67197110e..3f92f0017 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -11,8 +11,10 @@ type Props = { cardTitle: string, metricDataDays: DayOfMetricData[], axisUnits: string, + unitFormatFn?: (val: number) => string|number, + style: any, } -const MetricsCard = ({cardTitle, metricDataDays, axisUnits, style}: Props) => { +const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, unitFormatFn, style}: Props) => { const { colors } = useTheme(); const [viewMode, setViewMode] = useState<'details'|'graph'>('details'); @@ -42,9 +44,10 @@ const MetricsCard = ({cardTitle, metricDataDays, axisUnits, style}: Props) => { // for each label, sum up cumulative values across all days const vals = {}; uniqueLabels.forEach(label => { - vals[label] = metricDataDays.reduce((acc, day) => ( + const sum = metricDataDays.reduce((acc, day) => ( acc + (day[`label_${label}`] || 0) ), 0); + vals[label] = unitFormatFn ? unitFormatFn(sum) : sum; }); return vals; }, [metricDataDays]); @@ -85,7 +88,7 @@ const MetricsCard = ({cardTitle, metricDataDays, axisUnits, style}: Props) => { { Object.keys(metricSumValues).map((label, i) => {label} - {metricSumValues[label]} + {metricSumValues[label] + ' ' + axisUnits} )} diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 96a8eb9cf..1819573c3 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -7,9 +7,10 @@ import { useTranslation } from "react-i18next"; import { DateTime } from "luxon"; import { MetricsData } from "./metricsTypes"; import MetricsCard from "./MetricsCard"; -import { useImperialConfig } from "../config/useImperialConfig"; +import { formatForDisplay, useImperialConfig } from "../config/useImperialConfig"; import MetricsDateSelect from "./MetricsDateSelect"; import ActiveMinutesCard from "./ActiveMinutesCard"; +import { secondsToHours, secondsToMinutes } from "./metricsHelper"; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; @@ -30,8 +31,8 @@ async function fetchMetricsFromServer(type: 'user'|'aggregate', dateRange: [Date const MetricsTab = () => { const { t } = useTranslation(); - const { distanceSuffix, speedSuffix } = useImperialConfig(); - const CommHelper = getAngularService('CommHelper'); + const { getFormattedSpeed, speedSuffix, + getFormattedDistance, distanceSuffix } = useImperialConfig(); const [dateRange, setDateRange] = useState<[DateTime, DateTime]>(() => { const now = DateTime.utc().startOf('day'); @@ -86,18 +87,22 @@ const MetricsTab = () => { diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index 385988842..c6cd05119 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -1,3 +1,4 @@ +import { formatForDisplay } from "../config/useImperialConfig"; import { DayOfMetricData } from "./metricsTypes"; export function getUniqueLabelsForDays(metricDataDays: DayOfMetricData[]) { @@ -14,4 +15,7 @@ export function getUniqueLabelsForDays(metricDataDays: DayOfMetricData[]) { } export const secondsToMinutes = (seconds: number) => - Math.round(seconds / 60); + formatForDisplay(seconds / 60); + +export const secondsToHours = (seconds: number) => + formatForDisplay(seconds / 3600); From 7a3a6c6ac8e83adfb00c0f7baf8424db0ce8661a Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 21:12:49 -0400 Subject: [PATCH 027/164] support aggregate metrics on MetricsCard Added a second pair of SegmentedButtons to allow toggling between 'user' and 'aggregate' metrics. So now there are two toggles - instead of repeating all that SegmentedButtons code, I extracted it out into a new ToggleSwitch component, which is a generic wrapper around SegmentedButtons for our particular uses. Each MetricsCard now accepts both userMetricsDays and aggMetricsDays, instead of just one metricDataDays, and it determines which to use based on the value of the toggleswitch - "populationMode" --- www/js/metrics/MetricsCard.tsx | 35 ++++++++++++--------------------- www/js/metrics/MetricsTab.tsx | 12 +++++++---- www/js/metrics/ToggleSwitch.tsx | 30 ++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 26 deletions(-) create mode 100644 www/js/metrics/ToggleSwitch.tsx diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index 3f92f0017..f644b7b05 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -5,11 +5,12 @@ import { Card, SegmentedButtons, Text, useTheme } from 'react-native-paper'; import BarChart from '../components/BarChart'; import { DayOfMetricData } from './metricsTypes'; import { getUniqueLabelsForDays } from './metricsHelper'; +import ToggleSwitch from './ToggleSwitch'; type Props = { - style: any, cardTitle: string, - metricDataDays: DayOfMetricData[], + userMetricsDays: DayOfMetricData[], + aggMetricsDays: DayOfMetricData[], axisUnits: string, unitFormatFn?: (val: number) => string|number, style: any, @@ -18,6 +19,10 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni const { colors } = useTheme(); const [viewMode, setViewMode] = useState<'details'|'graph'>('details'); + const [populationMode, setPopulationMode] = useState<'user'|'aggregate'>('user'); + const metricDataDays = useMemo(() => ( + populationMode == 'user' ? userMetricsDays : aggMetricsDays + ), [populationMode, userMetricsDays, aggMetricsDays]); // for each label, format data for chart, with a record for each day with that label const chartData = useMemo(() => { @@ -60,26 +65,12 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni titleStyle={{color: colors.onPrimary, fontWeight: '500', textAlign: 'center'}} titleNumberOfLines={2} right={() => - setViewMode(v)} - density='medium' - buttons={[{ - icon: 'abacus', value: 'details', - uncheckedColor: colors.onSurfaceDisabled, - style: { - minWidth: 0, - backgroundColor: viewMode == 'details' ? colors.elevation.level2 : colors.surfaceDisabled - }, - showSelectedCheck: true - }, { - icon: 'chart-bar', - uncheckedColor: colors.onSurfaceDisabled, - value: 'graph', - style: { - minWidth: 0, - backgroundColor: viewMode == 'graph' ? colors.elevation.level2 : colors.surfaceDisabled - }, - showSelectedCheck: true - }]} /> + + + + } style={{backgroundColor: colors.primary, paddingHorizontal: 8, minHeight: 60}} /> diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 1819573c3..aeff78978 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -85,22 +85,26 @@ const MetricsTab = () => { // @ts-ignore, RN doesn't recognize `scrollSnapType`, but it does work on RN Web style={{scrollSnapType: 'x mandatory', paddingVertical: 10}}> diff --git a/www/js/metrics/ToggleSwitch.tsx b/www/js/metrics/ToggleSwitch.tsx new file mode 100644 index 000000000..5b0ed47e2 --- /dev/null +++ b/www/js/metrics/ToggleSwitch.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { SegmentedButtons, useTheme } from "react-native-paper"; + +type Props = { + value: string, + setValue: (value: string) => void, + options: { icon: string, value: string }[], +} +const ToggleSwitch = ({ value, setValue, options }) => { + + const { colors} = useTheme(); + + return ( + setValue(v)} + density='high' + buttons={options.map(o => ({ + value: o.value, + icon: o.icon, + uncheckedColor: colors.onSurfaceDisabled, + showSelectedCheck: true, + style: { + minWidth: 0, + backgroundColor: value == o.value ? colors.elevation.level2 : colors.surfaceDisabled + }, + }))} /> + ) +} + +export default ToggleSwitch; From b04aab791cc212b33f2ef95f873959fe7a59bb44 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 21:38:23 -0400 Subject: [PATCH 028/164] fix MetricsCard not always updating details view viewMode was not in the dependency array, so if it was changed in isolation, metricSumValues would not recompute. --- www/js/metrics/MetricsCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index f644b7b05..b5f1ad25b 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -55,7 +55,7 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni vals[label] = unitFormatFn ? unitFormatFn(sum) : sum; }); return vals; - }, [metricDataDays]); + }, [metricDataDays, viewMode]); return ( From b6dfb4f9cc543720774eab1400c798ecf424474e Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 21:42:56 -0400 Subject: [PATCH 029/164] fix vertical scrolling on new Dashboard tab I noticed that when the content was tall enough to allow vertical scrolling, you could not scroll to see the bottom even though the content was in a ScrollView. Upon inspection, the new Dashboard tab was growing infinitely tall, past the bottom of its Angular parent container. We've had this happen a couple times before when embedding React components into Angular views. The way we get around this is with a CSS class called 'fill-container'. I modified it slightly to only affect the *direct* child, not all grand- and great-grand-children. Without this modification, the entire tab would be hidden from view. --- www/css/style.css | 2 +- www/js/main.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/www/css/style.css b/www/css/style.css index 9a6f708bd..401ac7814 100644 --- a/www/css/style.css +++ b/www/css/style.css @@ -26,7 +26,7 @@ position: relative; } -.fill-container div[class*='css-'] { +.fill-container > div[class*='css-'] { height: 100%; width: 100%; position: absolute; diff --git a/www/js/main.js b/www/js/main.js index bf1c7d876..f8f5371e4 100644 --- a/www/js/main.js +++ b/www/js/main.js @@ -28,7 +28,7 @@ angular.module('emission.main', ['emission.main.diary', views: { 'main-metrics': { // templateUrl: 'templates/main-metrics.html', - template: "", + template: ``, controller: 'MetricsCtrl' } } From d0763cd64e89dc5a0aea493e28459bb4c8f82432 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 23 Aug 2023 22:34:08 -0400 Subject: [PATCH 030/164] use i18n for ActiveMinutesCard --- www/i18n/en.json | 1 + www/js/metrics/ActiveMinutesCard.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index 0949d5c5c..4c71a4d98 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -106,6 +106,7 @@ "pick-a-date": "Pick a date", "trips": "trips", "hours": "hours", + "minutes": "minutes", "custom": "Custom" }, diff --git a/www/js/metrics/ActiveMinutesCard.tsx b/www/js/metrics/ActiveMinutesCard.tsx index 3b16251e5..c6688ae37 100644 --- a/www/js/metrics/ActiveMinutesCard.tsx +++ b/www/js/metrics/ActiveMinutesCard.tsx @@ -6,6 +6,7 @@ import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; import { useImperialConfig } from '../config/useImperialConfig'; import { secondsToMinutes } from './metricsHelper'; +import { useTranslation } from 'react-i18next'; const ACTIVE_MODES = ['walk', 'bike']; @@ -13,6 +14,7 @@ type Props = { userMetrics: MetricsData } const ActiveMinutesCard = ({ userMetrics }: Props) => { const { colors } = useTheme(); + const { t } = useTranslation(); // number of minutes for each of [walk, bike] const activeModesDurations = useMemo(() => { @@ -28,7 +30,7 @@ const ActiveMinutesCard = ({ userMetrics }: Props) => { return ( { { activeModesDurations.map((mode, i) => ( {ACTIVE_MODES[i]} - {`${activeModesDurations[i]} minutes`}{/* TODO i18n*/} + {`${mode} ${t('metrics.minutes')}`} ))} From d074cab770b3e1cb216ccf37a1135961ffb7f209 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 24 Aug 2023 16:35:54 -0400 Subject: [PATCH 031/164] update BarChart for categorical data & flat inputs We have been showing time-based data on the BarChart, where one of the axes (depending if horizontal or vertical) has values as Unix timestamps. We'd like this component to also work with categorical data, so we will have a boolean prop `timeAxis` to say whether it should be timestamps (true) or categories (false). Also, the way BarChart receives input data has been changed. ChartJS accepts input, categorized by 'datasets' which have their own label. For us, this correlates to the different labeled modes. However, the metrics we get from the server lend themselves better to a 'flattened' structure - we receive an array of days, and what labels were for that day; we don't receive an array of labels with what their value was on each day. So we would have to flip this data around in each place that the BarChart was used. Instead, it's simpler for BarChart to accept a flattened structure - and we can do the flipping here in BarChart before the flipped data is fed into ChartJS. And to better keep track of all this, let's introduce typings for the input, flat 'records' and for the resulting, flipped 'chartData'. --- www/js/components/BarChart.tsx | 156 ++++++++++++++------------------- 1 file changed, 67 insertions(+), 89 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index a294230d7..22691fc37 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -1,10 +1,9 @@ -import React, { useRef, useState } from 'react'; -import { array, string, bool } from 'prop-types'; +import React, { useRef, useState, useMemo } from 'react'; import { angularize } from '../angular-react-helper'; import { View } from 'react-native'; import { useTheme } from 'react-native-paper'; -import { Chart, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, TimeScale } from 'chart.js'; +import { Chart, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, TimeScale, ChartData } from 'chart.js'; import { Bar } from 'react-chartjs-2'; import Annotation, { AnnotationOptions } from 'chartjs-plugin-annotation'; @@ -19,27 +18,47 @@ Chart.register( Annotation, ); -const BarChart = ({ chartData, axisTitle, lineAnnotations=null, isHorizontal=false }) => { +type BarChartData = { + label: string, + data: { x: number|string, y: number|string }[], +}[]; + +type Props = { + records: { label: string, x: number|string, y: number|string }[], + axisTitle: string, + lineAnnotations?: { value: number, label: string }[], + isHorizontal?: boolean, + timeAxis?: boolean, +} +const BarChart = ({ records, axisTitle, lineAnnotations=null, isHorizontal=false, timeAxis }: Props) => { const { colors } = useTheme(); const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); + const indexAxis = isHorizontal ? 'y' : 'x'; const barChartRef = useRef(null); - const defaultPalette = [ - '#c95465', // red oklch(60% 0.15 14) - '#4a71b1', // blue oklch(55% 0.11 260) - '#d2824e', // orange oklch(68% 0.12 52) - '#856b5d', // brown oklch(55% 0.04 50) - '#59894f', // green oklch(58% 0.1 140) - '#e0cc55', // yellow oklch(84% 0.14 100) - '#b273ac', // purple oklch(64% 0.11 330) - '#f09da6', // pink oklch(78% 0.1 12) - '#b3aca8', // grey oklch(75% 0.01 55) - '#80afad', // teal oklch(72% 0.05 192) - ] - - const indexAxis = isHorizontal ? 'y' : 'x'; + // group records by label (this is the format that Chart.js expects) + const chartData = useMemo(() => { + return records?.reduce((acc, record) => { + const existing = acc.find(e => e.label == record.label); + if (!existing) { + acc.push({ + label: record.label, + data: [{ + x: record.x, + y: record.y, + }], + }); + } else { + existing.data.push({ + x: record.x, + y: record.y, + }); + } + return acc; + }, [] as BarChartData); + }, [records]); function getChartHeight() { /* when horizontal charts have more data, they should get taller @@ -47,7 +66,7 @@ const BarChart = ({ chartData, axisTitle, lineAnnotations=null, isHorizontal=fal if (isHorizontal) { // 'ideal' chart height is based on the number of datasets and number of unique index values const uniqueIndexVals = []; - chartData.forEach(e => e.records.forEach(r => { + chartData.forEach(e => e.data.forEach(r => { if (!uniqueIndexVals.includes(r[indexAxis])) uniqueIndexVals.push(r[indexAxis]); })); const numIndexVals = uniqueIndexVals.length; @@ -60,21 +79,17 @@ const BarChart = ({ chartData, axisTitle, lineAnnotations=null, isHorizontal=fal // return whichever is greater return { height: Math.max(idealChartHeight, minChartHeight) }; } - // vertical charts will just match the parent container - return { height: '100%' }; + return {}; } return ( ({ - label: d.label, - data: d.records, - // cycle through the default palette, repeat if necessary - backgroundColor: defaultPalette[i % defaultPalette.length], - })) - }} + data={{datasets: chartData.map((e, i) => ({ + ...e, + // cycle through the default palette, repeat if necessary + backgroundColor: defaultPalette[i % defaultPalette.length], + }))}} options={{ indexAxis: indexAxis, responsive: true, @@ -84,14 +99,14 @@ const BarChart = ({ chartData, axisTitle, lineAnnotations=null, isHorizontal=fal ...(isHorizontal ? { y: { offset: true, - type: 'time', - adapters: { + type: timeAxis ? 'time' : 'category', + adapters: timeAxis ? { date: { zone: 'utc' }, - }, - time: { + } : {}, + time: timeAxis ? { unit: 'day', tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 - }, + } : {}, beforeUpdate: (axis) => { setNumVisibleDatasets(axis.chart.getVisibleDatasetCount()) }, @@ -103,14 +118,14 @@ const BarChart = ({ chartData, axisTitle, lineAnnotations=null, isHorizontal=fal } : { x: { offset: true, - type: 'time', - adapters: { + type: timeAxis ? 'time' : 'category', + adapters: timeAxis ? { date: { zone: 'utc' }, - }, - time: { + } : {}, + time: timeAxis ? { unit: 'day', tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 - }, + } : {}, }, y: { title: { display: true, text: axisTitle }, @@ -146,56 +161,19 @@ const BarChart = ({ chartData, axisTitle, lineAnnotations=null, isHorizontal=fal ) } -BarChart.propTypes = { - chartData: array, - axisTitle: string, - lineAnnotations: array, - isHorizontal: bool, -}; +const defaultPalette = [ + '#c95465', // red oklch(60% 0.15 14) + '#4a71b1', // blue oklch(55% 0.11 260) + '#d2824e', // orange oklch(68% 0.12 52) + '#856b5d', // brown oklch(55% 0.04 50) + '#59894f', // green oklch(58% 0.1 140) + '#e0cc55', // yellow oklch(84% 0.14 100) + '#b273ac', // purple oklch(64% 0.11 330) + '#f09da6', // pink oklch(78% 0.1 12) + '#b3aca8', // grey oklch(75% 0.01 55) + '#80afad', // teal oklch(72% 0.05 192) +]; + angularize(BarChart, 'BarChart', 'emission.main.barchart'); export default BarChart; - -// const sampleAnnotations = [ -// { value: 35, label: 'Target1' }, -// { value: 65, label: 'Target2' }, -// ]; - -// const sampleChartData = [ -// { -// label: 'Primary', -// records: [ -// { x: moment('2023-06-20'), y: 20 }, -// { x: moment('2023-06-21'), y: 30 }, -// { x: moment('2023-06-23'), y: 80 }, -// { x: moment('2023-06-24'), y: 40 }, -// ], -// }, -// { -// label: 'Secondary', -// records: [ -// { x: moment('2023-06-21'), y: 10 }, -// { x: moment('2023-06-22'), y: 50 }, -// { x: moment('2023-06-23'), y: 30 }, -// { x: moment('2023-06-25'), y: 40 }, -// ], -// }, -// { -// label: 'Tertiary', -// records: [ -// { x: moment('2023-06-20'), y: 30 }, -// { x: moment('2023-06-22'), y: 40 }, -// { x: moment('2023-06-24'), y: 10 }, -// { x: moment('2023-06-25'), y: 60 }, -// ], -// }, -// { -// label: 'Quaternary', -// records: [ -// { x: moment('2023-06-22'), y: 10 }, -// { x: moment('2023-06-23'), y: 20 }, -// { x: moment('2023-06-24'), y: 30 }, -// { x: moment('2023-06-25'), y: 40 }, -// ], -// }, -// ]; From 6780dae6575023968a9a1342c988fe12676d73d2 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 24 Aug 2023 16:40:51 -0400 Subject: [PATCH 032/164] in MetricsCard, pass records to BarChart BarChart was reworked to accept a 'flat' array of records instead of a deep structure tailored for ChartJS. The flattened structure is more congruent with the userMetricsDays / aggMetricsDays that comes upstream from LabelTab. The BarChart component now handles tailoring this to a format that ChartJS can work with. --- www/js/metrics/MetricsCard.tsx | 26 +++++++++++++------------- www/js/metrics/metricsHelper.ts | 9 +++++++++ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index b5f1ad25b..78d9db34d 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -4,7 +4,7 @@ import { View } from 'react-native'; import { Card, SegmentedButtons, Text, useTheme } from 'react-native-paper'; import BarChart from '../components/BarChart'; import { DayOfMetricData } from './metricsTypes'; -import { getUniqueLabelsForDays } from './metricsHelper'; +import { getLabelsForDay, getUniqueLabelsForDays } from './metricsHelper'; import ToggleSwitch from './ToggleSwitch'; type Props = { @@ -24,21 +24,21 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni populationMode == 'user' ? userMetricsDays : aggMetricsDays ), [populationMode, userMetricsDays, aggMetricsDays]); - // for each label, format data for chart, with a record for each day with that label + // for each label on each day, create a record for the chart const chartData = useMemo(() => { if (!metricDataDays || viewMode != 'graph') return []; - const uniqueLabels = getUniqueLabelsForDays(metricDataDays); - - return uniqueLabels.map((label, i) => { - const daysWithThisLabel = metricDataDays.filter(e => e[`label_${label}`]); - return { + const records: {label: string, x: string|number, y: string|number}[] = []; + metricDataDays.forEach(day => { + const labels = getLabelsForDay(day); + labels.forEach(label => { + records.push({ label: label, - records: daysWithThisLabel.map(e => ({ - x: e[`label_${label}`], - y: e.ts * 1000, // time (as milliseconds) will go on Y axis because it will be a horizontal chart - })) - } + x: day[`label_${label}`], + y: day.ts * 1000, // time (as milliseconds) will go on Y axis because it will be a horizontal chart + }); + }); }); + return records; }, [metricDataDays, viewMode]); // for each label, sum up cumulative values across all days @@ -84,7 +84,7 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni )} } {viewMode=='graph' && - + } diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index c6cd05119..d2001629e 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -14,6 +14,15 @@ export function getUniqueLabelsForDays(metricDataDays: DayOfMetricData[]) { return uniqueLabels; } +export const getLabelsForDay = (metricDataDay: DayOfMetricData) => ( + Object.keys(metricDataDay).reduce((acc, k) => { + if (k.startsWith('label_')) { + acc.push(k.substring(6)); // remove 'label_' prefix leaving just the mode label + } + return acc; + }, [] as string[]) +); + export const secondsToMinutes = (seconds: number) => formatForDisplay(seconds / 60); From 4e7b0547534b66210beb307ddffa9abb5d780c8c Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 24 Aug 2023 16:43:46 -0400 Subject: [PATCH 033/164] styling tweaks and refactoring -move some common styles to a common location MetricsTab -shorten height of MetricsCard title bar -remove top/bottom borders of ToggleSwitch buttons --- www/js/metrics/MetricsCard.tsx | 11 ++++++----- www/js/metrics/MetricsTab.tsx | 15 +++++++++++---- www/js/metrics/ToggleSwitch.tsx | 4 +++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index 78d9db34d..a97ef0759 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -6,6 +6,7 @@ import BarChart from '../components/BarChart'; import { DayOfMetricData } from './metricsTypes'; import { getLabelsForDay, getUniqueLabelsForDays } from './metricsHelper'; import ToggleSwitch from './ToggleSwitch'; +import { cardStyles } from './MetricsTab'; type Props = { cardTitle: string, @@ -32,7 +33,7 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni const labels = getLabelsForDay(day); labels.forEach(label => { records.push({ - label: label, + label: label, x: day[`label_${label}`], y: day.ts * 1000, // time (as milliseconds) will go on Y axis because it will be a horizontal chart }); @@ -65,17 +66,17 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni titleStyle={{color: colors.onPrimary, fontWeight: '500', textAlign: 'center'}} titleNumberOfLines={2} right={() => - + } - style={{backgroundColor: colors.primary, paddingHorizontal: 8, minHeight: 60}} /> - + style={cardStyles.title(colors)} /> + {viewMode=='details' && - + { Object.keys(metricSumValues).map((label, i) => {label} diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index aeff78978..b1319daf8 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -76,14 +76,15 @@ const MetricsTab = () => { refresh()} /> - + + style={{scrollSnapType: 'x mandatory', paddingVertical: 10}} + contentContainerStyle={{alignItems: 'flex-start'}}> { ); } -const cardMargin = 8; +export const cardMargin = 8; const s = { scroll: { scrollSnapType: 'x mandatory', @@ -127,13 +128,19 @@ const s = { }; export const cardStyles: any = { title: (colors) => ({ - backgroundColor: colors.primary, paddingHorizontal: 8, minHeight: 60 + backgroundColor: colors.primary, + paddingHorizontal: 8, + minHeight: 52, }), titleText: (colors) => ({ color: colors.onPrimary, fontWeight: '500', textAlign: 'center' }), + content: { + padding: 8, + paddingBottom: 12, + } } angularize(MetricsTab, 'MetricsTab', 'emission.main.metricstab'); diff --git a/www/js/metrics/ToggleSwitch.tsx b/www/js/metrics/ToggleSwitch.tsx index 5b0ed47e2..e8dbcd19d 100644 --- a/www/js/metrics/ToggleSwitch.tsx +++ b/www/js/metrics/ToggleSwitch.tsx @@ -21,7 +21,9 @@ const ToggleSwitch = ({ value, setValue, options }) => { showSelectedCheck: true, style: { minWidth: 0, - backgroundColor: value == o.value ? colors.elevation.level2 : colors.surfaceDisabled + borderTopWidth: 0, + borderBottomWidth: 0, + backgroundColor: value == o.value ? colors.elevation.level2 : colors.surfaceDisabled, }, }))} /> ) From 8f8d229ea73f7ca268a530f56e01119ff2804c3c Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 24 Aug 2023 21:08:48 -0400 Subject: [PATCH 034/164] add chart to ActiveMinutesCard Considering the most recent two weeks for which days of metric data have been loaded, we calculate the total weekly time spend a) walking and b) biking. We store these totals as weeklyActiveMinutesRecords to be included in a vertical, categorical BarChart. `flex: 1` was added to the BarChart component (if it is vertical), as well as to the card content. This allows the "Active Minutes" chart to grow as tall as the Card and display the bar chart more effectively. Also add i18n key for "active minutes" --- www/i18n/en.json | 3 +- www/js/components/BarChart.tsx | 3 +- www/js/metrics/ActiveMinutesCard.tsx | 42 ++++++++++++++++++++++++---- www/js/metrics/MetricsTab.tsx | 1 + www/js/metrics/metricsHelper.ts | 10 +++++++ 5 files changed, 52 insertions(+), 7 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index 4c71a4d98..330159f65 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -174,7 +174,8 @@ "equals-icecream_one": "Equals at least {{count}} half cup vanilla ice cream", "equals-icecream_other": "Equals at least {{count}} half cups vanilla ice cream", "equals-bananas_one": "Equals at least {{count}} banana", - "equals-bananas_other": "Equals at least {{count}} bananas" + "equals-bananas_other": "Equals at least {{count}} bananas", + "active-minutes": "My Active Minutes" }, "main-inf-scroll" : { diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 22691fc37..519e2405b 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -79,7 +79,8 @@ const BarChart = ({ records, axisTitle, lineAnnotations=null, isHorizontal=false // return whichever is greater return { height: Math.max(idealChartHeight, minChartHeight) }; } - return {}; + // vertical charts should just fill the available space in the parent container + return { flex: 1 }; } return ( diff --git a/www/js/metrics/ActiveMinutesCard.tsx b/www/js/metrics/ActiveMinutesCard.tsx index c6688ae37..867462991 100644 --- a/www/js/metrics/ActiveMinutesCard.tsx +++ b/www/js/metrics/ActiveMinutesCard.tsx @@ -3,12 +3,14 @@ import React, { useMemo, useState } from 'react'; import { View } from 'react-native'; import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; -import { cardStyles } from './MetricsTab'; +import { cardMargin, cardStyles } from './MetricsTab'; import { useImperialConfig } from '../config/useImperialConfig'; -import { secondsToMinutes } from './metricsHelper'; +import { filterToRecentWeeks, secondsToMinutes } from './metricsHelper'; import { useTranslation } from 'react-i18next'; +import BarChart from '../components/BarChart'; -const ACTIVE_MODES = ['walk', 'bike']; +const ACTIVE_MODES = ['walk', 'bike'] as const; +type ActiveMode = typeof ACTIVE_MODES[number]; type Props = { userMetrics: MetricsData } const ActiveMinutesCard = ({ userMetrics }: Props) => { @@ -27,21 +29,51 @@ const ActiveMinutesCard = ({ userMetrics }: Props) => { }); }, [userMetrics?.duration]); + const weeklyActiveMinutesRecords = useMemo(() => { + const records = []; + const [ recentWeek, prevWeek ] = filterToRecentWeeks(userMetrics?.duration); + ACTIVE_MODES.forEach(mode => { + const recentSum = recentWeek?.reduce((acc, day) => ( + acc + (day[`label_${mode}`] || 0) + ), 0); + if (recentSum) { + records.push({label: mode, x: 'Past Week', y: recentSum / 60}); // TODO: i18n + } + const prevSum = prevWeek?.reduce((acc, day) => ( + acc + (day[`label_${mode}`] || 0) + ), 0); + if (prevSum) { + records.push({label: mode, x: 'Previous Week', y: prevSum / 60}); // TODO: i18n + } + }); + return records as {label: ActiveMode, x: string, y: number}[]; + }, [userMetrics?.duration]); + return ( - + - + { activeModesDurations.map((mode, i) => ( {ACTIVE_MODES[i]} {`${mode} ${t('metrics.minutes')}`} ))} + { weeklyActiveMinutesRecords.length ? + + : + + + {t('metrics.chart-no-data')} + + + } ) diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index b1319daf8..78b2031f2 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -140,6 +140,7 @@ export const cardStyles: any = { content: { padding: 8, paddingBottom: 12, + flex: 1, } } diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index d2001629e..0335a16af 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -28,3 +28,13 @@ export const secondsToMinutes = (seconds: number) => export const secondsToHours = (seconds: number) => formatForDisplay(seconds / 3600); + +// segments metricsDays into weeks, with the most recent week first +export function filterToRecentWeeks(metricsDays: DayOfMetricData[]) { + const weeks: DayOfMetricData[][] = []; + if (metricsDays?.length >= 7) + weeks.push(metricsDays.slice(-7)); + if (metricsDays?.length >= 14) + weeks.push(metricsDays.slice(-14, -7)); + return weeks; +} From 4aa123a5bf1e31d9ba137f2f013b1047b94547ba Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 25 Aug 2023 18:28:07 -0600 Subject: [PATCH 035/164] begin Carbon Footprint card Bulk of the work here is data formatting to use FootprintHelper, done in two main steps parseDataFromMetrics takes in the userData of one metric at a time from the server and gathers "mode bins" resulting in an array of objects with "key"(mode) and "values"(array of arrays of values, one array per day with times and value of the mode) THEN generateSummaryFromData takes each of those binned modes, and aggregates down to a single value - either with sum or avg The 'distance' mode from this summary is digestible to FootprintHelper, used so far to get low and high estimates lots TODO - splitting into "current" and "prev" weeks, agg metrics, goals ... --- www/js/metrics/CarbonFootprintCard.tsx | 168 +++++++++++++++++++++++++ www/js/metrics/MetricsTab.tsx | 2 + 2 files changed, 170 insertions(+) create mode 100644 www/js/metrics/CarbonFootprintCard.tsx diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx new file mode 100644 index 000000000..3892cc350 --- /dev/null +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -0,0 +1,168 @@ + +import React, { useEffect, useState } from 'react'; +import { View } from 'react-native'; +import { Card, Text, useTheme} from 'react-native-paper'; +import { MetricsData } from './metricsTypes'; +import { cardMargin, cardStyles, METRIC_LIST } from './MetricsTab'; +import { useImperialConfig } from '../config/useImperialConfig'; +import { filterToRecentWeeks, secondsToMinutes } from './metricsHelper'; +import { useTranslation } from 'react-i18next'; +import BarChart from '../components/BarChart'; +import { getAngularService } from '../angular-react-helper'; + +//modes considered on foot for carbon calculation, expandable as needed +const ON_FOOT_MODES = ['WALKING', 'RUNNING', 'ON_FOOT'] as const; +// type ActiveMode = typeof ACTIVE_MODES[number]; + +type Props = { userMetrics: MetricsData, aggMetrics: MetricsData } +const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { + const FootprintHelper = getAngularService("FootprintHelper"); + const { colors } = useTheme(); + const { t } = useTranslation(); + + console.log("metrics in carbon", userMetrics, aggMetrics); + + const [userCarbonData, setUserCarbonData] = useState([]); + + /* + * metric2val is a function that takes a metric entry and a field and returns + * the appropriate value. + * for regular data (user-specific), this will return the field value + * for avg data (aggregate), this will return the field value/nUsers + */ + const metricToValue = function(population:'user'|'aggreagte', metric, field) { + if(population == "user"){ + return metric[field]; + } + else{ + return metric[field]/metric.nUsers; + } + } + + //testing agains global list of what is "on foot" + //returns true | false + const isOnFoot = function(mode: string) { + for (let ped_mode in ON_FOOT_MODES) { + if (mode === ped_mode) { + return true; + } + } + return false; + } + + const parseDataFromMetrics = function(metrics, population) { + console.log("Called parseDataFromMetrics on ", metrics); + let mode_bins = {}; + metrics.forEach(function(metric) { + let onFootVal = 0; + + for (let field in metric) { + /*For modes inferred from sensor data, we check if the string is all upper case + by converting it to upper case and seeing if it is changed*/ + if(field == field.toUpperCase()) { + /*sum all possible on foot modes: see https://github.com/e-mission/e-mission-docs/issues/422 */ + if (isOnFoot(field)) { + onFootVal += metricToValue(population, metric, field); + field = 'ON_FOOT'; + } + if (!(field in mode_bins)) { + mode_bins[field] = []; + } + //for all except onFoot, add to bin - could discover mult onFoot modes + if (field != "ON_FOOT") { + mode_bins[field].push([metric.ts, metricToValue(population, metric, field), metric.fmt_time]); + } + } + //this section handles user lables, assuming 'label_' prefix + if(field.startsWith('label_')) { + let actualMode = field.slice(6, field.length); //remove prefix + console.log("Mapped field "+field+" to mode "+actualMode); + if (!(actualMode in mode_bins)) { + mode_bins[actualMode] = []; + } + mode_bins[actualMode].push([metric.ts, Math.round(metricToValue(population, metric, field)), moment(metric.fmt_time).format()]); + } + } + //handle the ON_FOOT modes once all have been summed + if ("ON_FOOT" in mode_bins) { + mode_bins["ON_FOOT"].push([metric.ts, Math.round(onFootVal), metric.fmt_time]); + } + }); + + console.log("testing metrics mode bins", mode_bins); + + let return_val = []; + for (let mode in mode_bins) { + return_val.push({key: mode, values: mode_bins[mode]}); + } + + return return_val; + } + + const generateSummaryFromData = function(modeMap, metric) { + console.log("Invoked getSummaryDataRaw on ", modeMap, "with", metric); + + let summaryMap = []; + + for (let i=0; i < modeMap.length; i++){ + let summary = {}; + summary['key'] = modeMap[i].key; + let sumVals = 0; + + for (let j = 0; j < modeMap[i].values.length; j++) + { + sumVals += modeMap[i].values[j][1]; //2nd item of array is value + } + if (metric === 'mean_speed'){ + //we care about avg speed, sum for other metrics + summary['values'] = Math.round(sumVals / modeMap[i].values.length); + } else { + summary['values'] = Math.round(sumVals); + } + + summaryMap.push(summary); + } + + return summaryMap; + } + + useEffect(() => { + if(userMetrics) { + let userModeMap = {}; + let userSummaryMap = {}; + + METRIC_LIST.forEach((m) => + userModeMap[m] = parseDataFromMetrics(userMetrics[m], 'user')); + + METRIC_LIST.forEach((m) => + userSummaryMap[m] = generateSummaryFromData(userModeMap[m], m)); + + let tempUserCarbon = []; + tempUserCarbon.push({label: "low estimate", value: FootprintHelper.getFootprintForMetrics(userSummaryMap.distance, 0)}); + tempUserCarbon.push({label: "high estimate", value:FootprintHelper.getFootprintForMetrics(userSummaryMap.distance, FootprintHelper.getHighestFootprint())}); + setUserCarbonData(tempUserCarbon); + } + }, [userMetrics]) + + return ( + + + + { userCarbonData.map((dataPoint) => ( + + {dataPoint.label} + {`${dataPoint.value} ${t("kg Co2")}`} + + ))} + + + ) +} + +export default CarbonFootprintCard; diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 78b2031f2..485756195 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -11,6 +11,7 @@ import { formatForDisplay, useImperialConfig } from "../config/useImperialConfig import MetricsDateSelect from "./MetricsDateSelect"; import ActiveMinutesCard from "./ActiveMinutesCard"; import { secondsToHours, secondsToMinutes } from "./metricsHelper"; +import CarbonFootprintCard from "./CarbonFootprintCard"; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; @@ -77,6 +78,7 @@ const MetricsTab = () => { refresh()} /> + Date: Mon, 28 Aug 2023 01:41:58 -0400 Subject: [PATCH 036/164] BarChart: support stacked bars BarCharts will be stacked by default for: -Active minutes ('walk' and 'bike' minutes stacked together) -MetricsCard charts - but they will be toggleable by a checkbox, so they can be either unstacked/stacked. Stacked looks a lot cleaner, especially with a lot of modes showing, so it will be the default. In BarChart.tsx, getChartHeight was modified to account for stacked charts - if labels are stacked on the chart, then the chart height doesn't depend on how many labels are showing and we can just use a constant for the height of each bar (36 was a comfy height). --- www/js/components/BarChart.tsx | 10 ++++++++-- www/js/metrics/ActiveMinutesCard.tsx | 3 ++- www/js/metrics/MetricsCard.tsx | 16 ++++++++++++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 519e2405b..d84beeaeb 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -29,8 +29,9 @@ type Props = { lineAnnotations?: { value: number, label: string }[], isHorizontal?: boolean, timeAxis?: boolean, + stacked?: boolean, } -const BarChart = ({ records, axisTitle, lineAnnotations=null, isHorizontal=false, timeAxis }: Props) => { +const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, stacked }: Props) => { const { colors } = useTheme(); const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); @@ -70,7 +71,8 @@ const BarChart = ({ records, axisTitle, lineAnnotations=null, isHorizontal=false if (!uniqueIndexVals.includes(r[indexAxis])) uniqueIndexVals.push(r[indexAxis]); })); const numIndexVals = uniqueIndexVals.length; - const idealChartHeight = numVisibleDatasets * numIndexVals * 8; + const heightPerIndexVal = stacked ? 36 : numVisibleDatasets * 8; + const idealChartHeight = heightPerIndexVal * numIndexVals; /* each index val should be at least 20px tall for visibility, and the graph itself should be at least 250px tall */ @@ -112,9 +114,11 @@ const BarChart = ({ records, axisTitle, lineAnnotations=null, isHorizontal=false setNumVisibleDatasets(axis.chart.getVisibleDatasetCount()) }, reverse: true, + stacked, }, x: { title: { display: true, text: axisTitle }, + stacked, }, } : { x: { @@ -127,9 +131,11 @@ const BarChart = ({ records, axisTitle, lineAnnotations=null, isHorizontal=false unit: 'day', tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 } : {}, + stacked, }, y: { title: { display: true, text: axisTitle }, + stacked, }, }), }, diff --git a/www/js/metrics/ActiveMinutesCard.tsx b/www/js/metrics/ActiveMinutesCard.tsx index 867462991..97c8cb509 100644 --- a/www/js/metrics/ActiveMinutesCard.tsx +++ b/www/js/metrics/ActiveMinutesCard.tsx @@ -66,7 +66,8 @@ const ActiveMinutesCard = ({ userMetrics }: Props) => { ))} { weeklyActiveMinutesRecords.length ? - + : diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index a97ef0759..f5f510287 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useState } from 'react'; import { View } from 'react-native'; -import { Card, SegmentedButtons, Text, useTheme } from 'react-native-paper'; +import { Card, Checkbox, Text, useTheme } from 'react-native-paper'; import BarChart from '../components/BarChart'; import { DayOfMetricData } from './metricsTypes'; import { getLabelsForDay, getUniqueLabelsForDays } from './metricsHelper'; @@ -21,6 +21,7 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni const { colors } = useTheme(); const [viewMode, setViewMode] = useState<'details'|'graph'>('details'); const [populationMode, setPopulationMode] = useState<'user'|'aggregate'>('user'); + const [graphIsStacked, setGraphIsStacked] = useState(true); const metricDataDays = useMemo(() => ( populationMode == 'user' ? userMetricsDays : aggMetricsDays ), [populationMode, userMetricsDays, aggMetricsDays]); @@ -84,9 +85,16 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni )} - } {viewMode=='graph' && - - } + } {viewMode=='graph' && <> + + + Stack bars: + setGraphIsStacked(!graphIsStacked)} /> + + } ) From 23a479c287bcbf1c120750a19dd6b67c7740e32e Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 28 Aug 2023 02:15:20 -0400 Subject: [PATCH 037/164] show date ranges for weeks on ActiveMinutesCard To make it clearer what data is being used to calculate week-wise metrics, "Past week" and "Previous week" should be annotated with the specific dates they encompass. For this to not clutter the chart, I wanted these to be on a newline, like this: Past Week 8/21-8/27 ChartJS doesn't support this by itself - `\n` is just rendered as a space - but we can remedy this by adding a callback function under ChartJS options > 'scales' > 'ticks' that splits a label into an array if it has `\n`. --- www/js/components/BarChart.tsx | 8 ++++++++ www/js/metrics/ActiveMinutesCard.tsx | 13 ++++++++++--- www/js/metrics/metricsHelper.ts | 9 +++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index d84beeaeb..596fe6fab 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -131,6 +131,14 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, unit: 'day', tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 } : {}, + ticks: { + callback: timeAxis ? undefined : (value, i) => { + const label = chartData[i].data[i].x; + if (typeof label == 'string' && label.includes('\n')) + return label.split('\n'); + return label; + }, + }, stacked, }, y: { diff --git a/www/js/metrics/ActiveMinutesCard.tsx b/www/js/metrics/ActiveMinutesCard.tsx index 97c8cb509..9e798718f 100644 --- a/www/js/metrics/ActiveMinutesCard.tsx +++ b/www/js/metrics/ActiveMinutesCard.tsx @@ -5,7 +5,7 @@ import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardMargin, cardStyles } from './MetricsTab'; import { useImperialConfig } from '../config/useImperialConfig'; -import { filterToRecentWeeks, secondsToMinutes } from './metricsHelper'; +import { filterToRecentWeeks, formatDateRangeOfDays, secondsToMinutes } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; @@ -17,6 +17,11 @@ const ActiveMinutesCard = ({ userMetrics }: Props) => { const { colors } = useTheme(); const { t } = useTranslation(); + + userMetrics?.duration.forEach(day => { + day.label_walk = day.label_walk || 400 + (Math.random() * 300); + day.label_bike = day.label_bike || 200 + (Math.random() * 300); + }); // number of minutes for each of [walk, bike] const activeModesDurations = useMemo(() => { @@ -37,13 +42,15 @@ const ActiveMinutesCard = ({ userMetrics }: Props) => { acc + (day[`label_${mode}`] || 0) ), 0); if (recentSum) { - records.push({label: mode, x: 'Past Week', y: recentSum / 60}); // TODO: i18n + const xLabel = `Past Week\n(${formatDateRangeOfDays(recentWeek)})`; // TODO: i18n + records.push({label: mode, x: xLabel, y: recentSum / 60}); } const prevSum = prevWeek?.reduce((acc, day) => ( acc + (day[`label_${mode}`] || 0) ), 0); if (prevSum) { - records.push({label: mode, x: 'Previous Week', y: prevSum / 60}); // TODO: i18n + const xLabel = `Previous Week\n(${formatDateRangeOfDays(prevWeek)})`; // TODO: i18n + records.push({label: mode, x: xLabel, y: prevSum / 60}); } }); return records as {label: ActiveMode, x: string, y: number}[]; diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index 0335a16af..1c01b7fee 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -1,3 +1,4 @@ +import { DateTime } from "luxon"; import { formatForDisplay } from "../config/useImperialConfig"; import { DayOfMetricData } from "./metricsTypes"; @@ -38,3 +39,11 @@ export function filterToRecentWeeks(metricsDays: DayOfMetricData[]) { weeks.push(metricsDays.slice(-14, -7)); return weeks; } + +export function formatDateRangeOfDays(days: DayOfMetricData[]) { + const firstDayDt = DateTime.fromISO(days[0].fmt_time, { zone: 'utc' }); + const lastDayDt = DateTime.fromISO(days[days.length - 1].fmt_time, { zone: 'utc' }); + const firstDay = firstDayDt.toLocaleString({...DateTime.DATE_SHORT, year: undefined}); + const lastDay = lastDayDt.toLocaleString({...DateTime.DATE_SHORT, year: undefined}); + return `${firstDay} - ${lastDay}`; +} From c480d67217eb18cf7c9fe24595a3bec04a9dc69e Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 28 Aug 2023 03:06:42 -0400 Subject: [PATCH 038/164] readable labels on new dashboard the dashboard works with the label keys "e.g. 'walk', 'drove_alone'. To show these as readable text we can repurpose a function from `confirmHelper.ts` which is used for "other" text. Let's rename it to be more generic, make it work with more than just 1 underscore, and export it so we can use it on the dashboard too, --- www/js/metrics/ActiveMinutesCard.tsx | 7 ++++--- www/js/metrics/MetricsCard.tsx | 5 +++-- www/js/survey/multilabel/confirmHelper.ts | 7 ++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/www/js/metrics/ActiveMinutesCard.tsx b/www/js/metrics/ActiveMinutesCard.tsx index 9e798718f..d626df540 100644 --- a/www/js/metrics/ActiveMinutesCard.tsx +++ b/www/js/metrics/ActiveMinutesCard.tsx @@ -8,6 +8,7 @@ import { useImperialConfig } from '../config/useImperialConfig'; import { filterToRecentWeeks, formatDateRangeOfDays, secondsToMinutes } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; +import { labelKeyToReadable } from '../survey/multilabel/confirmHelper'; const ACTIVE_MODES = ['walk', 'bike'] as const; type ActiveMode = typeof ACTIVE_MODES[number]; @@ -43,14 +44,14 @@ const ActiveMinutesCard = ({ userMetrics }: Props) => { ), 0); if (recentSum) { const xLabel = `Past Week\n(${formatDateRangeOfDays(recentWeek)})`; // TODO: i18n - records.push({label: mode, x: xLabel, y: recentSum / 60}); + records.push({label: labelKeyToReadable(mode), x: xLabel, y: recentSum / 60}); } const prevSum = prevWeek?.reduce((acc, day) => ( acc + (day[`label_${mode}`] || 0) ), 0); if (prevSum) { const xLabel = `Previous Week\n(${formatDateRangeOfDays(prevWeek)})`; // TODO: i18n - records.push({label: mode, x: xLabel, y: prevSum / 60}); + records.push({label: labelKeyToReadable(mode), x: xLabel, y: prevSum / 60}); } }); return records as {label: ActiveMode, x: string, y: number}[]; @@ -68,7 +69,7 @@ const ActiveMinutesCard = ({ userMetrics }: Props) => { { activeModesDurations.map((mode, i) => ( - {ACTIVE_MODES[i]} + {labelKeyToReadable(ACTIVE_MODES[i])} {`${mode} ${t('metrics.minutes')}`} ))} diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index f5f510287..1155c96d4 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -7,6 +7,7 @@ import { DayOfMetricData } from './metricsTypes'; import { getLabelsForDay, getUniqueLabelsForDays } from './metricsHelper'; import ToggleSwitch from './ToggleSwitch'; import { cardStyles } from './MetricsTab'; +import { labelKeyToReadable } from '../survey/multilabel/confirmHelper'; type Props = { cardTitle: string, @@ -34,7 +35,7 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni const labels = getLabelsForDay(day); labels.forEach(label => { records.push({ - label: label, + label: labelKeyToReadable(label), x: day[`label_${label}`], y: day.ts * 1000, // time (as milliseconds) will go on Y axis because it will be a horizontal chart }); @@ -80,7 +81,7 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni { Object.keys(metricSumValues).map((label, i) => - {label} + {labelKeyToReadable(label)} {metricSumValues[label] + ' ' + axisUnits} )} diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index d5e43b826..05e3a71ad 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -99,8 +99,9 @@ export function getLabelInputDetails(appConfigParam?) { export const getLabelInputs = () => Object.keys(getLabelInputDetails()); export const getBaseLabelInputs = () => Object.keys(baseLabelInputDetails); -const otherValueToText = (otherValue) => { - const words = otherValue.replace("_", " ").split(" "); +// replace all underscores with spaces, then capitalize each word +export const labelKeyToReadable = (otherValue: string) => { + const words = otherValue.replace(/_/g, " ").split(" "); if (words.length == 0) return ""; return words.map((word) => word[0].toUpperCase() + word.slice(1) @@ -111,6 +112,6 @@ const otherTextToValue = (otherText) => otherText.toLowerCase().replace(" ", "_"); export const getFakeEntry = (otherValue) => ({ - text: otherValueToText(otherValue), + text: labelKeyToReadable(otherValue), value: otherValue, }); From 9e4025299f6c36e19ed30cb3579cc0cd9d8efd8c Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 28 Aug 2023 11:14:02 -0400 Subject: [PATCH 039/164] fix axis units on MetricsCard `unitFormatFn` should be used on the chart so the values appear in the listed unit and not the raw value. (e.g., '3 miles' instead of '4828 meters') --- www/js/metrics/MetricsCard.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index 1155c96d4..6a407c9ca 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -34,9 +34,10 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni metricDataDays.forEach(day => { const labels = getLabelsForDay(day); labels.forEach(label => { + const rawVal = day[`label_${label}`]; records.push({ label: labelKeyToReadable(label), - x: day[`label_${label}`], + x: unitFormatFn ? unitFormatFn(rawVal) : rawVal, y: day.ts * 1000, // time (as milliseconds) will go on Y axis because it will be a horizontal chart }); }); From fde2283e1b5c0185d6dabfa5c6dc94c3c70cd805 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 28 Aug 2023 12:00:43 -0400 Subject: [PATCH 040/164] refactor new dashboard into Carousel The horizontal ScrollView requires a few tricks to get the scrolling to snap correctly while managing the card widths and margins. Let's extract this logic to a reusable "Carousel" component so we can use it multiple times without MetricsTab getting too cluttered. --- www/js/components/Carousel.tsx | 40 ++++++++++++++++++++++++++++++++++ www/js/metrics/MetricsCard.tsx | 5 ++--- www/js/metrics/MetricsTab.tsx | 35 ++++++----------------------- 3 files changed, 49 insertions(+), 31 deletions(-) create mode 100644 www/js/components/Carousel.tsx diff --git a/www/js/components/Carousel.tsx b/www/js/components/Carousel.tsx new file mode 100644 index 000000000..a8f958bfa --- /dev/null +++ b/www/js/components/Carousel.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { ScrollView, View } from 'react-native'; + +type Props = { + children: React.ReactNode, + cardWidth: number, + cardMargin: number, +} +const Carousel = ({ children, cardWidth, cardMargin }: Props) => { + return ( + + {React.Children.map(children, child => ( + + {child} + + ))} + + ) +}; + +export const s = { + carouselScroll: { + // @ts-ignore, RN doesn't recognize `scrollSnapType`, but it does work on RN Web + scrollSnapType: 'x mandatory', + paddingVertical: 10, + }, + carouselCard: (cardWidth, cardMargin) => ({ + margin: cardMargin, + width: cardWidth, + scrollSnapAlign: 'center', + scrollSnapStop: 'always', + }), +}; + +export default Carousel; diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index 6a407c9ca..65f5d0ddc 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -15,9 +15,8 @@ type Props = { aggMetricsDays: DayOfMetricData[], axisUnits: string, unitFormatFn?: (val: number) => string|number, - style: any, } -const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, unitFormatFn, style}: Props) => { +const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, unitFormatFn}: Props) => { const { colors } = useTheme(); const [viewMode, setViewMode] = useState<'details'|'graph'>('details'); @@ -62,7 +61,7 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni }, [metricDataDays, viewMode]); return ( - + { - + + unitFormatFn={getFormattedDistance} /> + unitFormatFn={formatForDisplay} /> + unitFormatFn={secondsToHours} /> - + unitFormatFn={getFormattedSpeed} /> + ); } -export const cardMargin = 8; -const s = { - scroll: { - scrollSnapType: 'x mandatory', - }, - card: (cardWidth) => ({ - margin: cardMargin, - width: cardWidth, - scrollSnapAlign: 'center', - scrollSnapStop: 'always', - }), -}; export const cardStyles: any = { title: (colors) => ({ backgroundColor: colors.primary, From 167e3d15716f8d41d0ec79541463df3abe2d7b73 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 28 Aug 2023 12:09:54 -0400 Subject: [PATCH 041/164] refactoring metrics card styles --- www/js/metrics/MetricsCard.tsx | 4 ++-- www/js/metrics/MetricsTab.tsx | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index 65f5d0ddc..c3ceb72c8 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -61,11 +61,11 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni }, [metricDataDays, viewMode]); return ( - + diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 5a7065ebb..6f0e94882 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -81,7 +81,7 @@ const MetricsTab = () => { - + { ); } +export const cardMargin = 8; + export const cardStyles: any = { + card: { + overflow: 'hidden', + minHeight: 300, + }, title: (colors) => ({ backgroundColor: colors.primary, paddingHorizontal: 8, From 76ebb17a25edb2bb3e93b305a78be34e2c0d3bfa Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 28 Aug 2023 11:51:00 -0600 Subject: [PATCH 042/164] more user metrics card now retrieves and stores user data for past and previous weeks, worst-case emissions, and US goals used filterToRecentWeeks from metricsHelper.ts to separate user data into the last two weeks, and display data for both if the range is 2 weeks or more worst-case carbon is calculated with a method in FootPrintHelper called on the total distance for the most recent week --- www/js/metrics/CarbonFootprintCard.tsx | 84 +++++++++++++++++++++----- 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 3892cc350..424193042 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -1,10 +1,10 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { View } from 'react-native'; import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardMargin, cardStyles, METRIC_LIST } from './MetricsTab'; -import { useImperialConfig } from '../config/useImperialConfig'; +import { formatForDisplay } from '../config/useImperialConfig'; import { filterToRecentWeeks, secondsToMinutes } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; @@ -126,23 +126,75 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { return summaryMap; } - useEffect(() => { + const createOrCollapseRange = function(low, high) { + let range = []; + if(high == low) { + range.push(low); + } + else { + range.push(low); + range.push(high); + } + return range; + } + + const userCarbonRecords = useMemo(() => { if(userMetrics) { - let userModeMap = {}; - let userSummaryMap = {}; + console.log("before testing metrics", userMetrics); + + let thisWeekDistance = filterToRecentWeeks(userMetrics?.distance)[0]; + let lastWeekDistance = filterToRecentWeeks(userMetrics?.distance)[1]; + let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); + let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); + let worstDistance = userThisWeekSummaryMap.reduce((prevDistance, currModeSummary) => prevDistance + currModeSummary.values, 0); + + let userLastWeekModeMap = {}; + let userLastWeekSummaryMap = {}; + if(lastWeekDistance) { + userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); + userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); + + console.log("testing metric map here", userThisWeekModeMap, userLastWeekModeMap); + console.log("testing summary data!!", userThisWeekSummaryMap, userLastWeekSummaryMap); + } + + //setting up data to be displayed //TODO i18n for labels + let tempUserCarbon = []; - METRIC_LIST.forEach((m) => - userModeMap[m] = parseDataFromMetrics(userMetrics[m], 'user')); + //calculate low-high and format range for past week + let userPastWeek = { + low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), + high: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, FootprintHelper.getHighestFootprint()), + }; + let valueArray = createOrCollapseRange(userPastWeek.low, userPastWeek.high); + let value = valueArray[1] ? valueArray[0] + '-' + valueArray[1] : valueArray[0]; + tempUserCarbon.push({label: "past week", value: value}); + + //calculate low-high and format range for prev week, if exists + if(userLastWeekSummaryMap[0]) { + let userPrevWeek = { + low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), + high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) + }; + valueArray = createOrCollapseRange(userPrevWeek.low, userPrevWeek.high); + value = valueArray[1] ? valueArray[0] + '-' + valueArray[1] : valueArray[0]; + tempUserCarbon.push({label: "previous week", value: value}); + } + + //calculate worst-case carbon footprint + let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); - METRIC_LIST.forEach((m) => - userSummaryMap[m] = generateSummaryFromData(userModeMap[m], m)); + tempUserCarbon.push({label: "if all taxi", value: worstCarbon}); - let tempUserCarbon = []; - tempUserCarbon.push({label: "low estimate", value: FootprintHelper.getFootprintForMetrics(userSummaryMap.distance, 0)}); - tempUserCarbon.push({label: "high estimate", value:FootprintHelper.getFootprintForMetrics(userSummaryMap.distance, FootprintHelper.getHighestFootprint())}); - setUserCarbonData(tempUserCarbon); + //push in goals + tempUserCarbon.push({label: "US 2030 goal", value: 54}); + tempUserCarbon.push({label: "US 2050 goal", value: 14}); + + console.log("testing, the data is: ", tempUserCarbon); + + return tempUserCarbon; } - }, [userMetrics]) + }, [userMetrics?.distance]) return ( { titleNumberOfLines={2} style={cardStyles.title(colors)} /> - { userCarbonData.map((dataPoint) => ( + { userCarbonRecords?.map((dataPoint) => ( {dataPoint.label} - {`${dataPoint.value} ${t("kg Co2")}`} + {`${formatForDisplay(dataPoint.value)} ${t("kg Co2")}`} ))} From 8972b51ba4abd879271e1dcd2f76ff044f5cfe94 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 28 Aug 2023 16:03:20 -0600 Subject: [PATCH 043/164] add percent change to carbon card if there is a previous week, we use current and previous to calculate a percent change over the two and display it to the user --- www/js/metrics/CarbonFootprintCard.tsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 424193042..1ff14f2ad 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -126,6 +126,14 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { return summaryMap; } + const calculatePercentChange = function(pastWeekRange, previousWeekRange) { + let greaterLesserPct = { + low: (pastWeekRange.low/previousWeekRange.low) * 100 - 100, + high: (pastWeekRange.high/previousWeekRange.high) * 100 - 100, + } + return greaterLesserPct; + } + const createOrCollapseRange = function(low, high) { let range = []; if(high == low) { @@ -179,6 +187,10 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { valueArray = createOrCollapseRange(userPrevWeek.low, userPrevWeek.high); value = valueArray[1] ? valueArray[0] + '-' + valueArray[1] : valueArray[0]; tempUserCarbon.push({label: "previous week", value: value}); + + let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); + let changeRange = createOrCollapseRange(pctChange.low, pctChange.high); + setEmissionsChange(changeRange[1] ? changeRange[0] + '-' + changeRange[1] : changeRange[0]); } //calculate worst-case carbon footprint @@ -196,6 +208,14 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { } }, [userMetrics?.distance]) + let changeSection; + if(emissionsChange) { + changeSection = + {"change in emissions"} + {`${formatForDisplay(emissionsChange)} ${t("% this week")}`} + ; + } + return ( From 9c8e8847affb5bf60aae753ad5a22f575498ca33 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 28 Aug 2023 16:06:08 -0600 Subject: [PATCH 044/164] rough draft carbon chart using bar chart, we are able to present the carbon metrics to the user visually. There will be two colors on the graph, one for "certain" and one for "uncertain" to display the range -- graph in rough draft stage currently, want to add baselines and adjust colors also tweaked the labeling in BarChart in response to an error thrown when there were more x axis than labels -- this change shows correct x axis labels for both cards that rely on these categories --- www/js/components/BarChart.tsx | 2 +- www/js/metrics/CarbonFootprintCard.tsx | 29 +++++++++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 596fe6fab..179b514dd 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -133,7 +133,7 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, } : {}, ticks: { callback: timeAxis ? undefined : (value, i) => { - const label = chartData[i].data[i].x; + const label = chartData[0].data[i].x; if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); return label; diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 1ff14f2ad..14cbbb127 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -5,7 +5,7 @@ import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardMargin, cardStyles, METRIC_LIST } from './MetricsTab'; import { formatForDisplay } from '../config/useImperialConfig'; -import { filterToRecentWeeks, secondsToMinutes } from './metricsHelper'; +import { filterToRecentWeeks, formatDateRangeOfDays } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; import { getAngularService } from '../angular-react-helper'; @@ -22,7 +22,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { console.log("metrics in carbon", userMetrics, aggMetrics); - const [userCarbonData, setUserCarbonData] = useState([]); + const [emissionsChange, setEmissionsChange] = useState(); + const [graphRecords, setGraphRecords] = useState([]); /* * metric2val is a function that takes a metric entry and a field and returns @@ -168,6 +169,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //setting up data to be displayed //TODO i18n for labels let tempUserCarbon = []; + let graphRecords = []; //calculate low-high and format range for past week let userPastWeek = { @@ -176,6 +178,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { }; let valueArray = createOrCollapseRange(userPastWeek.low, userPastWeek.high); let value = valueArray[1] ? valueArray[0] + '-' + valueArray[1] : valueArray[0]; + graphRecords.push({label: 'certain', x: `Past Week\n(${formatDateRangeOfDays(thisWeekDistance)})`, y: valueArray[0]}); + graphRecords.push({label: "uncertain", x: `Past Week\n(${formatDateRangeOfDays(thisWeekDistance)})`, y: userPastWeek.high - userPastWeek.low}) tempUserCarbon.push({label: "past week", value: value}); //calculate low-high and format range for prev week, if exists @@ -186,6 +190,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { }; valueArray = createOrCollapseRange(userPrevWeek.low, userPrevWeek.high); value = valueArray[1] ? valueArray[0] + '-' + valueArray[1] : valueArray[0]; + graphRecords.push({label: 'certain', x: `Previous Week\n(${formatDateRangeOfDays(lastWeekDistance)})`, y: valueArray[0]}); + graphRecords.push({label: "uncertain", x: `Previous Week\n(${formatDateRangeOfDays(lastWeekDistance)})`, y: userPrevWeek.high - userPrevWeek.low}) tempUserCarbon.push({label: "previous week", value: value}); let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); @@ -196,13 +202,17 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //calculate worst-case carbon footprint let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); + graphRecords.push({label: 'certain', x: 'if all taxi', y: worstCarbon}); tempUserCarbon.push({label: "if all taxi", value: worstCarbon}); //push in goals + graphRecords.push({label: 'certain', x: 'US 2030 goal', y: 54}); + graphRecords.push({label: 'certain', x: 'US 2050 goal', y: 14}); tempUserCarbon.push({label: "US 2030 goal", value: 54}); tempUserCarbon.push({label: "US 2050 goal", value: 14}); console.log("testing, the data is: ", tempUserCarbon); + setGraphRecords(graphRecords); return tempUserCarbon; } @@ -226,12 +236,17 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { titleNumberOfLines={2} style={cardStyles.title(colors)} /> - { userCarbonRecords?.map((dataPoint) => ( - - {dataPoint.label} - {`${formatForDisplay(dataPoint.value)} ${t("kg Co2")}`} + { graphRecords.length ? + + : + + + {t('metrics.chart-no-data')} + - ))} + } + {changeSection} ) From 8839613f9016388407151da23de4448e871fae0f Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 28 Aug 2023 18:16:59 -0400 Subject: [PATCH 045/164] comment out "average speed" card "Average speed" needs to be handled differently because it is not mathematically correct to average it across days without considering differences in distance and duration between those days. (described in https://github.com/e-mission/e-mission-docs/issues/961) We can comment this out; maybe revisit later --- www/js/metrics/MetricsTab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 6f0e94882..eb1ba8b0b 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -97,11 +97,11 @@ const MetricsTab = () => { aggMetricsDays={aggMetrics?.duration} axisUnits={t('metrics.hours')} unitFormatFn={secondsToHours} /> - + unitFormatFn={getFormattedSpeed} /> */} ); From 764389557eb50c3c845684e511c346387b032b08 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 28 Aug 2023 18:19:36 -0400 Subject: [PATCH 046/164] dashboard: initial load 15 days ago to yesterday Let's change the initial loaded range so that we get 2 full weeks of data. This is so that the weekwise comparisons can show "past week" and "previous week" - if we only had 13 days we could only show "past week" and not the full "previous week" --- www/js/metrics/MetricsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index eb1ba8b0b..ce8d54b9e 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -38,7 +38,7 @@ const MetricsTab = () => { const [dateRange, setDateRange] = useState<[DateTime, DateTime]>(() => { const now = DateTime.utc().startOf('day'); - const start = now.minus({ days: 14 }); + const start = now.minus({ days: 15 }); const end = now.minus({ days: 1 }); return [start, end]; }); From 35cd467820d786b5a12a5968af500886822d19d9 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 28 Aug 2023 17:03:05 -0600 Subject: [PATCH 047/164] add carbon goal lines using the lineAnnotations attribute, adding the goal lines to the Carbon data graph --- www/js/metrics/CarbonFootprintCard.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 14cbbb127..55676db9f 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -226,6 +226,9 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { ; } + //hardcoded here, could be read from config at later customization? + let carbonGoals = [{label:"US 2030", value: 54}, {label:"US 2050", value: 14}]; + return ( @@ -237,8 +240,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { style={cardStyles.title(colors)} /> { graphRecords.length ? - + : From 0fa637d67f052e696b534521255135e7ca1ad8dc Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 29 Aug 2023 12:51:47 -0600 Subject: [PATCH 048/164] move %change to Card Title Right component of the CarbonFootprintCard is now an indication of change over past two weeks The element is only visible when there is a change to show, is red when carbon increasing and yellow when carbon decreasing, and shows ranges when labeling leaves doubt The new ChangeIndicator component accepts an array of numbers of length 1 or 2 to display change as a single value or a range. This new component separates the logic and styling, and could be used on another card later if desired. --- www/js/appTheme.ts | 2 + www/js/metrics/CarbonFootprintCard.tsx | 15 ++---- www/js/metrics/ChangeIndicator.tsx | 75 ++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 www/js/metrics/ChangeIndicator.tsx diff --git a/www/js/appTheme.ts b/www/js/appTheme.ts index 2aa6cc352..836da652b 100644 --- a/www/js/appTheme.ts +++ b/www/js/appTheme.ts @@ -25,6 +25,8 @@ const AppTheme = { level4: '#e0f0ff', // lch(94% 50 250) level5: '#d6ebff', // lch(92% 50 250) }, + success: '#38872e', // lch(50% 55 135) + danger: '#f23934' // lch(55% 85 35) }, roundness: 5, }; diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 55676db9f..ea4adf1f3 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -9,6 +9,7 @@ import { filterToRecentWeeks, formatDateRangeOfDays } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; import { getAngularService } from '../angular-react-helper'; +import ChangeIndicator from './ChangeIndicator'; //modes considered on foot for carbon calculation, expandable as needed const ON_FOOT_MODES = ['WALKING', 'RUNNING', 'ON_FOOT'] as const; @@ -22,7 +23,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { console.log("metrics in carbon", userMetrics, aggMetrics); - const [emissionsChange, setEmissionsChange] = useState(); + const [emissionsChange, setEmissionsChange] = useState([]); const [graphRecords, setGraphRecords] = useState([]); /* @@ -196,7 +197,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); let changeRange = createOrCollapseRange(pctChange.low, pctChange.high); - setEmissionsChange(changeRange[1] ? changeRange[0] + '-' + changeRange[1] : changeRange[0]); + setEmissionsChange(changeRange); } //calculate worst-case carbon footprint @@ -218,14 +219,6 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { } }, [userMetrics?.distance]) - let changeSection; - if(emissionsChange) { - changeSection = - {"change in emissions"} - {`${formatForDisplay(emissionsChange)} ${t("% this week")}`} - ; - } - //hardcoded here, could be read from config at later customization? let carbonGoals = [{label:"US 2030", value: 54}, {label:"US 2050", value: 14}]; @@ -237,6 +230,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { titleVariant='titleLarge' titleStyle={cardStyles.titleText(colors)} titleNumberOfLines={2} + right={(props) => } style={cardStyles.title(colors)} /> { graphRecords.length ? @@ -249,7 +243,6 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { } - {changeSection} ) diff --git a/www/js/metrics/ChangeIndicator.tsx b/www/js/metrics/ChangeIndicator.tsx new file mode 100644 index 000000000..82c23ae63 --- /dev/null +++ b/www/js/metrics/ChangeIndicator.tsx @@ -0,0 +1,75 @@ +import React, {useMemo} from 'react'; +import { View } from 'react-native'; +import { useTheme, Text } from "react-native-paper"; +import { useTranslation } from 'react-i18next'; +import { formatForDisplay } from '../config/useImperialConfig'; +import { Colors } from 'chart.js'; + +type Props = { + change: number[], +} + +const ChangeIndicator = ({ change }) => { + const { colors } = useTheme(); + const { t } = useTranslation(); + + const changeSign = function(changeNum) { + if(changeNum > 0) { + return "+"; + } else { + return "-"; + } + }; + + const changeText = useMemo(() => { + console.log("testing what change is", change); + if(change) { + if(change.length == 1) + { + let text = changeSign(change[0]) + formatForDisplay(change[0]); + return text; + } else { + let text = changeSign(change[0]) + formatForDisplay(change[0]) + " / " + changeSign(change[1]) + formatForDisplay(change[1]); + return text; + } + } + },[change]) + + return ( + (change[0]) ? + 0 ? colors.danger : colors.success)}> + + {`${changeText} \n`} + + + {`${t("% this week")}`} + + + : + <> + ) +} + +const styles: any = { + text: (colors) => ({ + color: colors.onPrimary, + fontWeight: '400', + textAlign: 'center' + }), + importantText: (colors) => ({ + color: colors.onPrimary, + fontWeight: '500', + textAlign: 'center', + fontSize: 16, + }), + view: (color) => ({ + backgroundColor: color, + padding: 4, + borderStyle: 'solid', + borderWidth: 2, + borderRadius: 5, + }), + + } + +export default ChangeIndicator; \ No newline at end of file From f4179f3aa9e1bfbbd0be45b7f4736f331c5b95db Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 29 Aug 2023 13:59:13 -0600 Subject: [PATCH 049/164] flip bar chart to horizonal change the boolean for isHorizonal to true and flip x and y values (since x and y are flipped to turn horizontal) --- www/js/metrics/CarbonFootprintCard.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index ea4adf1f3..6657c0653 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -179,8 +179,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { }; let valueArray = createOrCollapseRange(userPastWeek.low, userPastWeek.high); let value = valueArray[1] ? valueArray[0] + '-' + valueArray[1] : valueArray[0]; - graphRecords.push({label: 'certain', x: `Past Week\n(${formatDateRangeOfDays(thisWeekDistance)})`, y: valueArray[0]}); - graphRecords.push({label: "uncertain", x: `Past Week\n(${formatDateRangeOfDays(thisWeekDistance)})`, y: userPastWeek.high - userPastWeek.low}) + graphRecords.push({label: 'certain', x: valueArray[0], y: `Past Week\n(${formatDateRangeOfDays(thisWeekDistance)})`}); + graphRecords.push({label: "uncertain", x: userPastWeek.high - userPastWeek.low, y: `Past Week\n(${formatDateRangeOfDays(thisWeekDistance)})`}) tempUserCarbon.push({label: "past week", value: value}); //calculate low-high and format range for prev week, if exists @@ -191,8 +191,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { }; valueArray = createOrCollapseRange(userPrevWeek.low, userPrevWeek.high); value = valueArray[1] ? valueArray[0] + '-' + valueArray[1] : valueArray[0]; - graphRecords.push({label: 'certain', x: `Previous Week\n(${formatDateRangeOfDays(lastWeekDistance)})`, y: valueArray[0]}); - graphRecords.push({label: "uncertain", x: `Previous Week\n(${formatDateRangeOfDays(lastWeekDistance)})`, y: userPrevWeek.high - userPrevWeek.low}) + graphRecords.push({label: 'certain', x: valueArray[0], y: `Previous Week\n(${formatDateRangeOfDays(lastWeekDistance)})`}); + graphRecords.push({label: "uncertain", x: userPrevWeek.high - userPrevWeek.low, y: `Previous Week\n(${formatDateRangeOfDays(lastWeekDistance)})`}) tempUserCarbon.push({label: "previous week", value: value}); let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); @@ -203,7 +203,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //calculate worst-case carbon footprint let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); - graphRecords.push({label: 'certain', x: 'if all taxi', y: worstCarbon}); + graphRecords.push({label: 'certain', x: worstCarbon, y: 'if all taxi'}); tempUserCarbon.push({label: "if all taxi", value: worstCarbon}); //push in goals @@ -220,7 +220,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { }, [userMetrics?.distance]) //hardcoded here, could be read from config at later customization? - let carbonGoals = [{label:"US 2030", value: 54}, {label:"US 2050", value: 14}]; + let carbonGoals = [{label:t('us-2030-goal'), value: 54}, {label:t('us-2050-goal'), value: 14}]; return ( { style={cardStyles.title(colors)} /> { graphRecords.length ? - + : From 3d0c910a02b9ada8cb021d3a1d7f186cc1867415 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 29 Aug 2023 14:37:48 -0600 Subject: [PATCH 050/164] clean up CarbonFootprintCard remove unused code, unused imports, and development log statements also refactored userCarbonRecords useMemo to handle the graph records, old UserCarbon was used for text-based display in initial draft --- www/js/metrics/CarbonFootprintCard.tsx | 43 +++++++------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 6657c0653..f7c16d558 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -1,10 +1,9 @@ -import React, { useEffect, useState, useMemo } from 'react'; +import React, { useState, useMemo } from 'react'; import { View } from 'react-native'; import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; -import { cardMargin, cardStyles, METRIC_LIST } from './MetricsTab'; -import { formatForDisplay } from '../config/useImperialConfig'; +import { cardMargin, cardStyles } from './MetricsTab'; import { filterToRecentWeeks, formatDateRangeOfDays } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; @@ -13,7 +12,6 @@ import ChangeIndicator from './ChangeIndicator'; //modes considered on foot for carbon calculation, expandable as needed const ON_FOOT_MODES = ['WALKING', 'RUNNING', 'ON_FOOT'] as const; -// type ActiveMode = typeof ACTIVE_MODES[number]; type Props = { userMetrics: MetricsData, aggMetrics: MetricsData } const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { @@ -21,10 +19,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { const { colors } = useTheme(); const { t } = useTranslation(); - console.log("metrics in carbon", userMetrics, aggMetrics); - const [emissionsChange, setEmissionsChange] = useState([]); - const [graphRecords, setGraphRecords] = useState([]); /* * metric2val is a function that takes a metric entry and a field and returns @@ -91,8 +86,6 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { } }); - console.log("testing metrics mode bins", mode_bins); - let return_val = []; for (let mode in mode_bins) { return_val.push({key: mode, values: mode_bins[mode]}); @@ -128,6 +121,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { return summaryMap; } + //from two weeks fo low and high values, calculates low and high change const calculatePercentChange = function(pastWeekRange, previousWeekRange) { let greaterLesserPct = { low: (pastWeekRange.low/previousWeekRange.low) * 100 - 100, @@ -136,6 +130,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { return greaterLesserPct; } + //digests low - high values to an array of 1 or 2 items (detects 0 diff) const createOrCollapseRange = function(low, high) { let range = []; if(high == low) { @@ -150,26 +145,24 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { const userCarbonRecords = useMemo(() => { if(userMetrics) { - console.log("before testing metrics", userMetrics); - + //separate data into weeks let thisWeekDistance = filterToRecentWeeks(userMetrics?.distance)[0]; let lastWeekDistance = filterToRecentWeeks(userMetrics?.distance)[1]; + + //formatted distance data from this week let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); let worstDistance = userThisWeekSummaryMap.reduce((prevDistance, currModeSummary) => prevDistance + currModeSummary.values, 0); + //formatted data from last week let userLastWeekModeMap = {}; let userLastWeekSummaryMap = {}; if(lastWeekDistance) { userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); - - console.log("testing metric map here", userThisWeekModeMap, userLastWeekModeMap); - console.log("testing summary data!!", userThisWeekSummaryMap, userLastWeekSummaryMap); } //setting up data to be displayed //TODO i18n for labels - let tempUserCarbon = []; let graphRecords = []; //calculate low-high and format range for past week @@ -181,7 +174,6 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { let value = valueArray[1] ? valueArray[0] + '-' + valueArray[1] : valueArray[0]; graphRecords.push({label: 'certain', x: valueArray[0], y: `Past Week\n(${formatDateRangeOfDays(thisWeekDistance)})`}); graphRecords.push({label: "uncertain", x: userPastWeek.high - userPastWeek.low, y: `Past Week\n(${formatDateRangeOfDays(thisWeekDistance)})`}) - tempUserCarbon.push({label: "past week", value: value}); //calculate low-high and format range for prev week, if exists if(userLastWeekSummaryMap[0]) { @@ -193,7 +185,6 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { value = valueArray[1] ? valueArray[0] + '-' + valueArray[1] : valueArray[0]; graphRecords.push({label: 'certain', x: valueArray[0], y: `Previous Week\n(${formatDateRangeOfDays(lastWeekDistance)})`}); graphRecords.push({label: "uncertain", x: userPrevWeek.high - userPrevWeek.low, y: `Previous Week\n(${formatDateRangeOfDays(lastWeekDistance)})`}) - tempUserCarbon.push({label: "previous week", value: value}); let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); let changeRange = createOrCollapseRange(pctChange.low, pctChange.high); @@ -202,20 +193,9 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //calculate worst-case carbon footprint let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); - graphRecords.push({label: 'certain', x: worstCarbon, y: 'if all taxi'}); - tempUserCarbon.push({label: "if all taxi", value: worstCarbon}); - //push in goals - graphRecords.push({label: 'certain', x: 'US 2030 goal', y: 54}); - graphRecords.push({label: 'certain', x: 'US 2050 goal', y: 14}); - tempUserCarbon.push({label: "US 2030 goal", value: 54}); - tempUserCarbon.push({label: "US 2050 goal", value: 14}); - - console.log("testing, the data is: ", tempUserCarbon); - setGraphRecords(graphRecords); - - return tempUserCarbon; + return graphRecords; } }, [userMetrics?.distance]) @@ -230,11 +210,12 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { titleVariant='titleLarge' titleStyle={cardStyles.titleText(colors)} titleNumberOfLines={2} + // {(props) => {}} />} right={(props) => } style={cardStyles.title(colors)} /> - { graphRecords.length ? - : From c93961b5c72635ea5f083a9163dc80a7c06c3804 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 29 Aug 2023 14:39:30 -0600 Subject: [PATCH 051/164] support split-line labels in horizontal BarChart incorporating a patch from @JGreenlee to allow the labels to split lines when the graph is horizontal --- www/js/components/BarChart.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 179b514dd..dda07f333 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -113,6 +113,14 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, beforeUpdate: (axis) => { setNumVisibleDatasets(axis.chart.getVisibleDatasetCount()) }, + ticks: { + callback: timeAxis ? undefined : (value, i) => { + const label = chartData[0].data[i].y; + if (typeof label == 'string' && label.includes('\n')) + return label.split('\n'); + return label; + }, + }, reverse: true, stacked, }, From 9e8aebc33a654e43a2cb7e79cf293f75e47d52fe Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 29 Aug 2023 15:03:46 -0600 Subject: [PATCH 052/164] i18n for CarbonFootprintCard made updates to en.json and CarbonCard to enable i18n translation of labels, titles, etc. "(kg Co2)" is now the only hard-coded string here --- www/i18n/en.json | 4 +++- www/js/metrics/CarbonFootprintCard.tsx | 16 ++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index 330159f65..e21a796c0 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -160,11 +160,13 @@ "how-it-compares": "Ballpark comparisons", "optimal": "Optimal (perfect mode choice for all my trips):", "average": "Average for group:", - "avoided": "CO₂ avoided (vs. all 'taxi'):", + "worst-case": "If all 'taxi':", "label-to-squish": "Label trips to collapse the range into a single number", "lastweek": "My last week value:", "us-2030-goal": "US 2030 Goal Estimate:", "us-2050-goal": "US 2050 Goal Estimate:", + "past-week" : "Past Week", + "prev-week" : "Previous Week", "calories": "My Calories", "calibrate": "Calibrate", "no-summary-data": "No summary data", diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index f7c16d558..ce00b893c 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -162,7 +162,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); } - //setting up data to be displayed //TODO i18n for labels + //setting up data to be displayed let graphRecords = []; //calculate low-high and format range for past week @@ -172,8 +172,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { }; let valueArray = createOrCollapseRange(userPastWeek.low, userPastWeek.high); let value = valueArray[1] ? valueArray[0] + '-' + valueArray[1] : valueArray[0]; - graphRecords.push({label: 'certain', x: valueArray[0], y: `Past Week\n(${formatDateRangeOfDays(thisWeekDistance)})`}); - graphRecords.push({label: "uncertain", x: userPastWeek.high - userPastWeek.low, y: `Past Week\n(${formatDateRangeOfDays(thisWeekDistance)})`}) + graphRecords.push({label: 'certain', x: valueArray[0], y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); + graphRecords.push({label: "uncertain", x: userPastWeek.high - userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}) //calculate low-high and format range for prev week, if exists if(userLastWeekSummaryMap[0]) { @@ -183,8 +183,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { }; valueArray = createOrCollapseRange(userPrevWeek.low, userPrevWeek.high); value = valueArray[1] ? valueArray[0] + '-' + valueArray[1] : valueArray[0]; - graphRecords.push({label: 'certain', x: valueArray[0], y: `Previous Week\n(${formatDateRangeOfDays(lastWeekDistance)})`}); - graphRecords.push({label: "uncertain", x: userPrevWeek.high - userPrevWeek.low, y: `Previous Week\n(${formatDateRangeOfDays(lastWeekDistance)})`}) + graphRecords.push({label: 'certain', x: valueArray[0], y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}); + graphRecords.push({label: "uncertain", x: userPrevWeek.high - userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}) let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); let changeRange = createOrCollapseRange(pctChange.low, pctChange.high); @@ -193,14 +193,14 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //calculate worst-case carbon footprint let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); - graphRecords.push({label: 'certain', x: worstCarbon, y: 'if all taxi'}); + graphRecords.push({label: 'certain', x: worstCarbon, y: `${t('main-metrics.worst-case')}`}); return graphRecords; } }, [userMetrics?.distance]) //hardcoded here, could be read from config at later customization? - let carbonGoals = [{label:t('us-2030-goal'), value: 54}, {label:t('us-2050-goal'), value: 14}]; + let carbonGoals = [{label: t('main-metrics.us-2030-goal'), value: 54}, {label: t('main-metrics.us-2050-goal'), value: 14}]; return ( { style={cardStyles.title(colors)} /> { userCarbonRecords?.length ? - : From 5aad944445f29e971bca4289091e92f9ff061d2e Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 29 Aug 2023 16:47:38 -0600 Subject: [PATCH 053/164] add group metrics to chart using aggMetrics and the same data processing functions, group average is shown alongside user specific metrics We might need to consider hiding this if it is too far out (I'm thinking of nrel-commute, for example, where about 5/6 trips are unlabeled) --- www/js/metrics/CarbonFootprintCard.tsx | 38 +++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index ce00b893c..3efaf5100 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -199,6 +199,42 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { } }, [userMetrics?.distance]) + const groupCarbonRecords = useMemo(() => { + if(aggMetrics) + { + //separate data into weeks + let thisWeekDistance = filterToRecentWeeks(aggMetrics?.distance)[0]; + console.log("testing agg metrics" , aggMetrics, thisWeekDistance); + //let lastWeekDistance = filterToRecentWeeks(aggMetrics?.distance)[1]; + + let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, "aggregate"); + let aggThisWeekSummary = generateSummaryFromData(aggThisWeekModeMap, "distance"); + + // Issue 422: + // https://github.com/e-mission/e-mission-docs/issues/422 + let aggCarbonData = []; + for (var i in aggThisWeekSummary) { + aggCarbonData.push(aggThisWeekSummary[i]); + if (isNaN(aggCarbonData[i].values)) { + console.warn("WARNING in calculating groupCarbonRecords: value is NaN for mode " + aggCarbonData[i].key + ", changing to 0"); + aggCarbonData[i].values = 0; + } + } + + let aggCarbon = { + low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), + high: FootprintHelper.getFootprintForMetrics(aggCarbonData, FootprintHelper.getHighestFootprint()), + } + let aggRange = createOrCollapseRange(aggCarbon.low, aggCarbon.high); + + let groupRecords = []; + groupRecords.push({label: 'certain', x: aggRange[0], y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); + groupRecords.push({label: "uncertain", x: aggCarbon.high - aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}) + + return groupRecords; + } + }, [aggMetrics]) + //hardcoded here, could be read from config at later customization? let carbonGoals = [{label: t('main-metrics.us-2030-goal'), value: 54}, {label: t('main-metrics.us-2050-goal'), value: 14}]; @@ -215,7 +251,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { style={cardStyles.title(colors)} /> { userCarbonRecords?.length ? - : From e6370c03c70d0a916c321efff24147f9652ac690 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 29 Aug 2023 16:48:39 -0600 Subject: [PATCH 054/164] adjust styling of ChangeIdicator remove accidental import, and adjust to better match the wireframe --- www/js/metrics/ChangeIndicator.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/www/js/metrics/ChangeIndicator.tsx b/www/js/metrics/ChangeIndicator.tsx index 82c23ae63..b3ea88983 100644 --- a/www/js/metrics/ChangeIndicator.tsx +++ b/www/js/metrics/ChangeIndicator.tsx @@ -3,7 +3,6 @@ import { View } from 'react-native'; import { useTheme, Text } from "react-native-paper"; import { useTranslation } from 'react-i18next'; import { formatForDisplay } from '../config/useImperialConfig'; -import { Colors } from 'chart.js'; type Props = { change: number[], @@ -39,10 +38,10 @@ const ChangeIndicator = ({ change }) => { (change[0]) ? 0 ? colors.danger : colors.success)}> - {`${changeText} \n`} + {`${changeText}% \n`} - {`${t("% this week")}`} + {`${t("this week")}`} : @@ -64,12 +63,12 @@ const styles: any = { }), view: (color) => ({ backgroundColor: color, - padding: 4, + padding: 2, borderStyle: 'solid', + borderColor: color, borderWidth: 2, - borderRadius: 5, + borderRadius: 10, }), - - } +} export default ChangeIndicator; \ No newline at end of file From 258721c74cd5a25fd08cc1e6d840742128613a3e Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 30 Aug 2023 13:58:01 -0600 Subject: [PATCH 055/164] prevent double negative in ChangeIndicator, discovered an error where negative values showed prefixed by -- instead of - added abs calls to prevent this double negative --- www/js/metrics/ChangeIndicator.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/ChangeIndicator.tsx b/www/js/metrics/ChangeIndicator.tsx index b3ea88983..f8bcbe65d 100644 --- a/www/js/metrics/ChangeIndicator.tsx +++ b/www/js/metrics/ChangeIndicator.tsx @@ -25,10 +25,10 @@ const ChangeIndicator = ({ change }) => { if(change) { if(change.length == 1) { - let text = changeSign(change[0]) + formatForDisplay(change[0]); + let text = changeSign(change[0]) + formatForDisplay(Math.abs(change[0])); return text; } else { - let text = changeSign(change[0]) + formatForDisplay(change[0]) + " / " + changeSign(change[1]) + formatForDisplay(change[1]); + let text = changeSign(change[0]) + formatForDisplay(Math.abs(change[0])) + " / " + changeSign(change[1]) + formatForDisplay(Math.abs(change[1])); return text; } } From 8151d7f7cc798f3895f7f669584d455b20e62aa6 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 30 Aug 2023 15:35:01 -0600 Subject: [PATCH 056/164] fix time ticks When we override the callback in any manner, we're responsible for all formatting of the ticks https://www.chartjs.org/docs/latest/axes/labelling.html overriding to "undefined" meant that the default tick formatter was never called, and the time ticks were unformatted By bumping the timeAxis switch up a level, we prevent overriding the default time ticks, and the dates are shown in an intuitive way --- www/js/components/BarChart.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index dda07f333..dc09c70b2 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -113,8 +113,8 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, beforeUpdate: (axis) => { setNumVisibleDatasets(axis.chart.getVisibleDatasetCount()) }, - ticks: { - callback: timeAxis ? undefined : (value, i) => { + ticks: timeAxis ? {} : { + callback: (value, i) => { const label = chartData[0].data[i].y; if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); @@ -139,8 +139,9 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, unit: 'day', tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 } : {}, - ticks: { - callback: timeAxis ? undefined : (value, i) => { + ticks: timeAxis ? {} : { + callback: (value, i) => { + console.log("testing vertical", chartData, i); const label = chartData[0].data[i].x; if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); From 4c44a0089d12ed64c75a1be432271a0dc8d1e638 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 30 Aug 2023 17:05:07 -0600 Subject: [PATCH 057/164] patches to % change when switching from 2+ weeks to a date range < 2 weeks, we should take the change indicator off the card, we accomplish this by setting the emissionsChange to "{}" in the case of NaN, we want to display the change as infinite, since it's a result of dividing by a 0 value for the previous week --- www/js/metrics/CarbonFootprintCard.tsx | 2 ++ www/js/metrics/ChangeIndicator.tsx | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 3efaf5100..7a4de4082 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -189,6 +189,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); let changeRange = createOrCollapseRange(pctChange.low, pctChange.high); setEmissionsChange(changeRange); + } else { + setEmissionsChange({}); } //calculate worst-case carbon footprint diff --git a/www/js/metrics/ChangeIndicator.tsx b/www/js/metrics/ChangeIndicator.tsx index f8bcbe65d..d76ad30c0 100644 --- a/www/js/metrics/ChangeIndicator.tsx +++ b/www/js/metrics/ChangeIndicator.tsx @@ -24,11 +24,13 @@ const ChangeIndicator = ({ change }) => { console.log("testing what change is", change); if(change) { if(change.length == 1) + let low = isNaN(change.low) ? '∞' : formatForDisplay(Math.abs(change.low)); + let high = isNaN(change.high) ? '∞' : formatForDisplay(Math.abs(change.high)); { - let text = changeSign(change[0]) + formatForDisplay(Math.abs(change[0])); + let text = changeSign(change.low) + low; return text; } else { - let text = changeSign(change[0]) + formatForDisplay(Math.abs(change[0])) + " / " + changeSign(change[1]) + formatForDisplay(Math.abs(change[1])); + let text = changeSign(change.low) + low + " / " + changeSign(change.high) + high; return text; } } From 8cc6d7f857133b7db380455a41bc8c9da4241272 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 30 Aug 2023 17:11:21 -0600 Subject: [PATCH 058/164] eliminate create range function This range is no longer needed, as we can use the {low, high} object without converting it --- www/js/metrics/CarbonFootprintCard.tsx | 29 +++++--------------------- www/js/metrics/ChangeIndicator.tsx | 9 ++++---- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 7a4de4082..27dc6a117 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -19,7 +19,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { const { colors } = useTheme(); const { t } = useTranslation(); - const [emissionsChange, setEmissionsChange] = useState([]); + const [emissionsChange, setEmissionsChange] = useState({}); /* * metric2val is a function that takes a metric entry and a field and returns @@ -123,26 +123,13 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //from two weeks fo low and high values, calculates low and high change const calculatePercentChange = function(pastWeekRange, previousWeekRange) { - let greaterLesserPct = { + let greaterLesserPct = { low: (pastWeekRange.low/previousWeekRange.low) * 100 - 100, high: (pastWeekRange.high/previousWeekRange.high) * 100 - 100, } return greaterLesserPct; } - //digests low - high values to an array of 1 or 2 items (detects 0 diff) - const createOrCollapseRange = function(low, high) { - let range = []; - if(high == low) { - range.push(low); - } - else { - range.push(low); - range.push(high); - } - return range; - } - const userCarbonRecords = useMemo(() => { if(userMetrics) { //separate data into weeks @@ -170,10 +157,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), high: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, FootprintHelper.getHighestFootprint()), }; - let valueArray = createOrCollapseRange(userPastWeek.low, userPastWeek.high); - let value = valueArray[1] ? valueArray[0] + '-' + valueArray[1] : valueArray[0]; - graphRecords.push({label: 'certain', x: valueArray[0], y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); graphRecords.push({label: "uncertain", x: userPastWeek.high - userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}) + graphRecords.push({label: 'certain', x: userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); //calculate low-high and format range for prev week, if exists if(userLastWeekSummaryMap[0]) { @@ -181,14 +166,10 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) }; - valueArray = createOrCollapseRange(userPrevWeek.low, userPrevWeek.high); - value = valueArray[1] ? valueArray[0] + '-' + valueArray[1] : valueArray[0]; - graphRecords.push({label: 'certain', x: valueArray[0], y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}); graphRecords.push({label: "uncertain", x: userPrevWeek.high - userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}) let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); - let changeRange = createOrCollapseRange(pctChange.low, pctChange.high); - setEmissionsChange(changeRange); + setEmissionsChange(pctChange); } else { setEmissionsChange({}); } @@ -220,7 +201,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { if (isNaN(aggCarbonData[i].values)) { console.warn("WARNING in calculating groupCarbonRecords: value is NaN for mode " + aggCarbonData[i].key + ", changing to 0"); aggCarbonData[i].values = 0; - } + } } let aggCarbon = { diff --git a/www/js/metrics/ChangeIndicator.tsx b/www/js/metrics/ChangeIndicator.tsx index d76ad30c0..047ca4794 100644 --- a/www/js/metrics/ChangeIndicator.tsx +++ b/www/js/metrics/ChangeIndicator.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { formatForDisplay } from '../config/useImperialConfig'; type Props = { - change: number[], + change: {low: number, high: number}, } const ChangeIndicator = ({ change }) => { @@ -21,11 +21,10 @@ const ChangeIndicator = ({ change }) => { }; const changeText = useMemo(() => { - console.log("testing what change is", change); if(change) { - if(change.length == 1) let low = isNaN(change.low) ? '∞' : formatForDisplay(Math.abs(change.low)); let high = isNaN(change.high) ? '∞' : formatForDisplay(Math.abs(change.high)); + if(change.low == change.high) { let text = changeSign(change.low) + low; return text; @@ -37,8 +36,8 @@ const ChangeIndicator = ({ change }) => { },[change]) return ( - (change[0]) ? - 0 ? colors.danger : colors.success)}> + (change.low) ? + 0 ? colors.danger : colors.success)}> {`${changeText}% \n`} From 9b128b7b4ecb058485080ae2cc8ec82adfbac595 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 30 Aug 2023 17:15:57 -0600 Subject: [PATCH 059/164] show certain/uncertain and goal lines show group bar with user metrics for "seeing how you stack up" add goal lines in yellow and red to show that "less is more" --- www/i18n/en.json | 10 +++--- www/js/appTheme.ts | 1 + www/js/components/BarChart.tsx | 11 +++--- www/js/metrics/CarbonFootprintCard.tsx | 48 +++++++++++++++++--------- 4 files changed, 43 insertions(+), 27 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index e21a796c0..84dff390a 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -159,14 +159,14 @@ "footprint": "My Footprint", "how-it-compares": "Ballpark comparisons", "optimal": "Optimal (perfect mode choice for all my trips):", - "average": "Average for group:", + "average": "Group Average:", "worst-case": "If all 'taxi':", "label-to-squish": "Label trips to collapse the range into a single number", "lastweek": "My last week value:", - "us-2030-goal": "US 2030 Goal Estimate:", - "us-2050-goal": "US 2050 Goal Estimate:", - "past-week" : "Past Week", - "prev-week" : "Previous Week", + "us-2030-goal": "❗ US 2030 Goal", + "us-2050-goal": "⚠️ US 2050 Goal", + "past-week" : "Past Week:", + "prev-week" : "Previous Week:", "calories": "My Calories", "calibrate": "Calibrate", "no-summary-data": "No summary data", diff --git a/www/js/appTheme.ts b/www/js/appTheme.ts index 836da652b..83271b86b 100644 --- a/www/js/appTheme.ts +++ b/www/js/appTheme.ts @@ -26,6 +26,7 @@ const AppTheme = { level5: '#d6ebff', // lch(92% 50 250) }, success: '#38872e', // lch(50% 55 135) + warn: '#f8cf53', //lch(85% 65 85) danger: '#f23934' // lch(55% 85 35) }, roundness: 5, diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index dc09c70b2..737945a46 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -26,12 +26,13 @@ type BarChartData = { type Props = { records: { label: string, x: number|string, y: number|string }[], axisTitle: string, - lineAnnotations?: { value: number, label: string }[], + lineAnnotations?: { value: number, label: string, color: string }[], isHorizontal?: boolean, timeAxis?: boolean, stacked?: boolean, + customPalette?: {}, } -const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, stacked }: Props) => { +const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, stacked, customPalette }: Props) => { const { colors } = useTheme(); const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); @@ -91,7 +92,7 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, data={{datasets: chartData.map((e, i) => ({ ...e, // cycle through the default palette, repeat if necessary - backgroundColor: defaultPalette[i % defaultPalette.length], + backgroundColor: customPalette ? customPalette[chartData[i].label]: defaultPalette[i % defaultPalette.length], }))}} options={{ indexAxis: indexAxis, @@ -173,8 +174,8 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, }, ...(isHorizontal ? { xMin: a.value, xMax: a.value } : { yMin: a.value, yMax: a.value }), - borderColor: colors.onBackground, - borderWidth: 2, + borderColor: a.color, + borderWidth: 3, borderDash: [3, 3], } satisfies AnnotationOptions)), } diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 27dc6a117..f0b939f5f 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -167,6 +167,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) }; graphRecords.push({label: "uncertain", x: userPrevWeek.high - userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}) + graphRecords.push({label: 'certain', x: userPastWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}); let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); setEmissionsChange(pctChange); @@ -188,7 +189,6 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //separate data into weeks let thisWeekDistance = filterToRecentWeeks(aggMetrics?.distance)[0]; console.log("testing agg metrics" , aggMetrics, thisWeekDistance); - //let lastWeekDistance = filterToRecentWeeks(aggMetrics?.distance)[1]; let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, "aggregate"); let aggThisWeekSummary = generateSummaryFromData(aggThisWeekModeMap, "distance"); @@ -204,22 +204,37 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { } } + let groupRecords = []; + let aggCarbon = { low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), high: FootprintHelper.getFootprintForMetrics(aggCarbonData, FootprintHelper.getHighestFootprint()), } - let aggRange = createOrCollapseRange(aggCarbon.low, aggCarbon.high); - - let groupRecords = []; - groupRecords.push({label: 'certain', x: aggRange[0], y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); - groupRecords.push({label: "uncertain", x: aggCarbon.high - aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}) + console.log("testing group past week", aggCarbon); + groupRecords.push({label: "uncertain", x: aggCarbon.high - aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); + groupRecords.push({label: 'certain', x: aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); return groupRecords; } }, [aggMetrics]) + const chartData = useMemo(() => { + let tempChartData = []; + if(userCarbonRecords?.length) { + tempChartData = tempChartData.concat(userCarbonRecords); + } + if(groupCarbonRecords?.length) { + tempChartData = tempChartData.concat(groupCarbonRecords); + } + tempChartData = tempChartData.reverse(); + console.log("testing chart data", tempChartData); + return tempChartData; + }, [userCarbonRecords, groupCarbonRecords]); + //hardcoded here, could be read from config at later customization? - let carbonGoals = [{label: t('main-metrics.us-2030-goal'), value: 54}, {label: t('main-metrics.us-2050-goal'), value: 14}]; + let carbonGoals = [ {label: t('main-metrics.us-2030-goal'), value: 54, color: colors.danger}, + {label: t('main-metrics.us-2050-goal'), value: 14, color: colors.warn}]; + let colorPalette = {certain: colors.primary, uncertain: colors.primaryContainer}; return ( { titleVariant='titleLarge' titleStyle={cardStyles.titleText(colors)} titleNumberOfLines={2} - // {(props) => {}} />} right={(props) => } style={cardStyles.title(colors)} /> - { userCarbonRecords?.length ? - - : - - - {t('metrics.chart-no-data')} - - + { chartData?.length > 0 ? + + : + + + {t('metrics.chart-no-data')} + + } From 8fc1c65ba62e54d9cda3f452f589dc0a76112143 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 30 Aug 2023 20:47:31 -0400 Subject: [PATCH 060/164] make ToggleSwitch more generic This is basically just a wrapper around SegmentedButtons; let's type it as such and still allow any other SegmentedButtonsprops to be passed through --- www/js/metrics/MetricsCard.tsx | 4 ++-- www/js/metrics/ToggleSwitch.tsx | 16 +++++++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index c3ceb72c8..6a10fcb1f 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -69,9 +69,9 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni titleNumberOfLines={2} right={() => - setViewMode(v as any)} options={[{ icon: 'abacus', value: 'details' }, { icon: 'chart-bar', value: 'graph' }]} /> - setPopulationMode(p as any)} options={[{ icon: 'account', value: 'user' }, { icon: 'account-group', value: 'aggregate' }]} /> } diff --git a/www/js/metrics/ToggleSwitch.tsx b/www/js/metrics/ToggleSwitch.tsx index e8dbcd19d..70b51f5d7 100644 --- a/www/js/metrics/ToggleSwitch.tsx +++ b/www/js/metrics/ToggleSwitch.tsx @@ -1,19 +1,17 @@ import React from 'react'; -import { SegmentedButtons, useTheme } from "react-native-paper"; +import { SegmentedButtons, SegmentedButtonsProps, useTheme } from "react-native-paper"; -type Props = { +type Props = Omit & { + onValueChange: (value: string) => void, value: string, - setValue: (value: string) => void, options: { icon: string, value: string }[], } -const ToggleSwitch = ({ value, setValue, options }) => { +const ToggleSwitch = ({ options, onValueChange, value, ...rest }: Props) => { const { colors} = useTheme(); return ( - setValue(v)} - density='high' + ({ value: o.value, icon: o.icon, @@ -21,8 +19,8 @@ const ToggleSwitch = ({ value, setValue, options }) => { showSelectedCheck: true, style: { minWidth: 0, - borderTopWidth: 0, - borderBottomWidth: 0, + borderTopWidth: rest.density == 'high' ? 0 : 1, + borderBottomWidth: rest.density == 'high' ? 0 : 1, backgroundColor: value == o.value ? colors.elevation.level2 : colors.surfaceDisabled, }, }))} /> From c4684782bd19fbc6d12e8de9194d6a395a732f07 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 30 Aug 2023 20:50:11 -0400 Subject: [PATCH 061/164] move ToggleSwitch to /js/components So it may be used on other screens in the future --- .../{metrics => components}/ToggleSwitch.tsx | 4 +- www/js/metrics/MetricsCard.tsx | 2 +- www/js/metrics/metricsHelper.ts | 49 +++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) rename www/js/{metrics => components}/ToggleSwitch.tsx (90%) diff --git a/www/js/metrics/ToggleSwitch.tsx b/www/js/components/ToggleSwitch.tsx similarity index 90% rename from www/js/metrics/ToggleSwitch.tsx rename to www/js/components/ToggleSwitch.tsx index 70b51f5d7..2a73b70c0 100644 --- a/www/js/metrics/ToggleSwitch.tsx +++ b/www/js/components/ToggleSwitch.tsx @@ -11,7 +11,7 @@ const ToggleSwitch = ({ options, onValueChange, value, ...rest }: Props) => { const { colors} = useTheme(); return ( - ({ value: o.value, icon: o.icon, @@ -23,7 +23,7 @@ const ToggleSwitch = ({ options, onValueChange, value, ...rest }: Props) => { borderBottomWidth: rest.density == 'high' ? 0 : 1, backgroundColor: value == o.value ? colors.elevation.level2 : colors.surfaceDisabled, }, - }))} /> + }))} {...rest} /> ) } diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index 6a10fcb1f..77a565171 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -5,7 +5,7 @@ import { Card, Checkbox, Text, useTheme } from 'react-native-paper'; import BarChart from '../components/BarChart'; import { DayOfMetricData } from './metricsTypes'; import { getLabelsForDay, getUniqueLabelsForDays } from './metricsHelper'; -import ToggleSwitch from './ToggleSwitch'; +import ToggleSwitch from '../components/ToggleSwitch'; import { cardStyles } from './MetricsTab'; import { labelKeyToReadable } from '../survey/multilabel/confirmHelper'; diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index 1c01b7fee..4f3df156a 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -1,6 +1,7 @@ import { DateTime } from "luxon"; import { formatForDisplay } from "../config/useImperialConfig"; import { DayOfMetricData } from "./metricsTypes"; +import { getAngularService } from "../angular-react-helper"; export function getUniqueLabelsForDays(metricDataDays: DayOfMetricData[]) { const uniqueLabels: string[] = []; @@ -47,3 +48,51 @@ export function formatDateRangeOfDays(days: DayOfMetricData[]) { const lastDay = lastDayDt.toLocaleString({...DateTime.DATE_SHORT, year: undefined}); return `${firstDay} - ${lastDay}`; } + +/* for each mode label present in `days`, sum the cumulative distance and duration to get + the average speed for that mode */ +export function getAvgSpeedsForDays(days: DayOfMetricData[]) { + const avgSpeeds: { [key: string]: number } = {}; + const sumDistances: { [key: string]: number } = {}; + const sumDurations: { [key: string]: number } = {}; + days.forEach(day => { + const labels = getLabelsForDay(day); + labels.forEach(label => { + sumDistances[label] = (sumDistances[label] || 0) + day[`distance_${label}`]; + sumDurations[label] = (sumDurations[label] || 0) + day[`duration_${label}`]; + }); + }); + Object.keys(sumDistances).forEach(label => { + avgSpeeds[label] = sumDistances[label] / sumDurations[label]; + }); + return avgSpeeds; +} + +function getMETs() { + if (getAngularService('CalorieCal').useCustom == true) { + return getAngularService('CustomDatasetHelper').getCustomMETs(); + } + return getAngularService('METDatasetHelper').getStandardMETs(); +} + +const MPS_TO_MPH = 2.23694; +export function getMetForModeAndSpeed(mode: string, speed: number, defaultIfMissing = 0) { + if (mode == 'ON_FOOT') { + console.log("CalorieCal.getMet() converted 'ON_FOOT' to 'WALKING'"); + mode = 'WALKING'; + } + let currentMETs = getMETs(); + if (!currentMETs[mode]) { + console.warn("CalorieCal.getMet() Illegal mode: " + mode); + return defaultIfMissing; //So the calorie sum does not break with wrong return type + } + for (var i in currentMETs[mode]) { + const speedMph = speed * MPS_TO_MPH; + if (speedMph > currentMETs[mode][i].range[0] && speedMph < currentMETs[mode][i].range[1]) { + return currentMETs[mode][i].mets; + } else if (speedMph < 0 ) { + console.log("CalorieCal.getMet() Negative speed: " + speedMph); + return 0; + } + } +} From 29a0ab11fd4bd9bc9ef189dbedd8b8e18c29363d Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 30 Aug 2023 21:15:05 -0400 Subject: [PATCH 062/164] match ToggleSwitch props to SegmentedButtonProps Now this is a perfect wrapper around SegmentedButton, accepting exactly the same props and just applying a different default style. This should aid in reusing this component on other screens. --- www/js/components/ToggleSwitch.tsx | 14 +++++--------- www/js/metrics/MetricsCard.tsx | 4 ++-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/www/js/components/ToggleSwitch.tsx b/www/js/components/ToggleSwitch.tsx index 2a73b70c0..3b9b5cf25 100644 --- a/www/js/components/ToggleSwitch.tsx +++ b/www/js/components/ToggleSwitch.tsx @@ -1,18 +1,13 @@ import React from 'react'; import { SegmentedButtons, SegmentedButtonsProps, useTheme } from "react-native-paper"; -type Props = Omit & { - onValueChange: (value: string) => void, - value: string, - options: { icon: string, value: string }[], -} -const ToggleSwitch = ({ options, onValueChange, value, ...rest }: Props) => { +const ToggleSwitch = ({ value, buttons, ...rest}: SegmentedButtonsProps) => { - const { colors} = useTheme(); + const { colors } = useTheme(); return ( - ({ + rest.onValueChange(v as any)} + buttons={buttons.map(o => ({ value: o.value, icon: o.icon, uncheckedColor: colors.onSurfaceDisabled, @@ -23,6 +18,7 @@ const ToggleSwitch = ({ options, onValueChange, value, ...rest }: Props) => { borderBottomWidth: rest.density == 'high' ? 0 : 1, backgroundColor: value == o.value ? colors.elevation.level2 : colors.surfaceDisabled, }, + ...o }))} {...rest} /> ) } diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index 77a565171..a20bf598b 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -70,9 +70,9 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni right={() => setViewMode(v as any)} - options={[{ icon: 'abacus', value: 'details' }, { icon: 'chart-bar', value: 'graph' }]} /> + buttons={[{ icon: 'abacus', value: 'details' }, { icon: 'chart-bar', value: 'graph' }]} /> setPopulationMode(p as any)} - options={[{ icon: 'account', value: 'user' }, { icon: 'account-group', value: 'aggregate' }]} /> + buttons={[{ icon: 'account', value: 'user' }, { icon: 'account-group', value: 'aggregate' }]} /> } style={cardStyles.title(colors)} /> From d2a8dc5f1186dafa4d25c9d455f9d62309f09170 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 31 Aug 2023 01:21:01 -0400 Subject: [PATCH 063/164] revert extra functions in metricsHelper.ts These functions were committed by accident in c4684782bd19fbc6d12e8de9194d6a395a732f07 and did not belong there --- www/js/metrics/metricsHelper.ts | 49 --------------------------------- 1 file changed, 49 deletions(-) diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index 4f3df156a..1c01b7fee 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -1,7 +1,6 @@ import { DateTime } from "luxon"; import { formatForDisplay } from "../config/useImperialConfig"; import { DayOfMetricData } from "./metricsTypes"; -import { getAngularService } from "../angular-react-helper"; export function getUniqueLabelsForDays(metricDataDays: DayOfMetricData[]) { const uniqueLabels: string[] = []; @@ -48,51 +47,3 @@ export function formatDateRangeOfDays(days: DayOfMetricData[]) { const lastDay = lastDayDt.toLocaleString({...DateTime.DATE_SHORT, year: undefined}); return `${firstDay} - ${lastDay}`; } - -/* for each mode label present in `days`, sum the cumulative distance and duration to get - the average speed for that mode */ -export function getAvgSpeedsForDays(days: DayOfMetricData[]) { - const avgSpeeds: { [key: string]: number } = {}; - const sumDistances: { [key: string]: number } = {}; - const sumDurations: { [key: string]: number } = {}; - days.forEach(day => { - const labels = getLabelsForDay(day); - labels.forEach(label => { - sumDistances[label] = (sumDistances[label] || 0) + day[`distance_${label}`]; - sumDurations[label] = (sumDurations[label] || 0) + day[`duration_${label}`]; - }); - }); - Object.keys(sumDistances).forEach(label => { - avgSpeeds[label] = sumDistances[label] / sumDurations[label]; - }); - return avgSpeeds; -} - -function getMETs() { - if (getAngularService('CalorieCal').useCustom == true) { - return getAngularService('CustomDatasetHelper').getCustomMETs(); - } - return getAngularService('METDatasetHelper').getStandardMETs(); -} - -const MPS_TO_MPH = 2.23694; -export function getMetForModeAndSpeed(mode: string, speed: number, defaultIfMissing = 0) { - if (mode == 'ON_FOOT') { - console.log("CalorieCal.getMet() converted 'ON_FOOT' to 'WALKING'"); - mode = 'WALKING'; - } - let currentMETs = getMETs(); - if (!currentMETs[mode]) { - console.warn("CalorieCal.getMet() Illegal mode: " + mode); - return defaultIfMissing; //So the calorie sum does not break with wrong return type - } - for (var i in currentMETs[mode]) { - const speedMph = speed * MPS_TO_MPH; - if (speedMph > currentMETs[mode][i].range[0] && speedMph < currentMETs[mode][i].range[1]) { - return currentMETs[mode][i].mets; - } else if (speedMph < 0 ) { - console.log("CalorieCal.getMet() Negative speed: " + speedMph); - return 0; - } - } -} From d9f478beb9818aa56686947dc56d29482171e897 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 31 Aug 2023 01:43:27 -0400 Subject: [PATCH 064/164] ActiveMinutesCard -> WeeklyActiveMinutesCard refactoring this and putting it in a Carousel in preparation of creating a new card component to place in the carousel --- www/js/metrics/MetricsTab.tsx | 12 ++++++++++-- ...veMinutesCard.tsx => WeeklyActiveMinutesCard.tsx} | 9 ++------- 2 files changed, 12 insertions(+), 9 deletions(-) rename www/js/metrics/{ActiveMinutesCard.tsx => WeeklyActiveMinutesCard.tsx} (92%) diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index ce8d54b9e..7a8dc4cab 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -9,7 +9,7 @@ import { MetricsData } from "./metricsTypes"; import MetricsCard from "./MetricsCard"; import { formatForDisplay, useImperialConfig } from "../config/useImperialConfig"; import MetricsDateSelect from "./MetricsDateSelect"; -import ActiveMinutesCard from "./ActiveMinutesCard"; +import WeeklyActiveMinutesCard from "./WeeklyActiveMinutesCard"; import { secondsToHours, secondsToMinutes } from "./metricsHelper"; import CarbonFootprintCard from "./CarbonFootprintCard"; import Carousel from "../components/Carousel"; @@ -69,6 +69,12 @@ const MetricsTab = () => { // TODO } + // fake data for testing active minutes - TODO: remove + userMetrics?.duration.forEach((day, i) => { + day.label_walk = day.label_walk || 65*i + (Math.random() * 600); + day.label_bike = day.label_bike || 25*i + (Math.random() * 400); + }); + const { width: windowWidth } = useWindowDimensions(); const cardWidth = windowWidth * .85; @@ -80,7 +86,9 @@ const MetricsTab = () => { - + + + { +const WeeklyActiveMinutesCard = ({ userMetrics }: Props) => { const { colors } = useTheme(); const { t } = useTranslation(); - - userMetrics?.duration.forEach(day => { - day.label_walk = day.label_walk || 400 + (Math.random() * 300); - day.label_bike = day.label_bike || 200 + (Math.random() * 300); - }); // number of minutes for each of [walk, bike] const activeModesDurations = useMemo(() => { @@ -88,4 +83,4 @@ const ActiveMinutesCard = ({ userMetrics }: Props) => { ) } -export default ActiveMinutesCard; +export default WeeklyActiveMinutesCard; From 4613f8003f2e7f7a1f9fe039049cad192bd4d889 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 31 Aug 2023 02:33:34 -0400 Subject: [PATCH 065/164] describe active minutes at bottom of card --- www/js/metrics/WeeklyActiveMinutesCard.tsx | 26 ++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/www/js/metrics/WeeklyActiveMinutesCard.tsx b/www/js/metrics/WeeklyActiveMinutesCard.tsx index 44a9c00d5..dadf9aa3a 100644 --- a/www/js/metrics/WeeklyActiveMinutesCard.tsx +++ b/www/js/metrics/WeeklyActiveMinutesCard.tsx @@ -53,7 +53,7 @@ const WeeklyActiveMinutesCard = ({ userMetrics }: Props) => { }, [userMetrics?.duration]); return ( - { titleNumberOfLines={2} style={cardStyles.title(colors)} /> - { activeModesDurations.map((mode, i) => ( - - {labelKeyToReadable(ACTIVE_MODES[i])} - {`${mode} ${t('metrics.minutes')}`} - - ))} { weeklyActiveMinutesRecords.length ? @@ -78,6 +72,24 @@ const WeeklyActiveMinutesCard = ({ userMetrics }: Props) => { } + {activeModesDurations?.length && + + + {`Overall for ${formatDateRangeOfDays(userMetrics.duration)}:`} + + + {activeModesDurations.map((mode, i) => + + {labelKeyToReadable(ACTIVE_MODES[i])} + {' - '} + + {`${mode} ${t('metrics.minutes')}`} + + + )} + + + } ) From 8a605e03ae05411a4e2a666e95464def02712e75 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 31 Aug 2023 02:36:24 -0400 Subject: [PATCH 066/164] add LineChart.tsx The LineChart has only a few differences from BarChart - there is no 'stacked' or 'annotations', but we must consider a few new properties: 'borderColor' 'borderWidth' and 'tension' for the lines between points --- www/js/components/LineChart.tsx | 142 ++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 www/js/components/LineChart.tsx diff --git a/www/js/components/LineChart.tsx b/www/js/components/LineChart.tsx new file mode 100644 index 000000000..9e707958a --- /dev/null +++ b/www/js/components/LineChart.tsx @@ -0,0 +1,142 @@ + +import React, { useRef, useMemo } from 'react'; +import { View } from 'react-native'; +import { Chart, CategoryScale, LinearScale, Title, Tooltip, Legend, TimeScale, PointElement, LineElement } from 'chart.js'; +import { Line } from 'react-chartjs-2'; + +Chart.register( + CategoryScale, + LinearScale, + TimeScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +); + +type BarChartData = { + label: string, + data: { x: number|string, y: number|string }[], +}[]; + +type Props = { + records: { label: string, x: number|string, y: number|string }[], + axisTitle: string, + isHorizontal?: boolean, + timeAxis?: boolean, +} +const LineChart = ({ records, axisTitle, isHorizontal=true, timeAxis }: Props) => { + + const indexAxis = isHorizontal ? 'y' : 'x'; + const lineChartRef = useRef(null); + + // group records by label (this is the format that Chart.js expects) + const chartData = useMemo(() => { + return records?.reduce((acc, record) => { + const existing = acc.find(e => e.label == record.label); + if (!existing) { + acc.push({ + label: record.label, + data: [{ + x: record.x, + y: record.y, + }], + }); + } else { + existing.data.push({ + x: record.x, + y: record.y, + }); + } + return acc; + }, [] as BarChartData); + }, [records]); + + return ( + + ({ + ...e, + // cycle through the default palette, repeat if necessary + backgroundColor: defaultPalette[i % defaultPalette.length], + borderColor: defaultPalette[i % defaultPalette.length], + borderWidth: 2, + tension: .2, + }))}} + options={{ + indexAxis: indexAxis, + responsive: true, + maintainAspectRatio: false, + resizeDelay: 1, + scales: { + ...(isHorizontal ? { + y: { + offset: true, + type: timeAxis ? 'time' : 'category', + adapters: timeAxis ? { + date: { zone: 'utc' }, + } : {}, + time: timeAxis ? { + unit: 'day', + tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 + } : {}, + ticks: timeAxis ? {} : { + callback: (value, i) => { + const label = chartData[0].data[i].y; + if (typeof label == 'string' && label.includes('\n')) + return label.split('\n'); + return label; + }, + }, + reverse: true, + }, + x: { + title: { display: true, text: axisTitle }, + }, + } : { + x: { + offset: true, + type: timeAxis ? 'time' : 'category', + adapters: timeAxis ? { + date: { zone: 'utc' }, + } : {}, + time: timeAxis ? { + unit: 'day', + tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 + } : {}, + ticks: timeAxis ? {} : { + callback: (value, i) => { + console.log("testing vertical", chartData, i); + const label = chartData[0].data[i].x; + if (typeof label == 'string' && label.includes('\n')) + return label.split('\n'); + return label; + }, + }, + }, + y: { + title: { display: true, text: axisTitle }, + }, + }), + }, + }} /> + + ) +} + +const defaultPalette = [ + '#c95465', // red oklch(60% 0.15 14) + '#4a71b1', // blue oklch(55% 0.11 260) + '#d2824e', // orange oklch(68% 0.12 52) + '#856b5d', // brown oklch(55% 0.04 50) + '#59894f', // green oklch(58% 0.1 140) + '#e0cc55', // yellow oklch(84% 0.14 100) + '#b273ac', // purple oklch(64% 0.11 330) + '#f09da6', // pink oklch(78% 0.1 12) + '#b3aca8', // grey oklch(75% 0.01 55) + '#80afad', // teal oklch(72% 0.05 192) +]; + + +export default LineChart; From 2aea2e5b876e5f8fab76f3e184184272da07fefc Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 31 Aug 2023 02:46:55 -0400 Subject: [PATCH 067/164] add DailyActiveMinutesCard to MetricsTab This component has a line chart showing (up to) the last 14 days, with active minutes for each day on the Y axis. There are separate lines for bike and walk. --- www/js/metrics/DailyActiveMinutesCard.tsx | 63 +++++++++++++++++++++++ www/js/metrics/MetricsTab.tsx | 2 + 2 files changed, 65 insertions(+) create mode 100644 www/js/metrics/DailyActiveMinutesCard.tsx diff --git a/www/js/metrics/DailyActiveMinutesCard.tsx b/www/js/metrics/DailyActiveMinutesCard.tsx new file mode 100644 index 000000000..ea4d4d8f5 --- /dev/null +++ b/www/js/metrics/DailyActiveMinutesCard.tsx @@ -0,0 +1,63 @@ + +import React, { useMemo } from 'react'; +import { View } from 'react-native'; +import { Card, Text, useTheme} from 'react-native-paper'; +import { MetricsData } from './metricsTypes'; +import { cardStyles } from './MetricsTab'; +import { useTranslation } from 'react-i18next'; +import { labelKeyToReadable } from '../survey/multilabel/confirmHelper'; +import LineChart from '../components/LineChart'; + +const ACTIVE_MODES = ['walk', 'bike'] as const; +type ActiveMode = typeof ACTIVE_MODES[number]; + +type Props = { userMetrics: MetricsData } +const DailyActiveMinutesCard = ({ userMetrics }: Props) => { + + const { colors } = useTheme(); + const { t } = useTranslation(); + + const dailyActiveMinutesRecords = useMemo(() => { + const records = []; + const recentDays = userMetrics?.duration?.slice(-14); + recentDays?.forEach(day => { + ACTIVE_MODES.forEach(mode => { + const sum = day[`label_${mode}`]; + if (sum) { + records.push({ + label: labelKeyToReadable(mode), + x: day.ts * 1000, // vertical chart, milliseconds on X axis + y: sum / 60, // minutes on Y axis + }); + } + }); + }); + return records as {label: ActiveMode, x: string, y: number}[]; + }, [userMetrics?.duration]); + + return ( + + + + { dailyActiveMinutesRecords.length ? + + : + + + {t('metrics.chart-no-data')} + + + } + + + ); +} + +export default DailyActiveMinutesCard; diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 7a8dc4cab..9701bcc6f 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -13,6 +13,7 @@ import WeeklyActiveMinutesCard from "./WeeklyActiveMinutesCard"; import { secondsToHours, secondsToMinutes } from "./metricsHelper"; import CarbonFootprintCard from "./CarbonFootprintCard"; import Carousel from "../components/Carousel"; +import DailyActiveMinutesCard from "./DailyActiveMinutesCard"; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; @@ -88,6 +89,7 @@ const MetricsTab = () => { + Date: Thu, 31 Aug 2023 03:09:55 -0400 Subject: [PATCH 068/164] BarChart: support positioning of lineAnnotations --- www/js/components/BarChart.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index dc09c70b2..bb85bd361 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -5,7 +5,7 @@ import { View } from 'react-native'; import { useTheme } from 'react-native-paper'; import { Chart, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, TimeScale, ChartData } from 'chart.js'; import { Bar } from 'react-chartjs-2'; -import Annotation, { AnnotationOptions } from 'chartjs-plugin-annotation'; +import Annotation, { AnnotationOptions, LabelPosition } from 'chartjs-plugin-annotation'; Chart.register( CategoryScale, @@ -26,7 +26,7 @@ type BarChartData = { type Props = { records: { label: string, x: number|string, y: number|string }[], axisTitle: string, - lineAnnotations?: { value: number, label: string }[], + lineAnnotations?: { value: number, label?: string, position?: LabelPosition }[], isHorizontal?: boolean, timeAxis?: boolean, stacked?: boolean, @@ -168,7 +168,7 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, backgroundColor: 'rgba(0,0,0,.7)', color: 'rgba(255,255,255,1)', font: { size: 10 }, - position: 'start', + position: a.position || 'start', content: a.label, }, ...(isHorizontal ? { xMin: a.value, xMax: a.value } From 41ba903a2ca675f9ee962f7ef337daab055d5f88 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 31 Aug 2023 11:27:32 -0400 Subject: [PATCH 069/164] add weekly minutes goal, fix lone "0" -adds the CDC recommended 150 minutes, positioned centered -the 'overall' totals should show if activeModesDurations exists and has a length greater than 0 - if we don't include the '>' operator the expression may evaluate as the number 0, which would be rendered as text --- www/js/metrics/WeeklyActiveMinutesCard.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/WeeklyActiveMinutesCard.tsx b/www/js/metrics/WeeklyActiveMinutesCard.tsx index dadf9aa3a..7397007b9 100644 --- a/www/js/metrics/WeeklyActiveMinutesCard.tsx +++ b/www/js/metrics/WeeklyActiveMinutesCard.tsx @@ -64,7 +64,9 @@ const WeeklyActiveMinutesCard = ({ userMetrics }: Props) => { { weeklyActiveMinutesRecords.length ? + isHorizontal={false} stacked={true} + // TODO i18n + lineAnnotations={[{ value: 150, label: 'Weekly Goal', position: 'center' }]}/> : @@ -72,7 +74,7 @@ const WeeklyActiveMinutesCard = ({ userMetrics }: Props) => { } - {activeModesDurations?.length && + {activeModesDurations?.length > 0 && {`Overall for ${formatDateRangeOfDays(userMetrics.duration)}:`} From 20b457ae84af210d43e745007f48a67bf5c18163 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 31 Aug 2023 11:31:58 -0400 Subject: [PATCH 070/164] allow gaps in daily active minutes line chart It's actually important to push records for every day on this chart, even if the values are undefined or otherwise falsy. For example, most people don't bike every day - so we need to show gaps in the line graph on the days they don't, and the only way to do that is to pass falsy values through. --- www/js/metrics/DailyActiveMinutesCard.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/www/js/metrics/DailyActiveMinutesCard.tsx b/www/js/metrics/DailyActiveMinutesCard.tsx index ea4d4d8f5..a2f460443 100644 --- a/www/js/metrics/DailyActiveMinutesCard.tsx +++ b/www/js/metrics/DailyActiveMinutesCard.tsx @@ -22,14 +22,12 @@ const DailyActiveMinutesCard = ({ userMetrics }: Props) => { const recentDays = userMetrics?.duration?.slice(-14); recentDays?.forEach(day => { ACTIVE_MODES.forEach(mode => { - const sum = day[`label_${mode}`]; - if (sum) { - records.push({ - label: labelKeyToReadable(mode), - x: day.ts * 1000, // vertical chart, milliseconds on X axis - y: sum / 60, // minutes on Y axis - }); - } + const activeSeconds = day[`label_${mode}`]; + records.push({ + label: labelKeyToReadable(mode), + x: day.ts * 1000, // vertical chart, milliseconds on X axis + y: activeSeconds && activeSeconds / 60, // minutes on Y axis + }); }); }); return records as {label: ActiveMode, x: string, y: number}[]; From bcc1010623a173b42f9ae67821ddc4b42cc0f6e3 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 31 Aug 2023 15:02:14 -0600 Subject: [PATCH 071/164] color meter chart added parameter to BarChart to allow for a "meter" chart -- with a green/yellow/red scale changing at specified "middle" and "high" values, dashed lines for uncertainty appear when labeled "dash_key" changed "goal" for "guideline" to further emphasize NOT exceeding it, shortened for better formatting added keys for "labeled" and "unlabeled", replaced what was hard-coded as "certain" and "uncertain" --- www/i18n/en.json | 9 ++-- www/js/components/BarChart.tsx | 66 ++++++++++++++++++++++++-- www/js/metrics/CarbonFootprintCard.tsx | 20 ++++---- 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index 84dff390a..a4472d550 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -163,8 +163,8 @@ "worst-case": "If all 'taxi':", "label-to-squish": "Label trips to collapse the range into a single number", "lastweek": "My last week value:", - "us-2030-goal": "❗ US 2030 Goal", - "us-2050-goal": "⚠️ US 2050 Goal", + "us-2030-goal": "2030 Guideline", + "us-2050-goal": "2050 Guideline", "past-week" : "Past Week:", "prev-week" : "Previous Week:", "calories": "My Calories", @@ -177,7 +177,10 @@ "equals-icecream_other": "Equals at least {{count}} half cups vanilla ice cream", "equals-bananas_one": "Equals at least {{count}} banana", "equals-bananas_other": "Equals at least {{count}} bananas", - "active-minutes": "My Active Minutes" + "active-minutes": "My Active Minutes", + "labeled": "Labeled", + "unlabeled": "Unlabeled", + "footprint-label": "Footprint (kg Co2)" }, "main-inf-scroll" : { diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 9c80843eb..41fb7ae7f 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -30,9 +30,9 @@ type Props = { isHorizontal?: boolean, timeAxis?: boolean, stacked?: boolean, - customPalette?: {}, + meter?: {high: number, middle: number, dash_key: string}, } -const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, stacked, customPalette }: Props) => { +const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, stacked, meter }: Props) => { const { colors } = useTheme(); const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); @@ -86,19 +86,79 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, return { flex: 1 }; } + function getBarHeight(stacks) { + let totalHeight = 0; + console.log("ctx stacks", stacks.x); + for(let val in stacks.x) { + if(!val.startsWith('_')){ + totalHeight += stacks.x[val]; + console.log("ctx added ", val ); + } + } + return totalHeight; + } + + //fill pattern creation + //https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns + function createDiagonalPattern(color = 'black') { + let shape = document.createElement('canvas') + shape.width = 10 + shape.height = 10 + let c = shape.getContext('2d') + c.strokeStyle = color + c.lineWidth = 2 + c.beginPath() + c.moveTo(2, 0) + c.lineTo(10, 8) + c.stroke() + c.beginPath() + c.moveTo(0, 8) + c.lineTo(2, 10) + c.stroke() + return c.createPattern(shape, 'repeat') + } + return ( ({ ...e, // cycle through the default palette, repeat if necessary - backgroundColor: customPalette ? customPalette[chartData[i].label]: defaultPalette[i % defaultPalette.length], + backgroundColor: (ctx: any) => { + if(meter) { + let bar_height = getBarHeight(ctx.parsed._stacks); + console.debug("bar height for", ctx.raw.y," is ", bar_height, "which in chart is", chartData[i]); + //if "unlabeled", etc -> stripes + if(chartData[i].label == meter.dash_key) { + if (bar_height > meter.high) return createDiagonalPattern(colors.danger); + else if (bar_height > meter.middle) return createDiagonalPattern(colors.warn); + else return createDiagonalPattern(colors.success); + } + //if :labeled", etc -> solid + if (bar_height > 54) return colors.danger; + else if (bar_height > 14) return colors.warn; + else return colors.success; + } + return defaultPalette[i % defaultPalette.length]; + }, + borderColor: (ctx: any) => { + let bar_height = getBarHeight(ctx.parsed._stacks); + if(meter) { + if (bar_height > 54) return colors.danger; + else if (bar_height > 14) return colors.warn; + else return colors.success; + }} }))}} options={{ indexAxis: indexAxis, responsive: true, maintainAspectRatio: false, resizeDelay: 1, + elements: { + bar: { + borderWidth: meter ? 2 : 0, + } + }, scales: { ...(isHorizontal ? { y: { diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index f0b939f5f..7693d51b3 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -157,8 +157,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), high: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, FootprintHelper.getHighestFootprint()), }; - graphRecords.push({label: "uncertain", x: userPastWeek.high - userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}) - graphRecords.push({label: 'certain', x: userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); + graphRecords.push({label: t('main-metrics.unlabeled'), x: userPastWeek.high - userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}) + graphRecords.push({label: t('main-metrics.labeled'), x: userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); //calculate low-high and format range for prev week, if exists if(userLastWeekSummaryMap[0]) { @@ -166,8 +166,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) }; - graphRecords.push({label: "uncertain", x: userPrevWeek.high - userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}) - graphRecords.push({label: 'certain', x: userPastWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}); + graphRecords.push({label: t('main-metrics.unlabeled'), x: userPrevWeek.high - userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}) + graphRecords.push({label: t('main-metrics.labeled'), x: userPastWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}); let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); setEmissionsChange(pctChange); @@ -177,7 +177,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //calculate worst-case carbon footprint let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); - graphRecords.push({label: 'certain', x: worstCarbon, y: `${t('main-metrics.worst-case')}`}); + graphRecords.push({label: t('main-metrics.labeled'), x: worstCarbon, y: `${t('main-metrics.worst-case')}`}); return graphRecords; } @@ -211,8 +211,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { high: FootprintHelper.getFootprintForMetrics(aggCarbonData, FootprintHelper.getHighestFootprint()), } console.log("testing group past week", aggCarbon); - groupRecords.push({label: "uncertain", x: aggCarbon.high - aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); - groupRecords.push({label: 'certain', x: aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); + groupRecords.push({label: t('main-metrics.unlabeled'), x: aggCarbon.high - aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); + groupRecords.push({label: t('main-metrics.labeled'), x: aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); return groupRecords; } @@ -234,7 +234,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //hardcoded here, could be read from config at later customization? let carbonGoals = [ {label: t('main-metrics.us-2030-goal'), value: 54, color: colors.danger}, {label: t('main-metrics.us-2050-goal'), value: 14, color: colors.warn}]; - let colorPalette = {certain: colors.primary, uncertain: colors.primaryContainer}; + let meter = { dash_key: t('main-metrics.unlabeled'), high: 54, middle: 14 }; return ( { style={cardStyles.title(colors)} /> { chartData?.length > 0 ? - + : From 71adf573846c7836c979161f3a0d5d969cb5a80e Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 31 Aug 2023 15:18:45 -0600 Subject: [PATCH 072/164] darken the guideline lines when the dashed guidelines are the same colors as the bars - and they cross- the line disappears By darkening the lines slightly, we preserve their visibility when the two objects cross https://github.com/e-mission/e-mission-docs/issues/961#issuecomment-1701768414 --- www/js/metrics/CarbonFootprintCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 7693d51b3..1de037bad 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -232,8 +232,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { }, [userCarbonRecords, groupCarbonRecords]); //hardcoded here, could be read from config at later customization? - let carbonGoals = [ {label: t('main-metrics.us-2030-goal'), value: 54, color: colors.danger}, - {label: t('main-metrics.us-2050-goal'), value: 14, color: colors.warn}]; + let carbonGoals = [ {label: t('main-metrics.us-2030-goal'), value: 54, color: color(colors.danger).darken(.25).rgb().toString()}, + {label: t('main-metrics.us-2050-goal'), value: 14, color: color(colors.warn).darken(.25).rgb().toString()}]; let meter = { dash_key: t('main-metrics.unlabeled'), high: 54, middle: 14 }; return ( From 950b16303c9f2e08493fe5e04683a13a7c223ac2 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 31 Aug 2023 16:47:23 -0600 Subject: [PATCH 073/164] forgotten color import this import should have been a part of 71adf57 --- www/js/metrics/CarbonFootprintCard.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 1de037bad..22fef378d 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; import { getAngularService } from '../angular-react-helper'; import ChangeIndicator from './ChangeIndicator'; +import color from "color"; //modes considered on foot for carbon calculation, expandable as needed const ON_FOOT_MODES = ['WALKING', 'RUNNING', 'ON_FOOT'] as const; From 09d4711ad08bf8c3c22cb2da46a61dd5732f259c Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 31 Aug 2023 16:48:09 -0600 Subject: [PATCH 074/164] add footnote adding an explanatory footnote to the Carbon card, so users know what the goals mean https://github.com/e-mission/e-mission-docs/issues/961#issuecomment-1701760128 --- www/js/metrics/CarbonFootprintCard.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 22fef378d..d3b380c9c 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -249,8 +249,14 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { style={cardStyles.title(colors)} /> { chartData?.length > 0 ? - + + + + {/* TODO i18n */} + Dashed lines: US decarbonization goals scaled to per-capita travel-related emissions. + + : From a9b311ab6db5897777e216f25d3bc355ea6aceb1 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Fri, 1 Sep 2023 13:07:06 -0400 Subject: [PATCH 075/164] refactor BarChart With all the charting features we've added, this file has gotten fairly long and messy. To keep things tidy, let's extract some of these functions to a separate file called charting.ts. In the process, let's ensure the BarChart is properly typed. Upon reading documentation, I found out that we can type the chart data by using ChartData<'bar', {Z}> where Z is the values that each data point has (in our case, x and y values, which can be number | string) This makes all the type definitions line up - before this, we had a lot of different type warnings because TypeScript didn't understand our data structure. --- www/js/components/BarChart.tsx | 146 ++++++++------------------------- www/js/components/charting.ts | 91 ++++++++++++++++++++ 2 files changed, 123 insertions(+), 114 deletions(-) create mode 100644 www/js/components/charting.ts diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 41fb7ae7f..409184c7a 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -1,13 +1,14 @@ -import React, { useRef, useState, useMemo } from 'react'; +import React, { useEffect, useRef, useState, useMemo } from 'react'; import { angularize } from '../angular-react-helper'; import { View } from 'react-native'; import { useTheme } from 'react-native-paper'; -import { Chart, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, TimeScale, ChartData } from 'chart.js'; -import { Bar } from 'react-chartjs-2'; +import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, TimeScale, ChartData, ScriptableContext, ChartArea } from 'chart.js'; +import { Chart } from 'react-chartjs-2'; import Annotation, { AnnotationOptions, LabelPosition } from 'chartjs-plugin-annotation'; +import { defaultPalette, getChartHeight, getMeteredBackgroundColor } from './charting'; -Chart.register( +ChartJS.register( CategoryScale, LinearScale, TimeScale, @@ -18,9 +19,10 @@ Chart.register( Annotation, ); -type BarChartData = { +type XYPair = { x: number|string, y: number|string }; +type ChartDatasets = { label: string, - data: { x: number|string, y: number|string }[], + data: XYPair[], }[]; type Props = { @@ -38,11 +40,24 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); const indexAxis = isHorizontal ? 'y' : 'x'; - const barChartRef = useRef(null); + const barChartRef = useRef>(null); + + const [chartDatasets, setChartDatasets] = useState([]); + const chartData = useMemo>(() => ({ + datasets: chartDatasets.map((e, i) => ({ + ...e, + backgroundColor: (barCtx) => + meter ? getMeteredBackgroundColor(meter, barCtx, chartDatasets[i], colors) + : defaultPalette[i % defaultPalette.length], + borderColor: (barCtx) => + meter ? getMeteredBackgroundColor(meter, barCtx, chartDatasets[i], colors, .25) + : defaultPalette[i % defaultPalette.length], + })), + }), [chartDatasets, meter]); // group records by label (this is the format that Chart.js expects) - const chartData = useMemo(() => { - return records?.reduce((acc, record) => { + useEffect(() => { + const d = records?.reduce((acc, record) => { const existing = acc.find(e => e.label == record.label); if (!existing) { acc.push({ @@ -59,96 +74,14 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, }); } return acc; - }, [] as BarChartData); + }, [] as ChartDatasets); + setChartDatasets(d); }, [records]); - function getChartHeight() { - /* when horizontal charts have more data, they should get taller - so they don't look squished */ - if (isHorizontal) { - // 'ideal' chart height is based on the number of datasets and number of unique index values - const uniqueIndexVals = []; - chartData.forEach(e => e.data.forEach(r => { - if (!uniqueIndexVals.includes(r[indexAxis])) uniqueIndexVals.push(r[indexAxis]); - })); - const numIndexVals = uniqueIndexVals.length; - const heightPerIndexVal = stacked ? 36 : numVisibleDatasets * 8; - const idealChartHeight = heightPerIndexVal * numIndexVals; - - /* each index val should be at least 20px tall for visibility, - and the graph itself should be at least 250px tall */ - const minChartHeight = Math.max(numIndexVals * 20, 250); - - // return whichever is greater - return { height: Math.max(idealChartHeight, minChartHeight) }; - } - // vertical charts should just fill the available space in the parent container - return { flex: 1 }; - } - - function getBarHeight(stacks) { - let totalHeight = 0; - console.log("ctx stacks", stacks.x); - for(let val in stacks.x) { - if(!val.startsWith('_')){ - totalHeight += stacks.x[val]; - console.log("ctx added ", val ); - } - } - return totalHeight; - } - - //fill pattern creation - //https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns - function createDiagonalPattern(color = 'black') { - let shape = document.createElement('canvas') - shape.width = 10 - shape.height = 10 - let c = shape.getContext('2d') - c.strokeStyle = color - c.lineWidth = 2 - c.beginPath() - c.moveTo(2, 0) - c.lineTo(10, 8) - c.stroke() - c.beginPath() - c.moveTo(0, 8) - c.lineTo(2, 10) - c.stroke() - return c.createPattern(shape, 'repeat') - } - return ( - - ({ - ...e, - // cycle through the default palette, repeat if necessary - backgroundColor: (ctx: any) => { - if(meter) { - let bar_height = getBarHeight(ctx.parsed._stacks); - console.debug("bar height for", ctx.raw.y," is ", bar_height, "which in chart is", chartData[i]); - //if "unlabeled", etc -> stripes - if(chartData[i].label == meter.dash_key) { - if (bar_height > meter.high) return createDiagonalPattern(colors.danger); - else if (bar_height > meter.middle) return createDiagonalPattern(colors.warn); - else return createDiagonalPattern(colors.success); - } - //if :labeled", etc -> solid - if (bar_height > 54) return colors.danger; - else if (bar_height > 14) return colors.warn; - else return colors.success; - } - return defaultPalette[i % defaultPalette.length]; - }, - borderColor: (ctx: any) => { - let bar_height = getBarHeight(ctx.parsed._stacks); - if(meter) { - if (bar_height > 54) return colors.danger; - else if (bar_height > 14) return colors.warn; - else return colors.success; - }} - }))}} + + { - const label = chartData[0].data[i].y; + const label = chartDatasets[0].data[i].y; if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); return label; @@ -203,7 +136,7 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, ticks: timeAxis ? {} : { callback: (value, i) => { console.log("testing vertical", chartData, i); - const label = chartData[0].data[i].x; + const label = chartDatasets[0].data[i].x; if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); return label; @@ -245,20 +178,5 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, ) } - -const defaultPalette = [ - '#c95465', // red oklch(60% 0.15 14) - '#4a71b1', // blue oklch(55% 0.11 260) - '#d2824e', // orange oklch(68% 0.12 52) - '#856b5d', // brown oklch(55% 0.04 50) - '#59894f', // green oklch(58% 0.1 140) - '#e0cc55', // yellow oklch(84% 0.14 100) - '#b273ac', // purple oklch(64% 0.11 330) - '#f09da6', // pink oklch(78% 0.1 12) - '#b3aca8', // grey oklch(75% 0.01 55) - '#80afad', // teal oklch(72% 0.05 192) -]; - - angularize(BarChart, 'BarChart', 'emission.main.barchart'); export default BarChart; diff --git a/www/js/components/charting.ts b/www/js/components/charting.ts new file mode 100644 index 000000000..f5f8ed198 --- /dev/null +++ b/www/js/components/charting.ts @@ -0,0 +1,91 @@ +// return getGradient(barChartRef.current, meter, barCtx, chartDatasets[i]); +// return getGradient(barChartRef.current, meter, barCtx, chartDatasets[i], 1, .25); + +import color from 'color'; + +export const defaultPalette = [ + '#c95465', // red oklch(60% 0.15 14) + '#4a71b1', // blue oklch(55% 0.11 260) + '#d2824e', // orange oklch(68% 0.12 52) + '#856b5d', // brown oklch(55% 0.04 50) + '#59894f', // green oklch(58% 0.1 140) + '#e0cc55', // yellow oklch(84% 0.14 100) + '#b273ac', // purple oklch(64% 0.11 330) + '#f09da6', // pink oklch(78% 0.1 12) + '#b3aca8', // grey oklch(75% 0.01 55) + '#80afad', // teal oklch(72% 0.05 192) +]; + +export function getChartHeight(chartDatasets, numVisibleDatasets, indexAxis, isHorizontal, stacked) { + /* when horizontal charts have more data, they should get taller + so they don't look squished */ + if (isHorizontal) { + // 'ideal' chart height is based on the number of datasets and number of unique index values + const uniqueIndexVals = []; + chartDatasets.forEach(e => e.data.forEach(r => { + if (!uniqueIndexVals.includes(r[indexAxis])) uniqueIndexVals.push(r[indexAxis]); + })); + const numIndexVals = uniqueIndexVals.length; + const heightPerIndexVal = stacked ? 36 : numVisibleDatasets * 8; + const idealChartHeight = heightPerIndexVal * numIndexVals; + + /* each index val should be at least 20px tall for visibility, + and the graph itself should be at least 250px tall */ + const minChartHeight = Math.max(numIndexVals * 20, 250); + + // return whichever is greater + return { height: Math.max(idealChartHeight, minChartHeight) }; + } + // vertical charts should just fill the available space in the parent container + return { flex: 1 }; +} + +function getBarHeight(stacks) { + let totalHeight = 0; + console.log("ctx stacks", stacks.x); + for(let val in stacks.x) { + if(!val.startsWith('_')){ + totalHeight += stacks.x[val]; + console.log("ctx added ", val ); + } + } + return totalHeight; +} + +//fill pattern creation +//https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns +function createDiagonalPattern(color = 'black') { + let shape = document.createElement('canvas') + shape.width = 10 + shape.height = 10 + let c = shape.getContext('2d') + c.strokeStyle = color + c.lineWidth = 2 + c.beginPath() + c.moveTo(2, 0) + c.lineTo(10, 8) + c.stroke() + c.beginPath() + c.moveTo(0, 8) + c.lineTo(2, 10) + c.stroke() + return c.createPattern(shape, 'repeat') +} + +export function getMeteredBackgroundColor(meter, barCtx, currDataset, colors, darken=0) { + let bar_height = getBarHeight(barCtx.parsed._stacks); + console.debug("bar height for", barCtx.raw.y, " is ", bar_height, "which in chart is", currDataset); + let meteredColor; + if (bar_height > meter.high) meteredColor = colors.danger; + else if (bar_height > meter.middle) meteredColor = colors.warn; + else meteredColor = colors.success; + if (darken) { + return color(meteredColor).darken(darken).hex(); + } + //if "unlabeled", etc -> stripes + if (currDataset.label == meter.dash_key) { + return createDiagonalPattern(meteredColor); + } + //if :labeled", etc -> solid + return meteredColor; +} From 0f4a4e910fa96507d299623a7d1e7d6f6dba981f Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 5 Sep 2023 17:02:44 -0600 Subject: [PATCH 076/164] add text to Carbon Card for accessibility purposes, the information should be available in ways other than the graph, so that it is easier for a screen reader to successfully navigate the page --- www/js/metrics/CarbonFootprintCard.tsx | 44 +++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index d3b380c9c..f2a056422 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -10,6 +10,7 @@ import BarChart from '../components/BarChart'; import { getAngularService } from '../angular-react-helper'; import ChangeIndicator from './ChangeIndicator'; import color from "color"; +import moment from 'moment'; //modes considered on foot for carbon calculation, expandable as needed const ON_FOOT_MODES = ['WALKING', 'RUNNING', 'ON_FOOT'] as const; @@ -21,6 +22,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { const { t } = useTranslation(); const [emissionsChange, setEmissionsChange] = useState({}); + const [userText, setUserText] = useState([]); + const [groupText, setGroupText] = useState([]); /* * metric2val is a function that takes a metric entry and a field and returns @@ -152,6 +155,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //setting up data to be displayed let graphRecords = []; + let textList = []; //calculate low-high and format range for past week let userPastWeek = { @@ -160,7 +164,9 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { }; graphRecords.push({label: t('main-metrics.unlabeled'), x: userPastWeek.high - userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}) graphRecords.push({label: t('main-metrics.labeled'), x: userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); - + textList.push({label: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`, + value: (userPastWeek.high - userPastWeek.low)==0 ? Math.round(userPastWeek.low) : Math.round(userPastWeek.low) + " - " + Math.round(userPastWeek.high)}); + //calculate low-high and format range for prev week, if exists if(userLastWeekSummaryMap[0]) { let userPrevWeek = { @@ -169,7 +175,9 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { }; graphRecords.push({label: t('main-metrics.unlabeled'), x: userPrevWeek.high - userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}) graphRecords.push({label: t('main-metrics.labeled'), x: userPastWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}); - + textList.push({label: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`, + value: (userPrevWeek.high - userPrevWeek.low)==0 ? Math.round(userPrevWeek.low) : Math.round(userPrevWeek.low) + " - " + Math.round(userPrevWeek.high)}); + let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); setEmissionsChange(pctChange); } else { @@ -179,7 +187,9 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //calculate worst-case carbon footprint let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); graphRecords.push({label: t('main-metrics.labeled'), x: worstCarbon, y: `${t('main-metrics.worst-case')}`}); + textList.push({label:t('main-metrics.worst-case'), value: Math.round(worstCarbon)}); + setUserText(textList); return graphRecords; } }, [userMetrics?.distance]) @@ -206,6 +216,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { } let groupRecords = []; + let groupText = []; let aggCarbon = { low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), @@ -214,7 +225,10 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { console.log("testing group past week", aggCarbon); groupRecords.push({label: t('main-metrics.unlabeled'), x: aggCarbon.high - aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); groupRecords.push({label: t('main-metrics.labeled'), x: aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); + groupText.push({label: t('main-metrics.average'), + value: (aggCarbon.high - aggCarbon.low)==0 ? Math.round(aggCarbon.low) : Math.round(aggCarbon.low) + " - " + Math.round(aggCarbon.high)}); + setGroupText(groupText); return groupRecords; } }, [aggMetrics]) @@ -232,8 +246,19 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { return tempChartData; }, [userCarbonRecords, groupCarbonRecords]); + const textEntries = useMemo(() => { + let tempText = [] + if(userText?.length){ + tempText = tempText.concat(userText); + } + if(groupText?.length) { + tempText = tempText.concat(groupText); + } + return tempText; + }, [userText, groupText]); + //hardcoded here, could be read from config at later customization? - let carbonGoals = [ {label: t('main-metrics.us-2030-goal'), value: 54, color: color(colors.danger).darken(.25).rgb().toString()}, + let carbonGoals = [ {label: t('main-metrics.us-2030-goal'), value: 54, color: color(colors.danger).darken(.25).rgb().toString()}, {label: t('main-metrics.us-2050-goal'), value: 14, color: color(colors.warn).darken(.25).rgb().toString()}]; let meter = { dash_key: t('main-metrics.unlabeled'), high: 54, middle: 14 }; @@ -262,8 +287,17 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { {t('metrics.chart-no-data')} - - } + } + { textEntries?.length > 0 && + + { Object.keys(textEntries).map((i) => + + {textEntries[i].label} + {textEntries[i].value + ' ' + "kg Co2"} + + )} + + } ) From 15ed685cb13ca0f6cdafbc961f80085347f0a2f8 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 6 Sep 2023 13:21:19 -0600 Subject: [PATCH 077/164] move carbon footprint text to dedicated card https://github.com/e-mission/e-mission-docs/issues/961#issuecomment-1707597185 -> implementing this suggestion isolate the text to a dedicated card, and place the "meter" card and "text" card in a carousel, now we have three rows, each a carousel. also isolated data management functions shared across the two cards into `metricsHelper.ts` The two cards keeps information easily accessible to those using a screen reader, while maintaining focus on the "meter" card and not cluttering the screen --- www/js/metrics/CarbonFootprintCard.tsx | 162 ++----------------------- www/js/metrics/CarbonTextCard.tsx | 135 +++++++++++++++++++++ www/js/metrics/MetricsTab.tsx | 6 +- www/js/metrics/metricsHelper.ts | 115 ++++++++++++++++++ 4 files changed, 262 insertions(+), 156 deletions(-) create mode 100644 www/js/metrics/CarbonTextCard.tsx diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index f2a056422..212453b39 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -1,19 +1,14 @@ - import React, { useState, useMemo } from 'react'; import { View } from 'react-native'; import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; -import { cardMargin, cardStyles } from './MetricsTab'; -import { filterToRecentWeeks, formatDateRangeOfDays } from './metricsHelper'; +import { cardStyles } from './MetricsTab'; +import { filterToRecentWeeks, formatDateRangeOfDays, parseDataFromMetrics, generateSummaryFromData, calculatePercentChange } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; import { getAngularService } from '../angular-react-helper'; import ChangeIndicator from './ChangeIndicator'; import color from "color"; -import moment from 'moment'; - -//modes considered on foot for carbon calculation, expandable as needed -const ON_FOOT_MODES = ['WALKING', 'RUNNING', 'ON_FOOT'] as const; type Props = { userMetrics: MetricsData, aggMetrics: MetricsData } const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { @@ -22,120 +17,9 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { const { t } = useTranslation(); const [emissionsChange, setEmissionsChange] = useState({}); - const [userText, setUserText] = useState([]); - const [groupText, setGroupText] = useState([]); - - /* - * metric2val is a function that takes a metric entry and a field and returns - * the appropriate value. - * for regular data (user-specific), this will return the field value - * for avg data (aggregate), this will return the field value/nUsers - */ - const metricToValue = function(population:'user'|'aggreagte', metric, field) { - if(population == "user"){ - return metric[field]; - } - else{ - return metric[field]/metric.nUsers; - } - } - - //testing agains global list of what is "on foot" - //returns true | false - const isOnFoot = function(mode: string) { - for (let ped_mode in ON_FOOT_MODES) { - if (mode === ped_mode) { - return true; - } - } - return false; - } - - const parseDataFromMetrics = function(metrics, population) { - console.log("Called parseDataFromMetrics on ", metrics); - let mode_bins = {}; - metrics.forEach(function(metric) { - let onFootVal = 0; - - for (let field in metric) { - /*For modes inferred from sensor data, we check if the string is all upper case - by converting it to upper case and seeing if it is changed*/ - if(field == field.toUpperCase()) { - /*sum all possible on foot modes: see https://github.com/e-mission/e-mission-docs/issues/422 */ - if (isOnFoot(field)) { - onFootVal += metricToValue(population, metric, field); - field = 'ON_FOOT'; - } - if (!(field in mode_bins)) { - mode_bins[field] = []; - } - //for all except onFoot, add to bin - could discover mult onFoot modes - if (field != "ON_FOOT") { - mode_bins[field].push([metric.ts, metricToValue(population, metric, field), metric.fmt_time]); - } - } - //this section handles user lables, assuming 'label_' prefix - if(field.startsWith('label_')) { - let actualMode = field.slice(6, field.length); //remove prefix - console.log("Mapped field "+field+" to mode "+actualMode); - if (!(actualMode in mode_bins)) { - mode_bins[actualMode] = []; - } - mode_bins[actualMode].push([metric.ts, Math.round(metricToValue(population, metric, field)), moment(metric.fmt_time).format()]); - } - } - //handle the ON_FOOT modes once all have been summed - if ("ON_FOOT" in mode_bins) { - mode_bins["ON_FOOT"].push([metric.ts, Math.round(onFootVal), metric.fmt_time]); - } - }); - - let return_val = []; - for (let mode in mode_bins) { - return_val.push({key: mode, values: mode_bins[mode]}); - } - - return return_val; - } - - const generateSummaryFromData = function(modeMap, metric) { - console.log("Invoked getSummaryDataRaw on ", modeMap, "with", metric); - - let summaryMap = []; - - for (let i=0; i < modeMap.length; i++){ - let summary = {}; - summary['key'] = modeMap[i].key; - let sumVals = 0; - - for (let j = 0; j < modeMap[i].values.length; j++) - { - sumVals += modeMap[i].values[j][1]; //2nd item of array is value - } - if (metric === 'mean_speed'){ - //we care about avg speed, sum for other metrics - summary['values'] = Math.round(sumVals / modeMap[i].values.length); - } else { - summary['values'] = Math.round(sumVals); - } - - summaryMap.push(summary); - } - - return summaryMap; - } - - //from two weeks fo low and high values, calculates low and high change - const calculatePercentChange = function(pastWeekRange, previousWeekRange) { - let greaterLesserPct = { - low: (pastWeekRange.low/previousWeekRange.low) * 100 - 100, - high: (pastWeekRange.high/previousWeekRange.high) * 100 - 100, - } - return greaterLesserPct; - } - + const userCarbonRecords = useMemo(() => { - if(userMetrics) { + if(userMetrics?.distance?.length > 0) { //separate data into weeks let thisWeekDistance = filterToRecentWeeks(userMetrics?.distance)[0]; let lastWeekDistance = filterToRecentWeeks(userMetrics?.distance)[1]; @@ -155,7 +39,6 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //setting up data to be displayed let graphRecords = []; - let textList = []; //calculate low-high and format range for past week let userPastWeek = { @@ -164,9 +47,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { }; graphRecords.push({label: t('main-metrics.unlabeled'), x: userPastWeek.high - userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}) graphRecords.push({label: t('main-metrics.labeled'), x: userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); - textList.push({label: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`, - value: (userPastWeek.high - userPastWeek.low)==0 ? Math.round(userPastWeek.low) : Math.round(userPastWeek.low) + " - " + Math.round(userPastWeek.high)}); - + //calculate low-high and format range for prev week, if exists if(userLastWeekSummaryMap[0]) { let userPrevWeek = { @@ -175,8 +56,6 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { }; graphRecords.push({label: t('main-metrics.unlabeled'), x: userPrevWeek.high - userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}) graphRecords.push({label: t('main-metrics.labeled'), x: userPastWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}); - textList.push({label: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`, - value: (userPrevWeek.high - userPrevWeek.low)==0 ? Math.round(userPrevWeek.low) : Math.round(userPrevWeek.low) + " - " + Math.round(userPrevWeek.high)}); let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); setEmissionsChange(pctChange); @@ -187,15 +66,13 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //calculate worst-case carbon footprint let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); graphRecords.push({label: t('main-metrics.labeled'), x: worstCarbon, y: `${t('main-metrics.worst-case')}`}); - textList.push({label:t('main-metrics.worst-case'), value: Math.round(worstCarbon)}); - setUserText(textList); return graphRecords; } }, [userMetrics?.distance]) const groupCarbonRecords = useMemo(() => { - if(aggMetrics) + if(aggMetrics?.distance?.length > 0) { //separate data into weeks let thisWeekDistance = filterToRecentWeeks(aggMetrics?.distance)[0]; @@ -216,7 +93,6 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { } let groupRecords = []; - let groupText = []; let aggCarbon = { low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), @@ -225,10 +101,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { console.log("testing group past week", aggCarbon); groupRecords.push({label: t('main-metrics.unlabeled'), x: aggCarbon.high - aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); groupRecords.push({label: t('main-metrics.labeled'), x: aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); - groupText.push({label: t('main-metrics.average'), - value: (aggCarbon.high - aggCarbon.low)==0 ? Math.round(aggCarbon.low) : Math.round(aggCarbon.low) + " - " + Math.round(aggCarbon.high)}); - setGroupText(groupText); return groupRecords; } }, [aggMetrics]) @@ -246,24 +119,13 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { return tempChartData; }, [userCarbonRecords, groupCarbonRecords]); - const textEntries = useMemo(() => { - let tempText = [] - if(userText?.length){ - tempText = tempText.concat(userText); - } - if(groupText?.length) { - tempText = tempText.concat(groupText); - } - return tempText; - }, [userText, groupText]); - //hardcoded here, could be read from config at later customization? let carbonGoals = [ {label: t('main-metrics.us-2030-goal'), value: 54, color: color(colors.danger).darken(.25).rgb().toString()}, {label: t('main-metrics.us-2050-goal'), value: 14, color: color(colors.warn).darken(.25).rgb().toString()}]; let meter = { dash_key: t('main-metrics.unlabeled'), high: 54, middle: 14 }; return ( - { {t('metrics.chart-no-data')} } - { textEntries?.length > 0 && - - { Object.keys(textEntries).map((i) => - - {textEntries[i].label} - {textEntries[i].value + ' ' + "kg Co2"} - - )} - - } ) diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx new file mode 100644 index 000000000..d2a6e620c --- /dev/null +++ b/www/js/metrics/CarbonTextCard.tsx @@ -0,0 +1,135 @@ +import React, { useMemo } from 'react'; +import { View } from 'react-native'; +import { Card, Text, useTheme} from 'react-native-paper'; +import { MetricsData } from './metricsTypes'; +import { cardStyles } from './MetricsTab'; +import { useTranslation } from 'react-i18next'; +import { filterToRecentWeeks, formatDateRangeOfDays, parseDataFromMetrics, generateSummaryFromData, calculatePercentChange } from './metricsHelper'; +import { getAngularService } from '../angular-react-helper'; + +type Props = { userMetrics: MetricsData, aggMetrics: MetricsData } +const DailyActiveMinutesCard = ({ userMetrics, aggMetrics }: Props) => { + + const { colors } = useTheme(); + const { t } = useTranslation(); + const FootprintHelper = getAngularService("FootprintHelper"); + + const userText = useMemo(() => { + if(userMetrics?.distance?.length > 0) { + //separate data into weeks + let thisWeekDistance = filterToRecentWeeks(userMetrics?.distance)[0]; + let lastWeekDistance = filterToRecentWeeks(userMetrics?.distance)[1]; + + //formatted distance data from this week + let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); + let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); + let worstDistance = userThisWeekSummaryMap.reduce((prevDistance, currModeSummary) => prevDistance + currModeSummary.values, 0); + + //formatted data from last week + let userLastWeekModeMap = {}; + let userLastWeekSummaryMap = {}; + if(lastWeekDistance) { + userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); + userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); + } + + //setting up data to be displayed + let textList = []; + + //calculate low-high and format range for past week + let userPastWeek = { + low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), + high: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, FootprintHelper.getHighestFootprint()), + }; + textList.push({label: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`, + value: (userPastWeek.high - userPastWeek.low)==0 ? Math.round(userPastWeek.low) : Math.round(userPastWeek.low) + " - " + Math.round(userPastWeek.high)}); + + //calculate low-high and format range for prev week, if exists + if(userLastWeekSummaryMap[0]) { + let userPrevWeek = { + low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), + high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) + }; + textList.push({label: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`, + value: (userPrevWeek.high - userPrevWeek.low)==0 ? Math.round(userPrevWeek.low) : Math.round(userPrevWeek.low) + " - " + Math.round(userPrevWeek.high)}); + } + + //calculate worst-case carbon footprint + let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); + textList.push({label:t('main-metrics.worst-case'), value: Math.round(worstCarbon)}); + + return textList; + } + }, [userMetrics]); + + const groupText = useMemo(() => { + if(aggMetrics?.distance?.length > 0) + { + //separate data into weeks + let thisWeekDistance = filterToRecentWeeks(aggMetrics?.distance)[0]; + + let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, "aggregate"); + let aggThisWeekSummary = generateSummaryFromData(aggThisWeekModeMap, "distance"); + + // Issue 422: + // https://github.com/e-mission/e-mission-docs/issues/422 + let aggCarbonData = []; + for (var i in aggThisWeekSummary) { + aggCarbonData.push(aggThisWeekSummary[i]); + if (isNaN(aggCarbonData[i].values)) { + console.warn("WARNING in calculating groupCarbonRecords: value is NaN for mode " + aggCarbonData[i].key + ", changing to 0"); + aggCarbonData[i].values = 0; + } + } + + let groupText = []; + + let aggCarbon = { + low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), + high: FootprintHelper.getFootprintForMetrics(aggCarbonData, FootprintHelper.getHighestFootprint()), + } + console.log("testing group past week", aggCarbon); + groupText.push({label: t('main-metrics.average'), + value: (aggCarbon.high - aggCarbon.low)==0 ? Math.round(aggCarbon.low) : Math.round(aggCarbon.low) + " - " + Math.round(aggCarbon.high)}); + + return groupText; + } + }, [aggMetrics]); + + const textEntries = useMemo(() => { + let tempText = [] + if(userText?.length){ + tempText = tempText.concat(userText); + } + if(groupText?.length) { + tempText = tempText.concat(groupText); + } + return tempText; +}, [userText, groupText]); + + return ( + + + + { textEntries?.length > 0 && + + { Object.keys(textEntries).map((i) => + + {textEntries[i].label} + {textEntries[i].value + ' ' + "kg Co2"} + + )} + + } + + + ) +} + +export default DailyActiveMinutesCard; diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 9701bcc6f..01b1bddce 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -14,6 +14,7 @@ import { secondsToHours, secondsToMinutes } from "./metricsHelper"; import CarbonFootprintCard from "./CarbonFootprintCard"; import Carousel from "../components/Carousel"; import DailyActiveMinutesCard from "./DailyActiveMinutesCard"; +import CarbonTextCard from "./CarbonTextCard"; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; @@ -86,7 +87,10 @@ const MetricsTab = () => { refresh()} /> - + + + + diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index 1c01b7fee..2703be19b 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -1,6 +1,7 @@ import { DateTime } from "luxon"; import { formatForDisplay } from "../config/useImperialConfig"; import { DayOfMetricData } from "./metricsTypes"; +import moment from 'moment'; export function getUniqueLabelsForDays(metricDataDays: DayOfMetricData[]) { const uniqueLabels: string[] = []; @@ -47,3 +48,117 @@ export function formatDateRangeOfDays(days: DayOfMetricData[]) { const lastDay = lastDayDt.toLocaleString({...DateTime.DATE_SHORT, year: undefined}); return `${firstDay} - ${lastDay}`; } + +/* formatting data form carbon footprint calculations */ + +//modes considered on foot for carbon calculation, expandable as needed +const ON_FOOT_MODES = ['WALKING', 'RUNNING', 'ON_FOOT'] as const; + +/* +* metric2val is a function that takes a metric entry and a field and returns +* the appropriate value. +* for regular data (user-specific), this will return the field value +* for avg data (aggregate), this will return the field value/nUsers +*/ +const metricToValue = function(population:'user'|'aggreagte', metric, field) { + if(population == "user"){ + return metric[field]; + } + else{ + return metric[field]/metric.nUsers; + } +} + +//testing agains global list of what is "on foot" +//returns true | false +const isOnFoot = function(mode: string) { + for (let ped_mode in ON_FOOT_MODES) { + if (mode === ped_mode) { + return true; + } + } + return false; +} + +//from two weeks fo low and high values, calculates low and high change +export function calculatePercentChange(pastWeekRange, previousWeekRange) { + let greaterLesserPct = { + low: (pastWeekRange.low/previousWeekRange.low) * 100 - 100, + high: (pastWeekRange.high/previousWeekRange.high) * 100 - 100, + } + return greaterLesserPct; +} + +export function parseDataFromMetrics(metrics, population) { + console.log("Called parseDataFromMetrics on ", metrics); + let mode_bins = {}; + metrics.forEach(function(metric) { + let onFootVal = 0; + + for (let field in metric) { + /*For modes inferred from sensor data, we check if the string is all upper case + by converting it to upper case and seeing if it is changed*/ + if(field == field.toUpperCase()) { + /*sum all possible on foot modes: see https://github.com/e-mission/e-mission-docs/issues/422 */ + if (isOnFoot(field)) { + onFootVal += metricToValue(population, metric, field); + field = 'ON_FOOT'; + } + if (!(field in mode_bins)) { + mode_bins[field] = []; + } + //for all except onFoot, add to bin - could discover mult onFoot modes + if (field != "ON_FOOT") { + mode_bins[field].push([metric.ts, metricToValue(population, metric, field), metric.fmt_time]); + } + } + //this section handles user lables, assuming 'label_' prefix + if(field.startsWith('label_')) { + let actualMode = field.slice(6, field.length); //remove prefix + console.log("Mapped field "+field+" to mode "+actualMode); + if (!(actualMode in mode_bins)) { + mode_bins[actualMode] = []; + } + mode_bins[actualMode].push([metric.ts, Math.round(metricToValue(population, metric, field)), moment(metric.fmt_time).format()]); + } + } + //handle the ON_FOOT modes once all have been summed + if ("ON_FOOT" in mode_bins) { + mode_bins["ON_FOOT"].push([metric.ts, Math.round(onFootVal), metric.fmt_time]); + } + }); + + let return_val = []; + for (let mode in mode_bins) { + return_val.push({key: mode, values: mode_bins[mode]}); + } + + return return_val; +} + +export function generateSummaryFromData(modeMap, metric) { + console.log("Invoked getSummaryDataRaw on ", modeMap, "with", metric); + + let summaryMap = []; + + for (let i=0; i < modeMap.length; i++){ + let summary = {}; + summary['key'] = modeMap[i].key; + let sumVals = 0; + + for (let j = 0; j < modeMap[i].values.length; j++) + { + sumVals += modeMap[i].values[j][1]; //2nd item of array is value + } + if (metric === 'mean_speed'){ + //we care about avg speed, sum for other metrics + summary['values'] = Math.round(sumVals / modeMap[i].values.length); + } else { + summary['values'] = Math.round(sumVals); + } + + summaryMap.push(summary); + } + + return summaryMap; +} \ No newline at end of file From 1c3b532757a12d5a86d34d8a385a59c69c62be5f Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 6 Sep 2023 16:41:11 -0600 Subject: [PATCH 078/164] shorten carbon chart labels --- www/i18n/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index dcc1e16cc..c74bff237 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -163,14 +163,14 @@ "footprint": "My Footprint", "how-it-compares": "Ballpark comparisons", "optimal": "Optimal (perfect mode choice for all my trips):", - "average": "Group Average:", + "average": "Group Avg.:", "worst-case": "If all 'taxi':", "label-to-squish": "Label trips to collapse the range into a single number", "lastweek": "My last week value:", "us-2030-goal": "2030 Guideline", "us-2050-goal": "2050 Guideline", "past-week" : "Past Week:", - "prev-week" : "Previous Week:", + "prev-week" : "Prev. Week:", "calories": "My Calories", "calibrate": "Calibrate", "no-summary-data": "No summary data", From b377933bbfa2d123ae76dfa73826bb4cede64601 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 6 Sep 2023 16:55:36 -0600 Subject: [PATCH 079/164] bars colored by mode Update the color scheme to correspond to the colors used on the label screen to represent each mode Otherwise duplicated colors are handled by darkening or lightening the base color for subsequent modes of the same "base" storing the modeColors with useMemo means they will only update when chartDatasets change (preventing bog-down issue encountered in development) --- www/js/components/BarChart.tsx | 17 ++++++++--- www/js/components/charting.ts | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 409184c7a..e4295aa84 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -6,7 +6,8 @@ import { useTheme } from 'react-native-paper'; import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, TimeScale, ChartData, ScriptableContext, ChartArea } from 'chart.js'; import { Chart } from 'react-chartjs-2'; import Annotation, { AnnotationOptions, LabelPosition } from 'chartjs-plugin-annotation'; -import { defaultPalette, getChartHeight, getMeteredBackgroundColor } from './charting'; +import { defaultPalette, getChartHeight, getMeteredBackgroundColor, makeColorMap } from './charting'; +import { getLabelOptions } from "../survey/multilabel/confirmHelper"; ChartJS.register( CategoryScale, @@ -38,20 +39,28 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, const { colors } = useTheme(); const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); + const [labelOptions, setLabelOptions] = useState(null); const indexAxis = isHorizontal ? 'y' : 'x'; const barChartRef = useRef>(null); - const [chartDatasets, setChartDatasets] = useState([]); + + const modeColors = useMemo(() => { + if(!meter){ + getLabelOptions().then((labelOptions) => setLabelOptions(labelOptions)); + return makeColorMap(chartDatasets, labelOptions); + } + }, [chartDatasets]) + const chartData = useMemo>(() => ({ datasets: chartDatasets.map((e, i) => ({ ...e, backgroundColor: (barCtx) => meter ? getMeteredBackgroundColor(meter, barCtx, chartDatasets[i], colors) - : defaultPalette[i % defaultPalette.length], + : modeColors[chartDatasets[i]["label"]], borderColor: (barCtx) => meter ? getMeteredBackgroundColor(meter, barCtx, chartDatasets[i], colors, .25) - : defaultPalette[i % defaultPalette.length], + : modeColors[chartDatasets[i]["label"]], })), }), [chartDatasets, meter]); diff --git a/www/js/components/charting.ts b/www/js/components/charting.ts index f5f8ed198..2ed64aa89 100644 --- a/www/js/components/charting.ts +++ b/www/js/components/charting.ts @@ -2,6 +2,7 @@ // return getGradient(barChartRef.current, meter, barCtx, chartDatasets[i], 1, .25); import color from 'color'; +import { getBaseModeByKey } from '../diary/diaryHelper'; export const defaultPalette = [ '#c95465', // red oklch(60% 0.15 14) @@ -89,3 +90,58 @@ export function getMeteredBackgroundColor(meter, barCtx, currDataset, colors, da //if :labeled", etc -> solid return meteredColor; } + +function darkenOrLighten(baseColor, change) { + let colorObj = color(baseColor); + if(change > 0) { + return colorObj.darken(Math.abs(change)).hex(); + } else { + return colorObj.lighten(Math.abs(change)).hex(); + } +} + +export function deduplicateColor(modeColor, colorList) { + const options = [ -0.3, 0.3, -0.6, 0.6]; + let newColor = modeColor; + let i = 0; + + if(Object.values(colorList).includes(modeColor)){ + for (let i=0; i< options.length; i++){ + if(i >= options.length - 1){ + newColor = modeColor; //just in case, if out of options, use original color + } else { + newColor = darkenOrLighten(modeColor, options[i]); + if(!(Object.values(colorList).includes(newColor))) {break;} + } + } + } + return newColor; +} + +export function formatLabelForSearch(label) { + let formatted = ""; + let splitLabel = label.split(" "); + for (let i = 0; i < splitLabel.length; i++){ + formatted += splitLabel[i]; + if(i < splitLabel.length -1){ + formatted += "_"; //no '_' on the end! + } + } + return formatted; +} + +export function makeColorMap(chartDatasets, labelOptions) { + let tempColorMap = {}; + for(let i = 0; i < chartDatasets.length; i++) { + let modeColor = tempColorMap[chartDatasets[i]["label"]]; + if(!modeColor){ + const searchFormat = formatLabelForSearch(chartDatasets[i]["label"].toLowerCase()); + const modeOption = labelOptions?.MODE?.find(opt => opt.value == searchFormat); + modeColor = getBaseModeByKey(modeOption?.baseMode || "OTHER").color; + modeColor = deduplicateColor(modeColor, tempColorMap); + + tempColorMap[chartDatasets[i]["label"]] = modeColor; + } + } + return tempColorMap; +} \ No newline at end of file From e2b6b9932f640a08bb48ad98a0042d876e9acbd0 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 6 Sep 2023 23:20:02 -0400 Subject: [PATCH 080/164] remove extraneous commented code this was committed by accident as an artifact of some earlier experimentation --- www/js/components/charting.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/www/js/components/charting.ts b/www/js/components/charting.ts index 2ed64aa89..8d62eecf3 100644 --- a/www/js/components/charting.ts +++ b/www/js/components/charting.ts @@ -1,6 +1,3 @@ -// return getGradient(barChartRef.current, meter, barCtx, chartDatasets[i]); -// return getGradient(barChartRef.current, meter, barCtx, chartDatasets[i], 1, .25); - import color from 'color'; import { getBaseModeByKey } from '../diary/diaryHelper'; From 4143bf085af0626da34235a2a293a46d6fa55541 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 6 Sep 2023 23:30:58 -0400 Subject: [PATCH 081/164] refactor makeColorMap - use 'forEach' as it is a bit more concise than the traditional for loop - instead of formatLabelForSearch, we can borrow a function from confirmHelper that does exactly this and was just sitting around waiting to be used. Let's also ensure this function follows the same process as its opponent function `labelKeyToReadable`, but in reverse, and unify their naming scheme and add descriptions. This function can also be used in MultilabelButtonGroup because we do the same formatting there. --- www/js/components/charting.ts | 27 ++++++------------- .../multilabel/MultiLabelButtonGroup.tsx | 4 +-- www/js/survey/multilabel/confirmHelper.ts | 7 ++--- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/www/js/components/charting.ts b/www/js/components/charting.ts index 8d62eecf3..b9630494e 100644 --- a/www/js/components/charting.ts +++ b/www/js/components/charting.ts @@ -1,5 +1,6 @@ import color from 'color'; import { getBaseModeByKey } from '../diary/diaryHelper'; +import { readableLabelToKey } from '../survey/multilabel/confirmHelper'; export const defaultPalette = [ '#c95465', // red oklch(60% 0.15 14) @@ -115,30 +116,18 @@ export function deduplicateColor(modeColor, colorList) { return newColor; } -export function formatLabelForSearch(label) { - let formatted = ""; - let splitLabel = label.split(" "); - for (let i = 0; i < splitLabel.length; i++){ - formatted += splitLabel[i]; - if(i < splitLabel.length -1){ - formatted += "_"; //no '_' on the end! - } - } - return formatted; -} - export function makeColorMap(chartDatasets, labelOptions) { let tempColorMap = {}; - for(let i = 0; i < chartDatasets.length; i++) { - let modeColor = tempColorMap[chartDatasets[i]["label"]]; + chartDatasets.forEach((dataset) => { + let modeColor = tempColorMap[dataset["label"]]; if(!modeColor){ - const searchFormat = formatLabelForSearch(chartDatasets[i]["label"].toLowerCase()); - const modeOption = labelOptions?.MODE?.find(opt => opt.value == searchFormat); + const labelKey = readableLabelToKey(dataset["label"]); + const modeOption = labelOptions?.MODE?.find(opt => opt.value == labelKey); modeColor = getBaseModeByKey(modeOption?.baseMode || "OTHER").color; modeColor = deduplicateColor(modeColor, tempColorMap); - tempColorMap[chartDatasets[i]["label"]] = modeColor; + tempColorMap[dataset["label"]] = modeColor; } - } + }); return tempColorMap; -} \ No newline at end of file +} diff --git a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx index a10f58e55..ec56295eb 100644 --- a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx +++ b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx @@ -10,7 +10,7 @@ import DiaryButton from "../../components/DiaryButton"; import { useTranslation } from "react-i18next"; import { LabelTabContext } from "../../diary/LabelTab"; import { displayErrorMsg, logDebug } from "../../plugin/logger"; -import { getLabelInputDetails, getLabelInputs } from "./confirmHelper"; +import { getLabelInputDetails, getLabelInputs, readableLabelToKey } from "./confirmHelper"; const MultilabelButtonGroup = ({ trip, buttonsInline=false }) => { const { colors } = useTheme(); @@ -55,7 +55,7 @@ const MultilabelButtonGroup = ({ trip, buttonsInline=false }) => { if (isOther) { /* Let's make the value for user entered inputs look consistent with our other values (i.e. lowercase, and with underscores instead of spaces) */ - chosenLabel = chosenLabel.toLowerCase().replace(" ", "_"); + chosenLabel = readableLabelToKey(chosenLabel); } const inputDataToStore = { "start_ts": trip.start_ts, diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index b1d4ea9cd..9a3357491 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -101,7 +101,7 @@ export function getLabelInputDetails(appConfigParam?) { export const getLabelInputs = () => Object.keys(getLabelInputDetails()); export const getBaseLabelInputs = () => Object.keys(baseLabelInputDetails); -// replace all underscores with spaces, then capitalize each word +/** @description replace all underscores with spaces, and capitalizes the first letter of each word */ export const labelKeyToReadable = (otherValue: string) => { const words = otherValue.replace(/_/g, " ").split(" "); if (words.length == 0) return ""; @@ -110,8 +110,9 @@ export const labelKeyToReadable = (otherValue: string) => { ).join(" "); } -const otherTextToValue = (otherText) => - otherText.toLowerCase().replace(" ", "_"); +/** @description replaces all spaces with underscores, and lowercases the string */ +export const readableLabelToKey = (otherText: string) => + otherText.replace(/ /g, "_").toLowerCase(); export const getFakeEntry = (otherValue) => ({ text: labelKeyToReadable(otherValue), From e0c7d821bbc03582d731450379eb088e7fdd3447 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 7 Sep 2023 01:38:57 -0400 Subject: [PATCH 082/164] refactor modeColors chartData itself depends on modeColors, and nothing else uses modeColors. modeColors and chartData both depend on the same thing (chartDatasets) and should update whenever it does. So we can just have modeColors be computed inside of the body of chartData. And to ensure we have labelOptions defined, we'll add that to the dependency array, returning blank data if it isn't defined --- www/js/components/BarChart.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index e4295aa84..71dadb0c0 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -45,14 +45,10 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, const barChartRef = useRef>(null); const [chartDatasets, setChartDatasets] = useState([]); - const modeColors = useMemo(() => { - if(!meter){ - getLabelOptions().then((labelOptions) => setLabelOptions(labelOptions)); - return makeColorMap(chartDatasets, labelOptions); - } - }, [chartDatasets]) - - const chartData = useMemo>(() => ({ + const chartData = useMemo>(() => { + if (!labelOptions) return { datasets: [] }; + const modeColors = makeColorMap(chartDatasets, labelOptions); + return { datasets: chartDatasets.map((e, i) => ({ ...e, backgroundColor: (barCtx) => @@ -62,10 +58,14 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, meter ? getMeteredBackgroundColor(meter, barCtx, chartDatasets[i], colors, .25) : modeColors[chartDatasets[i]["label"]], })), - }), [chartDatasets, meter]); + }; + }, [chartDatasets, meter, labelOptions]); // group records by label (this is the format that Chart.js expects) useEffect(() => { + if (!labelOptions) { + getLabelOptions().then((labelOptions) => setLabelOptions(labelOptions)); + } const d = records?.reduce((acc, record) => { const existing = acc.find(e => e.label == record.label); if (!existing) { From 40a9b5e519800f605dee40cf017853b1ab50a253 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 7 Sep 2023 01:50:43 -0400 Subject: [PATCH 083/164] slim the Carousel card margins The gap between cards should not be bigger than the gap before the first card / after the last card. To get this effect, the first and last card need full size margins, while the inner margins can be half that. Overall this gives us slimmer margins that are uniform. --- www/js/components/Carousel.tsx | 18 ++++++++++-------- www/js/metrics/MetricsTab.tsx | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/www/js/components/Carousel.tsx b/www/js/components/Carousel.tsx index a8f958bfa..28a31ff6a 100644 --- a/www/js/components/Carousel.tsx +++ b/www/js/components/Carousel.tsx @@ -7,15 +7,16 @@ type Props = { cardMargin: number, } const Carousel = ({ children, cardWidth, cardMargin }: Props) => { + const numCards = React.Children.count(children); return ( - {React.Children.map(children, child => ( - + {React.Children.map(children, (child, i) => ( + {child} ))} @@ -24,13 +25,14 @@ const Carousel = ({ children, cardWidth, cardMargin }: Props) => { }; export const s = { - carouselScroll: { + carouselScroll: (cardMargin) => ({ // @ts-ignore, RN doesn't recognize `scrollSnapType`, but it does work on RN Web scrollSnapType: 'x mandatory', paddingVertical: 10, - }, - carouselCard: (cardWidth, cardMargin) => ({ - margin: cardMargin, + }), + carouselCard: (cardWidth, cardMargin, isFirst, isLast) => ({ + marginLeft: isFirst ? cardMargin : cardMargin/2, + marginRight: isLast ? cardMargin : cardMargin/2, width: cardWidth, scrollSnapAlign: 'center', scrollSnapStop: 'always', diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 01b1bddce..424336f1e 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -121,7 +121,7 @@ const MetricsTab = () => { ); } -export const cardMargin = 8; +export const cardMargin = 10; export const cardStyles: any = { card: { From 96ee1c6db028a22745d90da73a6b21cf0be98372 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 7 Sep 2023 01:50:58 -0400 Subject: [PATCH 084/164] decrease Y axis font size on BarChart in an effort to give some more space to the chart itself https://github.com/e-mission/e-mission-docs/issues/961#issuecomment-1708983132 --- www/js/components/BarChart.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 71dadb0c0..5cd100fad 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -49,15 +49,15 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, if (!labelOptions) return { datasets: [] }; const modeColors = makeColorMap(chartDatasets, labelOptions); return { - datasets: chartDatasets.map((e, i) => ({ - ...e, - backgroundColor: (barCtx) => - meter ? getMeteredBackgroundColor(meter, barCtx, chartDatasets[i], colors) - : modeColors[chartDatasets[i]["label"]], - borderColor: (barCtx) => - meter ? getMeteredBackgroundColor(meter, barCtx, chartDatasets[i], colors, .25) - : modeColors[chartDatasets[i]["label"]], - })), + datasets: chartDatasets.map((e, i) => ({ + ...e, + backgroundColor: (barCtx) => + meter ? getMeteredBackgroundColor(meter, barCtx, chartDatasets[i], colors) + : modeColors[chartDatasets[i]["label"]], + borderColor: (barCtx) => + meter ? getMeteredBackgroundColor(meter, barCtx, chartDatasets[i], colors, .25) + : modeColors[chartDatasets[i]["label"]], + })), }; }, [chartDatasets, meter, labelOptions]); @@ -123,6 +123,7 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, return label.split('\n'); return label; }, + font: { size: 11 }, // default is 12, we want a tad smaller }, reverse: true, stacked, From 3335086d0dafe725bddf2f0f7694ae44aa83af94 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 7 Sep 2023 03:45:47 -0400 Subject: [PATCH 085/164] format CarbonTextCard as rows it seems more comprehensible when organized this way --- www/js/metrics/CarbonTextCard.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index d2a6e620c..1be12ef6c 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -41,7 +41,7 @@ const DailyActiveMinutesCard = ({ userMetrics, aggMetrics }: Props) => { low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), high: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, FootprintHelper.getHighestFootprint()), }; - textList.push({label: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`, + textList.push({label: `${t('main-metrics.past-week')} (${formatDateRangeOfDays(thisWeekDistance)})`, value: (userPastWeek.high - userPastWeek.low)==0 ? Math.round(userPastWeek.low) : Math.round(userPastWeek.low) + " - " + Math.round(userPastWeek.high)}); //calculate low-high and format range for prev week, if exists @@ -50,7 +50,7 @@ const DailyActiveMinutesCard = ({ userMetrics, aggMetrics }: Props) => { low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) }; - textList.push({label: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`, + textList.push({label: `${t('main-metrics.prev-week')} (${formatDateRangeOfDays(lastWeekDistance)})`, value: (userPrevWeek.high - userPrevWeek.low)==0 ? Math.round(userPrevWeek.low) : Math.round(userPrevWeek.low) + " - " + Math.round(userPrevWeek.high)}); } @@ -118,14 +118,12 @@ const DailyActiveMinutesCard = ({ userMetrics, aggMetrics }: Props) => { style={cardStyles.title(colors)} /> { textEntries?.length > 0 && - - { Object.keys(textEntries).map((i) => - + Object.keys(textEntries).map((i) => + {textEntries[i].label} {textEntries[i].value + ' ' + "kg Co2"} - )} - + ) } From 50c5292d81dc76b0c35784ba98ee1a43bf99a878 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 7 Sep 2023 03:49:00 -0400 Subject: [PATCH 086/164] fix warnings in MetricsCard React warns us about the missing 'key' prop if we don't put a key whenever we iterate and repeat components, so a `key` was added to this View. The way the brackets were arranged on line 89 was actually causing a single space " " to be rendered, which React was also warning us about in the console. --- www/js/metrics/MetricsCard.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index a20bf598b..419dcf5a8 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -80,13 +80,14 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni {viewMode=='details' && { Object.keys(metricSumValues).map((label, i) => - + {labelKeyToReadable(label)} {metricSumValues[label] + ' ' + axisUnits} )} - } {viewMode=='graph' && <> + } + {viewMode=='graph' && <> From 3cb062ac2130d8ef5e63c070e3dd9e9392c42809 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 7 Sep 2023 03:50:14 -0400 Subject: [PATCH 087/164] remove colons from carbon categories --- www/i18n/en.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index c74bff237..980b5d061 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -162,15 +162,15 @@ "speed": "My Speed", "footprint": "My Footprint", "how-it-compares": "Ballpark comparisons", - "optimal": "Optimal (perfect mode choice for all my trips):", - "average": "Group Avg.:", - "worst-case": "If all 'taxi':", + "optimal": "Optimal (perfect mode choice for all my trips)", + "average": "Group Avg.", + "worst-case": "If all 'taxi'", "label-to-squish": "Label trips to collapse the range into a single number", "lastweek": "My last week value:", "us-2030-goal": "2030 Guideline", "us-2050-goal": "2050 Guideline", - "past-week" : "Past Week:", - "prev-week" : "Prev. Week:", + "past-week" : "Past Week", + "prev-week" : "Prev. Week", "calories": "My Calories", "calibrate": "Calibrate", "no-summary-data": "No summary data", From aad133e5248365689f4dc4bb5cc5b2326114e3d0 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 7 Sep 2023 04:36:42 -0400 Subject: [PATCH 088/164] style US decarbonization goals text as footnote MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This description is basically a footnote - why not formally represent it as such? We'll use small text and a superscript ¹ to mark the footnote. --- www/i18n/en.json | 4 ++-- www/js/metrics/CarbonFootprintCard.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index 980b5d061..598660cac 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -167,8 +167,8 @@ "worst-case": "If all 'taxi'", "label-to-squish": "Label trips to collapse the range into a single number", "lastweek": "My last week value:", - "us-2030-goal": "2030 Guideline", - "us-2050-goal": "2050 Guideline", + "us-2030-goal": "2030 Guideline¹", + "us-2050-goal": "2050 Guideline¹", "past-week" : "Past Week", "prev-week" : "Prev. Week", "calories": "My Calories", diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 212453b39..4526e7ac2 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -139,9 +139,9 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { - + {/* TODO i18n */} - Dashed lines: US decarbonization goals scaled to per-capita travel-related emissions. + ¹US decarbonization goals scaled to per-capita travel-related emissions. : From 3ef68608efb12736832ccf9e7eb6948ed24a6e06 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 7 Sep 2023 04:37:22 -0400 Subject: [PATCH 089/164] add footnote for "Unlabeled" uncertainty --- www/i18n/en.json | 2 +- www/js/metrics/CarbonTextCard.tsx | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index 598660cac..b7c4c9e45 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -183,7 +183,7 @@ "equals-bananas_other": "Equals at least {{count}} bananas", "active-minutes": "My Active Minutes", "labeled": "Labeled", - "unlabeled": "Unlabeled", + "unlabeled": "Unlabeled²", "footprint-label": "Footprint (kg Co2)" }, diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index 1be12ef6c..78b664aa1 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -125,6 +125,11 @@ const DailyActiveMinutesCard = ({ userMetrics, aggMetrics }: Props) => { ) } + + {/* TODO i18n */} + {/* unlabeled means the mode is not labeled and the carbon footprint is uncertain, it may fall anywhere between 0 (best case) and 'taxi' (worst case) */} + ²The carbon footprint of unlabeled trips is uncertain. Estimates may fall anywhere within the shown range. + ) From ac59d009f492b7bf3325c7fecff9efc0211d8de1 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 7 Sep 2023 04:40:00 -0400 Subject: [PATCH 090/164] make Carousel cards slightly wider We want these cards as wide as possible to show all the info we have without it seeming cramped. But we cannot go too wide or the bleeding edge won't show. Here is a good balance. --- www/js/metrics/MetricsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 424336f1e..975ceb29a 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -78,7 +78,7 @@ const MetricsTab = () => { }); const { width: windowWidth } = useWindowDimensions(); - const cardWidth = windowWidth * .85; + const cardWidth = windowWidth * .88; return (<> From 99765161666f769d6c8ad55da2038bf38052d28d Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 7 Sep 2023 09:10:40 -0600 Subject: [PATCH 091/164] fix previous week bar value mistakenly, the previous and past weeks were showing the same "certain" value I noticed that even with a 43% change the bars looked the same, and was able to catch and fix my mistake --- www/js/metrics/CarbonFootprintCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 4526e7ac2..b79bf9211 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -55,7 +55,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) }; graphRecords.push({label: t('main-metrics.unlabeled'), x: userPrevWeek.high - userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}) - graphRecords.push({label: t('main-metrics.labeled'), x: userPastWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}); + graphRecords.push({label: t('main-metrics.labeled'), x: userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}); let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); setEmissionsChange(pctChange); From e4cadd24b2f1d6c398f2c0cef78306c9eb5d23b1 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 7 Sep 2023 21:15:04 -0400 Subject: [PATCH 092/164] adjust color-by-mode on dashboard We want our code used for charting to be fairly generic and not specific to any particular use case. So instead of handling the mode labels and their base modes in charting.ts, we should refactor this so the logic is encapsulated in the appropriate areas of the codebase. To accomplish this, BarChart can accept a function `getColorForLabel` that it will use on each of the labels it finds in `records`. This way, BarChart and charting.ts don't have to specificially deal with the lookup of the base mode colors. The logic that *does* get included in the charting code, because it is needed for visually distinguishing labels on the charts, is the de-duplication function. I refactored this to be more generic as well - not based on fixed 'stops' of darkening/lightening. Instead, it finds how many duplicates there are and evenly distributes them in lightness. So if there are 2, one will be lightened and the other darkened. With an odd number, the middle will stay the same. And the more duplicates, the closer in shade they will be. Darkening was toned down by a scale of .5 because it has a more drastic effect than lightening. The modeColors themselves were also slightly adjusted in diaryHelper.ts - notably, the difference in hue between 'pink' and 'red' was widened. Besides this, everything was made very slightly lighter and the oklch comments were updated to reflect in-gamut saturation values. --- www/js/components/BarChart.tsx | 17 ++++--- www/js/components/charting.ts | 58 ++++++++++------------ www/js/diary/diaryHelper.ts | 25 ++++++---- www/js/metrics/MetricsCard.tsx | 6 ++- www/js/metrics/WeeklyActiveMinutesCard.tsx | 6 ++- www/js/survey/multilabel/confirmHelper.ts | 6 +-- 6 files changed, 64 insertions(+), 54 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 5cd100fad..efcd962c1 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -3,10 +3,10 @@ import React, { useEffect, useRef, useState, useMemo } from 'react'; import { angularize } from '../angular-react-helper'; import { View } from 'react-native'; import { useTheme } from 'react-native-paper'; -import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, TimeScale, ChartData, ScriptableContext, ChartArea } from 'chart.js'; +import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, TimeScale, ChartData } from 'chart.js'; import { Chart } from 'react-chartjs-2'; import Annotation, { AnnotationOptions, LabelPosition } from 'chartjs-plugin-annotation'; -import { defaultPalette, getChartHeight, getMeteredBackgroundColor, makeColorMap } from './charting'; +import { dedupColors, getChartHeight, getMeteredBackgroundColor } from './charting'; import { getLabelOptions } from "../survey/multilabel/confirmHelper"; ChartJS.register( @@ -33,9 +33,10 @@ type Props = { isHorizontal?: boolean, timeAxis?: boolean, stacked?: boolean, + getColorForLabel?: (label: string) => string, meter?: {high: number, middle: number, dash_key: string}, } -const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, stacked, meter }: Props) => { +const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, stacked, getColorForLabel, meter }: Props) => { const { colors } = useTheme(); const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); @@ -47,16 +48,20 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, const chartData = useMemo>(() => { if (!labelOptions) return { datasets: [] }; - const modeColors = makeColorMap(chartDatasets, labelOptions); + let labelColorMap; // object mapping labels to colors + if (getColorForLabel) { + const colorEntries = chartDatasets.map(d => [d.label, getColorForLabel(d.label)] ); + labelColorMap = dedupColors(colorEntries); + } return { datasets: chartDatasets.map((e, i) => ({ ...e, backgroundColor: (barCtx) => meter ? getMeteredBackgroundColor(meter, barCtx, chartDatasets[i], colors) - : modeColors[chartDatasets[i]["label"]], + : labelColorMap[e.label], borderColor: (barCtx) => meter ? getMeteredBackgroundColor(meter, barCtx, chartDatasets[i], colors, .25) - : modeColors[chartDatasets[i]["label"]], + : labelColorMap[e.label], })), }; }, [chartDatasets, meter, labelOptions]); diff --git a/www/js/components/charting.ts b/www/js/components/charting.ts index b9630494e..676739dd6 100644 --- a/www/js/components/charting.ts +++ b/www/js/components/charting.ts @@ -89,45 +89,39 @@ export function getMeteredBackgroundColor(meter, barCtx, currDataset, colors, da return meteredColor; } -function darkenOrLighten(baseColor, change) { +/** + * @param baseColor a color string + * @param change a number between -1 and 1, indicating the amount to darken or lighten the color + * @returns an adjusted color, either darkened or lightened, depending on the sign of change + */ +function darkenOrLighten(baseColor: string, change: number) { let colorObj = color(baseColor); if(change > 0) { - return colorObj.darken(Math.abs(change)).hex(); + // darkening appears more drastic than lightening, so we will be less aggressive (scale change by .5) + return colorObj.darken(Math.abs(change * .5)).hex(); } else { return colorObj.lighten(Math.abs(change)).hex(); } } -export function deduplicateColor(modeColor, colorList) { - const options = [ -0.3, 0.3, -0.6, 0.6]; - let newColor = modeColor; - let i = 0; - - if(Object.values(colorList).includes(modeColor)){ - for (let i=0; i< options.length; i++){ - if(i >= options.length - 1){ - newColor = modeColor; //just in case, if out of options, use original color - } else { - newColor = darkenOrLighten(modeColor, options[i]); - if(!(Object.values(colorList).includes(newColor))) {break;} - } +/** + * @param colors an array of colors, each of which is an array of [key, color string] + * @returns an object mapping keys to colors, with duplicates darkened/lightened to be distinguishable + */ +export const dedupColors = (colors: string[][]) => { + const dedupedColors = {}; + const maxAdjustment = 0.7; // more than this is too drastic and the colors approach black/white + for (const [key, clr] of colors) { + const duplicates = colors.filter(([k, c]) => c == clr); + if (duplicates.length > 1) { + // there are duplicates; calculate an evenly-spaced adjustment for each one + duplicates.forEach(([k, c], i) => { + const change = -maxAdjustment + (maxAdjustment*2 / (duplicates.length - 1)) * i; + dedupedColors[k] = darkenOrLighten(clr, change); + }); + } else if (!dedupedColors[key]) { + dedupedColors[key] = clr; // not a dupe, & not already deduped, so use the color as-is } } - return newColor; -} - -export function makeColorMap(chartDatasets, labelOptions) { - let tempColorMap = {}; - chartDatasets.forEach((dataset) => { - let modeColor = tempColorMap[dataset["label"]]; - if(!modeColor){ - const labelKey = readableLabelToKey(dataset["label"]); - const modeOption = labelOptions?.MODE?.find(opt => opt.value == labelKey); - modeColor = getBaseModeByKey(modeOption?.baseMode || "OTHER").color; - modeColor = deduplicateColor(modeColor, tempColorMap); - - tempColorMap[dataset["label"]] = modeColor; - } - }); - return tempColorMap; + return dedupedColors; } diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts index 7a626f423..54776b372 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -3,17 +3,18 @@ import moment from "moment"; import { DateTime } from "luxon"; +import { LabelOptions, readableLabelToKey } from "../survey/multilabel/confirmHelper"; const modeColors = { - pink: '#d43678', // oklch(59% 0.2 0) // e-car - red: '#b9003d', // oklch(50% 0.37 15) // car - orange: '#b25200', // oklch(55% 0.37 50) // air, hsr - green: '#007e46', // oklch(52% 0.37 155) // bike, e-biek, moped - blue: '#0068a5', // oklch(50% 0.37 245) // walk - periwinkle: '#5e45cd', // oklch(50% 0.2 285) // light rail, train, tram, subway - magenta: '#8e35a1', // oklch(50% 0.18 320) // bus - grey: '#484848', // oklch(40% 0 0) // unprocessed / unknown - taupe: '#7d5857', // oklch(50% 0.05 15) // ferry, trolleybus, user-defined modes + pink: '#c32e85', // oklch(56% 0.2 350) // e-car + red: '#c21725', // oklch(52% 0.2 25) // car + orange: '#bf5900', // oklch(58% 0.16 50) // air, hsr + green: '#008148', // oklch(53% 0.14 155) // bike, e-bike, moped + blue: '#0074b7', // oklch(54% 0.14 245) // walk + periwinkle: '#6356bf', // oklch(52% 0.16 285) // light rail, train, tram, subway + magenta: '#9240a4', // oklch(52% 0.17 320) // bus + grey: '#555555', // oklch(45% 0 0) // unprocessed / unknown + taupe: '#7d585a', // oklch(50% 0.05 15) // ferry, trolleybus, user-defined modes } type BaseMode = { @@ -71,6 +72,12 @@ export function getBaseModeOfLabeledTrip(trip, labelOptions) { return getBaseModeByKey(modeOption?.baseMode || "OTHER"); } +export function getBaseModeByReadableLabel(label, labelOptions: LabelOptions) { + const labelKey = readableLabelToKey(label); + const modeOption = labelOptions?.MODE?.find(opt => opt.value == labelKey); + return getBaseModeByKey(modeOption?.baseMode || "OTHER"); +} + /** * @param beginFmtTime An ISO 8601 formatted timestamp (with timezone) * @param endTs An ISO 8601 formatted timestamp (with timezone) diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index 419dcf5a8..8a6f7d002 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -7,7 +7,8 @@ import { DayOfMetricData } from './metricsTypes'; import { getLabelsForDay, getUniqueLabelsForDays } from './metricsHelper'; import ToggleSwitch from '../components/ToggleSwitch'; import { cardStyles } from './MetricsTab'; -import { labelKeyToReadable } from '../survey/multilabel/confirmHelper'; +import { labelKeyToReadable, labelOptions } from '../survey/multilabel/confirmHelper'; +import { getBaseModeByReadableLabel } from '../diary/diaryHelper'; type Props = { cardTitle: string, @@ -89,7 +90,8 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni } {viewMode=='graph' && <> + isHorizontal={true} timeAxis={true} stacked={graphIsStacked} + getColorForLabel={(l) => getBaseModeByReadableLabel(l, labelOptions).color} /> Stack bars: { + lineAnnotations={[{ value: 150, label: 'Weekly Goal', position: 'center' }]} + getColorForLabel={(l) => getBaseModeByReadableLabel(l, labelOptions).color} /> : diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index 9a3357491..9bd3dd40a 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -13,7 +13,7 @@ type InputDetails = { key: string, } }; -type LabelOptions = { +export type LabelOptions = { [k in T]: { value: string, baseMode: string, @@ -26,8 +26,8 @@ type LabelOptions = { }}; let appConfig; -let labelOptions: LabelOptions<'MODE'|'PURPOSE'|'REPLACED_MODE'>; -let inputDetails: InputDetails<'MODE'|'PURPOSE'|'REPLACED_MODE'>; +export let labelOptions: LabelOptions<'MODE'|'PURPOSE'|'REPLACED_MODE'>; +export let inputDetails: InputDetails<'MODE'|'PURPOSE'|'REPLACED_MODE'>; export async function getLabelOptions(appConfigParam?) { if (appConfigParam) appConfig = appConfigParam; From 44d31851b8c3e1ff4edef57c075eb0d14667d430 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 11 Sep 2023 13:38:55 -0400 Subject: [PATCH 093/164] refactor: rename BarChart to Chart --- www/js/components/{BarChart.tsx => Chart.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename www/js/components/{BarChart.tsx => Chart.tsx} (100%) diff --git a/www/js/components/BarChart.tsx b/www/js/components/Chart.tsx similarity index 100% rename from www/js/components/BarChart.tsx rename to www/js/components/Chart.tsx From 25a5f5a8b0be6d7ff5b2afd5c8908debab50ae46 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 11 Sep 2023 15:35:17 -0400 Subject: [PATCH 094/164] big refactor of BarChart & LineChart Instead of repeating a bunch of code between BarChart and LineChart, let's refactor this into a common Chart component that works with a type of either 'bar' or 'line'. Then BarChart and LineCharts will simply be composed of a Chart with the appropriate type specified. Any logic specific to BarCharts or LineCharts can be put in the respective files. Currently, LineChart has no specific logic and the only thing BarCharts have specifically is the 'meter' prop. --- www/js/components/BarChart.tsx | 25 ++++++ www/js/components/Chart.tsx | 57 +++++-------- www/js/components/LineChart.tsx | 143 ++------------------------------ www/js/components/charting.ts | 4 +- 4 files changed, 56 insertions(+), 173 deletions(-) create mode 100644 www/js/components/BarChart.tsx diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx new file mode 100644 index 000000000..82a80fc01 --- /dev/null +++ b/www/js/components/BarChart.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import Chart, { Props as ChartProps } from "./Chart"; +import { useTheme } from "react-native-paper"; +import { getMeteredBackgroundColor } from "./charting"; + +type Props = Omit & { + meter?: {high: number, middle: number, dash_key: string}, +} +const BarChart = ({ meter, ...rest }: Props) => { + + const { colors } = useTheme(); + + if (meter) { + rest.getColorForLabel = (label, dataset, ctx, colorFor) => { + const darkenDegree = colorFor == 'border' ? 0.25 : 0; + return getMeteredBackgroundColor(meter, dataset, ctx, colors, darkenDegree); + } + } + + return ( + + ); +} + +export default BarChart; diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx index efcd962c1..a3a7bf472 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -1,19 +1,19 @@ import React, { useEffect, useRef, useState, useMemo } from 'react'; -import { angularize } from '../angular-react-helper'; import { View } from 'react-native'; import { useTheme } from 'react-native-paper'; -import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, TimeScale, ChartData } from 'chart.js'; -import { Chart } from 'react-chartjs-2'; +import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, TimeScale, ChartData, ChartType, ScriptableContext, PointElement, LineElement } from 'chart.js'; +import { Chart as ChartJSChart } from 'react-chartjs-2'; import Annotation, { AnnotationOptions, LabelPosition } from 'chartjs-plugin-annotation'; -import { dedupColors, getChartHeight, getMeteredBackgroundColor } from './charting'; -import { getLabelOptions } from "../survey/multilabel/confirmHelper"; +import { dedupColors, getChartHeight } from './charting'; ChartJS.register( CategoryScale, LinearScale, TimeScale, BarElement, + PointElement, + LineElement, Title, Tooltip, Legend, @@ -21,33 +21,32 @@ ChartJS.register( ); type XYPair = { x: number|string, y: number|string }; -type ChartDatasets = { +type ChartDataset = { label: string, data: XYPair[], -}[]; +}; -type Props = { +export type Props = { records: { label: string, x: number|string, y: number|string }[], axisTitle: string, + type: 'bar'|'line', + getColorForLabel: (label: string, currDataset?: ChartDataset, ctx?: ScriptableContext<'bar'|'line'>, colorFor?: 'background'|'border') => string|null, + borderWidth?: number, lineAnnotations?: { value: number, label?: string, color?:string, position?: LabelPosition }[], isHorizontal?: boolean, timeAxis?: boolean, stacked?: boolean, - getColorForLabel?: (label: string) => string, - meter?: {high: number, middle: number, dash_key: string}, } -const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, stacked, getColorForLabel, meter }: Props) => { +const Chart = ({ records, axisTitle, type, getColorForLabel, borderWidth, lineAnnotations, isHorizontal, timeAxis, stacked }: Props) => { const { colors } = useTheme(); const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); - const [labelOptions, setLabelOptions] = useState(null); const indexAxis = isHorizontal ? 'y' : 'x'; - const barChartRef = useRef>(null); - const [chartDatasets, setChartDatasets] = useState([]); + const chartRef = useRef>(null); + const [chartDatasets, setChartDatasets] = useState([]); - const chartData = useMemo>(() => { - if (!labelOptions) return { datasets: [] }; + const chartData = useMemo>(() => { let labelColorMap; // object mapping labels to colors if (getColorForLabel) { const colorEntries = chartDatasets.map(d => [d.label, getColorForLabel(d.label)] ); @@ -56,21 +55,15 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, return { datasets: chartDatasets.map((e, i) => ({ ...e, - backgroundColor: (barCtx) => - meter ? getMeteredBackgroundColor(meter, barCtx, chartDatasets[i], colors) - : labelColorMap[e.label], - borderColor: (barCtx) => - meter ? getMeteredBackgroundColor(meter, barCtx, chartDatasets[i], colors, .25) - : labelColorMap[e.label], + backgroundColor: (barCtx) => labelColorMap[e.label] || getColorForLabel(e.label, e, barCtx, 'background'), + borderColor: (barCtx) => labelColorMap[e.label] || getColorForLabel(e.label, e, barCtx, 'border'), + borderWidth: borderWidth || 2, })), }; - }, [chartDatasets, meter, labelOptions]); + }, [chartDatasets, getColorForLabel]); // group records by label (this is the format that Chart.js expects) useEffect(() => { - if (!labelOptions) { - getLabelOptions().then((labelOptions) => setLabelOptions(labelOptions)); - } const d = records?.reduce((acc, record) => { const existing = acc.find(e => e.label == record.label); if (!existing) { @@ -88,24 +81,19 @@ const BarChart = ({ records, axisTitle, lineAnnotations, isHorizontal, timeAxis, }); } return acc; - }, [] as ChartDatasets); + }, [] as ChartDataset[]); setChartDatasets(d); }, [records]); return ( - ) } -angularize(BarChart, 'BarChart', 'emission.main.barchart'); -export default BarChart; +export default Chart; diff --git a/www/js/components/LineChart.tsx b/www/js/components/LineChart.tsx index 9e707958a..66d21aac2 100644 --- a/www/js/components/LineChart.tsx +++ b/www/js/components/LineChart.tsx @@ -1,142 +1,11 @@ +import React from "react"; +import Chart, { Props as ChartProps } from "./Chart"; -import React, { useRef, useMemo } from 'react'; -import { View } from 'react-native'; -import { Chart, CategoryScale, LinearScale, Title, Tooltip, Legend, TimeScale, PointElement, LineElement } from 'chart.js'; -import { Line } from 'react-chartjs-2'; - -Chart.register( - CategoryScale, - LinearScale, - TimeScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, -); - -type BarChartData = { - label: string, - data: { x: number|string, y: number|string }[], -}[]; - -type Props = { - records: { label: string, x: number|string, y: number|string }[], - axisTitle: string, - isHorizontal?: boolean, - timeAxis?: boolean, -} -const LineChart = ({ records, axisTitle, isHorizontal=true, timeAxis }: Props) => { - - const indexAxis = isHorizontal ? 'y' : 'x'; - const lineChartRef = useRef(null); - - // group records by label (this is the format that Chart.js expects) - const chartData = useMemo(() => { - return records?.reduce((acc, record) => { - const existing = acc.find(e => e.label == record.label); - if (!existing) { - acc.push({ - label: record.label, - data: [{ - x: record.x, - y: record.y, - }], - }); - } else { - existing.data.push({ - x: record.x, - y: record.y, - }); - } - return acc; - }, [] as BarChartData); - }, [records]); - +type Props = Omit & { } +const LineChart = ({ ...rest }: Props) => { return ( - - ({ - ...e, - // cycle through the default palette, repeat if necessary - backgroundColor: defaultPalette[i % defaultPalette.length], - borderColor: defaultPalette[i % defaultPalette.length], - borderWidth: 2, - tension: .2, - }))}} - options={{ - indexAxis: indexAxis, - responsive: true, - maintainAspectRatio: false, - resizeDelay: 1, - scales: { - ...(isHorizontal ? { - y: { - offset: true, - type: timeAxis ? 'time' : 'category', - adapters: timeAxis ? { - date: { zone: 'utc' }, - } : {}, - time: timeAxis ? { - unit: 'day', - tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 - } : {}, - ticks: timeAxis ? {} : { - callback: (value, i) => { - const label = chartData[0].data[i].y; - if (typeof label == 'string' && label.includes('\n')) - return label.split('\n'); - return label; - }, - }, - reverse: true, - }, - x: { - title: { display: true, text: axisTitle }, - }, - } : { - x: { - offset: true, - type: timeAxis ? 'time' : 'category', - adapters: timeAxis ? { - date: { zone: 'utc' }, - } : {}, - time: timeAxis ? { - unit: 'day', - tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 - } : {}, - ticks: timeAxis ? {} : { - callback: (value, i) => { - console.log("testing vertical", chartData, i); - const label = chartData[0].data[i].x; - if (typeof label == 'string' && label.includes('\n')) - return label.split('\n'); - return label; - }, - }, - }, - y: { - title: { display: true, text: axisTitle }, - }, - }), - }, - }} /> - - ) + + ); } -const defaultPalette = [ - '#c95465', // red oklch(60% 0.15 14) - '#4a71b1', // blue oklch(55% 0.11 260) - '#d2824e', // orange oklch(68% 0.12 52) - '#856b5d', // brown oklch(55% 0.04 50) - '#59894f', // green oklch(58% 0.1 140) - '#e0cc55', // yellow oklch(84% 0.14 100) - '#b273ac', // purple oklch(64% 0.11 330) - '#f09da6', // pink oklch(78% 0.1 12) - '#b3aca8', // grey oklch(75% 0.01 55) - '#80afad', // teal oklch(72% 0.05 192) -]; - - export default LineChart; diff --git a/www/js/components/charting.ts b/www/js/components/charting.ts index 676739dd6..af72b0f35 100644 --- a/www/js/components/charting.ts +++ b/www/js/components/charting.ts @@ -71,7 +71,8 @@ function createDiagonalPattern(color = 'black') { return c.createPattern(shape, 'repeat') } -export function getMeteredBackgroundColor(meter, barCtx, currDataset, colors, darken=0) { +export function getMeteredBackgroundColor(meter, currDataset, barCtx, colors, darken=0) { + if (!barCtx || !currDataset) return; let bar_height = getBarHeight(barCtx.parsed._stacks); console.debug("bar height for", barCtx.raw.y, " is ", bar_height, "which in chart is", currDataset); let meteredColor; @@ -112,6 +113,7 @@ export const dedupColors = (colors: string[][]) => { const dedupedColors = {}; const maxAdjustment = 0.7; // more than this is too drastic and the colors approach black/white for (const [key, clr] of colors) { + if (!clr) continue; // skip empty colors const duplicates = colors.filter(([k, c]) => c == clr); if (duplicates.length > 1) { // there are duplicates; calculate an evenly-spaced adjustment for each one From 83aa95807d86adc70d1132f9aea79f3476382d62 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 11 Sep 2023 15:36:11 -0400 Subject: [PATCH 095/164] add base mode colors to DailyActiveMinutesCard Now that we refactored, the line chart can accept getColorForLabel in the same way that bar charts do. --- www/js/metrics/DailyActiveMinutesCard.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/DailyActiveMinutesCard.tsx b/www/js/metrics/DailyActiveMinutesCard.tsx index a2f460443..4dabf8b39 100644 --- a/www/js/metrics/DailyActiveMinutesCard.tsx +++ b/www/js/metrics/DailyActiveMinutesCard.tsx @@ -5,8 +5,9 @@ import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; import { useTranslation } from 'react-i18next'; -import { labelKeyToReadable } from '../survey/multilabel/confirmHelper'; +import { labelKeyToReadable, labelOptions } from '../survey/multilabel/confirmHelper'; import LineChart from '../components/LineChart'; +import { getBaseModeByReadableLabel } from '../diary/diaryHelper'; const ACTIVE_MODES = ['walk', 'bike'] as const; type ActiveMode = typeof ACTIVE_MODES[number]; @@ -45,7 +46,8 @@ const DailyActiveMinutesCard = ({ userMetrics }: Props) => { { dailyActiveMinutesRecords.length ? + timeAxis={true} isHorizontal={false} + getColorForLabel={(l) => getBaseModeByReadableLabel(l, labelOptions).color} /> : From c240f7926b4705ae6d0ac0d6d9f77fce46f78283 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 11 Sep 2023 16:53:42 -0600 Subject: [PATCH 096/164] convert the log page to React for react routing, we need to convert this page and the sync settings page maintain access for now by routing to a React page with angular routing using AlertBars for the error messages first draft: scrolling the FlashList is broken and the data doesn't load unless you manually tap refresh --- www/js/control/LogPage.tsx | 155 ++++++++++++++++++++++++++++++++++ www/js/main.js | 2 +- www/js/recent.js | 70 +-------------- www/templates/recent/log.html | 21 ----- 4 files changed, 159 insertions(+), 89 deletions(-) create mode 100644 www/js/control/LogPage.tsx delete mode 100644 www/templates/recent/log.html diff --git a/www/js/control/LogPage.tsx b/www/js/control/LogPage.tsx new file mode 100644 index 000000000..4daec09ed --- /dev/null +++ b/www/js/control/LogPage.tsx @@ -0,0 +1,155 @@ +import React, { useState, useMemo, useEffect } from "react"; +import { View, StyleSheet, ScrollView } from "react-native"; +import { useTheme, Text, Appbar, IconButton } from "react-native-paper"; +import { angularize, getAngularService } from "../angular-react-helper"; +import { useTranslation } from "react-i18next"; +import { FlashList } from '@shopify/flash-list'; +import useAppConfig from "../useAppConfig"; +import moment from "moment"; +import AlertBar from "./AlertBar"; + +type loadStats = { currentStart: number, gotMaxIndex: boolean, reachedEnd: boolean }; + +//any pure functions can go outside +const LogPage = () => { + // anything that mutates must go in --- depend on props or state... + const { t } = useTranslation(); + const { colors } = useTheme(); + const EmailHelper = getAngularService('EmailHelper'); + const $state = getAngularService('$state'); + const { appConfig, loading } = useAppConfig(); + + const [ loadStats, setLoadStats ] = useState(); + const [ entries, setEntries ] = useState([]); + const [ maxErrorVis, setMaxErrorVis ] = useState(false); + const [ logErrorVis, setLogErrorVis ] = useState(false); + + const [ maxMessage, setMaxMessage ] = useState(""); + const [ logMessage, setLogMessage ] = useState(""); + + var RETRIEVE_COUNT = 100; + + useEffect(() => { + if(!loading) { + refreshEntries(); + } + }, [appConfig]); + + const refreshEntries = function() { + window?.Logger.getMaxIndex().then(function(maxIndex) { + console.log("maxIndex = "+maxIndex); + let tempStats = {} as loadStats; + tempStats.currentStart = maxIndex; + tempStats.gotMaxIndex = true; + tempStats.reachedEnd = false; + setLoadStats(tempStats); + setEntries([]); + addEntries(); + }, function(error) { + let errorString = "While getting max index "+JSON.stringify(error, null, 2); + console.log(errorString); + setMaxMessage(errorString); + setMaxErrorVis(true); + }) + } + + const moreDataCanBeLoaded = useMemo(() => { + return loadStats?.gotMaxIndex && loadStats?.reachedEnd; + }, [loadStats]) + + const clear = function() { + window?.Logger.clearAll(); + window?.Logger.log(window.Logger.LEVEL_INFO, "Finished clearing entries from unified log"); + refreshEntries(); + } + + async function addEntries() { + console.log("calling addEntries"); + window.Logger.getMessagesFromIndex(loadStats?.currentStart, RETRIEVE_COUNT) + .then(function(entryList) { + processEntries(entryList); + console.log("entry list size = "+ entries.length); + //$scope.$broadcast('scroll.infiniteScrollComplete') //do I still need this? + }, function(error) { + let errStr = "While getting messages from the log "+JSON.stringify(error, null, 2); + console.log(errStr); + setLogMessage(errStr); + setLogErrorVis(true); + //$scope.$broadcast('scroll.infiniteScrollComplete') //do I still need this? + }) + } + + const processEntries = function(entryList) { + let tempEntries = []; + let tempLoadStats = {...loadStats}; + entryList.forEach(e => { + e.fmt_time = moment.unix(e.ts).format("llll"); + tempEntries.push(e); + }); + if (entryList.length == 0) { + console.log("Reached the end of the scrolling"); + tempLoadStats.reachedEnd = true; + } else { + tempLoadStats.currentStart = entryList[entryList.length-1].ID; + console.log("new start index = "+loadStats.currentStart); + } + setEntries([...entries].concat(tempEntries)); //push the new entries onto the list + setLoadStats(tempLoadStats); + } + + const emailLog = function () { + EmailHelper.sendEmail("loggerDB"); + } + + const separator = () => + const logItem = ({item: logItem}) => + {logItem.fmt_time} + {logItem.ID + "|" + logItem.level + "|" + logItem.message} + + + return ( + <> + {/* //appbar across the top with back to profile and "log" page title */} + + {$state.go("root.main.control");}}/> + + + + {/* //row of buttons to refresh, delete, or email */} + + refreshEntries()}/> + clear()}/> + emailLog()}/> + + + {/* //list of dates and times, each with some data associated */} + + item.ID} + ItemSeparatorComponent={separator} + onScroll={e => {if(moreDataCanBeLoaded){addEntries()}}}/> + + + + + + ); +}; +const styles = StyleSheet.create({ + date: (surfaceColor) => ({ + backgroundColor: surfaceColor, + }), + details: { + fontFamily: "monospace", + }, + entry: (surfaceColor) => ({ + backgroundColor: surfaceColor, + marginLeft: 5, + }), + }); + +angularize(LogPage, 'LogPage', 'emission.main.log.logPage'); +export default LogPage; \ No newline at end of file diff --git a/www/js/main.js b/www/js/main.js index 79bfdee0d..93b1c4abc 100644 --- a/www/js/main.js +++ b/www/js/main.js @@ -47,7 +47,7 @@ angular.module('emission.main', ['emission.main.diary', url: '/log', views: { 'main-control': { - templateUrl: 'templates/recent/log.html', + template: ``, controller: 'logCtrl' } } diff --git a/www/js/recent.js b/www/js/recent.js index 5fbfbf66c..0656d3463 100644 --- a/www/js/recent.js +++ b/www/js/recent.js @@ -1,72 +1,8 @@ -angular.module('emission.main.recent', ['emission.services']) +import LogPage from './control/LogPage'; +angular.module('emission.main.recent', ['emission.services', LogPage.module]) .controller('logCtrl', function(ControlHelper, $scope, EmailHelper) { - console.log("Launching logCtr"); - var RETRIEVE_COUNT = 100; - $scope.logCtrl = {}; - - $scope.refreshEntries = function() { - window.Logger.getMaxIndex().then(function(maxIndex) { - console.log("maxIndex = "+maxIndex); - $scope.logCtrl.currentStart = maxIndex; - $scope.logCtrl.gotMaxIndex = true; - $scope.logCtrl.reachedEnd = false; - $scope.entries = []; - $scope.addEntries(); - }, function (e) { - var errStr = "While getting max index "+JSON.stringify(e, null, 2); - console.log(errStr); - alert(errStr); - }); - } - - $scope.moreDataCanBeLoaded = function() { - return $scope.logCtrl.gotMaxIndex && !($scope.logCtrl.reachedEnd); - } - - $scope.clear = function() { - window.Logger.clearAll(); - window.Logger.log(window.Logger.LEVEL_INFO, "Finished clearing entries from unified log"); - $scope.refreshEntries(); - } - - $scope.addEntries = function() { - console.log("calling addEntries"); - window.Logger.getMessagesFromIndex($scope.logCtrl.currentStart, RETRIEVE_COUNT) - .then(function(entryList) { - $scope.$apply($scope.processEntries(entryList)); - console.log("entry list size = "+$scope.entries.length); - console.log("Broadcasting infinite scroll complete"); - $scope.$broadcast('scroll.infiniteScrollComplete') - }, function(e) { - var errStr = "While getting messages from the log "+JSON.stringify(e, null, 2); - console.log(errStr); - alert(errStr); - $scope.$broadcast('scroll.infiniteScrollComplete') - } - ) - } - - $scope.processEntries = function(entryList) { - for (let i = 0; i < entryList.length; i++) { - var currEntry = entryList[i]; - currEntry.fmt_time = moment.unix(currEntry.ts).format("llll"); - $scope.entries.push(currEntry); - } - if (entryList.length == 0) { - console.log("Reached the end of the scrolling"); - $scope.logCtrl.reachedEnd = true; - } else { - $scope.logCtrl.currentStart = entryList[entryList.length-1].ID - console.log("new start index = "+$scope.logCtrl.currentStart); - } - } - - $scope.emailLog = function () { - EmailHelper.sendEmail("loggerDB"); - } - - $scope.refreshEntries(); + //can remove when we have react routing! }) .controller('sensedDataCtrl', function($scope, $ionicActionSheet, EmailHelper) { diff --git a/www/templates/recent/log.html b/www/templates/recent/log.html deleted file mode 100644 index 455294705..000000000 --- a/www/templates/recent/log.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - -
- - - -
- - -
{{entry.fmt_time}}
-
{{entry.ID}} | {{entry.level}} | {{entry.message}}
-
- -
-
-
From 7f73823b106296443380990c72c5c48ade87fe43 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 12 Sep 2023 04:17:20 -0400 Subject: [PATCH 097/164] add subtitles to all dashboard cards --- www/i18n/en.json | 13 +++++++++---- www/js/metrics/CarbonFootprintCard.tsx | 9 ++++++++- www/js/metrics/CarbonTextCard.tsx | 11 +++++++++-- www/js/metrics/DailyActiveMinutesCard.tsx | 3 ++- www/js/metrics/MetricsCard.tsx | 13 +++++++++++-- www/js/metrics/MetricsTab.tsx | 6 ++++++ www/js/metrics/WeeklyActiveMinutesCard.tsx | 3 ++- www/js/metrics/metricsHelper.ts | 3 ++- 8 files changed, 49 insertions(+), 12 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index dc498e4e9..fdd22ab37 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -153,12 +153,13 @@ "summary": "My Summary", "chart": "Chart", "change-data": "Change dates:", - "distance": "My Distance", - "trips": "My Trips", - "duration": "My Duration", + "distance": "Distance", + "trips": "Trips", + "duration": "Duration", "fav-mode": "My Favorite Mode", "speed": "My Speed", "footprint": "My Footprint", + "estimated-emissions": "Estimated CO2 emissions", "how-it-compares": "Ballpark comparisons", "optimal": "Optimal (perfect mode choice for all my trips)", "average": "Group Avg.", @@ -173,13 +174,17 @@ "calibrate": "Calibrate", "no-summary-data": "No summary data", "mean-speed": "My Average Speed", + "user-totals": "My Totals", + "group-totals": "Group Totals", "equals-cookies_one": "Equals at least {{count}} homemade chocolate chip cookie", "equals-cookies_other": "Equals at least {{count}} homemade chocolate chip cookies", "equals-icecream_one": "Equals at least {{count}} half cup vanilla ice cream", "equals-icecream_other": "Equals at least {{count}} half cups vanilla ice cream", "equals-bananas_one": "Equals at least {{count}} banana", "equals-bananas_other": "Equals at least {{count}} bananas", - "active-minutes": "My Active Minutes", + "active-minutes": "Active Minutes", + "weekly-active-minutes": "Weekly minutes of active travel", + "daily-active-minutes": "Daily minutes of active travel", "labeled": "Labeled", "unlabeled": "Unlabeled²", "footprint-label": "Footprint (kg Co2)" diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index b79bf9211..2f1417d3a 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -119,6 +119,12 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { return tempChartData; }, [userCarbonRecords, groupCarbonRecords]); + const cardSubtitleText = useMemo(() => { + const recentEntries = filterToRecentWeeks(aggMetrics?.distance).reverse().flat(); + const recentEntriesRange = formatDateRangeOfDays(recentEntries); + return `${t('main-metrics.estimated-emissions')}, (${recentEntriesRange})`; + }, [aggMetrics?.distance]); + //hardcoded here, could be read from config at later customization? let carbonGoals = [ {label: t('main-metrics.us-2030-goal'), value: 54, color: color(colors.danger).darken(.25).rgb().toString()}, {label: t('main-metrics.us-2050-goal'), value: 14, color: color(colors.warn).darken(.25).rgb().toString()}]; @@ -131,7 +137,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { title={t('main-metrics.footprint')} titleVariant='titleLarge' titleStyle={cardStyles.titleText(colors)} - titleNumberOfLines={2} + subtitle={cardSubtitleText} + subtitleStyle={[cardStyles.titleText(colors), cardStyles.subtitleText]} right={(props) => } style={cardStyles.title(colors)} /> diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index 78b664aa1..54ed9524a 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -105,7 +105,13 @@ const DailyActiveMinutesCard = ({ userMetrics, aggMetrics }: Props) => { tempText = tempText.concat(groupText); } return tempText; -}, [userText, groupText]); + }, [userText, groupText]); + + const cardSubtitleText = useMemo(() => { + const recentEntries = filterToRecentWeeks(aggMetrics?.distance).reverse().flat(); + const recentEntriesRange = formatDateRangeOfDays(recentEntries); + return `${t('main-metrics.estimated-emissions')}, (${recentEntriesRange})`; + }, [aggMetrics?.distance]); return ( { title={t('main-metrics.footprint')} titleVariant='titleLarge' titleStyle={cardStyles.titleText(colors)} - titleNumberOfLines={2} + subtitle={cardSubtitleText} + subtitleStyle={[cardStyles.titleText(colors), cardStyles.subtitleText]} style={cardStyles.title(colors)} /> { textEntries?.length > 0 && diff --git a/www/js/metrics/DailyActiveMinutesCard.tsx b/www/js/metrics/DailyActiveMinutesCard.tsx index 4dabf8b39..0da88dca4 100644 --- a/www/js/metrics/DailyActiveMinutesCard.tsx +++ b/www/js/metrics/DailyActiveMinutesCard.tsx @@ -41,7 +41,8 @@ const DailyActiveMinutesCard = ({ userMetrics }: Props) => { title={t('main-metrics.active-minutes')} titleVariant='titleLarge' titleStyle={cardStyles.titleText(colors)} - titleNumberOfLines={2} + subtitle={t('main-metrics.daily-active-minutes')} + subtitleStyle={[cardStyles.titleText(colors), cardStyles.subtitleText]} style={cardStyles.title(colors)} /> { dailyActiveMinutesRecords.length ? diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index 8a6f7d002..fa8470ddb 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -4,11 +4,12 @@ import { View } from 'react-native'; import { Card, Checkbox, Text, useTheme } from 'react-native-paper'; import BarChart from '../components/BarChart'; import { DayOfMetricData } from './metricsTypes'; -import { getLabelsForDay, getUniqueLabelsForDays } from './metricsHelper'; +import { formatDateRangeOfDays, getLabelsForDay, getUniqueLabelsForDays } from './metricsHelper'; import ToggleSwitch from '../components/ToggleSwitch'; import { cardStyles } from './MetricsTab'; import { labelKeyToReadable, labelOptions } from '../survey/multilabel/confirmHelper'; import { getBaseModeByReadableLabel } from '../diary/diaryHelper'; +import { useTranslation } from 'react-i18next'; type Props = { cardTitle: string, @@ -20,6 +21,7 @@ type Props = { const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, unitFormatFn}: Props) => { const { colors } = useTheme(); + const { t } = useTranslation(); const [viewMode, setViewMode] = useState<'details'|'graph'>('details'); const [populationMode, setPopulationMode] = useState<'user'|'aggregate'>('user'); const [graphIsStacked, setGraphIsStacked] = useState(true); @@ -45,6 +47,12 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni return records; }, [metricDataDays, viewMode]); + const cardSubtitleText = useMemo(() => { + const groupText = populationMode == 'user' ? t('main-metrics.user-totals') + : t('main-metrics.group-totals'); + return `${groupText} (${formatDateRangeOfDays(metricDataDays)})`; + }, [metricDataDays, populationMode]); + // for each label, sum up cumulative values across all days const metricSumValues = useMemo(() => { if (!metricDataDays || viewMode != 'details') return []; @@ -67,7 +75,8 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni title={cardTitle} titleVariant='titleLarge' titleStyle={cardStyles.titleText(colors)} - titleNumberOfLines={2} + subtitle={cardSubtitleText} + subtitleStyle={[cardStyles.titleText(colors), cardStyles.subtitleText]} right={() => setViewMode(v as any)} diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 975ceb29a..e2d4aa811 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -138,6 +138,12 @@ export const cardStyles: any = { fontWeight: '500', textAlign: 'center' }), + subtitleText: { + fontSize: 13, + lineHeight: 13, + fontWeight: '400', + fontStyle: 'italic', + }, content: { padding: 8, paddingBottom: 12, diff --git a/www/js/metrics/WeeklyActiveMinutesCard.tsx b/www/js/metrics/WeeklyActiveMinutesCard.tsx index 8770e0a5c..f0f8c1c84 100644 --- a/www/js/metrics/WeeklyActiveMinutesCard.tsx +++ b/www/js/metrics/WeeklyActiveMinutesCard.tsx @@ -60,7 +60,8 @@ const WeeklyActiveMinutesCard = ({ userMetrics }: Props) => { title={t('main-metrics.active-minutes')} titleVariant='titleLarge' titleStyle={cardStyles.titleText(colors)} - titleNumberOfLines={2} + subtitle={t('main-metrics.weekly-active-minutes')} + subtitleStyle={[cardStyles.titleText(colors), cardStyles.subtitleText]} style={cardStyles.title(colors)} /> { weeklyActiveMinutesRecords.length ? diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index 2703be19b..015232bb6 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -42,6 +42,7 @@ export function filterToRecentWeeks(metricsDays: DayOfMetricData[]) { } export function formatDateRangeOfDays(days: DayOfMetricData[]) { + if (!days?.length) return ''; const firstDayDt = DateTime.fromISO(days[0].fmt_time, { zone: 'utc' }); const lastDayDt = DateTime.fromISO(days[days.length - 1].fmt_time, { zone: 'utc' }); const firstDay = firstDayDt.toLocaleString({...DateTime.DATE_SHORT, year: undefined}); @@ -161,4 +162,4 @@ export function generateSummaryFromData(modeMap, metric) { } return summaryMap; -} \ No newline at end of file +} From ca8edbe5e9b7b3824f44b943a80a968099c1979a Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 12 Sep 2023 04:27:47 -0400 Subject: [PATCH 098/164] =?UTF-8?q?change=20"Co2"=20to=20"CO=E2=82=82"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- www/i18n/en.json | 4 ++-- www/js/metrics/CarbonTextCard.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index fdd22ab37..c90bbfbd1 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -159,7 +159,7 @@ "fav-mode": "My Favorite Mode", "speed": "My Speed", "footprint": "My Footprint", - "estimated-emissions": "Estimated CO2 emissions", + "estimated-emissions": "Estimated CO₂ emissions", "how-it-compares": "Ballpark comparisons", "optimal": "Optimal (perfect mode choice for all my trips)", "average": "Group Avg.", @@ -187,7 +187,7 @@ "daily-active-minutes": "Daily minutes of active travel", "labeled": "Labeled", "unlabeled": "Unlabeled²", - "footprint-label": "Footprint (kg Co2)" + "footprint-label": "Footprint (kg CO₂)" }, "main-inf-scroll" : { diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index 54ed9524a..2c6ea10a0 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -128,7 +128,7 @@ const DailyActiveMinutesCard = ({ userMetrics, aggMetrics }: Props) => { Object.keys(textEntries).map((i) => {textEntries[i].label} - {textEntries[i].value + ' ' + "kg Co2"} + {textEntries[i].value + ' ' + "kg CO₂"} ) } From 6a353f87135ba4eecd9e2fb04919539d72689289 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 12 Sep 2023 05:46:52 -0400 Subject: [PATCH 099/164] fix name of CarbonTextCard --- www/js/metrics/CarbonTextCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index 2c6ea10a0..6d6d93838 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -8,7 +8,7 @@ import { filterToRecentWeeks, formatDateRangeOfDays, parseDataFromMetrics, gener import { getAngularService } from '../angular-react-helper'; type Props = { userMetrics: MetricsData, aggMetrics: MetricsData } -const DailyActiveMinutesCard = ({ userMetrics, aggMetrics }: Props) => { +const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { const { colors } = useTheme(); const { t } = useTranslation(); @@ -142,4 +142,4 @@ const DailyActiveMinutesCard = ({ userMetrics, aggMetrics }: Props) => { ) } -export default DailyActiveMinutesCard; +export default CarbonTextCard; From 62ea9f40200d7eeef0d0cc2b1baae80415a35d92 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 12 Sep 2023 05:51:47 -0400 Subject: [PATCH 100/164] add ActiveMinutesTableCard The new dashboard has a lot of charts. In consideration of accessibility, we want to also display this information in non-graphical text formats. This cards shows all the metrics for active minutes in the form of a paginated table. --- www/i18n/en.json | 1 + www/js/metrics/ActiveMinutesTableCard.tsx | 99 ++++++++++++++++++++++ www/js/metrics/MetricsTab.tsx | 12 +-- www/js/metrics/WeeklyActiveMinutesCard.tsx | 32 +------ www/js/metrics/metricsHelper.ts | 5 ++ 5 files changed, 114 insertions(+), 35 deletions(-) create mode 100644 www/js/metrics/ActiveMinutesTableCard.tsx diff --git a/www/i18n/en.json b/www/i18n/en.json index c90bbfbd1..a5c31c5c7 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -185,6 +185,7 @@ "active-minutes": "Active Minutes", "weekly-active-minutes": "Weekly minutes of active travel", "daily-active-minutes": "Daily minutes of active travel", + "active-minutes-table": "Table of active minutes metrics", "labeled": "Labeled", "unlabeled": "Unlabeled²", "footprint-label": "Footprint (kg CO₂)" diff --git a/www/js/metrics/ActiveMinutesTableCard.tsx b/www/js/metrics/ActiveMinutesTableCard.tsx new file mode 100644 index 000000000..b5f1333f7 --- /dev/null +++ b/www/js/metrics/ActiveMinutesTableCard.tsx @@ -0,0 +1,99 @@ +import React, { useMemo, useState } from 'react'; +import { Card, DataTable, useTheme } from 'react-native-paper'; +import { MetricsData } from './metricsTypes'; +import { cardStyles } from './MetricsTab'; +import { filterToRecentWeeks, formatDate, formatDateRangeOfDays, secondsToMinutes } from './metricsHelper'; +import { useTranslation } from 'react-i18next'; +import { ACTIVE_MODES } from './WeeklyActiveMinutesCard'; +import { labelKeyToReadable } from '../survey/multilabel/confirmHelper'; + +type Props = { userMetrics: MetricsData } +const ActiveMinutesTableCard = ({ userMetrics }: Props) => { + + const { colors } = useTheme(); + const { t } = useTranslation(); + + const cumulativeTotals = useMemo(() => { + if (!userMetrics?.duration) return []; + const totals = {}; + ACTIVE_MODES.forEach(mode => { + const sum = userMetrics.duration.reduce((acc, day) => ( + acc + (day[`label_${mode}`] || 0) + ), 0); + totals[mode] = secondsToMinutes(sum); + }); + totals['period'] = formatDateRangeOfDays(userMetrics.duration); + return totals; + }, [userMetrics?.duration]); + + const recentWeeksActiveModesTotals = useMemo(() => { + if (!userMetrics?.duration) return []; + return filterToRecentWeeks(userMetrics.duration).map(week => { + const totals = {}; + ACTIVE_MODES.forEach(mode => { + const sum = week.reduce((acc, day) => ( + acc + (day[`label_${mode}`] || 0) + ), 0); + totals[mode] = secondsToMinutes(sum); + }) + totals['period'] = formatDateRangeOfDays(week); + return totals; + }); + }, [userMetrics?.duration]); + + const dailyActiveModesTotals = useMemo(() => { + if (!userMetrics?.duration) return []; + return userMetrics.duration.map(day => { + const totals = {}; + ACTIVE_MODES.forEach(mode => { + const sum = day[`label_${mode}`] || 0; + totals[mode] = secondsToMinutes(sum); + }) + totals['period'] = formatDate(day); + return totals; + }).reverse(); + }, [userMetrics?.duration]); + + const allTotals = [cumulativeTotals, ...recentWeeksActiveModesTotals, ...dailyActiveModesTotals]; + + const itemsPerPage = 5; + const [page, setPage] = useState(0); + const from = page * itemsPerPage; + const to = Math.min((page + 1) * itemsPerPage, allTotals.length); + + return ( + + + + + + + {ACTIVE_MODES.map((mode, i) => + {labelKeyToReadable(mode)} + )} + + {allTotals.slice(from, to).map((total, i) => + + {total['period']} + {ACTIVE_MODES.map((mode, j) => + {total[mode]} {t('metrics.minutes')} + )} + + )} + setPage(p)} + numberOfPages={Math.ceil(allTotals.length / 5)} numberOfItemsPerPage={5} + label={`${page * 5 + 1}-${page * 5 + 5} of ${allTotals.length}`} /> + + + + ) +} + +export default ActiveMinutesTableCard; diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index e2d4aa811..f335a952b 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -15,6 +15,7 @@ import CarbonFootprintCard from "./CarbonFootprintCard"; import Carousel from "../components/Carousel"; import DailyActiveMinutesCard from "./DailyActiveMinutesCard"; import CarbonTextCard from "./CarbonTextCard"; +import ActiveMinutesTableCard from "./ActiveMinutesTableCard"; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; @@ -71,11 +72,11 @@ const MetricsTab = () => { // TODO } - // fake data for testing active minutes - TODO: remove - userMetrics?.duration.forEach((day, i) => { - day.label_walk = day.label_walk || 65*i + (Math.random() * 600); - day.label_bike = day.label_bike || 25*i + (Math.random() * 400); - }); + // // fake data for testing active minutes - TODO: remove + // userMetrics?.duration.forEach((day, i) => { + // day.label_walk = day.label_walk || 65*i + (Math.random() * 600); + // day.label_bike = day.label_bike || 25*i + (Math.random() * 400); + // }); const { width: windowWidth } = useWindowDimensions(); const cardWidth = windowWidth * .88; @@ -94,6 +95,7 @@ const MetricsTab = () => { + { const { colors } = useTheme(); const { t } = useTranslation(); - - // number of minutes for each of [walk, bike] - const activeModesDurations = useMemo(() => { - if (!userMetrics?.duration) return []; - return ACTIVE_MODES.map(mode => { - const sum = userMetrics.duration.reduce((acc, day) => ( - acc + (day[`label_${mode}`] || 0) - ), 0); - return secondsToMinutes(sum); - }); - }, [userMetrics?.duration]); + const weeklyActiveMinutesRecords = useMemo(() => { const records = []; @@ -77,24 +67,6 @@ const WeeklyActiveMinutesCard = ({ userMetrics }: Props) => {
} - {activeModesDurations?.length > 0 && - - - {`Overall for ${formatDateRangeOfDays(userMetrics.duration)}:`} - - - {activeModesDurations.map((mode, i) => - - {labelKeyToReadable(ACTIVE_MODES[i])} - {' - '} - - {`${mode} ${t('metrics.minutes')}`} - - - )} - - - }
) diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index 015232bb6..52feedfec 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -41,6 +41,11 @@ export function filterToRecentWeeks(metricsDays: DayOfMetricData[]) { return weeks; } +export function formatDate(day: DayOfMetricData) { + const dt = DateTime.fromISO(day.fmt_time, { zone: 'utc' }); + return dt.toLocaleString({...DateTime.DATE_SHORT, year: undefined}); +} + export function formatDateRangeOfDays(days: DayOfMetricData[]) { if (!days?.length) return ''; const firstDayDt = DateTime.fromISO(days[0].fmt_time, { zone: 'utc' }); From 17f401c9a15caedc1b9bafe8f8454117a04cf548 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 12 Sep 2023 05:58:18 -0400 Subject: [PATCH 101/164] remove the old Dashboard tab --- www/index.js | 1 - www/js/main.js | 5 +- www/js/metrics.js | 1448 ------------------------------- www/templates/main-metrics.html | 207 ----- 4 files changed, 2 insertions(+), 1659 deletions(-) delete mode 100644 www/js/metrics.js delete mode 100644 www/templates/main-metrics.html diff --git a/www/index.js b/www/index.js index 66a0d45df..880f522f5 100644 --- a/www/index.js +++ b/www/index.js @@ -40,7 +40,6 @@ import './js/survey/enketo/infinite_scroll_filters.js'; import './js/survey/enketo/enketo-trip-button.js'; import './js/survey/enketo/enketo-demographics.js'; import './js/survey/enketo/enketo-add-note-button.js'; -import './js/metrics.js'; import './js/control/general-settings.js'; import './js/control/emailService.js'; import './js/control/uploadService.js'; diff --git a/www/js/main.js b/www/js/main.js index e7562d899..a1d774f82 100644 --- a/www/js/main.js +++ b/www/js/main.js @@ -6,7 +6,8 @@ import MetricsTab from './metrics/MetricsTab'; angular.module('emission.main', ['emission.main.diary', 'emission.main.control', - 'emission.main.metrics', + 'emission.main.metrics.factory', + 'emission.main.metrics.mappings', 'emission.main.recent', 'emission.config.dynamic', 'emission.services', @@ -27,9 +28,7 @@ angular.module('emission.main', ['emission.main.diary', url: '/metrics', views: { 'main-metrics': { - // templateUrl: 'templates/main-metrics.html', template: ``, - controller: 'MetricsCtrl' } } }) diff --git a/www/js/metrics.js b/www/js/metrics.js deleted file mode 100644 index 9b6522fd7..000000000 --- a/www/js/metrics.js +++ /dev/null @@ -1,1448 +0,0 @@ -'use strict'; - -import angular from 'angular'; -import 'nvd3'; -import BarChart from './components/BarChart'; -angular.module('emission.main.metrics',['emission.services', - 'ionic-datepicker', - 'emission.config.imperial', - 'emission.main.metrics.factory', - 'emission.main.metrics.mappings', - 'emission.stats.clientstats', - 'emission.plugin.kvstore', - 'emission.plugin.logger', - BarChart.module]) - -.controller('MetricsCtrl', function($scope, $ionicActionSheet, $ionicLoading, - ClientStats, CommHelper, $window, $ionicPopup, - ionicDatePicker, $ionicPlatform, - FootprintHelper, CalorieCal, ImperialConfig, $ionicModal, $timeout, KVStore, CarbonDatasetHelper, - $rootScope, $location, $state, ReferHelper, Logger) { - var lastTwoWeeksQuery = true; - $scope.defaultTwoWeekUserCall = true; - - var DURATION = "duration"; - var MEAN_SPEED = "mean_speed"; - var COUNT = "count"; - var DISTANCE = "distance"; - - var METRIC_LIST = [DURATION, MEAN_SPEED, COUNT, DISTANCE]; - var COMPUTATIONAL_METRIC_LIST = [DURATION, MEAN_SPEED, DISTANCE]; - - /* - * BEGIN: Data structures to parse and store the data in different formats. - * So that we don't have to keep re-creating them over and over as we used - * to, slowing down the processing. - */ - - /* - These are metric maps, with the key as the metric, and the value as a - list of ModeStatTimeSummary objects for the metric. - i.e. {count: [ - {fmt_time: "2021-12-03T00:00:00+00:00", - label_drove_alone: 4 label_walk: 1 - local_dt: {year: 2021, month: 12, day: 3, hour: 0, minute: 0, …} - nUsers: 1 - ts: 1638489600},....], - duration: [...] - distance: [...] - mean_speed: [...]} - */ - $scope.userCurrentResults = {}; - $scope.userTwoWeeksAgo = {}; - $scope.aggCurrentResults = {}; - - /* - These are metric mode maps, with a nested map of maps. The outer key is - the metric, and the inner key is the , with the key as the metric, and the - inner key is the mode. The innermost value is the list of - ModeStatTimeSummary objects for that mode. - list of ModeStatTimeSummary objects for the metric. - i.e. { - count: [ - {key: drove_alone, values: : [[1638489600, 4, "2021-12-03T00:00:00+00:00"], ...]}, - { key: walk, values: [[1638489600, 4, "2021-12-03T00:00:00+00:00"],...]}], - duration: [ { key: drove_alone, values: [...]}, {key: walk, values: [...]} ], - distance: [ { key: drove_alone, values: [...]}, {key: walk, values: [...]} ], - mean_speed: [ { key: drove_alone, values: [...]}, {key: walk, values: [...]} ] - } - */ - $scope.userCurrentModeMap = {}; - $scope.userTwoWeeksAgoModeMap = {}; - $scope.userCurrentModeMapFormatted = {}; - $scope.aggCurrentModeMap = {}; - $scope.aggCurrentModeMapFormatted = {}; - $scope.aggCurrentPerCapitaModeMap = {}; - - /* - These are summary mode maps, which have the same structure as the mode - maps, but with a value that is a single array instead of an array of arrays. - The single array is the summation of the values in the individual arrays of the non-summary mode maps. - i.e. { - count: [{key: "drove_alone", values: [10, "trips", "10 trips"], - {key: "walk", values: [5, "trips", "5 trips"]}], - - duration: [ { key: drove_alone, values: [...]}, {key: walk, values: [...]} ], - distance: [ { key: drove_alone, values: [...]}, {key: walk, values: [...]} ], - mean_speed: [ { key: drove_alone, values: [...]}, {key: walk, values: [...]} ] - } - */ - $scope.userCurrentSummaryModeMap = {}; - $scope.userTwoWeeksAgoSummaryModeMap = {}; - $scope.aggCurrentSummaryModeMap = {}; - $scope.aggCurrentSummaryPerCapitaModeMap = {}; - - /* - $scope.onCurrentTrip = function() { - window.cordova.plugins.BEMDataCollection.getState().then(function(result) { - Logger.log("Current trip state" + JSON.stringify(result)); - if(JSON.stringify(result) == "\"STATE_ONGOING_TRIP\""|| - JSON.stringify(result) == "\"local.state.ongoing_trip\"") { - $state.go("root.main.current"); - } - }); - }; - */ - - $ionicPlatform.ready(function() { - CarbonDatasetHelper.loadCarbonDatasetLocale().then(function(result) { - getData(); - }); - // $scope.onCurrentTrip(); - }); - - // If we want to share this function (see the pun?) between the control screen and the dashboard, we need to put it into a service/factory. - // But it is not clear to me why it needs to be in the profile screen... - var prepopulateMessage = { - message: 'Have fun, support research and get active. Your privacy is protected. \nDownload the emission app:', // not supported on some apps (Facebook, Instagram) - subject: 'Help Berkeley become more bikeable and walkable', // fi. for email - url: 'https://bic2cal.eecs.berkeley.edu/#download' - } - - $scope.share = function() { - window.plugins.socialsharing.shareWithOptions(prepopulateMessage, function(result) { - console.log("Share completed? " + result.completed); // On Android apps mostly return false even while it's true - console.log("Shared to app: " + result.app); // On Android result.app is currently empty. On iOS it's empty when sharing is cancelled (result.completed=false) - }, function(msg) { - console.log("Sharing failed with message: " + msg); - }); - } - - // TODO: Move this out into its own service - var FOOD_COMPARE_KEY = 'foodCompare'; - $scope.setCookie = function(){ - $scope.foodCompare = 'cookie'; - return KVStore.set(FOOD_COMPARE_KEY, 'cookie'); - } - $scope.setIceCream = function(){ - $scope.foodCompare = 'iceCream'; - return KVStore.set(FOOD_COMPARE_KEY, 'iceCream'); - } - $scope.setBanana = function(){ - $scope.foodCompare = 'banana'; - return KVStore.set(FOOD_COMPARE_KEY, 'banana'); - } - $scope.handleChosenFood = function(retVal) { - if (retVal == null){ - $scope.setCookie(); - } else { - var choosenFood = retVal; - if(choosenFood == 'cookie') - $scope.setCookie(); - else if (choosenFood == 'iceCream') - $scope.setIceCream(); - else - $scope.setBanana(); - } - } - $ionicModal.fromTemplateUrl('templates/metrics/metrics-control.html', { - scope: $scope, - animation: 'slide-in-up' - }).then(function(modal) { - $scope.modal = modal; - }); - $scope.openModal = function(){ - $scope.modal.show(); - } - $scope.closeModal = function(){ - $scope.modal.hide(); - } - $scope.uictrl = { - showRange: true, - showFilter: false, - showVis: true, - showResult: true, - current: true, - currentString: i18next.t('metrics.last-week'), - showChart: false, - showSummary: true, - showMe: true, - showAggr: false, - showContent: false, - showTrips: false, - showDuration: false, - showDistance: false, - showSpeed: false, - } - $scope.showChart = function() { - $scope.uictrl.showSummary = false; - $scope.uictrl.showChart = true; - $scope.showDistance(); - } - $scope.showDistance = function() { - $scope.uictrl.showTrips = false; - $scope.uictrl.showDuration = false; - $scope.uictrl.showSpeed = false; - $scope.uictrl.showDistance = true; - } - $scope.showTrips = function() { - $scope.uictrl.showDistance = false; - $scope.uictrl.showSpeed = false; - $scope.uictrl.showDuration = false; - $scope.uictrl.showTrips = true; - } - $scope.showDuration = function() { - $scope.uictrl.showSpeed = false; - $scope.uictrl.showDistance = false; - $scope.uictrl.showTrips = false; - $scope.uictrl.showDuration = true; - } - $scope.showSpeed = function() { - $scope.uictrl.showTrips = false; - $scope.uictrl.showDuration = false; - $scope.uictrl.showDistance = false; - $scope.uictrl.showSpeed = true; - } - $scope.showSummary = function() { - $scope.uictrl.showChart = false; - $scope.uictrl.showSummary = true; - } - $scope.chartButtonClass = function() { - return $scope.uictrl.showChart? "metric-chart-button-active hvcenter" : "metric-chart-button hvcenter"; - } - $scope.summaryButtonClass = function() { - return $scope.uictrl.showSummary? "metric-summary-button-active hvcenter" : "metric-summary-button hvcenter"; - } - $scope.distanceButtonClass = function() { - return $scope.uictrl.showDistance? "distance-button-active hvcenter" : "distance-button hvcenter"; - } - $scope.tripsButtonClass = function() { - return $scope.uictrl.showTrips? "trips-button-active hvcenter" : "trips-button hvcenter"; - } - $scope.durationButtonClass = function() { - return $scope.uictrl.showDuration? "duration-button-active hvcenter" : "duration-button hvcenter"; - } - $scope.speedButtonClass = function() { - return $scope.uictrl.showSpeed? "speed-button-active hvcenter" : "speed-button hvcenter"; - } - $scope.rangeButtonClass = function() { - return $scope.uictrl.showRange? "metric-range-button-active hvcenter" : "metric-range-button hvcenter"; - } - $scope.filterButtonClass = function() { - return $scope.uictrl.showFilter? "metric-filter-button-active hvcenter" : "metric-filter-button hvcenter"; - } - $scope.getButtonClass = function() { - return ($scope.uictrl.showFilter || $scope.uictrl.showRange)? "metric-get-button hvcenter" : "metric-get-button-inactive hvcenter"; - } - $scope.fullToggleLeftClass = function() { - return $scope.userData.gender == 1? "full-toggle-left-active hvcenter" : "full-toggle-left hvcenter"; - } - $scope.fullToggleRightClass = function() { - return $scope.userData.gender == 0? "full-toggle-right-active hvcenter" : "full-toggle-right hvcenter"; - } - $scope.fullToggleLeftClass1 = function() { - return $scope.showca2020? "full-toggle-left-active hvcenter" : "full-toggle-left hvcenter"; - } - $scope.fullToggleRightClass1 = function() { - return $scope.showca2035? "full-toggle-right-active hvcenter" : "full-toggle-right hvcenter"; - } - $scope.heightToggleLeftClass = function() { - return $scope.userData.heightUnit == 1? "unit-toggle-left-active hvcenter" : "unit-toggle-left hvcenter"; - } - $scope.heightToggleRightClass = function() { - return $scope.userData.heightUnit == 0? "unit-toggle-right-active hvcenter" : "unit-toggle-right hvcenter"; - } - $scope.weightToggleLeftClass = function() { - return $scope.userData.weightUnit == 1? "unit-toggle-left-active hvcenter" : "unit-toggle-left hvcenter"; - } - $scope.weightToggleRightClass = function() { - return $scope.userData.weightUnit == 0? "unit-toggle-right-active hvcenter" : "unit-toggle-right hvcenter"; - } - $scope.currentQueryForCalorie = function() { - return $scope.uictrl.current ? "user-calorie-percentage" : "user-calorie-no-percentage"; - } - $scope.currentQueryForCarbon = function() { - return $scope.uictrl.current ? "user-carbon-percentage" : "user-carbon-no-percentage"; - } - $scope.showRange = function() { - if ($scope.uictrl.showFilter) { - $scope.uictrl.showFilter = false; - $scope.uictrl.showRange = true; - } - } - $scope.showFilter = function() { - if ($scope.uictrl.showRange) { - $scope.uictrl.showRange = false; - $scope.uictrl.showFilter = true; - } - } - - $scope.setHeightUnit = function(heightUnit) { - // 1 for cm, 0 for ft - $scope.userData.heightUnit = heightUnit; - } - $scope.setWeightUnit = function(weightUnit) { - // 1 for kg, 0 for lb - $scope.userData.weightUnit = weightUnit; - } - $scope.setGender = function(gender) { - $scope.userData.gender = gender; - } - - $scope.storeUserData = function() { - var info = {'gender': $scope.userData.gender, - 'heightUnit': $scope.userData.heightUnit, - 'weightUnit': $scope.userData.weightUnit, - 'height': $scope.userData.height, - 'weight': $scope.userData.weight, - 'age': $scope.userData.age, - 'userDataSaved': true}; - CalorieCal.set(info).then(function() { - $scope.savedUserData = info; - }); - } - - $scope.loadUserData = function() { - if(angular.isDefined($scope.savedUserData)) { - // loaded or set - return Promise.resolve(); - } else { - return CalorieCal.get().then(function(userDataFromStorage) { - $scope.savedUserData = userDataFromStorage; - }); - } - } - - $scope.userDataSaved = function() { - // console.log("saved vals = "+JSON.stringify($scope.savedUserData)); - if (angular.isDefined($scope.savedUserData) && $scope.savedUserData != null) { - return $scope.savedUserData.userDataSaved == true; - } else { - return false; - }; - } - - $scope.options = { - chart: { - type: 'multiBarChart', - width: $window.screen.width - 30, - height: $window.screen.height - 220, - margin : { - top: 20, - right: 20, - bottom: 40, - left: 55 - }, - noData: i18next.t('metrics.chart-no-data'), - showControls: false, - showValues: true, - stacked: false, - x: function(d){ return d[0]; }, - y: function(d){ return d[1]; }, - /* - average: function(d) { - var vals = d.values.map(function(item){ - return item[1]; - }); - return d3.mean(vals); - }, - */ - - color: d3.scale.category10().range(), - // duration: 300, - useInteractiveGuideline: false, - // clipVoronoi: false, - - xAxis: { - axisLabelDistance: 3, - axisLabel: i18next.t('metrics.chart-xaxis-date'), - tickFormat: function(d) { - var day = new Date(d * 1000) - day.setDate(day.getDate()+1) // Had to add a day to match date with data - return d3.time.format('%y-%m-%d')(day) - }, - showMaxMin: false, - staggerLabels: true - }, - yAxis: { - axisLabel: i18next.t('metrics.trips-yaxis-number'), - axisLabelDistance: -10 - }, - callback: function(chart) { - chart.multibar.dispatch.on('elementClick', function(bar) { - var date = bar.data[2].slice(0,10); - $rootScope.barDetailDate = moment(date); - $rootScope.barDetail = true; - $state.go('root.main.diary'); - console.log($rootScope.barDetailDate); - }) - } - } - }; - - var moment2Localdate = function(momentObj) { - return { - year: momentObj.year(), - month: momentObj.month() + 1, - day: momentObj.date(), - }; - } - var moment2Timestamp = function(momentObj) { - return momentObj.unix(); - } - - $scope.data = []; - - var getData = function(){ - $scope.getMetricsHelper(); - $scope.loadUserData(); - KVStore.get(FOOD_COMPARE_KEY).then(function(retVal) { - $scope.handleChosenFood(retVal); - }); - } - - $scope.getMetricsHelper = function() { - $scope.uictrl.showContent = false; - setMetricsHelper(getMetrics); - } - - var setMetricsHelper = function(metricsToGet) { - if ($scope.uictrl.showRange) { - setMetrics('timestamp', metricsToGet); - } else if ($scope.uictrl.showFilter) { - setMetrics('local_date', metricsToGet); - } else { - console.log("Illegal time_type"); // Notice that you need to set query - } - if(angular.isDefined($scope.modal) && $scope.modal.isShown()){ - $scope.modal.hide(); - } - } - - var data = {} - var theMode = ""; - - var setMetrics = function(mode, callback) { - theMode = mode; - if (['local_date', 'timestamp'].indexOf(mode) == -1) { - console.log('Illegal time_type'); - return; - } - - if (mode === 'local_date') { // local_date filter - var tempFrom = $scope.selectCtrl.fromDateLocalDate; - tempFrom.weekday = $scope.selectCtrl.fromDateWeekdayValue; - var tempTo = $scope.selectCtrl.toDateLocalDate; - tempTo.weekday = $scope.selectCtrl.toDateWeekdayValue; - data = { - freq: $scope.selectCtrl.freq, - start_time: tempFrom, - end_time: tempTo, - metric: "" - }; - } else if (mode === 'timestamp') { // timestamp range - if(lastTwoWeeksQuery) { - var tempFrom = moment2Timestamp(moment().utc().startOf('day').subtract(14, 'days')); - var tempTo = moment2Timestamp(moment().utc().startOf('day').subtract(1, 'days')) - lastTwoWeeksQuery = false; // Only get last week's data once - $scope.defaultTwoWeekUserCall = true; - } else { - var tempFrom = moment2Timestamp($scope.selectCtrl.fromDateTimestamp); - var tempTo = moment2Timestamp($scope.selectCtrl.toDateTimestamp); - $scope.defaultTwoWeekUserCall = false; - } - data = { - freq: $scope.selectCtrl.pandaFreq, - start_time: tempFrom, - end_time: tempTo, - metric: "" - }; - } else { - console.log('Illegal mode'); - return; - } - console.log("Sending data "+JSON.stringify(data)); - callback() - }; - - var getUserMetricsFromServer = function() { - var clonedData = angular.copy(data); - delete clonedData.metric; - clonedData.metric_list = METRIC_LIST; - clonedData.is_return_aggregate = false; - var getMetricsResult = CommHelper.getMetrics(theMode, clonedData); - return getMetricsResult; - } - var getAggMetricsFromServer = function() { - var clonedData = angular.copy(data); - delete clonedData.metric; - clonedData.metric_list = METRIC_LIST; - clonedData.is_return_aggregate = true; - var getMetricsResult = CommHelper.getAggregateData( - "result/metrics/timestamp", clonedData) - return getMetricsResult; - } - - var isValidNumber = function(number) { - if (angular.isDefined(Number.isFinite)) { - return Number.isFinite(number); - } else { - return !isNaN(number); - } - } - - var getMetrics = function() { - $ionicLoading.show({ - template: i18next.t('loading') - }); - if(!$scope.defaultTwoWeekUserCall){ - $scope.uictrl.currentString = i18next.t('metrics.custom'); - $scope.uictrl.current = false; - } - //$scope.uictrl.showRange = false; - //$scope.uictrl.showFilter = false; - $scope.uictrl.showVis = true; - $scope.uictrl.showResult = true; - $scope.uictrl.hasAggr = false; - - $scope.caloriesData = {}; - $scope.carbonData = {}; - $scope.summaryData = {}; - - $scope.carbonData.optimalCarbon = "0 kg CO₂"; - - $scope.summaryData.userSummary = []; - $scope.chartDataUser = {}; - $scope.chartDataAggr = {}; - $scope.food = { - 'chocolateChip' : 78, //16g 1 cookie - 'vanillaIceCream' : 137, //1/2 cup - 'banana' : 105, //medium banana 118g - }; - // calculation is at - // https://github.com/e-mission/e-mission-docs/issues/696#issuecomment-1018181638 - $scope.carbon = { - 'phoneCharge' : 8.22 * 10**(-3), // 8.22 x 10^-6 metric tons CO2/smartphone charge - }; - - getUserMetricsFromServer().then(function(results) { - $ionicLoading.hide(); - console.log("user results ", results); - if(results.user_metrics.length == 1){ - console.log("$scope.defaultTwoWeekUserCall = "+$scope.defaultTwoWeekUserCall); - $scope.defaultTwoWeekUserCall = false; - // If there is no data from last week (ex. new user) - // Don't store the any other data as last we data - } - $scope.fillUserValues(results.user_metrics); - $scope.summaryData.defaultSummary = $scope.summaryData.userSummary; - $scope.defaultTwoWeekUserCall = false; //If there is data from last week store the data only first time - $scope.uictrl.showContent = true; - if (angular.isDefined($scope.chartDataUser)) { - $scope.$apply(function() { - if ($scope.uictrl.showMe) { - $scope.showCharts($scope.chartDataUser); - } - }) - } else { - $scope.$apply(function() { - $scope.showCharts([]); - console.log("did not find aggregate result in response data "+JSON.stringify(results[2])); - }); - } - }) - .catch(function(error) { - $ionicLoading.hide(); - Logger.displayError("Error loading user data", error); - }) - - getAggMetricsFromServer().then(function(results) { - console.log("aggregate results ", results); - $scope.fillAggregateValues(results.aggregate_metrics); - $scope.uictrl.hasAggr = true; - if (angular.isDefined($scope.chartDataAggr)) { //Only have to check one because - // Restore the $apply if/when we go away from $http - $scope.$apply(function() { - if (!$scope.uictrl.showMe) { - $scope.showCharts($scope.chartDataAggr); - } - }) - } else { - $scope.$apply(function() { - $scope.showCharts([]); - console.log("did not find aggregate result in response data "+JSON.stringify(results[2])); - }); - } - }) - .catch(function(error) { - $ionicLoading.hide(); - $scope.carbonData.aggrCarbon = i18next.t('metrics.carbon-data-unknown'); - $scope.caloriesData.aggrCalories = i18next.t('metrics.calorie-data-unknown'); - Logger.displayError("Error loading aggregate data, averages not available", - error); - }); - }; - - $scope.fillUserValues = function(user_metrics_arr) { - var seventhDayAgo = moment().utc().startOf('day').subtract(7, 'days'); - METRIC_LIST.forEach((m) => $scope.userCurrentResults[m] = []); - - METRIC_LIST.forEach((m) => $scope.userTwoWeeksAgo[m] = []); - - if($scope.defaultTwoWeekUserCall){ - for(var i in user_metrics_arr[0]) { - if(seventhDayAgo.isSameOrBefore(moment.unix(user_metrics_arr[0][i].ts).utc())){ - METRIC_LIST.forEach((m, idx) => $scope.userCurrentResults[m].push(user_metrics_arr[idx][i])); - } else { - METRIC_LIST.forEach((m, idx) => $scope.userTwoWeeksAgo[m].push(user_metrics_arr[idx][i])); - } - } - METRIC_LIST.forEach((m) => console.log("userTwoWeeksAgo."+m+" = "+$scope.userTwoWeeksAgo[m])); - } else { - METRIC_LIST.forEach((m, idx) => $scope.userCurrentResults[m] = user_metrics_arr[idx]); - } - - METRIC_LIST.forEach((m) => - $scope.userCurrentModeMap[m] = getDataFromMetrics($scope.userCurrentResults[m], metric2valUser)); - - COMPUTATIONAL_METRIC_LIST.forEach((m) => - $scope.userTwoWeeksAgoModeMap[m] = getDataFromMetrics($scope.userTwoWeeksAgo[m], metric2valUser)); - - METRIC_LIST.forEach((m) => - $scope.userCurrentModeMapFormatted[m] = formatModeMap($scope.userCurrentModeMap[m], m)); - - METRIC_LIST.forEach((m) => - $scope.userCurrentSummaryModeMap[m] = getSummaryDataRaw($scope.userCurrentModeMap[m], m)); - - COMPUTATIONAL_METRIC_LIST.forEach((m) => - $scope.userTwoWeeksAgoSummaryModeMap[m] = getSummaryDataRaw($scope.userTwoWeeksAgoModeMap[m], m)); - - METRIC_LIST.forEach((m) => - $scope.summaryData.userSummary[m] = getSummaryDataRaw($scope.userCurrentModeMap[m], m)); - - $scope.isCustomLabelResult = isCustomLabels($scope.userCurrentModeMap); - FootprintHelper.setUseCustomFootprint($scope.isCustomLabelResult); - CalorieCal.setUseCustomFootprint($scope.isCustomLabelResult); - - $scope.chartDataUser = $scope.userCurrentModeMapFormatted; - - // Fill in user calorie information - $scope.fillCalorieCardUserVals($scope.userCurrentSummaryModeMap.duration, - $scope.userCurrentSummaryModeMap.mean_speed, - $scope.userTwoWeeksAgoSummaryModeMap.duration, - $scope.userTwoWeeksAgoSummaryModeMap.mean_speed); - $scope.fillFootprintCardUserVals($scope.userCurrentModeMap.distance, - $scope.userCurrentSummaryModeMap.distance, - $scope.userTwoWeeksAgoModeMap.distance, - $scope.userTwoWeeksAgoSummaryModeMap.distance); - } - - $scope.fillAggregateValues = function(agg_metrics_arr) { - METRIC_LIST.forEach((m) => $scope.aggCurrentResults[m] = []); - if ($scope.defaultTwoWeekUserCall) { - METRIC_LIST.forEach((m, idx) => $scope.aggCurrentResults[m] = agg_metrics_arr[idx].slice(0,7)); - } else { - METRIC_LIST.forEach((m, idx) => $scope.aggCurrentResults[m] = agg_metrics_arr[idx]); - } - - METRIC_LIST.forEach((m) => - $scope.aggCurrentModeMap[m] = getDataFromMetrics($scope.aggCurrentResults[m], metric2valUser)); - - METRIC_LIST.forEach((m) => - $scope.aggCurrentModeMapFormatted[m] = formatModeMap($scope.aggCurrentModeMap[m], m)); - - COMPUTATIONAL_METRIC_LIST.forEach((m) => - $scope.aggCurrentPerCapitaModeMap[m] = getDataFromMetrics($scope.aggCurrentResults[m], metric2valAvg)); - - COMPUTATIONAL_METRIC_LIST.forEach((m) => - $scope.aggCurrentSummaryPerCapitaModeMap[m] = getSummaryDataRaw($scope.aggCurrentPerCapitaModeMap[m], m)); - - $scope.chartDataAggr = $scope.aggCurrentModeMapFormatted; - $scope.fillCalorieAggVals($scope.aggCurrentSummaryPerCapitaModeMap.duration, - $scope.aggCurrentSummaryPerCapitaModeMap.mean_speed); - $scope.fillFootprintAggVals($scope.aggCurrentSummaryPerCapitaModeMap.distance); - } - - /* - * We use the results to determine whether these results are from custom - * labels or from the automatically sensed labels. Automatically sensedV - * labels are in all caps, custom labels are prefixed by label, but have had - * the label_prefix stripped out before this. Results should have either all - * sensed labels or all custom labels. - */ - var isCustomLabels = function(modeMap) { - const isSensed = (mode) => mode == mode.toUpperCase(); - const isCustom = (mode) => mode == mode.toLowerCase(); - const metricSummaryChecksCustom = []; - const metricSummaryChecksSensed = []; - for (const metric in modeMap) { - const metricKeys = modeMap[metric].map((e) => e.key); - const isSensedKeys = metricKeys.map(isSensed); - const isCustomKeys = metricKeys.map(isCustom); - console.log("Checking metric keys", metricKeys, " sensed ", isSensedKeys, - " custom ", isCustomKeys); - const isAllCustomForMetric = isAllCustom(isSensedKeys, isCustomKeys); - metricSummaryChecksSensed.push(!isAllCustomForMetric); - metricSummaryChecksCustom.push(isAllCustomForMetric); - } - console.log("overall custom/not results for each metric = ", metricSummaryChecksCustom); - return isAllCustom(metricSummaryChecksSensed, metricSummaryChecksCustom); - } - - var isAllCustom = function(isSensedKeys, isCustomKeys) { - const allSensed = isSensedKeys.reduce((a, b) => a && b, true); - const anySensed = isSensedKeys.reduce((a, b) => a || b, false); - const allCustom = isCustomKeys.reduce((a, b) => a && b, true); - const anyCustom = isCustomKeys.reduce((a, b) => a || b, false); - if ((allSensed && !anyCustom)) { - return false; // sensed, not custom - } - if ((!anySensed && allCustom)) { - return true; // custom, not sensed; false implies that the other option is true - } - Logger.displayError("Mixed entries that combine sensed and custom labels", - "Please report to your program admin"); - return undefined; - } - - $scope.fillCalorieCardUserVals = function(userDurationSummary, userMeanSpeedSummary, - twoWeeksAgoDurationSummary, twoWeeksAgoMeanSpeedSummary) { - $scope.caloriesData.userCalories = {low: 0, high: 0}; - const highestMET = CalorieCal.getHighestMET(); - for (var i in userDurationSummary) { - var lowMET = $scope.getCorrectedMetFromUserData(userDurationSummary[i], userMeanSpeedSummary[i], 0); - var highMET = $scope.getCorrectedMetFromUserData(userDurationSummary[i], userMeanSpeedSummary[i], highestMET); - $scope.caloriesData.userCalories.low += - Math.round(CalorieCal.getuserCalories(userDurationSummary[i].values / 3600, lowMET)) //+ ' cal' - $scope.caloriesData.userCalories.high += - Math.round(CalorieCal.getuserCalories(userDurationSummary[i].values / 3600, highMET)) //+ ' cal' - } - - $scope.numberOfCookies = { - low: Math.floor($scope.caloriesData.userCalories.low/ - $scope.food.chocolateChip), - high: Math.floor($scope.caloriesData.userCalories.high/ - $scope.food.chocolateChip), - }; - $scope.numberOfIceCreams = { - low: Math.floor($scope.caloriesData.userCalories.low/ - $scope.food.vanillaIceCream), - high: Math.floor($scope.caloriesData.userCalories.high/ - $scope.food.vanillaIceCream), - }; - $scope.numberOfBananas = { - low: Math.floor($scope.caloriesData.userCalories.low/ - $scope.food.banana), - high: Math.floor($scope.caloriesData.userCalories.high/ - $scope.food.banana), - }; - - if($scope.defaultTwoWeekUserCall) { - if (twoWeeksAgoDurationSummary.length > 0) { - var twoWeeksAgoCalories = {low: 0, high: 0}; - for (var i in twoWeeksAgoDurationSummary) { - var lowMET = $scope.getCorrectedMetFromUserData(twoWeeksAgoDurationSummary[i], - twoWeeksAgoMeanSpeedSummary[i], 0) - var highMET = $scope.getCorrectedMetFromUserData(twoWeeksAgoDurationSummary[i], - twoWeeksAgoMeanSpeedSummary[i], highestMET) - twoWeeksAgoCalories.low += - Math.round(CalorieCal.getuserCalories(twoWeeksAgoDurationSummary[i].values / 3600, lowMET)); - twoWeeksAgoCalories.high += - Math.round(CalorieCal.getuserCalories(twoWeeksAgoDurationSummary[i].values / 3600, highMET)); - } - $scope.caloriesData.lastWeekUserCalories = { - low: twoWeeksAgoCalories.low, - high: twoWeeksAgoCalories.high - }; - console.log("Running calorieData with ", $scope.caloriesData); - // TODO: Refactor this so that we can filter out bad values ahead of time - // instead of having to work around it here - $scope.caloriesData.greaterLesserPct = { - low: ($scope.caloriesData.userCalories.low/$scope.caloriesData.lastWeekUserCalories.low) * 100 - 100, - high: ($scope.caloriesData.userCalories.high/$scope.caloriesData.lastWeekUserCalories.high) * 100 - 100, - } - } - } - } - - $scope.fillCalorieAggVals = function(aggDurationSummaryAvg, aggMeanSpeedSummaryAvg) { - $scope.caloriesData.aggrCalories = {low: 0, high: 0}; - const highestMET = CalorieCal.getHighestMET(); - for (var i in aggDurationSummaryAvg) { - var lowMET = CalorieCal.getMet(aggDurationSummaryAvg[i].key, aggMeanSpeedSummaryAvg[i].values, 0); - var highMET = CalorieCal.getMet(aggDurationSummaryAvg[i].key, aggMeanSpeedSummaryAvg[i].values, highestMET); - $scope.caloriesData.aggrCalories.low += - CalorieCal.getuserCalories(aggDurationSummaryAvg[i].values / 3600, lowMET); //+ ' cal' - $scope.caloriesData.aggrCalories.high += - CalorieCal.getuserCalories(aggDurationSummaryAvg[i].values / 3600, highMET); //+ ' cal' - } - } - - $scope.getCorrectedMetFromUserData = function(currDurationData, currSpeedData, defaultIfMissing) { - if ($scope.userDataSaved()) { - // this is safe because userDataSaved will never be set unless there - // is stored user data that we have loaded - var userDataFromStorage = $scope.savedUserData; - var met = CalorieCal.getMet(currDurationData.key, currSpeedData.values, defaultIfMissing); - var gender = userDataFromStorage.gender; - var heightUnit = userDataFromStorage.heightUnit; - var height = userDataFromStorage.height; - var weightUnit = userDataFromStorage.weightUnit; - var weight = userDataFromStorage.weight; - var age = userDataFromStorage.age; - return CalorieCal.getCorrectedMet(met, gender, age, height, heightUnit, weight, weightUnit); - } else { - return CalorieCal.getMet(currDurationData.key, currSpeedData.values, defaultIfMissing); - } - }; - - $scope.fillFootprintCardUserVals = function( - userDistance, userDistanceSummary, - twoWeeksAgoDistance, twoWeeksAgoDistanceSummary) { - if (userDistance) { - // var optimalDistance = getOptimalFootprintDistance(userDistance); - var worstDistance = getWorstFootprintDistance(userDistanceSummary); - - var date1 = $scope.selectCtrl.fromDateTimestamp; - var date2 = $scope.selectCtrl.toDateTimestamp; - var duration = moment.duration(date2.diff(date1)); - var days = duration.asDays(); - - /* - * 54 and 14 are the per-week CO2 estimates. - * https://github.com/e-mission/e-mission-docs/issues/688 - * Since users can choose a custom range which can be less or greater - * than 7 days, we calculate the per day value by dividing by 7 and - * then multiplying by the actual number of days. - */ - $scope.carbonData.us2030 = Math.round(54 / 7 * days); // kg/day - $scope.carbonData.us2050 = Math.round(14 / 7 * days); - - $scope.carbonData.userCarbon = { - low: FootprintHelper.getFootprintForMetrics(userDistanceSummary,0), - high: FootprintHelper.getFootprintForMetrics(userDistanceSummary, - FootprintHelper.getHighestFootprint()), - }; - // $scope.carbonData.optimalCarbon = FootprintHelper.getLowestFootprintForDistance(optimalDistance); - $scope.carbonData.worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); - $scope.carbonData.carbonAvoided = { - low: $scope.carbonData.worstCarbon - $scope.carbonData.userCarbon.high, - high: $scope.carbonData.worstCarbon - $scope.carbonData.userCarbon.low, - }; - } - - $scope.numberOfCharges = { - low: Math.floor($scope.carbonData.carbonAvoided.low/ - $scope.carbon.phoneCharge), - high: Math.floor($scope.carbonData.carbonAvoided.high/ - $scope.carbon.phoneCharge), - }; - - if ($scope.defaultTwoWeekUserCall) { - // This is a default call in which we retrieved the current week and - // the previous week of data - if (twoWeeksAgoDistance.length > 0) { - // and this user has been around long enough that they have two weeks - // of data, or they haven't turned off tracking for all of last week, - // or.... - $scope.carbonData.lastWeekUserCarbon = { - low: FootprintHelper.getFootprintForMetrics(twoWeeksAgoDistanceSummary,0), - high: FootprintHelper.getFootprintForMetrics(twoWeeksAgoDistanceSummary, - FootprintHelper.getHighestFootprint()), - }; - - console.log("Running calculation with " + $scope.carbonData.userCarbon + " and " + $scope.carbonData.lastWeekUserCarbon); - console.log("Running calculation with ", $scope.carbonData); - $scope.carbonData.greaterLesserPct = { - low: ($scope.carbonData.userCarbon.low/$scope.carbonData.lastWeekUserCarbon.low) * 100 - 100, - high: ($scope.carbonData.userCarbon.high/$scope.carbonData.lastWeekUserCarbon.high) * 100 - 100, - } - } - } - }; - - $scope.fillFootprintAggVals = function(aggDistance) { - if (aggDistance) { - var aggrCarbonData = aggDistance; - - // Issue 422: - // https://github.com/e-mission/e-mission-docs/issues/422 - for (var i in aggrCarbonData) { - if (isNaN(aggrCarbonData[i].values)) { - console.warn("WARNING fillFootprintAggVals(): value is NaN for mode " + aggrCarbonData[i].key + ", changing to 0"); - aggrCarbonData[i].values = 0; - } - } - - $scope.carbonData.aggrCarbon = { - low: FootprintHelper.getFootprintForMetrics(aggrCarbonData, 0), - high: FootprintHelper.getFootprintForMetrics(aggrCarbonData, - FootprintHelper.getHighestFootprint()), - }; - } - }; - - $scope.showCharts = function(agg_metrics) { - $scope.data = agg_metrics; - $scope.countOptions = angular.copy($scope.options) - $scope.countOptions.chart.yAxis.axisLabel = i18next.t('metrics.trips-yaxis-number'); - $scope.distanceOptions = angular.copy($scope.options) - $scope.distanceOptions.chart.yAxis.axisLabel = ImperialConfig.getDistanceSuffix; - $scope.durationOptions = angular.copy($scope.options) - $scope.durationOptions.chart.yAxis.axisLabel = i18next.t('metrics.hours'); - $scope.speedOptions = angular.copy($scope.options) - $scope.speedOptions.chart.yAxis.axisLabel = ImperialConfig.getSpeedSuffix; - }; - $scope.pandaFreqOptions = [ - {text: i18next.t('metrics.pandafreqoptions-daily'), value: 'D'}, - {text: i18next.t('metrics.pandafreqoptions-weekly'), value: 'W'}, - {text: i18next.t('metrics.pandafreqoptions-biweekly'), value: '2W'}, - {text: i18next.t('metrics.pandafreqoptions-monthly'), value: 'M'}, - {text: i18next.t('metrics.pandafreqoptions-yearly'), value: 'A'} - ]; - $scope.freqOptions = [ - {text: i18next.t('metrics.freqoptions-daily'), value:'DAILY'}, - {text: i18next.t('metrics.freqoptions-monthly'), value: 'MONTHLY'}, - {text: i18next.t('metrics.freqoptions-yearly'), value: 'YEARLY'} - ]; - - /* - * metric2val is a function that takes a metric entry and a field and returns - * the appropriate value. - * for regular data (user-specific), this will return the field value - * for avg data (aggregate), this will return the field value/nUsers - */ - - var metric2valUser = function(metric, field) { - return metric[field]; - } - - var metric2valAvg = function(metric, field) { - return metric[field]/metric.nUsers; - } - - var getDataFromMetrics = function(metrics, metric2val) { - console.log("Called getDataFromMetrics on ", metrics); - var mode_bins = {}; - metrics.forEach(function(metric) { - var on_foot_val = 0; - for (var field in metric) { - // For modes inferred from sensor data, we check if the string - // is all upper case by converting it to upper case and seeing - // if it is changed - if (field == field.toUpperCase()) { - // since we can have multiple possible ON_FOOT modes, we - // add all of them up here - // see https://github.com/e-mission/e-mission-docs/issues/422 - if (field === "WALKING" || field === "RUNNING" || field === "ON_FOOT") { - on_foot_val = on_foot_val + metric2val(metric, field); - field = "ON_FOOT"; - } - if (field in mode_bins == false) { - mode_bins[field] = [] - } - // since we can have multiple on_foot entries, let's hold - // off on handling them until we have considered all fields - if (field != "ON_FOOT") { - mode_bins[field].push([metric.ts, metric2val(metric, field), metric.fmt_time]); - } - } - // For modes from user labels, we assume that the field stars with - // the label_ prefix - if (field.startsWith("label_")) { - // "label_" is 6 characters - let actualMode = field.slice(6, field.length); - console.log("Mapped field "+field+" to mode "+actualMode); - if (actualMode in mode_bins == false) { - mode_bins[actualMode] = [] - } - mode_bins[actualMode].push([metric.ts, Math.round(metric2val(metric, field)), moment(metric.fmt_time).format()]); - } - } - // here's where we handle the ON_FOOT - if ("ON_FOOT" in mode_bins == true) { - // we must have received one of the on_foot modes, so we can - // boldly insert the value - mode_bins["ON_FOOT"].push([metric.ts, Math.round(on_foot_val), metric.fmt_time]); - } - }); - var rtn = []; - for (var mode in mode_bins) { - var val_arrays = rtn.push({key: mode, values: mode_bins[mode]}); - } - return rtn; - } - - var getSummaryDataRaw = function(modeMap, metric) { - console.log("Invoked getSummaryDataRaw on ", modeMap, "with", metric); - let summaryMap = angular.copy(modeMap); - for (var i = 0; i < modeMap.length; i++) { - var temp = 0; - for (var j = 0; j < modeMap[i].values.length; j++) { - temp += modeMap[i].values[j][1]; - } - if (metric === "mean_speed") { - summaryMap[i].values = Math.round(temp / modeMap[i].values.length); - } else { - summaryMap[i].values = Math.round(temp); - } - - } - return summaryMap; - } - - /*var sortNumber = function(a,b) { - return a - b; - }*/ - - /* - * This is _broken_ because what we see on the client is summary values, - * not individual trip values. So value > longTrip just means that the - * overall travel by that mode was long, not that each individual trip - * was long. - * - * As an obvious example, if I had 10 1k car trips, they would show up as - * daily travel of 10k by car, and be counted as a long trip, although each - * individual trip was actually 1k and short. - * - * Leaving this disabled until we come up with a principled solution. - * https://github.com/e-mission/e-mission-docs/issues/688#issuecomment-1000626564 - */ - - var getOptimalFootprintDistance = function(metrics){ - var data = getDataFromMetrics(metrics, metric2valUser); - var distance = 0; - var longTrip = 5000; - // total distance for long trips using motorized vehicles - for(var i = 0; i < data.length; i++) { - if(data[i].key == "CAR" || data[i].key == "BUS" || data[i].key == "TRAIN" || data[i].key == "AIR_OR_HSR") { - for(var j = 0; j < data[i].values.length; j++){ - if(data[i].values[j][1] >= longTrip){ - distance += data[i].values[j][1]; - } - } - } - } - return distance; - } - var getWorstFootprintDistance = function(modeMapSummary) { - var totalDistance = modeMapSummary.reduce((prevDistance, currModeSummary) => prevDistance + currModeSummary.values, 0); - return totalDistance; - } - - $scope.formatCount = function(value) { - const formatVal = Math.round(value); - const unit = i18next.t('metrics.trips'); - const stringRep = formatVal + " " + unit; - return [formatVal, unit, stringRep]; - } - - $scope.formatDistance = function(value) { - const formatVal = Number.parseFloat(ImperialConfig.getFormattedDistance(value)); - const unit = ImperialConfig.getDistanceSuffix; - const stringRep = formatVal + " " + unit; - return [formatVal, unit, stringRep]; - } - - $scope.formatDuration = function(value) { - const durM = moment.duration(value * 1000); - const formatVal = durM.asHours(); - const unit = i18next.t('metrics.hours'); - const stringRep = durM.humanize(); - return [formatVal, unit, stringRep]; - } - - $scope.formatMeanSpeed = function(value) { - const formatVal = Number.parseFloat(ImperialConfig.getFormattedSpeed(value)); - const unit = ImperialConfig.getSpeedSuffix; - const stringRep = formatVal + " " + unit; - return [formatVal, unit, stringRep]; - } - - $scope.formatterMap = { - count: $scope.formatCount, - distance: $scope.formatDistance, - duration: $scope.formatDuration, - mean_speed: $scope.formatMeanSpeed - } - - var formatModeMap = function(modeMapList, metric) { - const formatter = $scope.formatterMap[metric]; - let formattedModeList = []; - modeMapList.forEach((modeMap) => { - let currMode = modeMap["key"]; - let modeStatList = modeMap["values"]; - let formattedModeStatList = modeStatList.map((modeStat) => { - let [formatVal, unit, stringRep] = formatter(modeStat[1]); - // horizontal graphs: date on y axis and value on x axis - return { y: modeStat[0] * 1000, x: formatVal }; - }); - formattedModeList.push({label: currMode, records: formattedModeStatList}); - }); - return formattedModeList; - } - - $scope.changeFromWeekday = function() { - return $scope.changeWeekday(function(newVal) { - $scope.selectCtrl.fromDateWeekdayString = newVal; - }, - 'from'); - } - - $scope.changeToWeekday = function() { - return $scope.changeWeekday(function(newVal) { - $scope.selectCtrl.toDateWeekdayString = newVal; - }, - 'to'); - } - - // $scope.show fil - - $scope.changeWeekday = function(stringSetFunction, target) { - var weekdayOptions = [ - {text: i18next.t('weekdays-all'), value: null}, - {text: moment.weekdays(1), value: 0}, - {text: moment.weekdays(2), value: 1}, - {text: moment.weekdays(3), value: 2}, - {text: moment.weekdays(4), value: 3}, - {text: moment.weekdays(5), value: 4}, - {text: moment.weekdays(6), value: 5}, - {text: moment.weekdays(0), value: 6} - ]; - $ionicActionSheet.show({ - buttons: weekdayOptions, - titleText: i18next.t('weekdays-select'), - cancelText: i18next.t('metrics.cancel'), - buttonClicked: function(index, button) { - stringSetFunction(button.text); - if (target === 'from') { - $scope.selectCtrl.fromDateWeekdayValue = button.value; - } else if (target === 'to') { - $scope.selectCtrl.toDateWeekdayValue = button.value; - } else { - console.log("Illegal target"); - } - return true; - } - }); - }; - $scope.changeFreq = function() { - $ionicActionSheet.show({ - buttons: $scope.freqOptions, - titleText: i18next.t('metrics.select-frequency'), - cancelText: i18next.t('metrics.cancel'), - buttonClicked: function(index, button) { - $scope.selectCtrl.freqString = button.text; - $scope.selectCtrl.freq = button.value; - return true; - } - }); - }; - - $scope.changePandaFreq = function() { - $ionicActionSheet.show({ - buttons: $scope.pandaFreqOptions, - titleText: i18next.t('metrics.select-pandafrequency'), - cancelText: i18next.t('metrics.cancel'), - buttonClicked: function(index, button) { - $scope.selectCtrl.pandaFreqString = button.text; - $scope.selectCtrl.pandaFreq = button.value; - return true; - } - }); - }; - - $scope.toggle = function() { - if (!$scope.uictrl.showMe) { - $scope.uictrl.showMe = true; - $scope.showCharts($scope.chartDataUser); - - } else { - $scope.uictrl.showMe = false; - $scope.showCharts($scope.chartDataAggr); - } - } - var initSelect = function() { - var now = moment().utc(); - var weekAgoFromNow = moment().utc().subtract(7, 'd'); - $scope.selectCtrl.freq = 'DAILY'; - $scope.selectCtrl.freqString = i18next.t('metrics.freqoptions-daily'); - $scope.selectCtrl.pandaFreq = 'D'; - $scope.selectCtrl.pandaFreqString = i18next.t('metrics.pandafreqoptions-daily'); - // local_date saved as localdate - $scope.selectCtrl.fromDateLocalDate = moment2Localdate(weekAgoFromNow); - $scope.selectCtrl.toDateLocalDate = moment2Localdate(now); - // ts saved as moment - $scope.selectCtrl.fromDateTimestamp= weekAgoFromNow; - $scope.selectCtrl.toDateTimestamp = now; - - $scope.selectCtrl.fromDateWeekdayString = i18next.t('weekdays-all'); - $scope.selectCtrl.toDateWeekdayString = i18next.t('weekdays-all'); - - $scope.selectCtrl.fromDateWeekdayValue = null; - $scope.selectCtrl.toDateWeekdayValue = null; - - $scope.selectCtrl.region = null; - }; - - - $scope.selectCtrl = {} - initSelect(); - - $scope.doRefresh = function() { - getMetrics(); - } - - $scope.$on('$ionicView.enter',function(){ - $scope.startTime = moment().utc() - ClientStats.addEvent(ClientStats.getStatKeys().OPENED_APP).then( - function() { - console.log("Added "+ClientStats.getStatKeys().OPENED_APP+" event"); - }); - }); - - $scope.$on('$ionicView.leave',function() { - var timeOnPage = moment().utc() - $scope.startTime; - ClientStats.addReading(ClientStats.getStatKeys().METRICS_TIME, timeOnPage); - }); - - $ionicPlatform.on("pause", function() { - if ($state.$current == "root.main.metrics") { - var timeOnPage = moment().utc() - $scope.startTime; - ClientStats.addReading(ClientStats.getStatKeys().METRICS_TIME, timeOnPage); - } - }) - - $ionicPlatform.on("resume", function() { - if ($state.$current == "root.main.metrics") { - $scope.startTime = moment().utc() - } - }) - - $scope.linkToMaps = function() { - let start = $scope.suggestionData.startCoordinates[1] + ',' + $scope.suggestionData.startCoordinates[0]; - let destination = $scope.suggestionData.endCoordinates[1] + ',' + $scope.suggestionData.endCoordinates[0]; - var mode = $scope.suggestionData.mode - if(ionic.Platform.isIOS()){ - if (mode === 'bike') { - mode = 'b'; - } else if (mode === 'public') { - mode = 'r'; - } else if (mode === 'walk') { - mode = 'w'; - } - window.open('https://www.maps.apple.com/?saddr=' + start + '&daddr=' + destination + '&dirflg=' + mode, '_system'); - } else { - if (mode === 'bike') { - mode = 'b'; - } else if (mode === 'public') { - mode = 'r'; - } else if (mode === 'walk') { - mode = 'w'; - } - window.open('https://www.google.com/maps?saddr=' + start + '&daddr=' + destination +'&dirflg=' + mode, '_system'); - } - } - - $scope.linkToDiary = function(trip_id) { - console.log("Loading trip "+trip_id); - window.location.href = "#/root/main/diary/" + trip_id; - } - - $scope.hasUsername = function(obj) { - return (obj.hasOwnProperty('username')); - } - - $scope.modeIcon = function(key) { - var icons = {"BICYCLING":"ion-android-bicycle", - "ON_FOOT":" ion-android-walk", - "WALKING":" ion-android-walk", - "IN_VEHICLE":"ion-speedometer", - "CAR":"ion-android-car", - "BUS": "ion-android-bus", - "LIGHT_RAIL":"lightrail fas fa-subway", - "TRAIN": "ion-android-train", - "TRAM": "fas fa-tram", - "SUBWAY":"fas fa-subway", - "UNKNOWN": "ion-ios-help", - "AIR_OR_HSR": "ion-plane"} - return icons[key]; - } - - $scope.setCurDayFrom = function(val) { - if (val) { - $scope.selectCtrl.fromDateTimestamp = moment(val).startOf('day'); - if ($scope.selectCtrl.fromDateTimestamp > $scope.selectCtrl.toDateTimestamp) { - const copyToDateTimestamp = $scope.selectCtrl.toDateTimestamp.clone(); - $scope.selectCtrl.fromDateTimestamp = copyToDateTimestamp.startOf('day'); - } - $scope.datepickerObjFrom.inputMoment = $scope.selectCtrl.fromDateTimestamp; - $scope.datepickerObjFrom.inputDate = $scope.selectCtrl.fromDateTimestamp.toDate(); - } else { - $scope.datepickerObjFrom.inputMoment = $scope.selectCtrl.fromDateTimestamp; - $scope.datepickerObjFrom.inputDate = $scope.selectCtrl.fromDateTimestamp.toDate(); - } - - }; - $scope.setCurDayTo = function(val) { - if (val) { - $scope.selectCtrl.toDateTimestamp = moment(val).endOf('day'); - if ($scope.selectCtrl.toDateTimestamp < $scope.selectCtrl.fromDateTimestamp) { - const copyFromDateTimestamp = $scope.selectCtrl.fromDateTimestamp.clone(); - $scope.selectCtrl.toDateTimestamp = copyFromDateTimestamp.endOf('day'); - } - $scope.datepickerObjTo.inputMoment = $scope.selectCtrl.toDateTimestamp; - $scope.datepickerObjTo.inputDate = $scope.selectCtrl.toDateTimestamp.toDate(); - } else { - $scope.datepickerObjTo.inputMoment = $scope.selectCtrl.toDateTimestamp; - $scope.datepickerObjTo.inputDate = $scope.selectCtrl.toDateTimestamp.toDate(); - } - }; - - - $scope.data = {}; - - $scope.userData = { - gender: -1, - heightUnit: 1, - weightUnit: 1 - }; - $scope.caloriePopup = function() { - $ionicPopup.show({ - templateUrl: 'templates/caloriePopup.html', - title: '', - scope: $scope, - buttons: [ - { text: i18next.t('metrics.cancel') }, - { - text: ''+ i18next.t('metrics.confirm') +'', - type: 'button-positive', - onTap: function(e) { - if (!($scope.userData.gender != -1 && $scope.userData.age && $scope.userData.weight && $scope.userData.height)) { - e.preventDefault(); - } else { - $scope.storeUserData(); - // refresh - } - } - } - ] - }); - } - - $scope.datepickerObjBase = { - todayLabel: i18next.t('list-datepicker-today'), //Optional - closeLabel: i18next.t('list-datepicker-close'), //Optional - setLabel: i18next.t('list-datepicker-set'), //Optional - titleLabel: i18next.t('metrics.pick-a-date'), - mondayFirst: false, - weeksList: moment.weekdaysMin(), - monthsList: moment.monthsShort(), - templateType: 'popup', - from: new Date(2015, 1, 1), - to: new Date(), - showTodayButton: true, - closeOnSelect: false, - // add this instruction if you want to exclude a particular weekday, e.g. Saturday disableWeekdays: [6] - }; - - $scope.datepickerObjFrom = angular.copy($scope.datepickerObjBase); - angular.extend($scope.datepickerObjFrom, { - callback: $scope.setCurDayFrom, - inputDate: $scope.selectCtrl.fromDateTimestamp.toDate(), - inputMoment: $scope.selectCtrl.fromDateTimestamp, - }); - - $scope.datepickerObjTo = angular.copy($scope.datepickerObjBase); - angular.extend($scope.datepickerObjTo, { - callback: $scope.setCurDayTo, - inputDate: $scope.selectCtrl.toDateTimestamp.toDate(), - inputMoment: $scope.selectCtrl.toDateTimestamp, - }); - - $scope.pickFromDay = function() { - ionicDatePicker.openDatePicker($scope.datepickerObjFrom); - } - - $scope.pickToDay = function() { - ionicDatePicker.openDatePicker($scope.datepickerObjTo); - } - - $scope.extendFootprintCard = function() { - if($scope.expandedf){ - $scope.expandedf = false; - } else { - $scope.expandedf = true - } - } - $scope.checkFootprintCardExpanded = function() { - return ($scope.expandedf)? "icon ion-chevron-up" : "icon ion-chevron-down"; - } - $scope.extendCalorieCard = function() { - if($scope.expandedc){ - $scope.expandedc = false; - } else { - $scope.expandedc = true - } - } - $scope.checkCalorieCardExpanded = function() { - return ($scope.expandedc)? "icon ion-chevron-up" : "icon ion-chevron-down"; - } - - $scope.changeFootprintCardHeight = function() { - return ($scope.expandedf)? "expanded-footprint-card" : "small-footprint-card"; - } - - $scope.changeCalorieCardHeight = function() { - return ($scope.expandedc)? "expanded-calorie-card" : "small-calorie-card"; - } - - -}) -.directive('diffdisplay', function() { - return { - scope: { - change: "=" - }, - link: function(scope) { - if (isNaN(scope.change.low)) scope.change.low = 0; - if (isNaN(scope.change.high)) scope.change.high = 0; - console.log("In diffdisplay, after changes, scope = ", scope); - }, - templateUrl: "templates/metrics/arrow-greater-lesser.html" - } -}) -.directive('rangedisplay', function() { - return { - scope: { - range: "=" - }, - link: function(scope) { - // console.log("RANGE DISPLAY "+JSON.stringify(scope.range)); - var humanize = function(num) { - if (Math.abs(num) < 1) { - return num.toFixed(2); - } else { - return num.toFixed(0); - } - } - - if (Math.abs(scope.range.high - scope.range.low) < 1) { - scope.tinyDiff = true; - } - scope.lowFmt = humanize(scope.range.low); - scope.highFmt = humanize(scope.range.high); - }, - templateUrl: "templates/metrics/range-display.html" - } -}); diff --git a/www/templates/main-metrics.html b/www/templates/main-metrics.html deleted file mode 100644 index a99f566d6..000000000 --- a/www/templates/main-metrics.html +++ /dev/null @@ -1,207 +0,0 @@ - - - - - - - - - - -
-
-
{{'main-metrics.summary'}}
-
{{'main-metrics.chart'}}
-
-
-
-
{{'main-metrics.change-data'}}
-
-
-
-
{{ selectCtrl.fromDateTimestamp.format('ll') }} ➡️ {{ selectCtrl.toDateTimestamp.format('ll') }}
-
-
-
-
- - - -
-
-
-
-
{{'main-metrics.distance'}}
-
{{'main-metrics.trips'}}
-
{{'main-metrics.duration'}}
-
{{'main-metrics.speed'}}
-
-
-
-
- - - - -
-
-
-
-
-
-
-
-

{{'main-metrics.footprint'}}

-
-
-
kg CO₂
-
{{ 'main-metrics.label-to-squish' | i18next }}
- -
-
-
-
{{'main-metrics.how-it-compares'}}
- -
{{'main-metrics.average'}} kg CO₂
-
{{'main-metrics.avoided'}} kg CO₂
-
{{'main-metrics.lastweek'}} kg CO₂
- -
{{'main-metrics.us-2030-goal'}} {{carbonData.us2030 | number}} kg CO₂
-
{{'main-metrics.us-2050-goal'}} {{carbonData.us2050 | number}} kg CO₂
-
-
-
- -
-
-
-
-
- -
-
- -
-
- -
-

{{'main-metrics.calories'}}

-
- -
-
-
kcal
-
{{'main-metrics.equals-cookies' | i18next:{count: numberOfCookies.low} }}
-
{{'main-metrics.equals-icecream' | i18next:{count: numberOfIceCreams.low} }}
-
{{'main-metrics.equals-bananas' | i18next:{count: numberOfBananas.low} }}
- -
-
-
-
{{'main-metrics.average'}} cal
-
{{'main-metrics.lastweek'}} cal
-
-
- -
-
- -
-
- -
-
-
-
- -
-
- - - - - - -
-

{{'main-metrics.trips'}}

-
{{'main-metrics.no-summary-data'}}
-
-
- -
- -
-
-
- {{ summaryData.defaultSummary.count[dIndex + i].key }} -
-
- {{ formatCount(summaryData.defaultSummary.count[dIndex + i].values).slice(-1)[0] }} -
-
-
-
-
-
- -
-

{{'main-metrics.duration'}}

-
{{'main-metrics.no-summary-data'}}
-
-
- -
- -
-
-
- {{ summaryData.defaultSummary.duration[dIndex + i].key }} -
-
- {{ formatDuration(summaryData.defaultSummary.duration[dIndex + i].values).slice(-1)[0] }} -
-
-
-
-
-
- -
-

{{'main-metrics.mean-speed'}}

-
{{'main-metrics.no-summary-data'}}
-
-
- -
- -
-
-
- {{ summaryData.defaultSummary.mean_speed[dIndex + i].key }} -
-
- {{ formatMeanSpeed(summaryData.defaultSummary.mean_speed[dIndex + i].values).slice(-1)[0] }} -
-
-
-
-
-
-
-
-
-
-
From 8ebc390a47a777e56c287b216bee5fb6177e788b Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 12 Sep 2023 11:02:32 -0400 Subject: [PATCH 102/164] implement refresh() on MetricsTab Also, the type of dateRange was changed to `DateTime[]` (since I realized it had been declared using the wrong typing style) --- www/js/metrics/MetricsDateSelect.tsx | 2 +- www/js/metrics/MetricsTab.tsx | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/www/js/metrics/MetricsDateSelect.tsx b/www/js/metrics/MetricsDateSelect.tsx index 0757d0826..c66218453 100644 --- a/www/js/metrics/MetricsDateSelect.tsx +++ b/www/js/metrics/MetricsDateSelect.tsx @@ -16,7 +16,7 @@ import NavBarButton from "../components/NavBarButton"; import { DateTime } from "luxon"; type Props = { - dateRange: [DateTime, DateTime], + dateRange: DateTime[], setDateRange: (dateRange: [DateTime, DateTime]) => void, } const MetricsDateSelect = ({ dateRange, setDateRange }: Props) => { diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index f335a952b..3ae3ddb02 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -19,7 +19,7 @@ import ActiveMinutesTableCard from "./ActiveMinutesTableCard"; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; -async function fetchMetricsFromServer(type: 'user'|'aggregate', dateRange: [DateTime, DateTime]) { +async function fetchMetricsFromServer(type: 'user'|'aggregate', dateRange: DateTime[]) { const CommHelper = getAngularService('CommHelper'); const query = { freq: 'D', @@ -33,18 +33,20 @@ async function fetchMetricsFromServer(type: 'user'|'aggregate', dateRange: [Date return CommHelper.getAggregateData("result/metrics/timestamp", query); } +function getLastTwoWeeksDtRange() { + const now = DateTime.utc().startOf('day'); + const start = now.minus({ days: 15 }); + const end = now.minus({ days: 1 }); + return [start, end]; +} + const MetricsTab = () => { const { t } = useTranslation(); const { getFormattedSpeed, speedSuffix, getFormattedDistance, distanceSuffix } = useImperialConfig(); - const [dateRange, setDateRange] = useState<[DateTime, DateTime]>(() => { - const now = DateTime.utc().startOf('day'); - const start = now.minus({ days: 15 }); - const end = now.minus({ days: 1 }); - return [start, end]; - }); + const [dateRange, setDateRange] = useState(getLastTwoWeeksDtRange); const [aggMetrics, setAggMetrics] = useState(null); const [userMetrics, setUserMetrics] = useState(null); @@ -53,7 +55,7 @@ const MetricsTab = () => { loadMetricsForPopulation('aggregate', dateRange); }, [dateRange]); - async function loadMetricsForPopulation(population: 'user'|'aggregate', dateRange: [DateTime, DateTime]) { + async function loadMetricsForPopulation(population: 'user'|'aggregate', dateRange: DateTime[]) { const serverResponse = await fetchMetricsFromServer(population, dateRange); console.debug("Got metrics = ", serverResponse); const metrics = {}; @@ -69,7 +71,7 @@ const MetricsTab = () => { } function refresh() { - // TODO + setDateRange(getLastTwoWeeksDtRange()); } // // fake data for testing active minutes - TODO: remove @@ -85,7 +87,7 @@ const MetricsTab = () => { - refresh()} /> + From 89e3d6491794d0806c25232d0772fddf913c031b Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 12 Sep 2023 11:02:32 -0400 Subject: [PATCH 103/164] add CDC recommendations footnote --- www/i18n/en.json | 2 ++ www/js/metrics/WeeklyActiveMinutesCard.tsx | 14 +++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index a5c31c5c7..5b468ed7b 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -186,6 +186,8 @@ "weekly-active-minutes": "Weekly minutes of active travel", "daily-active-minutes": "Daily minutes of active travel", "active-minutes-table": "Table of active minutes metrics", + "weekly-goal": "Weekly Goal³", + "weekly-goal-footnote": "³Weekly goal based on CDC recommendation of 150 minutes of moderate activity per week.", "labeled": "Labeled", "unlabeled": "Unlabeled²", "footprint-label": "Footprint (kg CO₂)" diff --git a/www/js/metrics/WeeklyActiveMinutesCard.tsx b/www/js/metrics/WeeklyActiveMinutesCard.tsx index 501e77296..adfcaced1 100644 --- a/www/js/metrics/WeeklyActiveMinutesCard.tsx +++ b/www/js/metrics/WeeklyActiveMinutesCard.tsx @@ -55,11 +55,15 @@ const WeeklyActiveMinutesCard = ({ userMetrics }: Props) => { style={cardStyles.title(colors)} /> { weeklyActiveMinutesRecords.length ? - getBaseModeByReadableLabel(l, labelOptions).color} /> + + getBaseModeByReadableLabel(l, labelOptions).color} /> + + {t('main-metrics.weekly-goal-footnote')} + + : From 06f7b2d7638b669a7eef3cd742758d71b571a521 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 12 Sep 2023 11:02:32 -0400 Subject: [PATCH 104/164] i18n + add footnote markers on CarbonTextCard --- www/i18n/en.json | 2 ++ www/js/metrics/CarbonFootprintCard.tsx | 3 +-- www/js/metrics/CarbonTextCard.tsx | 27 ++++++++++++++++---------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index 5b468ed7b..1e0fb83cc 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -165,9 +165,11 @@ "average": "Group Avg.", "worst-case": "If all 'taxi'", "label-to-squish": "Label trips to collapse the range into a single number", + "range-uncertain-footnote": "²Due to the uncertainty of unlabeled trips, estimates may fall anywhere within the shown range. Label more trips for richer estimates.", "lastweek": "My last week value:", "us-2030-goal": "2030 Guideline¹", "us-2050-goal": "2050 Guideline¹", + "us-goals-footnote": "¹Guidelines based on US decarbonization goals, scaled to per-capita travel-related emissions.", "past-week" : "Past Week", "prev-week" : "Prev. Week", "calories": "My Calories", diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 2f1417d3a..540c4fc47 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -147,8 +147,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { - {/* TODO i18n */} - ¹US decarbonization goals scaled to per-capita travel-related emissions. + {t('main-metrics.us-goals-footnote')} : diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index 6d6d93838..eb1058336 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -41,8 +41,11 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), high: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, FootprintHelper.getHighestFootprint()), }; - textList.push({label: `${t('main-metrics.past-week')} (${formatDateRangeOfDays(thisWeekDistance)})`, - value: (userPastWeek.high - userPastWeek.low)==0 ? Math.round(userPastWeek.low) : Math.round(userPastWeek.low) + " - " + Math.round(userPastWeek.high)}); + const label = `${t('main-metrics.past-week')} (${formatDateRangeOfDays(thisWeekDistance)})`; + if (userPastWeek.low == userPastWeek.high) + textList.push({label: label, value: Math.round(userPastWeek.low)}); + else + textList.push({label: label + '²', value: `${Math.round(userPastWeek.low)} - ${Math.round(userPastWeek.high)}`}); //calculate low-high and format range for prev week, if exists if(userLastWeekSummaryMap[0]) { @@ -50,9 +53,12 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) }; - textList.push({label: `${t('main-metrics.prev-week')} (${formatDateRangeOfDays(lastWeekDistance)})`, - value: (userPrevWeek.high - userPrevWeek.low)==0 ? Math.round(userPrevWeek.low) : Math.round(userPrevWeek.low) + " - " + Math.round(userPrevWeek.high)}); - } + const label = `${t('main-metrics.prev-week')} (${formatDateRangeOfDays(lastWeekDistance)})`; + if (userPrevWeek.low == userPrevWeek.high) + textList.push({label: label, value: Math.round(userPrevWeek.low)}); + else + textList.push({label: label + '²', value: `${Math.round(userPrevWeek.low)} - ${Math.round(userPrevWeek.high)}`}); + } //calculate worst-case carbon footprint let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); @@ -89,8 +95,11 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { high: FootprintHelper.getFootprintForMetrics(aggCarbonData, FootprintHelper.getHighestFootprint()), } console.log("testing group past week", aggCarbon); - groupText.push({label: t('main-metrics.average'), - value: (aggCarbon.high - aggCarbon.low)==0 ? Math.round(aggCarbon.low) : Math.round(aggCarbon.low) + " - " + Math.round(aggCarbon.high)}); + const label = t('main-metrics.average'); + if (aggCarbon.low == aggCarbon.high) + groupText.push({label: label, value: Math.round(aggCarbon.low)}); + else + groupText.push({label: label + '²', value: `${Math.round(aggCarbon.low)} - ${Math.round(aggCarbon.high)}`}); return groupText; } @@ -133,9 +142,7 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { ) } - {/* TODO i18n */} - {/* unlabeled means the mode is not labeled and the carbon footprint is uncertain, it may fall anywhere between 0 (best case) and 'taxi' (worst case) */} - ²The carbon footprint of unlabeled trips is uncertain. Estimates may fall anywhere within the shown range. + {t('main-metrics.range-uncertain-footnote')} From 72f6437273ae594ba7de90905d2092d98bac5707 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 12 Sep 2023 11:02:32 -0400 Subject: [PATCH 105/164] remove fake data that was used for testing --- www/js/metrics/MetricsTab.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 3ae3ddb02..16cbb742e 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -74,12 +74,6 @@ const MetricsTab = () => { setDateRange(getLastTwoWeeksDtRange()); } - // // fake data for testing active minutes - TODO: remove - // userMetrics?.duration.forEach((day, i) => { - // day.label_walk = day.label_walk || 65*i + (Math.random() * 600); - // day.label_bike = day.label_bike || 25*i + (Math.random() * 400); - // }); - const { width: windowWidth } = useWindowDimensions(); const cardWidth = windowWidth * .88; From 9208ec04b8a6baad86e4d2ab13dd45ad99cab5dc Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 12 Sep 2023 11:02:32 -0400 Subject: [PATCH 106/164] dashboard: use rich mode text, not readable label The dashboard should show the full text of the mode as defined in the label options - e.g. show "Gas Car Drove Alone", not "Drove Alone". According to https://github.com/e-mission/e-mission-phone/pull/1026#discussion_r1320590445 we shouldn't convert keys like "drove_alone" to readable like "Drove Alone", except in the case of user-defined labels ('other text') --- www/js/diary/diaryHelper.ts | 5 ++--- www/js/metrics/ActiveMinutesTableCard.tsx | 4 ++-- www/js/metrics/DailyActiveMinutesCard.tsx | 8 ++++---- www/js/metrics/MetricsCard.tsx | 10 +++++----- www/js/metrics/WeeklyActiveMinutesCard.tsx | 10 +++++----- www/js/survey/multilabel/confirmHelper.ts | 4 ++++ 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts index 54776b372..5c0588da0 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -72,9 +72,8 @@ export function getBaseModeOfLabeledTrip(trip, labelOptions) { return getBaseModeByKey(modeOption?.baseMode || "OTHER"); } -export function getBaseModeByReadableLabel(label, labelOptions: LabelOptions) { - const labelKey = readableLabelToKey(label); - const modeOption = labelOptions?.MODE?.find(opt => opt.value == labelKey); +export function getBaseModeByText(text, labelOptions: LabelOptions) { + const modeOption = labelOptions?.MODE?.find(opt => opt.text == text); return getBaseModeByKey(modeOption?.baseMode || "OTHER"); } diff --git a/www/js/metrics/ActiveMinutesTableCard.tsx b/www/js/metrics/ActiveMinutesTableCard.tsx index b5f1333f7..8bcdbf0a4 100644 --- a/www/js/metrics/ActiveMinutesTableCard.tsx +++ b/www/js/metrics/ActiveMinutesTableCard.tsx @@ -5,7 +5,7 @@ import { cardStyles } from './MetricsTab'; import { filterToRecentWeeks, formatDate, formatDateRangeOfDays, secondsToMinutes } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import { ACTIVE_MODES } from './WeeklyActiveMinutesCard'; -import { labelKeyToReadable } from '../survey/multilabel/confirmHelper'; +import { labelKeyToRichMode } from '../survey/multilabel/confirmHelper'; type Props = { userMetrics: MetricsData } const ActiveMinutesTableCard = ({ userMetrics }: Props) => { @@ -76,7 +76,7 @@ const ActiveMinutesTableCard = ({ userMetrics }: Props) => { {ACTIVE_MODES.map((mode, i) => - {labelKeyToReadable(mode)} + {labelKeyToRichMode(mode)} )} {allTotals.slice(from, to).map((total, i) => diff --git a/www/js/metrics/DailyActiveMinutesCard.tsx b/www/js/metrics/DailyActiveMinutesCard.tsx index 0da88dca4..479a5f5b5 100644 --- a/www/js/metrics/DailyActiveMinutesCard.tsx +++ b/www/js/metrics/DailyActiveMinutesCard.tsx @@ -5,9 +5,9 @@ import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; import { useTranslation } from 'react-i18next'; -import { labelKeyToReadable, labelOptions } from '../survey/multilabel/confirmHelper'; +import { labelKeyToRichMode, labelOptions } from '../survey/multilabel/confirmHelper'; import LineChart from '../components/LineChart'; -import { getBaseModeByReadableLabel } from '../diary/diaryHelper'; +import { getBaseModeByText } from '../diary/diaryHelper'; const ACTIVE_MODES = ['walk', 'bike'] as const; type ActiveMode = typeof ACTIVE_MODES[number]; @@ -25,7 +25,7 @@ const DailyActiveMinutesCard = ({ userMetrics }: Props) => { ACTIVE_MODES.forEach(mode => { const activeSeconds = day[`label_${mode}`]; records.push({ - label: labelKeyToReadable(mode), + label: labelKeyToRichMode(mode), x: day.ts * 1000, // vertical chart, milliseconds on X axis y: activeSeconds && activeSeconds / 60, // minutes on Y axis }); @@ -48,7 +48,7 @@ const DailyActiveMinutesCard = ({ userMetrics }: Props) => { { dailyActiveMinutesRecords.length ? getBaseModeByReadableLabel(l, labelOptions).color} /> + getColorForLabel={(l) => getBaseModeByText(l, labelOptions).color} /> : diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index fa8470ddb..9676ae6ec 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -7,8 +7,8 @@ import { DayOfMetricData } from './metricsTypes'; import { formatDateRangeOfDays, getLabelsForDay, getUniqueLabelsForDays } from './metricsHelper'; import ToggleSwitch from '../components/ToggleSwitch'; import { cardStyles } from './MetricsTab'; -import { labelKeyToReadable, labelOptions } from '../survey/multilabel/confirmHelper'; -import { getBaseModeByReadableLabel } from '../diary/diaryHelper'; +import { labelKeyToRichMode, labelOptions } from '../survey/multilabel/confirmHelper'; +import { getBaseModeByText } from '../diary/diaryHelper'; import { useTranslation } from 'react-i18next'; type Props = { @@ -38,7 +38,7 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni labels.forEach(label => { const rawVal = day[`label_${label}`]; records.push({ - label: labelKeyToReadable(label), + label: labelKeyToRichMode(label), x: unitFormatFn ? unitFormatFn(rawVal) : rawVal, y: day.ts * 1000, // time (as milliseconds) will go on Y axis because it will be a horizontal chart }); @@ -91,7 +91,7 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni { Object.keys(metricSumValues).map((label, i) => - {labelKeyToReadable(label)} + {labelKeyToRichMode(label)} {metricSumValues[label] + ' ' + axisUnits} )} @@ -100,7 +100,7 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni {viewMode=='graph' && <> getBaseModeByReadableLabel(l, labelOptions).color} /> + getColorForLabel={(l) => getBaseModeByText(l, labelOptions).color} /> Stack bars: { ), 0); if (recentSum) { const xLabel = `Past Week\n(${formatDateRangeOfDays(recentWeek)})`; // TODO: i18n - records.push({label: labelKeyToReadable(mode), x: xLabel, y: recentSum / 60}); + records.push({label: labelKeyToRichMode(mode), x: xLabel, y: recentSum / 60}); } const prevSum = prevWeek?.reduce((acc, day) => ( acc + (day[`label_${mode}`] || 0) ), 0); if (prevSum) { const xLabel = `Previous Week\n(${formatDateRangeOfDays(prevWeek)})`; // TODO: i18n - records.push({label: labelKeyToReadable(mode), x: xLabel, y: prevSum / 60}); + records.push({label: labelKeyToRichMode(mode), x: xLabel, y: prevSum / 60}); } }); return records as {label: ActiveMode, x: string, y: number}[]; @@ -59,7 +59,7 @@ const WeeklyActiveMinutesCard = ({ userMetrics }: Props) => { getBaseModeByReadableLabel(l, labelOptions).color} /> + getColorForLabel={(l) => getBaseModeByText(l, labelOptions).color} /> {t('main-metrics.weekly-goal-footnote')} diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index efed5ace6..6350745eb 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -20,6 +20,7 @@ export type LabelOptions = met?: {range: any[], mets: number} met_equivalent?: string, kgCo2PerKm: number, + text?: string, }[] } & { translations: { [lang: string]: { [translationKey: string]: string } @@ -118,3 +119,6 @@ export const getFakeEntry = (otherValue) => ({ text: labelKeyToReadable(otherValue), value: otherValue, }); + +export const labelKeyToRichMode = (labelKey: string) => + labelOptions?.MODE?.find(m => m.value == labelKey)?.text || labelKeyToReadable(labelKey); From 7dbc343dc107dd81146a124286d28a258f1e68eb Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 12 Sep 2023 11:02:32 -0400 Subject: [PATCH 107/164] remove old dashboard library nvd3 --- package.cordovabuild.json | 2 -- package.serve.json | 2 -- www/css/style.css | 12 ------------ 3 files changed, 16 deletions(-) diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 20672bad0..943f06520 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -109,7 +109,6 @@ "angular": "1.6.7", "angular-animate": "1.6.7", "angular-local-storage": "^0.7.1", - "angular-nvd3": "^1.0.7", "angular-sanitize": "1.6.7", "angular-simple-logger": "^0.1.7", "angular-translate": "^2.18.1", @@ -158,7 +157,6 @@ "moment-timezone": "^0.5.43", "ng-i18next": "^1.0.7", "npm": "^9.6.3", - "nvd3": "^1.8.6", "phonegap-plugin-barcodescanner": "git+https://github.com/phonegap/phonegap-plugin-barcodescanner#v8.1.0", "prop-types": "^15.8.1", "react": "^18.2.*", diff --git a/package.serve.json b/package.serve.json index 8137298a2..9e61cf834 100644 --- a/package.serve.json +++ b/package.serve.json @@ -51,7 +51,6 @@ "angular": "1.6.7", "angular-animate": "1.6.7", "angular-local-storage": "^0.7.1", - "angular-nvd3": "^1.0.7", "angular-sanitize": "1.6.7", "angular-simple-logger": "^0.1.7", "angular-translate": "^2.18.1", @@ -79,7 +78,6 @@ "moment-timezone": "^0.5.43", "ng-i18next": "^1.0.7", "npm": "^9.6.3", - "nvd3": "^1.8.6", "prop-types": "^15.8.1", "react": "^18.2.*", "react-chartjs-2": "^5.2.0", diff --git a/www/css/style.css b/www/css/style.css index 5e923f5bd..8910b2258 100644 --- a/www/css/style.css +++ b/www/css/style.css @@ -18,9 +18,6 @@ max-height: 50px; } -/* nvd3 styles */ -@import 'nvd3/build/nv.d3.css'; - .fill-container { display: block; position: relative; @@ -746,15 +743,6 @@ timestamp-badge[light-bg] { padding: 5% 10%; } -svg { - display: block; -} -#chart, #chart svg { - margin-right: 10px; -} -.nvd3, nv-noData { - font-weight: 300 !important; -} .metric-datepicker { /*height: 33px;*/ display: flex; /* establish flex container */ From c8ceafee6327b9edc75a52b279281a2fb663bd90 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 12 Sep 2023 10:22:58 -0600 Subject: [PATCH 108/164] refactor dialogStyle and create Action Menu instead of making dialogStyle a parameter, export and import the stylesheet where it is used, to maintain consistency and limit parameters Needed something similar to some of the dialogs in ProfileSettings for the SyncLogs -- so create a component `ActionMenu` to show the list of options, replacing the two hard-coded modals (carbon and force state) with an instance of ActionMenu --- www/js/components/ActionMenu.tsx | 41 +++++++++++++ www/js/control/AppStatusModal.tsx | 5 +- www/js/control/PopOpCode.jsx | 3 +- www/js/control/PrivacyPolicyModal.tsx | 5 +- www/js/control/ProfileSettings.jsx | 87 ++++++++------------------- www/js/diary/LabelTab.tsx | 1 - 6 files changed, 74 insertions(+), 68 deletions(-) create mode 100644 www/js/components/ActionMenu.tsx diff --git a/www/js/components/ActionMenu.tsx b/www/js/components/ActionMenu.tsx new file mode 100644 index 000000000..32546f3a7 --- /dev/null +++ b/www/js/components/ActionMenu.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { Modal } from "react-native"; +import { Dialog, Button, useTheme } from "react-native-paper"; +import { useTranslation } from "react-i18next"; +import { settingStyles } from "../control/ProfileSettings"; + +const ActionMenu = ({vis, setVis, actionSet, onAction, onExit}) => { + + const { t } = useTranslation(); + const { colors } = useTheme(); + + return ( + setVis(false)} + transparent={true}> + setVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {"Force State"} + + {actionSet.map((e) => + + )} + + + + + + + ) +} + +export default ActionMenu; \ No newline at end of file diff --git a/www/js/control/AppStatusModal.tsx b/www/js/control/AppStatusModal.tsx index 64c63f720..3f696adcc 100644 --- a/www/js/control/AppStatusModal.tsx +++ b/www/js/control/AppStatusModal.tsx @@ -8,8 +8,9 @@ import useAppConfig from "../useAppConfig"; import useAppStateChange from "../useAppStateChange"; import ExplainPermissions from "../appstatus/ExplainPermissions"; import AlertBar from "./AlertBar"; +import { settingStyles } from "./ProfileSettings"; -const AppStatusModal = ({permitVis, setPermitVis, dialogStyle, settingsScope}) => { +const AppStatusModal = ({permitVis, setPermitVis, settingsScope}) => { const { t } = useTranslation(); const { colors } = useTheme(); const { appConfig, loading } = useAppConfig(); @@ -412,7 +413,7 @@ const AppStatusModal = ({permitVis, setPermitVis, dialogStyle, settingsScope}) = setPermitVis(false)} transparent={true}> setPermitVis(false)} - style={dialogStyle}> + style={settingStyles.dialog(colors.elevation.level3)}> {t('consent.permissions')} diff --git a/www/js/control/PopOpCode.jsx b/www/js/control/PopOpCode.jsx index 8325b98c7..721b3d511 100644 --- a/www/js/control/PopOpCode.jsx +++ b/www/js/control/PopOpCode.jsx @@ -4,6 +4,7 @@ import { Button, Text, IconButton, Dialog, useTheme } from 'react-native-paper'; import { useTranslation } from "react-i18next"; import QrCode from "../components/QrCode"; import AlertBar from "./AlertBar"; +import { settingStyles } from "./ProfileSettings"; const PopOpCode = ({visibilityValue, tokenURL, action, setVis, dialogStyle}) => { const { t } = useTranslation(); @@ -31,7 +32,7 @@ const PopOpCode = ({visibilityValue, tokenURL, action, setVis, dialogStyle}) => transparent={true}> setVis(false)} - style={dialogStyle}> + style={settingStyles.dialog(colors.elevation.level3)}> {t("general-settings.qrcode")} {t("general-settings.qrcode-share-title")} diff --git a/www/js/control/PrivacyPolicyModal.tsx b/www/js/control/PrivacyPolicyModal.tsx index 7451581a6..9a0b06d07 100644 --- a/www/js/control/PrivacyPolicyModal.tsx +++ b/www/js/control/PrivacyPolicyModal.tsx @@ -4,8 +4,9 @@ import { Dialog, Button, useTheme } from 'react-native-paper'; import { useTranslation } from "react-i18next"; import useAppConfig from "../useAppConfig"; import i18next from "i18next"; +import { settingStyles } from "./ProfileSettings"; -const PrivacyPolicyModal = ({ privacyVis, setPrivacyVis, dialogStyle }) => { +const PrivacyPolicyModal = ({ privacyVis, setPrivacyVis }) => { const { t } = useTranslation(); const { height: windowHeight } = useWindowDimensions(); const { colors } = useTheme(); @@ -40,7 +41,7 @@ const PrivacyPolicyModal = ({ privacyVis, setPrivacyVis, dialogStyle }) => { setPrivacyVis(false)} transparent={true}> setPrivacyVis(false)} - style={dialogStyle}> + style={settingStyles.dialog(colors.elevation.level3)}> {t('consent-text.title')} diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index e3dab82e3..c59c1d4d7 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -14,6 +14,7 @@ import AlertBar from "./AlertBar"; import DataDatePicker from "./DataDatePicker"; import AppStatusModal from "./AppStatusModal"; import PrivacyPolicyModal from "./PrivacyPolicyModal"; +import ActionMenu from "../components/ActionMenu"; let controlUpdateCompleteListenerRegistered = false; @@ -458,6 +459,17 @@ const ProfileSettings = () => { }); } + const onSelectState = function(stateObject) { + ControlCollectionHelper.forceTransition(stateObject.transition); + } + + const onSelectCarbon = function(carbonObject) { + console.log("changeCarbonDataset(): chose locale " + carbonObject.value); + CarbonDatasetHelper.saveCurrentCarbonDatasetLocale(carbonObject.value); //there's some sort of error here + //Unhandled Promise Rejection: While logging, error -[NSNull UTF8String]: unrecognized selector sent to instance 0x7fff8a625fb0 + carbonDatasetString = i18next.t('general-settings.carbon-dataset') + ": " + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); + } + //conditional creation of setting sections let logUploadSection; @@ -484,7 +496,7 @@ const ProfileSettings = () => {
- + setPrivacyVis(true)}> {timePicker} @@ -521,7 +533,7 @@ const ProfileSettings = () => { transparent={true}> setNukeVis(false)} - style={styles.dialog(colors.elevation.level3)}> + style={settingStyles.dialog(colors.elevation.level3)}> {t('general-settings.clear-data')} - )} - - - - - - + clearNotifications()}> {/* force state sheet */} - setForceStateVis(false)} - transparent={true}> - setForceStateVis(false)} - style={styles.dialog(colors.elevation.level3)}> - {"Force State"} - - {stateActions.map((e) => - - )} - - - - - - + {/* opcode viewing popup */} - + {/* {view permissions} */} - + {/* {view privacy} */} - - + + {/* logout menu */} setLogoutVis(false)} transparent={true}> setLogoutVis(false)} - style={styles.dialog(colors.elevation.level3)}> + style={settingStyles.dialog(colors.elevation.level3)}> {t('general-settings.are-you-sure')} {t('general-settings.log-out-warning')} @@ -633,7 +596,7 @@ const ProfileSettings = () => { setDataPendingVis(false)} transparent={true}> setDataPendingVis(false)} - style={styles.dialog(colors.elevation.level3)}> + style={settingStyles.dialog(colors.elevation.level3)}> {t('data pending for push')} - -
- - -
{{entry.metadata.write_fmt_time}}
-
{{entry.data}}
-
-
- - From 7d0eb81b0dd6f3624decf98c39e7a3af59bb1307 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 12 Sep 2023 15:35:19 -0400 Subject: [PATCH 114/164] adjust ChangeIndicator colors and text -round percentage change to whole numbers because it is less cluttered that way -show % sign on both sides of the range - internationalize 'this week' -use transparency on background/border colors so it does not clash with the colors on the card -make 'success' color less washed-out --- www/i18n/en.json | 1 + www/js/appTheme.ts | 2 +- www/js/metrics/ChangeIndicator.tsx | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index 1e0fb83cc..8c9da147f 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -101,6 +101,7 @@ "less-than": " less than ", "less": " less ", "week-before": "vs. week before", + "this-week": "this week", "pick-a-date": "Pick a date", "trips": "trips", "hours": "hours", diff --git a/www/js/appTheme.ts b/www/js/appTheme.ts index a72388757..5f47f00b1 100644 --- a/www/js/appTheme.ts +++ b/www/js/appTheme.ts @@ -26,7 +26,7 @@ const AppTheme = { level4: '#e0f0ff', // lch(94% 50 250) level5: '#d6ebff', // lch(92% 50 250) }, - success: '#38872e', // lch(50% 55 135) + success: '#00a665', // lch(60% 55 155) warn: '#f8cf53', //lch(85% 65 85) danger: '#f23934' // lch(55% 85 35) }, diff --git a/www/js/metrics/ChangeIndicator.tsx b/www/js/metrics/ChangeIndicator.tsx index 047ca4794..ee73787b7 100644 --- a/www/js/metrics/ChangeIndicator.tsx +++ b/www/js/metrics/ChangeIndicator.tsx @@ -2,7 +2,7 @@ import React, {useMemo} from 'react'; import { View } from 'react-native'; import { useTheme, Text } from "react-native-paper"; import { useTranslation } from 'react-i18next'; -import { formatForDisplay } from '../config/useImperialConfig'; +import colorLib from "color"; type Props = { change: {low: number, high: number}, @@ -22,14 +22,14 @@ const ChangeIndicator = ({ change }) => { const changeText = useMemo(() => { if(change) { - let low = isNaN(change.low) ? '∞' : formatForDisplay(Math.abs(change.low)); - let high = isNaN(change.high) ? '∞' : formatForDisplay(Math.abs(change.high)); + let low = isNaN(change.low) ? '∞' : Math.round(Math.abs(change.low)); + let high = isNaN(change.high) ? '∞' : Math.round(Math.abs(change.high)); if(change.low == change.high) { - let text = changeSign(change.low) + low; + let text = changeSign(change.low) + low + "%"; return text; } else { - let text = changeSign(change.low) + low + " / " + changeSign(change.high) + high; + let text = `${changeSign(change.low) + low}% / ${changeSign(change.high) + high}%`; return text; } } @@ -39,10 +39,10 @@ const ChangeIndicator = ({ change }) => { (change.low) ? 0 ? colors.danger : colors.success)}> - {`${changeText}% \n`} + {changeText + '\n'} - {`${t("this week")}`} + {`${t("metrics.this-week")}`} : @@ -63,13 +63,13 @@ const styles: any = { fontSize: 16, }), view: (color) => ({ - backgroundColor: color, + backgroundColor: colorLib(color).alpha(0.7).rgb().toString(), padding: 2, borderStyle: 'solid', - borderColor: color, + borderColor: colorLib(color).lighten(0.25).alpha(0.8).rgb().toString(), borderWidth: 2, borderRadius: 10, }), } -export default ChangeIndicator; \ No newline at end of file +export default ChangeIndicator; From f1f9b57533b0dfa6f7f202f30049ae53b87abb27 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 12 Sep 2023 14:20:05 -0600 Subject: [PATCH 115/164] update log page to load more previously, was not loading more when you hit the end of the list, now the list loads more when you reach the end --- www/js/control/LogPage.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/www/js/control/LogPage.tsx b/www/js/control/LogPage.tsx index b0e438472..90a363bcc 100644 --- a/www/js/control/LogPage.tsx +++ b/www/js/control/LogPage.tsx @@ -14,7 +14,6 @@ const LogPage = ({pageVis, setPageVis}) => { const { t } = useTranslation(); const { colors } = useTheme(); const EmailHelper = getAngularService('EmailHelper'); - const $state = getAngularService('$state'); const { appConfig, loading } = useAppConfig(); const [ loadStats, setLoadStats ] = useState(); @@ -25,13 +24,15 @@ const LogPage = ({pageVis, setPageVis}) => { const [ maxMessage, setMaxMessage ] = useState(""); const [ logMessage, setLogMessage ] = useState(""); + const [ isFetching, setIsFetching ] = useState(false); + var RETRIEVE_COUNT = 100; useEffect(() => { refreshEntries(); }, [appConfig]); - const refreshEntries = function() { + async function refreshEntries() { window?.Logger.getMaxIndex().then(function(maxIndex) { console.log("maxIndex = "+maxIndex); let tempStats = {} as loadStats; @@ -40,17 +41,17 @@ const LogPage = ({pageVis, setPageVis}) => { tempStats.reachedEnd = false; setLoadStats(tempStats); setEntries([]); - addEntries(); }, function(error) { let errorString = "While getting max index "+JSON.stringify(error, null, 2); console.log(errorString); setMaxMessage(errorString); setMaxErrorVis(true); }) + addEntries(); } const moreDataCanBeLoaded = useMemo(() => { - return loadStats?.gotMaxIndex && loadStats?.reachedEnd; + return loadStats?.gotMaxIndex && !loadStats?.reachedEnd; }, [loadStats]) const clear = function() { @@ -61,16 +62,19 @@ const LogPage = ({pageVis, setPageVis}) => { async function addEntries() { console.log("calling addEntries"); + setIsFetching(true); window.Logger.getMessagesFromIndex(loadStats?.currentStart, RETRIEVE_COUNT) .then(function(entryList) { processEntries(entryList); console.log("entry list size = "+ entries.length); + setIsFetching(false); //$scope.$broadcast('scroll.infiniteScrollComplete') //do I still need this? }, function(error) { let errStr = "While getting messages from the log "+JSON.stringify(error, null, 2); console.log(errStr); setLogMessage(errStr); setLogErrorVis(true); + setIsFetching(false); //$scope.$broadcast('scroll.infiniteScrollComplete') //do I still need this? }) } @@ -123,7 +127,10 @@ const LogPage = ({pageVis, setPageVis}) => { estimatedItemSize={75} keyExtractor={(item) => item.ID} ItemSeparatorComponent={separator} - onScroll={e => {if(moreDataCanBeLoaded){addEntries()}}} + onEndReachedThreshold={0.5} + refreshing={isFetching} + onRefresh={() => {if(moreDataCanBeLoaded){addEntries()}}} + onEndReached={() => {if(moreDataCanBeLoaded){addEntries()}}} /> From e21cb6e1151b6154eb8e8907bbad16e2a957d69d Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 12 Sep 2023 14:45:13 -0600 Subject: [PATCH 116/164] load on enter load the log pages when their visibility changes, this seems cleaner than tying it to the config, since this is unrelated to the config --- www/js/control/LogPage.tsx | 5 ++--- www/js/control/SensedPage.tsx | 7 ++----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/www/js/control/LogPage.tsx b/www/js/control/LogPage.tsx index 90a363bcc..9e1628a10 100644 --- a/www/js/control/LogPage.tsx +++ b/www/js/control/LogPage.tsx @@ -4,7 +4,6 @@ import { useTheme, Text, Appbar, IconButton } from "react-native-paper"; import { getAngularService } from "../angular-react-helper"; import { useTranslation } from "react-i18next"; import { FlashList } from '@shopify/flash-list'; -import useAppConfig from "../useAppConfig"; import moment from "moment"; import AlertBar from "./AlertBar"; @@ -14,7 +13,6 @@ const LogPage = ({pageVis, setPageVis}) => { const { t } = useTranslation(); const { colors } = useTheme(); const EmailHelper = getAngularService('EmailHelper'); - const { appConfig, loading } = useAppConfig(); const [ loadStats, setLoadStats ] = useState(); const [ entries, setEntries ] = useState([]); @@ -28,9 +26,10 @@ const LogPage = ({pageVis, setPageVis}) => { var RETRIEVE_COUNT = 100; + //when opening the modal, load the entries useEffect(() => { refreshEntries(); - }, [appConfig]); + }, [pageVis]); async function refreshEntries() { window?.Logger.getMaxIndex().then(function(maxIndex) { diff --git a/www/js/control/SensedPage.tsx b/www/js/control/SensedPage.tsx index 86cbee3bc..a25422b98 100644 --- a/www/js/control/SensedPage.tsx +++ b/www/js/control/SensedPage.tsx @@ -4,7 +4,6 @@ import { useTheme, Appbar, IconButton } from "react-native-paper"; import { getAngularService } from "../angular-react-helper"; import { useTranslation } from "react-i18next"; import { FlashList } from '@shopify/flash-list'; -import useAppConfig from "../useAppConfig"; import moment from "moment"; import ActionMenu from "../components/ActionMenu"; @@ -14,7 +13,6 @@ const SensedPage = ({pageVis, setPageVis}) => { const { t } = useTranslation(); const { colors } = useTheme(); const EmailHelper = getAngularService('EmailHelper'); - const { appConfig, loading } = useAppConfig(); /* Let's keep a reference to the database for convenience */ const [ DB, setDB ]= useState(); @@ -25,8 +23,6 @@ const SensedPage = ({pageVis, setPageVis}) => { const [ entries, setEntries ] = useState([]); const setup = function() { - setDB(window?.cordova.plugins.BEMUserCache); - if(DB) { let tempConfig = {} as configObject; tempConfig.key_data_mapping = { @@ -108,8 +104,9 @@ const SensedPage = ({pageVis, setPageVis}) => { } useEffect(() => { + setDB(window.cordova.plugins.BEMUserCache); setup(); - }, [appConfig]); + }, [pageVis]); const separator = () => const cacheItem = ({item: cacheItem}) => ( From 92ad2eecd66075401d7791df1da695050bbaaeab Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 12 Sep 2023 16:45:49 -0400 Subject: [PATCH 117/164] fix dateRange issue The use of UTC time here was inconsistent with how MetricsDateSelect works - MetricsDateSelect uses the local timezone for date selection. DateTime.utc returns a DateTime object which may be on a different day than the day in the local timezone. MetricsDateSelect works properly for this and the initial load should follow suit. --- www/js/metrics/MetricsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 16cbb742e..25020b435 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -34,7 +34,7 @@ async function fetchMetricsFromServer(type: 'user'|'aggregate', dateRange: DateT } function getLastTwoWeeksDtRange() { - const now = DateTime.utc().startOf('day'); + const now = DateTime.now().startOf('day'); const start = now.minus({ days: 15 }); const end = now.minus({ days: 1 }); return [start, end]; From 496b353dc7c61e598bc1e05261b1f5acfc359901 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 12 Sep 2023 15:04:23 -0600 Subject: [PATCH 118/164] refactor .then() to try/catch --- www/js/control/LogPage.tsx | 40 +++++++++++++++++------------------ www/js/control/SensedPage.tsx | 12 +++++------ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/www/js/control/LogPage.tsx b/www/js/control/LogPage.tsx index 9e1628a10..8b8c572be 100644 --- a/www/js/control/LogPage.tsx +++ b/www/js/control/LogPage.tsx @@ -18,10 +18,8 @@ const LogPage = ({pageVis, setPageVis}) => { const [ entries, setEntries ] = useState([]); const [ maxErrorVis, setMaxErrorVis ] = useState(false); const [ logErrorVis, setLogErrorVis ] = useState(false); - const [ maxMessage, setMaxMessage ] = useState(""); const [ logMessage, setLogMessage ] = useState(""); - const [ isFetching, setIsFetching ] = useState(false); var RETRIEVE_COUNT = 100; @@ -32,7 +30,8 @@ const LogPage = ({pageVis, setPageVis}) => { }, [pageVis]); async function refreshEntries() { - window?.Logger.getMaxIndex().then(function(maxIndex) { + try { + let maxIndex = await window.Logger.getMaxIndex(); console.log("maxIndex = "+maxIndex); let tempStats = {} as loadStats; tempStats.currentStart = maxIndex; @@ -40,13 +39,14 @@ const LogPage = ({pageVis, setPageVis}) => { tempStats.reachedEnd = false; setLoadStats(tempStats); setEntries([]); - }, function(error) { + } catch(error) { let errorString = "While getting max index "+JSON.stringify(error, null, 2); console.log(errorString); setMaxMessage(errorString); setMaxErrorVis(true); - }) - addEntries(); + } finally { + addEntries(); + } } const moreDataCanBeLoaded = useMemo(() => { @@ -62,20 +62,20 @@ const LogPage = ({pageVis, setPageVis}) => { async function addEntries() { console.log("calling addEntries"); setIsFetching(true); - window.Logger.getMessagesFromIndex(loadStats?.currentStart, RETRIEVE_COUNT) - .then(function(entryList) { - processEntries(entryList); - console.log("entry list size = "+ entries.length); - setIsFetching(false); - //$scope.$broadcast('scroll.infiniteScrollComplete') //do I still need this? - }, function(error) { - let errStr = "While getting messages from the log "+JSON.stringify(error, null, 2); - console.log(errStr); - setLogMessage(errStr); - setLogErrorVis(true); - setIsFetching(false); - //$scope.$broadcast('scroll.infiniteScrollComplete') //do I still need this? - }) + try { + let entryList = await window.Logger.getMessagesFromIndex(loadStats?.currentStart, RETRIEVE_COUNT); + processEntries(entryList); + console.log("entry list size = "+ entries.length); + setIsFetching(false); + //$scope.$broadcast('scroll.infiniteScrollComplete') //do I still need this? + } catch(error) { + let errStr = "While getting messages from the log "+JSON.stringify(error, null, 2); + console.log(errStr); + setLogMessage(errStr); + setLogErrorVis(true); + setIsFetching(false); + //$scope.$broadcast('scroll.infiniteScrollComplete') //do I still need this? + } } const processEntries = function(entryList) { diff --git a/www/js/control/SensedPage.tsx b/www/js/control/SensedPage.tsx index a25422b98..d78b935d5 100644 --- a/www/js/control/SensedPage.tsx +++ b/www/js/control/SensedPage.tsx @@ -61,7 +61,7 @@ const SensedPage = ({pageVis, setPageVis}) => { setSelectedKey(newVal); } - const updateEntries = function() { + async function updateEntries() { let userCacheFn, userCacheKey; if(selectedKey == "") { userCacheFn = DB.getAllMessages; @@ -71,7 +71,8 @@ const SensedPage = ({pageVis, setPageVis}) => { userCacheKey = config.key_data_mapping[selectedKey]["key"]; } - userCacheFn(userCacheKey, true).then(function(entryList) { + try { + let entryList = await userCacheFn(userCacheKey, true); let tempEntries = []; entryList.forEach(entry => { entry.metadata.write_fmt_time = moment.unix(entry.metadata.write_ts) @@ -81,11 +82,10 @@ const SensedPage = ({pageVis, setPageVis}) => { tempEntries.push(entry); }); setEntries(tempEntries); - // This should really be within a try/catch/finally block - //$scope.$broadcast('scroll.refreshComplete'); //---> what to do instead? - }, function(error) { + } + catch(error) { window.Logger.log(window.Logger.LEVEL_ERROR, "Error updating entries"+ error); - }) + } } //update entries anytime the selected key changes From fe44ae6cac50a2a9e6b3f81fe06f83e86f3096a6 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 12 Sep 2023 15:27:54 -0600 Subject: [PATCH 119/164] resolve error I was getting a mysterious "unrecognized selector" error and came to the conclusion that it was because loadStats.currentStart was null on first load, by setting a fallback, the error is resolved --- www/js/control/LogPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/js/control/LogPage.tsx b/www/js/control/LogPage.tsx index 8b8c572be..08b0c86fb 100644 --- a/www/js/control/LogPage.tsx +++ b/www/js/control/LogPage.tsx @@ -62,8 +62,9 @@ const LogPage = ({pageVis, setPageVis}) => { async function addEntries() { console.log("calling addEntries"); setIsFetching(true); + let start = loadStats.currentStart ? loadStats.currentStart : 0; //set a default start to prevent initial fetch error try { - let entryList = await window.Logger.getMessagesFromIndex(loadStats?.currentStart, RETRIEVE_COUNT); + let entryList = await window.Logger.getMessagesFromIndex(start, RETRIEVE_COUNT); processEntries(entryList); console.log("entry list size = "+ entries.length); setIsFetching(false); From e50f0a1d1d41892e8249972440bad989de45d59c Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 12 Sep 2023 15:30:57 -0600 Subject: [PATCH 120/164] remove infiniteScrollComplete broadcasts these had been commented out, and by searching the codebase I came to the conclusion that they were for communicating with angular/ionic, which we no longer use unlike other broadcasts, this is not received by other parts of our code, to trigger some action, so we don't need it --- www/js/control/LogPage.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/www/js/control/LogPage.tsx b/www/js/control/LogPage.tsx index 08b0c86fb..d722127b3 100644 --- a/www/js/control/LogPage.tsx +++ b/www/js/control/LogPage.tsx @@ -68,14 +68,12 @@ const LogPage = ({pageVis, setPageVis}) => { processEntries(entryList); console.log("entry list size = "+ entries.length); setIsFetching(false); - //$scope.$broadcast('scroll.infiniteScrollComplete') //do I still need this? } catch(error) { let errStr = "While getting messages from the log "+JSON.stringify(error, null, 2); console.log(errStr); setLogMessage(errStr); setLogErrorVis(true); setIsFetching(false); - //$scope.$broadcast('scroll.infiniteScrollComplete') //do I still need this? } } From fd32aee97f2be31c6f3e3265d900fc9275d2d158 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 12 Sep 2023 15:45:03 -0600 Subject: [PATCH 121/164] fix sensed formatting I was using react-native Text when I wanted react-native-paper Text --- www/js/control/SensedPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/control/SensedPage.tsx b/www/js/control/SensedPage.tsx index d78b935d5..f46086ad7 100644 --- a/www/js/control/SensedPage.tsx +++ b/www/js/control/SensedPage.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; -import { View, StyleSheet, SafeAreaView, Text, Modal } from "react-native"; -import { useTheme, Appbar, IconButton } from "react-native-paper"; +import { View, StyleSheet, SafeAreaView, Modal } from "react-native"; +import { useTheme, Appbar, IconButton, Text } from "react-native-paper"; import { getAngularService } from "../angular-react-helper"; import { useTranslation } from "react-i18next"; import { FlashList } from '@shopify/flash-list'; From d20bf2e89333b6ac807abb04b49993f31653a65f Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 13 Sep 2023 09:14:12 -0600 Subject: [PATCH 122/164] i18n for ui strings Adding translations for the user-visible strings to ensure users can access the dev zone, the logs will be in English, but with headers and error messages in selected language participating in debugging will be easier --- www/i18n/en.json | 7 +++++++ www/js/control/LogPage.tsx | 10 +++++----- www/js/control/SensedPage.tsx | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index 758960ade..b7c14d0e6 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -36,7 +36,9 @@ "nuke-all": "Nuke all buffers and cache", "test-notification": "Test local notification", "check-log": "Check log", + "log-title" : "Log", "check-sensed-data": "Check sensed data", + "sensed-title": "Sensed Data", "collection": "Collection", "sync": "Sync", "button-accept": "I accept", @@ -48,6 +50,11 @@ "log-out": "Log Out" }, + "dev-errors" : { + "while-messages": "While getting messages from the log ", + "while-max" : "While getting max index " + }, + "general-settings":{ "choose-date" : "Choose date to download data", "choose-dataset" : "Choose a dataset for carbon footprint calculations", diff --git a/www/js/control/LogPage.tsx b/www/js/control/LogPage.tsx index d722127b3..6a417e879 100644 --- a/www/js/control/LogPage.tsx +++ b/www/js/control/LogPage.tsx @@ -40,7 +40,7 @@ const LogPage = ({pageVis, setPageVis}) => { setLoadStats(tempStats); setEntries([]); } catch(error) { - let errorString = "While getting max index "+JSON.stringify(error, null, 2); + let errorString = t('dev-errors.while-max')+JSON.stringify(error, null, 2); console.log(errorString); setMaxMessage(errorString); setMaxErrorVis(true); @@ -69,7 +69,7 @@ const LogPage = ({pageVis, setPageVis}) => { console.log("entry list size = "+ entries.length); setIsFetching(false); } catch(error) { - let errStr = "While getting messages from the log "+JSON.stringify(error, null, 2); + let errStr = t('dev-errors.while-messages')+JSON.stringify(error, null, 2); console.log(errStr); setLogMessage(errStr); setLogErrorVis(true); @@ -110,7 +110,7 @@ const LogPage = ({pageVis, setPageVis}) => { {setPageVis(false)}}/> - + @@ -132,8 +132,8 @@ const LogPage = ({pageVis, setPageVis}) => { /> - - + + ); }; diff --git a/www/js/control/SensedPage.tsx b/www/js/control/SensedPage.tsx index f46086ad7..cc667a467 100644 --- a/www/js/control/SensedPage.tsx +++ b/www/js/control/SensedPage.tsx @@ -119,7 +119,7 @@ const SensedPage = ({pageVis, setPageVis}) => { setPageVis(false)}/> - + From f132056736404a7f79825c8337ac1e226e806686 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 13 Sep 2023 09:14:38 -0600 Subject: [PATCH 123/164] remove old functions we don't need these show sensed or show logs anymore! --- www/js/control/general-settings.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/www/js/control/general-settings.js b/www/js/control/general-settings.js index 5225af1c6..e29702d6e 100644 --- a/www/js/control/general-settings.js +++ b/www/js/control/general-settings.js @@ -33,12 +33,4 @@ angular.module('emission.main.control',['emission.services', //to change reminder time if accessing profile by specific android notification flow //would open the date picker - //TODO create React pages and use React routing - $scope.showLog = function() { - $state.go("root.main.log"); - } - $scope.showSensed = function() { - $state.go("root.main.sensed"); - } - }); From 723c5e77385cd6450710286b42e94978ca2fb51c Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 13 Sep 2023 09:27:13 -0600 Subject: [PATCH 124/164] factor out "Locations" and "Motion Type" these two options are not supported, so we should not present them to the user -- removing them leaves us with one option, so no need for an options menu This reduces the complexity of the sensed data page significantly note about removing options: https://github.com/e-mission/e-mission-phone/pull/1029#issuecomment-1716907821 --- www/i18n/en.json | 2 +- www/js/control/SensedPage.tsx | 76 +++-------------------------------- 2 files changed, 6 insertions(+), 72 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index b7c14d0e6..f3c203b3d 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -38,7 +38,7 @@ "check-log": "Check log", "log-title" : "Log", "check-sensed-data": "Check sensed data", - "sensed-title": "Sensed Data", + "sensed-title": "Sensed Data: Transitions", "collection": "Collection", "sync": "Sync", "button-accept": "I accept", diff --git a/www/js/control/SensedPage.tsx b/www/js/control/SensedPage.tsx index cc667a467..8de1e7e5f 100644 --- a/www/js/control/SensedPage.tsx +++ b/www/js/control/SensedPage.tsx @@ -5,9 +5,6 @@ import { getAngularService } from "../angular-react-helper"; import { useTranslation } from "react-i18next"; import { FlashList } from '@shopify/flash-list'; import moment from "moment"; -import ActionMenu from "../components/ActionMenu"; - -type configObject = { key_data_mapping: object, keys: string[], keyMap: {}[] }; const SensedPage = ({pageVis, setPageVis}) => { const { t } = useTranslation(); @@ -16,61 +13,16 @@ const SensedPage = ({pageVis, setPageVis}) => { /* Let's keep a reference to the database for convenience */ const [ DB, setDB ]= useState(); - - const [ config, setConfig ] = useState(); - const [ selectedKey, setSelectedKey ] = useState(""); - const [ keysVisible, setKeysVisible ] = useState(false); const [ entries, setEntries ] = useState([]); - const setup = function() { - if(DB) { - let tempConfig = {} as configObject; - tempConfig.key_data_mapping = { - "Transitions": { - fn: DB.getAllMessages, - key: "statemachine/transition" - }, - "Locations": { - fn: DB.getAllSensorData, - key: "background/location" - }, - "Motion Type": { - fn: DB.getAllSensorData, - key: "background/motion_activity" - }, - } - - tempConfig.keys = []; - for (let key in tempConfig.key_data_mapping) { - tempConfig.keys.push(key); - } - - tempConfig.keyMap = mapForActionMenu(tempConfig.keys); - - setSelectedKey(tempConfig.keys[0]); - setConfig(tempConfig); - updateEntries(); - } - } - const emailCache = function() { EmailHelper.sendEmail("userCacheDB"); } - const setSelected = function(newVal) { - setSelectedKey(newVal); - } - async function updateEntries() { - let userCacheFn, userCacheKey; - if(selectedKey == "") { - userCacheFn = DB.getAllMessages; - userCacheKey = "statemachine/transition"; - } else { - userCacheFn = config.key_data_mapping[selectedKey]["fn"]; - userCacheKey = config.key_data_mapping[selectedKey]["key"]; - } - + //hardcoded function and keys after eliminating bit-rotted options + let userCacheFn = DB.getAllMessages; + let userCacheKey = "statemachine/transition"; try { let entryList = await userCacheFn(userCacheKey, true); let tempEntries = []; @@ -88,24 +40,9 @@ const SensedPage = ({pageVis, setPageVis}) => { } } - //update entries anytime the selected key changes - useEffect(() => { - if(DB){ - updateEntries(); - } - }, [selectedKey]) - - const mapForActionMenu = function(keys) { - let map = []; - keys.forEach(key => { - map.push({text: key}); - }); - return map; - } - useEffect(() => { setDB(window.cordova.plugins.BEMUserCache); - setup(); + updateEntries(); }, [pageVis]); const separator = () => @@ -119,13 +56,12 @@ const SensedPage = ({pageVis, setPageVis}) => { setPageVis(false)}/> - + updateEntries()}/> emailCache()}/> - setKeysVisible(true)}/> { ItemSeparatorComponent={separator} /> - - setSelected(key.text)} onExit={() => {}}> ); }; From d04d5ee8ed372ed142f979be6782b8a1f9d755cd Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 13 Sep 2023 09:47:56 -0600 Subject: [PATCH 125/164] combine dev-errors into errors --- www/i18n/en.json | 9 +++------ www/js/control/LogPage.tsx | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index f3c203b3d..2e164eacb 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -50,11 +50,6 @@ "log-out": "Log Out" }, - "dev-errors" : { - "while-messages": "While getting messages from the log ", - "while-max" : "While getting max index " - }, - "general-settings":{ "choose-date" : "Choose date to download data", "choose-dataset" : "Choose a dataset for carbon footprint calculations", @@ -367,7 +362,9 @@ "errors": { "while-populating-composite": "Error while populating composite trips", "while-loading-another-week": "Error while loading travel of {{when}} week", - "while-loading-specific-week": "Error while loading travel for the week of {{day}}" + "while-loading-specific-week": "Error while loading travel for the week of {{day}}", + "while-log-messages": "While getting messages from the log ", + "while-max-index" : "While getting max index " }, "consent-text": { "title":"NREL OPENPATH PRIVACY POLICY/TERMS OF USE", diff --git a/www/js/control/LogPage.tsx b/www/js/control/LogPage.tsx index 6a417e879..7d6d279ee 100644 --- a/www/js/control/LogPage.tsx +++ b/www/js/control/LogPage.tsx @@ -40,7 +40,7 @@ const LogPage = ({pageVis, setPageVis}) => { setLoadStats(tempStats); setEntries([]); } catch(error) { - let errorString = t('dev-errors.while-max')+JSON.stringify(error, null, 2); + let errorString = t('errors.while-max-index')+JSON.stringify(error, null, 2); console.log(errorString); setMaxMessage(errorString); setMaxErrorVis(true); @@ -69,7 +69,7 @@ const LogPage = ({pageVis, setPageVis}) => { console.log("entry list size = "+ entries.length); setIsFetching(false); } catch(error) { - let errStr = t('dev-errors.while-messages')+JSON.stringify(error, null, 2); + let errStr = t('errors.while-log-messages')+JSON.stringify(error, null, 2); console.log(errStr); setLogMessage(errStr); setLogErrorVis(true); From a3b0c08b28ab4cce96045f46592a9b5d298bfdae Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 13 Sep 2023 23:49:17 -0400 Subject: [PATCH 126/164] show Unlabeled as faint grey, last item in legend --- www/js/metrics/MetricsCard.tsx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index 9676ae6ec..7a0f8c8bc 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -2,13 +2,14 @@ import React, { useMemo, useState } from 'react'; import { View } from 'react-native'; import { Card, Checkbox, Text, useTheme } from 'react-native-paper'; +import colorLib from "color"; import BarChart from '../components/BarChart'; import { DayOfMetricData } from './metricsTypes'; import { formatDateRangeOfDays, getLabelsForDay, getUniqueLabelsForDays } from './metricsHelper'; import ToggleSwitch from '../components/ToggleSwitch'; import { cardStyles } from './MetricsTab'; import { labelKeyToRichMode, labelOptions } from '../survey/multilabel/confirmHelper'; -import { getBaseModeByText } from '../diary/diaryHelper'; +import { getBaseModeByKey, getBaseModeByText } from '../diary/diaryHelper'; import { useTranslation } from 'react-i18next'; type Props = { @@ -44,6 +45,12 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni }); }); }); + // sort records (affects the order they appear in the chart legend) + records.sort((a, b) => { + if (a.label == 'Unlabeled') return 1; // sort Unlabeled to the end + if (b.label == 'Unlabeled') return -1; // sort Unlabeled to the end + return (a.y as number) - (b.y as number); // otherwise, just sort by time + }); return records; }, [metricDataDays, viewMode]); @@ -69,6 +76,16 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni return vals; }, [metricDataDays, viewMode]); + // Unlabelled data shows up as 'UNKNOWN' grey and mostly transparent + // All other modes are colored according to their base mode + const getColorForLabel = (label: string) => { + if (label == "Unlabeled") { + const unknownModeColor = getBaseModeByKey('UNKNOWN').color; + return colorLib(unknownModeColor).alpha(0.15).rgb().string(); + } + return getBaseModeByText(label, labelOptions).color; + } + return ( getBaseModeByText(l, labelOptions).color} /> + getColorForLabel={getColorForLabel} /> Stack bars: Date: Wed, 13 Sep 2023 23:53:21 -0400 Subject: [PATCH 127/164] re-order "Prev. week" before "Past week", refactor Order changed on Carbon + Active minutes cards - Prev week comes first to establish a trend. - 'filterToRecentWeeks' refactored into 'segmentDaysByWeeks'. Although it does largely the same thing, the new one is more generic because it works for any number of weeks, not just 2 --- www/js/metrics/ActiveMinutesTableCard.tsx | 4 +- www/js/metrics/CarbonFootprintCard.tsx | 45 +++++++++++----------- www/js/metrics/CarbonTextCard.tsx | 45 +++++++++++----------- www/js/metrics/WeeklyActiveMinutesCard.tsx | 19 +++++---- www/js/metrics/metricsHelper.ts | 14 +++---- 5 files changed, 62 insertions(+), 65 deletions(-) diff --git a/www/js/metrics/ActiveMinutesTableCard.tsx b/www/js/metrics/ActiveMinutesTableCard.tsx index 8bcdbf0a4..ea360ce8e 100644 --- a/www/js/metrics/ActiveMinutesTableCard.tsx +++ b/www/js/metrics/ActiveMinutesTableCard.tsx @@ -2,7 +2,7 @@ import React, { useMemo, useState } from 'react'; import { Card, DataTable, useTheme } from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; -import { filterToRecentWeeks, formatDate, formatDateRangeOfDays, secondsToMinutes } from './metricsHelper'; +import { formatDate, formatDateRangeOfDays, secondsToMinutes, segmentDaysByWeeks } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import { ACTIVE_MODES } from './WeeklyActiveMinutesCard'; import { labelKeyToRichMode } from '../survey/multilabel/confirmHelper'; @@ -28,7 +28,7 @@ const ActiveMinutesTableCard = ({ userMetrics }: Props) => { const recentWeeksActiveModesTotals = useMemo(() => { if (!userMetrics?.duration) return []; - return filterToRecentWeeks(userMetrics.duration).map(week => { + return segmentDaysByWeeks(userMetrics.duration).reverse().map(week => { const totals = {}; ACTIVE_MODES.forEach(mode => { const sum = week.reduce((acc, day) => ( diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 540c4fc47..6e0b3eb81 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -3,7 +3,7 @@ import { View } from 'react-native'; import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; -import { filterToRecentWeeks, formatDateRangeOfDays, parseDataFromMetrics, generateSummaryFromData, calculatePercentChange } from './metricsHelper'; +import { formatDateRangeOfDays, parseDataFromMetrics, generateSummaryFromData, calculatePercentChange, segmentDaysByWeeks } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; import { getAngularService } from '../angular-react-helper'; @@ -21,13 +21,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { const userCarbonRecords = useMemo(() => { if(userMetrics?.distance?.length > 0) { //separate data into weeks - let thisWeekDistance = filterToRecentWeeks(userMetrics?.distance)[0]; - let lastWeekDistance = filterToRecentWeeks(userMetrics?.distance)[1]; - - //formatted distance data from this week - let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); - let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); - let worstDistance = userThisWeekSummaryMap.reduce((prevDistance, currModeSummary) => prevDistance + currModeSummary.values, 0); + const [thisWeekDistance, lastWeekDistance] = segmentDaysByWeeks(userMetrics?.distance, 2); //formatted data from last week let userLastWeekModeMap = {}; @@ -37,32 +31,37 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); } + //formatted distance data from this week + let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); + let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); + let worstDistance = userThisWeekSummaryMap.reduce((prevDistance, currModeSummary) => prevDistance + currModeSummary.values, 0); + //setting up data to be displayed let graphRecords = []; - //calculate low-high and format range for past week - let userPastWeek = { - low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, FootprintHelper.getHighestFootprint()), - }; - graphRecords.push({label: t('main-metrics.unlabeled'), x: userPastWeek.high - userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}) - graphRecords.push({label: t('main-metrics.labeled'), x: userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); - //calculate low-high and format range for prev week, if exists + let userPrevWeek; if(userLastWeekSummaryMap[0]) { - let userPrevWeek = { + userPrevWeek = { low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) }; graphRecords.push({label: t('main-metrics.unlabeled'), x: userPrevWeek.high - userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}) graphRecords.push({label: t('main-metrics.labeled'), x: userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}); - + } + + //calculate low-high and format range for past week + let userPastWeek = { + low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), + high: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, FootprintHelper.getHighestFootprint()), + }; + graphRecords.push({label: t('main-metrics.unlabeled'), x: userPastWeek.high - userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}) + graphRecords.push({label: t('main-metrics.labeled'), x: userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); + if (userPrevWeek) { let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); setEmissionsChange(pctChange); - } else { - setEmissionsChange({}); } - + //calculate worst-case carbon footprint let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); graphRecords.push({label: t('main-metrics.labeled'), x: worstCarbon, y: `${t('main-metrics.worst-case')}`}); @@ -75,7 +74,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { if(aggMetrics?.distance?.length > 0) { //separate data into weeks - let thisWeekDistance = filterToRecentWeeks(aggMetrics?.distance)[0]; + const thisWeekDistance = segmentDaysByWeeks(aggMetrics?.distance, 1)[0]; console.log("testing agg metrics" , aggMetrics, thisWeekDistance); let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, "aggregate"); @@ -120,7 +119,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { }, [userCarbonRecords, groupCarbonRecords]); const cardSubtitleText = useMemo(() => { - const recentEntries = filterToRecentWeeks(aggMetrics?.distance).reverse().flat(); + const recentEntries = segmentDaysByWeeks(aggMetrics?.distance, 2).reverse().flat(); const recentEntriesRange = formatDateRangeOfDays(recentEntries); return `${t('main-metrics.estimated-emissions')}, (${recentEntriesRange})`; }, [aggMetrics?.distance]); diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index eb1058336..31d4f717a 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -4,7 +4,7 @@ import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; import { useTranslation } from 'react-i18next'; -import { filterToRecentWeeks, formatDateRangeOfDays, parseDataFromMetrics, generateSummaryFromData, calculatePercentChange } from './metricsHelper'; +import { formatDateRangeOfDays, parseDataFromMetrics, generateSummaryFromData, calculatePercentChange, segmentDaysByWeeks } from './metricsHelper'; import { getAngularService } from '../angular-react-helper'; type Props = { userMetrics: MetricsData, aggMetrics: MetricsData } @@ -17,13 +17,7 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { const userText = useMemo(() => { if(userMetrics?.distance?.length > 0) { //separate data into weeks - let thisWeekDistance = filterToRecentWeeks(userMetrics?.distance)[0]; - let lastWeekDistance = filterToRecentWeeks(userMetrics?.distance)[1]; - - //formatted distance data from this week - let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); - let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); - let worstDistance = userThisWeekSummaryMap.reduce((prevDistance, currModeSummary) => prevDistance + currModeSummary.values, 0); + const [thisWeekDistance, lastWeekDistance] = segmentDaysByWeeks(userMetrics?.distance, 2); //formatted data from last week let userLastWeekModeMap = {}; @@ -33,9 +27,27 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); } + //formatted distance data from this week + let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); + let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); + let worstDistance = userThisWeekSummaryMap.reduce((prevDistance, currModeSummary) => prevDistance + currModeSummary.values, 0); + //setting up data to be displayed let textList = []; + //calculate low-high and format range for prev week, if exists + if(userLastWeekSummaryMap[0]) { + let userPrevWeek = { + low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), + high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) + }; + const label = `${t('main-metrics.prev-week')} (${formatDateRangeOfDays(lastWeekDistance)})`; + if (userPrevWeek.low == userPrevWeek.high) + textList.push({label: label, value: Math.round(userPrevWeek.low)}); + else + textList.push({label: label + '²', value: `${Math.round(userPrevWeek.low)} - ${Math.round(userPrevWeek.high)}`}); + } + //calculate low-high and format range for past week let userPastWeek = { low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), @@ -46,19 +58,6 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { textList.push({label: label, value: Math.round(userPastWeek.low)}); else textList.push({label: label + '²', value: `${Math.round(userPastWeek.low)} - ${Math.round(userPastWeek.high)}`}); - - //calculate low-high and format range for prev week, if exists - if(userLastWeekSummaryMap[0]) { - let userPrevWeek = { - low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) - }; - const label = `${t('main-metrics.prev-week')} (${formatDateRangeOfDays(lastWeekDistance)})`; - if (userPrevWeek.low == userPrevWeek.high) - textList.push({label: label, value: Math.round(userPrevWeek.low)}); - else - textList.push({label: label + '²', value: `${Math.round(userPrevWeek.low)} - ${Math.round(userPrevWeek.high)}`}); - } //calculate worst-case carbon footprint let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); @@ -72,7 +71,7 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { if(aggMetrics?.distance?.length > 0) { //separate data into weeks - let thisWeekDistance = filterToRecentWeeks(aggMetrics?.distance)[0]; + const thisWeekDistance = segmentDaysByWeeks(userMetrics?.distance, 1)[0]; let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, "aggregate"); let aggThisWeekSummary = generateSummaryFromData(aggThisWeekModeMap, "distance"); @@ -117,7 +116,7 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { }, [userText, groupText]); const cardSubtitleText = useMemo(() => { - const recentEntries = filterToRecentWeeks(aggMetrics?.distance).reverse().flat(); + const recentEntries = segmentDaysByWeeks(aggMetrics?.distance, 2).reverse().flat(); const recentEntriesRange = formatDateRangeOfDays(recentEntries); return `${t('main-metrics.estimated-emissions')}, (${recentEntriesRange})`; }, [aggMetrics?.distance]); diff --git a/www/js/metrics/WeeklyActiveMinutesCard.tsx b/www/js/metrics/WeeklyActiveMinutesCard.tsx index d2e117f8b..99bf9d425 100644 --- a/www/js/metrics/WeeklyActiveMinutesCard.tsx +++ b/www/js/metrics/WeeklyActiveMinutesCard.tsx @@ -4,8 +4,7 @@ import { View } from 'react-native'; import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardMargin, cardStyles } from './MetricsTab'; -import { useImperialConfig } from '../config/useImperialConfig'; -import { filterToRecentWeeks, formatDateRangeOfDays, secondsToMinutes } from './metricsHelper'; +import { formatDateRangeOfDays, segmentDaysByWeeks } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; import { labelKeyToRichMode, labelOptions } from '../survey/multilabel/confirmHelper'; @@ -23,15 +22,8 @@ const WeeklyActiveMinutesCard = ({ userMetrics }: Props) => { const weeklyActiveMinutesRecords = useMemo(() => { const records = []; - const [ recentWeek, prevWeek ] = filterToRecentWeeks(userMetrics?.duration); + const [ recentWeek, prevWeek ] = segmentDaysByWeeks(userMetrics?.duration, 2); ACTIVE_MODES.forEach(mode => { - const recentSum = recentWeek?.reduce((acc, day) => ( - acc + (day[`label_${mode}`] || 0) - ), 0); - if (recentSum) { - const xLabel = `Past Week\n(${formatDateRangeOfDays(recentWeek)})`; // TODO: i18n - records.push({label: labelKeyToRichMode(mode), x: xLabel, y: recentSum / 60}); - } const prevSum = prevWeek?.reduce((acc, day) => ( acc + (day[`label_${mode}`] || 0) ), 0); @@ -39,6 +31,13 @@ const WeeklyActiveMinutesCard = ({ userMetrics }: Props) => { const xLabel = `Previous Week\n(${formatDateRangeOfDays(prevWeek)})`; // TODO: i18n records.push({label: labelKeyToRichMode(mode), x: xLabel, y: prevSum / 60}); } + const recentSum = recentWeek?.reduce((acc, day) => ( + acc + (day[`label_${mode}`] || 0) + ), 0); + if (recentSum) { + const xLabel = `Past Week\n(${formatDateRangeOfDays(recentWeek)})`; // TODO: i18n + records.push({label: labelKeyToRichMode(mode), x: xLabel, y: recentSum / 60}); + } }); return records as {label: ActiveMode, x: string, y: number}[]; }, [userMetrics?.duration]); diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index 52feedfec..78861c60b 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -32,14 +32,14 @@ export const secondsToHours = (seconds: number) => formatForDisplay(seconds / 3600); // segments metricsDays into weeks, with the most recent week first -export function filterToRecentWeeks(metricsDays: DayOfMetricData[]) { +export function segmentDaysByWeeks (days: DayOfMetricData[], nWeeks?: number) { const weeks: DayOfMetricData[][] = []; - if (metricsDays?.length >= 7) - weeks.push(metricsDays.slice(-7)); - if (metricsDays?.length >= 14) - weeks.push(metricsDays.slice(-14, -7)); - return weeks; -} + for (let i = 0; i < days?.length - 1; i += 7) { + weeks.push(days.slice(i, i + 7)); + } + if (nWeeks) return weeks.reverse().slice(0, nWeeks); + return weeks.reverse(); +}; export function formatDate(day: DayOfMetricData) { const dt = DateTime.fromISO(day.fmt_time, { zone: 'utc' }); From 0ecfa58a7e7a0eda68fff131b41e064ce119b359 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 13 Sep 2023 23:54:24 -0400 Subject: [PATCH 128/164] tweak ChangeIndicator style A little bit better contrast --- www/js/metrics/ChangeIndicator.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/js/metrics/ChangeIndicator.tsx b/www/js/metrics/ChangeIndicator.tsx index ee73787b7..d225b2e89 100644 --- a/www/js/metrics/ChangeIndicator.tsx +++ b/www/js/metrics/ChangeIndicator.tsx @@ -63,11 +63,11 @@ const styles: any = { fontSize: 16, }), view: (color) => ({ - backgroundColor: colorLib(color).alpha(0.7).rgb().toString(), + backgroundColor: colorLib(color).alpha(0.85).rgb().toString(), padding: 2, borderStyle: 'solid', - borderColor: colorLib(color).lighten(0.25).alpha(0.8).rgb().toString(), - borderWidth: 2, + borderColor: colorLib(color).darken(0.4).rgb().toString(), + borderWidth: 2.5, borderRadius: 10, }), } From 54e6d8b445598479184abcce3738f5bb518ce8cf Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 13 Sep 2023 23:57:14 -0400 Subject: [PATCH 129/164] fix error sometimes if metrics is not defined yet If metrics is not defined, we can simply skip this and let the function return [] - it will be re-run from upstream once metrics is defined --- www/js/metrics/metricsHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index 78861c60b..4d8dac427 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -98,7 +98,7 @@ export function calculatePercentChange(pastWeekRange, previousWeekRange) { export function parseDataFromMetrics(metrics, population) { console.log("Called parseDataFromMetrics on ", metrics); let mode_bins = {}; - metrics.forEach(function(metric) { + metrics?.forEach(function(metric) { let onFootVal = 0; for (let field in metric) { From 907737b43b0e6f99827dafdbde1665df74694920 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 14 Sep 2023 00:00:06 -0400 Subject: [PATCH 130/164] add 'meter' gradients to carbon graph --- www/js/components/BarChart.tsx | 8 ++++-- www/js/components/Chart.tsx | 15 ++++++---- www/js/components/charting.ts | 51 ++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 82a80fc01..1e957923b 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -1,7 +1,7 @@ import React from "react"; import Chart, { Props as ChartProps } from "./Chart"; import { useTheme } from "react-native-paper"; -import { getMeteredBackgroundColor } from "./charting"; +import { getGradient } from "./charting"; type Props = Omit & { meter?: {high: number, middle: number, dash_key: string}, @@ -11,10 +11,12 @@ const BarChart = ({ meter, ...rest }: Props) => { const { colors } = useTheme(); if (meter) { - rest.getColorForLabel = (label, dataset, ctx, colorFor) => { + rest.getColorForChartEl = (chart, dataset, ctx, colorFor) => { const darkenDegree = colorFor == 'border' ? 0.25 : 0; - return getMeteredBackgroundColor(meter, dataset, ctx, colors, darkenDegree); + const alpha = colorFor == 'border' ? 1 : 0; + return getGradient(chart, meter, dataset, ctx, alpha, darkenDegree); } + rest.borderWidth = 3; } return ( diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx index 732250f92..b868d3d0b 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -5,7 +5,7 @@ import { useTheme } from 'react-native-paper'; import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, TimeScale, ChartData, ChartType, ScriptableContext, PointElement, LineElement } from 'chart.js'; import { Chart as ChartJSChart } from 'react-chartjs-2'; import Annotation, { AnnotationOptions, LabelPosition } from 'chartjs-plugin-annotation'; -import { dedupColors, getChartHeight } from './charting'; +import { dedupColors, getChartHeight, darkenForBorder } from './charting'; ChartJS.register( CategoryScale, @@ -30,14 +30,15 @@ export type Props = { records: { label: string, x: number|string, y: number|string }[], axisTitle: string, type: 'bar'|'line', - getColorForLabel: (label: string, currDataset?: ChartDataset, ctx?: ScriptableContext<'bar'|'line'>, colorFor?: 'background'|'border') => string|null, + getColorForLabel?: (label: string) => string, + getColorForChartEl?: (chart, currDataset: ChartDataset, ctx: ScriptableContext<'bar'|'line'>, colorFor: 'background'|'border') => string|CanvasGradient|null, borderWidth?: number, lineAnnotations?: { value: number, label?: string, color?:string, position?: LabelPosition }[], isHorizontal?: boolean, timeAxis?: boolean, stacked?: boolean, } -const Chart = ({ records, axisTitle, type, getColorForLabel, borderWidth, lineAnnotations, isHorizontal, timeAxis, stacked }: Props) => { +const Chart = ({ records, axisTitle, type, getColorForLabel, getColorForChartEl, borderWidth, lineAnnotations, isHorizontal, timeAxis, stacked }: Props) => { const { colors } = useTheme(); const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); @@ -55,8 +56,12 @@ const Chart = ({ records, axisTitle, type, getColorForLabel, borderWidth, lineAn return { datasets: chartDatasets.map((e, i) => ({ ...e, - backgroundColor: (barCtx) => labelColorMap[e.label] || getColorForLabel(e.label, e, barCtx, 'background'), - borderColor: (barCtx) => labelColorMap[e.label] || getColorForLabel(e.label, e, barCtx, 'border'), + backgroundColor: (barCtx) => ( + labelColorMap?.[e.label] || getColorForChartEl(chartRef.current, e, barCtx, 'background') + ), + borderColor: (barCtx) => ( + darkenForBorder(labelColorMap?.[e.label]) || getColorForChartEl(chartRef.current, e, barCtx, 'border') + ), borderWidth: borderWidth || 2, })), }; diff --git a/www/js/components/charting.ts b/www/js/components/charting.ts index af72b0f35..18b3ae807 100644 --- a/www/js/components/charting.ts +++ b/www/js/components/charting.ts @@ -90,6 +90,52 @@ export function getMeteredBackgroundColor(meter, currDataset, barCtx, colors, da return meteredColor; } +/* Gradient created using Josh Comeau's gradient-generator: + https://www.joshwcomeau.com/gradient-generator?colors=00da71|eccd00|ff8c00|ba0000|440000&angle=90&colorMode=lab&precision=16&easingCurve=0.05172413793103448|0.7243063038793104|1.05|0.3708580280172414 + Reformatted here as arrays to be used programatically */ +const gradientStops: [string, number][] = [ + ['151, 100%, 43%', 0], ['117, 61%, 62%', 2], ['91, 62%, 58%', 5], + ['73, 63%, 54%', 10], ['60, 66%, 50%', 16], ['52, 100%, 46%', 22], + ['48, 100%, 47%', 29], ['44, 100%, 48%', 36], ['40, 100%, 49%', 43], + ['37, 100%, 49%', 50], ['33, 100%, 50%', 58], ['30, 100%, 47%', 65], + ['26, 100%, 45%', 71], ['21, 100%, 42%', 78], ['14, 100%, 39%', 83], + ['0, 100%, 36%', 88], ['359, 100%, 32%', 93], ['358, 100%, 27%', 96], + ['358, 100%, 22%', 99], ['358, 100%, 18%', 100], // ['0, 100%, 13%', 100], +]; + +const clamp = (val: number, min: number, max: number) => + Math.min(max, Math.max(min, val)); + +function getColorsForPercentage(percentage: number, alpha = 1) { + const clampedEndPct = clamp(percentage, 0, 1); + const clampedStartPct = clamp(clampedEndPct - .1 - (clampedEndPct * .4), 0, 1); + const startIndex = gradientStops.findIndex(([_, pct]) => pct >= clampedStartPct * 100); + const endIndex = gradientStops.findIndex(([_, pct]) => pct >= clampedEndPct * 100); + const slice = gradientStops.slice(startIndex, endIndex + 1); + const colors = slice.map(([hsl, pct]) => [`hsla(${hsl}, ${alpha})`, pct / 100]); + return colors as [string, number][]; +} + +export function getGradient(chart, meter, currDataset, barCtx, alpha = null, darken = 0) { + const { ctx, chartArea, scales } = chart; + if (!chartArea) return null; + let gradient: CanvasGradient; + const chartWidth = chartArea.right - chartArea.left; + const total = getBarHeight(barCtx.parsed._stacks); + const approxLength = clamp(total / scales.x._range.max, 0, 1) * chartWidth; + gradient = ctx.createLinearGradient(chartArea.left, 0, chartArea.left + approxLength, 0); + alpha = alpha || (currDataset.label == meter.dash_key ? 0.2 : 1); + let colors = getColorsForPercentage(total / 60, alpha); + colors.forEach((c, i) => { + if (darken) { + const darkened = color(c[0]).darken(darken); + c = [darkened.hex(), c[1]]; + } + gradient.addColorStop(c[1], c[0]); + }); + return gradient; +} + /** * @param baseColor a color string * @param change a number between -1 and 1, indicating the amount to darken or lighten the color @@ -127,3 +173,8 @@ export const dedupColors = (colors: string[][]) => { } return dedupedColors; } + +export function darkenForBorder(clr: string) { + if (!clr) return clr; + return color(clr).darken(0.25).hex(); +} From a3264e234f6e652d1384ad2a779f3028c87ce032 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 14 Sep 2023 01:43:46 -0400 Subject: [PATCH 131/164] adjust carbon meter gradient We will now use the same yellow-orange-red gradient for all bars that have a gradient, instead of making a specific gradient for each bar. This allows the gradients on each bar to line up. The gradient will only apply to bars that exceed meter.low. Anything below low is green. Anything in middle is yellow, becoming orange-red as it gets higher. If it goes above high, it will start to become dark red up to high+20. https://github.com/e-mission/e-mission-phone/pull/1018#issuecomment-1718083794 --- www/js/components/charting.ts | 53 +++++++++++++---------------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/www/js/components/charting.ts b/www/js/components/charting.ts index 18b3ae807..87386a748 100644 --- a/www/js/components/charting.ts +++ b/www/js/components/charting.ts @@ -90,49 +90,34 @@ export function getMeteredBackgroundColor(meter, currDataset, barCtx, colors, da return meteredColor; } -/* Gradient created using Josh Comeau's gradient-generator: - https://www.joshwcomeau.com/gradient-generator?colors=00da71|eccd00|ff8c00|ba0000|440000&angle=90&colorMode=lab&precision=16&easingCurve=0.05172413793103448|0.7243063038793104|1.05|0.3708580280172414 - Reformatted here as arrays to be used programatically */ -const gradientStops: [string, number][] = [ - ['151, 100%, 43%', 0], ['117, 61%, 62%', 2], ['91, 62%, 58%', 5], - ['73, 63%, 54%', 10], ['60, 66%, 50%', 16], ['52, 100%, 46%', 22], - ['48, 100%, 47%', 29], ['44, 100%, 48%', 36], ['40, 100%, 49%', 43], - ['37, 100%, 49%', 50], ['33, 100%, 50%', 58], ['30, 100%, 47%', 65], - ['26, 100%, 45%', 71], ['21, 100%, 42%', 78], ['14, 100%, 39%', 83], - ['0, 100%, 36%', 88], ['359, 100%, 32%', 93], ['358, 100%, 27%', 96], - ['358, 100%, 22%', 99], ['358, 100%, 18%', 100], // ['0, 100%, 13%', 100], -]; - -const clamp = (val: number, min: number, max: number) => - Math.min(max, Math.max(min, val)); - -function getColorsForPercentage(percentage: number, alpha = 1) { - const clampedEndPct = clamp(percentage, 0, 1); - const clampedStartPct = clamp(clampedEndPct - .1 - (clampedEndPct * .4), 0, 1); - const startIndex = gradientStops.findIndex(([_, pct]) => pct >= clampedStartPct * 100); - const endIndex = gradientStops.findIndex(([_, pct]) => pct >= clampedEndPct * 100); - const slice = gradientStops.slice(startIndex, endIndex + 1); - const colors = slice.map(([hsl, pct]) => [`hsla(${hsl}, ${alpha})`, pct / 100]); - return colors as [string, number][]; +const meterColors = { + below: '#00cc95', // green oklch(75% 0.3 165) + // https://www.joshwcomeau.com/gradient-generator?colors=fcab00|ba0000&angle=90&colorMode=lab&precision=3&easingCurve=0.25|0.75|0.75|0.25 + between: ['#fcab00', '#ef8215', '#db5e0c', '#ce3d03', '#b70100'], // yellow-orange-red + above: '#440000', // dark red } export function getGradient(chart, meter, currDataset, barCtx, alpha = null, darken = 0) { const { ctx, chartArea, scales } = chart; if (!chartArea) return null; let gradient: CanvasGradient; - const chartWidth = chartArea.right - chartArea.left; const total = getBarHeight(barCtx.parsed._stacks); - const approxLength = clamp(total / scales.x._range.max, 0, 1) * chartWidth; - gradient = ctx.createLinearGradient(chartArea.left, 0, chartArea.left + approxLength, 0); alpha = alpha || (currDataset.label == meter.dash_key ? 0.2 : 1); - let colors = getColorsForPercentage(total / 60, alpha); - colors.forEach((c, i) => { - if (darken) { - const darkened = color(c[0]).darken(darken); - c = [darkened.hex(), c[1]]; - } - gradient.addColorStop(c[1], c[0]); + if (total < meter.middle) { + const adjColor = darken||alpha ? color(meterColors.below).darken(0.2).alpha(alpha).rgb().string() : meterColors.below; + return adjColor; + } + const scaleMaxX = scales.x._range.max; + gradient = ctx.createLinearGradient(chartArea.left, 0, chartArea.right, 0); + meterColors.between.forEach((clr, i) => { + const clrPosition = ((i + 1) / meterColors.between.length) * (meter.high - meter.middle) + meter.middle; + const adjColor = darken || alpha ? color(clr).darken(darken).alpha(alpha).rgb().string() : clr; + gradient.addColorStop(Math.min(clrPosition, scaleMaxX) / scaleMaxX, adjColor); }); + if (scaleMaxX > meter.high + 20) { + const adjColor = darken||alpha ? color(meterColors.above).darken(0.2).alpha(alpha).rgb().string() : meterColors.above; + gradient.addColorStop((meter.high+20) / scaleMaxX, adjColor); + } return gradient; } From 67111b8fe3bfa4c8b40d667e9bc88739fc1d9fb6 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 14 Sep 2023 01:49:51 -0400 Subject: [PATCH 132/164] refactor: remove darkenForBorder + fix We already have a function that serves this purpose, so let's just use it (darkenOrLighten) It turns out that it was actually flipped from what was intended, though -- negative numbers should equate to darkening and positive to lightening. It didn't really matter before because it was used for deduping where one duplicate is always lightened while the other is darkened. --- www/js/components/Chart.tsx | 4 ++-- www/js/components/charting.ts | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx index b868d3d0b..a645a094d 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -5,7 +5,7 @@ import { useTheme } from 'react-native-paper'; import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, TimeScale, ChartData, ChartType, ScriptableContext, PointElement, LineElement } from 'chart.js'; import { Chart as ChartJSChart } from 'react-chartjs-2'; import Annotation, { AnnotationOptions, LabelPosition } from 'chartjs-plugin-annotation'; -import { dedupColors, getChartHeight, darkenForBorder } from './charting'; +import { dedupColors, getChartHeight, darkenOrLighten } from './charting'; ChartJS.register( CategoryScale, @@ -60,7 +60,7 @@ const Chart = ({ records, axisTitle, type, getColorForLabel, getColorForChartEl, labelColorMap?.[e.label] || getColorForChartEl(chartRef.current, e, barCtx, 'background') ), borderColor: (barCtx) => ( - darkenForBorder(labelColorMap?.[e.label]) || getColorForChartEl(chartRef.current, e, barCtx, 'border') + darkenOrLighten(labelColorMap?.[e.label], -.5) || getColorForChartEl(chartRef.current, e, barCtx, 'border') ), borderWidth: borderWidth || 2, })), diff --git a/www/js/components/charting.ts b/www/js/components/charting.ts index 87386a748..9f7e35c2d 100644 --- a/www/js/components/charting.ts +++ b/www/js/components/charting.ts @@ -126,9 +126,10 @@ export function getGradient(chart, meter, currDataset, barCtx, alpha = null, dar * @param change a number between -1 and 1, indicating the amount to darken or lighten the color * @returns an adjusted color, either darkened or lightened, depending on the sign of change */ -function darkenOrLighten(baseColor: string, change: number) { +export function darkenOrLighten(baseColor: string, change: number) { + if (!baseColor) return baseColor; let colorObj = color(baseColor); - if(change > 0) { + if(change < 0) { // darkening appears more drastic than lightening, so we will be less aggressive (scale change by .5) return colorObj.darken(Math.abs(change * .5)).hex(); } else { @@ -158,8 +159,3 @@ export const dedupColors = (colors: string[][]) => { } return dedupedColors; } - -export function darkenForBorder(clr: string) { - if (!clr) return clr; - return color(clr).darken(0.25).hex(); -} From 5e7a50c08bb2e084f06a7c7d7cd1686db705546a Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 14 Sep 2023 09:09:56 -0600 Subject: [PATCH 133/164] React Control Helpers create a React version of ControlCollectionHelper and ControlSync Helper. These hold the logic for interfacing with the plugins, as well as UI elements for editing menus and various alerts / messages The place this is being stored has also changed, we no longer need to import from the other repos These helpers will eliminate the last parts of React routing, and allow us to no longer rely on the broadcast of "control update complete" to ensure the UI stays up to date --- www/js/control/ControlCollectionHelper.tsx | 273 +++++++++++++++++++++ www/js/control/ControlSyncHelper.tsx | 143 +++++++++++ www/js/control/ProfileSettings.jsx | 65 +++-- 3 files changed, 463 insertions(+), 18 deletions(-) create mode 100644 www/js/control/ControlCollectionHelper.tsx create mode 100644 www/js/control/ControlSyncHelper.tsx diff --git a/www/js/control/ControlCollectionHelper.tsx b/www/js/control/ControlCollectionHelper.tsx new file mode 100644 index 000000000..8d9b73878 --- /dev/null +++ b/www/js/control/ControlCollectionHelper.tsx @@ -0,0 +1,273 @@ +import React, { useEffect, useState } from "react"; +import { Modal, View } from "react-native"; +import { Dialog, Button, Switch, Text, useTheme, TextInput } from 'react-native-paper'; +import { useTranslation } from "react-i18next"; +import ActionMenu from "../components/ActionMenu"; +import { settingStyles } from "./ProfileSettings"; +import { getAngularService } from "../angular-react-helper"; + +type collectionConfig = { + is_duty_cycling: boolean, + simulate_user_interaction: boolean, + accuracy: number, + accuracy_threshold: number, + filter_distance: number, + filter_time: number, + geofence_radius: number, + ios_use_visit_notifications_for_detection: boolean, + ios_use_remote_push_for_sync: boolean, + android_geofence_responsiveness: number +}; + +// const Logger = getAngularService("Logger"); + +async function accuracy2String(config) { + var accuracy = config.accuracy; + let accuracyOptions = await getAccuracyOptions(); + for (var k in accuracyOptions) { + if (accuracyOptions[k] == accuracy) { + return k; + } + } + return accuracy; +} + +export async function isMediumAccuracy(config) { + if (config == null) { + return undefined; // config not loaded when loading ui, set default as false + } else { + var v = await accuracy2String(config); + if (window.cordova.platformId == 'ios') { + return v != "kCLLocationAccuracyBestForNavigation" && v != "kCLLocationAccuracyBest" && v != "kCLLocationAccuracyTenMeters"; + } else if (window.cordova.platformId == 'android') { + return v != "PRIORITY_HIGH_ACCURACY"; + } else { + // $ionicPopup.alert("Emission does not support this platform"); + } + } +} + +export async function helperToggleLowAccuracy(config) { + let tempConfig = {...config}; + let accuracyOptions = await getAccuracyOptions(); + if (isMediumAccuracy(config)) { + if (window.cordova.platformId == 'ios') { + tempConfig.accuracy = accuracyOptions["kCLLocationAccuracyBest"]; + } else if (window.cordova.platformId == 'android') { + tempConfig.accuracy = cch.accuracyOptions["PRIORITY_HIGH_ACCURACY"]; + } + } else { + if (window.cordova.platformId == 'ios') { + tempConfig.accuracy = accuracyOptions["kCLLocationAccuracyHundredMeters"]; + } else if (window.cordova.platformId == 'android') { + tempConfig.accuracy = accuracyOptions["PRIORITY_BALANCED_POWER_ACCURACY"]; + } + } + try{ + let set = setConfig(tempConfig); + console.log("setConfig Sucess"); + } catch (err) { + // Logger.displayError("Error while setting collection config", err); + } +} + +/* +* Simple read/write wrappers +*/ + +export const getState = function() { + return window.cordova.plugins.BEMDataCollection.getState(); +}; + +export async function getHelperCollectionSettings() { + let promiseList = []; + promiseList.push(getConfig()); + promiseList.push(getAccuracyOptions()); + let resultList = await Promise.all(promiseList); + let tempConfig = resultList[0]; + let tempAccuracyOptions = resultList[1]; + return formatConfigForDisplay(tempConfig, tempAccuracyOptions); +} + +const setConfig = function(config) { + return window.cordova.plugins.BEMDataCollection.setConfig(config); +}; + +const getConfig = function() { + return window.cordova.plugins.BEMDataCollection.getConfig(); +}; +const getAccuracyOptions = function() { + return window.cordova.plugins.BEMDataCollection.getAccuracyOptions(); +}; + +export const forceTransitionWrapper = function(transition) { + return window.cordova.plugins.BEMDataCollection.forceTransition(transition); +}; + +const formatConfigForDisplay = function(config, accuracyOptions) { + var retVal = []; + for (var prop in config) { + if (prop == "accuracy") { + for (var name in accuracyOptions) { + if (accuracyOptions[name] == config[prop]) { + retVal.push({'key': prop, 'val': name}); + } + } + } else { + retVal.push({'key': prop, 'val': config[prop]}); + } + } + return retVal; +} + +const ControlSyncHelper = ({ editVis, setEditVis, localConfig, setLocalConfig }) => { + const {colors} = useTheme(); + const Logger = getAngularService("Logger"); + + // const [ localConfig, setLocalConfig ] = useState(); + const [ accuracyActions, setAccuracyActions ] = useState([]); + const [ accuracyVis, setAccuracyVis ] = useState(false); + + async function getCollectionSettings() { + let promiseList = []; + promiseList.push(getConfig()); + promiseList.push(getAccuracyOptions()); + let resultList = await Promise.all(promiseList); + let tempConfig = resultList[0]; + setLocalConfig(tempConfig); + let tempAccuracyOptions = resultList[1]; + setAccuracyActions(formatAccuracyForActions(tempAccuracyOptions)); + return formatConfigForDisplay(tempConfig, tempAccuracyOptions); + } + + useEffect(() => { + getCollectionSettings(); + }, [editVis]) + + const formatAccuracyForActions = function(accuracyOptions) { + let tempAccuracyActions = []; + for (var name in accuracyOptions) { + tempAccuracyActions.push({text: name, value: accuracyOptions[name]}); + } + return tempAccuracyActions; + } + + /* + * Functions to edit and save values + */ + + async function saveAndReload() { + console.log("new config = ", localConfig); + try{ + let set = await setConfig(localConfig); + //TODO find way to not need control.update.complete event broadcast + } catch(err) { + Logger.displayError("Error while setting collection config", err); + } + } + + const onToggle = function(config_key) { + let tempConfig = {...localConfig}; + tempConfig[config_key] = !localConfig[config_key]; + setLocalConfig(tempConfig); + } + + const onChooseAccuracy = function(accuracyOption) { + let tempConfig = {...localConfig}; + tempConfig.accuracy = accuracyOption.value; + setLocalConfig(tempConfig); + } + + const onChangeText = function(newText, config_key) { + let tempConfig = {...localConfig}; + tempConfig[config_key] = parseInt(newText); + setLocalConfig(tempConfig); + } + + /*ios vs android*/ + let filterComponent; + if(window.cordova.platformId == 'ios') { + filterComponent = + Filter Distance + onChangeText(text, "filter_distance")}/> + + } else { + filterComponent = + Filter Interval + onChangeText(text, "filter_time")}/> + + } + let iosToggles; + if(window.cordova.platformId == 'ios') { + iosToggles = <> + {/* use visit notifications toggle NO ANDROID */} + + Use Visit Notifications + onToggle("ios_use_visit_notifications_for_detection")}> + + {/* sync on remote push toggle NO ANDROID */} + + Sync on remote push + onToggle("ios_use_remote_push_for_sync}")}> + + + } + let geofenceComponent; + if(window.cordova.platformId == 'android') { + geofenceComponent = + Geofence Responsiveness + onChangeText(text, "android_geofence_responsiveness")}/> + + } + + return ( + <> + setEditVis(false)} transparent={true}> + setEditVis(false)} style={settingStyles.dialog(colors.elevation.level3)}> + Edit Collection Settings + + {/* duty cycling toggle */} + + Duty Cycling + onToggle("is_duty_cycling")}> + + {/* simulate user toggle */} + + Simulate User + onToggle("simulate_user_interaction")}> + + {/* accuracy */} + + Accuracy + + + {/* accuracy threshold not editable*/} + + Accuracy Threshold + {localConfig?.accuracy_threshold} + + {filterComponent} + {/* geofence radius */} + + Geofence Radius + onChangeText(text, "geofence_radius")}/> + + {iosToggles} + {geofenceComponent} + + + + + + + + + {}}> + + ); + }; + +export default ControlSyncHelper; diff --git a/www/js/control/ControlSyncHelper.tsx b/www/js/control/ControlSyncHelper.tsx new file mode 100644 index 000000000..a5e3182ca --- /dev/null +++ b/www/js/control/ControlSyncHelper.tsx @@ -0,0 +1,143 @@ +import React, { useEffect, useState } from "react"; +import { Modal, View } from "react-native"; +import { Dialog, Button, Switch, Text, useTheme } from 'react-native-paper'; +import { useTranslation } from "react-i18next"; +import ActionMenu from "../components/ActionMenu"; +import { settingStyles } from "./ProfileSettings"; +import { getAngularService } from "../angular-react-helper"; + +/* +* BEGIN: Simple read/write wrappers +*/ +export function forcePluginSync() { + return window.cordova.plugins.BEMServerSync.forceSync(); +}; + +const formatConfigForDisplay = (configToFormat) => { + var formatted = []; + for (let prop in configToFormat) { + formatted.push({'key': prop, 'val': configToFormat[prop]}); + } + return formatted; +} + +const setConfig = function(config) { + return window.cordova.plugins.BEMServerSync.setConfig(config); + }; + +const getConfig = function() { + return window.cordova.plugins.BEMServerSync.getConfig(); +}; + +export async function getHelperSyncSettings() { + let tempConfig = await getConfig(); + return formatConfigForDisplay(tempConfig); +} + +type syncConfig = { sync_interval: number, + ios_use_remote_push: boolean }; + +const ControlSyncHelper = ({ editVis, setEditVis }) => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const CommHelper = getAngularService("CommHelper"); + const Logger = getAngularService("Logger"); + + const [ localConfig, setLocalConfig ] = useState(); + const [ intervalVis, setIntervalVis ] = useState(false); + + /* + * Functions to read and format values for display + */ + async function getSyncSettings() { + let tempConfig = await getConfig(); + setLocalConfig(tempConfig); + } + + useEffect(() => { + getSyncSettings(); + }, [editVis]) + + const syncIntervalActions = [ + {text: "1 min", value: 60}, + {text: "10 min", value: 10 * 60}, + {text: "30 min", value: 30 * 60}, + {text: "1 hr", value: 60 * 60} + ] + + /* + * Functions to edit and save values + */ + async function saveAndReload() { + console.log("new config = "+localConfig); + try{ + let set = setConfig(localConfig); + //NOTE -- we need to make sure we update these settings in ProfileSettings :) -- getting rid of broadcast handling for migration!! + CommHelper.updateUser({ + // TODO: worth thinking about where best to set this + // Currently happens in native code. Now that we are switching + // away from parse, we can store this from javascript here. + // or continue to store from native + // this is easier for people to see, but means that calls to + // native, even through the javascript interface are not complete + curr_sync_interval: localConfig.sync_interval + }); + } catch (err) + { + console.log("error with setting sync config", err); + Logger.displayError("Error while setting sync config", err); + } + } + + const onChooseInterval = function(interval) { + let tempConfig = {...localConfig}; + tempConfig.sync_interval = interval.value; + setLocalConfig(tempConfig); + } + + const onTogglePush = function() { + let tempConfig = {...localConfig}; + tempConfig.ios_use_remote_push = !localConfig.ios_use_remote_push; + setLocalConfig(tempConfig); + } + + /* + * configure the UI + */ + let toggle; + if(window.cordova.platformId == 'ios'){ + toggle = + Use Remote Push + + + } + + return ( + <> + {/* popup to show when we want to edit */} + setEditVis(false)} transparent={true}> + setEditVis(false)} style={settingStyles.dialog(colors.elevation.level3)}> + Edit Sync Settings + + + Sync Interval + + + {toggle} + + + + + + + + + {}}> + + ); + }; + +export default ControlSyncHelper; \ No newline at end of file diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 88ef53ce9..3856c77f2 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -17,6 +17,8 @@ import PrivacyPolicyModal from "./PrivacyPolicyModal"; import ActionMenu from "../components/ActionMenu"; import SensedPage from "./SensedPage" import LogPage from "./LogPage"; +import ControlSyncHelper, {forcePluginSync, getHelperSyncSettings} from "./ControlSyncHelper"; +import ControlCollectionHelper, {getHelperCollectionSettings, getState, isMediumAccuracy, helperToggleLowAccuracy, forceTransitionWrapper} from "./ControlCollectionHelper"; let controlUpdateCompleteListenerRegistered = false; @@ -36,8 +38,6 @@ const ProfileSettings = () => { const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); const UploadHelper = getAngularService('UploadHelper'); const EmailHelper = getAngularService('EmailHelper'); - const ControlCollectionHelper = getAngularService('ControlCollectionHelper'); - const ControlSyncHelper = getAngularService('ControlSyncHelper'); const KVStore = getAngularService('KVStore'); const NotificationScheduler = getAngularService('NotificationScheduler'); const ControlHelper = getAngularService('ControlHelper'); @@ -55,8 +55,8 @@ const ProfileSettings = () => { } //functions that come directly from an Angular service - const editCollectionConfig = ControlCollectionHelper.editConfig; - const editSyncConfig = ControlSyncHelper.editConfig; + const editCollectionConfig = () => setEditCollection(true); + const editSyncConfig = () => setEditSync(true); //states and variables used to control/create the settings const [opCodeVis, setOpCodeVis] = useState(false); @@ -75,7 +75,12 @@ const ProfileSettings = () => { const [privacyVis, setPrivacyVis] = useState(false); const [showingSensed, setShowingSensed] = useState(false); const [showingLog, setShowingLog] = useState(false); + const [editSync, setEditSync] = useState(false); + const [editCollection, setEditCollection] = useState(false); + const [forceResultVis, setForceResultVis] = useState(false); + const [forceResult, setForceResult] = useState(""); + const [collectConfig, setCollectConfig] = useState({}); const [collectSettings, setCollectSettings] = useState({}); const [notificationSettings, setNotificationSettings] = useState({}); const [authSettings, setAuthSettings] = useState({}); @@ -151,16 +156,16 @@ const ProfileSettings = () => { console.debug('about to refreshCollectSettings, collectSettings = ', collectSettings); const newCollectSettings = {}; - // refresh collect plugin configuration - const collectionPluginConfig = await ControlCollectionHelper.getCollectionSettings(); + // // refresh collect plugin configuration + const collectionPluginConfig = await getHelperCollectionSettings(); newCollectSettings.config = collectionPluginConfig; - const collectionPluginState = await ControlCollectionHelper.getState(); + const collectionPluginState = await getState(); newCollectSettings.state = collectionPluginState; newCollectSettings.trackingOn = collectionPluginState != "local.state.tracking_stopped" && collectionPluginState != "STATE_TRACKING_STOPPED"; - const isLowAccuracy = await ControlCollectionHelper.isMediumAccuracy(); + const isLowAccuracy = await isMediumAccuracy(); if (typeof isLowAccuracy != 'undefined') { newCollectSettings.lowAccuracy = isLowAccuracy; } @@ -168,6 +173,11 @@ const ProfileSettings = () => { setCollectSettings(newCollectSettings); } + //ensure ui table updated when editor closes + useEffect(() => { + refreshCollectSettings(); + }, [editCollection]) + async function refreshNotificationSettings() { console.debug('about to refreshNotificationSettings, notificationSettings = ', notificationSettings); const newNotificationSettings ={}; @@ -190,13 +200,18 @@ const ProfileSettings = () => { async function getSyncSettings() { console.log("getting sync settings"); var newSyncSettings = {}; - ControlSyncHelper.getSyncSettings().then(function(showConfig) { + getHelperSyncSettings().then(function(showConfig) { newSyncSettings.show_config = showConfig; setSyncSettings(newSyncSettings); console.log("sync settings are ", syncSettings); }); }; + //update sync settings in the table when close editor + useEffect(() => { + getSyncSettings(); + }, [editSync]); + async function getConnectURL() { ControlHelper.getSettings().then(function(response) { var newConnectSettings ={} @@ -256,10 +271,20 @@ const ProfileSettings = () => { async function userStartStopTracking() { const transitionToForce = collectSettings.trackingOn ? 'STOP_TRACKING' : 'START_TRACKING'; - ControlCollectionHelper.forceTransition(transitionToForce); - /* the ControlCollectionHelper.forceTransition call above will trigger a - 'control.update.complete' event when it's done, which will trigger refreshCollectSettings. - So we don't need to call refreshCollectSettings here. */ + forceTransition(transitionToForce); + refreshCollectSettings(); + } + + async function forceTransition(transition) { + try { + let result = forceTransitionWrapper(transition); + setForceResultVis(true); + setForceResult('success -> '+result) + } catch (err) { + console.log("error forcing state", err); + setForceResultVis(true); + setForceResult('error -> '+err) + } } @@ -279,7 +304,7 @@ const ProfileSettings = () => { } async function toggleLowAccuracy() { - ControlCollectionHelper.toggleLowAccuracy(); + helperToggleLowAccuracy(); refreshCollectSettings(); } @@ -353,7 +378,7 @@ const ProfileSettings = () => { async function getTransition(transKey) { var entry_data = {}; - const curr_state = await ControlCollectionHelper.getState(); + const curr_state = await getState(); entry_data.curr_state = curr_state; if (transKey == getEndTransitionKey()) { entry_data.curr_state = getOngoingTransitionState(); @@ -397,7 +422,7 @@ const ProfileSettings = () => { function() { console.log("Added "+ClientStats.getStatKeys().BUTTON_FORCE_SYNC+" event"); }); - ControlSyncHelper.forceSync().then(function() { + forcePluginSync().then(function() { /* * Change to sensorKey to "background/location" after fixing issues * with getLastSensorData and getLastMessages in the usercache @@ -461,7 +486,7 @@ const ProfileSettings = () => { } const onSelectState = function(stateObject) { - ControlCollectionHelper.forceTransition(stateObject.transition); + forceTransition(stateObject.transition); } const onSelectCarbon = function(carbonObject) { @@ -560,7 +585,7 @@ const ProfileSettings = () => { clearNotifications()}> {/* force state sheet */} - + {}}> {/* opcode viewing popup */} @@ -659,9 +684,13 @@ const ProfileSettings = () => { + + + + ); From 9255675b46ada642e254616d3ddd6cfcdf777bde Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 14 Sep 2023 11:29:20 -0600 Subject: [PATCH 134/164] fix medium accuracy medium accuracy switch was broken, now can be toggled smoothly --- www/js/control/ControlCollectionHelper.tsx | 14 ++++++++------ www/js/control/ProfileSettings.jsx | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/www/js/control/ControlCollectionHelper.tsx b/www/js/control/ControlCollectionHelper.tsx index 8d9b73878..cf8f95053 100644 --- a/www/js/control/ControlCollectionHelper.tsx +++ b/www/js/control/ControlCollectionHelper.tsx @@ -32,8 +32,9 @@ async function accuracy2String(config) { return accuracy; } -export async function isMediumAccuracy(config) { - if (config == null) { +export async function isMediumAccuracy() { + let config = await getConfig(); + if (!config || config == null) { return undefined; // config not loaded when loading ui, set default as false } else { var v = await accuracy2String(config); @@ -47,10 +48,11 @@ export async function isMediumAccuracy(config) { } } -export async function helperToggleLowAccuracy(config) { - let tempConfig = {...config}; +export async function helperToggleLowAccuracy() { + let tempConfig = await getConfig(); let accuracyOptions = await getAccuracyOptions(); - if (isMediumAccuracy(config)) { + let medium = await isMediumAccuracy(); + if (medium) { if (window.cordova.platformId == 'ios') { tempConfig.accuracy = accuracyOptions["kCLLocationAccuracyBest"]; } else if (window.cordova.platformId == 'android') { @@ -64,7 +66,7 @@ export async function helperToggleLowAccuracy(config) { } } try{ - let set = setConfig(tempConfig); + let set = await setConfig(tempConfig); console.log("setConfig Sucess"); } catch (err) { // Logger.displayError("Error while setting collection config", err); diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 3856c77f2..36185e13e 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -304,7 +304,7 @@ const ProfileSettings = () => { } async function toggleLowAccuracy() { - helperToggleLowAccuracy(); + let toggle = await helperToggleLowAccuracy(); refreshCollectSettings(); } From 1b8a43cad913e5ad54e061024a0767df92f16805 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 14 Sep 2023 11:31:04 -0600 Subject: [PATCH 135/164] remove update complete listeners no longer needed, as refreshCollect settings is called after toggling accuracy, toggling tracking, and when the settings editor is closed --- www/js/control/ProfileSettings.jsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 36185e13e..9819164ab 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -20,8 +20,6 @@ import LogPage from "./LogPage"; import ControlSyncHelper, {forcePluginSync, getHelperSyncSettings} from "./ControlSyncHelper"; import ControlCollectionHelper, {getHelperCollectionSettings, getState, isMediumAccuracy, helperToggleLowAccuracy, forceTransitionWrapper} from "./ControlCollectionHelper"; -let controlUpdateCompleteListenerRegistered = false; - //any pure functions can go outside const ProfileSettings = () => { // anything that mutates must go in --- depend on props or state... @@ -45,15 +43,6 @@ const ProfileSettings = () => { const StartPrefs = getAngularService('StartPrefs'); const DynamicConfig = getAngularService('DynamicConfig'); - if (!controlUpdateCompleteListenerRegistered) { - settingsScope.$on('control.update.complete', function() { - console.debug("Received control.update.complete event, refreshing screen"); - refreshScreen(); - refreshCollectSettings(); - }); - controlUpdateCompleteListenerRegistered = true; - } - //functions that come directly from an Angular service const editCollectionConfig = () => setEditCollection(true); const editSyncConfig = () => setEditSync(true); From 9078488b2b95b867ef960abd8831e8eb980d0b0c Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 14 Sep 2023 11:39:26 -0600 Subject: [PATCH 136/164] eliminate settingsScope no longer rely on settingsScope to catch recomputeAppStatus, because we check on resume, this is not needed --- www/js/control/AppStatusModal.tsx | 10 +--------- www/js/control/ProfileSettings.jsx | 7 +------ 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/www/js/control/AppStatusModal.tsx b/www/js/control/AppStatusModal.tsx index 3f696adcc..ff5d18ca1 100644 --- a/www/js/control/AppStatusModal.tsx +++ b/www/js/control/AppStatusModal.tsx @@ -10,13 +10,11 @@ import ExplainPermissions from "../appstatus/ExplainPermissions"; import AlertBar from "./AlertBar"; import { settingStyles } from "./ProfileSettings"; -const AppStatusModal = ({permitVis, setPermitVis, settingsScope}) => { +const AppStatusModal = ({permitVis, setPermitVis}) => { const { t } = useTranslation(); const { colors } = useTheme(); const { appConfig, loading } = useAppConfig(); - console.log("settings scope in app status modal", settingsScope); - const { height: windowHeight } = useWindowDimensions(); const [osver, setOsver] = useState(0); const [platform, setPlatform] = useState(""); @@ -370,12 +368,6 @@ const AppStatusModal = ({permitVis, setPermitVis, settingsScope}) => { refreshAllChecks(); }); - //refresh when recompute message is broadcast - settingsScope.$on("recomputeAppStatus", function() { - console.log("PERMISSION CHECK: recomputing state"); - refreshAllChecks(); - }); - //load when ready useEffect(() => { if (appConfig && window['device']?.platform) { diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 9819164ab..8580d1ea1 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -27,11 +27,6 @@ const ProfileSettings = () => { const { appConfig, loading } = useAppConfig(); const { colors } = useTheme(); - // get the scope of the general-settings.js file - const mainControlEl = document.getElementById('main-control'); - const settingsScope = angular.element(mainControlEl.querySelector('profile-settings')).scope(); - console.log("settings scope", settingsScope); - //angular services needed const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); const UploadHelper = getAngularService('UploadHelper'); @@ -580,7 +575,7 @@ const ProfileSettings = () => { {/* {view permissions} */} - + {/* {view privacy} */} From e02db4a25a131d0132e266c5fb743778cd09270a Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 14 Sep 2023 14:05:35 -0600 Subject: [PATCH 137/164] remove "safeToggle" the debouncing patch for toggleLowAccuracy is no longer needed! --- www/js/control/ProfileSettings.jsx | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 8580d1ea1..a714b2e13 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -75,7 +75,7 @@ const ProfileSettings = () => { const [uiConfig, setUiConfig] = useState({}); const [consentDoc, setConsentDoc] = useState({}); const [dumpDate, setDumpDate] = useState(new Date()); - const [toggleTime, setToggleTime] = useState(new Date()); + // const [toggleTime, setToggleTime] = useState(new Date()); let carbonDatasetString = t('general-settings.carbon-dataset') + ": " + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); const carbonOptions = CarbonDatasetHelper.getCarbonDatasetOptions(); @@ -272,20 +272,20 @@ const ProfileSettings = () => { } - const safeToggle = function() { - if(toggleTime){ - const prevTime = toggleTime.getTime(); - const currTime = new Date().getTime(); - if(prevTime + 2000 < currTime ){ - toggleLowAccuracy(); - setToggleTime(new Date()); - } - } - else { - toggleLowAccuracy(); - setToggleTime(new Date()); - } - } + // const safeToggle = function() { + // if(toggleTime){ + // const prevTime = toggleTime.getTime(); + // const currTime = new Date().getTime(); + // if(prevTime + 2000 < currTime ){ + // toggleLowAccuracy(); + // setToggleTime(new Date()); + // } + // } + // else { + // toggleLowAccuracy(); + // setToggleTime(new Date()); + // } + // } async function toggleLowAccuracy() { let toggle = await helperToggleLowAccuracy(); @@ -512,7 +512,7 @@ const ProfileSettings = () => { {timePicker} setPermitVis(true)}> - + setCarbonDataVis(true)}> setDateDumpVis(true)}> From d55760f3e4c68d8f120fa1788091f0b47e3c20ce Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 14 Sep 2023 14:24:53 -0600 Subject: [PATCH 138/164] prevent "missing argument errors" since these arguments are optional, and we render based on their existence, set the default to undefined --- www/js/control/AlertBar.jsx | 2 +- www/js/control/SettingRow.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/control/AlertBar.jsx b/www/js/control/AlertBar.jsx index c56db7ad2..fbac80056 100644 --- a/www/js/control/AlertBar.jsx +++ b/www/js/control/AlertBar.jsx @@ -4,7 +4,7 @@ import { Snackbar } from 'react-native-paper'; import { useTranslation } from "react-i18next"; import { SafeAreaView } from "react-native-safe-area-context"; -const AlertBar = ({visible, setVisible, messageKey, messageAddition}) => { +const AlertBar = ({visible, setVisible, messageKey, messageAddition=undefined}) => { const { t } = useTranslation(); const onDismissSnackBar = () => setVisible(false); diff --git a/www/js/control/SettingRow.jsx b/www/js/control/SettingRow.jsx index 3caf36d81..473a45d7f 100644 --- a/www/js/control/SettingRow.jsx +++ b/www/js/control/SettingRow.jsx @@ -3,7 +3,7 @@ import { StyleSheet } from 'react-native'; import { List, Switch, useTheme } from 'react-native-paper'; import { useTranslation } from "react-i18next"; -const SettingRow = ({textKey, iconName, action, desc, switchValue, descStyle=undefined}) => { +const SettingRow = ({textKey, iconName=undefined, action, desc=undefined, switchValue=undefined, descStyle=undefined}) => { const { t } = useTranslation(); //this accesses the translations const { colors } = useTheme(); // use this to get the theme colors instead of hardcoded #hex colors From 2cb11455f52f6c2af1e5002807fe4af4418c551a Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 14 Sep 2023 14:25:47 -0600 Subject: [PATCH 139/164] remove safe toggle code no longer need to debounce, so no need to keep code around --- www/js/control/ProfileSettings.jsx | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index a714b2e13..46c6b05ae 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -75,7 +75,6 @@ const ProfileSettings = () => { const [uiConfig, setUiConfig] = useState({}); const [consentDoc, setConsentDoc] = useState({}); const [dumpDate, setDumpDate] = useState(new Date()); - // const [toggleTime, setToggleTime] = useState(new Date()); let carbonDatasetString = t('general-settings.carbon-dataset') + ": " + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); const carbonOptions = CarbonDatasetHelper.getCarbonDatasetOptions(); @@ -271,22 +270,6 @@ const ProfileSettings = () => { } } - - // const safeToggle = function() { - // if(toggleTime){ - // const prevTime = toggleTime.getTime(); - // const currTime = new Date().getTime(); - // if(prevTime + 2000 < currTime ){ - // toggleLowAccuracy(); - // setToggleTime(new Date()); - // } - // } - // else { - // toggleLowAccuracy(); - // setToggleTime(new Date()); - // } - // } - async function toggleLowAccuracy() { let toggle = await helperToggleLowAccuracy(); refreshCollectSettings(); From 1189700c340c5a23f19dd12cfd5f269ab40e1ddc Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 14 Sep 2023 14:27:21 -0600 Subject: [PATCH 140/164] isolate forceSync code to ControlSyncHelper to start decluttering `ProfileSettings` move the component to the ControlSyncHelper then import it into `Profile Settings` by exporting a component, we can wrap the popups and their states into one place --- www/js/control/ControlSyncHelper.tsx | 95 ++++++++++++++++++++++++++++ www/js/control/ProfileSettings.jsx | 73 +-------------------- 2 files changed, 97 insertions(+), 71 deletions(-) diff --git a/www/js/control/ControlSyncHelper.tsx b/www/js/control/ControlSyncHelper.tsx index a5e3182ca..bb032f398 100644 --- a/www/js/control/ControlSyncHelper.tsx +++ b/www/js/control/ControlSyncHelper.tsx @@ -5,6 +5,8 @@ import { useTranslation } from "react-i18next"; import ActionMenu from "../components/ActionMenu"; import { settingStyles } from "./ProfileSettings"; import { getAngularService } from "../angular-react-helper"; +import SettingRow from "./SettingRow"; +import AlertBar from "./AlertBar"; /* * BEGIN: Simple read/write wrappers @@ -34,9 +36,102 @@ export async function getHelperSyncSettings() { return formatConfigForDisplay(tempConfig); } +const getEndTransitionKey = function() { + if(window.cordova.platformId == 'android') { + return "local.transition.stopped_moving"; + } + else if(window.cordova.platformId == 'ios') { + return "T_TRIP_ENDED"; + } +} + type syncConfig = { sync_interval: number, ios_use_remote_push: boolean }; +export const ForceSyncRow = () => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const ClientStats = getAngularService('ClientStats'); + const Logger = getAngularService('Logger'); + + const [dataPendingVis, setDataPendingVis] = useState(false); + const [dataPushedVis, setDataPushedVis] = useState(false); + + async function forceSync() { + ClientStats.addEvent(ClientStats.getStatKeys().BUTTON_FORCE_SYNC).then( + function() { + console.log("Added "+ClientStats.getStatKeys().BUTTON_FORCE_SYNC+" event"); + }); + forcePluginSync().then(function() { + /* + * Change to sensorKey to "background/location" after fixing issues + * with getLastSensorData and getLastMessages in the usercache + * See https://github.com/e-mission/e-mission-phone/issues/279 for details + */ + var sensorKey = "statemachine/transition"; + return window.cordova.plugins.BEMUserCache.getAllMessages(sensorKey, true); + }).then(function(sensorDataList) { + Logger.log("sensorDataList = "+JSON.stringify(sensorDataList)); + // If everything has been pushed, we should + // only have one entry for the battery, which is the one that was + // inserted on the last successful push. + var isTripEnd = function(entry) { + if (entry.metadata.key == getEndTransitionKey()) { + return true; + } else { + return false; + } + }; + var syncLaunchedCalls = sensorDataList.filter(isTripEnd); + var syncPending = (syncLaunchedCalls.length > 0); + Logger.log("sensorDataList.length = "+sensorDataList.length+ + ", syncLaunchedCalls.length = "+syncLaunchedCalls.length+ + ", syncPending? = "+syncPending); + return syncPending; + }).then(function(syncPending) { + Logger.log("sync launched = "+syncPending); + if (syncPending) { + Logger.log("data is pending, showing confirm dialog"); + setDataPendingVis(true); //consent handling in modal + } else { + setDataPushedVis(true); + } + }).catch(function(error) { + Logger.displayError("Error while forcing sync", error); + }); + }; + + return ( + <> + + + {/* dataPending */} + setDataPendingVis(false)} transparent={true}> + setDataPendingVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t('data pending for push')} + + + + + + + + + + ) +} + +//UI for editing the sync config const ControlSyncHelper = ({ editVis, setEditVis }) => { const { t } = useTranslation(); const { colors } = useTheme(); diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 46c6b05ae..8e6b98876 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -17,7 +17,7 @@ import PrivacyPolicyModal from "./PrivacyPolicyModal"; import ActionMenu from "../components/ActionMenu"; import SensedPage from "./SensedPage" import LogPage from "./LogPage"; -import ControlSyncHelper, {forcePluginSync, getHelperSyncSettings} from "./ControlSyncHelper"; +import ControlSyncHelper, {ForceSyncRow, getHelperSyncSettings} from "./ControlSyncHelper"; import ControlCollectionHelper, {getHelperCollectionSettings, getState, isMediumAccuracy, helperToggleLowAccuracy, forceTransitionWrapper} from "./ControlCollectionHelper"; //any pure functions can go outside @@ -49,8 +49,6 @@ const ProfileSettings = () => { const [forceStateVis, setForceStateVis] = useState(false); const [permitVis, setPermitVis] = useState(false); const [logoutVis, setLogoutVis] = useState(false); - const [dataPendingVis, setDataPendingVis] = useState(false); - const [dataPushedVis, setDataPushedVis] = useState(false); const [invalidateSuccessVis, setInvalidateSuccessVis] = useState(false); const [noConsentVis, setNoConsentVis] = useState(false); const [noConsentMessageVis, setNoConsentMessageVis] = useState(false); @@ -383,51 +381,6 @@ const ProfileSettings = () => { }).then(forceSync); } - //showing up in an odd space on the screen!! - async function forceSync() { - ClientStats.addEvent(ClientStats.getStatKeys().BUTTON_FORCE_SYNC).then( - function() { - console.log("Added "+ClientStats.getStatKeys().BUTTON_FORCE_SYNC+" event"); - }); - forcePluginSync().then(function() { - /* - * Change to sensorKey to "background/location" after fixing issues - * with getLastSensorData and getLastMessages in the usercache - * See https://github.com/e-mission/e-mission-phone/issues/279 for details - */ - var sensorKey = "statemachine/transition"; - return window.cordova.plugins.BEMUserCache.getAllMessages(sensorKey, true); - }).then(function(sensorDataList) { - Logger.log("sensorDataList = "+JSON.stringify(sensorDataList)); - // If everything has been pushed, we should - // only have one entry for the battery, which is the one that was - // inserted on the last successful push. - var isTripEnd = function(entry) { - if (entry.metadata.key == getEndTransitionKey()) { - return true; - } else { - return false; - } - }; - var syncLaunchedCalls = sensorDataList.filter(isTripEnd); - var syncPending = (syncLaunchedCalls.length > 0); - Logger.log("sensorDataList.length = "+sensorDataList.length+ - ", syncLaunchedCalls.length = "+syncLaunchedCalls.length+ - ", syncPending? = "+syncPending); - return syncPending; - }).then(function(syncPending) { - Logger.log("sync launched = "+syncPending); - if (syncPending) { - Logger.log("data is pending, showing confirm dialog"); - setDataPendingVis(true); //consent handling in modal - } else { - setDataPushedVis(true); - } - }).catch(function(error) { - Logger.displayError("Error while forcing sync", error); - }); - }; - async function invalidateCache() { window.cordova.plugins.BEMUserCache.invalidateAllCache().then(function(result) { console.log("invalidate result", result); @@ -497,7 +450,7 @@ const ProfileSettings = () => { setPermitVis(true)}> setCarbonDataVis(true)}> - + setDateDumpVis(true)}> {logUploadSection} @@ -585,27 +538,6 @@ const ProfileSettings = () => { - {/* dataPending */} - setDataPendingVis(false)} transparent={true}> - setDataPendingVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('data pending for push')} - - - - - - - {/* handle no consent */} setNoConsentVis(false)} transparent={true}> { minDate={new Date(appConfig?.intro?.start_year, appConfig?.intro?.start_month - 1, 1)}> - From df787582310684006fa62f3fd020f805e1dab586 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 14 Sep 2023 14:29:05 -0600 Subject: [PATCH 141/164] don't store a duplicate copy of the collectConfig this was one way I attempted to control the ControlSettings, but moved to set/get from a central location (plugin) for one-off changes (toggles) and using the local config, and saving once, when editing with the settings editor modal --- www/js/control/ControlCollectionHelper.tsx | 4 ++-- www/js/control/ProfileSettings.jsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/www/js/control/ControlCollectionHelper.tsx b/www/js/control/ControlCollectionHelper.tsx index cf8f95053..6d829a3a6 100644 --- a/www/js/control/ControlCollectionHelper.tsx +++ b/www/js/control/ControlCollectionHelper.tsx @@ -122,11 +122,11 @@ const formatConfigForDisplay = function(config, accuracyOptions) { return retVal; } -const ControlSyncHelper = ({ editVis, setEditVis, localConfig, setLocalConfig }) => { +const ControlSyncHelper = ({ editVis, setEditVis }) => { const {colors} = useTheme(); const Logger = getAngularService("Logger"); - // const [ localConfig, setLocalConfig ] = useState(); + const [ localConfig, setLocalConfig ] = useState(); const [ accuracyActions, setAccuracyActions ] = useState([]); const [ accuracyVis, setAccuracyVis ] = useState(false); diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 8e6b98876..05366d480 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -62,7 +62,7 @@ const ProfileSettings = () => { const [forceResultVis, setForceResultVis] = useState(false); const [forceResult, setForceResult] = useState(""); - const [collectConfig, setCollectConfig] = useState({}); + // const [collectConfig, setCollectConfig] = useState({}); const [collectSettings, setCollectSettings] = useState({}); const [notificationSettings, setNotificationSettings] = useState({}); const [authSettings, setAuthSettings] = useState({}); @@ -588,7 +588,7 @@ const ProfileSettings = () => { - + ); From fe2dd559dc83d97645250d031eaceed5db261866 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Fri, 15 Sep 2023 15:44:43 -0400 Subject: [PATCH 142/164] fix segmentDaysByWeeks if we had dates for the 1st through 15th (15 days), this function would mistakenly return the 8th through 14th and the 1st through 7th. We actually want the 9th through 15th and the 2nd throgh 8th, because it should be relative to the most recent day. To fix this we iterate over the days backwards, not forwards. With this approach we also don't have to reverse the weeks when we return them because they are already with the most recent first. --- www/js/metrics/metricsHelper.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index 4d8dac427..0e199bc7a 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -34,11 +34,11 @@ export const secondsToHours = (seconds: number) => // segments metricsDays into weeks, with the most recent week first export function segmentDaysByWeeks (days: DayOfMetricData[], nWeeks?: number) { const weeks: DayOfMetricData[][] = []; - for (let i = 0; i < days?.length - 1; i += 7) { - weeks.push(days.slice(i, i + 7)); + for (let i = days?.length - 1; i >= 0; i -= 7) { + weeks.push(days.slice(Math.max(i - 6, 0), i + 1)); } - if (nWeeks) return weeks.reverse().slice(0, nWeeks); - return weeks.reverse(); + if (nWeeks) return weeks.slice(0, nWeeks); + return weeks; }; export function formatDate(day: DayOfMetricData) { From f57603cd68b7fd48411bab7a2588c52d137b96c4 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 15 Sep 2023 13:57:18 -0600 Subject: [PATCH 143/164] use custom labels we had figured out that there were some differences https://github.com/e-mission/e-mission-docs/issues/961#issuecomment-1721692442 Eventually, we realized this was because the new dashboard was not using the custom labels. This commit adds the methods that check to see if the labels are custom or sensed to `metricsHelper`, checks for custom labels and indicates the need for custom footprint mappings in `CarbonFootprintCard` and the finally reverts back to "rich modes" rather than "base modes" in `metrics-factory` so we use the custom labels --- www/js/metrics-factory.js | 14 ++++----- www/js/metrics/CarbonFootprintCard.tsx | 7 ++++- www/js/metrics/metricsHelper.ts | 42 ++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/www/js/metrics-factory.js b/www/js/metrics-factory.js index caeb07ad4..ce813fbaa 100644 --- a/www/js/metrics-factory.js +++ b/www/js/metrics-factory.js @@ -36,17 +36,17 @@ angular.module('emission.main.metrics.factory', var footprint = fh.getFootprint(); var result = 0; for (var i in userMetrics) { - const baseMode = getBaseModeByValue(userMetrics[i].key, labelOptions).name; - if (baseMode == 'ON_FOOT') { - baseMode = 'WALKING'; + var mode = userMetrics[i].key; + if (mode == 'ON_FOOT') { + mode = 'WALKING'; } - if (baseMode in footprint) { - result += footprint[baseMode] * mtokm(userMetrics[i].values); - } else if (baseMode == 'IN_VEHICLE') { + if (mode in footprint) { + result += footprint[mode] * mtokm(userMetrics[i].values); + } else if (mode == 'IN_VEHICLE') { result += ((footprint['CAR'] + footprint['BUS'] + footprint["LIGHT_RAIL"] + footprint['TRAIN'] + footprint['TRAM'] + footprint['SUBWAY']) / 6) * mtokm(userMetrics[i].values); } else { - console.warn('WARNING FootprintHelper.getFootprintFromMetrics() was requested for an unknown mode: ' + baseMode + " metrics JSON: " + JSON.stringify(userMetrics)); + console.warn('WARNING FootprintHelper.getFootprintFromMetrics() was requested for an unknown mode: ' + mode + " metrics JSON: " + JSON.stringify(userMetrics)); result += defaultIfMissing * mtokm(userMetrics[i].values); } } diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 6e0b3eb81..2ed313f89 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -3,7 +3,7 @@ import { View } from 'react-native'; import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; -import { formatDateRangeOfDays, parseDataFromMetrics, generateSummaryFromData, calculatePercentChange, segmentDaysByWeeks } from './metricsHelper'; +import { formatDateRangeOfDays, parseDataFromMetrics, generateSummaryFromData, calculatePercentChange, segmentDaysByWeeks, isCustomLabels } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; import { getAngularService } from '../angular-react-helper'; @@ -39,6 +39,11 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //setting up data to be displayed let graphRecords = []; + //set custon dataset, if the labels are custom + if(isCustomLabels(userThisWeekModeMap)){ + FootprintHelper.setUseCustomFootprint(); + } + //calculate low-high and format range for prev week, if exists let userPrevWeek; if(userLastWeekSummaryMap[0]) { diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index 4d8dac427..fd44fc2ab 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -168,3 +168,45 @@ export function generateSummaryFromData(modeMap, metric) { return summaryMap; } + +/* +* We use the results to determine whether these results are from custom +* labels or from the automatically sensed labels. Automatically sensedV +* labels are in all caps, custom labels are prefixed by label, but have had +* the label_prefix stripped out before this. Results should have either all +* sensed labels or all custom labels. +*/ +export const isCustomLabels = function(modeMap) { + const isSensed = (mode) => mode == mode.toUpperCase(); + const isCustom = (mode) => mode == mode.toLowerCase(); + const metricSummaryChecksCustom = []; + const metricSummaryChecksSensed = []; + + const distanceKeys = modeMap.map((e) => e.key); + const isSensedKeys = distanceKeys.map(isSensed); + const isCustomKeys = distanceKeys.map(isCustom); + console.log("Checking metric keys", distanceKeys, " sensed ", isSensedKeys, + " custom ", isCustomKeys); + const isAllCustomForMetric = isAllCustom(isSensedKeys, isCustomKeys); + metricSummaryChecksSensed.push(!isAllCustomForMetric); + metricSummaryChecksCustom.push(isAllCustomForMetric); + + console.log("overall custom/not results for each metric = ", metricSummaryChecksCustom); + return isAllCustom(metricSummaryChecksSensed, metricSummaryChecksCustom); +} + +const isAllCustom = function(isSensedKeys, isCustomKeys) { + const allSensed = isSensedKeys.reduce((a, b) => a && b, true); + const anySensed = isSensedKeys.reduce((a, b) => a || b, false); + const allCustom = isCustomKeys.reduce((a, b) => a && b, true); + const anyCustom = isCustomKeys.reduce((a, b) => a || b, false); + if ((allSensed && !anyCustom)) { + return false; // sensed, not custom + } + if ((!anySensed && allCustom)) { + return true; // custom, not sensed; false implies that the other option is true + } + // Logger.displayError("Mixed entries that combine sensed and custom labels", + // "Please report to your program admin"); + return undefined; +} \ No newline at end of file From 388b5d74f1102d45af69bbe6cdb8781808bab26a Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Fri, 15 Sep 2023 16:13:41 -0400 Subject: [PATCH 144/164] carbon chart: fix border of green, 'low' emissions this hardcoded value caused the returned color to be the same for both the bar background and the border. The darkening should be based on the parameter allowing the border to use a darkened green while the background is not darkened. --- www/js/components/charting.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/components/charting.ts b/www/js/components/charting.ts index 9f7e35c2d..f0da14619 100644 --- a/www/js/components/charting.ts +++ b/www/js/components/charting.ts @@ -104,7 +104,7 @@ export function getGradient(chart, meter, currDataset, barCtx, alpha = null, dar const total = getBarHeight(barCtx.parsed._stacks); alpha = alpha || (currDataset.label == meter.dash_key ? 0.2 : 1); if (total < meter.middle) { - const adjColor = darken||alpha ? color(meterColors.below).darken(0.2).alpha(alpha).rgb().string() : meterColors.below; + const adjColor = darken||alpha ? color(meterColors.below).darken(darken).alpha(alpha).rgb().string() : meterColors.below; return adjColor; } const scaleMaxX = scales.x._range.max; From 44753f68bbb0fd64c0b34307115076a7e7059bbe Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Fri, 15 Sep 2023 16:14:43 -0400 Subject: [PATCH 145/164] add slight borderRadius to Charts This gives the bar charts a slightly softer feel, more in line with the rest of the UI which is pretty rounded off everywhere. --- www/js/components/Chart.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx index a645a094d..3969eb588 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -63,6 +63,7 @@ const Chart = ({ records, axisTitle, type, getColorForLabel, getColorForChartEl, darkenOrLighten(labelColorMap?.[e.label], -.5) || getColorForChartEl(chartRef.current, e, barCtx, 'border') ), borderWidth: borderWidth || 2, + borderRadius: 3, })), }; }, [chartDatasets, getColorForLabel]); From 9cad0eed41294d23108b9b82cc4884fb2f71bb82 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Fri, 15 Sep 2023 16:37:59 -0400 Subject: [PATCH 146/164] reorder goal lines + adjust colors If the scale of the graph is too large, the 2030 and 2050 guidelines labels might overlap. In that case, we should show the 2030 label on top. Also, the colors are kind of hard to see with the bars having their new gradient color scheme. So for the colors of the lines, let's just darken the yellow, and bump up the saturation of both. --- www/js/metrics/CarbonFootprintCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 2ed313f89..285c231e7 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -130,8 +130,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { }, [aggMetrics?.distance]); //hardcoded here, could be read from config at later customization? - let carbonGoals = [ {label: t('main-metrics.us-2030-goal'), value: 54, color: color(colors.danger).darken(.25).rgb().toString()}, - {label: t('main-metrics.us-2050-goal'), value: 14, color: color(colors.warn).darken(.25).rgb().toString()}]; + let carbonGoals = [ {label: t('main-metrics.us-2050-goal'), value: 14, color: color(colors.warn).darken(.65).saturate(.5).rgb().toString()}, + {label: t('main-metrics.us-2030-goal'), value: 54, color: color(colors.danger).saturate(.5).rgb().toString()} ]; let meter = { dash_key: t('main-metrics.unlabeled'), high: 54, middle: 14 }; return ( From c2c656c7081b6000ae403ee573c948def8b3cac3 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 15 Sep 2023 14:51:57 -0600 Subject: [PATCH 147/164] fix group text found a bug where the text for the group was being calculated off of user metrics! --- www/js/metrics/CarbonTextCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index 31d4f717a..de8bbfef3 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -71,7 +71,7 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { if(aggMetrics?.distance?.length > 0) { //separate data into weeks - const thisWeekDistance = segmentDaysByWeeks(userMetrics?.distance, 1)[0]; + const thisWeekDistance = segmentDaysByWeeks(aggMetrics?.distance, 1)[0]; let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, "aggregate"); let aggThisWeekSummary = generateSummaryFromData(aggThisWeekModeMap, "distance"); From dd126a7a0bcf9ef124f7a82ed972107212adf2e4 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 15 Sep 2023 16:03:52 -0600 Subject: [PATCH 148/164] only show metrics for whole weeks to avoid showing metrics for partial weeks, check the length before displaying. --- www/js/metrics/CarbonFootprintCard.tsx | 2 +- www/js/metrics/CarbonTextCard.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 285c231e7..72bfd76fc 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -26,7 +26,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //formatted data from last week let userLastWeekModeMap = {}; let userLastWeekSummaryMap = {}; - if(lastWeekDistance) { + if(lastWeekDistance && lastWeekDistance?.length >= 7) { userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); } diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index de8bbfef3..6d5b2df36 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -22,7 +22,7 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { //formatted data from last week let userLastWeekModeMap = {}; let userLastWeekSummaryMap = {}; - if(lastWeekDistance) { + if(lastWeekDistance && lastWeekDistance?.length >= 7) { userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); } From 1551eee6da264283aea38487b37ba11998db4fe9 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 15 Sep 2023 16:19:48 -0600 Subject: [PATCH 149/164] move functions into helpers moving endForceSync and forceTransition into their respective helpers --- www/js/control/ControlCollectionHelper.tsx | 12 +++- www/js/control/ControlSyncHelper.tsx | 56 ++++++++++++++++- www/js/control/ProfileSettings.jsx | 73 +--------------------- 3 files changed, 68 insertions(+), 73 deletions(-) diff --git a/www/js/control/ControlCollectionHelper.tsx b/www/js/control/ControlCollectionHelper.tsx index 6d829a3a6..7764f526b 100644 --- a/www/js/control/ControlCollectionHelper.tsx +++ b/www/js/control/ControlCollectionHelper.tsx @@ -21,6 +21,16 @@ type collectionConfig = { // const Logger = getAngularService("Logger"); +export async function forceTransition(transition) { + try { + let result = forceTransitionWrapper(transition); + window.alert('success -> '+result); + } catch (err) { + window.alert('error -> '+err); + console.log("error forcing state", err); + } +} + async function accuracy2String(config) { var accuracy = config.accuracy; let accuracyOptions = await getAccuracyOptions(); @@ -43,7 +53,7 @@ export async function isMediumAccuracy() { } else if (window.cordova.platformId == 'android') { return v != "PRIORITY_HIGH_ACCURACY"; } else { - // $ionicPopup.alert("Emission does not support this platform"); + window.alert("Emission does not support this platform"); } } } diff --git a/www/js/control/ControlSyncHelper.tsx b/www/js/control/ControlSyncHelper.tsx index bb032f398..15ba6b4c2 100644 --- a/www/js/control/ControlSyncHelper.tsx +++ b/www/js/control/ControlSyncHelper.tsx @@ -7,6 +7,7 @@ import { settingStyles } from "./ProfileSettings"; import { getAngularService } from "../angular-react-helper"; import SettingRow from "./SettingRow"; import AlertBar from "./AlertBar"; +import moment from "moment"; /* * BEGIN: Simple read/write wrappers @@ -48,7 +49,7 @@ const getEndTransitionKey = function() { type syncConfig = { sync_interval: number, ios_use_remote_push: boolean }; -export const ForceSyncRow = () => { +export const ForceSyncRow = ({getState}) => { const { t } = useTranslation(); const { colors } = useTheme(); const ClientStats = getAngularService('ClientStats'); @@ -101,9 +102,62 @@ export const ForceSyncRow = () => { }); }; + const getStartTransitionKey = function() { + if(window.cordova.platformId == 'android') { + return "local.transition.exited_geofence"; + } + else if(window.cordova.platformId == 'ios') { + return "T_EXITED_GEOFENCE"; + } + } + + const getEndTransitionKey = function() { + if(window.cordova.platformId == 'android') { + return "local.transition.stopped_moving"; + } + else if(window.cordova.platformId == 'ios') { + return "T_TRIP_ENDED"; + } + } + + const getOngoingTransitionState = function() { + if(window.cordova.platformId == 'android') { + return "local.state.ongoing_trip"; + } + else if(window.cordova.platformId == 'ios') { + return "STATE_ONGOING_TRIP"; + } + } + + async function getTransition(transKey) { + var entry_data = {}; + const curr_state = await getState(); + entry_data.curr_state = curr_state; + if (transKey == getEndTransitionKey()) { + entry_data.curr_state = getOngoingTransitionState(); + } + entry_data.transition = transKey; + entry_data.ts = moment().unix(); + return entry_data; + } + + async function endForceSync() { + /* First, quickly start and end the trip. Let's listen to the promise + * result for start so that we ensure ordering */ + var sensorKey = "statemachine/transition"; + return getTransition(getStartTransitionKey()).then(function(entry_data) { + return window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); + }).then(function() { + return getTransition(getEndTransitionKey()).then(function(entry_data) { + return window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); + }) + }).then(forceSync); + }; + return ( <> + {/* dataPending */} setDataPendingVis(false)} transparent={true}> diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 05366d480..82a282da2 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -18,7 +18,7 @@ import ActionMenu from "../components/ActionMenu"; import SensedPage from "./SensedPage" import LogPage from "./LogPage"; import ControlSyncHelper, {ForceSyncRow, getHelperSyncSettings} from "./ControlSyncHelper"; -import ControlCollectionHelper, {getHelperCollectionSettings, getState, isMediumAccuracy, helperToggleLowAccuracy, forceTransitionWrapper} from "./ControlCollectionHelper"; +import ControlCollectionHelper, {getHelperCollectionSettings, getState, isMediumAccuracy, helperToggleLowAccuracy, forceTransition} from "./ControlCollectionHelper"; //any pure functions can go outside const ProfileSettings = () => { @@ -59,8 +59,6 @@ const ProfileSettings = () => { const [showingLog, setShowingLog] = useState(false); const [editSync, setEditSync] = useState(false); const [editCollection, setEditCollection] = useState(false); - const [forceResultVis, setForceResultVis] = useState(false); - const [forceResult, setForceResult] = useState(""); // const [collectConfig, setCollectConfig] = useState({}); const [collectSettings, setCollectSettings] = useState({}); @@ -256,18 +254,6 @@ const ProfileSettings = () => { refreshCollectSettings(); } - async function forceTransition(transition) { - try { - let result = forceTransitionWrapper(transition); - setForceResultVis(true); - setForceResult('success -> '+result) - } catch (err) { - console.log("error forcing state", err); - setForceResultVis(true); - setForceResult('error -> '+err) - } - } - async function toggleLowAccuracy() { let toggle = await helperToggleLowAccuracy(); refreshCollectSettings(); @@ -313,46 +299,6 @@ const ProfileSettings = () => { //Platform.OS returns "web" now, but could be used once it's fully a Native app //for now, use window.cordova.platformId - // helper functions for endForceSync - const getStartTransitionKey = function() { - if(window.cordova.platformId == 'android') { - return "local.transition.exited_geofence"; - } - else if(window.cordova.platformId == 'ios') { - return "T_EXITED_GEOFENCE"; - } - } - - const getEndTransitionKey = function() { - if(window.cordova.platformId == 'android') { - return "local.transition.stopped_moving"; - } - else if(window.cordova.platformId == 'ios') { - return "T_TRIP_ENDED"; - } - } - - const getOngoingTransitionState = function() { - if(window.cordova.platformId == 'android') { - return "local.state.ongoing_trip"; - } - else if(window.cordova.platformId == 'ios') { - return "STATE_ONGOING_TRIP"; - } - } - - async function getTransition(transKey) { - var entry_data = {}; - const curr_state = await getState(); - entry_data.curr_state = curr_state; - if (transKey == getEndTransitionKey()) { - entry_data.curr_state = getOngoingTransitionState(); - } - entry_data.transition = transKey; - entry_data.ts = moment().unix(); - return entry_data; - } - const parseState = function(state) { console.log("state in parse state is", state); if (state) { @@ -368,19 +314,6 @@ const ProfileSettings = () => { } } - async function endForceSync() { - /* First, quickly start and end the trip. Let's listen to the promise - * result for start so that we ensure ordering */ - var sensorKey = "statemachine/transition"; - return getTransition(getStartTransitionKey()).then(function(entry_data) { - return window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); - }).then(function() { - return getTransition(getEndTransitionKey()).then(function(entry_data) { - return window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); - }) - }).then(forceSync); - } - async function invalidateCache() { window.cordova.plugins.BEMUserCache.invalidateAllCache().then(function(result) { console.log("invalidate result", result); @@ -450,14 +383,13 @@ const ProfileSettings = () => { setPermitVis(true)}> setCarbonDataVis(true)}> - + setDateDumpVis(true)}> {logUploadSection} - {notifSchedule} @@ -582,7 +514,6 @@ const ProfileSettings = () => { - From 1cfb190d65b1a7a7ab6636e5a86f671206c41c02 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 15 Sep 2023 16:41:38 -0600 Subject: [PATCH 150/164] restore log statement --- www/js/control/ControlCollectionHelper.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/www/js/control/ControlCollectionHelper.tsx b/www/js/control/ControlCollectionHelper.tsx index 7764f526b..b0855a733 100644 --- a/www/js/control/ControlCollectionHelper.tsx +++ b/www/js/control/ControlCollectionHelper.tsx @@ -19,8 +19,6 @@ type collectionConfig = { android_geofence_responsiveness: number }; -// const Logger = getAngularService("Logger"); - export async function forceTransition(transition) { try { let result = forceTransitionWrapper(transition); @@ -59,6 +57,7 @@ export async function isMediumAccuracy() { } export async function helperToggleLowAccuracy() { + const Logger = getAngularService("Logger"); let tempConfig = await getConfig(); let accuracyOptions = await getAccuracyOptions(); let medium = await isMediumAccuracy(); @@ -79,7 +78,7 @@ export async function helperToggleLowAccuracy() { let set = await setConfig(tempConfig); console.log("setConfig Sucess"); } catch (err) { - // Logger.displayError("Error while setting collection config", err); + Logger.displayError("Error while setting collection config", err); } } From f0129735229f40f2f9de72e9fae0705792800783 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 15 Sep 2023 16:42:07 -0600 Subject: [PATCH 151/164] fetch DB before updating --- www/js/control/SensedPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/control/SensedPage.tsx b/www/js/control/SensedPage.tsx index 8de1e7e5f..2f62d5c6c 100644 --- a/www/js/control/SensedPage.tsx +++ b/www/js/control/SensedPage.tsx @@ -21,6 +21,7 @@ const SensedPage = ({pageVis, setPageVis}) => { async function updateEntries() { //hardcoded function and keys after eliminating bit-rotted options + setDB(window.cordova.plugins.BEMUserCache); let userCacheFn = DB.getAllMessages; let userCacheKey = "statemachine/transition"; try { @@ -41,7 +42,6 @@ const SensedPage = ({pageVis, setPageVis}) => { } useEffect(() => { - setDB(window.cordova.plugins.BEMUserCache); updateEntries(); }, [pageVis]); From c46e2dcf2d3aa62662e6c5f9c13ab0b095ef1dd4 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 18 Sep 2023 10:01:51 -0600 Subject: [PATCH 152/164] move sync rows to devZone --- www/js/control/ProfileSettings.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 82a282da2..dcbedbd78 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -383,7 +383,6 @@ const ProfileSettings = () => { setPermitVis(true)}> setCarbonDataVis(true)}> - setDateDumpVis(true)}> {logUploadSection} @@ -391,6 +390,7 @@ const ProfileSettings = () => { + {notifSchedule} From 7f8ff42a67d72b1eb67fb72f720d23e5581dccaf Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 18 Sep 2023 10:33:02 -0600 Subject: [PATCH 153/164] move from .then to await for consistency and readability, re-writing the forceSync to rely on await rather than chained .then statements --- www/js/control/ControlSyncHelper.tsx | 60 +++++++++++++--------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/www/js/control/ControlSyncHelper.tsx b/www/js/control/ControlSyncHelper.tsx index 15ba6b4c2..341d9bd86 100644 --- a/www/js/control/ControlSyncHelper.tsx +++ b/www/js/control/ControlSyncHelper.tsx @@ -2,9 +2,9 @@ import React, { useEffect, useState } from "react"; import { Modal, View } from "react-native"; import { Dialog, Button, Switch, Text, useTheme } from 'react-native-paper'; import { useTranslation } from "react-i18next"; -import ActionMenu from "../components/ActionMenu"; import { settingStyles } from "./ProfileSettings"; import { getAngularService } from "../angular-react-helper"; +import ActionMenu from "../components/ActionMenu"; import SettingRow from "./SettingRow"; import AlertBar from "./AlertBar"; import moment from "moment"; @@ -49,6 +49,7 @@ const getEndTransitionKey = function() { type syncConfig = { sync_interval: number, ios_use_remote_push: boolean }; +//forceSync and endForceSync SettingRows & their actions export const ForceSyncRow = ({getState}) => { const { t } = useTranslation(); const { colors } = useTheme(); @@ -59,47 +60,40 @@ export const ForceSyncRow = ({getState}) => { const [dataPushedVis, setDataPushedVis] = useState(false); async function forceSync() { - ClientStats.addEvent(ClientStats.getStatKeys().BUTTON_FORCE_SYNC).then( - function() { - console.log("Added "+ClientStats.getStatKeys().BUTTON_FORCE_SYNC+" event"); - }); - forcePluginSync().then(function() { + try { + let addedEvent = ClientStats.addEvent(ClientStats.getStatKeys().BUTTON_FORCE_SYNC); + console.log("Added "+ClientStats.getStatKeys().BUTTON_FORCE_SYNC+" event"); + + let sync = await forcePluginSync(); /* - * Change to sensorKey to "background/location" after fixing issues - * with getLastSensorData and getLastMessages in the usercache - * See https://github.com/e-mission/e-mission-phone/issues/279 for details - */ + * Change to sensorKey to "background/location" after fixing issues + * with getLastSensorData and getLastMessages in the usercache + * See https://github.com/e-mission/e-mission-phone/issues/279 for details + */ var sensorKey = "statemachine/transition"; - return window.cordova.plugins.BEMUserCache.getAllMessages(sensorKey, true); - }).then(function(sensorDataList) { - Logger.log("sensorDataList = "+JSON.stringify(sensorDataList)); + let sensorDataList = await window.cordova.plugins.BEMUserCache.getAllMessages(sensorKey, true); + // If everything has been pushed, we should - // only have one entry for the battery, which is the one that was - // inserted on the last successful push. - var isTripEnd = function(entry) { - if (entry.metadata.key == getEndTransitionKey()) { - return true; - } else { - return false; - } - }; - var syncLaunchedCalls = sensorDataList.filter(isTripEnd); - var syncPending = (syncLaunchedCalls.length > 0); + // have no more trip end transitions left + let isTripEnd = function(entry) { + return entry.metadata == getEndTransitionKey(); + } + let syncLaunchedCalls = sensorDataList.filter(isTripEnd); + let syncPending = syncLaunchedCalls.length > 0; Logger.log("sensorDataList.length = "+sensorDataList.length+ - ", syncLaunchedCalls.length = "+syncLaunchedCalls.length+ - ", syncPending? = "+syncPending); - return syncPending; - }).then(function(syncPending) { + ", syncLaunchedCalls.length = "+syncLaunchedCalls.length+ + ", syncPending? = "+syncPending); Logger.log("sync launched = "+syncPending); - if (syncPending) { - Logger.log("data is pending, showing confirm dialog"); - setDataPendingVis(true); //consent handling in modal + + if(syncPending) { + Logger.log(Logger.log("data is pending, showing confirm dialog")); + setDataPendingVis(true); //consent handling in modal } else { setDataPushedVis(true); } - }).catch(function(error) { + } catch (error) { Logger.displayError("Error while forcing sync", error); - }); + } }; const getStartTransitionKey = function() { From 287687291e6e2931959c85502c6a7a96aa370934 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 18 Sep 2023 10:38:55 -0600 Subject: [PATCH 154/164] endTripForceSync using await for readability, switch from chained .then statements to using await calls --- www/js/control/ControlSyncHelper.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/www/js/control/ControlSyncHelper.tsx b/www/js/control/ControlSyncHelper.tsx index 341d9bd86..490672c4d 100644 --- a/www/js/control/ControlSyncHelper.tsx +++ b/www/js/control/ControlSyncHelper.tsx @@ -139,13 +139,11 @@ export const ForceSyncRow = ({getState}) => { /* First, quickly start and end the trip. Let's listen to the promise * result for start so that we ensure ordering */ var sensorKey = "statemachine/transition"; - return getTransition(getStartTransitionKey()).then(function(entry_data) { - return window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); - }).then(function() { - return getTransition(getEndTransitionKey()).then(function(entry_data) { - return window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); - }) - }).then(forceSync); + let entry_data = await getTransition(getStartTransitionKey()); + let messagePut = await window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); + entry_data = await getTransition(getEndTransitionKey()); + messagePut = await window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); + forceSync(); }; return ( From de20d3bf3510aead45ad5302391c9cb7fe504e80 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 18 Sep 2023 11:45:19 -0600 Subject: [PATCH 155/164] "taxi" -> "worst case" https://github.com/e-mission/e-mission-phone/pull/1018#issuecomment-1718722779 --- www/i18n/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index 8c9da147f..c362bb112 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -164,7 +164,7 @@ "how-it-compares": "Ballpark comparisons", "optimal": "Optimal (perfect mode choice for all my trips)", "average": "Group Avg.", - "worst-case": "If all 'taxi'", + "worst-case": "Worse Case", "label-to-squish": "Label trips to collapse the range into a single number", "range-uncertain-footnote": "²Due to the uncertainty of unlabeled trips, estimates may fall anywhere within the shown range. Label more trips for richer estimates.", "lastweek": "My last week value:", From 8f5fb8907bcdd3cfd51e98d20fdc7d0e53f03d96 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 18 Sep 2023 13:19:43 -0600 Subject: [PATCH 156/164] remove calorie keys we've moved away from showing "calories burned in active travel" and started to show "minutes spent in active travel" Therefore, we no longer need the keys about calories, calibrating to user date (already left profile) or comparing the calories to food. --- www/i18n/en.json | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index c362bb112..8bbb283fd 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -142,13 +142,6 @@ "no-travel-hint": "To see more, change the filters above or go record some travel!" }, - "user-gender": "Gender", - "gender-male": "Male", - "gender-female": "Female", - "user-height": "Height", - "user-weight": "Weight", - "user-age": "Age", - "main-metrics":{ "dashboard": "Dashboard", "summary": "My Summary", @@ -173,18 +166,10 @@ "us-goals-footnote": "¹Guidelines based on US decarbonization goals, scaled to per-capita travel-related emissions.", "past-week" : "Past Week", "prev-week" : "Prev. Week", - "calories": "My Calories", - "calibrate": "Calibrate", "no-summary-data": "No summary data", "mean-speed": "My Average Speed", "user-totals": "My Totals", "group-totals": "Group Totals", - "equals-cookies_one": "Equals at least {{count}} homemade chocolate chip cookie", - "equals-cookies_other": "Equals at least {{count}} homemade chocolate chip cookies", - "equals-icecream_one": "Equals at least {{count}} half cup vanilla ice cream", - "equals-icecream_other": "Equals at least {{count}} half cups vanilla ice cream", - "equals-bananas_one": "Equals at least {{count}} banana", - "equals-bananas_other": "Equals at least {{count}} bananas", "active-minutes": "Active Minutes", "weekly-active-minutes": "Weekly minutes of active travel", "daily-active-minutes": "Daily minutes of active travel", From 8c6bdb2d2f4021cc91037b0742a46c2e7f752b66 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 25 Sep 2023 11:12:52 -0600 Subject: [PATCH 157/164] update comments about prev vs past week since we have room in the code, clarify what we mean by referencing "how many days ago" the calculations cover --- www/js/metrics/CarbonFootprintCard.tsx | 8 ++++---- www/js/metrics/CarbonTextCard.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 72bfd76fc..ac25dc131 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -23,7 +23,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //separate data into weeks const [thisWeekDistance, lastWeekDistance] = segmentDaysByWeeks(userMetrics?.distance, 2); - //formatted data from last week + //formatted data from last week, if exists (14 days ago -> 8 days ago) let userLastWeekModeMap = {}; let userLastWeekSummaryMap = {}; if(lastWeekDistance && lastWeekDistance?.length >= 7) { @@ -31,7 +31,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); } - //formatted distance data from this week + //formatted distance data from this week (7 days ago -> yesterday) let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); let worstDistance = userThisWeekSummaryMap.reduce((prevDistance, currModeSummary) => prevDistance + currModeSummary.values, 0); @@ -44,7 +44,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { FootprintHelper.setUseCustomFootprint(); } - //calculate low-high and format range for prev week, if exists + //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) let userPrevWeek; if(userLastWeekSummaryMap[0]) { userPrevWeek = { @@ -55,7 +55,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { graphRecords.push({label: t('main-metrics.labeled'), x: userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}); } - //calculate low-high and format range for past week + //calculate low-high and format range for past week (7 days ago -> yesterday) let userPastWeek = { low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), high: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, FootprintHelper.getHighestFootprint()), diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index 6d5b2df36..d3132e2ae 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -19,7 +19,7 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { //separate data into weeks const [thisWeekDistance, lastWeekDistance] = segmentDaysByWeeks(userMetrics?.distance, 2); - //formatted data from last week + //formatted data from last week, if exists (14 days ago -> 8 days ago) let userLastWeekModeMap = {}; let userLastWeekSummaryMap = {}; if(lastWeekDistance && lastWeekDistance?.length >= 7) { @@ -27,7 +27,7 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); } - //formatted distance data from this week + //formatted distance data from this week (7 days ago -> yesterday) let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); let worstDistance = userThisWeekSummaryMap.reduce((prevDistance, currModeSummary) => prevDistance + currModeSummary.values, 0); @@ -35,7 +35,7 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { //setting up data to be displayed let textList = []; - //calculate low-high and format range for prev week, if exists + //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) if(userLastWeekSummaryMap[0]) { let userPrevWeek = { low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), @@ -48,7 +48,7 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { textList.push({label: label + '²', value: `${Math.round(userPrevWeek.low)} - ${Math.round(userPrevWeek.high)}`}); } - //calculate low-high and format range for past week + //calculate low-high and format range for past week (7 days ago -> yesterday) let userPastWeek = { low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), high: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, FootprintHelper.getHighestFootprint()), From 752674b7067701c072621a89b83e5c6cbcb2f3f8 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 25 Sep 2023 11:23:33 -0600 Subject: [PATCH 158/164] not >= 7 day weeks, == 7 day weeks --- www/js/metrics/CarbonFootprintCard.tsx | 2 +- www/js/metrics/CarbonTextCard.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index ac25dc131..6012cb61a 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -26,7 +26,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //formatted data from last week, if exists (14 days ago -> 8 days ago) let userLastWeekModeMap = {}; let userLastWeekSummaryMap = {}; - if(lastWeekDistance && lastWeekDistance?.length >= 7) { + if(lastWeekDistance && lastWeekDistance?.length == 7) { userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); } diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index d3132e2ae..223ae709f 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -22,7 +22,7 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { //formatted data from last week, if exists (14 days ago -> 8 days ago) let userLastWeekModeMap = {}; let userLastWeekSummaryMap = {}; - if(lastWeekDistance && lastWeekDistance?.length >= 7) { + if(lastWeekDistance && lastWeekDistance?.length == 7) { userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); } From bb4fdd0317ba1ae9d0c8d35332e9180355c48a29 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 25 Sep 2023 17:08:17 -0600 Subject: [PATCH 159/164] update infinity behavior on changeIndicator if the change is infinite, show that. If the change is infinite, and infinitely uncertain (-inf/+inf) don't show, and otherwise show the finite range --- www/js/metrics/ChangeIndicator.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/www/js/metrics/ChangeIndicator.tsx b/www/js/metrics/ChangeIndicator.tsx index d225b2e89..eafd3460e 100644 --- a/www/js/metrics/ChangeIndicator.tsx +++ b/www/js/metrics/ChangeIndicator.tsx @@ -22,13 +22,17 @@ const ChangeIndicator = ({ change }) => { const changeText = useMemo(() => { if(change) { - let low = isNaN(change.low) ? '∞' : Math.round(Math.abs(change.low)); - let high = isNaN(change.high) ? '∞' : Math.round(Math.abs(change.high)); - if(change.low == change.high) + let low = isFinite(change.low) ? Math.round(Math.abs(change.low)): '∞'; + let high = isFinite(change.high) ? Math.round(Math.abs(change.high)) : '∞'; + + if(Math.round(change.low) == Math.round(change.high)) { let text = changeSign(change.low) + low + "%"; return text; - } else { + } else if(!(isFinite(change.low) || isFinite(change.high))) { + return ""; //if both are not finite, no information is really conveyed, so don't show + } + else { let text = `${changeSign(change.low) + low}% / ${changeSign(change.high) + high}%`; return text; } @@ -36,7 +40,7 @@ const ChangeIndicator = ({ change }) => { },[change]) return ( - (change.low) ? + (changeText != "") ? 0 ? colors.danger : colors.success)}> {changeText + '\n'} From 00d808dd0422673afb315c5d43f020fec2ae0a35 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 27 Sep 2023 17:53:27 -0400 Subject: [PATCH 160/164] register all ChartJS elements as workaround Described in https://github.com/e-mission/e-mission-docs/issues/986#issuecomment-1738074971, we have a production-specific error where the needed ChartJS elements are not recognized as being registered. Likely an incompatibility between ChartJS and Webpack. As a workaround, we will register all of the `registerables`. Unfortunately, we will no longer benefit from tree-shaking with this approach. --- www/js/components/Chart.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx index 3969eb588..79c6e40e4 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -2,23 +2,12 @@ import React, { useEffect, useRef, useState, useMemo } from 'react'; import { View } from 'react-native'; import { useTheme } from 'react-native-paper'; -import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, TimeScale, ChartData, ChartType, ScriptableContext, PointElement, LineElement } from 'chart.js'; +import { Chart as ChartJS, registerables } from 'chart.js'; import { Chart as ChartJSChart } from 'react-chartjs-2'; import Annotation, { AnnotationOptions, LabelPosition } from 'chartjs-plugin-annotation'; import { dedupColors, getChartHeight, darkenOrLighten } from './charting'; -ChartJS.register( - CategoryScale, - LinearScale, - TimeScale, - BarElement, - PointElement, - LineElement, - Title, - Tooltip, - Legend, - Annotation, -); +ChartJS.register(...registerables, Annotation); type XYPair = { x: number|string, y: number|string }; type ChartDataset = { From 8b6c3165d91ae176b92e7ba09eeefa0d4fe573b4 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 28 Sep 2023 12:26:08 -0400 Subject: [PATCH 161/164] fix: handle undetermined state of appstatus checks In AppStatusModal, we listen for the value of overallStatus and if it is false, we trigger the permissions popup. But there is an issue where the AppStatusModal sometimes shows up even if all the checks are green. This is because it can take some time for the checks to initialize and return back their value. During this time, we want to have a sort of 'undetermined' state for overallStatus, so that we don't show the popup prematurely. We'll do this by allowing overallStatus to be either true/false OR undefined. If there are no checks yet, overallStatus will be undefined and we won't show the popup yet. Once the checks are initialized, we'll be able to determine the status and it will return true or false. For the individual checks themselves, they also need to be able to report an 'undetermined' state. If their statusStates are false by default, and are false before they have actually been determined, then overallStatus will also be false while the check is still ongoing. So we should remove the initial 'false' condition from the few checks that had it, allowing the initial to be 'undefined', and when we iterate through the checks, we will explicitly check if `statusState === false` instead of just checking if `!statusState`. Result of this is that AppStatusModal only shows if any permissions are broken. Tested by loading the app, with all of the correct permissions, 20 times both before and after the change. Before the change, 2 of the attempts resulted in the AppStatusModal incorrectly appearing. After the change, the AppStatusModal did not appear on any of the 20 attempts. After this, I disallowed the Location permission and the modal did still appear as expected. --- www/js/control/AppStatusModal.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/www/js/control/AppStatusModal.tsx b/www/js/control/AppStatusModal.tsx index ff5d18ca1..5cb2d3e00 100644 --- a/www/js/control/AppStatusModal.tsx +++ b/www/js/control/AppStatusModal.tsx @@ -31,10 +31,12 @@ const AppStatusModal = ({permitVis, setPermitVis}) => { let iconMap = (statusState) => statusState ? "check-circle-outline" : "alpha-x-circle-outline"; let colorMap = (statusState) => statusState ? colors.success : colors.danger; - const overallStatus = useMemo(() => { + const overallStatus = useMemo(() => { let status = true; + if (!checkList?.length) return undefined; // if checks not loaded yet, status is undetermined checkList.forEach((lc) => { - if(!lc.statusState){ + console.debug('check in permission status for ' + lc.name + ':', lc.statusState); + if (lc.statusState === false) { status = false; } }) @@ -114,14 +116,12 @@ const AppStatusModal = ({permitVis, setPermitVis}) => { let locSettingsCheck = { name: t("intro.appstatus.locsettings.name"), desc: t(androidSettingsDescTag), - statusState: false, fix: fixSettings, refresh: checkSettings } let locPermissionsCheck = { name: t("intro.appstatus.locperms.name"), desc: t(androidPermDescTag), - statusState: false, fix: fixPerms, refresh: checkPerms } @@ -161,14 +161,12 @@ const AppStatusModal = ({permitVis, setPermitVis}) => { const locSettingsCheck = { name: t("intro.appstatus.locsettings.name"), desc: t(iOSSettingsDescTag), - statusState: false, fix: fixSettings, refresh: checkSettings }; const locPermissionsCheck = { name: t("intro.appstatus.locperms.name"), desc: t(iOSPermDescTag), - statusState: false, fix: fixPerms, refresh: checkPerms }; @@ -443,4 +441,4 @@ const AppStatusModal = ({permitVis, setPermitVis}) => { ) } -export default AppStatusModal; \ No newline at end of file +export default AppStatusModal; From 8e061a0ca954dfe6b2c9180fe2c789d6d0b6a11c Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 28 Sep 2023 12:17:24 -0600 Subject: [PATCH 162/164] fix blank checkList problem on some occasions, the checkList was failing to be initialized. Upon investigation, this turned out to be because platform and osver were "" and 0, respectively. The underlying issue is that they were held in useState, which is updated asyncronously. useRef is more appropriate here, as the platform and osver only need to be set once, and are not anticipated to change. The access changed, as useRef variables are read and set from "var.current" Before these changes, roughly 1/3 of the time, the platform and osver were not set yet, and so the checks were not set up as the platform was interpreted as not valid. After the change, the platform and osver are always set properly in time to call createChecklist. I also updated the useEffect that calls the createChecklist to do so if checkList does not exist or has a length of 0, rather than the else that I had before. --- www/js/control/AppStatusModal.tsx | 39 ++++++++++++++++--------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/www/js/control/AppStatusModal.tsx b/www/js/control/AppStatusModal.tsx index 5cb2d3e00..34f5820bf 100644 --- a/www/js/control/AppStatusModal.tsx +++ b/www/js/control/AppStatusModal.tsx @@ -1,5 +1,5 @@ //component to view and manage permission settings -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect, useMemo, useRef } from "react"; import { Modal, useWindowDimensions, ScrollView } from "react-native"; import { Dialog, Button, Text, useTheme } from 'react-native-paper'; import { useTranslation } from "react-i18next"; @@ -16,8 +16,8 @@ const AppStatusModal = ({permitVis, setPermitVis}) => { const { appConfig, loading } = useAppConfig(); const { height: windowHeight } = useWindowDimensions(); - const [osver, setOsver] = useState(0); - const [platform, setPlatform] = useState(""); + const osver = useRef(0); + const platform = useRef(""); const [error, setError] = useState(""); const [errorVis, setErrorVis] = useState(false); @@ -98,17 +98,17 @@ const AppStatusModal = ({permitVis, setPermitVis}) => { return checkOrFix(locPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidLocationPermissions, false); }; var androidSettingsDescTag = "intro.appstatus.locsettings.description.android-gte-9"; - if (osver < 9) { + if (osver.current < 9) { androidSettingsDescTag = "intro.appstatus.locsettings.description.android-lt-9"; } var androidPermDescTag = "intro.appstatus.locperms.description.android-gte-12"; - if(osver < 6) { + if(osver.current < 6) { androidPermDescTag = 'intro.appstatus.locperms.description.android-lt-6'; - } else if (osver < 10) { + } else if (osver.current < 10) { androidPermDescTag = "intro.appstatus.locperms.description.android-6-9"; - } else if (osver < 11) { + } else if (osver.current < 11) { androidPermDescTag= "intro.appstatus.locperms.description.android-10"; - } else if (osver < 12) { + } else if (osver.current < 12) { androidPermDescTag= "intro.appstatus.locperms.description.android-11"; } console.log("description tags are "+androidSettingsDescTag+" "+androidPermDescTag); @@ -153,7 +153,7 @@ const AppStatusModal = ({permitVis, setPermitVis}) => { }; var iOSSettingsDescTag = "intro.appstatus.locsettings.description.ios"; var iOSPermDescTag = "intro.appstatus.locperms.description.ios-gte-13"; - if(osver < 13) { + if(osver.current < 13) { iOSPermDescTag = 'intro.appstatus.locperms.description.ios-lt-13'; } console.log("description tags are "+iOSSettingsDescTag+" "+iOSPermDescTag); @@ -176,7 +176,7 @@ const AppStatusModal = ({permitVis, setPermitVis}) => { } function setupAndroidFitnessChecks() { - if(osver >= 10){ + if(osver.current >= 10){ let fixPerms = function() { console.log("fix and refresh fitness permissions"); return checkOrFix(fitnessPermissionsCheck, window['cordova'].plugins.BEMDataCollection.fixFitnessPermissions, @@ -267,10 +267,10 @@ const AppStatusModal = ({permitVis, setPermitVis}) => { false); }; var androidUnusedDescTag = "intro.appstatus.unusedapprestrict.description.android-disable-gte-13"; - if (osver == 12) { + if (osver.current == 12) { androidUnusedDescTag= "intro.appstatus.unusedapprestrict.description.android-disable-12"; } - else if (osver < 12) { + else if (osver.current < 12) { androidUnusedDescTag= "intro.appstatus.unusedapprestrict.description.android-disable-lt-12"; } let unusedAppsUnrestrictedCheck = { @@ -295,9 +295,9 @@ const AppStatusModal = ({permitVis, setPermitVis}) => { let overallFitnessName = t('intro.appstatus.overall-fitness-name-android'); let locExplanation = t('intro.appstatus.overall-loc-description'); - if(platform == "ios") { + if(platform.current == "ios") { overallFitnessName = t('intro.appstatus.overall-fitness-name-ios'); - if(osver < 13) { + if(osver.current < 13) { locExplanation = (t("intro.permissions.locationPermExplanation-ios-lt-13")); } else { locExplanation = (t("intro.permissions.locationPermExplanation-ios-gte-13")); @@ -316,12 +316,13 @@ const AppStatusModal = ({permitVis, setPermitVis}) => { } function createChecklist(){ - if(platform == "android") { + console.debug("setting up checks, platform is " + platform.current + "and osver is " + osver.current); + if(platform.current == "android") { setupAndroidLocChecks(); setupAndroidFitnessChecks(); setupAndroidNotificationChecks(); setupAndroidBackgroundRestrictionChecks(); - } else if (platform == "ios") { + } else if (platform.current == "ios") { setupIOSLocChecks(); setupIOSFitnessChecks(); setupAndroidNotificationChecks(); @@ -369,8 +370,8 @@ const AppStatusModal = ({permitVis, setPermitVis}) => { //load when ready useEffect(() => { if (appConfig && window['device']?.platform) { - setPlatform(window['device'].platform.toLowerCase()); - setOsver(window['device'].version.split(".")[0]); + platform.current = window['device'].platform.toLowerCase(); + osver.current = window['device'].version.split(".")[0]; if(!haveSetText) { @@ -378,7 +379,7 @@ const AppStatusModal = ({permitVis, setPermitVis}) => { setupPermissionText(); setHaveSetText(true); } - else{ + if(!checkList || checkList.length == 0) { console.log("setting up permissions"); createChecklist(); } From faaeda60bf93190e0998fecab6c529da9e3e412e Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 28 Sep 2023 14:54:13 -0400 Subject: [PATCH 163/164] export modeColors so it can be used in tests Something I noticed while adding new tests - modeColors is used in diaryHelper.test.ts, but it isn't exported from diaryHelper.ts. I think this was due to a bad merge conflict. Simply exporting it fixes the tests, so I'm just going to bundle in that fix here. --- www/js/diary/diaryHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts index c836d9db2..0b834a485 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -5,7 +5,7 @@ import moment from "moment"; import { DateTime } from "luxon"; import { LabelOptions, readableLabelToKey } from "../survey/multilabel/confirmHelper"; -const modeColors = { +export const modeColors = { pink: '#c32e85', // oklch(56% 0.2 350) // e-car red: '#c21725', // oklch(52% 0.2 25) // car orange: '#bf5900', // oklch(58% 0.16 50) // air, hsr From e5b2834df09bf01cea814525e3f09a872d0e1fc5 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 28 Sep 2023 13:28:46 -0600 Subject: [PATCH 164/164] fix toggle issue - remove cch I forgot to remove cch from one place in this file when I translated it from it's former angular version, which was causing the problem with android devices and the medium accuracy toggle While here, resolve the syntax highlighting related to window.cordova by replacing it with window['cordova'], not fixing this syntax in the first place likely contributed to me overlooking this error. --- www/js/control/ControlCollectionHelper.tsx | 31 +++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/www/js/control/ControlCollectionHelper.tsx b/www/js/control/ControlCollectionHelper.tsx index b0855a733..99318b1ac 100644 --- a/www/js/control/ControlCollectionHelper.tsx +++ b/www/js/control/ControlCollectionHelper.tsx @@ -46,9 +46,10 @@ export async function isMediumAccuracy() { return undefined; // config not loaded when loading ui, set default as false } else { var v = await accuracy2String(config); - if (window.cordova.platformId == 'ios') { + console.log("window platform is", window['cordova'].platformId); + if (window['cordova'].platformId == 'ios') { return v != "kCLLocationAccuracyBestForNavigation" && v != "kCLLocationAccuracyBest" && v != "kCLLocationAccuracyTenMeters"; - } else if (window.cordova.platformId == 'android') { + } else if (window['cordova'].platformId == 'android') { return v != "PRIORITY_HIGH_ACCURACY"; } else { window.alert("Emission does not support this platform"); @@ -62,15 +63,15 @@ export async function helperToggleLowAccuracy() { let accuracyOptions = await getAccuracyOptions(); let medium = await isMediumAccuracy(); if (medium) { - if (window.cordova.platformId == 'ios') { + if (window['cordova'].platformId == 'ios') { tempConfig.accuracy = accuracyOptions["kCLLocationAccuracyBest"]; - } else if (window.cordova.platformId == 'android') { - tempConfig.accuracy = cch.accuracyOptions["PRIORITY_HIGH_ACCURACY"]; + } else if (window['cordova'].platformId == 'android') { + tempConfig.accuracy = accuracyOptions["PRIORITY_HIGH_ACCURACY"]; } } else { - if (window.cordova.platformId == 'ios') { + if (window['cordova'].platformId == 'ios') { tempConfig.accuracy = accuracyOptions["kCLLocationAccuracyHundredMeters"]; - } else if (window.cordova.platformId == 'android') { + } else if (window['cordova'].platformId == 'android') { tempConfig.accuracy = accuracyOptions["PRIORITY_BALANCED_POWER_ACCURACY"]; } } @@ -87,7 +88,7 @@ export async function helperToggleLowAccuracy() { */ export const getState = function() { - return window.cordova.plugins.BEMDataCollection.getState(); + return window['cordova'].plugins.BEMDataCollection.getState(); }; export async function getHelperCollectionSettings() { @@ -101,18 +102,18 @@ export async function getHelperCollectionSettings() { } const setConfig = function(config) { - return window.cordova.plugins.BEMDataCollection.setConfig(config); + return window['cordova'].plugins.BEMDataCollection.setConfig(config); }; const getConfig = function() { - return window.cordova.plugins.BEMDataCollection.getConfig(); + return window['cordova'].plugins.BEMDataCollection.getConfig(); }; const getAccuracyOptions = function() { - return window.cordova.plugins.BEMDataCollection.getAccuracyOptions(); + return window['cordova'].plugins.BEMDataCollection.getAccuracyOptions(); }; export const forceTransitionWrapper = function(transition) { - return window.cordova.plugins.BEMDataCollection.forceTransition(transition); + return window['cordova'].plugins.BEMDataCollection.forceTransition(transition); }; const formatConfigForDisplay = function(config, accuracyOptions) { @@ -197,7 +198,7 @@ const ControlSyncHelper = ({ editVis, setEditVis }) => { /*ios vs android*/ let filterComponent; - if(window.cordova.platformId == 'ios') { + if(window['cordova'].platformId == 'ios') { filterComponent = Filter Distance onChangeText(text, "filter_distance")}/> @@ -209,7 +210,7 @@ const ControlSyncHelper = ({ editVis, setEditVis }) => { } let iosToggles; - if(window.cordova.platformId == 'ios') { + if(window['cordova'].platformId == 'ios') { iosToggles = <> {/* use visit notifications toggle NO ANDROID */} @@ -224,7 +225,7 @@ const ControlSyncHelper = ({ editVis, setEditVis }) => { } let geofenceComponent; - if(window.cordova.platformId == 'android') { + if(window['cordova'].platformId == 'android') { geofenceComponent = Geofence Responsiveness onChangeText(text, "android_geofence_responsiveness")}/>