Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web/metrics in analyze sleep #93

Merged
merged 45 commits into from
Nov 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
cd1a7ca
added metrics in preview & changed class to classname
conorato Nov 21, 2020
424ab9c
added first card
conorato Nov 21, 2020
57023a9
added stats
conorato Nov 21, 2020
77f1db2
fixed cards
conorato Nov 21, 2020
e4ce3db
renamed and fixed cards appearences
conorato Nov 21, 2020
2cc92fd
added color to calculated metrics
conorato Nov 21, 2020
ff74849
moved barchart cards to right && added bootstrap rows
conorato Nov 21, 2020
a61435b
added sleep mechanisms & tips
conorato Nov 22, 2020
2987300
fixed btn hero
conorato Nov 22, 2020
21ed0b7
changed spectro cards size
conorato Nov 22, 2020
293f76e
fixed rem latency in server (time between sleep onset & first rem)
conorato Nov 22, 2020
3d1ee75
extracted metrics
conorato Nov 22, 2020
e16613e
added recommanded time sleep
conorato Nov 22, 2020
b6f9624
reviewed spectro
conorato Nov 22, 2020
b14f3c8
added spectro references
conorato Nov 22, 2020
ffd5431
support wake sleep sequence
conorato Nov 22, 2020
32ccc0d
fixed labels and rectangle width issues
conorato Nov 22, 2020
9612103
fixed cards responsive
conorato Nov 22, 2020
a17e74e
tooltip begins to work
conorato Nov 22, 2020
517a201
add content about hormones
Nov 22, 2020
3b2987a
Merge branch 'web/metrics-in-analyze-sleep' of https://github.com/Pol…
Nov 22, 2020
d363c05
add stage shifts and noctural awakening metrics
Nov 22, 2020
922fac7
fixed tool tip position
conorato Nov 22, 2020
ccc047b
add some text to the intro
Nov 22, 2020
b9caffc
fixed tooltips
conorato Nov 22, 2020
0923bfd
change all font-size to lead
Nov 22, 2020
5c7f7b8
Merge branch 'web/metrics-in-analyze-sleep' of github.com:PolyCortex/…
conorato Nov 22, 2020
700b4d8
Add color to sleep stages
Nov 22, 2020
73a869b
Merge branch 'web/metrics-in-analyze-sleep' of https://github.com/Pol…
Nov 22, 2020
a269ea6
change sleep stage solor
Nov 22, 2020
4ac626f
added description of freq band
conorato Nov 22, 2020
8dedd65
Merge branch 'web/metrics-in-analyze-sleep' of github.com:PolyCortex/…
conorato Nov 22, 2020
336efee
add content on sleep stages
Nov 22, 2020
25b1f62
change text outro evolutive chart
Nov 22, 2020
20005b7
completed spectro cards
conorato Nov 22, 2020
5b0f79c
Merge branch 'web/metrics-in-analyze-sleep' of github.com:PolyCortex/…
conorato Nov 22, 2020
d299f17
reviewed
conorato Nov 22, 2020
5cc500b
revert ispreviewmode
conorato Nov 22, 2020
e6be154
Update web/src/views/sleep_analysis_results/index.js
Nov 22, 2020
db5a022
Update web/src/views/sleep_analysis_results/evolving_chart_scrollytel…
conorato Nov 22, 2020
9909cf4
Apply suggestions from code review
Nov 23, 2020
60a3688
remove adenosine explanation in card
Nov 23, 2020
eff48e3
Apply suggestions from code review
Nov 23, 2020
ec7c63e
Apply suggested change
Nov 23, 2020
d4a2fc7
apply suggested changes
Nov 23, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions backend/backend/metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,26 @@ def _rem_onset(self):
return rem_latency + self.bedtime

def _initialize_sleep_offset(self):
if not self.has_slept:
sleep_offset = None
else:
if self.has_slept:
sleep_nb_epochs = (self.sleep_indexes[-1] + 1) if len(self.sleep_indexes) else len(self.sleep_stages)
sleep_offset = sleep_nb_epochs * EPOCH_DURATION + self.bedtime
else:
sleep_offset = None

self._sleep_offset = sleep_offset

def _initialize_sleep_latency(self):
self._sleep_latency = self._get_latency_of_stage(self.is_sleeping_stages)

