Graph cleanup

This commit is contained in:
Martin Bauer 2020-06-30 18:06:37 +02:00
parent a870a0af85
commit 23eb634c30
7 changed files with 84 additions and 53 deletions

View File

@ -1,71 +1,101 @@
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, Polygon, Rect, G} from 'react-native-svg-web'; //import Svg, {Polyline, Polygon, Rect, G} from 'react-native-svg-web';
import Svg, { Polyline, Polygon, Rect, G, Text } from 'react-native-svg'; import Svg, { Polyline, Polygon, Rect, G, Text, Circle } from 'react-native-svg';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
function computeTickMark(largest, mostTicks) { function computeTickMarks(largest, mostTicks) {
const minimum = largest / mostTicks const minimum = largest / mostTicks
const magnitude = 10 ** Math.floor(Math.log10(minimum)) const magnitude = 10 ** Math.floor(Math.log10(minimum))
const residual = minimum / magnitude const residual = minimum / magnitude
let tickInterval = 0;
if (residual > 5) if (residual > 5)
return 10 * magnitude tickInterval = 10 * magnitude;
else if (residual > 2) else if (residual > 2)
return 5 * magnitude tickInterval = 5 * magnitude;
else if (residual > 1) else if (residual > 1)
return 2 * magnitude tickInterval = 2 * magnitude;
else else
return magnitude tickInterval = magnitude;
let result = [];
let nextTick = tickInterval;
while (nextTick < largest) {
result.push(nextTick);
nextTick += tickInterval;
}
return result;
} }
let isFirstRender = true;
const Graph = props => { const Graph = props => {
const graphHeight = 100; const graphHeight = 100;
const numPoints = 300;
const yLabelSpace = 40;
const graphWidth = numPoints + yLabelSpace;
const minKgScale = 3; // scale such that upper graph value is n kg
const data = props.data.slice(-300); const data = props.data.slice(-numPoints);
const maxElement = data.reduce((running, x) => Math.max(x, running), 2 / props.kgFactor); const maxValueDeviceCoord = data.reduce((running, x) => Math.max(x, running), minKgScale / props.kgFactor);
const maxValueKg = Math.max(maxValueDeviceCoord * props.kgFactor, minKgScale);
const coordStr = data.map((element, i) => `${i}, ${100 - (element * 100 / maxElement)}`); const dataInDeviceCoordToSvgCoordX = x => yLabelSpace + x;
const dataInDeviceCoordToSvgCoordY = y => graphHeight - (y * 100 / maxValueDeviceCoord);
const dataInKgToSvgCoordX = dataInDeviceCoordToSvgCoordX;
const dataInKgToSvgCoordY = y => dataInDeviceCoordToSvgCoordY(y / props.kgFactor);
const tick = computeTickMark(maxElement * props.kgFactor * 0.6, 4); const coordStr = data.map((element, i) => `${dataInDeviceCoordToSvgCoordX(i)}, ${dataInDeviceCoordToSvgCoordY(element)}`).join(" ");
let ticks = []; const ticks = computeTickMarks(maxValueKg * 0.9, 4);
let nextTick = tick; let viewBox = `0 0 ${graphWidth} ${graphHeight}`;
while (nextTick < maxElement * props.kgFactor) {
ticks.push(nextTick); const cutOffIndex = Math.max(0, props.data.size - numPoints);
nextTick += tick; const peaksToDisplay = props.peaks.filter(p => p > cutOffIndex)
} const peaksXCoords = peaksToDisplay.map(p => dataInDeviceCoordToSvgCoordX(p - cutOffIndex));
const peaksYCoords = peaksToDisplay.map(p => dataInDeviceCoordToSvgCoordY(props.data.get(p)));
return ( return (
<View style={{ justifyContent: 'center', alignItems: 'center' }}> <View style={{ aspectRatio: 3.4 }}>
<Svg height={graphHeight} width="80%" viewbox="0 0 300 100"> <Svg height="100%" width="100%" viewBox={viewBox} preserveAspectRatio="none" >
<Polyline <Polyline
points={coordStr.join(" ")} points={coordStr}
stroke="black" stroke="black"
strokeWidth="3" strokeWidth="2"
strokeOpacity="0.5" strokeLinejoin="round"
strokeLinejoin="round" fill="none"
fill="none" />
/> {ticks.map(tick => (
{ticks.map(tick => ( <React.Fragment key={`fragment${tick}`} >
<React.Fragment> <Polyline
<Polyline points={`${yLabelSpace}, ${dataInKgToSvgCoordY(tick)} ${graphWidth}, ${dataInKgToSvgCoordY(tick)}`}
points={`40, ${100 - (tick / props.kgFactor * 100 / maxElement)} 300, ${100 - (tick / props.kgFactor * 100 / maxElement)}`} stroke="black"
stroke="black" key={`line${tick}`}
strokeWidth="1" strokeWidth="1"
strokeDasharray="5,5" strokeDasharray="5,5"
strokeOpacity="0.5" strokeLinejoin="round"
strokeLinejoin="round" fill="none"
fill="none" />
/> <Text x="5" y={`${dataInKgToSvgCoordY(tick)}`}
<Text x="5" y={`${100 - (tick / props.kgFactor * 100 / maxElement - 4) }`}
alignmentBaseline="center" alignmentBaseline="center"
strokeOpacity="0.5" key={`label${tick}`}
fillOpacity="0.5"
fontSize="9pt">{tick} kg </Text> fontSize="9pt">{tick} kg </Text>
</React.Fragment> </React.Fragment>
) )
)} )}
{peaksXCoords.zip(peaksYCoords).map(peak => (
<Circle
cx={peak[0]}
key={`peak${peak[0]}`}
stroke="black"
fill="black"
//cy={dataInDeviceCoordToSvgCoord(0, props.data[peak])[1]}
cy={peak[1]}
r="3"
/>
))}
</Svg> </Svg>
</View> </View>
); );
@ -75,9 +105,9 @@ const Graph = props => {
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
return { return {
data: state.deviceState.measurements, data: state.deviceState.measurements,
kgFactor: state.settings.analysis.kgFactor kgFactor: state.settings.analysis.kgFactor,
peaks: state.deviceState.analysis.peaks,
}; };
}; };
export default connect(mapStateToProps)(Graph); export default connect(mapStateToProps)(Graph);

View File

@ -30,7 +30,7 @@ const styles = StyleSheet.create({
}); });
IconCard.defaultProps = { IconCard.defaultProps = {
fontSize: 85, fontSize: 65,
flex: 1 flex: 1
}; };

