Skip to content

Commit ba9b1a5

Browse files
conoratoWilliam Harveyabelfodilmateobelanger
authored
Web/metrics in analyze sleep (#93)
* added metrics in preview & changed class to classname * added first card * added stats * fixed cards * renamed and fixed cards appearences * added color to calculated metrics * moved barchart cards to right && added bootstrap rows * added sleep mechanisms & tips * fixed btn hero * changed spectro cards size * fixed rem latency in server (time between sleep onset & first rem) * extracted metrics * added recommanded time sleep * reviewed spectro * added spectro references * support wake sleep sequence * fixed labels and rectangle width issues * fixed cards responsive * tooltip begins to work * add content about hormones * add stage shifts and noctural awakening metrics * fixed tool tip position * add some text to the intro * fixed tooltips * change all font-size to lead * Add color to sleep stages * change sleep stage solor * added description of freq band * add content on sleep stages * change text outro evolutive chart * completed spectro cards * reviewed * revert ispreviewmode * Update web/src/views/sleep_analysis_results/index.js Co-authored-by: Anes Belfodil <abelfodil@users.noreply.github.com> * Update web/src/views/sleep_analysis_results/evolving_chart_scrollytelling.js Co-authored-by: Anes Belfodil <abelfodil@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Mathieu Bélanger <mbelanger.poly@gmail.com> Co-authored-by: Anes Belfodil <abelfodil@users.noreply.github.com> * remove adenosine explanation in card * Apply suggestions from code review Co-authored-by: Anes Belfodil <abelfodil@users.noreply.github.com> * Apply suggested change * apply suggested changes Co-authored-by: William Harvey <william.harvey@polymtl.ca> Co-authored-by: Anes Belfodil <abelfodil@users.noreply.github.com> Co-authored-by: Mathieu Bélanger <mbelanger.poly@gmail.com>
1 parent 49d808d commit ba9b1a5

File tree

26 files changed

+1067
-431
lines changed

26 files changed

+1067
-431
lines changed

backend/backend/metric.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,20 +106,26 @@ def _rem_onset(self):
106106
return rem_latency + self.bedtime
107107

108108
def _initialize_sleep_offset(self):
109-
if not self.has_slept:
110-
sleep_offset = None
111-
else:
109+
if self.has_slept:
112110
sleep_nb_epochs = (self.sleep_indexes[-1] + 1) if len(self.sleep_indexes) else len(self.sleep_stages)
113111
sleep_offset = sleep_nb_epochs * EPOCH_DURATION + self.bedtime
112+
else:
113+
sleep_offset = None
114114

115115
self._sleep_offset = sleep_offset
116116

117117
def _initialize_sleep_latency(self):
118118
self._sleep_latency = self._get_latency_of_stage(self.is_sleeping_stages)
119119

120120
def _initialize_rem_latency(self):
121-
"""Time it took to enter REM stage"""
122-
self._rem_latency = self._get_latency_of_stage(self.sleep_stages == SleepStage.REM.name)
121+
"""Time from the sleep onset to the first epoch of REM sleep"""
122+
if self.has_slept:
123+
bedtime_to_rem_duration = self._get_latency_of_stage(self.sleep_stages == SleepStage.REM.name)
124+
rem_latency = bedtime_to_rem_duration - self._sleep_latency if bedtime_to_rem_duration is not None else None
125+
else:
126+
rem_latency = None
127+
128+
self._rem_latency = rem_latency
123129

124130
def _initialize_transition_based_metrics(self):
125131
consecutive_stages_occurences = Counter(zip(self.sleep_stages[:-1], self.sleep_stages[1:]))

backend/tests/test_response.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ class TestReportLatenciesOnset():
157157
dict(
158158
sequence=['W', 'N1', 'N2', 'N1', 'REM', 'W'],
159159
test_rem=True,
160-
latency=4 * EPOCH_DURATION,
160+
latency=3 * EPOCH_DURATION,
161161
), dict(
162162
sequence=['W', 'W', 'N1', 'W', 'N1', 'W'],
163163
test_rem=False,
@@ -189,7 +189,7 @@ def test_sequence_has_no_stage(self, sequence, test_rem):
189189
self.assert_latency_equals_expected(expected_latency, expected_onset, sequence, test_rem)
190190

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

web/src/assets/data/predicted_william_cyton.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,35 @@
11
{
2+
"subject": {
3+
"age": 30,
4+
"sex": "M"
5+
},
6+
"metadata": {
7+
"bedTime": 1582441980,
8+
"sessionEndTime": 1582473261.596,
9+
"sessionStartTime": 1582436280,
10+
"totalBedTime": 28260,
11+
"totalSessionTime": 36981.596,
12+
"wakeUpTime": 1582470240
13+
},
14+
"report": {
15+
"N1Time": 630,
16+
"N2Time": 11280,
17+
"N3Time": 7530,
18+
"REMTime": 6150,
19+
"WASO": 960,
20+
"WTime": 2310,
21+
"awakenings": 4,
22+
"efficientSleepTime": 25950,
23+
"remLatency": 5940,
24+
"remOnset": 1582447920,
25+
"sleepEfficiency": 0.9182590233545648,
26+
"sleepLatency": 1260,
27+
"sleepOffset": 1582470150,
28+
"sleepOnset": 1582443240,
29+
"sleepTime": 26910,
30+
"stageShifts": 43,
31+
"wakeAfterSleepOffset": 90
32+
},
233
"epochs": {
334
"timestamps": [
435
1582441980,

web/src/components/floating_card.js

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,14 @@ import PropTypes from 'prop-types';
33
import { Card, Col, Row } from 'reactstrap';
44

55
const FloatingCard = ({ cardClassName, headerText, bodyText, button }) => (
6-
<Card className={`${cardClassName} shadow-lg border-0 h-100 card-lift--hover`}>
7-
<div className="p-5 h-100">
8-
<Row className="align-items-center h-100">
9-
<Col className="h-100 d-flex flex-column">
10-
<h3 className="text-white">{headerText}</h3>
11-
<p className="lead text-white text-justify mt-3">{bodyText}</p>
12-
<Row className="justify-content-center mt-auto">{button}</Row>
13-
</Col>
14-
</Row>
15-
</div>
6+
<Card className={`${cardClassName} p-5 h-100 shadow-lg border-0 h-100 card-lift--hover`}>
7+
<Row className="align-items-center h-100">
8+
<Col className="h-100 d-flex flex-column">
9+
{headerText !== null && <h3 className="text-white">{headerText}</h3>}
10+
<p className="lead text-white text-justify mt-3">{bodyText}</p>
11+
<Row className="justify-content-center mt-auto">{button}</Row>
12+
</Col>
13+
</Row>
1614
</Card>
1715
);
1816

web/src/components/footer/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ PlatformButton.defaultProps = {
3737
const Copyright = () => {
3838
return (
3939
<div className="copyright">
40-
© {new Date().getFullYear()}{' '}
40+
© {new Date().getFullYear()}&nbsp;
4141
<a href="http://polycortex.polymtl.ca/" target="_blank" rel="noopener noreferrer">
4242
{text['footer_copyright_polycortex']}
4343
</a>

web/src/d3/evolving_chart/chart_states.js

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as d3 from 'd3';
22
import _ from 'lodash';
33
import { Duration } from 'luxon';
44

5-
import { BAR_HEIGHT, DIMENSION } from './constants';
5+
import { BAR_HEIGHT, DIMENSION, HOVERED_RECT_OPACITY } from './constants';
66
import { EPOCH_DURATION_MS, TRANSITION_TIME_MS, STAGES_ORDERED } from '../constants';
77

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

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

@@ -21,8 +23,9 @@ export const createTimelineChartCallbacks = (g, xTime, xTimeAxis, color, tooltip
2123
},
2224
fromInstance: () => {
2325
const annotationRects = g.selectAll('.rect-stacked').interrupt();
24-
26+
g.selectAll('text.proportion').remove();
2527
g.selectAll('.y.visualization__axis').remove();
28+
d3.selectAll('div.tooltip').style('opacity', 0);
2629

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

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

3843
createVerticalAxis(g, yAxis, color);
3944
transitionHorizontalAxis(g, STAGES_ORDERED.length * BAR_HEIGHT);
40-
setAttrOnAnnotationRects(annotationRects, xTime, getVerticalPositionCallback, color, tooltip);
45+
setAttrOnAnnotationRects(annotationRects, xTime, getVerticalPositionCallback(), color, tooltip);
4146
},
4247
fromBarChart: () => {
4348
const annotationRects = g.selectAll('.rect-stacked').interrupt();
4449
const xProportionCallback = getOffsetSleepStageProportionCallback(data);
4550
annotationRects.attr('x', xProportionCallback).attr('width', ({ end, start }) => xTime(end) - xTime(start));
4651

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

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

52-
setAttrOnAnnotationRects(annotationRects, xTime, getVerticalPositionCallback, color, tooltip);
58+
setAttrOnAnnotationRects(annotationRects, xTime, getVerticalPositionCallback(), color, tooltip);
5359
},
5460
});
5561

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

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

66-
setTooltip(annotationRects, tip)
72+
setTooltip(annotationRects, tip, getVerticalPositionCallback(20))
6773
.transition()
6874
.duration(TRANSITION_TIME_MS)
69-
.attr('y', getVerticalPositionCallback)
75+
.attr('y', getVerticalPositionCallback())
7076
.attr('x', xProportionCallback)
71-
.on('end', () => {
72-
// Only keep the first rectangle of each stage to be visible
73-
g.selectAll('.rect-stacked')
74-
.attr('x', 0)
75-
.attr('width', getFirstRectangleProportionWidthCallback(firstStageIndexes, stageTimeProportions));
76-
createProportionLabels(g, data);
77-
});
77+
.on('end', () => setFirstRectangleToBeAsWideAsStageProportion(data, g));
7878
},
7979
fromStackedBarChart: () => {
8080
const annotationRects = g.selectAll('.rect-stacked').interrupt();
8181
g.selectAll('text.proportion').remove();
82+
d3.selectAll('div.tooltip').style('opacity', 0);
8283

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

86-
annotationRects
87+
setTooltip(annotationRects, tip, getVerticalPositionCallback(20))
8788
.transition()
8889
.duration(TRANSITION_TIME_MS / 2)
8990
.attr('y', (d) => BAR_HEIGHT * STAGES_ORDERED.indexOf(d.stage))
9091
.transition()
9192
.duration(TRANSITION_TIME_MS / 2)
9293
.attr('x', 0)
93-
.on('end', () => createProportionLabels(g, data));
94+
.on('end', () => setFirstRectangleToBeAsWideAsStageProportion(data, g));
9495
},
9596
});
9697

97-
export const createStackedBarChartCallbacks = (g, data) =>
98+
export const createStackedBarChartCallbacks = (g, data, tip) =>
9899
Object({
99100
fromBarChart: () => {
100101
const { annotations, firstStageIndexes, stageTimeProportions, epochs } = data;
@@ -103,6 +104,8 @@ export const createStackedBarChartCallbacks = (g, data) =>
103104
(getCumulativeProportionOfNightAtStart(stage, stageTimeProportions) + stageTimeProportions[stage] / 2) *
104105
DIMENSION.WIDTH;
105106
const annotationRects = g.selectAll('.rect-stacked').interrupt();
107+
setTooltip(annotationRects, tip, 0);
108+
d3.selectAll('div.tooltip').style('opacity', 0);
106109

107110
g.selectAll('.y.visualization__axis').remove();
108111
g.selectAll('text.proportion').remove();
@@ -137,28 +140,42 @@ export const createStackedBarChartCallbacks = (g, data) =>
137140
},
138141
});
139142

140-
const setAttrOnAnnotationRects = (annotationRects, x, yPosition, color, tooltip) =>
141-
setTooltip(annotationRects, tooltip)
143+
const setAttrOnAnnotationRects = (annotationRects, x, yBarPosition, color, tooltip) =>
144+
setTooltip(annotationRects, tooltip, yBarPosition)
142145
.attr('height', BAR_HEIGHT)
143146
.transition()
144147
.duration(TRANSITION_TIME_MS)
145148
.attr('x', ({ start }) => x(start))
146-
.attr('y', yPosition)
149+
.attr('y', yBarPosition)
147150
.attr('width', ({ end, start }) => x(end) - x(start))
148151
.attr('fill', ({ stage }) => color(stage));
149152

150-
const setTooltip = (element, tooltip) =>
153+
const setFirstRectangleToBeAsWideAsStageProportion = (data, g) => {
154+
const { firstStageIndexes, stageTimeProportions } = data;
155+
156+
// Only keep the first rectangle of each stage to be visible
157+
g.selectAll('.rect-stacked')
158+
.attr('x', 0)
159+
.attr('width', getFirstRectangleProportionWidthCallback(firstStageIndexes, stageTimeProportions));
160+
createProportionLabels(g, data);
161+
};
162+
163+
const setTooltip = (element, tooltip, y) =>
151164
element
152-
.on('mouseover', function (d) {
153-
tooltip.show(d, this);
154-
d3.select(this).style('opacity', 0.8);
165+
.on('mouseover', function () {
166+
tooltip.mouseover();
167+
d3.select(this).style('stroke', 'black').style('opacity', HOVERED_RECT_OPACITY);
168+
})
169+
.on('mousemove', function (d) {
170+
tooltip.mousemove(d, d3.mouse(this), `${y === 0 ? 0 : y(d)}px`);
155171
})
156172
.on('mouseout', function () {
157-
tooltip.hide();
158-
d3.select(this).style('opacity', 1);
173+
tooltip.mouseleave();
174+
d3.select(this).style('stroke', 'none').style('opacity', 1);
159175
});
160176

161-
const getVerticalPositionCallback = (d) => BAR_HEIGHT * STAGES_ORDERED.indexOf(d.stage);
177+
const getVerticalPositionCallback = (cardOffset = 0) => (d) =>
178+
BAR_HEIGHT * STAGES_ORDERED.indexOf(d.stage) + cardOffset;
162179

163180
const getFirstRectangleProportionWidthCallback = (firstStageIndexes, stageTimeProportions) => ({ stage }, i) =>
164181
i === firstStageIndexes[stage] ? stageTimeProportions[stage] * DIMENSION.WIDTH : 0;

web/src/d3/evolving_chart/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ export const DIMENSION = {
1616
};
1717

1818
export const BAR_HEIGHT = DIMENSION.HEIGHT / STAGES_ORDERED.length;
19+
export const HOVERED_RECT_OPACITY = 0.7;

web/src/d3/evolving_chart/evolving_chart.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ const bindAnnotationsToRects = (g, annotations) =>
5454
g.selectAll('.rect').data(annotations).enter().append('rect').attr('class', 'rect-stacked');
5555

5656
const createEvolvingChart = (containerNode, data) => {
57-
const svg = d3.select(containerNode).attr('viewBox', `0, 0, ${CANVAS_DIMENSION.WIDTH}, ${CANVAS_DIMENSION.HEIGHT}`);
57+
const svg = d3
58+
.select(containerNode)
59+
.append('svg')
60+
.attr('viewBox', `0, 0, ${CANVAS_DIMENSION.WIDTH}, ${CANVAS_DIMENSION.HEIGHT}`);
5861
const { xTime, xLinear, y, colors } = initializeScales();
5962
const { xTimeAxis, xLinearAxis, yAxis } = initializeAxes(xTime, xLinear, y);
6063
const g = createDrawingGroup(svg);
@@ -63,7 +66,7 @@ const createEvolvingChart = (containerNode, data) => {
6366
data = preprocessData(data);
6467

6568
setDomainOnScales(xTime, xLinear, y, colors, data.epochs);
66-
const { barToolTip, stackedToolTip } = initializeTooltips(svg, data);
69+
const { barToolTip, stackedToolTip } = initializeTooltips(containerNode, data);
6770
bindAnnotationsToRects(g, data.annotations);
6871

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

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

75-
stackedBarChartCallbacks = createStackedBarChartCallbacks(g, data);
78+
stackedBarChartCallbacks = createStackedBarChartCallbacks(g, data, stackedToolTip);
7679

7780
timelineChartCallbacks.fromInitial();
7881
};

web/src/d3/evolving_chart/mouse_over.js

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
1-
import tip from 'd3-tip';
2-
import './style.css';
1+
import * as d3 from 'd3';
32

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

7-
export const initializeTooltips = (svg, data) => {
8-
const barToolTip = initializeTooltip(svg, getBarToolTipText);
9-
const stackedToolTip = initializeTooltip(svg, (d) =>
6+
export const initializeTooltips = (containerNode, data) => {
7+
const stackedToolTip = initializeTooltip(containerNode, (d) =>
108
getStackedToolTipText(d, data.stageTimeProportions, data.epochs.length),
119
);
10+
const barToolTip = initializeTooltip(containerNode, getBarToolTipText);
1211

1312
return { barToolTip, stackedToolTip };
1413
};
1514

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

21-
return tooltip;
26+
var mouseover = (d) => {
27+
tooltip.style('opacity', 1);
28+
};
29+
var mousemove = function (d, mouse, yPosition) {
30+
// localize d3.mouse into viewbox: https://stackoverflow.com/a/11936865
31+
tooltip.style('opacity', 1).style('left', `${mouse[0]}px`).style('top', yPosition);
32+
tooltipText.html(() => getToolTipText(d));
33+
};
34+
var mouseleave = function () {
35+
tooltip.style('opacity', 0).style('left', `0px`).style('top', '0px');
36+
};
37+
38+
return { mouseover, mousemove, mouseleave };
2239
};
2340

2441
const getBarToolTipText = (d) => `Stage : <strong> ${d.stage} </strong> <br>

web/src/d3/evolving_chart/style.css

Lines changed: 0 additions & 7 deletions
This file was deleted.

web/src/d3/spectrogram/constants.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export const NB_SPECTROGRAM = 2;
33
export const FREQUENCY_KEY = 'frequencies';
44
export const HYPNOGRAM_KEY = 'epochs';
55
export const NB_POINTS_COLOR_INTERPOLATION = 3;
6-
export const NOT_HIGHLIGHTED_RECTANGLE_OPACITY = 0.5;
6+
export const NOT_HIGHLIGHTED_RECTANGLE_OPACITY = 0.7;
77
export const CANVAS_WIDTH_TO_HEIGHT_RATIO = 700 / 1000; // width to height ratio
88
export const CANVAS_HEIGHT_WINDOW_FACTOR = 0.8;
99
export const MARGIN = {

0 commit comments

Comments
 (0)