diff --git a/react-graph/package-lock.json b/react-graph/package-lock.json index 77b002e1e..6bd865c90 100644 --- a/react-graph/package-lock.json +++ b/react-graph/package-lock.json @@ -1,16 +1,18 @@ { "name": "@gpa-gemstone/react-graph", - "version": "1.0.30", + "version": "1.0.51", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@gpa-gemstone/react-graph", - "version": "1.0.30", + "version": "1.0.51", "license": "MIT", "dependencies": { - "@gpa-gemstone/gpa-symbols": "0.0.26", - "@gpa-gemstone/helper-functions": "0.0.20", + "@gpa-gemstone/gpa-symbols": "0.0.34", + "@gpa-gemstone/helper-functions": "0.0.30", + "@gpa-gemstone/react-table": "^1.2.44", + "html2canvas": "^1.4.1", "lodash": "^4.17.21", "moment": "^2.29.4", "react": "^18.2.0" @@ -26,7 +28,7 @@ "jest": "^27.0.6", "prettier": "^2.3.2", "ts-jest": "^27.0.4", - "typescript": "4.4.4" + "typescript": "5.5.3" } }, "node_modules/@ampproject/remapping": { @@ -728,9 +730,9 @@ } }, "node_modules/@gpa-gemstone/gpa-symbols": { - "version": "0.0.26", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/gpa-symbols/-/gpa-symbols-0.0.26.tgz", - "integrity": "sha512-KReSXpKvWN0VfPfUPSU8x7eJXL1sUk8tzWrLBhElE5OyNxZFT+Cn333zFGXmKt21EEbTZrinTwN+qXAk/ns83A==", + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/gpa-symbols/-/gpa-symbols-0.0.34.tgz", + "integrity": "sha512-tP7d8fbt+G+AqQVYiYjIXm5ildUYMAaRdgGH+E01Ryg3ecCeSbAhfafOOQqCNRiozlyBOLEKplwGJbX565iEEA==", "dependencies": { "@babel/preset-typescript": "^7.14.5", "babel-jest": "^27.0.6", @@ -743,9 +745,25 @@ } }, "node_modules/@gpa-gemstone/helper-functions": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/helper-functions/-/helper-functions-0.0.20.tgz", - "integrity": "sha512-1KD3XGT0MBVKu0Fj0x2oI8XMllaHx3Q2pMujFV8OfCi4cl3wvwtOgHf8bLxQ5CyhuYwJd8LZYrYrBYeTCOk2fQ==" + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/helper-functions/-/helper-functions-0.0.30.tgz", + "integrity": "sha512-oqJsXOrrh+5GgOE1pnirDDSDkpq/dyWU81taYwXi9EdgemEWva9PraZ9LKvxtF6D1oKLZuVaKrow9wbE2NfjoA==", + "dependencies": { + "react": "^18.2.0" + } + }, + "node_modules/@gpa-gemstone/react-table": { + "version": "1.2.44", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-table/-/react-table-1.2.44.tgz", + "integrity": "sha512-zYpeu71KZb0WwhaQm/tnnXowguebuOvg4R95j07LmHNQWY5+zwIoylkZH8YYnnGTxpEeOTCxXCYlXgYYg1Fqjw==", + "dependencies": { + "@gpa-gemstone/gpa-symbols": "0.0.34", + "@gpa-gemstone/helper-functions": "0.0.30", + "@types/lodash": "^4.14.171", + "@types/react": "^17.0.14", + "lodash": "^4.17.21", + "react": "^18.2.0" + } }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", @@ -1575,8 +1593,7 @@ "node_modules/@types/lodash": { "version": "4.14.192", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.192.tgz", - "integrity": "sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==", - "dev": true + "integrity": "sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==" }, "node_modules/@types/mocha": { "version": "9.0.0", @@ -1597,14 +1614,12 @@ "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/react": { "version": "17.0.58", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.58.tgz", "integrity": "sha512-c1GzVY97P0fGxwGxhYq989j4XwlcHQoto6wQISOC2v6wm3h0PORRWJFHlkRjfGsiG3y1609WdQ+J+tKxvrEd6A==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1614,8 +1629,7 @@ "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, "node_modules/@types/semver": { "version": "7.5.0", @@ -2483,6 +2497,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -2750,6 +2772,14 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -2774,8 +2804,7 @@ "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/data-urls": { "version": "2.0.0", @@ -3786,6 +3815,18 @@ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", @@ -6846,6 +6887,14 @@ "node": ">=8" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -7027,16 +7076,16 @@ } }, "node_modules/typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/universalify": { @@ -7089,6 +7138,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", @@ -8001,9 +8058,9 @@ "dev": true }, "@gpa-gemstone/gpa-symbols": { - "version": "0.0.26", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/gpa-symbols/-/gpa-symbols-0.0.26.tgz", - "integrity": "sha512-KReSXpKvWN0VfPfUPSU8x7eJXL1sUk8tzWrLBhElE5OyNxZFT+Cn333zFGXmKt21EEbTZrinTwN+qXAk/ns83A==", + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/gpa-symbols/-/gpa-symbols-0.0.34.tgz", + "integrity": "sha512-tP7d8fbt+G+AqQVYiYjIXm5ildUYMAaRdgGH+E01Ryg3ecCeSbAhfafOOQqCNRiozlyBOLEKplwGJbX565iEEA==", "requires": { "@babel/preset-typescript": "^7.14.5", "babel-jest": "^27.0.6", @@ -8016,9 +8073,25 @@ } }, "@gpa-gemstone/helper-functions": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/helper-functions/-/helper-functions-0.0.20.tgz", - "integrity": "sha512-1KD3XGT0MBVKu0Fj0x2oI8XMllaHx3Q2pMujFV8OfCi4cl3wvwtOgHf8bLxQ5CyhuYwJd8LZYrYrBYeTCOk2fQ==" + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/helper-functions/-/helper-functions-0.0.30.tgz", + "integrity": "sha512-oqJsXOrrh+5GgOE1pnirDDSDkpq/dyWU81taYwXi9EdgemEWva9PraZ9LKvxtF6D1oKLZuVaKrow9wbE2NfjoA==", + "requires": { + "react": "^18.2.0" + } + }, + "@gpa-gemstone/react-table": { + "version": "1.2.44", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-table/-/react-table-1.2.44.tgz", + "integrity": "sha512-zYpeu71KZb0WwhaQm/tnnXowguebuOvg4R95j07LmHNQWY5+zwIoylkZH8YYnnGTxpEeOTCxXCYlXgYYg1Fqjw==", + "requires": { + "@gpa-gemstone/gpa-symbols": "0.0.34", + "@gpa-gemstone/helper-functions": "0.0.30", + "@types/lodash": "^4.14.171", + "@types/react": "^17.0.14", + "lodash": "^4.17.21", + "react": "^18.2.0" + } }, "@humanwhocodes/config-array": { "version": "0.11.10", @@ -8669,8 +8742,7 @@ "@types/lodash": { "version": "4.14.192", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.192.tgz", - "integrity": "sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==", - "dev": true + "integrity": "sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==" }, "@types/mocha": { "version": "9.0.0", @@ -8691,14 +8763,12 @@ "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "@types/react": { "version": "17.0.58", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.58.tgz", "integrity": "sha512-c1GzVY97P0fGxwGxhYq989j4XwlcHQoto6wQISOC2v6wm3h0PORRWJFHlkRjfGsiG3y1609WdQ+J+tKxvrEd6A==", - "dev": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -8708,8 +8778,7 @@ "@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, "@types/semver": { "version": "7.5.0", @@ -9347,6 +9416,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==" + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -9538,6 +9612,14 @@ "which": "^2.0.1" } }, + "css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "requires": { + "utrie": "^1.0.2" + } + }, "cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -9561,8 +9643,7 @@ "csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "data-urls": { "version": "2.0.0", @@ -10289,6 +10370,15 @@ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" }, + "html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "requires": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + } + }, "http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", @@ -12476,6 +12566,14 @@ "minimatch": "^3.0.4" } }, + "text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "requires": { + "utrie": "^1.0.2" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -12599,9 +12697,9 @@ } }, "typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true }, "universalify": { @@ -12635,6 +12733,14 @@ "requires-port": "^1.0.0" } }, + "utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "requires": { + "base64-arraybuffer": "^1.0.2" + } + }, "v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", diff --git a/react-graph/package.json b/react-graph/package.json index a6e236ab5..73b3c5f6c 100644 --- a/react-graph/package.json +++ b/react-graph/package.json @@ -4,8 +4,10 @@ "description": "Interactive UI Components for GPA products", "main": "lib/index.js", "types": "lib/index.d.ts", - "files": ["lib/**/*"], - "scripts": { + "files": [ + "lib/**/*" + ], + "scripts": { "test": "jest --config jestconfig.json", "build": "tsc", "format": "prettier --write \"src/**/*.tsx\"", @@ -16,11 +18,11 @@ "version": "npm run format && git add -A src", "postversion": "git push && git push --tags" }, - "repository": { + "repository": { "type": "git", "url": "https://github.com/GridProtectionAlliance/gpa-gemstone.git" }, - "keywords": [ + "keywords": [ "React", "Interactive", "GSF", @@ -29,7 +31,9 @@ ], "author": "GridProtectionAlliance", "license": "MIT", - "bugs": {"url": "https://github.com/GridProtectionAlliance/gpa-gemstone/issues"}, + "bugs": { + "url": "https://github.com/GridProtectionAlliance/gpa-gemstone/issues" + }, "homepage": "https://github.com/GridProtectionAlliance/gpa-gemstone#readme", "devDependencies": { "@types/jest": "^27.0.0", @@ -47,10 +51,13 @@ "dependencies": { "@gpa-gemstone/gpa-symbols": "0.0.34", "@gpa-gemstone/helper-functions": "0.0.30", + "@gpa-gemstone/react-table": "^1.2.44", "html2canvas": "^1.4.1", "lodash": "^4.17.21", "moment": "^2.29.4", "react": "^18.2.0" }, - "publishConfig": {"access": "public"} -} \ No newline at end of file + "publishConfig": { + "access": "public" + } +} diff --git a/react-graph/src/Bar/Bar.tsx b/react-graph/src/Bar/Bar.tsx new file mode 100644 index 000000000..186018ee8 --- /dev/null +++ b/react-graph/src/Bar/Bar.tsx @@ -0,0 +1,239 @@ +// ****************************************************************************************************** +// Bar.tsx - Gbtc +// +// Copyright © 2020, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 03/28/2024 - Preston Crawford +// Generated original version of source code. +// +// ****************************************************************************************************** + +import * as React from 'react'; +import { GraphContext, AxisIdentifier, AxisMap } from '../GraphContext'; +import { PointNode } from '../PointNode'; +import DataLegend from '../DataLegend'; +import { CreateGuid } from '@gpa-gemstone/helper-functions'; +import { IBarContext, IBarDataSeries } from './BarGroup'; +import { IInteractionData } from '../WhiskerLine' + +interface IProps { + /** + * Array of data points to be represented by bars, each point as a [x, y] tuple. + */ + Data: [number, number][], + /** + * Color of the bars. + */ + Color: string, + /** + * Identifier for the axis the bars are associated with. + * @type {AxisIdentifier} + */ + Axis?: AxisIdentifier, + /** + * Legend text for the bars. + */ + Legend?: string, + /** + * Minimum width of the bars. + */ + MinWidth?: number, + /** + * Maximum width of the bars. + */ + MaxWidth?: number, + /** + * Opacity of the bars. + */ + Opacity?: number, + /** + * Stroke color of the bars. + */ + StrokeColor?: string, + /** + * Stroke width of the bars. + */ + StrokeWidth?: number, +} + +interface IContextlessProps { + Context: IBarContext + BarProps: IProps +} + +export const ContexlessBar = (props: IContextlessProps) => { + const [guid, setGuid] = React.useState(""); + const [dataGuid, setDataGuid] = React.useState(""); + + const [enabled, setEnabled] = React.useState(true); + const [data, setData] = React.useState(null); + const [visibleData, setVisibleData] = React.useState<[...number[]][]>([]); + + const createLegend = React.useCallback(() => { + if (props.BarProps.Legend === undefined) + return undefined; + + return ; + }, [props.BarProps.Color, enabled, dataGuid]); + + const createContextData = React.useCallback(() => { + const contextData: IBarDataSeries = { + legend: createLegend(), + axis: props.BarProps.Axis, + enabled: enabled, + getMax: (t: [number, number]) => (data == null || !enabled ? -Infinity : data.GetLimits(t[0], t[1])[1]), + getMin: (t: [number, number]) => (data == null || !enabled ? Infinity : data.GetLimits(t[0], t[1])[0]), + getPoints: (t: number, n?: number | undefined) => (data == null || !enabled ? undefined : data.GetPoints(t, n ?? 1)) + }; + + if (props.Context.YTransformation != null) + contextData.getPoint = (t) => (data == null || !enabled ? undefined : data.GetPoint(t)); + + return contextData as IBarDataSeries; + }, [props.BarProps.Axis, enabled, dataGuid, createLegend, props.Context.YTransformation]); + + React.useEffect(() => { + if (guid === "") + return; + props.Context.UpdateData(guid, createContextData(), props.BarProps.Legend); + }, [createContextData]); + + React.useEffect(() => { + setDataGuid(CreateGuid()); + }, [data]); + + React.useEffect(() => { + if (props.BarProps.Data == null || props.BarProps.Data.length === 0) + setData(null); + else + setData(new PointNode(props.BarProps.Data)); + }, [props.BarProps.Data]); + + React.useEffect(() => { + if (guid === "") + return; + props.Context.SetLegend(guid, createLegend()); + }, [enabled]); + + React.useEffect(() => { + if (data == null) { + setVisibleData([]); + return; + } + setVisibleData(data.GetData(props.Context.XDomain[0], props.Context.XDomain[1], true)); + }, [dataGuid, props.Context.XDomain[0], props.Context.XDomain[1]]) + + React.useEffect(() => { + const id = props.Context.AddData(createContextData(), props.BarProps.Legend); + setGuid(id); + return () => { props.Context.RemoveData(id) } + }, []); + + const Bars = React.useMemo(() => { + if (visibleData.length === 0 || !enabled) return <> + + const baseYPosition = props.Context.YTransformation(0, AxisMap.get(props.BarProps.Axis)); + const barWidth = getBarWidth(visibleData, props.Context, props.BarProps.MinWidth, props.BarProps.MaxWidth) + + return visibleData.map((pt, index) => { + const [xValue, yValue] = pt; + let height = baseYPosition - props.Context.YTransformation(yValue, AxisMap.get(props.BarProps.Axis)); + if (isNaN(height) || height > 9999) + height = 0 + + const xPosition = props.Context.XTransformation(xValue); + let yPosition = props.Context.GetYPosition != null ? props.Context.GetYPosition(xValue, guid, props.BarProps.Axis) : baseYPosition - height + yPosition = sanitizeYPosition(yPosition, height, baseYPosition, yValue); + + return ( + + ); + }); + + }, [visibleData, props.Context.YTransformation, props.Context.XTransformation, createContextData, enabled, props.Context.DataGuid]); + + return {Bars} +} + +//Helper functions +const getBarWidth = (data: [...number[]][], context: IBarContext, minWidth: number | undefined, maxWidth: number | undefined) => { + // Calculate intervals between points for bar width + const intervals = []; + if (data.length === 1 || data.length === 2) { + intervals.push(50); // if one bar just use 50 for now.. + } else { + for (let i = 0; i < data.length - 1; i++) { + const currentX = context.XTransformation(data[i][0]); + const nextX = context.XTransformation(data[i + 1][0]); + intervals.push(nextX - currentX); + } + } + + // Determine the bar width as the smallest interval + let calculatedBarWidth = Math.min(...intervals); + + if (minWidth != null && calculatedBarWidth < minWidth) + calculatedBarWidth = minWidth + + if (maxWidth != null && calculatedBarWidth > maxWidth) + calculatedBarWidth = maxWidth; + return calculatedBarWidth; +} + +const sanitizeYPosition = (yPosition: number, height: number, baseYPosition: number, yValue: number) => { + let sanitizedYPosition = yPosition; + if (yPosition === undefined) + sanitizedYPosition = baseYPosition - height + + //When negative yVal just use baseY for yPosition for now + if (yValue < 0) { + height = Math.abs(height) + sanitizedYPosition = baseYPosition + } + + if (isNaN(yPosition)) + sanitizedYPosition = -999 + + return sanitizedYPosition; +} + +/** + Renders multiple bars with the ability to turn them off and on +*/ +const Bar = (props: IProps) => { + const context = React.useContext(GraphContext); + return +} + +export default Bar; + diff --git a/react-graph/src/Bar/BarGroup.tsx b/react-graph/src/Bar/BarGroup.tsx new file mode 100644 index 000000000..90c4ae771 --- /dev/null +++ b/react-graph/src/Bar/BarGroup.tsx @@ -0,0 +1,236 @@ +// ****************************************************************************************************** +// BarGroup.tsx - Gbtc +// +// Copyright © 2020, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 06/18/2024 - Preston Crawford +// Generated original version of source code. +// +// ****************************************************************************************************** + +import * as React from 'react'; +import Bar, { ContexlessBar } from './Bar'; +import { IDataSeries, GraphContext, IGraphContext, AxisIdentifier, AxisMap } from '../GraphContext'; +import Infobox from '../Infobox'; +import { CreateGuid } from '@gpa-gemstone/helper-functions'; +import { ReactTable } from '@gpa-gemstone/react-table'; + +interface IProps { + /** + * Flag to determine if infobox is shown when hovering a stacked bar. + */ + ShowHoverInfoBox?: boolean, + /** + * Background color of the hoverable infobox table. + */ + HoverColor?: string, +} + +interface IContextData { + getPoint?: (xVal: number) => number[] | undefined, + enabled: boolean, + legendName?: string +} + +interface IHoverData { + Name?: string, + Value: number +} + +export interface IBarContext extends IGraphContext { + GetYPosition?: (xVal: number, barGuid: string, axis: AxisIdentifier | undefined) => number, + AddData: (d: IBarDataSeries, legendName?: string) => string, + UpdateData: (key: string, d: IBarDataSeries, legendName?: string) => void, +} + +export interface IBarDataSeries extends IDataSeries { + getPoint?: (xVal: number) => number[] | undefined +} + +/** + Wraps bar components to vertically stacks bars with matching x values. +*/ +const BarGroup: React.FC = (props) => { + const context = React.useContext(GraphContext); + + const guid = React.useRef(CreateGuid()); + const hoverRef = React.useRef(null); + const map = React.useRef>(new Map()); + + const [barGuids, setBarGuids] = React.useState([]); + const [hoverData, setHoverData] = React.useState(null); + + const GetYPosition = (xVal: number, barGuid: string, axis: AxisIdentifier): number | undefined => { + const matchedGUID = barGuids.find(uid => uid === barGuid); + if (matchedGUID == null) return; + + const dataSeries = map.current.get(barGuid); + if (dataSeries == null || dataSeries.getPoint == null) return; + let data = dataSeries.getPoint(xVal) + + if (data == null) return; + + //Not supporting negative values for now + const yValue = data[1] + if (yValue < 0) return; + + const baseY = context.YTransformation(0, AxisMap.get(axis)); + const height = baseY - context.YTransformation(yValue, AxisMap.get(axis)) + let totalHeight = 0; + + for (let index = 0; index < barGuids.length; index++) { + const guid = barGuids[index]; + + if (matchedGUID === guid) + break; + + const dataSeries = map.current.get(guid); + if (dataSeries == null || !dataSeries.enabled || dataSeries?.getPoint == null) continue; + + let data = dataSeries.getPoint(xVal); + if (data == null) continue; + + const yVal = context.YTransformation(data[1], AxisMap.get(axis)); + const height = baseY - yVal; + + totalHeight += height; + } + + const yPosition = baseY - totalHeight - height + if (isNaN(yPosition)) + return; + + return baseY - totalHeight - height + }; + + //Effect to set hoverData + React.useEffect(() => { + if ((props.ShowHoverInfoBox ?? false) === false) return; + if (barGuids.length === 0 || isNaN(context.XHover) || context.XHover > context.XDomain[1] || context.XHover < context.XDomain[0]) { + setHoverData(null); + return; + } + const points: IHoverData[] = []; + + barGuids.forEach(guid => { + const contextData = map.current.get(guid); + if (contextData == null || contextData?.getPoint == null) return; + let point = contextData.getPoint(context.XHover); + if (point == null) return; + points.push({ Name: contextData.legendName, Value: point[1] }) + }) + + if (points.length === 0) { + setHoverData(null); + return; + } + + setHoverData(points) + }, [context.XHover, context.YHover]) + + const hoverContent = React.useMemo(() => { + if (hoverData == null || hoverData.length === 0) return <> + return ( +
+ + Data={hoverData} + SortKey='' + Ascending={false} + OnSort={() => {/* nothing */ }} + KeySelector={row => row.Name ?? row.Value} + RowStyle={{ whiteSpace: 'nowrap' }} + > + + Key={`Name`} + Field={'Name'} + AllowSort={false} + > + {'\u200B'} + + + Key={`Value`} + Field={`Value`} + AllowSort={false} + RowStyle={{ textAlign: 'right' }} + > + {'\u200B'} + + +
+ ) + }, [hoverData]) + + const addData = (d: IBarDataSeries, legendName?: string): string => { + const guid = context.AddData(d); + map.current.set(guid, { enabled: d.enabled, legendName, getPoint: d.getPoint }); + setBarGuids(guids => [...guids, guid]) + return guid; + } + + const updateData = (guid: string, d: IBarDataSeries, legendName?: string) => { + context.UpdateData(guid, d); + map.current.set(guid, { enabled: d.enabled, legendName, getPoint: d.getPoint }) + } + + const barContext = React.useMemo(() => { + return { + ...context, + GetYPosition: GetYPosition, + AddData: addData, + UpdateData: updateData + } as IBarContext; + }, [context]); + + return ( + + {React.Children.map(props.children, (element) => { + if (!React.isValidElement(element)) return null; + + if (element.type === Bar) { + return ( + + ); + } + })} + {hoverData != null ? + +
{hoverContent}
+
+ : null} +
+ ); +} + +const getOrigin = (hoverRef: HTMLDivElement | null,context: IBarContext) => { + if(hoverRef == null) return 'upper-left'; + + const middleOfXDomain = (context.XDomain[0] + context.XDomain[1]) / 2 + if(context.XHover > middleOfXDomain) + return 'upper-right' + return 'upper-left' +} + +const getYHoverPosition = (hoverRef: HTMLDivElement | null, context: IBarContext): number => { + if(hoverRef == null) return context.YHover[0]; + const middleOfPlot = (context.YDomain[0][0] + context.YDomain [0][1]) / 2; + return middleOfPlot +} + +export default BarGroup; diff --git a/react-graph/src/LineLegend.tsx b/react-graph/src/DataLegend.tsx similarity index 91% rename from react-graph/src/LineLegend.tsx rename to react-graph/src/DataLegend.tsx index fc47e4647..44dcf3bce 100644 --- a/react-graph/src/LineLegend.tsx +++ b/react-graph/src/DataLegend.tsx @@ -28,10 +28,12 @@ import { GetTextWidth, GetTextHeight, CreateGuid } from '@gpa-gemstone/helper-f import { Warning } from '@gpa-gemstone/gpa-symbols'; import { ILegendRequiredProps, LegendContext } from './LegendContext'; +type LegendStyle = LineStyle | 'bar' + export interface IProps extends ILegendRequiredProps { label: string, color: string, - lineStyle: LineStyle, + legendStyle: LegendStyle, setEnabled: (arg: boolean) => void, hasNoData: boolean } @@ -39,7 +41,7 @@ const fontFamily = `-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetic const nonTextualWidth = 45; const cssStyle = `margin: auto auto auto 0px; display: inline-block; font-weight: 400; font-family: ${fontFamily};` -function LineLegend(props: IProps) { +function DataLegend(props: IProps) { const [label, setLabel] = React.useState(props.label); const [legendWidth, setLegendWith] = React.useState(100); const [legendHeight, setLegendHeight] = React.useState(100); @@ -87,8 +89,9 @@ function LineLegend(props: IProps) { return (
props.setEnabled(!props.enabled)} style={{ width: '100%', display: 'flex', alignItems: 'center', marginRight: '5px', height: '100%' }}> - {(props.lineStyle === '-' ? + {(props.legendStyle === '-' ?
: + props.legendStyle === 'bar' ?
:
)} @@ -97,4 +100,4 @@ function LineLegend(props: IProps) { ); } -export default LineLegend; \ No newline at end of file +export default DataLegend; \ No newline at end of file diff --git a/react-graph/src/Infobox.tsx b/react-graph/src/Infobox.tsx index c10c4d63e..32bbebc02 100644 --- a/react-graph/src/Infobox.tsx +++ b/react-graph/src/Infobox.tsx @@ -24,96 +24,136 @@ import * as React from 'react'; import { AxisIdentifier, AxisMap, GraphContext, IHandlers } from './GraphContext'; +export type origin = ("upper-right" | "upper-left" | "upper-center" | "lower-right" | "lower-left" | "lower-center") + interface IProps { - // Specifies the upper left corner of the box (or other spots depending on origin) - x: number, - y: number, - usePixelPositioning?: boolean, - disallowSnapping?: boolean, - axis?: AxisIdentifier, - origin?: "upper-right" | "upper-left" | "upper-center" | "lower-right" | "lower-left" | "lower-center", - // Specifies the offset of the pox from the origin point, In pixels - offset?: number, - // Dom ID of child, used for sizing of child - childId: string, - opacity?: number, - // Function to move box - setPosition?: (x: number, y: number) => void, - onMouseMove?: (x: number, y: number) => void + /** + * Specifies the X coordinate of the upper left corner of the box (or other spots depending on origin). + */ + X: number, + /** + * Specifies the Y coordinate of the upper left corner of the box (or other spots depending on origin). + */ + Y: number, + /** + * Determines if the positioning should be based on pixel values. + */ + UsePixelPositioning?: boolean, + /** + * If true, snapping of the box to grid or other elements is disallowed. + */ + DisallowSnapping?: boolean, + /** + * Identifier for the axis the box is associated with. + */ + Axis?: AxisIdentifier, + /** + * Specifies the origin point for positioning the box. + */ + Origin?: origin, + /** + * Specifies the offset of the box from the origin point, in pixels. + */ + Offset?: number, + /** + * DOM ID of the child element, used for sizing the child. + */ + ChildID: string, + /** + * Opacity of the box. + */ + Opacity?: number, + /** + * Function to set the position of the box. + */ + SetPosition?: (x: number, y: number) => void, + /** + * Event handler for mouse move events. + */ + OnMouseMove?: (x: number, y: number) => void, +} + +interface IGraphicProps { + X: number, + Y: number, + Width: number, + Height: number, + Opacity?: number } +const offsetDefault = 0; +const widthPadding = 3; + const Infobox: React.FunctionComponent = (props) => { const context = React.useContext(GraphContext); const [isSelected, setSelected] = React.useState(false); - const [position, setPosition] = React.useState<{x: number, y: number}>({x: props.x, y: props.y}); - const [dimension, setDimensions] = React.useState<{width: number, height: number}>({width: 100, height: 100}); + const [position, setPosition] = React.useState<{ x: number, y: number }>({ x: props.X, y: props.Y }); + const [size, setSize] = React.useState<{ width: number, height: number }>({ width: 100, height: 100 }); const [guid, setGuid] = React.useState(""); - const offsetDefault = 0; - + // Functions const calculateX = React.useCallback((xArg: number) => { - let x: number = (props.usePixelPositioning ?? false) ? context.XApplyPixelOffset(xArg) : context.XTransformation(xArg); + let x: number = (props.UsePixelPositioning ?? false) ? context.XApplyPixelOffset(xArg) : context.XTransformation(xArg); // Convert x/y to upper-left corner - switch(props.origin) { + switch (props.Origin) { case "lower-right": case "upper-right": { - x -= (dimension.width + (props.offset ?? offsetDefault)); + x -= (size.width + (props.Offset ?? offsetDefault)); break; } case "lower-center": case "upper-center": { - x -= Math.floor(dimension.width / 2); + x -= Math.floor(size.width / 2); break; } // Do-nothing case - case undefined: + case undefined: case "lower-left": case "upper-left": - x += props.offset ?? offsetDefault; + x += props.Offset ?? offsetDefault; break; } return x; - }, [context.XApplyPixelOffset, context.XTransformation, props.origin, props.offset, props.usePixelPositioning, dimension]); - + }, [context.XApplyPixelOffset, context.XTransformation, props.Origin, props.Offset, props.UsePixelPositioning, size]); + const calculateY = React.useCallback((yArg: number) => { - let y: number = (props.usePixelPositioning ?? false) ? context.YApplyPixelOffset(yArg) : context.YTransformation(yArg, AxisMap.get(props.axis)); + let y: number = (props.UsePixelPositioning ?? false) ? context.YApplyPixelOffset(yArg) : context.YTransformation(yArg, AxisMap.get(props.Axis)); // Convert x/y to upper-left corner - switch(props.origin) { - case undefined: + switch (props.Origin) { + case undefined: case "upper-left": case "upper-right": case "upper-center": - y += props.offset ?? offsetDefault; + y += props.Offset ?? offsetDefault; break; case "lower-left": case "lower-right": case "lower-center": - y -= (dimension.height + (props.offset ?? offsetDefault)); + y -= (size.height + (props.Offset ?? offsetDefault)); break; } return y; - }, [context.YApplyPixelOffset, context.YTransformation, props.origin, props.offset, props.usePixelPositioning, props.axis, dimension]); - + }, [context.YApplyPixelOffset, context.YTransformation, props.Origin, props.Offset, props.UsePixelPositioning, props.Axis, size]); + const onClick = React.useCallback((xArg: number, yArg: number) => { - const xP = calculateX(props.x); + const xP = calculateX(props.X); const xT = context.XTransformation(xArg); - const yP = calculateY(props.y); - const yT = context.YTransformation(yArg, AxisMap.get(props.axis)); - if (xT <= xP + dimension.width && xT >= xP && yT <= yP + dimension.height && yT >= yP) { + const yP = calculateY(props.Y); + const yT = context.YTransformation(yArg, AxisMap.get(props.Axis)); + if (xT <= xP + size.width && xT >= xP && yT <= yP + size.height && yT >= yP) { setSelected(true); } - }, [props.x, props.y, calculateX, calculateY, dimension, setSelected, context.XTransformation, context.YTransformation, props.axis]); - - // Note: this is the only function not effected by usePixelPositioning - const onMove = props.onMouseMove === undefined ? undefined : React.useCallback((xArg: number, yArg: number) => { - if (props.onMouseMove !== undefined) props.onMouseMove(xArg, yArg); - }, [props.onMouseMove]); + }, [props.X, props.Y, calculateX, calculateY, size, setSelected, context.XTransformation, context.YTransformation, props.Axis]); + // Note: this is the only function not effected by usePixelPositioning + const onMove = props.OnMouseMove === undefined ? undefined : React.useCallback((xArg: number, yArg: number) => { + if (props.OnMouseMove !== undefined) props.OnMouseMove(xArg, yArg); + }, [props.OnMouseMove]); // useEffect React.useEffect(() => { const id = context.RegisterSelect({ - axis: props.axis, + axis: props.Axis, allowSnapping: false, onRelease: (_) => setSelected(false), onPlotLeave: (_) => setSelected(false), @@ -123,82 +163,82 @@ const Infobox: React.FunctionComponent = (props) => { setGuid(id) return () => { context.RemoveSelect(id) } }, []); - + React.useEffect(() => { if (guid === "") return; - + context.UpdateSelect(guid, { - axis: props.axis, + axis: props.Axis, allowSnapping: false, onRelease: (_) => setSelected(false), onPlotLeave: (_) => setSelected(false), onClick, onMove } as IHandlers) - }, [onClick, onMove, props.axis]); - + }, [onClick, onMove, props.Axis]); + React.useEffect(() => { - setPosition({x: props.x, y: props.y}); - }, [props.x, props.y]); + setPosition({ x: props.X, y: props.Y }); + }, [props.X, props.Y]); React.useEffect(() => { - if (props.setPosition === undefined) + if (props.SetPosition === undefined) return; - if (!isSelected && (props.x !== position.x || props.y !== position.y)) - props.setPosition(position.x, position.y); + if (!isSelected && (props.X !== position.x || props.Y !== position.y)) + props.SetPosition(position.x, position.y); }, [isSelected, position]); - + React.useEffect(() => { if (context.CurrentMode !== 'select') setSelected(false); - },[context.CurrentMode]); - + }, [context.CurrentMode]); + React.useEffect(() => { - if (isSelected && !(props.disallowSnapping ?? false)) - setPosition({x: context.XHoverSnap, y: context.YHoverSnap[AxisMap.get(props.axis)]}); - }, [context.XHoverSnap, context.YHoverSnap, props.axis]); - + if (isSelected && !(props.DisallowSnapping ?? false)) + setPosition({ x: context.XHoverSnap, y: context.YHoverSnap[AxisMap.get(props.Axis)] }); + }, [context.XHoverSnap, context.YHoverSnap, props.Axis]); + React.useEffect(() => { - if (isSelected && (props.disallowSnapping ?? false)) - setPosition({x: context.XHover, y: context.YHover[AxisMap.get(props.axis)]}); - }, [context.XHover, context.YHover, props.axis]); - + if (isSelected && (props.DisallowSnapping ?? false)) + setPosition({ x: context.XHover, y: context.YHover[AxisMap.get(props.Axis)] }); + }, [context.XHover, context.YHover, props.Axis]); + // Get Heights and Widths React.useEffect(() => { - const domEle = document.getElementById(props.childId); - if (domEle == null) { - console.error(`Invalid element id passed for child element in infobox ${props.childId}`); - setDimensions({width: 100, height: 100}); - return; - } - if (dimension.width === Math.ceil(domEle.clientWidth) && dimension.height === Math.ceil(domEle.clientHeight)) return; - setDimensions({width: Math.ceil(domEle.clientWidth), height: Math.ceil(domEle.clientHeight)}); - }, [props.children, props.childId]); + const newSize = getSize(props.ChildID); + if (newSize.width - widthPadding !== size.width || newSize.height !== size.height) + setSize(newSize); + + }, [props.children, props.ChildID]); return ( - - + + {props.children} - {props.setPosition !== undefined && (props.x !== position.x || props.y !== position.y) ? - + {props.SetPosition !== undefined && (props.X !== position.x || props.Y !== position.y) ? + : null} ); } -interface IGraphicProps { - x: number, - y: number, - width: number, - height: number, - opacity?: number -} const InfoGraphic: React.FunctionComponent = (props) => { return ( - + ); } +//helper functions +const getSize = (childID: string): { width: number, height: number } => { + const childElement = document.getElementById(childID); + if (childElement == null) { + console.error(`Invalid element id passed for child element in infobox ${childID}`); + return { width: 100, height: 100 }; + } + const width = Math.ceil(childElement.clientWidth) + widthPadding + const height = Math.ceil(childElement.clientHeight) + return { width, height } +} export default Infobox; diff --git a/react-graph/src/Line.tsx b/react-graph/src/Line.tsx index f93d8fdda..74120addd 100644 --- a/react-graph/src/Line.tsx +++ b/react-graph/src/Line.tsx @@ -26,7 +26,7 @@ import * as React from 'react'; import {IDataSeries, GraphContext, LineStyle, AxisIdentifier, AxisMap, LineMap} from './GraphContext'; import * as moment from 'moment'; import {PointNode} from './PointNode'; -import LineLegend from './LineLegend'; +import DataLegend from './DataLegend'; import { CreateGuid } from '@gpa-gemstone/helper-functions'; @@ -66,8 +66,8 @@ function Line(props: IProps) { if ((props.highlightHover ?? false) && !isNaN(highlight[0]) && !isNaN(highlight[1])) txt = txt + ` (${moment.utc(highlight[0]).format('MM/DD/YY hh:mm:ss')}: ${highlight[1].toPrecision(6)})` - return ; }, [props.color, props.lineStyle, enabled, data]); diff --git a/react-graph/src/LineWithThreshold.tsx b/react-graph/src/LineWithThreshold.tsx index 71a6c7cac..753097593 100644 --- a/react-graph/src/LineWithThreshold.tsx +++ b/react-graph/src/LineWithThreshold.tsx @@ -29,7 +29,7 @@ import * as moment from 'moment'; import {PointNode} from './PointNode'; import {GetTextWidth} from '@gpa-gemstone/helper-functions'; import {IProps as ILineProps} from './Line'; -import LineLegend from './LineLegend'; +import DataLegend from './DataLegend'; export interface IProps extends ILineProps { threshHolds: IThreshold[], @@ -119,8 +119,8 @@ function LineWithThreshold(props: IProps) { if ((props.highlightHover ?? false) && !isNaN(highlight[0]) && !isNaN(highlight[1])) txt = txt + ` (${moment.utc(highlight[0]).format('MM/DD/YY hh:mm:ss')}: ${highlight[1].toPrecision(6)})` - return ; } diff --git a/react-graph/src/Plot.tsx b/react-graph/src/Plot.tsx index e61fb1beb..7e6779872 100644 --- a/react-graph/src/Plot.tsx +++ b/react-graph/src/Plot.tsx @@ -1,4 +1,4 @@ -// ****************************************************************************************************** +// ****************************************************************************************************** // Plot.tsx - Gbtc // // Copyright © 2020, Grid Protection Alliance. All Rights Reserved. @@ -35,6 +35,9 @@ import Legend from './Legend'; import LineWithThreshold from './LineWithThreshold'; import Line from './Line'; import Button from './Button'; +import Bar from './Bar/Bar'; +import BarGroup from './Bar/BarGroup'; +import WhiskerLine from './WhiskerLine'; import HorizontalMarker from './HorizontalMarker'; import VerticalMarker from './VerticalMarker'; import SymbolicMarker from './SymbolicMarker'; @@ -679,11 +682,13 @@ const Plot: React.FunctionComponent = (props) => { } applyToYDomain(zoomYAxis); } - setMousePosition([ptTransform.x, ptTransform.y]); + if(!_.isEqual(mousePosition, [ptTransform.x, ptTransform.y])) + setMousePosition([ptTransform.x, ptTransform.y]); // Here on mouse is snapped (if neccessary) let ptFinal: {x: number, y: number}; if (props.snapMouse ?? false) ptFinal = snapMouseToClosestSeries(ptTransform); else ptFinal = ptTransform; + if(!_.isEqual(mousePositionSnap, [ptFinal.x, ptFinal.y])) setMousePositionSnap([ptFinal.x, ptFinal.y]); if (handlers.current.size > 0) handlers.current.forEach((v) => (v.onMove !== undefined? v.onMove(xInvTransform(v.allowSnapping ? ptFinal.x : ptTransform.x), yInvTransform(v.allowSnapping ? ptFinal.y : ptTransform.y, v.axis)) : null)); @@ -715,8 +720,8 @@ const Plot: React.FunctionComponent = (props) => { xInvTransform(ptFinal.x), [...AxisMap.values()].map(axis => yInvTransform(ptFinal.y, axis)), { - setTDomain: updateXDomain as React.SetStateAction<[number,number]>, - setYDomain: updateYDomain as React.SetStateAction<[number,number][]> + setTDomain: updateXDomain as React.SetStateAction<[number,number]>, + setYDomain: updateYDomain as React.SetStateAction<[number,number][]> }); if (handlers.current.size > 0 && selectedMode === 'select') handlers.current.forEach((v) => (v.onClick !== undefined? v.onClick(xInvTransform(v.allowSnapping ? ptFinal.x : ptTransform.x), yInvTransform(v.allowSnapping ? ptFinal.y : ptTransform.y, v.axis)) : null)); @@ -850,13 +855,13 @@ const Plot: React.FunctionComponent = (props) => { - {React.Children.map(props.children, (element) => { + {React.Children.map(props.children, (element) => { if (!React.isValidElement(element)) return null; if ((element as React.ReactElement).type === Line || (element as React.ReactElement).type === LineWithThreshold || (element as React.ReactElement).type === Infobox || (element as React.ReactElement).type === HorizontalMarker || (element as React.ReactElement).type === VerticalMarker || (element as React.ReactElement).type === SymbolicMarker || (element as React.ReactElement).type === Circle || (element as React.ReactElement).type === AggregatingCircles || (element as React.ReactElement).type === HeatMapChart - ) + || (element as React.ReactElement).type === Bar || (element as React.ReactElement).type === BarGroup || (element as React.ReactElement).type === WhiskerLine) return element; return null; })} diff --git a/react-graph/src/PointNode.tsx b/react-graph/src/PointNode.tsx index dd32648b2..a00e3a8aa 100644 --- a/react-graph/src/PointNode.tsx +++ b/react-graph/src/PointNode.tsx @@ -53,10 +53,15 @@ export class PointNode { this.points = null; if (data.length <= MaxPoints) { - if (data.some((point)=> point.length != this.dim)) throw new TypeError(`Jagged data passed to PointNode. All points should all be ${this.dim} dimensions.`) + if (data.some(point => point.length != this.dim)) throw new TypeError(`Jagged data passed to PointNode. All points should all be ${this.dim} dimensions.`) this.points = data; - for (let index = 1; index < this.dim; index++) this.minV[index-1] = Math.min(...data.filter(pt => !isNaN(pt[index])).map(pt => pt[index])); - for (let index = 1; index < this.dim; index++) this.maxV[index-1] = Math.max(...data.filter(pt => !isNaN(pt[index])).map(pt => pt[index])); + + for (let index = 1; index < this.dim; index++) { + const values = data.filter(pt => !isNaN(pt[index])).map(pt => pt[index]); + this.minV[index - 1] = Math.min(...values); + this.maxV[index - 1] = Math.max(...values); + this.avgV[index - 1] = values.reduce((sum, val) => sum + val, 0) / values.length; + } return; } @@ -69,8 +74,12 @@ export class PointNode { this.children.push(new PointNode(data.slice(index, index + blockSize))); index = index + blockSize; } - for (let index = 0; index < this.dim-1; index++) this.minV[index] = Math.min(...this.children.map(node => node.minV[index])); - for (let index = 0; index < this.dim-1; index++) this.maxV[index] = Math.max(...this.children.map(node => node.maxV[index])); + + for (let index = 0; index < this.dim-1; index++){ + this.minV[index] = Math.min(...this.children.map(node => node.minV[index])); + this.maxV[index] = Math.max(...this.children.map(node => node.maxV[index])); + this.avgV[index] = Math.max(...this.children.map(node => node.avgV[index])); + } } public GetData(Tstart: number, Tend: number, IncludeEdges?: boolean): [...number[]][] { @@ -218,4 +227,9 @@ export class PointNode { } else throw new RangeError(`Both children and points are null for PointNode, unabled to find point with time value of ${tVal}`); } + + public AggregateData = (tStart: number, tEnd: number, numPoints: number): [...number[]] => { + const center = ( tStart + tEnd ) / 2; + return this.GetPoint(center); + } } diff --git a/react-graph/src/WhiskerLine.tsx b/react-graph/src/WhiskerLine.tsx new file mode 100644 index 000000000..4f9e2e462 --- /dev/null +++ b/react-graph/src/WhiskerLine.tsx @@ -0,0 +1,279 @@ +// ****************************************************************************************************** +// Bar.tsx - Gbtc +// +// Copyright © 2020, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 03/28/2024 - Preston Crawford +// Generated original version of source code. +// +// ****************************************************************************************************** + +import * as React from 'react'; +import { IDataSeries, GraphContext, IHandlers, AxisIdentifier, AxisMap } from './GraphContext'; +import { PointNode } from './PointNode'; +import DataLegend from './DataLegend'; +import { CreateGuid } from '@gpa-gemstone/helper-functions'; +import Infobox, { origin } from './Infobox'; + +interface IProps { + /** + * Array of data points to be represented by whisker lines, each point as a [x, y[]] tuple. + */ + Data: [number, number[]][], + /** + * Identifier for the axis the whisker lines are associated with. + * @type {AxisIdentifier} + */ + Axis?: AxisIdentifier, + /** + * Legend text. + */ + Legend?: string, + /** + * First color will be used to color the whiskerlines, legend, and the first and last index for yValues. The second color will be used to color all values in between the first and last yValue. + */ + Colors?: [string, string] + /** + * Flag to determine if infobox is shown when hovering a whisker line. + */ + ShowHoverInfoBox?: boolean, + /** + * Flag to determine if infobox is shown when a point is clicked. + */ + ShowClickInfoBox?: boolean + /** + * Array of names used in the infobox. Each y-value index corresponds to the index in this array. + */ + Names?: string[] +} + +export interface IInteractionData { + XPosition: number, + YPosition: number, + Content: { Value: number, Name: string }[], + Origin: origin +} + +export const WhiskerLine = (props: IProps) => { + const context = React.useContext(GraphContext); + + const [guid, setGuid] = React.useState(""); + const [dataGuid, setDataGuid] = React.useState(""); + + const [enabled, setEnabled] = React.useState(true); + const [data, setData] = React.useState(null); + const [visibleData, setVisibleData] = React.useState<[...number[]][]>([]); + + const [hoverData, setHoverData] = React.useState(null); + const [clickData, setClickData] = React.useState(null); + + const createLegend = React.useCallback(() => { + if (props.Legend === undefined) + return undefined; + + return ; + }, [enabled, data]); + + const createContextData = React.useCallback(() => { + return { + legend: createLegend(), + axis: props.Axis, + enabled: enabled, + getMax: (t) => (data == null || !enabled ? -Infinity : data.GetLimits(t[0], t[1], data.dim)[1]), + getMin: (t) => (data == null || !enabled ? Infinity : data.GetLimits(t[0], t[1], data.dim)[0]), + getPoints: (t, n?) => (data == null || !enabled ? NaN : data.GetPoints(t, n ?? 1)) + } as IDataSeries; + }, [props.Axis, enabled, dataGuid, createLegend]); + + React.useEffect(() => { + if (guid === "") + return; + context.UpdateData(guid, createContextData()); + }, [createContextData]); + + React.useEffect(() => { + setDataGuid(CreateGuid()); + }, [data]); + + // Set up a click handler if provided in props + React.useEffect(() => { + if (guid === "" || props.ShowClickInfoBox === undefined) + return; + context.RegisterSelect({ onClick } as IHandlers) + context.UpdateSelect(guid, { onClick } as IHandlers) + }, [props.ShowClickInfoBox, context.UpdateFlag]) + + const onClick = (x: number, y: number) => { + if ((props.ShowHoverInfoBox ?? false) === false) return; + const isDataInValid = data == null || props.Data == null || props.Data.length === 0; + if (isNaN(x) || isNaN(y) || isDataInValid || context.CurrentMode === 'select') { + setClickData(null) + return; + } + + setClickData(handleDataInteraction(data, x, context.XDomain, context.YDomain[0], props.Names)) + } + + React.useEffect(() => { + if ((props.ShowHoverInfoBox ?? false) === false) return; + const isDataInValid = data == null || props.Data == null || props.Data.length === 0; + + if (isDataInValid || isNaN(context.XHover)) { + setHoverData(null); + return; + } + + setHoverData(handleDataInteraction(data, context.XHover, context.XDomain, context.YDomain[0], props.Names)) + }, [context.XHover, context.YHoverSnap, data]) + + React.useEffect(() => { + if (props.Data == null || props.Data.length === 0) + setData(null); + else + setData(new PointNode(props.Data.map(d => [d[0], ...d[1]]))); + }, [props.Data]); + + React.useEffect(() => { + if (guid === "") + return; + context.SetLegend(guid, createLegend()); + }, [enabled]); + + React.useEffect(() => { + if (data == null) { + setVisibleData([]); + return; + } + setVisibleData(data.GetData(context.XDomain[0], context.XDomain[1], true)); + }, [data, context.XDomain[0], context.XDomain[1]]) + + React.useEffect(() => { + const id = context.AddData(createContextData()); + setGuid(id); + return () => { context.RemoveData(id) } + }, []); + + const Whiskers = React.useMemo(() => { + if (visibleData.length === 0 || !enabled) return <>; + + return visibleData.map((pt, index) => { + const x = context.XTransformation(pt[0]); + const yValues = pt.slice(1).flat(); + const bottomPoint = context.YTransformation(yValues[0], AxisMap.get(props.Axis)) + const topPoint = context.YTransformation(yValues[yValues.length - 1], AxisMap.get(props.Axis)) + + const circles = yValues.map((yValue, index) => { + const y = context.YTransformation(yValue, AxisMap.get(props.Axis)) + let color = 'black' + const isFirstOrLastIndex = (index === 0 || index === yValues.length - 1) + + if (props.Colors?.[0] != null && isFirstOrLastIndex) + color = props.Colors[0] + + if (props.Colors?.[1] != null && !isFirstOrLastIndex) + color = props.Colors[1]; + + return ( + + ); + }); + + return ( + + + {circles} + + ); + }); + }, [visibleData, context.YTransformation, context.XTransformation, props.Axis, props.Colors, enabled, context.DataGuid]); + + return ( + <> + {Whiskers} + {hoverData != null ? + +

+ {hoverData.Content.map((d, index) => ( + <> + {d.Name}{d.Value.toFixed(3)} + {index < hoverData.Content.length - 1 ?
: null} + + ))} +

+
+ : null} + {clickData != null && context.CurrentMode === 'select' ? + +

+ {clickData.Content.map((d, index) => ( + <> + {d.Name}{d.Value.toFixed(3)} + {index < clickData.Content.length - 1 ?
: null} + + ))} +

+
+ : null} + + ); +} + +const handleDataInteraction = (data: PointNode, xValue: number, xDomain: [number, number], yDomain: [number, number], names?: string[]): IInteractionData | null => { + const point = data.GetPoint(xValue); + if (point === null) + return null; + + const yVals = point.slice(1); + + if (yVals == null) + return null; + + const content = yVals.map((yVal, index) => ({ + Name: names != null ? names[index] + ': ' : "", + Value: yVal + })) + + const middleYOfPlot = (yDomain[0] + yDomain[1]) / 2 + const middleOfXDomain = (xDomain[0] + xDomain[1]) / 2 + let origin = 'upper-left' + if (xValue > middleOfXDomain) + origin = 'upper-right' + + return { Content: content, XPosition: point[0], YPosition: middleYOfPlot, Origin: origin as origin } +} + +export default WhiskerLine; + diff --git a/react-graph/src/index.ts b/react-graph/src/index.ts index 3d1fb4665..2c2cb04e0 100644 --- a/react-graph/src/index.ts +++ b/react-graph/src/index.ts @@ -10,6 +10,9 @@ import AggregatingCircles from './AggregatingCircles'; import Circle from './Circle'; import Infobox from './Infobox'; import { AxisMap } from './GraphContext'; +import Bar from './Bar/Bar' +import BarGroup from './Bar/BarGroup'; +import WhiskerLine from './WhiskerLine'; export { Plot, @@ -23,5 +26,8 @@ export { Circle, AggregatingCircles, Infobox, - AxisMap + AxisMap, + Bar, + BarGroup, + WhiskerLine };