MVP in-stroke analysis comparison
This commit is contained in:
@@ -10,7 +10,7 @@ from django.contrib.admin.widgets import FilteredSelectMultiple
|
|||||||
from rowers.models import (
|
from rowers.models import (
|
||||||
Workout, Rower, Team, PlannedSession, GeoCourse,
|
Workout, Rower, Team, PlannedSession, GeoCourse,
|
||||||
VirtualRace, VirtualRaceResult, IndoorVirtualRaceResult,
|
VirtualRace, VirtualRaceResult, IndoorVirtualRaceResult,
|
||||||
PaidPlan
|
PaidPlan, InStrokeAnalysis
|
||||||
)
|
)
|
||||||
from rowers.rows import validate_file_extension, must_be_csv, validate_image_extension, validate_kml
|
from rowers.rows import validate_file_extension, must_be_csv, validate_image_extension, validate_kml
|
||||||
from django.contrib.auth.forms import UserCreationForm
|
from django.contrib.auth.forms import UserCreationForm
|
||||||
@@ -1250,6 +1250,13 @@ class WorkoutSingleSelectForm(forms.Form):
|
|||||||
self.fields['workout'].queryset = workouts
|
self.fields['workout'].queryset = workouts
|
||||||
|
|
||||||
|
|
||||||
|
class InStrokeMultipleCompareForm(forms.Form):
|
||||||
|
analyses = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=InStrokeAnalysis.objects.all(),
|
||||||
|
widget=forms.CheckboxSelectMultiple()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WorkoutMultipleCompareForm(forms.Form):
|
class WorkoutMultipleCompareForm(forms.Form):
|
||||||
workouts = forms.ModelMultipleChoiceField(
|
workouts = forms.ModelMultipleChoiceField(
|
||||||
queryset=Workout.objects.filter(),
|
queryset=Workout.objects.filter(),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from rowers.metrics import rowingmetrics, metricsdicts
|
|||||||
from scipy.spatial import ConvexHull, Delaunay
|
from scipy.spatial import ConvexHull, Delaunay
|
||||||
from scipy.stats import linregress, percentileofscore
|
from scipy.stats import linregress, percentileofscore
|
||||||
from pytz import timezone as tz, utc
|
from pytz import timezone as tz, utc
|
||||||
from rowers.models import course_spline, VirtualRaceResult
|
from rowers.models import course_spline, VirtualRaceResult, InStrokeAnalysis
|
||||||
from bokeh.palettes import Category20c, Category10
|
from bokeh.palettes import Category20c, Category10
|
||||||
from bokeh.layouts import layout, widgetbox
|
from bokeh.layouts import layout, widgetbox
|
||||||
from bokeh.resources import CDN, INLINE
|
from bokeh.resources import CDN, INLINE
|
||||||
@@ -4078,6 +4078,81 @@ def interactive_streamchart(id=0, promember=0):
|
|||||||
|
|
||||||
return [script, div]
|
return [script, div]
|
||||||
|
|
||||||
|
def instroke_multi_interactive_chart(selected):
|
||||||
|
df_plot = pd.DataFrame()
|
||||||
|
ids = [analysis.id for analysis in selected]
|
||||||
|
for analysis in selected:
|
||||||
|
#start_second, end_second, spm_min, spm_max, name
|
||||||
|
activeminutesmin = int(analysis.start_second/60.)
|
||||||
|
activeminutesmax = int(analysis.end_second/60.)
|
||||||
|
rowdata = rrdata(csvfile=analysis.workout.csvfilename)
|
||||||
|
data = rowdata.get_instroke_data(
|
||||||
|
analysis.metric,
|
||||||
|
spm_min=analysis.spm_min,
|
||||||
|
spm_max=analysis.spm_max,
|
||||||
|
activeminutesmin=activeminutesmin,
|
||||||
|
activeminutesmax=activeminutesmax,
|
||||||
|
)
|
||||||
|
mean_vals = data.mean()
|
||||||
|
xvals = np.arange(len(mean_vals))
|
||||||
|
xname = 'x_'+str(analysis.id)
|
||||||
|
yname = 'y_'+str(analysis.id)
|
||||||
|
df_plot[xname] = xvals
|
||||||
|
df_plot[yname] = mean_vals
|
||||||
|
|
||||||
|
source = ColumnDataSource(
|
||||||
|
df_plot
|
||||||
|
)
|
||||||
|
|
||||||
|
TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,crosshair'
|
||||||
|
plot = Figure(plot_width=920,tools=TOOLS,
|
||||||
|
toolbar_location='above',
|
||||||
|
toolbar_sticky=False)
|
||||||
|
|
||||||
|
plot.sizing_mode = 'stretch_both'
|
||||||
|
|
||||||
|
# add watermark
|
||||||
|
watermarkurl = "/static/img/logo7.png"
|
||||||
|
|
||||||
|
watermarkrange = Range1d(start=0, end=1)
|
||||||
|
watermarkalpha = 0.6
|
||||||
|
watermarkx = 0.99
|
||||||
|
watermarky = 0.01
|
||||||
|
watermarkw = 184
|
||||||
|
watermarkh = 35
|
||||||
|
watermarkanchor = 'bottom_right'
|
||||||
|
plot.extra_y_ranges = {"watermark": watermarkrange}
|
||||||
|
plot.extra_x_ranges = {"watermark": watermarkrange}
|
||||||
|
|
||||||
|
plot.image_url([watermarkurl], watermarkx, watermarky,
|
||||||
|
watermarkw, watermarkh,
|
||||||
|
global_alpha=watermarkalpha,
|
||||||
|
w_units='screen',
|
||||||
|
h_units='screen',
|
||||||
|
anchor=watermarkanchor,
|
||||||
|
dilate=True,
|
||||||
|
x_range_name="watermark",
|
||||||
|
y_range_name="watermark",
|
||||||
|
)
|
||||||
|
|
||||||
|
colors = itertools.cycle(palette)
|
||||||
|
|
||||||
|
try:
|
||||||
|
items = itertools.izip(ids, colors)
|
||||||
|
except AttributeError:
|
||||||
|
items = zip(ids, colors)
|
||||||
|
|
||||||
|
for id, color in items:
|
||||||
|
xname = 'x_'+str(id)
|
||||||
|
yname = 'y_'+str(id)
|
||||||
|
analysis = InStrokeAnalysis.objects.get(id=id)
|
||||||
|
plot.line(xname,yname,source=source,legend_label=analysis.name,
|
||||||
|
line_width=2, color=color)
|
||||||
|
|
||||||
|
script, div = components(plot)
|
||||||
|
|
||||||
|
return (script, div)
|
||||||
|
|
||||||
def instroke_interactive_chart(df,metric, workout, spm_min, spm_max,
|
def instroke_interactive_chart(df,metric, workout, spm_min, spm_max,
|
||||||
activeminutesmin, activeminutesmax,
|
activeminutesmin, activeminutesmax,
|
||||||
individual_curves,
|
individual_curves,
|
||||||
|
|||||||
@@ -6,16 +6,37 @@
|
|||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
|
{{ js_res | safe }}
|
||||||
|
{{ css_res| safe }}
|
||||||
|
|
||||||
|
<script src="https://cdn.pydata.org/bokeh/release/bokeh-2.2.3.min.js"></script>
|
||||||
|
<script src="https://cdn.pydata.org/bokeh/release/bokeh-widgets-2.2.3.min.js"></script>
|
||||||
|
<script async="true" type="text/javascript">
|
||||||
|
Bokeh.set_log_level("info");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{ the_script |safe }}
|
||||||
|
|
||||||
<h1>In-Stroke Analysis for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
|
<h1>In-Stroke Analysis for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
|
||||||
|
|
||||||
|
<form enctype="multipart/form-data" method="post">
|
||||||
<ul class="main-content">
|
<ul class="main-content">
|
||||||
|
{% if the_div %}
|
||||||
|
<li class="grid_4">
|
||||||
|
<div id="theplot" class="flexplot">
|
||||||
|
{{ the_div|safe }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% if analyses %}
|
{% if analyses %}
|
||||||
{% for analysis in analyses %}
|
{% for analysis in analyses %}
|
||||||
<li class="grid_4 divlines" id="analysis_{{ analysis.id }}">
|
<li class="grid_4 divlines" id="analysis_{{ analysis.id }}">
|
||||||
{{ analysis.date }}
|
{{ analysis.date }}
|
||||||
<div><h3>{{ analysis.name }}</h3></div>
|
<div><h3>{{ analysis.name }}</h3></div>
|
||||||
<div class="analysiscontainer">
|
<div class="analysiscontainer">
|
||||||
|
<div class="workoutelement">
|
||||||
|
<input type="checkbox" name="analyses" value="{{ analysis.id }}" id="analyses_{{ analysis.id }}">
|
||||||
|
</div>
|
||||||
<div class="workoutelement">
|
<div class="workoutelement">
|
||||||
<a class="small" href="/rowers/workout/{{ analysis.workout.id|encode }}/instroke/interactive/{{ analysis.id }}/"
|
<a class="small" href="/rowers/workout/{{ analysis.workout.id|encode }}/instroke/interactive/{{ analysis.id }}/"
|
||||||
title="Edit">
|
title="Edit">
|
||||||
@@ -57,6 +78,9 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% csrf_token %}
|
||||||
|
<input name='instroke_compare' type="submit" value="Compare Selected">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1852,6 +1852,19 @@ def instrokeanalysis_view(request, userid=0):
|
|||||||
|
|
||||||
analyses = InStrokeAnalysis.objects.filter(rower=r).order_by("-date","-id")
|
analyses = InStrokeAnalysis.objects.filter(rower=r).order_by("-date","-id")
|
||||||
|
|
||||||
|
script = ""
|
||||||
|
div = ""
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = InStrokeMultipleCompareForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
cd = form.cleaned_data
|
||||||
|
selected = cd['analyses']
|
||||||
|
request.session['analyses'] = [a.id for a in selected]
|
||||||
|
# now should redirect to analysis
|
||||||
|
script, div = instroke_multi_interactive_chart(selected)
|
||||||
|
|
||||||
breadcrumbs = [
|
breadcrumbs = [
|
||||||
{
|
{
|
||||||
'url': '/rowers/analysis',
|
'url': '/rowers/analysis',
|
||||||
@@ -1868,6 +1881,8 @@ def instrokeanalysis_view(request, userid=0):
|
|||||||
'breadcrumbs': breadcrumbs,
|
'breadcrumbs': breadcrumbs,
|
||||||
'analyses': analyses,
|
'analyses': analyses,
|
||||||
'rower': r,
|
'rower': r,
|
||||||
|
'the_script': script,
|
||||||
|
'the_div': div,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ from rowers.forms import (
|
|||||||
VideoAnalysisMetricsForm, SurveyForm, HistorySelectForm,
|
VideoAnalysisMetricsForm, SurveyForm, HistorySelectForm,
|
||||||
StravaChartForm, FitnessFitForm, PerformanceManagerForm,
|
StravaChartForm, FitnessFitForm, PerformanceManagerForm,
|
||||||
TrainingPlanBillingForm, InstantPlanSelectForm,
|
TrainingPlanBillingForm, InstantPlanSelectForm,
|
||||||
TrainingZonesForm, InstrokeForm
|
TrainingZonesForm, InstrokeForm, InStrokeMultipleCompareForm
|
||||||
)
|
)
|
||||||
|
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ th.rotate > div > span {
|
|||||||
|
|
||||||
.analysiscontainer {
|
.analysiscontainer {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 50px repeat(auto-fit, minmax(calc((100% - 100px)/6), 1fr));
|
grid-template-columns: 50px repeat(auto-fit, minmax(calc((100% - 100px)/7), 1fr));
|
||||||
/* grid-template-columns: 50px repeat(auto-fit, minmax(100px, 1fr)) 50px; ????*/
|
/* grid-template-columns: 50px repeat(auto-fit, minmax(100px, 1fr)) 50px; ????*/
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
|||||||
Reference in New Issue
Block a user