Graph cleanup
This commit is contained in:
parent
a870a0af85
commit
23eb634c30
|
@ -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);
|
||||||
|
|
|
@ -30,7 +30,7 @@ const styles = StyleSheet.create({
|
||||||
});
|
});
|
||||||
|
|
||||||
IconCard.defaultProps = {
|
IconCard.defaultProps = {
|
||||||
fontSize: 85,
|
fontSize: 65,
|
||||||
flex: 1
|
flex: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue