diff --git a/package-lock.json b/package-lock.json
index f2b1957427..942ea2d032 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,8 @@
"@babel/runtime-corejs3": "7.28.3",
"bcryptjs": "3.0.3",
"body-parser": "2.2.1",
+ "chart.js": "^4.5.0",
+ "chartjs-adapter-date-fns": "^3.0.0",
"commander": "13.1.0",
"connect-flash": "0.1.1",
"copy-to-clipboard": "3.3.3",
@@ -38,6 +40,7 @@
"qrcode": "1.5.4",
"react": "16.14.0",
"react-ace": "14.0.1",
+ "react-chartjs-2": "^5.3.0",
"react-dnd": "10.0.2",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "16.14.0",
@@ -187,7 +190,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -2171,12 +2173,14 @@
"node_modules/@codemirror/state": {
"version": "0.20.1",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz",
- "integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ=="
+ "integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==",
+ "peer": true
},
"node_modules/@codemirror/view": {
"version": "0.20.7",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz",
"integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==",
+ "peer": true,
"dependencies": {
"@codemirror/state": "^0.20.0",
"style-mod": "^4.0.0",
@@ -2281,7 +2285,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
},
@@ -2305,7 +2308,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -3938,15 +3940,23 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
"node_modules/@lezer/common": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz",
- "integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA=="
+ "integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==",
+ "peer": true
},
"node_modules/@lezer/highlight": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-0.16.0.tgz",
"integrity": "sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==",
+ "peer": true,
"dependencies": {
"@lezer/common": "^0.16.0"
}
@@ -3955,6 +3965,7 @@
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.16.3.tgz",
"integrity": "sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==",
+ "peer": true,
"dependencies": {
"@lezer/common": "^0.16.0"
}
@@ -4039,7 +4050,6 @@
"integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.1",
@@ -4696,7 +4706,6 @@
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
"integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==",
- "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@@ -4998,7 +5007,6 @@
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz",
"integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==",
"dev": true,
- "peer": true,
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0",
@@ -5652,7 +5660,6 @@
"resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz",
"integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==",
"dev": true,
- "peer": true,
"bin": {
"marked": "bin/marked.js"
},
@@ -5802,7 +5809,6 @@
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.12.tgz",
"integrity": "sha512-0mhiCR/4sZb00RVFJIUlMuiBkW3NMpVIW2Gse7noqEMoFGkvfPPAImEQbkBV8xga4KOPP4FdTRYuLLy32R1fPw==",
"dev": true,
- "peer": true,
"dependencies": {
"@semantic-release/commit-analyzer": "^11.0.0",
"@semantic-release/error": "^4.0.0",
@@ -6840,7 +6846,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -6860,7 +6865,6 @@
"version": "17.0.24",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.24.tgz",
"integrity": "sha512-eIpyco99gTH+FTI3J7Oi/OH8MZoFMJuztNRimDOJwH4iGIsKV2qkGnk4M9VzlaVWeEEWLWSQRy0FEA0Kz218cg==",
- "peer": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -7725,7 +7729,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -7784,7 +7787,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -8802,7 +8804,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -9041,6 +9042,28 @@
"integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==",
"license": "MIT"
},
+ "node_modules/chart.js": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
+ "node_modules/chartjs-adapter-date-fns": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
+ "integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "chart.js": ">=2.8.0",
+ "date-fns": ">=2.0.0"
+ }
+ },
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -9306,8 +9329,7 @@
"node_modules/codemirror": {
"version": "5.65.9",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.9.tgz",
- "integrity": "sha512-19Jox5sAKpusTDgqgKB5dawPpQcY+ipQK7xoEI+MVucEF9qqFaXpeqY1KaoyGBso/wHQoDa4HMMxMjdsS3Zzzw==",
- "peer": true
+ "integrity": "sha512-19Jox5sAKpusTDgqgKB5dawPpQcY+ipQK7xoEI+MVucEF9qqFaXpeqY1KaoyGBso/wHQoDa4HMMxMjdsS3Zzzw=="
},
"node_modules/codemirror-graphql": {
"version": "2.0.0",
@@ -9905,6 +9927,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -10301,8 +10334,7 @@
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz",
"integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==",
"dev": true,
- "license": "BSD-3-Clause",
- "peer": true
+ "license": "BSD-3-Clause"
},
"node_modules/diff-match-patch": {
"version": "1.0.5",
@@ -10931,7 +10963,6 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -13044,7 +13075,6 @@
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz",
"integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
@@ -14404,7 +14434,6 @@
"resolved": "https://registry.npmjs.org/jest/-/jest-30.0.4.tgz",
"integrity": "sha512-9QE0RS4WwTj/TtTC4h/eFVmFAhGNVerSB9XpJh8sqaXlP73ILcPcZ7JWjjEtJJe2m8QyBLKKfPQuK+3F+Xij/g==",
"dev": true,
- "peer": true,
"dependencies": {
"@jest/core": "30.0.4",
"@jest/types": "30.0.1",
@@ -15655,7 +15684,6 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -16306,7 +16334,6 @@
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"marked": "bin/marked.js"
},
@@ -19302,7 +19329,6 @@
"dev": true,
"inBundle": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -20431,7 +20457,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
@@ -21121,7 +21146,6 @@
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@@ -21159,6 +21183,16 @@
"pure-color": "^1.2.0"
}
},
+ "node_modules/react-chartjs-2": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz",
+ "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react-clientside-effect": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz",
@@ -21221,7 +21255,6 @@
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
"integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@@ -21290,8 +21323,7 @@
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "peer": true
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-json-view": {
"version": "1.21.3",
@@ -22121,7 +22153,6 @@
"integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -22256,7 +22287,6 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -22300,7 +22330,6 @@
"integrity": "sha512-6qGjWccl5yoyugHt3jTgztJ9Y0JVzyH8/Voc/D8PlLat9pwxQYXz7W1Dpnq5h0/G5GCYGUaDSlYcyk3AMh5A6g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/error": "^4.0.0",
@@ -24691,7 +24720,6 @@
"dev": true,
"inBundle": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -26024,7 +26052,8 @@
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
- "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
+ "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
+ "peer": true
},
"node_modules/stylus-lookup": {
"version": "6.1.0",
@@ -26499,7 +26528,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -26821,7 +26849,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -27275,7 +27302,8 @@
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
- "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
+ "peer": true
},
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
@@ -27358,7 +27386,6 @@
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -27408,7 +27435,6 @@
"integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.6.1",
"@webpack-cli/configtest": "^3.0.1",
diff --git a/package.json b/package.json
index d830c0b53d..bb084791fe 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,8 @@
"@babel/runtime-corejs3": "7.28.3",
"bcryptjs": "3.0.3",
"body-parser": "2.2.1",
+ "chart.js": "^4.5.0",
+ "chartjs-adapter-date-fns": "^3.0.0",
"commander": "13.1.0",
"connect-flash": "0.1.1",
"copy-to-clipboard": "3.3.3",
@@ -65,6 +67,7 @@
"qrcode": "1.5.4",
"react": "16.14.0",
"react-ace": "14.0.1",
+ "react-chartjs-2": "^5.3.0",
"react-dnd": "10.0.2",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "16.14.0",
diff --git a/src/components/ChartVisualization/ChartVisualization.react.js b/src/components/ChartVisualization/ChartVisualization.react.js
new file mode 100644
index 0000000000..cfd205c411
--- /dev/null
+++ b/src/components/ChartVisualization/ChartVisualization.react.js
@@ -0,0 +1,651 @@
+/*
+ * Copyright (c) 2016-present, Parse, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the license found in the LICENSE file in
+ * the root directory of this source tree.
+ */
+import PropTypes from 'lib/PropTypes';
+import React, { useMemo, useState } from 'react';
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ LineElement,
+ PointElement,
+ ArcElement,
+ Title,
+ Tooltip,
+ Legend,
+ TimeScale,
+} from 'chart.js';
+import { Bar, Line, Pie } from 'react-chartjs-2';
+import 'chartjs-adapter-date-fns';
+import styles from './ChartVisualization.scss';
+
+// Register necessary Chart.js components
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ LineElement,
+ PointElement,
+ ArcElement,
+ Title,
+ Tooltip,
+ Legend,
+ TimeScale
+);
+
+// Utility functions for chart data processing
+const validateInputData = (selectedData, selectedCells, data) => {
+ if (!selectedData || selectedData.length === 0 || !selectedCells || !data || !Array.isArray(data)) {
+ return false;
+ }
+
+ const { rowStart, rowEnd, colStart } = selectedCells;
+
+ // Check if we have valid data and if indices are valid
+ if (rowStart === -1 || colStart === -1 || rowEnd >= data.length || rowStart < 0) {
+ return false;
+ }
+
+ // Check if all row indices are valid
+ for (let rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) {
+ if (!data[rowIndex] || !data[rowIndex].attributes) {
+ return false; // Inconsistent data, abort
+ }
+ }
+
+ return true;
+};
+
+const detectTimeSeriesData = (selectedCells, data, order, columns) => {
+ const { rowStart, rowEnd, colStart, colEnd } = selectedCells;
+ let isTimeSeries = false;
+ let dateColumnName = null;
+ let dateColumnIndex = -1;
+
+ // Look for any date column in the selection (not just the first)
+ if (colEnd > colStart && columns) {
+ for (let colIndex = colStart; colIndex <= colEnd; colIndex++) {
+ const columnName = order[colIndex]?.name;
+ if (!columnName) {
+ continue;
+ }
+
+ // Check the column type in the schema
+ const columnType = columns[columnName]?.type;
+ const isDateColumn = columnType === 'Date' ||
+ /^(date|time|created|updated|when|at)$/i.test(columnName) ||
+ columnName.toLowerCase().includes('date') ||
+ columnName.toLowerCase().includes('time');
+
+ if (isDateColumn) {
+ // Check if the column actually contains valid dates
+ let dateCount = 0;
+ const totalRows = Math.min(3, rowEnd - rowStart + 1); // Check up to 3 rows
+
+ for (let rowIndex = rowStart; rowIndex < rowStart + totalRows; rowIndex++) {
+ // Check if the index is valid before accessing
+ if (rowIndex >= data.length || !data[rowIndex] || !data[rowIndex].attributes) {
+ continue;
+ }
+ const value = data[rowIndex].attributes[columnName];
+ if (value instanceof Date ||
+ (typeof value === 'string' && !isNaN(Date.parse(value)) && new Date(value).getFullYear() > 1900)) {
+ dateCount++;
+ }
+ }
+
+ if (dateCount >= totalRows * 0.6) { // 60% must be valid dates
+ isTimeSeries = true;
+ dateColumnName = columnName;
+ dateColumnIndex = colIndex;
+ break; // Found a valid date column
+ }
+ }
+ }
+ }
+
+ return { isTimeSeries, dateColumnName, dateColumnIndex };
+};
+
+const processTimeSeriesData = (selectedCells, data, order, dateColumnName, dateColumnIndex) => {
+ const { rowStart, rowEnd, colStart, colEnd } = selectedCells;
+ const datasets = [];
+ let datasetIndex = 0;
+
+ // Create a dataset for each numeric column (except the date column)
+ for (let colIndex = colStart; colIndex <= colEnd; colIndex++) {
+ // Skip the date column
+ if (colIndex === dateColumnIndex) {
+ continue;
+ }
+
+ const columnName = order[colIndex]?.name;
+ if (!columnName) {
+ continue;
+ }
+
+ const dataPoints = [];
+
+ for (let rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) {
+ // Check if the index is valid
+ if (rowIndex >= data.length || !data[rowIndex] || !data[rowIndex].attributes) {
+ continue;
+ }
+ const timeValue = data[rowIndex].attributes[dateColumnName];
+ const numericValue = data[rowIndex].attributes[columnName];
+
+ if (timeValue && typeof numericValue === 'number' && !isNaN(numericValue)) {
+ dataPoints.push({
+ x: new Date(timeValue),
+ y: numericValue
+ });
+ }
+ }
+
+ if (dataPoints.length > 0) {
+ datasets.push({
+ label: columnName,
+ data: dataPoints,
+ borderColor: `hsl(${datasetIndex * 60}, 70%, 50%)`,
+ backgroundColor: `hsla(${datasetIndex * 60}, 70%, 50%, 0.1)`,
+ tension: 0.1
+ });
+ datasetIndex++;
+ }
+ }
+
+ return {
+ type: 'timeSeries',
+ datasets,
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ x: {
+ type: 'time',
+ time: {
+ displayFormats: {
+ day: 'MMM dd',
+ hour: 'HH:mm'
+ }
+ },
+ title: {
+ display: true,
+ text: dateColumnName
+ }
+ },
+ y: {
+ title: {
+ display: true,
+ text: 'Value'
+ }
+ }
+ },
+ plugins: {
+ title: {
+ display: true,
+ text: 'Time Series Visualization'
+ },
+ legend: {
+ display: datasets.length > 1
+ }
+ }
+ }
+ };
+};
+
+const processNumericSeriesData = (selectedCells, data, order, columns, chartType) => {
+ const { rowStart, rowEnd, colStart, colEnd } = selectedCells;
+ const labels = [];
+ const dataPoints = [];
+
+ // If multiple columns, create separate datasets for each column
+ if (colEnd > colStart) {
+ const datasets = [];
+
+ for (let colIndex = colStart; colIndex <= colEnd; colIndex++) {
+ const columnName = order[colIndex]?.name;
+ if (!columnName) {
+ continue;
+ }
+
+ // Collect all values from this column
+ const columnValues = [];
+
+ for (let rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) {
+ // Check if the index is valid
+ if (rowIndex >= data.length || !data[rowIndex] || !data[rowIndex].attributes) {
+ continue;
+ }
+ const value = data[rowIndex].attributes[columnName];
+ if (typeof value === 'number' && !isNaN(value)) {
+ columnValues.push(value);
+ }
+ }
+
+ if (columnValues.length > 0) {
+ datasets.push({
+ label: columnName,
+ data: columnValues,
+ backgroundColor: `hsla(${(colIndex - colStart) * 60}, 70%, 60%, 0.8)`,
+ borderColor: `hsl(${(colIndex - colStart) * 60}, 70%, 50%)`,
+ borderWidth: 2,
+ borderRadius: chartType === 'bar' ? 4 : 0,
+ tension: chartType === 'line' ? 0.4 : 0
+ });
+ }
+ }
+
+ // Use labels from the first column (all should have the same number of rows)
+ for (let rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) {
+ labels.push(`Row ${rowIndex + 1}`);
+ }
+
+ return {
+ type: 'numberSeries',
+ data: {
+ labels,
+ datasets
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ interaction: {
+ intersect: false,
+ },
+ plugins: {
+ title: {
+ display: true,
+ text: 'Selected Data Visualization',
+ font: { size: 16, weight: 'bold' },
+ color: '#333'
+ },
+ legend: {
+ display: datasets.length > 1 // Show legend if multiple columns
+ },
+ tooltip: {
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
+ titleColor: '#fff',
+ bodyColor: '#fff',
+ borderColor: '#169cee',
+ borderWidth: 1
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ title: { display: true, text: 'Value', font: { size: 14, weight: 'bold' }, color: '#555' },
+ grid: { color: 'rgba(0, 0, 0, 0.1)' },
+ ticks: { color: '#666' }
+ },
+ x: {
+ title: { display: true, text: 'Categories', font: { size: 14, weight: 'bold' }, color: '#555' },
+ grid: { color: 'rgba(0, 0, 0, 0.1)' },
+ ticks: { color: '#666' }
+ }
+ }
+ }
+ };
+ } else {
+ // Single column: use row indices as labels
+ const columnName = order[colStart]?.name;
+ if (columnName) {
+ for (let rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) {
+ // Check if the index is valid
+ if (rowIndex >= data.length || !data[rowIndex] || !data[rowIndex].attributes) {
+ continue;
+ }
+ labels.push(`Row ${rowIndex + 1}`);
+ const value = data[rowIndex].attributes[columnName];
+ dataPoints.push(typeof value === 'number' && !isNaN(value) ? value : 0);
+ }
+ }
+
+ if (labels.length === 0 || dataPoints.length === 0) {
+ return null;
+ }
+
+ return {
+ type: 'numberSeries',
+ data: {
+ labels,
+ datasets: [{
+ label: 'Selected Values',
+ data: dataPoints,
+ backgroundColor: chartType === 'bar'
+ ? dataPoints.map((_, index) => `hsla(${index * 360 / dataPoints.length}, 70%, 60%, 0.8)`)
+ : 'rgba(22, 156, 238, 0.7)',
+ borderColor: chartType === 'bar'
+ ? dataPoints.map((_, index) => `hsl(${index * 360 / dataPoints.length}, 70%, 50%)`)
+ : 'rgba(22, 156, 238, 1)',
+ borderWidth: 2,
+ borderRadius: chartType === 'bar' ? 4 : 0,
+ tension: chartType === 'line' ? 0.4 : 0
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ interaction: {
+ intersect: false,
+ },
+ plugins: {
+ title: {
+ display: true,
+ text: 'Selected Data Visualization',
+ font: { size: 16, weight: 'bold' },
+ color: '#333'
+ },
+ legend: {
+ display: false // Single column doesn't need legend
+ },
+ tooltip: {
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
+ titleColor: '#fff',
+ bodyColor: '#fff',
+ borderColor: '#169cee',
+ borderWidth: 1
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ title: { display: true, text: 'Value', font: { size: 14, weight: 'bold' }, color: '#555' },
+ grid: { color: 'rgba(0, 0, 0, 0.1)' },
+ ticks: { color: '#666' }
+ },
+ x: {
+ title: { display: true, text: 'Categories', font: { size: 14, weight: 'bold' }, color: '#555' },
+ grid: { color: 'rgba(0, 0, 0, 0.1)' },
+ ticks: { color: '#666' }
+ }
+ }
+ }
+ };
+ }
+};
+
+const ChartVisualization = ({
+ selectedData,
+ selectedCells,
+ data,
+ order,
+ columns
+}) => {
+ const [chartType, setChartType] = useState('bar');
+
+ // Process selected data to determine the type of visualization
+ const chartData = useMemo(() => {
+ if (!validateInputData(selectedData, selectedCells, data)) {
+ return null;
+ }
+
+ const timeSeriesInfo = detectTimeSeriesData(selectedCells, data, order, columns);
+
+ if (timeSeriesInfo.isTimeSeries) {
+ return processTimeSeriesData(selectedCells, data, order, timeSeriesInfo.dateColumnName, timeSeriesInfo.dateColumnIndex);
+ } else {
+ return processNumericSeriesData(selectedCells, data, order, columns, chartType);
+ }
+ }, [selectedData, selectedCells, data, order, columns, chartType]);
+
+ const renderChart = () => {
+ // Safety check to prevent crashes
+ if (!chartData) {
+ return null;
+ }
+
+ if (chartData.type === 'timeSeries') {
+ return (
+
No positive values for pie chart
No valid data selected for charting.
+Please select numeric or date columns to visualize.
+