Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
Philip Elson
CARA
Commits
97ac2ab7
Commit
97ac2ab7
authored
Sep 07, 2021
by
Philip Elson
🐍
Browse files
Merge branch 'feature/alternative_scenario_d3' into 'master'
Alternative scenarios plot with D3 See merge request cara/cara!246
parents
0414e15e
250b7c05
Pipeline
#2998416
failed with stages
in 5 minutes and 29 seconds
Changes
3
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
cara/apps/calculator/report_generator.py
View file @
97ac2ab7
...
...
@@ -9,11 +9,9 @@ import zlib
import
loky
import
jinja2
import
matplotlib
matplotlib
.
use
(
'agg'
)
import
matplotlib.pyplot
as
plt
import
numpy
as
np
import
qrcode
import
json
from
cara
import
models
from
...
import
monte_carlo
as
mc
...
...
@@ -162,50 +160,13 @@ def _img2bytes(figure):
return
img_data
def
_figure2bytes
(
figure
):
# Draw the image
img_data
=
io
.
BytesIO
()
figure
.
savefig
(
img_data
,
format
=
'png'
,
bbox_inches
=
"tight"
,
transparent
=
True
)
return
img_data
def
img2base64
(
img_data
)
->
str
:
plt
.
close
()
img_data
.
seek
(
0
)
pic_hash
=
base64
.
b64encode
(
img_data
.
read
()).
decode
(
'ascii'
)
# A src suitable for a tag such as f'<img id="scenario_concentration_plot" src="{result}">.
return
f
'data:image/png;base64,
{
pic_hash
}
'
def
plot
(
times
,
concentrations
,
model
:
models
.
ExposureModel
):
fig
=
plt
.
figure
()
ax
=
fig
.
add_subplot
(
1
,
1
,
1
)
datetimes
=
[
datetime
(
1970
,
1
,
1
)
+
timedelta
(
hours
=
time
)
for
time
in
times
]
ax
.
plot
(
datetimes
,
concentrations
,
lw
=
2
,
color
=
'#1f77b4'
,
label
=
'Mean concentration'
)
ax
.
spines
[
'right'
].
set_visible
(
False
)
ax
.
spines
[
'top'
].
set_visible
(
False
)
ax
.
set_xlabel
(
'Time of day'
)
ax
.
set_ylabel
(
'Mean concentration ($virions/m^{3}$)'
)
ax
.
set_title
(
'Mean concentration of virions'
)
ax
.
xaxis
.
set_major_formatter
(
matplotlib
.
dates
.
DateFormatter
(
"%H:%M"
))
# Plot presence of exposed person
for
i
,
(
presence_start
,
presence_finish
)
in
enumerate
(
model
.
exposed
.
presence
.
boundaries
()):
plt
.
fill_between
(
datetimes
,
concentrations
,
0
,
where
=
(
np
.
array
(
times
)
>
presence_start
)
&
(
np
.
array
(
times
)
<
presence_finish
),
color
=
"#1f77b4"
,
alpha
=
0.1
,
label
=
"Presence of exposed person(s)"
if
i
==
0
else
""
)
# Place a legend outside of the axes itself.
ax
.
legend
(
bbox_to_anchor
=
(
1.05
,
1
),
loc
=
'upper left'
)
ax
.
set_ylim
(
0
)
return
fig
def
minutes_to_time
(
minutes
:
int
)
->
str
:
minute_string
=
str
(
minutes
%
60
)
minute_string
=
"0"
*
(
2
-
len
(
minute_string
))
+
minute_string
...
...
@@ -281,39 +242,7 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, mc.Exp
return
scenarios
def
comparison_plot
(
scenarios
:
typing
.
Dict
[
str
,
dict
],
sample_times
:
typing
.
List
[
float
]):
fig
=
plt
.
figure
()
ax
=
fig
.
add_subplot
(
1
,
1
,
1
)
dash_styled_scenarios
=
[
'Base scenario with FFP2 masks'
,
'Base scenario with HEPA filter'
,
'Base scenario with HEPA and FFP2 masks'
,
]
sample_dts
=
[
datetime
(
1970
,
1
,
1
)
+
timedelta
(
hours
=
time
)
for
time
in
sample_times
]
for
name
,
statistics
in
scenarios
.
items
():
concentrations
=
statistics
[
'concentrations'
]
if
name
in
dash_styled_scenarios
:
ax
.
plot
(
sample_dts
,
concentrations
,
label
=
name
,
linestyle
=
'--'
)
else
:
ax
.
plot
(
sample_dts
,
concentrations
,
label
=
name
,
linestyle
=
'-'
,
alpha
=
0.5
)
# Place a legend outside of the axes itself.
ax
.
legend
(
bbox_to_anchor
=
(
1.05
,
1
),
loc
=
'upper left'
)
ax
.
spines
[
'right'
].
set_visible
(
False
)
ax
.
spines
[
'top'
].
set_visible
(
False
)
ax
.
xaxis
.
set_major_formatter
(
matplotlib
.
dates
.
DateFormatter
(
"%H:%M"
))
ax
.
set_xlabel
(
'Time of day'
)
ax
.
set_ylabel
(
'Mean concentration ($virions/m^{3}$)'
)
ax
.
set_title
(
'Mean concentration of virions'
)
return
fig
def
scenario_statistics
(
mc_model
:
mc
.
ExposureModel
,
sample_times
:
typing
.
List
[
float
]):
def
scenario_statistics
(
mc_model
:
mc
.
ExposureModel
,
sample_times
:
np
.
ndarray
):
model
=
mc_model
.
build_model
(
size
=
_DEFAULT_MC_SAMPLE_SIZE
)
return
{
'probability_of_infection'
:
np
.
mean
(
model
.
infection_probability
()),
...
...
@@ -342,7 +271,6 @@ def comparison_report(
for
(
name
,
model
),
model_stats
in
zip
(
scenarios
.
items
(),
results
):
statistics
[
name
]
=
model_stats
return
{
'plot'
:
img2base64
(
_figure2bytes
(
comparison_plot
(
statistics
,
sample_times
))),
'stats'
:
statistics
,
}
...
...
@@ -405,6 +333,7 @@ class ReportGenerator:
env
.
filters
[
'minutes_to_time'
]
=
minutes_to_time
env
.
filters
[
'float_format'
]
=
"{0:.2f}"
.
format
env
.
filters
[
'int_format'
]
=
"{:0.0f}"
.
format
env
.
filters
[
'JSONify'
]
=
json
.
dumps
return
env
def
render
(
self
,
context
:
dict
)
->
str
:
...
...
cara/apps/calculator/static/js/report.js
View file @
97ac2ab7
...
...
@@ -25,56 +25,16 @@ function draw_concentration_plot(svg_id, times, concentrations, exposed_presence
yAxis
=
d3
.
axisLeft
(
yRange
);
// Plot tittle.
vis
.
append
(
'
svg:foreignObject
'
)
.
attr
(
"
background-color
"
,
"
transparent
"
)
.
attr
(
'
width
'
,
width
)
.
attr
(
'
height
'
,
margins
.
top
)
.
style
(
'
text-align
'
,
'
center
'
)
.
html
(
'
<b>Mean concentration of virions</b>
'
);
plot_title
(
vis
,
width
,
margins
.
top
,
'
Mean concentration of virions
'
);
// Line representing the mean concentration.
var
lineFunc
=
d3
.
line
()
.
defined
(
d
=>
!
isNaN
(
d
.
concentration
))
.
x
(
d
=>
xTimeRange
(
d
.
time
))
.
y
(
d
=>
yRange
(
d
.
concentration
))
.
curve
(
d3
.
curveBasis
);
plot_scenario_data
(
vis
,
data
,
xTimeRange
,
yRange
,
'
#1f77b4
'
);
vis
.
append
(
'
svg:path
'
)
.
attr
(
'
d
'
,
lineFunc
(
data
))
.
attr
(
'
stroke
'
,
'
#1f77b4
'
)
.
attr
(
'
stroke-width
'
,
2
)
.
attr
(
'
fill
'
,
'
none
'
);
// X axis.
plot_x_axis
(
vis
,
height
,
width
,
margins
,
xAxis
,
'
Time of day
'
);
// X axis declaration.
vis
.
append
(
'
svg:g
'
)
.
attr
(
'
class
'
,
'
x axis
'
)
.
attr
(
'
transform
'
,
'
translate(0,
'
+
(
height
-
margins
.
bottom
)
+
'
)
'
)
.
call
(
xAxis
);
// X axis label.
vis
.
append
(
'
text
'
)
.
attr
(
'
class
'
,
'
x label
'
)
.
attr
(
'
fill
'
,
'
black
'
)
.
attr
(
'
text-anchor
'
,
'
middle
'
)
.
attr
(
'
x
'
,
(
width
+
margins
.
right
)
/
2
)
.
attr
(
'
y
'
,
height
*
0.97
)
.
text
(
'
Time of day
'
)
// Y axis declaration.
vis
.
append
(
'
svg:g
'
)
.
attr
(
'
class
'
,
'
y axis
'
)
.
attr
(
'
transform
'
,
'
translate(
'
+
margins
.
left
+
'
,0)
'
)
.
call
(
yAxis
);
// Y axis label.
vis
.
append
(
'
svg:text
'
)
.
attr
(
'
class
'
,
'
y label
'
)
.
attr
(
'
fill
'
,
'
black
'
)
.
attr
(
'
transform
'
,
'
rotate(-90, 0,
'
+
height
+
'
)
'
)
.
attr
(
'
text-anchor
'
,
'
middle
'
)
.
attr
(
'
x
'
,
(
height
+
margins
.
bottom
)
/
2
)
.
attr
(
'
y
'
,
(
height
+
margins
.
left
)
*
0.92
)
.
text
(
'
Mean concentration (virions/m³)
'
);
// Y axis
plot_y_axis
(
vis
,
height
,
width
,
margins
,
yAxis
,
'
Mean concentration (virions/m³)
'
)
// Area representing the presence of exposed person(s).
exposed_presence_intervals
.
forEach
(
b
=>
{
...
...
@@ -181,4 +141,178 @@ function draw_concentration_plot(svg_id, times, concentrations, exposed_presence
focus
.
select
(
'
#tooltip-time
'
).
text
(
'
x =
'
+
time_format
(
d
.
hour
));
focus
.
select
(
'
#tooltip-concentration
'
).
text
(
'
y =
'
+
d
.
concentration
.
toFixed
(
2
));
}
}
// Generate the alternative scenarios plot using d3 library.
// 'alternative_scenarios' is a dictionary with all the alternative scenarios
// 'times' is a list of times for all the scenarios
function
draw_alternative_scenarios_plot
(
svg_id
,
width
,
height
,
alternative_scenarios
,
times
)
{
// H:M format
var
time_format
=
d3
.
timeFormat
(
'
%H:%M
'
);
// D3 array of ten categorical colors represented as RGB hexadecimal strings.
var
colors
=
d3
.
schemeAccent
;
// Variable for the highest concentration for all the scenarios
var
highest_concentration
=
0
.
var
data_for_scenarios
=
{}
for
(
scenario
in
alternative_scenarios
)
{
scenario_concentrations
=
alternative_scenarios
[
scenario
].
concentrations
highest_concentration
=
Math
.
max
(
highest_concentration
,
Math
.
max
(...
scenario_concentrations
))
var
data
=
[]
times
.
map
((
time
,
index
)
=>
data
.
push
({
'
time
'
:
time
,
'
hour
'
:
new
Date
().
setHours
(
Math
.
trunc
(
time
),
(
time
-
Math
.
trunc
(
time
))
*
60
),
'
concentration
'
:
scenario_concentrations
[
index
]
}))
// Add data into lines dictionary
data_for_scenarios
[
scenario
]
=
data
}
// We need one scenario to get the time range
var
first_scenario
=
Object
.
values
(
data_for_scenarios
)[
0
]
var
vis
=
d3
.
select
(
svg_id
),
width
=
width
,
height
=
height
,
margins
=
{
top
:
30
,
right
:
20
,
bottom
:
50
,
left
:
50
},
// H:M time format for x axis.
xRange
=
d3
.
scaleTime
().
range
([
margins
.
left
,
width
-
margins
.
right
]).
domain
([
first_scenario
[
0
].
hour
,
first_scenario
[
first_scenario
.
length
-
1
].
hour
]),
xTimeRange
=
d3
.
scaleLinear
().
range
([
margins
.
left
,
width
-
margins
.
right
]).
domain
([
times
[
0
],
times
[
times
.
length
-
1
]]),
yRange
=
d3
.
scaleLinear
().
range
([
height
-
margins
.
bottom
,
margins
.
top
]).
domain
([
0
.,
highest_concentration
]),
xAxis
=
d3
.
axisBottom
(
xRange
).
tickFormat
(
d
=>
time_format
(
d
)),
yAxis
=
d3
.
axisLeft
(
yRange
);
// Plot title.
plot_title
(
vis
,
width
,
margins
.
top
,
'
Mean concentration of virions
'
);
// Line representing the mean concentration for each scenario.
for
(
const
[
scenario_name
,
data
]
of
Object
.
entries
(
data_for_scenarios
))
{
var
scenario_index
=
Object
.
keys
(
data_for_scenarios
).
indexOf
(
scenario_name
)
// Line representing the mean concentration.
plot_scenario_data
(
vis
,
data
,
xTimeRange
,
yRange
,
colors
[
scenario_index
])
// Legend for the plot elements - lines.
var
size
=
20
*
(
scenario_index
+
1
)
vis
.
append
(
'
rect
'
)
.
attr
(
'
x
'
,
width
+
20
)
.
attr
(
'
y
'
,
margins
.
top
+
size
)
.
attr
(
'
width
'
,
20
)
.
attr
(
'
height
'
,
3
)
.
style
(
'
fill
'
,
colors
[
scenario_index
]);
vis
.
append
(
'
text
'
)
.
attr
(
'
x
'
,
width
+
3
*
20
)
.
attr
(
'
y
'
,
margins
.
top
+
size
)
.
text
(
scenario_name
)
.
style
(
'
font-size
'
,
'
15px
'
)
.
attr
(
'
alignment-baseline
'
,
'
central
'
);
}
// X axis.
plot_x_axis
(
vis
,
height
,
width
,
margins
,
xAxis
,
"
Time of day
"
);
// Y axis declaration.
vis
.
append
(
'
svg:g
'
)
.
attr
(
'
class
'
,
'
y axis
'
)
.
attr
(
'
transform
'
,
'
translate(
'
+
margins
.
left
+
'
,0)
'
)
.
call
(
yAxis
);
// Y axis label.
vis
.
append
(
'
svg:text
'
)
.
attr
(
'
class
'
,
'
y label
'
)
.
attr
(
'
fill
'
,
'
black
'
)
.
attr
(
'
transform
'
,
'
rotate(-90, 0,
'
+
height
+
'
)
'
)
.
attr
(
'
text-anchor
'
,
'
middle
'
)
.
attr
(
'
x
'
,
(
height
+
margins
.
bottom
)
/
2
)
.
attr
(
'
y
'
,
(
height
+
margins
.
left
)
*
0.92
)
.
text
(
'
Mean concentration (virions/m³)
'
);
// Legend bounding box.
vis
.
append
(
'
rect
'
)
.
attr
(
'
width
'
,
275
)
.
attr
(
'
height
'
,
25
*
(
Object
.
keys
(
data_for_scenarios
).
length
))
.
attr
(
'
x
'
,
width
*
1.005
)
.
attr
(
'
y
'
,
margins
.
top
+
5
)
.
attr
(
'
stroke
'
,
'
lightgrey
'
)
.
attr
(
'
stroke-width
'
,
'
2
'
)
.
attr
(
'
rx
'
,
'
5px
'
)
.
attr
(
'
ry
'
,
'
5px
'
)
.
attr
(
'
stroke-linejoin
'
,
'
round
'
)
.
attr
(
'
fill
'
,
'
none
'
);
}
// Functions used to build the plots' components
function
plot_title
(
vis
,
width
,
margin_top
,
title
)
{
vis
.
append
(
'
svg:foreignObject
'
)
.
attr
(
'
width
'
,
width
)
.
attr
(
'
height
'
,
margin_top
)
.
attr
(
'
fill
'
,
'
none
'
)
.
append
(
'
xhtml:div
'
)
.
style
(
'
text-align
'
,
'
center
'
)
.
html
(
title
);
return
vis
;
}
function
plot_x_axis
(
vis
,
height
,
width
,
margins
,
xAxis
,
label
)
{
// X axis declaration
vis
.
append
(
'
svg:g
'
)
.
attr
(
'
class
'
,
'
x axis
'
)
.
attr
(
'
transform
'
,
'
translate(0,
'
+
(
height
-
margins
.
bottom
)
+
'
)
'
)
.
call
(
xAxis
);
// X axis label.
vis
.
append
(
'
text
'
)
.
attr
(
'
class
'
,
'
x label
'
)
.
attr
(
'
fill
'
,
'
black
'
)
.
attr
(
'
text-anchor
'
,
'
middle
'
)
.
attr
(
'
x
'
,
(
width
+
margins
.
right
)
/
2
)
.
attr
(
'
y
'
,
height
*
0.97
)
.
text
(
label
);
return
vis
;
}
function
plot_y_axis
(
vis
,
height
,
width
,
margins
,
yAxis
,
label
)
{
// Y axis declaration.
vis
.
append
(
'
svg:g
'
)
.
attr
(
'
class
'
,
'
y axis
'
)
.
attr
(
'
transform
'
,
'
translate(
'
+
margins
.
left
+
'
,0)
'
)
.
call
(
yAxis
);
// Y axis label.
vis
.
append
(
'
svg:text
'
)
.
attr
(
'
class
'
,
'
y label
'
)
.
attr
(
'
fill
'
,
'
black
'
)
.
attr
(
'
transform
'
,
'
rotate(-90, 0,
'
+
height
+
'
)
'
)
.
attr
(
'
text-anchor
'
,
'
middle
'
)
.
attr
(
'
x
'
,
(
height
+
margins
.
bottom
)
/
2
)
.
attr
(
'
y
'
,
(
height
+
margins
.
left
)
*
0.92
)
.
text
(
label
);
return
vis
;
}
function
plot_scenario_data
(
vis
,
data
,
xTimeRange
,
yRange
,
line_color
)
{
var
lineFunc
=
d3
.
line
()
.
defined
(
d
=>
!
isNaN
(
d
.
concentration
))
.
x
(
d
=>
xTimeRange
(
d
.
time
))
.
y
(
d
=>
yRange
(
d
.
concentration
))
.
curve
(
d3
.
curveBasis
);
vis
.
append
(
'
svg:path
'
)
.
attr
(
'
d
'
,
lineFunc
(
data
))
.
attr
(
"
stroke
"
,
line_color
)
.
attr
(
'
stroke-width
'
,
2
)
.
attr
(
'
fill
'
,
'
none
'
);
return
vis
;
}
\ No newline at end of file
cara/apps/calculator/templates/base/calculator.report.html.j2
View file @
97ac2ab7
...
...
@@ -88,9 +88,9 @@
<p
id=
"section1"
>
* The results are based on the parameters and assumptions published in the CERN Open Report
<a
href=
"https://cds.cern.ch/record/2756083"
>
CERN-OPEN-2021-004
</a>
.
</p>
<svg
id=
"result_plot"
width=
"900"
height=
"400"
></svg>
<script
type=
"application/javascript"
>
var
times
=
{{
times
}}
var
concentrations
=
{{
concentrations
}}
var
exposed_presence_intervals
=
{{
exposed_presence_intervals
}}
var
times
=
{{
times
|
JSONify
}}
var
concentrations
=
{{
concentrations
|
JSONify
}}
var
exposed_presence_intervals
=
{{
exposed_presence_intervals
|
JSONify
}}
draw_concentration_plot
(
"
#result_plot
"
,
times
,
concentrations
,
exposed_presence_intervals
);
</script>
</p>
...
...
@@ -108,8 +108,12 @@
<div
class=
"collapse"
id=
"collapseAlternativeScenarios"
>
<div
class=
"card-body"
>
<div>
<img
id=
"scenario_concentration_plot"
src=
"{{ alternative_scenarios.plot }}"
/>
<svg
id=
"alternative_scenario_plot"
width=
"900"
height=
"400"
></svg>
<script
type=
"application/javascript"
>
var
alternative_scenarios
=
{{
alternative_scenarios
.
stats
|
JSONify
}}
var
times
=
{{
times
|
JSONify
}}
draw_alternative_scenarios_plot
(
"
#alternative_scenario_plot
"
,
width
=
600
,
height
=
400
,
alternative_scenarios
,
times
);
</script>
{% block report_scenarios_summary_table %}
<table
class=
"table w-auto"
>
<thead
class=
"thead-light"
>
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment