From 7d4bd53df91094cce02f1c03fea1d718748792de Mon Sep 17 00:00:00 2001 From: Lee Gordon <40238160+LeeGordon83@users.noreply.github.com> Date: Wed, 16 Aug 2023 12:37:03 +0100 Subject: [PATCH] FSR-705 Accessibility Changes (#448) * update to release notes * Accessibility Fixes https://eaflood.atlassian.net/browse/FSR-705 https://eaflood.atlassian.net/browse/FSR-701 https://eaflood.atlassian.net/browse/FSR-886 * altered telemetry model to match prototype data set, change relevant front end to display new station chart, uunit tests also fixed * altered line thickness on historical events chart * threshold label error fixed * removed forecast html that isn't needed yet from station page * added forecast banner. Amended all other banners to load banner svg as part of model * code smells removed * further code smell cleared * removal of duplicate core.js script call in station.html * bug relating to font on station page sorted * fixed broken map loading from station link issue, also implemented suggested code changes on PR * fixed graph zoom issue * threshold-label * fix threshold display issue * add explicit unsetting of color in css for svgs in high contrast mode also add missing inline svg for chevron in secondary start button * remove transparent outline on search button * add thin top, right and bottom border to status items in high contrast mode * Fix SVG in nav and tables on high contrast * Add explicit support for high contrast mode on line charts * Add client side test for bar-chart controls, refactor bar-chart code to separate dir * Add pagination tests * extract update control methods into separate files * extract control creation from main bar-chart method * Update resolution display text on bar-chart * move pagination controls on bar-chart into controls row and update display text * update bar-chart resolution controls to use buttons rather than inputs * rename segmented controls to resolution controls * update class name for pagination buttons on bar chart * update resolution and pagination ontrols to be more generic * Update bar chart css for high contrast mode * update client side tests to be less flakey * update high contrast mode media queries to use mixin * remove upstream/downstream icons * subnav markup and contrast fix. Amended unit tests to reflect * fix issues with bells and warning triangles on hcm * fix outlook map button * remove background on chart buttons when focussed in hcm mode * remove full stop from sign-up-for-flood-warnings * fix custom class support on map buttons --------- Co-authored-by: nikiwycherley Co-authored-by: Max Bladen-Clark --- package-lock.json | 1134 +++++++- package.json | 7 +- server/models/severity.js | 12 +- server/models/views/location.js | 4 + server/models/views/station.js | 125 +- server/models/views/target-area.js | 1 + .../{bar-chart.js => bar-chart/index.mjs} | 160 +- .../bar-chart/pagination-controls.mjs | 83 + .../bar-chart/resolution-controls.mjs | 48 + server/src/js/components/charts.js | 473 ---- server/src/js/components/line-chart.js | 734 +++++ server/src/js/components/map/button.js | 21 + server/src/js/components/map/live.js | 14 +- server/src/js/components/map/outlook.js | 14 +- .../src/js/components/toggle-list-display.js | 2 +- server/src/js/{core.js => core.mjs} | 73 +- server/src/js/pages/rainfall.js | 4 +- server/src/js/pages/station.js | 77 +- server/src/sass/components/_bar-chart.scss | 38 +- .../src/sass/components/_chart-controls.scss | 128 +- .../sass/components/_flood-impact-list.scss | 132 +- .../sass/components/_flood-levels-table.scss | 3 + server/src/sass/components/_flood-nav.scss | 22 - server/src/sass/components/_flood-status.scss | 176 +- server/src/sass/components/_line-chart.scss | 268 +- server/src/sass/components/_navbar.scss | 62 +- server/src/sass/components/_search.scss | 1 - server/src/sass/objects/_buttons.scss | 40 +- server/src/sass/tools/_mixins.scss | 9 + server/views/location.html | 4 +- server/views/national.html | 5 +- .../partials/sign-up-for-flood-warnings.html | 5 + server/views/rainfall-station.html | 20 +- server/views/river-and-sea-levels.html | 217 +- server/views/station.html | 110 +- server/views/target-area.html | 12 +- test/data/telemetry.json | 2425 +++++++++++++++++ test/dom.js | 30 + test/routes/station.js | 44 +- test/src/js/components/bar-chart.js | 142 + webpack.config.js | 2 +- 41 files changed, 5578 insertions(+), 1303 deletions(-) rename server/src/js/components/{bar-chart.js => bar-chart/index.mjs} (74%) create mode 100644 server/src/js/components/bar-chart/pagination-controls.mjs create mode 100644 server/src/js/components/bar-chart/resolution-controls.mjs delete mode 100644 server/src/js/components/charts.js create mode 100644 server/src/js/components/line-chart.js create mode 100644 server/src/js/components/map/button.js rename server/src/js/{core.js => core.mjs} (79%) create mode 100644 server/views/partials/sign-up-for-flood-warnings.html create mode 100644 test/data/telemetry.json create mode 100644 test/dom.js create mode 100644 test/src/js/components/bar-chart.js diff --git a/package-lock.json b/package-lock.json index d0cc07143..5d0eac41b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,8 +59,12 @@ "webpack-cli": "^4.9.1" }, "devDependencies": { + "dotenv": "^16.3.1", "husky": "^8.0.3", - "node-html-parser": "^6.1.1" + "jsdom": "^22.1.0", + "mockdate": "^3.0.5", + "node-html-parser": "^6.1.1", + "nodemon": "^3.0.1" }, "engines": { "node": ">=18" @@ -4699,6 +4703,12 @@ "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==" }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -4821,7 +4831,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "optional": true, + "devOptional": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -4943,6 +4953,12 @@ "node": "*" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -5039,7 +5055,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "optional": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -5260,13 +5276,13 @@ "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "devOptional": true, "funding": [ { "type": "individual", "url": "https://paulmillr.com/funding/" } ], - "optional": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -5367,6 +5383,18 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -5500,6 +5528,18 @@ "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==" }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/d3": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.1.tgz", @@ -5896,6 +5936,20 @@ "node": ">=12" } }, + "node_modules/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/datatables.net": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.13.1.tgz", @@ -5979,6 +6033,12 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "node_modules/deep-equal": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", @@ -6001,9 +6061,9 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", "dependencies": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -6028,6 +6088,15 @@ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz", "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -6110,6 +6179,18 @@ } ] }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", @@ -6139,6 +6220,18 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/duplexify": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", @@ -7277,6 +7370,20 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -7298,6 +7405,20 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -7436,12 +7557,13 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" }, "funding": { @@ -7800,6 +7922,18 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -7891,6 +8025,12 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -8034,12 +8174,12 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz", - "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "is-typed-array": "^1.1.10" }, "funding": { @@ -8066,7 +8206,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "optional": true, + "devOptional": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -8246,6 +8386,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -8399,6 +8545,71 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/jsdom/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -8944,6 +9155,12 @@ "node": ">=12" } }, + "node_modules/mockdate": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz", + "integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==", + "dev": true + }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -9479,6 +9696,97 @@ "node": ">=6" } }, + "node_modules/nodemon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -9541,7 +9849,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "optional": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -9590,6 +9898,12 @@ "node": ">= 6" } }, + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9843,6 +10157,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10114,6 +10440,18 @@ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -10134,13 +10472,19 @@ } }, "node_modules/punycode": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.2.0.tgz", - "integrity": "sha512-LN6QV1IJ9ZhxWTNdktaPClrNfp8xdSAYS0Zk2ddX7XsXZAxckMHPCBcHRo0cTcEIgYPRiGEkmji3Idkh2yFtYw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "engines": { "node": ">=6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10281,7 +10625,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "optional": true, + "devOptional": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -10366,13 +10710,13 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -10448,6 +10792,12 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -10535,6 +10885,12 @@ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-2.0.4.tgz", "integrity": "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==" }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10616,6 +10972,18 @@ "node": ">=12" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", @@ -10716,6 +11084,51 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/sinon": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/sinon/-/sinon-12.0.1.tgz", @@ -11381,6 +11794,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "node_modules/table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", @@ -11602,6 +12021,60 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/touch/node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", @@ -11710,6 +12183,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -11762,6 +12241,15 @@ "imurmurhash": "^0.1.4" } }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", @@ -11795,6 +12283,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -11834,6 +12332,18 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -11851,6 +12361,15 @@ "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==" }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/webpack": { "version": "5.88.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.1.tgz", @@ -12007,6 +12526,40 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -12129,6 +12682,27 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", @@ -12137,11 +12711,26 @@ "node": ">=8" } }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/xml-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.3.0.tgz", "integrity": "sha512-i4PIrX33Wd66dvwo4syicwlwmnr6wuvvn4f2ku9hA67C2Uk62Xubczuhct+Evnd12/DV71qKNeDdJwES8HX1RA==" }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -15747,6 +16336,12 @@ "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==" }, + "abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -15837,7 +16432,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "optional": true, + "devOptional": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -15926,6 +16521,12 @@ "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", "integrity": "sha512-VUeSMD8nEGBWaZK4lizI1sf3yEC7pnAQ/mrI7pC2fBz2s/tq5jWWEngTwaf0Gruu/OoXRGLGg1XFqpYBiGTYJA==" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -15993,7 +16594,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "optional": true + "devOptional": true }, "body-scroll-lock": { "version": "3.1.5", @@ -16150,7 +16751,7 @@ "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "optional": true, + "devOptional": true, "requires": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -16225,6 +16826,15 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -16336,6 +16946,15 @@ "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==" }, + "cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "requires": { + "rrweb-cssom": "^0.6.0" + } + }, "d3": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.1.tgz", @@ -16628,6 +17247,17 @@ "d3-transition": "2 - 3" } }, + "data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "requires": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + } + }, "datatables.net": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.13.1.tgz", @@ -16693,6 +17323,12 @@ } } }, + "decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "deep-equal": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", @@ -16712,9 +17348,9 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", "requires": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -16735,6 +17371,12 @@ } } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -16793,6 +17435,15 @@ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true }, + "domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "requires": { + "webidl-conversions": "^7.0.0" + } + }, "domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", @@ -16813,6 +17464,12 @@ "domhandler": "^5.0.1" } }, + "dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true + }, "duplexify": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", @@ -17659,6 +18316,17 @@ "is-callable": "^1.1.3" } }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -17677,6 +18345,13 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -17791,12 +18466,13 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" } }, @@ -18047,6 +18723,15 @@ } } }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, "http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -18103,6 +18788,12 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -18203,12 +18894,12 @@ } }, "is-array-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz", - "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", "requires": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "is-typed-array": "^1.1.10" } }, @@ -18229,7 +18920,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "optional": true, + "devOptional": true, "requires": { "binary-extensions": "^2.0.0" } @@ -18340,6 +19031,12 @@ "isobject": "^3.0.1" } }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -18454,6 +19151,56 @@ "esprima": "^4.0.0" } }, + "jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "requires": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "dependencies": { + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + } + } + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -18884,6 +19631,12 @@ "pkg-up": "3.x.x" } }, + "mockdate": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz", + "integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==", + "dev": true + }, "moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -19305,6 +20058,74 @@ "sorted-array-functions": "^1.3.0" } }, + "nodemon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -19351,7 +20172,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "optional": true + "devOptional": true }, "nth-check": { "version": "2.1.1", @@ -19379,6 +20200,12 @@ } } }, + "nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -19564,6 +20391,15 @@ "lines-and-columns": "^1.1.6" } }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -19772,6 +20608,18 @@ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -19792,9 +20640,15 @@ } }, "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" + }, + "querystringify": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.2.0.tgz", - "integrity": "sha512-LN6QV1IJ9ZhxWTNdktaPClrNfp8xdSAYS0Zk2ddX7XsXZAxckMHPCBcHRo0cTcEIgYPRiGEkmji3Idkh2yFtYw==" + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true }, "queue-microtask": { "version": "1.2.3", @@ -19902,7 +20756,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "optional": true, + "devOptional": true, "requires": { "picomatch": "^2.2.1" } @@ -19969,13 +20823,13 @@ } }, "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" } }, "regexpp": { @@ -20026,6 +20880,12 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -20087,6 +20947,12 @@ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-2.0.4.tgz", "integrity": "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==" }, + "rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -20131,6 +20997,15 @@ "yargs": "^17.2.1" } }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, "schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", @@ -20209,6 +21084,41 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "requires": { + "semver": "^7.5.3" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "sinon": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/sinon/-/sinon-12.0.1.tgz", @@ -20702,6 +21612,12 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", @@ -20865,6 +21781,47 @@ } } }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "requires": { + "abbrev": "1" + } + } + } + }, + "tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + } + }, + "tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "requires": { + "punycode": "^2.3.0" + } + }, "trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", @@ -20945,6 +21902,12 @@ "which-boxed-primitive": "^1.0.2" } }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -20985,6 +21948,12 @@ "imurmurhash": "^0.1.4" } }, + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + }, "update-browserslist-db": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", @@ -21002,6 +21971,16 @@ "punycode": "^2.1.0" } }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -21038,6 +22017,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "requires": { + "xml-name-validator": "^4.0.0" + } + }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -21052,6 +22040,12 @@ "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==" }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, "webpack": { "version": "5.88.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.1.tgz", @@ -21146,6 +22140,31 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==" }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + } + }, + "whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true + }, + "whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "requires": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -21240,16 +22259,35 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "requires": {} + }, "xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" }, + "xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true + }, "xml-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.3.0.tgz", "integrity": "sha512-i4PIrX33Wd66dvwo4syicwlwmnr6wuvvn4f2ku9hA67C2Uk62Xubczuhct+Evnd12/DV71qKNeDdJwES8HX1RA==" }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 96df1eb94..98d14dc31 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ ], "scripts": { "start": "pm2 start config/pm2.json", + "start:local": "nodemon -r dotenv/config index.js", "build:clean": "build/clean-src", "build:cp-assets": "build/cp-assets", "build:js": "build/build-js", @@ -81,7 +82,11 @@ ] }, "devDependencies": { + "dotenv": "^16.3.1", "husky": "^8.0.3", - "node-html-parser": "^6.1.1" + "jsdom": "^22.1.0", + "mockdate": "^3.0.5", + "node-html-parser": "^6.1.1", + "nodemon": "^3.0.1" } } diff --git a/server/models/severity.js b/server/models/severity.js index fb3da625c..04f6e126c 100644 --- a/server/models/severity.js +++ b/server/models/severity.js @@ -7,7 +7,8 @@ const severity = [{ subTitle: 'Danger to life', tagline: 'act now', isActive: true, - actionLink: '/what-to-do-in-a-flood#what-to-do-if-you-get-a-severe-flood-warning' + actionLink: '/what-to-do-in-a-flood#what-to-do-if-you-get-a-severe-flood-warning', + icon: '' }, { id: 2, title: 'Flood warning', @@ -17,7 +18,8 @@ const severity = [{ subTitle: 'Flooding is expected', tagline: 'act now', isActive: true, - actionLink: '/what-to-do-in-a-flood#what-to-do-if-you-get-a-flood-warning' + actionLink: '/what-to-do-in-a-flood#what-to-do-if-you-get-a-flood-warning', + icon: '' }, { id: 1, title: 'Flood alert', @@ -27,7 +29,8 @@ const severity = [{ subTitle: 'Flooding is possible', tagline: 'be prepared', isActive: true, - actionLink: '/what-to-do-in-a-flood#what-to-do-if-you-get-a-flood-alert' + actionLink: '/what-to-do-in-a-flood#what-to-do-if-you-get-a-flood-alert', + icon: '' }, { id: 4, title: 'Flood warning removed', @@ -37,7 +40,8 @@ const severity = [{ subTitle: 'in the last 24 hours', tagline: '', isActive: false, - actionLink: '/what-to-do-in-a-flood' + actionLink: '/what-to-do-in-a-flood', + icon: '' }] module.exports = severity diff --git a/server/models/views/location.js b/server/models/views/location.js index 1fd29fff2..02acbd316 100644 --- a/server/models/views/location.js +++ b/server/models/views/location.js @@ -24,6 +24,7 @@ class ViewModel { dateFormatted: 'Up to date as of ' + moment.tz('Europe/London').format('h:mma') + ' on ' + moment.tz('Europe/London').format('D MMMM YYYY'), feedback: false, dataError, + signUpForFloodWarnings: 'Location:Get warnings:Location - Get warnings', planAhead: 'Location:Related-content:Plan-ahead-for-flooding', whatToDo: 'Location:Related-content:What-to-do-in-a-flood', recoverAfter: 'Location:Related-content:Recover-after-a-flood', @@ -112,6 +113,7 @@ class ViewModel { groupSevere (group, location) { this.bannerSevereSub = 'There is a danger to life' this.severitySevereTitle = group.severity.title + this.severeIcon = group.severity.icon if (group.floods.length === 1) { this.bannerSevereMainLink = `/target-area/${group.floods[0].ta_code}` this.bannerSevereMainText = `Severe flood warning for ${group.floods[0].ta_name}` @@ -125,6 +127,7 @@ class ViewModel { this.bannerSub = 'Flooding is expected' this.severity = group.severity.hash this.severityTitle = group.severity.title + this.mainIcon = group.severity.icon if (group.floods.length === 1) { this.bannerMainLink = `/target-area/${group.floods[0].ta_code}` this.bannerMainText = `Flood warning for ${group.floods[0].ta_name}` @@ -146,6 +149,7 @@ class ViewModel { this.bannerMainLink = `/alerts-and-warnings?q=${encodeURIComponent(location)}#alerts` this.bannerMainText = `${group.floods.length} flood alerts in this area` } + this.mainIcon = group.severity.icon } else { this.alerts = group.floods.length if (group.floods.length === 1) { diff --git a/server/models/views/station.js b/server/models/views/station.js index fd562e29a..2dac0cd2c 100644 --- a/server/models/views/station.js +++ b/server/models/views/station.js @@ -1,6 +1,7 @@ // const hoek = require('@hapi/hoek') const moment = require('moment-timezone') const config = require('../../config') +const severity = require('../severity') const Station = require('./station-data') const Forecast = require('./station-forecast') const util = require('../../util') @@ -13,6 +14,7 @@ class ViewModel { this.station = new Station(station) this.station.riverNavigation = river + this.id = station.id this.twitterEvent = 'Station:Share Page:Station - Share to Twitter' this.facebookEvent = 'Station:Share Page:Station - Share to Facebook' @@ -63,7 +65,6 @@ class ViewModel { this.isAlertLinkRendered = true } } - switch (numWarnings) { case 0: break @@ -114,11 +115,14 @@ class ViewModel { this.isSevereLinkRenedered = true this.isWarningLinkRendered = false this.isAlertLinkRendered = false + this.mainIcon = getBannerIcon(3) } else if (numWarnings && numAlerts) { this.isWarningLinkRendered = true this.isAlertLinkRendered = false + this.mainIcon = getBannerIcon(2) } else { this.isAlertLinkRendered = true + this.mainIcon = getBannerIcon(1) } this.id = this.station.id this.telemetry = telemetry || [] @@ -171,7 +175,7 @@ class ViewModel { oneHourAgo.setHours(oneHourAgo.getHours() - 1) - // check if recent value is over one hour old + // check if recent value is over one hour old0 this.dataOverHourOld = new Date(this.recentValue.ts) < oneHourAgo this.recentValue.dateWhen = 'on ' + moment.tz(this.recentValue.ts, tz).format('D/MM/YY') @@ -181,35 +185,6 @@ class ViewModel { this.recentValue.dateWhen = 'yesterday' } - // FFOI processing - if (forecast) { - // Note: thresolds from forecasting is probably now redundant (thresholds now come from the IMTD API - // We still process the thresolds but the discard them in favour of the IMTD ones - // Need to remove the redundant threshold prcessing code as a tech debt item. - const { thresholds } = forecast - - // In the absence of thresholds, need to decide what would be an indicator of a forecast station - // Options would be: - // * presence of forecast on s3 - // * entry in ffoi_stations - // * some other option - this.isFfoi = thresholds.length > 0 - if (this.isFfoi) { - this.ffoi = new Forecast(forecast, this.station.isCoastal, this.station.recentValue) - this.hasForecast = this.ffoi.hasForecastData - - const highestPoint = this.ffoi.maxValue || null - if (highestPoint !== null) { - const forecastHighestPoint = parseFloat(highestPoint._).toFixed(2) - const forecastHighestPointTime = highestPoint.formattedTimestamp - - this.forecastDetails = `The highest level in our forecast is ${forecastHighestPoint}m at ${forecastHighestPointTime}. Forecasts come from a computer model and can change.` - } - } - - this.phase = this.isFfoi ? 'beta' : false - } - if (this.station.percentile5 && this.station.percentile95) { if (isNaN(this.station.percentile5) || isNaN(this.station.percentile95)) { this.station.hasPercentiles = false @@ -234,20 +209,14 @@ class ViewModel { } } - // Set Lat long + // // Set Lat long const coordinates = JSON.parse(this.station.coordinates).coordinates - coordinates.reverse() + + this.centre = coordinates.join(',') // Set pageTitle, metaDescription - let stationType + const stationType = stationTypeCalculator(this.station.type) const stationLocation = this.station.name - if (this.station.type === 'c') { - stationType = 'Sea' - } else if (this.station.type === 'g') { - stationType = 'Groundwater' - } else { - stationType = 'River' - } if (this.station.type === 'g') { this.pageTitle = `Groundwater level at ${stationLocation}` @@ -385,6 +354,80 @@ class ViewModel { // Set canonical url this.metaCanonical = `/station/${this.station.id}${this.station.direction === 'upstream' ? '' : '/downstream'}` this.liveServiceUrl = `/station/${this.station.id}${this.station.direction === 'downstream' ? '?direction=d' : ''}` + + // Map + this.zoom = 14 + + // Forecast Data Calculations + + let forecastData + if (forecast) { + const { thresholds } = forecast + this.isFfoi = thresholds.length > 0 + if (this.isFfoi) { + forecastData = new Forecast(forecast, this.station.isCoastal, this.station.recentValue) + this.isForecast = forecastData.hasForecastData + const highestPoint = forecastData.maxValue || null + + if (highestPoint !== null) { + const forecastHighestPoint = parseFloat(highestPoint._).toFixed(2) + const forecastHighestPointTime = `${moment.tz(highestPoint.ts, tz).format('D MMMM')} at ${moment.tz(highestPoint.ts, tz).format('h:mma')}` + + this.forecastHighest = forecastHighestPoint + this.forecastHighestTime = forecastHighestPointTime + this.forecastDetails = `The highest level in our forecast is ${forecastHighestPoint}m at ${forecastHighestPointTime}. Forecasts come from a computer model and can change.` + } + } + } + let telemetryData + if (telemetry.length) { + telemetryData = telemetryForecastBuilder(this.telemetry, forecastData, this.station.type) + } + this.telemetryRefined = telemetryData || [] + } +} + +function getBannerIcon (id) { + return severity.find(item => item.id === id)?.icon +} + +function stationTypeCalculator (stationTypeData) { + let stationType + if (stationTypeData === 'c') { + stationType = 'Sea' + } else if (stationTypeData === 'g') { + stationType = 'Groundwater' + } else { + stationType = 'River' + } + return stationType +} +function telemetryForecastBuilder (telemetryRawData, forecastRawData, stationType) { + const observed = telemetryRawData.map(function (telemetry) { + return { + dateTime: telemetry.ts, + value: telemetry._ + } + }) + + let forecastData = [] + + if (forecastRawData) { + forecastData = forecastRawData.processedValues.map(function (forecast) { + return { + dateTime: forecast.ts, + value: Number(forecast._) + } + }) + } + + return { + type: stationTypeCalculator(stationType).toLowerCase(), + latestDateTime: telemetryRawData[0].ts, + dataStartDateTime: moment(telemetryRawData[0].ts).subtract(5, 'days').toISOString().replace(/.\d+Z$/g, 'Z'), + dataEndDateTime: moment().toISOString().replace(/.\d+Z$/g, 'Z'), + observed: observed, + forecast: forecastData } } diff --git a/server/models/views/target-area.js b/server/models/views/target-area.js index 650d8b2a5..3579ccd6c 100644 --- a/server/models/views/target-area.js +++ b/server/models/views/target-area.js @@ -75,6 +75,7 @@ class ViewModel { mapTitle, floodRiskUrl, bingMaps: bingKeyMaps, + signUpForFloodWarnings: 'Target Area:Get Warnings:TA - Get warnings', planAhead: 'Target Area:Related-content:Plan-ahead-for-flooding', whatToDo: 'Target Area:Related-content:What-to-do-in-a-flood', recoverAfter: 'Target Area:Related-content:Recover-after-a-flood', diff --git a/server/src/js/components/bar-chart.js b/server/src/js/components/bar-chart/index.mjs similarity index 74% rename from server/src/js/components/bar-chart.js rename to server/src/js/components/bar-chart/index.mjs index 064386340..b225bef06 100644 --- a/server/src/js/components/bar-chart.js +++ b/server/src/js/components/bar-chart/index.mjs @@ -7,6 +7,8 @@ import { timeFormat } from 'd3-time-format' import { select, pointer } from 'd3-selection' import { max } from 'd3-array' import { timeMinute } from 'd3-time' +import { createResolutionControls, updateResolutionControls } from './resolution-controls.mjs' +import { createPaginationControls, updatePagination } from './pagination-controls.mjs' // const { xhr } = window.flood.utils const { forEach } = window.flood.utils @@ -237,52 +239,6 @@ function BarChart (containerId, stationId, data) { locatorLine.classed('locator__line--visible', false) locatorBackground.classed('locator__background--visible', false) } - - const updateSegmentedControl = () => { - const now = new Date() - const dataDurationDays = (new Date(now.getTime() - dataStart.getTime())) / (1000 * 60 * 60 * 24) - // Check there are at least 2 telemetry arrays - let numBands = 0 - for (let i = 0; i < bands.length; i++) { - numBands += Object.getOwnPropertyDescriptor(dataCache, bands[i].period) ? 1 : 0 - } - // Determine which controls to display - forEach(segmentedControl.querySelectorAll('.defra-chart-segmented-control input'), input => { - const isBand = period === input.getAttribute('data-period') - const band = bands.find(x => x.period === input.getAttribute('data-period')) - input.checked = isBand - input.parentNode.style.display = (band.days <= dataDurationDays) && numBands > 1 ? 'inline-block' : 'none' - input.parentNode.classList.toggle('defra-chart-segmented-control__segment--selected', isBand) - }) - } - - const updatePagination = (start, end, duration, durationHours) => { - // Set paging values and ensure they are within data range - const now = new Date() - let nextStart = new Date(start.getTime() + duration) - let nextEnd = new Date(end.getTime() + duration) - let previousStart = new Date(start.getTime() - duration) - let previousEnd = new Date(end.getTime() - duration) - nextEnd = nextEnd.getTime() <= now.getTime() ? nextEnd.toISOString().replace(/.\d+Z$/g, 'Z') : null - nextStart = nextEnd ? nextStart.toISOString().replace(/.\d+Z$/g, 'Z') : null - previousStart = previousStart.getTime() >= dataStart.getTime() ? previousStart.toISOString().replace(/.\d+Z$/g, 'Z') : null - previousEnd = previousStart ? previousEnd.toISOString().replace(/.\d+Z$/g, 'Z') : null - // Set properties - paginationInner.style.display = (nextStart || previousEnd) ? 'inline-block' : 'none' - pageForward.setAttribute('data-start', nextStart) - pageForward.setAttribute('data-end', nextEnd) - pageBack.setAttribute('data-start', previousStart) - pageBack.setAttribute('data-end', previousEnd) - pageForward.setAttribute('aria-disabled', !(nextStart && nextEnd)) - pageBack.setAttribute('data-journey-click', 'Rainfall:Chart Interaction:Rainfall - Previous 24hrs') - pageForward.setAttribute('data-journey-click', 'Rainfall:Chart Interaction:Rainfall - Next 24hrs') - pageBack.setAttribute('aria-disabled', !(previousStart && previousEnd)) - pageForwardText.innerText = `Next ${durationHours > 1 ? durationHours : duration} ${durationHours > 1 ? 'hours' : 'minutes'}` - pageBackText.innerText = `Previous ${durationHours > 1 ? durationHours : duration} ${durationHours > 1 ? 'hours' : 'minutes'}` - pageForwardDescription.innerText = '' - pageBackDescription.innerText = '' - } - const updateGrid = (colcount, total, hours, days, start, end) => { // Update grid properites grid.attr('aria-rowcount', 1) @@ -344,22 +300,26 @@ function BarChart (containerId, stationId, data) { dataItem = direction === 'forward' ? dataPage[positiveDataItems[positiveDataItems.length - 1]] : dataPage[positiveDataItems[0]] } // Update html control properties - updateSegmentedControl() - updatePagination(pageStart, pageEnd, pageDuration, pageDurationHours) + updateResolutionControls({ bands, dataCache, dataStart, period, resolutionControlGroup }) + updatePagination({ + start: pageStart, + end: pageEnd, + duration: pageDuration, + dataStart, + paginationControlGroup: pagination, + pageForward, + pageForwardText, + pageForwardDescription, + pageBack, + pageBackText, + pageBackDescription + }) const totalPageRainfall = dataPage.reduce((a, b) => { return a + b.value }, 0) const pageValueStart = new Date(new Date(dataPage[dataPage.length - 1].dateTime).getTime() - valueDuration) const pageValueEnd = new Date(dataPage[0].dateTime) updateGrid(positiveDataItems.length, totalPageRainfall, pageDurationHours, pageDurationDays, pageValueStart, pageValueEnd) } - const changePage = (event) => { - const target = event.target - direction = target.getAttribute('data-direction') - pageStart = new Date(target.getAttribute('data-start')) - pageEnd = new Date(target.getAttribute('data-end')) - initChart() - } - const scaleBandInvert = (scale) => { // D3 doesnt currently support inverting of a scaleBand const domain = scale.domain() @@ -410,27 +370,14 @@ function BarChart (containerId, stationId, data) { container.appendChild(controls) // Data resolutions in days, ascending order - const bands = [{ period: 'minutes', label: 'Minutes', days: 1 }, { period: 'hours', label: 'Hours', days: 5 }] + const bands = [ + { period: 'minutes', label: '24 hours', days: 1 }, + { period: 'hours', label: '5 days', days: 5 } + ] // Add time scale buttons - const segmentedControl = document.createElement('div') - segmentedControl.className = 'defra-chart-segmented-control' - for (let i = bands.length - 1; i >= 0; i--) { - const control = document.createElement('div') - control.className = 'defra-chart-segmented-control__segment' - control.style.display = 'none' - let start = new Date() - let end = new Date() - start.setHours(start.getHours() - (bands.find(x => x.period === bands[i].period).days * 24)) - start = start.toISOString().replace(/.\d+Z$/g, 'Z') - end = end.toISOString().replace(/.\d+Z$/g, 'Z') - control.innerHTML = ` - - - ` - segmentedControl.appendChild(control) - } - controls.appendChild(segmentedControl) + const resolutionControlGroup = createResolutionControls({ bands }) + controls.appendChild(resolutionControlGroup) // Create chart container elements const svg = select(`#${containerId}`).append('svg') @@ -463,41 +410,17 @@ function BarChart (containerId, stationId, data) { const tooltipDescription = tooltipText.append('tspan').attr('class', 'tooltip-text__small') // Add paging control - const pagination = document.createElement('div') - pagination.className = 'defra-chart-pagination' - const paginationInner = document.createElement('div') - paginationInner.style.display = 'none' - paginationInner.className = 'defra-chart-pagination_inner' - const pageBack = document.createElement('button') - pageBack.className = 'defra-chart-pagination__button defra-chart-pagination__button--back' - pageBack.setAttribute('data-direction', 'back') - pageBack.setAttribute('aria-controls', 'bar-chart') - pageBack.setAttribute('aria-describedby', 'page-back-description') - const pageBackText = document.createElement('span') - pageBackText.className = 'defra-chart-pagination__text' - pageBack.appendChild(pageBackText) - const pageBackDescription = document.createElement('span') - pageBackDescription.id = 'page-back-description' - pageBackDescription.className = 'govuk-visually-hidden' - pageBackDescription.setAttribute('aria-live', 'polite') - pageBack.appendChild(pageBackDescription) - const pageForward = document.createElement('button') - pageForward.className = 'defra-chart-pagination__button defra-chart-pagination__button--forward' - pageForward.setAttribute('data-direction', 'forward') - pageForward.setAttribute('aria-controls', 'bar-chart') - pageForward.setAttribute('aria-describedby', 'page-forward-description') - const pageForwardText = document.createElement('span') - pageForwardText.className = 'defra-chart-pagination__text' - pageForward.appendChild(pageForwardText) - const pageForwardDescription = document.createElement('span') - pageForwardDescription.id = 'page-forward-description' - pageForwardDescription.className = 'govuk-visually-hidden' - pageForwardDescription.setAttribute('aria-live', 'polite') - pageForward.appendChild(pageForwardDescription) - paginationInner.appendChild(pageBack) - paginationInner.appendChild(pageForward) - pagination.appendChild(paginationInner) - container.appendChild(pagination) + const { + pagination, + pageForward, + pageForwardText, + pageForwardDescription, + pageBack, + pageBackText, + pageBackDescription + } = createPaginationControls() + + controls.appendChild(pagination) // Set defaults let width, height, xScale, yScale, dataStart, dataPage, dataItem, latestDateTime, period, positiveDataItems, direction, interfaceType @@ -542,17 +465,20 @@ function BarChart (containerId, stationId, data) { }) container.addEventListener('click', (e) => { - const classNames = ['defra-chart-segmented-control__input', 'defra-chart-pagination__button'] - if (!classNames.some(className => e.target.classList.contains(className))) return - if (e.target.getAttribute('aria-disabled') === 'true') { - const container = e.target.classList.contains('defra-chart-pagination__button--back') ? pageBackDescription : pageForwardDescription - container.innerText = '' + const button = e.target.closest('.defra-chart-controls__button') + if (!button) return + if (button.getAttribute('aria-disabled') === 'true') { + const description = button.querySelector('.govuk-visually-hidden') + description.innerText = '' window.setTimeout(() => { - container.innerText = container === pageBackDescription ? 'No previous data' : 'No more data' + description.innerText = button.dataset.direction === 'back' ? 'No previous data' : 'No more data' }, 100) return } - changePage(e) + direction = button.getAttribute('data-direction') + pageStart = new Date(button.getAttribute('data-start')) + pageEnd = new Date(button.getAttribute('data-end')) + initChart() }) document.addEventListener('keyup', (e) => { diff --git a/server/src/js/components/bar-chart/pagination-controls.mjs b/server/src/js/components/bar-chart/pagination-controls.mjs new file mode 100644 index 000000000..74a6ec723 --- /dev/null +++ b/server/src/js/components/bar-chart/pagination-controls.mjs @@ -0,0 +1,83 @@ + +export function createPaginationControls () { + const pagination = document.createElement('div') + pagination.classList.add('defra-chart-controls__group', 'defra-chart-controls__group--pagination') + pagination.style.display = 'none' + + const pageBack = document.createElement('button') + pageBack.className = 'defra-chart-controls__button' + pageBack.setAttribute('data-direction', 'back') + pageBack.setAttribute('aria-controls', 'bar-chart') + pageBack.setAttribute('aria-describedby', 'page-back-description') + + const pageBackText = document.createElement('span') + pageBackText.className = 'defra-chart-controls__button-text' + + const pageBackDescription = document.createElement('span') + pageBackDescription.id = 'page-back-description' + pageBackDescription.className = 'govuk-visually-hidden' + pageBackDescription.setAttribute('aria-live', 'polite') + + pageBack.appendChild(pageBackText) + pageBack.appendChild(pageBackDescription) + + const pageForward = document.createElement('button') + pageForward.className = 'defra-chart-controls__button' + pageForward.setAttribute('data-direction', 'forward') + pageForward.setAttribute('aria-controls', 'bar-chart') + pageForward.setAttribute('aria-describedby', 'page-forward-description') + + const pageForwardText = document.createElement('span') + pageForwardText.className = 'defra-chart-controls__text' + + const pageForwardDescription = document.createElement('span') + pageForwardDescription.id = 'page-forward-description' + pageForwardDescription.className = 'govuk-visually-hidden' + pageForwardDescription.setAttribute('aria-live', 'polite') + + pageForward.appendChild(pageForwardText) + pageForward.appendChild(pageForwardDescription) + + pagination.appendChild(pageBack) + pagination.appendChild(pageForward) + + return { + pagination, + pageForward, + pageForwardText, + pageForwardDescription, + pageBack, + pageBackText, + pageBackDescription + } +} +export function updatePagination ({ + start, end, duration, dataStart, paginationControlGroup, + pageForward, pageForwardText, pageForwardDescription, + pageBack, pageBackText, pageBackDescription +}) { + // Set paging values and ensure they are within data range + const now = new Date() + let nextStart = new Date(start.getTime() + duration) + let nextEnd = new Date(end.getTime() + duration) + let previousStart = new Date(start.getTime() - duration) + let previousEnd = new Date(end.getTime() - duration) + nextEnd = nextEnd.getTime() <= now.getTime() ? nextEnd.toISOString().replace(/.\d+Z$/g, 'Z') : null + nextStart = nextEnd ? nextStart.toISOString().replace(/.\d+Z$/g, 'Z') : null + previousStart = previousStart.getTime() >= dataStart.getTime() ? previousStart.toISOString().replace(/.\d+Z$/g, 'Z') : null + previousEnd = previousStart ? previousEnd.toISOString().replace(/.\d+Z$/g, 'Z') : null + // Set properties + paginationControlGroup.style.display = (nextStart || previousEnd) ? 'inline-block' : 'none' + pageForward.setAttribute('data-start', nextStart) + pageForward.setAttribute('data-end', nextEnd) + pageBack.setAttribute('data-start', previousStart) + pageBack.setAttribute('data-end', previousEnd) + pageForward.setAttribute('aria-disabled', !(nextStart && nextEnd)) + pageBack.setAttribute('data-journey-click', 'Rainfall:Chart Interaction:Rainfall - Previous 24hrs') + pageForward.setAttribute('data-journey-click', 'Rainfall:Chart Interaction:Rainfall - Next 24hrs') + pageBack.setAttribute('aria-disabled', !(previousStart && previousEnd)) + pageForwardText.innerText = 'Forward' + pageBackText.innerText = 'Back' + pageForwardDescription.innerText = '' + pageBackDescription.innerText = '' +} diff --git a/server/src/js/components/bar-chart/resolution-controls.mjs b/server/src/js/components/bar-chart/resolution-controls.mjs new file mode 100644 index 000000000..03ee33748 --- /dev/null +++ b/server/src/js/components/bar-chart/resolution-controls.mjs @@ -0,0 +1,48 @@ + +const { forEach } = window.flood.utils + +export function createResolutionControls ({ bands }) { + const resolutionControlGroup = document.createElement('div') + resolutionControlGroup.classList.add('defra-chart-controls__group', 'defra-chart-controls__group--resolution') + for (let i = bands.length - 1; i >= 0; i--) { + const band = bands[i] + const control = document.createElement('button') + + const start = new Date() + const end = new Date() + start.setHours(start.getHours() - (bands.find(({ period }) => period === band.period).days * 24)) + + control.className = 'defra-chart-controls__button' + control.style.display = 'none' + control.setAttribute('data-period', band.period) + control.setAttribute('data-start', start.toISOString().replace(/.\d+Z$/g, 'Z')) + control.setAttribute('data-end', end.toISOString().replace(/.\d+Z$/g, 'Z')) + control.setAttribute('aria-controls', 'bar-chart') + + const text = document.createElement('span') + text.className = 'defra-chart-controls__text' + text.innerText = band.label + + control.appendChild(text) + resolutionControlGroup.appendChild(control) + } + return resolutionControlGroup +} + +export function updateResolutionControls ({ bands, dataCache, dataStart, period, resolutionControlGroup }) { + const now = new Date() + const dataDurationDays = (new Date(now.getTime() - dataStart.getTime())) / (1000 * 60 * 60 * 24) + // Check there are at least 2 telemetry arrays + let numBands = 0 + for (let i = 0; i < bands.length; i++) { + numBands += Object.getOwnPropertyDescriptor(dataCache, bands[i].period) ? 1 : 0 + } + // Determine which controls to display + forEach(resolutionControlGroup.querySelectorAll('.defra-chart-controls__button'), button => { + const isBand = period === button.getAttribute('data-period') + const band = bands.find(x => x.period === button.getAttribute('data-period')) + button.checked = isBand + button.style.display = (band.days <= dataDurationDays) && numBands > 1 ? 'inline-block' : 'none' + button.classList.toggle('defra-chart-controls__button--selected', isBand) + }) +} diff --git a/server/src/js/components/charts.js b/server/src/js/components/charts.js deleted file mode 100644 index d8622c38d..000000000 --- a/server/src/js/components/charts.js +++ /dev/null @@ -1,473 +0,0 @@ -'use strict' -// Chart component - -import { area as d3Area, line as d3Line, curveMonotoneX } from 'd3-shape' - -import { axisBottom, axisLeft } from 'd3-axis' - -import { scaleLinear, scaleTime } from 'd3-scale' - -import { timeFormat } from 'd3-time-format' - -import { timeDay } from 'd3-time' - -import { select, selectAll, pointer } from 'd3-selection' - -import { bisector, extent } from 'd3-array' - -function LineChart (containerId, data) { - // Settings - const windowBreakPoint = 640 - const svgBreakPoint = 576 - - const chart = document.getElementById(containerId) - - // Setup array to combine observed and forecast points and identify startPoint for locator - let lines = [] - let dataPoint - let hasObserved = false - let hasForecast = false - if (data.observed.length) { - const errorFilter = l => !l.err - const errorAndNegativeFilter = l => errorFilter(l)// *DBL below zero addition - const filterFunction = data.plotNegativeValues ? errorFilter : errorAndNegativeFilter - lines = data.observed.filter(filterFunction).map(l => ({ ...l, type: 'observed' })).reverse() - dataPoint = lines[lines.length - 1] ? JSON.parse(JSON.stringify(lines[lines.length - 1])) : null - hasObserved = lines.length > 0 - } - if (data.forecast.length) { - lines = lines.concat(data.forecast.map(l => ({ ...l, type: 'forecast' }))) - hasForecast = true - } - - // Set dataPointLatest - const dataPointLatest = JSON.parse(JSON.stringify(dataPoint)) - let dataPointLocator = dataPointLatest - - // Area generator - const area = d3Area().curve(curveMonotoneX) - .x((d) => { return xScale(new Date(d.ts)) }) - .y0((d) => { return height }) - .y1(d => { return yScale(data.plotNegativeValues === false && d._ < 0 ? 0 : d._) }) // *DBL below zero addition - - // Line generator - const line = d3Line().curve(curveMonotoneX) - .x((d) => { return xScale(new Date(d.ts)) }) - .y((d) => { return yScale(data.plotNegativeValues === false && d._ < 0 ? 0 : d._) }) // *DBL below zero addition - - // Set level and date formats - const parseTime = timeFormat('%-I:%M%p') - const parseDate = timeFormat('%e %b') - const parseDateShort = timeFormat('%-e/%-m') - const parseDateLong = timeFormat('%a, %e %b') - - // - // Private methods - // - - const renderChart = () => { - // Set isMobile boolean - const parentWidth = Math.floor(select('#' + containerId).node().getBoundingClientRect().width) - const isMobile = window.innerWidth <= windowBreakPoint && parentWidth <= svgBreakPoint - - // Draw axis - const xAxis = axisBottom().tickSizeOuter(0) - xAxis.scale(xScale).ticks(timeDay).tickFormat((d) => { - return isMobile ? parseDateShort(d) : parseDateLong(d) - }) - yAxis = axisLeft().ticks(5).tickFormat((d) => { - return parseFloat(d).toFixed(2) + 'm' - }).tickSizeOuter(0) - yAxis.scale(yScale) - - // Update svg and clip elements with new dimensions - svg.select('.x.axis').attr('transform', 'translate(0,' + height + ')').call(xAxis) - svg.select('.y.axis').call(yAxis) - svg.selectAll('.x.axis text').attr('y', 12) - svgInner.attr('transform', 'translate(' + (margin.left + margin.right) + ',' + 0 + ')') - clip.attr('width', width).attr('height', height) - - // Update grid lines - svg.select('.x.grid') - .attr('transform', 'translate(0,' + height + ')') - .call(axisBottom(xScale) - .ticks(timeDay) - .tickSize(-height, 0, 0) - .tickFormat('') - ) - svg.select('.y.grid') - .attr('transform', 'translate(0,' + 0 + ')') - .call(axisLeft(yScale) - .ticks(5) - .tickSize(-width, 0, 0) - .tickFormat('') - ) - - // Update time line - const timeX = Math.floor(xScale(new Date(data.now))) - svg.select('.time-line').attr('y1', 0).attr('y2', height) - timeLine.attr('y1', 0).attr('y2', height).attr('transform', 'translate(' + timeX + ',0)') - timeLabel.attr('y', height + 12).attr('transform', 'translate(' + timeX + ',0)').attr('dy', '0.71em') - - // Add height to locator line - svg.select('.locator-line').attr('y1', 0).attr('y2', height) - - // Draw lines and areas - if (hasObserved) { - observed.attr('d', line) - observedArea.attr('d', area) - } - if (hasForecast) { - forecast.attr('d', line) - forecastArea.attr('d', area) - } - - // Update locator position - locator.attr('transform', 'translate(' + locatorX + ',' + 0 + ')') - locator.select('.locator-point').attr('transform', 'translate(' + 0 + ',' + locatorY + ')') - - // Hide x axis labels that overlap with time now label - const timeNowX = timeLabel.node().getBoundingClientRect().left - const timeNowWidth = timeLabel.node().getBoundingClientRect().width - const ticks = selectAll('.x .tick') - ticks.each((d, i, n) => { - const tick = n[i] - const tickX = tick.getBoundingClientRect().left - const tickWidth = tick.getBoundingClientRect().width - const isOverlap = (tickX + tickWidth + 5) > timeNowX && tickX <= (timeNowX + timeNowWidth + 5) - select(tick).classed('tick--hidden', isOverlap) - }) - } - - const renderThresholds = () => { - // Empty thresholds container - thresholdsContainer.selectAll('*').remove() - // Add thresholds - thresholds.forEach(threshold => { - const thresholdContainer = thresholdsContainer.append('g').attr('class', 'threshold threshold--' + threshold.id) - thresholdContainer.attr('data-id', threshold.id) - thresholdContainer.classed('threshold--selected', !!threshold.isSelected) - const bg = thresholdContainer.append('rect').attr('class', 'threshold__bg').attr('x', 0).attr('y', -4).attr('height', 8) - const line = thresholdContainer.append('line').attr('class', 'threshold__line') - const label = thresholdContainer.append('g').attr('class', 'threshold-label') - const labelBgPath = label.append('path').attr('class', 'threshold-label__bg') - const text = label.append('text').attr('class', 'threshold-label__text').text(threshold.name) - const remove = label.append('g').attr('class', 'threshold__remove') - remove.append('rect').attr('x', -6).attr('y', -6).attr('width', 20).attr('height', 20) - remove.append('line').attr('x1', -0.5).attr('y1', -0.5).attr('x2', 7.5).attr('y2', 7.5) - remove.append('line').attr('x1', 7.5).attr('y1', -0.5).attr('x2', -0.5).attr('y2', 7.5) - // Set individual elements size and position - const textWidth = text.node().getBBox().width - labelBgPath.attr('d', 'm-0.5,-0.5 l' + Math.round(textWidth + 40) + ',0 l0,36 l-' + (Math.round(textWidth + 40) - 50) + ',0 l-7.5,7.5 l-7.5,-7.5 l-35,0 l0,-36 l0,0') - text.attr('x', 10).attr('y', 22) - remove.attr('transform', 'translate(' + Math.round(textWidth + 20) + ',' + 14 + ')') - const labelX = Math.round(width / 8) - label.attr('transform', 'translate(' + labelX + ',' + -46 + ')') - thresholdContainer.attr('transform', 'translate(0,' + Math.round(yScale(threshold.level)) + ')') - bg.attr('width', xScale(xExtent[1])) - line.attr('x2', xScale(xExtent[1])).attr('y2', 0) - }) - } - - const updateToolTipBackground = () => { - // Set Background size - const bg = toolTip.select('rect') - const text = toolTip.select('text') - // const textWidth = text.node().getBBox().width - const textHeight = Math.round(text.node().getBBox().height) - bg.attr('height', textHeight + 23) - const toolTipWidth = bg.node().getBBox().width - const toolTipHeight = bg.node().getBBox().height - // Set background left or right position - const containerWidth = xScale(xExtent[1]) - if (toolTipX >= containerWidth - (toolTipWidth + 10)) { - // On the left - toolTipX -= (toolTipWidth + 10) - } else { - // On the right - toolTipX += 10 - } - // Set background above or below position - if (toolTipY >= toolTipHeight + 10) { - toolTipY -= toolTipHeight + 10 - } else { - toolTipY += 10 - } - toolTipX = toolTipX.toFixed(0) - toolTipY = toolTipY.toFixed(0) - } - - const updateLocator = () => { - dataPointLocator = dataPoint // Set locator position - locatorX = Math.floor(xScale(new Date(dataPointLocator.ts))) - locatorY = Math.floor(yScale(data.plotNegativeValues === false && dataPointLocator._ < 0 ? 0 : dataPointLocator._)) // *DBL below zero addition - const latestX = Math.floor(xScale(new Date(dataPointLatest.ts))) - locator.classed('locator--offset', true) - locator.classed('locator--forecast', locatorX > latestX) - locator.attr('transform', 'translate(' + locatorX + ',' + 0 + ')') - locator.select('.locator-point').attr('transform', 'translate(' + 0 + ',' + locatorY + ')') - } - - const showTooltip = (e) => { - // Remove existing content - toolTip.select('text').selectAll('*').remove() - const mouseDate = xScale.invert(pointer(e)[0]) - const bisectDate = bisector((d) => { return new Date(d.ts) }).left - const i = bisectDate(lines, mouseDate, 1) // returns the index to the current data item - const d0 = lines[i - 1] - const d1 = lines[i] || lines[i - 1] - // Determine which date value is closest to the mouse - const d = mouseDate - new Date(d0.ts) > new Date(d1.ts) - mouseDate ? d1 : d0 - dataPoint.ts = d.ts - dataPoint._ = parseFloat(d._).toFixed(2) - toolTipX = xScale(new Date(dataPoint.ts)) - toolTipY = pointer(e)[1] - - const value = data.plotNegativeValues === false && (Math.round(dataPoint._ * 100) / 100) <= 0 ? '0' : dataPoint._ // *DBL below zero addition - toolTip.select('text').append('tspan').attr('class', 'tool-tip-text__strong').attr('dy', '0.5em').text(value + 'm') // *DBL below zero addition - toolTip.select('text').append('tspan').attr('x', 12).attr('dy', '1.4em').text(parseTime(new Date(dataPoint.ts)).toLowerCase() + ', ' + parseDate(new Date(dataPoint.ts))) - - // Update tooltip left/right background - updateToolTipBackground() - // Update tooltip location - toolTip.attr('transform', 'translate(' + toolTipX + ',' + toolTipY + ')') - toolTip.classed('tool-tip--visible', true) - } - - const hideTooltip = () => { - toolTip.classed('tool-tip--visible', false) - } - - const resetLocator = () => { - // Update locator location - locatorX = Math.floor(xScale(new Date(dataPointLatest.ts))) - locatorY = Math.floor(yScale(dataPointLatest._)) - locator.classed('locator--offset', false) - locator.classed('locator--forecast', false) - locator.attr('transform', 'translate(' + locatorX + ',' + 0 + ')') - locator.select('.locator-point').attr('transform', 'translate(' + 0 + ',' + locatorY + ')') - // Reset locator marker to latest - dataPointLocator = dataPointLatest - } - - const updateAxisY = () => { - // Extend or reduce y extent - const maxThreshold = Math.max.apply(Math, thresholds.map((x) => { return x.level })) - const minThreshold = Math.min.apply(Math, thresholds.map((x) => { return x.level })) - const maxData = Math.max(maxThreshold, yExtentDataMax) - const minData = Math.min(minThreshold, yExtentDataMin) - let range = maxData - minData - range = range < 1 ? 1 : range - // Add 1/3rd or range above and below, capped at zero for non-negative ranges - const yRangeUpperBuffered = (maxData + (range / 3)).toFixed(2) - const yRangeLowerBuffered = (minData - (range / 3)).toFixed(2) - yExtent[1] = yExtentDataMax <= yRangeUpperBuffered ? yRangeUpperBuffered : yExtentDataMax - yExtent[0] = window.flood.model.station.isRiver ? (yRangeLowerBuffered < 0 ? 0 : yRangeLowerBuffered) : yRangeLowerBuffered - // Update y scale - yScale = scaleLinear().domain(yExtent).nice() - yScale.range([height, 0]) - // Update y axis - yAxis = axisLeft() - yAxis.ticks(5).tickFormat((d) => { return parseFloat(d).toFixed(2) + 'm' }).tickSizeOuter(0) - yAxis.scale(yScale) - } - - const addThreshold = (threshold) => { - // Update thresholds array - thresholds = thresholds.filter((x) => { return x.id !== threshold.id }) - thresholds.forEach(x => { x.isSelected = false }) - threshold.isSelected = true - thresholds.push(threshold) - updateAxisY() - // Re-render - resetLocator() - renderChart() - renderThresholds() - } - - const removeThreshold = (id) => { - // Update thresholds array - thresholds = thresholds.filter((x) => { return x.id !== id }) - updateAxisY() - // Re-render - updateLocator() - renderChart() - renderThresholds() - } - - // - // Setup - // - - const svg = select('#' + containerId).append('svg').style('pointer-events', 'none') - const svgInner = svg.append('g').style('pointer-events', 'all') - svgInner.append('g').classed('y grid', true) - svgInner.append('g').classed('x grid', true) - svgInner.append('g').classed('x axis', true) - svgInner.append('g').classed('y axis', true) - const clip = svgInner.append('defs').append('clipPath').attr('id', 'clip').append('rect').attr('x', 0).attr('y', 0) - const clipInner = svgInner.append('g').attr('clip-path', 'url(#clip)') - - // Add observed and forecast elements - let observedArea, observed, forecastArea, forecast - if (hasObserved) { - clipInner.append('g').classed('observed observed-focus', true) - const observedLines = lines.filter(l => l.type === 'observed') - observedArea = svg.select('.observed').append('path').datum(observedLines).classed('observed-area', true) - observed = svg.select('.observed').append('path').datum(observedLines).classed('observed-line', true) - } - if (hasForecast) { - clipInner.append('g').classed('forecast', true) - const forecastLine = lines.filter(l => l.type === 'forecast') - forecastArea = svg.select('.forecast').append('path').datum(forecastLine).classed('forecast-area', true) - forecast = svg.select('.forecast').append('path').datum(forecastLine).classed('forecast-line', true) - } - - // Add timeline - const timeLine = clipInner.append('line').classed('time-line', true) - const dateNow = new Date() - const time = (dateNow.getHours() % 12 || 12) + ':' + (dateNow.getMinutes() < 10 ? '0' : '') + dateNow.getMinutes() + (dateNow.getHours() >= 12 ? 'pm' : 'am') - const timeLabel = svgInner.append('text').attr('class', 'time-now-text').attr('x', -26).text(time) - - // Add locator - const locator = clipInner.append('g').classed('locator', true) - locator.append('line').classed('locator-line', true) - locator.append('circle').attr('r', 4.5).classed('locator-point', true) - - // Add thresholds group - const thresholdsContainer = clipInner.append('g').classed('thresholds', true) - - // Add tooltip container - const toolTip = clipInner.append('g').attr('class', 'tool-tip') - toolTip.append('rect').attr('class', 'tool-tip-bg').attr('width', 147) - toolTip.append('text').attr('class', 'tool-tip-text').attr('x', 12).attr('y', 20) - - // Get width and height - const margin = { top: 25, bottom: 25, left: 28, right: 28 } - const containerBoundingRect = select('#' + containerId).node().getBoundingClientRect() - let width = Math.floor(containerBoundingRect.width) - margin.left - margin.right - let height = Math.floor(containerBoundingRect.height) - margin.top - margin.bottom - - // Note: xExtent uses observed and forecast data rather than lines for the scenario where river levels - // start or end as -ve since we still need to determine the datetime span of the graph even if the - // values are excluded from plotting by virtue of being -ve - - // Set x scale extent - const xExtent = extent(data.observed.concat(data.forecast), (d, i) => { return new Date(d.ts) }) - // Increase x extent by 5% from now value - let date = new Date(data.now) - const percentile = Math.round(Math.abs(xExtent[0] - date) * 0.05) - date = new Date(Number(data.now) + Number(percentile)) - const xRange = [xExtent[0], xExtent[1]] - xRange.push(date) - xExtent[0] = Math.min.apply(Math, xRange) - xExtent[1] = Math.max.apply(Math, xRange) - // Set x input domain - const xScaleInitial = scaleTime().domain(xExtent) - const xScale = scaleTime().domain(xExtent) - // Set x output range - xScaleInitial.range([0, width]) - xScale.range([0, width]) - - // Set y scale extent - const yExtent = extent(lines, (d, i) => { return d._ }) - // Adjust y extent to highest and lowest values from the data - yExtent[0] = Math.min.apply(Math, yExtent) - yExtent[1] = Math.max.apply(Math, yExtent) - // Reference to intial y extent min and max used when removing thresholds - const yExtentDataMin = yExtent[0] - const yExtentDataMax = yExtent[1] - // Add 1/3rd or range above and below, capped at zero for non-negative ranges - let yRange = yExtent[1] - yExtent[0] - yRange = yRange < 1 ? 1 : yRange // make range minimum 1m to stop zigzag - const yRangeUpperBuffer = (yExtent[1] + (yRange / 3)).toFixed(2) - const yRangeLowerBuffer = (yExtent[0] - (yRange / 3)).toFixed(2) - - yExtent[0] = window.flood.model.station.isRiver ? (yRangeLowerBuffer < 0 ? 0 : yRangeLowerBuffer) : yRangeLowerBuffer - - yExtent[1] = yRangeUpperBuffer - // Set y input domain - let yScale = scaleLinear().domain(yExtent).nice() - // Set y output range - yScale.range([height, 0]) - - // State properties - let locatorX = -1 - let locatorY = -1 - let toolTipX, toolTipY, yAxis - let thresholds = [] - - if (hasObserved || hasForecast) { - renderChart() - // - // Public methods - // - - this.removeThreshold = (id) => { - removeThreshold(id) - } - - this.addThreshold = (threshold) => { - addThreshold(threshold) - } - - this.chart = chart - - // - // Events - // - - window.addEventListener('resize', () => { - const containerBoundingRect = select('#' + containerId).node().getBoundingClientRect() - width = Math.floor(containerBoundingRect.width) - margin.left - margin.right - height = Math.floor(containerBoundingRect.height) - margin.top - margin.bottom - xScale.range([0, width]) - yScale.range([height, 0]) - hideTooltip() - updateLocator() - renderChart() - renderThresholds() - }) - - svgInner.on('click', (e) => { - if (e.target.closest('.threshold')) return - updateLocator() - showTooltip(e) - }) - - svgInner.on('mousemove', (e) => { - if (e.target.closest('.threshold')) return - updateLocator() - showTooltip(e) - }) - - svgInner.on('mouseleave', (e) => { - hideTooltip() - resetLocator() - }) - - thresholdsContainer.on('click', (e) => { - e.stopPropagation() - const thresholdContainer = e.target.closest('.threshold') - if (e.target.closest('.threshold__remove')) { - removeThreshold(thresholdContainer.getAttribute('data-id')) - } else if (thresholdContainer) { - const threshold = thresholds.find((x) => { return x.id === thresholdContainer.getAttribute('data-id') }) - addThreshold(threshold) - } - }) - - thresholdsContainer.on('mouseover', (e) => { - if (e.target.closest('.threshold')) hideTooltip() - }) - } else { - // no Values so hide chart div - document.getElementsByClassName('defra-line-chart')[0].style.display = 'none' - } -} - -window.flood.charts = { - createLineChart: (containerId, data) => { - return new LineChart(containerId, data) - } -} diff --git a/server/src/js/components/line-chart.js b/server/src/js/components/line-chart.js new file mode 100644 index 000000000..25ca59f70 --- /dev/null +++ b/server/src/js/components/line-chart.js @@ -0,0 +1,734 @@ +'use strict' +// Chart component + +import { area as d3Area, line as d3Line, curveMonotoneX } from 'd3-shape' +import { axisBottom, axisLeft } from 'd3-axis' +import { scaleLinear, scaleTime } from 'd3-scale' +import { timeFormat } from 'd3-time-format' +import { timeDay } from 'd3-time' +import { select, selectAll, pointer } from 'd3-selection' +import { bisector, extent } from 'd3-array' +const { simplify } = window.flood.utils + +function LineChart (containerId, stationId, data, options = {}) { + const renderChart = () => { + // Set scales + setScaleX() + setScaleY() + + // Set right margin depending on length of labels + // const numChars = yScale.domain()[1].toFixed(2).length - 1 + const numChars = yScale.domain()[1].toFixed(1).length - 2 + margin = { top: 5, bottom: 45, left: 15, right: (isMobile ? 31 : 36) + (numChars * 9) } + + // Get width and height + const containerBoundingRect = container.getBoundingClientRect() + width = Math.floor(containerBoundingRect.width) - margin.left - margin.right + height = Math.floor(containerBoundingRect.height) - margin.top - margin.bottom + + // Calculate new xScale and yScales height and width + xScale.range([0, width]) + yScale.range([height, 0]) + + // Draw axis + const xAxis = axisBottom().tickSizeOuter(0) + xAxis.scale(xScale).ticks(timeDay).tickFormat(d => { return '' }) + yAxis = axisLeft().ticks(5).tickFormat(d => { + // return parseFloat(d).toFixed(2) + 'm' + return parseFloat(d).toFixed(1) + }).tickSizeOuter(0) + yAxis.scale(yScale) + + // Position axis bottom and right + svg.select('.x.axis').attr('transform', 'translate(0,' + height + ')').call(xAxis) + svg.select('.y.axis').attr('transform', 'translate(' + width + ', 0)').call(yAxis) + + // Format X Axis ticks + svg.select('.x.axis').selectAll('text').each(formatLabelsX) + + // Position y ticks + svg.select('.y.axis').style('text-anchor', 'start') + svg.selectAll('.y.axis .tick line').attr('x2', 6) + svg.selectAll('.y.axis .tick text').attr('x', 9) + + // Update grid lines + svg.select('.x.grid') + .attr('transform', 'translate(0,' + height + ')') + .call(axisBottom(xScale) + .ticks(timeDay) + .tickSize(-height, 0, 0) + .tickFormat('') + ) + svg.select('.y.grid') + .attr('transform', 'translate(0,' + 0 + ')') + .call(axisLeft(yScale) + .ticks(5) + .tickSize(-width, 0, 0) + .tickFormat('') + ) + + // Update time line + const timeX = Math.floor(xScale(new Date())) + svg.select('.time-line').attr('y1', 0).attr('y2', height) + timeLine.attr('y1', 0).attr('y2', height).attr('transform', 'translate(' + timeX + ',0)') + timeLabel.attr('y', height + 9).attr('transform', 'translate(' + timeX + ',0)') + .attr('dy', '0.71em') + .attr('x', isMobile ? -20 : -24) + + // X Axis time label + timeLabel.select('.time-now-text__time').text(timeFormat('%-I:%M%p')(new Date()).toLowerCase()) + timeLabel.select('.time-now-text__date').text(timeFormat('%-e %b')(new Date())) + + // Add height to locator line + svg.select('.locator-line').attr('y1', 0).attr('y2', height) + + // Draw lines and areas + if (dataCache.observed.length) { + observedArea.datum(observedPoints).attr('d', area) + observedLine.datum(observedPoints).attr('d', line) + } + if (dataCache.forecast.length) { + forecastArea.datum(forecastPoints).attr('d', area) + forecastLine.datum(forecastPoints).attr('d', line) + } + + // Add thresholds + thresholdsContainer.selectAll('*').remove() + thresholds.forEach(threshold => { + const thresholdContainer = thresholdsContainer + .append('g').attr('class', 'threshold') + .classed('threshold--selected', !!threshold.isSelected) + .attr('data-id', threshold.id) + .attr('data-threshold', '') + thresholdContainer.append('rect') + .attr('class', 'threshold__bg') + .attr('aria-hidden', true) + .attr('x', 0).attr('y', -10).attr('height', 20) + .attr('width', xScale(xExtent[1])) + thresholdContainer.append('line') + .attr('class', 'threshold__line') + .attr('aria-hidden', true) + .attr('x2', xScale(xExtent[1])).attr('y2', 0) + const label = thresholdContainer.append('g') + .attr('class', 'threshold-label') + const path = label.append('path') + .attr('aria-hidden', true) + .attr('class', 'threshold-label__bg') + const text = label.append('text') + .attr('class', 'threshold-label__text') + text.append('tspan').attr('font-size', 0).text('Threshold: ') + text.append('tspan').attr('x', 10).attr('y', 22).text(`${threshold.level}m ${threshold.name}`) + const textWidth = Math.round(text.node().getBBox().width) + path.attr('d', `m-0.5,-0.5 l${textWidth + 20},0 l0,36 l-${((textWidth + 20) / 2) - 7.5},0 l-7.5,7.5 l-7.5,-7.5 l-${((textWidth + 20) / 2) - 7.5},0 l0,-36 l0,0`) + label.attr('transform', `translate(${Math.round(width / 2 - ((textWidth + 20) / 2))}, -46)`) + const remove = thresholdContainer.append('a') + .attr('role', 'button') + .attr('class', 'threshold__remove') + .attr('tabindex', 0) + .attr('data-threshold-remove', '') + .attr('aria-label', `Remove ${threshold.level}m threshold (Visual only)`) + .attr('aria-controls', `${containerId}-visualisation`) + .attr('transform', 'translate(20,0)') + remove.append('circle').attr('class', 'threshold__remove-bg').attr('r', 16).attr('x1', -5).attr('y1', -5) + remove.append('circle').attr('class', 'threshold__remove-button').attr('r', 11) + remove.append('line').attr('x1', -3).attr('y1', -3).attr('x2', 3).attr('y2', 3) + remove.append('line').attr('y1', -3).attr('x2', -3).attr('x1', 3).attr('y2', 3) + // Set individual elements size and position + thresholdContainer.attr('transform', 'translate(0,' + Math.round(yScale(threshold.level)) + ')') + }) + + // Update clip text + // clipText.attr('width', width + 5).attr('height', height) + + // Add significant points + significantContainer.selectAll('*').remove() + const significantObserved = observedPoints.filter(x => x.isSignificant).map(p => ({ ...p, type: 'observed' })) + const significantForecast = forecastPoints.filter(x => x.isSignificant).map(p => ({ ...p, type: 'forecast' })) + significantPoints = significantObserved.concat(significantForecast) + const significantCells = significantContainer + .attr('aria-rowcount', 1) + .attr('aria-colcount', significantPoints.length) + .selectAll('.point').data(significantPoints).enter() + .append('g') + .attr('role', 'gridcell') + .attr('class', d => { return 'point point--' + d.type }) + .attr('tabindex', (d, i) => i === significantPoints.length - 1 ? 0 : -1) + .attr('data-point', '') + .attr('data-index', (d, i) => { return i }) + significantCells.append('circle').attr('aria-hidden', true) + .attr('r', '5') + .attr('cx', d => xScale(new Date(d.dateTime))) + .attr('cy', d => yScale(dataCache.type === 'river' && d.value < 0 ? 0 : d.value)) + significantCells.insert('text') + .attr('x', d => xScale(new Date(d.dateTime))) + .attr('y', d => yScale(dataCache.type === 'river' && d.value < 0 ? 0 : d.value)) + .text(d => { + const value = `${dataCache.type === 'river' && d.value < 0 ? 0 : d.value.toFixed(2)}m` + const time = timeFormat('%-I:%M%p')(new Date(d.dateTime)).toLowerCase() + const date = timeFormat('%e %b')(new Date(d.dateTime)) + return `${value} ${time}, ${date}` + }) + + // Hide x axis labels that overlap with time now label + const timeNowX = timeLabel.node().getBoundingClientRect().left + const timeNowWidth = timeLabel.node().getBoundingClientRect().width + const ticks = selectAll('.x .tick') + ticks.each((d, i, n) => { + const tick = n[i] + const tickX = tick.getBoundingClientRect().left + const tickWidth = tick.getBoundingClientRect().width + const isOverlap = (tickX + tickWidth + 5) > timeNowX && tickX <= (timeNowX + timeNowWidth + 5) + select(tick).classed('tick--hidden', isOverlap) + }) + } + + const getNextDataItemIndex = (e) => { + let index = parseInt(e.target.getAttribute('data-index'), 10) + const first = 0 + const last = significantPoints.length - 1 + if (e.key === 'Home') { + index = first + } else if (e.key === 'End') { + index = last + } else if (e.key === 'ArrowRight') { + index = index < last ? index += 1 : last + } else if (e.key === 'ArrowLeft') { + index = index > first ? index -= 1 : first + } + return index + } + + const swapCell = (e) => { + // Add threshold + const nextIndex = getNextDataItemIndex(e) + const cell = e.target + const nextCell = cell.parentNode.children[nextIndex] + cell.setAttribute('focusable', false) + cell.removeAttribute('id') + nextCell.setAttribute('focusable', true) + // nextCell.id = 'focussed-cell' + cell.tabIndex = -1 + nextCell.tabIndex = 0 + nextCell.focus() + dataPoint = significantPoints[nextIndex] + // Below needed to change zIndex of focussed point + } + + const getDataPointByX = (x) => { + const mouseDate = xScale.invert(x) + const bisectDate = bisector((d) => { return new Date(d.dateTime) }).left + const i = bisectDate(lines, mouseDate, 1) // returns the index to the current data item + const d0 = lines[i - 1] + const d1 = lines[i] || lines[i - 1] + // Determine which date value is closest to the mouse + const d = mouseDate - new Date(d0.dateTime) > new Date(d1.dateTime) - mouseDate ? d1 : d0 + dataPoint = d + } + + const setTooltipPosition = (x, y) => { + // Set Background size + const text = tooltip.select('text') + const txtHeight = Math.round(text.node().getBBox().height) + 23 + const pathLength = 140 + const pathCentre = `M${pathLength},${txtHeight}l0,-${txtHeight}l-${pathLength},0l0,${txtHeight}l${pathLength},0Z` + // Set tooltip layout + tooltipText.attr('x', 0).attr('y', 20) + tooltipPath.attr('d', pathCentre) + // Centre tooltip + x -= pathLength / 2 + if (x <= 0) { + // tooltip on the left + x = 0 + } else if (x + pathLength >= (width + margin.right) - 15) { + // tooltip on the right + x = (width + margin.right) - 15 - pathLength + } + // Set background above or below position + const tooltipHeight = tooltipPath.node().getBBox().height + const tooltipMarginTop = 10 + const tooltipMarginBottom = height - (tooltipHeight + 10) + // Tooltip 40 px above cursor + y -= tooltipHeight + 40 + y = y < tooltipMarginTop ? tooltipMarginTop : y > tooltipMarginBottom ? tooltipMarginBottom : y + tooltip.attr('transform', 'translate(' + x.toFixed(0) + ',' + y.toFixed(0) + ')') + tooltip.classed('tooltip--visible', true) + // Update locator + const locatorX = Math.floor(xScale(new Date(dataPoint.dateTime))) + const locatorY = Math.floor(yScale(dataCache.type === 'river' && dataPoint.value < 0 ? 0 : dataPoint.value)) // *DBL + const isForecast = (new Date(dataPoint.dateTime)) > (new Date(dataCache.latestDateTime)) + locator.classed('locator--forecast', isForecast) + locator.attr('transform', 'translate(' + locatorX + ',' + 0 + ')') + locator.select('.locator-point').attr('transform', 'translate(' + 0 + ',' + locatorY + ')') + } + + const showTooltip = (tooltipY = 10) => { + if (!dataPoint) return + // Hide threshold label + thresholdsContainer.select('.threshold--selected .threshold-label').style('visibility', 'hidden') + // Set tooltip text + const value = dataCache.type === 'river' && (Math.round(dataPoint.value * 100) / 100) <= 0 ? '0' : dataPoint.value.toFixed(2) // *DBL below zero addition + tooltipValue.text(`${value}m`) // *DBL below zero addition + tooltipDescription.text(`${timeFormat('%-I:%M%p')(new Date(dataPoint.dateTime)).toLowerCase()}, ${timeFormat('%e %b')(new Date(dataPoint.dateTime))}`) + // Set locator properties + locator.classed('locator--visible', true) + // Update tooltip left/right background + const tooltipX = xScale(new Date(dataPoint.dateTime)) + setTooltipPosition(tooltipX, tooltipY) + } + + const hideTooltip = () => { + tooltip.classed('tooltip--visible', false) + locator.classed('locator--visible', false) + } + + const showThreshold = (threshold) => { + thresholdsContainer.selectAll('.threshold').classed('threshold--selected', false) + threshold.classed('threshold--selected', true) + // svg.select('.focussed-cell').remove() + } + + const hideThreshold = () => { + thresholdsContainer.selectAll('.threshold').classed('threshold--selected', false) + } + + const addThreshold = (threshold) => { + // Update thresholds array + thresholds = thresholds.filter((x) => { return x.id !== threshold.id }) + thresholds.forEach(x => { x.isSelected = false }) + threshold.isSelected = true + thresholds.push(threshold) + thresholds.sort((a, b) => (a.level <= b.level) ? 1 : -1) + // Re-render + renderChart() + } + + const removeThreshold = (id) => { + // Update thresholds array + thresholds = thresholds.filter(x => x.id.toString() !== id) + if (thresholds.length) thresholds[0].isSelected = true + // Re-render + renderChart() + } + + const setScaleX = () => { + // Set x scale extent + xExtent = extent(dataCache.observed.concat(dataCache.forecast), (d, i) => { return new Date(d.dateTime) }) + // Increase x extent by 5% from now value + let date = new Date() + const percentile = Math.round(Math.abs(xExtent[0] - date) * 0.05) + date = new Date(Number(date) + Number(percentile)) + const xRange = [xExtent[0], xExtent[1]] + xRange.push(date) + xExtent[0] = Math.min.apply(Math, xRange) + xExtent[1] = Math.max.apply(Math, xRange) + // Set x input domain + xScaleInitial = scaleTime().domain(xExtent) + xScaleInitial.range([0, width]) + xScale = scaleTime().domain(xExtent) + } + + const setScaleY = () => { + // Extend or reduce y extent + const maxThreshold = Math.max.apply(Math, thresholds.map((x) => { return x.level })) + const minThreshold = Math.min.apply(Math, thresholds.map((x) => { return x.level })) + const maxData = Math.max(maxThreshold, yExtentDataMax) + const minData = Math.min(minThreshold, yExtentDataMin) + // Add 1/3rd or range above and below, capped at zero for non-negative ranges + let range = maxData - minData + range = range < 1 ? 1 : range + const yRangeUpperBuffered = (maxData + (range / 3)) + const yRangeLowerBuffered = (minData - (range / 3)) + yExtent[1] = yExtentDataMax <= yRangeUpperBuffered ? yRangeUpperBuffered : yExtentDataMax + yExtent[0] = dataCache.type === 'river' ? (yRangeLowerBuffered < 0 ? 0 : yRangeLowerBuffered) : yRangeLowerBuffered + // Set min y axis to 1 metre + yExtent[1] = yExtent[1] < 1 ? 1 : yExtent[1] + // Update y scale + yScale = scaleLinear().domain(yExtent).nice(5) + yScale.range([height, 0]) + // Update y axis + yAxis = axisLeft() + yAxis.ticks(5).tickFormat((d) => { return parseFloat(d).toFixed(2) + 'm' }) + yAxis.scale(yScale) + } + + const getDataPage = (start, end) => { + const cacheStart = new Date(dataCache.cacheStartDateTime) + const cacheEnd = new Date(dataCache.cacheEndDateTime) + const pageStart = new Date(start) + const pageEnd = new Date(end) + + // If page dates are outside cache range then load another data cache + if (pageStart.getTime() < cacheStart.getTime() || pageEnd.getTime() > cacheEnd.getTime()) { + // Rebuild the cache when we have more data + // Set cache start and end + // Set page start and end + // Load new data and reinitialise the chart + // New XMLHttp request + return + } + + // To follow + // Determin which resolution and range to display + // Using raw data for now + + // Setup array to combine observed and forecast points and identify startPoint for locator + if (dataCache.observed.length) { + // Add isSignificant property to points + // Simply function could be improved using dynamic tolerance to better place key points + dataCache.observed = simplify(dataCache.observed, dataCache.type === 'tide' ? 10000000 : 1000000) + const errorFilter = l => !l.err + const errorAndNegativeFilter = l => errorFilter(l) // && l.value >= 0 *DBL below zero addition + const filterNegativeValues = ['groundwater', 'tide'].includes(dataCache.type) ? errorFilter : errorAndNegativeFilter + lines = dataCache.observed.filter(filterNegativeValues).map(l => ({ ...l, type: 'observed' })).reverse() + dataPoint = lines[lines.length - 1] || null + } + if (dataCache.forecast.length) { + // Add isSignificant property to points + dataCache.forecast = simplify(dataCache.forecast, dataCache.type === 'tide' ? 10000000 : 1000000) + // Set 1st forecast isSignificant to false if it is the same time and value as the latest observed + const latestTime = (new Date(dataCache.observed[0].dateTime).getTime()) + const forecastStartTime = (new Date(dataCache.forecast[0].dateTime).getTime()) + const latestValue = dataCache.observed[0].value + const forecastStartValue = dataCache.forecast[0].value + const isSame = latestTime === forecastStartTime && latestValue === forecastStartValue + dataCache.forecast[0].isSignificant = !isSame + // Merge points + lines = lines.concat(dataCache.forecast.map(l => ({ ...l, type: 'forecast' }))) + } + + // Get reference to oberved and forecast sections + observedPoints = lines.filter(l => l.type === 'observed') + forecastPoints = lines.filter(l => l.type === 'forecast') + + // Create area generator + area = d3Area().curve(curveMonotoneX) + .x(d => { return xScale(new Date(d.dateTime)) }) + .y0(d => { return height }) + .y1(d => { return yScale(dataCache.type === 'river' && d.value < 0 ? 0 : d.value) }) // *DBL below zero addition + + // Create line generator + line = d3Line().curve(curveMonotoneX) + .x((d) => { return xScale(new Date(d.dateTime)) }) + .y((d) => { return yScale(dataCache.type === 'river' && d.value < 0 ? 0 : d.value) }) // *DBL below zero addition + + // Note: xExtent uses observed and forecast data rather than lines for the scenario where river levels + // start or end as -ve since we still need to determine the datetime span of the graph even if the + // values are excluded from plotting by virtue of being -ve + + // Set reference to yExtent before any thresholds are added + yExtent = extent(lines, (d, i) => { return d.value }) + yExtentDataMin = yExtent[0] + yExtentDataMax = yExtent[1] + } + + const formatLabelsX = (d, i, nodes) => { + // Format X Axis labels + const element = select(nodes[i]) + const formattedTime = timeFormat('%-I%p')(new Date(d)).toLocaleLowerCase() + const formattedDate = timeFormat('%-e %b')(new Date(d)) + element.append('tspan').text(formattedTime) + element.append('tspan').attr('x', 0).attr('dy', '15').text(formattedDate) + } + + const initChart = () => { + // Get page data + getDataPage(pageStart, pageEnd) + // Render chart + renderChart() + } + + // + // Setup + // + + const defaults = { + btnAddThresholdClass: 'defra-button-text-s' + } + options = Object.assign({}, defaults, options) + + const container = document.getElementById(containerId) + + // Description + const description = document.createElement('span') + description.className = 'govuk-visually-hidden' + description.setAttribute('aria-live', 'polite') + description.setAttribute('id', 'line-chart-description') + container.appendChild(description) + + // Create chart container elements + const svg = select(`#${containerId}`).append('svg') + .attr('id', `${containerId}-visualisation`) + .attr('aria-label', 'Line chart') + .attr('aria-describedby', 'line-chart-description') + .attr('focusable', 'false') + + // Clip path to visually hide text + // const clipText = svg.append('defs').append('clipPath').attr('id', 'clip-text').append('rect').attr('x', -5).attr('y', 0) + + // Add grid containers + svg.append('g').attr('class', 'y grid').attr('aria-hidden', true) + svg.append('g').attr('class', 'x grid').attr('aria-hidden', true) + svg.append('g').attr('class', 'x axis').attr('aria-hidden', true) + svg.append('g').attr('class', 'y axis').attr('aria-hidden', true).style('text-anchor', 'start') + + // Add containers for observed and forecast lines + const inner = svg.append('g').attr('class', 'inner').attr('aria-hidden', true) // .attr('clip-path', 'url(#clip-text)') + inner.append('g').attr('class', 'observed observed-focus') + inner.append('g').attr('class', 'forecast') + const observedArea = inner.select('.observed').append('path').attr('class', 'observed-area') + const observedLine = inner.select('.observed').append('path').attr('class', 'observed-line') + const forecastArea = inner.select('.forecast').append('path').attr('class', 'forecast-area') + const forecastLine = inner.select('.forecast').append('path').attr('class', 'forecast-line') + + // Add timeline + const timeLine = svg.append('line').attr('class', 'time-line').attr('aria-hidden', true) + const timeLabel = svg.append('text').attr('class', 'time-now-text').attr('aria-hidden', true) + timeLabel.append('tspan').attr('class', 'time-now-text__time') + timeLabel.append('tspan').attr('text-anchor', 'middle').attr('class', 'time-now-text__date').attr('x', 0).attr('dy', '15') + + // Add locator + const locator = inner.append('g').attr('class', 'locator') + locator.append('line').attr('class', 'locator-line') + locator.append('circle').attr('r', 4.5).attr('class', 'locator-point') + + // Add thresholds and significant containers + const thresholdsContainer = svg.append('g').attr('class', 'thresholds') + const significantContainer = svg.append('g').attr('class', 'significant').attr('role', 'grid').append('g').attr('role', 'row') // .attr('clip-path', 'url(#clip-text)') + + // Add tooltip container + const tooltip = svg.append('g').attr('class', 'tooltip').attr('aria-hidden', true) + const tooltipPath = tooltip.append('path').attr('class', 'tooltip-bg') + const tooltipText = tooltip.append('text').attr('class', 'tooltip-text') + const tooltipValue = tooltipText.append('tspan').attr('class', 'tooltip-text__strong').attr('x', 12).attr('dy', '0.5em') + const tooltipDescription = tooltipText.append('tspan').attr('class', 'tooltip-text').attr('x', 12).attr('dy', '1.4em') + + // Add optional 'Add threshold' buttons + document.querySelectorAll('[data-threshold-add]').forEach(container => { + const button = document.createElement('button') + button.className = options.btnAddThresholdClass + button.innerHTML = `Show ${container.getAttribute('data-level')}m threshold on chart (Visual only)` + button.setAttribute('aria-controls', `${containerId}-visualisation`) + button.setAttribute('data-id', container.getAttribute('data-id')) + button.setAttribute('data-threshold-add', '') + button.setAttribute('data-level', container.getAttribute('data-level')) + button.setAttribute('data-name', container.getAttribute('data-name')) + container.parentElement.replaceChild(button, container) + }) + + // Define globals + let isMobile, interfaceType, nextFocusElement + let dataPoint + let width, height, margin, xScaleInitial, xScale, yScale, xExtent, yAxis, yExtent, yExtentDataMin, yExtentDataMax + let lines, area, line, observedPoints, forecastPoints, significantPoints + let thresholds = [] + + // Create a mobile width media query + const mobileMediaQuery = window.matchMedia('(max-width: 640px)') + isMobile = mobileMediaQuery.matches + + // Default page size is 5 days + let pageStart = new Date() + let pageEnd = new Date() + pageStart.setHours(pageStart.getHours() - (5 * 24)) + pageStart = pageStart.toISOString().replace(/.\d+Z$/g, 'Z') + pageEnd = pageEnd.toISOString().replace(/.\d+Z$/g, 'Z') + + // XMLHttpRequest to get data if hasn't already been passed through + const dataCache = data + initChart() + + // + // Public methods + // + + this.removeThreshold = (id) => { + removeThreshold(id) + } + + this.addThreshold = (threshold) => { + addThreshold(threshold) + } + + this.chart = container + + // + // Events + // + + // addListener deprectaed but required or ie11 and Safari < 14 + mobileMediaQuery[mobileMediaQuery.addEventListener ? 'addEventListener' : 'addListener']('change', (e) => { + isMobile = e.matches + hideTooltip() + renderChart() + }) + + window.addEventListener('resize', () => { + // touchmove/scroll on mobile devices also fires resize + if (interfaceType === 'touch') return + hideTooltip() + renderChart() + }) + + document.addEventListener('click', (e) => { + // Hide points and focussed cell + significantContainer.node().parentNode.classList.remove('significant--visible') + // svg.select('.focussed-cell').remove() + // Add threshold button + if (!e.target.hasAttribute('data-threshold-add')) return + const button = e.target + addThreshold({ + id: button.getAttribute('data-id'), + level: Number(button.getAttribute('data-level')), + name: button.getAttribute('data-name') + }) + // Scroll upto chart from add threshold button + const y = container.getBoundingClientRect().top + window.pageYOffset + window.scrollTo(0, y) + }) + + document.addEventListener('keydown', (e) => { + interfaceType = 'keyboard' + const gridKeys = ['ArrowUp', 'ArrowDown', 'ArrowRight', 'ArrowLeft', 'Home', 'End'] + if (!(e.target.classList.contains('point') && gridKeys.includes(e.key))) return // DB: Needs to be more specific + e.preventDefault() + const keys = ['ArrowRight', 'ArrowLeft', 'Home', 'End'] + if (!keys.includes(e.key)) return + swapCell(e) + showTooltip(10) + }) + + document.addEventListener('keyup', (e) => { + // Show points if within line chart + const significantParent = significantContainer.node().parentNode + significantParent.classList.toggle('significant--visible', !!e.target.closest('.defra-line-chart')) + // Threshold + if (e.target.closest('[data-threshold]')) { + const threshold = e.target.closest('[data-threshold]') + // Remove threshold + if (['Enter', 'Space'].includes(e.key)) { + const id = threshold.getAttribute('data-id') + removeThreshold(id) + const index = thresholds.findIndex(x => x.id.toString() === id) + const nextId = index < thresholds.length - 1 ? thresholds[index + 1].id : null + if (nextFocusElement) { + nextFocusElement.focus() + } else if (nextId) { + const nextThreshold = document.querySelector(`[data-threshold][data-id="${nextId}"]`) + showThreshold(select(nextThreshold)) + nextFocusElement = nextThreshold.querySelector('a') + } else { + nextFocusElement = document.querySelector('[data-point][tabindex="0"]') + } + } else { + // Select threshold + hideTooltip() + showThreshold(select(threshold)) + nextFocusElement = threshold.querySelector('a') + } + nextFocusElement.focus() + nextFocusElement = null + return + } + // Select point + if (e.target.hasAttribute('data-point')) { + if (e.key === 'Tab') { + swapCell(e) + hideThreshold() + showTooltip(10) + } + return + } + // Add threshold button + if (e.target.hasAttribute('data-threshold-add') && ['Enter', 'Space'].includes(e.key)) { + nextFocusElement = e.target + const thresholdId = e.target.getAttribute('data-id') + const thresholdRemoveButton = document.querySelector(`.threshold[data-id="${thresholdId}"] a`) + thresholdRemoveButton.focus() + return + } + // Outside chart + hideTooltip() + const threshold = thresholds.find(x => x.isSelected) + if (threshold) { + // Reinstate default threshold? + showThreshold(thresholdsContainer.select(`[data-id="${threshold.id}"]`)) + } + // Hide significant points + significantContainer.node().parentNode.classList.remove('significant--visible') + // Remove focussed significant point + }, true) + + container.addEventListener('mouseleave', (e) => { + hideTooltip() + const threshold = thresholds.find(x => x.isSelected) + if (threshold) { + showThreshold(thresholdsContainer.select(`[data-id="${threshold.id}"`)) + } + }) + + svg.on('click', (e) => { + if (e.target.closest('.threshold')) return + getDataPointByX(pointer(e)[0]) + hideThreshold() + showTooltip(pointer(e)[1]) + }) + + let lastClientX, lastClientY + svg.on('mousemove', (e) => { + // Safari bug where modifier keys trigger mousemove + if (lastClientX === e.clientX && lastClientY === e.clientY) return + lastClientX = e.clientX + lastClientY = e.clientY + if (!xScale || e.target.closest('.threshold')) return + if (interfaceType === 'touch') { + interfaceType = 'mouse' + return + } + interfaceType = 'mouse' + getDataPointByX(pointer(e)[0]) + hideThreshold() + showTooltip(pointer(e)[1]) + }) + + svg.on('touchstart', (e) => { + interfaceType = 'touch' + }) + + svg.on('touchmove', (e) => { + if (!xScale || e.target.closest('.threshold')) return + const touchEvent = e.targetTouches[0] + const elementOffsetX = svg.node().getBoundingClientRect().left + getDataPointByX(pointer(touchEvent)[0] - elementOffsetX) + hideThreshold() + showTooltip(10) + }) + + svg.on('touchend', (e) => { + interfaceType = null + }) + + thresholdsContainer.on('click', (e) => { + e.stopPropagation() + const thresholdContainer = e.target.closest('.threshold') + if (e.target.closest('.threshold__remove')) { + removeThreshold(thresholdContainer.getAttribute('data-id')) + } else if (thresholdContainer) { + hideTooltip() + showThreshold(select(thresholdContainer)) + } + }) + + thresholdsContainer.on('mouseover', (e) => { + const thresholdContainer = e.target.closest('.threshold') + if (thresholdContainer) { + hideTooltip() + showThreshold(select(thresholdContainer)) + } + }) +} + +window.flood.charts = { + createLineChart: (containerId, stationId, data) => { + return new LineChart(containerId, stationId, data) + } +} diff --git a/server/src/js/components/map/button.js b/server/src/js/components/map/button.js new file mode 100644 index 000000000..ba277376a --- /dev/null +++ b/server/src/js/components/map/button.js @@ -0,0 +1,21 @@ + +export function createMapButton (buttonContainer, uri, options) { + const mapId = buttonContainer.id + + // Create map button + const button = document.createElement('a') + button.setAttribute('href', uri) + if (options.btnType !== 'link') { + button.setAttribute('role', 'button') + button.setAttribute('data-module', 'govuk-button') + } + button.id = mapId + '-btn' + button.innerHTML = `${options.btnText || 'View map'}(Visual only)` + button.className = options.btnClasses || (options.btnType === 'link' ? 'defra-link-icon-s' : 'defra-button-secondary') + + if (options.data && options.data.button) { + button.setAttribute('data-journey-click', options.data.button) + } + + return button +} diff --git a/server/src/js/components/map/live.js b/server/src/js/components/map/live.js index c4946a123..882485072 100644 --- a/server/src/js/components/map/live.js +++ b/server/src/js/components/map/live.js @@ -13,6 +13,7 @@ import { Point, MultiPolygon } from 'ol/geom' import { buffer, containsExtent, getCenter } from 'ol/extent' import { Vector as VectorSource } from 'ol/source' import moment from 'moment-timezone' +import { createMapButton } from './button' const { addOrUpdateParameter, getParameterByName, forEach } = window.flood.utils const maps = window.flood.maps @@ -690,18 +691,7 @@ maps.createLiveMap = (mapId, options = {}) => { // Create map button const btnContainer = document.getElementById(mapId) - const button = document.createElement('a') - button.setAttribute('href', uri) - if (options.btnType !== 'link') { - button.setAttribute('role', 'button') - button.setAttribute('data-module', 'govuk-button') - } - button.id = mapId + '-btn' - button.innerHTML = `${options.btnText || 'View map'}(Visual only)` - button.className = options.btnClasses || (options.btnType === 'link' ? 'defra-link-icon-s' : 'defra-button-secondary defra-button-secondary--icon') - if (options.data && options.data.button) { - button.setAttribute('data-journey-click', options.data.button) - } + const button = createMapButton(btnContainer, uri, options) btnContainer.parentNode.replaceChild(button, btnContainer) // Detect keyboard interaction diff --git a/server/src/js/components/map/outlook.js b/server/src/js/components/map/outlook.js index 5033c0ba0..0b6c2116c 100644 --- a/server/src/js/components/map/outlook.js +++ b/server/src/js/components/map/outlook.js @@ -7,6 +7,7 @@ import { Point } from 'ol/geom' import { getCenter } from 'ol/extent' import { unByKey } from 'ol/Observable' import { Control } from 'ol/control' +import { createMapButton } from './button' const { addOrUpdateParameter, getParameterByName, forEach } = window.flood.utils const maps = window.flood.maps @@ -413,15 +414,12 @@ maps.createOutlookMap = (mapId, options = {}) => { window.history.replaceState(data, title, uri) } - // Create map button + // Build default uri + let uri = window.location.href + uri = addOrUpdateParameter(uri, 'v', mapId) + const btnContainer = document.getElementById(mapId) - const button = document.createElement('button') - button.id = mapId + '-btn' - button.innerHTML = `${options.btnText || 'View map'}(Visual only)` - button.className = options.btnClasses || 'defra-button-secondary defra-button-secondary--icon' - if (options.data && options.data.button) { - button.setAttribute('data-journey-click', options.data.button) - } + const button = createMapButton(btnContainer, uri, options) btnContainer.parentNode.replaceChild(button, btnContainer) // Detect keyboard interaction diff --git a/server/src/js/components/toggle-list-display.js b/server/src/js/components/toggle-list-display.js index 030eb8d1e..8ee99bcb8 100644 --- a/server/src/js/components/toggle-list-display.js +++ b/server/src/js/components/toggle-list-display.js @@ -8,7 +8,7 @@ const ToggleListDisplay = (container, options) => { const list = document.querySelector('.defra-flood-impact-list') const items = list.querySelectorAll(`[data-toggle-list-display-item="${options.type}"]`) const button = document.createElement('button') - button.className = 'defra-button-text govuk-!-margin-bottom-2' + button.className = 'defra-button-text-s govuk-!-margin-bottom-2' button.setAttribute('aria-controls', list.id) container.appendChild(button) diff --git a/server/src/js/core.js b/server/src/js/core.mjs similarity index 79% rename from server/src/js/core.js rename to server/src/js/core.mjs index 3c0b279c2..8bf5707b0 100755 --- a/server/src/js/core.js +++ b/server/src/js/core.mjs @@ -1,8 +1,50 @@ 'use strict' // "flood" represents the global namespace for // client-side javascript across all our pages -import 'core-js/modules/es6.promise' -import 'core-js/modules/es6.array.iterator' +import 'core-js/modules/es6.promise.js' +import 'core-js/modules/es6.array.iterator.js' + +// Math.log2 Polyfil +if (!Math.log2) { + Math.log2 = (x) => { + console.log('Using Math.log2') + return Math.log(x) * Math.LOG2E + } +} + +// Element closest Polyfil +if (!window.Element.prototype.matches) { + window.Element.prototype.matches = window.Element.prototype.msMatchesSelector || window.Element.prototype.webkitMatchesSelector +} +if (!window.Element.prototype.closest) { + window.Element.prototype.closest = (s) => { + let el = this + do { + if (window.Element.prototype.matches.call(el, s)) return el + el = el.parentElement || el.parentNode + } while (el !== null && el.nodeType === 1) + return null + } +} + +// Simplification algorythom +const douglasPeucker = (points, tolerance) => { + const last = points.length - 1 + const p1 = points[0] + const p2 = points[last] + const x21 = p2.timestamp - p1.timestamp + const y21 = p2.value - p1.value + const [dMax, x] = points.slice(1, last) + .map(p => Math.abs(y21 * p.timestamp - x21 * p.value + p2.timestamp * p1.value - p2.value * p1.timestamp)) + .reduce((p, c, i) => { + const v = Math.max(p[0], c) + return [v, v === p[0] ? p[1] : i + 1] + }, [-1, 0]) + if (dMax > tolerance) { + return [...douglasPeucker(points.slice(0, x + 1), tolerance), ...douglasPeucker(points.slice(x), tolerance).slice(1)] + } + return [points[0], points[last]] +} window.flood = { utils: { @@ -103,8 +145,35 @@ window.flood = { } ` document.head.appendChild(script) + }, + // Takes a valuesobject and concatentates items using commas and 'and'. + getSummaryList: (values) => { + const lines = [] + let summary = '' + values.forEach((v, i) => { + if (v.count) { + lines.push(`${v.count} ${v.text}${v.count !== 1 ? 's' : ''}`) + } + }) + lines.forEach((l, i) => { + summary += l + (i + 1 === lines.length - 1 ? ' and ' : i + 1 < lines.length ? ', ' : '') + }) + return summary + }, + // Takes a points collection and adds an isSignificant property to key points + simplify: (points, tolerance) => { + points = points.map(obj => ({ ...obj, timestamp: parseInt((new Date(obj.dateTime)).getTime()) })) + const significant = douglasPeucker(points, tolerance) + const result = points.map((obj, i) => ({ + dateTime: obj.dateTime, + value: obj.value, + type: obj.type, + isSignificant: !!significant.find(x => x.timestamp === obj.timestamp) + })) + return result } } + } const elem = document.getElementById('cookie-banner') diff --git a/server/src/js/pages/rainfall.js b/server/src/js/pages/rainfall.js index 5a721536e..1e4d097d6 100644 --- a/server/src/js/pages/rainfall.js +++ b/server/src/js/pages/rainfall.js @@ -1,6 +1,6 @@ 'use strict' import 'elm-pep' -import '../components/bar-chart' +import '../components/bar-chart/index.mjs' import '../components/nunjucks' import '../components/map/maps' import '../components/map/styles' @@ -12,7 +12,7 @@ import '../components/toggletip' // Create LiveMap if (document.getElementById('map')) { window.flood.maps.createLiveMap('map', { - btnText: 'View map', + btnText: 'Map', btnClasses: 'defra-link-icon-s', btnType: 'link', data: { diff --git a/server/src/js/pages/station.js b/server/src/js/pages/station.js index 64a2f574f..a933605c5 100644 --- a/server/src/js/pages/station.js +++ b/server/src/js/pages/station.js @@ -1,6 +1,6 @@ 'use strict' import 'elm-pep' -import '../components/charts' +import '../components/line-chart' import '../components/nunjucks' import '../components/map/maps' import '../components/map/styles' @@ -12,7 +12,7 @@ import '../components/toggletip' // Create LiveMap window.flood.maps.createLiveMap('map', { - btnText: 'View map', + btnText: 'Map', btnClasses: 'defra-link-icon-s', layers: 'mv,ri,ti,gr,rf', data: { @@ -20,70 +20,21 @@ window.flood.maps.createLiveMap('map', { checkBox: 'Station:Map interaction:Map - Layer interaction', aerial: 'Station:Map interaction:View-satelite-basemap' }, - centre: JSON.parse(window.flood.model.station.coordinates).coordinates, - selectedId: 'stations.' + window.flood.model.station.id, + centre: window.flood.model.centre.split(','), + selectedId: 'stations.' + window.flood.model.id, zoom: 14 }) -const chart = document.querySelector('.defra-line-chart') -if (chart) { - // If javascript is enabled make content visible to all but assitive technology - // var figure = chart.parentNode - chart.setAttribute('aria-hidden', true) - chart.removeAttribute('hidden') - // Create line chart instance - const lineChart = window.flood.charts.createLineChart('line-chart', { - now: new Date(), - observed: window.flood.model.telemetry, - forecast: window.flood.model.ffoi && !window.flood.model.forecastOutOfDate ? window.flood.model.ffoi.processedValues : [], - plotNegativeValues: window.flood.model.station.plotNegativeValues - }) - if (Object.keys(lineChart).length) { - if (window.flood.utils.getParameterByName('t')) { - // Find threshold in model - const thresholdId = window.flood.utils.getParameterByName('t') - let matchedThresholds = [] - window.flood.model.thresholds.forEach(function (threshold) { - matchedThresholds = matchedThresholds.concat(threshold.values.filter(function (value) { - return (value.id.toString() === thresholdId) - })) - }) - const threshold = matchedThresholds[0] - lineChart.addThreshold({ - id: threshold.id, - level: threshold.value, - name: threshold.shortname - }) - } else { - const typical = document.querySelector('.defra-flood-impact-list__value[data-id="pc5"]:last-child') - if (typical) { - lineChart.addThreshold({ - id: typical.getAttribute('data-id'), - level: Number(typical.getAttribute('data-level')), - name: typical.getAttribute('data-name') - }) - } - } - // Add threshold buttons - Array.from(document.querySelectorAll('.defra-flood-impact-list__value')).forEach(value => { - const button = document.createElement('button') - button.innerHTML = 'Show on chart (Visual only)' - button.setAttribute('data-journey-click', 'Station:Chart interaction:Station - show on chart') - button.className = 'defra-button-text-s' - button.addEventListener('click', function (e) { - lineChart.addThreshold({ - id: value.getAttribute('data-id'), - level: Number(value.getAttribute('data-level')), - name: value.getAttribute('data-name') - }) - // Scroll viewport to chart - const offsetTop = chart.getBoundingClientRect().top + window.pageYOffset - window.scrollTo(0, offsetTop) - }) - const action = value.querySelector('.defra-flood-impact-list__action') - if (action) { - action.appendChild(button) - } +// Line chart +if (document.getElementById('line-chart')) { + const lineChart = window.flood.charts.createLineChart('line-chart', window.flood.model.id, window.flood.model.telemetry) + const thresholdId = 'threshold-pc5' + const threshold = document.querySelector(`[data-id="${thresholdId}"]`) + if (threshold) { + lineChart.addThreshold({ + id: thresholdId, + name: threshold.getAttribute('data-name'), + level: Number(threshold.getAttribute('data-level')) }) } } diff --git a/server/src/sass/components/_bar-chart.scss b/server/src/sass/components/_bar-chart.scss index dd0dbf034..16969ef5e 100644 --- a/server/src/sass/components/_bar-chart.scss +++ b/server/src/sass/components/_bar-chart.scss @@ -24,19 +24,22 @@ @include mq ($from: desktop) { height:450px; } - .axis.y { + .axis.y { @include govuk-font($size: 14, $tabular: true); } - .axis.x { - @include govuk-font($size: 14); + .axis.x { + @include govuk-font($size: 14); } .axis.y text, .axis.x text { fill: $govuk-secondary-text-colour; + @include high-contrast-mode-only { + fill: currentColor; + } } .axis.y .tick line, .axis.y path, - .grid.y path.domain { + .grid.y path.domain { display: none; } .axis path, @@ -45,6 +48,9 @@ stroke: $govuk-border-colour; stroke-width: 1; shape-rendering: crispEdges; + @include high-contrast-mode-only { + stroke: currentColor; + } } .bar { pointer-events: none; @@ -55,12 +61,21 @@ } .bar__fill { fill: govuk-colour('blue'); + @include high-contrast-mode-only { + fill: currentColor; + } } .bar--selected .bar__fill { fill: govuk-colour('dark-blue'); + @include high-contrast-mode-only { + fill: currentColor; + } } .bar--incomplete .bar__fill { fill: govuk-colour('mid-grey'); + @include high-contrast-mode-only { + fill: currentColor; + } } .grid { pointer-events: none; @@ -77,9 +92,15 @@ stroke: govuk-colour('dark-grey'); stroke-dasharray: 3 3; shape-rendering: crispEdges; + @include high-contrast-mode-only { + stroke: currentColor; + } } .bar--selected .latest-line { stroke: govuk-colour('black'); + @include high-contrast-mode-only { + stroke: currentColor; + } } // Locator line .locator { @@ -94,6 +115,9 @@ &__background--visible { visibility: visible; fill: $govuk-focus-colour; + @include high-contrast-mode-only { + fill: currentColor; + } } &__line { visibility: hidden; @@ -113,13 +137,17 @@ visibility: visible; } .tooltip-bg { - fill:white; + fill: window; stroke-width: 1; stroke: $govuk-secondary-text-colour; shape-rendering: geometricPrecision; + @include high-contrast-mode-only { + stroke: currentColor; + } } .tooltip-text { @include govuk-font($size: 16); + fill: currentColor; } .tooltip-text__strong { @include govuk-font($size: 19, $weight:bold); diff --git a/server/src/sass/components/_chart-controls.scss b/server/src/sass/components/_chart-controls.scss index 215553514..063d6a447 100644 --- a/server/src/sass/components/_chart-controls.scss +++ b/server/src/sass/components/_chart-controls.scss @@ -1,91 +1,66 @@ .defra-chart-controls { position: relative; z-index: 1; -} -.defra-chart-segmented-control { - margin: auto; - &__segment { + overflow: auto; + margin: -3px -3px 0 -3px; + padding: 3px 3px 0 3px; + + &__group { + margin: auto; display: inline-block; - position: relative; - text-align: center; - margin-right: 15px; + padding-bottom: 4px; } - input { - position: absolute; - opacity: 0; - margin: 0; - top: 0; - right: 0; - bottom: 0; - left: 0; - width: 100%; - height: 100%; - cursor: pointer; + + &__group--pagination { + float: right; + margin-right: 10px; } - label { - cursor: pointer; - display: block; - @include govuk-font($size: 16); - padding: 5px 0; - border-bottom: 5px solid transparent; + + &__button { + @extend .defra-button-text-s; + touch-action: manipulation; + padding: 7.5px; + display: inline-block; position: relative; - outline: 3px solid transparent; + text-align: center; + text-decoration: none; + margin-bottom: -4px; + cursor: pointer; color: govuk-colour('blue'); } - input:hover + label { + + &__button:hover { color: $govuk-link-hover-colour; } - input:checked + label { - border-bottom: 5px solid govuk-colour('blue'); + &__button--selected { + border-bottom: 4px solid govuk-colour('blue'); } - input:focus + label { - border-bottom: 5px solid govuk-colour('black'); + &__button:focus { + border-bottom: 4px solid govuk-colour('black'); background-color: $govuk-focus-colour; color: govuk-colour('black'); - } -} -.defra-chart-pagination { - display: block; - padding-bottom: 15px; - &__button { - border: 0px; - background-color: white; - margin: 0px; - padding: 0px; - width: auto; - outline: 3px solid transparent; - color: $govuk-link-colour; - cursor: pointer; - touch-action: manipulation; - @include govuk-font($size: 16); - .defra-chart-pagination__text { - pointer-events: none; - } - &[aria-disabled="true"], - &[aria-disabled="true"]:after, - &[aria-disabled="true"]:before { - color: govuk-colour('mid-grey'); + @include high-contrast-mode-only { + outline: 3px solid transparent; + margin-bottom: 1px; + border: none; } - &--back { - margin-right: 10px; - } - @include mq ($from: tablet) { - margin-top: 5px; + } + &__button[aria-disabled="true"]:focus { + @include high-contrast-mode-only { + background-color: transparent; + box-shadow: none; } } - &__button[aria-disabled="true"] { + &__button[aria-disabled="true"], + &__button[aria-disabled="true"]:after, + &__button[aria-disabled="true"]:before { + color: govuk-colour('mid-grey'); cursor: not-allowed; + forced-color-adjust: none; } - &__button:hover:not([aria-disabled="true"]) { - color: $govuk-link-hover-colour; - &:after, &:before { - color: $govuk-link-hover-colour; - } - } - &__button--forward:after, - &__button--back:before { + &__button:after, + &__button:before { display: inline-block; - content: ''; border-width: 0 2px 2px 0; width: 8px; height: 8px; @@ -94,25 +69,16 @@ margin-right: 2px; margin-left: 2px; } - &__button--forward:after { + &__button[data-direction="forward"]:after { + content: ''; -webkit-transform: rotate(-45deg); -ms-transform: rotate(-45deg); transform: rotate(-45deg); } - &__button--back:before { + &__button[data-direction="back"]:before { + content: ''; -webkit-transform: rotate(135deg); -ms-transform: rotate(135deg); transform: rotate(135deg); } - &__button:focus:not([aria-disabled="true"]) { - color: $govuk-focus-text-colour; - background-color: $govuk-focus-colour; - box-shadow: 0 -2px $govuk-focus-colour, 0 4px govuk-colour('black'); - &:after, &:before { - color: $govuk-focus-text-colour; - } - } - &__button:focus[aria-disabled="true"] { - box-shadow: 0px 0px 0px 2px govuk-colour('black'), 0px 0px 0px 5px $govuk-focus-colour; - } } diff --git a/server/src/sass/components/_flood-impact-list.scss b/server/src/sass/components/_flood-impact-list.scss index 5b1af7657..299033f5f 100644 --- a/server/src/sass/components/_flood-impact-list.scss +++ b/server/src/sass/components/_flood-impact-list.scss @@ -1,69 +1,75 @@ .defra-flood-impact-list { @include govuk-font($size: 19); - border-bottom:1px solid $govuk-border-colour; + margin-top: 0px; + border-bottom: 1px solid $govuk-border-colour; &__row { - position:relative; - margin-left:3.65em; - overflow:visible; + position: relative; + margin-left: 3.65em; + overflow: visible; } &__row:before { - position:absolute; - content:''; - width:3px; - top:11px; - bottom:-20px; - background-color: govuk-colour('mid-grey'); - margin-left:-1.5px; - left:8%; + position: absolute; + content: ''; + width: 3px; + top: 11px; + bottom: -20px; + color: govuk-colour('mid-grey'); + border-left: 3px solid currentcolor; + margin-left: -1px; + left: 8%; @include mq ($from: tablet) { - left:4%; + left: 4%; } } &__key { @include govuk-font($size: false, $tabular: true, $weight: bold); - position:absolute; - left:-3.65em; - width:3.65em; - top:0px; - text-align:right; + position: absolute; + left: -3.65em; + width: 3.65em; + top: 0; + text-align: right; } &__value { - position:relative; - margin:0; + position: relative; + margin: 0; padding-left: 16%; - padding-bottom:10px; + padding-bottom: 10px; @include mq ($from: tablet) { - padding-left:8%; - padding-bottom:15px; + padding-left: 8%; + padding-bottom: 15px; } } + &__value--latest { + font-weight: bold; + } &__value:nth-child(2):before { position: absolute; content:''; - width:14px; - height:3px; - border-top:1px solid white; - border-bottom:1px solid white; - background-color:govuk-colour('mid-grey'); - margin-left:-6.5px; - top:8px; - left:8%; + width: 13px; + height: 3px; + color: govuk-colour('mid-grey'); + border-top: 3px solid currentcolor; + border-bottom: 0; + background-color: transparent; + margin-left: -6px; + top: 8px; + left: 8%; @include mq ($from: tablet) { - top:11px; - left:4%; + top: 11px; + left: 4%; } } &__row:last-child { &:before { - bottom:0px; + bottom: 0; } .defra-flood-impact-list__value:last-child { padding-bottom:0; } .defra-flood-impact-list__value:last-child { - padding-bottom:0; + padding-bottom: 0; .defra-flood-impact-list__container { - border-bottom:0px; + border-bottom: 0; } } } @@ -71,18 +77,17 @@ padding-bottom:10px; border-bottom:1px solid $govuk-border-colour; @include mq ($from: tablet) { - padding-right:28%; - padding-bottom:15px; + padding-right: 28%; + padding-bottom: 15px; } } - &__action { - display:block; - margin-top:5px; + &__container button { + margin-top: 5px; @include mq ($from: tablet) { position:absolute; - right:0; - top:0px; - margin-top:0px; + right: 0; + top: 0; + margin-top: 0; } } .defra-button-text-s { @@ -92,25 +97,28 @@ display: none; } &__row--exceeded { - &:before { - background-color: govuk-colour('blue'); - } - .defra-flood-impact-list__value:nth-child(2):before { - background-color: govuk-colour('blue'); - border:0px; - } + &:before { + margin-left: -2px; + color: govuk-colour('blue'); + border-left: 5px solid currentcolor; + } + .defra-flood-impact-list__value:nth-child(2):before { + margin-left: -6px; + color: govuk-colour('blue'); + border-top: 5px solid currentcolor; + } } &__row--current { - &:before { - background-color: govuk-colour('blue'); - } - .defra-flood-impact-list__value { - font-weight: bold; - } - .defra-flood-impact-list__value:before { - background-color: govuk-colour('blue'); - border:0px; - } + &:before { + margin-left: -2px; + color: govuk-colour('blue'); + border-left: 5px solid currentcolor; + } + .defra-flood-impact-list__value:before { + margin-left: -6px; + color: govuk-colour('blue'); + border-top: 5px solid currentcolor; + } } &__alert { z-index:1; @@ -141,4 +149,4 @@ @extend .defra-flood-impact-list__alert; background-image: svg-url(''); } -} \ No newline at end of file + } diff --git a/server/src/sass/components/_flood-levels-table.scss b/server/src/sass/components/_flood-levels-table.scss index 0cdbdaf30..850070711 100644 --- a/server/src/sass/components/_flood-levels-table.scss +++ b/server/src/sass/components/_flood-levels-table.scss @@ -179,6 +179,9 @@ left: 4px; top: 4px; color: govuk-colour('black'); + @include high-contrast-mode-only { + color: currentColor; + } @include mq ($from: tablet) { left:6px; top: 6px; diff --git a/server/src/sass/components/_flood-nav.scss b/server/src/sass/components/_flood-nav.scss index fd225882f..85c4bba17 100644 --- a/server/src/sass/components/_flood-nav.scss +++ b/server/src/sass/components/_flood-nav.scss @@ -19,27 +19,5 @@ &__link:focus { color: $govuk-text-colour; } - &__link--upstream, - &__link--downstream { - position: relative; - padding-left: 18px; - } - &__link--upstream:before, - &__link--downstream:before { - content: ''; - position: absolute; - top: 0px; - left: 0px; - background-image: svg-url(''); - background-color: transparent; - background-size: 14px 18px; - background-repeat: no-repeat; - background-position: top left; - width: 14px; - height: 18px; - } - &__link--downstream:before { - background-image: svg-url(''); - } } diff --git a/server/src/sass/components/_flood-status.scss b/server/src/sass/components/_flood-status.scss index ba748f0ac..aa09b7d7e 100644 --- a/server/src/sass/components/_flood-status.scss +++ b/server/src/sass/components/_flood-status.scss @@ -1,26 +1,63 @@ +$white: #ffffff; +$red: #e3000f; +$amber: #f18700; + .defra-flood-status { padding-bottom:10px; @include mq ($from: tablet) { padding-bottom:15px; } + svg { + vertical-align: middle; + width: 40px; + height: 40px; + @include mq ($from: tablet) { + width: 50px; + height: 50px; + } + } + &--small svg { + width: 30px; + height: 30px; + @include mq ($from: tablet) { + width: 35px; + height: 35px; + } + } + &--full &-item__container { + @include mq ($from: desktop) { + width: 66.66%; + } + } } .defra-flood-status-item { @extend .govuk-inset-text; - background-color:govuk-colour('light-grey'); - position:relative; - display: table; - width:100%; - box-sizing: border-box; - padding:0px; - margin-top:0px; + position: relative; + padding: 0; + background-color: govuk-colour('light-grey'); + margin-top:0; margin-bottom:10px; + + @include high-contrast-mode-only { + border-top-width: 1px; + border-bottom-width: 1px; + border-right-width: 1px; + border-style: solid; + } + &:last-of-type { margin-bottom:5px; } + &__container { + position:relative; + display: table; + width: 100%; + box-sizing: border-box; + } &__text { display: table-cell; vertical-align: middle; - width:100%; + width: 100%; padding:15px 15px 15px 0px; @include govuk-font($size: 19); strong { @@ -39,72 +76,93 @@ .defra-flood-status-item__icon { display: table-cell; vertical-align: middle; - padding:7.5px; + padding:7.5px 10px; @include mq ($from: tablet) { padding:10px 12.5px; } } - .defra-flood-status-item__icon:after { - display: block; - content:''; - background: transparent url('#{$govuk-images-path}icon-flood-symbols.svg') no-repeat; - background-size: 80px 200px; - width: 40px; - height: 40px; - @include mq ($from: tablet) { - background-size: 100px 250px; - width: 50px; - height: 50px; - } + &:after { + position: absolute; + box-sizing: border-box; + content: ''; + left: 0; + top: 0; + bottom: 0; + right: 0; + border: 1px solid transparent; + pointer-events: none; } } .defra-flood-status-item--severe { - background-color: mix(white, #e3000f, 93%); - border-color:#e3000f; - /* - color:white; - a { - color:white; - } - */ - .defra-flood-status-item__icon:after { - background-position: 100% 0%; - } - /* - &:before { - content:''; - position:absolute; - top:0px; - bottom:0px; - width:1px; - left:0px; - background-color: white; - } - */ + background-color: govuk-tint($red, 93); + border-color:$red; + &:after { + color: govuk-tint($red, 93); + border-color: currentColor; + } } .defra-flood-status-item--warning { - background-color: mix(white, #e3000f, 93%); - border-color:#e3000f; - .defra-flood-status-item__icon:after { - background-position: 100% 25%; + background-color:govuk-tint($red, 93); + border-color:$red; + &:after { + color: govuk-tint($red, 93); + border-color: currentColor; } } .defra-flood-status-item--alert { - background-color: mix(white, #f18700, 90%); - border-color:#f18700; - .defra-flood-status-item__icon:after { - background-position: 100% 50%; + background-color: govuk-tint($amber, 90); + border-color:$amber; + &:after { + color: govuk-tint($amber, 90); + border-color: currentColor; } } -/* -.defra-flood-status-item--removed { - .defra-flood-status-item__icon:after { - background-position: 100% 75%; +.defra-flood-status-item--important { + background-color: govuk-tint(govuk-colour('blue'), 90); + border-color: govuk-colour('blue'); + &:after { + color: govuk-tint(govuk-colour('blue'), 90%); + border-color: currentColor; + } + svg { + vertical-align: middle; + color: white; + width: 20px; + height: 20px; + margin: 0 5px; + @include mq ($from: tablet) { + width: 25px; + height: 25px; + } + > circle { + color: transparent; + fill: govuk-colour('black'); + stroke: currentColor; + stroke-width: 2px; + } + } +} +.defra-flood-status-item--information { + background-color: transparent; + border-color: govuk-colour('mid-grey'); + &:after { + color: govuk-colour('mid-grey'); + border-color: currentColor; + } + svg { + vertical-align: middle; + color: govuk-colour('dark-grey'); + width: 20px; + height: 20px; + margin: 0px 5px; + @include mq ($from: tablet) { + width: 25px; + height: 25px; + } } } -*/ .defra-flood-status__text { @extend .govuk-body; - @include govuk-responsive-margin(4, 'top'); - margin-bottom:0px; -} \ No newline at end of file + @include govuk-responsive-margin(3, 'top'); + margin-bottom: 5px; +} diff --git a/server/src/sass/components/_line-chart.scss b/server/src/sass/components/_line-chart.scss index e4c1e2b54..f27b088dd 100644 --- a/server/src/sass/components/_line-chart.scss +++ b/server/src/sass/components/_line-chart.scss @@ -1,221 +1,303 @@ // Line graph .defra-line-chart { - position:relative; - // border-bottom:1px solid $govuk-border-colour; - // padding-bottom:15px; - margin:0; + color: $govuk-border-colour; + border-bottom: 1px solid currentColor; } -.defra-line-chart-data { - border-top:1px solid $govuk-border-colour; - padding-top:20px; +.js-enabled .defra-line-chart__container { + position: relative; + margin: 0 -15px; + padding-right: 15px; + padding-left: 15px; } -.js-enabled .defra-line-chart-data { - @extend .govuk-visually-hidden; -} -.defra-line-chart__caption { - display:block; - @extend .govuk-body; - margin-bottom:30px; - @include mq ($from: tablet) { - margin-bottom:40px; - } - margin-right:0px; - text-align:left; -} -.defra-line-chart__container { - position:relative; - margin:0; -} -.defra-line-chart svg { - position:relative; - overflow:visible; - height:300px; - width:100%; - @include mq ($from: desktop) { - height:450px; - } - .axis.y { +.defra-line-chart__container svg { + position: relative; + overflow: visible; + height: 300px; + width: 100%; + -webkit-user-select: none; /* Safari */ + -ms-user-select: none; /* IE 10 and IE 11 */ + user-select: none; /* Standard syntax */ + forced-color-adjust: auto; + cursor: default; + @include mq($from: desktop) { + height: 450px; + } + .axis.y { @include govuk-font($size: 14, $tabular: true); } - .axis.x { - @include govuk-font($size: 14); + .axis.x { + @include govuk-font($size: 14); } .axis.y text, .axis.x text { - fill: $govuk-secondary-text-colour; + color: $govuk-secondary-text-colour; + fill: currentColor; } .axis path, .axis line { fill: none; - stroke: $govuk-border-colour; + color: $govuk-border-colour; + stroke: currentColor; stroke-width: 1; shape-rendering: crispEdges; } .axis.y path { - visibility:hidden; + visibility: hidden; } .axis.x .tick--hidden { visibility: hidden; pointer-events: none; } .axis.x .tick line { - visibility:hidden; + visibility: hidden; } .axis.y .tick line { - stroke: rgba($govuk-text-colour, 0.1); + color: rgba($govuk-text-colour, 0.1); + stroke: currentColor; } .axis.y .tick:first-of-type line { - stroke: $govuk-border-colour; + color: $govuk-border-colour; + stroke: currentColor; } .grid line { fill: none; - stroke: rgba($govuk-text-colour, 0.1); + color: rgba($govuk-text-colour, 0.1); + stroke: currentColor; stroke-width: 1; shape-rendering: crispEdges; } .grid path { visibility: hidden; } - // Observed - .observed-line { - stroke: $govuk-link-colour; + .observed-line { + color: $govuk-link-colour; + stroke: currentColor; stroke-width: 3; fill: none; } - .observed-area { + .observed-area { fill: rgba($govuk-link-colour, 0.1); + @include high-contrast-mode-only { + fill: none; + } } // Forecast - .forecast-line { - stroke-dasharray: 4, 2; - stroke: $govuk-secondary-text-colour; + .forecast-line { + stroke-dasharray: 4, 2; + color: $govuk-secondary-text-colour; + stroke: currentColor; stroke-width: 3; fill: none; } - .forecast-area { + .forecast-area { fill: rgba($govuk-border-colour, 0.1); + @include high-contrast-mode-only { + fill: none; + } } // Locator + .locator { + visibility: hidden; + } + .locator--visible { + visibility: visible; + } .locator-point { stroke-width: 3; - stroke: $govuk-link-colour; - fill: white; + color: $govuk-link-colour; + stroke: currentColor; + fill: white; } .locator-line { - display:none; stroke-width: 1; - stroke: rgba($govuk-text-colour, 0.1); - shape-rendering: crispEdges; - } - .locator--offset .locator-line { - display:block; + color: govuk-colour('mid-grey'); + stroke: currentColor; + shape-rendering: crispEdges; } .locator--forecast .locator-point { - stroke: $govuk-secondary-text-colour; + color: $govuk-secondary-text-colour; + stroke: currentColor; + } + @include high-contrast-mode-only { + .locator-point { + fill: window; + } } // Tooltip - .tool-tip { + .tooltip { visibility: hidden; + pointer-events: none; } - .tool-tip--visible { + .tooltip--visible { visibility: visible; } - .tool-tip-bg { + .tooltip-bg { fill:white; stroke-width: 1; - stroke: $govuk-secondary-text-colour; + color: $govuk-secondary-text-colour; + stroke: currentColor; shape-rendering: crispEdges; } - .tool-tip-text { + .tooltip-text { @include govuk-font($size: 16); + color: $govuk-text-colour; + fill: currentColor; } - .tool-tip-text__strong { + .tooltip-text__strong { @include govuk-font($size: 19, $weight:bold); } + @include high-contrast-mode-only { + .tooltip-bg { + fill: window; + } + } // Now .time-line { stroke-width: 1; - stroke: $govuk-secondary-text-colour; - shape-rendering: crispEdges; + color: $govuk-secondary-text-colour; + stroke: currentColor; + shape-rendering: crispEdges; } .time-now-text { @include govuk-font($size: 14, $weight: bold); + color: $govuk-text-colour; + fill: currentColor; } // Thresholds .threshold__line { - stroke: $govuk-border-colour; + color: govuk-colour('mid-grey'); + stroke: currentColor; stroke-width: 3; - shape-rendering: crispEdges; + shape-rendering: geometricPrecision; } .threshold__bg { - stroke: transparent; - fill: transparent; + stroke: none; + fill: none; + pointer-events: bounding-box; } .threshold__remove { - display:none; + opacity: 0; } .threshold__remove line { stroke-width: 2; - stroke: $govuk-text-colour; + stroke: govuk-colour('mid-grey'); shape-rendering: crispEdges; stroke-linecap: square; } - .threshold__remove rect { + .threshold__remove-button { + stroke-width: 1; + stroke: govuk-colour('mid-grey'); + fill: white; + } + .threshold__remove-bg { stroke-width: 0; - stroke: transparent; - fill: white; + stroke: none; + fill: none; + pointer-events: bounding-box; + } + .threshold__remove:focus { + .threshold__remove-button { + stroke-width: 2; + } + .threshold__remove-bg { + fill: $govuk-focus-colour; + } } .threshold__remove:hover { cursor: pointer; + .threshold__remove-bg { + fill: govuk-colour('light-grey'); + } } .threshold-label { visibility: hidden; pointer-events: none; } .threshold-label__bg { - fill:white; + fill: white; stroke-width: 1; - stroke: govuk-colour('dark-grey'); + color: govuk-colour('dark-grey'); + stroke: currentColor; shape-rendering: auto; stroke-linecap: square; } .threshold-label__text { @include govuk-font($size: 16); - fill: $govuk-text-colour; + color: $govuk-text-colour; + fill: currentColor; + } + @include high-contrast-mode-only { + .threshold__line { + stroke: currentColor; + } + .threshold__remove-button, + .threshold-label__bg { + fill: window; + } + .threshold__remove-bg { + fill: none !important; + pointer-events: bounding-box; + } } .threshold.threshold--selected { + .threshold__remove { + opacity: 1; + } .threshold-label { visibility: visible; pointer-events: auto; } - .threshold__remove { - display: block; - } .threshold__line { + color: $govuk-text-colour; + stroke: currentColor; + } + .threshold__remove circle, + .threshold__remove line { stroke: $govuk-text-colour; } + @include high-contrast-mode-only { + .threshold__line { + stroke: currentColor; + } + .threshold__remove circle, + .threshold__remove line { + stroke: ButtonText; + } + } } .threshold.threshold--mouseover { - cursor:pointer; + cursor: pointer; .threshold__line { - stroke: $govuk-text-colour; + color: $govuk-text-colour; + stroke: currentColor; } } .threshold:not(.threshold--selected) .threshold__bg, .threshold:not(.threshold--selected) .threshold__line { cursor: pointer; } - // Typical/Alert - .threshold--alert { - .threshold__line { - stroke: $govuk-border-colour; - } + // Significant points + .significant .point circle { + opacity: 0; + fill: govuk-colour('blue'); } - .threshold.threshold--selected.threshold--alert, - .threshold.threshold--mouseover.threshold--alert { - .threshold__line { - stroke: $govuk-text-colour; + .significant .point--forecast circle { + fill: govuk-colour('dark-grey'); + } + .significant--visible .point circle { + opacity: 1; + } + .point text { + font-size: 0; + } + .point:focus { + outline: 3px solid transparent; + circle { + fill: govuk-colour('black'); + stroke: govuk-colour('yellow'); + paint-order: stroke; + stroke-width: 10; } } } diff --git a/server/src/sass/components/_navbar.scss b/server/src/sass/components/_navbar.scss index 5aee7de6b..9d097f82b 100644 --- a/server/src/sass/components/_navbar.scss +++ b/server/src/sass/components/_navbar.scss @@ -10,6 +10,7 @@ display: flex; flex-wrap: nowrap; overflow-x: auto; + overflow-y: hidden; -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; -webkit-overflow-scrolling: touch; @@ -28,17 +29,13 @@ &__inner { flex: 0 0 auto; padding: 3px 15px 4px 15px; - // @include mq ($from: tablet) { - // padding-left: 0px; - // padding-right: 0px; - // } } &__group { float: left; } &__list { list-style: none; - margin: 0px 0px -1px; + margin: 0px; padding: 0px; } &__list:after { @@ -70,6 +67,12 @@ border: 0px; box-shadow: 0 4px 0 govuk-colour('black'); outline: 3px solid transparent; + outline-offset: 0px; + } + a:not([keyboard-focus]):focus { + background-color: white; + box-shadow: none; + text-decoration: underline; } // Overides .defra-link-icon-s { @@ -81,7 +84,6 @@ margin-right: 10px; &:not([keyboard-focus]):focus { background-color: white; - // border-color: govuk-colour('mid-grey'); border: 0px; color: govuk-colour('black'); box-shadow: none; @@ -89,13 +91,6 @@ } &:hover { color: govuk-colour('black'); - background-color: transparent; - svg { - color: govuk-colour('black'); - } - } - &:active { - color: govuk-colour('black'); } } // Modifiers @@ -139,9 +134,9 @@ float: left; border: 0px; color: $govuk-link-colour; - margin: 0px 7.5px 4px 0px; - padding-top: 7.5px; - padding-bottom: 7.5px; + margin: 0px 7px 3px 0px; + padding-top: 7px; + padding-bottom: 7px; text-decoration: none; &:visited { color: $govuk-link-colour; @@ -159,6 +154,7 @@ color: $govuk-link-colour; box-shadow: none; border: 0px; + text-decoration: none; } &:after { content: 'Map'; @@ -166,20 +162,23 @@ svg { position: relative; display: inline-block; - color: govuk-colour('black'); + color: currentColor; vertical-align: top; left: 0px; margin: 0px 7px -3px 1px; top: -1px; } - span { + .defra-button-secondary__icon { + color: govuk-colour('black'); + } + .defra-button-secondary__text { @include defra-visually-hidden(); } } .defra-navbar__item a { color: $govuk-link-colour; text-decoration: none; - padding: 7.5px; + padding: 7px 6px; } .defra-navbar__item a:visited { color: $govuk-link-colour; @@ -190,16 +189,33 @@ color: $govuk-link-hover-colour; } } + .defra-navbar__item--selected a{ + background-color: transparent; + color: $govuk-link-colour; + &:after { + content: ''; + position: absolute; + bottom: -4px; + left: 0px; + right: 0px; + border-bottom: 4px solid currentColor; + } + } .defra-navbar__item a:focus { color: govuk-colour('black'); + background-color: $govuk-focus-colour; + text-decoration: none; + box-shadow: 0 4px 0 govuk-colour('black'); + outline: 3px solid transparent; + outline-offset: 0px; svg { color: govuk-colour('black'); } } - .defra-navbar__item--selected a:not(:focus) { - background-color: transparent; - border-bottom: 4px solid currentColor; + .defra-navbar__item a:not([keyboard-focus]):focus { + background-color: white; color: $govuk-link-colour; - } + box-shadow: none; + } } } diff --git a/server/src/sass/components/_search.scss b/server/src/sass/components/_search.scss index a07aaf036..65d09ea78 100644 --- a/server/src/sass/components/_search.scss +++ b/server/src/sass/components/_search.scss @@ -45,7 +45,6 @@ z-index: 1; cursor: pointer; color: white; - outline: 3px solid transparent; svg { position:absolute; top: 8px; diff --git a/server/src/sass/objects/_buttons.scss b/server/src/sass/objects/_buttons.scss index b32330313..3cb5022ff 100644 --- a/server/src/sass/objects/_buttons.scss +++ b/server/src/sass/objects/_buttons.scss @@ -9,6 +9,9 @@ margin-left:5px; vertical-align: middle; top:-1px; + @include high-contrast-mode-only { + color: currentColor; + } } } @@ -23,12 +26,12 @@ button.defra-button-secondary { border: 1px solid govuk-colour('mid-grey'); box-shadow: none; background-color: white; - padding: 9px 10px 10px; + padding: 10px; cursor: pointer; - color: $govuk-text-colour; + color: govuk-colour('black'); text-decoration: none; &:visited { - color: $govuk-text-colour; + color: govuk-colour('black'); } &:hover { background-color: govuk-colour('light-grey'); @@ -37,21 +40,25 @@ button.defra-button-secondary { background-color: $govuk-focus-colour; border-color: $govuk-focus-colour; box-shadow: 0 2px 0 govuk-colour('black'); - outline: 3px solid transparent; + outline-offset: 0; + outline: 2px solid transparent; } &:not([keyboard-focus]):focus { background-color: white; border: 1px solid govuk-colour('mid-grey'); box-shadow: none; } - &--icon { - padding-left: 31px; - svg { - color: govuk-colour('black'); - position: absolute; - left: 10px; - top: 50%; - margin-top: -11px; + &__icon { + color: govuk-colour('black'); + } + svg { + color: currentColor; + position: relative; + display: inline-block; + vertical-align: middle; + margin-right: 8px; + @include mq ($from: tablet) { + top: -1px; } } } @@ -105,13 +112,22 @@ button.defra-button-secondary { margin-right: 8px; top: 4px; } + @include high-contrast-mode-only { + color: currentColor; + } } &:hover svg { color: $govuk-link-hover-colour; + @include high-contrast-mode-only { + color: currentColor; + } } &:focus svg, &:active svg { color: govuk-colour('black'); + @include high-contrast-mode-only { + color: currentColor; + } } } .defra-link-icon-s { diff --git a/server/src/sass/tools/_mixins.scss b/server/src/sass/tools/_mixins.scss index bbdf86cce..14f11d3d8 100644 --- a/server/src/sass/tools/_mixins.scss +++ b/server/src/sass/tools/_mixins.scss @@ -15,6 +15,15 @@ white-space: nowrap !important; } +@mixin high-contrast-mode-only() { + @media + screen and (forced-colors: active), + screen and (-ms-high-contrast: active) + { + @content + } +} + // // Function to create an optimized svg url diff --git a/server/views/location.html b/server/views/location.html index 7c035ce8a..f80eb6fc6 100644 --- a/server/views/location.html +++ b/server/views/location.html @@ -39,6 +39,7 @@

{% if model.bannerSevereMainLink %}
+ {{ model.severeIcon | safe }} {{model.severitySevereTitle}}
@@ -52,6 +53,7 @@

{% if model.bannerMainLink%}
+ {{ model.mainIcon | safe }} {{model.severityTitle}}
@@ -86,7 +88,7 @@

This service tells you your risk of flooding from rivers, the sea and groundwater. Contact your local council about surface water flooding (also known as flash flooding).

{% endif %} -

Get flood warnings by phone, text or email

+ {% include "partials/sign-up-for-flood-warnings.html" %} {% if model.hasActiveFloods %} diff --git a/server/views/national.html b/server/views/national.html index 32a65b35c..efaaedf22 100644 --- a/server/views/national.html +++ b/server/views/national.html @@ -26,7 +26,9 @@

{% if group.count > 0 %} {% if group.severity.id < 4 %}
- + + {{ group.severity.icon | safe }} +
@@ -56,6 +58,7 @@

Check for flooding near you +

Latest river, sea, groundwater and rainfall levels

diff --git a/server/views/partials/sign-up-for-flood-warnings.html b/server/views/partials/sign-up-for-flood-warnings.html new file mode 100644 index 000000000..326299230 --- /dev/null +++ b/server/views/partials/sign-up-for-flood-warnings.html @@ -0,0 +1,5 @@ +

+ + Get flood warnings by phone, text or email + +

\ No newline at end of file diff --git a/server/views/rainfall-station.html b/server/views/rainfall-station.html index 291630202..5b6780950 100644 --- a/server/views/rainfall-station.html +++ b/server/views/rainfall-station.html @@ -49,14 +49,22 @@

This measuring s
-
+
@@ -105,15 +113,15 @@

Rainfall over the last 5 days in millimetres

- Download rainfall data CSV (12KB) + Download data CSV (12KB)

{% endif %}
- {% include "partials/context-footer.html" %} {% include "partials/social.html" %} + {% include "partials/context-footer.html" %} {% include "partials/related-content.html" %}
diff --git a/server/views/river-and-sea-levels.html b/server/views/river-and-sea-levels.html index 858c48c35..e0e1fba10 100644 --- a/server/views/river-and-sea-levels.html +++ b/server/views/river-and-sea-levels.html @@ -1,126 +1,137 @@ {% extends 'layout.html' %} {% block content %} - + -
-
-

- Find river, sea, groundwater and rainfall levels -

- {% if model.distStatement %}

{{ model.distStatement }}

{% endif %} +
+
+

+ Find river, sea, groundwater and rainfall levels +

+ {% if model.distStatement %} +

{{ model.distStatement }}

+ {% endif %} - -
-{% if model.q and not model.stations | length and model.q and not model.rivers | length or model.isEngland === false and not model.rivers %} -
-
-

- No results for '{{ model.q }}', England -

-

If you searched for a river or place in England, you should:

-
    -
  • check the spelling
  • -
  • enter a broader location
  • -
-

If you want to search for a place outside England, you should go to:

- -
-
-{% else %} - {% if model.stations | length %} + {% if model.q and not model.stations | length and model.q and not model.rivers | length or model.isEngland === false and not model.rivers %}
-
-
- -
- +

+ No results for '{{ model.q }}', England +

+

If you searched for a river or place in England, you should:

+
    +
  • check the spelling
  • +
  • enter a broader location
  • +
+

If you want to search for a place outside England, you should go to:

+ +
+
+ {% else %} + {% if model.stations | length %} +
+
+
+
-
-
-
- - - - {%- set hideHeader -%} - {% if model.queryGroup == 'rainfall' %}style="display:none"{% endif %} - {%- endset -%} - {%- set hideRainfallHeader -%} - {% if model.queryGroup != 'rainfall' %}style="display:none"{% endif %} - {%- endset -%} - - - - - - - - - - - - - - - {% for item in model.stations %} - {% include "partials/level-row.html" %} - {% endfor %} - -
- Results for {{ model.q }}, showing {{ model.queryGroup }} levels -
Measuring station1 hour6 hours24 hours
Measuring stationHeightTrendState
+
+
+ + + + {%- set hideHeader -%} + {% if model.queryGroup == 'rainfall' %}style="display:none"{% endif %} + {%- endset -%} + {%- set hideRainfallHeader -%} + {% if model.queryGroup != 'rainfall' %}style="display:none"{% endif %} + {%- endset -%} + + + + + + + + + + + + + + + {% for item in model.stations %} + {% include "partials/level-row.html" %} + {% endfor %} + +
+ Results for + {{ model.q }}, showing + {{ model.queryGroup }} + levels +
Measuring station1 hour6 hours24 hours
Measuring stationHeightTrendState
+
-
+ {% endif %} {% endif %} -{% endif %} -
-
- {% include "partials/context-footer.html" %} - {% include "partials/related-content.html" %} +
+
+ {% include "partials/context-footer.html" %} + {% include "partials/related-content.html" %} +
-
- {% endblock %} {% block bodyEnd %} -{{ super() }} - - - + {{ super() }} + + + {% endblock %} diff --git a/server/views/station.html b/server/views/station.html index 686d1931e..8139375ee 100644 --- a/server/views/station.html +++ b/server/views/station.html @@ -7,7 +7,7 @@ {% if model.banner %}
- + {{ model.mainIcon | safe }}
{% if model.severeBanner and model.isSevereLinkRenedered %} {{ model.severeBanner }} @@ -26,7 +26,7 @@
-

+

{% if model.station.isGroundwater %}Groundwater{% elif model.station.isCoastal %}Sea{% else %} {{ model.station.river }} {% endif %} level {% if model.station.isMulti %} {% if model.station.isDownstream %}downstream{% else %}upstream{% endif %}{% endif %} at {{ model.station.name }}

@@ -35,11 +35,27 @@

-
@@ -49,9 +65,9 @@

{% if model.station.isDownstream %} -

This measuring station takes 2 measurements. You're viewing the downstream level. View the upstream level.

+

This measuring station takes 2 measurements. You're viewing the downstream level. View the upstream level.

{% else %} -

This measuring station takes 2 measurements. You're viewing the upstream level. View the downstream level.

+

This measuring station takes 2 measurements. You're viewing the upstream level. View the downstream level.

{% endif %}
@@ -198,40 +214,60 @@

There's a proble {% endif %} -{# Graph #} + {% if model.station.isActive and model.readings %}
- +
+

Height in metres over the last 5 days {% if model.isForecast %}and up to 36 hour forecast{% endif %}

+ + {# Forecast information #} + {% if model.isForecast %} +
+
+
+ + + +
+ Information: +

This station includes an automated model which you should consider alongside other factors. The highest level in the model is {{ model.forecastHighest | round(2) }}m at {{ model.forecastHighestTime }}.

+
+
+
+
+ {% endif %} + + {# Graph #} + +
+ {% if model.dataOverHourOld %} +

We take measurements more often as the risk of flooding increases.

+ {% endif %} + {% if model.station.isRiver and model.hasNegativeValues %} +

Levels that are very low or below zero are normal for some stations.

+ {% endif %} + + Download data CSV ({% if model.isFfoi %}16{% else %}12{% endif %}KB) +
-Download height data CSV ({% if model.isFfoi %}16{% else %}12{% endif %}KB) - {% endif %} {# Impacts and thresholds #} {% if model.thresholds.length > 0 %}
-

How levels here could affect nearby areas

+

How levels here could affect nearby areas

{% if model.station.hasImpacts %} {% endif %} -
+
{% for band in model.thresholds %}
@@ -245,11 +281,11 @@

How levels here could affect ne {% endif %}

{% for threshold in band.values %} -
+
{{ threshold.description | safe }} - {% if not band.isLatest %} - + {% if threshold.id != 'latest' %} + {% endif %}
@@ -267,8 +303,8 @@

How levels here could affect ne

How we measure river, sea and groundwater levels

- {% include "partials/context-footer.html" %} {% include "partials/social.html" %} + {% include "partials/context-footer.html" %} {% include "partials/related-content.html" %}

@@ -280,7 +316,17 @@

How levels here could affect ne {% block bodyEnd %} {{ super() }} diff --git a/server/views/target-area.html b/server/views/target-area.html index 0df0d19bf..06a04c9be 100644 --- a/server/views/target-area.html +++ b/server/views/target-area.html @@ -12,7 +12,9 @@
{% if model.flood and model.severity.id < 4%}
- + + {{model.severity.icon | safe }} +
{{model.severity.subTitle}} - {{model.severity.tagline}}
@@ -65,18 +67,14 @@

{{ model.pageTitle }}

Find a river, sea, groundwater or rainfall level in this area

-

- - Get flood warnings by phone, text or email. - -

+ {% include "partials/sign-up-for-flood-warnings.html" %} {% if model.severity %}

Could this information be better? Tell us how to improve it.

{% endif %} - {% include "partials/context-footer.html" %} {% include "partials/social.html" %} + {% include "partials/context-footer.html" %}
diff --git a/test/data/telemetry.json b/test/data/telemetry.json new file mode 100644 index 000000000..ade840ee4 --- /dev/null +++ b/test/data/telemetry.json @@ -0,0 +1,2425 @@ +{ + "latestDateTime": "2023-07-18T13:00:00Z", + "rangeStartDateTime": "2023-07-13T14:38:27Z", + "rangeEndDateTime": "2023-07-18T14:38:27Z", + "dataStartDateTime": "2023-07-13T14:38:27Z", + "dataEndDateTime": "2023-07-18T14:38:27Z", + "latest1hr": 2, + "latest6hr": 3.8, + "latest24hr": 5.8, + "minutes": { + "latestDateTime": "2023-07-18T13:00:00Z", + "values": [ + { + "dateTime": "2023-07-18T14:45:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T14:30:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T14:15:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T14:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T13:45:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T13:30:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T13:15:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T13:00:00Z", + "value": 0.6 + }, + { + "dateTime": "2023-07-18T12:45:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-18T12:30:00Z", + "value": 0.6 + }, + { + "dateTime": "2023-07-18T12:15:00Z", + "value": 0.6 + }, + { + "dateTime": "2023-07-18T12:00:00Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-18T11:45:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-18T11:30:00Z", + "value": 0.6 + }, + { + "dateTime": "2023-07-18T11:15:00Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-18T11:00:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-18T10:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T10:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T10:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T10:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T09:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T09:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T09:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T09:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T08:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T08:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T08:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T08:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T07:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T07:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T07:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T07:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T06:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T06:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T06:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T06:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T05:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T05:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T05:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T05:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T04:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T04:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T04:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T04:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T03:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T03:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T03:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T03:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T02:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T02:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T02:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T02:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T01:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T01:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T01:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T01:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T00:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T00:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T00:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T00:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T23:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T23:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T23:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T23:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T22:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T22:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T22:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T22:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T21:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T21:30:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-17T21:15:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-17T21:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T20:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T20:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T20:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T20:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T19:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T19:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T19:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T19:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T18:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T18:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T18:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T18:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T17:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T17:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T17:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T17:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T16:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T16:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T16:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T16:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T15:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T15:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T15:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T15:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T14:45:00Z", + "value": 1 + }, + { + "dateTime": "2023-07-17T14:30:00Z", + "value": 0.6 + }, + { + "dateTime": "2023-07-17T14:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T14:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T13:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T13:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T13:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T13:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T12:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T12:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T12:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T12:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T11:45:00Z", + "value": 0.8 + }, + { + "dateTime": "2023-07-17T11:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T11:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T11:00:00Z", + "value": 0.8 + }, + { + "dateTime": "2023-07-17T10:45:00Z", + "value": 10.8 + }, + { + "dateTime": "2023-07-17T10:30:00Z", + "value": 1.6 + }, + { + "dateTime": "2023-07-17T10:15:00Z", + "value": 2.2 + }, + { + "dateTime": "2023-07-17T10:00:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-17T09:45:00Z", + "value": 0.8 + }, + { + "dateTime": "2023-07-17T09:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T09:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T09:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T08:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T08:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T08:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T08:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T07:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T07:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T07:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T07:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T06:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T06:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T06:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T06:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T05:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T05:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T05:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T05:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T04:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T04:30:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-17T04:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T04:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T03:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T03:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T03:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T03:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T02:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T02:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T02:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T02:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T01:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T01:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T01:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T01:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T00:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T00:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T00:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T00:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T23:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T23:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T23:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T23:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T22:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T22:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T22:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T22:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T21:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T21:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T21:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T21:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T20:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T20:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T20:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T20:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T19:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T19:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T19:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T19:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T18:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T18:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T18:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T18:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T17:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T17:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T17:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T17:00:00Z", + "value": 1.6 + }, + { + "dateTime": "2023-07-16T16:45:00Z", + "value": 0.6 + }, + { + "dateTime": "2023-07-16T16:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T16:15:00Z", + "value": 0.6 + }, + { + "dateTime": "2023-07-16T16:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T15:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T15:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T15:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T15:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T14:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T14:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T14:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T14:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T13:45:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-16T13:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T13:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T13:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T12:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T12:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T12:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T12:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T11:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T11:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T11:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T11:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T10:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T10:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T10:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T10:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T09:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T09:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T09:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T09:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T08:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T08:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T08:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T08:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T07:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T07:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T07:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T07:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T06:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T06:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T06:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T06:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T05:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T05:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T05:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T05:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T04:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T04:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T04:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T04:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T03:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T03:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T03:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T03:00:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-16T02:45:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-16T02:30:00Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-16T02:15:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-16T02:00:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-16T01:45:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-16T01:30:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-16T01:15:00Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-16T01:00:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-16T00:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T00:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T00:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T00:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T23:45:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T23:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T23:15:00Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-15T23:00:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T22:45:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T22:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T22:15:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T22:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T21:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T21:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T21:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T21:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T20:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T20:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T20:15:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T20:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T19:45:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T19:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T19:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T19:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T18:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T18:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T18:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T18:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T17:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T17:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T17:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T17:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T16:45:00Z", + "value": 1.2 + }, + { + "dateTime": "2023-07-15T16:30:00Z", + "value": 3.4 + }, + { + "dateTime": "2023-07-15T16:15:00Z", + "value": 1.6 + }, + { + "dateTime": "2023-07-15T16:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T15:45:00Z", + "value": 1.6 + }, + { + "dateTime": "2023-07-15T15:30:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T15:15:00Z", + "value": 0.8 + }, + { + "dateTime": "2023-07-15T15:00:00Z", + "value": 2 + }, + { + "dateTime": "2023-07-15T14:45:00Z", + "value": 1.4 + }, + { + "dateTime": "2023-07-15T14:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T14:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T14:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T13:45:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T13:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T13:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T13:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T12:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T12:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T12:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T12:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T11:45:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T11:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T11:15:00Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-15T11:00:00Z", + "value": 3.6 + }, + { + "dateTime": "2023-07-15T10:45:00Z", + "value": 0.8 + }, + { + "dateTime": "2023-07-15T10:30:00Z", + "value": 1 + }, + { + "dateTime": "2023-07-15T10:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T10:00:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T09:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T09:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T09:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T09:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T08:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T08:30:00Z", + "value": 1.6 + }, + { + "dateTime": "2023-07-15T08:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T08:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T07:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T07:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T07:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T07:00:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T06:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T06:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T06:15:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T06:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T05:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T05:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T05:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T05:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T04:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T04:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T04:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T04:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T03:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T03:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T03:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T03:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T02:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T02:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T02:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T02:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T01:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T01:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T01:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T01:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T00:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T00:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T00:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T00:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T23:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T23:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T23:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T23:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T22:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T22:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T22:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T22:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T21:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T21:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T21:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T21:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T20:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T20:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T20:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T20:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T19:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T19:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T19:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T19:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T18:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T18:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T18:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T18:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T17:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T17:30:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T17:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T17:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T16:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T16:30:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T16:15:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T16:00:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T15:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T15:30:00Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-14T15:15:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T15:00:00Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-14T14:45:00Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-14T14:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T14:15:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T14:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T13:45:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T13:30:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T13:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T13:00:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T12:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T12:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T12:15:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T12:00:00Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-14T11:45:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T11:30:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T11:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T11:00:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T10:45:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T10:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T10:15:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T10:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T09:45:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T09:30:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T09:15:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T09:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T08:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T08:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T08:15:00Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T08:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T07:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T07:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T07:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T07:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T06:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T06:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T06:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T06:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T05:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T05:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T05:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T05:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T04:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T04:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T04:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T04:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T03:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T03:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T03:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T03:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T02:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T02:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T02:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T02:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T01:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T01:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T01:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T01:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T00:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T00:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T00:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T00:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T23:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T23:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T23:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T23:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T22:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T22:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T22:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T22:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T21:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T21:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T21:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T21:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T20:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T20:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T20:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T20:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T19:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T19:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T19:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T19:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T18:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T18:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T18:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T18:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T17:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T17:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T17:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T17:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T16:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T16:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T16:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T16:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T15:45:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T15:30:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T15:15:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T15:00:00Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T14:45:00Z", + "value": 0 + } + ] + }, + "hours": { + "latestDateTime": "2023-07-18T13:00:00.000Z", + "values": [ + { + "dateTime": "2023-07-18T15:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T14:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T13:00:00.000Z", + "value": 2 + }, + { + "dateTime": "2023-07-18T12:00:00.000Z", + "value": 1.6 + }, + { + "dateTime": "2023-07-18T11:00:00.000Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-18T10:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T09:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T08:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T07:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T06:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T05:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T04:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T03:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T02:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T01:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-18T00:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T23:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T22:00:00.000Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-17T21:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T20:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T19:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T18:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T17:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T16:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T15:00:00.000Z", + "value": 1.6 + }, + { + "dateTime": "2023-07-17T14:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T13:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T12:00:00.000Z", + "value": 0.8 + }, + { + "dateTime": "2023-07-17T11:00:00.000Z", + "value": 15.4 + }, + { + "dateTime": "2023-07-17T10:00:00.000Z", + "value": 1 + }, + { + "dateTime": "2023-07-17T09:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T08:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T07:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T06:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T05:00:00.000Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-17T04:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T03:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T02:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T01:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-17T00:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T23:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T22:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T21:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T20:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T19:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T18:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T17:00:00.000Z", + "value": 2.8 + }, + { + "dateTime": "2023-07-16T16:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T15:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T14:00:00.000Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-16T13:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T12:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T11:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T10:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T09:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T08:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T07:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T06:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T05:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T04:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-16T03:00:00.000Z", + "value": 1 + }, + { + "dateTime": "2023-07-16T02:00:00.000Z", + "value": 1 + }, + { + "dateTime": "2023-07-16T01:00:00.000Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-16T00:00:00.000Z", + "value": 0.6 + }, + { + "dateTime": "2023-07-15T23:00:00.000Z", + "value": 0.6 + }, + { + "dateTime": "2023-07-15T22:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T21:00:00.000Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T20:00:00.000Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T19:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T18:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T17:00:00.000Z", + "value": 6.2 + }, + { + "dateTime": "2023-07-15T16:00:00.000Z", + "value": 2.6 + }, + { + "dateTime": "2023-07-15T15:00:00.000Z", + "value": 3.4 + }, + { + "dateTime": "2023-07-15T14:00:00.000Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T13:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T12:00:00.000Z", + "value": 0.6 + }, + { + "dateTime": "2023-07-15T11:00:00.000Z", + "value": 5.4 + }, + { + "dateTime": "2023-07-15T10:00:00.000Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-15T09:00:00.000Z", + "value": 1.6 + }, + { + "dateTime": "2023-07-15T08:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T07:00:00.000Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-15T06:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T05:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T04:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T03:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T02:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T01:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-15T00:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T23:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T22:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T21:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T20:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T19:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T18:00:00.000Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T17:00:00.000Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-14T16:00:00.000Z", + "value": 0.8 + }, + { + "dateTime": "2023-07-14T15:00:00.000Z", + "value": 1 + }, + { + "dateTime": "2023-07-14T14:00:00.000Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-14T13:00:00.000Z", + "value": 0.4 + }, + { + "dateTime": "2023-07-14T12:00:00.000Z", + "value": 0.8 + }, + { + "dateTime": "2023-07-14T11:00:00.000Z", + "value": 0.6 + }, + { + "dateTime": "2023-07-14T10:00:00.000Z", + "value": 0.6 + }, + { + "dateTime": "2023-07-14T09:00:00.000Z", + "value": 0.2 + }, + { + "dateTime": "2023-07-14T08:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T07:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T06:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T05:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T04:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T03:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T02:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T01:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-14T00:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T23:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T22:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T21:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T20:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T19:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T18:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T17:00:00.000Z", + "value": 0 + }, + { + "dateTime": "2023-07-13T16:00:00.000Z", + "value": 0 + } + ] + }, + "status": "success" +} diff --git a/test/dom.js b/test/dom.js new file mode 100644 index 000000000..d361ee63e --- /dev/null +++ b/test/dom.js @@ -0,0 +1,30 @@ +const { JSDOM } = require('jsdom') + +module.exports = { + setupDOM, + cleanupDOM +} +function setupDOM () { + const dom = new JSDOM('') + global.window = dom.window + global.document = dom.window.document + dom.window.flood = {} + polyfillSVG(dom.window) +} + +function cleanupDOM () { + delete global.window + delete global.document +} + +function polyfillSVG (window) { + Object.defineProperty(window.SVGElement.prototype, 'getBBox', { + writable: true, + value: () => ({ + x: 0, + y: 0, + width: 0, + height: 0 + }) + }) +} diff --git a/test/routes/station.js b/test/routes/station.js index db3244bee..8a159d7ef 100644 --- a/test/routes/station.js +++ b/test/routes/station.js @@ -407,10 +407,10 @@ lab.experiment('Test - /station/{id}', () => { Code.expect(response.payload).to.contain('Normal') Code.expect(response.payload).to.contain('Steady') Code.expect(response.payload).to.contain('Normal range 0.15m to 3.50m') - Code.expect(response.payload).to.contain('Nearby levels') - Code.expect(response.payload).to.contain('Upstream') + Code.expect(response.payload).to.contain('Nearby levels') + Code.expect(response.payload).to.contain('Upstream') Code.expect(response.payload).to.contain('

Share this page

') - Code.expect(response.payload).to.contain('Download height data CSV (12KB)') + Code.expect(response.payload).to.contain('Download data CSV (12KB)') }) lab.test('GET station/2042/downstream ', async () => { const floodService = require('../../server/services/flood') @@ -543,8 +543,8 @@ lab.experiment('Test - /station/{id}', () => { Code.expect(response.statusCode).to.equal(200) Code.expect(response.payload).to.contain('River Avon level downstream at Lilbourne - GOV.UK') Code.expect(response.payload).to.contain('This measuring station takes 2 measurements.') - Code.expect(response.payload).to.contain('Nearby levels') - Code.expect(response.payload).to.contain('Downstream') + Code.expect(response.payload).to.contain('Nearby levels') + Code.expect(response.payload).to.contain('Downstream') }) lab.test('GET station/5146 with High river level ', async () => { const floodService = require('../../server/services/flood') @@ -679,8 +679,8 @@ lab.experiment('Test - /station/{id}', () => { Code.expect(response.payload).to.contain('High') Code.expect(response.payload).to.contain('Falling') Code.expect(response.payload).to.contain('Latest at 1:30am') - Code.expect(response.payload).to.contain('Nearby levels') - Code.expect(response.payload).to.contain('Upstream') + Code.expect(response.payload).to.contain('Nearby levels') + Code.expect(response.payload).to.contain('Upstream') Code.expect(response.payload).to.not.contain('Go downstream') }) lab.test('GET Closed station ', async () => { @@ -911,8 +911,8 @@ lab.experiment('Test - /station/{id}', () => { Code.expect(response.payload).to.contain('Low\n') Code.expect(response.payload).to.contain('Rising') Code.expect(response.payload).to.contain('Latest at 1:30am') - Code.expect(response.payload).to.contain('Nearby levels') - Code.expect(response.payload).to.contain('Upstream') + Code.expect(response.payload).to.contain('Nearby levels') + Code.expect(response.payload).to.contain('Upstream') }) lab.test('GET station/3130 Coastal ', async () => { const floodService = require('../../server/services/flood') @@ -1293,7 +1293,7 @@ lab.experiment('Test - /station/{id}', () => { Code.expect(response.statusCode).to.equal(200) Code.expect(response.payload).to.not.contain('The highest level in the forecast is') Code.expect(response.payload).to.not.contain('') - Code.expect(response.payload).to.contain('Download height data CSV (16KB)') + Code.expect(response.payload).to.contain('Download data CSV (16KB)') }) lab.test('GET station/7333 ffoi with max value ', async () => { const floodService = require('../../server/services/flood') @@ -1543,11 +1543,11 @@ lab.experiment('Test - /station/{id}', () => { const response = await server.inject(options) Code.expect(response.statusCode).to.equal(200) - Code.expect(response.payload).to.contain('The highest level in our forecast is') + Code.expect(response.payload).to.contain('The highest level in the model is') Code.expect(response.payload).to.not.contain('') - Code.expect(response.payload).to.contain('Upstream') - Code.expect(response.payload).to.contain('Downstream') - Code.expect(response.payload).to.contain('Nearby levels') + Code.expect(response.payload).to.contain('Upstream') + Code.expect(response.payload).to.contain('Downstream') + Code.expect(response.payload).to.contain('Nearby levels') }) lab.test('GET station/5146 with latest value over hour old but < 24 hours ', async () => { const floodService = require('../../server/services/flood') @@ -1681,7 +1681,7 @@ lab.experiment('Test - /station/{id}', () => { Code.expect(response.statusCode).to.equal(200) Code.expect(response.payload).to.contain('We take measurements more often as the risk of flooding increases.') }) - lab.test('GET station/2033 should redirect to nrw page ', async () => { + lab.test('GET station/2033 should redirect to new page ', async () => { const floodService = require('../../server/services/flood') const fakeStationData = () => { @@ -1839,7 +1839,7 @@ lab.experiment('Test - /station/{id}', () => { Code.expect(response.statusCode).to.equal(302) }) - lab.test('GET station/5146 with status date showing time data interupted', async () => { + lab.test('GET station/5146 with status date showing time data interrupted', async () => { const floodService = require('../../server/services/flood') const dateInterupted = new Date() @@ -1971,10 +1971,10 @@ lab.experiment('Test - /station/{id}', () => { Code.expect(response.statusCode).to.equal(200) Code.expect(response.payload).to.contain('River Ribble level at Walton-Le-Dale - GOV.UK') Code.expect(response.payload).to.contain('This data feed was interrupted') - Code.expect(response.payload).to.contain('Nearby levels') - Code.expect(response.payload).to.contain('Upstream') + Code.expect(response.payload).to.contain('Nearby levels') + Code.expect(response.payload).to.contain('Upstream') Code.expect(response.payload).to.contain('

Share this page

') - Code.expect(response.payload).to.contain('Download height data CSV (12KB)') + Code.expect(response.payload).to.contain('Download data CSV (12KB)') }) lab.test('GET station/5146 with Normal river level shows IMTD thresholds if present', async () => { const floodService = require('../../server/services/flood') @@ -2367,9 +2367,9 @@ lab.experiment('Test - /station/{id}', () => { Code.expect(response.payload).to.contain('River Ribble level at Walton-Le-Dale - GOV.UK') Code.expect(response.payload).to.contain('Steady') Code.expect(response.payload).to.not.contain('Normal range ') - Code.expect(response.payload).to.contain('Nearby levels') - Code.expect(response.payload).to.contain('Upstream') + Code.expect(response.payload).to.contain('Nearby levels') + Code.expect(response.payload).to.contain('Upstream') Code.expect(response.payload).to.contain('

Share this page

') - Code.expect(response.payload).to.contain('Download height data CSV (12KB)') + Code.expect(response.payload).to.contain('Download data CSV (12KB)') }) }) diff --git a/test/src/js/components/bar-chart.js b/test/src/js/components/bar-chart.js new file mode 100644 index 000000000..81104ed4b --- /dev/null +++ b/test/src/js/components/bar-chart.js @@ -0,0 +1,142 @@ +const Lab = require('@hapi/lab') +const { expect } = require('@hapi/code') +const mockdate = require('mockdate') + +const telemetryFixture = require('../../../data/telemetry.json') +const { cleanupDOM, setupDOM } = require('../../../dom') + +const { experiment, test, before, after, beforeEach } = exports.lab = Lab.script() +const initialTimezone = process.env.TZ +experiment('BarChart', () => { + before(async () => { + setupDOM() + process.env.TZ = 'Etc/UTC' + mockdate.set('2023-07-19T00:00:00.000Z') + await import('../../../../server/src/js/core.mjs') + await import('../../../../server/src/js/components/bar-chart/index.mjs') + }) + + after(() => { + process.env.TZ = initialTimezone + cleanupDOM() + mockdate.reset() + }) + + beforeEach(() => { + document.body.innerHTML = '' + }) + + test('The hours and minutes controls are rendered with hours as the default', async () => { + // Arrange + const chartId = 'example-chart-id' + const telemetry = telemetryFixture + const chartContainer = document.createElement('div') + chartContainer.setAttribute('id', 'bar-chart-container') + document.body.appendChild(chartContainer) + + // Act + window.flood.charts.createBarChart('bar-chart-container', chartId, telemetry) + const controlsContainer = chartContainer.querySelector('.defra-chart-controls__group--resolution') + const [fiveDaysControl, twentyFourHoursControl] = controlsContainer.children + + // Assert + expect(controlsContainer).not.to.equal(null) + expect(controlsContainer.children).to.have.length(2) + + expect(fiveDaysControl.children[0].innerText).to.equal('5 days') + expect({ ...fiveDaysControl.dataset }).to.equal({ + period: 'hours', + start: '2023-07-14T00:00:00Z', + end: '2023-07-19T00:00:00Z' + }) + + expect(twentyFourHoursControl.children[0].innerText).to.equal('24 hours') + expect({ ...twentyFourHoursControl.dataset }).to.equal({ + period: 'minutes', + start: '2023-07-18T00:00:00Z', + end: '2023-07-19T00:00:00Z' + }) + + const description = chartContainer.querySelector('#bar-chart-description').textContent + expect(description).to.contain('Showing 5 days') + expect(description).to.contain('from 14 July 2023 at 1:00AM to 18 July 2023 at 3:00PM in hourly totals.') + }) + + test('The 24 hours control switches the chart to 24 hour range', async () => { + // Arrange + const chartId = 'example-chart-id' + const telemetry = telemetryFixture + const chartContainer = document.createElement('div') + chartContainer.setAttribute('id', 'bar-chart-container') + document.body.appendChild(chartContainer) + window.flood.charts.createBarChart('bar-chart-container', chartId, telemetry) + const twentyFourHoursControl = chartContainer.querySelector('.defra-chart-controls__group--resolution').children[1] + + // Act + twentyFourHoursControl.click() + + // Assert + const description = chartContainer.querySelector('#bar-chart-description').textContent + expect(description).to.contain('Showing 24 hours') + expect(description).to.contain('from 18 July 2023 at 12:15AM to 18 July 2023 at 2:45PM in 15 minute totals.') + }) + + test('The pagination buttons are shown when the chart is in the 24 hour range', async () => { + // Arrange + const chartId = 'example-chart-id' + const telemetry = telemetryFixture + const chartContainer = document.createElement('div') + chartContainer.setAttribute('id', 'bar-chart-container') + document.body.appendChild(chartContainer) + + // Act + window.flood.charts.createBarChart('bar-chart-container', chartId, telemetry) + chartContainer.querySelector('.defra-chart-controls__group--resolution .defra-chart-controls__button[data-period="minutes"]').click() + + // Assert + const outerContainer = document.querySelector('.defra-chart-controls__group--pagination') + expect(outerContainer).not.to.equal(null) + expect(outerContainer.style.display).to.equal('inline-block') + expect(outerContainer.children).to.have.length(2) + + expect(outerContainer.children[0].dataset.direction).to.equal('back') + expect(outerContainer.children[1].dataset.direction).to.equal('forward') + }) + + test('The pagination buttons are not shown when the chart is in the 5 day range', async () => { + // Arrange + const chartId = 'example-chart-id' + const telemetry = telemetryFixture + const chartContainer = document.createElement('div') + chartContainer.setAttribute('id', 'bar-chart-container') + document.body.appendChild(chartContainer) + + // Act + window.flood.charts.createBarChart('bar-chart-container', chartId, telemetry) + chartContainer.querySelector('.defra-chart-controls__group--resolution .defra-chart-controls__button[data-period="hours"]').click() + + // Assert + const paginationControls = document.querySelector('.defra-chart-controls__group--pagination') + expect(paginationControls).not.to.equal(null) + expect(paginationControls.style.display).to.equal('none') + }) + + test('The pagination buttons allow changing the page range', async () => { + // Arrange + const chartId = 'example-chart-id' + const telemetry = telemetryFixture + const chartContainer = document.createElement('div') + chartContainer.setAttribute('id', 'bar-chart-container') + document.body.appendChild(chartContainer) + + // Act + window.flood.charts.createBarChart('bar-chart-container', chartId, telemetry) + chartContainer.querySelector('.defra-chart-controls__group--resolution .defra-chart-controls__button[data-period="minutes"]').click() + chartContainer.querySelector('.defra-chart-controls__group--pagination .defra-chart-controls__button[data-direction="back"]').click() + + // Assert + const description = chartContainer.querySelector('#bar-chart-description').textContent + expect(description).to.contain('Showing 24 hours') + expect(description).to.contain('from 17 July 2023 at 12:15AM to 18 July 2023 at 12:15AM in 15 minute totals') + }) +}) diff --git a/webpack.config.js b/webpack.config.js index d41b9d4f3..3ef02b093 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,7 +6,7 @@ module.exports = (env, argv) => ({ mode: !inDev ? 'production' : 'development', devtool: !inDev ? false : 'source-map', entry: { - core: './server/src/js/core', + core: './server/src/js/core.mjs', 'alerts-and-warnings': './server/src/js/pages/alerts-and-warnings', impacts: './server/src/js/pages/impacts', national: './server/src/js/pages/national',