diff --git a/package-lock.json b/package-lock.json index d594daa563..958c9955be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,11 +34,7 @@ "recharts": "^3.0.2", "styled-components": "^5.2.1", "styled-system": "^5.1.5", - "uuid": "^13.0.0", - "vega": "^5.17.3", - "vega-embed": "6.0.0", - "vega-lite": "5.0.0", - "vega-tooltip": "0.27.0" + "uuid": "^13.0.0" }, "devDependencies": { "@babel/cli": "^7.17.10", @@ -5402,11 +5398,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/clone": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@types/clone/-/clone-2.1.4.tgz", - "integrity": "sha512-NKRWaEGaVGVLnGLB2GazvDaZnyweW9FJLLFL5LhywGJB3aqGMT9R/EUoJoSRP4nzofYnZysuDmrEJtJdAqUOtQ==" - }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", @@ -5512,21 +5503,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/fast-json-stable-stringify": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@types/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.2.tgz", - "integrity": "sha512-vsxcbfLDdjytnCnHXtinE40Xl46Wr7l/VGRGt7ewJwCPMKEHOdEsTxXX8xwgoR7cbc+6dE8SB4jlMrOV2zAg7g==", - "deprecated": "This is a stub types definition. fast-json-stable-stringify provides its own type definitions, so you do not need this installed.", - "dependencies": { - "fast-json-stable-stringify": "*" - } - }, - "node_modules/@types/geojson": { - "version": "7946.0.4", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.4.tgz", - "integrity": "sha512-MHmwBtCb7OCv1DSivz2UNJXPGU/1btAWRKlqJ2saEhVJkpkvqHMMaOpKg0v4sAbDWSQekHGvPVMM8nQ+Jen03Q==", + "dev": true, "license": "MIT" }, "node_modules/@types/history": { @@ -5655,12 +5632,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", - "dev": true - }, "node_modules/@types/node": { "version": "22.8.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz", @@ -6856,6 +6827,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -6864,6 +6836,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -6918,14 +6891,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-flat-polyfill": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-flat-polyfill/-/array-flat-polyfill-1.0.1.tgz", - "integrity": "sha512-hfJmKupmQN0lwi0xG6FQ5U8Rd97RnIERplymOv/qpq8AoNKPPAnxJadjFA23FNWm88wykh9HmpLJUUwUtNU/iw==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/array-includes": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", @@ -7790,14 +7755,6 @@ "node": ">= 10.0" } }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "engines": { - "node": ">=0.8" - } - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -7820,6 +7777,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -7830,7 +7788,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/colorette": { "version": "2.0.20", @@ -8194,73 +8153,6 @@ "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==", - "license": "ISC", - "dependencies": { - "delaunator": "5" - }, - "engines": { - "node": ">=12" - } - }, - "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==", - "license": "ISC", - "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==", - "license": "ISC", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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", @@ -8270,20 +8162,6 @@ "node": ">=12" } }, - "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==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-format": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", @@ -8292,57 +8170,6 @@ "node": ">=12" } }, - "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==", - "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo-projection": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz", - "integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==", - "license": "ISC", - "dependencies": { - "commander": "7", - "d3-array": "1 - 3", - "d3-geo": "1.12.0 - 3" - }, - "bin": { - "geo2svg": "bin/geo2svg.js", - "geograticule": "bin/geograticule.js", - "geoproject": "bin/geoproject.js", - "geoquantize": "bin/geoquantize.js", - "geostitch": "bin/geostitch.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo-projection/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==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "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==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", @@ -8362,15 +8189,6 @@ "node": ">=12" } }, - "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==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", @@ -8386,19 +8204,6 @@ "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==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", @@ -8618,15 +8423,6 @@ "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==", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -9182,6 +8978,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "engines": { "node": ">=6" } @@ -10524,15 +10321,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-equals": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", - "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", - "engines": { - "node": ">=6.0.0" - } + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-glob": { "version": "3.3.2", @@ -10557,15 +10347,11 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-json-patch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", - "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -10994,6 +10780,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -12069,6 +11856,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "engines": { "node": ">=8" } @@ -13994,11 +13782,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/json-stringify-pretty-compact": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz", - "integrity": "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -15272,48 +15055,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -16886,6 +16627,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -16978,12 +16720,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", - "license": "Unlicense" - }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -17026,12 +16762,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, "node_modules/safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -17096,7 +16826,8 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true }, "node_modules/saxes": { "version": "6.0.0", @@ -17142,6 +16873,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -17610,6 +17342,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -17658,12 +17391,14 @@ "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/string-width/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -18396,26 +18131,6 @@ "node": ">=8.0" } }, - "node_modules/topojson-client": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", - "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", - "license": "ISC", - "dependencies": { - "commander": "2" - }, - "bin": { - "topo2geo": "bin/topo2geo", - "topomerge": "bin/topomerge", - "topoquantize": "bin/topoquantize" - } - }, - "node_modules/topojson-client/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, "node_modules/tough-cookie": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", @@ -19017,537 +18732,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/vega": { - "version": "5.33.0", - "resolved": "https://registry.npmjs.org/vega/-/vega-5.33.0.tgz", - "integrity": "sha512-jNAGa7TxLojOpMMMrKMXXBos4K6AaLJbCgGDOw1YEkLRjUkh12pcf65J2lMSdEHjcEK47XXjKiOUVZ8L+MniBA==", - "license": "BSD-3-Clause", - "dependencies": { - "vega-crossfilter": "~4.1.3", - "vega-dataflow": "~5.7.7", - "vega-encode": "~4.10.2", - "vega-event-selector": "~3.0.1", - "vega-expression": "~5.2.0", - "vega-force": "~4.2.2", - "vega-format": "~1.1.3", - "vega-functions": "~5.18.0", - "vega-geo": "~4.4.3", - "vega-hierarchy": "~4.1.3", - "vega-label": "~1.3.1", - "vega-loader": "~4.5.3", - "vega-parser": "~6.6.0", - "vega-projection": "~1.6.2", - "vega-regression": "~1.3.1", - "vega-runtime": "~6.2.1", - "vega-scale": "~7.4.2", - "vega-scenegraph": "~4.13.1", - "vega-statistics": "~1.9.0", - "vega-time": "~2.1.3", - "vega-transforms": "~4.12.1", - "vega-typings": "~1.5.0", - "vega-util": "~1.17.2", - "vega-view": "~5.16.0", - "vega-view-transforms": "~4.6.1", - "vega-voronoi": "~4.2.4", - "vega-wordcloud": "~4.1.6" - } - }, - "node_modules/vega-canvas": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/vega-canvas/-/vega-canvas-1.2.7.tgz", - "integrity": "sha512-OkJ9CACVcN9R5Pi9uF6MZBF06pO6qFpDYHWSKBJsdHP5o724KrsgR6UvbnXFH82FdsiTOff/HqjuaG8C7FL+9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/vega-crossfilter": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/vega-crossfilter/-/vega-crossfilter-4.1.3.tgz", - "integrity": "sha512-nyPJAXAUABc3EocUXvAL1J/IWotZVsApIcvOeZaUdEQEtZ7bt8VtP2nj3CLbHBA8FZZVV+K6SmdwvCOaAD4wFQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.2", - "vega-dataflow": "^5.7.7", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-dataflow": { - "version": "5.7.7", - "resolved": "https://registry.npmjs.org/vega-dataflow/-/vega-dataflow-5.7.7.tgz", - "integrity": "sha512-R2NX2HvgXL+u4E6u+L5lKvvRiCtnE6N6l+umgojfi53suhhkFP+zB+2UAQo4syxuZ4763H1csfkKc4xpqLzKnw==", - "license": "BSD-3-Clause", - "dependencies": { - "vega-format": "^1.1.3", - "vega-loader": "^4.5.3", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-embed": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/vega-embed/-/vega-embed-6.0.0.tgz", - "integrity": "sha512-CH3/FtiVFek5SmQCLbZVXbpslHIjCH7D0xNbfK6UrtKt4Mr1AmmsCo4rH3L4qqXqDwK6w9Vv4sNrF/6HOH8sDg==", - "dependencies": { - "fast-json-patch": "^3.0.0-1", - "json-stringify-pretty-compact": "^2.0.0", - "semver": "^6.3.0", - "vega-schema-url-parser": "^1.1.0", - "vega-themes": "^2.5.0", - "vega-tooltip": "^0.19.1" - }, - "peerDependencies": { - "vega": "^5.7.1", - "vega-lite": "*" - } - }, - "node_modules/vega-embed/node_modules/vega-tooltip": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/vega-tooltip/-/vega-tooltip-0.19.1.tgz", - "integrity": "sha512-BNZ5T866SLOai+NZyGxg60U6hZhNINHuX313/z1TrUTeCprYLfCR1Ex4qRozY1WPY3HfxQcd5czLJMhoAFDotQ==", - "dependencies": { - "vega-util": "^1.11.1" - } - }, - "node_modules/vega-encode": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/vega-encode/-/vega-encode-4.10.2.tgz", - "integrity": "sha512-fsjEY1VaBAmqwt7Jlpz0dpPtfQFiBdP9igEefvumSpy7XUxOJmDQcRDnT3Qh9ctkv3itfPfI9g8FSnGcv2b4jQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.2", - "d3-interpolate": "^3.0.1", - "vega-dataflow": "^5.7.7", - "vega-scale": "^7.4.2", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-event-selector": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-3.0.1.tgz", - "integrity": "sha512-K5zd7s5tjr1LiOOkjGpcVls8GsH/f2CWCrWcpKy74gTCp+llCdwz0Enqo013ZlGaRNjfgD/o1caJRt3GSaec4A==", - "license": "BSD-3-Clause" - }, - "node_modules/vega-expression": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.2.0.tgz", - "integrity": "sha512-WRMa4ny3iZIVAzDlBh3ipY2QUuLk2hnJJbfbncPgvTF7BUgbIbKq947z+JicWksYbokl8n1JHXJoqi3XvpG0Zw==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/estree": "^1.0.0", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-force": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/vega-force/-/vega-force-4.2.2.tgz", - "integrity": "sha512-cHZVaY2VNNIG2RyihhSiWniPd2W9R9kJq0znxzV602CgUVgxEfTKtx/lxnVCn8nNrdKAYrGiqIsBzIeKG1GWHw==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-force": "^3.0.0", - "vega-dataflow": "^5.7.7", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-format": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/vega-format/-/vega-format-1.1.3.tgz", - "integrity": "sha512-wQhw7KR46wKJAip28FF/CicW+oiJaPAwMKdrxlnTA0Nv8Bf7bloRlc+O3kON4b4H1iALLr9KgRcYTOeXNs2MOA==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.2", - "d3-format": "^3.1.0", - "d3-time-format": "^4.1.0", - "vega-time": "^2.1.3", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-functions": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/vega-functions/-/vega-functions-5.18.0.tgz", - "integrity": "sha512-+D+ey4bDAhZA2CChh7bRZrcqRUDevv05kd2z8xH+il7PbYQLrhi6g1zwvf8z3KpgGInFf5O13WuFK5DQGkz5lQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.2", - "d3-color": "^3.1.0", - "d3-geo": "^3.1.0", - "vega-dataflow": "^5.7.7", - "vega-expression": "^5.2.0", - "vega-scale": "^7.4.2", - "vega-scenegraph": "^4.13.1", - "vega-selections": "^5.6.0", - "vega-statistics": "^1.9.0", - "vega-time": "^2.1.3", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-geo": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/vega-geo/-/vega-geo-4.4.3.tgz", - "integrity": "sha512-+WnnzEPKIU1/xTFUK3EMu2htN35gp9usNZcC0ZFg2up1/Vqu6JyZsX0PIO51oXSIeXn9bwk6VgzlOmJUcx92tA==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.2", - "d3-color": "^3.1.0", - "d3-geo": "^3.1.0", - "vega-canvas": "^1.2.7", - "vega-dataflow": "^5.7.7", - "vega-projection": "^1.6.2", - "vega-statistics": "^1.9.0", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-hierarchy": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/vega-hierarchy/-/vega-hierarchy-4.1.3.tgz", - "integrity": "sha512-0Z+TYKRgOEo8XYXnJc2HWg1EGpcbNAhJ9Wpi9ubIbEyEHqIgjCIyFVN8d4nSfsJOcWDzsSmRqohBztxAhOCSaw==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-hierarchy": "^3.1.2", - "vega-dataflow": "^5.7.7", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-label": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vega-label/-/vega-label-1.3.1.tgz", - "integrity": "sha512-Emx4b5s7pvuRj3fBkAJ/E2snCoZACfKAwxVId7f/4kYVlAYLb5Swq6W8KZHrH4M9Qds1XJRUYW9/Y3cceqzEFA==", - "license": "BSD-3-Clause", - "dependencies": { - "vega-canvas": "^1.2.7", - "vega-dataflow": "^5.7.7", - "vega-scenegraph": "^4.13.1", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-lite": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-5.0.0.tgz", - "integrity": "sha512-CrMAy3D2E662qtShrOeGttwwthRxUOZUfdu39THyxkOfLNJBCLkNjfQpFekEidxwbtFTO1zMZzyFIP3AE2I8kQ==", - "dependencies": { - "@types/clone": "~2.1.0", - "@types/fast-json-stable-stringify": "^2.0.0", - "array-flat-polyfill": "^1.0.1", - "clone": "~2.1.2", - "fast-deep-equal": "~3.1.3", - "fast-json-stable-stringify": "~2.1.0", - "json-stringify-pretty-compact": "~3.0.0", - "tslib": "~2.1.0", - "vega-event-selector": "~2.0.6", - "vega-expression": "~4.0.1", - "vega-util": "~1.16.0", - "yargs": "~16.2.0" - }, - "bin": { - "vl2pdf": "bin/vl2pdf", - "vl2png": "bin/vl2png", - "vl2svg": "bin/vl2svg", - "vl2vg": "bin/vl2vg" - }, - "peerDependencies": { - "vega": "^5.19.1" - } - }, - "node_modules/vega-lite/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/vega-lite/node_modules/json-stringify-pretty-compact": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz", - "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==" - }, - "node_modules/vega-lite/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/vega-lite/node_modules/tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" - }, - "node_modules/vega-lite/node_modules/vega-event-selector": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-2.0.6.tgz", - "integrity": "sha512-UwCu50Sqd8kNZ1X/XgiAY+QAyQUmGFAwyDu7y0T5fs6/TPQnDo/Bo346NgSgINBEhEKOAMY1Nd/rPOk4UEm/ew==" - }, - "node_modules/vega-lite/node_modules/vega-expression": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-4.0.1.tgz", - "integrity": "sha512-ZrDj0hP8NmrCpdLFf7Rd/xMUHGoSYsAOTaYp7uXZ2dkEH5x0uPy5laECMc8TiQvL8W+8IrN2HAWCMRthTSRe2Q==", - "dependencies": { - "vega-util": "^1.16.0" - } - }, - "node_modules/vega-lite/node_modules/vega-util": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-1.16.1.tgz", - "integrity": "sha512-FdgD72fmZMPJE99FxvFXth0IL4BbLA93WmBg/lvcJmfkK4Uf90WIlvGwaIUdSePIsdpkZjBPyQcHMQ8OcS8Smg==" - }, - "node_modules/vega-lite/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/vega-lite/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/vega-lite/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } - }, - "node_modules/vega-loader": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vega-loader/-/vega-loader-4.5.3.tgz", - "integrity": "sha512-dUfIpxTLF2magoMaur+jXGvwMxjtdlDZaIS8lFj6N7IhUST6nIvBzuUlRM+zLYepI5GHtCLOnqdKU4XV0NggCA==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-dsv": "^3.0.1", - "node-fetch": "^2.6.7", - "topojson-client": "^3.1.0", - "vega-format": "^1.1.3", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-parser": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-6.6.0.tgz", - "integrity": "sha512-jltyrwCTtWeidi/6VotLCybhIl+ehwnzvFWYOdWNUP0z/EskdB64YmawNwjCjzTBMemeiQtY6sJPPbewYqe3Vg==", - "license": "BSD-3-Clause", - "dependencies": { - "vega-dataflow": "^5.7.7", - "vega-event-selector": "^3.0.1", - "vega-functions": "^5.18.0", - "vega-scale": "^7.4.2", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-projection": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/vega-projection/-/vega-projection-1.6.2.tgz", - "integrity": "sha512-3pcVaQL9R3Zfk6PzopLX6awzrQUeYOXJzlfLGP2Xd93mqUepBa6m/reVrTUoSFXA3v9lfK4W/PS2AcVzD/MIcQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-geo": "^3.1.0", - "d3-geo-projection": "^4.0.0", - "vega-scale": "^7.4.2" - } - }, - "node_modules/vega-regression": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vega-regression/-/vega-regression-1.3.1.tgz", - "integrity": "sha512-AmccF++Z9uw4HNZC/gmkQGe6JsRxTG/R4QpbcSepyMvQN1Rj5KtVqMcmVFP1r3ivM4dYGFuPlzMWvuqp0iKMkQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.2", - "vega-dataflow": "^5.7.7", - "vega-statistics": "^1.9.0", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-runtime": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/vega-runtime/-/vega-runtime-6.2.1.tgz", - "integrity": "sha512-b4eot3tWKCk++INWqot+6sLn3wDTj/HE+tRSbiaf8aecuniPMlwJEK7wWuhVGeW2Ae5n8fI/8TeTViaC94bNHA==", - "license": "BSD-3-Clause", - "dependencies": { - "vega-dataflow": "^5.7.7", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-scale": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-7.4.2.tgz", - "integrity": "sha512-o6Hl76aU1jlCK7Q8DPYZ8OGsp4PtzLdzI6nGpLt8rxoE78QuB3GBGEwGAQJitp4IF7Lb2rL5oAXEl3ZP6xf9jg==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.2", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-scale-chromatic": "^3.1.0", - "vega-time": "^2.1.3", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-scenegraph": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-4.13.1.tgz", - "integrity": "sha512-LFY9+sLIxRfdDI9ZTKjLoijMkIAzPLBWHpPkwv4NPYgdyx+0qFmv+puBpAUGUY9VZqAZ736Uj5NJY9zw+/M3yQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-path": "^3.1.0", - "d3-shape": "^3.2.0", - "vega-canvas": "^1.2.7", - "vega-loader": "^4.5.3", - "vega-scale": "^7.4.2", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-schema-url-parser": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vega-schema-url-parser/-/vega-schema-url-parser-1.1.0.tgz", - "integrity": "sha512-Tc85J2ofMZZOsxiqDM9sbvfsa+Vdo3GwNLjEEsPOsCDeYqsUHKAlc1IpbbhPLZ6jusyM9Lk0e1izF64GGklFDg==" - }, - "node_modules/vega-selections": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/vega-selections/-/vega-selections-5.6.0.tgz", - "integrity": "sha512-UE2w78rUUbaV3Ph+vQbQDwh8eywIJYRxBiZdxEG/Tr/KtFMLdy2BDgNZuuDO1Nv8jImPJwONmqjNhNDYwM0VJQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "3.2.4", - "vega-expression": "^5.2.0", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-statistics": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/vega-statistics/-/vega-statistics-1.9.0.tgz", - "integrity": "sha512-GAqS7mkatpXcMCQKWtFu1eMUKLUymjInU0O8kXshWaQrVWjPIO2lllZ1VNhdgE0qGj4oOIRRS11kzuijLshGXQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.2" - } - }, - "node_modules/vega-themes": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/vega-themes/-/vega-themes-2.15.0.tgz", - "integrity": "sha512-DicRAKG9z+23A+rH/3w3QjJvKnlGhSbbUXGjBvYGseZ1lvj9KQ0BXZ2NS/+MKns59LNpFNHGi9us/wMlci4TOA==", - "peerDependencies": { - "vega": "*", - "vega-lite": "*" - } - }, - "node_modules/vega-time": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/vega-time/-/vega-time-2.1.3.tgz", - "integrity": "sha512-hFcWPdTV844IiY0m97+WUoMLADCp+8yUQR1NStWhzBzwDDA7QEGGwYGxALhdMOaDTwkyoNj3V/nox2rQAJD/vQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.2", - "d3-time": "^3.1.0", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-tooltip": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/vega-tooltip/-/vega-tooltip-0.27.0.tgz", - "integrity": "sha512-FRcHNfMNo9D/7an5nZuP6JC2JGEsc85qcGjyMU7VlPpjQj9eBj1P+sZSNbb54Z20g7inVSBRyd8qgNn5EYTxJA==", - "dependencies": { - "vega-util": "^1.16.0" - } - }, - "node_modules/vega-transforms": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-4.12.1.tgz", - "integrity": "sha512-Qxo+xeEEftY1jYyKgzOGc9NuW4/MqGm1YPZ5WrL9eXg2G0410Ne+xL/MFIjHF4hRX+3mgFF4Io2hPpfy/thjLg==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.2", - "vega-dataflow": "^5.7.7", - "vega-statistics": "^1.9.0", - "vega-time": "^2.1.3", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-typings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-1.5.0.tgz", - "integrity": "sha512-tcZ2HwmiQEOXIGyBMP8sdCnoFoVqHn4KQ4H0MQiHwzFU1hb1EXURhfc+Uamthewk4h/9BICtAM3AFQMjBGpjQA==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/geojson": "7946.0.4", - "vega-event-selector": "^3.0.1", - "vega-expression": "^5.2.0", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-util": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-1.17.3.tgz", - "integrity": "sha512-nSNpZLUrRvFo46M5OK4O6x6f08WD1yOcEzHNlqivF+sDLSsVpstaF6fdJYwrbf/debFi2L9Tkp4gZQtssup9iQ==", - "license": "BSD-3-Clause" - }, - "node_modules/vega-view": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-5.16.0.tgz", - "integrity": "sha512-Nxp1MEAY+8bphIm+7BeGFzWPoJnX9+hgvze6wqCAPoM69YiyVR0o0VK8M2EESIL+22+Owr0Fdy94hWHnmon5tQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.2", - "d3-timer": "^3.0.1", - "vega-dataflow": "^5.7.7", - "vega-format": "^1.1.3", - "vega-functions": "^5.18.0", - "vega-runtime": "^6.2.1", - "vega-scenegraph": "^4.13.1", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-view-transforms": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/vega-view-transforms/-/vega-view-transforms-4.6.1.tgz", - "integrity": "sha512-RYlyMJu5kZV4XXjmyTQKADJWDB25SMHsiF+B1rbE1p+pmdQPlp5tGdPl9r5dUJOp3p8mSt/NGI8GPGucmPMxtw==", - "license": "BSD-3-Clause", - "dependencies": { - "vega-dataflow": "^5.7.7", - "vega-scenegraph": "^4.13.1", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-voronoi": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-4.2.4.tgz", - "integrity": "sha512-lWNimgJAXGeRFu2Pz8axOUqVf1moYhD+5yhBzDSmckE9I5jLOyZc/XvgFTXwFnsVkMd1QW1vxJa+y9yfUblzYw==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-delaunay": "^6.0.2", - "vega-dataflow": "^5.7.7", - "vega-util": "^1.17.3" - } - }, - "node_modules/vega-wordcloud": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/vega-wordcloud/-/vega-wordcloud-4.1.6.tgz", - "integrity": "sha512-lFmF3u9/ozU0P+WqPjeThQfZm0PigdbXDwpIUCxczrCXKYJLYFmZuZLZR7cxtmpZ0/yuvRvAJ4g123LXbSZF8A==", - "license": "BSD-3-Clause", - "dependencies": { - "vega-canvas": "^1.2.7", - "vega-dataflow": "^5.7.7", - "vega-scale": "^7.4.2", - "vega-statistics": "^1.9.0", - "vega-util": "^1.17.3" - } - }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -20066,6 +19250,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -20114,6 +19299,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, diff --git a/package.json b/package.json index e2f2ce264d..15b5d707dd 100644 --- a/package.json +++ b/package.json @@ -110,11 +110,7 @@ "recharts": "^3.0.2", "styled-components": "^5.2.1", "styled-system": "^5.1.5", - "uuid": "^13.0.0", - "vega": "^5.17.3", - "vega-embed": "6.0.0", - "vega-lite": "5.0.0", - "vega-tooltip": "0.27.0" + "uuid": "^13.0.0" }, "homepage": "https://scality.github.io/core-ui/", "publishConfig": { diff --git a/src/lib/components/barchart/BarChart.component.tsx b/src/lib/components/barchart/BarChart.component.tsx deleted file mode 100644 index 9c1f902542..0000000000 --- a/src/lib/components/barchart/BarChart.component.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { VegaChart } from '../vegachartv2/VegaChartV2.component'; -type Props = { - id: string; - data: Array>; - xAxis: Record; - yAxis: Record; - color?: Record; - height?: number; - barConfig?: Record; -}; - -/** - * @deprecated Use Barchart v2 instead - * @example import { Barchart } from '@scality/core-ui/dist/next'; - - */ -function BarChart({ - id, - data, - xAxis, - yAxis, - color, - height = 200, - barConfig, -}: Props) { - const spec = { - mark: { - type: 'bar', - ...barConfig, - }, - width: 'container', - height, - data: { - values: data, - }, - encoding: { - x: xAxis, - y: yAxis, - color, - }, - }; - return ( - - ); -} - -export { BarChart }; diff --git a/src/lib/components/linetemporalchart/MetricTimespanProvider.tsx b/src/lib/components/charts/MetricsTimeSpanProvider.tsx similarity index 100% rename from src/lib/components/linetemporalchart/MetricTimespanProvider.tsx rename to src/lib/components/charts/MetricsTimeSpanProvider.tsx diff --git a/src/lib/components/barchartv2/Barchart.component.test.tsx b/src/lib/components/charts/barchart/Barchart.test.tsx similarity index 92% rename from src/lib/components/barchartv2/Barchart.component.test.tsx rename to src/lib/components/charts/barchart/Barchart.test.tsx index 208cf92b9d..74dd35e261 100644 --- a/src/lib/components/barchartv2/Barchart.component.test.tsx +++ b/src/lib/components/charts/barchart/Barchart.test.tsx @@ -1,8 +1,9 @@ import { render, screen, waitFor } from '@testing-library/react'; -import { getWrapper } from '../../testUtils'; -import { Barchart, CustomTick } from './Barchart.component'; -import { ChartLegendWrapper } from '../chartlegend/ChartLegendWrapper'; +import { getWrapper } from '../../../testUtils'; +import { Barchart } from './Barchart'; +import { ChartLegendWrapper } from '../legend/ChartLegendWrapper'; import React from 'react'; +import { CustomTick } from '../common/SharedComponents'; const ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000; const ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000; @@ -60,21 +61,27 @@ describe('Barchart', () => { render( - + , ); - + expect(screen.getByText('Test Title')).toBeInTheDocument(); expect(screen.getByText('category1')).toBeInTheDocument(); expect(screen.getByText('category2')).toBeInTheDocument(); expect(screen.getByText('category3')).toBeInTheDocument(); }); + it('should render the Barchart component with time data', async () => { const { Wrapper } = getWrapper(); render( { render( - + , ); @@ -111,7 +123,12 @@ describe('Barchart', () => { render( - + , ); @@ -122,7 +139,11 @@ describe('Barchart', () => { render( - + , ); @@ -139,6 +160,7 @@ describe('Barchart', () => { { Failed: 'lineColor2', }} > - + , ); @@ -239,7 +261,7 @@ describe('Barchart', () => { render( - + , ); @@ -270,6 +292,7 @@ describe('Barchart', () => { { { { diff --git a/src/lib/components/barchartv2/Barchart.component.tsx b/src/lib/components/charts/barchart/Barchart.tsx similarity index 58% rename from src/lib/components/barchartv2/Barchart.component.tsx rename to src/lib/components/charts/barchart/Barchart.tsx index dc5856d755..5f192cffd5 100644 --- a/src/lib/components/barchartv2/Barchart.component.tsx +++ b/src/lib/components/charts/barchart/Barchart.tsx @@ -1,26 +1,28 @@ import { useState, useRef } from 'react'; import { Bar, - BarChart, + BarChart as RechartsBarChart, CartesianGrid, - ResponsiveContainer, Tooltip, TooltipContentProps, XAxis, YAxis, } from 'recharts'; -import styled, { useTheme } from 'styled-components'; -import { spacing, Stack, Wrap } from '../../spacing'; -import { chartColors, ChartColors, fontSize } from '../../style/theme'; -import { Box } from '../box/Box'; -import { useChartLegend } from '../chartlegend/ChartLegendWrapper'; -import { ConstrainedText } from '../constrainedtext/Constrainedtext.component'; -import { FormattedDateTime } from '../date/FormattedDateTime'; -import { IconHelp } from '../iconhelper/IconHelper'; -import { Loader } from '../loader/Loader.component'; -import { Text } from '../text/Text.component'; +import { useTheme } from 'styled-components'; +import { Stack } from '../../../spacing'; +import { chartColors, ChartColors, fontSize } from '../../../style/theme'; +import { useChartLegend } from '../legend/ChartLegendWrapper'; import { BarchartTooltip } from './BarchartTooltip'; -import { getTicks, UnitRange, useChartData } from './utils'; +import { getTicks } from '../common/chartUtils'; +import { useChartData } from './Barchart.utils'; +import { + ChartHeader, + ChartError, + ChartLoading, + CustomTick, + StyledResponsiveContainer, +} from '../common/SharedComponents'; +import { TimeType, CategoryType, UnitRange } from '../types'; const CHART_CONSTANTS = { TICK_WIDTH_OFFSET: 4, @@ -34,23 +36,9 @@ const CHART_CONSTANTS = { bottom: 0, }, }; -const maxWidthTooltip = { maxWidth: '20rem' }; /* ---------------------------------- TYPE ---------------------------------- */ -export type TimeType = { - type: 'time'; - timeRange: { - startDate: Date; - endDate: Date; - interval: number; - }; -}; - -export type CategoryType = { - type: 'category'; - gap?: number; -}; export type Point = { key: string | number; values: { label: string; value: number }[]; @@ -98,166 +86,6 @@ export type BarchartProps = { isError?: boolean; }; -interface CustomTickProps { - x: number; - y: number; - payload: { - value: number; - }; - visibleTicksCount: number; - width: number; - type: TimeType; -} - -/* ---------------------------------- COMPONENTS ---------------------------------- */ - -/** - * Get the format of the date based on the duration - * @param duration - Duration in milliseconds - * @returns Formatted string - */ -export const formatDate = ( - duration: number, -): 'time' | 'day-month-abbreviated' | 'chart-long-term-date' => { - if (duration <= 24 * 60 * 60 * 1000) { - return 'time'; - } else if (duration <= 7 * 24 * 60 * 60 * 1000) { - return 'day-month-abbreviated'; - } else { - return 'chart-long-term-date'; - } -}; - -export const CustomTick = ({ - x, - y, - payload, - visibleTicksCount, - width, - type, -}: CustomTickProps) => { - const theme = useTheme(); - const tickWidth = - width / visibleTicksCount - CHART_CONSTANTS.TICK_WIDTH_OFFSET; - const centerX = x - tickWidth / 2; - - const duration = - type.type === 'time' - ? type.timeRange.endDate.getTime() - type.timeRange.startDate.getTime() - : 0; - - return ( - - - {type.type === 'time' ? ( - - ) : ( - String(payload.value) - )} - - } - centered - tooltipStyle={{ - backgroundColor: theme.backgroundLevel1, - padding: spacing.r10, - borderRadius: spacing.r8, - border: `1px solid ${theme.border}`, - position: 'absolute', - }} - /> - - ); -}; - -export const StyledResponsiveContainer = styled(ResponsiveContainer)` - // Avoid tooltip over constrained text to be cut off - & .recharts-surface { - outline: none; - overflow: visible; - } -`; - -const ChartHeader = ({ - title, - secondaryTitle, - helpTooltip, - rightTitle, -}: { - title?: string; - secondaryTitle?: string; - helpTooltip?: React.ReactNode; - rightTitle?: React.ReactNode; -}) => { - return ( - - - {title} - {helpTooltip && ( - - )} - - {secondaryTitle && ( - - {secondaryTitle} - - )} - - - {rightTitle && {rightTitle}} - - ); -}; - -const Error = ({ height }: { height: number }) => { - return ( - - Chart data is not available - - ); -}; - -const Loading = ({ height }: { height: number }) => { - return ( - - Loading Chart Data...} /> - - ); -}; - /* ---------------------------------- MAIN COMPONENT ---------------------------------- */ export const Barchart = (props: BarchartProps) => { @@ -315,12 +143,12 @@ export const Barchart = (props: BarchartProps) => { rightTitle={rightTitle} /> {isError || (!bars && !isLoading) ? ( - + ) : isLoading ? ( - + ) : ( - (props: BarchartProps) => { } + tick={(props) => ( + + )} type="category" interval={0} allowDataOverflow={true} @@ -402,7 +236,7 @@ export const Barchart = (props: BarchartProps) => { )} cursor={false} /> - + )} diff --git a/src/lib/components/barchartv2/utils.test.ts b/src/lib/components/charts/barchart/Barchart.utils.test.ts similarity index 82% rename from src/lib/components/barchartv2/utils.test.ts rename to src/lib/components/charts/barchart/Barchart.utils.test.ts index 2d1b4e36d7..df54820734 100644 --- a/src/lib/components/barchartv2/utils.test.ts +++ b/src/lib/components/charts/barchart/Barchart.utils.test.ts @@ -1,18 +1,14 @@ -import { coreUIAvailableThemes } from '../../style/theme'; +import { coreUIAvailableThemes } from '../../../style/theme'; import { applySortingToData, - computeUnitLabelAndRoundReferenceValue, filterChartDataAndBarsByLegendSelection, formatPrometheusDataToRechartsDataAndBars, getCurrentPoint, getMaxBarValue, - getRoundReferenceValue, - getTicks, sortStackedBars, transformCategoryData, transformTimeData, - UnitRange, -} from './utils'; +} from './Barchart.utils'; // Test date constants to avoid repetition const TEST_DATES = { @@ -504,77 +500,6 @@ describe('applySortingToData', () => { }); }); -describe('getRoundReferenceValue', () => { - it('should return appropriate rounded values with 10% buffer', () => { - // Small values (< 10) - expect(getRoundReferenceValue(0.1)).toBe(0.2); // 0.1 → 0.11 → 0.2 - expect(getRoundReferenceValue(1)).toBe(2); // 1.1 → 1.5 → 2 - expect(getRoundReferenceValue(2)).toBe(5); // 2.2 → 3 → 5 - expect(getRoundReferenceValue(3)).toBe(5); // 3.3 → 4 → 5 - - // Values 5-10 range - expect(getRoundReferenceValue(6)).toBe(10); // 6.6 → 10 (skip 7.5 for values < 10) - expect(getRoundReferenceValue(9)).toBe(10); // 9.9 → 10 - - // Larger values get 10% buffer applied - expect(getRoundReferenceValue(15)).toBe(20); // 16.5 → 20 - expect(getRoundReferenceValue(35)).toBe(40); // 38.5 → 40 - expect(getRoundReferenceValue(75)).toBe(100); // 82.5 → 100 - expect(getRoundReferenceValue(150)).toBe(200); // 165 → 200 - expect(getRoundReferenceValue(350)).toBe(400); // 385 → 400 - expect(getRoundReferenceValue(750)).toBe(1000); // 825 → 1000 - expect(getRoundReferenceValue(1500)).toBe(2000); // 1650 → 2000 - expect(getRoundReferenceValue(3500)).toBe(4000); // 3850 → 4000 - expect(getRoundReferenceValue(7500)).toBe(10000); // 8250 → 10000 - expect(getRoundReferenceValue(15000)).toBe(20000); // 16500 → 20000 - }); -}); - -describe('getTicks', () => { - describe('small values (< 10)', () => { - it('should return 2 ticks for non-symmetrical small values', () => { - expect(getTicks(1, false)).toEqual([0, 1]); - expect(getTicks(2, false)).toEqual([0, 2]); - expect(getTicks(5, false)).toEqual([0, 5]); - }); - - it('should return 3 ticks for symmetrical small values', () => { - expect(getTicks(1, true)).toEqual([-1, 0, 1]); - expect(getTicks(2, true)).toEqual([-2, 0, 2]); - expect(getTicks(5, true)).toEqual([-5, 0, 5]); - }); - }); - - describe('even topValue (divisible by 2)', () => { - it('should return 3 ticks for non-symmetrical even values', () => { - expect(getTicks(10, false)).toEqual([0, 5, 10]); - expect(getTicks(20, false)).toEqual([0, 10, 20]); - expect(getTicks(40, false)).toEqual([0, 20, 40]); - expect(getTicks(50, false)).toEqual([0, 25, 50]); - expect(getTicks(100, false)).toEqual([0, 50, 100]); - expect(getTicks(1000, false)).toEqual([0, 500, 1000]); - }); - - it('should return 5 ticks for symmetrical even values', () => { - expect(getTicks(10, true)).toEqual([-10, -5, 0, 5, 10]); - expect(getTicks(100, true)).toEqual([-100, -50, 0, 50, 100]); - }); - }); - - describe('odd topValue (not divisible by 2) - 7.5 multiples', () => { - it('should return 4 ticks for non-symmetrical values from 7.5 × magnitude', () => { - expect(getTicks(75, false)).toEqual([0, 25, 50, 75]); - expect(getTicks(750, false)).toEqual([0, 250, 500, 750]); - expect(getTicks(7500, false)).toEqual([0, 2500, 5000, 7500]); - }); - - it('should return 7 ticks for symmetrical values from 7.5 × magnitude', () => { - expect(getTicks(75, true)).toEqual([-75, -50, -25, 0, 25, 50, 75]); - expect(getTicks(750, true)).toEqual([-750, -500, -250, 0, 250, 500, 750]); - }); - }); -}); - describe('getMaxBarValue', () => { it('should return the maximum value from chart data', () => { const data = [ @@ -716,70 +641,6 @@ describe('formatPrometheusDataToRechartsDataAndBars', () => { }); }); -describe('computeUnitLabelAndRoundReferenceValue', () => { - it('should compute the unit label and round reference value correctly when reaching threshold', () => { - const data = [ - { - category: 'category1', - success: 1680, - }, - ]; - const maxValue = 1680; - const unitRange: UnitRange = [ - { - threshold: 1000, - label: 'kB', - }, - ]; - const result = computeUnitLabelAndRoundReferenceValue( - data, - maxValue, - unitRange, - ); - - expect(result.unitLabel).toBe('kB'); - // 1680 / 1000 = 1.68, with buffer: 1.848 → rounds to 2 - expect(result.roundReferenceValue).toBe(2); - expect(result.rechartsData).toEqual([ - { - category: 'category1', - success: 1.68, - }, - ]); - }); - it('should compute the unit label and round reference value correctly when threshold is 0', () => { - const data = [ - { - category: 'category1', - success: 680, - }, - ]; - const maxValue = 680; - const unitRange: UnitRange = [ - { - threshold: 0, - label: 'B', - }, - { - threshold: 1000, - label: 'kB', - }, - ]; - const result = computeUnitLabelAndRoundReferenceValue( - data, - maxValue, - unitRange, - ); - - expect(result.unitLabel).toBe('B'); - // 680 with buffer: 748 → rounds to 750 (7.5 * 100, value > 10) - expect(result.roundReferenceValue).toBe(750); - expect(result.rechartsData).toEqual([ - { category: 'category1', success: 680 }, - ]); - }); -}); - describe('sortStackedBars', () => { const bars = [ { dataKey: 'bar1', fill: 'blue' }, diff --git a/src/lib/components/barchartv2/utils.ts b/src/lib/components/charts/barchart/Barchart.utils.ts similarity index 73% rename from src/lib/components/barchartv2/utils.ts rename to src/lib/components/charts/barchart/Barchart.utils.ts index 0b362e72b9..02289c6ada 100644 --- a/src/lib/components/barchartv2/utils.ts +++ b/src/lib/components/charts/barchart/Barchart.utils.ts @@ -1,59 +1,9 @@ -import { BarchartProps, BarchartBars } from './Barchart.component'; +import { BarchartProps, BarchartBars } from './Barchart'; import { TooltipContentProps } from 'recharts'; -import { chartColors, ChartColors } from '../../style/theme'; -import { useChartLegend } from '../chartlegend/ChartLegendWrapper'; - -export const getRoundReferenceValue = (value: number): number => { - if (value <= 0) return 1; // Default for zero or negative values - - // Get the magnitude (10^n where n is the number of digits - 1) - const magnitude = Math.pow(10, Math.floor(Math.log10(value))); - - // Buffer the value by 10% to avoid being too close to the edge of the chart - const bufferedValue = value * 1.1; - - // Normalized value between 1 and 10 - const normalized = bufferedValue / magnitude; - - // Round to nice numbers based on normalized value - // skip 1.5, 3, 4, 7.5 as top value for better chart - // appearance for small values - let result: number; - - if (normalized <= 1) result = magnitude; - else if (normalized <= 2) result = 2 * magnitude; - else if (value > 10 && normalized <= 4) result = 4 * magnitude; - else if (normalized <= 5) result = 5 * magnitude; - else if (value > 10 && normalized <= 7.5) result = 7.5 * magnitude; - else result = 10 * magnitude; - - return result; -}; - -export const getTicks = (topValue: number, isSymmetrical: boolean) => { - if (topValue < 10) { - if (isSymmetrical) { - return [-topValue, 0, topValue]; - } else { - return [0, topValue]; - } - } - const numberOfTicks = topValue % 3 === 0 ? 4 : 3; - const tickInterval = topValue / (numberOfTicks - 1); - const ticks = Array.from( - { length: numberOfTicks }, - (_, index) => index * tickInterval, - ); - if (isSymmetrical) { - // Create negative ticks in order without 0 - const negativeTicks = Array.from( - { length: numberOfTicks - 1 }, - (_, index) => -(numberOfTicks - 1 - index) * tickInterval, - ); - ticks.unshift(...negativeTicks); - } - return ticks; -}; +import { chartColors, ChartColors } from '../../../style/theme'; +import { useChartLegend } from '../legend/ChartLegendWrapper'; +import { normalizeChartDataWithUnits } from '../common/chartUtils'; +import { UnitRange } from '../types'; export const getMaxBarValue = ( data: { [key: string]: string | number }[], @@ -226,14 +176,19 @@ export const applySortingToData = ( defaultSort: BarchartProps['defaultSort'], ) => { const points = data.map((item) => { - const point: any = { category: item.category }; + const point: Record = { category: item.category }; barDataKeys.forEach((dataKey) => { point[dataKey] = Number(item[dataKey]) || 0; }); return point; }); - points.sort(defaultSort); + points.sort( + defaultSort as ( + a: Record, + b: Record, + ) => number, + ); return points.map((point) => { const dataItem: { [key: string]: string | number } = { @@ -320,88 +275,6 @@ export const formatPrometheusDataToRechartsDataAndBars = < }; }; -export type UnitRange = { - threshold: number; - label: string; -}[]; - -export const computeUnitLabelAndRoundReferenceValue = ( - data: any, - maxValue: number, - unitRange: UnitRange | undefined, -) => { - if (!unitRange) { - const roundReferenceValue = getRoundReferenceValue(maxValue); - return { unitLabel: undefined, roundReferenceValue, rechartsData: data }; - } - - const { valueBase, unitLabel } = getUnitLabel(unitRange, maxValue); - const topValue = maxValue / valueBase; - const roundReferenceValue = getRoundReferenceValue(topValue); - const rechartsData = data.map((dataPoint) => { - const normalizedDataPoint = { ...dataPoint }; - Object.entries(dataPoint).forEach(([key, value]) => { - if (key !== 'category' && typeof value === 'number') { - normalizedDataPoint[key] = value / valueBase; - } - }); - return normalizedDataPoint; - }); - return { unitLabel, roundReferenceValue, rechartsData }; -}; - -/** - * Return the unit label base on the current dataset, and the valueBase which is used to convert the data - * @param {any} unitRange - * @param {any} maxValue the maximum value among the data set - * @returns {any} - */ -export function getUnitLabel( - unitRange: { - threshold: number; - label: string; - }[], - maxValue: number, -): { - valueBase: number; - unitLabel: string; -} { - // first sort the unitRange - unitRange.sort( - ( - unitA: { - threshold: number; - label: string; - }, - unitB: { - threshold: number; - label: string; - }, - ) => { - return unitA.threshold - unitB.threshold; - }, - ); - let index = unitRange.findIndex((range) => range.threshold > maxValue); - - // last unit - if (index === -1) { - index = unitRange.length; - } - - if (index === 0) { - return { - valueBase: unitRange[index].threshold, - unitLabel: unitRange[index].label, - }; - } - - return { - // if the threshold is 0, we use 1 as the value base to avoid division by 0 - valueBase: unitRange[index - 1].threshold || 1, - unitLabel: unitRange[index - 1].label, - }; -} - // Sort stacked bars by their average values in descending order or by legend order // This ensures the largest bars appear at the bottom of the stack (default) or follow legend order export const sortStackedBars = ( @@ -527,13 +400,17 @@ export const useChartData = ( const maxValue = getMaxBarValue(filteredData, stacked); - const { unitLabel, roundReferenceValue, rechartsData } = - computeUnitLabelAndRoundReferenceValue(filteredData, maxValue, unitRange); + const { unitLabel, topValue, rechartsData } = normalizeChartDataWithUnits( + filteredData, + maxValue, + unitRange, + 'category', + ); return { rechartsBars: filteredRechartsBars, - unitLabel, - roundReferenceValue, + unitLabel: unitLabel, + roundReferenceValue: topValue, rechartsData, }; }; diff --git a/src/lib/components/barchartv2/BarchartTooltip.test.tsx b/src/lib/components/charts/barchart/BarchartTooltip.test.tsx similarity index 100% rename from src/lib/components/barchartv2/BarchartTooltip.test.tsx rename to src/lib/components/charts/barchart/BarchartTooltip.test.tsx diff --git a/src/lib/components/barchartv2/BarchartTooltip.tsx b/src/lib/components/charts/barchart/BarchartTooltip.tsx similarity index 90% rename from src/lib/components/barchartv2/BarchartTooltip.tsx rename to src/lib/components/charts/barchart/BarchartTooltip.tsx index 72b42a9957..b496aebdea 100644 --- a/src/lib/components/barchartv2/BarchartTooltip.tsx +++ b/src/lib/components/charts/barchart/BarchartTooltip.tsx @@ -1,19 +1,15 @@ import { TooltipContentProps } from 'recharts'; -import { LegendShape } from '../chartlegend/ChartLegend'; +import { LegendShape } from '../legend/ChartLegend'; import { ChartTooltipPortal, ChartTooltipHeader, ChartTooltipItem, ChartTooltipItemsContainer, TooltipHeader, -} from '../charttooltip/ChartTooltip'; -import { - BarchartBars, - BarchartTooltipFn, - CategoryType, - TimeType, -} from './Barchart.component'; -import { getCurrentPoint } from './utils'; +} from '../common/ChartTooltip'; +import { BarchartBars, BarchartTooltipFn } from './Barchart'; +import { CategoryType, TimeType } from '../types'; +import { getCurrentPoint } from './Barchart.utils'; export const BarchartTooltip = ({ type, diff --git a/src/lib/components/charttooltip/ChartTooltip.tsx b/src/lib/components/charts/common/ChartTooltip.tsx similarity index 91% rename from src/lib/components/charttooltip/ChartTooltip.tsx rename to src/lib/components/charts/common/ChartTooltip.tsx index 9f81bab030..6b1163f5d2 100644 --- a/src/lib/components/charttooltip/ChartTooltip.tsx +++ b/src/lib/components/charts/common/ChartTooltip.tsx @@ -10,9 +10,10 @@ import { Middleware, } from '@floating-ui/react'; import styled from 'styled-components'; -import { spacing } from '../../spacing'; -import { fontSize, fontWeight } from '../../style/theme'; -import { FormattedDateTime } from '../date/FormattedDateTime'; +import { spacing } from '../../../spacing'; +import { fontSize, fontWeight } from '../../../style/theme'; +import { FormattedDateTime } from '../../date/FormattedDateTime'; +import { getTooltipDateFormat } from './chartUtils'; export const ChartTooltipContainer = styled.div` border: 1px solid ${({ theme }) => theme.border}; @@ -106,25 +107,17 @@ export type TooltipDateFormat = | 'day-month-abbreviated-hour-minute-second' | 'day-month-abbreviated-hour-minute'; -const getTooltipDateFormat: (duration: number) => TooltipDateFormat = ( - duration: number, -) => { - if (duration <= 60 * 60 * 1000) { - return 'day-month-abbreviated-hour-minute-second'; - } else if (duration <= 7 * 24 * 60 * 60 * 1000) { - return 'day-month-abbreviated-hour-minute'; - } else { - return 'day-month-abbreviated-year-hour-minute'; - } -}; - -export const TooltipHeader = ({ - duration, - value, -}: { +export type TooltipHeaderProps = { duration: number; value: string | number; -}) => { +}; +/** + * Tooltip header component + * @param duration - Duration in seconds + * @param value - Value to format + * @returns Formatted string type + */ +export const TooltipHeader = ({ duration, value }: TooltipHeaderProps) => { const timeFormat = getTooltipDateFormat(duration); return ( diff --git a/src/lib/components/charts/common/SharedComponents.tsx b/src/lib/components/charts/common/SharedComponents.tsx new file mode 100644 index 0000000000..ba08f37686 --- /dev/null +++ b/src/lib/components/charts/common/SharedComponents.tsx @@ -0,0 +1,180 @@ +import { ResponsiveContainer } from 'recharts'; +import styled, { useTheme } from 'styled-components'; +import { spacing, Stack, Wrap } from '../../../spacing'; +import { Box } from '../../box/Box'; +import { ConstrainedText } from '../../constrainedtext/Constrainedtext.component'; +import { FormattedDateTime } from '../../date/FormattedDateTime'; +import { IconHelp } from '../../iconhelper/IconHelper'; +import { Loader } from '../../loader/Loader.component'; +import { Text } from '../../text/Text.component'; +import { formatXAxisDate, maxWidthTooltip } from './chartUtils'; +import { TimeType } from '../types'; + +/** + * Styled ResponsiveContainer for charts + * Shared by Barchart and LineTimeSerieChart + * Ensures tooltip overflow is visible and removes outline + */ +export const StyledResponsiveContainer = styled(ResponsiveContainer)` + // Avoid tooltip over constrained text to be cut off + & .recharts-surface { + outline: none; + overflow: visible; + } +`; + +interface ChartLoadingOrErrorProps { + height: number; +} + +/** + * Error state component for charts + */ +export const ChartError = ({ height }: ChartLoadingOrErrorProps) => { + return ( + + Chart data is not available + + ); +}; + +/** + * Loading state component for charts + */ +export const ChartLoading = ({ height }: ChartLoadingOrErrorProps) => { + return ( + + Loading Chart Data...} /> + + ); +}; + +interface ChartHeaderProps { + title?: string; + secondaryTitle?: string; + helpTooltip?: React.ReactNode; + rightTitle?: React.ReactNode; +} + +/** + * Shared chart header component + * Used by Barchart and can be used by other charts + */ +export const ChartHeader = ({ + title, + secondaryTitle, + helpTooltip, + rightTitle, +}: ChartHeaderProps) => { + return ( + + + {title} + {helpTooltip && ( + + )} + + {secondaryTitle && ( + + {secondaryTitle} + + )} + + + {rightTitle && {rightTitle}} + + ); +}; + +interface CustomTickProps { + x: number; + y: number; + payload: { + value: number; + }; + visibleTicksCount: number; + width: number; + type: TimeType; + tickWidthOffset?: number; +} + +/** + * Custom tick component for charts + * Used by Barchart for time-based x-axis ticks + */ +export const CustomTick = ({ + x, + y, + payload, + visibleTicksCount, + width, + type, + tickWidthOffset = 4, +}: CustomTickProps) => { + const theme = useTheme(); + const tickWidth = width / visibleTicksCount - tickWidthOffset; + const centerX = x - tickWidth / 2; + + const duration = + type.type === 'time' + ? (type.timeRange.endDate.getTime() - + type.timeRange.startDate.getTime()) / + 1000 + : 0; + + return ( + + + {type.type === 'time' ? ( + + ) : ( + String(payload.value) + )} + + } + centered + tooltipStyle={{ + backgroundColor: theme.backgroundLevel1, + padding: spacing.r10, + borderRadius: spacing.r8, + border: `1px solid ${theme.border}`, + position: 'absolute', + }} + /> + + ); +}; diff --git a/src/lib/components/charts/common/chartUtils.test.ts b/src/lib/components/charts/common/chartUtils.test.ts new file mode 100644 index 0000000000..2d7fcc0c34 --- /dev/null +++ b/src/lib/components/charts/common/chartUtils.test.ts @@ -0,0 +1,396 @@ +import { + getRoundReferenceValue, + getTicks, + getUnitLabel, + addMissingDataPoint, + formatXAxisDate, + normalizeChartDataWithUnits, + getTooltipDateFormat, +} from './chartUtils'; +import { NAN_STRING } from '../../constants'; +import { UnitRange } from '../types'; + +describe('getRoundReferenceValue', () => { + it('should return 1 for values <= 0', () => { + expect(getRoundReferenceValue(0)).toBe(1); + expect(getRoundReferenceValue(-5)).toBe(1); + }); + + it('should round to nice numbers', () => { + expect(getRoundReferenceValue(1)).toBe(2); + expect(getRoundReferenceValue(5)).toBe(10); + expect(getRoundReferenceValue(10)).toBe(20); + expect(getRoundReferenceValue(50)).toBe(75); + expect(getRoundReferenceValue(100)).toBe(200); + }); + + it('should handle edge cases', () => { + expect(getRoundReferenceValue(0.5)).toBe(1); + expect(getRoundReferenceValue(9)).toBe(10); + expect(getRoundReferenceValue(99)).toBe(100); + }); +}); + +describe('getTicks', () => { + it('should return simple ticks for values < 10', () => { + expect(getTicks(5, false)).toEqual([0, 5]); + expect(getTicks(5, true)).toEqual([-5, 0, 5]); + }); + + it('should generate evenly spaced ticks for larger values', () => { + const ticks = getTicks(100, false); + expect(ticks).toHaveLength(3); + expect(ticks[0]).toBe(0); + expect(ticks[ticks.length - 1]).toBe(100); + }); + + it('should generate symmetrical ticks when isSymmetrical is true', () => { + const ticks = getTicks(100, true); + expect(ticks[0]).toBe(-100); + expect(ticks[ticks.length - 1]).toBe(100); + // Should have 0 in the middle + const middleIndex = Math.floor(ticks.length / 2); + expect(ticks[middleIndex]).toBe(0); + }); + + it('should handle values divisible by 3', () => { + const ticks = getTicks(90, false); + expect(ticks).toHaveLength(4); // numberOfTicks = 4 when topValue % 3 === 0 + }); +}); + +describe('getUnitLabel', () => { + const unitRange: UnitRange = [ + { threshold: 1, label: 'B' }, + { threshold: 1000, label: 'KB' }, + { threshold: 1000000, label: 'MB' }, + { threshold: 1000000000, label: 'GB' }, + ]; + it('should return correct unit label and threshold', () => { + const result = getUnitLabel(unitRange, 500); + expect(result).toEqual({ valueBase: 1, unitLabel: 'B' }); + const result2 = getUnitLabel(unitRange, 500000); + expect(result2).toEqual({ valueBase: 1000, unitLabel: 'KB' }); + const result3 = getUnitLabel(unitRange, 500000000); + expect(result3).toEqual({ valueBase: 1000000, unitLabel: 'MB' }); + const result4 = getUnitLabel(unitRange, 500000000000); + expect(result4).toEqual({ valueBase: 1000000000, unitLabel: 'GB' }); + }); + + it('should return correct unit for medium values even if range is disordered', () => { + const unsortedRange = [ + { threshold: 1000000, label: 'MB' }, + { threshold: 1000, label: 'KB' }, + { threshold: 1000000000, label: 'GB' }, + { threshold: 1, label: 'B' }, + ]; + const result = getUnitLabel(unsortedRange, 50000); + expect(result).toEqual({ valueBase: 1000, unitLabel: 'KB' }); + }); +}); + +describe('addMissingDataPoint', () => { + it('should return empty array for invalid inputs', () => { + expect(addMissingDataPoint([], 0, 100, 10)).toEqual([]); + expect(addMissingDataPoint([[10, 5]], undefined, 100, 10)).toEqual([]); + expect(addMissingDataPoint([[10, 5]], 0, 0, 10)).toEqual([]); + expect(addMissingDataPoint([[10, 5]], -1, 100, 10)).toEqual([]); + }); + + it('should add missing data points at the beginning', () => { + const original: [number, number][] = [ + [20, 5], + [30, 10], + ]; + const result = addMissingDataPoint(original, 0, 100, 10); + + expect(result[0]).toEqual([0, NAN_STRING]); + expect(result[1]).toEqual([10, NAN_STRING]); + expect(result[2]).toEqual([20, 5]); + }); + + it('should add missing data points in the middle', () => { + const original: [number, number][] = [ + [0, 5], + [30, 10], + ]; + const result = addMissingDataPoint(original, 0, 100, 10); + + expect(result[0]).toEqual([0, 5]); + expect(result[1]).toEqual([10, NAN_STRING]); + expect(result[2]).toEqual([20, NAN_STRING]); + expect(result[3]).toEqual([30, 10]); + }); + + it('should add missing data points at the end', () => { + const original: [number, number][] = [ + [0, 5], + [10, 10], + ]; + const result = addMissingDataPoint(original, 0, 40, 10); + + expect(result[result.length - 3]).toEqual([10, 10]); + expect(result[result.length - 2]).toEqual([20, NAN_STRING]); + expect(result[result.length - 1]).toEqual([30, NAN_STRING]); + }); + + it('should handle data points with null values', () => { + const original: [number, number | null][] = [ + [0, 5], + [10, null], + [20, 10], + ]; + const result = addMissingDataPoint(original, 0, 30, 10); + + expect(result).toEqual([ + [0, 5], + [10, null], + [20, 10], + ]); + }); + + it('should handle string values', () => { + const original: [number, string][] = [ + [0, '5'], + [10, '10'], + ]; + const result = addMissingDataPoint(original, 0, 30, 10); + + expect(result[0]).toEqual([0, '5']); + expect(result[1]).toEqual([10, '10']); + expect(result[2]).toEqual([20, NAN_STRING]); + }); +}); + +describe('formatXAxisDate', () => { + const ONE_DAY = 24 * 60 * 60; + const ONE_WEEK = 7 * ONE_DAY; + + it('should return "time" for durations <= 1 day', () => { + expect(formatXAxisDate(ONE_DAY)).toBe('time'); + expect(formatXAxisDate(ONE_DAY / 2)).toBe('time'); + expect(formatXAxisDate(1000)).toBe('time'); + }); + + it('should return "day-month-abbreviated" for durations <= 1 week', () => { + expect(formatXAxisDate(ONE_DAY * 2)).toBe('day-month-abbreviated'); + expect(formatXAxisDate(ONE_WEEK - 1000)).toBe('day-month-abbreviated'); + }); + + it('should return "chart-long-term-date" for durations > 1 week', () => { + expect(formatXAxisDate(ONE_WEEK + 1000)).toBe('chart-long-term-date'); + expect(formatXAxisDate(ONE_DAY * 30)).toBe('chart-long-term-date'); + expect(formatXAxisDate(ONE_DAY * 365)).toBe('chart-long-term-date'); + }); +}); + +describe('getTooltipDateFormat', () => { + it('should return "day-month-abbreviated-hour-minute-second" for durations <= 1 hour', () => { + expect(getTooltipDateFormat(60)).toBe( + 'day-month-abbreviated-hour-minute-second', + ); + expect(getTooltipDateFormat(60 * 40)).toBe( + 'day-month-abbreviated-hour-minute-second', + ); + expect(getTooltipDateFormat(60 * 60)).toBe( + 'day-month-abbreviated-hour-minute-second', + ); + }); + it('should return "day-month-abbreviated-hour-minute" for durations <= 7 days', () => { + expect(getTooltipDateFormat(60 * 60 * 2)).toBe( + 'day-month-abbreviated-hour-minute', + ); + expect(getTooltipDateFormat(60 * 60 * 24)).toBe( + 'day-month-abbreviated-hour-minute', + ); + expect(getTooltipDateFormat(60 * 60 * 24 * 7)).toBe( + 'day-month-abbreviated-hour-minute', + ); + }); + it('should return "day-month-abbreviated-year-hour-minute" for durations > 7 days', () => { + expect(getTooltipDateFormat(60 * 60 * 24 * 7.1)).toBe( + 'day-month-abbreviated-year-hour-minute', + ); + expect(getTooltipDateFormat(60 * 60 * 24 * 30)).toBe( + 'day-month-abbreviated-year-hour-minute', + ); + }); +}); + +describe('normalizeChartDataWithUnits', () => { + describe('with Barchart (category as excludeKey)', () => { + it('should compute unit label and normalize data when unit range is provided', () => { + const data = [ + { category: 'category1', success: 1680 }, + { category: 'category2', success: 2000 }, + ]; + const maxValue = 2000; + const unitRange: UnitRange = [{ threshold: 1000, label: 'kB' }]; + + const result = normalizeChartDataWithUnits( + data, + maxValue, + unitRange, + 'category', + ); + + expect(result.unitLabel).toBe('kB'); + // 2000 / 1000 = 2, with buffer: 2.2 → rounds to 5 + expect(result.topValue).toBe(5); + expect(result.rechartsData).toEqual([ + { category: 'category1', success: 1.68 }, + { category: 'category2', success: 2 }, + ]); + }); + + it('should handle threshold of 0 (bytes)', () => { + const data = [{ category: 'category1', success: 680 }]; + const maxValue = 680; + const unitRange: UnitRange = [ + { threshold: 0, label: 'B' }, + { threshold: 1000, label: 'kB' }, + ]; + + const result = normalizeChartDataWithUnits( + data, + maxValue, + unitRange, + 'category', + ); + + expect(result.unitLabel).toBe('B'); + // 680 with buffer: 748 → rounds to 750 + expect(result.topValue).toBe(750); + expect(result.rechartsData).toEqual([ + { category: 'category1', success: 680 }, + ]); + }); + + it('should not normalize when no unit range provided', () => { + const data = [ + { category: 'A', value: 100 }, + { category: 'B', value: 200 }, + ]; + const maxValue = 200; + + const result = normalizeChartDataWithUnits( + data, + maxValue, + undefined, + 'category', + ); + + expect(result.unitLabel).toBeUndefined(); + // 200 with 10% buffer: 220 → rounds to 400 + expect(result.topValue).toBe(400); + expect(result.rechartsData).toEqual(data); + }); + + it('should exclude category key from normalization', () => { + const data = [{ category: 1000, value: 1000 }]; + const maxValue = 1000; + const unitRange: UnitRange = [{ threshold: 1000, label: 'k' }]; + + const result = normalizeChartDataWithUnits( + data, + maxValue, + unitRange, + 'category', + ); + + // category should remain unchanged (1000, not normalized to 1) + expect(result.rechartsData[0].category).toBe(1000); + // value should be normalized + expect(result.rechartsData[0].value).toBe(1); + }); + }); + + describe('with LineTimeSerieChart (timestamp as excludeKey)', () => { + it('should normalize data and exclude timestamp', () => { + const data = [ + { timestamp: 1634567890000, metric1: 5000, metric2: 3000 }, + { timestamp: 1634567900000, metric1: 6000, metric2: 4000 }, + ]; + const maxValue = 6000; + const unitRange: UnitRange = [{ threshold: 1000, label: 'k' }]; + + const result = normalizeChartDataWithUnits( + data, + maxValue, + unitRange, + 'timestamp', + ); + + expect(result.unitLabel).toBe('k'); + expect(result.topValue).toBe(10); + expect(result.rechartsData).toEqual([ + { timestamp: 1634567890000, metric1: 5, metric2: 3 }, + { timestamp: 1634567900000, metric1: 6, metric2: 4 }, + ]); + }); + + it('should handle multiple metrics with timestamp', () => { + const data = [{ timestamp: 100, cpu: 2500, memory: 1500 }]; + const maxValue = 2500; + const unitRange: UnitRange = [{ threshold: 1000, label: 'k' }]; + + const result = normalizeChartDataWithUnits( + data, + maxValue, + unitRange, + 'timestamp', + ); + + expect(result.rechartsData[0].timestamp).toBe(100); // unchanged + expect(result.rechartsData[0].cpu).toBe(2.5); // normalized + expect(result.rechartsData[0].memory).toBe(1.5); // normalized + }); + }); + + describe('edge cases', () => { + it('should handle empty data array', () => { + const result = normalizeChartDataWithUnits([], 0, undefined, 'category'); + + expect(result.unitLabel).toBeUndefined(); + expect(result.topValue).toBe(1); // Default for 0 + expect(result.rechartsData).toEqual([]); + }); + + it('should handle data with only exclude key', () => { + const data = [{ category: 'A' }, { category: 'B' }]; + const result = normalizeChartDataWithUnits( + data, + 10, + undefined, + 'category', + ); + + expect(result.rechartsData).toEqual(data); + }); + + it('should handle mixed string and number values', () => { + const data = [{ category: 'test', value1: 1000, value2: 'text' }]; + const unitRange: UnitRange = [{ threshold: 1000, label: 'k' }]; + + const result = normalizeChartDataWithUnits( + data, + 1000, + unitRange, + 'category', + ); + + expect(result.rechartsData[0].value1).toBe(1); // normalized + expect(result.rechartsData[0].value2).toBe('text'); // unchanged + }); + + it('should handle empty unit range array', () => { + const data = [{ category: 'A', value: 100 }]; + const result = normalizeChartDataWithUnits(data, 100, [], 'category'); + + expect(result.unitLabel).toBeUndefined(); + // 100 with 10% buffer: 110 → rounds to 200 + expect(result.topValue).toBe(200); + expect(result.rechartsData).toEqual(data); + }); + }); +}); diff --git a/src/lib/components/charts/common/chartUtils.ts b/src/lib/components/charts/common/chartUtils.ts new file mode 100644 index 0000000000..052a54191c --- /dev/null +++ b/src/lib/components/charts/common/chartUtils.ts @@ -0,0 +1,320 @@ +import { NAN_STRING } from '../../constants'; +import { TooltipDateFormat } from './ChartTooltip'; +import { UnitRange } from '../types'; + +/* -------------------------------------------------------------------------- */ +/* constants */ +/* -------------------------------------------------------------------------- */ + +export const maxWidthTooltip = { maxWidth: '20rem' }; + +/* -------------------------------------------------------------------------- */ +/* utils functions */ +/* -------------------------------------------------------------------------- */ + +/** + * Round a value to a nice number for chart display + * Used by Barchart and LineTimeSerieChart for Y-axis scaling + */ +export const getRoundReferenceValue = (value: number): number => { + if (value <= 0) return 1; // Default for zero or negative values + + // Get the magnitude (10^n where n is the number of digits - 1) + const magnitude = Math.pow(10, Math.floor(Math.log10(value))); + + // Buffer the value by 10% to avoid being too close to the edge of the chart + const bufferedValue = value * 1.1; + + // Normalized value between 1 and 10 + const normalized = bufferedValue / magnitude; + + // Round to nice numbers based on normalized value + // appearance for small values + let result: number; + + if (normalized <= 1) result = magnitude; + else if (normalized <= 2) result = 2 * magnitude; + else if (value > 10 && normalized <= 4) result = 4 * magnitude; + else if (normalized <= 5) result = 5 * magnitude; + else if (value > 10 && normalized <= 7.5) result = 7.5 * magnitude; + else result = 10 * magnitude; + + return result; +}; + +/** + * Generate tick values for Y-axis + * Used by Barchart and LineTimeSerieChart + */ +export const getTicks = (topValue: number, isSymmetrical: boolean) => { + if (topValue < 10) { + if (isSymmetrical) { + return [-topValue, 0, topValue]; + } else { + return [0, topValue]; + } + } + const possibleTickNumbers = [4, 3, 6]; + const numberOfTicks = + possibleTickNumbers.find((number) => topValue % (number - 1) === 0) || 2; // Default to 2 ticks if no match + + const tickInterval = topValue / (numberOfTicks - 1); + const ticks = Array.from( + { length: numberOfTicks }, + (_, index) => index * tickInterval, + ); + if (isSymmetrical) { + // Create negative ticks in order without 0 + const negativeTicks = Array.from( + { length: numberOfTicks - 1 }, + (_, index) => (index - numberOfTicks + 1) * tickInterval, + ); + ticks.unshift(...negativeTicks); + } + return ticks; +}; + +/** + * Return the unit label based on the current dataset, and the valueBase which is used to convert the data + * Used by LineTimeSerieChart + * @param unitRange - Array of threshold and label pairs + * @param maxValue - The maximum value among the data set + * @returns Object with valueBase and unitLabel + */ +export function getUnitLabel( + unitRange: { + threshold: number; + label: string; + }[], + maxValue: number, +): { + valueBase: number; + unitLabel: string | undefined; +} { + if (!unitRange || unitRange.length === 0) { + return { + valueBase: 1, + unitLabel: undefined, + }; + } + // first sort the unitRange + unitRange.sort( + ( + unitA: { + threshold: number; + label: string; + }, + unitB: { + threshold: number; + label: string; + }, + ) => { + return unitA.threshold - unitB.threshold; + }, + ); + let index = unitRange.findIndex((range) => range.threshold > maxValue); + + // last unit + if (index === -1) { + index = unitRange.length; + } + + if (index === 0) { + return { + valueBase: unitRange[index].threshold || 1, + unitLabel: unitRange[index].label, + }; + } + + return { + // if the threshold is 0, we use 1 as the value base to avoid division by 0 + valueBase: unitRange[index - 1].threshold || 1, + unitLabel: unitRange[index - 1].label, + }; +} + +/** + * Computes unit label and normalizes chart data based on unit range. + * This is shared logic used by both Barchart and LineTimeSerieChart. + * + * @param data - Chart data to normalize + * @param maxValue - Maximum value in the dataset + * @param unitRange - Optional unit range configuration for automatic scaling + * @param excludeKey - Key to exclude from normalization (e.g., 'category' for Barchart, 'timestamp' for LineTimeSerieChart) + * @returns Object containing unit label, top value for Y-axis, and normalized data + */ +export const normalizeChartDataWithUnits = >( + data: T[], + maxValue: number, + unitRange: UnitRange | undefined, + excludeKey: string, +): { + unitLabel: string | undefined; + topValue: number; + rechartsData: T[]; +} => { + // If no unit range provided, just calculate top value without unit conversion + if (!unitRange || unitRange.length === 0) { + const topValue = getRoundReferenceValue(maxValue); + return { unitLabel: undefined, topValue, rechartsData: data }; + } + + // Get appropriate unit and value base for normalization + const { valueBase, unitLabel } = getUnitLabel(unitRange, maxValue); + const topValue = getRoundReferenceValue(maxValue / valueBase); + + // Normalize all numeric values by dividing by valueBase + const rechartsData = data.map((dataPoint) => { + const normalizedDataPoint: Record = { + ...dataPoint, + }; + Object.entries(dataPoint).forEach(([key, value]) => { + if (key !== excludeKey && typeof value === 'number') { + normalizedDataPoint[key] = value / valueBase; + } + }); + return normalizedDataPoint as T; + }); + + return { unitLabel, topValue, rechartsData }; +}; + +/** + * This function manually adds the missing data points with `null` value caused by downtime of the VMs + * Missing data points are only added when the gap between consecutive data points is bigger than 2 intervals + * Used by LineTimeSerieChart and Sparkline + * + * @param orginalValues - The array of the data points are already sorted according to the time series + * @param startingTimeStamp - The starting timestamp in seconds + * @param sampleDuration - The time span value in seconds + * @param sampleInterval - The time difference between two data points in seconds + */ +export function addMissingDataPoint( + originalValues: [number, number | string | null][], + startingTimeStamp?: number, + sampleDuration?: number, + sampleInterval?: number, +): [number, number | string | null][] { + if ( + !originalValues || + startingTimeStamp === undefined || + !sampleDuration || + !sampleInterval || + startingTimeStamp < 0 || + sampleDuration <= 0 || + sampleInterval <= 0 + ) { + return []; + } + + // If there are no original values, return empty array + if (originalValues.length === 0) { + return []; + } + + const newValues: [number, number | string | null][] = []; + + // add missing data points for the starting time + for ( + let i = startingTimeStamp; + i < originalValues[0][0]; + i += sampleInterval + ) { + newValues.push([i, NAN_STRING]); + } + + // Process all but the last element + for (let i = 0; i < originalValues.length - 1; i++) { + if ( + originalValues[i][0] < startingTimeStamp || + originalValues[i][0] > startingTimeStamp + sampleDuration + ) { + continue; + } + + // Always add the current data point + newValues.push(originalValues[i]); + + const currentTimestamp = originalValues[i][0]; + const nextTimestamp = originalValues[i + 1][0]; + const gap = nextTimestamp - currentTimestamp; + + // Calculate how many missing points to add + const missingIntervals = Math.floor(gap / sampleInterval) - 1; + + // Add missing data points with NAN_STRING (only executes if missingIntervals > 0) + for (let j = 1; j <= missingIntervals; j++) { + const missingTimestamp = currentTimestamp + j * sampleInterval; + newValues.push([missingTimestamp, NAN_STRING]); + } + } + + // Add the last element + newValues.push(originalValues[originalValues.length - 1]); + + // add missing data points for the ending time + for ( + let i = originalValues[originalValues.length - 1][0] + sampleInterval; + i < startingTimeStamp + sampleDuration; + i += sampleInterval + ) { + newValues.push([i, NAN_STRING]); + } + + return newValues; +} +/** + * Date Format Reference Table + * ============================ + * + * This table documents the date formatting logic used across charts: + * - X-Axis Format: Used for chart axis labels (formatXAxisDate + LineTimeSerieChart's formatXAxisLabel) + * - Tooltip Format: Used for tooltip headers (getTooltipDateFormat) + * + * ┌─────────────────┬──────────────┬────────────────────────┬──────────────────┬──────────────────────────────────────────┬───────────────────────────┐ + * │ Interval │ Duration (s) │ X-axis format │ Example (X-axis) │ Tooltip format │ Example (Tooltip) │ + * ├─────────────────┼──────────────┼────────────────────────┼──────────────────┼──────────────────────────────────────────┼───────────────────────────┤ + * │ Last hour │ ≤ 3,600 │ HH:MM │ 14:05 │ DD MMM HH:MM:SS │ 01 Oct 00:15:00 │ + * │ Last 24 hours │ ≤ 86,400 │ HH:MM │ 23:00 │ DD MMM HH:MM │ 01 Oct 00:15 │ + * │ Last 7 days │ ≤ 604,800 │ DD MMM HH:MM │ 27 Sep 10:12 │ DD MMM HH:MM │ 01 Oct 00:15 │ + * │ Long term │ > 604,800 │ DDMMMYY │ 15Sep25 │ DD MMM YYYY HH:MM │ 01 Oct 2025 00:15 │ + * └─────────────────┴──────────────┴────────────────────────┴──────────────────┴──────────────────────────────────────────┴───────────────────────────┘ + * + * Note: Duration is in seconds. Some intervals share the same format, which is why both functions only have 3 cases. + */ + +/** + * Get the format of the date based on the duration + * Used by Barchart CustomTick component + * @param duration - Duration in seconds + * @returns Formatted string type + */ +export const formatXAxisDate = ( + duration: number, +): 'time' | 'day-month-abbreviated' | 'chart-long-term-date' => { + if (duration <= 24 * 60 * 60) { + return 'time'; + } else if (duration <= 7 * 24 * 60 * 60) { + return 'day-month-abbreviated'; + } else { + return 'chart-long-term-date'; + } +}; + +/** + * Get the format of the date based on the duration + * Used by TooltipHeader component + * @param duration - Duration in seconds + * @returns Formatted string type + */ +export const getTooltipDateFormat: (duration: number) => TooltipDateFormat = ( + duration: number, +) => { + if (duration <= 60 * 60) { + return 'day-month-abbreviated-hour-minute-second'; + } else if (duration <= 7 * 24 * 60 * 60) { + return 'day-month-abbreviated-hour-minute'; + } else { + return 'day-month-abbreviated-year-hour-minute'; + } +}; diff --git a/src/lib/components/globalhealthbar/useHealthBarData.spec.tsx b/src/lib/components/charts/globalhealthbar/GlobalHealthBar.hooks.test.tsx similarity index 99% rename from src/lib/components/globalhealthbar/useHealthBarData.spec.tsx rename to src/lib/components/charts/globalhealthbar/GlobalHealthBar.hooks.test.tsx index 9392386ef0..0e0a4e17b3 100644 --- a/src/lib/components/globalhealthbar/useHealthBarData.spec.tsx +++ b/src/lib/components/charts/globalhealthbar/GlobalHealthBar.hooks.test.tsx @@ -1,4 +1,4 @@ -import { useHealthBarData, Alert } from './useHealthBarData'; +import { useHealthBarData, Alert } from './GlobalHealthBar.hooks'; import { renderHook } from '@testing-library/react'; describe('useHealthBarData', () => { const mockTimestamp = { diff --git a/src/lib/components/globalhealthbar/useHealthBarData.ts b/src/lib/components/charts/globalhealthbar/GlobalHealthBar.hooks.ts similarity index 100% rename from src/lib/components/globalhealthbar/useHealthBarData.ts rename to src/lib/components/charts/globalhealthbar/GlobalHealthBar.hooks.ts diff --git a/src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component.tsx b/src/lib/components/charts/globalhealthbar/GlobalHealthBar.tsx similarity index 96% rename from src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component.tsx rename to src/lib/components/charts/globalhealthbar/GlobalHealthBar.tsx index 881307a01e..5b0729e39c 100644 --- a/src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component.tsx +++ b/src/lib/components/charts/globalhealthbar/GlobalHealthBar.tsx @@ -8,14 +8,14 @@ import { YAxis, } from 'recharts'; import styled, { useTheme } from 'styled-components'; -import { GlobalHealthBarTooltip } from './components/GlobalHealthBarTooltip'; -import { HealthBarXAxis } from './components/HealthBarXAxis'; +import { GlobalHealthBarTooltip } from './GlobalHealthBarTooltip'; +import { HealthBarXAxis } from './HealthBarXAxis'; import { CHART_CONFIG, getNavigationAction, getNavigationStateUpdate, -} from './healthBarUtils'; -import { Alert, useHealthBarData } from './useHealthBarData'; +} from './GlobalHealthBar.utils'; +import { Alert, useHealthBarData } from './GlobalHealthBar.hooks'; export interface GlobalHealthProps { id: string; diff --git a/src/lib/components/globalhealthbar/healthBarUtils.spec.ts b/src/lib/components/charts/globalhealthbar/GlobalHealthBar.utils.test.ts similarity index 99% rename from src/lib/components/globalhealthbar/healthBarUtils.spec.ts rename to src/lib/components/charts/globalhealthbar/GlobalHealthBar.utils.test.ts index c54e8c1bef..7aebf857ff 100644 --- a/src/lib/components/globalhealthbar/healthBarUtils.spec.ts +++ b/src/lib/components/charts/globalhealthbar/GlobalHealthBar.utils.test.ts @@ -10,7 +10,7 @@ import { getNavigationAction, calculateNavigationIndex, getNavigationStateUpdate, -} from './healthBarUtils'; +} from './GlobalHealthBar.utils'; describe('Health Bar Utils', () => { describe('Tick Calculations', () => { diff --git a/src/lib/components/globalhealthbar/healthBarUtils.ts b/src/lib/components/charts/globalhealthbar/GlobalHealthBar.utils.ts similarity index 99% rename from src/lib/components/globalhealthbar/healthBarUtils.ts rename to src/lib/components/charts/globalhealthbar/GlobalHealthBar.utils.ts index 46e6dabdf6..8560270c10 100644 --- a/src/lib/components/globalhealthbar/healthBarUtils.ts +++ b/src/lib/components/charts/globalhealthbar/GlobalHealthBar.utils.ts @@ -1,4 +1,4 @@ -import { fontSize } from '../../style/theme'; +import { fontSize } from '../../../style/theme'; // ============================================================================= // CONSTANTS diff --git a/src/lib/components/globalhealthbar/components/GlobalHealthBarTooltip.tsx b/src/lib/components/charts/globalhealthbar/GlobalHealthBarTooltip.tsx similarity index 89% rename from src/lib/components/globalhealthbar/components/GlobalHealthBarTooltip.tsx rename to src/lib/components/charts/globalhealthbar/GlobalHealthBarTooltip.tsx index 5c95d0e406..809b5b9a93 100644 --- a/src/lib/components/globalhealthbar/components/GlobalHealthBarTooltip.tsx +++ b/src/lib/components/charts/globalhealthbar/GlobalHealthBarTooltip.tsx @@ -1,11 +1,15 @@ import React from 'react'; import styled, { css, useTheme } from 'styled-components'; -import { FormattedDateTime, Stack, Text, Wrap, spacing } from '../../../index'; -import { Alert } from '../GlobalHealthBarRecharts.component'; import { TooltipContentProps } from 'recharts'; +import { FormattedDateTime } from '../../date/FormattedDateTime'; +import { Stack } from '../../../spacing'; +import { Text } from '../../text/Text.component'; +import { Wrap } from '../../../spacing'; +import { spacing } from '../../../spacing'; +import { Alert } from './GlobalHealthBar.hooks'; import { zIndex } from '../../../style/theme'; -import { CHART_CONFIG, getTooltipPosition } from '../healthBarUtils'; -import { ChartTooltipPortal } from '../../charttooltip/ChartTooltip'; +import { CHART_CONFIG, getTooltipPosition } from './GlobalHealthBar.utils'; +import { ChartTooltipPortal } from '../common/ChartTooltip'; interface GlobalHealthBarTooltipProps { tooltipData: Alert | null; diff --git a/src/lib/components/globalhealthbar/components/HealthBarXAxis.tsx b/src/lib/components/charts/globalhealthbar/HealthBarXAxis.tsx similarity index 98% rename from src/lib/components/globalhealthbar/components/HealthBarXAxis.tsx rename to src/lib/components/charts/globalhealthbar/HealthBarXAxis.tsx index d3d6659844..1dc922d075 100644 --- a/src/lib/components/globalhealthbar/components/HealthBarXAxis.tsx +++ b/src/lib/components/charts/globalhealthbar/HealthBarXAxis.tsx @@ -6,7 +6,7 @@ import { calculateLabelVisibility, TIME_CONSTANTS, getEdgeMargin, -} from '../healthBarUtils'; +} from './GlobalHealthBar.utils'; import { FormattedDateTime } from '../../date/FormattedDateTime'; interface HealthBarXAxisProps { diff --git a/src/lib/components/charts/index.ts b/src/lib/components/charts/index.ts new file mode 100644 index 0000000000..8f9eb11ba4 --- /dev/null +++ b/src/lib/components/charts/index.ts @@ -0,0 +1,59 @@ +// Components +export { Barchart } from './barchart/Barchart'; +export type { + BarchartProps, + BarchartBars, + BarchartTooltipFn, + BarchartSortFn, + Point, +} from './barchart/Barchart'; + +export { LineTimeSerieChart } from './linetimeseries/LineTimeSerieChart'; +export type { + LineChartProps, + Serie, +} from './linetimeseries/LineTimeSerieChart'; + +export { GlobalHealthBar } from './globalhealthbar/GlobalHealthBar'; +export type { GlobalHealthProps } from './globalhealthbar/GlobalHealthBar'; +export type { Alert } from './globalhealthbar/GlobalHealthBar.hooks'; + +export { Sparkline } from './sparkline/Sparkline'; + +// Legend +export { ChartLegend } from './legend/ChartLegend'; +export { + ChartLegendWrapper, + useChartId, + useChartLegend, +} from './legend/ChartLegendWrapper'; + +// Tooltips (for advanced usage) +export { BarchartTooltip } from './barchart/BarchartTooltip'; +export { + ChartTooltipContainer, + ChartTooltipItem, + ChartTooltipHeader, + ChartTooltipItemsContainer, + ChartTooltipPortal, +} from './common/ChartTooltip'; + +// Shared utilities (for advanced usage) +export { + getRoundReferenceValue, + getTicks, + getUnitLabel, + addMissingDataPoint, + formatXAxisDate, + getTooltipDateFormat, + normalizeChartDataWithUnits, +} from './common/chartUtils'; + +// Context Providers (for backward compatibility) +export { + MetricsTimeSpanProvider, + useMetricsTimeSpan, +} from './MetricsTimeSpanProvider'; + +// Types +export type { UnitRange, TimeType, CategoryType } from './types'; diff --git a/src/lib/components/chartlegend/ChartLegend.test.tsx b/src/lib/components/charts/legend/ChartLegend.test.tsx similarity index 100% rename from src/lib/components/chartlegend/ChartLegend.test.tsx rename to src/lib/components/charts/legend/ChartLegend.test.tsx diff --git a/src/lib/components/chartlegend/ChartLegend.tsx b/src/lib/components/charts/legend/ChartLegend.tsx similarity index 96% rename from src/lib/components/chartlegend/ChartLegend.tsx rename to src/lib/components/charts/legend/ChartLegend.tsx index d1304ac92e..0a676fd78c 100644 --- a/src/lib/components/chartlegend/ChartLegend.tsx +++ b/src/lib/components/charts/legend/ChartLegend.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; import { useChartLegend } from './ChartLegendWrapper'; -import { Text, TextVariant } from '../text/Text.component'; -import { chartColors } from '../../style/theme'; +import { Text, TextVariant } from '../../text/Text.component'; +import { chartColors } from '../../../style/theme'; import { useCallback } from 'react'; type ChartLegendProps = { diff --git a/src/lib/components/chartlegend/ChartLegendWrapper.test.tsx b/src/lib/components/charts/legend/ChartLegendWrapper.test.tsx similarity index 100% rename from src/lib/components/chartlegend/ChartLegendWrapper.test.tsx rename to src/lib/components/charts/legend/ChartLegendWrapper.test.tsx diff --git a/src/lib/components/chartlegend/ChartLegendWrapper.tsx b/src/lib/components/charts/legend/ChartLegendWrapper.tsx similarity index 98% rename from src/lib/components/chartlegend/ChartLegendWrapper.tsx rename to src/lib/components/charts/legend/ChartLegendWrapper.tsx index ec7b8fe9e8..d7e9e24609 100644 --- a/src/lib/components/chartlegend/ChartLegendWrapper.tsx +++ b/src/lib/components/charts/legend/ChartLegendWrapper.tsx @@ -9,7 +9,7 @@ import { useRef, } from 'react'; import { v4 as uuidv4 } from 'uuid'; -import { ChartColors } from '../../style/theme'; +import { ChartColors } from '../../../style/theme'; export const useChartId = (): string => { const idRef = useRef(null); diff --git a/src/lib/components/linetimeseriechart/linetimeseriechart.test.tsx b/src/lib/components/charts/linetimeseries/LineTimeSerieChart.test.tsx similarity index 89% rename from src/lib/components/linetimeseriechart/linetimeseriechart.test.tsx rename to src/lib/components/charts/linetimeseries/LineTimeSerieChart.test.tsx index 0541457449..03f9c8ebd4 100644 --- a/src/lib/components/linetimeseriechart/linetimeseriechart.test.tsx +++ b/src/lib/components/charts/linetimeseries/LineTimeSerieChart.test.tsx @@ -1,12 +1,9 @@ import { render } from '@testing-library/react'; import React from 'react'; -import { - LineChartProps, - LineTimeSerieChart, -} from './linetimeseriechart.component'; -import { ChartLegendWrapper } from '../chartlegend/ChartLegendWrapper'; +import { LineChartProps, LineTimeSerieChart } from './LineTimeSerieChart'; +import { ChartLegendWrapper } from '../legend/ChartLegendWrapper'; import { ThemeProvider } from 'styled-components'; -import { coreUIAvailableThemes } from '../../style/theme'; +import { coreUIAvailableThemes } from '../../../style/theme'; const TestSeries = [ { diff --git a/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx b/src/lib/components/charts/linetimeseries/LineTimeSerieChart.tsx similarity index 92% rename from src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx rename to src/lib/components/charts/linetimeseries/LineTimeSerieChart.tsx index de9fff9169..843c0f4c03 100644 --- a/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +++ b/src/lib/components/charts/linetimeseries/LineTimeSerieChart.tsx @@ -1,3 +1,4 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { CartesianGrid, Line, @@ -8,32 +9,30 @@ import { XAxis, YAxis, } from 'recharts'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; import styled, { useTheme } from 'styled-components'; -import { Stack } from '../../spacing'; -import { fontSize } from '../../style/theme'; -import { useChartLegend } from '../chartlegend/ChartLegendWrapper'; +import { Stack } from '../../../spacing'; +import { fontSize } from '../../../style/theme'; +import { IconHelp } from '../../iconhelper/IconHelper'; +import { Loader } from '../../loader/Loader.component'; +import { ChartTitleText } from '../../text/Text.component'; +import { LegendShape } from '../legend/ChartLegend'; +import { useChartLegend } from '../legend/ChartLegendWrapper'; +import { StyledResponsiveContainer } from '../common/SharedComponents'; import { - addMissingDataPoint, - getUnitLabel, -} from '../linetemporalchart/ChartUtil'; -import { Loader } from '../loader/Loader.component'; -import { ChartTitleText } from '../text/Text.component'; -import { formatXAxisLabel } from './utils'; -import { - ChartTooltipPortal, - ChartTooltipItem, ChartTooltipHeader, + ChartTooltipItem, ChartTooltipItemsContainer, + ChartTooltipPortal, ChartTooltipSeparator, TooltipHeader, -} from '../charttooltip/ChartTooltip'; -import { LegendShape } from '../chartlegend/ChartLegend'; -import { StyledResponsiveContainer } from '../barchartv2/Barchart.component'; -import { getRoundReferenceValue, getTicks } from '../barchartv2/utils'; -import { IconHelp } from '../iconhelper/IconHelper'; - -const maxWidthTooltip = { maxWidth: '20rem' }; +} from '../common/ChartTooltip'; +import { + addMissingDataPoint, + getTicks, + maxWidthTooltip, + normalizeChartDataWithUnits, +} from '../common/chartUtils'; +import { formatXAxisLabel } from './LineTimeSerieChart.utils'; const LineTemporalChartWrapper = styled.div` display: flex; @@ -211,6 +210,16 @@ const isSymmetricalSeries = ( return 'above' in series && 'below' in series; }; +/** + * Props for LineTimeSerieChart component + * @param series - The data series to display + * @param title - The title of the chart + * @param height - The height of the chart in pixels + * @param startingTimeStamp - Starting timestamp in seconds + * @param interval - Interval between data points in seconds + * @param duration - Total duration of the chart in seconds + * + */ export function LineTimeSerieChart({ series, title, @@ -392,21 +401,19 @@ export function LineTimeSerieChart({ const bottom = Math.abs(Math.min(...values)); const maxValue = Math.max(top, bottom); - const { valueBase, unitLabel } = getUnitLabel(unitRange ?? [], maxValue); - // Use round reference value to add extra padding to the top value - const topValue = getRoundReferenceValue(maxValue / valueBase); - - const rechartsData = chartData.map((dataPoint) => { - const normalizedDataPoint = { ...dataPoint }; - Object.entries(dataPoint).forEach(([key, value]) => { - if (key !== 'timestamp' && typeof value === 'number') { - normalizedDataPoint[key] = value / valueBase; - } - }); - return normalizedDataPoint; - }); + // Use shared normalization function + const result = normalizeChartDataWithUnits( + chartData, + maxValue, + unitRange, + 'timestamp', // LineTimeSerieChart uses 'timestamp' as the key to exclude + ); - return { topValue, unitLabel, rechartsData }; + return { + topValue: result.topValue, + unitLabel: result.unitLabel, + rechartsData: result.rechartsData, + }; }, [chartData, yAxisType, unitRange]); // Group series by resource and create color mapping diff --git a/src/lib/components/linetimeseriechart/utils.test.ts b/src/lib/components/charts/linetimeseries/LineTimeSerieChart.utils.test.ts similarity index 96% rename from src/lib/components/linetimeseriechart/utils.test.ts rename to src/lib/components/charts/linetimeseries/LineTimeSerieChart.utils.test.ts index 851709a956..4cfe3ea70b 100644 --- a/src/lib/components/linetimeseriechart/utils.test.ts +++ b/src/lib/components/charts/linetimeseries/LineTimeSerieChart.utils.test.ts @@ -1,4 +1,4 @@ -import { formatXAxisLabel } from './utils'; +import { formatXAxisLabel } from './LineTimeSerieChart.utils'; describe('formatXAxisLabel', () => { const mockTimestamp = new Date('2025-09-15T14:30:00Z').getTime(); diff --git a/src/lib/components/linetimeseriechart/utils.ts b/src/lib/components/charts/linetimeseries/LineTimeSerieChart.utils.ts similarity index 65% rename from src/lib/components/linetimeseriechart/utils.ts rename to src/lib/components/charts/linetimeseries/LineTimeSerieChart.utils.ts index 0543aba227..529b631e57 100644 --- a/src/lib/components/linetimeseriechart/utils.ts +++ b/src/lib/components/charts/linetimeseries/LineTimeSerieChart.utils.ts @@ -2,7 +2,7 @@ import { TIME_FORMATER, DAY_MONTH_ABBREVIATED_YEAR, DAY_MONTH_ABBREVIATED_HOUR_MINUTE, -} from '../date/FormattedDateTime'; +} from '../../date/FormattedDateTime'; export const ONE_YEAR_MILLISECONDS = 366 * 24 * 60 * 60 * 1000; @@ -11,13 +11,11 @@ export type ChartDataPoint = { } & Record; /** - * Formats timestamp for X-axis labels based on time format and data range: - * For 'date-time' format, return day-month-abbreviated-hour-minute format - * For 'date' format, return YYYY-MM-DD format if time range is greater than 1 year, otherwise return MM-DD format + * Formats timestamp for X-axis labels based on duration + * * * @param timestamp - The timestamp to format in milliseconds - * @param timeFormat - The format type ('date-time' or 'date') - * @param chartData - The chart data to determine time range for optimal formatting + * @param duration - The duration in seconds * @returns Formatted string for display on X-axis */ export const formatXAxisLabel = ( diff --git a/src/lib/components/charts/sparkline/Sparkline.tsx b/src/lib/components/charts/sparkline/Sparkline.tsx new file mode 100644 index 0000000000..24c8e9cf1c --- /dev/null +++ b/src/lib/components/charts/sparkline/Sparkline.tsx @@ -0,0 +1,80 @@ +import { useMemo } from 'react'; +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + YAxis, +} from 'recharts'; +import { useTheme } from 'styled-components'; +import { chartColors } from '../../../style/theme'; +import { addMissingDataPoint } from '../common/chartUtils'; + +type SparklineProps = { + serie: { + data: [number, number | null][]; + color?: string; // exa color code like '#ff0000' + }; + startingTimeStamp: number; + sampleDuration: number; + sampleInterval: number; + yAxisType?: 'default' | 'percentage'; +}; + +/** + * Sparkline is a simple dynamically sized area chart. + * Used to show trends in data over time. + * @param serie - The data series to display + * @param startingTimeStamp - The starting timestamp in seconds + * @param sampleDuration - The total duration in seconds to cover in the sparkline + * @param sampleInterval - The interval in seconds between data points + * @param yAxisType - The type of y-axis to display + * @returns The sparkline component + */ +export function Sparkline({ + serie, + startingTimeStamp, + sampleDuration, + sampleInterval, + yAxisType, +}: SparklineProps) { + const data = useMemo(() => { + const dataMdp = addMissingDataPoint( + serie.data, + startingTimeStamp, + sampleDuration, + sampleInterval, + ); + return dataMdp.map(([x, y]) => ({ x, y })); + }, [serie.data]); + const color = serie.color ?? chartColors.lineColor1; + const strokeGridColor = useTheme().border; + + return ( + + + + + + + + + + + {yAxisType === 'percentage' && } + + + ); +} diff --git a/src/lib/components/charts/types.ts b/src/lib/components/charts/types.ts new file mode 100644 index 0000000000..ee1214b610 --- /dev/null +++ b/src/lib/components/charts/types.ts @@ -0,0 +1,36 @@ +/** + * Shared types used across chart components + */ + +/** + * Unit range configuration for automatic unit scaling + * Should at least have base unit with threshold 1 + * @example [{ threshold: 1, label: 'B' }, { threshold: 1000, label: 'kB' }] + */ +export type UnitRange = { + threshold: number; + label: string; +}[]; + +/** + * Time-based chart configuration + * @param startDate - Start date + * @param endDate - End date + * @param interval - Interval in milliseconds + */ +export type TimeType = { + type: 'time'; + timeRange: { + startDate: Date; + endDate: Date; + interval: number; + }; +}; + +/** + * Category-based chart configuration + */ +export type CategoryType = { + type: 'category'; + gap?: number; +}; diff --git a/src/lib/components/globalhealthbar/GlobalHealthBar.component.tsx b/src/lib/components/globalhealthbar/GlobalHealthBar.component.tsx deleted file mode 100644 index 1e5acad2f8..0000000000 --- a/src/lib/components/globalhealthbar/GlobalHealthBar.component.tsx +++ /dev/null @@ -1,204 +0,0 @@ -// @ts-nocheck -import { useMemo } from 'react'; -import { VegaChart } from '../vegachartv2/VegaChartV2.component'; -import { useTheme } from 'styled-components'; -import { formatValue } from './tooltip/index'; -export const TOP = 'top'; -export const BOTTOM = 'bottom'; -type Position = typeof TOP | typeof BOTTOM; -export type GlobalHealthProps = { - id: string; - alerts: { - description: string; - startsAt: string; - endsAt: string; - severity: string; - }[]; - start: string; - end: string; - height?: number; - tooltipPosition?: Position; -}; -/** - * @deprecated Use GlobalHealthBar v2 instead - * @example import { GlobalHealthBar } from '@scality/core-ui/dist/next'; - */ -function GlobalHealthBar({ - id, - alerts, - start, - end, - height = 8, - tooltipPosition = TOP, -}: GlobalHealthProps) { - const theme = useTheme(); - const trimAlerts = alerts.map((alert) => { - if (new Date(alert.startsAt) < new Date(start)) { - return { ...alert, startsAt: start }; - } - - return { ...alert }; - }); - trimAlerts.unshift({ - startsAt: start, - endsAt: end, - severity: 'healthy', - }); - const spec = { - width: 'container', - height, - data: { - values: trimAlerts, - }, - transform: [ - { - calculate: - "datum.description !== '' ? 'View details on alert page' : ''", - as: 'title', - }, - ], - view: { - cornerRadius: 6, - }, - config: { - style: { - cell: { - stroke: 'transparent', - strokeWidth: 0, - }, - }, - }, - layer: [ - // Paint the entire bar with green - { - mark: { - type: 'rect', - clip: true, - }, - encoding: { - color: { - value: theme.statusHealthy, - }, - }, - }, // Paint the timespan as x-axis - { - mark: { - type: 'rect', - cursor: 'pointer', - clip: true, - }, - encoding: { - x: { - field: 'startsAt', - type: 'temporal', - title: null, - stack: null, - axis: { - format: '%d%b %H:%M:%S', - ticks: true, - tickCount: 5, - //A desired number of ticks, for axes visualizing quantitative scales. The resulting number may be different so that values are “nice” (multiples of 2, 5, 10) and lie within the underlying scale’s range. - labelFlush: 20, - labelColor: theme.textSecondary, - }, - }, - x2: { - field: 'endsAt', - }, - color: { - value: theme.statusHealthy, - }, - }, - }, - { - mark: { - type: 'rect', - tooltip: true, - cursor: 'pointer', - clip: true, - }, - params: [ - { - name: 'highlight', - // The supported DOM event types for mark items are https://vega.github.io/vega/docs/event-streams/ - select: { - type: 'point', - on: 'mouseover', - clear: 'mouseout', - }, - }, - ], - encoding: { - x: { - timeUnit: 'yearmonthdatehoursminutes', - field: 'startsAt', - type: 'temporal', - title: null, - }, - x2: { - field: 'endsAt', - }, - color: { - field: 'severity', - type: 'nominal', - title: 'null', - scale: { - domain: ['healthy', 'critical', 'unavailable', 'warning'], - range: [ - theme.statusHealthy, - theme.statusCritical, - theme.textSecondary, - theme.statusWarning, - ], - }, - legend: null, - }, - tooltip: [ - { - field: 'severity', - title: 'Severity', - }, - { - field: 'startsAt', - title: 'Start', - type: 'temporal', - timeUnit: 'yearmonthdatehoursminutes', - }, - { - field: 'endsAt', - title: 'End', - type: 'temporal', - timeUnit: 'yearmonthdatehoursminutes', - }, - { - field: 'title', - title: 'title', - }, - { - field: 'description', - title: 'Description', - }, - ], - opacity: { - condition: { - param: 'highlight', - value: 1, - }, - value: 0.6, - }, - }, - }, - ], - }; - return ( - formatValue(), [])} - > - ); -} - -export { GlobalHealthBar }; diff --git a/src/lib/components/globalhealthbar/tooltip/index.ts b/src/lib/components/globalhealthbar/tooltip/index.ts deleted file mode 100644 index f498246aac..0000000000 --- a/src/lib/components/globalhealthbar/tooltip/index.ts +++ /dev/null @@ -1,72 +0,0 @@ -// @ts-nocheck -import { stringify } from 'vega-lite'; -import { isArray, isObject, isString } from 'vega-util'; - -/** - * Format the value to be shown in the tooltip. - * - * @param value The value to show in the tooltip. - * @param valueToHtml Function to convert a single cell value to an HTML string - */ -export function formatValue() { - return ( - value: any, - valueToHtml: (value: any) => string, - maxDepth: number, - ): string => { - if (isArray(value)) { - return `[${value - .map((v) => valueToHtml(isString(v) ? v : stringify(v, maxDepth))) - .join(', ')}]`; - } - - if (isObject(value)) { - let content = ''; - const { title, image, ...rest } = value; - - if (title) { - content += `

