diff --git a/package-lock.json b/package-lock.json
index 29a99bf36..68bc1cd28 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"bootstrap": "~5.3.3",
"csv": "~5.3.2",
"csv-stringify": "~5.6.5",
+ "d3": "~7.8.5",
"dotenv": "~16.4.5",
"escape-html": "~1.0.3",
"express": "~4.19.2",
@@ -57,6 +58,7 @@
},
"devDependencies": {
"@redux-devtools/extension": "~3.2.5",
+ "@types/d3": "~7.4.3",
"@types/lodash": "~4.17.4",
"@types/node": "~20.14.10",
"@types/plotly.js": "~2.29.2",
@@ -976,6 +978,259 @@
"integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
"dev": true
},
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "dev": true,
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
+ "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
+ "dev": true
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "dev": true,
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "dev": true,
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+ "dev": true
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "dev": true
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "dev": true,
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+ "dev": true
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz",
+ "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==",
+ "dev": true
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "dev": true
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "dev": true
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "dev": true,
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "dev": true
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "dev": true
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "dev": true
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "dev": true,
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==",
+ "dev": true
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+ "dev": true
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "dev": true
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+ "dev": true
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
+ "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz",
+ "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==",
+ "dev": true
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz",
+ "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==",
+ "dev": true
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz",
+ "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==",
+ "dev": true,
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz",
+ "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==",
+ "dev": true
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "dev": true
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "dev": true
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz",
+ "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "dev": true,
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
"node_modules/@types/eslint": {
"version": "8.44.7",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.7.tgz",
@@ -1002,6 +1257,12 @@
"integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==",
"dev": true
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.14",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz",
+ "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==",
+ "dev": true
+ },
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
@@ -1633,14 +1894,6 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
- "node_modules/add-dom-event-listener": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz",
- "integrity": "sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==",
- "dependencies": {
- "object-assign": "4.x"
- }
- },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -3018,11 +3271,85 @@
"type": "^1.0.1"
}
},
+ "node_modules/d3": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz",
+ "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-array": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
},
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-collection": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz",
@@ -3036,11 +3363,118 @@
"node": ">=12"
}
},
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour/node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-dispatch": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz",
"integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA=="
},
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-force": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz",
@@ -3104,11 +3538,84 @@
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
},
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-quadtree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz",
"integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA=="
},
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale/node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale/node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-shape": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
@@ -3135,6 +3642,155 @@
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz",
"integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw=="
},
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -3230,6 +3886,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delaunator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
+ "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -5234,6 +5898,14 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/interpret": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
@@ -8329,6 +9001,11 @@
"inherits": "^2.0.1"
}
},
+ "node_modules/robust-predicates": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
+ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
diff --git a/package.json b/package.json
index f9a48c1bb..fa5f6d8de 100644
--- a/package.json
+++ b/package.json
@@ -64,6 +64,7 @@
"bootstrap": "~5.3.3",
"csv": "~5.3.2",
"csv-stringify": "~5.6.5",
+ "d3": "~7.8.5",
"dotenv": "~16.4.5",
"escape-html": "~1.0.3",
"express": "~4.19.2",
@@ -104,6 +105,7 @@
},
"devDependencies": {
"@redux-devtools/extension": "~3.2.5",
+ "@types/d3": "~7.4.3",
"@types/lodash": "~4.17.4",
"@types/node": "~20.14.10",
"@types/plotly.js": "~2.29.2",
diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx
index 21bd5288b..e800c5895 100644
--- a/src/client/app/components/HeaderButtonsComponent.tsx
+++ b/src/client/app/components/HeaderButtonsComponent.tsx
@@ -64,6 +64,7 @@ export default function HeaderButtonsComponent() {
shouldCSVReadingsButtonDisabled: true,
shouldUnitsButtonDisabled: true,
shouldConversionsButtonDisabled: true,
+ shouldVisualUnitMapButtonDisabled: true,
// Translated menu title that depend on whether logged in.
menuTitle: '',
// link to help page for page choices. Should not see default but use general help URL.
@@ -99,7 +100,8 @@ export default function HeaderButtonsComponent() {
shouldCSVMetersButtonDisabled: pathname === '/csvMeters',
shouldCSVReadingsButtonDisabled: pathname === '/csvReadings',
shouldUnitsButtonDisabled: pathname === '/units',
- shouldConversionsButtonDisabled: pathname === '/conversions'
+ shouldConversionsButtonDisabled: pathname === '/conversions',
+ shouldVisualUnitMapButtonDisabled: pathname === '/visual-unit'
}));
}, [pathname]);
@@ -227,6 +229,13 @@ export default function HeaderButtonsComponent() {
to="/units">
+
+
+
},
{ path: 'maps', element: },
{ path: 'units', element: },
- { path: 'users', element: }
+ { path: 'users', element: },
+ { path: 'visual-unit', element: }
]
},
{
diff --git a/src/client/app/components/TooltipHelpComponent.tsx b/src/client/app/components/TooltipHelpComponent.tsx
index 5a49d17c9..f48de775e 100644
--- a/src/client/app/components/TooltipHelpComponent.tsx
+++ b/src/client/app/components/TooltipHelpComponent.tsx
@@ -70,7 +70,8 @@ export default function TooltipHelpComponent(props: TooltipHelpProps) {
'help.home.toggle.chart.link': { link: `${helpUrl}/chartLink/` },
'help.groups.groupdetails': { link: `${helpUrl}/groupViewing/#groupDetails` },
'help.groups.groupview': { link: `${helpUrl}/groupViewing/` },
- 'help.meters.meterview': { link: `${helpUrl}/meterViewing/` }
+ 'help.meters.meterview': { link: `${helpUrl}/meterViewing/` },
+ 'help.admin.unitconversionvisuals': { link: `${helpUrl}/adminUnitVisual/` }
};
return (
diff --git a/src/client/app/components/visual-unit/CreateVisualUnitComponent.tsx b/src/client/app/components/visual-unit/CreateVisualUnitComponent.tsx
new file mode 100644
index 000000000..e9e70757a
--- /dev/null
+++ b/src/client/app/components/visual-unit/CreateVisualUnitComponent.tsx
@@ -0,0 +1,240 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import * as React from 'react';
+import * as d3 from 'd3';
+import { useEffect } from 'react';
+import { useIntl } from 'react-intl';
+import { useAppSelector } from '../../redux/reduxHooks';
+import { ConversionData } from 'types/redux/conversions';
+import { CikData } from 'types/redux/ciks';
+import { selectAllUnits } from '../../redux/api/unitsApi';
+
+/**
+ * Visual graph component that shows the relationship between units and conversions
+ * entered by an admin.
+ * @returns D3 force graph visual
+ */
+interface CreateVisualUnitProps {
+ conversions: ConversionData[] | CikData[];
+ isCik?: boolean;
+}
+
+export const CreateVisualUnitComponent: React.FC = ({
+ conversions,
+ isCik = false
+}) => {
+ const intl = useIntl();
+ /* Get unit data from redux */
+ const units = useAppSelector(selectAllUnits);
+
+ /* creating color schema for nodes based on their unit type */
+ const colors = ['#1F77B4', '#2CA02C', '#fd7e14', '#e377c2'];
+ const colorSchema = d3.scaleOrdinal()
+ .domain(['meter', 'unit', 'suffix', 'suffix.input'])
+ .range(colors);
+
+ /* Create data container to pass to D3 force graph */
+ const data: { nodes: any[], links: any[] } = {
+ nodes: [],
+ links: []
+ };
+
+ units.map(value =>
+ data.nodes.push({
+ 'name': value.name,
+ 'id': value.id,
+ 'typeOfUnit': value.typeOfUnit,
+ 'suffix': value.suffix
+ })
+ );
+
+ conversions.map(value => {
+ if (isCik) {
+ const cikValue = value as CikData;
+ data.links.push({
+ 'source': cikValue.meterUnitId,
+ 'target': cikValue.nonMeterUnitId,
+ 'bidirectional': false
+ });
+ } else {
+ const conversionValue = value as ConversionData;
+ data.links.push({
+ 'source': conversionValue.sourceId,
+ 'target': conversionValue.destinationId,
+ /* boolean value */
+ 'bidirectional': conversionValue.bidirectional
+ });
+ }
+ });
+
+ /* Visuals start here */
+ useEffect(() => {
+ /* View-box dimensions */
+ const width = window.innerWidth;
+ const height = isCik ? 1000 : 750;
+
+ /* Grab data */
+ const nodes = data.nodes.map(d => ({...d}));
+ const links = data.links.map(d => ({...d}));
+
+ const simulation = d3.forceSimulation(nodes)
+ .force('link', d3.forceLink(links)
+ /* Set all link ids (from data.links) */
+ .id((d: any) => d.id)
+ /* This controls how long each link is */
+ .distance(isCik ? 120 : 90)
+ )
+ /* Create new many-body force */
+ .force('charge', d3.forceManyBody()
+ /* This controls the 'repelling' force on each node */
+ .strength(isCik ? -800 : -500)
+ )
+ .force('x', d3.forceX())
+ .force('y', d3.forceY());
+
+ const svg = d3.select(isCik ? '#sample-cik' : '#sample')
+ .append('svg')
+ .attr('width', width)
+ .attr('height', height)
+ .attr('viewBox', [-width / 2, -height / 2, width, height])
+ .attr('style', 'max-width: 100%; height: auto;')
+ .append('g');
+
+ /* End arrow head */
+ svg.append('defs').append('marker')
+ .attr('id', 'arrow-end')
+ .attr('viewBox', '0 -5 10 10')
+ .attr('refX', 25)
+ .attr('refY', 0)
+ .attr('markerWidth', 4)
+ .attr('markerHeight', 4)
+ /* auto: point towards dest. node */
+ .attr('orient', 'auto')
+ .append('svg:path')
+ .attr('d', 'M0,-5L10,0L0,5');
+
+ if (!isCik) {
+ /* Start arrow head (for bidirectional edges) */
+ svg.append('defs').append('marker')
+ .attr('id', 'arrow-start')
+ .attr('viewBox', '0 -5 10 10')
+ .attr('refX', 25)
+ .attr('refY', 0)
+ .attr('markerWidth', 4)
+ .attr('markerHeight', 4)
+ /* auto-start-reverse: point towards src. node */
+ .attr('orient', 'auto-start-reverse')
+ .append('svg:path')
+ .attr('d', 'M0,-5L10,0L0,5');
+ }
+
+ /* Link style */
+ const link = svg.selectAll('line')
+ .data(links)
+ .enter().append('line')
+ .style('stroke', '#aaa')
+ .attr('stroke-width', 3)
+ .attr('marker-end', 'url(#arrow-end)')
+ /* Only draw start arrow head if bidirectional */
+ .attr('marker-start', d => d.bidirectional === true ? 'url(#arrow-start)' : '');
+
+ /* Node style */
+ const node = svg.selectAll('.node')
+ .data(nodes)
+ .enter().append('circle')
+ /* Node radius */
+ .attr('r', 20)
+ /* checks if unit has a non empty suffix to color differently */
+ .attr('fill', d => d.suffix && d.typeOfUnit === 'unit' ? colorSchema('suffix.input') : colorSchema(d.typeOfUnit));
+
+ /* Drag behavior */
+ node.call(d3.drag()
+ .on('start', dragstart)
+ .on('drag', dragged)
+ .on('end', dragend));
+
+ /* Node label style */
+ const label = svg.selectAll('.label')
+ .data(nodes)
+ .enter()
+ .append('text')
+ .text(function (d) { return d.name; })
+ .style('text-anchor', 'middle')
+ .style('fill', '#000')
+ .style('font-family', 'Arial')
+ .style('font-size', 14);
+
+ /* Update element positions when moved */
+ simulation.on('tick', () => {
+ link
+ .attr('x1', d => d.source.x)
+ .attr('y1', d => d.source.y)
+ .attr('x2', d => d.target.x)
+ .attr('y2', d => d.target.y);
+
+ node
+ .attr('cx', d => d.x)
+ .attr('cy', d => d.y);
+
+ label
+ .attr('x', function(d){ return d.x; })
+ .attr('y', function (d) {return d.y - 25; });
+ });
+
+ // eslint-disable-next-line jsdoc/require-jsdoc
+ function dragstart(event: any) {
+ if (!event.active) simulation.alphaTarget(0.3).restart();
+ event.subject.fx = event.subject.x;
+ event.subject.fy = event.subject.y;
+ }
+
+ // eslint-disable-next-line jsdoc/require-jsdoc
+ function dragged(event: any) {
+ event.subject.fx = event.x;
+ event.subject.fy = event.y;
+ }
+
+ // eslint-disable-next-line jsdoc/require-jsdoc
+ function dragend(event: any) {
+ if (!event.active) simulation.alphaTarget(0);
+ event.subject.fx = null;
+ event.subject.fy = null;
+ }
+
+ /* Color Legend */
+ const legend = svg.append('g')
+ .attr('transform', `translate(${-width / 2 + 20}, ${-height / 2 + 20})`);
+
+ colorSchema.domain().forEach((item, i) => {
+ const legendEntry = legend.append('g')
+ .attr('transform', `translate(0, ${i * 30})`);
+
+ // Rectangle color box
+ legendEntry.append('circle')
+ .attr('r', 15)
+ .attr('cx', 15) // Center the circle horizontally
+ .attr('cy', 15) // Center the circle vertically
+ .attr('fill', colorSchema(item));
+
+ // Text label
+ legendEntry.append('text')
+ .attr('x', 40) // Position the text to the right of the circle
+ .attr('y', 20) // Align the text vertically with the circle
+ .style('fill', '#000')
+ .style('font-size', '14px')
+ .style('alignment-middle', 'middle')
+ /* internationalizing color legend text */
+ .text(intl.formatMessage({id : `legend.graph.text.${item}`}));
+ });
+
+ // Empty dependency array to run the effect only once
+ }, []);
+
+ return(
+
+ );
+};
\ No newline at end of file
diff --git a/src/client/app/components/visual-unit/VisualUnitDetailComponent.tsx b/src/client/app/components/visual-unit/VisualUnitDetailComponent.tsx
new file mode 100644
index 000000000..bc4e8dc28
--- /dev/null
+++ b/src/client/app/components/visual-unit/VisualUnitDetailComponent.tsx
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { FormattedMessage } from 'react-intl';
+import TooltipHelpComponent from '../TooltipHelpComponent';
+import * as React from 'react';
+import { CreateVisualUnitComponent } from './CreateVisualUnitComponent';
+import TooltipMarkerComponent from '../TooltipMarkerComponent';
+import { selectCik } from '../../redux/api/conversionsApi';
+import { selectConversionsDetails } from '../../redux/api/conversionsApi';
+import { useAppSelector } from '../../redux/reduxHooks';
+
+/**
+ * Defines the units and conversion graphics view.
+ * @returns Units visual graphics page element
+ */
+export default function VisualUnitDetailComponent() {
+ /* Get conversion data from redux */
+ const conversionData = useAppSelector(selectConversionsDetails);
+ const cikData = useAppSelector(selectCik);
+
+
+
+ const titleStyle: React.CSSProperties = {
+ textAlign: 'center'
+ };
+
+ const tooltipStyle = {
+ display: 'inline-block',
+ fontSize: '50%',
+ // For now, only an admin can see the unit visualization page.
+ tooltipVisualUnitView: 'help.admin.unitconversionvisuals'
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts
index 0bc5d4ad4..9a07c131c 100644
--- a/src/client/app/translations/data.ts
+++ b/src/client/app/translations/data.ts
@@ -225,6 +225,7 @@ const LocaleTranslationData = {
"help.admin.metercreate": "This page allows admins to create meters. Please visit {link} for further details and information.",
"help.admin.meteredit": "This page allows admins to edit meters. Please visit {link} for further details and information.",
"help.admin.meterview": "This page allows admins to view and edit meters. Please visit {link} for further details and information.",
+ "help.admin.unitconversionvisuals": "The \"Input Units Visual Graphic\" visually shows the units and conversions made by admins. The \"Analyzed Units Visual Graphic\" visually shows the analysis OED does on the units and conversions entered by admins. Please visit {link} for further details and information.",
"help.admin.unitcreate": "This page allows admins to create units. Please visit {link} for further details and information.",
"help.admin.unitedit": "This page allows admins to edit units. Please visit {link} for further details and information.",
"help.admin.unitview": "This page shows information on units. Please visit {link} for further details and information.",
@@ -282,6 +283,10 @@ const LocaleTranslationData = {
"last.four.weeks": "Last four weeks",
"last.week": "Last week",
"leave": "Leave",
+ "legend.graph.text.meter": "Meter",
+ "legend.graph.text.suffix": "Suffix Analyzed",
+ "legend.graph.text.suffix.input": "Suffix Input",
+ "legend.graph.text.unit": "Unit",
"less.energy": "less energy",
"line": "Line",
"log.in": "Log in",
@@ -481,6 +486,7 @@ const LocaleTranslationData = {
"unit.type.of.unit": "Type of Unit:",
"unit.type.of.unit.suffix": "Added suffix will set type of unit to suffix",
"units": "Units",
+ "units.conversion.page.title": "Units and Conversions Visual Graphics",
"unsaved.failure": "Changes failed to save",
"unsaved.success": "Changes saved",
"unsaved.warning": "You have unsaved change(s). Are you sure you want to leave?",
@@ -512,6 +518,9 @@ const LocaleTranslationData = {
"uses": "uses",
"view.groups": "View Groups",
"visit": " or visit our ",
+ "visual.unit": "Units Visual Graphics",
+ "visual.input.units.graphic": "Input Units Visual Graphic",
+ "visual.analyzed.units.graphic": "Analyzed Units Visual Graphic",
"website": "website",
"week": "Week",
"yes": "yes",
@@ -737,6 +746,7 @@ const LocaleTranslationData = {
"help.admin.metercreate": "This page allows admins to create meters. Please visit {link} for further details and information.\u{26A1}",
"help.admin.meteredit": "This page allows admins to edit meters. Please visit {link} for further details and information.\u{26A1}",
"help.admin.meterview": "This page allows admins to view and edit meters. Please visit {link} for further details and information.\u{26A1}",
+ "help.admin.unitconversionvisuals": "The \"Input Units Visual Graphic\" visually shows the units and conversions made by admins. The \"Analyzed Units Visual Graphic\" visually shows the analysis OED does on the units and conversions entered by admins. Please visit {link} for further details and information.\u{26A1}",
"help.admin.unitcreate": "This page allows admins to create units. Please visit {link} for further details and information.\u{26A1}",
"help.admin.unitedit": "This page allows admins to edit units. Please visit {link} for further details and information.",
"help.admin.unitview": "This page shows information on units. Please visit {link} for further details and information.\u{26A1}",
@@ -794,6 +804,10 @@ const LocaleTranslationData = {
"last.four.weeks": "Quatre dernières semaines",
"last.week": "La semaine dernière",
"leave": "Leave\u{26A1}",
+ "legend.graph.text.meter": "Meter\u{26A1}",
+ "legend.graph.text.suffix": "Suffix Analyzed\u{26A1}",
+ "legend.graph.text.suffix.input": "Suffix Input\u{26A1}",
+ "legend.graph.text.unit": "Unit\u{26A1}",
"less.energy": "moins d'énergie",
"line": "Ligne",
"log.in": "Se Connecter",
@@ -993,6 +1007,7 @@ const LocaleTranslationData = {
"unit.type.of.unit": "Type of Unit:\u{26A1}",
"unit.type.of.unit.suffix": "Added suffix will set type of unit to suffix\u{26A1}",
"units": "Units\u{26A1}",
+ "units.conversion.page.title": "Units and Conversions Visual Graphics\u{26A1}",
"unsaved.failure": "Changes failed to save\u{26A1}",
"unsaved.success": "Changes saved\u{26A1}",
"unsaved.warning": "You have unsaved change(s). Are you sure you want to leave?\u{26A1}",
@@ -1024,6 +1039,9 @@ const LocaleTranslationData = {
"uses": "uses\u{26A1}",
"view.groups": "Visionner les groupes",
"visit": " ou visitez notre ",
+ "visual.unit": "Units Visual Graphics\u{26A1}",
+ "visual.input.units.graphic": "Graphique Visuel des Unités D'Entrée",
+ "visual.analyzed.units.graphic": "Graphique Visuel des Unités Analysées",
"website": "site web",
"week": "Semaine",
"yes": " yes\u{26A1}",
@@ -1250,6 +1268,7 @@ const LocaleTranslationData = {
"help.admin.metercreate": "Esta página permite a los administradores crear medidores. Por favor, visite {link} para más detalles e información.",
"help.admin.meteredit": "Esta página permite a los administradores editar medidores. Por favor, visite {link} para más detalles e información.",
"help.admin.meterview": "Esta página permite a los administradores ver y editar mediores. Por favor, visite {link} para más detalles e información.",
+ "help.admin.unitconversionvisuals": "El \"Gráfico Visual de Unidades de Entrada\" muestra visualmente las unidades y las conversiones ingresadas por los administradores. El \"Gráfico Visual de Unidades Analizadas\" muestra visualmente el análisis que realiza el OED en las unidades y las conversiones ingresadas por los administradores. Por favor, visite {link} para más detalles e información.",
"help.admin.unitcreate": "Esta página permite a los administradores crear unidades. Por favor, visite {link} para más detalles e información.",
"help.admin.unitedit": "Esta página permite a los administradores editar unidades Por favor, visite {link} para más detalles e información.",
"help.admin.unitview": "Esta página muestra información sobre unidades. Por favor, visite {link} para más detalles e información.",
@@ -1307,6 +1326,10 @@ const LocaleTranslationData = {
"last.four.weeks": "Últimas cuatro semanas",
"last.week": "La semana pasada",
"leave": "Salir",
+ "legend.graph.text.meter": "Medidor",
+ "legend.graph.text.suffix": "Sufijo Analizado",
+ "legend.graph.text.suffix.input": "Sufijo Entrado",
+ "legend.graph.text.unit": "Unidad",
"less.energy": "menos energía",
"line": "Línea",
"log.in": "Iniciar sesión",
@@ -1506,6 +1529,7 @@ const LocaleTranslationData = {
"unit.type.of.unit": "Tipo de unidad:",
"unit.type.of.unit.suffix": "El sufijo agregado determina que el tipo de la unidad es sufijo",
"units": "Unidades",
+ "units.conversion.page.title": "Gráficos Visuales de Unidades y Conversiones",
"unsaved.failure": "No se pudieron guardar los cambios",
"unsaved.success": "Se guardaron los cambios",
"unsaved.warning": "Tienes cambios sin guardar. ¿Estás seguro que quieres salir?",
@@ -1537,6 +1561,9 @@ const LocaleTranslationData = {
"uses": "uses\u{26A1}",
"view.groups": "Ver grupos",
"visit": " o visite nuestro ",
+ "visual.unit": "Units Visual Graphics\u{26A1}",
+ "visual.input.units.graphic": "Gráfico Visual de Unidades de Entrada",
+ "visual.analyzed.units.graphic": "Gráfico Visual de Unidades Analizadas",
"website": "sitio web",
"week": "semana",
"yes": "sí",