diff --git a/frontend/rctool/templates/rctool/rctool/develop/develop_input_settings.html b/frontend/rctool/templates/rctool/rctool/develop/develop_input_settings.html index f8f08d2c..0efa9077 100644 --- a/frontend/rctool/templates/rctool/rctool/develop/develop_input_settings.html +++ b/frontend/rctool/templates/rctool/rctool/develop/develop_input_settings.html @@ -16,10 +16,10 @@
- - - - + + + +
@@ -90,10 +90,10 @@
- - - - + + + +
diff --git a/frontend/rctool/templates/rctool/rctool/develop/rctool_develop_js.html b/frontend/rctool/templates/rctool/rctool/develop/rctool_develop_js.html index 4ee9e988..e37a26f3 100644 --- a/frontend/rctool/templates/rctool/rctool/develop/rctool_develop_js.html +++ b/frontend/rctool/templates/rctool/rctool/develop/rctool_develop_js.html @@ -250,7 +250,8 @@ }; // update residual chart - updateResidualChart(newSegData, idx) + let newSegPlotData = newSegData.map(item => ({ x: item[2], y: item[0] })).slice(1,-1); + updateResidualChart(newSegPlotData, idx) idx++; @@ -312,6 +313,7 @@ } } updateChartSegments() + updateFieldData() // redraw chart with updated lines rcChart.update(); @@ -375,16 +377,11 @@ } } - - - function updateResidualChart(newResData, r_idx) { - var newResidualChartData = newResData.map(item => ({ x: item[2], y: item[0] })).slice(1,-1); + function updateResidualChart(newResidualChartData, r_idx) { residualChart.data.datasets[r_idx].data = newResidualChartData; residualChart.update(); - }; - /* ------------------------------------------------------------------------------------------------------------------------------------------- B.1) USER INPUT - HIDE / USE DATA POINTS ----------------------------------------------------------------------------------------------------------------------------------------------*/ @@ -399,8 +396,6 @@ }; function togglePoint(idx, checked) { - var rowIndex = idx - if (checked == true){ // set point to checked now fielddatacsv.toggle_point[idx] = 'checked'; @@ -409,6 +404,25 @@ fielddatacsv.toggle_point[idx] = 'unchecked'; } + // update field data in the plot + updateFieldData() + } + + function calc_q_model(h, c, exp, offset) { + return c * (h - offset) ** exp; + } + + function calc_h_model(q, c, exp, offset) { + return Math.exp(1/exp * Math.log(q / c)) + offset + } + + function calc_residual_percent(h, q, c, exp, offset) { + var q_model = calc_q_model(h, c, exp, offset) + return 100 * (q - q_model) / q + } + + + function updateFieldData() { // redraw field data in the plot, but do not call autofit (it will update when the user hits that button) var datasetIndices = Object.keys(rcChart.data.datasets) var fieldIdx = null; @@ -430,23 +444,14 @@ id: id, x: Object.values(fielddatacsv.discharge)[index], y: Object.values(fielddatacsv.stage)[index], - active: Object.values(fielddatacsv.toggle_point)[index] + active: Object.values(fielddatacsv.toggle_point)[index], } }); - // filter out which data is active/inactive - var fieldDataActive = dataArray.filter(data => data.active == 'checked') - var fieldDataInActive = dataArray.filter(data => data.active == 'unchecked') - // remove the active attribute - fieldDataActive.forEach(object => { - delete object['active']; - delete object['id']; - }); - fieldDataInActive.forEach(object => { - delete object['active']; - delete object['id']; - }); + // filter out which data is active/inactive + var fieldDataActive = Array.from(dataArray.filter(data => data.active == 'checked')); + var fieldDataInActive = Array.from(dataArray.filter(data => data.active == 'unchecked')); // update data in the chart objects directly: rcChart.data.datasets[fieldIdx].data = fieldDataActive; @@ -468,6 +473,27 @@ // update export page variables updateExportVariables() + + // update residual chart + for (var seg = 0; seg < rcParam.length; seg++) { + var dataArray_residual = Object.keys(fielddatacsv.toggle_point).map((id, index) => { + return { + id: id, + x: calc_residual_percent(Object.values(fielddatacsv.stage)[index], Object.values(fielddatacsv.discharge)[index], rcParam[seg].const, rcParam[seg].exp, rcParam[seg].offset), + y: Object.values(fielddatacsv.stage)[index], + active: Object.values(fielddatacsv.toggle_point)[index], + } + }); + + // filter out which data is active/inactive + var fieldDataActive_residual = Array.from(dataArray_residual.filter(data => data.active == 'checked')); + + // filter out data that is not within the segment bounds + fieldDataActive_residual = fieldDataActive_residual.filter(item => ( item.y >= rcParam[seg].seg_bounds[0][0] && item.y <= rcParam[seg].seg_bounds[1][0])); + + let newSegPlotData = fieldDataActive_residual.map(item => ({ x: item.x, y: item.y })); + updateResidualChart(newSegPlotData, seg) + } } /* ------------------------------------------------------------------------------------------------------------------------------------------- @@ -707,9 +733,9 @@ this.min = this.max; this.max = temp; } - let minPowerData = Math.log((this.min)) / Math.log(rcParam[0].exp); - let maxPowerData = Math.log((this.max)) / Math.log(rcParam[0].exp); - + let exponent = rcParam[0].exp; + let minPowerData = Math.log((this.min)) / Math.log(exponent); + let maxPowerData = Math.log((this.max)) / Math.log(exponent); if (minPowerData > maxPowerData) { minPowerData = -minPowerData; @@ -727,14 +753,13 @@ while ((power > minPower) && (power >= minPowerData)) { power -= powerStep; } - // add ticks from smallest to largest while (power <= (maxPower + powerStep)) { - let tickVal = Math.pow(rcParam[0].exp, power); + let tickVal = Math.pow(exponent, power); // if exponent is less than 1, take negative - if (rcParam[0].exp < 1) { - tickVal = Math.pow(rcParam[0].exp, -power); + if (exponent < 1) { + tickVal = Math.pow(exponent, -power); } // adjust to use rounded tick value based on log10 of value @@ -748,8 +773,23 @@ power += powerStep; } - this.min = ticks[0].value; - this.max = ticks[ticks.length - 1].value; + try { + this.min = ticks[0].value; + this.max = ticks[ticks.length - 1].value; + } catch (e) { + // show temporary ticks: + this.min = 0; + this.max = 1; + + // display warning/error message on top + updateWarningMessage( + {'error_title': 'Error', 'error_text': 'No ticks found for logarithmic scale! Please adjust the exponent of the rating curve by changing your endpoints or triggering the autofit again!'}, + {'title':'none', 'url':'none'} + ); + + // force non-log scale: + document.getElementById("toggle_axis_format").checked = false; + } return ticks; } @@ -805,6 +845,24 @@ modifierKey: 'shift', } }; + console.log('disallowed zoom and pan!') + // TODO: allow zoom and pan for logarithmic scale + // disallow zoom and pan: + rcChart.options.plugins.zoom = { + zoom: { + wheel: { + enabled: false + }, + pinch: { + enabled: false + }, + mode: 'xy' + }, + pan: { + enabled: false, + modifierKey: 'shift', + } + }; } else { axisFormat = 'linear'; @@ -825,6 +883,23 @@ modifierKey: 'shift', } }; + + // allow zoom and pan: + rcChart.options.plugins.zoom = { + zoom: { + wheel: { + enabled: true, + }, + pinch: { + enabled: true, + }, + mode: 'xy' + }, + pan: { + enabled: true, + modifierKey: 'shift', + } + }; }; rcChart.options.scales = { @@ -1206,8 +1281,8 @@ for (var j in residualData[seg].data){ if (residualData[seg].data[j][0] == fielddatacsv.stage[idx]){ q_model = residualData[seg].data[j][1] - h_model = Math.exp(1/rcParam[seg - 1].exp * Math.log(context.parsed.x / rcParam[seg - 1].const)) + rcParam[seg - 1].offset - r_percent = -residualData[seg].data[j][2] + h_model = calc_h_model(context.parsed.x, rcParam[seg - 1].const, rcParam[seg - 1].exp, rcParam[seg - 1].offset) + r_percent = calc_residual_percent(context.parsed.y, context.parsed.x, rcParam[seg - 1].const, rcParam[seg - 1].exp, rcParam[seg - 1].offset) r_volumetric = r_percent/100 * context.parsed.x } } @@ -1217,6 +1292,7 @@ label.push(['date: ' + fielddatacsv.datetime[idx]]); if (h_model !== null) { label.push(['H shift: ' + (h_model - context.parsed.y).toFixed(2) + ' m']) + label.push(['H shift: ' + (h_model - context.parsed.y).toFixed(2) + ' m']) } //if (q_model !== null){ // label.push(['Q model: ' + q_model.toFixed(2) + ' m³/s']); @@ -1346,6 +1422,7 @@ toggleAxisFormat(document.getElementById("toggle_axis_format").checked) + /*----------------------------------------------------------------------------------------------------------------------------------------------- C.2) REGRESSION RESIDUAL CHART ------------------------------------------------------------------------------------------------------------------------------------------------*/ @@ -1369,6 +1446,9 @@ }, options: { maintainAspectRatio: false, + animation: { + duration: 0 + }, scales: { x: { title: { @@ -1386,6 +1466,74 @@ } }, + plugins: { + zoom: { + zoom: { + wheel: { + enabled: true, + }, + pinch: { + enabled: true, + }, + mode: 'xy' + }, + pan: { + enabled: true, + modifierKey: 'shift', + }, + }, + tooltip: { + bodyFont: { + size: 10, + family: 'monospace', + }, + callbacks: { + label: function(context) { + var label = context.dataset.label || ''; + + // find discharge from rating curve for this stage + let h_field = context.parsed.y + let r_percent = context.parsed.x + + label = []; + for (var seg = 0; seg < rcParam.length; seg++){ + let q_model = calc_q_model(h_field, rcParam[seg].const, rcParam[seg].exp, rcParam[seg].offset) + // r_percent = 100 * (q_field - q_model) / q_field + let q_field = - 100 * q_model / (r_percent - 100) + let q_field_rounded = Math.round(q_field * 100) / 100 + let h_model = calc_h_model(q_field, rcParam[seg].const, rcParam[seg].exp, rcParam[seg].offset) + let r_volumetric = r_percent/100 * q_field + + for (var idx in fielddatacsv.stage) { + if (fielddatacsv.stage[idx] == h_field && fielddatacsv.discharge[idx] == q_field_rounded) { + label.push(['H: ' + context.parsed.y.toFixed(2) + ' m']); + label.push(['Q: ' + context.parsed.x.toFixed(2) + ' m³/s']); + label.push(['date: ' + fielddatacsv.datetime[idx]]); + if (h_model !== null) { + label.push(['H shift: ' + (h_model - context.parsed.y).toFixed(2) + ' m']) + } + //if (q_model !== null){ + // label.push(['Q model: ' + q_model.toFixed(2) + ' m³/s']); + //} + if (r_percent !== null){ + if (r_percent > 0){ + // show + sign for positive values + label.push(['R%: +' + r_percent.toFixed(2) + ' %']); + label.push(['R volume:+' + r_volumetric.toFixed(2) + ' m3/s']); + } else { + label.push(['R%: ' + r_percent.toFixed(2) + ' %']); + label.push(['R volume:' + r_volumetric.toFixed(2) + ' m3/s']); + } + } + } + } + } + + return label; + }, + } + }, + }, tooltips: { mode: 'index', intersect: true @@ -1426,10 +1574,10 @@ for (let i = 2; i < residualData.length; i++) { var residualLabel = residualData[i]['label'] var colorLabel = pallet[i] - var residualChartData = residualData[i]['data'].map(item => ({ x: item[2], y: item[0] })).slice(1, -1); addResidualPlot(residualData, residualLabel, colorLabel) }; + updateChartFromGlobal(); /*----------------------------------------------------------------------------------------------------------------------------------------------- C.3) FORMS @@ -1493,6 +1641,8 @@ warningDiv.style.display = "block"; document.getElementById('error-title').innerHTML = warningDict['error_title']; document.getElementById('error-text').innerHTML = warningDict['error_text']; + document.getElementById('error-link').setAttribute('href', ''); + document.getElementById('error-link-text').innerHTML = ''; }; }; diff --git a/frontend/rctool/templates/rctool/rctool/import/rctool_import.html b/frontend/rctool/templates/rctool/rctool/import/rctool_import.html index ad91ee7b..c672847a 100644 --- a/frontend/rctool/templates/rctool/rctool/import/rctool_import.html +++ b/frontend/rctool/templates/rctool/rctool/import/rctool_import.html @@ -33,6 +33,10 @@
load data {{form.csv_upload}} + + + *csv format only, download sample file here: sample_data.csv +
diff --git a/frontend/static/sample_data/sample_data.csv b/frontend/static/sample_data/sample_data.csv new file mode 100644 index 00000000..64f1f980 --- /dev/null +++ b/frontend/static/sample_data/sample_data.csv @@ -0,0 +1,11 @@ +datetime,discharge,stage,uncertainty,comments +2021-11-22 10:00,0.13,0.33,0.05,"21 panels, some snow on ground" +2021-04-06 14:00,0.01,0.24,0.05,19 panels +2021-03-08 9:00,0.05,0.28,0.03,"35 panels, recent high flow event in December" +2021-02-24 13:00,0.03,0.27,0.05,"21 panels, rain overnight" +2021-01-28 14:00,0.12,0.32,0.04,"21 panels, good mmt" +2021-01-11 10:00,0.18,0.39,0.04,"21 panels, recent high flows after consistent rain. High turbidity" +2020-12-30 11:30,0.36,0.45,0.03,"24 panels, recent high flows and heavy rains" +2020-11-24 13:30,0.03,0.26,0.04,21 panels. Rain overnight +2021-12-13 8:00,0.29,0.4,0.04,20 panels +2020-01-12 17:00,0.67,0.55,0.03,"23 panels, recent rain on snow event, creek rising during mmt"