${valueToHtml(title)}

`; - } - - if (image) { - content += ``; - } - - const keys = Object.keys(rest); - - if (keys.length > 0) { - content += ''; - - for (const key of keys) { - let val = rest[key]; - - // ignore undefined properties - if (val === undefined) { - continue; - } - - if (isObject(val)) { - val = stringify(val, maxDepth); - } - - if (val && val !== 'undefined' && val !== 'null') { - content += ` - - - `; - } - } - - content += `
- ${valueToHtml(key)}: - - ${valueToHtml(val)} -
`; - } - - return content || '{}'; // show empty object if there are no properties - } - - return valueToHtml(value); - }; -} diff --git a/src/lib/components/icon/Icon.component.tsx b/src/lib/components/icon/Icon.component.tsx index 747467d063..7e7991612f 100644 --- a/src/lib/components/icon/Icon.component.tsx +++ b/src/lib/components/icon/Icon.component.tsx @@ -153,7 +153,10 @@ type IconProps = { title?: string; }; -export const customIcons: Record JSX.Element) & { displayName?: string }> = { +export const customIcons: Record< + string, + ((props: IconProps) => JSX.Element) & { displayName?: string } +> = { 'Remote-user': ({ 'aria-label': ariaLabel, color, size }) => ( ), @@ -259,7 +262,8 @@ function NonWrappedIcon({ } const [iconType, iconClass] = iconInfo.split(' '); - const fontAwesomeType = iconType === 'far' ? 'free-regular-svg-icons' : 'free-solid-svg-icons'; + const fontAwesomeType = + iconType === 'far' ? 'free-regular-svg-icons' : 'free-solid-svg-icons'; const cacheKey = `${fontAwesomeType}/${iconClass}`; if (iconCache[cacheKey]) { setIcon(iconCache[cacheKey]); @@ -267,11 +271,10 @@ function NonWrappedIcon({ } // Handle FontAwesome icons with dynamic import - import(`@fortawesome/${fontAwesomeType}/${iconClass}.js`) - .then((module) => { - setIcon(module[iconClass]); - iconCache[cacheKey] = module[iconClass]; - }); + import(`@fortawesome/${fontAwesomeType}/${iconClass}.js`).then((module) => { + setIcon(module[iconClass]); + iconCache[cacheKey] = module[iconClass]; + }); return () => setIcon(undefined); }, [name, iconInfo]); @@ -292,7 +295,7 @@ function NonWrappedIcon({ title={title} aria-label={`${name} ${ariaLabel}`} {...rest} - /> + /> ); } diff --git a/src/lib/components/linetemporalchart/ChartUtil.test.ts b/src/lib/components/linetemporalchart/ChartUtil.test.ts deleted file mode 100644 index 756cc94467..0000000000 --- a/src/lib/components/linetemporalchart/ChartUtil.test.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { - convert2VegaData, - getUnitLabel, - addMissingDataPoint, -} from './ChartUtil'; -import { Serie } from './LineTemporalChart.component'; -const series: Serie[] = [ - { - resource: 'node1', - data: [ - [1627460232, '18.73333333333335'], - [1627460952, '18.73333333333335'], - ], - getTooltipLabel: (metricPrefix, resource) => { - return `${resource}`; - }, - isLineDashed: false, - }, - { - resource: 'node2', - data: [ - [1627460232, '18.73333333333335'], - [1627460952, null], - ], - getTooltipLabel: (metricPrefix, resource) => { - return `${resource}`; - }, - isLineDashed: false, - }, -]; -const seriesSymmetrical: Serie[] = [ - { - metricPrefix: 'read', - resource: 'node1', - data: [ - [1627460232, '18.73333333333335'], - [1627460952, '18.73333333333335'], - ], - getTooltipLabel: (metricPrefix, resource) => { - return `${resource}-${metricPrefix}`; - }, - isLineDashed: false, - }, - { - metricPrefix: 'write', - resource: 'node1', - data: [ - [1627460232, '18.73333333333335'], - [1627460952, '18.73333333333335'], - ], - getTooltipLabel: (metricPrefix, resource) => { - return `${resource}-${metricPrefix}`; - }, - isLineDashed: false, - }, -]; -it('converts the series to a flat data structure', () => { - const result = convert2VegaData(series); - expect(result).toEqual([ - { - timestamp: 1627460232000, - label: 'node1', - resource: 'node1', - value: 18.73333333333335, - isNegativeValue: false, - isDashed: false, - }, - { - timestamp: 1627460952000, - label: 'node1', - resource: 'node1', - value: 18.73333333333335, - isNegativeValue: false, - isDashed: false, - }, - { - timestamp: 1627460232000, - label: 'node2', - resource: 'node2', - value: 18.73333333333335, - isNegativeValue: false, - isDashed: false, - }, - { - timestamp: 1627460952000, - label: 'node2', - resource: 'node2', - value: 'NAN', - isNegativeValue: false, - isDashed: false, - }, - ]); -}); -it('converts the series to a flat data structure for symmetrical chart', () => { - const result = convert2VegaData(seriesSymmetrical); - expect(result).toEqual([ - { - timestamp: 1627460232000, - label: 'node1-read', - resource: 'node1', - value: 18.73333333333335, - isNegativeValue: true, - isDashed: false, - }, - { - timestamp: 1627460952000, - label: 'node1-read', - resource: 'node1', - value: 18.73333333333335, - isNegativeValue: true, - isDashed: false, - }, - { - timestamp: 1627460232000, - label: 'node1-write', - resource: 'node1', - value: 18.73333333333335, - isNegativeValue: false, - isDashed: false, - }, - { - timestamp: 1627460952000, - label: 'node1-write', - resource: 'node1', - value: 18.73333333333335, - isNegativeValue: false, - isDashed: false, - }, - ]); -}); -const unitRange = [ - { - threshold: 0, - label: 'B/Sec', - }, - { - threshold: 1024, - label: 'KiB/Sec', - }, - { - threshold: 1024 * 1024, - label: 'MiB/Sec', - }, - { - threshold: 1024 * 1024 * 1024, - label: 'GiB/Sec', - }, -]; -it('returns the unit label B/Sec with 0 as valueBase', () => { - const maxValue = 1023; - const { unitLabel, valueBase } = getUnitLabel(unitRange, maxValue); - expect(unitLabel).toEqual('B/Sec'); - expect(valueBase).toEqual(0); -}); -it('returns the unit label KiB/Sec', () => { - const maxValue = 1024; - const { unitLabel, valueBase } = getUnitLabel(unitRange, maxValue); - expect(unitLabel).toEqual('KiB/Sec'); - expect(valueBase).toEqual(1024); -}); -it('returns the unit label GiB/Sec', () => { - const maxValue = 1024 * 1024 * 1024 + 1; - const { unitLabel, valueBase } = getUnitLabel(unitRange, maxValue); - expect(unitLabel).toEqual('GiB/Sec'); - expect(valueBase).toEqual(1024 * 1024 * 1024); -}); -// test for addMissingDataPoint function -const originalValue: [number, number | string | null][] = [ - [0, '0'], - [1, '1'], - [2, '2'], - [3, '3'], - [4, '4'], - [5, '5'], - [6, '6'], - [8, '8'], - [9, '9'], - [10, '10'], -]; -const startingTimeStamp = 0; -const sampleDuration = 10; -const sampleFrequency = 1; -const newValues = [ - [0, '0'], - [1, '1'], - [2, '2'], - [3, '3'], - [4, '4'], - [5, '5'], - [6, '6'], - [7, 'NAN'], - [8, '8'], - [9, '9'], - [10, '10'], -]; -it('should add missing data point with null', () => { - const result = addMissingDataPoint( - originalValue, - startingTimeStamp, - sampleDuration, - sampleFrequency, - ); - expect(result).toEqual(newValues); -}); -// We manually add -it('should return the array with string NAN when the original dataset is empty', () => { - const result = addMissingDataPoint( - [], - startingTimeStamp, - sampleDuration, - sampleFrequency, - ); - expect(result).toEqual([]); -}); -it('should return an empty array when the starting timestamp is undefined', () => { - const result = addMissingDataPoint( - originalValue, - undefined, - sampleDuration, - sampleFrequency, - ); - expect(result).toEqual([]); -}); -it('should return an empty array when sample duration is less than or equal to zero', () => { - const result = addMissingDataPoint( - originalValue, - startingTimeStamp, - 0, - sampleFrequency, - ); - expect(result).toEqual([]); -}); -it('should return an empty array when sample frequency is less than or equal to zero', () => { - const result = addMissingDataPoint( - originalValue, - startingTimeStamp, - sampleDuration, - -1, - ); - expect(result).toEqual([]); -}); -it('should return an empty array when sample frequency is undefined', () => { - const result = addMissingDataPoint( - originalValue, - startingTimeStamp, - sampleDuration, - undefined, - ); - expect(result).toEqual([]); -}); diff --git a/src/lib/components/linetemporalchart/ChartUtil.ts b/src/lib/components/linetemporalchart/ChartUtil.ts deleted file mode 100644 index 5cb72e0792..0000000000 --- a/src/lib/components/linetemporalchart/ChartUtil.ts +++ /dev/null @@ -1,225 +0,0 @@ -// @ts-nocheck -import { Serie } from './LineTemporalChart.component'; -import './LineTemporalChart.component'; -import { NAN_STRING } from '../constants'; -export type VegaData = { - timestamp: number; - label: string; - // same as the tooltip label - value: number | 'NAN'; - // the "NAN" is used by the tooltip to display a dash for the data which are not exist. - isNegativeValue: boolean; - // if the metricPrefix is read and out, we need to convert the value to negative before assigning it to the vega-lite spec - isDashed: boolean; -}[]; -//Given a field containing dots ".", vega is interpreting this as an accessor -//for nested objects, breaking then the internal mechnism to retrieve tooltip -//data from the mouse position. We are fixing it by replacing the dots by a -//well-known string that we expect no one would use to label a serie. -export function normlizeVegaFieldName(fieldName: string) { - return fieldName.replace(/(\.)/g, 'VEGADOESNTSUPPORTDOTINFIELDNAME'); -} -export function convert2VegaData( - addedMissingDataPointSeries: Serie[], -): VegaData { - const flatArr = []; - addedMissingDataPointSeries.forEach((line) => { - line.data.forEach((datum) => { - const obj = { - timestamp: datum[0] * 1000, - // convert to million second - label: normlizeVegaFieldName( - line.getTooltipLabel(line.metricPrefix, line.resource), - ), - resource: line.resource, - value: - datum[1] && datum[1] !== NAN_STRING ? Number(datum[1]) : NAN_STRING, - isNegativeValue: - line.metricPrefix === 'read' || line.metricPrefix === 'out', - isDashed: line.isLineDashed || false, - }; - flatArr.push(obj); - }); - }); - return flatArr; -} -// base on the current base value, convert the original vegadata to -export function convertDataBaseValue(data: VegaData, base: number): VegaData { - return data.map((datum) => { - return { - ...datum, - value: - typeof datum.value === 'number' - ? getRelativeValue(datum.value, base) - : NAN_STRING, - }; - }); -} - -/** - * Return the unit label base on the current dataset, and the valueBase which is used to convert the data - * @param {any} unitRange - * @param {any} maxValue the maximum value among the data set - * @returns {any} - */ -export function getUnitLabel( - unitRange: { - threshold: number; - label: string; - }[], - maxValue: number, -): { - valueBase: number; - unitLabel: string; -} { - if (!unitRange || unitRange.length === 0) { - return { - valueBase: 1, - unitLabel: '', - }; - } - // first sort the unitRange - unitRange.sort( - ( - unitA: { - threshold: number; - label: string; - }, - unitB: { - threshold: number; - label: string; - }, - ) => { - return unitA.threshold - unitB.threshold; - }, - ); - let index = unitRange.findIndex((range) => range.threshold > maxValue); - - // last unit - if (index === -1) { - index = unitRange.length; - } - - if (index === 0) { - return { - valueBase: unitRange[index].threshold, - unitLabel: unitRange[index].label, - }; - } - - return { - valueBase: unitRange[index - 1].threshold, - unitLabel: unitRange[index - 1].label, - }; -} - -/** - * This function manually adds the missing data points with `null` value caused by downtime of the VMs - * Missing data points are only added when the gap between consecutive data points is bigger than 2 intervals - * - * @param {array} orginalValues - The array of the data points are already sorted according to the time series - * @param {number} startingTimeStamp - The starting timestamp in seconds - * @param {number} sampleDuration - The time span value in seconds - * @param {number} sampleInterval - The time difference between two data points in seconds - * - */ -export function addMissingDataPoint( - orginalValues: [number, number | string | null][], - startingTimeStamp?: number, - sampleDuration?: number, - sampleInterval?: number, -): [number, number | string | null][] { - if ( - !orginalValues || - startingTimeStamp === undefined || - !sampleDuration || - !sampleInterval || - startingTimeStamp < 0 || - sampleDuration <= 0 || - sampleInterval <= 0 - ) { - return []; - } - - // If there are no original values, return empty array - if (orginalValues.length === 0) { - return []; - } - - const newValues: [number, number | string | null][] = []; - - // add missing data points for the starting time - for ( - let i = startingTimeStamp; - i < orginalValues[0][0]; - i += sampleInterval - ) { - newValues.push([i, NAN_STRING]); - } - - // Process all but the last element - for (let i = 0; i < orginalValues.length - 1; i++) { - if ( - orginalValues[i][0] < startingTimeStamp || - orginalValues[i][0] > startingTimeStamp + sampleDuration - ) { - continue; - } - - // Always add the current data point - newValues.push(orginalValues[i]); - - const currentTimestamp = orginalValues[i][0]; - const nextTimestamp = orginalValues[i + 1][0]; - const gap = nextTimestamp - currentTimestamp; - - // Calculate how many missing points to add - const missingIntervals = Math.floor(gap / sampleInterval) - 1; - - // Add missing data points with NAN_STRING (only executes if missingIntervals > 0) - for (let j = 1; j <= missingIntervals; j++) { - const missingTimestamp = currentTimestamp + j * sampleInterval; - newValues.push([missingTimestamp, NAN_STRING]); - } - } - - // Add the last element - newValues.push(orginalValues[orginalValues.length - 1]); - - // add missing data points for the ending time - for ( - let i = orginalValues[orginalValues.length - 1][0] + sampleInterval; - i < startingTimeStamp + sampleDuration; - i += sampleInterval - ) { - newValues.push([i, NAN_STRING]); - } - - return newValues; -} - -// get the value for the based value -// TODO: We need to handle the negative value in the future -export const getRelativeValue = (value: number, base: number) => { - return value / (base || 1); -}; -export const relativeDatumToOriginalDatum = (datum: T, base: number): T => { - // $FlowFixMe - return Object.fromEntries( - Object.entries(datum).map(([key, value]) => [ - key, - getAbsoluteValue(value, base), - ]), - ); -}; -export const getAbsoluteValue = (relativeValue: number, base: number) => { - return relativeValue * (base || 1); -}; -// extract the labels from getTooltipLabel function to define the domain in color -export const getColorDomains = (series: Serie[]): string[] => { - return series.map((serie) => { - return normlizeVegaFieldName( - serie.getTooltipLabel(serie.metricPrefix, serie.resource), - ); - }); -}; diff --git a/src/lib/components/linetemporalchart/LineTemporalChart.component.tsx b/src/lib/components/linetemporalchart/LineTemporalChart.component.tsx deleted file mode 100644 index f4aacd2f2e..0000000000 --- a/src/lib/components/linetemporalchart/LineTemporalChart.component.tsx +++ /dev/null @@ -1,800 +0,0 @@ -// @ts-nocheck -import { useMemo, useRef, useLayoutEffect, Fragment } from 'react'; -import styled, { useTheme } from 'styled-components'; -import { lighten, darken } from 'polished'; -import { expressionFunction } from 'vega'; -import { - lineColor1, - lineColor2, - lineColor3, - lineColor4, - lineColor5, - lineColor6, - lineColor7, - lineColor8, -} from '../../style/theme'; -import { VegaChart } from '../vegachartv2/VegaChartV2.component'; -import { useCursorX } from '../vegachartv2/SyncedCursorCharts'; -import { ChartTitleText } from '../text/Text.component'; -import { - convert2VegaData, - getUnitLabel, - convertDataBaseValue, - addMissingDataPoint, - getRelativeValue, - getColorDomains, - relativeDatumToOriginalDatum, - normlizeVegaFieldName, -} from './ChartUtil'; -import { useMetricsTimeSpan } from './MetricTimespanProvider'; -import { spacing } from '../../spacing'; -import { SmallerText } from '../text/Text.component'; -import { Loader } from '../loader/Loader.component'; -import { formatValue } from './tooltip/index'; -import { Tooltip as TooltipComponent } from '../tooltip/Tooltip.component'; -import { Icon } from '../icon/Icon.component'; -// some predefined values -export const YAXIS_TITLE_READ_WRITE = 'write(+) / read(-)'; -export const YAXIS_TITLE_IN_OUT = 'in(+) / out(-)'; -export const UNIT_RANGE_BS = [ - { - threshold: 0, - label: 'B/s', - }, - { - threshold: 1024, - label: 'KiB/s', - }, - { - threshold: 1024 * 1024, - label: 'MiB/s', - }, - { - threshold: 1024 * 1024 * 1024, - label: 'GiB/s', - }, - { - threshold: 1024 * 1024 * 1024 * 1024, - label: 'TiB/s', - }, -]; -const LineTemporalChartWrapper = styled.div` - display: flex; - flex-direction: column; - justify-content: flex-start; // to make sure the header, the graph itself and legend are aligned -`; -const Legends = styled.div` - display: flex; - align-items: center; -`; -const LegendStroke = styled.div` - margin: 0 ${spacing.r8} 0 ${spacing.r16}; - height: ${spacing.r2}; - background: ${(props) => - props.isLineDashed - ? `repeating-linear-gradient(to right,${props.lineColor} 0,${props.lineColor} ${spacing.r1},transparent ${spacing.r1},transparent ${spacing.r2})` - : props.lineColor}; - width: ${spacing.r8}; -`; -const ChartHeader = styled.div` - display: flex; - align-items: center; -`; - -/* -We need to convert the data from raw prometheus data in the following steps: -prometheusData => series => addMissingDataPoint(series.data, startTimeStamp, sampleDuration, sampleFrequency) => VegaData - -The data structure of multi-series chart using Vega-lite library -https://vega.github.io/vega-lite/examples/interactive_multi_line_pivot_tooltip.html -*/ -export type Serie = { - resource: string; - // the name of the resource - data: [number, string | null][]; - // the original data format from prometheus - getTooltipLabel: (metricPrefix?: string, resource?: string) => string; - // it's mandatory to display tooltip label in the tooltip - getLegendLabel?: (metricPrefix?: string, resource?: string) => string; - // get the legend label for each of the series - color?: string; - // optional color field to specify the color of the line - metricPrefix?: string; - // the name of the metric prefix with read, write, in, out - isLineDashed?: boolean; // to specify if the line is dash -}; -export type LineChartProps = { - series: Serie[]; - title: string; - height: number; - startingTimeStamp: number; - // pass to addMissingDataPoint() - unitRange?: { - threshold: number; - label: string; - }[]; - isLoading?: boolean; - // if to display a loader next to the title - isLegendHidden?: boolean; - yAxisType?: 'default' | 'percentage' | 'symmetrical'; - yAxisTitle?: string; - helpText?: string | JSX.Element; - onHover?: (dataPoint: any) => void; - renderTooltipSerie?: ( - arg0: { - color?: string; - isLineDashed?: boolean; - name: string; - value: string; - key: string; - unitLabel: string; - }, - tooltipData: any, - ) => string; -}; -// Custom formatter to display negative value as an absolute value in read/write, in/out chart -expressionFunction('negativeValueFormatter', function (datum) { - return Math.abs(datum).toFixed(2); -}); -// We use 8 main color from the palette and decline them (lighter/ darker) when we have more than 8 datasets -const colorRange = [ - lineColor1, - lineColor2, - lineColor3, - lineColor4, - lineColor5, - lineColor6, - lineColor7, - lineColor8, - lighten(0.3, lineColor1), - lighten(0.3, lineColor2), - lighten(0.3, lineColor3), - lighten(0.3, lineColor4), - lighten(0.3, lineColor5), - lighten(0.3, lineColor6), - lighten(0.3, lineColor7), - lighten(0.3, lineColor8), - darken(0.2, lineColor1), - darken(0.2, lineColor2), - darken(0.2, lineColor3), - darken(0.2, lineColor4), - darken(0.3, lineColor5), - darken(0.3, lineColor6), - darken(0.3, lineColor7), - darken(0.3, lineColor8), -]; - -// Note: we need to make sure the start time and end timefor the prometheus query between the series are the same. -/** - * @deprecated Use LineTimeSerieChart instead - * @example import { LineTimeSerieChart } from '@scality/core-ui/dist/next'; - */ -function LineTemporalChart({ - series, - title, - height, - startingTimeStamp, - unitRange, - isLoading = false, - isLegendHidden = false, - yAxisType = 'default', - yAxisTitle, - helpText, - renderTooltipSerie, - onHover, - ...rest -}: LineChartProps) { - // property validation - if (!['default', 'percentage', 'symmetrical'].includes(yAxisType)) { - console.error( - `Invalid yAxisType props, expected default, percentage or symmetrical but received ${yAxisType}`, - ); - } - - if (!height) { - console.error('Please specify the height of the chart.'); - } - - if (!title) { - console.error('Please specify the title of the chart.'); - } - - // we have to make sure that when prop unitRange exists, there is at least one item defined. - if (unitRange) { - if (unitRange.length === 0) { - console.error('Please provide at least one entry in unitRange.'); - } - } - - const vegaViewRef = useRef(); - const theme = useTheme(); - const { frequency, duration } = useMetricsTimeSpan(); - //##### Data Transformation Start - - /** - * 1. Add missing data points - * During the downtime of the platform, there is no data returned by Prometheus API. - * Hence, we can see a line link the last data point before the downtime and the first data point once the platform restarts. - * However, this is not what we expect to see. - * So we need to manually add the missing data points with the value `null` to make sure there is nothing displayed on the graph during the downtime. - */ - const addedMissingDataPointSeries = useMemo(() => { - return series.map((line) => { - return { - ...line, - data: addMissingDataPoint( - line.data, - startingTimeStamp, - duration, - frequency, - ), - }; - }); - }, [series, startingTimeStamp, duration, frequency]); - // 2. Change the data structure to a flat array which is required by Vega-lite - const vegaData = useMemo(() => { - return convert2VegaData(addedMissingDataPointSeries); - }, [addedMissingDataPointSeries]); - // 3. Search for the biggest value in order to define the unit for the chart, if the unit is needed. - const maxValue = useMemo(() => { - return Math.max.apply( - Math, - vegaData.map(function (datum: { - timestamp: number; - label: string; - value: number | 'NAN'; - }): number { - if (datum.value && typeof datum.value === 'number') { - return datum.value; - } - - return 0; - }), - ); - }, [vegaData]); - // 4. Recompute the value base on the unit - const valueBase = useMemo(() => { - return unitRange ? getUnitLabel(unitRange, maxValue).valueBase : 1; - }, [maxValue, unitRange]); - const vegaDataWithUnit = unitRange - ? convertDataBaseValue(vegaData, valueBase) - : vegaData; - // 5. Convert the values of metric prefix `read` and `out` to negative. - const vegaSpecValues = vegaDataWithUnit.map( - (data: { - timestamp: number; - label: string; - // same as the tooltip label - value: number | 'NAN'; - // manually set it to a string. It's for the tooltip to display a hyphen for the data that don't exist - isNegativeValue: boolean; - }) => { - if ( - data.isNegativeValue && - data.value && - typeof data.value === 'number' - ) { - return { ...data, value: 0 - data.value }; - } else return { ...data }; - }, - ); - //##### Data Transformation End - const customizedColorRange = useMemo(() => { - const customizedColors = []; - series.map((line) => { - if (line.color) { - return customizedColors.push(line.color); - } - }); - return customizedColors; - }, [series]); - // $FlowFixMe - const seriesResources = [...new Set(series.map((serie) => serie.resource))]; - // for the series with the same resource, the color of legend and tooltip should be the same. - const legendLabels = useMemo(() => { - const uniqueLabel = []; - series.forEach((serie, index) => { - if (serie.getLegendLabel) { - const legend = serie.getLegendLabel(serie.metricPrefix, serie.resource); - - if (!uniqueLabel.find((uLabel) => uLabel === legend)) { - const serieIndex = - yAxisType === 'symmetrical' && !customizedColorRange.length - ? seriesResources.findIndex( - (serieResource) => serieResource === serie.resource, - ) - : index; - uniqueLabel.push({ - legend, - serie, - serieIndex, - }); - } - } - }); - return uniqueLabel; - }, [series]); - const tooltipLabels = useMemo( - () => - series.map((line) => { - return line.getTooltipLabel(line.metricPrefix, line.resource); - }), - [series], - ); - const syncedVerticalRuler = { - mark: 'rule', - encoding: { - x: { - datum: { - expr: 'toDate(cursorX)', - }, // convert the timestamp in milliseconds to Date object - //https://vega.github.io/vega-lite/docs/datetime.html - }, - color: { - value: theme.selectedActive, - }, - - /* - According to the design, the vertical ruler should be hided when the mouse points out of the graph area. - We can use param `isCursorDisplayed` to control the size of the vertical line. - */ - size: { - value: 0, - condition: { - test: 'isCursorDisplayed', - value: 1, - }, - }, - }, - }; - const syncedVerticalRulerPercentage = { - mark: 'rule', - encoding: { - x: { - datum: { - expr: 'toDate(cursorX)', - }, // convert the timestamp to Date object - }, - // We draw the line manually for the percentage chart to solve the issue that the synced vertical line can - // only reach the max value one the line. - y: { - datum: 0, - }, - y2: { - datum: 100, - }, - color: { - value: theme.highlight, - opacity: 0.3, - }, - - /* - According to the design, the vertical ruler should be hided when the mouse points out of the graph area. - We can use param `isCursorDisplayed` to control the size of the vertical line. - */ - size: { - value: 0, - condition: { - test: 'isCursorDisplayed', - value: 1, - }, - }, - }, - }; - const syncedVerticalRulerSymmetrical = { - mark: 'rule', - encoding: { - x: { - datum: { - expr: 'toDate(cursorX)', - }, // convert the timestamp to Date object - }, - y: { - expr: '-yAxisMaxValue', - }, - y2: { - expr: 'yAxisMaxValue', - }, - color: { - value: theme.highlight, - opacity: 0.3, - }, - - /* - According to the design, the vertical ruler should be hided when the mouse points out of the graph area. - We can use param `isCursorDisplayed` to control the size of the vertical line. - */ - size: { - value: 0, - condition: { - test: 'isCursorDisplayed', - value: 1, - }, - }, - }, - }; - const xAxis = { - field: 'timestamp', - type: 'temporal', - axis: { - // Refer to all the available time format: https://github.com/d3/d3-time-format#locale_format - format: '%d %b %H:%M', - ticks: true, - tickCount: 5, - labelColor: theme.textSecondary, - // TODO: labelFontSize is not responsiveness - labelSeparation: 12, // The minimum separation that must be between label bounding boxes for them to be considered non-overlapping (default 0). This property is ignored if labelOverlap resolution is not enabled. - }, - title: null, - }; - const yAxis = useMemo(() => { - return { - field: 'value', - type: 'quantitative', - axis: { - title: yAxisTitle ? yAxisTitle : ' ', - orient: 'right', - translate: -5, - // translate both the x and y coordinates by 5 pixel - tickOffset: 5, - // shift back the y translate to make sure the tick align with the 0 seperation line - labelBaseline: 'middle', - labelPadding: 6, - labelFlush: true, - }, - scale: - yAxisType === 'symmetrical' - ? { - domain: [ - { - expr: '-yAxisMaxValue', - }, - { - expr: 'yAxisMaxValue', - }, - ], - } - : yAxisType === 'percentage' - ? { - domain: [0, 100], - } - : undefined, - }; - }, [yAxisTitle, yAxisType]); - const symmetricalColorRange = - yAxisType !== 'symmetrical' - ? colorRange - : series.map( - (serie) => colorRange[seriesResources.indexOf(serie.resource)], - ); - const color = { - field: 'label', - type: 'nominal', - scale: { - domain: getColorDomains(series), - // the order of the domain should be the same as the order of colorRange, otherwise the colors will be assigned to the line base on the alphabetical order: ; - range: customizedColorRange.length - ? customizedColorRange - : symmetricalColorRange, //if there is no customized color range, we will use the default the line color - }, - legend: null, - }; - - // In order to add the tooltip we refered this example - // https://vega.github.io/vega-lite/examples/interactive_multi_line_pivot_tooltip.html - const getTooltipConfig = ( - fields: { - field: string; - type: string; - title: string; - format: string; - }[], - ) => { - const tooltipConfigBase = { - // The pivot transform maps unique values from a field to new aggregated fields (columns) in the output stream. - // https://vega.github.io/vega-lite/docs/pivot.html - transform: [ - { - pivot: 'label', - value: 'value', - groupby: ['timestamp'], - }, - ], - mark: 'rule', - encoding: { - x: xAxis, - opacity: { - // to be check if we can remove this channel, since we don't need to have a rule next to the tooltip - condition: { - value: 0, - selection: 'hover', - }, - value: 0, - }, - tooltip: [ - { - field: 'timestamp', - type: 'temporal', - axis: { - format: '%d %b %H:%M:%S', - ticks: true, - tickCount: 4, - labelAngle: -50, - labelColor: '#B5B5B5', - }, - title: 'title', - }, - ], - }, - selection: { - hover: { - type: 'single', - fields: ['timestamp'], - nearest: true, - on: 'mouseover', - empty: 'none', - clear: 'mouseout', - }, - }, - }; - - if (fields.length) { - const newFields = [...tooltipConfigBase.encoding.tooltip, ...fields]; - const newConfig = Object.assign({}, tooltipConfigBase); - newConfig.encoding.tooltip = newFields; - return newConfig; - } - - return tooltipConfigBase; - }; - - const tooltipConfig = useMemo( - () => - getTooltipConfig( - (() => { - const res = []; - tooltipLabels.forEach((label) => { - res.push({ - field: `${normlizeVegaFieldName(label)}`, - type: 'quantitative', - title: `${label}`, - format: '.2f', - formatType: 'negativeValueFormatter', - }); - }); - return res; - })(), - ), - [tooltipLabels], - ); - // we need to retrieve the vega view in order to update the signal - useLayoutEffect(() => { - if (vegaViewRef.current && yAxisType === 'symmetrical') { - vegaViewRef.current - .signal( - 'yAxisMaxValue', - Math.ceil(getRelativeValue(maxValue, valueBase)), - ) - .run(); - } - }, [maxValue, valueBase, vegaViewRef, yAxisType]); - // $FlowFixMe - const cursorX = useCursorX().cursorX; - // the specification of the Vega-lite chart - const spec = { - data: { - values: vegaSpecValues, - }, - height, - width: 'container', - // set responsive width - mark: { - type: 'line', - tooltip: true, - }, - // Add two params to control the display of the vertical ruler. - params: [ - { - name: 'cursorX', - value: cursorX || Date.now(), // the value of signal can't be null - }, - { - name: 'isCursorDisplayed', - value: false, - }, - ], - layer: [ - { - encoding: { - x: xAxis, - y: yAxis, - strokeDash: { - field: 'isDashed', - type: 'nominal', - legend: null, - condition: { - test: 'datum.isDashed === true', - value: [4, 2], // Change the value here if the dash is not visible. https://vega.github.io/vega-lite/docs/mark.html#stroke - }, - }, - color: color, - opacity: { - condition: { - test: 'datum.isDashed === true', - // for the dashed line, set the opacity to 0.5 - value: 0.6, - }, - value: 1, - }, - }, - layer: [ - { - mark: { - type: 'line', - strokeWidth: 1, - }, - }, // the width of the line should be 1px - { - mark: 'point', - encoding: { - size: { - value: 0, - condition: { - selection: 'hover', - value: 10, - }, - }, - }, - }, - yAxisType === 'percentage' - ? { - // for percentage chart we need to manually draw the line from 0-100 - ...syncedVerticalRuler, - encoding: { - ...syncedVerticalRuler.encoding, - ...syncedVerticalRulerPercentage.encoding, - }, - } - : yAxisType === 'symmetrical' - ? { - // for symmetrical chart we manually draw the line from minValue to maxValue - ...syncedVerticalRuler, - encoding: { - ...syncedVerticalRuler.encoding, - ...syncedVerticalRulerSymmetrical.encoding, - }, - } - : syncedVerticalRuler, - ], - }, - tooltipConfig, - ], - ...rest, - }; - // the seperation line for symmetrical charts - const seperationLine = { - mark: 'rule', - encoding: { - y: { - datum: 0, - }, - color: { - value: theme.border, - opacity: 1, - }, - }, - }; - - if (yAxisType === 'symmetrical') { - spec.layer.unshift(seperationLine); - spec.params.push({ - name: 'yAxisMaxValue', - value: Math.ceil(getRelativeValue(maxValue, valueBase)), - }); - } - - const seriesNames = series - .map( - (serie) => - title + serie.resource + (serie.metricPrefix ? serie.metricPrefix : ''), - ) - .join(','); - const unitLabel = unitRange - ? getUnitLabel(unitRange, maxValue).unitLabel - : yAxisType === 'percentage' - ? '%' - : ''; - return ( - - - {unitLabel ? ( - - {title} ({unitLabel}) - // for the chart doesn't have title - ) : ( - {title} - )} - {helpText && ( - - {helpText} - - } - > - - - )} - {isLoading && ( - - )} - - {/* When the chart is in loading status, we display the chart skeleton */} - { - if (onHover) { - onHover({ - ...datum, - metadata: { - unitLabel, - valueBase, - }, - originalData: { - ...relativeDatumToOriginalDatum(datum, valueBase), - timestamp: datum.timestamp, - }, - }); - } - }} - theme={'custom'} - ref={vegaViewRef} - formatTooltip={useMemo( - () => - formatValue( - series, - customizedColorRange, - colorRange, - unitLabel, - yAxisType, - renderTooltipSerie, - ), - [unitLabel, seriesNames, renderTooltipSerie], - )} - > - {/* if it's for read/write and in/out graph, we only display the legends for the instances. */} - {!isLegendHidden && ( - - {legendLabels.map(({ legend, serie, serieIndex }, index) => { - return ( - - - {legend} - - ); - })} - - )} - - ); -} - -export { LineTemporalChart }; diff --git a/src/lib/components/linetemporalchart/tooltip/index.ts b/src/lib/components/linetemporalchart/tooltip/index.ts deleted file mode 100644 index 2ef140de5a..0000000000 --- a/src/lib/components/linetemporalchart/tooltip/index.ts +++ /dev/null @@ -1,178 +0,0 @@ -// @ts-nocheck -import { stringify } from 'vega-lite'; -import { Options } from 'vega-tooltip'; -import { Handler } from 'vega-tooltip'; -import { isArray, isObject, isString } from 'vega-util'; -import { spacing } from '../../../style/theme'; -import { Serie } from '../LineTemporalChart.component'; -export function defaultRenderTooltipSerie({ - color, - isLineDashed, - name, - value, - key, -}: { - color?: string; - isLineDashed?: boolean; - name: string; - value: string; - key: string; -}) { - return ` - - ${ - color !== undefined - ? `` - : '' - } - - - ${name} - - - ${value} - - `; -} -export class TooltipHandlerWithPaint extends Handler { - constructor(options?: Options, onHover?: (datum: any) => void) { - super(options); - this.prevCall = this.call; - this.onHover = onHover; - this.call = this.newCall.bind(this); - } - - newCall(handler: any, event: MouseEvent, item: any, value: any) { - if ( - item && - this.onHover && - JSON.stringify(value) !== JSON.stringify(this.value) - ) { - this.onHover(item.datum.datum); - } - - this.handler = handler; - this.event = event; - this.item = item; - this.value = value; - this.prevCall(handler, event, item, value); - } - - paint() { - this.prevCall(this.handler, this.event, this.item, this.value); - } -} - -/** - * Format the value to be shown in the tooltip. - * - * @param value The value to show in the tooltip. - * @param valueToHtml Function to convert a single cell value to an HTML string - */ -export function formatValue( - series: Serie[], - customizedColorRange: string[], - colorRange: string[], - unitLabel: string, - yAxisType?: 'default' | 'percentage' | 'symmetrical', - renderTooltipSerie: ( - arg0: { - color?: string; - isLineDashed?: boolean; - name: string; - value: string; - key: string; - unitLabel: string; - }, - tooltipData: any, - ) => string = defaultRenderTooltipSerie, -) { - return ( - value: any, - valueToHtml: (value: any) => string, - maxDepth: number, - ): string => { - if (isArray(value)) { - return `[${value - .map((v) => valueToHtml(isString(v) ? v : stringify(v, maxDepth))) - .join(', ')}]`; - } - - if (isObject(value)) { - let content = ''; - const { title, image, ...rest } = value; - - if (title) { - content += `

