From 07e768f2376e30a4b65fb4b4cbec61a6ad9fb5c0 Mon Sep 17 00:00:00 2001 From: Weronika Ciesielska Date: Fri, 31 Jan 2025 12:38:04 +0100 Subject: [PATCH 1/2] feat: calculate and return data for histogram --- src/constants/index.js | 1 + src/controllers/histogram.js | 33 ++++++++++ src/controllers/index.js | 3 +- src/routers/histogram.js | 39 ++++++++++++ src/routers/index.js | 2 + src/utils/histogram-service.js | 108 +++++++++++++++++++++++++++++++++ 6 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/controllers/histogram.js create mode 100644 src/routers/histogram.js create mode 100644 src/utils/histogram-service.js diff --git a/src/constants/index.js b/src/constants/index.js index 6e91dda..4c06070 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -8,6 +8,7 @@ const COLLECTION_NAMES = { unsummarized: 'unsummarizedtrappings', users: 'users', blogPost: 'blogs', + histogram: 'histogram', }; /** diff --git a/src/controllers/histogram.js b/src/controllers/histogram.js new file mode 100644 index 0000000..4b12640 --- /dev/null +++ b/src/controllers/histogram.js @@ -0,0 +1,33 @@ +import { COLLECTION_NAMES, RESPONSE_CODES } from '../constants'; +import { queryFetch } from '../utils'; +import { saveHistogramData, computeHistogramData } from '../utils/histogram-service'; + +/** + * @description retrieves histogram data object + * @returns {Promise} promise that resolves to histogram data object or error + */ +export const getHistogramData = async () => { + try { + const histogramData = await queryFetch(COLLECTION_NAMES.histogram); + return { ...RESPONSE_CODES.SUCCESS, data: histogramData[0] }; + } catch (error) { + console.error(error); + return error; + } +}; + +/** + * @description recalculates and saves histogram data object to the database + * @returns {Promise} promise that resolves to histogram data object or error + */ +export const updateHistogramData = async () => { + try { + const histogramData = await computeHistogramData(); + + const savedHistogramData = await saveHistogramData(histogramData); + return { ...RESPONSE_CODES.SUCCESS, data: savedHistogramData }; + } catch (error) { + console.error(error); + return error; + } +}; diff --git a/src/controllers/index.js b/src/controllers/index.js index 153789a..5c23ad9 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -1,5 +1,6 @@ /* eslint-disable import/prefer-default-export */ import * as User from './user'; import * as Blog from './blog'; +import * as Histogram from './histogram'; -export { User, Blog }; +export { User, Blog, Histogram }; diff --git a/src/routers/histogram.js b/src/routers/histogram.js new file mode 100644 index 0000000..27b91ac --- /dev/null +++ b/src/routers/histogram.js @@ -0,0 +1,39 @@ +import { Router } from 'express'; +import { + generateResponse, + RESPONSE_TYPES, + RESPONSE_CODES, +} from '../constants'; +import { Histogram } from '../controllers'; +import { updateHistogramData } from '../controllers/histogram'; +import { requireAuth } from '../middleware'; + +const histogramRouter = Router(); + +// returns histogram data +histogramRouter.route('/').get(async (_req, res) => { + try { + const data = await Histogram.getHistogramData(); + + res.send(generateResponse(RESPONSE_TYPES.SUCCESS, data.data)); + } catch (error) { + res + .status(RESPONSE_CODES.INTERNAL_ERROR.status) + .send(generateResponse(RESPONSE_TYPES.INTERNAL_ERROR, error)); + } +}); + +// calculates histogram data +histogramRouter.route('/update').post([requireAuth], async (req, res) => { + try { + const data = await updateHistogramData(); + + res.send(generateResponse(RESPONSE_TYPES.SUCCESS, data)); + } catch (error) { + res + .status(RESPONSE_CODES.INTERNAL_ERROR.status) + .send(generateResponse(RESPONSE_TYPES.INTERNAL_ERROR, error)); + } +}); + +export default histogramRouter; diff --git a/src/routers/index.js b/src/routers/index.js index ecf57f3..f77ab20 100644 --- a/src/routers/index.js +++ b/src/routers/index.js @@ -4,6 +4,7 @@ import summarizedRangerDistrict from './summarized-ranger-district'; import unsummarized from './unsummarized'; import user from './user'; import blog from './blog'; +import histogram from './histogram'; export default { healthcheck, @@ -12,4 +13,5 @@ export default { 'unsummarized-trapping': unsummarized, user, blog, + histogram, }; diff --git a/src/utils/histogram-service.js b/src/utils/histogram-service.js new file mode 100644 index 0000000..b2129a8 --- /dev/null +++ b/src/utils/histogram-service.js @@ -0,0 +1,108 @@ +import { COLLECTION_NAMES, RESPONSE_CODES } from '../constants'; +import { specifiedQueryFetch } from './query-fetch'; + +// x axis of the histogram - predicted probability of an outbreak +export const probRanges = [ + { min: 0, max: 0.025 }, + { min: 0.025, max: 0.05 }, + { min: 0.05, max: 0.15 }, + { min: 0.15, max: 0.25 }, + { min: 0.25, max: 0.4 }, + { min: 0.4, max: 0.6 }, + { min: 0.6, max: 0.8 }, + { min: 0.8, max: 1 }, +]; + +// y axis of the histogram - actual number of spots observed +const spotsRanges = [ + { min: 0, max: 0 }, + { min: 1, max: 9 }, + { min: 10, max: 19 }, + { min: 20, max: 49 }, + { min: 50, max: 99 }, + { min: 100, max: 249 }, + { min: 250, max: Infinity }, +]; + +const categorizeRecords = (records) => { + const frequencyArray = probRanges.map(({ min, max }) => { + const rangeLabel = `${min === 0 ? '0' : min}-${max}`; + const rangeRecords = records.filter((record) => { + return record.probSpotsGT50 > min && record.probSpotsGT50 <= max; + }); + + // eslint-disable-next-line no-shadow + const data = spotsRanges.map(({ min, max }) => { + return rangeRecords.filter((record) => { + return record.spotst0 >= min && record.spotst0 <= max; + }).length; + }); + + return { + range: rangeLabel, + frequency: rangeRecords.length, + withBorder: false, + data, + }; + }); + + return frequencyArray; +}; +function validateRecords(records) { + return records.filter((record) => { + return ( + record.probSpotsGT50 !== null + && record.probSpotsGT50 !== undefined + && record.spotst0 !== null + && record.spotst0 !== undefined + ); + }); +} + +export const computeHistogramData = async () => { + try { + const counties = await specifiedQueryFetch( + COLLECTION_NAMES.summarizedCounty, + { + hasPredictionAndOutcome: 1, + }, + ); + + const rangerDistricts = await specifiedQueryFetch( + COLLECTION_NAMES.summarizedRangerDistrict, + { + hasPredictionAndOutcome: 1, + }, + ); + + const data = [...counties, ...rangerDistricts]; + const validRecords = validateRecords(data); + const frequency = validRecords.length; + const frequencyArray = categorizeRecords(validRecords); + + return { ...RESPONSE_CODES.SUCCESS, frequency, frequencyArray }; + } catch (e) { + console.error(e); + return e; + } +}; + +export const saveHistogramData = async (histogramData) => { + const cursor = global.connection.collection('histogram'); + + const { frequency, frequencyArray } = histogramData; + + const savedData = await cursor.updateOne( + { _id: 'chartData' }, + { + $set: { + timestamp: new Date(), + frequencyArray, + frequency, + }, + }, + { upsert: true }, + ); + + return savedData; +}; From 0e75973574e206f036059595bc3a6c4c05a4b663 Mon Sep 17 00:00:00 2001 From: Weronika Ciesielska Date: Mon, 3 Feb 2025 13:58:30 +0100 Subject: [PATCH 2/2] docs: update documentation for histogram endpoints --- docs/HISTOGRAM.md | 10 ++++++++++ docs/ROUTES.md | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 docs/HISTOGRAM.md diff --git a/docs/HISTOGRAM.md b/docs/HISTOGRAM.md new file mode 100644 index 0000000..6ebc16d --- /dev/null +++ b/docs/HISTOGRAM.md @@ -0,0 +1,10 @@ +# Histogram Operations + +## `GET /histogram` + +Returns the histogram data stored in the database. + +## `POST /histogram/update` + +Expects authorization header with Bearer token. +If no histogram data is stored in the database, creates one. If there is previous data, updates the existing data with the results of latest calculations. diff --git a/docs/ROUTES.md b/docs/ROUTES.md index 54c27bf..f1cc56a 100644 --- a/docs/ROUTES.md +++ b/docs/ROUTES.md @@ -1,8 +1,9 @@ # Route Documentation +- [Blog](./BLOG.md) - [Healthcheck](./HEALTHCHECK.md) +- [Histogram](./HISTOGRAM.md) - [Summarized County Data](./SUMMARIZED-COUNTY.md) - [Summarized Ranger District Data](./SUMMARIZED-RD.md) - [Unsummarized Trapping Data](./UNSUMMARIZED.md) - [Users](./USERS.md) -- [Blog](./BLOG.md)