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