def _initialize_rem_latency(self):
"""Time it took to enter REM stage"""
self._rem_latency = self._get_latency_of_stage(self.sleep_stages == SleepStage.REM.name)
"""Time from the sleep onset to the first epoch of REM sleep"""
if self.has_slept:
bedtime_to_rem_duration = self._get_latency_of_stage(self.sleep_stages == SleepStage.REM.name)
rem_latency = bedtime_to_rem_duration - self._sleep_latency if bedtime_to_rem_duration is not None else None
else:
rem_latency = None

self._rem_latency = rem_latency

def _initialize_transition_based_metrics(self):
consecutive_stages_occurences = Counter(zip(self.sleep_stages[:-1], self.sleep_stages[1:]))
Expand Down
4 changes: 2 additions & 2 deletions backend/tests/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ class TestReportLatenciesOnset():
dict(
sequence=['W', 'N1', 'N2', 'N1', 'REM', 'W'],
test_rem=True,
latency=4 * EPOCH_DURATION,
latency=3 * EPOCH_DURATION,
), dict(
sequence=['W', 'W', 'N1', 'W', 'N1', 'W'],
test_rem=False,
Expand Down Expand Up @@ -189,7 +189,7 @@ def test_sequence_has_no_stage(self, sequence, test_rem):
self.assert_latency_equals_expected(expected_latency, expected_onset, sequence, test_rem)

def test_sequence_ends_with_stage(self, sequence, test_rem):
expected_latency = EPOCH_DURATION * (len(sequence) - 1)
expected_latency = EPOCH_DURATION * (len(sequence) - 1) if not test_rem else 0
expected_onset = expected_latency + self.MOCK_REQUEST.bedtime
self.assert_latency_equals_expected(expected_latency, expected_onset, sequence, test_rem)

Expand Down
31 changes: 31 additions & 0 deletions web/src/assets/data/predicted_william_cyton.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,35 @@
{
"subject": {
"age": 30,
"sex": "M"
},
"metadata": {
"bedTime": 1582441980,
"sessionEndTime": 1582473261.596,
"sessionStartTime": 1582436280,
"totalBedTime": 28260,
"totalSessionTime": 36981.596,
"wakeUpTime": 1582470240
},
"report": {
"N1Time": 630,
"N2Time": 11280,
"N3Time": 7530,
"REMTime": 6150,
"WASO": 960,
"WTime": 2310,
"awakenings": 4,
"efficientSleepTime": 25950,
"remLatency": 5940,
"remOnset": 1582447920,
"sleepEfficiency": 0.9182590233545648,
"sleepLatency": 1260,
"sleepOffset": 1582470150,
"sleepOnset": 1582443240,
"sleepTime": 26910,
"stageShifts": 43,
"wakeAfterSleepOffset": 90
},
"epochs": {
"timestamps": [
1582441980,
Expand Down
18 changes: 8 additions & 10 deletions web/src/components/floating_card.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ import PropTypes from 'prop-types';
import { Card, Col, Row } from 'reactstrap';

const FloatingCard = ({ cardClassName, headerText, bodyText, button }) => (
<Card className={`${cardClassName} shadow-lg border-0 h-100 card-lift--hover`}>
<div className="p-5 h-100">
<Row className="align-items-center h-100">
<Col className="h-100 d-flex flex-column">
<h3 className="text-white">{headerText}</h3>
<p className="lead text-white text-justify mt-3">{bodyText}</p>
<Row className="justify-content-center mt-auto">{button}</Row>
</Col>
</Row>
</div>
<Card className={`${cardClassName} p-5 h-100 shadow-lg border-0 h-100 card-lift--hover`}>
<Row className="align-items-center h-100">
<Col className="h-100 d-flex flex-column">
{headerText !== null && <h3 className="text-white">{headerText}</h3>}
<p className="lead text-white text-justify mt-3">{bodyText}</p>
<Row className="justify-content-center mt-auto">{button}</Row>
</Col>
</Row>
</Card>
);

Expand Down
2 changes: 1 addition & 1 deletion web/src/components/footer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ PlatformButton.defaultProps = {
const Copyright = () => {
return (
<div className="copyright">
© {new Date().getFullYear()}{' '}
© {new Date().getFullYear()}&nbsp;
<a href="http://polycortex.polymtl.ca/" target="_blank" rel="noopener noreferrer">
{text['footer_copyright_polycortex']}
</a>
Expand Down
71 changes: 44 additions & 27 deletions web/src/d3/evolving_chart/chart_states.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as d3 from 'd3';
import _ from 'lodash';
import { Duration } from 'luxon';

import { BAR_HEIGHT, DIMENSION } from './constants';
import { BAR_HEIGHT, DIMENSION, HOVERED_RECT_OPACITY } from './constants';
import { EPOCH_DURATION_MS, TRANSITION_TIME_MS, STAGES_ORDERED } from '../constants';

import '../style.css';
Expand All @@ -11,6 +11,8 @@ export const createTimelineChartCallbacks = (g, xTime, xTimeAxis, color, tooltip
Object({
fromInitial: () => {
const annotationRects = g.selectAll('.rect-stacked').interrupt();
g.selectAll('text.proportion').remove();
d3.selectAll('div.tooltip').style('opacity', 0);

setAttrOnAnnotationRects(annotationRects, xTime, 0, color, tooltip);

Expand All @@ -21,8 +23,9 @@ export const createTimelineChartCallbacks = (g, xTime, xTimeAxis, color, tooltip
},
fromInstance: () => {
const annotationRects = g.selectAll('.rect-stacked').interrupt();

g.selectAll('text.proportion').remove();
g.selectAll('.y.visualization__axis').remove();
d3.selectAll('div.tooltip').style('opacity', 0);

setAttrOnAnnotationRects(annotationRects, xTime, 0, color, tooltip);

Expand All @@ -34,67 +37,65 @@ export const createInstanceChartCallbacks = (g, data, xTime, xTimeAxis, yAxis, c
Object({
fromTimeline: () => {
const annotationRects = g.selectAll('.rect-stacked').interrupt();
g.selectAll('text.proportion').remove();
d3.selectAll('div.tooltip').style('opacity', 0);

createVerticalAxis(g, yAxis, color);
transitionHorizontalAxis(g, STAGES_ORDERED.length * BAR_HEIGHT);
setAttrOnAnnotationRects(annotationRects, xTime, getVerticalPositionCallback, color, tooltip);
setAttrOnAnnotationRects(annotationRects, xTime, getVerticalPositionCallback(), color, tooltip);
},
fromBarChart: () => {
const annotationRects = g.selectAll('.rect-stacked').interrupt();
const xProportionCallback = getOffsetSleepStageProportionCallback(data);
annotationRects.attr('x', xProportionCallback).attr('width', ({ end, start }) => xTime(end) - xTime(start));

g.selectAll('text.proportion').remove();
d3.selectAll('div.tooltip').style('opacity', 0);

g.select('.x.visualization__axis').interrupt().transition().call(xTimeAxis);
transitionHorizontalAxis(g, STAGES_ORDERED.length * BAR_HEIGHT);

setAttrOnAnnotationRects(annotationRects, xTime, getVerticalPositionCallback, color, tooltip);
setAttrOnAnnotationRects(annotationRects, xTime, getVerticalPositionCallback(), color, tooltip);
},
});

export const createBarChartCallbacks = (g, data, xAxisLinear, yAxis, color, tip) =>
Object({
fromInstance: () => {
const { firstStageIndexes, stageTimeProportions } = data;
const annotationRects = g.selectAll('.rect-stacked').interrupt();
const xProportionCallback = getOffsetSleepStageProportionCallback(data);

d3.selectAll('div.tooltip').style('opacity', 0);
g.select('.x.visualization__axis').transition().call(xAxisLinear);
transitionHorizontalAxis(g, STAGES_ORDERED.length * BAR_HEIGHT);

setTooltip(annotationRects, tip)
setTooltip(annotationRects, tip, getVerticalPositionCallback(20))
.transition()
.duration(TRANSITION_TIME_MS)
.attr('y', getVerticalPositionCallback)
.attr('y', getVerticalPositionCallback())
.attr('x', xProportionCallback)
.on('end', () => {
// Only keep the first rectangle of each stage to be visible
g.selectAll('.rect-stacked')
.attr('x', 0)
.attr('width', getFirstRectangleProportionWidthCallback(firstStageIndexes, stageTimeProportions));
createProportionLabels(g, data);
});
.on('end', () => setFirstRectangleToBeAsWideAsStageProportion(data, g));
},
fromStackedBarChart: () => {
const annotationRects = g.selectAll('.rect-stacked').interrupt();
g.selectAll('text.proportion').remove();
d3.selectAll('div.tooltip').style('opacity', 0);

createVerticalAxis(g, yAxis, color);
transitionHorizontalAxis(g, STAGES_ORDERED.length * BAR_HEIGHT);

annotationRects
setTooltip(annotationRects, tip, getVerticalPositionCallback(20))
.transition()
.duration(TRANSITION_TIME_MS / 2)
.attr('y', (d) => BAR_HEIGHT * STAGES_ORDERED.indexOf(d.stage))
.transition()
.duration(TRANSITION_TIME_MS / 2)
.attr('x', 0)
.on('end', () => createProportionLabels(g, data));
.on('end', () => setFirstRectangleToBeAsWideAsStageProportion(data, g));
},
});

export const createStackedBarChartCallbacks = (g, data) =>
export const createStackedBarChartCallbacks = (g, data, tip) =>
Object({
fromBarChart: () => {
const { annotations, firstStageIndexes, stageTimeProportions, epochs } = data;
Expand All @@ -103,6 +104,8 @@ export const createStackedBarChartCallbacks = (g, data) =>
(getCumulativeProportionOfNightAtStart(stage, stageTimeProportions) + stageTimeProportions[stage] / 2) *
DIMENSION.WIDTH;
const annotationRects = g.selectAll('.rect-stacked').interrupt();
setTooltip(annotationRects, tip, 0);
d3.selectAll('div.tooltip').style('opacity', 0);

g.selectAll('.y.visualization__axis').remove();
g.selectAll('text.proportion').remove();
Expand Down Expand Up @@ -137,28 +140,42 @@ export const createStackedBarChartCallbacks = (g, data) =>
},
});

const setAttrOnAnnotationRects = (annotationRects, x, yPosition, color, tooltip) =>
setTooltip(annotationRects, tooltip)
const setAttrOnAnnotationRects = (annotationRects, x, yBarPosition, color, tooltip) =>
setTooltip(annotationRects, tooltip, yBarPosition)
.attr('height', BAR_HEIGHT)
.transition()
.duration(TRANSITION_TIME_MS)
.attr('x', ({ start }) => x(start))
.attr('y', yPosition)
.attr('y', yBarPosition)
.attr('width', ({ end, start }) => x(end) - x(start))
.attr('fill', ({ stage }) => color(stage));

const setTooltip = (element, tooltip) =>
const setFirstRectangleToBeAsWideAsStageProportion = (data, g) => {
const { firstStageIndexes, stageTimeProportions } = data;

// Only keep the first rectangle of each stage to be visible
g.selectAll('.rect-stacked')
.attr('x', 0)
.attr('width', getFirstRectangleProportionWidthCallback(firstStageIndexes, stageTimeProportions));
createProportionLabels(g, data);
};

const setTooltip = (element, tooltip, y) =>
element
.on('mouseover', function (d) {
tooltip.show(d, this);
d3.select(this).style('opacity', 0.8);
.on('mouseover', function () {
tooltip.mouseover();
d3.select(this).style('stroke', 'black').style('opacity', HOVERED_RECT_OPACITY);
})
.on('mousemove', function (d) {
tooltip.mousemove(d, d3.mouse(this), `${y === 0 ? 0 : y(d)}px`);
})
.on('mouseout', function () {
tooltip.hide();
d3.select(this).style('opacity', 1);
tooltip.mouseleave();
d3.select(this).style('stroke', 'none').style('opacity', 1);
});

const getVerticalPositionCallback = (d) => BAR_HEIGHT * STAGES_ORDERED.indexOf(d.stage);
const getVerticalPositionCallback = (cardOffset = 0) => (d) =>
BAR_HEIGHT * STAGES_ORDERED.indexOf(d.stage) + cardOffset;

const getFirstRectangleProportionWidthCallback = (firstStageIndexes, stageTimeProportions) => ({ stage }, i) =>
i === firstStageIndexes[stage] ? stageTimeProportions[stage] * DIMENSION.WIDTH : 0;
Expand Down
1 change: 1 addition & 0 deletions web/src/d3/evolving_chart/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export const DIMENSION = {
};

export const BAR_HEIGHT = DIMENSION.HEIGHT / STAGES_ORDERED.length;
export const HOVERED_RECT_OPACITY = 0.7;
9 changes: 6 additions & 3 deletions web/src/d3/evolving_chart/evolving_chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ const bindAnnotationsToRects = (g, annotations) =>
g.selectAll('.rect').data(annotations).enter().append('rect').attr('class', 'rect-stacked');

const createEvolvingChart = (containerNode, data) => {
const svg = d3.select(containerNode).attr('viewBox', `0, 0, ${CANVAS_DIMENSION.WIDTH}, ${CANVAS_DIMENSION.HEIGHT}`);
const svg = d3
.select(containerNode)
.append('svg')
.attr('viewBox', `0, 0, ${CANVAS_DIMENSION.WIDTH}, ${CANVAS_DIMENSION.HEIGHT}`);
const { xTime, xLinear, y, colors } = initializeScales();
const { xTimeAxis, xLinearAxis, yAxis } = initializeAxes(xTime, xLinear, y);
const g = createDrawingGroup(svg);
Expand All @@ -63,7 +66,7 @@ const createEvolvingChart = (containerNode, data) => {
data = preprocessData(data);

setDomainOnScales(xTime, xLinear, y, colors, data.epochs);
const { barToolTip, stackedToolTip } = initializeTooltips(svg, data);
const { barToolTip, stackedToolTip } = initializeTooltips(containerNode, data);
bindAnnotationsToRects(g, data.annotations);

timelineChartCallbacks = createTimelineChartCallbacks(g, xTime, xTimeAxis, colors, barToolTip);
Expand All @@ -72,7 +75,7 @@ const createEvolvingChart = (containerNode, data) => {

barChartCallbacks = createBarChartCallbacks(g, data, xLinearAxis, yAxis, colors, stackedToolTip);

stackedBarChartCallbacks = createStackedBarChartCallbacks(g, data);
stackedBarChartCallbacks = createStackedBarChartCallbacks(g, data, stackedToolTip);

timelineChartCallbacks.fromInitial();
};
Expand Down
37 changes: 27 additions & 10 deletions web/src/d3/evolving_chart/mouse_over.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
import tip from 'd3-tip';
import './style.css';
import * as d3 from 'd3';

import { EPOCH_DURATION_MS } from '../constants';
import { DateTime, Duration } from 'luxon';

export const initializeTooltips = (svg, data) => {
const barToolTip = initializeTooltip(svg, getBarToolTipText);
const stackedToolTip = initializeTooltip(svg, (d) =>
export const initializeTooltips = (containerNode, data) => {
const stackedToolTip = initializeTooltip(containerNode, (d) =>
getStackedToolTipText(d, data.stageTimeProportions, data.epochs.length),
);
const barToolTip = initializeTooltip(containerNode, getBarToolTipText);

return { barToolTip, stackedToolTip };
};

const initializeTooltip = (svg, getToolTipText) => {
const tooltip = tip().attr('class', 'evolving_chart__tooltip').offset([-10, 0]);
svg.call(tooltip);
tooltip.html(getToolTipText);
const initializeTooltip = (containerNode, getToolTipText) => {
var tooltip = d3
.select(containerNode)
.append('div')
.attr('class', 'tooltip')
.attr('border-radius', '2px')
.style('background-color', 'rgba(235, 235, 235, 0.9)')
.style('opacity', 0)
.style('position', 'absolute');
var tooltipText = tooltip.append('div').style('padding', '1em').style('font-size', '1em');

return tooltip;
var mouseover = (d) => {
tooltip.style('opacity', 1);
};
var mousemove = function (d, mouse, yPosition) {
// localize d3.mouse into viewbox: https://stackoverflow.com/a/11936865
tooltip.style('opacity', 1).style('left', `${mouse[0]}px`).style('top', yPosition);
tooltipText.html(() => getToolTipText(d));
};
var mouseleave = function () {
tooltip.style('opacity', 0).style('left', `0px`).style('top', '0px');
};

return { mouseover, mousemove, mouseleave };
};

const getBarToolTipText = (d) => `Stage : <strong> ${d.stage} </strong> <br>
Expand Down
7 changes: 0 additions & 7 deletions web/src/d3/evolving_chart/style.css

This file was deleted.

2 changes: 1 addition & 1 deletion web/src/d3/spectrogram/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const NB_SPECTROGRAM = 2;
export const FREQUENCY_KEY = 'frequencies';
export const HYPNOGRAM_KEY = 'epochs';
export const NB_POINTS_COLOR_INTERPOLATION = 3;
export const NOT_HIGHLIGHTED_RECTANGLE_OPACITY = 0.5;
export const NOT_HIGHLIGHTED_RECTANGLE_OPACITY = 0.7;
export const CANVAS_WIDTH_TO_HEIGHT_RATIO = 700 / 1000; // width to height ratio
export const CANVAS_HEIGHT_WINDOW_FACTOR = 0.8;
export const MARGIN = {
Expand Down
Loading