CycleView & more
This commit is contained in:
parent
23eb634c30
commit
39632d4eec
4
App.js
4
App.js
|
@ -31,7 +31,7 @@ export default class App extends React.Component {
|
||||||
...Ionicons.font,
|
...Ionicons.font,
|
||||||
});
|
});
|
||||||
this.setState({ isReady: true });
|
this.setState({ isReady: true });
|
||||||
this.device = new DeviceReduxCoupling(store);
|
this.device = new DeviceReduxCoupling(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -41,7 +41,7 @@ export default class App extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ThemedStackNavigation/>
|
<ThemedStackNavigation />
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Animated, TouchableWithoutFeedback } from 'react-native';
|
||||||
|
import { View } from 'native-base';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
|
||||||
|
export default class CycleView extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
activeView: props.initialView
|
||||||
|
};
|
||||||
|
|
||||||
|
this.opacityArr = React.Children.map(props.children,
|
||||||
|
(c, i) => { console.log("iter", c, i); return new Animated.Value(i === props.initialView ? 1 : 0); });
|
||||||
|
|
||||||
|
this.fadeInAnimations = this.opacityArr.map(a => Animated.timing(a, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: props.fadeDuration,
|
||||||
|
useNativeDriver: true
|
||||||
|
}));
|
||||||
|
this.fadeOutAnimations = this.opacityArr.map(a => Animated.timing(a, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: props.fadeDuration,
|
||||||
|
useNativeDriver: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log("opacity arr length", this.opacityArr.length, React.Children.count(props.children));
|
||||||
|
}
|
||||||
|
|
||||||
|
_onTouch = () => {
|
||||||
|
const currentViewIdx = this.state.activeView;
|
||||||
|
const nextViewIdx = (currentViewIdx + 1) % React.Children.count(this.props.children);
|
||||||
|
this.fadeOutAnimations[currentViewIdx].start();
|
||||||
|
this.fadeInAnimations[nextViewIdx].start();
|
||||||
|
this.setState({ activeView: nextViewIdx });
|
||||||
|
this.props.onViewChange(nextViewIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const children = React.Children.map(this.props.children, (c, i) => c);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableWithoutFeedback onPress={this._onTouch} styles={{ flex: 1 }}>
|
||||||
|
<View styles={{ flex: 1, flexDirection: 'row', width: "100%" }}>
|
||||||
|
{
|
||||||
|
React.Children.map(this.props.children, (c, i) => {
|
||||||
|
return <Animated.View style={{
|
||||||
|
position: i === 0 ? 'relative' : 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
opacity: this.opacityArr[i],
|
||||||
|
transform: [{
|
||||||
|
scale: this.opacityArr[i].interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [this.props.minScaleOnFade, 1],
|
||||||
|
})
|
||||||
|
}]
|
||||||
|
}}>
|
||||||
|
{c}
|
||||||
|
</Animated.View>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CycleView.propTypes = {
|
||||||
|
initialView: PropTypes.number,
|
||||||
|
fadeDuration: PropTypes.number,
|
||||||
|
minScaleOnFade: PropTypes.number,
|
||||||
|
onViewChange: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
CycleView.defaultProps = {
|
||||||
|
initialView: 0,
|
||||||
|
fadeDuration: 600,
|
||||||
|
minScaleOnFade: 0.4,
|
||||||
|
onViewChange: viewIdx => null,
|
||||||
|
}
|
|
@ -1,9 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, StyleSheet } from 'react-native';
|
import { View, StyleSheet } from 'react-native';
|
||||||
import { useWindowDimensions } from 'react-native';
|
import Svg, { Polyline, Text, Circle } from 'react-native-svg';
|
||||||
|
|
||||||
//import Svg, {Polyline, Polygon, Rect, G} from 'react-native-svg-web';
|
|
||||||
import Svg, { Polyline, Polygon, Rect, G, Text, Circle } from 'react-native-svg';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,35 +27,33 @@ function computeTickMarks(largest, mostTicks) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
let isFirstRender = true;
|
|
||||||
|
|
||||||
const Graph = props => {
|
const Graph = props => {
|
||||||
const graphHeight = 100;
|
const graphHeight = 100;
|
||||||
const numPoints = 300;
|
const numPoints = 300;
|
||||||
const yLabelSpace = 40;
|
const yLabelSpace = 40;
|
||||||
const graphWidth = numPoints + yLabelSpace;
|
const graphWidth = numPoints + yLabelSpace;
|
||||||
const minKgScale = 3; // scale such that upper graph value is n kg
|
const minKgScale = 4; // scale such that upper graph value is n kg
|
||||||
|
|
||||||
const data = props.data.slice(-numPoints);
|
const data = props.data.slice(-numPoints);
|
||||||
const maxValueDeviceCoord = data.reduce((running, x) => Math.max(x, running), minKgScale / props.kgFactor);
|
const maxValueDeviceCoord = data.reduce((running, x) => Math.max(x, running), minKgScale / props.kgFactor);
|
||||||
const maxValueKg = Math.max(maxValueDeviceCoord * props.kgFactor, minKgScale);
|
const maxValueKg = Math.max(maxValueDeviceCoord * props.kgFactor, minKgScale);
|
||||||
|
|
||||||
const dataInDeviceCoordToSvgCoordX = x => yLabelSpace + x;
|
const devCoordToSvgX = x => yLabelSpace + x;
|
||||||
const dataInDeviceCoordToSvgCoordY = y => graphHeight - (y * 100 / maxValueDeviceCoord);
|
const devCoordToSvgY = y => graphHeight - (y * 100 / maxValueDeviceCoord);
|
||||||
const dataInKgToSvgCoordX = dataInDeviceCoordToSvgCoordX;
|
const dataInKgToSvgCoordX = devCoordToSvgX;
|
||||||
const dataInKgToSvgCoordY = y => dataInDeviceCoordToSvgCoordY(y / props.kgFactor);
|
const dataInKgToSvgCoordY = y => devCoordToSvgY(y / props.kgFactor);
|
||||||
|
|
||||||
const coordStr = data.map((element, i) => `${dataInDeviceCoordToSvgCoordX(i)}, ${dataInDeviceCoordToSvgCoordY(element)}`).join(" ");
|
const coordStr = data.map((element, i) => `${devCoordToSvgX(i)}, ${devCoordToSvgY(element)}`).join(" ");
|
||||||
const ticks = computeTickMarks(maxValueKg * 0.9, 4);
|
const ticks = computeTickMarks(maxValueKg * 0.9, 4);
|
||||||
let viewBox = `0 0 ${graphWidth} ${graphHeight}`;
|
let viewBox = `0 0 ${graphWidth} ${graphHeight}`;
|
||||||
|
|
||||||
const cutOffIndex = Math.max(0, props.data.size - numPoints);
|
const cutOffIndex = Math.max(0, props.data.size - numPoints);
|
||||||
const peaksToDisplay = props.peaks.filter(p => p > cutOffIndex)
|
const peaksToDisplay = props.peaks.filter(p => p > cutOffIndex)
|
||||||
const peaksXCoords = peaksToDisplay.map(p => dataInDeviceCoordToSvgCoordX(p - cutOffIndex));
|
const peaksXCoords = peaksToDisplay.map(p => devCoordToSvgX(p - cutOffIndex));
|
||||||
const peaksYCoords = peaksToDisplay.map(p => dataInDeviceCoordToSvgCoordY(props.data.get(p)));
|
const peaksYCoords = peaksToDisplay.map(p => devCoordToSvgY(props.data.get(p)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ aspectRatio: 3.4 }}>
|
<View style={{ aspectRatio: graphWidth / graphHeight }}>
|
||||||
<Svg height="100%" width="100%" viewBox={viewBox} preserveAspectRatio="none" >
|
<Svg height="100%" width="100%" viewBox={viewBox} preserveAspectRatio="none" >
|
||||||
<Polyline
|
<Polyline
|
||||||
points={coordStr}
|
points={coordStr}
|
||||||
|
@ -91,7 +86,6 @@ const Graph = props => {
|
||||||
key={`peak${peak[0]}`}
|
key={`peak${peak[0]}`}
|
||||||
stroke="black"
|
stroke="black"
|
||||||
fill="black"
|
fill="black"
|
||||||
//cy={dataInDeviceCoordToSvgCoord(0, props.data[peak])[1]}
|
|
||||||
cy={peak[1]}
|
cy={peak[1]}
|
||||||
r="3"
|
r="3"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -21,10 +21,10 @@ const IconCard = props => {
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
card: {
|
card: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||||
margin: 5,
|
margin: 5,
|
||||||
padding: 5,
|
padding: 5,
|
||||||
borderRadius: 3,
|
borderRadius: 6,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet, Animated } from 'react-native';
|
||||||
import { Button, Content, Text } from 'native-base';
|
import { Button, Content, Text, View } from 'native-base';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import IconCard from './IconCard';
|
import IconCard from './IconCard';
|
||||||
import Graph from './Graph';
|
import Graph from './Graph';
|
||||||
|
@ -8,6 +8,8 @@ import { connect } from 'react-redux';
|
||||||
import backgroundColors from './Themes';
|
import backgroundColors from './Themes';
|
||||||
import { useKeepAwake } from 'expo-keep-awake';
|
import { useKeepAwake } from 'expo-keep-awake';
|
||||||
import { stopSession } from '../state/DeviceReduxCoupling';
|
import { stopSession } from '../state/DeviceReduxCoupling';
|
||||||
|
import CycleView from './CycleView';
|
||||||
|
|
||||||
|
|
||||||
function LiveTrainingView(props) {
|
function LiveTrainingView(props) {
|
||||||
const analysis = props.session.analysis;
|
const analysis = props.session.analysis;
|
||||||
|
@ -28,16 +30,19 @@ function LiveTrainingView(props) {
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
>
|
>
|
||||||
<Content padder contentContainerStyle={{ justifyContent: 'space-around', flex: 1, paddingTop: 60 }}>
|
<Content padder contentContainerStyle={{ justifyContent: 'space-around', flex: 1, paddingTop: 60 }}>
|
||||||
<IconCard label="BAHNEN" value={laps} iconName="retweet" iconType="AntDesign" />
|
|
||||||
<IconCard label="ZÜGE" value={analysis.peaks.size} iconName="dashboard" iconType="AntDesign" />
|
|
||||||
<IconCard label="KRAFT" value={totalMomentum} iconName="ruler" iconType="Entypo" />
|
|
||||||
<Graph></Graph>
|
|
||||||
{/*
|
|
||||||
<IconCard label="ZÜGE" value={this.state.numPeaks} iconName="dashboard" iconType="AntDesign" />
|
|
||||||
<IconCard label="BAHNEN" value={this.state.numLaps} iconName="retweet" iconType="AntDesign" />
|
|
||||||
|
|
||||||
<IconCard label="KRAFT" value="120" iconName="ruler" iconType="Entypo" />
|
<CycleView>
|
||||||
*/}
|
<IconCard label="BAHNEN" value={laps} iconName="retweet" iconType="AntDesign" />
|
||||||
|
<IconCard label="ZÜGE" value={analysis.peaks.size} iconName="dashboard" iconType="AntDesign" />
|
||||||
|
</CycleView>
|
||||||
|
|
||||||
|
<CycleView>
|
||||||
|
<IconCard label="DAUER" value="00:42" iconName="clock" iconType="FontAwesome5" />
|
||||||
|
<IconCard label="AKTIVE DAUER" value="42:00" iconName="stopwatch" iconType="FontAwesome5" />
|
||||||
|
</CycleView>
|
||||||
|
<IconCard label="KRAFT" value={totalMomentum} iconName="ruler" iconType="Entypo" />
|
||||||
|
|
||||||
|
<Graph></Graph>
|
||||||
<Button block secondary onPress={onStopClick}><Text>Stop</Text></Button>
|
<Button block secondary onPress={onStopClick}><Text>Stop</Text></Button>
|
||||||
</Content>
|
</Content>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
@ -52,16 +57,6 @@ const styles = StyleSheet.create({
|
||||||
padding: 5,
|
padding: 5,
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
/*
|
|
||||||
shadowColor: "#000",
|
|
||||||
shadowOffset: {
|
|
||||||
width: 0,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
shadowOpacity: 0.18,
|
|
||||||
shadowRadius: 1.00,
|
|
||||||
|
|
||||||
elevation: 1,*/
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
const backgroundColors = {
|
const backgroundColors = {
|
||||||
'hot': ['#830e5f', '#fd5139'],
|
'hot': ['#830e5f', '#fd5139'],
|
||||||
'darkBlue': ['#4265a3', '#cfada7'],
|
'darkBlue': ['#4265a3', '#cfada7'],
|
||||||
'lightBlue': ['#50a4db', '#74bbe2'],
|
//'lightBlue': ['#50a4db', '#74bbe2'],
|
||||||
|
'lightBlue': ['#24acdc ', '#65fae6'],
|
||||||
'foggy': ['#bc8db8', '#5d5e90'],
|
'foggy': ['#bc8db8', '#5d5e90'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { PeakDetectorSimple } from './PeakDetection';
|
import { PeakDetectorSimple } from './PeakDetection';
|
||||||
|
import { MovingAverage} from './MovingAverage';
|
||||||
import { List } from 'immutable';
|
import { List } from 'immutable';
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,8 +26,16 @@ export default class DataAnalysis {
|
||||||
console.log("cache reset");
|
console.log("cache reset");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newDataArr = newData.toArray();
|
||||||
|
|
||||||
|
// active time
|
||||||
|
const newAverages = this.movingAverage.addVector(newDataArr);
|
||||||
|
this.activeMeasurements += newAverages.reduce((n, val) => {
|
||||||
|
return n + (val >= analysisParameters.activeTimeThreshold);
|
||||||
|
});
|
||||||
|
|
||||||
// peaks
|
// peaks
|
||||||
const newPeaks = this.peakDetectorSimple.addVector(newData.toArray());
|
const newPeaks = this.peakDetectorSimple.addVector(newDataArr);
|
||||||
this.allPeaks = this.allPeaks.concat(List(newPeaks));
|
this.allPeaks = this.allPeaks.concat(List(newPeaks));
|
||||||
|
|
||||||
// aggregated sum/max
|
// aggregated sum/max
|
||||||
|
@ -38,11 +47,13 @@ export default class DataAnalysis {
|
||||||
const windowed = allMeasurements.slice(-windowNumDataPoints);
|
const windowed = allMeasurements.slice(-windowNumDataPoints);
|
||||||
const peakMaxWindow = windowed.reduce((running, x) => Math.max(x, running), 0);
|
const peakMaxWindow = windowed.reduce((running, x) => Math.max(x, running), 0);
|
||||||
const momentumWindow = windowed.reduce((sum, x) => sum + x, 0);
|
const momentumWindow = windowed.reduce((sum, x) => sum + x, 0);
|
||||||
|
|
||||||
|
|
||||||
this.analyzedUpToIdx = allMeasurements.size;
|
this.analyzedUpToIdx = allMeasurements.size;
|
||||||
return {
|
return {
|
||||||
peaks: this.allPeaks,
|
peaks: this.allPeaks,
|
||||||
totalTime: allMeasurements / analysisParameters.numMeasurementsPerSec,
|
totalTime: allMeasurements / analysisParameters.numMeasurementsPerSec,
|
||||||
|
activeTime: this.activeMeasurements / analysisParameters.numMeasurementsPerSec,
|
||||||
|
|
||||||
totalMomentum: this.aggregatedMomentum,
|
totalMomentum: this.aggregatedMomentum,
|
||||||
peakMax: this.peakMax,
|
peakMax: this.peakMax,
|
||||||
|
@ -54,6 +65,9 @@ export default class DataAnalysis {
|
||||||
|
|
||||||
|
|
||||||
_resetCache(analysisParameters, sessionId) {
|
_resetCache(analysisParameters, sessionId) {
|
||||||
|
this.movingAverage = analysisParameters ? new MovingAverage(analysisParameters.movingAverageWindowSize) : null;
|
||||||
|
this.activeMeasurements = 0;
|
||||||
|
|
||||||
this.peakDetectorSimple = analysisParameters ? new PeakDetectorSimple(analysisParameters.peakDetectorSimpleThreshold) : null;
|
this.peakDetectorSimple = analysisParameters ? new PeakDetectorSimple(analysisParameters.peakDetectorSimpleThreshold) : null;
|
||||||
this.allPeaks = List();
|
this.allPeaks = List();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A moving average computation
|
||||||
|
*/
|
||||||
|
export class MovingAverage {
|
||||||
|
constructor(windowSize) {
|
||||||
|
this._windowSize = windowSize;
|
||||||
|
this._queue = [];
|
||||||
|
this._queueSum = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
windowSize() {
|
||||||
|
return this._windowSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
addVector(vec) {
|
||||||
|
return vec.map(this.add.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
add(value) {
|
||||||
|
this._queueSum += value;
|
||||||
|
this._queue.push(value);
|
||||||
|
if(this._queue.length > this._windowSize) {
|
||||||
|
this._queueSum -= this._queue[0];
|
||||||
|
this._queue.shift();
|
||||||
|
}
|
||||||
|
return this._queueSum / this._queue.length;
|
||||||
|
}
|
||||||
|
};
|
|
@ -30,11 +30,11 @@ const INITIAL_SETTINGS = {
|
||||||
swimTrackerHost: "192.168.178.110",
|
swimTrackerHost: "192.168.178.110",
|
||||||
|
|
||||||
analysis: {
|
analysis: {
|
||||||
peaksPerLap: 30,
|
peaksPerLap: 30,
|
||||||
windowSizeInSecs: 5,
|
windowSizeInSecs: 5,
|
||||||
numMeasurementsPerSec: 10,
|
numMeasurementsPerSec: 10,
|
||||||
|
|
||||||
kgFactor: 1.0 / 700.0,
|
kgFactor: 1.0 / 701.0,
|
||||||
|
|
||||||
peakDetector: 'SIMPLE', // either 'SIMPLE' or 'ZSCORE'
|
peakDetector: 'SIMPLE', // either 'SIMPLE' or 'ZSCORE'
|
||||||
peakDetectorSimpleThreshold: 2000,
|
peakDetectorSimpleThreshold: 2000,
|
||||||
|
@ -42,6 +42,9 @@ const INITIAL_SETTINGS = {
|
||||||
peakDetectorZScoreLag: 8, // peak detector z-score values
|
peakDetectorZScoreLag: 8, // peak detector z-score values
|
||||||
peakDetectorZScoreThreshold: 2,
|
peakDetectorZScoreThreshold: 2,
|
||||||
peakDetectorZScoreInfluence: 0.1,
|
peakDetectorZScoreInfluence: 0.1,
|
||||||
|
|
||||||
|
activeTimeThreshold: 300,
|
||||||
|
movingAverageWindowSize: 10*5,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue