diff --git a/cara/apps/calculator/static/css/report.css b/cara/apps/calculator/static/css/report.css index c9b2b3ab..b8f6ae17 100644 --- a/cara/apps/calculator/static/css/report.css +++ b/cara/apps/calculator/static/css/report.css @@ -136,11 +136,9 @@ p.notes { .card { page-break-inside: avoid; } - /* CSS styling to avoid page breaks. */ - .break-after { - page-break-after: always; - } - .break-avoid { + #disclaimer { + border: 2px solid black; + padding: 15px; page-break-inside: avoid; } } @@ -154,7 +152,8 @@ p.notes { position: relative; text-align: center; border-radius: 100px; - z-index: 1 + z-index: 1; + -webkit-print-color-adjust: exact!important; } .intro-banner-vdo-play-btn i { @@ -227,7 +226,7 @@ p.notes { .split>* { flex-basis: 100%; } - .header-text { + .paragraph-title { text-align: left; } .split>*+* { diff --git a/cara/apps/calculator/static/js/form.js b/cara/apps/calculator/static/js/form.js index abb888c5..88cf09e3 100644 --- a/cara/apps/calculator/static/js/form.js +++ b/cara/apps/calculator/static/js/form.js @@ -365,6 +365,14 @@ function validate_form(form) { } } + if (submit) { + $("#generate_report").prop("disabled", true); + //Add spinner to button + $("#generate_report").html( + `Loading...` + ); + } + return submit; } @@ -479,12 +487,17 @@ function parseTimeToMins(cTime) { return parseInt(time[1]*60) + parseInt(time[2]); } +// Prevent spinner when clicking on back button +window.onpagehide = function(){ + $('loading_spinner').remove(); + $("#generate_report").prop("disabled", false).html(`Generate report`); +}; + /* -------On Load------- */ $(document).ready(function () { var url = new URL(decodeURIComponent(window.location.href)); //Pre-fill form with known values url.searchParams.forEach((value, name) => { - //If element exists if(document.getElementsByName(name).length > 0) { var elemObj = document.getElementsByName(name)[0]; diff --git a/cara/apps/calculator/static/js/report.js b/cara/apps/calculator/static/js/report.js index b815da2f..29f9b907 100644 --- a/cara/apps/calculator/static/js/report.js +++ b/cara/apps/calculator/static/js/report.js @@ -1,93 +1,95 @@ /* 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'); + // H:M time format for x axis. 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] })) + // Prepare 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 }, + // Add main SVG element + var plot_div = document.getElementById(svg_id); + var vis = d3.select(plot_div).append('svg'); + + var xRange = d3.scaleTime().domain([data[0].hour, data[data.length - 1].hour]); + var xTimeRange = d3.scaleLinear().domain([data[0].time, data[data.length - 1].time]); + var bisecHour = d3.bisector((d) => { return d.hour; }).left; - // 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]), - xTimeRange = d3.scaleLinear().range([margins.left, width - margins.right]).domain([data[0].time, data[data.length - 1].time]), - bisecHour = d3.bisector((d) => { return d.hour; }).left, - - yRange = d3.scaleLinear().range([height - margins.bottom, margins.top]).domain([0., Math.max(...concentrations)]), - - xAxis = d3.axisBottom(xRange).tickFormat(d => time_format(d)), - yAxis = d3.axisLeft(yRange); - - // Plot tittle. - plot_title(vis, width, margins.top, 'Mean concentration of virions'); + var yRange = d3.scaleLinear().domain([0., Math.max(...concentrations)]); // Line representing the mean concentration. - plot_scenario_data(vis, data, xTimeRange, yRange, '#1f77b4'); - - // X axis. - plot_x_axis(vis, height, width, margins, xAxis, 'Time of day'); - - // Y axis - plot_y_axis(vis, height, width, margins, yAxis, 'Mean concentration (virions/m³)') + var lineFunc = d3.line(); + var draw_line = vis.append('svg:path') + .attr('stroke', '#1f77b4') + .attr('stroke-width', 2) + .attr('fill', 'none'); // Area representing the presence of exposed person(s). - exposed_presence_intervals.forEach(b => { - 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] - }))) + var exposedArea = {}; + var drawArea = {}; + exposed_presence_intervals.forEach((b, index) => { + exposedArea[index] = d3.area(); + drawArea[index] = vis.append('svg:path') .attr('fill', '#1f77b4') .attr('fill-opacity', '0.1'); - }) + }); + + // Plot tittle. + var plotTitleEl = vis.append('svg:foreignObject') + .attr("background-color", "transparent") + .attr('height', 30) + .style('text-align', 'center') + .html('Mean concentration of virions'); + + // 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³)'); // Legend for the plot elements - line and area. - var size = 20 - vis.append('rect') - .attr('x', width + size) - .attr('y', margins.top + size) + var legendLineIcon = vis.append('rect') .attr('width', 20) .attr('height', 3) .style('fill', '#1f77b4'); - vis.append('rect') - .attr('x', width + size) - .attr('y', 3 * size) + var legendAreaIcon = vis.append('rect') .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) + var legendLineText = vis.append('text') .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) + var legendAreaText = vis.append('text') .text('Presence of exposed person(s)') .style('font-size', '15px') .attr('alignment-baseline', 'central'); - // Legend bounding box. - vis.append('rect') + // Legend bounding + var legendBBox = 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') @@ -102,36 +104,148 @@ function draw_concentration_plot(svg_id, times, concentrations, exposed_presence focus.append('circle') .attr('r', 3); - focus.append('rect') + var tooltip_rect = 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') + var tooltip_time = focus.append('text') .attr('id', 'tooltip-time') - .attr('x', 18) .attr('y', -2); - focus.append('text') + var tooltip_concentration = focus.append('text') .attr('id', 'tooltip-concentration') - .attr('x', 18) .attr('y', 18); - vis.append('rect') + var toolBox = 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); + var graph_width; + var graph_height; + + 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', 'margin-top': '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', 'margin-top': '3rem'}; + 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. + + // 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]) + + // Line. + lineFunc.defined(d => !isNaN(d.concentration)) + .x(d => xTimeRange(d.time)) + .y(d => yRange(d.concentration)); + draw_line.attr("d", lineFunc(data)); + + // Area. + exposed_presence_intervals.forEach((b, index) => { + exposedArea[index].x(d => xTimeRange(d.time)) + .y0(graph_height - margins.bottom) + .y1(d => yRange(d.concentration)); + + drawArea[index].attr('d', exposedArea[index](data.filter(d => { + return d.time >= b[0] && d.time <= b[1] + }))); + }); + + // Title. + plotTitleEl.attr('width', graph_width); + + // Axis. + var xAxis = d3.axisBottom(xRange).tickFormat(d => time_format(d)); + var yAxis = d3.axisLeft(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('x', (graph_height * 0.9 + margins.bottom) / 2) + .attr('y', (graph_height + margins.left) * 0.9) + .attr('transform', 'rotate(-90, 0,' + graph_height + ')'); + + // Legend on right side. + const size = 20; + if (plot_div.clientWidth >= 900) { + legendLineIcon.attr('x', graph_width + size) + .attr('y', margins.top + size); + legendLineText.attr('x', graph_width + 3 * size) + .attr('y', margins.top + size); + legendAreaIcon.attr('x', graph_width + size) + .attr('y', margins.top + 1.5 * size); + legendAreaText.attr('x', graph_width + 3 * size) + .attr('y', margins.top + 2 * size); + legendBBox.attr('x', graph_width * 1.005) + .attr('y', margins.top * 1.2); + } + // Legend on the bottom. + else { + legendLineIcon.attr('x', size * 0.5) + .attr('y', graph_height * 1.05); + legendLineText.attr('x', 2 * size) + .attr('y', graph_height * 1.05); + legendAreaIcon.attr('x', size * 0.50) + .attr('y', graph_height * 1.01 + size); + legendAreaText.attr('x', 2 * size) + .attr('y', graph_height + 1.7 * size); + legendBBox.attr('x', 1) + .attr('y', graph_height); + } + + // ToolBox. + toolBox.attr('width', graph_width - margins.right) + .attr('height', graph_height); + } + + // Draw for the first time to initialize. + redraw(); + function mousemove() { + if (d3.pointer(event)[0] < graph_width / 2) { + tooltip_rect.attr('x', 10) + tooltip_time.attr('x', 18) + tooltip_concentration.attr('x', 18); + } + else { + tooltip_rect.attr('x', -90) + tooltip_time.attr('x', -82) + tooltip_concentration.attr('x', -82) + } var x0 = xRange.invert(d3.pointer(event, this)[0]), i = bisecHour(data, x0, 1), d0 = data[i - 1], @@ -141,12 +255,17 @@ 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)); } + + // Redraw based on the new size whenever the browser window is resized. + window.addEventListener("resize", redraw); + + } // 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) { +function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_plot_svg_id, times, alternative_scenarios) { // H:M format var time_format = d3.timeFormat('%H:%M'); // D3 array of ten categorical colors represented as RGB hexadecimal strings. @@ -171,148 +290,252 @@ function draw_alternative_scenarios_plot(svg_id, width, height, alternative_scen // 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 }, + // Add main SVG element + var alternative_plot_div = document.getElementById(alternative_plot_svg_id); + var vis = d3.select(alternative_plot_div).append('svg'); - // 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]]), + 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; - 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'); + var yRange = d3.scaleLinear().domain([0., highest_concentration]); // 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. - plot_scenario_data(vis, data, xTimeRange, yRange, colors[scenario_index]) + lineFuncs[scenario_name] = d3.line(); + + draw_lines[scenario_name] = vis.append('svg:path') + .attr("stroke", colors[scenario_index]) + .attr('stroke-width', 2) + .attr('fill', 'none'); // Legend for the plot elements - lines. - var size = 20 * (scenario_index + 1) - vis.append('rect') - .attr('x', width + 20) - .attr('y', margins.top + size) + label_icons[scenario_name] = vis.append('rect') .attr('width', 20) .attr('height', 3) .style('fill', colors[scenario_index]); - vis.append('text') - .attr('x', width + 3 * 20) - .attr('y', margins.top + size) + label_text[scenario_name] = vis.append('text') .text(scenario_name) .style('font-size', '15px') .attr('alignment-baseline', 'central'); } + // Plot title. + var plotTitleEl = vis.append('svg:foreignObject') + .attr("background-color", "transparent") + .attr('height', 30) + .style('text-align', 'center') + .html('Mean concentration of virions'); + + // X axis. - plot_x_axis(vis, height, width, margins, xAxis, "Time of day"); + 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. - vis.append('svg:g') - .attr('class', 'y axis') - .attr('transform', 'translate(' + margins.left + ',0)') - .call(yAxis); + var yAxisEl = vis.append('svg:g') + .attr('class', 'y axis'); // Y axis label. - vis.append('svg:text') + var yAxisLabelEl = 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') + var legendBBox = 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'); + + // 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); + } + + 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 >= 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 * .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. + + // 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]); + + for (const [scenario_name, data] of Object.entries(data_for_scenarios)) { + var scenario_index = Object.keys(data_for_scenarios).indexOf(scenario_name) + // Lines. + lineFuncs[scenario_name].defined(d => !isNaN(d.concentration)) + .x(d => xTimeRange(d.time)) + .y(d => yRange(d.concentration)); + draw_lines[scenario_name].attr("d", lineFuncs[scenario_name](data)); + + // 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 + 20) + .attr('y', margins.top + size); + label_text[scenario_name].attr('x', graph_width + 3 * 20) + .attr('y', margins.top + size); + } + // Legend on the bottom. + else { + label_icons[scenario_name].attr('x', margins.left * 0.3) + .attr('y', graph_height + size); + label_text[scenario_name].attr('x', margins.left * 1.4) + .attr('y', graph_height + size); + } + + } + + // Title. + plotTitleEl.attr('width', graph_width); + + // Axis. + var xAxis = d3.axisBottom(xRange).tickFormat(d => time_format(d)); + var yAxis = d3.axisLeft(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); + } + } + + // Draw for the first time to initialize. + redraw(); + + 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)); + } + } + + // Redraw based on the new size whenever the browser window is resized. + window.addEventListener("resize", redraw); } +function copy_clipboard(shareable_link) { -// 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; + $("#mobile_link").attr('title', 'Copied!') + .tooltip('_fixTitle') + .tooltip('show'); + + navigator.clipboard.writeText(shareable_link); } \ 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 d06a21bc..705ee449 100644 --- a/cara/apps/calculator/templates/base/calculator.report.html.j2 +++ b/cara/apps/calculator/templates/base/calculator.report.html.j2 @@ -2,12 +2,13 @@ - + Report | CARA (COVID Airborne Risk Assessment) + @@ -17,14 +18,14 @@ {% block report_header %} -
- -
-

CARA - CALCULATOR REPORT

-

Created {{ creation_date }} using CARA calculator version v{{ form.calculator_version }}

+
+ +
+

CARA - CALCULATOR REPORT

+

Created {{ creation_date }} using CARA calculator version v{{ form.calculator_version }}

- - + +
{% endblock report_header %} @@ -59,8 +60,8 @@

- -
+
+
Probability of infection (%)
{% block warning_animation %} @@ -74,23 +75,26 @@ {% endblock warning_animation %}
- - {% block report_summary %} - - {% endblock report_summary %} +
+ {% block report_summary %} + + {% endblock report_summary %} +
+
{% block report_summary_footnote %} {% endblock report_summary_footnote %}

* The results are based on the parameters and assumptions published in the CERN Open Report CERN-OPEN-2021-004.

- + +

@@ -107,12 +111,13 @@
- +
+
{% block report_scenarios_summary_table %} @@ -150,7 +155,7 @@ {% endblock report_results %} {% block report_footer %} -