/* Generate the concentration plot using d3 library. */ function draw_plot(svg_id) { // Used for controlling the short-range interactions let button_full_exposure = document.getElementById("button_full_exposure"); let button_hide_high_concentration = document.getElementById("button_hide_high_concentration"); let long_range_checkbox = document.getElementById('long_range_cumulative_checkbox') let show_sr_legend = short_range_expirations.length > 0; var data_for_graphs = { 'concentrations': [], 'cumulative_doses': [], 'long_range_cumulative_doses': [], } times.map((time, index) => data_for_graphs.concentrations.push({ 'time': time, 'hour': new Date().setHours(Math.trunc(time), (time - Math.trunc(time)) * 60), 'concentration': concentrations[index]})); times.map((time, index) => data_for_graphs.cumulative_doses.push({ 'time': time, 'hour': new Date().setHours(Math.trunc(time), (time - Math.trunc(time)) * 60), 'concentration': cumulative_doses[index]})); times.map((time, index) => data_for_graphs.long_range_cumulative_doses.push({ 'time': time, 'hour': new Date().setHours(Math.trunc(time), (time - Math.trunc(time)) * 60), 'concentration': long_range_cumulative_doses[index]})); const tooltip_data_for_graphs = Object.fromEntries(Object.entries(data_for_graphs).filter(([key]) => !key.includes('long_range_cumulative_doses'))); // Add main SVG element var plot_div = document.getElementById(svg_id); var vis = d3.select(plot_div).append('svg'); var time_format = d3.timeFormat('%H:%M'); // H:M time format for x axis. xRange = d3.scaleTime().domain([data_for_graphs.concentrations[0].hour, data_for_graphs.concentrations[data_for_graphs.concentrations.length - 1].hour]), xTimeRange = d3.scaleLinear().domain([data_for_graphs.concentrations[0].time, data_for_graphs.concentrations[data_for_graphs.concentrations.length - 1].time]), bisecHour = d3.bisector((d) => { return d.hour; }).left, yRange = d3.scaleLinear(), yCumulativeRange = d3.scaleLinear(), yAxis = d3.axisLeft(), yCumulativeAxis = d3.axisRight(); // X axis declaration. var xAxisEl = vis.append('svg:g') .attr('class', 'x axis'); // X axis label. var xAxisLabelEl = vis.append('text') .attr('class', 'x label') .attr('fill', 'black') .attr('text-anchor', 'middle') .text('Time of day') // Y axis declaration. var yAxisEl = vis.append('svg:g') .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³)'); // Y cumulative concentration axis declaration. var yAxisCumEl = vis.append('svg:g') .attr('class', 'y axis') .style("stroke-dasharray", "5 5"); // Y cumulated concentration axis label. var yAxisCumLabelEl = vis.append('svg:text') .attr('class', 'y label') .attr('fill', 'black') .attr('text-anchor', 'middle') .text('Mean cumulative dose (infectious virus)'); // Legend for the plot elements - line and area. // Concentration line icon var legendLineIcon = vis.append('rect') .attr('width', 20) .attr('height', 3) .style('fill', '#1f77b4'); // Concentration line text var legendLineText = vis.append('text') .text('Mean concentration') .style('font-size', '15px'); // Cumulative dose line icon var legendCumulativeIcon = vis.append('line') .style("stroke-dasharray", "5 5") //dashed array for line .attr('stroke-width', '2') .style("stroke", '#1f77b4'); // Cumulative dose line text var legendCumutiveText = vis.append('text') .text('Cumulative dose') .style('font-size', '15px'); // Area line icon var legendAreaIcon = vis.append('rect') .attr('width', 20) .attr('height', 15) .attr('fill', '#1f77b4') .attr('fill-opacity', '0.1'); // Area line text var legendAreaText = vis.append('text') .text('Presence of exposed person(s)') .style('font-size', '15px'); sr_unique_activities = [...new Set(short_range_expirations)] if (show_sr_legend) { // Long-range cumulative dose line legend - line and area var legendLongCumulativeIcon = vis.append('line') .style("stroke-dasharray", "5 5") //dashed array for line .attr('stroke-width', '2') .style("stroke", 'purple') .attr('opacity', 0); var legendLongCumutiveText = vis.append('text') .text('Long-range cumulative dose') .style('font-size', '15px') .attr('opacity', 0); // Short-range area icon var legendShortRangeAreaIcon = {}; sr_unique_activities.forEach((b, index) => { legendShortRangeAreaIcon[index] = vis.append('rect') .attr('width', 20) .attr('height', 15); // Short-range area icon colors if (sr_unique_activities[index] == 'Breathing') legendShortRangeAreaIcon[index].attr('fill', 'red').attr('fill-opacity', '0.2'); else if (sr_unique_activities[index] == 'Speaking') legendShortRangeAreaIcon[index].attr('fill', 'green').attr('fill-opacity', '0.1'); else legendShortRangeAreaIcon[index].attr('fill', 'blue').attr('fill-opacity', '0.1'); }); // Short-range area text var legendShortRangeText = {}; sr_unique_activities.forEach((b, index) => { legendShortRangeText[index] = vis.append('text') .text('Short-range - ' + sr_unique_activities[index]) .style('font-size', '15px'); }); } // Legend bounding if (show_sr_legend) legendBBox_height = 68 + 20 * sr_unique_activities.length; else legendBBox_height = 68; var legendBBox = vis.append('rect') .attr('width', 255) .attr('height', legendBBox_height) .attr('stroke', 'lightgrey') .attr('stroke-width', '2') .attr('rx', '5px') .attr('ry', '5px') .attr('stroke-linejoin', 'round') .attr('fill', 'none'); var clip = vis.append("defs").append("svg:clipPath") .attr("id", "clip") .append("svg:rect"); var draw_area = vis.append('svg:g') .attr('clip-path', 'url(#clip)'); // Line representing the mean concentration. var lineFunc = d3.line(); draw_area.append('svg:path') .attr('class', 'line') .attr('stroke', '#1f77b4') .attr('stroke-width', 2) .attr('fill', 'none'); // Line representing the cumulative concentration. var lineCumulative = d3.line(); var draw_cumulative_line = draw_area.append('svg:path') .attr('stroke', '#1f77b4') .attr('stroke-width', 2) .style("stroke-dasharray", "5 5") .attr('fill', 'none'); // Line representing the long-range cumulative concentration. if (show_sr_legend) { var longRangeCumulative = d3.line(); var draw_long_range_cumulative_line = draw_area.append('svg:path') .attr('stroke', 'purple') .attr('stroke-width', 2) .style("stroke-dasharray", "5 5") .attr('fill', 'none') .attr('opacity', 0); } // Area representing the presence of exposed person(s). var exposedArea = {}; var drawArea = {}; exposed_presence_intervals.forEach((b, index) => { exposedArea[index] = d3.area(); drawArea[index] = draw_area.append('svg:path') .attr('fill', '#1f77b4') .attr('fill-opacity', '0.1'); }); // Area representing the short-range interaction(s). var shortRangeArea = {}; var drawShortRangeArea = {}; short_range_intervals.forEach((b, index) => { shortRangeArea[index] = d3.area(); drawShortRangeArea[index] = draw_area.append('svg:path'); if (short_range_expirations[index] == 'Breathing') drawShortRangeArea[index].attr('fill', 'red').attr('fill-opacity', '0.2'); else if (short_range_expirations[index] == 'Speaking') drawShortRangeArea[index].attr('fill', 'green').attr('fill-opacity', '0.1'); else drawShortRangeArea[index].attr('fill', 'blue').attr('fill-opacity', '0.1'); }); // Tooltip. var focus = {}, tooltip_rect = {}, tooltip_time = {}, tooltip_concentration = {}, toolBox = {}; for (const [concentration, data] of Object.entries(tooltip_data_for_graphs)) { focus[concentration] = vis.append('svg:g') .style('display', 'none'); focus[concentration].append('circle') .attr('r', 3); tooltip_rect[concentration] = focus[concentration].append('rect') .attr('fill', 'white') .attr('stroke', '#000') .attr('width', 85) .attr('height', 50) .attr('y', -22) .attr('rx', 4) .attr('ry', 4); tooltip_time[concentration] = focus[concentration].append('text') .attr('id', 'tooltip-time') .attr('x', 18) .attr('y', -2); tooltip_concentration[concentration] = focus[concentration].append('text') .attr('id', 'tooltip-concentration') .attr('x', 18) .attr('y', 18); toolBox[concentration] = vis.append('rect') .attr('fill', 'none') .attr('pointer-events', 'all') .on('mouseover', () => { for (const [concentration, data] of Object.entries(focus)) focus[concentration].style('display', null); }) .on('mouseout', () => { for (const [concentration, data] of Object.entries(focus)) focus[concentration].style('display', 'none'); }) .on('mousemove', mousemove);; } function update_concentration_plot(concentration_data, cumulative_data) { yRange.domain([0., Math.max(...concentration_data)*1.1]); yAxisEl.transition().duration(1000).call(yAxis); yCumulativeRange.domain([0., Math.max(...cumulative_data)*1.1]); yAxisCumEl.transition().duration(1000).call(yCumulativeAxis) // Concentration line lineFunc.defined(d => !isNaN(d.concentration)) .x(d => xTimeRange(d.time)) .y(d => yRange(d.concentration)); draw_area.select('.line') .transition() .duration(1000) .attr("d", lineFunc(data_for_graphs.concentrations)); // Cumulative line. lineCumulative.defined(d => !isNaN(d.concentration)) .x(d => xTimeRange(d.time)) .y(d => yCumulativeRange(d.concentration)); draw_cumulative_line.transition() .duration(1000) .attr("d", lineCumulative(data_for_graphs.cumulative_doses)); // Long-range cumulative line. if (show_sr_legend) { longRangeCumulative.defined(d => !isNaN(d.concentration)) .x(d => xTimeRange(d.time)) .y(d => yCumulativeRange(d.concentration)); draw_long_range_cumulative_line.transition() .duration(1000) .attr("d", lineCumulative(data_for_graphs.long_range_cumulative_doses)); } // Area. exposed_presence_intervals.forEach((b, index) => { exposedArea[index].x(d => xTimeRange(d.time)) .y0(graph_height - 50) .y1(d => yRange(d.concentration) ); drawArea[index].transition().duration(1000).attr('d', exposedArea[index](data_for_graphs.concentrations.filter(d => { return d.time >= b[0] && d.time <= b[1] }))); }); // Short-Range Area. short_range_intervals.forEach((b, index) => { shortRangeArea[index].x(d => xTimeRange(d.time)) .y0(graph_height - 50) .y1(d => yRange(d.concentration)); drawShortRangeArea[index].transition().duration(1000).attr('d', shortRangeArea[index](data_for_graphs.concentrations.filter(d => { return d.time >= b[0] && d.time <= b[1] }))); }); } function redraw() { // Define width and height according to the screen size. var div_width = plot_div.clientWidth; var div_height = plot_div.clientHeight; graph_width = div_width; graph_height = div_height 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. const svg_margins = {'margin-left': '-1rem'}; Object.entries(svg_margins).forEach(([prop,val]) => vis.style(prop,val)); }; // Use the extracted size to set the size of the SVG element. vis.attr("width", div_width) .attr('height', div_height); // SVG components according to the width and height. // clipPath: everything out of this area won't be drawn. clip.attr("x", margins.left) .attr("y", margins.top) .attr("width", graph_width - margins.right - margins.left) .attr("height", graph_height - margins.top - margins.bottom); // Axis ranges. xRange.range([margins.left, graph_width - margins.right]); xTimeRange.range([margins.left, graph_width - margins.right]); yRange.range([graph_height - margins.bottom, margins.top]); yCumulativeRange.range([graph_height - margins.bottom, margins.top]); // Axis. var xAxis = d3.axisBottom(xRange).tickFormat(d => time_format(d)); yAxis.scale(yRange); yCumulativeAxis.scale(yCumulativeRange); xAxisEl.attr('transform', 'translate(0,' + (graph_height - margins.bottom) + ')') .call(xAxis); xAxisLabelEl.attr('x', (graph_width + margins.right) / 2) .attr('y', graph_height * 0.97); yAxisEl.attr('transform', 'translate(' + margins.left + ',0)'); yAxisLabelEl.attr('x', (graph_height * 0.9 + margins.bottom) / 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); 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); } else { yAxisCumLabelEl.attr('transform', 'rotate(-90, 0,' + graph_height + ')') .attr('x', (graph_height + margins.bottom * 0.55) / 2) .attr('y', graph_width + 290); } const size = 20; var legend_x_start = 50; const space_between_text_icon = 30; const text_height = 6; // Legend on right side. if (plot_div.clientWidth >= 900) { legendLineIcon.attr('x', graph_width + legend_x_start) .attr('y', margins.top + size); legendLineText.attr('x', graph_width + legend_x_start + space_between_text_icon) .attr('y', margins.top + size + text_height); legendCumulativeIcon.attr("x1", graph_width + legend_x_start) .attr("x2", graph_width + legend_x_start + 20) .attr("y1", margins.top + 2 * size) .attr("y2", margins.top + 2 * size); legendCumutiveText.attr('x', graph_width + legend_x_start + space_between_text_icon) .attr('y', margins.top + 2 * size + text_height); legendAreaIcon.attr('x', graph_width + legend_x_start) .attr('y', margins.top + (3 * size) - 15/2); legendAreaText.attr('x', graph_width + legend_x_start + space_between_text_icon) .attr('y', margins.top + 3 * size + text_height); if (show_sr_legend) { sr_unique_activities.forEach((b, index) => { legendShortRangeAreaIcon[index].attr('x', graph_width + legend_x_start) .attr('y', margins.top + (4 + index) * size - 15/2); legendShortRangeText[index].attr('x', graph_width + legend_x_start + space_between_text_icon) .attr('y', margins.top + (4 + index) * size + text_height); }); legendLongCumulativeIcon.attr("x1", graph_width + legend_x_start) .attr("x2", graph_width + legend_x_start + 20) .attr("y1", margins.top + (4 + sr_unique_activities.length) * size) .attr("y2", margins.top + (4 + sr_unique_activities.length) * size); legendLongCumutiveText.attr('x', graph_width + legend_x_start + space_between_text_icon) .attr('y', margins.top + (4 + sr_unique_activities.length) * size + + text_height); } legendBBox.attr('x', graph_width * 1.07) .attr('y', margins.top * 1.2); } // Legend on the bottom. else { legend_x_start = 10; legendLineIcon.attr('x', legend_x_start) .attr('y', graph_height + size); legendLineText.attr('x', legend_x_start + space_between_text_icon) .attr('y', graph_height + size + text_height); legendCumulativeIcon.attr("x1", legend_x_start) .attr("x2", legend_x_start + 20) .attr("y1", graph_height + 2 * size) .attr("y2", graph_height + 2 * size); legendCumutiveText.attr('x', legend_x_start + space_between_text_icon) .attr('y', graph_height + 2 * size + text_height); legendAreaIcon.attr('x', legend_x_start) .attr('y', graph_height + 3 * size - 15/2); legendAreaText.attr('x', legend_x_start + space_between_text_icon) .attr('y', graph_height + 3 * size + text_height); if (show_sr_legend) { sr_unique_activities.forEach((b, index) => { legendShortRangeAreaIcon[index].attr('x', legend_x_start) .attr('y', graph_height + (4 + index) * size - 15/2); legendShortRangeText[index].attr('x', legend_x_start + space_between_text_icon) .attr('y', graph_height + (4 + index) * size + text_height); }); legendLongCumulativeIcon.attr("x1", legend_x_start) .attr("x2", legend_x_start + 20) .attr("y1", graph_height + (4 + sr_unique_activities.length) * size) .attr("y2", graph_height + (4 + sr_unique_activities.length) * size) legendLongCumutiveText.attr('x', legend_x_start + space_between_text_icon) .attr('y', graph_height + (4 + sr_unique_activities.length) * size + text_height); } legendBBox.attr('x', 1) .attr('y', graph_height + 6); } // ToolBox. for (const [concentration, data] of Object.entries(tooltip_data_for_graphs)) { toolBox[concentration].attr('width', graph_width - margins.right) .attr('height', graph_height); } } if (show_sr_legend) { long_range_checkbox.addEventListener("click", () => { if (long_range_checkbox.checked) { draw_long_range_cumulative_line.transition().duration(1000).attr("opacity", 1); legendBBox.transition().duration(1000).attr("height", legendBBox_height + 20); legendLongCumulativeIcon.transition().duration(1000).attr("opacity", 1); legendLongCumutiveText.transition().duration(1000).attr("opacity", 1); } else { draw_long_range_cumulative_line.transition().duration(1000).attr("opacity", 0); legendBBox.transition().duration(1000).attr("height", legendBBox_height); legendLongCumulativeIcon.transition().duration(1000).attr("opacity", 0); legendLongCumutiveText.transition().duration(1000).attr("opacity", 0); } }); }; if (button_full_exposure) { button_full_exposure.addEventListener("click", () => { update_concentration_plot(concentrations, cumulative_doses); button_full_exposure.disabled = true; button_hide_high_concentration.disabled = false; }); } if (button_hide_high_concentration) { button_hide_high_concentration.addEventListener("click", () => { update_concentration_plot(concentrations_zoomed, long_range_cumulative_doses); button_full_exposure.disabled = false; button_hide_high_concentration.disabled = true; }); } function mousemove() { for (const [scenario, data] of Object.entries(tooltip_data_for_graphs)) { if (d3.pointer(event)[0] < graph_width / 2) { tooltip_rect[scenario].attr('x', 10) tooltip_time[scenario].attr('x', 18) 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) } } // Concentration line var x0 = xRange.invert(d3.pointer(event, this)[0]), i = bisecHour(data_for_graphs.concentrations, x0, 1), d0 = data_for_graphs.concentrations[i - 1], d1 = data_for_graphs.concentrations[i]; if (d1) { var d = x0 - d0.hour > d1.hour - x0 ? d1 : d0; focus.concentrations.attr('transform', 'translate(' + xRange(d.hour) + ',' + yRange(d.concentration) + ')'); focus.concentrations.select('#tooltip-time').text('x = ' + time_format(d.hour)); focus.concentrations.select('#tooltip-concentration').text('y = ' + d.concentration.toFixed(2)); } // Cumulative line var x0 = xRange.invert(d3.pointer(event, this)[0]), i = bisecHour(data_for_graphs.cumulative_doses, x0, 1), d0 = data_for_graphs.cumulative_doses[i - 1], d1 = data_for_graphs.cumulative_doses[i]; if (d1 && d1.concentration) { var d = x0 - d0.hour > d1.hour - x0 ? d1 : d0; focus.cumulative_doses.attr('transform', 'translate(' + xRange(d.hour) + ',' + yCumulativeRange(d.concentration) + ')'); focus.cumulative_doses.select('#tooltip-time').text('x = ' + time_format(d.hour)); focus.cumulative_doses.select('#tooltip-concentration').text('y = ' + d.concentration.toFixed(2)); } } // Draw for the first time to initialize. redraw(); update_concentration_plot(concentrations, cumulative_doses); // 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_concentration_plot(concentrations, cumulative_doses); else update_concentration_plot(concentrations_zoomed, long_range_cumulative_doses) }); } // 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 // The method is prepared to consider short-range interactions if needed. function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_plot_svg_id) { // 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"); // 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] // Add main SVG element var alternative_plot_div = document.getElementById(alternative_plot_svg_id); var vis = d3.select(alternative_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]]); var bisecHour = d3.bisector((d) => { return d.hour; }).left; var yRange = d3.scaleLinear(); var yAxis = d3.axisLeft(); // X axis. var xAxisEl = vis.append('svg:g') .attr('class', 'x axis'); // X axis label. var xAxisLabelEl = vis.append('text') .attr('class', 'x label') .attr('fill', 'black') .attr('text-anchor', 'middle') .text('Time of day'); // Y axis declaration. var yAxisEl = vis.append('svg:g') .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³)'); // Legend bounding box. max_key_length = Math.max(...(Object.keys(data_for_scenarios).map(el => el.length))); var legendBBox = vis.append('rect') .attr('width', 8.25 * max_key_length ) .attr('height', 25 * (Object.keys(data_for_scenarios).length)) .attr('stroke', 'lightgrey') .attr('stroke-width', '2') .attr('rx', '5px') .attr('ry', '5px') .attr('stroke-linejoin', 'round') .attr('fill', 'none'); var clip = vis.append("defs").append("svg:clipPath") .attr("id", "clip") .append("svg:rect"); var draw_area = vis.append('svg:g') .attr('clip-path', 'url(#clip)'); // Line representing the mean concentration for each scenario. var lineFuncs = {}, draw_lines = {}, label_icons = {}, label_text = {}; 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. lineFuncs[scenario_name] = d3.line(); draw_lines[scenario_name] = draw_area.append('svg:path') .attr("stroke", colors[scenario_index]) .attr('stroke-width', 2) .attr('fill', 'none'); // Legend for the plot elements - lines. label_icons[scenario_name] = vis.append('rect') .attr('width', 20) .attr('height', 3) .style('fill', colors[scenario_index]); label_text[scenario_name] = vis.append('text') .text(scenario_name) .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)) { focus[scenario_name] = vis.append('svg:g') .style('display', 'none'); focus[scenario_name].append('circle') .attr('r', 3); tooltip_rect[scenario_name] = focus[scenario_name].append('rect') .attr('fill', 'white') .attr('stroke', '#000') .attr('width', 80) .attr('height', 50) .attr('y', -22) .attr('rx', 4) .attr('ry', 4); tooltip_time[scenario_name] = focus[scenario_name].append('text') .attr('id', 'tooltip-time') .attr('y', -2); tooltip_concentration[scenario_name] = focus[scenario_name].append('text') .attr('id', 'tooltip-concentration') .attr('y', 18); toolBox[scenario_name] = vis.append('rect') .attr('fill', 'none') .attr('pointer-events', 'all') .on('mouseover', () => { for (const [scenario_name, data] of Object.entries(focus)) focus[scenario_name].style('display', null); }) .on('mouseout', () => { for (const [scenario_name, data] of Object.entries(focus)) focus[scenario_name].style('display', 'none'); }) .on('mousemove', mousemove); } function update_alternative_concentration_plot(concentration_data) { var highest_concentration = 0. for (scenario in alternative_scenarios) { scenario_concentrations = alternative_scenarios[scenario][concentration_data]; highest_concentration = Math.max(highest_concentration, Math.max(...scenario_concentrations)); } yRange.domain([0., highest_concentration*1.1]); yAxisEl.transition().duration(1000).call(yAxis); for (const [scenario_name, data] of Object.entries(data_for_scenarios)) { // Lines. lineFuncs[scenario_name].defined(d => !isNaN(d.concentration)) .x(d => xTimeRange(d.time)) .y(d => yRange(d.concentration)); draw_lines[scenario_name].transition().duration(1000).attr("d", lineFuncs[scenario_name](data)); } } 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; 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; 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. const svg_margins = {'margin-left': '-1rem'}; Object.entries(svg_margins).forEach(([prop,val]) => vis.style(prop,val)); }; // Use the extracted size to set the size of the SVG element. vis.attr("width", div_width) .attr('height', div_height); // SVG components according to the width and height. // clipPath: everything out of this area won't be drawn. clip.attr("x", margins.left) .attr("y", margins.top) .attr("width", graph_width - margins.right - margins.left) .attr("height", graph_height - margins.top - margins.bottom); // Axis ranges. xRange.range([margins.left, graph_width - margins.right]); 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); xAxisEl.attr('transform', 'translate(0,' + (graph_height - margins.bottom) + ')') .call(xAxis); xAxisLabelEl.attr('x', (graph_width + margins.right) / 2) .attr('y', graph_height * 0.97) 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); // Legend on right side. if (document.getElementById(concentration_plot_svg_id).clientWidth >= 900) { legendBBox.attr('x', graph_width * 1.02) .attr('y', margins.top * 1.15); } // Legend on the bottom. else { legendBBox.attr('x', 1) .attr('y', graph_height * 1.02) } // ToolBox. for (const [scenario_name, data] of Object.entries(data_for_scenarios)) { toolBox[scenario_name].attr('width', graph_width - margins.right) .attr('height', graph_height); } } 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) { tooltip_rect[scenario_name].attr('x', 10) tooltip_time[scenario_name].attr('x', 18) 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) } 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[scenario_name].attr('transform', 'translate(' + xRange(d.hour) + ',' + yRange(d.concentration) + ')'); focus[scenario_name].select('#tooltip-time').text('x = ' + time_format(d.hour)); focus[scenario_name].select('#tooltip-concentration').text('y = ' + d.concentration.toFixed(2)); } } // Draw for the first time to initialize. redraw(); update_alternative_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') }); } function copy_clipboard(shareable_link) { $("#mobile_link").attr('title', 'Copied!') .tooltip('_fixTitle') .tooltip('show'); navigator.clipboard.writeText(shareable_link); } function check_download_button() { // Handle the disable property of the download button let download_button = document.getElementById('downloadCSV'); document.querySelectorAll('input[type="checkbox"]:checked').length <= 1 ? download_button.disabled = true : download_button.disabled = false; } function display_column_name_warning(checked) { let warning_element = document.getElementById("alternative_scenario_warning"); checked ? warning_element.style.display = 'flex' : warning_element.style.display = 'none'; } function display_rename_column(bool, id) { check_download_button(); // Change the visibility of renaming section if (bool) document.getElementById(id).style.display = 'flex'; else document.getElementById(id).style.display = 'none'; } function export_csv() { // This function generates a CSV file according to the user's input. // It is composed of a list of lists. // The first item of the main list corresponds to the columns' name. // The remaining items correspond to each of the file row, i.e. the // respective data from the selected inputs. let final_export = []; // Verify which items are checked let export_lists = document.getElementsByName('checkedItems'); let checked_items = []; // The column to be added, with the id to be identified. let checked_names = []; // The column with the respective rename. let has_alternative_scenario = false; export_lists.forEach(e => { if (e.checked) { if (e.id == "Alternative Scenarios") { Object.entries(alternative_scenarios).map((scenario) => { if (scenario[0] != 'Current scenario') { checked_names.push(`Alternative scenario concentration - ${scenario[0]} \u2028(virions m⁻³)`); has_alternative_scenario = true; }; }); } else if (e.id == "Cumulative Dose") { var has_rename = document.getElementById(`${e.id}__rename`).value; var column_name = has_rename != '' ? has_rename : e.id; if (short_range_expirations.length > 0) { checked_names.push(`Long-Range ${column_name} \u2028(infectious virus)`); checked_items.push('Long-Range Dose'); // When we have short range interactions, we want the column for the cumulative dose to have the "Total" word before the column name checked_names.push(`Total ${column_name} \u2028(infectious virus)`); } else { checked_names.push(`${column_name} \u2028(infectious virus)`); } checked_items.push(e.id); } else { var has_rename = document.getElementById(`${e.id}__rename`).value; 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⁻³)`); checked_items.push(e.id); } } }); final_export.push(checked_names); // Add data for each column. times.forEach((e, i) => { let this_row = []; checked_items.includes("Time") && this_row.push(times[i].toFixed(2)); 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]); if (has_alternative_scenario) { Object.entries(alternative_scenarios).map((scenario) => { if (scenario[0] != 'Current scenario') { this_row.push(scenario[1].concentrations[i]); }; }); }; final_export.push(this_row); }); // Prepare the CSV file. let csvContent = "data:text/csv;charset=utf8," + final_export.map(e => e.join(",")).join("\n"); var encodedUri = encodeURI(csvContent); // Set a name for the file. var link = document.createElement("a"); link.setAttribute("href", encodedUri); link.setAttribute("download", "report_data.csv"); document.body.appendChild(link); link.click(); }