diff --git a/MATScommon b/MATScommon index 987bd7df59..e51d1eeaae 160000 --- a/MATScommon +++ b/MATScommon @@ -1 +1 @@ -Subproject commit 987bd7df5980828fe5f8b8873cc7ae87ccd2427c +Subproject commit e51d1eeaae0486549e95a7f4c3a38044e3620929 diff --git a/METexpress b/METexpress index 160a78352a..cbc8892207 160000 --- a/METexpress +++ b/METexpress @@ -1 +1 @@ -Subproject commit 160a78352a7e73202fe5027cfd3cea0e03f26436 +Subproject commit cbc889220763e14c1c2cf9f4543bdea5553e8312 diff --git a/apps/airQuality/server/main.js b/apps/airQuality/server/main.js index 23c95bdea2..d868a66dd7 100644 --- a/apps/airQuality/server/main.js +++ b/apps/airQuality/server/main.js @@ -708,7 +708,7 @@ const doCurveParams = async function () { "FAR (False Alarm Ratio)": ["ctc", "x100", 0], - "Bias (forecast/actual)": ["ctc", "Ratio", 1], + "Bias (Forecast / Actual)": ["ctc", "Ratio", 1], "HSS (Heidke Skill Score)": ["ctc", "x100", 100], diff --git a/apps/cb-metar/server/dataFunctions/data_contour.js b/apps/cb-metar/server/dataFunctions/data_contour.js index ca5eaf8be6..c2c9a24652 100644 --- a/apps/cb-metar/server/dataFunctions/data_contour.js +++ b/apps/cb-metar/server/dataFunctions/data_contour.js @@ -123,7 +123,7 @@ global.dataContour = async function (plotParams) { } else { // threshold is not an axis param and this is a scalar app // so create a dummy threshold for the loop later - allThresholds = ["NA"]; + allThresholds = ["All Data"]; } const validTimes = curve["valid-time"] === undefined ? [] : curve["valid-time"]; @@ -135,6 +135,15 @@ global.dataContour = async function (plotParams) { ).optionsMap; const statType = statisticOptionsMap[variable][statisticSelect][0]; + if ( + variableValuesMap[queryVariable][1] && + statisticOptionsMap[variable][statisticSelect][0] === "scalar" + ) { + throw new Error( + "INFO: Plotting thresholded variables (e.g. ceiling, visibility) with scalar statistics (e.g. RMSE, additive bias) is not available for this plot type." + ); + } + let queryTemplate; const regionType = curve["region-type"]; if (regionType === "Select stations") { diff --git a/apps/cb-metar/server/dataFunctions/data_contour_diff.js b/apps/cb-metar/server/dataFunctions/data_contour_diff.js index cf3e720d5c..4622468ddc 100644 --- a/apps/cb-metar/server/dataFunctions/data_contour_diff.js +++ b/apps/cb-metar/server/dataFunctions/data_contour_diff.js @@ -132,7 +132,7 @@ global.dataContourDiff = async function (plotParams) { } else { // threshold is not an axis param and this is a scalar app // so create a dummy threshold for the loop later - allThresholds = ["NA"]; + allThresholds = ["All Data"]; } const validTimes = curve["valid-time"] === undefined ? [] : curve["valid-time"]; @@ -145,6 +145,15 @@ global.dataContourDiff = async function (plotParams) { [statType] = statisticOptionsMap[variable][statisticSelect]; allStatTypes.push(statType); + if ( + variableValuesMap[queryVariable][1] && + statisticOptionsMap[variable][statisticSelect][0] === "scalar" + ) { + throw new Error( + "INFO: Plotting thresholded variables (e.g. ceiling, visibility) with scalar statistics (e.g. RMSE, additive bias) is not available for this plot type." + ); + } + let queryTemplate; const regionType = curve["region-type"]; if (regionType === "Select stations") { diff --git a/apps/cb-metar/server/dataFunctions/data_dailymodelcycle.js b/apps/cb-metar/server/dataFunctions/data_dailymodelcycle.js index a5f6b50d34..4c77d2eeda 100644 --- a/apps/cb-metar/server/dataFunctions/data_dailymodelcycle.js +++ b/apps/cb-metar/server/dataFunctions/data_dailymodelcycle.js @@ -179,7 +179,15 @@ global.dataDailyModelCycle = async function (plotParams) { let queryTemplate; let sitesList; const regionType = - filterModelBy === "None" && filterObsBy === "None" + filterModelBy === "None" && // not filtering the model by anything + filterObsBy === "None" && // not filtering the obs by anything + !( + // not a thresholded variable that we're forcing into a scalar stat + ( + variableValuesMap[queryVariable][1] && + statisticOptionsMap[variable][statisticSelect][0] === "scalar" + ) + ) ? curve["region-type"] : "Select stations"; if (curve["region-type"] === "Predefined region") { diff --git a/apps/cb-metar/server/dataFunctions/data_dieoff.js b/apps/cb-metar/server/dataFunctions/data_dieoff.js index e4f122e6bd..428b2c6e34 100644 --- a/apps/cb-metar/server/dataFunctions/data_dieoff.js +++ b/apps/cb-metar/server/dataFunctions/data_dieoff.js @@ -179,7 +179,15 @@ global.dataDieoff = async function (plotParams) { let sitesList; let singleCycle; const regionType = - filterModelBy === "None" && filterObsBy === "None" + filterModelBy === "None" && // not filtering the model by anything + filterObsBy === "None" && // not filtering the obs by anything + !( + // not a thresholded variable that we're forcing into a scalar stat + ( + variableValuesMap[queryVariable][1] && + statisticOptionsMap[variable][statisticSelect][0] === "scalar" + ) + ) ? curve["region-type"] : "Select stations"; if (curve["region-type"] === "Predefined region") { diff --git a/apps/cb-metar/server/dataFunctions/data_histogram.js b/apps/cb-metar/server/dataFunctions/data_histogram.js index 6c9c741268..6a994be2d3 100644 --- a/apps/cb-metar/server/dataFunctions/data_histogram.js +++ b/apps/cb-metar/server/dataFunctions/data_histogram.js @@ -97,6 +97,15 @@ global.dataHistogram = async function (plotParams) { [statType] = statisticOptionsMap[variable][statisticSelect]; allStatTypes.push(statType); + if ( + variableValuesMap[queryVariable][1] && + statisticOptionsMap[variable][statisticSelect][0] === "scalar" + ) { + throw new Error( + "INFO: Plotting thresholded variables (e.g. ceiling, visibility) with scalar statistics (e.g. RMSE, additive bias) is not available for this plot type." + ); + } + let queryTemplate; const regionType = curve["region-type"]; if (regionType === "Select stations") { diff --git a/apps/cb-metar/server/dataFunctions/data_series.js b/apps/cb-metar/server/dataFunctions/data_series.js index 945ea19c78..9bc24427d0 100644 --- a/apps/cb-metar/server/dataFunctions/data_series.js +++ b/apps/cb-metar/server/dataFunctions/data_series.js @@ -180,7 +180,15 @@ global.dataSeries = async function (plotParams) { let queryTemplate; let sitesList; const regionType = - filterModelBy === "None" && filterObsBy === "None" + filterModelBy === "None" && // not filtering the model by anything + filterObsBy === "None" && // not filtering the obs by anything + !( + // not a thresholded variable that we're forcing into a scalar stat + ( + variableValuesMap[queryVariable][1] && + statisticOptionsMap[variable][statisticSelect][0] === "scalar" + ) + ) ? curve["region-type"] : "Select stations"; if (curve["region-type"] === "Predefined region") { diff --git a/apps/cb-metar/server/dataFunctions/data_simple_scatter.js b/apps/cb-metar/server/dataFunctions/data_simple_scatter.js new file mode 100644 index 0000000000..f079d1255f --- /dev/null +++ b/apps/cb-metar/server/dataFunctions/data_simple_scatter.js @@ -0,0 +1,556 @@ +/* + * Copyright (c) 2021 Colorado State University and Regents of the University of Colorado. All rights reserved. + */ + +/* global Assets */ + +import { + matsCollections, + matsTypes, + matsDataUtils, + matsDataQueryUtils, + matsDataCurveOpsUtils, + matsDataProcessUtils, + matsMiddleSimpleScatter, +} from "meteor/randyp:mats-common"; +import moment from "moment"; + +/* eslint-disable no-await-in-loop */ + +global.dataSimpleScatter = async function (plotParams) { + // initialize variables common to all curves + const appParams = { + plotType: matsTypes.PlotTypes.simpleScatter, + matching: plotParams.plotAction === matsTypes.PlotActions.matched, + completeness: plotParams.completeness, + outliers: plotParams.outliers, + hideGaps: plotParams.noGapsCheck, + hasLevels: false, + }; + + const totalProcessingStart = moment(); + const dataRequests = {}; // used to store data queries + let dataFoundForCurve = true; + let dataFoundForAnyCurve = false; + + const curves = JSON.parse(JSON.stringify(plotParams.curves)); + const curvesLength = curves.length; + + const axisXMap = Object.create(null); + const axisYMap = Object.create(null); + let xmax = -1 * Number.MAX_VALUE; + let ymax = -1 * Number.MAX_VALUE; + let xmin = Number.MAX_VALUE; + let ymin = Number.MAX_VALUE; + + let statTypeX; + let statTypeY; + let varUnitsX; + let varUnitsY; + const allStatTypes = []; + + let statement = ""; + let rows = ""; + let error = ""; + const dataset = []; + + for (let curveIndex = 0; curveIndex < curvesLength; curveIndex += 1) { + // initialize variables specific to each curve + const curve = curves[curveIndex]; + const { label } = curve; + const { diffFrom } = curve; + + const binParam = curve["bin-parameter"]; + const binClause = ( + await matsCollections["bin-parameter"].findOneAsync({ + name: "bin-parameter", + }) + ).optionsMap[binParam]; + + const variableX = curve["x-variable"]; + const variableY = curve["y-variable"]; + const variableValuesMap = ( + await matsCollections.variable.findOneAsync({ + name: "variable", + }) + ).valuesMap; + const queryVariableX = Object.keys(variableValuesMap).filter( + (qv) => Object.keys(variableValuesMap[qv][0]).indexOf(variableX) !== -1 + )[0]; + const queryVariableY = Object.keys(variableValuesMap).filter( + (qv) => Object.keys(variableValuesMap[qv][0]).indexOf(variableY) !== -1 + )[0]; + const variableDetailsX = variableValuesMap[queryVariableX][0][variableX]; + const variableDetailsY = variableValuesMap[queryVariableY][0][variableY]; + + const modelOptionsMap = ( + await matsCollections["data-source"].findOneAsync({ name: "data-source" }) + ).optionsMap; + if (!modelOptionsMap[variableX] || !modelOptionsMap[variableY]) { + throw new Error( + "INFO: At least one of your selected variables is not available for this model." + ); + } + const model = modelOptionsMap[variableX][curve["data-source"]][0]; + + if (binParam === "Threshold") { + throw new Error( + `INFO: Binning by thresholds is currently not supported for scatter plots in this app (performance diagrams and contours only). Please select a different binning parameter.` + ); + } + + const thresholdStrX = curve["x-threshold"]; + const thresholdStrY = curve["y-threshold"]; + let thresholdX = ""; + let thresholdY = ""; + if (variableValuesMap[queryVariableX][1]) { + const thresholdValuesX = ( + await matsCollections["x-threshold"].findOneAsync({ name: "x-threshold" }) + ).valuesMap[variableX]; + thresholdX = Object.keys(thresholdValuesX).find( + (key) => thresholdValuesX[key] === thresholdStrX + ); + thresholdX = thresholdX.replace(/_/g, "."); + } + if (variableValuesMap[queryVariableY][1]) { + const thresholdValuesY = ( + await matsCollections["y-threshold"].findOneAsync({ name: "y-threshold" }) + ).valuesMap[variableY]; + thresholdY = Object.keys(thresholdValuesY).find( + (key) => thresholdValuesY[key] === thresholdStrY + ); + thresholdY = thresholdY.replace(/_/g, "."); + } + + const validTimes = curve["valid-time"] === undefined ? [] : curve["valid-time"]; + let forecastLength = curve["forecast-length"]; + const dateRange = matsDataUtils.getDateRange(curve["curve-dates"]); + const fromSecs = dateRange.fromSeconds; + const toSecs = dateRange.toSeconds; + + const statisticSelectX = curve["x-statistic"]; + const statisticSelectY = curve["y-statistic"]; + const statisticOptionsMap = ( + await matsCollections.statistic.findOneAsync({ name: "statistic" }) + ).optionsMap; + [statTypeX] = statisticOptionsMap[variableX][statisticSelectX]; + [statTypeY] = statisticOptionsMap[variableY][statisticSelectY]; + + const filterModelBy = curve["filter-model-by"]; + const filterObsBy = curve["filter-obs-by"]; + const filterInfo = {}; + + if (filterModelBy !== "None") { + // get the variable text that we'll query off of + const filterModelVariable = Object.keys(variableValuesMap).filter( + (fv) => Object.keys(variableValuesMap[fv][0]).indexOf(filterModelBy) !== -1 + )[0]; + const filterModelVariableDetails = + variableValuesMap[filterModelVariable][0][filterModelBy]; + [, filterInfo.filterModelBy] = filterModelVariableDetails; + + // get the bounds and make sure they're in the right units + let filterModelMin = Number(curve["filter-model-min"]); + let filterModelMax = Number(curve["filter-model-max"]); + if ( + filterModelBy.toLowerCase().includes("temperature") || + filterModelBy.toLowerCase().includes("dewpoint") + ) { + // convert temperature and dewpoint bounds from Celsius + // to Fahrenheit, which is in the database + filterModelMin = filterModelMin * 1.8 + 32; + filterModelMax = filterModelMax * 1.8 + 32; + } else if ( + filterModelBy.toLowerCase().includes("wind") && + filterModelBy.toLowerCase().includes("speed") + ) { + // convert wind speed bounds from m/s + // to mph, which is in the database. + // Note that the u- and v- components are stored in m/s + filterModelMin *= 2.23693629; + filterModelMax *= 2.23693629; + } + filterInfo.filterModelMin = filterModelMin; + filterInfo.filterModelMax = filterModelMax; + } + + if (filterObsBy !== "None") { + // get the variable text that we'll query off of + const filterObsVariable = Object.keys(variableValuesMap).filter( + (fv) => Object.keys(variableValuesMap[fv][0]).indexOf(filterObsBy) !== -1 + )[0]; + const filterObsVariableDetails = + variableValuesMap[filterObsVariable][0][filterObsBy]; + [, filterInfo.filterObsBy] = filterObsVariableDetails; + + // get the bounds and make sure they're in the right units + let filterObsMin = Number(curve["filter-obs-min"]); + let filterObsMax = Number(curve["filter-obs-max"]); + if ( + filterObsBy.toLowerCase().includes("temperature") || + filterObsBy.toLowerCase().includes("dewpoint") + ) { + // convert temperature and dewpoint bounds from Celsius + // to Fahrenheit, which is in the database + filterObsMin = filterObsMin * 1.8 + 32; + filterObsMax = filterObsMax * 1.8 + 32; + } else if ( + filterObsBy.toLowerCase().includes("wind") && + filterObsBy.toLowerCase().includes("speed") + ) { + // convert wind speed bounds from m/s + // to mph, which is in the database. + // Note that the u- and v- components are stored in m/s + filterObsMin *= 2.23693629; + filterObsMax *= 2.23693629; + } + filterInfo.filterObsMin = filterObsMin; + filterInfo.filterObsMax = filterObsMax; + } + + let queryTemplate; + let sitesList; + const regionType = + filterModelBy === "None" && // not filtering the model by anything + filterObsBy === "None" && // not filtering the obs by anything + !( + // not a thresholded variable that we're forcing into a scalar stat + ( + variableValuesMap[queryVariableX][1] && + statisticOptionsMap[variableX][statisticSelectX][0] === "scalar" + ) + ) && + !( + // not a thresholded variable that we're forcing into a scalar stat + ( + variableValuesMap[queryVariableY][1] && + statisticOptionsMap[variableY][statisticSelectY][0] === "scalar" + ) + ) + ? curve["region-type"] + : "Select stations"; + if (curve["region-type"] === "Predefined region") { + // either a true predefined region or a station plot masquerading + // as a predefined region that we will have to do filtering on. + // the regionType constant defined above knows which on. + const regionStr = curve.region; + const regionValues = ( + await matsCollections.region.findOneAsync({ name: "region" }) + ).valuesMap; + const region = Object.keys(regionValues).find( + (key) => regionValues[key] === regionStr + ); + + if (regionType === "Predefined region") { + // Predefined region, no filtering. + let statTemplate; + queryTemplate = await Assets.getTextAsync("sqlTemplates/tmpl_Scatter.sql"); + queryTemplate = queryTemplate.replace(/{{vxMODEL}}/g, model); + queryTemplate = queryTemplate.replace(/{{vxREGION}}/g, region); + queryTemplate = queryTemplate.replace(/{{vxFROM_SECS}}/g, fromSecs); + queryTemplate = queryTemplate.replace(/{{vxTO_SECS}}/g, toSecs); + if (queryVariableX === queryVariableY) { + queryTemplate = queryTemplate.replace( + /IN \['{{vxVARIABLEX}}', '{{vxVARIABLEY}}'\]/g, + `= '${queryVariableX.toUpperCase()}'` + ); + } else { + queryTemplate = queryTemplate.replace( + /{{vxVARIABLEX}}/g, + queryVariableX.toUpperCase() + ); + queryTemplate = queryTemplate.replace( + /{{vxVARIABLEY}}/g, + queryVariableY.toUpperCase() + ); + } + if (binParam !== "Fcst lead time") { + if (forecastLength === undefined) { + throw new Error( + `INFO: ${label}'s forecast lead time is undefined. Please assign it a value.` + ); + } + queryTemplate = queryTemplate.replace(/{{vxFCST_LEN}}/g, forecastLength); + } else { + queryTemplate = global.cbPool.trfmSQLRemoveClause( + queryTemplate, + "{{vxFCST_LEN}}" + ); + } + queryTemplate = queryTemplate.replace(/{{vxBIN_CLAUSE}}/g, binClause); + if (statTypeX === "ctc" && statTypeY === "ctc") { + statTemplate = await Assets.getTextAsync("sqlTemplates/tmpl_CTC_2d.sql"); + queryTemplate = queryTemplate.replace(/{{vxSTATISTIC}}/g, statTemplate); + queryTemplate = queryTemplate.replace(/{{vxTHRESHOLDX}}/g, thresholdX); + queryTemplate = queryTemplate.replace(/{{vxTHRESHOLDY}}/g, thresholdY); + queryTemplate = queryTemplate.replace( + /IN \['{{vxTYPEX}}', '{{vxTYPEY}}'\]/g, + "= 'CTC'" + ); + } else if (statTypeX === "ctc" && statTypeY === "scalar") { + statTemplate = await Assets.getTextAsync( + "sqlTemplates/tmpl_CTC_PartialSums_2d.sql" + ); + queryTemplate = queryTemplate.replace(/{{vxSTATISTIC}}/g, statTemplate); + queryTemplate = queryTemplate.replace(/{{vxTHRESHOLDX}}/g, thresholdX); + queryTemplate = queryTemplate.replace( + /{{vxSUBVARIABLEY}}/g, + variableDetailsY[0] + ); + queryTemplate = queryTemplate.replace(/{{vxTYPEX}}/g, "CTC"); + queryTemplate = queryTemplate.replace(/{{vxTYPEY}}/g, "SUMS"); + } else if (statTypeX === "scalar" && statTypeY === "ctc") { + statTemplate = await Assets.getTextAsync( + "sqlTemplates/tmpl_PartialSums_CTC_2d.sql" + ); + queryTemplate = queryTemplate.replace(/{{vxSTATISTIC}}/g, statTemplate); + queryTemplate = queryTemplate.replace( + /{{vxSUBVARIABLEX}}/g, + variableDetailsX[0] + ); + queryTemplate = queryTemplate.replace(/{{vxTHRESHOLDY}}/g, thresholdY); + queryTemplate = queryTemplate.replace(/{{vxTYPEX}}/g, "SUMS"); + queryTemplate = queryTemplate.replace(/{{vxTYPEY}}/g, "CTC"); + } else { + statTemplate = await Assets.getTextAsync( + "sqlTemplates/tmpl_PartialSums_2d.sql" + ); + queryTemplate = queryTemplate.replace(/{{vxSTATISTIC}}/g, statTemplate); + queryTemplate = queryTemplate.replace( + /{{vxSUBVARIABLEX}}/g, + variableDetailsX[0] + ); + queryTemplate = queryTemplate.replace( + /{{vxSUBVARIABLEY}}/g, + variableDetailsY[0] + ); + queryTemplate = queryTemplate.replace( + /IN \['{{vxTYPEX}}', '{{vxTYPEY}}'\]/g, + "= 'SUMS'" + ); + } + + let dateString = ""; + if (binParam === "Init Date") { + dateString = "m0.fcstValidEpoch-m0.fcstLen*3600"; + } else { + dateString = "m0.fcstValidEpoch"; + } + queryTemplate = queryTemplate.replace(/{{vxDATE_STRING}}/g, dateString); + if (binParam !== "Valid UTC hour") { + if (validTimes.length !== 0 && validTimes !== matsTypes.InputTypes.unused) { + queryTemplate = queryTemplate.replace( + /{{vxVALID_TIMES}}/g, + global.cbPool.trfmListToCSVString(validTimes, null, false) + ); + } else { + queryTemplate = global.cbPool.trfmSQLRemoveClause( + queryTemplate, + "{{vxVALID_TIMES}}" + ); + } + } else { + queryTemplate = global.cbPool.trfmSQLRemoveClause( + queryTemplate, + "{{vxVALID_TIMES}}" + ); + } + } else { + // Predefined region, with filtering. Treat like station plot. + sitesList = await matsDataQueryUtils.getStationsInCouchbaseRegion( + global.cbPool, + region + ); + } + } else { + // Station plot, with or without filtering + sitesList = curve.sites === undefined ? [] : curve.sites; + if (sitesList.length === 0 || sitesList === matsTypes.InputTypes.unused) { + throw new Error( + "INFO: Please add sites in order to get a single/multi station plot." + ); + } + } + const elevMap = ( + await matsCollections.StationMap.findOneAsync({ + name: "elevations", + }) + ).optionsMap; + + // axisKey is used to determine which axis a curve should use. + // This axisKeySet object is used like a set and if a curve has the same + // units (axisKey) it will use the same axis. + // The axis number is assigned to the axisKeySet value, which is the axisKey. + const trimmedVariableX = variableX.split(" (")[0]; + const trimmedVariableY = variableY.split(" (")[0]; + const trimmedStatisticX = statisticSelectX.split(" (")[0]; + const trimmedStatisticY = statisticSelectY.split(" (")[0]; + varUnitsX = + statisticOptionsMap[variableX][statisticSelectX][1] === "Unknown" + ? variableDetailsX[2] + : statisticOptionsMap[variableX][statisticSelectX][1]; + varUnitsY = + statisticOptionsMap[variableY][statisticSelectY][1] === "Unknown" + ? variableDetailsY[2] + : statisticOptionsMap[variableY][statisticSelectY][1]; + varUnitsX = `${trimmedVariableX} ${trimmedStatisticX} (${varUnitsX})`; + varUnitsY = `${trimmedVariableY} ${trimmedStatisticY} (${varUnitsY})`; + allStatTypes.push([statTypeX, statTypeY]); + + let d; + if (!diffFrom) { + let queryResult; + const startMoment = moment(); + let finishMoment; + try { + // math is done on forecastLength later on -- set all analyses to 0 + if (forecastLength === "-99") { + forecastLength = "0"; + } + + if (regionType === "Predefined region") { + statement = global.cbPool.trfmSQLForDbTarget(queryTemplate); + } else { + // send to matsMiddle + statement = "Station plot -- no one query."; + const tss = new matsMiddleSimpleScatter.MatsMiddleSimpleScatter( + global.cbPool + ); + rows = await tss.processStationQuery( + binParam, + statTypeX, + statTypeY, + variableDetailsX[1], + variableDetailsY[1], + sitesList, + model, + forecastLength, + thresholdX, + thresholdY, + fromSecs, + toSecs, + validTimes, + filterInfo, + elevMap + ); + } + + // send the query statement to the query function + queryResult = await matsDataQueryUtils.queryDBSimpleScatter( + global.cbPool, + regionType === "Predefined region" ? statement : rows, + appParams, + statTypeX, + statTypeY, + statTypeX === "ctc" ? statisticSelectX : `${statisticSelectX}_${variableX}`, + statTypeY === "ctc" ? statisticSelectY : `${statisticSelectY}_${variableY}` + ); + + finishMoment = moment(); + dataRequests[label] = statement; + dataRequests[`data retrieval (query) time - ${label}`] = { + begin: startMoment.format(), + finish: finishMoment.format(), + duration: `${moment + .duration(finishMoment.diff(startMoment)) + .asSeconds()} seconds`, + recordCount: queryResult.data.x.length, + }; + // get the data back from the query + d = queryResult.data; + } catch (e) { + // this is an error produced by a bug in the query function, not an error returned by the mysql database + e.message = `Error in queryDB: ${e.message} for statement: ${statement}`; + throw new Error(e.message); + } + + if (queryResult.error !== undefined && queryResult.error !== "") { + if (queryResult.error === matsTypes.Messages.NO_DATA_FOUND) { + // this is NOT an error just a no data condition + dataFoundForCurve = false; + } else { + // this is an error returned by the mysql database + error += `Error from verification query:
${queryResult.error}
query:
${statement}
`; + throw new Error(error); + } + } else { + dataFoundForAnyCurve = true; + } + + // set axis limits based on returned data + if (dataFoundForCurve) { + xmin = xmin < d.xmin ? xmin : d.xmin; + xmax = xmax > d.xmax ? xmax : d.xmax; + ymin = ymin < d.ymin ? ymin : d.ymin; + ymax = ymax > d.ymax ? ymax : d.ymax; + } + } else { + // this is a difference curve -- not supported for scatter plots + throw new Error( + "INFO: Difference curves are not supported for performance diagrams, as they do not feature consistent x or y values across all curves." + ); + } + + // set curve annotation to be the curve mean -- may be recalculated later + // also pass previously calculated axis stats to curve options + const postQueryStartMoment = moment(); + const mean = d.sum / d.x.length; + const annotation = + mean === undefined + ? `${label}- mean = NoData` + : `${label}- mean = ${mean.toPrecision(4)}`; + curve.annotation = annotation; + curve.xmin = d.xmin; + curve.xmax = d.xmax; + curve.ymin = d.ymin; + curve.ymax = d.ymax; + curve.axisXKey = varUnitsX; + curve.axisYKey = varUnitsY; + curve.binParam = binParam; + const cOptions = await matsDataCurveOpsUtils.generateScatterCurveOptions( + curve, + curveIndex, + axisXMap, + axisYMap, + d, + appParams + ); // generate plot with data, curve annotation, axis labels, etc. + dataset.push(cOptions); + const postQueryFinishMoment = moment(); + dataRequests[`post data retrieval (query) process time - ${label}`] = { + begin: postQueryStartMoment.format(), + finish: postQueryFinishMoment.format(), + duration: `${moment + .duration(postQueryFinishMoment.diff(postQueryStartMoment)) + .asSeconds()} seconds`, + }; + } // end for curves + + if (!dataFoundForAnyCurve) { + // we found no data for any curves so don't bother proceeding + throw new Error("INFO: No valid data for any curves."); + } + + // process the data returned by the query + const curveInfoParams = { + curves, + curvesLength, + statType: allStatTypes, + axisXMap, + axisYMap, + xmax, + xmin, + }; + const bookkeepingParams = { + dataRequests, + totalProcessingStart, + }; + const result = await matsDataProcessUtils.processDataSimpleScatter( + dataset, + appParams, + curveInfoParams, + plotParams, + bookkeepingParams + ); + return result; +}; diff --git a/apps/cb-metar/server/dataFunctions/data_threshold.js b/apps/cb-metar/server/dataFunctions/data_threshold.js index 208f573986..2167788fd9 100644 --- a/apps/cb-metar/server/dataFunctions/data_threshold.js +++ b/apps/cb-metar/server/dataFunctions/data_threshold.js @@ -97,6 +97,15 @@ global.dataThreshold = async function (plotParams) { [statType] = statisticOptionsMap[variable][statisticSelect]; allStatTypes.push(statType); + if ( + variableValuesMap[queryVariable][1] && + statisticOptionsMap[variable][statisticSelect][0] === "scalar" + ) { + throw new Error( + "INFO: Plotting thresholded variables (e.g. ceiling, visibility) with scalar statistics (e.g. RMSE, additive bias) is not available for this plot type." + ); + } + let queryTemplate; const regionType = curve["region-type"]; if (regionType === "Select stations") { diff --git a/apps/cb-metar/server/dataFunctions/data_validtime.js b/apps/cb-metar/server/dataFunctions/data_validtime.js index bad7331152..be1d60650b 100644 --- a/apps/cb-metar/server/dataFunctions/data_validtime.js +++ b/apps/cb-metar/server/dataFunctions/data_validtime.js @@ -172,7 +172,15 @@ global.dataValidTime = async function (plotParams) { let queryTemplate; let sitesList; const regionType = - filterModelBy === "None" && filterObsBy === "None" + filterModelBy === "None" && // not filtering the model by anything + filterObsBy === "None" && // not filtering the obs by anything + !( + // not a thresholded variable that we're forcing into a scalar stat + ( + variableValuesMap[queryVariable][1] && + statisticOptionsMap[variable][statisticSelect][0] === "scalar" + ) + ) ? curve["region-type"] : "Select stations"; if (curve["region-type"] === "Predefined region") { diff --git a/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_CTC_2d.sql b/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_CTC_2d.sql new file mode 100644 index 0000000000..9245b4bb95 --- /dev/null +++ b/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_CTC_2d.sql @@ -0,0 +1,18 @@ + SUM(m0.data.['{{vxTHRESHOLDX}}'].hits) hitX, + SUM(m0.data.['{{vxTHRESHOLDX}}'].false_alarmsX) faX, + SUM(m0.data.['{{vxTHRESHOLDX}}'].missesX) missX, + SUM(m0.data.['{{vxTHRESHOLDX}}'].correct_negatives) cnX, + SUM(m0.data.['{{vxTHRESHOLDY}}'].hits) hitY, + SUM(m0.data.['{{vxTHRESHOLDY}}'].false_alarms) faY, + SUM(m0.data.['{{vxTHRESHOLDY}}'].misses) missY, + SUM(m0.data.['{{vxTHRESHOLDY}}'].correct_negatives) cnY, + ARRAY_SORT( ARRAY_AGG( CASE WHEN m0.fcstValidEpoch IS NOT NULL THEN TO_STRING(m0.fcstValidEpoch) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDX}}'].hits IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDX}}'].hits) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDX}}'].false_alarms IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDX}}'].false_alarms) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDX}}'].misses IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDX}}'].misses) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDX}}'].correct_negatives IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDX}}'].correct_negatives) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDY}}'].hits IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDY}}'].hits) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDY}}'].false_alarms IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDY}}'].false_alarms) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDY}}'].misses IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDY}}'].misses) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDY}}'].correct_negatives IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDY}}'].correct_negatives) ELSE "NULL" END ) ) sub_data, + COUNT(m0.data.['{{vxTHRESHOLDX}}'].hits) n0 diff --git a/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_CTC_PartialSums_2d.sql b/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_CTC_PartialSums_2d.sql new file mode 100644 index 0000000000..205a11a6cf --- /dev/null +++ b/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_CTC_PartialSums_2d.sql @@ -0,0 +1,22 @@ + SUM(m0.data.['{{vxTHRESHOLDX}}'].hits) hitX, + SUM(m0.data.['{{vxTHRESHOLDX}}'].false_alarmsX) faX, + SUM(m0.data.['{{vxTHRESHOLDX}}'].missesX) missX, + SUM(m0.data.['{{vxTHRESHOLDX}}'].correct_negatives) cnX, + SUM(m0.data.['{{vxSUBVARIABLEY}}'].sum2_diff) square_diff_sumY, + SUM(m0.data.['{{vxSUBVARIABLEY}}'].num_recs) N_sumY, + SUM(m0.data.['{{vxSUBVARIABLEY}}'].sum_diff * -1) obs_model_diff_sumY, + SUM(m0.data.['{{vxSUBVARIABLEY}}'].sum_model) model_sumY, + SUM(m0.data.['{{vxSUBVARIABLEY}}'].sum_obs) obs_sumY, + SUM(m0.data.['{{vxSUBVARIABLEY}}'].sum_abs) abs_sumY, + ARRAY_SORT( ARRAY_AGG( CASE WHEN m0.fcstValidEpoch IS NOT NULL THEN TO_STRING(m0.fcstValidEpoch) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDX}}'].hits IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDX}}'].hits) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDX}}'].false_alarms IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDX}}'].false_alarms) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDX}}'].misses IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDX}}'].misses) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDX}}'].correct_negatives IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDX}}'].correct_negatives) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEY}}'].sum2_diff IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEY}}'].sum2_diff) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEY}}'].num_recs IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEY}}'].num_recs) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEY}}'].sum_diff IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEY}}'].sum_diff * -1) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEY}}'].sum_model IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEY}}'].sum_model) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEY}}'].sum_obs IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEY}}'].sum_obs) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEY}}'].sum_abs IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEY}}'].sum_abs) ELSE "NULL" END ) ) sub_data, + COUNT(m0.data.['{{vxTHRESHOLDX}}'].hits) n0 diff --git a/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_PartialSums_2d.sql b/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_PartialSums_2d.sql new file mode 100644 index 0000000000..10ab167651 --- /dev/null +++ b/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_PartialSums_2d.sql @@ -0,0 +1,26 @@ + SUM(m0.data.['{{vxSUBVARIABLEX}}'].sum2_diff) square_diff_sumX, + SUM(m0.data.['{{vxSUBVARIABLEX}}'].num_recs) N_sumX, + SUM(m0.data.['{{vxSUBVARIABLEX}}'].sum_diff * -1) obs_model_diff_sumX, + SUM(m0.data.['{{vxSUBVARIABLEX}}'].sum_model) model_sumX, + SUM(m0.data.['{{vxSUBVARIABLEX}}'].sum_obs) obs_sumX, + SUM(m0.data.['{{vxSUBVARIABLEX}}'].sum_abs) abs_sumX, + SUM(m0.data.['{{vxSUBVARIABLEY}}'].sum2_diff) square_diff_sumY, + SUM(m0.data.['{{vxSUBVARIABLEY}}'].num_recs) N_sumY, + SUM(m0.data.['{{vxSUBVARIABLEY}}'].sum_diff * -1) obs_model_diff_sumY, + SUM(m0.data.['{{vxSUBVARIABLEY}}'].sum_model) model_sumY, + SUM(m0.data.['{{vxSUBVARIABLEY}}'].sum_obs) obs_sumY, + SUM(m0.data.['{{vxSUBVARIABLEY}}'].sum_abs) abs_sumY, + ARRAY_SORT( ARRAY_AGG( CASE WHEN m0.fcstValidEpoch IS NOT NULL THEN TO_STRING(m0.fcstValidEpoch) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEX}}'].sum2_diff IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEX}}'].sum2_diff) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEX}}'].num_recs IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEX}}'].num_recs) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEX}}'].sum_diff IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEX}}'].sum_diff * -1) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEX}}'].sum_model IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEX}}'].sum_model) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEX}}'].sum_obs IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEX}}'].sum_obs) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEX}}'].sum_abs IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEX}}'].sum_abs) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEY}}'].sum2_diff IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEY}}'].sum2_diff) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEY}}'].num_recs IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEY}}'].num_recs) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEY}}'].sum_diff IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEY}}'].sum_diff * -1) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEY}}'].sum_model IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEY}}'].sum_model) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEY}}'].sum_obs IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEY}}'].sum_obs) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEY}}'].sum_abs IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEY}}'].sum_abs) ELSE "NULL" END ) ) sub_data, + COUNT(m0.data.['{{vxSUBVARIABLEX}}'].sum2_diff) n0 diff --git a/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_PartialSums_CTC_2d.sql b/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_PartialSums_CTC_2d.sql new file mode 100644 index 0000000000..2760ab5f5d --- /dev/null +++ b/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_PartialSums_CTC_2d.sql @@ -0,0 +1,22 @@ + SUM(m0.data.['{{vxSUBVARIABLEX}}'].sum2_diff) square_diff_sumX, + SUM(m0.data.['{{vxSUBVARIABLEX}}'].num_recs) N_sumX, + SUM(m0.data.['{{vxSUBVARIABLEX}}'].sum_diff * -1) obs_model_diff_sumX, + SUM(m0.data.['{{vxSUBVARIABLEX}}'].sum_model) model_sumX, + SUM(m0.data.['{{vxSUBVARIABLEX}}'].sum_obs) obs_sumX, + SUM(m0.data.['{{vxSUBVARIABLEX}}'].sum_abs) abs_sumX, + SUM(m0.data.['{{vxTHRESHOLDY}}'].hits) hitY, + SUM(m0.data.['{{vxTHRESHOLDY}}'].false_alarms) faY, + SUM(m0.data.['{{vxTHRESHOLDY}}'].misses) missY, + SUM(m0.data.['{{vxTHRESHOLDY}}'].correct_negatives) cnY, + ARRAY_SORT( ARRAY_AGG( CASE WHEN m0.fcstValidEpoch IS NOT NULL THEN TO_STRING(m0.fcstValidEpoch) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEX}}'].sum2_diff IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEX}}'].sum2_diff) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEX}}'].num_recs IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEX}}'].num_recs) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEX}}'].sum_diff IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEX}}'].sum_diff * -1) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEX}}'].sum_model IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEX}}'].sum_model) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEX}}'].sum_obs IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEX}}'].sum_obs) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxSUBVARIABLEX}}'].sum_abs IS NOT NULL THEN TO_STRING(m0.data.['{{vxSUBVARIABLEX}}'].sum_abs) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDY}}'].hits IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDY}}'].hits) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDY}}'].false_alarms IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDY}}'].false_alarms) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDY}}'].misses IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDY}}'].misses) ELSE "NULL" END + || ';' || CASE WHEN m0.data.['{{vxTHRESHOLDY}}'].correct_negatives IS NOT NULL THEN TO_STRING(m0.data.['{{vxTHRESHOLDY}}'].correct_negatives) ELSE "NULL" END ) ) sub_data, + COUNT(m0.data.['{{vxSUBVARIABLEX}}'].sum2_diff) n0 diff --git a/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_Scatter.sql b/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_Scatter.sql new file mode 100644 index 0000000000..39d7ae9153 --- /dev/null +++ b/apps/cb-metar/server/dataFunctions/sqlTemplates/tmpl_Scatter.sql @@ -0,0 +1,19 @@ +SELECT {{vxBIN_CLAUSE}} AS binVal, + COUNT(DISTINCT m0.fcstValidEpoch) nTimes, + MIN(m0.fcstValidEpoch) min_secs, + MAX(m0.fcstValidEpoch) max_secs, + {{vxSTATISTIC}} +FROM {{vxDBTARGET}} m0 +WHERE m0.type = 'DD' + AND m0.docType IN ['{{vxTYPEX}}', '{{vxTYPEY}}'] + AND m0.subDocType IN ['{{vxVARIABLEX}}', '{{vxVARIABLEY}}'] + AND m0.subset = 'METAR' + AND m0.version = 'V01' + AND m0.model = '{{vxMODEL}}' + AND m0.region = '{{vxREGION}}' + AND m0.fcstLen = {{vxFCST_LEN}} + AND m0.fcstValidEpoch %(24 * 3600) / 3600 IN [{{vxVALID_TIMES}}] + AND {{vxDATE_STRING}} >= {{vxFROM_SECS}} + AND {{vxDATE_STRING}} <= {{vxTO_SECS}} +GROUP BY {{vxBIN_CLAUSE}} +ORDER BY binVal; diff --git a/apps/cb-metar/server/main.js b/apps/cb-metar/server/main.js index d244591168..fb8fa5c28d 100644 --- a/apps/cb-metar/server/main.js +++ b/apps/cb-metar/server/main.js @@ -32,7 +32,7 @@ const variableMetadataDocs = { "Elevation (m)": ["Elevation", "Elevation", "m"], "Temperature at 2m (°C)": ["Temperature", "Temperature", "°C"], "Dewpoint at 2m (°C)": ["DewPoint", "DewPoint", "°C"], - "Relative Humidity at 2m (%)": ["RelativeHumidity", "RH", "RH (%)"], + "Relative Humidity at 2m (%)": ["RelativeHumidity", "RH", "%"], "Surface Pressure (hPa)": ["SurfacePressure", "Surface Pressure", "hPa"], "Normalized Surface Pressure (hPa)": [ "Normalized Surface Pressure", @@ -419,7 +419,7 @@ const doCurveParams = async function () { } allVariablesYesThreshold = allVariablesYesThreshold.concat(subVariables); } else { - rows = [{ NA: "NA" }]; + rows = [{ "All Data": "All Data" }]; allVariablesNoThreshold = allVariablesNoThreshold.concat(subVariables); } for (let sidx = 0; sidx < subVariables.length; sidx += 1) { @@ -488,7 +488,7 @@ const doCurveParams = async function () { forecastLengthOptionsMap[subVariable][model] = rows[i].fcstLens.map(String); // we want the full threshold descriptions in thresholdsModelOptionsMap, not just the thresholds - const thresholds = rows[i].thresholds ? rows[i].thresholds : ["NA"]; + const thresholds = rows[i].thresholds ? rows[i].thresholds : ["All Data"]; thresholdsModelOptionsMap[subVariable][model] = thresholds .sort(function (a, b) { return Number(a) - Number(b); @@ -609,6 +609,7 @@ const doCurveParams = async function () { matsTypes.PlotTypes.histogram, matsTypes.PlotTypes.contour, matsTypes.PlotTypes.contourDiff, + matsTypes.PlotTypes.simpleScatter, ], dependentNames: ["variable"], controlButtonCovered: false, @@ -632,6 +633,7 @@ const doCurveParams = async function () { varOptionsMap[matsTypes.PlotTypes.histogram] = allVariables; varOptionsMap[matsTypes.PlotTypes.contour] = allVariables; varOptionsMap[matsTypes.PlotTypes.contourDiff] = allVariables; + varOptionsMap[matsTypes.PlotTypes.simpleScatter] = allVariables; if ( (await matsCollections.variable.findOneAsync({ name: "variable" })) === undefined @@ -644,17 +646,14 @@ const doCurveParams = async function () { valuesMap: variableMetadataDocs, dates: modelDateRangeMap, superiorNames: ["plot-type"], - dependentNames: ["data-source", "statistic"], + dependentNames: ["data-source", "statistic", "threshold"], controlButtonCovered: true, default: varOptionsMap[defaultPlotType][0], - hideOtherFor: { - threshold: allVariablesNoThreshold, - }, unique: false, controlButtonVisibility: "block", - displayOrder: 3, + displayOrder: 2, displayPriority: 1, - displayGroup: 1, + displayGroup: 2, }); } else { // it is defined but check for necessary update @@ -681,6 +680,98 @@ const doCurveParams = async function () { } } + if ( + (await matsCollections["x-variable"].findOneAsync({ name: "x-variable" })) === + undefined + ) { + await matsCollections["x-variable"].insertAsync({ + name: "x-variable", + type: matsTypes.InputTypes.select, + options: varOptionsMap[defaultPlotType], + optionsMap: varOptionsMap, + valuesMap: variableMetadataDocs, + dates: modelDateRangeMap, + superiorNames: ["plot-type"], + dependentNames: ["data-source", "x-statistic", "x-threshold"], + controlButtonCovered: true, + default: varOptionsMap[defaultPlotType][0], + unique: false, + controlButtonVisibility: "block", + displayOrder: 3, + displayPriority: 1, + displayGroup: 2, + }); + } else { + // it is defined but check for necessary update + const currentParam = await matsCollections["x-variable"].findOneAsync({ + name: "x-variable", + }); + if ( + !matsDataUtils.areObjectsEqual(currentParam.optionsMap, varOptionsMap) || + !matsDataUtils.areObjectsEqual(currentParam.dates, modelDateRangeMap) + ) { + // have to reload variable data + await matsCollections["x-variable"].updateAsync( + { name: "x-variable" }, + { + $set: { + options: varOptionsMap[defaultPlotType], + optionsMap: varOptionsMap, + valuesMap: variableMetadataDocs, + dates: modelDateRangeMap, + default: varOptionsMap[defaultPlotType][0], + }, + } + ); + } + } + + if ( + (await matsCollections["y-variable"].findOneAsync({ name: "y-variable" })) === + undefined + ) { + await matsCollections["y-variable"].insertAsync({ + name: "y-variable", + type: matsTypes.InputTypes.select, + options: varOptionsMap[defaultPlotType], + optionsMap: varOptionsMap, + valuesMap: variableMetadataDocs, + dates: modelDateRangeMap, + superiorNames: ["plot-type"], + dependentNames: ["data-source", "y-statistic", "y-threshold"], + controlButtonCovered: true, + default: varOptionsMap[defaultPlotType][0], + unique: false, + controlButtonVisibility: "block", + displayOrder: 4, + displayPriority: 1, + displayGroup: 2, + }); + } else { + // it is defined but check for necessary update + const currentParam = await matsCollections["y-variable"].findOneAsync({ + name: "y-variable", + }); + if ( + !matsDataUtils.areObjectsEqual(currentParam.optionsMap, varOptionsMap) || + !matsDataUtils.areObjectsEqual(currentParam.dates, modelDateRangeMap) + ) { + // have to reload variable data + await matsCollections["y-variable"].updateAsync( + { name: "y-variable" }, + { + $set: { + options: varOptionsMap[defaultPlotType], + optionsMap: varOptionsMap, + valuesMap: variableMetadataDocs, + dates: modelDateRangeMap, + default: varOptionsMap[defaultPlotType][0], + }, + } + ); + } + } + if ( (await matsCollections["region-type"].findOneAsync({ name: "region-type" })) === undefined @@ -697,9 +788,9 @@ const doCurveParams = async function () { }, controlButtonCovered: true, controlButtonText: "Region mode", - displayOrder: 1, + displayOrder: 3, displayPriority: 1, - displayGroup: 2, + displayGroup: 1, }); } @@ -766,7 +857,7 @@ const doCurveParams = async function () { Object.keys(regionModelOptionsMap[allVariables[0]])[0] ][0], controlButtonVisibility: "block", - displayOrder: 2, + displayOrder: 1, displayPriority: 1, displayGroup: 2, }); @@ -798,80 +889,133 @@ const doCurveParams = async function () { } } - if ( - (await matsCollections.statistic.findOneAsync({ name: "statistic" })) === undefined - ) { - const ctcOptionsMap = { - "CSI (Critical Success Index)": ["ctc", "x100", 100], + const ctcOptionsMap = { + "CSI (Critical Success Index)": ["ctc", "x100", 100], - "TSS (True Skill Score)": ["ctc", "x100", 100], + "TSS (True Skill Score)": ["ctc", "x100", 100], - "PODy (POD of value < threshold)": ["ctc", "x100", 100], + "PODy (POD of value < threshold)": ["ctc", "x100", 100], - "PODn (POD of value > threshold)": ["ctc", "x100", 100], + "PODn (POD of value > threshold)": ["ctc", "x100", 100], - "FAR (False Alarm Ratio)": ["ctc", "x100", 0], + "FAR (False Alarm Ratio)": ["ctc", "x100", 0], - "Bias (forecast/actual)": ["ctc", "Ratio", 1], + "Bias (Forecast / Actual)": ["ctc", "Ratio", 1], - "HSS (Heidke Skill Score)": ["ctc", "x100", 100], + "Bias (Model - Obs)": ["scalar", "Unknown", null], - "ETS (Equitable Threat Score)": ["ctc", "x100", 100], + "HSS (Heidke Skill Score)": ["ctc", "x100", 100], - "Nlow (Number of obs < threshold (hits + misses))": ["ctc", "Number", null], + "ETS (Equitable Threat Score)": ["ctc", "x100", 100], - "Nhigh (Number of obs > threshold (false alarms + correct nulls))": [ - "ctc", - "Number", - null, - ], + "Model average": ["scalar", "Unknown", null], - "Ntot (Total number of obs, (Nlow + Nhigh))": ["ctc", "Number", null], + "Obs average": ["scalar", "Unknown", null], - "Ratio Nlow / Ntot ((hit + miss)/(hit + miss + fa + cn))": ["ctc", "Ratio", null], + RMSE: ["scalar", "Unknown", null], - "Ratio Nhigh / Ntot ((fa + cn)/(hit + miss + fa + cn))": ["ctc", "Ratio", null], + "Std deviation": ["scalar", "Unknown", null], - "N times*levels(*stations if station plot) per graph point": [ - "ctc", - "Number", - null, - ], - }; - const scalarOptionsMap = { - RMSE: ["scalar", "Unknown", null], + "MAE (temp and dewpoint only)": ["scalar", "Unknown", null], - "Bias (Model - Obs)": ["scalar", "Unknown", null], + "Nlow (Number of obs < threshold (hits + misses))": ["ctc", "Number", null], - N: ["scalar", "Number", null], + "Nhigh (Number of obs > threshold (false alarms + correct nulls))": [ + "ctc", + "Number", + null, + ], - "Model average": ["scalar", "Unknown", null], + "Ntot (Total number of obs, (Nlow + Nhigh))": ["ctc", "Number", null], - "Obs average": ["scalar", "Unknown", null], + "Ratio Nlow / Ntot ((hit + miss)/(hit + miss + fa + cn))": ["ctc", "Ratio", null], - "Std deviation": ["scalar", "Unknown", null], + "Ratio Nhigh / Ntot ((fa + cn)/(hit + miss + fa + cn))": ["ctc", "Ratio", null], - "MAE (temp and dewpoint only)": ["scalar", "Unknown", null], - }; - const optionsMap = {}; - for (let vidx = 0; vidx < allVariables.length; vidx += 1) { - const variable = allVariables[vidx]; - optionsMap[variable] = - allVariablesYesThreshold.indexOf(variable) !== -1 - ? ctcOptionsMap - : scalarOptionsMap; - } + "N times*levels(*stations if station plot) per graph point": [ + "ctc", + "Number", + null, + ], + }; + const scalarOptionsMap = { + RMSE: ["scalar", "Unknown", null], + + "Bias (Model - Obs)": ["scalar", "Unknown", null], + + N: ["scalar", "Number", null], + + "Model average": ["scalar", "Unknown", null], + + "Obs average": ["scalar", "Unknown", null], + + "Std deviation": ["scalar", "Unknown", null], + + "MAE (temp and dewpoint only)": ["scalar", "Unknown", null], + }; + const statOptionsMap = {}; + for (let vidx = 0; vidx < allVariables.length; vidx += 1) { + const variable = allVariables[vidx]; + statOptionsMap[variable] = + allVariablesYesThreshold.indexOf(variable) !== -1 + ? ctcOptionsMap + : scalarOptionsMap; + } + + if ( + (await matsCollections.statistic.findOneAsync({ name: "statistic" })) === undefined + ) { await matsCollections.statistic.insertAsync({ name: "statistic", type: matsTypes.InputTypes.select, - optionsMap, - options: Object.keys(optionsMap), + optionsMap: statOptionsMap, + options: Object.keys(statOptionsMap), superiorNames: ["variable"], controlButtonCovered: true, unique: false, - default: Object.keys(optionsMap)[0], + default: Object.keys(statOptionsMap)[0], controlButtonVisibility: "block", - displayOrder: 2, + displayOrder: 4, + displayPriority: 1, + displayGroup: 3, + }); + } + + if ( + (await matsCollections["x-statistic"].findOneAsync({ name: "x-statistic" })) === + undefined + ) { + await matsCollections["x-statistic"].insertAsync({ + name: "x-statistic", + type: matsTypes.InputTypes.select, + optionsMap: statOptionsMap, + options: Object.keys(statOptionsMap), + superiorNames: ["x-variable"], + controlButtonCovered: true, + unique: false, + default: Object.keys(statOptionsMap)[0], + controlButtonVisibility: "block", + displayOrder: 5, + displayPriority: 1, + displayGroup: 3, + }); + } + + if ( + (await matsCollections["y-statistic"].findOneAsync({ name: "y-statistic" })) === + undefined + ) { + await matsCollections["y-statistic"].insertAsync({ + name: "y-statistic", + type: matsTypes.InputTypes.select, + optionsMap: statOptionsMap, + options: Object.keys(statOptionsMap), + superiorNames: ["y-variable"], + controlButtonCovered: true, + unique: false, + default: Object.keys(statOptionsMap)[0], + controlButtonVisibility: "block", + displayOrder: 6, displayPriority: 1, displayGroup: 3, }); @@ -934,6 +1078,122 @@ const doCurveParams = async function () { } } + if ( + (await matsCollections["x-threshold"].findOneAsync({ name: "x-threshold" })) === + undefined + ) { + await matsCollections["x-threshold"].insertAsync({ + name: "x-threshold", + type: matsTypes.InputTypes.select, + optionsMap: thresholdsModelOptionsMap, + options: + thresholdsModelOptionsMap[allVariables[0]][ + Object.keys(thresholdsModelOptionsMap[allVariables[0]])[0] + ], + valuesMap: allThresholdValuesMap, + superiorNames: ["x-variable", "data-source"], + controlButtonCovered: true, + unique: false, + default: + thresholdsModelOptionsMap[allVariables[0]][ + Object.keys(thresholdsModelOptionsMap[allVariables[0]])[0] + ][0], + controlButtonVisibility: "block", + displayOrder: 2, + displayPriority: 1, + displayGroup: 3, + }); + } else { + // it is defined but check for necessary update + const currentParam = await matsCollections["x-threshold"].findOneAsync({ + name: "x-threshold", + }); + if ( + !matsDataUtils.areObjectsEqual( + currentParam.optionsMap, + thresholdsModelOptionsMap + ) || + !matsDataUtils.areObjectsEqual(currentParam.valuesMap, allThresholdValuesMap) + ) { + // have to reload threshold data + await matsCollections["x-threshold"].updateAsync( + { name: "x-threshold" }, + { + $set: { + optionsMap: thresholdsModelOptionsMap, + valuesMap: allThresholdValuesMap, + options: + thresholdsModelOptionsMap[allVariables[0]][ + Object.keys(thresholdsModelOptionsMap[allVariables[0]])[0] + ], + default: + thresholdsModelOptionsMap[allVariables[0]][ + Object.keys(thresholdsModelOptionsMap[allVariables[0]])[0] + ][0], + }, + } + ); + } + } + + if ( + (await matsCollections["y-threshold"].findOneAsync({ name: "y-threshold" })) === + undefined + ) { + await matsCollections["y-threshold"].insertAsync({ + name: "y-threshold", + type: matsTypes.InputTypes.select, + optionsMap: thresholdsModelOptionsMap, + options: + thresholdsModelOptionsMap[allVariables[0]][ + Object.keys(thresholdsModelOptionsMap[allVariables[0]])[0] + ], + valuesMap: allThresholdValuesMap, + superiorNames: ["y-variable", "data-source"], + controlButtonCovered: true, + unique: false, + default: + thresholdsModelOptionsMap[allVariables[0]][ + Object.keys(thresholdsModelOptionsMap[allVariables[0]])[0] + ][0], + controlButtonVisibility: "block", + displayOrder: 3, + displayPriority: 1, + displayGroup: 3, + }); + } else { + // it is defined but check for necessary update + const currentParam = await matsCollections["y-threshold"].findOneAsync({ + name: "y-threshold", + }); + if ( + !matsDataUtils.areObjectsEqual( + currentParam.optionsMap, + thresholdsModelOptionsMap + ) || + !matsDataUtils.areObjectsEqual(currentParam.valuesMap, allThresholdValuesMap) + ) { + // have to reload threshold data + await matsCollections["y-threshold"].updateAsync( + { name: "y-threshold" }, + { + $set: { + optionsMap: thresholdsModelOptionsMap, + valuesMap: allThresholdValuesMap, + options: + thresholdsModelOptionsMap[allVariables[0]][ + Object.keys(thresholdsModelOptionsMap[allVariables[0]])[0] + ], + default: + thresholdsModelOptionsMap[allVariables[0]][ + Object.keys(thresholdsModelOptionsMap[allVariables[0]])[0] + ][0], + }, + } + ); + } + } + if ( (await matsCollections["forecast-length"].findOneAsync({ name: "forecast-length", @@ -1895,6 +2155,53 @@ const doCurveTextPatterns = async function () { ], groupSize: 6, }); + await matsCollections.CurveTextPatterns.insertAsync({ + plotType: matsTypes.PlotTypes.simpleScatter, + textPattern: [ + ["", "label", ": "], + ["", "data-source", " in "], + ["", "sites", ", "], + ["", "region", ", "], + ["", "forecast-length", "h "], + ["", "x-variable", " "], + ["", "x-statistic", " at "], + ["", "x-threshold", " vs "], + ["", "y-variable", " "], + ["", "y-statistic", " at "], + ["", "y-threshold", ", "], + ["valid at: ", "valid-time", " UTC, "], + ["Model filtered by: ", "filter-model-by", " "], + ["range: ", "filter-model-min", " "], + ["to ", "filter-model-max", ". "], + ["Obs filtered by: ", "filter-obs-by", " "], + ["range: ", "filter-obs-min", " "], + ["to ", "filter-obs-max", ". "], + ], + displayParams: [ + "label", + "x-variable", + "y-variable", + "data-source", + "region-type", + "region", + "x-statistic", + "x-threshold", + "y-statistic", + "y-threshold", + "forecast-length", + "valid-time", + "sites", + "bin-parameter", + "curve-dates", + "filter-model-by", + "filter-model-min", + "filter-model-max", + "filter-obs-by", + "filter-obs-min", + "filter-obs-max", + ], + groupSize: 6, + }); } }; @@ -1985,6 +2292,12 @@ const doPlotGraph = async function () { dataFunction: "dataContourDiff", checked: false, }); + await matsCollections.PlotGraphFunctions.insertAsync({ + plotType: matsTypes.PlotTypes.simpleScatter, + graphFunction: "graphPlotly", + dataFunction: "dataSimpleScatter", + checked: false, + }); } }; diff --git a/apps/ceil-vis/server/main.js b/apps/ceil-vis/server/main.js index 761238f9d1..55ced8962d 100644 --- a/apps/ceil-vis/server/main.js +++ b/apps/ceil-vis/server/main.js @@ -673,7 +673,7 @@ const doCurveParams = async function () { "FAR (False Alarm Ratio)": ["ctc", "x100", 0], - "Bias (forecast/actual)": ["ctc", "Ratio", 1], + "Bias (Forecast / Actual)": ["ctc", "Ratio", 1], "HSS (Heidke Skill Score)": ["ctc", "x100", 100], diff --git a/apps/ceil-vis15/server/main.js b/apps/ceil-vis15/server/main.js index 80999c101b..399345030f 100644 --- a/apps/ceil-vis15/server/main.js +++ b/apps/ceil-vis15/server/main.js @@ -725,7 +725,7 @@ const doCurveParams = async function () { "FAR (False Alarm Ratio)": ["ctc", "x100", 0], - "Bias (forecast/actual)": ["ctc", "Ratio", 1], + "Bias (Forecast / Actual)": ["ctc", "Ratio", 1], "HSS (Heidke Skill Score)": ["ctc", "x100", 100], diff --git a/apps/ensemble/server/main.js b/apps/ensemble/server/main.js index 71d64ee1b1..6ef1cb5a29 100644 --- a/apps/ensemble/server/main.js +++ b/apps/ensemble/server/main.js @@ -632,7 +632,7 @@ const doCurveParams = async function () { (await matsCollections.statistic.findOneAsync({ name: "statistic" })) === undefined ) { const statOptionsMap = { - "Bias (forecast/actual)": [ + "Bias (Forecast / Actual)": [ "avg((m0.nhdfcstcount/m0.mem)/m0.nhdhitcount) as stat, group_concat(m0.time, ';', (m0.nhdfcstcount/m0.mem)/m0.nhdhitcount order by m0.time) as sub_data, count((m0.nhdfcstcount/m0.mem)/m0.nhdhitcount) as n0", "precalculated", "count", @@ -670,7 +670,7 @@ const doCurveParams = async function () { "GridScaleProb", "Reliability", ], - radius: ["Bias (forecast/actual)", "GridScaleProb", "Reliability"], + radius: ["Bias (Forecast / Actual)", "GridScaleProb", "Reliability"], }, superiorNames: ["plot-type"], controlButtonCovered: true, diff --git a/apps/landuse/server/dataFunctions/data_simple_scatter.js b/apps/landuse/server/dataFunctions/data_simple_scatter.js index 333a741c14..d84f542ff5 100644 --- a/apps/landuse/server/dataFunctions/data_simple_scatter.js +++ b/apps/landuse/server/dataFunctions/data_simple_scatter.js @@ -40,9 +40,11 @@ global.dataSimpleScatter = async function (plotParams) { let xmin = Number.MAX_VALUE; let ymin = Number.MAX_VALUE; - let statType; + let statTypeX; + let statTypeY; let varUnitsX; let varUnitsY; + const allStatTypes = []; let statement = ""; let error = ""; @@ -125,9 +127,11 @@ global.dataSimpleScatter = async function (plotParams) { const { statVarUnitMap } = await matsCollections.variable.findOneAsync({ name: "variable", }); - statType = statisticOptionsMap[statisticXSelect]; + statTypeX = statisticOptionsMap[statisticXSelect]; + statTypeY = statisticOptionsMap[statisticYSelect]; varUnitsX = statVarUnitMap[statisticXSelect][variableXStr]; varUnitsY = statVarUnitMap[statisticYSelect][variableYStr]; + allStatTypes.push([statTypeX, statTypeY]); let d; if (!diffFrom) { @@ -166,6 +170,8 @@ global.dataSimpleScatter = async function (plotParams) { global.sumPool, statement, appParams, + statTypeX, + statTypeY, `${statisticXSelect}_${variableXStr}`, `${statisticYSelect}_${variableYStr}` ); @@ -265,7 +271,7 @@ global.dataSimpleScatter = async function (plotParams) { const curveInfoParams = { curves, curvesLength, - statType, + statType: allStatTypes, axisXMap, axisYMap, xmax, diff --git a/apps/precipAccum/server/main.js b/apps/precipAccum/server/main.js index 80a236e515..f7c3a5edc6 100644 --- a/apps/precipAccum/server/main.js +++ b/apps/precipAccum/server/main.js @@ -639,7 +639,7 @@ const doCurveParams = async function () { "FAR (False Alarm Ratio)": ["ctc", "x100", 0], - "Bias (forecast/actual)": ["ctc", "Ratio", 1], + "Bias (Forecast / Actual)": ["ctc", "Ratio", 1], "HSS (Heidke Skill Score)": ["ctc", "x100", 100], diff --git a/apps/precipGauge/server/main.js b/apps/precipGauge/server/main.js index 477e565837..8790e661b8 100644 --- a/apps/precipGauge/server/main.js +++ b/apps/precipGauge/server/main.js @@ -621,7 +621,7 @@ const doCurveParams = async function () { "FAR (False Alarm Ratio)": ["ctc", "x100", 0], - "Bias (forecast/actual)": ["ctc", "Ratio", 1], + "Bias (Forecast / Actual)": ["ctc", "Ratio", 1], "HSS (Heidke Skill Score)": ["ctc", "x100", 100], diff --git a/apps/precipitation1hr/server/main.js b/apps/precipitation1hr/server/main.js index 0407ad72cf..2817eb7bb4 100644 --- a/apps/precipitation1hr/server/main.js +++ b/apps/precipitation1hr/server/main.js @@ -604,7 +604,7 @@ const doCurveParams = async function () { "FAR (False Alarm Ratio)": ["ctc", "x100", 0], - "Bias (forecast/actual)": ["ctc", "Ratio", 1], + "Bias (Forecast / Actual)": ["ctc", "Ratio", 1], "HSS (Heidke Skill Score)": ["ctc", "x100", 100], diff --git a/apps/radar/server/main.js b/apps/radar/server/main.js index 42f5c4ac89..3419acad44 100644 --- a/apps/radar/server/main.js +++ b/apps/radar/server/main.js @@ -631,7 +631,7 @@ const doCurveParams = async function () { "FAR (False Alarm Ratio)": ["ctc", "x100", 0], - "Bias (forecast/actual)": ["ctc", "Ratio", 1], + "Bias (Forecast / Actual)": ["ctc", "Ratio", 1], "HSS (Heidke Skill Score)": ["ctc", "x100", 100], diff --git a/apps/surface/server/dataFunctions/data_simple_scatter.js b/apps/surface/server/dataFunctions/data_simple_scatter.js index 50410d8c93..9dc6f57f88 100644 --- a/apps/surface/server/dataFunctions/data_simple_scatter.js +++ b/apps/surface/server/dataFunctions/data_simple_scatter.js @@ -40,9 +40,11 @@ global.dataSimpleScatter = async function (plotParams) { let xmin = Number.MAX_VALUE; let ymin = Number.MAX_VALUE; - let statType; + let statTypeX; + let statTypeY; let varUnitsX; let varUnitsY; + const allStatTypes = []; let statement = ""; let error = ""; @@ -135,9 +137,11 @@ global.dataSimpleScatter = async function (plotParams) { const { statVarUnitMap } = await matsCollections.variable.findOneAsync({ name: "variable", }); - statType = statisticOptionsMap[variableXStr][statisticXSelect]; + statTypeX = statisticOptionsMap[variableXStr][statisticXSelect]; + statTypeY = statisticOptionsMap[variableYStr][statisticYSelect]; varUnitsX = statVarUnitMap[statisticXSelect][variableXStr]; varUnitsY = statVarUnitMap[statisticYSelect][variableYStr]; + allStatTypes.push([statTypeX, statTypeY]); let d; if (!diffFrom) { @@ -174,6 +178,8 @@ global.dataSimpleScatter = async function (plotParams) { global.sumPool, statement, appParams, + statTypeX, + statTypeY, `${statisticXSelect}_${variableXStr}`, `${statisticYSelect}_${variableYStr}` ); @@ -273,7 +279,7 @@ global.dataSimpleScatter = async function (plotParams) { const curveInfoParams = { curves, curvesLength, - statType, + statType: allStatTypes, axisXMap, axisYMap, xmax, diff --git a/apps/surfrad/server/dataFunctions/data_simple_scatter.js b/apps/surfrad/server/dataFunctions/data_simple_scatter.js index 42e714b95a..f28134883e 100644 --- a/apps/surfrad/server/dataFunctions/data_simple_scatter.js +++ b/apps/surfrad/server/dataFunctions/data_simple_scatter.js @@ -40,9 +40,11 @@ global.dataSimpleScatter = async function (plotParams) { let xmin = Number.MAX_VALUE; let ymin = Number.MAX_VALUE; - let statType; + let statTypeX; + let statTypeY; let varUnitsX; let varUnitsY; + const allStatTypes = []; let statement = ""; let error = ""; @@ -169,9 +171,11 @@ global.dataSimpleScatter = async function (plotParams) { const { statVarUnitMap } = await matsCollections.variable.findOneAsync({ name: "variable", }); - statType = statisticOptionsMap[statisticXSelect]; + statTypeX = statisticOptionsMap[statisticXSelect]; + statTypeY = statisticOptionsMap[statisticYSelect]; varUnitsX = statVarUnitMap[statisticXSelect][variableXStr]; varUnitsY = statVarUnitMap[statisticYSelect][variableYStr]; + allStatTypes.push([statTypeX, statTypeY]); let d; if (!diffFrom) { @@ -210,6 +214,8 @@ global.dataSimpleScatter = async function (plotParams) { global.sumPool, statement, appParams, + statTypeX, + statTypeY, `${statisticXSelect}_${variableXStr}`, `${statisticYSelect}_${variableYStr}` ); @@ -303,7 +309,7 @@ global.dataSimpleScatter = async function (plotParams) { const curveInfoParams = { curves, curvesLength, - statType, + statType: allStatTypes, axisXMap, axisYMap, xmax, diff --git a/apps/upperair/server/dataFunctions/data_simple_scatter.js b/apps/upperair/server/dataFunctions/data_simple_scatter.js index 8f7ec89167..7ea643ffc2 100644 --- a/apps/upperair/server/dataFunctions/data_simple_scatter.js +++ b/apps/upperair/server/dataFunctions/data_simple_scatter.js @@ -40,9 +40,11 @@ global.dataSimpleScatter = async function (plotParams) { let xmin = Number.MAX_VALUE; let ymin = Number.MAX_VALUE; - let statType; + let statTypeX; + let statTypeY; let varUnitsX; let varUnitsY; + const allStatTypes = []; let statement = ""; let error = ""; @@ -190,9 +192,11 @@ global.dataSimpleScatter = async function (plotParams) { const { statVarUnitMap } = await matsCollections.variable.findOneAsync({ name: "variable", }); - statType = statisticOptionsMap[statisticXSelect]; + statTypeX = statisticOptionsMap[statisticXSelect]; + statTypeY = statisticOptionsMap[statisticYSelect]; varUnitsX = statVarUnitMap[statisticXSelect][variableXStr]; varUnitsY = statVarUnitMap[statisticYSelect][variableYStr]; + allStatTypes.push([statTypeX, statTypeY]); let d; if (!diffFrom) { @@ -239,6 +243,8 @@ global.dataSimpleScatter = async function (plotParams) { global.sumPool, statement, appParams, + statTypeX, + statTypeY, `${statisticXSelect}_${variableXStr}`, `${statisticYSelect}_${variableYStr}` ); @@ -338,7 +344,7 @@ global.dataSimpleScatter = async function (plotParams) { const curveInfoParams = { curves, curvesLength, - statType, + statType: allStatTypes, axisXMap, axisYMap, xmax, diff --git a/tests/src/features/cb-metar/basic/addRemoveCurveFiltered.feature b/tests/src/features/cb-metar/basic/addRemoveCurveFiltered.feature new file mode 100644 index 0000000000..be9a1d5ec9 --- /dev/null +++ b/tests/src/features/cb-metar/basic/addRemoveCurveFiltered.feature @@ -0,0 +1,43 @@ +Feature: Add Remove Curve + + As an unauthenticated user to the app, + with the app in its default state, + I want to add one curve + then plot that curve and see the graph, + then go back to the curve management page, + then delete that curve. + + Background: + Given I load the app "/cb-metar" + Then I expect the app title to be "METAR" + + @watch + Scenario: addRemoveCurve + When I set the plot type to "TimeSeries" + Then the plot type should be "TimeSeries" + When I change the "variable" parameter to "Ceiling (ft)" + Then the "variable" parameter value matches "Ceiling (ft)" + When I change the "data-source" parameter to "HRRR_OPS" + Then the "data-source" parameter value matches "HRRR_OPS" + When I change the "filter-model-by" parameter to "Wind Speed at 10m (m/s)" + Then the "filter-model-by" parameter value matches "Wind Speed at 10m (m/s)" + When I change the "filter-obs-by" parameter to "Temperature at 2m (°C)" + Then the "filter-obs-by" parameter value matches "Temperature at 2m (°C)" + When I set the dates to "08/01/2023 00:00 - 08/15/2023 00:00" + Then the dates value is "08/01/2023 00:00 - 08/15/2023 00:00" + Then I click the "Add Curve" button + Then "Curve0" is added + And I should see a list of curves containing "Curve0" + + When I click the "Plot Unmatched" button + Then I should be on the graph page + And I should have a "Time Series" plot + + When I click the "Back" button + Then I should be on the main page + And the "Plot Unmatched" button should be visible + + Then I click the "Remove Curve0" button + And the "Remove curve Curve0" button should be visible + Then I click the "Remove curve Curve0" button + Then I should have 0 curves diff --git a/tests/src/features/cb-metar/basic/addRemoveDailyModelCycleCurveFiltered.feature b/tests/src/features/cb-metar/basic/addRemoveDailyModelCycleCurveFiltered.feature new file mode 100644 index 0000000000..a7db4d0935 --- /dev/null +++ b/tests/src/features/cb-metar/basic/addRemoveDailyModelCycleCurveFiltered.feature @@ -0,0 +1,44 @@ +Feature: Add Remove DailyModelCycle Curve + + As an unauthenticated user to the app, + with the app in its default state, + I want click the dailyModelCycle radio button, + I want to add one curve + then plot that curve and see the graph, + then go back to the curve management page, + then delete that curve. + + Background: + Given I load the app "/cb-metar" + Then I expect the app title to be "METAR" + + @watch + Scenario: addRemoveDailyModelCycleCurve + When I set the plot type to "DailyModelCycle" + Then the plot type should be "DailyModelCycle" + When I change the "variable" parameter to "Ceiling (ft)" + Then the "variable" parameter value matches "Ceiling (ft)" + When I change the "data-source" parameter to "HRRR_OPS" + Then the "data-source" parameter value matches "HRRR_OPS" + When I change the "filter-model-by" parameter to "Wind Speed at 10m (m/s)" + Then the "filter-model-by" parameter value matches "Wind Speed at 10m (m/s)" + When I change the "filter-obs-by" parameter to "Temperature at 2m (°C)" + Then the "filter-obs-by" parameter value matches "Temperature at 2m (°C)" + When I set the dates to "08/01/2023 00:00 - 08/15/2023 00:00" + Then the dates value is "08/01/2023 00:00 - 08/15/2023 00:00" + Then I click the "Add Curve" button + Then "Curve0" is added + And I should see a list of curves containing "Curve0" + + When I click the "Plot Unmatched" button + Then I should be on the graph page + And I should have a "Daily Model Cycle" plot + + When I click the "Back" button + Then I should be on the main page + And the "Plot Unmatched" button should be visible + + Then I click the "Remove Curve0" button + And the "Remove curve Curve0" button should be visible + Then I click the "Remove curve Curve0" button + Then I should have 0 curves diff --git a/tests/src/features/cb-metar/basic/addRemoveDieOffCurveFiltered.feature b/tests/src/features/cb-metar/basic/addRemoveDieOffCurveFiltered.feature new file mode 100644 index 0000000000..b96936d2e8 --- /dev/null +++ b/tests/src/features/cb-metar/basic/addRemoveDieOffCurveFiltered.feature @@ -0,0 +1,45 @@ +Feature: Add Remove Dieoff Curve + + As an unauthenticated user to the app, + with the app in its default state, + I want click the dieoff radio button, + I want to set the forecast-length selector to dieoff + I want to add one curve + then plot that curve and see the graph, + then go back to the curve management page, + then delete that curve. + + Background: + Given I load the app "/cb-metar" + Then I expect the app title to be "METAR" + + @watch + Scenario: addRemoveDieoffCurve + When I set the plot type to "Dieoff" + Then the plot type should be "Dieoff" + When I change the "variable" parameter to "Ceiling (ft)" + Then the "variable" parameter value matches "Ceiling (ft)" + When I change the "data-source" parameter to "HRRR_OPS" + Then the "data-source" parameter value matches "HRRR_OPS" + When I change the "filter-model-by" parameter to "Wind Speed at 10m (m/s)" + Then the "filter-model-by" parameter value matches "Wind Speed at 10m (m/s)" + When I change the "filter-obs-by" parameter to "Temperature at 2m (°C)" + Then the "filter-obs-by" parameter value matches "Temperature at 2m (°C)" + When I set the curve-dates to "08/01/2023 00:00 - 08/15/2023 00:00" + Then the curve-dates value is "08/01/2023 00:00 - 08/15/2023 00:00" + Then I click the "Add Curve" button + Then "Curve0" is added + And I should see a list of curves containing "Curve0" + + When I click the "Plot Unmatched" button + Then I should be on the graph page + And I should have a "Dieoff" plot + + When I click the "Back" button + Then I should be on the main page + And the "Plot Unmatched" button should be visible + + Then I click the "Remove Curve0" button + And the "Remove curve Curve0" button should be visible + Then I click the "Remove curve Curve0" button + Then I should have 0 curves diff --git a/tests/src/features/cb-metar/basic/addRemoveMapFiltered.feature b/tests/src/features/cb-metar/basic/addRemoveMapFiltered.feature new file mode 100644 index 0000000000..3ebf54a244 --- /dev/null +++ b/tests/src/features/cb-metar/basic/addRemoveMapFiltered.feature @@ -0,0 +1,45 @@ +Feature: Add Remove Map + + As an unauthenticated user to the app, + with the app in its default state, + I want to add one curve + then plot that curve and see the graph, + then go back to the curve management page, + then delete that curve. + + Background: + Given I load the app "/cb-metar" + Then I expect the app title to be "METAR" + + @watch + Scenario: addRemoveMap + When I set the plot type to "Map" + Then the plot type should be "Map" + When I change the "variable" parameter to "Ceiling (ft)" + Then the "variable" parameter value matches "Ceiling (ft)" + When I change the "data-source" parameter to "HRRR_OPS" + Then the "data-source" parameter value matches "HRRR_OPS" + When I change the "sites" parameter to "KDEN" + Then the "sites" parameter value matches "KDEN" + When I change the "filter-model-by" parameter to "Wind Speed at 10m (m/s)" + Then the "filter-model-by" parameter value matches "Wind Speed at 10m (m/s)" + When I change the "filter-obs-by" parameter to "Temperature at 2m (°C)" + Then the "filter-obs-by" parameter value matches "Temperature at 2m (°C)" + When I set the dates to "08/01/2023 00:00 - 08/15/2023 00:00" + Then the dates value is "08/01/2023 00:00 - 08/15/2023 00:00" + Then I click the "Add Curve" button + Then "Curve0" is added + And I should see a list of curves containing "Curve0" + + When I click the "Plot Unmatched" button + Then I should be on the graph page + And I should have a "Map" plot + + When I click the "Back" button + Then I should be on the main page + And the "Plot Unmatched" button should be visible + + Then I click the "Remove Curve0" button + And the "Remove curve Curve0" button should be visible + Then I click the "Remove curve Curve0" button + Then I should have 0 curves diff --git a/tests/src/features/cb-metar/basic/addRemoveSimpleScatterCTC.feature b/tests/src/features/cb-metar/basic/addRemoveSimpleScatterCTC.feature new file mode 100644 index 0000000000..4f2f0e071f --- /dev/null +++ b/tests/src/features/cb-metar/basic/addRemoveSimpleScatterCTC.feature @@ -0,0 +1,47 @@ +Feature: Add Remove Curve + + As an unauthenticated user to the app, + with the app in its default state, + I want to add one curve + then plot that curve and see the graph, + then go back to the curve management page, + then delete that curve. + + Background: + Given I load the app "/cb-metar" + Then I expect the app title to be "METAR" + + @watch + Scenario: addRemoveCurve + When I set the plot type to "SimpleScatter" + Then the plot type should be "SimpleScatter" + When I change the "x-variable" parameter to "Ceiling (ft)" + Then the "x-variable" parameter value matches "Ceiling (ft)" + When I change the "y-variable" parameter to "Ceiling (ft)" + Then the "y-variable" parameter value matches "Ceiling (ft)" + When I change the "data-source" parameter to "HRRR_OPS" + Then the "data-source" parameter value matches "HRRR_OPS" + When I change the "x-statistic" parameter to "CSI (Critical Success Index)" + Then the "x-statistic" parameter value matches "CSI (Critical Success Index)" + When I change the "y-statistic" parameter to "Model average" + Then the "y-statistic" parameter value matches "Model average" + When I change the "x-threshold" parameter to "3000 (ceiling <3000 ft)" + Then the "x-threshold" parameter value matches "3000 (ceiling <3000 ft)" + When I set the curve-dates to "08/01/2023 00:00 - 08/15/2023 00:00" + Then the curve-dates value is "08/01/2023 00:00 - 08/15/2023 00:00" + Then I click the "Add Curve" button + Then "Curve0" is added + And I should see a list of curves containing "Curve0" + + When I click the "Plot Unmatched" button + Then I should be on the graph page + And I should have a "Simple Scatter" plot + + When I click the "Back" button + Then I should be on the main page + And the "Plot Unmatched" button should be visible + + Then I click the "Remove Curve0" button + And the "Remove curve Curve0" button should be visible + Then I click the "Remove curve Curve0" button + Then I should have 0 curves diff --git a/tests/src/features/cb-metar/basic/addRemoveSimpleScatterCTCStations.feature b/tests/src/features/cb-metar/basic/addRemoveSimpleScatterCTCStations.feature new file mode 100644 index 0000000000..f26cc87620 --- /dev/null +++ b/tests/src/features/cb-metar/basic/addRemoveSimpleScatterCTCStations.feature @@ -0,0 +1,51 @@ +Feature: Add Remove Curve + + As an unauthenticated user to the app, + with the app in its default state, + I want to add one curve + then plot that curve and see the graph, + then go back to the curve management page, + then delete that curve. + + Background: + Given I load the app "/cb-metar" + Then I expect the app title to be "METAR" + + @watch + Scenario: addRemoveCurve + When I set the plot type to "SimpleScatter" + Then the plot type should be "SimpleScatter" + When I change the "x-variable" parameter to "Ceiling (ft)" + Then the "x-variable" parameter value matches "Ceiling (ft)" + When I change the "y-variable" parameter to "Ceiling (ft)" + Then the "y-variable" parameter value matches "Ceiling (ft)" + When I change the "data-source" parameter to "HRRR_OPS" + Then the "data-source" parameter value matches "HRRR_OPS" + When I change the "x-statistic" parameter to "CSI (Critical Success Index)" + Then the "x-statistic" parameter value matches "CSI (Critical Success Index)" + When I change the "y-statistic" parameter to "Model average" + Then the "y-statistic" parameter value matches "Model average" + When I change the "x-threshold" parameter to "60000 (ceiling <60000 ft -- any cloud)" + Then the "x-threshold" parameter value matches "60000 (ceiling <60000 ft -- any cloud)" + When I change the "region-type" parameter to "Select stations" + Then the "region-type" parameter value matches "Select stations" + When I change the "sites" parameter to "KDEN" + Then the "sites" parameter value matches "KDEN" + When I set the curve-dates to "10/12/2024 00:00 - 10/12/2024 23:59" + Then the curve-dates value is "10/12/2024 00:00 - 10/12/2024 23:59" + Then I click the "Add Curve" button + Then "Curve0" is added + And I should see a list of curves containing "Curve0" + + When I click the "Plot Unmatched" button + Then I should be on the graph page + And I should have a "Simple Scatter" plot + + When I click the "Back" button + Then I should be on the main page + And the "Plot Unmatched" button should be visible + + Then I click the "Remove Curve0" button + And the "Remove curve Curve0" button should be visible + Then I click the "Remove curve Curve0" button + Then I should have 0 curves diff --git a/tests/src/features/cb-metar/basic/addRemoveSimpleScatterFiltered.feature b/tests/src/features/cb-metar/basic/addRemoveSimpleScatterFiltered.feature new file mode 100644 index 0000000000..9aaae87f79 --- /dev/null +++ b/tests/src/features/cb-metar/basic/addRemoveSimpleScatterFiltered.feature @@ -0,0 +1,51 @@ +Feature: Add Remove Curve + + As an unauthenticated user to the app, + with the app in its default state, + I want to add one curve + then plot that curve and see the graph, + then go back to the curve management page, + then delete that curve. + + Background: + Given I load the app "/cb-metar" + Then I expect the app title to be "METAR" + + @watch + Scenario: addRemoveCurve + When I set the plot type to "SimpleScatter" + Then the plot type should be "SimpleScatter" + When I change the "x-variable" parameter to "Ceiling (ft)" + Then the "x-variable" parameter value matches "Ceiling (ft)" + When I change the "y-variable" parameter to "Ceiling (ft)" + Then the "y-variable" parameter value matches "Ceiling (ft)" + When I change the "data-source" parameter to "HRRR_OPS" + Then the "data-source" parameter value matches "HRRR_OPS" + When I change the "x-statistic" parameter to "CSI (Critical Success Index)" + Then the "x-statistic" parameter value matches "CSI (Critical Success Index)" + When I change the "y-statistic" parameter to "Model average" + Then the "y-statistic" parameter value matches "Model average" + When I change the "x-threshold" parameter to "3000 (ceiling <3000 ft)" + Then the "x-threshold" parameter value matches "3000 (ceiling <3000 ft)" + When I change the "filter-model-by" parameter to "Wind Speed at 10m (m/s)" + Then the "filter-model-by" parameter value matches "Wind Speed at 10m (m/s)" + When I change the "filter-obs-by" parameter to "Temperature at 2m (°C)" + Then the "filter-obs-by" parameter value matches "Temperature at 2m (°C)" + When I set the curve-dates to "08/01/2023 00:00 - 08/15/2023 00:00" + Then the curve-dates value is "08/01/2023 00:00 - 08/15/2023 00:00" + Then I click the "Add Curve" button + Then "Curve0" is added + And I should see a list of curves containing "Curve0" + + When I click the "Plot Unmatched" button + Then I should be on the graph page + And I should have a "Simple Scatter" plot + + When I click the "Back" button + Then I should be on the main page + And the "Plot Unmatched" button should be visible + + Then I click the "Remove Curve0" button + And the "Remove curve Curve0" button should be visible + Then I click the "Remove curve Curve0" button + Then I should have 0 curves diff --git a/tests/src/features/cb-metar/basic/addRemoveSimpleScatterSums.feature b/tests/src/features/cb-metar/basic/addRemoveSimpleScatterSums.feature new file mode 100644 index 0000000000..b41db54c7f --- /dev/null +++ b/tests/src/features/cb-metar/basic/addRemoveSimpleScatterSums.feature @@ -0,0 +1,45 @@ +Feature: Add Remove Curve + + As an unauthenticated user to the app, + with the app in its default state, + I want to add one curve + then plot that curve and see the graph, + then go back to the curve management page, + then delete that curve. + + Background: + Given I load the app "/cb-metar" + Then I expect the app title to be "METAR" + + @watch + Scenario: addRemoveCurve + When I set the plot type to "SimpleScatter" + Then the plot type should be "SimpleScatter" + When I change the "x-variable" parameter to "Temperature at 2m (°C)" + Then the "x-variable" parameter value matches "Temperature at 2m (°C)" + When I change the "y-variable" parameter to "Temperature at 2m (°C)" + Then the "y-variable" parameter value matches "Temperature at 2m (°C)" + When I change the "data-source" parameter to "HRRR_OPS" + Then the "data-source" parameter value matches "HRRR_OPS" + When I change the "x-statistic" parameter to "Model average" + Then the "x-statistic" parameter value matches "Model average" + When I change the "y-statistic" parameter to "Obs average" + Then the "y-statistic" parameter value matches "Obs average" + When I set the curve-dates to "08/01/2023 00:00 - 08/15/2023 00:00" + Then the curve-dates value is "08/01/2023 00:00 - 08/15/2023 00:00" + Then I click the "Add Curve" button + Then "Curve0" is added + And I should see a list of curves containing "Curve0" + + When I click the "Plot Unmatched" button + Then I should be on the graph page + And I should have a "Simple Scatter" plot + + When I click the "Back" button + Then I should be on the main page + And the "Plot Unmatched" button should be visible + + Then I click the "Remove Curve0" button + And the "Remove curve Curve0" button should be visible + Then I click the "Remove curve Curve0" button + Then I should have 0 curves diff --git a/tests/src/features/cb-metar/basic/addRemoveSimpleScatterSumsStations.feature b/tests/src/features/cb-metar/basic/addRemoveSimpleScatterSumsStations.feature new file mode 100644 index 0000000000..01cbab7945 --- /dev/null +++ b/tests/src/features/cb-metar/basic/addRemoveSimpleScatterSumsStations.feature @@ -0,0 +1,49 @@ +Feature: Add Remove Curve + + As an unauthenticated user to the app, + with the app in its default state, + I want to add one curve + then plot that curve and see the graph, + then go back to the curve management page, + then delete that curve. + + Background: + Given I load the app "/cb-metar" + Then I expect the app title to be "METAR" + + @watch + Scenario: addRemoveCurve + When I set the plot type to "SimpleScatter" + Then the plot type should be "SimpleScatter" + When I change the "x-variable" parameter to "Temperature at 2m (°C)" + Then the "x-variable" parameter value matches "Temperature at 2m (°C)" + When I change the "y-variable" parameter to "Temperature at 2m (°C)" + Then the "y-variable" parameter value matches "Temperature at 2m (°C)" + When I change the "data-source" parameter to "HRRR_OPS" + Then the "data-source" parameter value matches "HRRR_OPS" + When I change the "x-statistic" parameter to "Model average" + Then the "x-statistic" parameter value matches "Model average" + When I change the "y-statistic" parameter to "Obs average" + Then the "y-statistic" parameter value matches "Obs average" + When I change the "region-type" parameter to "Select stations" + Then the "region-type" parameter value matches "Select stations" + When I change the "sites" parameter to "KDEN" + Then the "sites" parameter value matches "KDEN" + When I set the curve-dates to "10/12/2024 00:00 - 10/12/2024 23:59" + Then the curve-dates value is "10/12/2024 00:00 - 10/12/2024 23:59" + Then I click the "Add Curve" button + Then "Curve0" is added + And I should see a list of curves containing "Curve0" + + When I click the "Plot Unmatched" button + Then I should be on the graph page + And I should have a "Simple Scatter" plot + + When I click the "Back" button + Then I should be on the main page + And the "Plot Unmatched" button should be visible + + Then I click the "Remove Curve0" button + And the "Remove curve Curve0" button should be visible + Then I click the "Remove curve Curve0" button + Then I should have 0 curves diff --git a/tests/src/features/cb-metar/basic/addRemoveValidTimeCurveFiltered.feature b/tests/src/features/cb-metar/basic/addRemoveValidTimeCurveFiltered.feature new file mode 100644 index 0000000000..cf1839f2d8 --- /dev/null +++ b/tests/src/features/cb-metar/basic/addRemoveValidTimeCurveFiltered.feature @@ -0,0 +1,44 @@ +Feature: Add Remove Valid Time Curve + + As an unauthenticated user to the app, + with the app in its default state, + I want click the validtime radio button, + I want to add one curve + then plot that curve and see the graph, + then go back to the curve management page, + then delete that curve. + + Background: + Given I load the app "/cb-metar" + Then I expect the app title to be "METAR" + + @watch + Scenario: addRemoveValidTimeCurve + When I set the plot type to "ValidTime" + Then the plot type should be "ValidTime" + When I change the "variable" parameter to "Ceiling (ft)" + Then the "variable" parameter value matches "Ceiling (ft)" + When I change the "data-source" parameter to "HRRR_OPS" + Then the "data-source" parameter value matches "HRRR_OPS" + When I change the "filter-model-by" parameter to "Wind Speed at 10m (m/s)" + Then the "filter-model-by" parameter value matches "Wind Speed at 10m (m/s)" + When I change the "filter-obs-by" parameter to "Temperature at 2m (°C)" + Then the "filter-obs-by" parameter value matches "Temperature at 2m (°C)" + When I set the curve-dates to "08/01/2023 00:00 - 08/15/2023 00:00" + Then the curve-dates value is "08/01/2023 00:00 - 08/15/2023 00:00" + Then I click the "Add Curve" button + Then "Curve0" is added + And I should see a list of curves containing "Curve0" + + When I click the "Plot Unmatched" button + Then I should be on the graph page + And I should have a "Valid Time" plot + + When I click the "Back" button + Then I should be on the main page + And the "Plot Unmatched" button should be visible + + Then I click the "Remove Curve0" button + And the "Remove curve Curve0" button should be visible + Then I click the "Remove curve Curve0" button + Then I should have 0 curves diff --git a/tests/src/features/cb-metar/basic/matchUnmatchSimpleScatterPR.feature b/tests/src/features/cb-metar/basic/matchUnmatchSimpleScatterPR.feature new file mode 100644 index 0000000000..7a79cf3dcd --- /dev/null +++ b/tests/src/features/cb-metar/basic/matchUnmatchSimpleScatterPR.feature @@ -0,0 +1,61 @@ +Feature: Match Unmatch Diff Curves SimpleScatter + + As an unauthenticated user to the app, + with the app in its default state so that the plots are time series, + I want to add two curves, plot unmatched, and then return to the main page. + I then want to add a matched difference curve, plot unmatched, return to the main page, plot matched, and then return to the main page. + I then want to add a piecewise difference curve, plot unmatched, return to the main page, plot matched, and then return to the main page. + I want to end by removing all of the curves. + + Background: + Given I load the app "/cb-metar" + Then I expect the app title to be "METAR" + + @watch + Scenario: matchUnmatchDiffCurvesSimpleScatter + When I set the plot type to "SimpleScatter" + Then the plot type should be "SimpleScatter" + When I change the "x-variable" parameter to "Temperature at 2m (°C)" + Then the "x-variable" parameter value matches "Temperature at 2m (°C)" + When I change the "y-variable" parameter to "Temperature at 2m (°C)" + Then the "y-variable" parameter value matches "Temperature at 2m (°C)" + When I change the "data-source" parameter to "HRRR_OPS" + Then the "data-source" parameter value matches "HRRR_OPS" + When I change the "x-statistic" parameter to "Model average" + Then the "x-statistic" parameter value matches "Model average" + When I change the "y-statistic" parameter to "Obs average" + Then the "y-statistic" parameter value matches "Obs average" + When I set the curve-dates to "08/01/2023 00:00 - 08/15/2023 00:00" + Then the curve-dates value is "08/01/2023 00:00 - 08/15/2023 00:00" + Then I click the "Add Curve" button + Then "Curve0" is added + And I should see a list of curves containing "Curve0" + + When I change the "x-variable" parameter to "Dewpoint at 2m (°C)" + Then the "x-variable" parameter value matches "Dewpoint at 2m (°C)" + When I change the "y-variable" parameter to "Dewpoint at 2m (°C)" + Then the "y-variable" parameter value matches "Dewpoint at 2m (°C)" + When I click the "Add Curve" button + Then "Curve1" is added + And I should see a list of curves containing "Curve0,Curve1" + + When I click the "Plot Unmatched" button + Then I should be on the graph page + And I should have a "Simple Scatter" plot + + When I click the "Back" button + Then I should be on the main page + And the "Plot Unmatched" button should be visible + + When I click the "Plot Matched" button + Then I should be on the graph page + And I should have a "Simple Scatter" plot + + When I click the "Back" button + Then I should be on the main page + And the "Plot Matched" button should be visible + + When I click the "Remove All" button + And the "Remove all the curves" button should be visible + Then I click the "Remove all the curves" button + Then I should have 0 curves diff --git a/tests/src/features/cb-metar/basic/matchUnmatchSimpleScatterSites.feature b/tests/src/features/cb-metar/basic/matchUnmatchSimpleScatterSites.feature new file mode 100644 index 0000000000..c89d6f6755 --- /dev/null +++ b/tests/src/features/cb-metar/basic/matchUnmatchSimpleScatterSites.feature @@ -0,0 +1,65 @@ +Feature: Match Unmatch Diff Curves SimpleScatter + + As an unauthenticated user to the app, + with the app in its default state so that the plots are time series, + I want to add two curves, plot unmatched, and then return to the main page. + I then want to add a matched difference curve, plot unmatched, return to the main page, plot matched, and then return to the main page. + I then want to add a piecewise difference curve, plot unmatched, return to the main page, plot matched, and then return to the main page. + I want to end by removing all of the curves. + + Background: + Given I load the app "/cb-metar" + Then I expect the app title to be "METAR" + + @watch + Scenario: matchUnmatchDiffCurvesSimpleScatter + When I set the plot type to "SimpleScatter" + Then the plot type should be "SimpleScatter" + When I change the "x-variable" parameter to "Temperature at 2m (°C)" + Then the "x-variable" parameter value matches "Temperature at 2m (°C)" + When I change the "y-variable" parameter to "Temperature at 2m (°C)" + Then the "y-variable" parameter value matches "Temperature at 2m (°C)" + When I change the "data-source" parameter to "HRRR_OPS" + Then the "data-source" parameter value matches "HRRR_OPS" + When I change the "x-statistic" parameter to "Model average" + Then the "x-statistic" parameter value matches "Model average" + When I change the "y-statistic" parameter to "Obs average" + Then the "y-statistic" parameter value matches "Obs average" + When I change the "region-type" parameter to "Select stations" + Then the "region-type" parameter value matches "Select stations" + When I change the "sites" parameter to "KDEN" + Then the "sites" parameter value matches "KDEN" + When I set the curve-dates to "08/12/2023 00:00 - 08/15/2023 00:00" + Then the curve-dates value is "08/12/2023 00:00 - 08/15/2023 00:00" + Then I click the "Add Curve" button + Then "Curve0" is added + And I should see a list of curves containing "Curve0" + + When I change the "x-variable" parameter to "Dewpoint at 2m (°C)" + Then the "x-variable" parameter value matches "Dewpoint at 2m (°C)" + When I change the "y-variable" parameter to "Dewpoint at 2m (°C)" + Then the "y-variable" parameter value matches "Dewpoint at 2m (°C)" + When I click the "Add Curve" button + Then "Curve1" is added + And I should see a list of curves containing "Curve0,Curve1" + + When I click the "Plot Unmatched" button + Then I should be on the graph page + And I should have a "Simple Scatter" plot + + When I click the "Back" button + Then I should be on the main page + And the "Plot Unmatched" button should be visible + + When I click the "Plot Matched" button + Then I should be on the graph page + And I should have a "Simple Scatter" plot + + When I click the "Back" button + Then I should be on the main page + And the "Plot Matched" button should be visible + + When I click the "Remove All" button + And the "Remove all the curves" button should be visible + Then I click the "Remove all the curves" button + Then I should have 0 curves