diff --git a/README.md b/README.md
index 1aacec04..1be26e55 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ Each event modelled is unique, and the results generated therein are only as acc
## Authors
CARA was developed by following members of CERN - European Council for Nuclear Research (visit https://home.cern/):
-Andre Henriques1, Marco Andreini1, Gabriella Azzopardi2, James Devine3, Philip Elson4, Nicolas Mounet2, Markus Kongstein Rognlien2,6, Nicola Tarocco5
+Andre Henriques1, Luis Aleixo1, Marco Andreini1, Gabriella Azzopardi2, James Devine3, Philip Elson4, Nicolas Mounet2, Markus Kongstein Rognlien2,6, Nicola Tarocco5
1HSE Unit, Occupational Health & Safety Group, CERN
2Beams Department, Accelerators and Beam Physics Group, CERN
diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py
index 28f99b85..bf91281c 100644
--- a/cara/apps/calculator/report_generator.py
+++ b/cara/apps/calculator/report_generator.py
@@ -44,13 +44,13 @@ def calculate_report_data(model: models.ExposureModel):
return {
"times": list(times),
+ "exposed_presence_intervals": [list(interval) for interval in model.exposed.presence.boundaries()],
"concentrations": concentrations,
"highest_const": highest_const,
"prob_inf": prob,
"emission_rate": er,
"exposed_occupants": exposed_occupants,
"expected_new_cases": expected_new_cases,
- "scenario_plot_src": img2base64(_figure2bytes(plot(times, concentrations, model))),
}
@@ -133,7 +133,7 @@ def plot(times, concentrations, model: models.ExposureModel):
ax.set_ylim(0)
return fig
-
+
def minutes_to_time(minutes: int) -> str:
minute_string = str(minutes % 60)
diff --git a/cara/apps/calculator/static/js/report.js b/cara/apps/calculator/static/js/report.js
new file mode 100644
index 00000000..99499e8d
--- /dev/null
+++ b/cara/apps/calculator/static/js/report.js
@@ -0,0 +1,190 @@
+/* Generate the concentration plot using d3 library. */
+function draw_concentration_plot(svg_id, times, concentrations, exposed_presence_intervals) {
+ var visBoundingBox = d3.select(svg_id)
+ .node()
+ .getBoundingClientRect();
+
+ var time_format = d3.timeFormat('%H:%M');
+
+ var data = []
+ times.map((time, index) => data.push({ 'time': time, 'hour': new Date().setHours(Math.trunc(time), (time - Math.trunc(time)) * 60), 'concentration': concentrations[index] }))
+
+ var vis = d3.select(svg_id),
+ width = visBoundingBox.width - 300,
+ height = visBoundingBox.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([data[0].hour, data[data.length - 1].hour]),
+ bisecHour = d3.bisector((d) => { return d.hour; }).left,
+
+ yRange = d3.scaleLinear().range([height - margins.bottom, margins.top]).domain([data[0].concentration, data[data.length - 1].concentration]),
+ xTimeRange = d3.scaleLinear().range([margins.left, width - margins.right]).domain([data[0].time, data[data.length - 1].time]),
+
+ xAxis = d3.axisBottom(xRange).tickFormat(d => time_format(d)),
+ yAxis = d3.axisLeft(yRange);
+
+ // Plot tittle.
+ vis.append('svg:foreignObject')
+ .attr('width', width)
+ .attr('height', margins.top)
+ .append('xhtml:body')
+ .style('text-align', 'center')
+ .html('Mean concentration of infectious quanta');
+
+ // 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);
+
+ vis.append('svg:path')
+ .attr('d', lineFunc(data))
+ .attr('stroke', '#1f77b4')
+ .attr('stroke-width', 2)
+ .attr('fill', 'none');
+
+ // 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 (q/m^3)');
+
+ // Area representing the presence of exposed person(s).
+ exposed_presence_intervals.forEach(b => {
+ vis.append('svg:path')
+ .attr('d', lineFunc(data.filter(d => {
+ return d.time >= b[0] && d.time <= b[1]
+ })))
+ .attr('fill', 'none');
+
+ var curveFunc = d3.area()
+ .x(d => xTimeRange(d.time))
+ .y0(height - margins.bottom)
+ .y1(d => yRange(d.concentration));
+
+ vis.append('svg:path')
+ .attr('d', curveFunc(data.filter(d => {
+ return d.time >= b[0] && d.time <= b[1]
+ })))
+ .attr('fill', '#1f77b4')
+ .attr('fill-opacity', '0.1');
+ })
+
+ // Legend for the plot elements - line and area.
+ var size = 20
+ vis.append('rect')
+ .attr('x', width + size)
+ .attr('y', margins.top + size)
+ .attr('width', 20)
+ .attr('height', 3)
+ .style('fill', '#1f77b4');
+
+ vis.append('rect')
+ .attr('x', width + size)
+ .attr('y', 3 * size)
+ .attr('width', 20)
+ .attr('height', 20)
+ .attr('fill', '#1f77b4')
+ .attr('fill-opacity', '0.1');
+
+ vis.append('text')
+ .attr('x', width + 3 * size)
+ .attr('y', margins.top + size)
+ .text('Mean concentration')
+ .style('font-size', '15px')
+ .attr('alignment-baseline', 'central');
+
+ vis.append('text')
+ .attr('x', width + 3 * size)
+ .attr('y', margins.top + 2 * size)
+ .text('Presence of exposed person(s)')
+ .style('font-size', '15px')
+ .attr('alignment-baseline', 'central');
+
+ // Legend bounding box.
+ vis.append('rect')
+ .attr('width', 275)
+ .attr('height', 50)
+ .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');
+
+ // Tooltip.
+ var focus = vis.append('svg:g')
+ .style('display', 'none');
+
+ focus.append('circle')
+ .attr('r', 3);
+
+ focus.append('rect')
+ .attr('fill', 'white')
+ .attr('stroke', '#000')
+ .attr('width', 80)
+ .attr('height', 50)
+ .attr('x', 10)
+ .attr('y', -22)
+ .attr('rx', 4)
+ .attr('ry', 4);
+
+ focus.append('text')
+ .attr('id', 'tooltip-time')
+ .attr('x', 18)
+ .attr('y', -2);
+
+ focus.append('text')
+ .attr('id', 'tooltip-concentration')
+ .attr('x', 18)
+ .attr('y', 18);
+
+ vis.append('rect')
+ .attr('fill', 'none')
+ .attr('pointer-events', 'all')
+ .attr('width', width - margins.right)
+ .attr('height', height)
+ .on('mouseover', () => { focus.style('display', null); })
+ .on('mouseout', () => { focus.style('display', 'none'); })
+ .on('mousemove', mousemove);
+
+ function mousemove() {
+ var x0 = xRange.invert(d3.pointer(event, this)[0]),
+ i = bisecHour(data, x0, 1),
+ d0 = data[i - 1],
+ d1 = data[i],
+ d = x0 - d0.hour > d1.hour - x0 ? d1 : d0;
+ focus.attr('transform', 'translate(' + xRange(d.hour) + ',' + yRange(d.concentration) + ')');
+ focus.select('#tooltip-time').text('x = ' + time_format(d.hour));
+ focus.select('#tooltip-concentration').text('y = ' + d.concentration.toFixed(2));
+ }
+}
\ No newline at end of file
diff --git a/cara/apps/calculator/templates/base/calculator.report.html.j2 b/cara/apps/calculator/templates/base/calculator.report.html.j2
index 61cf107c..d45a089a 100644
--- a/cara/apps/calculator/templates/base/calculator.report.html.j2
+++ b/cara/apps/calculator/templates/base/calculator.report.html.j2
@@ -8,6 +8,10 @@
+
+
+
+
[*] The results are based on the parameters and assumptions published in the CERN Open Report CERN-OPEN-2021-004
-Alternative scenarios:
diff --git a/cara/apps/templates/index.html.j2 b/cara/apps/templates/index.html.j2 index dddba6e4..c774420c 100644 --- a/cara/apps/templates/index.html.j2 +++ b/cara/apps/templates/index.html.j2 @@ -43,7 +43,7 @@
-