diff --git a/cara/apps/calculator/__init__.py b/cara/apps/calculator/__init__.py index a60d5f74..407382e3 100644 --- a/cara/apps/calculator/__init__.py +++ b/cara/apps/calculator/__init__.py @@ -33,7 +33,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 CARA version (found at ``cara.__version__``). -__version__ = "3.0.1" +__version__ = "3.2.0" class BaseRequestHandler(RequestHandler): diff --git a/cara/apps/calculator/static/css/form.css b/cara/apps/calculator/static/css/form.css index 65e64531..4543fefc 100644 --- a/cara/apps/calculator/static/css/form.css +++ b/cara/apps/calculator/static/css/form.css @@ -18,6 +18,18 @@ font-size: 9pt; } +.center_radio { + align-self: center; +} + +.start_time, .finish_time { + margin-bottom: 10px; +} + +.sub_title { + margin-bottom: 5px; +} + /* -------Tool tip ------- */ .tooltip_text { diff --git a/cara/apps/calculator/static/css/report.css b/cara/apps/calculator/static/css/report.css index 52aeb1ea..b8f6ae17 100644 --- a/cara/apps/calculator/static/css/report.css +++ b/cara/apps/calculator/static/css/report.css @@ -226,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 36e08e0d..2a916544 100644 --- a/cara/apps/calculator/static/js/report.js +++ b/cara/apps/calculator/static/js/report.js @@ -1,9 +1,7 @@ /* Generate the concentration plot using d3 library. */ function draw_concentration_plot(svg_id, times, concentrations, cumulative_doses, exposed_presence_intervals) { - var visBoundingBox = d3.select(svg_id) - .node() - .getBoundingClientRect(); + console.log(cumulative_doses) var time_format = d3.timeFormat('%H:%M'); @@ -14,121 +12,124 @@ function draw_concentration_plot(svg_id, times, concentrations, 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]})); - var vis = d3.select(svg_id), - width = visBoundingBox.width - 400, - 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'); - // H:M time format for x axis. - xRange = d3.scaleTime().range([margins.left, width - margins.right]).domain([data_for_graphs.concentrations[0].hour, data_for_graphs.concentrations[data_for_graphs.concentrations.length - 1].hour]), - xTimeRange = d3.scaleLinear().range([margins.left, width - margins.right]).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, + // 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().range([height - margins.bottom, margins.top]).domain([0., Math.max(...concentrations)]), - yCumulatedRange = d3.scaleLinear().range([height - margins.bottom, margins.top]).domain([0., Math.max(...cumulative_doses)*1.1]), + yRange = d3.scaleLinear().domain([0., Math.max(...concentrations)]), + yCumulativeRange = d3.scaleLinear().domain([0., Math.max(...cumulative_doses)*1.1]), - xAxis = d3.axisBottom(xRange).tickFormat(d => time_format(d)), - yAxis = d3.axisLeft(yRange).ticks(4), - yCumulatedAxis = d3.axisRight(yCumulatedRange).ticks(4); + xAxis = d3.axisBottom(xRange).tickFormat(d => time_format(d)), + yAxis = d3.axisLeft(yRange).ticks(4), + yCumulativeAxis = d3.axisRight(yCumulativeRange).ticks(4); // Line representing the mean concentration. - plot_scenario_data(vis, data_for_graphs.concentrations, xTimeRange, yRange, '#1f77b4'); - // Line representing the cumulative concentration. - plot_cumulative_data(vis, data_for_graphs.cumulative_doses, xTimeRange, yCumulatedRange, '#1f77b4'); + var lineFunc = d3.line(); + var draw_line = vis.append('svg:path') + .attr('stroke', '#1f77b4') + .attr('stroke-width', 2) + .attr('fill', 'none'); - // 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³)') - - // Y cumulative concentration axis declaration. - vis.append('svg:g') - .attr('class', 'y axis') - .style('font-size', 14) + var lineCumulative = d3.line(); + var draw_cumulative_line = vis.append('svg:path') + .attr('stroke', '#1f77b4') + .attr('stroke-width', 2) .style("stroke-dasharray", "5 5") - .attr('transform', 'translate(' + (width - margins.right) + ',0)') - .call(yCumulatedAxis); - - // Y cumulated concentration 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', 1.71 * width) - .text('Mean cumulative dose (virions)'); + .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_for_graphs.concentrations.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³)'); + + // Y cumulative concentration axis declaration. + var yAxisCumEl = vis.append('svg:g') + .attr('class', 'y axis') + .style('font-size', 14) + .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 (virions)'); // Legend for the plot elements - line and area. - var size = 20 - vis.append('rect') - .attr('x', width + size + 50) - .attr('y', margins.top + size) + var legendLineIcon = vis.append('rect') .attr('width', 20) .attr('height', 3) .style('fill', '#1f77b4'); - vis.append('line') - .attr("x1", width + size + 50) - .attr("x2", width + 2 * size + 52) - .attr("y1", 3.5 * size) - .attr("y2", 3.5 * size) + var legendCumulativeIcon = vis.append('line') .style("stroke-dasharray", "5 5") //dashed array for line .attr('stroke-width', '2') .style("stroke", '#1f77b4'); - vis.append('rect') - .attr('x', width + size + 50) - .attr('y', 4 * 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 + 50) - .attr('y', margins.top + size) - .text('Viral concentration') + var legendLineText = vis.append('text') + .text('Mean concentration') .style('font-size', '15px') .attr('alignment-baseline', 'central'); - vis.append('text') - .attr('x', width + 3 * size + 50) - .attr('y', margins.top + 2 * size) + var legendCumutiveText = vis.append('text') .text('Cumulative dose') .style('font-size', '15px') .attr('alignment-baseline', 'central'); - vis.append('text') - .attr('x', width + 3 * size + 50) - .attr('y', margins.top + 3 * 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') - .attr('width', 270) + // Legend bounding + var legendBBox = vis.append('rect') + .attr('width', 255) .attr('height', 70) - .attr('x', width * 1.1) - .attr('y', margins.top + 5) .attr('stroke', 'lightgrey') .attr('stroke-width', '2') .attr('rx', '5px') @@ -169,16 +170,163 @@ function draw_concentration_plot(svg_id, times, concentrations, cumulative_doses toolBox[concentration] = vis.append('rect') .attr('fill', 'none') .attr('pointer-events', 'all') - .attr('width', width - margins.right) - .attr('height', height) .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); } + 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 * .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', '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]); + yCumulativeRange.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_for_graphs.concentrations)); + + // Cumulative line + lineCumulative.defined(d => !isNaN(d.concentration)) + .x(d => xTimeRange(d.time)) + .y(d => yCumulativeRange(d.concentration)); + draw_cumulative_line.attr("d", lineCumulative(data_for_graphs.cumulative_doses)); + + // 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_for_graphs.concentrations.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 + ')'); + + 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); + } + + // Legend on right side. + const size = 20; + if (plot_div.clientWidth >= 900) { + legendLineIcon.attr('x', graph_width + size * 2.5) + .attr('y', margins.top + size); + legendLineText.attr('x', graph_width + 4 * size) + .attr('y', margins.top + size); + + legendCumulativeIcon.attr("x1", graph_width + size + 30) + .attr("x2", graph_width + 2 * size + 32) + .attr("y1", 3.5 * size) + .attr("y2", 3.5 * size); + legendCumutiveText.attr('x', graph_width + 2.5 * size + 30) + .attr('y', margins.top + 2 * size); + + legendAreaIcon.attr('x', graph_width + size * 2.5) + .attr('y', margins.top + 2.5 * size); + legendAreaText.attr('x', graph_width + 4 * size) + .attr('y', margins.top + 3 * size); + + legendBBox.attr('x', graph_width * 1.07) + .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); + + legendCumulativeIcon.attr("x1", size * 0.5) + .attr("x2", size * 1.55) + .attr("y1", graph_height * 1.05 + size) + .attr("y2", graph_height * 1.05 + size); + legendCumutiveText.attr('x', 2 * size) + .attr('y', graph_height + 1.65 * size); + + legendAreaIcon.attr('x', size * 0.50) + .attr('y', graph_height * 1.09 + size); + legendAreaText.attr('x', 2 * size) + .attr('y', graph_height + 2.7 * size); + + legendBBox.attr('x', 1) + .attr('y', graph_height); + } + + // ToolBox. + for (const [concentration, data] of Object.entries(data_for_graphs)) { + toolBox[concentration].attr('width', graph_width - margins.right) + .attr('height', graph_height); + } + } + + // Draw for the first time to initialize. + redraw(); + function mousemove() { for (const [scenario, data] of Object.entries(data_for_graphs)) { - if (d3.pointer(event)[0] < width / 2) { + 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); @@ -207,17 +355,22 @@ function draw_concentration_plot(svg_id, times, concentrations, cumulative_doses 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) + ',' + yCumulatedRange(d.concentration) + ')'); + 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)); } } + + // 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. @@ -226,7 +379,7 @@ function draw_alternative_scenarios_plot(svg_id, width, height, alternative_scen // Variable for the highest concentration for all the scenarios var highest_concentration = 0. - var data_for_graphs = {} + var data_for_scenarios = {} for (scenario in alternative_scenarios) { scenario_concentrations = alternative_scenarios[scenario].concentrations @@ -236,158 +389,258 @@ function draw_alternative_scenarios_plot(svg_id, width, height, alternative_scen 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_graphs[scenario] = data + data_for_scenarios[scenario] = data } // We need one scenario to get the time range - var first_scenario = Object.values(data_for_graphs)[0] + 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); + var yRange = d3.scaleLinear().domain([0., highest_concentration]); // Line representing the mean concentration for each scenario. - for (const [scenario_name, data] of Object.entries(data_for_graphs)) { - var scenario_index = Object.keys(data_for_graphs).indexOf(scenario_name) + 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_graphs).length)) - .attr('x', width * 1.005) - .attr('y', margins.top + 5) + .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'); + + // 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_x_axis(vis, height, width, margins, xAxis, label) { - // X axis declaration - vis.append('svg:g') - .attr('class', 'x axis') - .style('font-size', 14) - .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') - .style('font-size', 14) - .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; -} - -function plot_cumulative_data(vis, data, xTimeRange, yCumulativeRange, line_color) { - var lineCumulativeFunc = d3.line() - .defined(d => !isNaN(d.concentration)) - .x(d => xTimeRange(d.time)) - .y(d => yCumulativeRange(d.concentration)) - .curve(d3.curveBasis); - - vis.append('svg:path') - .attr('d', lineCumulativeFunc(data)) - .attr('stroke', line_color) - .attr('stroke-width', 2) - .style("stroke-dasharray", "5 5") - .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 e609c0b5..f63b0884 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 @@
- +
- Created {{ creation_date }} using CARA calculator version v{{ form.calculator_version }}
+
+ Created {{ creation_date }} using CARA calculator version v{{ form.calculator_version }}
* The results are based on the parameters and assumptions published in the CERN Open Report CERN-OPEN-2021-004.
- + +