-
Notifications
You must be signed in to change notification settings - Fork 3
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
Changes from all commits
e6adb2f
5e479fa
ab14548
2d8d488
fa9b968
5dee8ad
a7149fe
357c0a8
29f2bd2
202dc2f
2aff6ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isLoaded et setIsLoaded? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Le nom est |
||
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; |
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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
@@ -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); | ||
|
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})`, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
), | ||
}); | ||
|
||
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; |
This file was deleted.
There was a problem hiding this comment.
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.