Merge branch 'feature/CO2_profile' into 'master'

CO₂ Concentration Plot

Closes #307 and #297

See merge request caimira/caimira!428
This commit is contained in:
Andre Henriques 2023-06-16 14:58:58 +02:00
commit f6579fb31a
5 changed files with 230 additions and 116 deletions

View file

@ -38,7 +38,7 @@ from .user import AuthenticatedUser, AnonymousUser
# calculator version. If the calculator needs to make breaking changes (e.g. change
# form attributes) then it can also increase its MAJOR version without needing to
# increase the overall CAiMIRA version (found at ``caimira.__version__``).
__version__ = "4.10"
__version__ = "4.11"
LOG = logging.getLogger(__name__)

View file

@ -304,9 +304,8 @@ class FormData:
if total_percentage != 100:
raise ValueError(f'The sum of all respiratory activities should be 100. Got {total_percentage}.')
def build_mc_model(self) -> mc.ExposureModel:
def initialize_room(self) -> models.Room:
# Initializes room with volume either given directly or as product of area and height
if self.volume_type == 'room_volume_explicit':
volume = self.room_volume
@ -323,7 +322,10 @@ class FormData:
humidity = float(self.humidity)
inside_temp = self.inside_temp
room = models.Room(volume=volume, inside_temp=models.PiecewiseConstant((0, 24), (inside_temp,)), humidity=humidity)
return models.Room(volume=volume, inside_temp=models.PiecewiseConstant((0, 24), (inside_temp,)), humidity=humidity)
def build_mc_model(self) -> mc.ExposureModel:
room = self.initialize_room()
infected_population = self.infected_population()
@ -357,6 +359,32 @@ class FormData:
def build_model(self, sample_size=DEFAULT_MC_SAMPLE_SIZE) -> models.ExposureModel:
return self.build_mc_model().build_model(size=sample_size)
def build_CO2_model(self, sample_size=DEFAULT_MC_SAMPLE_SIZE) -> models.CO2ConcentrationModel:
infected_population: models.InfectedPopulation = self.infected_population().build_model(sample_size)
exposed_population: models.Population = self.exposed_population().build_model(sample_size)
state_change_times = set(infected_population.presence_interval().transition_times())
state_change_times.update(exposed_population.presence_interval().transition_times())
transition_times = sorted(state_change_times)
total_people = [infected_population.people_present(stop) + exposed_population.people_present(stop)
for _, stop in zip(transition_times[:-1], transition_times[1:])]
population = mc.Population(
number=models.IntPiecewiseConstant(transition_times=tuple(transition_times), values=tuple(total_people)),
presence=None,
mask=models.Mask.types[self.mask_type],
activity=activity_distributions[ACTIVITIES[ACTIVITY_TYPES.index(self.activity_type)]['activity']],
host_immunity=0.,
)
# Builds a CO2 concentration model based on model inputs
return mc.CO2ConcentrationModel(
room=self.initialize_room(),
ventilation=self.ventilation(),
CO2_emitters=population,
).build_model(size=sample_size)
def tz_name_and_utc_offset(self) -> typing.Tuple[str, float]:
"""
Return the timezone name (e.g. CET), and offset, in hours, that need to

View file

@ -133,6 +133,12 @@ def calculate_report_data(form: FormData, model: models.ExposureModel) -> typing
for time1, time2 in zip(times[:-1], times[1:])
])
CO2_model: models.CO2ConcentrationModel = form.build_CO2_model()
CO2_concentrations = {'CO₂': {'concentrations': [
np.array(CO2_model.concentration(float(time))).mean()
for time in times
]}}
prob = np.array(model.infection_probability())
prob_dist_count, prob_dist_bins = np.histogram(prob/100, bins=100, density=True)
prob_probabilistic_exposure = np.array(model.total_probability_rule()).mean()
@ -158,6 +164,7 @@ def calculate_report_data(form: FormData, model: models.ExposureModel) -> typing
"prob_probabilistic_exposure": prob_probabilistic_exposure,
"expected_new_cases": expected_new_cases,
"uncertainties_plot_src": uncertainties_plot_src,
"CO2_concentrations": CO2_concentrations,
}

View file

@ -209,6 +209,7 @@ function draw_plot(svg_id) {
// Tooltip.
var focus = {}, tooltip_rect = {}, tooltip_time = {}, tooltip_concentration = {}, toolBox = {};
for (const [concentration, data] of Object.entries(tooltip_data_for_graphs)) {
let data_length = String(Math.floor(Math.max(...data.map(el => el.concentration !== undefined ? el.concentration : 0)))).length;
focus[concentration] = vis.append('svg:g')
.style('display', 'none');
@ -219,7 +220,7 @@ function draw_plot(svg_id) {
tooltip_rect[concentration] = focus[concentration].append('rect')
.attr('fill', 'white')
.attr('stroke', '#000')
.attr('width', 85)
.attr('width', 65 + data_length * 8)
.attr('height', 50)
.attr('y', -22)
.attr('rx', 4)
@ -308,18 +309,17 @@ function draw_plot(svg_id) {
var div_height = plot_div.clientHeight;
graph_width = div_width;
graph_height = div_height
var margins = { top: 30, right: 20, bottom: 50, left: 60 };
if (div_width >= 900) { // For screens with width > 900px legend can be on the graph's right side.
var margins = { top: 30, right: 20, bottom: 50, left: 60 };
div_width = 900;
graph_width = div_width * (2/3);
const svg_margins = {'margin-left': '0rem'};
Object.entries(svg_margins).forEach(([prop,val]) => vis.style(prop,val));
}
else {
var margins = { top: 30, right: 20, bottom: 50, left: 40 };
div_width = div_width * 1.1
graph_width = div_width * .9;
graph_height = div_height * 0.65; // On mobile screen sizes we want the legend to be on the bottom of the graph.
graph_height = div_height * .75; // On mobile screen sizes we want the legend to be on the bottom of the graph.
const svg_margins = {'margin-left': '-1rem'};
Object.entries(svg_margins).forEach(([prop,val]) => vis.style(prop,val));
};
@ -353,23 +353,19 @@ function draw_plot(svg_id) {
.attr('y', graph_height * 0.97);
yAxisEl.attr('transform', 'translate(' + margins.left + ',0)');
yAxisLabelEl.attr('x', (graph_height * 0.9 + margins.bottom) / 2)
yAxisLabelEl.attr('x', (graph_height + margins.bottom * .55) / 2)
.attr('y', (graph_height + margins.left) * 0.9)
.attr('transform', 'rotate(-90, 0,' + graph_height + ')');
yAxisCumEl.attr('transform', 'translate(' + (graph_width - margins.right) + ',0)').call(yCumulativeAxis);
yAxisCumLabelEl.attr('transform', 'rotate(-90, 0,' + graph_height + ')')
.attr('x', (graph_height + margins.bottom) / 2);
.attr('x', (graph_height + margins.bottom) / 2.1);
if (plot_div.clientWidth >= 900) {
yAxisCumLabelEl.attr('transform', 'rotate(-90, 0,' + graph_height + ')')
.attr('x', (graph_height + margins.bottom) / 2)
.attr('y', 1.71 * graph_width);
yAxisCumLabelEl.attr('y', graph_width * 1.7);
}
else {
yAxisCumLabelEl.attr('transform', 'rotate(-90, 0,' + graph_height + ')')
.attr('x', (graph_height + margins.bottom * 0.55) / 2)
.attr('y', graph_width + 290);
yAxisCumLabelEl.attr('y', graph_width + 325);
}
const size = 20;
@ -415,8 +411,7 @@ function draw_plot(svg_id) {
}
// Legend on the bottom.
else {
legend_x_start = 10;
legend_x_start = margins.left + 10;
legendLineIcon.attr('x', legend_x_start)
.attr('y', graph_height + size);
legendLineText.attr('x', legend_x_start + space_between_text_icon)
@ -449,7 +444,7 @@ function draw_plot(svg_id) {
.attr('y', graph_height + (4 + sr_unique_activities.length) * size + text_height);
}
legendBBox.attr('x', 1)
legendBBox.attr('x', margins.left)
.attr('y', graph_height + 6);
}
@ -501,9 +496,9 @@ function draw_plot(svg_id) {
tooltip_concentration[scenario].attr('x', 18);
}
else {
tooltip_rect[scenario].attr('x', -90)
tooltip_time[scenario].attr('x', -82)
tooltip_concentration[scenario].attr('x', -82)
tooltip_rect[scenario].attr('x', -10 - tooltip_rect[scenario].attr('width'))
tooltip_time[scenario].attr('x', -2 - tooltip_rect[scenario].attr('width'))
tooltip_concentration[scenario].attr('x', -2 - tooltip_rect[scenario].attr('width'))
}
}
// Concentration line
@ -542,26 +537,27 @@ function draw_plot(svg_id) {
});
}
// Generate the alternative scenarios plot using d3 library.
// 'alternative_scenarios' is a dictionary with all the alternative scenarios
// Generate a scenarios plot using d3 library.
// 'list_of_scenarios' is a dictionary with all the scenarios
// 'times' is a list of times for all the scenarios
// The method is prepared to consider short-range interactions if needed.
function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_plot_svg_id) {
function draw_generic_concentration_plot(
plot_svg_id,
y_axis_label,
h_lines,
) {
list_of_scenarios = (plot_svg_id === 'CO2_concentration_graph') ? CO2_concentrations : alternative_scenarios
// 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;
// Used for controlling the short-range interactions
let button_full_exposure = document.getElementById("button_alternative_full_exposure");
let button_hide_high_concentration = document.getElementById("button_alternative_hide_high_concentration");
var colors = d3.schemeCategory10;
// 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;
for (scenario in list_of_scenarios) {
scenario_concentrations = list_of_scenarios[scenario].concentrations;
highest_concentration = Math.max(highest_concentration, Math.max(...scenario_concentrations))
@ -576,8 +572,8 @@ function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_
var first_scenario = Object.values(data_for_scenarios)[0]
// Add main SVG element
var alternative_plot_div = document.getElementById(alternative_plot_svg_id);
var vis = d3.select(alternative_plot_div).append('svg');
var plot_div = document.getElementById(plot_svg_id);
var vis = d3.select(plot_div).append('svg');
var xRange = d3.scaleTime().domain([first_scenario[0].hour, first_scenario[first_scenario.length - 1].hour]);
var xTimeRange = d3.scaleLinear().domain([times[0], times[times.length - 1]]);
@ -599,20 +595,23 @@ function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_
// Y axis declaration.
var yAxisEl = vis.append('svg:g')
.attr('class', 'y axis');
.attr('class', 'y axis');
// Y axis label.
var yAxisLabelEl = vis.append('svg:text')
.attr('class', 'y label')
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.text('Mean concentration (virions/m³)');
.text(y_axis_label);
// Legend bounding box.
max_key_length = Math.max(...(Object.keys(data_for_scenarios).map(el => el.length)));
let h_lines_lenght = h_lines ? h_lines.length : 0
let h_line_max_key = h_lines ? Math.max(...(h_lines.map(el => el.label.length))) : 0
max_key_length = Math.max(Math.max(...(Object.keys(data_for_scenarios).map(el => el.length))), h_line_max_key);
var legendBBox = vis.append('rect')
.attr('width', 8.25 * max_key_length )
.attr('height', 25 * (Object.keys(data_for_scenarios).length))
.attr('width', 9 * max_key_length )
.attr('height', 25 * ((Object.keys(data_for_scenarios).length) + h_lines_lenght))
.attr('stroke', 'lightgrey')
.attr('stroke-width', '2')
.attr('rx', '5px')
@ -649,13 +648,31 @@ function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_
label_text[scenario_name] = vis.append('text')
.text(scenario_name)
.style('font-size', '15px');
}
if (h_lines) {
var h_lines_draw = {}, h_line_label_icon = {}, h_line_label_text = {};
h_lines.map((line) => {
h_lines_draw[line.label] = draw_area.append('svg:line')
.attr('stroke', line.color)
.attr('stroke-width', 2)
.attr('stroke-dasharray', line.style == 'dashed' ? (5, 5) : 0)
// Legend for each of the horizontal lines
h_line_label_icon[line.label] = vis.append('line')
.style("stroke-dasharray", line.style == 'dashed' ? (5, 5) : 0)
.attr('stroke-width', '2')
.style("stroke", line.color);
h_line_label_text[line.label] = vis.append('text')
.text(line.label)
.style('font-size', '15px');
})
}
// Tooltip.
var focus = {}, tooltip_rect = {}, tooltip_time = {}, tooltip_concentration = {}, toolBox = {};
for (const [scenario_name, data] of Object.entries(data_for_scenarios)) {
let data_length = String(Math.floor(Math.max(...data.map(el => el.concentration !== undefined ? el.concentration : 0)))).length;
focus[scenario_name] = vis.append('svg:g')
.style('display', 'none');
@ -665,7 +682,7 @@ function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_
tooltip_rect[scenario_name] = focus[scenario_name].append('rect')
.attr('fill', 'white')
.attr('stroke', '#000')
.attr('width', 80)
.attr('width', 65 + data_length * 8)
.attr('height', 50)
.attr('y', -22)
.attr('rx', 4)
@ -687,11 +704,12 @@ function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_
.on('mousemove', mousemove);
}
function update_alternative_concentration_plot(concentration_data) {
function update_concentration_plot(concentration_data) {
list_of_scenarios = (plot_svg_id === 'CO2_concentration_graph') ? CO2_concentrations : alternative_scenarios
var highest_concentration = 0.
for (scenario in alternative_scenarios) {
scenario_concentrations = alternative_scenarios[scenario][concentration_data];
for (scenario in list_of_scenarios) {
scenario_concentrations = list_of_scenarios[scenario][concentration_data];
highest_concentration = Math.max(highest_concentration, Math.max(...scenario_concentrations));
}
@ -705,29 +723,39 @@ function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_
.y(d => yRange(d.concentration));
draw_lines[scenario_name].transition().duration(1000).attr("d", lineFuncs[scenario_name](data));
}
if (h_lines) {
h_lines.map((line) => {
h_lines_draw[line.label]
.attr("x1", xTimeRange(times[0]))
.attr("y1", yRange(line.y))
.attr("x2", xTimeRange(times[times.length - 1]))
.attr("y2", yRange(line.y));
})
}
}
var graph_width;
var graph_height;
function redraw() {
// Define width and height according to the screen size.
var div_width = document.getElementById(concentration_plot_svg_id).clientWidth;
var div_height = document.getElementById(concentration_plot_svg_id).clientHeight;
// Define width and height according to the screen size. Always use an already defined
var window_width = document.getElementById('concentration_plot').clientWidth;
var div_width = window_width;
var div_height = document.getElementById('concentration_plot').clientHeight;
graph_width = div_width;
graph_height = div_height
if (div_width >= 1200) { // For screens with width > 900px legend can be on the graph's right side.
var margins = { top: 30, right: 20, bottom: 50, left: 60 };
div_width = 1200;
graph_width = 600;
graph_height = div_height;
var margins = { top: 30, right: 20, bottom: 50, left: 60 };
if (window_width >= 900) { // For screens with width > 900px legend can be on the graph's right side.
div_width = 900;
graph_width = div_width * (2/3);
const svg_margins = {'margin-left': '0rem'};
Object.entries(svg_margins).forEach(([prop,val]) => vis.style(prop,val));
}
else {
var margins = { top: 30, right: 20, bottom: 50, left: 40 };
div_width = div_width * 1.1
graph_width = div_width * .95;
graph_height = div_height * 0.65; // On mobile screen sizes we want the legend to be on the bottom of the graph.
graph_width = div_width * .9;
graph_height = div_height * .75; // On mobile screen sizes we want the legend to be on the bottom of the graph.
const svg_margins = {'margin-left': '-1rem'};
Object.entries(svg_margins).forEach(([prop,val]) => vis.style(prop,val));
};
@ -748,31 +776,6 @@ function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_
xTimeRange.range([margins.left, graph_width - margins.right]);
yRange.range([graph_height - margins.bottom, margins.top]);
var legend_x_start = 25;
const space_between_text_icon = 30;
const text_height = 6;
for (const [scenario_name, data] of Object.entries(data_for_scenarios)) {
var scenario_index = Object.keys(data_for_scenarios).indexOf(scenario_name)
// Legend on right side.
var size = 20 * (scenario_index + 1);
if (document.getElementById(concentration_plot_svg_id).clientWidth >= 900) {
label_icons[scenario_name].attr('x', graph_width + legend_x_start)
.attr('y', margins.top + size);
label_text[scenario_name].attr('x', graph_width + legend_x_start + space_between_text_icon)
.attr('y', margins.top + size + text_height);
}
// Legend on the bottom.
else {
legend_x_start = 10;
label_icons[scenario_name].attr('x', legend_x_start)
.attr('y', graph_height + size);
label_text[scenario_name].attr('x', legend_x_start + space_between_text_icon)
.attr('y', graph_height + size + text_height);
}
}
// Axis.
var xAxis = d3.axisBottom(xRange).tickFormat(d => time_format(d));
yAxis.scale(yRange);
@ -784,19 +787,66 @@ function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_
yAxisEl.attr('transform', 'translate(' + margins.left + ',0)')
.call(yAxis);
yAxisLabelEl.attr('transform', 'rotate(-90, 0,' + graph_height + ')')
.attr('x', (graph_height * 0.9 + margins.bottom) / 2)
.attr('y', (graph_height + margins.left) * 0.90);
yAxisLabelEl.attr('x', (graph_height + margins.bottom * .55) / 2)
.attr('y', (graph_height + margins.left) * 0.9)
.attr('transform', 'rotate(-90, 0,' + graph_height + ')');
// Legend items
var legend_x_start = 25;
const space_between_text_icon = 30;
const text_height = 6;
for (const [scenario_name, data] of Object.entries(data_for_scenarios)) {
var scenario_index = Object.keys(data_for_scenarios).indexOf(scenario_name)
// Legend on right side.
var size = 20 * (scenario_index + 1);
if (window_width >= 900) {
label_icons[scenario_name].attr('x', graph_width + legend_x_start)
.attr('y', margins.top + size);
label_text[scenario_name].attr('x', graph_width + legend_x_start + space_between_text_icon)
.attr('y', margins.top + size + text_height);
}
// Legend on the bottom.
else {
legend_x_start = margins.left + 10;
label_icons[scenario_name].attr('x', legend_x_start)
.attr('y', graph_height + size);
label_text[scenario_name].attr('x', legend_x_start + space_between_text_icon)
.attr('y', graph_height + size + text_height);
}
}
if (h_lines) {
h_lines.map((line, index) => {
size = 21 * (scenario_index + index + 2); // account for previous legend elements
if (window_width >= 900) {
h_line_label_icon[line.label].attr("x1", graph_width + legend_x_start)
.attr("x2", graph_width + legend_x_start + 20)
.attr("y1", margins.top + size)
.attr("y2", margins.top + size);
h_line_label_text[line.label].attr('x', graph_width + legend_x_start + space_between_text_icon)
.attr('y', margins.top + size + text_height);
}
// Legend on the bottom.
else {
legend_x_start = margins.left + 10;
h_line_label_icon[line.label].attr("x1", legend_x_start)
.attr("x2", legend_x_start + 20)
.attr("y1", graph_height + size)
.attr("y2", graph_height + size);
h_line_label_text[line.label].attr('x', legend_x_start + space_between_text_icon)
.attr('y', graph_height + size + text_height);
}
})
}
// Legend on right side.
if (document.getElementById(concentration_plot_svg_id).clientWidth >= 900) {
if (window_width >= 900) {
legendBBox.attr('x', graph_width * 1.02)
.attr('y', margins.top * 1.15);
}
// Legend on the bottom.
else {
legendBBox.attr('x', 1)
legendBBox.attr('x', margins.left)
.attr('y', graph_height * 1.02)
}
@ -807,21 +857,6 @@ function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_
}
}
if (button_full_exposure) {
button_full_exposure.addEventListener("click", () => {
update_alternative_concentration_plot('concentrations');
button_full_exposure.disabled = true;
button_hide_high_concentration.disabled = false;
});
}
if (button_hide_high_concentration) {
button_hide_high_concentration.addEventListener("click", () => {
update_alternative_concentration_plot('concentrations_zoomed');
button_full_exposure.disabled = false;
button_hide_high_concentration.disabled = true;
});
}
function mousemove() {
for (const [scenario_name, data] of Object.entries(data_for_scenarios)) {
if (d3.pointer(event)[0] < graph_width / 2) {
@ -830,9 +865,9 @@ function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_
tooltip_concentration[scenario_name].attr('x', 18);
}
else {
tooltip_rect[scenario_name].attr('x', -90)
tooltip_time[scenario_name].attr('x', -82)
tooltip_concentration[scenario_name].attr('x', -82)
tooltip_rect[scenario_name].attr('x', -10 - tooltip_rect[scenario_name].attr('width'))
tooltip_time[scenario_name].attr('x', - tooltip_rect[scenario_name].attr('width'))
tooltip_concentration[scenario_name].attr('x', - tooltip_rect[scenario_name].attr('width'))
}
var x0 = xRange.invert(d3.pointer(event, this)[0]),
i = bisecHour(data, x0, 1),
@ -847,13 +882,12 @@ function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_
// Draw for the first time to initialize.
redraw();
update_alternative_concentration_plot('concentrations');
update_concentration_plot('concentrations');
// Redraw based on the new size whenever the browser window is resized.
window.addEventListener("resize", e => {
redraw();
if (button_full_exposure && button_full_exposure.disabled) update_alternative_concentration_plot('concentrations');
else update_alternative_concentration_plot('concentrations')
update_concentration_plot('concentrations');
});
}
@ -1130,6 +1164,7 @@ function export_csv() {
var column_name = has_rename != '' ? has_rename : e.id;
if (e.id == "Time") checked_names.push(`${column_name} \u2028(h)`);
else if (e.id == "Concentration") checked_names.push(`${column_name} \u2028(virions m⁻³)`);
else if (e.id == "CO2_Concentration") checked_names.push(`${column_name} \u2028(ppm)`);
checked_items.push(e.id);
}
}
@ -1143,6 +1178,7 @@ function export_csv() {
checked_items.includes("Concentration") && this_row.push(concentrations[i]);
checked_items.includes("Cumulative Dose") && this_row.push(cumulative_doses[i]);
checked_items.includes("Long-Range Dose") && this_row.push(long_range_cumulative_doses[i]);
checked_items.includes("CO2_Concentration") && this_row.push(CO2_concentrations[Object.keys(CO2_concentrations)[0]].concentrations[i]);
if (has_alternative_scenario) {
Object.entries(alternative_scenarios).map((scenario) => {
if (scenario[0] != 'Current scenario') {
@ -1163,4 +1199,4 @@ function export_csv() {
link.setAttribute("download", "report_data.csv");
document.body.appendChild(link);
link.click();
}
}

View file

@ -224,6 +224,42 @@
</div>
</div>
<div class="card bg-light mb-3">
<div class="card-header"><strong>Predictive CO₂ Concentration Profile</strong>
<button class="icon_button p-0 float-right" data-toggle="collapse" href="#collapseCO₂Profile" role="button" aria-expanded="false" aria-controls="collapseCO₂Profile">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-expand" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z"/>
</svg>
</button>
</div>
<div class="collapse" id="collapseCO₂Profile">
<div class="card-body">
<div>
<div id="CO2_concentration_graph" style="height: 400px"></div>
<script type="application/javascript">
var CO2_concentrations = {{ CO2_concentrations | JSONify }}
draw_generic_concentration_plot(
"CO2_concentration_graph",
"Mean concentration (ppm)",
h_lines = [
{'label': 'Acceptable level',
'y': 800,
'color': 'forestgreen',
'style': 'dashed'
},
{'label': 'Insufficient level',
'y': 1500,
'color': 'firebrick',
'style': 'dashed'
},
]
);
</script>
</div>
</div>
</div>
</div>
{% if form.short_range_option == "short_range_no" %}
<div class="card bg-light mb-3">
<div class="card-header"><strong>Alternative scenarios</strong>
@ -236,16 +272,13 @@
<div class="collapse" id="collapseAlternativeScenarios">
<div class="card-body">
<div>
{% if form.short_range_option == "short_range_yes" %}
{% if 'Speaking' in form.short_range_interactions|string or 'Shouting' in form.short_range_interactions|string %}
<button class="btn btn-sm btn-primary" id="button_alternative_full_exposure" disabled>Show full exposure</button>
<button class="btn btn-sm btn-primary ml-0" id="button_alternative_hide_high_concentration">Hide high concentration</button>
{% endif %}
{% endif %}
<div id="alternative_scenario_plot" style="height: 400px"></div>
<script type="application/javascript">
let alternative_scenarios = {{ alternative_scenarios.stats | JSONify }};
draw_alternative_scenarios_plot("concentration_plot", "alternative_scenario_plot");
var alternative_scenarios = {{ alternative_scenarios.stats | JSONify }}
draw_generic_concentration_plot(
"alternative_scenario_plot",
"Mean concentration (virions/m³)",
);
</script>
<br>
{% block report_scenarios_summary_table %}
@ -283,6 +316,7 @@
</div>
</div>
{% endif %}
{% endblock report_results %}
<!-- Export Data Concentration Modal -->
@ -327,6 +361,15 @@
<input type="text" class="form-control form-control-sm col-sm-4 ml-2" id="Cumulative Dose__rename" placeholder="Cumulative Dose" value="Cumulative Dose">
</div>
</div>
</li>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="checkedItems" id="CO2_Concentration" onclick="display_rename_column(this.checked, 'CO2-rename-div')">
<label class="form-check-label" for="CO2_Concentration">CO₂ Concentration</label>
<div id="CO2-rename-div" class="align-items-center" style="display:none">
<label class="col-form-label" for="CO2-rename-div">Column name:</label>
<input type="text" class="form-control form-control-sm col-sm-4 ml-2" id="CO2_Concentration__rename" placeholder="CO₂ Concentration" value="CO₂ Concentration">
</div>
</div>
{% if form.short_range_option == "short_range_no" %}
</li>
<li>