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

Feature/spectrogram scrolly #17

Merged
merged 11 commits into from
Sep 21, 2020
4 changes: 2 additions & 2 deletions .vscode/.prettierrc.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
trailingComma: "all"
trailingComma: 'all'
tabWidth: 2
semi: true
singleQuote: true
printWidth: 150
printWidth: 80
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bonne idée, cela dépasse de l'écran autrement.

3 changes: 3 additions & 0 deletions web/src/components/d3component.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const D3Component = React.memo(({ callback, data, useDiv = false }) => {
D3Component.propTypes = {
callback: PropTypes.func.isRequired,
data: PropTypes.any,
// Using a div node instead of a svg node allows usage of child components of other types:
// i.g., for performance issues, we used both canvas & svg child elements in a visualisation.
useDiv: PropTypes.bool,
conorato marked this conversation as resolved.
Show resolved Hide resolved
};

export default D3Component;
32 changes: 32 additions & 0 deletions web/src/components/d3component_scrollytelling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import D3Component from './d3component';

const D3ComponentScrollyTelling = ({
callback,
data,
isInitialized,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isLoaded et setIsLoaded?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

En fait, ces variables sont plutôt utilisées pour définir si la visualisation a été initialisée ou non. C'est donc utilisé plus tard quand on va appeller les callbacks (ex. spectrogramCallbacks['W']) quand on va scroller à travers un waypoint.

Le nom est isInitialized est alors, selon moi, plus appropriée, comme on ne load pas de données dans ce cas (désolé, c'est moi qui était confuse tout à l'heure en expliquant pourquoi on faisait ça).

setIsInitialized,
useDiv = false,
}) => {
const createCallback = (svg, data) => {
if (!isInitialized) {
setIsInitialized(true);
callback(svg, data);
}
};

return <D3Component callback={createCallback} data={data} useDiv={useDiv} />;
};

D3ComponentScrollyTelling.propTypes = {
callback: PropTypes.func.isRequired,
isInitialized: PropTypes.bool,
setIsInitialized: PropTypes.func,
data: PropTypes.any,
// Using a div node instead of a svg node allows usage of child components of other types:
// i.g., for performance issues, we used both canvas & svg child elements in a visualisation.
useDiv: PropTypes.bool,
};

export default D3ComponentScrollyTelling;
37 changes: 2 additions & 35 deletions web/src/d3/evolving_chart/preproc.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import _ from 'lodash';

import { convertTimestampsToDates } from '../utils';
import { STAGES_ORDERED, EPOCH_DURATION_SEC } from '../constants';
import { convertTimestampsToDates, convertEpochsToAnnotations } from '../utils';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C'est peut-être plus le rôle du back-end de convertir les epochs de 30s en plage de stade de sommeil contigües. Pour l'instant c'est ok comme ça fonctionne, mais je me dis que ce sera important de déplacer ça.

import { STAGES_ORDERED } from '../constants';

export const preprocessData = (data) => {
data = convertTimestampsToDates(data);
Expand All @@ -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);
Expand Down
157 changes: 157 additions & 0 deletions web/src/d3/spectrogram/axes_legend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import * as d3 from 'd3';
import _ from 'lodash';
import {
MARGIN,
NB_POINTS_COLOR_INTERPOLATION,
TITLE_FONT_SIZE,
TITLE_POSITION_Y,
} 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})`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'imagine que ce serait mieux de faire la différence entre les margin de la légende et du spectrogramme. Peut-être créer deux objets même s'ils ont les mêmes valeurs serait préférable.

Copy link
Contributor Author

@conorato conorato Sep 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dans ce cas-ci, on ne définit pas le margin de la légende, mais plutôt le déplacement à faire en X pour arriver à la position voulue de la légende. On additionne donc le margin de droite, la largeur du spectrogramme, et on arrive à l'endroit en X où mettre la légende.
J'allais suggérer de faire une constante pour définir les positions en X et Y, mais comme il faudrait en fait faire une fonction qui prend en paramètre spectrogramWidth, je ne crois pas que ça l'aiderait tant à la lisibilité.

),
});

const drawTitle = (g, channelName, spectrogramWidth) =>
g
.append('text')
.attr('x', spectrogramWidth / 2)
.attr('y', TITLE_POSITION_Y)
.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)');
};

const drawSpectrogramAxesAndLegend = (
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;
5 changes: 4 additions & 1 deletion web/src/d3/spectrogram/constants.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
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 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 = {
Expand All @@ -11,3 +12,5 @@ export const MARGIN = {
BOTTOM: 50,
LEFT: 70,
};
export const TITLE_FONT_SIZE = '18px';
export const TITLE_POSITION_Y = -MARGIN.TOP / 3;
53 changes: 0 additions & 53 deletions web/src/d3/spectrogram/legend.js

This file was deleted.

Loading