View File

@ -16,7 +16,7 @@ function LiveTrainingView(props) {
props.navigation.navigate('Home'); props.navigation.navigate('Home');
}; };
const laps = (analysis.peaks.size / props.peaksPerLap).toFixed(1); const laps = (analysis.peaks.size / props.peaksPerLap).toFixed(1);
const totalMomentum = Math.trunc(analysis.totalMomentum / 10000); const totalMomentum = Math.trunc(analysis.totalMomentum * props.kgFactor / 10 / 60);
useKeepAwake(); useKeepAwake();
@ -70,6 +70,7 @@ const mapStateToProps = (state) => {
session: state.deviceState, session: state.deviceState,
peaksPerLap: state.settings.analysis.peaksPerLap, peaksPerLap: state.settings.analysis.peaksPerLap,
theme: state.settings.theme, theme: state.settings.theme,
kgFactor: state.settings.analysis.kgFactor,
}; };
}; };

View File

@ -22,10 +22,11 @@ export default class DataAnalysis {
else { else {
this._resetCache(analysisParameters, sessionId); this._resetCache(analysisParameters, sessionId);
newData = allMeasurements; newData = allMeasurements;
console.log("cache reset");
} }
// peaks // peaks
const newPeaks = this.peakDetectorSimple.addVector(newData); const newPeaks = this.peakDetectorSimple.addVector(newData.toArray());
this.allPeaks = this.allPeaks.concat(List(newPeaks)); this.allPeaks = this.allPeaks.concat(List(newPeaks));
// aggregated sum/max // aggregated sum/max
@ -43,7 +44,7 @@ export default class DataAnalysis {
peaks: this.allPeaks, peaks: this.allPeaks,
totalTime: allMeasurements / analysisParameters.numMeasurementsPerSec, totalTime: allMeasurements / analysisParameters.numMeasurementsPerSec,
totalMomentum: this.aggregatedMomentum / allMeasurements.size, totalMomentum: this.aggregatedMomentum,
peakMax: this.peakMax, peakMax: this.peakMax,
momentumWindow: momentumWindow, momentumWindow: momentumWindow,

View File

@ -21,6 +21,7 @@ class PeakDetectorSimple {
addVector(vec) { addVector(vec) {
let result = []; let result = [];
for (let i = 0; i < vec.length; ++i) { for (let i = 0; i < vec.length; ++i) {
const res = this.add(vec[i]); const res = this.add(vec[i]);
if(res !== null) if(res !== null)

View File

@ -126,10 +126,8 @@ export const deviceStateReducer = (state = INITIAL_DEVICE_STATE, action) => {
case DEVICE_DISCONNECT: case DEVICE_DISCONNECT:
return { ...INITIAL_DEVICE_STATE, connState: ConnState.DISCONNECTED }; return { ...INITIAL_DEVICE_STATE, connState: ConnState.DISCONNECTED };
case SESSION_STARTED: case SESSION_STARTED:
console.log("session started");
return { ...INITIAL_DEVICE_STATE, connState: ConnState.CONNECTED_RUNNING, sessionId: action.sessionId }; return { ...INITIAL_DEVICE_STATE, connState: ConnState.CONNECTED_RUNNING, sessionId: action.sessionId };
case SESSION_STOPPED: case SESSION_STOPPED:
console.log("session stopped");
return { ...INITIAL_DEVICE_STATE, connState: ConnState.CONNECTED_STOPPED }; return { ...INITIAL_DEVICE_STATE, connState: ConnState.CONNECTED_STOPPED };
case START_SESSION: case START_SESSION:
if(state.connState === ConnState.SESSION_STARTED) if(state.connState === ConnState.SESSION_STARTED)

View File

@ -34,10 +34,10 @@ const INITIAL_SETTINGS = {
windowSizeInSecs: 5, windowSizeInSecs: 5,
numMeasurementsPerSec: 10, numMeasurementsPerSec: 10,
kgFactor: 1.0 / 1100.0, kgFactor: 1.0 / 700.0,
peakDetector: 'SIMPLE', // either 'SIMPLE' or 'ZSCORE' peakDetector: 'SIMPLE', // either 'SIMPLE' or 'ZSCORE'
peakDetectorSimpleThreshold: 2500, peakDetectorSimpleThreshold: 2000,
peakDetectorZScoreLag: 8, // peak detector z-score values peakDetectorZScoreLag: 8, // peak detector z-score values
peakDetectorZScoreThreshold: 2, peakDetectorZScoreThreshold: 2,