${valueToHtml(title)}

`; - } - - if (image) { - content += ``; - } - - const keys = Object.keys(rest); - - if (keys.length > 0) { - content += ''; - - for (const key of keys) { - let val = rest[key]; - - // ignore undefined properties - if (val === undefined) { - continue; - } - - if (isObject(val)) { - val = stringify(val, maxDepth); - } - - const currentSerie = series.find( - (serie) => - serie.getTooltipLabel(serie.metricPrefix, serie.resource) === key, - ); - const currentSerieIndex = series.findIndex( - (serie) => - serie.getTooltipLabel(serie.metricPrefix, serie.resource) === key, - ); - const serieIndex = - yAxisType === 'symmetrical' && !customizedColorRange.length // $FlowFixMe - ? [...new Set(series.map((serie) => serie.resource))].findIndex( - (serieResource) => - serieResource === - (currentSerie ? currentSerie.resource : null), - ) - : currentSerieIndex; - const serieColorRange = customizedColorRange.length - ? customizedColorRange - : colorRange; - content += renderTooltipSerie( - { - key, - color: - serieIndex !== -1 ? serieColorRange[serieIndex] : undefined, - isLineDashed: - serieIndex !== -1 ? series[serieIndex].isLineDashed : undefined, - name: valueToHtml(key), - value: val !== 'NaN' ? `${valueToHtml(val)} ${unitLabel}` : '-', - unitLabel, - }, - value, - ); - } - - content += `
`; - } - - return content || '{}'; // show empty object if there are no properties - } - - return valueToHtml(value); - }; -} diff --git a/src/lib/components/sparkline/sparkline.component.tsx b/src/lib/components/sparkline/sparkline.component.tsx deleted file mode 100644 index 467e5b2637..0000000000 --- a/src/lib/components/sparkline/sparkline.component.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useMemo } from "react"; -import { Area, AreaChart, CartesianGrid, ResponsiveContainer, YAxis } from "recharts"; -import { useTheme } from "styled-components"; -import { chartColors } from "../../style/theme"; -import { addMissingDataPoint } from "../linetemporalchart/ChartUtil"; - -type SparklineProps = { - serie: { - data: [number, number|null][], - color?: string, // exa color code like '#ff0000' - }, - startingTimeStamp: number, - sampleDuration: number, - sampleInterval: number, - yAxisType?: 'default' | 'percentage', -}; - -/** - * Sparkline is a simple dynamically sized area chart. - * Used to show trends in data over time. - */ -export function Sparkline({ serie, startingTimeStamp, sampleDuration, sampleInterval, yAxisType }: SparklineProps) { - const data = useMemo( - () => { - const dataMdp = addMissingDataPoint(serie.data, startingTimeStamp, sampleDuration, sampleInterval); - return dataMdp.map(([x, y]) => ({ x, y })); - }, - [serie.data] - ); - const color = serie.color ?? chartColors.lineColor1; - const strokeGridColor = useTheme().border; - - return ( - - - - - - - - - - - {yAxisType === 'percentage' && } - - - ); -} diff --git a/src/lib/components/vegachartv2/SyncedCursorCharts.tsx b/src/lib/components/vegachartv2/SyncedCursorCharts.tsx deleted file mode 100644 index b697b81eb2..0000000000 --- a/src/lib/components/vegachartv2/SyncedCursorCharts.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useState, createContext, useContext, useMemo, useCallback } from 'react'; -export const SyncedCursorChartsContext = createContext<{ - cursorX: number; - setCursorX: (cursorX: number) => void; -} | null>(null); -export const useCursorX = (): { - cursorX: number; - setCursorX: (cursorX: number) => void; -} | null => { - const contextValue = useContext(SyncedCursorChartsContext); - - if (contextValue === null) { - console.error("Can't use useCursorX() outside SyncedCursorCharts"); - } - - return contextValue; -}; -export function SyncedCursorCharts({ children }: { children: JSX.Element }) { - const [cursorX, setCursorX] = useState(0); - - const contextValue = useMemo(() => ({cursorX, setCursorX}), [cursorX]); - - return ( - - {children} - - ); -} diff --git a/src/lib/components/vegachartv2/VegaChartV2.component.tsx b/src/lib/components/vegachartv2/VegaChartV2.component.tsx deleted file mode 100644 index cdb40139f2..0000000000 --- a/src/lib/components/vegachartv2/VegaChartV2.component.tsx +++ /dev/null @@ -1,276 +0,0 @@ -// @ts-nocheck -import { useEffect, useRef, useLayoutEffect, useMemo, forwardRef } from 'react'; -import * as vega from 'vega'; -import vegaEmbed, { Result } from 'vega-embed'; -import { createGlobalStyle, css, useTheme } from 'styled-components'; -import { useCursorX, SyncedCursorChartsContext } from './SyncedCursorCharts'; -import { TooltipHandlerWithPaint } from '../linetemporalchart/tooltip'; -export const TOP = 'top'; -export const BOTTOM = 'bottom'; -type Position = typeof TOP | typeof BOTTOM; -type Props = { - spec: Record; - tooltipPosition?: Position; - theme?: 'light' | 'dark' | 'custom'; - id?: string; - onHover?: (dataPoint: any) => void; - formatTooltip?: ( - value: any, - valueToHtml: (value: any) => string, - maxDepth: number, - ) => string; -}; - -/* How to theme tooltip: -https://github.com/vega/vega-tooltip/blob/master/docs/customizing_your_tooltip.md -*/ -const VegaTooltipTheme = createGlobalStyle` - #vg-tooltip-element.vg-tooltip.custom-theme { - ${(props) => { - const { theme } = props; - return css` - padding: 8px; - position: fixed; - z-index: 1000; - width: calc(100vw / 6); - font-family: 'Lato'; - font-size: 12px; - border-radius: 3px; - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); - color: ${theme.textPrimary}; - background-color: ${theme.backgroundLevel1}; - border: 1px solid ${theme.border}; - // customize the title - h2 { - color: ${theme.textPrimary}; - margin-bottom: 10px; - font-size: 12px; - } - table { - width: 100%; - } - table tr td.key { - color: ${theme.textSecondary}; - } - `; - }} - - } -`; - -function VegaChartInternal( - { - spec, - tooltipPosition = BOTTOM, - theme = 'custom', - formatTooltip, - onHover, - }: Props, - ref?: { - current: typeof vega.View | null; - }, -) { - // $FlowFixMe - const { cursorX, setCursorX } = useCursorX(); - const currentTheme = useTheme(); - const themeConfig = { - config: { - background: 'transparent', - axis: { - labelColor: currentTheme.textSecondary, - titleColor: currentTheme.textSecondary, - grid: false, - domainColor: 'transparent', - }, - title: { - color: currentTheme.textPrimary, - font: 'Lato', - }, - view: { - stroke: currentTheme.border, - strokeWidth: 0.5, - fill: currentTheme.backgroundLevel1, - }, - // the headers provide a title and labels for faceted plots. - header: { - labelColor: currentTheme.textPrimary, - }, - // the label of max/min - text: { - color: currentTheme.textPrimary, - font: 'Lato', - }, - legend: { - labelColor: currentTheme.textSecondary, - }, - }, - }; - const themedSpec = { ...spec, ...themeConfig }; - const vegaInstance = useRef(); - const vegaDOMInstance = useRef(null); - let tooltipOptions = { - theme: theme, - formatTooltip: formatTooltip, - }; - - if (tooltipPosition === TOP) { - tooltipOptions = { - theme: theme, - offsetX: -85, - offsetY: -140, - formatTooltip: formatTooltip, - }; - } - - const tooltipHandler = useMemo( - () => new TooltipHandlerWithPaint(tooltipOptions, onHover), - [theme], - ); - - /* - useEffect() and useEffectLayout(): - The first effect will only render once, to initalize the chart and add the event lisener. - The second useEffectLayout is in charge of updating the chart when the `themedSpec` or `tooltipOptions` get updated. - Note it's important to useEffectLayout for the performance. - */ - useEffect(() => { - let isMounted = true; - // embed(el, spec[, opt]) the el can be a DOM element or CSS selector. https://github.com/vega/vega-embed - vegaDOMInstance && - vegaDOMInstance.current && - vegaEmbed(vegaDOMInstance.current, themedSpec, { - renderer: 'svg', - // Override the DEFAULT_OPTIONS https://github.com/vega/vega-tooltip/blob/master/src/defaults.ts - tooltip: tooltipHandler.call, - - /* Determines if action links - ("Export as PNG/SVG", "View Source", "View Vega" (only for Vega-Lite), "Open in Vega Editor") are included with the embedded view. - If the value is true, all action links will be shown and none if the value is false. */ - actions: false, - }) - .then((result) => { - vegaInstance.current = result; - // result.view contains the Vega view - // get the current state of view: result.view.getState() - const view = result.view; - - if (ref) { - ref.current = view; - } - - if (SyncedCursorChartsContext && view) { - view.addEventListener('mouseover', function (event, item) { - const currentTime = - item && - item.datum && - item.datum.datum && - item.datum.datum.timestamp; - - if (currentTime) { - setCursorX(currentTime); - } - }); - - /*When the mouse leaves the chart area, set the cursorX to null*/ - view.addEventListener('mouseleave', function (event, item) { - setCursorX(0); - }); - } - }) - .catch((...args) => { - if (isMounted) { - console.error(...args); // TODO: we should handle this with a retry or an error state of the component - } - }); - return () => { - isMounted = false; - - if (vegaInstance.current) { - vegaInstance.current.view.finalize(); - } - }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [vegaDOMInstance, currentTheme]); - - useLayoutEffect(() => { - if (vegaInstance.current) { - const view = vegaInstance.current.view; - tooltipHandler.options.formatTooltip = formatTooltip; - tooltipHandler.onHover = onHover; - view.tooltip(tooltipHandler.call).run(); - tooltipHandler.paint(); // to repaint the tooltip - } - }, [formatTooltip, vegaInstance, onHover]); - - useLayoutEffect(() => { - if (vegaInstance.current) { - const view = vegaInstance.current.view; - let changeset = vega - .changeset() - .remove(() => true) - .insert(themedSpec.data.values); - //only the data.values changes trigger the graph's repaint - // For some reason source_0 is the default dataset name - view - .change('source_0', changeset) - .runAsync() - .then(() => { - // call resize() after the data is loaded to make sure the width is set correctly. - view.resize().runAsync(); - }); - } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - // eslint-disable-next-line - JSON.stringify(themedSpec.data.values), - vegaInstance, - ]); - - useLayoutEffect(() => { - if (vegaInstance.current && themedSpec.params) { - const view = vegaInstance.current.view; - - // when the mouse go out, we trigger the event to set cursorX to null - if ( - !themedSpec.params.find((param) => param.name === 'cursorX').value || - !cursorX - ) { - view - .signal( - 'cursorX', - (themedSpec && - themedSpec.data && - themedSpec.data.values && - themedSpec.data.values[0] && - themedSpec.data.values[0].timestamp) || - Date.now(), - ) - .run(); - view.signal('isCursorDisplayed', false).run(); - } else { - view - .signal( - 'cursorX', - themedSpec.params.find((param) => param.name === 'cursorX').value, - ) - .run(); - view.signal('isCursorDisplayed', true).run(); - } - } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - // eslint-disable-next-line - JSON.stringify(themedSpec), - vegaInstance, - ]); - return ( -
- -
- ); -} // @ts-expect-error - -export const VegaChart = forwardRef(VegaChartInternal); diff --git a/src/lib/index.ts b/src/lib/index.ts index 9e93d00f3f..5f959e7c69 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -28,11 +28,13 @@ export { Tooltip } from './components/tooltip/Tooltip.component'; export { ProgressBar } from './components/progressbar/ProgressBar.component'; export { TextArea } from './components/textarea/TextArea.component'; -export { BarChart } from './components/barchart/BarChart.component'; +// BarChart (deprecated) - Use Barchart from @scality/core-ui/dist/next instead +// export { BarChart } from './components/barchart/BarChart.component'; export { CircularProgressBar } from './components/circularprogressbar/CircularProgressBar.component'; export { LateralNavbarLayout } from './components/lateralnavbarlayout/LateralNavbarLayout.component'; -export { GlobalHealthBar } from './components/globalhealthbar/GlobalHealthBar.component'; +// GlobalHealthBar (deprecated vega version) - Use GlobalHealthBar from @scality/core-ui/dist/next instead +// export { GlobalHealthBar } from './components/globalhealthbar/GlobalHealthBar.component'; export { ConstrainedText } from './components/constrainedtext/Constrainedtext.component'; export { EmptyState } from './components/emptystate/Emptystate.component'; export { EmptyTable } from './components/emptytable/Emptytable.component'; diff --git a/src/lib/next.ts b/src/lib/next.ts index 337242f61e..865038797e 100644 --- a/src/lib/next.ts +++ b/src/lib/next.ts @@ -4,36 +4,49 @@ export { Button } from './components/buttonv2/Buttonv2.component'; export { CopyButton } from './components/buttonv2/CopyButton.component'; export { Tabs, Tab } from './components/tabsv2/Tabsv2.component'; export { Table } from './components/tablev2/Tablev2.component'; -export { LineTemporalChart } from './components/linetemporalchart/LineTemporalChart.component'; + +// Keep MetricsTimeSpanProvider for backward compatibility (still used in external projects) export { MetricsTimeSpanProvider, useMetricsTimeSpan, -} from './components/linetemporalchart/MetricTimespanProvider'; -export { SyncedCursorCharts } from './components/vegachartv2/SyncedCursorCharts'; +} from './components/charts/MetricsTimeSpanProvider'; + export { Select } from './components/selectv2/Selectv2.component'; export { HealthSelector } from './components/healthselectorv2/HealthSelector.component'; export { CoreUiThemeProvider } from './components/coreuithemeprovider/CoreUiThemeProvider'; export { Box } from './components/box/Box'; export { Input } from './components/inputv2/inputv2'; export { Accordion } from './components/accordion/Accordion.component'; + +// Export all chart components from the consolidated charts folder export { Barchart, BarchartSortFn, BarchartTooltipFn, -} from './components/barchartv2/Barchart.component'; -export { BarchartTooltip } from './components/barchartv2/BarchartTooltip'; -export { + BarchartTooltip, + LineTimeSerieChart, + GlobalHealthBar, + Sparkline, + ChartLegend, ChartLegendWrapper, useChartId, -} from './components/chartlegend/ChartLegendWrapper'; -export { ChartLegend } from './components/chartlegend/ChartLegend'; -export { LineTimeSerieChart } from './components/linetimeseriechart/linetimeseriechart.component'; -export { + useChartLegend, ChartTooltipContainer, ChartTooltipItem, ChartTooltipHeader, ChartTooltipItemsContainer, -} from './components/charttooltip/ChartTooltip'; +} from './components/charts'; + +export type { + BarchartProps, + BarchartBars, + LineChartProps, + Serie, + GlobalHealthProps, + Alert, + UnitRange, + TimeType, + CategoryType, +} from './components/charts'; + export { CoreUITheme } from './style/theme'; -export { GlobalHealthBar } from './components/globalhealthbar/GlobalHealthBarRecharts.component'; -export { Sparkline } from './components/sparkline/sparkline.component'; diff --git a/stories/BarChart/barchart.stories.tsx b/stories/BarChart/barchart.stories.tsx index 631eb1de3c..76a3defcbb 100644 --- a/stories/BarChart/barchart.stories.tsx +++ b/stories/BarChart/barchart.stories.tsx @@ -6,14 +6,14 @@ import { BarchartProps, BarchartSortFn, BarchartTooltipFn, -} from '../../src/lib/components/barchartv2/Barchart.component'; + ChartLegendWrapper, + ChartLegend, +} from '../../src/lib/components/charts'; import { Button } from '../../src/lib/components/buttonv2/Buttonv2.component'; import { Text } from '../../src/lib/components/text/Text.component'; import { spacing, Stack, Wrap } from '../../src/lib/spacing'; import { CoreUITheme } from '../../src/lib/style/theme'; import { Wrapper } from '../common'; -import { ChartLegendWrapper } from '../../src/lib/components/chartlegend/ChartLegendWrapper'; -import { ChartLegend } from '../../src/lib/components/chartlegend/ChartLegend'; type Story = StoryObj; @@ -39,19 +39,19 @@ export const Playground: Story = { { label: 'Success', data: [ - ['category1', 1], - ['category2', 1], - ['category3', 2], - ], - }, - { - label: 'Failed', - data: [ - ['category1', 1], - ['category2', 1], - ['category3', 2], + ['category1', 0.001], + ['category2', 0.005], + ['category3', 0.002], ], }, + // { + // label: 'Failed', + // data: [ + // ['category1', 1], + // ['category2', 1], + // ['category3', 2], + // ], + // }, ] as const; return ( ; const meta: Meta = { - title: 'Components/GlobalHealthBarRecharts', + title: 'Components/DataDisplay/Charts/GlobalHealthBar', component: GlobalHealthBar, }; export default meta; diff --git a/stories/GlobalHealthBar/globalheathbarrecharts.guideline.mdx b/stories/GlobalHealthBar/globalheathbarrecharts.guideline.mdx index ebddbc5620..615e793043 100644 --- a/stories/GlobalHealthBar/globalheathbarrecharts.guideline.mdx +++ b/stories/GlobalHealthBar/globalheathbarrecharts.guideline.mdx @@ -1,5 +1,5 @@ import { Meta } from '@storybook/blocks'; -import { GlobalHealthBar } from '../../src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component'; -import * as GlobalHealthBarStories from './globalhealthbarRecharts.stories'; +import { GlobalHealthBar } from '../../src/lib/components/charts'; +import * as GlobalHealthBarStories from './globalhealthbar.stories'; diff --git a/stories/barchart.stories.tsx b/stories/barchart.stories.tsx deleted file mode 100644 index b75cf06c76..0000000000 --- a/stories/barchart.stories.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import React from 'react'; -import { BarChart } from '../src/lib/components/barchart/BarChart.component'; -import { Wrapper } from './common'; -import { - verticalStackedData, - horizontalStackedData, - barchartData, -} from './data/barchart'; -import { SyncedCursorCharts } from '../src/lib/components/vegachartv2/SyncedCursorCharts'; - -import { Component } from '@storybook/blocks'; - -const width = 800; -// the size control the size of each small item of the bar - -const barConfig = { - cornerRadius: 8, - size: 12, -}; -// props for vertical stacked bar chart -const idVerticalStacked = 'vis_vertical_stacked'; - -// props for vertical stacked bar chart - -const verticalStackedBarChartArgs = { - id: 'vis_vertical_stacked', - xAxis: { - field: 'xlabel', - type: 'ordinal', - title: null, - axis: { - labelAngle: 0, - }, - sort: { - order: 'ascending', - }, - }, - yAxis: { - aggregate: 'count', - field: '*', - title: null, - type: 'quantitative', - scale: { - padding: 1, - }, - }, - color: { - field: 'status', - type: 'nominal', - legend: { - direction: 'horizontal', - orient: 'top', - title: null, - symbolType: 'circle', - columnPadding: 50, - }, - scale: { - domain: ['2XX', '401', '404', '4XX', '503', '5XX'], - range: ['#4BE4E2', '#E45834', '#FEFA52', '#968BFF', '#BE2543', '#DC90F1'], - }, - }, - data: verticalStackedData, - barConfig, -}; - -// props for horizontal stacked bar chart -const horizontalStackedChartArgs = { - id: 'vis_horizontal_stacked', - xAxis: { - aggregate: 'sum', - field: 'yield', - type: 'quantitative', - }, - yAxis: { - field: 'variety', - type: 'nominal', - }, - color: { - field: 'site', - type: 'nominal', - }, - data: horizontalStackedData, -}; - -// props for vertical bar chart props -const verticalBarChartArgs = { - id: 'vis_vertical', - xAxis: { - field: 'a', - type: 'ordinal', - }, - yAxis: { - field: 'b', - type: 'quantitative', - }, - data: barchartData, -}; - -//props for horizontal bar chart -const horizontalBarChartArgs = { - id: 'vis_horizontal', - xAxis: { - field: 'b', - type: 'quantitative', - }, - yAxis: { - field: 'a', - type: 'ordinal', - }, - data: barchartData, -}; - -export default { - title: 'Components/Deprecated/Charts/BarChart', - component: BarChart, - decorators: [ - (story: Component) => {story()}, - ], - argTypes: { - data: { - table: { - disable: true, - }, - }, - }, -}; - -export const verticalStackedChart = { - args: { ...verticalStackedBarChartArgs }, -}; - -export const horizontalStackedchart = { - args: { ...horizontalStackedChartArgs }, -}; - -export const verticalBarChart = { - args: { ...verticalBarChartArgs }, -}; - -export const horizontalBarChart = { - args: { ...horizontalBarChartArgs }, -}; diff --git a/stories/globalhealthbar.stories.tsx b/stories/globalhealthbar.stories.tsx deleted file mode 100644 index 54c571c430..0000000000 --- a/stories/globalhealthbar.stories.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import React from 'react'; -import { GlobalHealthBar } from '../src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component'; -import { GlobalHealthBar as VegaGlobalHealthBar } from '../src/lib/components/globalhealthbar/GlobalHealthBar.component'; -import { SyncedCursorCharts } from '../src/lib/components/vegachartv2/SyncedCursorCharts'; -import { Wrapper } from './common'; -import { Stack } from '../src/lib/spacing'; -const alerts = [ - { - id: '1', - severity: 'warning', - startsAt: '2021-02-01T07:00:00Z', - endsAt: '2021-02-01T21:00:00Z', - description: 'Global health warning', - }, - { - id: '2', - severity: 'warning', - startsAt: '2021-02-01T23:00:00Z', - endsAt: '2021-02-02T23:00:00Z', - description: 'Global health warning', - }, - { - id: '3', - severity: 'critical', - startsAt: '2021-02-03T00:00:00Z', - endsAt: '2021-02-04T00:00:00Z', - description: 'Global health critical', - }, - { - id: '4', - severity: 'warning', - startsAt: '2021-02-04T10:00:00Z', - endsAt: '2021-02-06T00:00:00Z', - description: 'Global health warning', - }, - { - id: '5', - severity: 'warning', - startsAt: '2021-02-06T10:00:00Z', - endsAt: '2021-02-06T20:00:00Z', - description: 'Global health warning', - }, - { - id: '6', - severity: 'warning', - startsAt: '2021-02-06T12:00:00Z', - endsAt: '2021-02-06T22:30:00Z', - description: 'Global health warning', - }, -]; -const alertsLast24h = [ - { - id: '1', - severity: 'warning', - startsAt: '2021-02-01T07:00:00Z', - endsAt: '2021-02-01T08:00:00Z', - description: 'Global health warning', - }, - { - id: '5', - severity: 'warning', - startsAt: '2021-02-01T10:00:00Z', - endsAt: '2021-02-01T20:00:00Z', - description: 'Global health warning', - }, - { - id: '6', - severity: 'unavailable', - startsAt: '2021-02-01T02:00:00Z', - endsAt: '2021-02-01T03:00:00Z', - description: 'unavailable', - }, -]; -const emptyAlert = []; -const alertRetrieveBefore = [ - { - id: '1', - severity: 'warning', - startsAt: '2021-01-31T23:00:00Z', - endsAt: '2021-02-03T21:00:00Z', - description: 'Global health warning', - }, - { - id: '2', - severity: 'critical', - startsAt: '2021-02-05T23:00:00Z', - endsAt: '2021-02-07T00:00:00Z', - description: 'Global health warning', - }, -]; -const alertTriggerNotFirstDay = [ - { - id: '1', - severity: 'warning', - startsAt: '2021-02-24T21:00:00Z', - endsAt: '2021-02-25T21:00:00Z', - description: 'Global health warning', - }, - { - id: '2', - severity: 'critical', - startsAt: '2021-02-26T23:00:00Z', - endsAt: '2021-02-27T21:00:00Z', - description: 'Global health warning', - }, -]; - -const start = '2021-01-31T23:00:00Z'; // UTC time -const end = '2021-02-06T23:00:00Z'; -const startLast24h = '2021-02-01T00:00:00Z'; -const endLast24h = '2021-02-02T00:00:00Z'; -const startNotFirstDay = '2021-02-23T23:00:00Z'; -const endNotFirstDay = '2021-03-01T23:00:00Z'; - -export default { - title: 'Components/Data Display/Charts/GlobalHealthBar', - component: (props) => ( - - - - - ), - decorators: [ - (story) => ( - -
- {story()} -
-
- ), - ], - args: { - start, - end, - width: 500, - }, - argTypes: { - start: { - control: 'date', - }, - end: { - control: 'date', - }, - }, -}; - -export const GlobalHealthComponentDemo = { - args: { - id: 'vis_globalhealth', - alerts, - width: 500, - }, -}; - -export const GlobalHealthLast24Hours = { - args: { - id: 'vis_globalhealth_24h', - alerts: alertsLast24h, - start: startLast24h, - end: endLast24h, - width: 500, - }, -}; -export const GlobalHealthEmpty = { - args: { - id: 'vis_globalhealth_empty', - alerts: emptyAlert, - width: 500, - }, -}; - -export const AlertTriggeredEarlierThanTheStartingTime = { - args: { - id: 'vis_globalhealth_alert_retrieve_before', - alerts: alertRetrieveBefore, - width: 500, - }, -}; - -export const FirstLabel = { - name: 'First Label always includes the month label', - args: { - id: 'vis_globalhealth_display_month_1st_label', - alerts: alertTriggerNotFirstDay, - start: startNotFirstDay, - end: endNotFirstDay, - width: 500, - }, -}; diff --git a/stories/guideline/chart-guideline.mdx b/stories/guideline/chart-guideline.mdx index 61097e4d91..5cd5a03192 100644 --- a/stories/guideline/chart-guideline.mdx +++ b/stories/guideline/chart-guideline.mdx @@ -1,5 +1,4 @@ import { Meta, Story } from '@storybook/blocks'; -import { ChartExample } from './mdxExampleComponents'; import { SuccessBanner } from '../banner.stories'; @@ -14,10 +13,6 @@ Used mostly for monitoring-related tasks. Don’t depend too heavily on tooltips. They are as providing supplemental or expanded information, but shouldn’t be the only way a user can see the plotted value. -### Grid lines - -Only use grid lines when it’s helpful. Possible to use only horizontal or vertical. - ### Elements - Title @@ -28,36 +23,4 @@ _produce an example WIP_ ## Type -4 types for most cases: pie/donut, bar/column, line, or area. - -### Pie charts - -- No more than 6 categories -- Values should be different enough (otherwise select Bar charts) -- Should be a 100% total -- Categories in the desc order - -_produce an example WIP_ - -_(Currently, there is no Pie chart in the UI)_ - -### Bar and column charts - -- Should start at a baseline of zero. - {/* */} - -### Line charts - -- No more than 5–7 different lines -- Avoid randomly chosen color (otherwise risk of contrast issues) - -Here, the colors for Get and List are too close. - -Components/Chart/LineChart - -Area charts - -- Could be a single area, overlapping area, or stacked area -- Max 3-4 categories - -{/* */} +{/* TODO: add Guidelines for charts types and add date format guidelines here or in format.mdx */} diff --git a/stories/guideline/mdxExampleComponents.tsx b/stories/guideline/mdxExampleComponents.tsx deleted file mode 100644 index 81ed02f2db..0000000000 --- a/stories/guideline/mdxExampleComponents.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import { BarChart } from '../../src/lib/components/barchart/BarChart.component'; -import { verticalStackedData } from '../data/barchart'; -// props for vertical stacked bar chart -const idVerticalStacked = 'vis_vertical_stacked'; -const xAxisVerticalStacked = { - field: 'xlabel', - type: 'ordinal', - title: null, - axis: { - labelAngle: 0, - }, - sort: { - order: 'ascending', - }, -}; -const yAxisVerticalStacked = { - aggregate: 'count', - field: '*', - title: null, - type: 'quantitative', - scale: { - padding: 1, - }, -}; -const colorVerticalStacked = { - field: 'status', - type: 'nominal', - legend: { - direction: 'horizontal', - orient: 'top', - title: null, - symbolType: 'circle', - columnPadding: 50, - }, - scale: { - domain: ['2XX', '401', '404', '4XX', '503', '5XX'], - range: ['#4BE4E2', '#E45834', '#FEFA52', '#968BFF', '#BE2543', '#DC90F1'], - }, -}; -const width = 800; -// the size control the size of each small item of the bar -const barConfig = { - cornerRadius: 8, - size: 12, -}; -export const ChartExample = ({}) => ( - -); diff --git a/stories/linecharttemporal.stories.tsx b/stories/linecharttemporal.stories.tsx deleted file mode 100644 index 9737981bbe..0000000000 --- a/stories/linecharttemporal.stories.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { useCallback, useState, useEffect } from 'react'; -import { BrowserRouter } from 'react-router-dom'; -import { SyncedCursorCharts } from '../src/lib/components/vegachartv2/SyncedCursorCharts'; -import { - LineTemporalChart, - YAXIS_TITLE_READ_WRITE, -} from '../src/lib/components/linetemporalchart/LineTemporalChart.component'; -import { MetricsTimeSpanProvider } from '../src/lib/components/linetemporalchart/MetricTimespanProvider'; -import { Wrapper } from './common'; -import { dataLineChartV2, dataLineChartV2_readwrite } from './data/linechart'; -import { defaultRenderTooltipSerie } from '../src/lib/components/linetemporalchart/tooltip'; -export default { - title: 'Components/Deprecated/Charts/LineTemporalChart', - component: LineTemporalChart, - decorators: [ - (story) => ( - - - - {story()} - - - - ), - ], - args: { - heigth: 300, - startingTimeStamp: 1629306229, - }, -}; - -export const CPUUsage = { - render: (args) => { - const [tooltipText, setTooltipText] = useState('initial text'); - useEffect(() => { - setInterval(() => { - setTooltipText('New text ' + new Date().toISOString()); - }, 500); - }, []); - return ( - { - if (serie.key === 'bootstrap') { - return ( - defaultRenderTooltipSerie(serie) + - `${tooltipText}` - ); - } - return defaultRenderTooltipSerie(serie); - }, - [tooltipText], - )} - {...args} - /> - ); - }, - args: { - title: 'CPU Usage', - yAxisType: 'default', - series: dataLineChartV2, - helpText: ( - <> - This charts represents lorem ipsum -
- This charts represents lorem ipsum -
- This charts represents lorem ipsum -
- This charts represents lorem ipsum -
- This charts represents lorem ipsum -
- This charts represents lorem ipsum -
- - ), - }, -}; - -export const IOPS = { - args: { - title: 'IOPS', - series: dataLineChartV2_readwrite, - yAxisTitle: YAXIS_TITLE_READ_WRITE, - yAxisType: 'symmetrical', - }, -}; diff --git a/stories/linetimeseriechart.stories.tsx b/stories/linetimeseriechart.stories.tsx index 3c6185726a..b6a796a138 100644 --- a/stories/linetimeseriechart.stories.tsx +++ b/stories/linetimeseriechart.stories.tsx @@ -3,14 +3,12 @@ import { Meta, StoryObj } from '@storybook/react'; import { LineTimeSerieChart, Serie, -} from '../src/lib/components/linetimeseriechart/linetimeseriechart.component'; -import { ChartLegendWrapper } from '../src/lib/components/chartlegend/ChartLegendWrapper'; -import { lineTimeSeriesColorRange } from '../src/lib/style/theme'; -import { ChartLegend } from '../src/lib/components/chartlegend/ChartLegend'; -import { + ChartLegendWrapper, + ChartLegend, useChartId, useChartLegend, -} from '../src/lib/components/chartlegend/ChartLegendWrapper'; +} from '../src/lib/components/charts'; +import { lineTimeSeriesColorRange } from '../src/lib/style/theme'; import { useEffect } from 'react'; import { SAMPLE_DURATION_LAST_TWENTY_FOUR_HOURS, diff --git a/stories/sparkline.stories.tsx b/stories/sparkline.stories.tsx index ff65b4a7b9..aecb575f46 100644 --- a/stories/sparkline.stories.tsx +++ b/stories/sparkline.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { Sparkline } from '../src/lib/components/sparkline/sparkline.component'; +import React from 'react'; +import { Sparkline } from '../src/lib/components/charts'; import { lineColor5, lineColor6 } from '../src/lib/style/theme'; const meta: Meta = { @@ -10,21 +11,21 @@ const meta: Meta = { }, tags: ['autodocs'], argTypes: { - serie: { + serie: { control: 'object', - description: 'Data series containing array of [timestamp, value] pairs' + description: 'Data series containing array of [timestamp, value] pairs', }, - startingTimeStamp: { + startingTimeStamp: { control: 'number', - description: 'Starting timestamp in seconds for the data series' + description: 'Starting timestamp in seconds for the data series', }, - sampleDuration: { + sampleDuration: { control: 'number', - description: 'Total duration in seconds to cover in the sparkline' + description: 'Total duration in seconds to cover in the sparkline', }, - sampleInterval: { + sampleInterval: { control: 'number', - description: 'Interval in seconds between data points' + description: 'Interval in seconds between data points', }, }, decorators: [ @@ -82,7 +83,7 @@ const trendingDownData: [number, number][] = [ [1740412800, 22.1], ]; -const flatData: [number, number|null][] = [ +const flatData: [number, number | null][] = [ [1740405600, 50.0], [1740406320, 50.0], [1740407760, 50.0], @@ -105,7 +106,8 @@ export const Default: Story = { parameters: { docs: { description: { - story: 'Sparkline displaying highly volatile data with frequent peaks and valleys.', + story: + 'Sparkline displaying highly volatile data with frequent peaks and valleys.', }, }, },