From 0ce172bfc10039ca259f5c7a2e61e046686ab782 Mon Sep 17 00:00:00 2001 From: Giuliano Silvestro Date: Fri, 13 Feb 2026 22:06:35 +1000 Subject: [PATCH] Initial commit: data-viz-elements-ui library Co-Authored-By: Claude Opus 4.6 --- .gitignore | 4 + build-for-dev.sh | 12 + ng-package.json | 21 + package-lock.json | 4083 +++++++++++++++++ package.json | 67 + src/components/index.ts | 18 + src/components/viz-area-chart/index.ts | 1 + .../viz-area-chart.component.html | 9 + .../viz-area-chart.component.scss | 46 + .../viz-area-chart.component.ts | 223 + src/components/viz-bar-chart/index.ts | 1 + .../viz-bar-chart.component.html | 9 + .../viz-bar-chart.component.scss | 55 + .../viz-bar-chart/viz-bar-chart.component.ts | 279 ++ src/components/viz-box-plot/index.ts | 1 + .../viz-box-plot/viz-box-plot.component.html | 1 + .../viz-box-plot/viz-box-plot.component.scss | 17 + .../viz-box-plot/viz-box-plot.component.ts | 217 + src/components/viz-data-table/index.ts | 1 + .../viz-data-table.component.html | 90 + .../viz-data-table.component.scss | 149 + .../viz-data-table.component.ts | 122 + src/components/viz-gauge/index.ts | 1 + .../viz-gauge/viz-gauge.component.html | 1 + .../viz-gauge/viz-gauge.component.scss | 9 + .../viz-gauge/viz-gauge.component.ts | 238 + src/components/viz-heatmap/index.ts | 1 + .../viz-heatmap/viz-heatmap.component.html | 1 + .../viz-heatmap/viz-heatmap.component.scss | 19 + .../viz-heatmap/viz-heatmap.component.ts | 183 + src/components/viz-histogram/index.ts | 1 + .../viz-histogram.component.html | 1 + .../viz-histogram.component.scss | 27 + .../viz-histogram/viz-histogram.component.ts | 171 + src/components/viz-legend/index.ts | 1 + .../viz-legend/viz-legend.component.html | 18 + .../viz-legend/viz-legend.component.scss | 59 + .../viz-legend/viz-legend.component.ts | 43 + src/components/viz-line-chart/index.ts | 1 + .../viz-line-chart.component.html | 9 + .../viz-line-chart.component.scss | 57 + .../viz-line-chart.component.ts | 290 ++ src/components/viz-pie-chart/index.ts | 1 + .../viz-pie-chart.component.html | 9 + .../viz-pie-chart.component.scss | 39 + .../viz-pie-chart/viz-pie-chart.component.ts | 231 + src/components/viz-progress-bar/index.ts | 1 + .../viz-progress-bar.component.html | 24 + .../viz-progress-bar.component.scss | 49 + .../viz-progress-bar.component.ts | 48 + src/components/viz-progress-ring/index.ts | 1 + .../viz-progress-ring.component.html | 33 + .../viz-progress-ring.component.scss | 25 + .../viz-progress-ring.component.ts | 38 + src/components/viz-scatter-chart/index.ts | 1 + .../viz-scatter-chart.component.html | 9 + .../viz-scatter-chart.component.scss | 55 + .../viz-scatter-chart.component.ts | 222 + src/components/viz-sparkline/index.ts | 1 + .../viz-sparkline.component.html | 1 + .../viz-sparkline.component.scss | 14 + .../viz-sparkline/viz-sparkline.component.ts | 132 + src/components/viz-stat-card/index.ts | 1 + .../viz-stat-card.component.html | 17 + .../viz-stat-card.component.scss | 61 + .../viz-stat-card/viz-stat-card.component.ts | 40 + src/components/viz-time-series/index.ts | 1 + .../viz-time-series.component.html | 9 + .../viz-time-series.component.scss | 29 + .../viz-time-series.component.ts | 227 + src/components/viz-treemap/index.ts | 1 + .../viz-treemap/viz-treemap.component.html | 1 + .../viz-treemap/viz-treemap.component.scss | 19 + .../viz-treemap/viz-treemap.component.ts | 182 + src/components/viz-trend-indicator/index.ts | 1 + .../viz-trend-indicator.component.html | 9 + .../viz-trend-indicator.component.scss | 36 + .../viz-trend-indicator.component.ts | 25 + src/index.ts | 48 + src/providers/index.ts | 1 + src/providers/viz-config.provider.ts | 23 + src/services/index.ts | 4 + src/services/viz-export.service.ts | 66 + src/services/viz-resize.service.ts | 39 + src/services/viz-theme.service.ts | 33 + src/services/viz-tooltip.service.ts | 74 + src/styles/_index.scss | 2 + src/styles/_mixins.scss | 58 + src/styles/_tokens.scss | 67 + src/types/chart.types.ts | 71 + src/types/config.types.ts | 40 + src/types/event.types.ts | 25 + src/types/index.ts | 3 + src/utils/color.utils.ts | 24 + src/utils/format.utils.ts | 26 + src/utils/index.ts | 3 + src/utils/scale.utils.ts | 47 + tsconfig.json | 28 + 98 files changed, 8832 insertions(+) create mode 100644 .gitignore create mode 100755 build-for-dev.sh create mode 100644 ng-package.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/components/index.ts create mode 100644 src/components/viz-area-chart/index.ts create mode 100644 src/components/viz-area-chart/viz-area-chart.component.html create mode 100644 src/components/viz-area-chart/viz-area-chart.component.scss create mode 100644 src/components/viz-area-chart/viz-area-chart.component.ts create mode 100644 src/components/viz-bar-chart/index.ts create mode 100644 src/components/viz-bar-chart/viz-bar-chart.component.html create mode 100644 src/components/viz-bar-chart/viz-bar-chart.component.scss create mode 100644 src/components/viz-bar-chart/viz-bar-chart.component.ts create mode 100644 src/components/viz-box-plot/index.ts create mode 100644 src/components/viz-box-plot/viz-box-plot.component.html create mode 100644 src/components/viz-box-plot/viz-box-plot.component.scss create mode 100644 src/components/viz-box-plot/viz-box-plot.component.ts create mode 100644 src/components/viz-data-table/index.ts create mode 100644 src/components/viz-data-table/viz-data-table.component.html create mode 100644 src/components/viz-data-table/viz-data-table.component.scss create mode 100644 src/components/viz-data-table/viz-data-table.component.ts create mode 100644 src/components/viz-gauge/index.ts create mode 100644 src/components/viz-gauge/viz-gauge.component.html create mode 100644 src/components/viz-gauge/viz-gauge.component.scss create mode 100644 src/components/viz-gauge/viz-gauge.component.ts create mode 100644 src/components/viz-heatmap/index.ts create mode 100644 src/components/viz-heatmap/viz-heatmap.component.html create mode 100644 src/components/viz-heatmap/viz-heatmap.component.scss create mode 100644 src/components/viz-heatmap/viz-heatmap.component.ts create mode 100644 src/components/viz-histogram/index.ts create mode 100644 src/components/viz-histogram/viz-histogram.component.html create mode 100644 src/components/viz-histogram/viz-histogram.component.scss create mode 100644 src/components/viz-histogram/viz-histogram.component.ts create mode 100644 src/components/viz-legend/index.ts create mode 100644 src/components/viz-legend/viz-legend.component.html create mode 100644 src/components/viz-legend/viz-legend.component.scss create mode 100644 src/components/viz-legend/viz-legend.component.ts create mode 100644 src/components/viz-line-chart/index.ts create mode 100644 src/components/viz-line-chart/viz-line-chart.component.html create mode 100644 src/components/viz-line-chart/viz-line-chart.component.scss create mode 100644 src/components/viz-line-chart/viz-line-chart.component.ts create mode 100644 src/components/viz-pie-chart/index.ts create mode 100644 src/components/viz-pie-chart/viz-pie-chart.component.html create mode 100644 src/components/viz-pie-chart/viz-pie-chart.component.scss create mode 100644 src/components/viz-pie-chart/viz-pie-chart.component.ts create mode 100644 src/components/viz-progress-bar/index.ts create mode 100644 src/components/viz-progress-bar/viz-progress-bar.component.html create mode 100644 src/components/viz-progress-bar/viz-progress-bar.component.scss create mode 100644 src/components/viz-progress-bar/viz-progress-bar.component.ts create mode 100644 src/components/viz-progress-ring/index.ts create mode 100644 src/components/viz-progress-ring/viz-progress-ring.component.html create mode 100644 src/components/viz-progress-ring/viz-progress-ring.component.scss create mode 100644 src/components/viz-progress-ring/viz-progress-ring.component.ts create mode 100644 src/components/viz-scatter-chart/index.ts create mode 100644 src/components/viz-scatter-chart/viz-scatter-chart.component.html create mode 100644 src/components/viz-scatter-chart/viz-scatter-chart.component.scss create mode 100644 src/components/viz-scatter-chart/viz-scatter-chart.component.ts create mode 100644 src/components/viz-sparkline/index.ts create mode 100644 src/components/viz-sparkline/viz-sparkline.component.html create mode 100644 src/components/viz-sparkline/viz-sparkline.component.scss create mode 100644 src/components/viz-sparkline/viz-sparkline.component.ts create mode 100644 src/components/viz-stat-card/index.ts create mode 100644 src/components/viz-stat-card/viz-stat-card.component.html create mode 100644 src/components/viz-stat-card/viz-stat-card.component.scss create mode 100644 src/components/viz-stat-card/viz-stat-card.component.ts create mode 100644 src/components/viz-time-series/index.ts create mode 100644 src/components/viz-time-series/viz-time-series.component.html create mode 100644 src/components/viz-time-series/viz-time-series.component.scss create mode 100644 src/components/viz-time-series/viz-time-series.component.ts create mode 100644 src/components/viz-treemap/index.ts create mode 100644 src/components/viz-treemap/viz-treemap.component.html create mode 100644 src/components/viz-treemap/viz-treemap.component.scss create mode 100644 src/components/viz-treemap/viz-treemap.component.ts create mode 100644 src/components/viz-trend-indicator/index.ts create mode 100644 src/components/viz-trend-indicator/viz-trend-indicator.component.html create mode 100644 src/components/viz-trend-indicator/viz-trend-indicator.component.scss create mode 100644 src/components/viz-trend-indicator/viz-trend-indicator.component.ts create mode 100644 src/index.ts create mode 100644 src/providers/index.ts create mode 100644 src/providers/viz-config.provider.ts create mode 100644 src/services/index.ts create mode 100644 src/services/viz-export.service.ts create mode 100644 src/services/viz-resize.service.ts create mode 100644 src/services/viz-theme.service.ts create mode 100644 src/services/viz-tooltip.service.ts create mode 100644 src/styles/_index.scss create mode 100644 src/styles/_mixins.scss create mode 100644 src/styles/_tokens.scss create mode 100644 src/types/chart.types.ts create mode 100644 src/types/config.types.ts create mode 100644 src/types/event.types.ts create mode 100644 src/types/index.ts create mode 100644 src/utils/color.utils.ts create mode 100644 src/utils/format.utils.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/scale.utils.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d1996f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/ +node_modules/ +.angular/ +*.tgz diff --git a/build-for-dev.sh b/build-for-dev.sh new file mode 100755 index 0000000..6b703e4 --- /dev/null +++ b/build-for-dev.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build the library +npm run build + +# Link it locally for development +cd dist +npm link + +echo "✓ Library built and linked successfully" +echo "Run 'npm link @sda/data-viz-elements-ui' in your consuming app" diff --git a/ng-package.json b/ng-package.json new file mode 100644 index 0000000..1e9b9b6 --- /dev/null +++ b/ng-package.json @@ -0,0 +1,21 @@ +{ + "$schema": "node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "src/index.ts" + }, + "dest": "dist", + "deleteDestPath": true, + "allowedNonPeerDependencies": [ + "d3-array", + "d3-axis", + "d3-color", + "d3-format", + "d3-hierarchy", + "d3-interpolate", + "d3-scale", + "d3-selection", + "d3-shape", + "d3-time-format", + "d3-transition" + ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..29a2da7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4083 @@ +{ + "name": "@sda/data-viz-elements-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@sda/data-viz-elements-ui", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "d3-array": "^3.2.0", + "d3-axis": "^3.0.0", + "d3-color": "^3.1.0", + "d3-format": "^3.1.0", + "d3-hierarchy": "^3.1.0", + "d3-interpolate": "^3.0.0", + "d3-scale": "^4.0.0", + "d3-selection": "^3.0.0", + "d3-shape": "^3.2.0", + "d3-time-format": "^4.1.0", + "d3-transition": "^3.0.0" + }, + "devDependencies": { + "@angular/common": "^19.1.0", + "@angular/compiler": "^19.1.0", + "@angular/compiler-cli": "^19.1.0", + "@angular/core": "^19.1.0", + "@angular/forms": "^19.2.18", + "@types/d3-array": "^3.2.0", + "@types/d3-axis": "^3.0.0", + "@types/d3-color": "^3.1.0", + "@types/d3-format": "^3.0.0", + "@types/d3-hierarchy": "^3.1.0", + "@types/d3-interpolate": "^3.0.0", + "@types/d3-scale": "^4.0.0", + "@types/d3-selection": "^3.0.0", + "@types/d3-shape": "^3.1.0", + "@types/d3-time-format": "^4.0.0", + "@types/d3-transition": "^3.0.0", + "ng-packagr": "^19.1.0", + "typescript": "~5.7.2" + }, + "peerDependencies": { + "@angular/common": "^19.0.0", + "@angular/core": "^19.0.0", + "@sda/base-ui": "*" + }, + "peerDependenciesMeta": { + "@sda/base-ui": { + "optional": true + } + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular/common": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.18.tgz", + "integrity": "sha512-CrV02Omzw/QtfjlEVXVPJVXipdx83NuA+qSASZYrxrhKFusUZyK3P/Zznqg+wiAeNDbedQwMUVqoAARHf0xQrw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.2.18", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.18.tgz", + "integrity": "sha512-3MscvODxRVxc3Cs0ZlHI5Pk5rEvE80otfvxZTMksOZuPlv1B+S8MjWfc3X3jk9SbyUEzODBEH55iCaBHD48V3g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.18.tgz", + "integrity": "sha512-N4TMtLfImJIoMaRL6mx7885UBeQidywptHH6ACZj71Ar6++DBc1mMlcwuvbeJCd3r3y8MQ5nLv5PZSN/tHr13w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "7.26.9", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^4.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/compiler": "19.2.18", + "typescript": ">=5.5 <5.9" + } + }, + "node_modules/@angular/core": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.18.tgz", + "integrity": "sha512-+QRrf0Igt8ccUWXHA+7doK5W6ODyhHdqVyblSlcQ8OciwkzIIGGEYNZom5OZyWMh+oI54lcSeyV2O3xaDepSrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0" + } + }, + "node_modules/@angular/forms": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.18.tgz", + "integrity": "sha512-pe40934jWhoS7DyGl7jyZdoj1gvBgur2t1zrJD+csEkTitYnW14+La2Pv6SW1pNX5nIzFsgsS9Nex1KcH5S6Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.18", + "@angular/core": "19.2.18", + "@angular/platform-browser": "19.2.18", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.18.tgz", + "integrity": "sha512-eahtsHPyXTYLARs9YOlXhnXGgzw0wcyOcDkBvNWK/3lA0NHIgIHmQgXAmBo+cJ+g9skiEQTD2OmSrrwbFKWJkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/animations": "19.2.18", + "@angular/common": "19.2.18", + "@angular/core": "19.2.18" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/wasm-node": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.57.1.tgz", + "integrity": "sha512-b0rcJH8ykEanfgTeDtlPubhphIUOx0oaAek+3hizTaFkoC1FBSTsY0GixwB4D5HZ5r3Gt2yI9c8M13OcW/kW5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "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, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "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-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "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", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "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, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/injection-js": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/injection-js/-/injection-js-2.6.1.tgz", + "integrity": "sha512-dbR5bdhi7TWDoCye9cByZqeg/gAfamm8Vu3G1KZOTYkOif8WkuM8CD0oeDPtZYMzT5YH76JAFB7bkmyY9OJi2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/less": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/less/-/less-4.5.1.tgz", + "integrity": "sha512-UKgI3/KON4u6ngSsnDADsUERqhZknsVZbnuzlRZXLQCmfC/MDld42fTydUE9B+Mla1AL6SJ/Pp6SlEFi/AVGfw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/ng-packagr": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-19.2.2.tgz", + "integrity": "sha512-dFuwFsDJMBSd1YtmLLcX5bNNUCQUlRqgf34aXA+79PmkOP+0eF8GP2949wq3+jMjmFTNm80Oo8IUYiSLwklKCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-json": "^6.1.0", + "@rollup/wasm-node": "^4.24.0", + "ajv": "^8.17.1", + "ansi-colors": "^4.1.3", + "browserslist": "^4.22.1", + "chokidar": "^4.0.1", + "commander": "^13.0.0", + "convert-source-map": "^2.0.0", + "dependency-graph": "^1.0.0", + "esbuild": "^0.25.0", + "fast-glob": "^3.3.2", + "find-cache-dir": "^3.3.2", + "injection-js": "^2.4.0", + "jsonc-parser": "^3.3.1", + "less": "^4.2.0", + "ora": "^5.1.0", + "piscina": "^4.7.0", + "postcss": "^8.4.47", + "rxjs": "^7.8.1", + "sass": "^1.81.0" + }, + "bin": { + "ng-packagr": "cli/main.js" + }, + "engines": { + "node": "^18.19.1 || >=20.11.1" + }, + "optionalDependencies": { + "rollup": "^4.24.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^19.0.0 || ^19.1.0-next.0 || ^19.2.0-next.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.5 <5.9" + }, + "peerDependenciesMeta": { + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/ng-packagr/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/piscina": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.9.2.tgz", + "integrity": "sha512-Fq0FERJWFEUpB4eSY59wSNwXD4RYqR+nR/WiEVcZW8IWfVBxJJafcgTEZDQo8k3w0sUarJ8RyVbbUF4GQ2LGbQ==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/sass": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "peer": true + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/wrap-ansi": { + "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, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zone.js": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", + "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", + "dev": true, + "license": "MIT", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..67f755b --- /dev/null +++ b/package.json @@ -0,0 +1,67 @@ +{ + "name": "@sda/data-viz-elements-ui", + "version": "0.1.0", + "description": "Angular components for data visualization, charts, and dashboard widgets powered by D3.js", + "keywords": [ + "angular", + "data-visualization", + "charts", + "d3", + "dashboard", + "components", + "ui" + ], + "repository": { + "type": "git", + "url": "https://git.sky-ai.com/ui-core-design/data-viz-elements-ui.git" + }, + "license": "MIT", + "sideEffects": false, + "scripts": { + "build": "ng-packagr -p ng-package.json", + "build:dev": "./build-for-dev.sh" + }, + "peerDependencies": { + "@angular/common": "^19.0.0", + "@angular/core": "^19.0.0", + "@sda/base-ui": "*" + }, + "peerDependenciesMeta": { + "@sda/base-ui": { + "optional": true + } + }, + "dependencies": { + "d3-array": "^3.2.0", + "d3-axis": "^3.0.0", + "d3-color": "^3.1.0", + "d3-format": "^3.1.0", + "d3-hierarchy": "^3.1.0", + "d3-interpolate": "^3.0.0", + "d3-scale": "^4.0.0", + "d3-selection": "^3.0.0", + "d3-shape": "^3.2.0", + "d3-time-format": "^4.1.0", + "d3-transition": "^3.0.0" + }, + "devDependencies": { + "@angular/common": "^19.1.0", + "@angular/compiler": "^19.1.0", + "@angular/compiler-cli": "^19.1.0", + "@angular/core": "^19.1.0", + "@angular/forms": "^19.2.18", + "@types/d3-array": "^3.2.0", + "@types/d3-axis": "^3.0.0", + "@types/d3-color": "^3.1.0", + "@types/d3-format": "^3.0.0", + "@types/d3-hierarchy": "^3.1.0", + "@types/d3-interpolate": "^3.0.0", + "@types/d3-scale": "^4.0.0", + "@types/d3-selection": "^3.0.0", + "@types/d3-shape": "^3.1.0", + "@types/d3-time-format": "^4.0.0", + "@types/d3-transition": "^3.0.0", + "ng-packagr": "^19.1.0", + "typescript": "~5.7.2" + } +} diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..dde7973 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,18 @@ +export * from './viz-bar-chart'; +export * from './viz-line-chart'; +export * from './viz-area-chart'; +export * from './viz-pie-chart'; +export * from './viz-scatter-chart'; +export * from './viz-legend'; +export * from './viz-histogram'; +export * from './viz-box-plot'; +export * from './viz-heatmap'; +export * from './viz-treemap'; +export * from './viz-gauge'; +export * from './viz-sparkline'; +export * from './viz-time-series'; +export * from './viz-stat-card'; +export * from './viz-progress-ring'; +export * from './viz-progress-bar'; +export * from './viz-trend-indicator'; +export * from './viz-data-table'; diff --git a/src/components/viz-area-chart/index.ts b/src/components/viz-area-chart/index.ts new file mode 100644 index 0000000..5224c53 --- /dev/null +++ b/src/components/viz-area-chart/index.ts @@ -0,0 +1 @@ +export * from './viz-area-chart.component'; diff --git a/src/components/viz-area-chart/viz-area-chart.component.html b/src/components/viz-area-chart/viz-area-chart.component.html new file mode 100644 index 0000000..281d6c2 --- /dev/null +++ b/src/components/viz-area-chart/viz-area-chart.component.html @@ -0,0 +1,9 @@ +
+ @if (legend().visible && (legend().position === 'top' || legend().position === 'left')) { + + } +
+ @if (legend().visible && (legend().position === 'bottom' || legend().position === 'right')) { + + } +
diff --git a/src/components/viz-area-chart/viz-area-chart.component.scss b/src/components/viz-area-chart/viz-area-chart.component.scss new file mode 100644 index 0000000..b7ecded --- /dev/null +++ b/src/components/viz-area-chart/viz-area-chart.component.scss @@ -0,0 +1,46 @@ +:host { + display: block; + position: relative; + width: 100%; +} + +.viz-area-chart { + display: flex; + flex-direction: column; + width: 100%; + + &--legend-left, + &--legend-right { + flex-direction: row; + } + + > div:not(.viz-legend) { + flex: 1; + min-width: 0; + } + + ::ng-deep { + .viz-area-chart-svg { + display: block; + overflow: visible; + } + + .domain { + stroke: var(--viz-axis-color); + } + + .tick line { + stroke: var(--viz-tick-color); + } + + .tick text { + fill: var(--viz-text-muted); + font-size: var(--viz-font-size-xs); + } + + .grid-line { + stroke: var(--viz-grid-color); + stroke-dasharray: 2, 2; + } + } +} diff --git a/src/components/viz-area-chart/viz-area-chart.component.ts b/src/components/viz-area-chart/viz-area-chart.component.ts new file mode 100644 index 0000000..b389f93 --- /dev/null +++ b/src/components/viz-area-chart/viz-area-chart.component.ts @@ -0,0 +1,223 @@ +import { + Component, ChangeDetectionStrategy, ElementRef, NgZone, OnDestroy, + inject, input, output, viewChild, effect, computed, signal, +} from '@angular/core'; +import { select, Selection } from 'd3-selection'; +import { scaleLinear, scaleTime } from 'd3-scale'; +import { axisBottom, axisLeft } from 'd3-axis'; +import { area, line, curveMonotoneX } from 'd3-shape'; +import { extent, max } from 'd3-array'; +import 'd3-transition'; +import { VIZ_CONFIG } from '../../providers/viz-config.provider'; +import { VizThemeService } from '../../services/viz-theme.service'; +import { VizResizeService } from '../../services/viz-resize.service'; +import { VizTooltipService } from '../../services/viz-tooltip.service'; +import type { CartesianDataPoint, ChartSeries } from '../../types/chart.types'; +import type { ChartMargin, AxisConfig, LegendConfig } from '../../types/config.types'; +import type { ChartHoverEvent } from '../../types/event.types'; +import { withOpacity } from '../../utils/color.utils'; +import { VizLegendComponent, LegendItem } from '../viz-legend/viz-legend.component'; + +@Component({ + selector: 'viz-area-chart', + standalone: true, + imports: [VizLegendComponent], + templateUrl: './viz-area-chart.component.html', + styleUrl: './viz-area-chart.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizAreaChartComponent implements OnDestroy { + readonly series = input.required(); + readonly width = input('auto'); + readonly height = input(300); + readonly margin = input({ top: 20, right: 20, bottom: 40, left: 50 }); + readonly xAxis = input({}); + readonly yAxis = input({ gridLines: true }); + readonly legend = input({ visible: true, position: 'top' }); + readonly animate = input(undefined); + readonly stacked = input(false); + readonly gradient = input(true); + + readonly areaHover = output>(); + + readonly hiddenItems = signal>(new Set()); + + readonly legendItems = computed(() => + this.series().map((s, i) => ({ + label: s.name, + color: s.color ?? this.themeService.getColor(i), + active: !this.hiddenItems().has(s.name), + })) + ); + + private readonly chartRef = viewChild.required>('chart'); + + private readonly ngZone = inject(NgZone); + private readonly config = inject(VIZ_CONFIG); + private readonly themeService = inject(VizThemeService); + private readonly resizeService = inject(VizResizeService); + private readonly tooltipService = inject(VizTooltipService); + + private svg: Selection | null = null; + private resizeCleanup: (() => void) | null = null; + + constructor() { + effect(() => { + const _ = this.series(); + const __ = this.hiddenItems(); + this.ngZone.runOutsideAngular(() => { + if (!this.svg) { + this.createChart(); + this.setupResize(); + } else { + this.updateChart(); + } + }); + }); + } + + onLegendClick(event: { item: LegendItem; index: number }): void { + const hidden = new Set(this.hiddenItems()); + if (hidden.has(event.item.label)) { + hidden.delete(event.item.label); + } else { + hidden.add(event.item.label); + } + this.hiddenItems.set(hidden); + } + + ngOnDestroy(): void { + this.resizeCleanup?.(); + this.svg?.remove(); + } + + private getWidth(): number { + const w = this.width(); + return w === 'auto' ? (this.chartRef().nativeElement.clientWidth || 600) : w; + } + + private createChart(): void { + const el = this.chartRef().nativeElement; + this.svg = select(el) + .append('svg') + .attr('class', 'viz-area-chart-svg'); + + this.svg.append('defs'); + this.svg.append('g').attr('class', 'x-axis'); + this.svg.append('g').attr('class', 'y-axis'); + this.svg.append('g').attr('class', 'areas'); + this.svg.append('g').attr('class', 'lines'); + + this.updateChart(); + } + + private updateChart(): void { + if (!this.svg) return; + + const allSeries = this.series().filter(s => s.visible !== false && !this.hiddenItems().has(s.name)); + const w = this.getWidth(); + const h = this.height(); + const m = this.margin(); + const innerW = w - m.left - m.right; + const innerH = h - m.top - m.bottom; + const shouldAnimate = this.animate() ?? this.config.animate; + const duration = shouldAnimate ? this.config.animationDuration : 0; + const yAxisConfig = this.yAxis(); + + this.svg.attr('width', w).attr('height', h); + + const allPoints: CartesianDataPoint[] = []; + for (const s of allSeries) { for (const d of s.data) { allPoints.push(d); } } + const isTime = allPoints.length > 0 && allPoints[0].x instanceof Date; + + const xScale = isTime + ? scaleTime().domain(extent(allPoints, (d: CartesianDataPoint) => d.x as Date) as [Date, Date]).range([0, innerW]) + : scaleLinear().domain(extent(allPoints, (d: CartesianDataPoint) => d.x as number) as [number, number]).range([0, innerW]); + + const yMax = yAxisConfig.max ?? (max(allPoints, (d: CartesianDataPoint) => d.y) ?? 0); + const yScale = scaleLinear() + .domain([yAxisConfig.min ?? 0, yMax]) + .range([innerH, 0]) + .nice(); + + // Axes + this.svg.select('.x-axis') + .attr('transform', `translate(${m.left},${m.top + innerH})`) + .call(axisBottom(xScale as any)); + + const yAxisG = this.svg.select('.y-axis') + .attr('transform', `translate(${m.left},${m.top})`) + .call(axisLeft(yScale).ticks(yAxisConfig.tickCount ?? 5)); + + if (yAxisConfig.gridLines) { + yAxisG.selectAll('.grid-line').remove(); + yAxisG.selectAll('.tick line') + .clone() + .attr('x2', innerW) + .attr('stroke', 'var(--viz-grid-color, #f3f4f6)') + .attr('stroke-dasharray', '2,2') + .attr('class', 'grid-line'); + } + + // Gradients + if (this.gradient()) { + const defs = this.svg.select('defs'); + defs.selectAll('*').remove(); + + allSeries.forEach((s, i) => { + const color = s.color ?? this.themeService.getColor(i); + const grad = defs.append('linearGradient') + .attr('id', `area-gradient-${i}`) + .attr('x1', '0%').attr('y1', '0%') + .attr('x2', '0%').attr('y2', '100%'); + grad.append('stop').attr('offset', '0%').attr('stop-color', color).attr('stop-opacity', 0.4); + grad.append('stop').attr('offset', '100%').attr('stop-color', color).attr('stop-opacity', 0.05); + }); + } + + // Area generator + const areaGen = area() + .x(d => (xScale as any)(d.x)) + .y0(innerH) + .y1(d => yScale(d.y)) + .curve(curveMonotoneX); + + const lineGen = line() + .x(d => (xScale as any)(d.x)) + .y(d => yScale(d.y)) + .curve(curveMonotoneX); + + // Areas + const areasG = this.svg.select('.areas').attr('transform', `translate(${m.left},${m.top})`); + const areas = areasG.selectAll('path').data(allSeries, (d: ChartSeries) => d.name); + areas.exit().remove(); + + areas.enter().append('path') + .merge(areas) + .transition().duration(duration) + .attr('d', d => areaGen(d.data) ?? '') + .attr('fill', (_, i) => this.gradient() ? `url(#area-gradient-${i})` : withOpacity(this.themeService.getColor(i), 0.2)); + + // Lines + const linesG = this.svg.select('.lines').attr('transform', `translate(${m.left},${m.top})`); + const lines = linesG.selectAll('path').data(allSeries, (d: ChartSeries) => d.name); + lines.exit().remove(); + + lines.enter().append('path') + .attr('fill', 'none') + .attr('stroke-width', 2) + .merge(lines) + .transition().duration(duration) + .attr('d', d => lineGen(d.data) ?? '') + .attr('stroke', (d, i) => d.color ?? this.themeService.getColor(i)); + } + + private setupResize(): void { + if (this.width() === 'auto' && this.config.responsive) { + this.resizeCleanup = this.resizeService.observe( + this.chartRef().nativeElement, + () => this.updateChart(), + ); + } + } +} diff --git a/src/components/viz-bar-chart/index.ts b/src/components/viz-bar-chart/index.ts new file mode 100644 index 0000000..97c5213 --- /dev/null +++ b/src/components/viz-bar-chart/index.ts @@ -0,0 +1 @@ +export * from './viz-bar-chart.component'; diff --git a/src/components/viz-bar-chart/viz-bar-chart.component.html b/src/components/viz-bar-chart/viz-bar-chart.component.html new file mode 100644 index 0000000..f1a73a9 --- /dev/null +++ b/src/components/viz-bar-chart/viz-bar-chart.component.html @@ -0,0 +1,9 @@ +
+ @if (legend().visible && (legend().position === 'top' || legend().position === 'left')) { + + } +
+ @if (legend().visible && (legend().position === 'bottom' || legend().position === 'right')) { + + } +
diff --git a/src/components/viz-bar-chart/viz-bar-chart.component.scss b/src/components/viz-bar-chart/viz-bar-chart.component.scss new file mode 100644 index 0000000..ee8369c --- /dev/null +++ b/src/components/viz-bar-chart/viz-bar-chart.component.scss @@ -0,0 +1,55 @@ +:host { + display: block; + position: relative; + width: 100%; +} + +.viz-bar-chart { + display: flex; + flex-direction: column; + width: 100%; + + &--legend-left, + &--legend-right { + flex-direction: row; + } + + > div:not(.viz-legend) { + flex: 1; + min-width: 0; + } + + ::ng-deep { + .viz-bar-chart-svg { + display: block; + overflow: visible; + } + + .domain { + stroke: var(--viz-axis-color); + } + + .tick line { + stroke: var(--viz-tick-color); + } + + .tick text { + fill: var(--viz-text-muted); + font-size: var(--viz-font-size-xs); + } + + .grid-line { + stroke: var(--viz-grid-color); + stroke-dasharray: 2, 2; + } + + rect { + cursor: pointer; + transition: opacity var(--viz-transition); + + &:hover { + opacity: 0.8; + } + } + } +} diff --git a/src/components/viz-bar-chart/viz-bar-chart.component.ts b/src/components/viz-bar-chart/viz-bar-chart.component.ts new file mode 100644 index 0000000..560488e --- /dev/null +++ b/src/components/viz-bar-chart/viz-bar-chart.component.ts @@ -0,0 +1,279 @@ +import { + Component, ChangeDetectionStrategy, ElementRef, NgZone, OnDestroy, + inject, input, output, viewChild, effect, computed, signal, +} from '@angular/core'; +import { select, Selection } from 'd3-selection'; +import { scaleBand, scaleLinear } from 'd3-scale'; +import { axisBottom, axisLeft } from 'd3-axis'; +import { max } from 'd3-array'; +import 'd3-transition'; +import { VIZ_CONFIG } from '../../providers/viz-config.provider'; +import { VizThemeService } from '../../services/viz-theme.service'; +import { VizResizeService } from '../../services/viz-resize.service'; +import { VizTooltipService } from '../../services/viz-tooltip.service'; +import type { ChartDataPoint } from '../../types/chart.types'; +import type { ChartMargin, AxisConfig, LegendConfig } from '../../types/config.types'; +import type { ChartClickEvent, ChartHoverEvent } from '../../types/event.types'; +import { VizLegendComponent, LegendItem } from '../viz-legend/viz-legend.component'; + +@Component({ + selector: 'viz-bar-chart', + standalone: true, + imports: [VizLegendComponent], + templateUrl: './viz-bar-chart.component.html', + styleUrl: './viz-bar-chart.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizBarChartComponent implements OnDestroy { + readonly data = input.required(); + readonly width = input('auto'); + readonly height = input(300); + readonly margin = input({ top: 20, right: 20, bottom: 40, left: 50 }); + readonly xAxis = input({}); + readonly yAxis = input({ gridLines: true }); + readonly legend = input({ visible: false, position: 'top' }); + readonly animate = input(undefined); + readonly orientation = input<'vertical' | 'horizontal'>('vertical'); + readonly stacked = input(false); + readonly grouped = input(false); + + readonly barClick = output>(); + readonly barHover = output>(); + + readonly hiddenItems = signal>(new Set()); + + readonly legendItems = computed(() => + this.data().map((d, i) => ({ + label: d.label, + color: d.color ?? this.themeService.getColor(i), + active: !this.hiddenItems().has(d.label), + })) + ); + + private readonly chartRef = viewChild.required>('chart'); + + private readonly ngZone = inject(NgZone); + private readonly config = inject(VIZ_CONFIG); + private readonly themeService = inject(VizThemeService); + private readonly resizeService = inject(VizResizeService); + private readonly tooltipService = inject(VizTooltipService); + + private svg: Selection | null = null; + private resizeCleanup: (() => void) | null = null; + + constructor() { + effect(() => { + const _ = this.data(); + const __ = this.hiddenItems(); + this.ngZone.runOutsideAngular(() => { + if (!this.svg) { + this.createChart(); + this.setupResize(); + } else { + this.updateChart(); + } + }); + }); + } + + onLegendClick(event: { item: LegendItem; index: number }): void { + const hidden = new Set(this.hiddenItems()); + if (hidden.has(event.item.label)) { + hidden.delete(event.item.label); + } else { + hidden.add(event.item.label); + } + this.hiddenItems.set(hidden); + } + + ngOnDestroy(): void { + this.resizeCleanup?.(); + this.svg?.remove(); + } + + private getWidth(): number { + const w = this.width(); + if (w === 'auto') { + return this.chartRef().nativeElement.clientWidth || 600; + } + return w; + } + + private createChart(): void { + const el = this.chartRef().nativeElement; + const w = this.getWidth(); + const h = this.height(); + + this.svg = select(el) + .append('svg') + .attr('width', w) + .attr('height', h) + .attr('class', 'viz-bar-chart-svg'); + + this.svg.append('g').attr('class', 'x-axis'); + this.svg.append('g').attr('class', 'y-axis'); + this.svg.append('g').attr('class', 'bars'); + + this.updateChart(); + } + + private updateChart(): void { + if (!this.svg) return; + + const data = this.data().filter(d => !this.hiddenItems().has(d.label)); + const w = this.getWidth(); + const h = this.height(); + const m = this.margin(); + const innerW = w - m.left - m.right; + const innerH = h - m.top - m.bottom; + const shouldAnimate = this.animate() ?? this.config.animate; + const duration = shouldAnimate ? this.config.animationDuration : 0; + const yAxisConfig = this.yAxis(); + const isVertical = this.orientation() === 'vertical'; + + this.svg.attr('width', w).attr('height', h); + + if (isVertical) { + const x = scaleBand() + .domain(data.map(d => d.label)) + .range([0, innerW]) + .padding(0.2); + + const y = scaleLinear() + .domain([yAxisConfig.min ?? 0, yAxisConfig.max ?? (max(data, d => d.value) ?? 0)]) + .range([innerH, 0]) + .nice(); + + // X axis + this.svg.select('.x-axis') + .attr('transform', `translate(${m.left},${m.top + innerH})`) + .call(axisBottom(x)); + + // Y axis + const yAxisG = this.svg.select('.y-axis') + .attr('transform', `translate(${m.left},${m.top})`) + .call(axisLeft(y).ticks(yAxisConfig.tickCount ?? 5)); + + // Grid lines + if (yAxisConfig.gridLines) { + yAxisG.selectAll('.tick line') + .clone() + .attr('x2', innerW) + .attr('stroke', 'var(--viz-grid-color, #f3f4f6)') + .attr('stroke-dasharray', '2,2') + .attr('class', 'grid-line'); + } + + // Bars + const barsG = this.svg.select('.bars') + .attr('transform', `translate(${m.left},${m.top})`); + + const bars = barsG.selectAll('rect') + .data(data, (d: ChartDataPoint) => d.label); + + bars.exit().transition().duration(duration).attr('height', 0).attr('y', innerH).remove(); + + const enter = bars.enter() + .append('rect') + .attr('x', d => x(d.label) ?? 0) + .attr('width', x.bandwidth()) + .attr('y', innerH) + .attr('height', 0) + .attr('rx', 2) + .attr('fill', (d, i) => d.color ?? this.themeService.getColor(i)); + + enter.merge(bars) + .on('click', (event: MouseEvent, d: ChartDataPoint) => { + this.ngZone.run(() => this.barClick.emit({ data: d, index: data.indexOf(d), event })); + }) + .on('mouseenter', (event: MouseEvent, d: ChartDataPoint) => { + if (this.config.tooltips) { + this.tooltipService.show(event.clientX, event.clientY, `${d.label}: ${d.value}`); + } + this.ngZone.run(() => this.barHover.emit({ data: d, index: data.indexOf(d), event })); + }) + .on('mousemove', (event: MouseEvent) => { + this.tooltipService.update(event.clientX, event.clientY); + }) + .on('mouseleave', (event: MouseEvent) => { + this.tooltipService.hide(); + this.ngZone.run(() => this.barHover.emit({ data: null, index: -1, event })); + }) + .transition() + .duration(duration) + .attr('x', d => x(d.label) ?? 0) + .attr('width', x.bandwidth()) + .attr('y', d => y(d.value)) + .attr('height', d => innerH - y(d.value)) + .attr('fill', (d, i) => d.color ?? this.themeService.getColor(i)); + } else { + // Horizontal bars + const y = scaleBand() + .domain(data.map(d => d.label)) + .range([0, innerH]) + .padding(0.2); + + const x = scaleLinear() + .domain([0, yAxisConfig.max ?? (max(data, d => d.value) ?? 0)]) + .range([0, innerW]) + .nice(); + + this.svg.select('.x-axis') + .attr('transform', `translate(${m.left},${m.top + innerH})`) + .call(axisBottom(x).ticks(yAxisConfig.tickCount ?? 5)); + + this.svg.select('.y-axis') + .attr('transform', `translate(${m.left},${m.top})`) + .call(axisLeft(y)); + + const barsG = this.svg.select('.bars') + .attr('transform', `translate(${m.left},${m.top})`); + + const bars = barsG.selectAll('rect') + .data(data, (d: ChartDataPoint) => d.label); + + bars.exit().transition().duration(duration).attr('width', 0).remove(); + + const enter = bars.enter() + .append('rect') + .attr('x', 0) + .attr('y', d => y(d.label) ?? 0) + .attr('width', 0) + .attr('height', y.bandwidth()) + .attr('rx', 2) + .attr('fill', (d, i) => d.color ?? this.themeService.getColor(i)); + + enter.merge(bars) + .on('click', (event: MouseEvent, d: ChartDataPoint) => { + this.ngZone.run(() => this.barClick.emit({ data: d, index: data.indexOf(d), event })); + }) + .on('mouseenter', (event: MouseEvent, d: ChartDataPoint) => { + if (this.config.tooltips) { + this.tooltipService.show(event.clientX, event.clientY, `${d.label}: ${d.value}`); + } + this.ngZone.run(() => this.barHover.emit({ data: d, index: data.indexOf(d), event })); + }) + .on('mousemove', (event: MouseEvent) => { + this.tooltipService.update(event.clientX, event.clientY); + }) + .on('mouseleave', (event: MouseEvent) => { + this.tooltipService.hide(); + this.ngZone.run(() => this.barHover.emit({ data: null, index: -1, event })); + }) + .transition() + .duration(duration) + .attr('y', d => y(d.label) ?? 0) + .attr('height', y.bandwidth()) + .attr('width', d => x(d.value)); + } + } + + private setupResize(): void { + if (this.width() === 'auto' && this.config.responsive) { + this.resizeCleanup = this.resizeService.observe( + this.chartRef().nativeElement, + () => this.updateChart(), + ); + } + } +} diff --git a/src/components/viz-box-plot/index.ts b/src/components/viz-box-plot/index.ts new file mode 100644 index 0000000..ec16375 --- /dev/null +++ b/src/components/viz-box-plot/index.ts @@ -0,0 +1 @@ +export * from './viz-box-plot.component'; diff --git a/src/components/viz-box-plot/viz-box-plot.component.html b/src/components/viz-box-plot/viz-box-plot.component.html new file mode 100644 index 0000000..d4aa59b --- /dev/null +++ b/src/components/viz-box-plot/viz-box-plot.component.html @@ -0,0 +1 @@ +
diff --git a/src/components/viz-box-plot/viz-box-plot.component.scss b/src/components/viz-box-plot/viz-box-plot.component.scss new file mode 100644 index 0000000..5d1218a --- /dev/null +++ b/src/components/viz-box-plot/viz-box-plot.component.scss @@ -0,0 +1,17 @@ +:host { + display: block; + position: relative; + width: 100%; +} + +.viz-box-plot { + width: 100%; + + ::ng-deep { + .viz-box-plot-svg { display: block; overflow: visible; } + .domain { stroke: var(--viz-axis-color); } + .tick line { stroke: var(--viz-tick-color); } + .tick text { fill: var(--viz-text-muted); font-size: var(--viz-font-size-xs); } + .grid-line { stroke: var(--viz-grid-color); stroke-dasharray: 2, 2; } + } +} diff --git a/src/components/viz-box-plot/viz-box-plot.component.ts b/src/components/viz-box-plot/viz-box-plot.component.ts new file mode 100644 index 0000000..0630c61 --- /dev/null +++ b/src/components/viz-box-plot/viz-box-plot.component.ts @@ -0,0 +1,217 @@ +import { + Component, ChangeDetectionStrategy, ElementRef, NgZone, OnDestroy, + inject, input, output, viewChild, effect, +} from '@angular/core'; +import { select, Selection } from 'd3-selection'; +import { scaleBand, scaleLinear } from 'd3-scale'; +import { axisBottom, axisLeft } from 'd3-axis'; +import { min, max } from 'd3-array'; +import 'd3-transition'; +import { VIZ_CONFIG } from '../../providers/viz-config.provider'; +import { VizThemeService } from '../../services/viz-theme.service'; +import { VizResizeService } from '../../services/viz-resize.service'; +import { VizTooltipService } from '../../services/viz-tooltip.service'; +import type { BoxPlotData } from '../../types/chart.types'; +import type { ChartMargin, AxisConfig } from '../../types/config.types'; +import type { ChartHoverEvent } from '../../types/event.types'; + +@Component({ + selector: 'viz-box-plot', + standalone: true, + templateUrl: './viz-box-plot.component.html', + styleUrl: './viz-box-plot.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizBoxPlotComponent implements OnDestroy { + readonly data = input.required(); + readonly width = input('auto'); + readonly height = input(300); + readonly margin = input({ top: 20, right: 20, bottom: 40, left: 50 }); + readonly yAxis = input({ gridLines: true }); + readonly animate = input(undefined); + readonly showOutliers = input(true); + readonly orientation = input<'vertical' | 'horizontal'>('vertical'); + + readonly boxHover = output>(); + + private readonly chartRef = viewChild.required>('chart'); + + private readonly ngZone = inject(NgZone); + private readonly config = inject(VIZ_CONFIG); + private readonly themeService = inject(VizThemeService); + private readonly resizeService = inject(VizResizeService); + private readonly tooltipService = inject(VizTooltipService); + + private svg: Selection | null = null; + private resizeCleanup: (() => void) | null = null; + + constructor() { + effect(() => { + const _ = this.data(); + this.ngZone.runOutsideAngular(() => { + if (!this.svg) { + this.createChart(); + this.setupResize(); + } else { + this.updateChart(); + } + }); + }); + } + + ngOnDestroy(): void { + this.resizeCleanup?.(); + this.svg?.remove(); + } + + private getWidth(): number { + const w = this.width(); + return w === 'auto' ? (this.chartRef().nativeElement.clientWidth || 600) : w; + } + + private createChart(): void { + const el = this.chartRef().nativeElement; + this.svg = select(el).append('svg').attr('class', 'viz-box-plot-svg'); + this.svg.append('g').attr('class', 'x-axis'); + this.svg.append('g').attr('class', 'y-axis'); + this.svg.append('g').attr('class', 'boxes'); + this.updateChart(); + } + + private updateChart(): void { + if (!this.svg) return; + + const data = this.data(); + const w = this.getWidth(); + const h = this.height(); + const m = this.margin(); + const innerW = w - m.left - m.right; + const innerH = h - m.top - m.bottom; + const shouldAnimate = this.animate() ?? this.config.animate; + const duration = shouldAnimate ? this.config.animationDuration : 0; + + this.svg.attr('width', w).attr('height', h); + + const xScale = scaleBand() + .domain(data.map(d => d.label)) + .range([0, innerW]) + .padding(0.3); + + const allValues: number[] = []; + for (const d of data) { allValues.push(d.min, d.max, ...(d.outliers ?? [])); } + const yScale = scaleLinear() + .domain([min(allValues) ?? 0, max(allValues) ?? 0]) + .range([innerH, 0]) + .nice(); + + this.svg.select('.x-axis') + .attr('transform', `translate(${m.left},${m.top + innerH})`) + .call(axisBottom(xScale)); + + const yAxisG = this.svg.select('.y-axis') + .attr('transform', `translate(${m.left},${m.top})`) + .call(axisLeft(yScale)); + + if (this.yAxis().gridLines) { + yAxisG.selectAll('.grid-line').remove(); + yAxisG.selectAll('.tick line') + .clone() + .attr('x2', innerW) + .attr('stroke', 'var(--viz-grid-color, #f3f4f6)') + .attr('stroke-dasharray', '2,2') + .attr('class', 'grid-line'); + } + + const boxesG = this.svg.select('.boxes').attr('transform', `translate(${m.left},${m.top})`); + boxesG.selectAll('*').remove(); + + const bandwidth = xScale.bandwidth(); + const center = bandwidth / 2; + + data.forEach((d, i) => { + const x = xScale(d.label) ?? 0; + const g = boxesG.append('g').attr('transform', `translate(${x},0)`); + const color = this.themeService.getColor(i); + + // Vertical line min-max (whisker) + g.append('line') + .attr('x1', center).attr('x2', center) + .attr('y1', yScale(d.min)).attr('y2', yScale(d.max)) + .attr('stroke', 'var(--viz-axis-color, #6b7280)') + .attr('stroke-width', 1); + + // Min whisker cap + g.append('line') + .attr('x1', center - bandwidth * 0.2).attr('x2', center + bandwidth * 0.2) + .attr('y1', yScale(d.min)).attr('y2', yScale(d.min)) + .attr('stroke', 'var(--viz-axis-color, #6b7280)') + .attr('stroke-width', 1); + + // Max whisker cap + g.append('line') + .attr('x1', center - bandwidth * 0.2).attr('x2', center + bandwidth * 0.2) + .attr('y1', yScale(d.max)).attr('y2', yScale(d.max)) + .attr('stroke', 'var(--viz-axis-color, #6b7280)') + .attr('stroke-width', 1); + + // Box (Q1 to Q3) + g.append('rect') + .attr('x', 0) + .attr('y', yScale(d.q3)) + .attr('width', bandwidth) + .attr('height', yScale(d.q1) - yScale(d.q3)) + .attr('fill', color) + .attr('opacity', 0.6) + .attr('stroke', color) + .attr('stroke-width', 1.5) + .attr('rx', 2); + + // Median line + g.append('line') + .attr('x1', 0).attr('x2', bandwidth) + .attr('y1', yScale(d.median)).attr('y2', yScale(d.median)) + .attr('stroke', 'var(--viz-text, #111827)') + .attr('stroke-width', 2); + + // Outliers + if (this.showOutliers() && d.outliers) { + d.outliers.forEach(o => { + g.append('circle') + .attr('cx', center) + .attr('cy', yScale(o)) + .attr('r', 3) + .attr('fill', 'none') + .attr('stroke', color) + .attr('stroke-width', 1.5); + }); + } + + // Hover area + g.append('rect') + .attr('x', 0).attr('y', 0) + .attr('width', bandwidth).attr('height', innerH) + .attr('fill', 'transparent') + .on('mouseenter', (event: MouseEvent) => { + if (this.config.tooltips) { + this.tooltipService.show(event.clientX, event.clientY, + `${d.label}
Min: ${d.min} | Q1: ${d.q1}
Median: ${d.median}
Q3: ${d.q3} | Max: ${d.max}`); + } + this.ngZone.run(() => this.boxHover.emit({ data: d, index: i, event })); + }) + .on('mousemove', (event: MouseEvent) => this.tooltipService.update(event.clientX, event.clientY)) + .on('mouseleave', (event: MouseEvent) => { + this.tooltipService.hide(); + this.ngZone.run(() => this.boxHover.emit({ data: null, index: -1, event })); + }); + }); + } + + private setupResize(): void { + if (this.width() === 'auto' && this.config.responsive) { + this.resizeCleanup = this.resizeService.observe( + this.chartRef().nativeElement, + () => this.updateChart(), + ); + } + } +} diff --git a/src/components/viz-data-table/index.ts b/src/components/viz-data-table/index.ts new file mode 100644 index 0000000..a4ac55a --- /dev/null +++ b/src/components/viz-data-table/index.ts @@ -0,0 +1 @@ +export * from './viz-data-table.component'; diff --git a/src/components/viz-data-table/viz-data-table.component.html b/src/components/viz-data-table/viz-data-table.component.html new file mode 100644 index 0000000..c32fb20 --- /dev/null +++ b/src/components/viz-data-table/viz-data-table.component.html @@ -0,0 +1,90 @@ +
+ @if (filterable()) { +
+ +
+ } + +
+ + + + @for (col of columns(); track col.key) { + + } + + + + @for (row of pagedData(); track $index; let i = $index) { + + @for (col of columns(); track col.key) { + + } + + } + @if (pagedData().length === 0) { + + + + } + +
+ + {{ col.label }} + @if (sortable() && col.sortable !== false) { + {{ getSortIcon(col) }} + } + +
+ {{ getCellValue(row, col) }} +
+ No data available +
+
+ + @if (paginated() && totalPages() > 1) { +
+ + @for (page of pageNumbers(); track page) { + + } + +
+ } +
diff --git a/src/components/viz-data-table/viz-data-table.component.scss b/src/components/viz-data-table/viz-data-table.component.scss new file mode 100644 index 0000000..c92555b --- /dev/null +++ b/src/components/viz-data-table/viz-data-table.component.scss @@ -0,0 +1,149 @@ +:host { + display: block; + width: 100%; +} + +.viz-data-table { + &__filter { + margin-bottom: var(--viz-spacing-md); + } + + &__filter-input { + width: 100%; + max-width: 300px; + padding: var(--viz-spacing-sm) var(--viz-spacing-md); + border: 1px solid var(--viz-border); + border-radius: var(--viz-radius-md); + background: var(--viz-bg); + color: var(--viz-text); + font-size: var(--viz-font-size-sm); + outline: none; + transition: border-color var(--viz-transition); + + &:focus { + border-color: var(--viz-color-1, #3b82f6); + } + + &::placeholder { + color: var(--viz-text-muted); + } + } + + &__wrapper { + overflow-x: auto; + } + + &__table { + width: 100%; + border-collapse: collapse; + font-size: var(--viz-font-size-sm); + + th, + td { + padding: var(--viz-spacing-sm) var(--viz-spacing-md); + border-bottom: 1px solid var(--viz-border); + } + + th { + font-weight: 600; + color: var(--viz-text-muted); + text-transform: uppercase; + font-size: var(--viz-font-size-xs); + letter-spacing: 0.05em; + background: var(--viz-bg); + position: sticky; + top: 0; + user-select: none; + } + + td { + color: var(--viz-text); + } + + &--striped { + tbody tr:nth-child(even) { + background: var(--viz-grid-color); + } + } + + &--hoverable { + tbody tr { + cursor: pointer; + transition: background-color var(--viz-transition); + + &:hover { + background: var(--viz-grid-color); + } + } + } + } + + &__th--sortable { + cursor: pointer; + + &:hover { + color: var(--viz-text); + } + } + + &__th-content { + display: inline-flex; + align-items: center; + gap: var(--viz-spacing-xs); + } + + &__sort-icon { + font-size: 0.625rem; + opacity: 0.5; + } + + &__empty { + text-align: center; + color: var(--viz-text-muted); + padding: var(--viz-spacing-lg) !important; + } + + &__pagination { + display: flex; + justify-content: center; + align-items: center; + gap: var(--viz-spacing-xs); + margin-top: var(--viz-spacing-md); + padding: var(--viz-spacing-sm) 0; + } + + &__page-btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 32px; + height: 32px; + padding: 0 var(--viz-spacing-sm); + border: 1px solid var(--viz-border); + border-radius: var(--viz-radius-sm); + background: var(--viz-bg); + color: var(--viz-text); + font-size: var(--viz-font-size-sm); + cursor: pointer; + transition: all var(--viz-transition); + + &:hover:not(:disabled) { + background: var(--viz-grid-color); + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } + + &--active { + background: var(--viz-color-1, #3b82f6); + color: #fff; + border-color: var(--viz-color-1, #3b82f6); + + &:hover { + background: var(--viz-color-1, #3b82f6); + } + } + } +} diff --git a/src/components/viz-data-table/viz-data-table.component.ts b/src/components/viz-data-table/viz-data-table.component.ts new file mode 100644 index 0000000..c735507 --- /dev/null +++ b/src/components/viz-data-table/viz-data-table.component.ts @@ -0,0 +1,122 @@ +import { + Component, ChangeDetectionStrategy, input, output, computed, signal, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import type { TableColumn, TableSort } from '../../types/chart.types'; +import type { TablePageEvent, TableSortEvent } from '../../types/event.types'; + +@Component({ + selector: 'viz-data-table', + standalone: true, + imports: [CommonModule], + templateUrl: './viz-data-table.component.html', + styleUrl: './viz-data-table.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizDataTableComponent> { + readonly data = input.required(); + readonly columns = input.required[]>(); + readonly sortable = input(true); + readonly filterable = input(false); + readonly paginated = input(false); + readonly pageSize = input(10); + readonly striped = input(true); + readonly hoverable = input(true); + + readonly sortChange = output(); + readonly pageChange = output(); + readonly rowClick = output<{ row: T; index: number }>(); + + readonly currentSort = signal(null); + readonly currentPage = signal(0); + readonly filterText = signal(''); + + readonly filteredData = computed(() => { + let result = this.data(); + const filter = this.filterText().toLowerCase(); + + if (filter && this.filterable()) { + const cols = this.columns().filter(c => c.filterable !== false); + result = result.filter(row => + cols.some(col => { + const val = (row as any)[col.key]; + return String(val ?? '').toLowerCase().includes(filter); + }) + ); + } + + const sort = this.currentSort(); + if (sort) { + result = [...result].sort((a, b) => { + const aVal = (a as any)[sort.column]; + const bVal = (b as any)[sort.column]; + const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0; + return sort.direction === 'asc' ? cmp : -cmp; + }); + } + + return result; + }); + + readonly pagedData = computed(() => { + const filtered = this.filteredData(); + if (!this.paginated()) return filtered; + + const start = this.currentPage() * this.pageSize(); + return filtered.slice(start, start + this.pageSize()); + }); + + readonly totalPages = computed(() => { + if (!this.paginated()) return 1; + return Math.ceil(this.filteredData().length / this.pageSize()); + }); + + readonly pageNumbers = computed(() => { + const total = this.totalPages(); + return Array.from({ length: total }, (_, i) => i); + }); + + onSort(column: TableColumn): void { + if (!this.sortable() || column.sortable === false) return; + + const current = this.currentSort(); + let direction: 'asc' | 'desc' = 'asc'; + + if (current?.column === column.key) { + direction = current.direction === 'asc' ? 'desc' : 'asc'; + } + + this.currentSort.set({ column: column.key, direction }); + this.sortChange.emit({ column: column.key, direction }); + } + + onPageChange(page: number): void { + if (page < 0 || page >= this.totalPages()) return; + this.currentPage.set(page); + this.pageChange.emit({ page, pageSize: this.pageSize() }); + } + + onRowClick(row: T, index: number): void { + this.rowClick.emit({ row, index }); + } + + getCellValue(row: T, column: TableColumn): string { + const value = (row as any)[column.key]; + if (column.format) { + return column.format(value, row); + } + return String(value ?? ''); + } + + getSortIcon(column: TableColumn): string { + const sort = this.currentSort(); + if (sort?.column !== column.key) return '\u2195'; + return sort.direction === 'asc' ? '\u2191' : '\u2193'; + } + + onFilterInput(event: Event): void { + const target = event.target as HTMLInputElement; + this.filterText.set(target.value); + this.currentPage.set(0); + } +} diff --git a/src/components/viz-gauge/index.ts b/src/components/viz-gauge/index.ts new file mode 100644 index 0000000..2879cc9 --- /dev/null +++ b/src/components/viz-gauge/index.ts @@ -0,0 +1 @@ +export * from './viz-gauge.component'; diff --git a/src/components/viz-gauge/viz-gauge.component.html b/src/components/viz-gauge/viz-gauge.component.html new file mode 100644 index 0000000..9852858 --- /dev/null +++ b/src/components/viz-gauge/viz-gauge.component.html @@ -0,0 +1 @@ +
diff --git a/src/components/viz-gauge/viz-gauge.component.scss b/src/components/viz-gauge/viz-gauge.component.scss new file mode 100644 index 0000000..35067ca --- /dev/null +++ b/src/components/viz-gauge/viz-gauge.component.scss @@ -0,0 +1,9 @@ +:host { + display: inline-block; +} + +.viz-gauge { + ::ng-deep { + .viz-gauge-svg { display: block; overflow: visible; } + } +} diff --git a/src/components/viz-gauge/viz-gauge.component.ts b/src/components/viz-gauge/viz-gauge.component.ts new file mode 100644 index 0000000..da2d362 --- /dev/null +++ b/src/components/viz-gauge/viz-gauge.component.ts @@ -0,0 +1,238 @@ +import { + Component, ChangeDetectionStrategy, ElementRef, NgZone, OnDestroy, + inject, input, viewChild, effect, computed, +} from '@angular/core'; +import { select, Selection } from 'd3-selection'; +import { arc } from 'd3-shape'; +import { scaleLinear } from 'd3-scale'; +import { interpolate } from 'd3-interpolate'; +import 'd3-transition'; +import { VIZ_CONFIG } from '../../providers/viz-config.provider'; +import { VizThemeService } from '../../services/viz-theme.service'; + +export interface GaugeThreshold { + value: number; + color: string; +} + +@Component({ + selector: 'viz-gauge', + standalone: true, + templateUrl: './viz-gauge.component.html', + styleUrl: './viz-gauge.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizGaugeComponent implements OnDestroy { + readonly value = input.required(); + readonly min = input(0); + readonly max = input(100); + readonly width = input(200); + readonly height = input(150); + readonly animate = input(undefined); + readonly variant = input<'radial' | 'linear'>('radial'); + readonly label = input(''); + readonly format = input<(v: number) => string>(v => v.toFixed(0)); + readonly thresholds = input([ + { value: 33, color: '#10b981' }, + { value: 66, color: '#f59e0b' }, + { value: 100, color: '#ef4444' }, + ]); + + private readonly chartRef = viewChild.required>('chart'); + + private readonly ngZone = inject(NgZone); + private readonly config = inject(VIZ_CONFIG); + private readonly themeService = inject(VizThemeService); + + private svg: Selection | null = null; + + readonly displayValue = computed(() => this.format()(this.value())); + + constructor() { + effect(() => { + const _ = this.value(); + this.ngZone.runOutsideAngular(() => { + if (!this.svg) { + this.createChart(); + } else { + this.updateChart(); + } + }); + }); + } + + ngOnDestroy(): void { + this.svg?.remove(); + } + + private getColorForValue(value: number): string { + const thresholds = this.thresholds(); + const pct = ((value - this.min()) / (this.max() - this.min())) * 100; + for (const t of thresholds) { + if (pct <= t.value) return t.color; + } + return thresholds[thresholds.length - 1]?.color ?? this.themeService.getColor(0); + } + + private createChart(): void { + if (this.variant() === 'linear') { + this.createLinearGauge(); + } else { + this.createRadialGauge(); + } + } + + private updateChart(): void { + if (this.variant() === 'linear') { + this.updateLinearGauge(); + } else { + this.updateRadialGauge(); + } + } + + private createRadialGauge(): void { + const el = this.chartRef().nativeElement; + const w = this.width(); + const h = this.height(); + this.svg = select(el).append('svg') + .attr('width', w) + .attr('height', h) + .attr('class', 'viz-gauge-svg'); + + const radius = Math.min(w, h) * 0.45; + const g = this.svg.append('g') + .attr('transform', `translate(${w / 2},${h * 0.6})`); + + // Background arc + const bgArc = arc() + .innerRadius(radius * 0.7) + .outerRadius(radius) + .startAngle(-Math.PI * 0.75) + .endAngle(Math.PI * 0.75) + .cornerRadius(4); + + g.append('path') + .attr('d', bgArc({}) ?? '') + .attr('fill', 'var(--viz-grid-color, #f3f4f6)'); + + // Value arc + g.append('path').attr('class', 'value-arc'); + + // Value text + g.append('text') + .attr('class', 'value-text') + .attr('text-anchor', 'middle') + .attr('dy', '-0.1em') + .attr('font-size', radius * 0.35) + .attr('font-weight', '600') + .attr('fill', 'var(--viz-text, #111827)'); + + // Label text + g.append('text') + .attr('class', 'label-text') + .attr('text-anchor', 'middle') + .attr('dy', '1.4em') + .attr('font-size', 'var(--viz-font-size-sm, 14px)') + .attr('fill', 'var(--viz-text-muted, #6b7280)'); + + this.updateRadialGauge(); + } + + private updateRadialGauge(): void { + if (!this.svg) return; + + const w = this.width(); + const h = this.height(); + const radius = Math.min(w, h) * 0.45; + const value = this.value(); + const minVal = this.min(); + const maxVal = this.max(); + const shouldAnimate = this.animate() ?? this.config.animate; + const duration = shouldAnimate ? this.config.animationDuration : 0; + + const scale = scaleLinear() + .domain([minVal, maxVal]) + .range([-Math.PI * 0.75, Math.PI * 0.75]) + .clamp(true); + + const valueArc = arc() + .innerRadius(radius * 0.7) + .outerRadius(radius) + .startAngle(-Math.PI * 0.75) + .cornerRadius(4); + + const endAngle = scale(value); + + this.svg.select('.value-arc') + .transition() + .duration(duration) + .attrTween('d', function() { + const current = (this as any)._currentAngle ?? -Math.PI * 0.75; + const i = interpolate(current, endAngle); + (this as any)._currentAngle = endAngle; + return (t: number) => valueArc({ endAngle: i(t) }) ?? ''; + }) + .attr('fill', this.getColorForValue(value)); + + this.svg.select('.value-text').text(this.displayValue()); + this.svg.select('.label-text').text(this.label()); + } + + private createLinearGauge(): void { + const el = this.chartRef().nativeElement; + const w = this.width(); + const h = 40; + this.svg = select(el).append('svg') + .attr('width', w) + .attr('height', h) + .attr('class', 'viz-gauge-svg viz-gauge-linear'); + + // Background + this.svg.append('rect') + .attr('class', 'bg-bar') + .attr('x', 0).attr('y', 8) + .attr('width', w).attr('height', 12) + .attr('rx', 6) + .attr('fill', 'var(--viz-grid-color, #f3f4f6)'); + + // Value bar + this.svg.append('rect') + .attr('class', 'value-bar') + .attr('x', 0).attr('y', 8) + .attr('height', 12) + .attr('rx', 6); + + // Value label + this.svg.append('text') + .attr('class', 'value-label') + .attr('y', 36) + .attr('font-size', 'var(--viz-font-size-sm, 14px)') + .attr('fill', 'var(--viz-text, #111827)') + .attr('font-weight', '600'); + + this.updateLinearGauge(); + } + + private updateLinearGauge(): void { + if (!this.svg) return; + + const w = this.width(); + const value = this.value(); + const minVal = this.min(); + const maxVal = this.max(); + const pct = Math.max(0, Math.min(1, (value - minVal) / (maxVal - minVal))); + const shouldAnimate = this.animate() ?? this.config.animate; + const duration = shouldAnimate ? this.config.animationDuration : 0; + + this.svg.select('.value-bar') + .transition() + .duration(duration) + .attr('width', w * pct) + .attr('fill', this.getColorForValue(value)); + + this.svg.select('.value-label') + .attr('x', w * pct) + .attr('text-anchor', pct > 0.1 ? 'end' : 'start') + .text(this.displayValue()); + } +} diff --git a/src/components/viz-heatmap/index.ts b/src/components/viz-heatmap/index.ts new file mode 100644 index 0000000..c8d5e99 --- /dev/null +++ b/src/components/viz-heatmap/index.ts @@ -0,0 +1 @@ +export * from './viz-heatmap.component'; diff --git a/src/components/viz-heatmap/viz-heatmap.component.html b/src/components/viz-heatmap/viz-heatmap.component.html new file mode 100644 index 0000000..78bf4ec --- /dev/null +++ b/src/components/viz-heatmap/viz-heatmap.component.html @@ -0,0 +1 @@ +
diff --git a/src/components/viz-heatmap/viz-heatmap.component.scss b/src/components/viz-heatmap/viz-heatmap.component.scss new file mode 100644 index 0000000..51953d5 --- /dev/null +++ b/src/components/viz-heatmap/viz-heatmap.component.scss @@ -0,0 +1,19 @@ +:host { + display: block; + position: relative; + width: 100%; +} + +.viz-heatmap { + width: 100%; + + ::ng-deep { + .viz-heatmap-svg { display: block; overflow: visible; } + + rect { + cursor: pointer; + transition: opacity var(--viz-transition); + &:hover { opacity: 0.8; stroke: var(--viz-text); stroke-width: 1; } + } + } +} diff --git a/src/components/viz-heatmap/viz-heatmap.component.ts b/src/components/viz-heatmap/viz-heatmap.component.ts new file mode 100644 index 0000000..a3322e3 --- /dev/null +++ b/src/components/viz-heatmap/viz-heatmap.component.ts @@ -0,0 +1,183 @@ +import { + Component, ChangeDetectionStrategy, ElementRef, NgZone, OnDestroy, + inject, input, output, viewChild, effect, +} from '@angular/core'; +import { select, Selection } from 'd3-selection'; +import { scaleBand, scaleSequential } from 'd3-scale'; +import { extent } from 'd3-array'; +import { interpolateRgb } from 'd3-interpolate'; +import 'd3-transition'; +import { VIZ_CONFIG } from '../../providers/viz-config.provider'; +import { VizResizeService } from '../../services/viz-resize.service'; +import { VizTooltipService } from '../../services/viz-tooltip.service'; +import type { HeatmapCell } from '../../types/chart.types'; +import type { ChartMargin } from '../../types/config.types'; +import type { ChartClickEvent, ChartHoverEvent } from '../../types/event.types'; + +@Component({ + selector: 'viz-heatmap', + standalone: true, + templateUrl: './viz-heatmap.component.html', + styleUrl: './viz-heatmap.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizHeatmapComponent implements OnDestroy { + readonly data = input.required(); + readonly width = input('auto'); + readonly height = input(300); + readonly margin = input({ top: 30, right: 20, bottom: 40, left: 60 }); + readonly xLabels = input([]); + readonly yLabels = input([]); + readonly animate = input(undefined); + readonly colorScale = input<'blues' | 'reds' | 'greens'>('blues'); + + readonly cellClick = output>(); + readonly cellHover = output>(); + + private readonly chartRef = viewChild.required>('chart'); + + private readonly ngZone = inject(NgZone); + private readonly config = inject(VIZ_CONFIG); + private readonly resizeService = inject(VizResizeService); + private readonly tooltipService = inject(VizTooltipService); + + private svg: Selection | null = null; + private resizeCleanup: (() => void) | null = null; + + constructor() { + effect(() => { + const _ = this.data(); + this.ngZone.runOutsideAngular(() => { + if (!this.svg) { + this.createChart(); + this.setupResize(); + } else { + this.updateChart(); + } + }); + }); + } + + ngOnDestroy(): void { + this.resizeCleanup?.(); + this.svg?.remove(); + } + + private getWidth(): number { + const w = this.width(); + return w === 'auto' ? (this.chartRef().nativeElement.clientWidth || 600) : w; + } + + private createChart(): void { + const el = this.chartRef().nativeElement; + this.svg = select(el).append('svg').attr('class', 'viz-heatmap-svg'); + this.svg.append('g').attr('class', 'x-axis'); + this.svg.append('g').attr('class', 'y-axis'); + this.svg.append('g').attr('class', 'cells'); + this.updateChart(); + } + + private updateChart(): void { + if (!this.svg) return; + + const data = this.data(); + const w = this.getWidth(); + const h = this.height(); + const m = this.margin(); + const innerW = w - m.left - m.right; + const innerH = h - m.top - m.bottom; + const shouldAnimate = this.animate() ?? this.config.animate; + const duration = shouldAnimate ? this.config.animationDuration : 0; + + this.svg.attr('width', w).attr('height', h); + + const xLabels = this.xLabels().length + ? this.xLabels() + : [...new Set(data.map(d => String(d.x)))]; + const yLabels = this.yLabels().length + ? this.yLabels() + : [...new Set(data.map(d => String(d.y)))]; + + const xScale = scaleBand().domain(xLabels).range([0, innerW]).padding(0.05); + const yScale = scaleBand().domain(yLabels).range([0, innerH]).padding(0.05); + + const [minVal, maxVal] = extent(data, d => d.value) as [number, number]; + const color = scaleSequential(interpolateRgb('#f7fbff', '#08519c')).domain([minVal, maxVal]); + + this.svg.select('.x-axis') + .attr('transform', `translate(${m.left},${m.top + innerH})`) + .call((g) => { + g.selectAll('text').remove(); + xLabels.forEach(label => { + g.append('text') + .attr('x', (xScale(label) ?? 0) + xScale.bandwidth() / 2) + .attr('y', 15) + .attr('text-anchor', 'middle') + .attr('fill', 'var(--viz-text-muted)') + .attr('font-size', 'var(--viz-font-size-xs)') + .text(label); + }); + }); + + this.svg.select('.y-axis') + .attr('transform', `translate(${m.left},${m.top})`) + .call((g) => { + g.selectAll('text').remove(); + yLabels.forEach(label => { + g.append('text') + .attr('x', -8) + .attr('y', (yScale(label) ?? 0) + yScale.bandwidth() / 2) + .attr('text-anchor', 'end') + .attr('dominant-baseline', 'middle') + .attr('fill', 'var(--viz-text-muted)') + .attr('font-size', 'var(--viz-font-size-xs)') + .text(label); + }); + }); + + const cellsG = this.svg.select('.cells').attr('transform', `translate(${m.left},${m.top})`); + + const cells = cellsG.selectAll('rect') + .data(data, (d: HeatmapCell) => `${d.x}-${d.y}`); + + cells.exit().transition().duration(duration).attr('opacity', 0).remove(); + + cells.enter() + .append('rect') + .attr('rx', 2) + .attr('opacity', 0) + .merge(cells) + .on('mouseenter', (event: MouseEvent, d) => { + if (this.config.tooltips) { + this.tooltipService.show(event.clientX, event.clientY, + `${d.x}, ${d.y}: ${d.value}`); + } + this.ngZone.run(() => this.cellHover.emit({ data: d, index: data.indexOf(d), event })); + }) + .on('mousemove', (event: MouseEvent) => this.tooltipService.update(event.clientX, event.clientY)) + .on('mouseleave', (event: MouseEvent) => { + this.tooltipService.hide(); + this.ngZone.run(() => this.cellHover.emit({ data: null, index: -1, event })); + }) + .on('click', (event: MouseEvent, d) => { + this.ngZone.run(() => this.cellClick.emit({ data: d, index: data.indexOf(d), event })); + }) + .transition() + .duration(duration) + .attr('x', d => xScale(String(d.x)) ?? 0) + .attr('y', d => yScale(String(d.y)) ?? 0) + .attr('width', xScale.bandwidth()) + .attr('height', yScale.bandwidth()) + .attr('fill', d => color(d.value)) + .attr('opacity', 1); + } + + private setupResize(): void { + if (this.width() === 'auto' && this.config.responsive) { + this.resizeCleanup = this.resizeService.observe( + this.chartRef().nativeElement, + () => this.updateChart(), + ); + } + } +} diff --git a/src/components/viz-histogram/index.ts b/src/components/viz-histogram/index.ts new file mode 100644 index 0000000..47f5f6e --- /dev/null +++ b/src/components/viz-histogram/index.ts @@ -0,0 +1 @@ +export * from './viz-histogram.component'; diff --git a/src/components/viz-histogram/viz-histogram.component.html b/src/components/viz-histogram/viz-histogram.component.html new file mode 100644 index 0000000..63fb52a --- /dev/null +++ b/src/components/viz-histogram/viz-histogram.component.html @@ -0,0 +1 @@ +
diff --git a/src/components/viz-histogram/viz-histogram.component.scss b/src/components/viz-histogram/viz-histogram.component.scss new file mode 100644 index 0000000..ccbd052 --- /dev/null +++ b/src/components/viz-histogram/viz-histogram.component.scss @@ -0,0 +1,27 @@ +:host { + display: block; + position: relative; + width: 100%; +} + +.viz-histogram { + width: 100%; + + ::ng-deep { + .viz-histogram-svg { + display: block; + overflow: visible; + } + + .domain { stroke: var(--viz-axis-color); } + .tick line { stroke: var(--viz-tick-color); } + .tick text { fill: var(--viz-text-muted); font-size: var(--viz-font-size-xs); } + .grid-line { stroke: var(--viz-grid-color); stroke-dasharray: 2, 2; } + + rect { + cursor: pointer; + transition: opacity var(--viz-transition); + &:hover { opacity: 0.8; } + } + } +} diff --git a/src/components/viz-histogram/viz-histogram.component.ts b/src/components/viz-histogram/viz-histogram.component.ts new file mode 100644 index 0000000..66d06d0 --- /dev/null +++ b/src/components/viz-histogram/viz-histogram.component.ts @@ -0,0 +1,171 @@ +import { + Component, ChangeDetectionStrategy, ElementRef, NgZone, OnDestroy, + inject, input, output, viewChild, effect, +} from '@angular/core'; +import { select, Selection } from 'd3-selection'; +import { scaleLinear } from 'd3-scale'; +import { axisBottom, axisLeft } from 'd3-axis'; +import { bin, max, extent } from 'd3-array'; +import 'd3-transition'; +import { VIZ_CONFIG } from '../../providers/viz-config.provider'; +import { VizThemeService } from '../../services/viz-theme.service'; +import { VizResizeService } from '../../services/viz-resize.service'; +import { VizTooltipService } from '../../services/viz-tooltip.service'; +import type { ChartMargin, AxisConfig } from '../../types/config.types'; +import type { ChartClickEvent } from '../../types/event.types'; + +@Component({ + selector: 'viz-histogram', + standalone: true, + templateUrl: './viz-histogram.component.html', + styleUrl: './viz-histogram.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizHistogramComponent implements OnDestroy { + readonly values = input.required(); + readonly width = input('auto'); + readonly height = input(300); + readonly margin = input({ top: 20, right: 20, bottom: 40, left: 50 }); + readonly xAxis = input({}); + readonly yAxis = input({ gridLines: true }); + readonly animate = input(undefined); + readonly bins = input(20); + readonly density = input(false); + readonly color = input(undefined); + + readonly binClick = output>(); + + private readonly chartRef = viewChild.required>('chart'); + + private readonly ngZone = inject(NgZone); + private readonly config = inject(VIZ_CONFIG); + private readonly themeService = inject(VizThemeService); + private readonly resizeService = inject(VizResizeService); + private readonly tooltipService = inject(VizTooltipService); + + private svg: Selection | null = null; + private resizeCleanup: (() => void) | null = null; + + constructor() { + effect(() => { + const _ = this.values(); + this.ngZone.runOutsideAngular(() => { + if (!this.svg) { + this.createChart(); + this.setupResize(); + } else { + this.updateChart(); + } + }); + }); + } + + ngOnDestroy(): void { + this.resizeCleanup?.(); + this.svg?.remove(); + } + + private getWidth(): number { + const w = this.width(); + return w === 'auto' ? (this.chartRef().nativeElement.clientWidth || 600) : w; + } + + private createChart(): void { + const el = this.chartRef().nativeElement; + this.svg = select(el).append('svg').attr('class', 'viz-histogram-svg'); + this.svg.append('g').attr('class', 'x-axis'); + this.svg.append('g').attr('class', 'y-axis'); + this.svg.append('g').attr('class', 'bars'); + this.updateChart(); + } + + private updateChart(): void { + if (!this.svg) return; + + const values = this.values(); + const w = this.getWidth(); + const h = this.height(); + const m = this.margin(); + const innerW = w - m.left - m.right; + const innerH = h - m.top - m.bottom; + const shouldAnimate = this.animate() ?? this.config.animate; + const duration = shouldAnimate ? this.config.animationDuration : 0; + const barColor = this.color() ?? this.themeService.getColor(0); + + this.svg.attr('width', w).attr('height', h); + + const [minVal, maxVal] = extent(values) as [number, number]; + const xScale = scaleLinear().domain([minVal, maxVal]).range([0, innerW]).nice(); + + const histogram = bin().domain(xScale.domain() as [number, number]).thresholds(this.bins()); + const binsData = histogram(values); + + const yScale = scaleLinear() + .domain([0, max(binsData, d => d.length) ?? 0]) + .range([innerH, 0]) + .nice(); + + this.svg.select('.x-axis') + .attr('transform', `translate(${m.left},${m.top + innerH})`) + .call(axisBottom(xScale)); + + const yAxisG = this.svg.select('.y-axis') + .attr('transform', `translate(${m.left},${m.top})`) + .call(axisLeft(yScale).ticks(this.yAxis().tickCount ?? 5)); + + if (this.yAxis().gridLines) { + yAxisG.selectAll('.grid-line').remove(); + yAxisG.selectAll('.tick line') + .clone() + .attr('x2', innerW) + .attr('stroke', 'var(--viz-grid-color, #f3f4f6)') + .attr('stroke-dasharray', '2,2') + .attr('class', 'grid-line'); + } + + const barsG = this.svg.select('.bars').attr('transform', `translate(${m.left},${m.top})`); + + const bars = barsG.selectAll('rect') + .data(binsData); + + bars.exit().transition().duration(duration).attr('height', 0).attr('y', innerH).remove(); + + bars.enter() + .append('rect') + .attr('y', innerH) + .attr('height', 0) + .attr('rx', 1) + .attr('fill', barColor) + .merge(bars) + .on('mouseenter', (event: MouseEvent, d) => { + if (this.config.tooltips) { + this.tooltipService.show(event.clientX, event.clientY, + `${d.x0?.toFixed(1)} – ${d.x1?.toFixed(1)}: ${d.length}`); + } + }) + .on('mousemove', (event: MouseEvent) => this.tooltipService.update(event.clientX, event.clientY)) + .on('mouseleave', () => this.tooltipService.hide()) + .on('click', (event: MouseEvent, d) => { + this.ngZone.run(() => this.binClick.emit({ + data: { x0: d.x0 ?? 0, x1: d.x1 ?? 0, count: d.length }, + index: binsData.indexOf(d), + event, + })); + }) + .transition() + .duration(duration) + .attr('x', d => xScale(d.x0 ?? 0)) + .attr('width', d => Math.max(0, xScale(d.x1 ?? 0) - xScale(d.x0 ?? 0) - 1)) + .attr('y', d => yScale(d.length)) + .attr('height', d => innerH - yScale(d.length)); + } + + private setupResize(): void { + if (this.width() === 'auto' && this.config.responsive) { + this.resizeCleanup = this.resizeService.observe( + this.chartRef().nativeElement, + () => this.updateChart(), + ); + } + } +} diff --git a/src/components/viz-legend/index.ts b/src/components/viz-legend/index.ts new file mode 100644 index 0000000..b8ce7c7 --- /dev/null +++ b/src/components/viz-legend/index.ts @@ -0,0 +1 @@ +export * from './viz-legend.component'; diff --git a/src/components/viz-legend/viz-legend.component.html b/src/components/viz-legend/viz-legend.component.html new file mode 100644 index 0000000..200396f --- /dev/null +++ b/src/components/viz-legend/viz-legend.component.html @@ -0,0 +1,18 @@ +
+ @for (item of items(); track item.label; let i = $index) { + + } +
diff --git a/src/components/viz-legend/viz-legend.component.scss b/src/components/viz-legend/viz-legend.component.scss new file mode 100644 index 0000000..cf629c2 --- /dev/null +++ b/src/components/viz-legend/viz-legend.component.scss @@ -0,0 +1,59 @@ +:host { + display: block; +} + +.viz-legend { + display: flex; + flex-wrap: wrap; + gap: var(--viz-spacing-md, 12px); + padding: var(--viz-spacing-sm) 0; + + &--top, + &--bottom { + flex-direction: row; + justify-content: flex-start; + } + + &--left, + &--right { + flex-direction: column; + align-items: flex-start; + } + + &__item { + display: inline-flex; + align-items: center; + gap: var(--viz-spacing-sm, 6px); + background: none; + border: none; + padding: 4px var(--viz-spacing-sm, 8px); + font-size: var(--viz-font-size-sm); + color: var(--viz-text); + border-radius: var(--viz-radius-sm); + transition: opacity var(--viz-transition), background-color var(--viz-transition); + + &--interactive { + cursor: pointer; + + &:hover { + background-color: var(--viz-grid-color); + } + } + + &--inactive { + opacity: 0.4; + } + } + + &__swatch { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 2px; + flex-shrink: 0; + } + + &__label { + white-space: nowrap; + } +} diff --git a/src/components/viz-legend/viz-legend.component.ts b/src/components/viz-legend/viz-legend.component.ts new file mode 100644 index 0000000..c7d2052 --- /dev/null +++ b/src/components/viz-legend/viz-legend.component.ts @@ -0,0 +1,43 @@ +import { + Component, ChangeDetectionStrategy, inject, input, output, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { VizThemeService } from '../../services/viz-theme.service'; + +export interface LegendItem { + label: string; + color?: string; + active?: boolean; +} + +@Component({ + selector: 'viz-legend', + standalone: true, + imports: [CommonModule], + templateUrl: './viz-legend.component.html', + styleUrl: './viz-legend.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizLegendComponent { + readonly items = input.required(); + readonly position = input<'top' | 'bottom' | 'left' | 'right'>('top'); + readonly interactive = input(true); + + readonly itemClick = output<{ item: LegendItem; index: number }>(); + + private readonly themeService = inject(VizThemeService); + + getColor(item: LegendItem, index: number): string { + return item.color ?? this.themeService.getColor(index); + } + + onItemClick(item: LegendItem, index: number): void { + if (this.interactive()) { + this.itemClick.emit({ item, index }); + } + } + + isActive(item: LegendItem): boolean { + return item.active !== false; + } +} diff --git a/src/components/viz-line-chart/index.ts b/src/components/viz-line-chart/index.ts new file mode 100644 index 0000000..105bf0a --- /dev/null +++ b/src/components/viz-line-chart/index.ts @@ -0,0 +1 @@ +export * from './viz-line-chart.component'; diff --git a/src/components/viz-line-chart/viz-line-chart.component.html b/src/components/viz-line-chart/viz-line-chart.component.html new file mode 100644 index 0000000..b8ec5f3 --- /dev/null +++ b/src/components/viz-line-chart/viz-line-chart.component.html @@ -0,0 +1,9 @@ +
+ @if (legend().visible && (legend().position === 'top' || legend().position === 'left')) { + + } +
+ @if (legend().visible && (legend().position === 'bottom' || legend().position === 'right')) { + + } +
diff --git a/src/components/viz-line-chart/viz-line-chart.component.scss b/src/components/viz-line-chart/viz-line-chart.component.scss new file mode 100644 index 0000000..05dfed4 --- /dev/null +++ b/src/components/viz-line-chart/viz-line-chart.component.scss @@ -0,0 +1,57 @@ +:host { + display: block; + position: relative; + width: 100%; +} + +.viz-line-chart { + display: flex; + flex-direction: column; + width: 100%; + + &--legend-left, + &--legend-right { + flex-direction: row; + } + + > div:not(.viz-legend) { + flex: 1; + min-width: 0; + } + + ::ng-deep { + .viz-line-chart-svg { + display: block; + overflow: visible; + } + + .domain { + stroke: var(--viz-axis-color); + } + + .tick line { + stroke: var(--viz-tick-color); + } + + .tick text { + fill: var(--viz-text-muted); + font-size: var(--viz-font-size-xs); + } + + .grid-line { + stroke: var(--viz-grid-color); + stroke-dasharray: 2, 2; + } + + circle { + cursor: pointer; + stroke: var(--viz-bg, #fff); + stroke-width: 2; + transition: r var(--viz-transition); + + &:hover { + r: 5; + } + } + } +} diff --git a/src/components/viz-line-chart/viz-line-chart.component.ts b/src/components/viz-line-chart/viz-line-chart.component.ts new file mode 100644 index 0000000..aa9ce4d --- /dev/null +++ b/src/components/viz-line-chart/viz-line-chart.component.ts @@ -0,0 +1,290 @@ +import { + Component, ChangeDetectionStrategy, ElementRef, NgZone, OnDestroy, + inject, input, output, viewChild, effect, computed, signal, +} from '@angular/core'; +import { select, Selection } from 'd3-selection'; +import { scaleLinear, scaleTime } from 'd3-scale'; +import { axisBottom, axisLeft } from 'd3-axis'; +import { line, area, curveBasis, curveCardinal, curveLinear, curveMonotoneX, curveStep, CurveFactory } from 'd3-shape'; +import { extent, max } from 'd3-array'; +import 'd3-transition'; +import { VIZ_CONFIG } from '../../providers/viz-config.provider'; +import { VizThemeService } from '../../services/viz-theme.service'; +import { VizResizeService } from '../../services/viz-resize.service'; +import { VizTooltipService } from '../../services/viz-tooltip.service'; +import type { CartesianDataPoint, ChartSeries } from '../../types/chart.types'; +import type { ChartMargin, AxisConfig, LegendConfig } from '../../types/config.types'; +import type { ChartClickEvent, ChartHoverEvent } from '../../types/event.types'; +import { VizLegendComponent, LegendItem } from '../viz-legend/viz-legend.component'; + +const CURVES: Record = { + linear: curveLinear, + basis: curveBasis, + cardinal: curveCardinal, + monotone: curveMonotoneX, + step: curveStep, +}; + +interface FlatDotPoint extends CartesianDataPoint { + seriesIndex: number; + seriesName: string; + seriesColor: string | undefined; +} + +@Component({ + selector: 'viz-line-chart', + standalone: true, + imports: [VizLegendComponent], + templateUrl: './viz-line-chart.component.html', + styleUrl: './viz-line-chart.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizLineChartComponent implements OnDestroy { + readonly series = input.required(); + readonly width = input('auto'); + readonly height = input(300); + readonly margin = input({ top: 20, right: 20, bottom: 40, left: 50 }); + readonly xAxis = input({}); + readonly yAxis = input({ gridLines: true }); + readonly legend = input({ visible: true, position: 'top' }); + readonly animate = input(undefined); + readonly curve = input<'linear' | 'basis' | 'cardinal' | 'monotone' | 'step'>('monotone'); + readonly showDots = input(true); + readonly areaFill = input(false); + + readonly pointClick = output>(); + readonly pointHover = output>(); + + readonly hiddenItems = signal>(new Set()); + + readonly legendItems = computed(() => + this.series().map((s, i) => ({ + label: s.name, + color: s.color ?? this.themeService.getColor(i), + active: !this.hiddenItems().has(s.name), + })) + ); + + private readonly chartRef = viewChild.required>('chart'); + + private readonly ngZone = inject(NgZone); + private readonly config = inject(VIZ_CONFIG); + private readonly themeService = inject(VizThemeService); + private readonly resizeService = inject(VizResizeService); + private readonly tooltipService = inject(VizTooltipService); + + private svg: Selection | null = null; + private resizeCleanup: (() => void) | null = null; + + constructor() { + effect(() => { + const _ = this.series(); + const __ = this.hiddenItems(); + this.ngZone.runOutsideAngular(() => { + if (!this.svg) { + this.createChart(); + this.setupResize(); + } else { + this.updateChart(); + } + }); + }); + } + + onLegendClick(event: { item: LegendItem; index: number }): void { + const hidden = new Set(this.hiddenItems()); + if (hidden.has(event.item.label)) { + hidden.delete(event.item.label); + } else { + hidden.add(event.item.label); + } + this.hiddenItems.set(hidden); + } + + ngOnDestroy(): void { + this.resizeCleanup?.(); + this.svg?.remove(); + } + + private getWidth(): number { + const w = this.width(); + return w === 'auto' ? (this.chartRef().nativeElement.clientWidth || 600) : w; + } + + private createChart(): void { + const el = this.chartRef().nativeElement; + this.svg = select(el) + .append('svg') + .attr('class', 'viz-line-chart-svg'); + + this.svg.append('g').attr('class', 'x-axis'); + this.svg.append('g').attr('class', 'y-axis'); + this.svg.append('g').attr('class', 'areas'); + this.svg.append('g').attr('class', 'lines'); + this.svg.append('g').attr('class', 'dots'); + + this.updateChart(); + } + + private updateChart(): void { + if (!this.svg) return; + + const allSeries = this.series().filter(s => s.visible !== false && !this.hiddenItems().has(s.name)); + const w = this.getWidth(); + const h = this.height(); + const m = this.margin(); + const innerW = w - m.left - m.right; + const innerH = h - m.top - m.bottom; + const shouldAnimate = this.animate() ?? this.config.animate; + const duration = shouldAnimate ? this.config.animationDuration : 0; + const yAxisConfig = this.yAxis(); + const curveFactory = CURVES[this.curve()] ?? curveMonotoneX; + + this.svg.attr('width', w).attr('height', h); + + // Compute domains across all series + const allPoints: CartesianDataPoint[] = []; + for (const s of allSeries) { + for (const d of s.data) { + allPoints.push(d); + } + } + const isTimeScale = allPoints.length > 0 && allPoints[0].x instanceof Date; + + const xScale = isTimeScale + ? scaleTime() + .domain(extent(allPoints, (d: CartesianDataPoint) => d.x as Date) as [Date, Date]) + .range([0, innerW]) + : scaleLinear() + .domain(extent(allPoints, (d: CartesianDataPoint) => d.x as number) as [number, number]) + .range([0, innerW]); + + const yDomainMax = yAxisConfig.max ?? (max(allPoints, (d: CartesianDataPoint) => d.y) ?? 0); + const yScale = scaleLinear() + .domain([yAxisConfig.min ?? 0, yDomainMax]) + .range([innerH, 0]) + .nice(); + + // Axes + this.svg.select('.x-axis') + .attr('transform', `translate(${m.left},${m.top + innerH})`) + .call(axisBottom(xScale as any).ticks(this.xAxis().tickCount ?? 6)); + + const yAxisG = this.svg.select('.y-axis') + .attr('transform', `translate(${m.left},${m.top})`) + .call(axisLeft(yScale).ticks(yAxisConfig.tickCount ?? 5)); + + if (yAxisConfig.gridLines) { + yAxisG.selectAll('.grid-line').remove(); + yAxisG.selectAll('.tick line') + .clone() + .attr('x2', innerW) + .attr('stroke', 'var(--viz-grid-color, #f3f4f6)') + .attr('stroke-dasharray', '2,2') + .attr('class', 'grid-line'); + } + + // Line generator + const lineGen = line() + .x(d => (xScale as any)(d.x)) + .y(d => yScale(d.y)) + .curve(curveFactory); + + // Area generator + const areaGen = area() + .x(d => (xScale as any)(d.x)) + .y0(innerH) + .y1(d => yScale(d.y)) + .curve(curveFactory); + + // Areas + if (this.areaFill()) { + const areas = this.svg.select('.areas') + .attr('transform', `translate(${m.left},${m.top})`) + .selectAll('path') + .data(allSeries, (d: ChartSeries) => d.name); + + areas.exit().remove(); + + areas.enter() + .append('path') + .merge(areas) + .transition() + .duration(duration) + .attr('d', (d: ChartSeries) => areaGen(d.data) ?? '') + .attr('fill', (d: ChartSeries, i: number) => d.color ?? this.themeService.getColor(i)) + .attr('opacity', 0.15); + } + + // Lines + const lines = this.svg.select('.lines') + .attr('transform', `translate(${m.left},${m.top})`) + .selectAll('path') + .data(allSeries, (d: ChartSeries) => d.name); + + lines.exit().remove(); + + lines.enter() + .append('path') + .attr('fill', 'none') + .attr('stroke-width', 2) + .merge(lines) + .transition() + .duration(duration) + .attr('d', (d: ChartSeries) => lineGen(d.data) ?? '') + .attr('stroke', (d: ChartSeries, i: number) => d.color ?? this.themeService.getColor(i)); + + // Dots + if (this.showDots()) { + const dotsG = this.svg.select('.dots') + .attr('transform', `translate(${m.left},${m.top})`); + + const flatData: FlatDotPoint[] = []; + allSeries.forEach((s, si) => { + for (const d of s.data) { + flatData.push({ ...d, seriesIndex: si, seriesName: s.name, seriesColor: s.color }); + } + }); + + const dots = dotsG.selectAll('circle') + .data(flatData); + + dots.exit().remove(); + + dots.enter() + .append('circle') + .attr('r', 3) + .merge(dots) + .on('mouseenter', (event: MouseEvent, d: FlatDotPoint) => { + if (this.config.tooltips) { + const xLabel = d.x instanceof Date ? d.x.toLocaleDateString() : d.x; + this.tooltipService.show(event.clientX, event.clientY, + `${d.seriesName}
${xLabel}: ${d.y}`); + } + this.ngZone.run(() => this.pointHover.emit({ data: d, index: 0, event })); + }) + .on('mousemove', (event: MouseEvent) => this.tooltipService.update(event.clientX, event.clientY)) + .on('mouseleave', (event: MouseEvent) => { + this.tooltipService.hide(); + this.ngZone.run(() => this.pointHover.emit({ data: null, index: -1, event })); + }) + .on('click', (event: MouseEvent, d: FlatDotPoint) => { + this.ngZone.run(() => this.pointClick.emit({ data: d, index: 0, event })); + }) + .transition() + .duration(duration) + .attr('cx', (d: FlatDotPoint) => (xScale as any)(d.x)) + .attr('cy', (d: FlatDotPoint) => yScale(d.y)) + .attr('fill', (d: FlatDotPoint) => d.seriesColor ?? this.themeService.getColor(d.seriesIndex)); + } + } + + private setupResize(): void { + if (this.width() === 'auto' && this.config.responsive) { + this.resizeCleanup = this.resizeService.observe( + this.chartRef().nativeElement, + () => this.updateChart(), + ); + } + } +} diff --git a/src/components/viz-pie-chart/index.ts b/src/components/viz-pie-chart/index.ts new file mode 100644 index 0000000..71cc33c --- /dev/null +++ b/src/components/viz-pie-chart/index.ts @@ -0,0 +1 @@ +export * from './viz-pie-chart.component'; diff --git a/src/components/viz-pie-chart/viz-pie-chart.component.html b/src/components/viz-pie-chart/viz-pie-chart.component.html new file mode 100644 index 0000000..7066a79 --- /dev/null +++ b/src/components/viz-pie-chart/viz-pie-chart.component.html @@ -0,0 +1,9 @@ +
+ @if (legend().visible && (legend().position === 'top' || legend().position === 'left')) { + + } +
+ @if (legend().visible && (legend().position === 'bottom' || legend().position === 'right')) { + + } +
diff --git a/src/components/viz-pie-chart/viz-pie-chart.component.scss b/src/components/viz-pie-chart/viz-pie-chart.component.scss new file mode 100644 index 0000000..b952c9e --- /dev/null +++ b/src/components/viz-pie-chart/viz-pie-chart.component.scss @@ -0,0 +1,39 @@ +:host { + display: block; + position: relative; + width: 100%; +} + +.viz-pie-chart { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + + &--legend-left, + &--legend-right { + flex-direction: row; + align-items: center; + } + + > div:not(.viz-legend) { + flex: 1; + min-width: 0; + } + + ::ng-deep { + .viz-pie-chart-svg { + display: block; + overflow: visible; + } + + path { + cursor: pointer; + transition: filter var(--viz-transition); + + &:hover { + filter: brightness(1.05); + } + } + } +} diff --git a/src/components/viz-pie-chart/viz-pie-chart.component.ts b/src/components/viz-pie-chart/viz-pie-chart.component.ts new file mode 100644 index 0000000..4280899 --- /dev/null +++ b/src/components/viz-pie-chart/viz-pie-chart.component.ts @@ -0,0 +1,231 @@ +import { + Component, ChangeDetectionStrategy, ElementRef, NgZone, OnDestroy, + inject, input, output, viewChild, effect, computed, signal, +} from '@angular/core'; +import { select, Selection } from 'd3-selection'; +import { pie, arc, PieArcDatum } from 'd3-shape'; +import { interpolate } from 'd3-interpolate'; +import 'd3-transition'; +import { VIZ_CONFIG } from '../../providers/viz-config.provider'; +import { VizThemeService } from '../../services/viz-theme.service'; +import { VizResizeService } from '../../services/viz-resize.service'; +import { VizTooltipService } from '../../services/viz-tooltip.service'; +import type { ChartDataPoint } from '../../types/chart.types'; +import type { ChartMargin, LegendConfig } from '../../types/config.types'; +import type { ChartClickEvent, ChartHoverEvent } from '../../types/event.types'; +import { VizLegendComponent, LegendItem } from '../viz-legend/viz-legend.component'; + +@Component({ + selector: 'viz-pie-chart', + standalone: true, + imports: [VizLegendComponent], + templateUrl: './viz-pie-chart.component.html', + styleUrl: './viz-pie-chart.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizPieChartComponent implements OnDestroy { + readonly data = input.required(); + readonly width = input('auto'); + readonly height = input(300); + readonly margin = input({ top: 20, right: 20, bottom: 20, left: 20 }); + readonly legend = input({ visible: true, position: 'right' }); + readonly animate = input(undefined); + readonly donut = input(false); + readonly innerRadius = input(0.6); + readonly labels = input(true); + + readonly sliceClick = output>(); + readonly sliceHover = output>(); + + readonly hiddenItems = signal>(new Set()); + + readonly legendItems = computed(() => + this.data().map((d, i) => ({ + label: d.label, + color: d.color ?? this.themeService.getColor(i), + active: !this.hiddenItems().has(d.label), + })) + ); + + private readonly chartRef = viewChild.required>('chart'); + + private readonly ngZone = inject(NgZone); + private readonly config = inject(VIZ_CONFIG); + private readonly themeService = inject(VizThemeService); + private readonly resizeService = inject(VizResizeService); + private readonly tooltipService = inject(VizTooltipService); + + private svg: Selection | null = null; + private resizeCleanup: (() => void) | null = null; + + constructor() { + effect(() => { + const _ = this.data(); + const __ = this.hiddenItems(); + this.ngZone.runOutsideAngular(() => { + if (!this.svg) { + this.createChart(); + this.setupResize(); + } else { + this.updateChart(); + } + }); + }); + } + + onLegendClick(event: { item: LegendItem; index: number }): void { + const hidden = new Set(this.hiddenItems()); + if (hidden.has(event.item.label)) { + hidden.delete(event.item.label); + } else { + hidden.add(event.item.label); + } + this.hiddenItems.set(hidden); + } + + ngOnDestroy(): void { + this.resizeCleanup?.(); + this.svg?.remove(); + } + + private getWidth(): number { + const w = this.width(); + return w === 'auto' ? (this.chartRef().nativeElement.clientWidth || 400) : w; + } + + private createChart(): void { + const el = this.chartRef().nativeElement; + this.svg = select(el) + .append('svg') + .attr('class', 'viz-pie-chart-svg'); + + this.svg.append('g').attr('class', 'slices'); + this.svg.append('g').attr('class', 'labels'); + + this.updateChart(); + } + + private updateChart(): void { + if (!this.svg) return; + + const data = this.data().filter(d => !this.hiddenItems().has(d.label)); + const w = this.getWidth(); + const h = this.height(); + const m = this.margin(); + const shouldAnimate = this.animate() ?? this.config.animate; + const duration = shouldAnimate ? this.config.animationDuration : 0; + + const radius = Math.min(w - m.left - m.right, h - m.top - m.bottom) / 2; + const inner = this.donut() ? radius * this.innerRadius() : 0; + + this.svg.attr('width', w).attr('height', h); + + const pieGen = pie() + .value(d => d.value) + .sort(null); + + const arcGen = arc>() + .innerRadius(inner) + .outerRadius(radius); + + const arcHover = arc>() + .innerRadius(inner) + .outerRadius(radius + 6); + + const labelArc = arc>() + .innerRadius(radius * 0.7) + .outerRadius(radius * 0.7); + + const pieData = pieGen(data); + const centerX = w / 2; + const centerY = h / 2; + + // Slices + const slicesG = this.svg.select('.slices') + .attr('transform', `translate(${centerX},${centerY})`); + + const slices = slicesG.selectAll>('path') + .data(pieData, d => d.data.label); + + slices.exit().transition().duration(duration).attrTween('d', function(datum) { + const d = datum as PieArcDatum; + const end = { ...d, startAngle: d.endAngle }; + const i = interpolate(d, end); + return (t: number) => arcGen(i(t)) ?? ''; + }).remove(); + + const enter = slices.enter() + .append('path') + .attr('fill', (d, i) => d.data.color ?? this.themeService.getColor(i)) + .attr('stroke', 'var(--viz-bg, #fff)') + .attr('stroke-width', 2); + + enter.merge(slices) + .on('mouseenter', (event: MouseEvent, d) => { + select(event.currentTarget as SVGPathElement) + .transition().duration(100) + .attr('d', arcHover(d) ?? ''); + if (this.config.tooltips) { + const total = data.reduce((sum, dp) => sum + dp.value, 0); + const pct = ((d.data.value / total) * 100).toFixed(1); + this.tooltipService.show(event.clientX, event.clientY, + `${d.data.label}: ${d.data.value} (${pct}%)`); + } + this.ngZone.run(() => this.sliceHover.emit({ data: d.data, index: d.index, event })); + }) + .on('mousemove', (event: MouseEvent) => this.tooltipService.update(event.clientX, event.clientY)) + .on('mouseleave', (event: MouseEvent, d) => { + select(event.currentTarget as SVGPathElement) + .transition().duration(100) + .attr('d', arcGen(d) ?? ''); + this.tooltipService.hide(); + this.ngZone.run(() => this.sliceHover.emit({ data: null, index: -1, event })); + }) + .on('click', (event: MouseEvent, d) => { + this.ngZone.run(() => this.sliceClick.emit({ data: d.data, index: d.index, event })); + }) + .transition() + .duration(duration) + .attrTween('d', function(d) { + const current = (this as any)._current ?? { startAngle: 0, endAngle: 0 }; + const i = interpolate(current, d); + (this as any)._current = d; + return (t: number) => arcGen(i(t)) ?? ''; + }) + .attr('fill', (d, i) => d.data.color ?? this.themeService.getColor(i)); + + // Labels + if (this.labels()) { + const labelsG = this.svg.select('.labels') + .attr('transform', `translate(${centerX},${centerY})`); + + const labelSel = labelsG.selectAll>('text') + .data(pieData, d => d.data.label); + + labelSel.exit().remove(); + + labelSel.enter() + .append('text') + .attr('text-anchor', 'middle') + .attr('font-size', 'var(--viz-font-size-xs, 12px)') + .attr('fill', 'var(--viz-text, #111827)') + .merge(labelSel) + .transition() + .duration(duration) + .attr('transform', d => `translate(${labelArc.centroid(d)})`) + .text(d => { + const angle = d.endAngle - d.startAngle; + return angle > 0.3 ? d.data.label : ''; + }); + } + } + + private setupResize(): void { + if (this.width() === 'auto' && this.config.responsive) { + this.resizeCleanup = this.resizeService.observe( + this.chartRef().nativeElement, + () => this.updateChart(), + ); + } + } +} diff --git a/src/components/viz-progress-bar/index.ts b/src/components/viz-progress-bar/index.ts new file mode 100644 index 0000000..b34bba5 --- /dev/null +++ b/src/components/viz-progress-bar/index.ts @@ -0,0 +1 @@ +export * from './viz-progress-bar.component'; diff --git a/src/components/viz-progress-bar/viz-progress-bar.component.html b/src/components/viz-progress-bar/viz-progress-bar.component.html new file mode 100644 index 0000000..d04d128 --- /dev/null +++ b/src/components/viz-progress-bar/viz-progress-bar.component.html @@ -0,0 +1,24 @@ +
+ @if (showLabel()) { +
+ {{ percentage() | number:'1.0-0' }}% +
+ } +
+ @if (computedSegments()) { + @for (seg of computedSegments(); track seg.label ?? $index) { +
+ } + } @else { +
+ } +
+
diff --git a/src/components/viz-progress-bar/viz-progress-bar.component.scss b/src/components/viz-progress-bar/viz-progress-bar.component.scss new file mode 100644 index 0000000..2bc50b6 --- /dev/null +++ b/src/components/viz-progress-bar/viz-progress-bar.component.scss @@ -0,0 +1,49 @@ +:host { + display: block; + width: 100%; +} + +.viz-progress-bar { + &__header { + display: flex; + justify-content: flex-end; + margin-bottom: var(--viz-spacing-xs); + } + + &__percentage { + font-size: var(--viz-font-size-sm); + font-weight: 600; + color: var(--viz-text); + } + + &__track { + width: 100%; + background: var(--viz-grid-color, #f3f4f6); + border-radius: 999px; + overflow: hidden; + display: flex; + } + + &__fill { + height: 100%; + border-radius: 999px; + transition: width var(--viz-transition); + } + + &__segment { + height: 100%; + transition: width var(--viz-transition); + + &:first-child { + border-radius: 999px 0 0 999px; + } + + &:last-child { + border-radius: 0 999px 999px 0; + } + + &:only-child { + border-radius: 999px; + } + } +} diff --git a/src/components/viz-progress-bar/viz-progress-bar.component.ts b/src/components/viz-progress-bar/viz-progress-bar.component.ts new file mode 100644 index 0000000..d4563b0 --- /dev/null +++ b/src/components/viz-progress-bar/viz-progress-bar.component.ts @@ -0,0 +1,48 @@ +import { + Component, ChangeDetectionStrategy, input, computed, inject, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { VizThemeService } from '../../services/viz-theme.service'; + +export interface ProgressSegment { + value: number; + color?: string; + label?: string; +} + +@Component({ + selector: 'viz-progress-bar', + standalone: true, + imports: [CommonModule], + templateUrl: './viz-progress-bar.component.html', + styleUrl: './viz-progress-bar.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizProgressBarComponent { + readonly value = input(0); + readonly max = input(100); + readonly segments = input(undefined); + readonly showLabel = input(true); + readonly color = input(undefined); + readonly height = input(8); + + private readonly themeService = inject(VizThemeService); + + readonly percentage = computed(() => { + return Math.min(100, Math.max(0, (this.value() / this.max()) * 100)); + }); + + readonly barColor = computed(() => this.color() ?? this.themeService.getColor(0)); + + readonly computedSegments = computed(() => { + const segs = this.segments(); + if (!segs) return null; + + const total = segs.reduce((sum, s) => sum + s.value, 0); + return segs.map((s, i) => ({ + ...s, + width: (s.value / total) * 100, + color: s.color ?? this.themeService.getColor(i), + })); + }); +} diff --git a/src/components/viz-progress-ring/index.ts b/src/components/viz-progress-ring/index.ts new file mode 100644 index 0000000..7302d3e --- /dev/null +++ b/src/components/viz-progress-ring/index.ts @@ -0,0 +1 @@ +export * from './viz-progress-ring.component'; diff --git a/src/components/viz-progress-ring/viz-progress-ring.component.html b/src/components/viz-progress-ring/viz-progress-ring.component.html new file mode 100644 index 0000000..b1ede06 --- /dev/null +++ b/src/components/viz-progress-ring/viz-progress-ring.component.html @@ -0,0 +1,33 @@ +
+ + + + + @if (showValue()) { + {{ displayValue() }} + } +
diff --git a/src/components/viz-progress-ring/viz-progress-ring.component.scss b/src/components/viz-progress-ring/viz-progress-ring.component.scss new file mode 100644 index 0000000..3ae6579 --- /dev/null +++ b/src/components/viz-progress-ring/viz-progress-ring.component.scss @@ -0,0 +1,25 @@ +:host { + display: inline-block; +} + +.viz-progress-ring { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + + svg { + display: block; + } + + &__value { + transition: stroke-dashoffset var(--viz-transition); + } + + &__label { + position: absolute; + font-size: var(--viz-font-size-sm); + font-weight: 600; + color: var(--viz-text); + } +} diff --git a/src/components/viz-progress-ring/viz-progress-ring.component.ts b/src/components/viz-progress-ring/viz-progress-ring.component.ts new file mode 100644 index 0000000..61b2021 --- /dev/null +++ b/src/components/viz-progress-ring/viz-progress-ring.component.ts @@ -0,0 +1,38 @@ +import { + Component, ChangeDetectionStrategy, input, computed, inject, +} from '@angular/core'; +import { VizThemeService } from '../../services/viz-theme.service'; + +@Component({ + selector: 'viz-progress-ring', + standalone: true, + templateUrl: './viz-progress-ring.component.html', + styleUrl: './viz-progress-ring.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizProgressRingComponent { + readonly value = input.required(); + readonly max = input(100); + readonly size = input(80); + readonly thickness = input(8); + readonly color = input(undefined); + readonly showValue = input(true); + readonly format = input<(v: number, max: number) => string>((v, max) => `${Math.round((v / max) * 100)}%`); + + private readonly themeService = inject(VizThemeService); + + readonly radius = computed(() => (this.size() - this.thickness()) / 2); + readonly circumference = computed(() => 2 * Math.PI * this.radius()); + + readonly dashOffset = computed(() => { + const pct = Math.min(1, Math.max(0, this.value() / this.max())); + return this.circumference() * (1 - pct); + }); + + readonly strokeColor = computed(() => this.color() ?? this.themeService.getColor(0)); + + readonly displayValue = computed(() => this.format()(this.value(), this.max())); + + readonly viewBox = computed(() => `0 0 ${this.size()} ${this.size()}`); + readonly center = computed(() => this.size() / 2); +} diff --git a/src/components/viz-scatter-chart/index.ts b/src/components/viz-scatter-chart/index.ts new file mode 100644 index 0000000..a45d59d --- /dev/null +++ b/src/components/viz-scatter-chart/index.ts @@ -0,0 +1 @@ +export * from './viz-scatter-chart.component'; diff --git a/src/components/viz-scatter-chart/viz-scatter-chart.component.html b/src/components/viz-scatter-chart/viz-scatter-chart.component.html new file mode 100644 index 0000000..d200508 --- /dev/null +++ b/src/components/viz-scatter-chart/viz-scatter-chart.component.html @@ -0,0 +1,9 @@ +
+ @if (legend().visible && (legend().position === 'top' || legend().position === 'left')) { + + } +
+ @if (legend().visible && (legend().position === 'bottom' || legend().position === 'right')) { + + } +
diff --git a/src/components/viz-scatter-chart/viz-scatter-chart.component.scss b/src/components/viz-scatter-chart/viz-scatter-chart.component.scss new file mode 100644 index 0000000..91929d6 --- /dev/null +++ b/src/components/viz-scatter-chart/viz-scatter-chart.component.scss @@ -0,0 +1,55 @@ +:host { + display: block; + position: relative; + width: 100%; +} + +.viz-scatter-chart { + display: flex; + flex-direction: column; + width: 100%; + + &--legend-left, + &--legend-right { + flex-direction: row; + } + + > div:not(.viz-legend) { + flex: 1; + min-width: 0; + } + + ::ng-deep { + .viz-scatter-chart-svg { + display: block; + overflow: visible; + } + + .domain { + stroke: var(--viz-axis-color); + } + + .tick line { + stroke: var(--viz-tick-color); + } + + .tick text { + fill: var(--viz-text-muted); + font-size: var(--viz-font-size-xs); + } + + .grid-line { + stroke: var(--viz-grid-color); + stroke-dasharray: 2, 2; + } + + circle { + cursor: pointer; + transition: opacity var(--viz-transition); + + &:hover { + opacity: 1; + } + } + } +} diff --git a/src/components/viz-scatter-chart/viz-scatter-chart.component.ts b/src/components/viz-scatter-chart/viz-scatter-chart.component.ts new file mode 100644 index 0000000..9717dbb --- /dev/null +++ b/src/components/viz-scatter-chart/viz-scatter-chart.component.ts @@ -0,0 +1,222 @@ +import { + Component, ChangeDetectionStrategy, ElementRef, NgZone, OnDestroy, + inject, input, output, viewChild, effect, computed, signal, +} from '@angular/core'; +import { select, Selection } from 'd3-selection'; +import { scaleLinear, scaleSqrt } from 'd3-scale'; +import { axisBottom, axisLeft } from 'd3-axis'; +import { extent, max } from 'd3-array'; +import 'd3-transition'; +import { VIZ_CONFIG } from '../../providers/viz-config.provider'; +import { VizThemeService } from '../../services/viz-theme.service'; +import { VizResizeService } from '../../services/viz-resize.service'; +import { VizTooltipService } from '../../services/viz-tooltip.service'; +import type { ScatterDataPoint, ChartSeries } from '../../types/chart.types'; +import type { ChartMargin, AxisConfig, LegendConfig } from '../../types/config.types'; +import type { ChartClickEvent, ChartHoverEvent } from '../../types/event.types'; +import { VizLegendComponent, LegendItem } from '../viz-legend/viz-legend.component'; + +@Component({ + selector: 'viz-scatter-chart', + standalone: true, + imports: [VizLegendComponent], + templateUrl: './viz-scatter-chart.component.html', + styleUrl: './viz-scatter-chart.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizScatterChartComponent implements OnDestroy { + readonly series = input.required[]>(); + readonly width = input('auto'); + readonly height = input(300); + readonly margin = input({ top: 20, right: 20, bottom: 40, left: 50 }); + readonly xAxis = input({}); + readonly yAxis = input({ gridLines: true }); + readonly legend = input({ visible: true, position: 'top' }); + readonly animate = input(undefined); + readonly bubbleSize = input<[number, number]>([4, 30]); + + readonly pointClick = output>(); + readonly pointHover = output>(); + + readonly hiddenItems = signal>(new Set()); + + readonly legendItems = computed(() => + this.series().map((s, i) => ({ + label: s.name, + color: s.color ?? this.themeService.getColor(i), + active: !this.hiddenItems().has(s.name), + })) + ); + + private readonly chartRef = viewChild.required>('chart'); + + private readonly ngZone = inject(NgZone); + private readonly config = inject(VIZ_CONFIG); + private readonly themeService = inject(VizThemeService); + private readonly resizeService = inject(VizResizeService); + private readonly tooltipService = inject(VizTooltipService); + + private svg: Selection | null = null; + private resizeCleanup: (() => void) | null = null; + + constructor() { + effect(() => { + const _ = this.series(); + const __ = this.hiddenItems(); + this.ngZone.runOutsideAngular(() => { + if (!this.svg) { + this.createChart(); + this.setupResize(); + } else { + this.updateChart(); + } + }); + }); + } + + onLegendClick(event: { item: LegendItem; index: number }): void { + const hidden = new Set(this.hiddenItems()); + if (hidden.has(event.item.label)) { + hidden.delete(event.item.label); + } else { + hidden.add(event.item.label); + } + this.hiddenItems.set(hidden); + } + + ngOnDestroy(): void { + this.resizeCleanup?.(); + this.svg?.remove(); + } + + private getWidth(): number { + const w = this.width(); + return w === 'auto' ? (this.chartRef().nativeElement.clientWidth || 600) : w; + } + + private createChart(): void { + const el = this.chartRef().nativeElement; + this.svg = select(el) + .append('svg') + .attr('class', 'viz-scatter-chart-svg'); + + this.svg.append('g').attr('class', 'x-axis'); + this.svg.append('g').attr('class', 'y-axis'); + this.svg.append('g').attr('class', 'points'); + + this.updateChart(); + } + + private updateChart(): void { + if (!this.svg) return; + + const allSeries = this.series().filter(s => s.visible !== false && !this.hiddenItems().has(s.name)); + const w = this.getWidth(); + const h = this.height(); + const m = this.margin(); + const innerW = w - m.left - m.right; + const innerH = h - m.top - m.bottom; + const shouldAnimate = this.animate() ?? this.config.animate; + const duration = shouldAnimate ? this.config.animationDuration : 0; + const xAxisConfig = this.xAxis(); + const yAxisConfig = this.yAxis(); + const [minSize, maxSize] = this.bubbleSize(); + + this.svg.attr('width', w).attr('height', h); + + const allPoints: ScatterDataPoint[] = []; + for (const s of allSeries) { for (const d of s.data) { allPoints.push(d); } } + + const xScale = scaleLinear() + .domain(extent(allPoints, (d: ScatterDataPoint) => d.x as number) as [number, number]) + .range([0, innerW]) + .nice(); + + const yExtent = extent(allPoints, (d: ScatterDataPoint) => d.y); + const yScale = scaleLinear() + .domain([ + yAxisConfig.min ?? (yExtent[0] ?? 0), + yAxisConfig.max ?? (yExtent[1] ?? 0), + ]) + .range([innerH, 0]) + .nice(); + + const hasSizes = allPoints.some((d: ScatterDataPoint) => d.size != null); + const sizeScale = hasSizes + ? scaleSqrt() + .domain([0, max(allPoints, (d: ScatterDataPoint) => d.size ?? 0) ?? 1]) + .range([minSize, maxSize]) + : null; + + // Axes + this.svg.select('.x-axis') + .attr('transform', `translate(${m.left},${m.top + innerH})`) + .call(axisBottom(xScale).ticks(xAxisConfig.tickCount ?? 6)); + + const yAxisG = this.svg.select('.y-axis') + .attr('transform', `translate(${m.left},${m.top})`) + .call(axisLeft(yScale).ticks(yAxisConfig.tickCount ?? 5)); + + if (yAxisConfig.gridLines) { + yAxisG.selectAll('.grid-line').remove(); + yAxisG.selectAll('.tick line') + .clone() + .attr('x2', innerW) + .attr('stroke', 'var(--viz-grid-color, #f3f4f6)') + .attr('stroke-dasharray', '2,2') + .attr('class', 'grid-line'); + } + + // Points + const pointsG = this.svg.select('.points').attr('transform', `translate(${m.left},${m.top})`); + + const flatData: Array = []; + allSeries.forEach((s, si) => { + for (const d of s.data) { + flatData.push({ ...d, seriesIndex: si, seriesColor: s.color }); + } + }); + + const circles = pointsG.selectAll('circle') + .data(flatData); + + circles.exit().transition().duration(duration).attr('r', 0).remove(); + + circles.enter() + .append('circle') + .attr('r', 0) + .attr('opacity', 0.7) + .merge(circles) + .on('mouseenter', (event: MouseEvent, d) => { + if (this.config.tooltips) { + const label = d.label ?? `(${d.x}, ${d.y})`; + const sizeText = d.size != null ? ` | Size: ${d.size}` : ''; + this.tooltipService.show(event.clientX, event.clientY, `${label}${sizeText}`); + } + this.ngZone.run(() => this.pointHover.emit({ data: d, index: 0, event })); + }) + .on('mousemove', (event: MouseEvent) => this.tooltipService.update(event.clientX, event.clientY)) + .on('mouseleave', (event: MouseEvent) => { + this.tooltipService.hide(); + this.ngZone.run(() => this.pointHover.emit({ data: null, index: -1, event })); + }) + .on('click', (event: MouseEvent, d) => { + this.ngZone.run(() => this.pointClick.emit({ data: d, index: 0, event })); + }) + .transition() + .duration(duration) + .attr('cx', d => xScale(d.x as number)) + .attr('cy', d => yScale(d.y)) + .attr('r', d => sizeScale ? sizeScale(d.size ?? 0) : minSize) + .attr('fill', d => d.seriesColor ?? this.themeService.getColor(d.seriesIndex)); + } + + private setupResize(): void { + if (this.width() === 'auto' && this.config.responsive) { + this.resizeCleanup = this.resizeService.observe( + this.chartRef().nativeElement, + () => this.updateChart(), + ); + } + } +} diff --git a/src/components/viz-sparkline/index.ts b/src/components/viz-sparkline/index.ts new file mode 100644 index 0000000..2a7b43e --- /dev/null +++ b/src/components/viz-sparkline/index.ts @@ -0,0 +1 @@ +export * from './viz-sparkline.component'; diff --git a/src/components/viz-sparkline/viz-sparkline.component.html b/src/components/viz-sparkline/viz-sparkline.component.html new file mode 100644 index 0000000..9ef3ebc --- /dev/null +++ b/src/components/viz-sparkline/viz-sparkline.component.html @@ -0,0 +1 @@ + diff --git a/src/components/viz-sparkline/viz-sparkline.component.scss b/src/components/viz-sparkline/viz-sparkline.component.scss new file mode 100644 index 0000000..60fb9bb --- /dev/null +++ b/src/components/viz-sparkline/viz-sparkline.component.scss @@ -0,0 +1,14 @@ +:host { + display: inline-block; + vertical-align: middle; +} + +.viz-sparkline { + display: inline-block; + + ::ng-deep { + .viz-sparkline-svg { + display: block; + } + } +} diff --git a/src/components/viz-sparkline/viz-sparkline.component.ts b/src/components/viz-sparkline/viz-sparkline.component.ts new file mode 100644 index 0000000..29cc865 --- /dev/null +++ b/src/components/viz-sparkline/viz-sparkline.component.ts @@ -0,0 +1,132 @@ +import { + Component, ChangeDetectionStrategy, ElementRef, NgZone, OnDestroy, + inject, input, viewChild, effect, +} from '@angular/core'; +import { select, Selection } from 'd3-selection'; +import { scaleLinear } from 'd3-scale'; +import { line, area, curveMonotoneX } from 'd3-shape'; +import { extent } from 'd3-array'; +import 'd3-transition'; +import { VIZ_CONFIG } from '../../providers/viz-config.provider'; +import { VizThemeService } from '../../services/viz-theme.service'; +import { withOpacity } from '../../utils/color.utils'; + +@Component({ + selector: 'viz-sparkline', + standalone: true, + templateUrl: './viz-sparkline.component.html', + styleUrl: './viz-sparkline.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizSparklineComponent implements OnDestroy { + readonly data = input.required(); + readonly width = input(120); + readonly height = input(32); + readonly type = input<'line' | 'bar' | 'area'>('line'); + readonly color = input(undefined); + readonly showLastValue = input(false); + readonly animate = input(undefined); + + private readonly chartRef = viewChild.required>('chart'); + + private readonly ngZone = inject(NgZone); + private readonly config = inject(VIZ_CONFIG); + private readonly themeService = inject(VizThemeService); + + private svg: Selection | null = null; + + constructor() { + effect(() => { + const _ = this.data(); + this.ngZone.runOutsideAngular(() => { + if (!this.svg) { + this.createChart(); + } else { + this.updateChart(); + } + }); + }); + } + + ngOnDestroy(): void { + this.svg?.remove(); + } + + private createChart(): void { + const el = this.chartRef().nativeElement; + this.svg = select(el).append('svg') + .attr('class', 'viz-sparkline-svg') + .attr('width', this.width()) + .attr('height', this.height()); + this.updateChart(); + } + + private updateChart(): void { + if (!this.svg) return; + + const data = this.data(); + const w = this.width(); + const h = this.height(); + const sparkColor = this.color() ?? this.themeService.getColor(0); + const shouldAnimate = this.animate() ?? this.config.animate; + const duration = shouldAnimate ? this.config.animationDuration : 0; + + this.svg.attr('width', w).attr('height', h); + this.svg.selectAll('*').remove(); + + if (data.length === 0) return; + + const xScale = scaleLinear().domain([0, data.length - 1]).range([2, w - 2]); + const [minVal, maxVal] = extent(data) as [number, number]; + const yScale = scaleLinear().domain([minVal, maxVal]).range([h - 2, 2]); + + const type = this.type(); + + if (type === 'bar') { + const barW = Math.max(1, (w - 4) / data.length - 1); + this.svg.selectAll('rect') + .data(data) + .enter() + .append('rect') + .attr('x', (_, i) => xScale(i) - barW / 2) + .attr('y', d => yScale(d)) + .attr('width', barW) + .attr('height', d => h - 2 - yScale(d)) + .attr('fill', sparkColor) + .attr('rx', 0.5); + } else { + if (type === 'area') { + const areaGen = area() + .x((_, i) => xScale(i)) + .y0(h) + .y1(d => yScale(d)) + .curve(curveMonotoneX); + + this.svg.append('path') + .attr('d', areaGen(data) ?? '') + .attr('fill', withOpacity(sparkColor, 0.15)); + } + + const lineGen = line() + .x((_, i) => xScale(i)) + .y(d => yScale(d)) + .curve(curveMonotoneX); + + this.svg.append('path') + .attr('d', lineGen(data) ?? '') + .attr('fill', 'none') + .attr('stroke', sparkColor) + .attr('stroke-width', 1.5); + + // Last point dot + if (this.showLastValue() && data.length > 0) { + const lastIdx = data.length - 1; + this.svg.append('circle') + .attr('cx', xScale(lastIdx)) + .attr('cy', yScale(data[lastIdx])) + .attr('r', 2.5) + .attr('fill', sparkColor); + } + } + } +} diff --git a/src/components/viz-stat-card/index.ts b/src/components/viz-stat-card/index.ts new file mode 100644 index 0000000..3cd4dad --- /dev/null +++ b/src/components/viz-stat-card/index.ts @@ -0,0 +1 @@ +export * from './viz-stat-card.component'; diff --git a/src/components/viz-stat-card/viz-stat-card.component.html b/src/components/viz-stat-card/viz-stat-card.component.html new file mode 100644 index 0000000..9755284 --- /dev/null +++ b/src/components/viz-stat-card/viz-stat-card.component.html @@ -0,0 +1,17 @@ +
+ @if (icon()) { +
{{ icon() }}
+ } +
+ {{ label() }} + {{ displayValue() }} + @if (trend()) { + + {{ trendIcon() }} + @if (trendValue()) { + {{ trendValue() }} + } + + } +
+
diff --git a/src/components/viz-stat-card/viz-stat-card.component.scss b/src/components/viz-stat-card/viz-stat-card.component.scss new file mode 100644 index 0000000..a2774a7 --- /dev/null +++ b/src/components/viz-stat-card/viz-stat-card.component.scss @@ -0,0 +1,61 @@ +:host { + display: block; +} + +.viz-stat-card { + display: flex; + align-items: center; + gap: var(--viz-spacing-md); + background: var(--viz-bg); + border: 1px solid var(--viz-border); + border-radius: var(--viz-radius-md); + padding: var(--viz-spacing-lg); + + &__icon { + font-size: 1.5rem; + flex-shrink: 0; + } + + &__content { + display: flex; + flex-direction: column; + gap: 2px; + } + + &__label { + font-size: var(--viz-font-size-sm); + color: var(--viz-text-muted); + font-weight: 500; + } + + &__value { + font-size: 1.75rem; + font-weight: 700; + color: var(--viz-text); + line-height: 1.2; + } + + &__trend { + display: inline-flex; + align-items: center; + gap: 2px; + font-size: var(--viz-font-size-sm); + font-weight: 500; + + &--up { + color: var(--viz-stat-positive); + } + + &--down { + color: var(--viz-stat-negative); + } + + &--flat { + color: var(--viz-stat-neutral); + } + } + + &__trend-icon { + font-weight: 700; + } +} diff --git a/src/components/viz-stat-card/viz-stat-card.component.ts b/src/components/viz-stat-card/viz-stat-card.component.ts new file mode 100644 index 0000000..e94112f --- /dev/null +++ b/src/components/viz-stat-card/viz-stat-card.component.ts @@ -0,0 +1,40 @@ +import { + Component, ChangeDetectionStrategy, input, computed, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'viz-stat-card', + standalone: true, + imports: [CommonModule], + templateUrl: './viz-stat-card.component.html', + styleUrl: './viz-stat-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizStatCardComponent { + readonly value = input.required(); + readonly label = input.required(); + readonly trend = input<'up' | 'down' | 'flat' | undefined>(undefined); + readonly trendValue = input(''); + readonly icon = input(''); + readonly prefix = input(''); + readonly suffix = input(''); + + readonly trendClass = computed(() => { + const t = this.trend(); + if (t === 'up') return 'viz-stat-card__trend--up'; + if (t === 'down') return 'viz-stat-card__trend--down'; + return 'viz-stat-card__trend--flat'; + }); + + readonly trendIcon = computed(() => { + const t = this.trend(); + if (t === 'up') return '\u2191'; + if (t === 'down') return '\u2193'; + return '\u2192'; + }); + + readonly displayValue = computed(() => { + return `${this.prefix()}${this.value()}${this.suffix()}`; + }); +} diff --git a/src/components/viz-time-series/index.ts b/src/components/viz-time-series/index.ts new file mode 100644 index 0000000..3348c66 --- /dev/null +++ b/src/components/viz-time-series/index.ts @@ -0,0 +1 @@ +export * from './viz-time-series.component'; diff --git a/src/components/viz-time-series/viz-time-series.component.html b/src/components/viz-time-series/viz-time-series.component.html new file mode 100644 index 0000000..357770e --- /dev/null +++ b/src/components/viz-time-series/viz-time-series.component.html @@ -0,0 +1,9 @@ +
+ @if (legend().visible && (legend().position === 'top' || legend().position === 'left')) { + + } +
+ @if (legend().visible && (legend().position === 'bottom' || legend().position === 'right')) { + + } +
diff --git a/src/components/viz-time-series/viz-time-series.component.scss b/src/components/viz-time-series/viz-time-series.component.scss new file mode 100644 index 0000000..a00d917 --- /dev/null +++ b/src/components/viz-time-series/viz-time-series.component.scss @@ -0,0 +1,29 @@ +:host { + display: block; + position: relative; + width: 100%; +} + +.viz-time-series { + display: flex; + flex-direction: column; + width: 100%; + + &--legend-left, + &--legend-right { + flex-direction: row; + } + + > div:not(.viz-legend) { + flex: 1; + min-width: 0; + } + + ::ng-deep { + .viz-time-series-svg { display: block; overflow: visible; } + .domain { stroke: var(--viz-axis-color); } + .tick line { stroke: var(--viz-tick-color); } + .tick text { fill: var(--viz-text-muted); font-size: var(--viz-font-size-xs); } + .grid-line { stroke: var(--viz-grid-color); stroke-dasharray: 2, 2; } + } +} diff --git a/src/components/viz-time-series/viz-time-series.component.ts b/src/components/viz-time-series/viz-time-series.component.ts new file mode 100644 index 0000000..16dcdee --- /dev/null +++ b/src/components/viz-time-series/viz-time-series.component.ts @@ -0,0 +1,227 @@ +import { + Component, ChangeDetectionStrategy, ElementRef, NgZone, OnDestroy, + inject, input, output, viewChild, effect, computed, signal, +} from '@angular/core'; +import { select, Selection } from 'd3-selection'; +import { scaleTime, scaleLinear } from 'd3-scale'; +import { axisBottom, axisLeft } from 'd3-axis'; +import { line, area, curveMonotoneX } from 'd3-shape'; +import { extent, max } from 'd3-array'; +import 'd3-transition'; +import { VIZ_CONFIG } from '../../providers/viz-config.provider'; +import { VizThemeService } from '../../services/viz-theme.service'; +import { VizResizeService } from '../../services/viz-resize.service'; +import { VizTooltipService } from '../../services/viz-tooltip.service'; +import type { CartesianDataPoint, ChartSeries } from '../../types/chart.types'; +import type { ChartMargin, AxisConfig, LegendConfig } from '../../types/config.types'; +import type { ChartHoverEvent } from '../../types/event.types'; +import { withOpacity } from '../../utils/color.utils'; +import { VizLegendComponent, LegendItem } from '../viz-legend/viz-legend.component'; + +@Component({ + selector: 'viz-time-series', + standalone: true, + imports: [VizLegendComponent], + templateUrl: './viz-time-series.component.html', + styleUrl: './viz-time-series.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizTimeSeriesComponent implements OnDestroy { + readonly series = input.required(); + readonly width = input('auto'); + readonly height = input(300); + readonly margin = input({ top: 20, right: 20, bottom: 40, left: 50 }); + readonly xAxis = input({}); + readonly yAxis = input({ gridLines: true }); + readonly legend = input({ visible: true, position: 'top' }); + readonly animate = input(undefined); + readonly timeWindow = input(undefined); + readonly autoScroll = input(true); + readonly brushEnabled = input(false); + readonly showArea = input(true); + + readonly pointHover = output>(); + + readonly hiddenItems = signal>(new Set()); + + readonly legendItems = computed(() => + this.series().map((s, i) => ({ + label: s.name, + color: s.color ?? this.themeService.getColor(i), + active: !this.hiddenItems().has(s.name), + })) + ); + + private readonly chartRef = viewChild.required>('chart'); + + private readonly ngZone = inject(NgZone); + private readonly config = inject(VIZ_CONFIG); + private readonly themeService = inject(VizThemeService); + private readonly resizeService = inject(VizResizeService); + private readonly tooltipService = inject(VizTooltipService); + + private svg: Selection | null = null; + private resizeCleanup: (() => void) | null = null; + + constructor() { + effect(() => { + const _ = this.series(); + const __ = this.hiddenItems(); + this.ngZone.runOutsideAngular(() => { + if (!this.svg) { + this.createChart(); + this.setupResize(); + } else { + this.updateChart(); + } + }); + }); + } + + onLegendClick(event: { item: LegendItem; index: number }): void { + const hidden = new Set(this.hiddenItems()); + if (hidden.has(event.item.label)) { + hidden.delete(event.item.label); + } else { + hidden.add(event.item.label); + } + this.hiddenItems.set(hidden); + } + + ngOnDestroy(): void { + this.resizeCleanup?.(); + this.svg?.remove(); + } + + private getWidth(): number { + const w = this.width(); + return w === 'auto' ? (this.chartRef().nativeElement.clientWidth || 600) : w; + } + + private createChart(): void { + const el = this.chartRef().nativeElement; + this.svg = select(el).append('svg').attr('class', 'viz-time-series-svg'); + + this.svg.append('defs').append('clipPath').attr('id', 'ts-clip').append('rect'); + this.svg.append('g').attr('class', 'x-axis'); + this.svg.append('g').attr('class', 'y-axis'); + this.svg.append('g').attr('class', 'chart-area').attr('clip-path', 'url(#ts-clip)'); + + this.updateChart(); + } + + private updateChart(): void { + if (!this.svg) return; + + const allSeries = this.series().filter(s => s.visible !== false && !this.hiddenItems().has(s.name)); + const w = this.getWidth(); + const h = this.height(); + const m = this.margin(); + const innerW = w - m.left - m.right; + const innerH = h - m.top - m.bottom; + const shouldAnimate = this.animate() ?? this.config.animate; + const duration = shouldAnimate ? this.config.animationDuration : 0; + const yAxisConfig = this.yAxis(); + + this.svg.attr('width', w).attr('height', h); + + // Clip path + this.svg.select('#ts-clip rect') + .attr('width', innerW) + .attr('height', innerH); + + const allPoints: CartesianDataPoint[] = []; + for (const s of allSeries) { for (const d of s.data) { allPoints.push(d); } } + if (allPoints.length === 0) return; + + let timeDomain = extent(allPoints, (d: CartesianDataPoint) => d.x as Date) as [Date, Date]; + + // Apply time window + const tw = this.timeWindow(); + if (tw && this.autoScroll()) { + const latestTime = timeDomain[1].getTime(); + timeDomain = [new Date(latestTime - tw), timeDomain[1]]; + } + + const xScale = scaleTime().domain(timeDomain).range([0, innerW]); + + const yScale = scaleLinear() + .domain([yAxisConfig.min ?? 0, yAxisConfig.max ?? (max(allPoints, (d: CartesianDataPoint) => d.y) ?? 0)]) + .range([innerH, 0]) + .nice(); + + // Axes + this.svg.select('.x-axis') + .attr('transform', `translate(${m.left},${m.top + innerH})`) + .transition().duration(duration) + .call(axisBottom(xScale) as any); + + const yAxisG = this.svg.select('.y-axis') + .attr('transform', `translate(${m.left},${m.top})`) + .call(axisLeft(yScale).ticks(yAxisConfig.tickCount ?? 5)); + + if (yAxisConfig.gridLines) { + yAxisG.selectAll('.grid-line').remove(); + yAxisG.selectAll('.tick line') + .clone() + .attr('x2', innerW) + .attr('stroke', 'var(--viz-grid-color, #f3f4f6)') + .attr('stroke-dasharray', '2,2') + .attr('class', 'grid-line'); + } + + const chartArea = this.svg.select('.chart-area') + .attr('transform', `translate(${m.left},${m.top})`); + + const lineGen = line() + .x(d => xScale(d.x as Date)) + .y(d => yScale(d.y)) + .curve(curveMonotoneX); + + const areaGen = area() + .x(d => xScale(d.x as Date)) + .y0(innerH) + .y1(d => yScale(d.y)) + .curve(curveMonotoneX); + + // Render each series + allSeries.forEach((s, i) => { + const color = s.color ?? this.themeService.getColor(i); + const seriesClass = `series-${i}`; + + // Area + if (this.showArea()) { + let areaPath = chartArea.select(`.area-${seriesClass}`); + if (areaPath.empty()) { + areaPath = chartArea.append('path').attr('class', `area-${seriesClass}`); + } + areaPath + .transition().duration(duration) + .attr('d', areaGen(s.data) ?? '') + .attr('fill', withOpacity(color, 0.1)); + } + + // Line + let linePath = chartArea.select(`.line-${seriesClass}`); + if (linePath.empty()) { + linePath = chartArea.append('path') + .attr('class', `line-${seriesClass}`) + .attr('fill', 'none') + .attr('stroke-width', 2); + } + linePath + .transition().duration(duration) + .attr('d', lineGen(s.data) ?? '') + .attr('stroke', color); + }); + } + + private setupResize(): void { + if (this.width() === 'auto' && this.config.responsive) { + this.resizeCleanup = this.resizeService.observe( + this.chartRef().nativeElement, + () => this.updateChart(), + ); + } + } +} diff --git a/src/components/viz-treemap/index.ts b/src/components/viz-treemap/index.ts new file mode 100644 index 0000000..8f6c259 --- /dev/null +++ b/src/components/viz-treemap/index.ts @@ -0,0 +1 @@ +export * from './viz-treemap.component'; diff --git a/src/components/viz-treemap/viz-treemap.component.html b/src/components/viz-treemap/viz-treemap.component.html new file mode 100644 index 0000000..00ed6ad --- /dev/null +++ b/src/components/viz-treemap/viz-treemap.component.html @@ -0,0 +1 @@ +
diff --git a/src/components/viz-treemap/viz-treemap.component.scss b/src/components/viz-treemap/viz-treemap.component.scss new file mode 100644 index 0000000..3dd6f5c --- /dev/null +++ b/src/components/viz-treemap/viz-treemap.component.scss @@ -0,0 +1,19 @@ +:host { + display: block; + position: relative; + width: 100%; +} + +.viz-treemap { + width: 100%; + + ::ng-deep { + .viz-treemap-svg { display: block; overflow: visible; } + + rect { + cursor: pointer; + transition: opacity var(--viz-transition); + &:hover { opacity: 1; } + } + } +} diff --git a/src/components/viz-treemap/viz-treemap.component.ts b/src/components/viz-treemap/viz-treemap.component.ts new file mode 100644 index 0000000..8974002 --- /dev/null +++ b/src/components/viz-treemap/viz-treemap.component.ts @@ -0,0 +1,182 @@ +import { + Component, ChangeDetectionStrategy, ElementRef, NgZone, OnDestroy, + inject, input, output, viewChild, effect, +} from '@angular/core'; +import { select, Selection } from 'd3-selection'; +import { + treemap, hierarchy, treemapSquarify, treemapBinary, + treemapSlice, treemapDice, HierarchyRectangularNode, +} from 'd3-hierarchy'; +import 'd3-transition'; +import { VIZ_CONFIG } from '../../providers/viz-config.provider'; +import { VizThemeService } from '../../services/viz-theme.service'; +import { VizResizeService } from '../../services/viz-resize.service'; +import { VizTooltipService } from '../../services/viz-tooltip.service'; +import type { TreemapNode } from '../../types/chart.types'; +import type { ChartMargin } from '../../types/config.types'; +import type { ChartClickEvent, ChartHoverEvent } from '../../types/event.types'; + +const TILINGS: Record = { + squarify: treemapSquarify, + binary: treemapBinary as any, + slice: treemapSlice as any, + dice: treemapDice as any, +}; + +@Component({ + selector: 'viz-treemap', + standalone: true, + templateUrl: './viz-treemap.component.html', + styleUrl: './viz-treemap.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizTreemapComponent implements OnDestroy { + readonly data = input.required(); + readonly width = input('auto'); + readonly height = input(400); + readonly margin = input({ top: 10, right: 10, bottom: 10, left: 10 }); + readonly animate = input(undefined); + readonly tiling = input<'squarify' | 'binary' | 'slice' | 'dice'>('squarify'); + readonly showLabels = input(true); + + readonly nodeClick = output>(); + readonly nodeHover = output>(); + + private readonly chartRef = viewChild.required>('chart'); + + private readonly ngZone = inject(NgZone); + private readonly config = inject(VIZ_CONFIG); + private readonly themeService = inject(VizThemeService); + private readonly resizeService = inject(VizResizeService); + private readonly tooltipService = inject(VizTooltipService); + + private svg: Selection | null = null; + private resizeCleanup: (() => void) | null = null; + + constructor() { + effect(() => { + const _ = this.data(); + this.ngZone.runOutsideAngular(() => { + if (!this.svg) { + this.createChart(); + this.setupResize(); + } else { + this.updateChart(); + } + }); + }); + } + + ngOnDestroy(): void { + this.resizeCleanup?.(); + this.svg?.remove(); + } + + private getWidth(): number { + const w = this.width(); + return w === 'auto' ? (this.chartRef().nativeElement.clientWidth || 600) : w; + } + + private createChart(): void { + const el = this.chartRef().nativeElement; + this.svg = select(el).append('svg').attr('class', 'viz-treemap-svg'); + this.svg.append('g').attr('class', 'nodes'); + this.updateChart(); + } + + private updateChart(): void { + if (!this.svg) return; + + const data = this.data(); + const w = this.getWidth(); + const h = this.height(); + const m = this.margin(); + const innerW = w - m.left - m.right; + const innerH = h - m.top - m.bottom; + const shouldAnimate = this.animate() ?? this.config.animate; + const duration = shouldAnimate ? this.config.animationDuration : 0; + const tilingFn = TILINGS[this.tiling()] ?? treemapSquarify; + + this.svg.attr('width', w).attr('height', h); + + const root = hierarchy(data) + .sum(d => d.value ?? 0) + .sort((a, b) => (b.value ?? 0) - (a.value ?? 0)); + + const treemapLayout = treemap() + .size([innerW, innerH]) + .padding(2) + .tile(tilingFn); + + treemapLayout(root); + + const leaves = root.leaves() as HierarchyRectangularNode[]; + const nodesG = this.svg.select('.nodes').attr('transform', `translate(${m.left},${m.top})`); + + const groups = nodesG.selectAll>('g') + .data(leaves, (d: HierarchyRectangularNode) => d.data.name); + + groups.exit().transition().duration(duration).attr('opacity', 0).remove(); + + const enter = groups.enter().append('g').attr('opacity', 0); + + enter.append('rect'); + enter.append('text'); + enter.append('clipPath').attr('id', (_, i) => `clip-treemap-${i}`).append('rect'); + + const merged = enter.merge(groups); + + merged.transition().duration(duration).attr('opacity', 1); + + merged.select('rect') + .on('mouseenter', (event: MouseEvent, d) => { + if (this.config.tooltips) { + this.tooltipService.show(event.clientX, event.clientY, + `${d.data.name}: ${d.value}`); + } + this.ngZone.run(() => this.nodeHover.emit({ data: d.data, index: leaves.indexOf(d), event })); + }) + .on('mousemove', (event: MouseEvent) => this.tooltipService.update(event.clientX, event.clientY)) + .on('mouseleave', (event: MouseEvent) => { + this.tooltipService.hide(); + this.ngZone.run(() => this.nodeHover.emit({ data: null, index: -1, event })); + }) + .on('click', (event: MouseEvent, d) => { + this.ngZone.run(() => this.nodeClick.emit({ data: d.data, index: leaves.indexOf(d), event })); + }) + .transition() + .duration(duration) + .attr('x', d => d.x0) + .attr('y', d => d.y0) + .attr('width', d => d.x1 - d.x0) + .attr('height', d => d.y1 - d.y0) + .attr('fill', (d, i) => d.data.color ?? this.themeService.getColor(i)) + .attr('rx', 3) + .attr('opacity', 0.85); + + if (this.showLabels()) { + merged.select('text') + .transition() + .duration(duration) + .attr('x', d => d.x0 + 4) + .attr('y', d => d.y0 + 14) + .attr('fill', '#fff') + .attr('font-size', 'var(--viz-font-size-xs, 12px)') + .attr('font-weight', '500') + .text(d => { + const nodeW = d.x1 - d.x0; + const nodeH = d.y1 - d.y0; + return nodeW > 40 && nodeH > 20 ? d.data.name : ''; + }); + } + } + + private setupResize(): void { + if (this.width() === 'auto' && this.config.responsive) { + this.resizeCleanup = this.resizeService.observe( + this.chartRef().nativeElement, + () => this.updateChart(), + ); + } + } +} diff --git a/src/components/viz-trend-indicator/index.ts b/src/components/viz-trend-indicator/index.ts new file mode 100644 index 0000000..45de862 --- /dev/null +++ b/src/components/viz-trend-indicator/index.ts @@ -0,0 +1 @@ +export * from './viz-trend-indicator.component'; diff --git a/src/components/viz-trend-indicator/viz-trend-indicator.component.html b/src/components/viz-trend-indicator/viz-trend-indicator.component.html new file mode 100644 index 0000000..cf4b403 --- /dev/null +++ b/src/components/viz-trend-indicator/viz-trend-indicator.component.html @@ -0,0 +1,9 @@ + + {{ icon() }} + @if (value()) { + {{ value() }} + } + @if (label()) { + {{ label() }} + } + diff --git a/src/components/viz-trend-indicator/viz-trend-indicator.component.scss b/src/components/viz-trend-indicator/viz-trend-indicator.component.scss new file mode 100644 index 0000000..30b66c6 --- /dev/null +++ b/src/components/viz-trend-indicator/viz-trend-indicator.component.scss @@ -0,0 +1,36 @@ +:host { + display: inline-flex; +} + +.viz-trend { + display: inline-flex; + align-items: center; + gap: var(--viz-spacing-xs); + font-size: var(--viz-font-size-sm); + font-weight: 500; + + &--up { + color: var(--viz-stat-positive); + } + + &--down { + color: var(--viz-stat-negative); + } + + &--flat { + color: var(--viz-stat-neutral); + } + + &__icon { + font-size: 0.625rem; + } + + &__value { + font-weight: 600; + } + + &__label { + color: var(--viz-text-muted); + font-weight: 400; + } +} diff --git a/src/components/viz-trend-indicator/viz-trend-indicator.component.ts b/src/components/viz-trend-indicator/viz-trend-indicator.component.ts new file mode 100644 index 0000000..ad23173 --- /dev/null +++ b/src/components/viz-trend-indicator/viz-trend-indicator.component.ts @@ -0,0 +1,25 @@ +import { + Component, ChangeDetectionStrategy, input, computed, +} from '@angular/core'; + +@Component({ + selector: 'viz-trend-indicator', + standalone: true, + templateUrl: './viz-trend-indicator.component.html', + styleUrl: './viz-trend-indicator.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VizTrendIndicatorComponent { + readonly direction = input.required<'up' | 'down' | 'flat'>(); + readonly value = input(''); + readonly label = input(''); + + readonly icon = computed(() => { + const d = this.direction(); + if (d === 'up') return '\u25B2'; + if (d === 'down') return '\u25BC'; + return '\u25C6'; + }); + + readonly directionClass = computed(() => `viz-trend--${this.direction()}`); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..f6b26e0 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,48 @@ +/** + * Data Viz Elements UI + * Main library entry point + * + * Angular components for data visualization, charts, and dashboard widgets powered by D3.js + * + * @example + * ```typescript + * import { Component } from '@angular/core'; + * import { VizBarChartComponent, provideVizConfig } from '@sda/data-viz-elements-ui'; + * + * @Component({ + * standalone: true, + * imports: [VizBarChartComponent], + * template: ` + * + * + * ` + * }) + * export class AppComponent { + * data = [ + * { label: 'A', value: 10 }, + * { label: 'B', value: 20 }, + * ]; + * + * onBarClick(event: any) { + * console.log('Bar clicked:', event); + * } + * } + * ``` + */ + +// Types +export * from './types'; + +// Utils +export * from './utils'; + +// Providers +export * from './providers'; + +// Services +export * from './services'; + +// Components +export * from './components'; diff --git a/src/providers/index.ts b/src/providers/index.ts new file mode 100644 index 0000000..f35063e --- /dev/null +++ b/src/providers/index.ts @@ -0,0 +1 @@ +export * from './viz-config.provider'; diff --git a/src/providers/viz-config.provider.ts b/src/providers/viz-config.provider.ts new file mode 100644 index 0000000..00dcc4f --- /dev/null +++ b/src/providers/viz-config.provider.ts @@ -0,0 +1,23 @@ +import { InjectionToken, makeEnvironmentProviders, EnvironmentProviders } from '@angular/core'; +import { VizConfig } from '../types/config.types'; + +export const DEFAULT_VIZ_CONFIG: VizConfig = { + colorPalette: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#f97316', '#ec4899'], + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + animate: true, + animationDuration: 300, + responsive: true, + locale: 'en-US', + tooltips: true, +}; + +export const VIZ_CONFIG = new InjectionToken('VIZ_CONFIG', { + providedIn: 'root', + factory: () => DEFAULT_VIZ_CONFIG, +}); + +export function provideVizConfig(config: Partial = {}): EnvironmentProviders { + return makeEnvironmentProviders([ + { provide: VIZ_CONFIG, useValue: { ...DEFAULT_VIZ_CONFIG, ...config } }, + ]); +} diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..bc92ec7 --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,4 @@ +export * from './viz-theme.service'; +export * from './viz-resize.service'; +export * from './viz-tooltip.service'; +export * from './viz-export.service'; diff --git a/src/services/viz-export.service.ts b/src/services/viz-export.service.ts new file mode 100644 index 0000000..d0c79b3 --- /dev/null +++ b/src/services/viz-export.service.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class VizExportService { + exportSvg(element: SVGSVGElement): string { + const serializer = new XMLSerializer(); + const svgString = serializer.serializeToString(element); + return `\n${svgString}`; + } + + async exportPng(element: SVGSVGElement, width: number, height: number): Promise { + const svgString = this.exportSvg(element); + const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' }); + const url = URL.createObjectURL(svgBlob); + + try { + const img = new Image(); + img.width = width; + img.height = height; + + await new Promise((resolve, reject) => { + img.onload = () => resolve(); + img.onerror = reject; + img.src = url; + }); + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d')!; + ctx.drawImage(img, 0, 0, width, height); + + return new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + } else { + reject(new Error('Failed to create PNG blob')); + } + }, 'image/png'); + }); + } finally { + URL.revokeObjectURL(url); + } + } + + downloadSvg(element: SVGSVGElement, filename = 'chart.svg'): void { + const svgString = this.exportSvg(element); + const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' }); + this.downloadBlob(blob, filename); + } + + async downloadPng(element: SVGSVGElement, width: number, height: number, filename = 'chart.png'): Promise { + const blob = await this.exportPng(element, width, height); + this.downloadBlob(blob, filename); + } + + private downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } +} diff --git a/src/services/viz-resize.service.ts b/src/services/viz-resize.service.ts new file mode 100644 index 0000000..ea2fd50 --- /dev/null +++ b/src/services/viz-resize.service.ts @@ -0,0 +1,39 @@ +import { Injectable, NgZone, inject } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class VizResizeService { + private readonly ngZone = inject(NgZone); + private observer: ResizeObserver | null = null; + private readonly callbacks = new Map void>(); + private debounceTimer: ReturnType | null = null; + + observe(element: Element, callback: (entry: ResizeObserverEntry) => void): () => void { + if (!this.observer) { + this.observer = new ResizeObserver((entries) => { + if (this.debounceTimer) { + clearTimeout(this.debounceTimer); + } + this.debounceTimer = setTimeout(() => { + this.ngZone.runOutsideAngular(() => { + for (const entry of entries) { + const cb = this.callbacks.get(entry.target); + cb?.(entry); + } + }); + }, 150); + }); + } + + this.callbacks.set(element, callback); + this.observer.observe(element); + + return () => { + this.callbacks.delete(element); + this.observer?.unobserve(element); + if (this.callbacks.size === 0) { + this.observer?.disconnect(); + this.observer = null; + } + }; + } +} diff --git a/src/services/viz-theme.service.ts b/src/services/viz-theme.service.ts new file mode 100644 index 0000000..22c0479 --- /dev/null +++ b/src/services/viz-theme.service.ts @@ -0,0 +1,33 @@ +import { Injectable, inject, signal, computed } from '@angular/core'; +import { VIZ_CONFIG } from '../providers/viz-config.provider'; + +export const PALETTES: Record = { + default: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#f97316', '#ec4899'], + categorical: ['#6366f1', '#ec4899', '#14b8a6', '#f97316', '#8b5cf6', '#06b6d4', '#eab308', '#ef4444'], + sequential: ['#eff6ff', '#bfdbfe', '#93c5fd', '#60a5fa', '#3b82f6', '#2563eb', '#1d4ed8', '#1e40af'], + diverging: ['#ef4444', '#f87171', '#fca5a5', '#f3f4f6', '#93c5fd', '#60a5fa', '#3b82f6', '#2563eb'], +}; + +@Injectable({ providedIn: 'root' }) +export class VizThemeService { + private readonly config = inject(VIZ_CONFIG); + + private readonly paletteSignal = signal(this.config.colorPalette); + + readonly palette = this.paletteSignal.asReadonly(); + + readonly paletteSize = computed(() => this.paletteSignal().length); + + getColor(index: number): string { + const p = this.paletteSignal(); + return p[index % p.length]; + } + + setPalette(palette: string[] | keyof typeof PALETTES): void { + if (typeof palette === 'string') { + this.paletteSignal.set(PALETTES[palette] ?? PALETTES['default']); + } else { + this.paletteSignal.set(palette); + } + } +} diff --git a/src/services/viz-tooltip.service.ts b/src/services/viz-tooltip.service.ts new file mode 100644 index 0000000..8aba141 --- /dev/null +++ b/src/services/viz-tooltip.service.ts @@ -0,0 +1,74 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class VizTooltipService { + private tooltipEl: HTMLDivElement | null = null; + + private ensureElement(): HTMLDivElement { + if (!this.tooltipEl) { + this.tooltipEl = document.createElement('div'); + this.tooltipEl.className = 'viz-tooltip'; + this.tooltipEl.style.cssText = ` + position: fixed; + pointer-events: none; + z-index: 9999; + padding: 6px 10px; + background: var(--viz-tooltip-bg, #1f2937); + color: var(--viz-tooltip-text, #f9fafb); + border-radius: 4px; + box-shadow: var(--viz-tooltip-shadow, 0 4px 12px rgba(0,0,0,0.15)); + font-size: 0.875rem; + white-space: nowrap; + opacity: 0; + transition: opacity 150ms ease-in-out; + `; + document.body.appendChild(this.tooltipEl); + } + return this.tooltipEl; + } + + show(x: number, y: number, content: string): void { + const el = this.ensureElement(); + el.innerHTML = content; + el.style.opacity = '1'; + this.position(el, x, y); + } + + update(x: number, y: number): void { + if (this.tooltipEl) { + this.position(this.tooltipEl, x, y); + } + } + + hide(): void { + if (this.tooltipEl) { + this.tooltipEl.style.opacity = '0'; + } + } + + private position(el: HTMLDivElement, x: number, y: number): void { + const offset = 10; + const rect = el.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + let left = x + offset; + let top = y - rect.height - offset; + + if (left + rect.width > viewportWidth) { + left = x - rect.width - offset; + } + if (top < 0) { + top = y + offset; + } + if (left < 0) { + left = offset; + } + if (top + rect.height > viewportHeight) { + top = viewportHeight - rect.height - offset; + } + + el.style.left = `${left}px`; + el.style.top = `${top}px`; + } +} diff --git a/src/styles/_index.scss b/src/styles/_index.scss new file mode 100644 index 0000000..ec2276a --- /dev/null +++ b/src/styles/_index.scss @@ -0,0 +1,2 @@ +@forward 'tokens'; +@forward 'mixins'; diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss new file mode 100644 index 0000000..18af3aa --- /dev/null +++ b/src/styles/_mixins.scss @@ -0,0 +1,58 @@ +@mixin viz-chart-container { + display: block; + position: relative; + width: 100%; + font-family: var(--viz-font-family, inherit); +} + +@mixin viz-svg-root { + display: block; + width: 100%; + height: 100%; + overflow: visible; +} + +@mixin viz-axis { + .domain { + stroke: var(--viz-axis-color); + } + + .tick { + line { + stroke: var(--viz-tick-color); + } + + text { + fill: var(--viz-text-muted); + font-size: var(--viz-font-size-xs); + } + } +} + +@mixin viz-grid-lines { + .grid-line { + stroke: var(--viz-grid-color); + stroke-dasharray: 2, 2; + } +} + +@mixin viz-tooltip { + position: fixed; + pointer-events: none; + z-index: 9999; + padding: var(--viz-spacing-sm) var(--viz-spacing-md); + background: var(--viz-tooltip-bg); + color: var(--viz-tooltip-text); + border-radius: var(--viz-radius-md); + box-shadow: var(--viz-tooltip-shadow); + font-size: var(--viz-font-size-sm); + white-space: nowrap; + transition: opacity var(--viz-transition), transform var(--viz-transition); +} + +@mixin viz-card { + background: var(--viz-bg); + border: 1px solid var(--viz-border); + border-radius: var(--viz-radius-md); + padding: var(--viz-spacing-lg); +} diff --git a/src/styles/_tokens.scss b/src/styles/_tokens.scss new file mode 100644 index 0000000..ec2117d --- /dev/null +++ b/src/styles/_tokens.scss @@ -0,0 +1,67 @@ +:root { + // Chart colors (matching default palette) + --viz-color-1: #3b82f6; + --viz-color-2: #10b981; + --viz-color-3: #f59e0b; + --viz-color-4: #ef4444; + --viz-color-5: #8b5cf6; + --viz-color-6: #06b6d4; + --viz-color-7: #f97316; + --viz-color-8: #ec4899; + + // Backgrounds + --viz-bg: #ffffff; + --viz-border: #e5e7eb; + --viz-text: #111827; + --viz-text-muted: #6b7280; + + // Axes & grid + --viz-axis-color: #6b7280; + --viz-grid-color: #f3f4f6; + --viz-tick-color: #9ca3af; + + // Tooltip + --viz-tooltip-bg: #1f2937; + --viz-tooltip-text: #f9fafb; + --viz-tooltip-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + + // KPI widgets + --viz-stat-positive: #10b981; + --viz-stat-negative: #ef4444; + --viz-stat-neutral: #6b7280; + + // Spacing + --viz-spacing-xs: 0.25rem; + --viz-spacing-sm: 0.5rem; + --viz-spacing-md: 0.75rem; + --viz-spacing-lg: 1rem; + + // Radius + --viz-radius-sm: 0.25rem; + --viz-radius-md: 0.375rem; + + // Typography + --viz-font-size-xs: 0.75rem; + --viz-font-size-sm: 0.875rem; + --viz-font-size-base: 1rem; + + // Transition + --viz-transition: 200ms ease-in-out; +} + +@media (prefers-color-scheme: dark) { + :root { + --viz-bg: #111827; + --viz-border: #374151; + --viz-text: #f9fafb; + --viz-text-muted: #9ca3af; + + --viz-axis-color: #9ca3af; + --viz-grid-color: #1f2937; + --viz-tick-color: #6b7280; + + --viz-tooltip-bg: #f9fafb; + --viz-tooltip-text: #111827; + --viz-tooltip-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + } +} diff --git a/src/types/chart.types.ts b/src/types/chart.types.ts new file mode 100644 index 0000000..ece143d --- /dev/null +++ b/src/types/chart.types.ts @@ -0,0 +1,71 @@ +/** Single data point for categorical charts (bar, pie) */ +export interface ChartDataPoint { + label: string; + value: number; + color?: string; + metadata?: Record; +} + +/** Data point for continuous axes (line, area, scatter, time-series) */ +export interface CartesianDataPoint { + x: number | Date; + y: number; + metadata?: Record; +} + +/** A named series of data points */ +export interface ChartSeries { + name: string; + data: T[]; + color?: string; + visible?: boolean; +} + +/** Scatter/bubble point with optional size */ +export interface ScatterDataPoint extends CartesianDataPoint { + size?: number; + label?: string; +} + +/** Heatmap cell */ +export interface HeatmapCell { + x: string | number; + y: string | number; + value: number; +} + +/** Box plot statistics */ +export interface BoxPlotData { + label: string; + min: number; + q1: number; + median: number; + q3: number; + max: number; + outliers?: number[]; +} + +/** Treemap node */ +export interface TreemapNode { + name: string; + value?: number; + children?: TreemapNode[]; + color?: string; +} + +/** Table column definition */ +export interface TableColumn { + key: string; + label: string; + sortable?: boolean; + filterable?: boolean; + width?: string; + align?: 'left' | 'center' | 'right'; + format?: (value: unknown, row: T) => string; +} + +/** Table sort state */ +export interface TableSort { + column: string; + direction: 'asc' | 'desc'; +} diff --git a/src/types/config.types.ts b/src/types/config.types.ts new file mode 100644 index 0000000..6d24c7b --- /dev/null +++ b/src/types/config.types.ts @@ -0,0 +1,40 @@ +/** Global library configuration */ +export interface VizConfig { + colorPalette: string[]; + fontFamily: string; + animate: boolean; + animationDuration: number; + responsive: boolean; + locale: string; + tooltips: boolean; +} + +/** Shared chart margin options */ +export interface ChartMargin { + top: number; + right: number; + bottom: number; + left: number; +} + +/** Axis configuration */ +export interface AxisConfig { + label?: string; + tickCount?: number; + tickFormat?: (value: unknown) => string; + gridLines?: boolean; + min?: number; + max?: number; +} + +/** Legend configuration */ +export interface LegendConfig { + visible: boolean; + position: 'top' | 'bottom' | 'left' | 'right'; +} + +/** Tooltip configuration */ +export interface TooltipConfig { + enabled: boolean; + format?: (data: unknown) => string; +} diff --git a/src/types/event.types.ts b/src/types/event.types.ts new file mode 100644 index 0000000..97a372c --- /dev/null +++ b/src/types/event.types.ts @@ -0,0 +1,25 @@ +export interface ChartClickEvent { + data: T; + index: number; + event: MouseEvent; +} + +export interface ChartHoverEvent { + data: T | null; + index: number; + event: MouseEvent; +} + +export interface ChartSelectionEvent { + selected: T[]; +} + +export interface TablePageEvent { + page: number; + pageSize: number; +} + +export interface TableSortEvent { + column: string; + direction: 'asc' | 'desc'; +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..a2928a1 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,3 @@ +export * from './chart.types'; +export * from './config.types'; +export * from './event.types'; diff --git a/src/utils/color.utils.ts b/src/utils/color.utils.ts new file mode 100644 index 0000000..f1322e5 --- /dev/null +++ b/src/utils/color.utils.ts @@ -0,0 +1,24 @@ +import { interpolateRgb } from 'd3-interpolate'; +import { color as d3Color } from 'd3-color'; + +export function lighten(hex: string, amount: number): string { + return interpolateRgb(hex, '#ffffff')(amount); +} + +export function darken(hex: string, amount: number): string { + return interpolateRgb(hex, '#000000')(amount); +} + +export function withOpacity(hex: string, opacity: number): string { + const c = d3Color(hex); + if (c) { + c.opacity = opacity; + return c.formatRgb(); + } + return hex; +} + +export function interpolateColors(colorA: string, colorB: string, steps: number): string[] { + const interpolator = interpolateRgb(colorA, colorB); + return Array.from({ length: steps }, (_, i) => interpolator(i / (steps - 1))); +} diff --git a/src/utils/format.utils.ts b/src/utils/format.utils.ts new file mode 100644 index 0000000..2b0017c --- /dev/null +++ b/src/utils/format.utils.ts @@ -0,0 +1,26 @@ +import { format as d3Format } from 'd3-format'; +import { timeFormat } from 'd3-time-format'; + +export function formatNumber(value: number, specifier = ',.0f'): string { + return d3Format(specifier)(value); +} + +export function formatPercent(value: number, decimals = 1): string { + return d3Format(`.${decimals}%`)(value); +} + +export function formatCompact(value: number): string { + return d3Format('.2s')(value); +} + +export function formatDate(date: Date, pattern = '%b %d, %Y'): string { + return timeFormat(pattern)(date); +} + +export function formatTime(date: Date, pattern = '%H:%M'): string { + return timeFormat(pattern)(date); +} + +export function formatDateTime(date: Date, pattern = '%b %d %H:%M'): string { + return timeFormat(pattern)(date); +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..8afbee9 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './scale.utils'; +export * from './format.utils'; +export * from './color.utils'; diff --git a/src/utils/scale.utils.ts b/src/utils/scale.utils.ts new file mode 100644 index 0000000..3664110 --- /dev/null +++ b/src/utils/scale.utils.ts @@ -0,0 +1,47 @@ +import { scaleLinear, scaleBand, scaleTime, scaleOrdinal, scaleSequential } from 'd3-scale'; +import { min, max, extent } from 'd3-array'; +import type { AxisConfig } from '../types/config.types'; + +export function createLinearScale( + data: number[], + range: [number, number], + axisConfig?: AxisConfig, +) { + const domain: [number, number] = [ + axisConfig?.min ?? min(data) ?? 0, + axisConfig?.max ?? max(data) ?? 0, + ]; + return scaleLinear().domain(domain).range(range).nice(); +} + +export function createBandScale( + labels: string[], + range: [number, number], + padding = 0.2, +) { + return scaleBand().domain(labels).range(range).padding(padding); +} + +export function createTimeScale( + dates: Date[], + range: [number, number], +) { + const [minDate, maxDate] = extent(dates); + return scaleTime() + .domain([minDate ?? new Date(), maxDate ?? new Date()]) + .range(range); +} + +export function createOrdinalScale( + domain: string[], + colors: string[], +) { + return scaleOrdinal().domain(domain).range(colors); +} + +export function createSequentialScale( + domain: [number, number], + interpolator: (t: number) => string, +) { + return scaleSequential(interpolator).domain(domain); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..dddab58 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": true, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "dom"], + "useDefineForClassFields": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +}