From e6adb2fcaeddeb248c90c8050ac7b2ac8058a9c4 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Mon, 7 Sep 2020 11:54:53 -0400 Subject: [PATCH 01/10] load hypnogram for spectrogram viz --- web/src/d3/spectrogram/constants.js | 1 + web/src/d3/spectrogram/spectrogram.js | 3 ++- web/src/views/sleep_analysis/sleep_analysis.js | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/web/src/d3/spectrogram/constants.js b/web/src/d3/spectrogram/constants.js index 5ae945a1..cfa4392e 100644 --- a/web/src/d3/spectrogram/constants.js +++ b/web/src/d3/spectrogram/constants.js @@ -1,6 +1,7 @@ export const PADDING = 100; export const NB_SPECTROGRAM = 2; export const FREQUENCY_KEY = 'frequencies'; +export const HYPNOGRAM_KEY = 'hypnogram'; export const NB_POINTS_COLOR_INTERPOLATION = 3; export const TITLE_FONT_SIZE = '18px'; export const CANVAS_WIDTH_TO_HEIGHT_RATIO = 700 / 1000; // width to height ratio diff --git a/web/src/d3/spectrogram/spectrogram.js b/web/src/d3/spectrogram/spectrogram.js index fef0b5b3..b37fc61f 100644 --- a/web/src/d3/spectrogram/spectrogram.js +++ b/web/src/d3/spectrogram/spectrogram.js @@ -5,6 +5,7 @@ import { MARGIN, CANVAS_WIDTH_TO_HEIGHT_RATIO, FREQUENCY_KEY, + HYPNOGRAM_KEY, TITLE_FONT_SIZE, NB_SPECTROGRAM, PADDING, @@ -173,7 +174,7 @@ const createSpectrogram = (containerNode, data) => { singleSpectrogramHeight: (canvasHeight - MARGIN.BOTTOM - MARGIN.TOP - PADDING) / NB_SPECTROGRAM, }; - const channelNames = _.filter(_.keys(data), (keyName) => keyName !== FREQUENCY_KEY); + const channelNames = _.filter(_.keys(data), (keyName) => !_.includes([FREQUENCY_KEY, HYPNOGRAM_KEY], keyName)); const scalesAndAxesBySpectrogram = _.map(channelNames, (name) => getScalesAndAxes(data, name, dimensions)); const canvas = parentDiv diff --git a/web/src/views/sleep_analysis/sleep_analysis.js b/web/src/views/sleep_analysis/sleep_analysis.js index cf37cffc..75069a9e 100644 --- a/web/src/views/sleep_analysis/sleep_analysis.js +++ b/web/src/views/sleep_analysis/sleep_analysis.js @@ -9,6 +9,7 @@ import D3Component from '../../components/d3component'; import WIPWarning from '../../components/wip_warning'; import { createSingleHypnogram } from '../../d3/hypnogram/hypnogram'; +import { HYPNOGRAM_KEY } from '../../d3/spectrogram/constants'; import text from './text.json'; import createSpectrogram from '../../d3/spectrogram/spectrogram'; @@ -20,6 +21,7 @@ import spectrogramData from 'assets/data/spectrograms.json'; const SleepAnalysis = () => { const csvDataSleepEDF = useCSVData(hypnogramDataSleepEDFPath); + const spectrogramWithHypnogramData = csvDataSleepEDF ? { ...spectrogramData, [HYPNOGRAM_KEY]: csvDataSleepEDF } : null; return (
@@ -104,7 +106,7 @@ const SleepAnalysis = () => { that decomposes sound frequency from your microphone.

- +

Generally, when talking about brain waves, we group certain frequencies together into bands. There are overall five frequency bands, where each has a general associated behaviour, or state of mind. We will cover those when looking at time frames corresponding to each sleep From 5e479faed234a0dbcd93df48a28534c595a221a9 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Mon, 7 Sep 2020 12:31:39 -0400 Subject: [PATCH 02/10] refactored spectrogram dimensions --- web/src/d3/spectrogram/spectrogram.js | 47 ++++++++++++++------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/web/src/d3/spectrogram/spectrogram.js b/web/src/d3/spectrogram/spectrogram.js index b37fc61f..e3262e37 100644 --- a/web/src/d3/spectrogram/spectrogram.js +++ b/web/src/d3/spectrogram/spectrogram.js @@ -14,6 +14,30 @@ import { import { EPOCH_DURATION_SEC } from '../constants'; import { createLegend } from './legend'; +const getDimensions = (parentDiv) => { + const canvasWidth = parentDiv.node().getBoundingClientRect().width; + const canvasHeight = Math.min(canvasWidth * CANVAS_WIDTH_TO_HEIGHT_RATIO, window.innerHeight * CANVAS_HEIGHT_WINDOW_FACTOR); + const spectrogramsHeight = canvasHeight - MARGIN.TOP - MARGIN.BOTTOM; + const singleSpectrogramCanvasHeight = _.range(NB_SPECTROGRAM).map((x) => { + let height = spectrogramsHeight / NB_SPECTROGRAM; + if (x === 0) { + height += MARGIN.TOP; + } else if (x === NB_SPECTROGRAM - 1) { + height += MARGIN.BOTTOM; + } + return height; + }); + + return { + canvasWidth, + canvasHeight, + spectrogramsHeight, + singleSpectrogramCanvasHeight, + spectrogramWidth: canvasWidth - MARGIN.LEFT - MARGIN.RIGHT, + singleSpectrogramHeight: (spectrogramsHeight - PADDING) / NB_SPECTROGRAM, + }; +}; + const initializeScales = ({ spectrogramWidth, singleSpectrogramHeight }) => Object({ x: d3.scaleLinear([0, spectrogramWidth]), @@ -141,17 +165,6 @@ const createSpectrogramAxesAndLegend = ( createLegend(legendDrawingGroup, color, yColor, singleSpectrogramHeight); }); -const getSpectrogramCanvasHeight = (spectrogramHeight) => - _.range(NB_SPECTROGRAM).map((x) => { - let height = spectrogramHeight / NB_SPECTROGRAM; - if (x === 0) { - height += MARGIN.TOP; - } else if (x === NB_SPECTROGRAM - 1) { - height += MARGIN.BOTTOM; - } - return height; - }); - const createSpectrogram = (containerNode, data) => { /* Considering the number of rectangles to display is well over 1k, @@ -163,17 +176,7 @@ const createSpectrogram = (containerNode, data) => { setting the first element's position, in this case the canvas, to absolute. */ const parentDiv = d3.select(containerNode); - const canvasWidth = parentDiv.node().getBoundingClientRect().width; - const canvasHeight = Math.min(canvasWidth * CANVAS_WIDTH_TO_HEIGHT_RATIO, window.innerHeight * CANVAS_HEIGHT_WINDOW_FACTOR); - const dimensions = { - canvasWidth: canvasWidth, - canvasHeight: canvasHeight, - spectrogramWidth: canvasWidth - MARGIN.LEFT - MARGIN.RIGHT, - spectrogramsHeight: canvasHeight - MARGIN.TOP - MARGIN.BOTTOM, - singleSpectrogramCanvasHeight: getSpectrogramCanvasHeight(canvasHeight - MARGIN.TOP - MARGIN.BOTTOM), - singleSpectrogramHeight: (canvasHeight - MARGIN.BOTTOM - MARGIN.TOP - PADDING) / NB_SPECTROGRAM, - }; - + const dimensions = getDimensions(parentDiv); const channelNames = _.filter(_.keys(data), (keyName) => !_.includes([FREQUENCY_KEY, HYPNOGRAM_KEY], keyName)); const scalesAndAxesBySpectrogram = _.map(channelNames, (name) => getScalesAndAxes(data, name, dimensions)); From ab145487b668fe70224221d5792380ea6c3510dc Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Mon, 7 Sep 2020 18:12:55 -0400 Subject: [PATCH 03/10] changed x axis of spectro to scaleTime --- .vscode/.prettierrc.yaml | 4 +- web/src/d3/evolving_chart/preproc.js | 37 +---- web/src/d3/spectrogram/spectrogram.js | 215 +++++++++++++++++++------- web/src/d3/utils.js | 45 +++++- 4 files changed, 205 insertions(+), 96 deletions(-) diff --git a/.vscode/.prettierrc.yaml b/.vscode/.prettierrc.yaml index a51674bd..f51d99fc 100644 --- a/.vscode/.prettierrc.yaml +++ b/.vscode/.prettierrc.yaml @@ -1,5 +1,5 @@ -trailingComma: "all" +trailingComma: 'all' tabWidth: 2 semi: true singleQuote: true -printWidth: 150 +printWidth: 80 diff --git a/web/src/d3/evolving_chart/preproc.js b/web/src/d3/evolving_chart/preproc.js index 3558f9d8..01987222 100644 --- a/web/src/d3/evolving_chart/preproc.js +++ b/web/src/d3/evolving_chart/preproc.js @@ -1,7 +1,7 @@ import _ from 'lodash'; -import { convertTimestampsToDates } from '../utils'; -import { STAGES_ORDERED, EPOCH_DURATION_SEC } from '../constants'; +import { convertTimestampsToDates, convertEpochsToAnnotations } from '../utils'; +import { STAGES_ORDERED } from '../constants'; export const preprocessData = (data) => { data = convertTimestampsToDates(data); @@ -17,39 +17,6 @@ export const preprocessData = (data) => { }; }; -const convertEpochsToAnnotations = (data) => { - const annotations = []; - const nbEpochs = data.length; - let currentAnnotationStart = data[0].timestamp; - let currentSleepStage = data[0].sleepStage; - let currentAnnotationEpochCount = 0; - - const isNextAnnotation = (sleepStage, index) => sleepStage !== currentSleepStage || index === data.length - 1; - - const saveCurrentAnnotation = (timestamp) => { - annotations.push({ - stage: currentSleepStage, - proportion: currentAnnotationEpochCount / nbEpochs, - start: currentAnnotationStart, - end: timestamp, - duration: currentAnnotationEpochCount * EPOCH_DURATION_SEC, - }); - }; - - data.forEach(({ timestamp, sleepStage }, index) => { - currentAnnotationEpochCount++; - - if (isNextAnnotation(sleepStage, index)) { - saveCurrentAnnotation(timestamp); - currentAnnotationStart = timestamp; - currentSleepStage = sleepStage; - currentAnnotationEpochCount = 0; - } - }); - - return annotations; -}; - const getStageTimeProportions = (data) => { const nbEpochPerSleepStage = _.countBy(data.map((x) => x.sleepStage)); const proportionPerSleepStage = _.mapValues(nbEpochPerSleepStage, (countPerStage) => countPerStage / data.length); diff --git a/web/src/d3/spectrogram/spectrogram.js b/web/src/d3/spectrogram/spectrogram.js index e3262e37..1b933963 100644 --- a/web/src/d3/spectrogram/spectrogram.js +++ b/web/src/d3/spectrogram/spectrogram.js @@ -11,12 +11,15 @@ import { PADDING, CANVAS_HEIGHT_WINDOW_FACTOR, } from './constants'; -import { EPOCH_DURATION_SEC } from '../constants'; import { createLegend } from './legend'; +import { convertTimestampsToDates } from '../utils'; const getDimensions = (parentDiv) => { const canvasWidth = parentDiv.node().getBoundingClientRect().width; - const canvasHeight = Math.min(canvasWidth * CANVAS_WIDTH_TO_HEIGHT_RATIO, window.innerHeight * CANVAS_HEIGHT_WINDOW_FACTOR); + const canvasHeight = Math.min( + canvasWidth * CANVAS_WIDTH_TO_HEIGHT_RATIO, + window.innerHeight * CANVAS_HEIGHT_WINDOW_FACTOR, + ); const spectrogramsHeight = canvasHeight - MARGIN.TOP - MARGIN.BOTTOM; const singleSpectrogramCanvasHeight = _.range(NB_SPECTROGRAM).map((x) => { let height = spectrogramsHeight / NB_SPECTROGRAM; @@ -40,7 +43,7 @@ const getDimensions = (parentDiv) => { const initializeScales = ({ spectrogramWidth, singleSpectrogramHeight }) => Object({ - x: d3.scaleLinear([0, spectrogramWidth]), + x: d3.scaleTime([0, spectrogramWidth]), yLinear: d3.scaleLinear([singleSpectrogramHeight, 0]), yBand: d3.scaleBand([singleSpectrogramHeight, 0]), yColor: d3.scaleLinear([singleSpectrogramHeight, 0]), @@ -49,48 +52,78 @@ const initializeScales = ({ spectrogramWidth, singleSpectrogramHeight }) => const initializeAxes = (x, y) => Object({ - xAxis: d3.axisBottom(x).tickFormat((d) => `${d}h`), + xAxis: d3.axisBottom(x).tickFormat((d) => `${d.getHours()}h`), yAxis: d3.axisLeft(y).ticks(5, 's'), }); -const setDomainOnScales = (currentData, frequencies, preprocessedData, x, yBand, yLinear, color, yColor) => { - x.domain([0, getHoursFromIndex(currentData.length)]); +const setDomainOnScales = ( + { rectangles, frequencies }, + x, + yBand, + yLinear, + color, + yColor, +) => { + x.domain([_.first(rectangles).Timestamp, _.last(rectangles).Timestamp]); yBand.domain(frequencies); yLinear.domain([_.first(frequencies), _.last(frequencies)]); - color.domain(d3.extent(preprocessedData, ({ Intensity }) => Intensity)); - yColor.domain(d3.extent(preprocessedData, ({ Intensity }) => Intensity)); + color.domain(d3.extent(rectangles, ({ Intensity }) => Intensity)); + yColor.domain(d3.extent(rectangles, ({ Intensity }) => Intensity)); }; -const preprocessData = (powerAmplitudesByTimestamp, frequencies) => - _.flatMap(powerAmplitudesByTimestamp, (powerAmplitudeSingleTimestamp, index) => - _.map(_.zip(powerAmplitudeSingleTimestamp, frequencies), ([intensity, frequency]) => - Object({ - Intensity: intensity, - Frequency: frequency, - Timestamp: getHoursFromIndex(index), - }), - ), - ); +const preprocessData = (channel, data) => { + const powerAmplitudesByTimestamp = data[channel]; + const frequencies = data[FREQUENCY_KEY]; + const hypnogram = convertTimestampsToDates(data[HYPNOGRAM_KEY]); -const getHoursFromIndex = (idx) => (idx * EPOCH_DURATION_SEC) / 3600; + return { + channel, + frequencies, + rectangles: _.flatMap( + _.zip(powerAmplitudesByTimestamp, hypnogram), + ([powerAmplitudeSingleTimestamp, { sleepStage, timestamp }]) => + _.map( + _.zip(powerAmplitudeSingleTimestamp, frequencies), + ([intensity, frequency]) => + Object({ + Intensity: intensity, + Frequency: frequency, + Timestamp: timestamp, + }), + ), + ), + }; +}; const createDrawingGroups = (g, spectrogramWidth) => Object({ - spectrogramDrawingGroup: g.append('g').attr('transform', `translate(${MARGIN.LEFT}, ${MARGIN.TOP})`), - legendDrawingGroup: g.append('g').attr('transform', `translate(${MARGIN.LEFT + spectrogramWidth}, ${MARGIN.TOP})`), + spectrogramDrawingGroup: g + .append('g') + .attr('transform', `translate(${MARGIN.LEFT}, ${MARGIN.TOP})`), + legendDrawingGroup: g + .append('g') + .attr( + 'transform', + `translate(${MARGIN.LEFT + spectrogramWidth}, ${MARGIN.TOP})`, + ), }); -const getScalesAndAxes = (data, channel, dimensions) => { +const getScalesAndAxes = (data, dimensions) => { const { x, yLinear, yBand, yColor, color } = initializeScales(dimensions); const { xAxis, yAxis } = initializeAxes(x, yLinear); - const preprocessedData = preprocessData(data[channel], data.frequencies); - setDomainOnScales(data[channel], data.frequencies, preprocessedData, x, yBand, yLinear, color, yColor); + setDomainOnScales(data, x, yBand, yLinear, color, yColor); - return { data: preprocessedData, x, yBand, yColor, color, xAxis, yAxis }; + return { x, yBand, yColor, color, xAxis, yAxis }; }; -const createAxes = (g, xAxis, yAxis, singleSpectrogramHeight, spectrogramWidth) => { +const createAxes = ( + g, + xAxis, + yAxis, + singleSpectrogramHeight, + spectrogramWidth, +) => { g.append('text') .attr('class', 'x axis') .attr('y', singleSpectrogramHeight + MARGIN.BOTTOM) @@ -116,7 +149,11 @@ const createAxes = (g, xAxis, yAxis, singleSpectrogramHeight, spectrogramWidth) .selectAll('text') .style('font-size', TITLE_FONT_SIZE); - g.append('g').attr('class', 'y axis').call(yAxis).selectAll('text').style('font-size', TITLE_FONT_SIZE); + g.append('g') + .attr('class', 'y axis') + .call(yAxis) + .selectAll('text') + .style('font-size', TITLE_FONT_SIZE); }; const createTitle = (g, channelName, spectrogramWidth) => @@ -128,42 +165,82 @@ const createTitle = (g, channelName, spectrogramWidth) => .style('font-size', TITLE_FONT_SIZE) .text(`Spectrogram of channel ${channelName}`); -const createSpectrogramRectangles = (canvas, scalesAndAxesBySpectrogram, { singleSpectrogramCanvasHeight }) => { +const createSpectrogramRectangles = ( + canvas, + scalesAndAxesBySpectrogram, + data, + { singleSpectrogramCanvasHeight }, +) => { const context = canvas.node().getContext('2d'); - _.each(scalesAndAxesBySpectrogram, ({ x, yBand, color, data }, index) => { - context.resetTransform(); - context.translate(MARGIN.LEFT, MARGIN.TOP + index * singleSpectrogramCanvasHeight[index]); - - _.each(data, ({ Timestamp, Frequency, Intensity }) => { - context.beginPath(); - context.fillRect(x(Timestamp), yBand(Frequency), x(getHoursFromIndex(1)), yBand.bandwidth()); - context.fillStyle = color(Intensity); - context.fill(); - context.stroke(); - }); - }); + _.each( + _.zip(scalesAndAxesBySpectrogram, data), + ([{ x, yBand, color }, { rectangles, frequencies }], index) => { + const rectangleWidth = + x(rectangles[frequencies.length].Timestamp) - + x(rectangles[0].Timestamp); + + context.resetTransform(); + context.translate( + MARGIN.LEFT, + MARGIN.TOP + index * singleSpectrogramCanvasHeight[index], + ); + + _.each(rectangles, ({ Timestamp, Frequency, Intensity }) => { + context.beginPath(); + context.fillRect( + x(Timestamp), + yBand(Frequency), + rectangleWidth, + yBand.bandwidth(), + ); + context.fillStyle = color(Intensity); + context.fill(); + context.stroke(); + }); + }, + ); }; const createSpectrogramAxesAndLegend = ( svg, scalesAndAxesBySpectrogram, - channelNames, - { canvasWidth, spectrogramWidth, singleSpectrogramCanvasHeight, singleSpectrogramHeight }, + data, + { + canvasWidth, + spectrogramWidth, + singleSpectrogramCanvasHeight, + singleSpectrogramHeight, + }, ) => - _.forEach(_.zip(scalesAndAxesBySpectrogram, channelNames), ([{ xAxis, yAxis, color, yColor }, channel], index) => { - const currentSpectrogramDrawingGroup = svg - .append('g') - .attr('transform', `translate(0, ${index * singleSpectrogramCanvasHeight[index]})`) - .attr('width', canvasWidth) - .attr('height', singleSpectrogramCanvasHeight[index]); + _.forEach( + _.zip(scalesAndAxesBySpectrogram, data), + ([{ xAxis, yAxis, color, yColor }, { channel }], index) => { + const currentSpectrogramDrawingGroup = svg + .append('g') + .attr( + 'transform', + `translate(0, ${index * singleSpectrogramCanvasHeight[index]})`, + ) + .attr('width', canvasWidth) + .attr('height', singleSpectrogramCanvasHeight[index]); - const { spectrogramDrawingGroup, legendDrawingGroup } = createDrawingGroups(currentSpectrogramDrawingGroup, spectrogramWidth); + const { + spectrogramDrawingGroup, + legendDrawingGroup, + } = createDrawingGroups(currentSpectrogramDrawingGroup, spectrogramWidth); - createTitle(spectrogramDrawingGroup, channel, spectrogramWidth); - createAxes(spectrogramDrawingGroup, xAxis, yAxis, singleSpectrogramHeight, spectrogramWidth); - createLegend(legendDrawingGroup, color, yColor, singleSpectrogramHeight); - }); + createTitle(spectrogramDrawingGroup, channel, spectrogramWidth); + createAxes( + spectrogramDrawingGroup, + xAxis, + yAxis, + singleSpectrogramHeight, + spectrogramWidth, + ); + createLegend(legendDrawingGroup, color, yColor, singleSpectrogramHeight); + }, + ); const createSpectrogram = (containerNode, data) => { /* @@ -177,18 +254,40 @@ const createSpectrogram = (containerNode, data) => { */ const parentDiv = d3.select(containerNode); const dimensions = getDimensions(parentDiv); - const channelNames = _.filter(_.keys(data), (keyName) => !_.includes([FREQUENCY_KEY, HYPNOGRAM_KEY], keyName)); - const scalesAndAxesBySpectrogram = _.map(channelNames, (name) => getScalesAndAxes(data, name, dimensions)); const canvas = parentDiv .append('canvas') .attr('width', dimensions.canvasWidth) .attr('height', dimensions.canvasHeight) .style('position', 'absolute'); - const svg = parentDiv.append('svg').attr('width', dimensions.canvasWidth).attr('height', dimensions.canvasHeight); + const svg = parentDiv + .append('svg') + .attr('width', dimensions.canvasWidth) + .attr('height', dimensions.canvasHeight); - createSpectrogramRectangles(canvas, scalesAndAxesBySpectrogram, dimensions); - createSpectrogramAxesAndLegend(svg, scalesAndAxesBySpectrogram, channelNames, dimensions); + const channelNames = _.filter( + _.keys(data), + (keyName) => !_.includes([FREQUENCY_KEY, HYPNOGRAM_KEY], keyName), + ); + const preprocessedData = _.map(channelNames, (channel) => + preprocessData(channel, data), + ); + const scalesAndAxesBySpectrogram = _.map(preprocessedData, (data) => + getScalesAndAxes(data, dimensions), + ); + + createSpectrogramRectangles( + canvas, + scalesAndAxesBySpectrogram, + preprocessedData, + dimensions, + ); + createSpectrogramAxesAndLegend( + svg, + scalesAndAxesBySpectrogram, + preprocessedData, + dimensions, + ); }; export default createSpectrogram; diff --git a/web/src/d3/utils.js b/web/src/d3/utils.js index 0c131470..74101856 100644 --- a/web/src/d3/utils.js +++ b/web/src/d3/utils.js @@ -1,3 +1,5 @@ +import { EPOCH_DURATION_SEC } from './constants'; + export const convertTimestampsToDates = (data) => data.map((row) => Object({ @@ -6,8 +8,49 @@ export const convertTimestampsToDates = (data) => }), ); +export const convertEpochsToAnnotations = (data) => { + const annotations = []; + const nbEpochs = data.length; + let currentAnnotationStart = data[0].timestamp; + let currentSleepStage = data[0].sleepStage; + let currentAnnotationEpochCount = 0; + + const isNextAnnotation = (sleepStage, index) => + sleepStage !== currentSleepStage || index === data.length - 1; + + const saveCurrentAnnotation = (timestamp) => { + annotations.push({ + stage: currentSleepStage, + proportion: currentAnnotationEpochCount / nbEpochs, + start: currentAnnotationStart, + end: timestamp, + duration: currentAnnotationEpochCount * EPOCH_DURATION_SEC, + }); + }; + + data.forEach(({ timestamp, sleepStage }, index) => { + currentAnnotationEpochCount++; + + if (isNextAnnotation(sleepStage, index)) { + saveCurrentAnnotation(timestamp); + currentAnnotationStart = timestamp; + currentSleepStage = sleepStage; + currentAnnotationEpochCount = 0; + } + }); + + return annotations; +}; + const parseTimestampToDate = (timestamp) => { // To convert UNIX timestamp to JS Date, we have to convert number of seconds to milliseconds. const date = new Date(timestamp * 1000); - return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDay(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()); + return new Date( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDay(), + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), + ); }; From 2d8d488d773295d47ce9feca170fdb81bb544542 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Mon, 7 Sep 2020 18:30:02 -0400 Subject: [PATCH 04/10] extracted legend and axes to specific file --- web/src/d3/spectrogram/axes_legend.js | 156 ++++++++++++++++++++++ web/src/d3/spectrogram/legend.js | 53 -------- web/src/d3/spectrogram/spectrogram.js | 182 ++++++-------------------- 3 files changed, 196 insertions(+), 195 deletions(-) create mode 100644 web/src/d3/spectrogram/axes_legend.js delete mode 100644 web/src/d3/spectrogram/legend.js diff --git a/web/src/d3/spectrogram/axes_legend.js b/web/src/d3/spectrogram/axes_legend.js new file mode 100644 index 00000000..34db4a47 --- /dev/null +++ b/web/src/d3/spectrogram/axes_legend.js @@ -0,0 +1,156 @@ +import * as d3 from 'd3'; +import _ from 'lodash'; +import { + MARGIN, + NB_POINTS_COLOR_INTERPOLATION, + TITLE_FONT_SIZE, +} from './constants'; + +const createDrawingGroups = (g, spectrogramWidth) => + Object({ + spectrogramDrawingGroup: g + .append('g') + .attr('transform', `translate(${MARGIN.LEFT}, ${MARGIN.TOP})`), + legendDrawingGroup: g + .append('g') + .attr( + 'transform', + `translate(${MARGIN.LEFT + spectrogramWidth}, ${MARGIN.TOP})`, + ), + }); + +const drawTitle = (g, channelName, spectrogramWidth) => + g + .append('text') + .attr('x', spectrogramWidth / 2) + .attr('y', -MARGIN.TOP / 3) + .style('text-anchor', 'middle') + .style('font-size', TITLE_FONT_SIZE) + .text(`Spectrogram of channel ${channelName}`); + +const drawAxes = ( + g, + xAxis, + yAxis, + singleSpectrogramHeight, + spectrogramWidth, +) => { + g.append('text') + .attr('class', 'x axis') + .attr('y', singleSpectrogramHeight + MARGIN.BOTTOM) + .attr('x', spectrogramWidth / 2) + .attr('fill', 'currentColor') + .style('text-anchor', 'middle') + .text('Time'); + + g.append('text') + .attr('class', 'y axis') + .attr('transform', 'rotate(-90)') + .attr('y', -MARGIN.LEFT) + .attr('x', -singleSpectrogramHeight / 2) + .attr('dy', '1em') + .attr('fill', 'currentColor') + .style('text-anchor', 'middle') + .text('Frequency (Hz)'); + + g.append('g') + .attr('class', 'x axis') + .attr('transform', `translate(0, ${singleSpectrogramHeight})`) + .call(xAxis) + .selectAll('text'); + + g.append('g').attr('class', 'y axis').call(yAxis).selectAll('text'); +}; + +const drawLegend = (svg, color, y, spectrogramHeight) => { + const interpolate = d3.interpolate(color.domain()[0], color.domain()[1]); + + const colors = _.map(_.range(NB_POINTS_COLOR_INTERPOLATION + 1), (x) => + color(interpolate(x / NB_POINTS_COLOR_INTERPOLATION)), + ); + + const svgDefs = svg.append('defs'); + const GRADIENT_ID = 'mainGradient'; + + svgDefs + .append('linearGradient') + .attr('id', GRADIENT_ID) + .attr('x1', '0%') + .attr('x2', '0%') + .attr('y1', '100%') + .attr('y2', '0%') + .selectAll('stop') + .data(colors) + .enter() + .append('stop') + .attr('stop-color', (d) => d) + .attr('offset', (_, i) => i / (colors.length - 1)); + svg + .append('rect') + .attr('fill', `url(#${GRADIENT_ID})`) + .attr('x', MARGIN.RIGHT / 10) + .attr('y', 0) + .attr('width', MARGIN.RIGHT / 6) + .attr('height', spectrogramHeight); + + const yAxis = d3.axisRight(y).ticks(5, 's'); + svg + .append('g') + .attr('class', 'y axis') + .attr('transform', `translate(${MARGIN.RIGHT / 3.7},0)`) + .call(yAxis) + .selectAll('text'); + + svg + .append('text') + .attr('class', 'y axis') + .attr('transform', 'rotate(90)') + .attr('y', -MARGIN.RIGHT) + .attr('x', spectrogramHeight / 2) + .attr('dy', '1em') + .attr('fill', 'currentColor') + .style('text-anchor', 'middle') + .text('Power (uV²/Hz)'); +}; + +createSpectrogramAxesAndLegend = ( + svg, + scalesAndAxesBySpectrogram, + data, + { + canvasWidth, + spectrogramWidth, + singleSpectrogramCanvasHeight, + singleSpectrogramHeight, + }, +) => + _.forEach( + _.zip(scalesAndAxesBySpectrogram, data), + ([{ xAxis, yAxis, color, yColor }, { channel }], index) => { + const currentSpectrogramDrawingGroup = svg + .append('g') + .attr( + 'transform', + `translate(0, ${index * singleSpectrogramCanvasHeight[index]})`, + ) + .attr('width', canvasWidth) + .attr('height', singleSpectrogramCanvasHeight[index]); + + const { + spectrogramDrawingGroup, + legendDrawingGroup, + } = createDrawingGroups(currentSpectrogramDrawingGroup, spectrogramWidth); + + drawTitle(spectrogramDrawingGroup, channel, spectrogramWidth); + drawAxes( + spectrogramDrawingGroup, + xAxis, + yAxis, + singleSpectrogramHeight, + spectrogramWidth, + ); + drawLegend(legendDrawingGroup, color, yColor, singleSpectrogramHeight); + }, + ); + +export default drawSpectrogramAxesAndLegend; diff --git a/web/src/d3/spectrogram/legend.js b/web/src/d3/spectrogram/legend.js deleted file mode 100644 index f8a7ad5c..00000000 --- a/web/src/d3/spectrogram/legend.js +++ /dev/null @@ -1,53 +0,0 @@ -import * as d3 from 'd3'; -import _ from 'lodash'; -import { MARGIN, NB_POINTS_COLOR_INTERPOLATION, TITLE_FONT_SIZE } from './constants'; - -export const createLegend = (svg, color, y, spectrogramHeight) => { - const interpolate = d3.interpolate(color.domain()[0], color.domain()[1]); - - const colors = _.map(_.range(NB_POINTS_COLOR_INTERPOLATION + 1), (x) => color(interpolate(x / NB_POINTS_COLOR_INTERPOLATION))); - - const svgDefs = svg.append('defs'); - const GRADIENT_ID = 'mainGradient'; - - svgDefs - .append('linearGradient') - .attr('id', GRADIENT_ID) - .attr('x1', '0%') - .attr('x2', '0%') - .attr('y1', '100%') - .attr('y2', '0%') - .selectAll('stop') - .data(colors) - .enter() - .append('stop') - .attr('stop-color', (d) => d) - .attr('offset', (_, i) => i / (colors.length - 1)); - svg - .append('rect') - .attr('fill', `url(#${GRADIENT_ID})`) - .attr('x', MARGIN.RIGHT / 10) - .attr('y', 0) - .attr('width', MARGIN.RIGHT / 6) - .attr('height', spectrogramHeight); - - const yAxis = d3.axisRight(y).ticks(5, 's'); - svg - .append('g') - .attr('class', 'y axis') - .attr('transform', `translate(${MARGIN.RIGHT / 3.7},0)`) - .call(yAxis) - .selectAll('text'); - - svg - .append('text') - .attr('class', 'y axis') - .attr('transform', 'rotate(90)') - .attr('y', -MARGIN.RIGHT) - .attr('x', spectrogramHeight / 2) - .attr('dy', '1em') - .attr('fill', 'currentColor') - .style('text-anchor', 'middle') - .style('font-size', TITLE_FONT_SIZE) - .text('Power (uV²/Hz)'); -}; diff --git a/web/src/d3/spectrogram/spectrogram.js b/web/src/d3/spectrogram/spectrogram.js index 1b933963..d538e134 100644 --- a/web/src/d3/spectrogram/spectrogram.js +++ b/web/src/d3/spectrogram/spectrogram.js @@ -6,12 +6,11 @@ import { CANVAS_WIDTH_TO_HEIGHT_RATIO, FREQUENCY_KEY, HYPNOGRAM_KEY, - TITLE_FONT_SIZE, NB_SPECTROGRAM, PADDING, CANVAS_HEIGHT_WINDOW_FACTOR, } from './constants'; -import { createLegend } from './legend'; +import drawSpectrogramAxesAndLegend from './axes_legend'; import { convertTimestampsToDates } from '../utils'; const getDimensions = (parentDiv) => { @@ -41,36 +40,6 @@ const getDimensions = (parentDiv) => { }; }; -const initializeScales = ({ spectrogramWidth, singleSpectrogramHeight }) => - Object({ - x: d3.scaleTime([0, spectrogramWidth]), - yLinear: d3.scaleLinear([singleSpectrogramHeight, 0]), - yBand: d3.scaleBand([singleSpectrogramHeight, 0]), - yColor: d3.scaleLinear([singleSpectrogramHeight, 0]), - color: d3.scaleSequential().interpolator(d3.interpolatePlasma), - }); - -const initializeAxes = (x, y) => - Object({ - xAxis: d3.axisBottom(x).tickFormat((d) => `${d.getHours()}h`), - yAxis: d3.axisLeft(y).ticks(5, 's'), - }); - -const setDomainOnScales = ( - { rectangles, frequencies }, - x, - yBand, - yLinear, - color, - yColor, -) => { - x.domain([_.first(rectangles).Timestamp, _.last(rectangles).Timestamp]); - yBand.domain(frequencies); - yLinear.domain([_.first(frequencies), _.last(frequencies)]); - color.domain(d3.extent(rectangles, ({ Intensity }) => Intensity)); - yColor.domain(d3.extent(rectangles, ({ Intensity }) => Intensity)); -}; - const preprocessData = (channel, data) => { const powerAmplitudesByTimestamp = data[channel]; const frequencies = data[FREQUENCY_KEY]; @@ -86,26 +55,43 @@ const preprocessData = (channel, data) => { _.zip(powerAmplitudeSingleTimestamp, frequencies), ([intensity, frequency]) => Object({ - Intensity: intensity, - Frequency: frequency, - Timestamp: timestamp, + intensity, + frequency, + timestamp, }), ), ), }; }; -const createDrawingGroups = (g, spectrogramWidth) => +const initializeScales = ({ spectrogramWidth, singleSpectrogramHeight }) => Object({ - spectrogramDrawingGroup: g - .append('g') - .attr('transform', `translate(${MARGIN.LEFT}, ${MARGIN.TOP})`), - legendDrawingGroup: g - .append('g') - .attr( - 'transform', - `translate(${MARGIN.LEFT + spectrogramWidth}, ${MARGIN.TOP})`, - ), + x: d3.scaleTime([0, spectrogramWidth]), + yLinear: d3.scaleLinear([singleSpectrogramHeight, 0]), + yBand: d3.scaleBand([singleSpectrogramHeight, 0]), + yColor: d3.scaleLinear([singleSpectrogramHeight, 0]), + color: d3.scaleSequential().interpolator(d3.interpolatePlasma), + }); + +const setDomainOnScales = ( + { rectangles, frequencies }, + x, + yBand, + yLinear, + color, + yColor, +) => { + x.domain([_.first(rectangles).timestamp, _.last(rectangles).timestamp]); + yBand.domain(frequencies); + yLinear.domain([_.first(frequencies), _.last(frequencies)]); + color.domain(d3.extent(rectangles, ({ intensity }) => intensity)); + yColor.domain(d3.extent(rectangles, ({ intensity }) => intensity)); +}; + +const initializeAxes = (x, y) => + Object({ + xAxis: d3.axisBottom(x).tickFormat((d) => `${d.getHours()}h`), + yAxis: d3.axisLeft(y).ticks(5, 's'), }); const getScalesAndAxes = (data, dimensions) => { @@ -117,55 +103,7 @@ const getScalesAndAxes = (data, dimensions) => { return { x, yBand, yColor, color, xAxis, yAxis }; }; -const createAxes = ( - g, - xAxis, - yAxis, - singleSpectrogramHeight, - spectrogramWidth, -) => { - g.append('text') - .attr('class', 'x axis') - .attr('y', singleSpectrogramHeight + MARGIN.BOTTOM) - .attr('x', spectrogramWidth / 2) - .attr('fill', 'currentColor') - .style('text-anchor', 'middle') - .text('Time'); - - g.append('text') - .attr('class', 'y axis') - .attr('transform', 'rotate(-90)') - .attr('y', -MARGIN.LEFT) - .attr('x', -singleSpectrogramHeight / 2) - .attr('dy', '1em') - .attr('fill', 'currentColor') - .style('text-anchor', 'middle') - .text('Frequency (Hz)'); - - g.append('g') - .attr('class', 'x axis') - .attr('transform', `translate(0, ${singleSpectrogramHeight})`) - .call(xAxis) - .selectAll('text') - .style('font-size', TITLE_FONT_SIZE); - - g.append('g') - .attr('class', 'y axis') - .call(yAxis) - .selectAll('text') - .style('font-size', TITLE_FONT_SIZE); -}; - -const createTitle = (g, channelName, spectrogramWidth) => - g - .append('text') - .attr('x', spectrogramWidth / 2) - .attr('y', -MARGIN.TOP / 3) - .style('text-anchor', 'middle') - .style('font-size', TITLE_FONT_SIZE) - .text(`Spectrogram of channel ${channelName}`); - -const createSpectrogramRectangles = ( +const drawSpectrogramRectangles = ( canvas, scalesAndAxesBySpectrogram, data, @@ -177,8 +115,8 @@ const createSpectrogramRectangles = ( _.zip(scalesAndAxesBySpectrogram, data), ([{ x, yBand, color }, { rectangles, frequencies }], index) => { const rectangleWidth = - x(rectangles[frequencies.length].Timestamp) - - x(rectangles[0].Timestamp); + x(rectangles[frequencies.length].timestamp) - + x(rectangles[0].timestamp); context.resetTransform(); context.translate( @@ -186,15 +124,15 @@ const createSpectrogramRectangles = ( MARGIN.TOP + index * singleSpectrogramCanvasHeight[index], ); - _.each(rectangles, ({ Timestamp, Frequency, Intensity }) => { + _.each(rectangles, ({ timestamp, frequency, intensity }) => { context.beginPath(); context.fillRect( - x(Timestamp), - yBand(Frequency), + x(timestamp), + yBand(frequency), rectangleWidth, yBand.bandwidth(), ); - context.fillStyle = color(Intensity); + context.fillStyle = color(intensity); context.fill(); context.stroke(); }); @@ -202,46 +140,6 @@ const createSpectrogramRectangles = ( ); }; -const createSpectrogramAxesAndLegend = ( - svg, - scalesAndAxesBySpectrogram, - data, - { - canvasWidth, - spectrogramWidth, - singleSpectrogramCanvasHeight, - singleSpectrogramHeight, - }, -) => - _.forEach( - _.zip(scalesAndAxesBySpectrogram, data), - ([{ xAxis, yAxis, color, yColor }, { channel }], index) => { - const currentSpectrogramDrawingGroup = svg - .append('g') - .attr( - 'transform', - `translate(0, ${index * singleSpectrogramCanvasHeight[index]})`, - ) - .attr('width', canvasWidth) - .attr('height', singleSpectrogramCanvasHeight[index]); - - const { - spectrogramDrawingGroup, - legendDrawingGroup, - } = createDrawingGroups(currentSpectrogramDrawingGroup, spectrogramWidth); - - createTitle(spectrogramDrawingGroup, channel, spectrogramWidth); - createAxes( - spectrogramDrawingGroup, - xAxis, - yAxis, - singleSpectrogramHeight, - spectrogramWidth, - ); - createLegend(legendDrawingGroup, color, yColor, singleSpectrogramHeight); - }, - ); - const createSpectrogram = (containerNode, data) => { /* Considering the number of rectangles to display is well over 1k, @@ -276,13 +174,13 @@ const createSpectrogram = (containerNode, data) => { getScalesAndAxes(data, dimensions), ); - createSpectrogramRectangles( + drawSpectrogramRectangles( canvas, scalesAndAxesBySpectrogram, preprocessedData, dimensions, ); - createSpectrogramAxesAndLegend( + drawSpectrogramAxesAndLegend( svg, scalesAndAxesBySpectrogram, preprocessedData, From fa9b9689fb1e59a4b1b2ce85b15f91990f1448b9 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Mon, 7 Sep 2020 18:30:54 -0400 Subject: [PATCH 05/10] error --- web/src/d3/spectrogram/axes_legend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/d3/spectrogram/axes_legend.js b/web/src/d3/spectrogram/axes_legend.js index 34db4a47..b90541ae 100644 --- a/web/src/d3/spectrogram/axes_legend.js +++ b/web/src/d3/spectrogram/axes_legend.js @@ -113,7 +113,7 @@ const drawLegend = (svg, color, y, spectrogramHeight) => { .text('Power (uV²/Hz)'); }; -createSpectrogramAxesAndLegend = ( +const drawSpectrogramAxesAndLegend = ( svg, scalesAndAxesBySpectrogram, data, From 5dee8ad545715653976f586f3cb9705370d5eb60 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Tue, 15 Sep 2020 23:41:12 -0400 Subject: [PATCH 06/10] added spectrogram callbacks for stage opacity --- web/src/d3/spectrogram/constants.js | 1 + web/src/d3/spectrogram/spectrogram.js | 49 ++++++++++++++++++++------- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/web/src/d3/spectrogram/constants.js b/web/src/d3/spectrogram/constants.js index cfa4392e..1b6f5005 100644 --- a/web/src/d3/spectrogram/constants.js +++ b/web/src/d3/spectrogram/constants.js @@ -4,6 +4,7 @@ export const FREQUENCY_KEY = 'frequencies'; export const HYPNOGRAM_KEY = 'hypnogram'; export const NB_POINTS_COLOR_INTERPOLATION = 3; export const TITLE_FONT_SIZE = '18px'; +export const NOT_HIGHLIGHTED_RECTANGLE_OPACITY = 0.5; export const CANVAS_WIDTH_TO_HEIGHT_RATIO = 700 / 1000; // width to height ratio export const CANVAS_HEIGHT_WINDOW_FACTOR = 0.8; export const MARGIN = { diff --git a/web/src/d3/spectrogram/spectrogram.js b/web/src/d3/spectrogram/spectrogram.js index d538e134..d941aa6c 100644 --- a/web/src/d3/spectrogram/spectrogram.js +++ b/web/src/d3/spectrogram/spectrogram.js @@ -9,10 +9,15 @@ import { NB_SPECTROGRAM, PADDING, CANVAS_HEIGHT_WINDOW_FACTOR, + NOT_HIGHLIGHTED_RECTANGLE_OPACITY, } from './constants'; +import { STAGES_ORDERED } from '../constants'; import drawSpectrogramAxesAndLegend from './axes_legend'; import { convertTimestampsToDates } from '../utils'; +// keys are the sleep stage for which we want to display the spectrogram +export let spectrogramCallbacks = {}; + const getDimensions = (parentDiv) => { const canvasWidth = parentDiv.node().getBoundingClientRect().width; const canvasHeight = Math.min( @@ -58,6 +63,7 @@ const preprocessData = (channel, data) => { intensity, frequency, timestamp, + sleepStage, }), ), ), @@ -108,8 +114,11 @@ const drawSpectrogramRectangles = ( scalesAndAxesBySpectrogram, data, { singleSpectrogramCanvasHeight }, + highlightedSleepStage, ) => { const context = canvas.node().getContext('2d'); + const isHighlightNotSelectedOrEqual = (sleepStage) => + !highlightedSleepStage || sleepStage == highlightedSleepStage; _.each( _.zip(scalesAndAxesBySpectrogram, data), @@ -124,7 +133,7 @@ const drawSpectrogramRectangles = ( MARGIN.TOP + index * singleSpectrogramCanvasHeight[index], ); - _.each(rectangles, ({ timestamp, frequency, intensity }) => { + _.each(rectangles, ({ timestamp, frequency, intensity, sleepStage }) => { context.beginPath(); context.fillRect( x(timestamp), @@ -132,6 +141,9 @@ const drawSpectrogramRectangles = ( rectangleWidth, yBand.bandwidth(), ); + context.globalAlpha = isHighlightNotSelectedOrEqual(sleepStage) + ? 1 + : NOT_HIGHLIGHTED_RECTANGLE_OPACITY; context.fillStyle = color(intensity); context.fill(); context.stroke(); @@ -174,18 +186,31 @@ const createSpectrogram = (containerNode, data) => { getScalesAndAxes(data, dimensions), ); - drawSpectrogramRectangles( - canvas, - scalesAndAxesBySpectrogram, - preprocessedData, - dimensions, - ); - drawSpectrogramAxesAndLegend( - svg, - scalesAndAxesBySpectrogram, - preprocessedData, - dimensions, + const createSpectrogramWithHighlightedStageCallback = ( + highlightedSleepStage, + ) => () => { + drawSpectrogramRectangles( + canvas, + scalesAndAxesBySpectrogram, + preprocessedData, + dimensions, + highlightedSleepStage, + ); + drawSpectrogramAxesAndLegend( + svg, + scalesAndAxesBySpectrogram, + preprocessedData, + dimensions, + ); + }; + + spectrogramCallbacks = _.zipObject( + [null, ...STAGES_ORDERED], + _.map([null, ...STAGES_ORDERED], (stage) => + createSpectrogramWithHighlightedStageCallback(stage), + ), ); + spectrogramCallbacks[null](); }; export default createSpectrogram; From a7149fea558866e8a9cc102fbde1ef27be719bb0 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Wed, 16 Sep 2020 00:07:25 -0400 Subject: [PATCH 07/10] extracted component to encapsulate d3 component that must not be refreshed --- web/src/components/d3component.js | 1 + .../components/d3component_scrollytelling.js | 31 ++++ web/src/d3/spectrogram/spectrogram.js | 2 +- .../views/sleep_analysis/sleep_analysis.js | 141 ++++++++++++------ .../spectrogram_scrollytelling.js | 29 ++++ .../stacked_bar_chart_scrollytelling.js | 117 ++++++++++----- 6 files changed, 236 insertions(+), 85 deletions(-) create mode 100644 web/src/components/d3component_scrollytelling.js create mode 100644 web/src/views/sleep_analysis/spectrogram_scrollytelling.js diff --git a/web/src/components/d3component.js b/web/src/components/d3component.js index 133881ec..560fc173 100644 --- a/web/src/components/d3component.js +++ b/web/src/components/d3component.js @@ -21,6 +21,7 @@ const D3Component = React.memo(({ callback, data, useDiv = false }) => { D3Component.propTypes = { callback: PropTypes.func.isRequired, data: PropTypes.any, + useDiv: PropTypes.bool, }; export default D3Component; diff --git a/web/src/components/d3component_scrollytelling.js b/web/src/components/d3component_scrollytelling.js new file mode 100644 index 00000000..79da1b2f --- /dev/null +++ b/web/src/components/d3component_scrollytelling.js @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import D3Component from './d3component'; + +const D3ComponentScrollyTelling = ({ + callback, + data, + isInitialized, + setIsInitialized, + useDiv = false, +}) => { + const createCallback = (svg, data) => { + if (!isInitialized) { + setIsInitialized(true); + console.log(callback); + callback(svg, data); + } + }; + + return ; +}; + +D3Component.propTypes = { + callback: PropTypes.func.isRequired, + isInitialized: PropTypes.bool.isRequired, + setIsInitialized: PropTypes.func.isRequired, + data: PropTypes.any, + useDiv: PropTypes.bool, +}; + +export default D3ComponentScrollyTelling; diff --git a/web/src/d3/spectrogram/spectrogram.js b/web/src/d3/spectrogram/spectrogram.js index d941aa6c..3a631200 100644 --- a/web/src/d3/spectrogram/spectrogram.js +++ b/web/src/d3/spectrogram/spectrogram.js @@ -118,7 +118,7 @@ const drawSpectrogramRectangles = ( ) => { const context = canvas.node().getContext('2d'); const isHighlightNotSelectedOrEqual = (sleepStage) => - !highlightedSleepStage || sleepStage == highlightedSleepStage; + !highlightedSleepStage || sleepStage === highlightedSleepStage; _.each( _.zip(scalesAndAxesBySpectrogram, data), diff --git a/web/src/views/sleep_analysis/sleep_analysis.js b/web/src/views/sleep_analysis/sleep_analysis.js index 75069a9e..aa937158 100644 --- a/web/src/views/sleep_analysis/sleep_analysis.js +++ b/web/src/views/sleep_analysis/sleep_analysis.js @@ -9,19 +9,16 @@ import D3Component from '../../components/d3component'; import WIPWarning from '../../components/wip_warning'; import { createSingleHypnogram } from '../../d3/hypnogram/hypnogram'; -import { HYPNOGRAM_KEY } from '../../d3/spectrogram/constants'; import text from './text.json'; -import createSpectrogram from '../../d3/spectrogram/spectrogram'; import StackedBarChartScrollyTelling from './stacked_bar_chart_scrollytelling'; +import SpectrogramScrollyTelling from './spectrogram_scrollytelling'; import { useCSVData } from '../../hooks/api_hooks'; import hypnogramDataSleepEDFPath from 'assets/data/hypnogram.csv'; -import spectrogramData from 'assets/data/spectrograms.json'; const SleepAnalysis = () => { const csvDataSleepEDF = useCSVData(hypnogramDataSleepEDFPath); - const spectrogramWithHypnogramData = csvDataSleepEDF ? { ...spectrogramData, [HYPNOGRAM_KEY]: csvDataSleepEDF } : null; return (

@@ -38,87 +35,137 @@ const SleepAnalysis = () => {

- Of course, we are analyzing only one night of sleep so it is therefore tricky to draw general conclusions about your sleep. It is however + Of course, we are analyzing only one night of sleep so it is therefore + tricky to draw general conclusions about your sleep. It is however fascinating to see how your night was.

Without further ado, this is what was your night of sleep:

- We have seen that sleep can be decomposed in mainly two stages, whereas REM and NREM, and that we can observe different stage proportions - across age, gender and different sleep disorders. We’ve also defined other measures of your sleep architecture, such as your sleep latency, - efficiency and total sleep time. In order to improve your sleep hygiene, many elements can be considered: + We have seen that sleep can be decomposed in mainly two stages, + whereas REM and NREM, and that we can observe different stage + proportions across age, gender and different sleep disorders. We’ve + also defined other measures of your sleep architecture, such as your + sleep latency, efficiency and total sleep time. In order to improve + your sleep hygiene, many elements can be considered:

  • - Alimentation: having a balanced diet and avoiding sources of caffeine can have a positive impact on one’s sleep. Chocolate, soft drink, - tea and decaffeinated coffee are unexpected sources of caffeine. + Alimentation: having a balanced diet and avoiding sources of + caffeine can have a positive impact on one’s sleep. Chocolate, soft + drink, tea and decaffeinated coffee are unexpected sources of + caffeine. +
  • +
  • + Routine: going to sleep about at the same time, in a darkened and + quiet environment. +
  • +
  • + Routine: going to sleep about at the same time, in a darkened and + quiet environment. +
  • +
  • + Routine: going to sleep about at the same time, in a darkened and + quiet environment.
  • -
  • Routine: going to sleep about at the same time, in a darkened and quiet environment.
  • -
  • Routine: going to sleep about at the same time, in a darkened and quiet environment.
  • -
  • Routine: going to sleep about at the same time, in a darkened and quiet environment.

- Although we’ve looked at many aspects of your night’s sleep, we haven’t properly looked at your sleep dynamics, whereas how your sleep + Although we’ve looked at many aspects of your night’s sleep, we + haven’t properly looked at your sleep dynamics, whereas how your sleep evolves overnight.

Hypnogram

- A hypnogram allows you to visually inspect the evolution of your night, through time. The vertical axis represents how hard it is to wake - up, namely the sleep deepness. We see that REM is one of the lightest sleep stages (along with N1), because we unknowingly wake up from that - stage. Those short periods of arousal often last no longer than 15 seconds, are followed by a lighter sleep stage, and cannot be remembered - the next morning. If they are too frequent, they can affect your sleep quality. [5] We can see that, throughout the night, stages follow - about the same pattern, whereas we go from NREM (either N1, N2 and N3) and then to REM, and so on. We call those sleep cycles, and those - typically range from four to six, each one lasting from 90 to 110 minutes. Another commonly looked at measurement is the time between sleep - onset and the first REM epoch, namely REM latency, which corresponds to 20 minutes. + A hypnogram allows you to visually inspect the evolution of your + night, through time. The vertical axis represents how hard it is to + wake up, namely the sleep deepness. We see that REM is one of the + lightest sleep stages (along with N1), because we unknowingly wake up + from that stage. Those short periods of arousal often last no longer + than 15 seconds, are followed by a lighter sleep stage, and cannot be + remembered the next morning. If they are too frequent, they can affect + your sleep quality. [5] We can see that, throughout the night, stages + follow about the same pattern, whereas we go from NREM (either N1, N2 + and N3) and then to REM, and so on. We call those sleep cycles, and + those typically range from four to six, each one lasting from 90 to + 110 minutes. Another commonly looked at measurement is the time + between sleep onset and the first REM epoch, namely REM latency, which + corresponds to 20 minutes.

- +

- Sleep cycles take place in a broader process, named the circadian rhythm. It is the one that regulates our wake and sleep cycles over a 24 - hours period. + Sleep cycles take place in a broader process, named the circadian + rhythm. It is the one that regulates our wake and sleep cycles over a + 24 hours period.

- You’ve been able to visualize and inspect your night of sleep, which we’ve classified only based on your EEG recordings. In a sleep lab, - electrophysiology technicians generally look at your EEG, EOG and submental EMG, and then manually classify each epoch of 30 seconds that - compose your night. By looking at your EEG recordings, we can see some patterns that can help electrophysiology technicians, and our + You’ve been able to visualize and inspect your night of sleep, which + we’ve classified only based on your EEG recordings. In a sleep lab, + electrophysiology technicians generally look at your EEG, EOG and + submental EMG, and then manually classify each epoch of 30 seconds + that compose your night. By looking at your EEG recordings, we can see + some patterns that can help electrophysiology technicians, and our classifier, discriminate sleep stages throughout the night.

Spectrogram

- Above, we can see the same chart from the first visualization, which represents your sleep stages through the night. Below it, there are - spectrograms of both your EEG channels. Spectrograms can be viewed as if we took all of your nights signal, we’ve separated it in contiguous - 30 seconds chunks, stacked then horizontally and to which we’ve applied the fast fourier transform. We then have, for each 30 seconds epoch, - the corresponding amplitudes for each frequency that makes up the signal, hence the spectra. We then converted the scale to logarithmic, to - better see the differences in the spectrums. We then speak of signal power instead of signal amplitude, because we look at the spectrums in - a logarithmic scale. + Above, we can see the same chart from the first visualization, which + represents your sleep stages through the night. Below it, there are + spectrograms of both your EEG channels. Spectrograms can be viewed as + if we took all of your nights signal, we’ve separated it in contiguous + 30 seconds chunks, stacked then horizontally and to which we’ve + applied the fast fourier transform. We then have, for each 30 seconds + epoch, the corresponding amplitudes for each frequency that makes up + the signal, hence the spectra. We then converted the scale to + logarithmic, to better see the differences in the spectrums. We then + speak of signal power instead of signal amplitude, because we look at + the spectrums in a logarithmic scale.

How to read it?

- Red therefore means that in that 30 seconds time frame, that particular frequency had a big amplitude. Green means that you had that - frequency with a lower amplitude. Dark blue means that you didn’t have that frequency in the signal. + Red therefore means that in that 30 seconds time frame, that + particular frequency had a big amplitude. Green means that you had + that frequency with a lower amplitude. Dark blue means that you didn’t + have that frequency in the signal.

- To get a better understanding at how spectrograms work, you can check out - + To get a better understanding at how spectrograms work, you can check + out + {' '} this visualization{' '} that decomposes sound frequency from your microphone.

- +

- Generally, when talking about brain waves, we group certain frequencies together into bands. There are overall five frequency bands, where - each has a general associated behaviour, or state of mind. We will cover those when looking at time frames corresponding to each sleep - stage. + Generally, when talking about brain waves, we group certain + frequencies together into bands. There are overall five frequency + bands, where each has a general associated behaviour, or state of + mind. We will cover those when looking at time frames corresponding to + each sleep stage.

- We can associate wake stages with low-amplitude activity in the 15 to 60 Hz frequency range, called the beta band. By slowly falling asleep, - the signal frequencies tend to decrease into the 4 to 8 Hz range, or the theta band, and to have larger amplitudes. These characteristics - are associated with N1. N2 stage has the same characteristics, and also includes sleep spindles. They last only a few seconds and are a - large oscillation in the 10 to 15 hz band. Because they do not occur during all of the 30 seconds period, they cannot be seen here. Stage - N3, also called slow wave sleep, is characterized by slower waves between 0.5 and 4 Hz, known as the delta range, with large amplitudes. REM - stage has the same characteristics as Wake stage, whereas there are low voltage high frequency activity. + We can associate wake stages with low-amplitude activity in the 15 to + 60 Hz frequency range, called the beta band. By slowly falling asleep, + the signal frequencies tend to decrease into the 4 to 8 Hz range, or + the theta band, and to have larger amplitudes. These characteristics + are associated with N1. N2 stage has the same characteristics, and + also includes sleep spindles. They last only a few seconds and are a + large oscillation in the 10 to 15 hz band. Because they do not occur + during all of the 30 seconds period, they cannot be seen here. Stage + N3, also called slow wave sleep, is characterized by slower waves + between 0.5 and 4 Hz, known as the delta range, with large amplitudes. + REM stage has the same characteristics as Wake stage, whereas there + are low voltage high frequency activity.

Wanna know how accurate this data is?