@ojiepermana/angular 22.1.11 → 22.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -26
- package/chart/README.md +272 -0
- package/component/README.md +89 -0
- package/component/accordion/README.md +195 -0
- package/component/alert/README.md +184 -0
- package/component/alert-dialog/README.md +255 -0
- package/component/aspect-ratio/README.md +116 -0
- package/component/avatar/README.md +182 -0
- package/component/badge/README.md +143 -0
- package/component/breadcrumb/README.md +223 -0
- package/component/button/README.md +141 -0
- package/component/button-group/README.md +214 -0
- package/component/calendar/README.md +137 -0
- package/component/card/README.md +232 -0
- package/component/carousel/README.md +276 -0
- package/component/checkbox/README.md +153 -0
- package/component/collapsible/README.md +216 -0
- package/component/combobox/README.md +202 -0
- package/component/command/README.md +287 -0
- package/component/composer/README.md +241 -0
- package/component/context-menu/README.md +268 -0
- package/component/date-picker/README.md +202 -0
- package/component/dialog/README.md +276 -0
- package/component/drawer/README.md +147 -0
- package/component/dropdown-menu/README.md +336 -0
- package/component/editor/README.md +152 -0
- package/component/empty/README.md +193 -0
- package/component/form/README.md +216 -0
- package/component/hover-card/README.md +150 -0
- package/component/icon/README.md +25 -0
- package/component/input/README.md +160 -0
- package/component/input-group/README.md +245 -0
- package/component/input-otp/README.md +331 -0
- package/component/item/README.md +254 -0
- package/component/kanban/README.md +81 -0
- package/component/kbd/README.md +141 -0
- package/component/label/README.md +137 -0
- package/component/menubar/README.md +270 -0
- package/component/native-select/README.md +176 -0
- package/component/navigation-menu/README.md +165 -0
- package/component/pagination/README.md +151 -0
- package/component/pillbox/README.md +68 -0
- package/component/popover/README.md +54 -0
- package/component/progress/README.md +160 -0
- package/component/radio/README.md +212 -0
- package/component/resizable/README.md +178 -0
- package/component/scroll-area/README.md +150 -0
- package/component/select/README.md +228 -0
- package/component/separator/README.md +175 -0
- package/component/sheet/README.md +190 -0
- package/component/skeleton/README.md +158 -0
- package/component/slider/README.md +207 -0
- package/component/spinner/README.md +160 -0
- package/component/switch/README.md +170 -0
- package/component/table/README.md +292 -0
- package/component/tabs/README.md +220 -0
- package/component/textarea/README.md +169 -0
- package/component/timeline/README.md +94 -0
- package/component/toast/README.md +173 -0
- package/component/toggle/README.md +133 -0
- package/component/toggle-group/README.md +234 -0
- package/component/tooltip/README.md +230 -0
- package/fesm2022/ojiepermana-angular-chart-area.mjs +289 -3
- package/fesm2022/ojiepermana-angular-chart-bar.mjs +722 -3
- package/fesm2022/ojiepermana-angular-chart-core.mjs +1482 -3
- package/fesm2022/ojiepermana-angular-chart-line.mjs +294 -3
- package/fesm2022/ojiepermana-angular-chart-pie.mjs +291 -3
- package/fesm2022/ojiepermana-angular-chart-primitives.mjs +1301 -3
- package/fesm2022/ojiepermana-angular-chart-radar.mjs +421 -3
- package/fesm2022/ojiepermana-angular-chart-radial.mjs +295 -3
- package/fesm2022/ojiepermana-angular-chart-scatter.mjs +306 -3
- package/fesm2022/ojiepermana-angular-chart.mjs +12 -3
- package/fesm2022/ojiepermana-angular-component-accordion.mjs +267 -3
- package/fesm2022/ojiepermana-angular-component-alert-dialog.mjs +359 -3
- package/fesm2022/ojiepermana-angular-component-alert.mjs +101 -3
- package/fesm2022/ojiepermana-angular-component-aspect-ratio.mjs +31 -3
- package/fesm2022/ojiepermana-angular-component-avatar.mjs +139 -3
- package/fesm2022/ojiepermana-angular-component-badge.mjs +57 -3
- package/fesm2022/ojiepermana-angular-component-breadcrumb.mjs +192 -3
- package/fesm2022/ojiepermana-angular-component-button-group.mjs +95 -3
- package/fesm2022/ojiepermana-angular-component-button.mjs +73 -3
- package/fesm2022/ojiepermana-angular-component-calendar.mjs +718 -3
- package/fesm2022/ojiepermana-angular-component-card.mjs +167 -3
- package/fesm2022/ojiepermana-angular-component-carousel.mjs +326 -3
- package/fesm2022/ojiepermana-angular-component-checkbox.mjs +190 -3
- package/fesm2022/ojiepermana-angular-component-collapsible.mjs +117 -3
- package/fesm2022/ojiepermana-angular-component-combobox.mjs +327 -3
- package/fesm2022/ojiepermana-angular-component-command.mjs +351 -3
- package/fesm2022/ojiepermana-angular-component-composer.mjs +359 -3
- package/fesm2022/ojiepermana-angular-component-context-menu.mjs +158 -3
- package/fesm2022/ojiepermana-angular-component-date-picker.mjs +482 -3
- package/fesm2022/ojiepermana-angular-component-dialog.mjs +400 -3
- package/fesm2022/ojiepermana-angular-component-drawer.mjs +1 -3
- package/fesm2022/ojiepermana-angular-component-dropdown-menu.mjs +689 -3
- package/fesm2022/ojiepermana-angular-component-editor.mjs +511 -3
- package/fesm2022/ojiepermana-angular-component-empty.mjs +134 -3
- package/fesm2022/ojiepermana-angular-component-form.mjs +438 -3
- package/fesm2022/ojiepermana-angular-component-hover-card.mjs +273 -3
- package/fesm2022/ojiepermana-angular-component-icon.mjs +139 -3
- package/fesm2022/ojiepermana-angular-component-input-group.mjs +176 -3
- package/fesm2022/ojiepermana-angular-component-input-otp.mjs +550 -3
- package/fesm2022/ojiepermana-angular-component-input.mjs +52 -3
- package/fesm2022/ojiepermana-angular-component-item.mjs +264 -3
- package/fesm2022/ojiepermana-angular-component-kanban.mjs +310 -3
- package/fesm2022/ojiepermana-angular-component-kbd.mjs +48 -3
- package/fesm2022/ojiepermana-angular-component-label.mjs +27 -3
- package/fesm2022/ojiepermana-angular-component-menubar.mjs +327 -3
- package/fesm2022/ojiepermana-angular-component-native-select.mjs +62 -3
- package/fesm2022/ojiepermana-angular-component-navigation-menu.mjs +378 -3
- package/fesm2022/ojiepermana-angular-component-pagination.mjs +238 -3
- package/fesm2022/ojiepermana-angular-component-pillbox.mjs +999 -3
- package/fesm2022/ojiepermana-angular-component-popover.mjs +201 -3
- package/fesm2022/ojiepermana-angular-component-progress.mjs +57 -3
- package/fesm2022/ojiepermana-angular-component-radio.mjs +180 -3
- package/fesm2022/ojiepermana-angular-component-resizable.mjs +493 -3
- package/fesm2022/ojiepermana-angular-component-scroll-area.mjs +51 -3
- package/fesm2022/ojiepermana-angular-component-segmented-control.mjs +227 -3
- package/fesm2022/ojiepermana-angular-component-select.mjs +609 -3
- package/fesm2022/ojiepermana-angular-component-separator.mjs +31 -3
- package/fesm2022/ojiepermana-angular-component-sheet.mjs +407 -3
- package/fesm2022/ojiepermana-angular-component-skeleton.mjs +25 -3
- package/fesm2022/ojiepermana-angular-component-slider.mjs +456 -3
- package/fesm2022/ojiepermana-angular-component-spinner.mjs +56 -3
- package/fesm2022/ojiepermana-angular-component-switch.mjs +145 -3
- package/fesm2022/ojiepermana-angular-component-table.mjs +155 -3
- package/fesm2022/ojiepermana-angular-component-tabs.mjs +315 -3
- package/fesm2022/ojiepermana-angular-component-textarea.mjs +46 -3
- package/fesm2022/ojiepermana-angular-component-timeline.mjs +237 -3
- package/fesm2022/ojiepermana-angular-component-toast.mjs +241 -3
- package/fesm2022/ojiepermana-angular-component-toggle-group.mjs +290 -3
- package/fesm2022/ojiepermana-angular-component-toggle.mjs +76 -3
- package/fesm2022/ojiepermana-angular-component-tooltip.mjs +519 -3
- package/fesm2022/ojiepermana-angular-component-utils-core.mjs +310 -3
- package/fesm2022/ojiepermana-angular-component-utils-overlay.mjs +62 -3
- package/fesm2022/ojiepermana-angular-component-utils-scale.mjs +34 -3
- package/fesm2022/ojiepermana-angular-component-utils.mjs +3 -2
- package/fesm2022/ojiepermana-angular-component.mjs +4 -4
- package/fesm2022/ojiepermana-angular-navigation-core.mjs +21 -3
- package/fesm2022/ojiepermana-angular-navigation-icon.mjs +20 -3
- package/fesm2022/ojiepermana-angular-navigation-navigation-badge-count.component-CDBxQ8rk.mjs +91 -0
- package/fesm2022/ojiepermana-angular-navigation-navigation-dockbar-menu.component-DBQHdvGa.mjs +512 -0
- package/fesm2022/ojiepermana-angular-navigation-navigation-entry-grid.component-Cd-jh6tg.mjs +623 -0
- package/fesm2022/ojiepermana-angular-navigation-navigation-flyout-menu.component-BCGvhoYj.mjs +404 -0
- package/fesm2022/ojiepermana-angular-navigation-navigation-horizontal.component-Dk_03e-6.mjs +466 -0
- package/fesm2022/ojiepermana-angular-navigation-ojiepermana-angular-navigation-DNKU6viC.mjs +2642 -0
- package/fesm2022/ojiepermana-angular-navigation-service.mjs +725 -3
- package/fesm2022/ojiepermana-angular-navigation-types.mjs +65 -4
- package/fesm2022/ojiepermana-angular-navigation.mjs +4 -10
- package/fesm2022/ojiepermana-angular-sdk.mjs +204 -4
- package/fesm2022/ojiepermana-angular-theme-component-settings.mjs +1387 -3
- package/fesm2022/ojiepermana-angular-theme-layout-services.mjs +331 -3
- package/fesm2022/ojiepermana-angular-theme-layout-types.mjs +45 -4
- package/fesm2022/ojiepermana-angular-theme-layout-wrapper.mjs +1044 -3
- package/fesm2022/ojiepermana-angular-theme-layout.mjs +729 -3
- package/fesm2022/ojiepermana-angular-theme-page-core.mjs +381 -3
- package/fesm2022/ojiepermana-angular-theme-page-filter.mjs +267 -3
- package/fesm2022/ojiepermana-angular-theme-page-root.mjs +361 -3
- package/fesm2022/ojiepermana-angular-theme-page-side.mjs +198 -3
- package/fesm2022/ojiepermana-angular-theme-page-slots.mjs +110 -3
- package/fesm2022/ojiepermana-angular-theme-page.mjs +7 -2
- package/fesm2022/ojiepermana-angular-theme-shell-services.mjs +338 -3
- package/fesm2022/ojiepermana-angular-theme-shell-types.mjs +160 -4
- package/fesm2022/ojiepermana-angular-theme-shell.mjs +811 -3
- package/fesm2022/ojiepermana-angular-theme-styles.mjs +783 -3
- package/fesm2022/ojiepermana-angular-theme.mjs +4 -4
- package/fesm2022/ojiepermana-angular.mjs +6 -11
- package/navigation/README.md +374 -0
- package/package.json +81 -20
- package/sdk/README.md +212 -0
- package/sdk/schematics/collection.json +20 -0
- package/sdk/schematics/init/index.js +83 -0
- package/sdk/schematics/init/schema.json +19 -0
- package/sdk/schematics/ng-add/index.js +147 -0
- package/sdk/schematics/ng-add/schema.json +14 -0
- package/sdk/schematics/package.json +3 -0
- package/sdk/schematics/sdk/index.js +175 -0
- package/sdk/schematics/sdk/schema.json +19 -0
- package/sdk/sdk.config.example.json +24 -0
- package/sdk/src/config/loader.js +43 -0
- package/sdk/src/config/schema.js +59 -0
- package/sdk/src/emit/client.js +251 -0
- package/sdk/src/emit/metadata.js +310 -0
- package/sdk/src/emit/models.js +104 -0
- package/sdk/src/emit/navigation.js +56 -0
- package/sdk/src/emit/operations.js +118 -0
- package/sdk/src/emit/public-api.js +54 -0
- package/sdk/src/emit/services.js +110 -0
- package/sdk/src/engine.js +67 -0
- package/sdk/src/layout/per-domain.js +360 -0
- package/sdk/src/package.json +3 -0
- package/sdk/src/parser/bundle.js +25 -0
- package/sdk/src/parser/ir.js +429 -0
- package/sdk/src/parser/types.js +7 -0
- package/sdk/src/render/template.js +152 -0
- package/sdk/src/writer/index.js +286 -0
- package/theme/README.md +126 -0
- package/theme/component/settings/README.md +135 -0
- package/theme/layout/README.md +539 -0
- package/theme/page/README.md +453 -0
- package/theme/shell/README.md +340 -0
- package/theme/styles/README.md +178 -0
- package/theme/styles/css/base/components.css +553 -0
- package/theme/styles/css/base/index.css +14 -0
- package/theme/styles/css/base/package.css +9 -0
- package/theme/styles/css/base/prefers-dark.css +86 -0
- package/theme/styles/css/base/print.css +62 -0
- package/theme/styles/css/base/status.css +38 -0
- package/theme/styles/css/base/tailwind.css +165 -0
- package/theme/styles/css/base/theme.css +374 -0
- package/theme/styles/css/base/tokens.css +361 -0
- package/theme/styles/css/color/amber.css +72 -0
- package/theme/styles/css/color/blue.css +72 -0
- package/theme/styles/css/color/brand.css +38 -0
- package/theme/styles/css/color/cyan.css +72 -0
- package/theme/styles/css/color/emerald.css +72 -0
- package/theme/styles/css/color/fuchsia.css +72 -0
- package/theme/styles/css/color/green.css +72 -0
- package/theme/styles/css/color/index.css +19 -0
- package/theme/styles/css/color/indigo.css +72 -0
- package/theme/styles/css/color/lime.css +72 -0
- package/theme/styles/css/color/orange.css +72 -0
- package/theme/styles/css/color/pink.css +72 -0
- package/theme/styles/css/color/purple.css +72 -0
- package/theme/styles/css/color/red.css +72 -0
- package/theme/styles/css/color/rose.css +72 -0
- package/theme/styles/css/color/sky.css +72 -0
- package/theme/styles/css/color/teal.css +72 -0
- package/theme/styles/css/color/violet.css +72 -0
- package/theme/styles/css/color/yellow.css +72 -0
- package/theme/styles/css/index.css +18 -0
- package/theme/styles/css/neutral/gray.css +63 -0
- package/theme/styles/css/neutral/index.css +11 -0
- package/theme/styles/css/neutral/mauve.css +63 -0
- package/theme/styles/css/neutral/mist.css +63 -0
- package/theme/styles/css/neutral/neutral.css +63 -0
- package/theme/styles/css/neutral/olive.css +63 -0
- package/theme/styles/css/neutral/slate.css +63 -0
- package/theme/styles/css/neutral/stone.css +63 -0
- package/theme/styles/css/neutral/taupe.css +63 -0
- package/theme/styles/css/neutral/zinc.css +63 -0
- package/theme/styles/css/radius/index.css +30 -0
- package/theme/styles/css/space/index.css +20 -0
- package/types/ojiepermana-angular-chart-area.d.ts +76 -1
- package/types/ojiepermana-angular-chart-bar.d.ts +201 -1
- package/types/ojiepermana-angular-chart-core.d.ts +811 -1
- package/types/ojiepermana-angular-chart-line.d.ts +75 -1
- package/types/ojiepermana-angular-chart-pie.d.ts +131 -1
- package/types/ojiepermana-angular-chart-primitives.d.ts +291 -1
- package/types/ojiepermana-angular-chart-radar.d.ts +116 -1
- package/types/ojiepermana-angular-chart-radial.d.ts +116 -1
- package/types/ojiepermana-angular-chart-scatter.d.ts +125 -1
- package/types/ojiepermana-angular-chart.d.ts +2 -1
- package/types/ojiepermana-angular-component-accordion.d.ts +85 -1
- package/types/ojiepermana-angular-component-alert-dialog.d.ts +140 -1
- package/types/ojiepermana-angular-component-alert.d.ts +42 -1
- package/types/ojiepermana-angular-component-aspect-ratio.d.ts +12 -1
- package/types/ojiepermana-angular-component-avatar.d.ts +56 -1
- package/types/ojiepermana-angular-component-badge.d.ts +24 -1
- package/types/ojiepermana-angular-component-breadcrumb.d.ts +46 -1
- package/types/ojiepermana-angular-component-button-group.d.ts +26 -1
- package/types/ojiepermana-angular-component-button.d.ts +27 -1
- package/types/ojiepermana-angular-component-calendar.d.ts +111 -1
- package/types/ojiepermana-angular-component-card.d.ts +69 -1
- package/types/ojiepermana-angular-component-carousel.d.ts +86 -1
- package/types/ojiepermana-angular-component-checkbox.d.ts +42 -1
- package/types/ojiepermana-angular-component-collapsible.d.ts +42 -1
- package/types/ojiepermana-angular-component-combobox.d.ts +60 -1
- package/types/ojiepermana-angular-component-command.d.ts +130 -1
- package/types/ojiepermana-angular-component-composer.d.ts +90 -1
- package/types/ojiepermana-angular-component-context-menu.d.ts +54 -1
- package/types/ojiepermana-angular-component-date-picker.d.ts +109 -1
- package/types/ojiepermana-angular-component-dialog.d.ts +136 -1
- package/types/ojiepermana-angular-component-drawer.d.ts +1 -1
- package/types/ojiepermana-angular-component-dropdown-menu.d.ts +248 -1
- package/types/ojiepermana-angular-component-editor.d.ts +135 -1
- package/types/ojiepermana-angular-component-empty.d.ts +50 -1
- package/types/ojiepermana-angular-component-form.d.ts +159 -1
- package/types/ojiepermana-angular-component-hover-card.d.ts +75 -1
- package/types/ojiepermana-angular-component-icon.d.ts +89 -1
- package/types/ojiepermana-angular-component-input-group.d.ts +51 -1
- package/types/ojiepermana-angular-component-input-otp.d.ts +155 -1
- package/types/ojiepermana-angular-component-input.d.ts +21 -1
- package/types/ojiepermana-angular-component-item.d.ts +93 -1
- package/types/ojiepermana-angular-component-kanban.d.ts +70 -1
- package/types/ojiepermana-angular-component-kbd.d.ts +16 -1
- package/types/ojiepermana-angular-component-label.d.ts +11 -1
- package/types/ojiepermana-angular-component-menubar.d.ts +93 -1
- package/types/ojiepermana-angular-component-native-select.d.ts +26 -1
- package/types/ojiepermana-angular-component-navigation-menu.d.ts +104 -1
- package/types/ojiepermana-angular-component-pagination.d.ts +33 -1
- package/types/ojiepermana-angular-component-pillbox.d.ts +222 -1
- package/types/ojiepermana-angular-component-popover.d.ts +66 -1
- package/types/ojiepermana-angular-component-progress.d.ts +17 -1
- package/types/ojiepermana-angular-component-radio.d.ts +60 -1
- package/types/ojiepermana-angular-component-resizable.d.ts +117 -1
- package/types/ojiepermana-angular-component-scroll-area.d.ts +19 -1
- package/types/ojiepermana-angular-component-segmented-control.d.ts +58 -1
- package/types/ojiepermana-angular-component-select.d.ts +122 -1
- package/types/ojiepermana-angular-component-separator.d.ts +14 -1
- package/types/ojiepermana-angular-component-sheet.d.ts +130 -1
- package/types/ojiepermana-angular-component-skeleton.d.ts +10 -1
- package/types/ojiepermana-angular-component-slider.d.ts +83 -1
- package/types/ojiepermana-angular-component-spinner.d.ts +13 -1
- package/types/ojiepermana-angular-component-switch.d.ts +44 -1
- package/types/ojiepermana-angular-component-table.d.ts +57 -1
- package/types/ojiepermana-angular-component-tabs.d.ts +124 -1
- package/types/ojiepermana-angular-component-textarea.d.ts +17 -1
- package/types/ojiepermana-angular-component-timeline.d.ts +64 -1
- package/types/ojiepermana-angular-component-toast.d.ts +64 -1
- package/types/ojiepermana-angular-component-toggle-group.d.ts +90 -1
- package/types/ojiepermana-angular-component-toggle.d.ts +25 -1
- package/types/ojiepermana-angular-component-tooltip.d.ts +143 -1
- package/types/ojiepermana-angular-component-utils-core.d.ts +184 -1
- package/types/ojiepermana-angular-component-utils-overlay.d.ts +21 -1
- package/types/ojiepermana-angular-component-utils-scale.d.ts +19 -1
- package/types/ojiepermana-angular-component-utils.d.ts +2 -1
- package/types/ojiepermana-angular-component.d.ts +2 -1
- package/types/ojiepermana-angular-navigation-core.d.ts +59 -1
- package/types/ojiepermana-angular-navigation-icon.d.ts +12 -1
- package/types/ojiepermana-angular-navigation-service.d.ts +116 -1
- package/types/ojiepermana-angular-navigation-types.d.ts +144 -1
- package/types/ojiepermana-angular-navigation.d.ts +507 -1
- package/types/ojiepermana-angular-sdk.d.ts +89 -1
- package/types/ojiepermana-angular-theme-component-settings.d.ts +154 -1
- package/types/ojiepermana-angular-theme-layout-services.d.ts +147 -1
- package/types/ojiepermana-angular-theme-layout-types.d.ts +40 -1
- package/types/ojiepermana-angular-theme-layout-wrapper.d.ts +299 -1
- package/types/ojiepermana-angular-theme-layout.d.ts +129 -1
- package/types/ojiepermana-angular-theme-page-core.d.ts +157 -1
- package/types/ojiepermana-angular-theme-page-filter.d.ts +77 -1
- package/types/ojiepermana-angular-theme-page-root.d.ts +88 -1
- package/types/ojiepermana-angular-theme-page-side.d.ts +50 -1
- package/types/ojiepermana-angular-theme-page-slots.d.ts +40 -1
- package/types/ojiepermana-angular-theme-page.d.ts +5 -1
- package/types/ojiepermana-angular-theme-shell-services.d.ts +193 -1
- package/types/ojiepermana-angular-theme-shell-types.d.ts +105 -1
- package/types/ojiepermana-angular-theme-shell.d.ts +100 -1
- package/types/ojiepermana-angular-theme-styles.d.ts +319 -1
- package/types/ojiepermana-angular-theme.d.ts +2 -1
- package/types/ojiepermana-angular.d.ts +5 -5
|
@@ -1,7 +1,1486 @@
|
|
|
1
|
-
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { signal, computed, Service, inject, effect, ElementRef, Renderer2, Component, NgZone, PLATFORM_ID, DestroyRef, viewChild, input, afterNextRender, Directive } from '@angular/core';
|
|
3
|
+
import { isPlatformBrowser, DOCUMENT } from '@angular/common';
|
|
4
|
+
import { scalePoint, scaleLinear, scaleBand } from 'd3-scale';
|
|
5
|
+
import { min, max } from 'd3-array';
|
|
6
|
+
import { curveLinear, curveStep, curveMonotoneX, line, area, stack, stackOffsetExpand } from 'd3-shape';
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
/** CSS selector under which a chart instance is scoped. */
|
|
9
|
+
const CHART_DATA_ATTRIBUTE = 'data-chart';
|
|
10
|
+
/** Default color schemes supported by the generated `<style>` block. */
|
|
11
|
+
const CHART_THEMES = [
|
|
12
|
+
{ key: 'light', selector: '' },
|
|
13
|
+
{ key: 'dark', selector: '[data-mode="dark"]' },
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Characters and sequences that could close the current declaration or rule and
|
|
17
|
+
* start a new one. A series color is interpolated into a `<style>` block, so any
|
|
18
|
+
* of these turns a color into arbitrary CSS.
|
|
19
|
+
*
|
|
20
|
+
* Control characters (newlines included) need no entry here: they are absent from
|
|
21
|
+
* {@link CSS_COLOR_CHARACTERS}, so the allow-list rejects them on its own.
|
|
22
|
+
*/
|
|
23
|
+
const CSS_CONTEXT_BREAKERS = /[;{}<>@\\"']|\/\*|\*\//;
|
|
24
|
+
/** Characters a CSS color value may legitimately contain. */
|
|
25
|
+
const CSS_COLOR_CHARACTERS = /^[a-zA-Z0-9#%.,+\-_()/ ]+$/;
|
|
26
|
+
/**
|
|
27
|
+
* Color functions a series value may call. Anything else (`url`, `attr`,
|
|
28
|
+
* `image-set`, an unknown function) is rejected: none of them belong in a color
|
|
29
|
+
* and `url()` in particular would let a config make a network request.
|
|
30
|
+
*/
|
|
31
|
+
const ALLOWED_COLOR_FUNCTIONS = new Set([
|
|
32
|
+
'rgb',
|
|
33
|
+
'rgba',
|
|
34
|
+
'hsl',
|
|
35
|
+
'hsla',
|
|
36
|
+
'hwb',
|
|
37
|
+
'lab',
|
|
38
|
+
'lch',
|
|
39
|
+
'oklab',
|
|
40
|
+
'oklch',
|
|
41
|
+
'color',
|
|
42
|
+
'color-mix',
|
|
43
|
+
'light-dark',
|
|
44
|
+
'var',
|
|
45
|
+
]);
|
|
46
|
+
/**
|
|
47
|
+
* Whether a value is safe to interpolate into the generated `<style>` block.
|
|
48
|
+
*
|
|
49
|
+
* The check is deliberately layered rather than one clever regular expression:
|
|
50
|
+
*
|
|
51
|
+
* 1. reject anything containing a character that can end a declaration or a
|
|
52
|
+
* rule, which is what actually stops an injection;
|
|
53
|
+
* 2. restrict the remaining characters to those a color can be built from;
|
|
54
|
+
* 3. require balanced parentheses, so a truncated function cannot swallow the
|
|
55
|
+
* rest of the block;
|
|
56
|
+
* 4. require every function called to be a color function.
|
|
57
|
+
*
|
|
58
|
+
* Real values keep working: `#2563eb`, `red`, `var(--chart-2)`,
|
|
59
|
+
* `hsl(var(--chart-1))`, `rgb(0 0 0 / 50%)`, `color-mix(in srgb, red, blue)`.
|
|
60
|
+
*/
|
|
61
|
+
function isSafeCssColorValue(value) {
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
if (!trimmed || trimmed.length > 200)
|
|
64
|
+
return false;
|
|
65
|
+
if (CSS_CONTEXT_BREAKERS.test(trimmed))
|
|
66
|
+
return false;
|
|
67
|
+
if (!CSS_COLOR_CHARACTERS.test(trimmed))
|
|
68
|
+
return false;
|
|
69
|
+
let depth = 0;
|
|
70
|
+
for (const character of trimmed) {
|
|
71
|
+
if (character === '(')
|
|
72
|
+
depth += 1;
|
|
73
|
+
else if (character === ')') {
|
|
74
|
+
depth -= 1;
|
|
75
|
+
if (depth < 0)
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (depth !== 0)
|
|
80
|
+
return false;
|
|
81
|
+
for (const match of trimmed.matchAll(/([a-zA-Z][a-zA-Z0-9-]*)\s*\(/g)) {
|
|
82
|
+
if (!ALLOWED_COLOR_FUNCTIONS.has(match[1].toLowerCase()))
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Generate the CSS rule-set for a chart instance: one `--color-<key>` per
|
|
89
|
+
* series, scoped to the owning `[data-chart]` element, with optional dark
|
|
90
|
+
* variant via `[data-mode="dark"] [data-chart="…"]`.
|
|
91
|
+
*
|
|
92
|
+
* Series without any color are skipped (consumer can fall back to a default), and
|
|
93
|
+
* so is any series whose color fails {@link isSafeCssColorValue}: the value never
|
|
94
|
+
* reaches the stylesheet, and the series falls back to its default exactly as an
|
|
95
|
+
* unset color would. `chartId` is escaped, so it cannot break out of the
|
|
96
|
+
* attribute selector either.
|
|
97
|
+
*
|
|
98
|
+
* @param chartId Unique chart id (used as attribute value).
|
|
99
|
+
* @param config Series configuration map.
|
|
100
|
+
*/
|
|
101
|
+
function buildChartCss(chartId, config) {
|
|
102
|
+
const entries = Object.entries(config).filter(([, cfg]) => cfg.color || cfg.theme);
|
|
103
|
+
if (entries.length === 0) {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
const scopeId = escapeCssIdent(chartId);
|
|
107
|
+
return CHART_THEMES.map(({ key, selector }) => {
|
|
108
|
+
const vars = entries
|
|
109
|
+
.map(([seriesKey, cfg]) => {
|
|
110
|
+
const value = cfg.theme?.[key] ?? cfg.color;
|
|
111
|
+
if (!value || !isSafeCssColorValue(value)) {
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
return ` --color-${escapeCssIdent(seriesKey)}: ${value.trim()};`;
|
|
115
|
+
})
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
.join('\n');
|
|
118
|
+
if (!vars) {
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
const scope = selector
|
|
122
|
+
? `${selector} [${CHART_DATA_ATTRIBUTE}="${scopeId}"]`
|
|
123
|
+
: `[${CHART_DATA_ATTRIBUTE}="${scopeId}"]`;
|
|
124
|
+
return `${scope} {\n${vars}\n}`;
|
|
125
|
+
})
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.join('\n');
|
|
128
|
+
}
|
|
129
|
+
/** What a CSS custom-property identifier and an attribute selector value may hold. */
|
|
130
|
+
const CSS_IDENT_CHARACTERS = /^[a-zA-Z0-9_-]*$/;
|
|
131
|
+
/**
|
|
132
|
+
* The suffix {@link escapeCssIdent} appends. A key that already ends this way is
|
|
133
|
+
* fingerprinted too, so a key that passes through untouched can never land on
|
|
134
|
+
* the identifier some other key was escaped into.
|
|
135
|
+
*/
|
|
136
|
+
const CSS_IDENT_FINGERPRINTED = /_h[0-9a-f]{8}$/;
|
|
137
|
+
/**
|
|
138
|
+
* Escape a string so it is safe to use as a CSS custom-property identifier or as
|
|
139
|
+
* an attribute selector value. The result only ever holds `[A-Za-z0-9_-]`.
|
|
140
|
+
*
|
|
141
|
+
* A string that is ALREADY an identifier passes through untouched, which is
|
|
142
|
+
* every real series key: `sales` stays `--color-sales`.
|
|
143
|
+
*
|
|
144
|
+
* Anything else has its unsafe characters replaced with `_` AND gains a short
|
|
145
|
+
* fingerprint of the ORIGINAL string, because the replacement alone is not
|
|
146
|
+
* injective. `'a b'` and `'a_b'` both collapse to `a_b`, and two series sharing
|
|
147
|
+
* one custom property is silent data loss: {@link buildChartCss} emits both
|
|
148
|
+
* declarations into the same rule so the last one wins, and
|
|
149
|
+
* {@link seriesColorVar} then resolves both series to that one token.
|
|
150
|
+
*/
|
|
151
|
+
function escapeCssIdent(input) {
|
|
152
|
+
if (CSS_IDENT_CHARACTERS.test(input) && !CSS_IDENT_FINGERPRINTED.test(input)) {
|
|
153
|
+
return input;
|
|
154
|
+
}
|
|
155
|
+
return `${input.replace(/[^a-zA-Z0-9_-]/g, '_')}_h${fingerprint(input)}`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* FNV-1a, 32 bits, hex. A short stable fingerprint used only to keep two escaped
|
|
159
|
+
* keys apart — not a security hash, and never parsed back.
|
|
160
|
+
*/
|
|
161
|
+
function fingerprint(input) {
|
|
162
|
+
let hash = 0x811c9dc5;
|
|
163
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
164
|
+
hash ^= input.charCodeAt(index);
|
|
165
|
+
hash = Math.imul(hash, 0x01000193);
|
|
166
|
+
}
|
|
167
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
168
|
+
}
|
|
169
|
+
/** Resolve the `var(--color-<key>)` reference for a given series. */
|
|
170
|
+
function seriesColorVar(seriesKey) {
|
|
171
|
+
return `var(--color-${escapeCssIdent(seriesKey)})`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Resolve a raw per-datum colour to a paint value for an SVG attribute
|
|
175
|
+
* (`[attr.fill]`, `[attr.stroke]`).
|
|
176
|
+
*
|
|
177
|
+
* `raw` is `unknown` so a caller can hand over `datum[colorKey]` directly
|
|
178
|
+
* without its own `typeof` guard.
|
|
179
|
+
*
|
|
180
|
+
* The rule, which is the union of the two copies this replaces:
|
|
181
|
+
* 1. not a string, or empty -> `fallback`
|
|
182
|
+
* 2. looks like a CSS colour or calls a
|
|
183
|
+
* function (`var(`, `#`, `rgb`,
|
|
184
|
+
* `hsl`, or a `(` anywhere) -> used verbatim
|
|
185
|
+
* 3. anything else is a series key -> `var(--color-<key>)`
|
|
186
|
+
*
|
|
187
|
+
* Deliberately NOT added: no `.trim()`, and no named-colour detector. Either
|
|
188
|
+
* would move an input that today lands on branch 3, so `'red'` stays a series
|
|
189
|
+
* key rather than becoming the colour red. That is the existing behaviour and
|
|
190
|
+
* changing it is a separate decision.
|
|
191
|
+
*/
|
|
192
|
+
function resolveSeriesColor(raw, fallback) {
|
|
193
|
+
if (typeof raw !== 'string' || raw.length === 0) {
|
|
194
|
+
return fallback;
|
|
195
|
+
}
|
|
196
|
+
if (raw.startsWith('var(') ||
|
|
197
|
+
raw.startsWith('#') ||
|
|
198
|
+
raw.startsWith('rgb') ||
|
|
199
|
+
raw.startsWith('hsl') ||
|
|
200
|
+
raw.includes('(')) {
|
|
201
|
+
return raw;
|
|
202
|
+
}
|
|
203
|
+
return seriesColorVar(raw);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Shared chart state provided by `ChartContainer` to all nested chart
|
|
208
|
+
* components (axes, grid, tooltip, legend, chart types).
|
|
209
|
+
*
|
|
210
|
+
* All state is exposed as signals so consumers can derive computed state
|
|
211
|
+
* (scales, visible series, tooltip position) without manual subscriptions.
|
|
212
|
+
*/
|
|
213
|
+
class ChartContext {
|
|
214
|
+
/** Stable instance id — used in the `data-chart` attribute and CSS scope. */
|
|
215
|
+
id = signal('', /* @ts-ignore */
|
|
216
|
+
...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
|
|
217
|
+
/** User-provided series config. */
|
|
218
|
+
config = signal({}, /* @ts-ignore */
|
|
219
|
+
...(ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []));
|
|
220
|
+
/** Measured render-area dimensions (ResizeObserver-driven). */
|
|
221
|
+
dimensions = signal({ width: 0, height: 0 }, /* @ts-ignore */
|
|
222
|
+
...(ngDevMode ? [{ debugName: "dimensions" }] : /* istanbul ignore next */ []));
|
|
223
|
+
/** Currently highlighted data point (tooltip / crosshair). */
|
|
224
|
+
activePoint = signal(null, /* @ts-ignore */
|
|
225
|
+
...(ngDevMode ? [{ debugName: "activePoint" }] : /* istanbul ignore next */ []));
|
|
226
|
+
/** Series keys the user has toggled off via legend. */
|
|
227
|
+
hiddenSeries = signal(new Set(), /* @ts-ignore */
|
|
228
|
+
...(ngDevMode ? [{ debugName: "hiddenSeries" }] : /* istanbul ignore next */ []));
|
|
229
|
+
/** Ordered list of series keys (from `config`). */
|
|
230
|
+
seriesKeys = computed(() => Object.keys(this.config()), /* @ts-ignore */
|
|
231
|
+
...(ngDevMode ? [{ debugName: "seriesKeys" }] : /* istanbul ignore next */ []));
|
|
232
|
+
/** Series keys currently visible (config order minus `hiddenSeries`). */
|
|
233
|
+
visibleSeriesKeys = computed(() => {
|
|
234
|
+
const hidden = this.hiddenSeries();
|
|
235
|
+
return this.seriesKeys().filter((k) => !hidden.has(k));
|
|
236
|
+
}, /* @ts-ignore */
|
|
237
|
+
...(ngDevMode ? [{ debugName: "visibleSeriesKeys" }] : /* istanbul ignore next */ []));
|
|
238
|
+
/** Toggle visibility of a series. */
|
|
239
|
+
toggleSeries(key) {
|
|
240
|
+
this.hiddenSeries.update((set) => {
|
|
241
|
+
const next = new Set(set);
|
|
242
|
+
if (next.has(key)) {
|
|
243
|
+
next.delete(key);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
next.add(key);
|
|
247
|
+
}
|
|
248
|
+
return next;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartContext, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
252
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.4", ngImport: i0, type: ChartContext, autoProvided: false });
|
|
253
|
+
}
|
|
254
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartContext, decorators: [{
|
|
255
|
+
type: Service,
|
|
256
|
+
args: [{ autoProvided: false }]
|
|
257
|
+
}] });
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Given a pointer event's local (x, y) relative to the chart's inner group,
|
|
261
|
+
* resolve the nearest category index along the categorical axis.
|
|
262
|
+
*
|
|
263
|
+
* @returns index into `ctx.categories()` (or −1 if no scale / no data).
|
|
264
|
+
*/
|
|
265
|
+
function nearestCategoryIndex(ctx, localX, localY) {
|
|
266
|
+
const scale = ctx.categoryScale();
|
|
267
|
+
const categories = ctx.categories();
|
|
268
|
+
if (!scale || categories.length === 0)
|
|
269
|
+
return -1;
|
|
270
|
+
const isVertical = ctx.orientation() === 'vertical';
|
|
271
|
+
const pointerAlong = isVertical ? localX : localY;
|
|
272
|
+
const bandwidth = scale.bandwidth();
|
|
273
|
+
// scale.step() is only defined for band scales; fall back to bandwidth.
|
|
274
|
+
const step = scale.step?.() ?? bandwidth;
|
|
275
|
+
let bestIndex = -1;
|
|
276
|
+
let bestDelta = Infinity;
|
|
277
|
+
for (let i = 0; i < categories.length; i++) {
|
|
278
|
+
const base = scale(categories[i]) ?? 0;
|
|
279
|
+
const center = base + bandwidth / 2;
|
|
280
|
+
const delta = Math.abs(pointerAlong - center);
|
|
281
|
+
if (delta < bestDelta) {
|
|
282
|
+
bestDelta = delta;
|
|
283
|
+
bestIndex = i;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Ignore clicks far outside any band (> 1 step away).
|
|
287
|
+
if (bestDelta > step) {
|
|
288
|
+
return -1;
|
|
289
|
+
}
|
|
290
|
+
return bestIndex;
|
|
291
|
+
}
|
|
292
|
+
/** Resolve the client-space center point of a focused or clicked SVG/HTML element. */
|
|
293
|
+
function elementClientCenter(target) {
|
|
294
|
+
const el = target;
|
|
295
|
+
if (!el || typeof el.getBoundingClientRect !== 'function') {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const rect = el.getBoundingClientRect();
|
|
299
|
+
return {
|
|
300
|
+
clientX: rect.left + rect.width / 2,
|
|
301
|
+
clientY: rect.top + rect.height / 2,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Where the tooltip should sit for whichever gesture activated a mark.
|
|
306
|
+
*
|
|
307
|
+
* A pointer carries a real cursor position, so the tooltip follows it. Keyboard
|
|
308
|
+
* focus carries none, and falls back to the centre of the mark that took focus.
|
|
309
|
+
*/
|
|
310
|
+
function activationClientPoint(event) {
|
|
311
|
+
return event instanceof PointerEvent
|
|
312
|
+
? { clientX: event.clientX, clientY: event.clientY }
|
|
313
|
+
: elementClientCenter(event.currentTarget ?? event.target);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function clamp(value, min, max) {
|
|
317
|
+
return Math.min(max, Math.max(min, value));
|
|
318
|
+
}
|
|
319
|
+
function normalizeIndexRange(start, end, maxCount) {
|
|
320
|
+
if (maxCount <= 0) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const lo = clamp(Math.floor(Math.min(start, end)), 0, maxCount - 1);
|
|
324
|
+
const hi = clamp(Math.floor(Math.max(start, end)), 0, maxCount - 1);
|
|
325
|
+
return { startIndex: lo, endIndex: hi };
|
|
326
|
+
}
|
|
327
|
+
function effectiveIndexRange(range, maxCount) {
|
|
328
|
+
if (maxCount <= 0) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
return range
|
|
332
|
+
? normalizeIndexRange(range.startIndex, range.endIndex, maxCount)
|
|
333
|
+
: { startIndex: 0, endIndex: maxCount - 1 };
|
|
334
|
+
}
|
|
335
|
+
function indexRangeSize(range, maxCount) {
|
|
336
|
+
const effective = effectiveIndexRange(range, maxCount);
|
|
337
|
+
return effective ? effective.endIndex - effective.startIndex + 1 : 0;
|
|
338
|
+
}
|
|
339
|
+
function sliceByIndexRange(data, range) {
|
|
340
|
+
if (!range) {
|
|
341
|
+
return data;
|
|
342
|
+
}
|
|
343
|
+
return data.slice(range.startIndex, range.endIndex + 1);
|
|
344
|
+
}
|
|
345
|
+
function zoomIndexRange(current, maxCount, anchorIndex, factor) {
|
|
346
|
+
const base = effectiveIndexRange(current, maxCount);
|
|
347
|
+
if (!base) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const currentSize = base.endIndex - base.startIndex + 1;
|
|
351
|
+
const nextSize = clamp(Math.round(currentSize * factor), 2, maxCount);
|
|
352
|
+
if (nextSize >= maxCount) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
const boundedAnchor = clamp(anchorIndex, base.startIndex, base.endIndex);
|
|
356
|
+
const ratio = currentSize <= 1 ? 0.5 : (boundedAnchor - base.startIndex) / (currentSize - 1);
|
|
357
|
+
let start = Math.round(boundedAnchor - ratio * (nextSize - 1));
|
|
358
|
+
start = clamp(start, 0, maxCount - nextSize);
|
|
359
|
+
return { startIndex: start, endIndex: start + nextSize - 1 };
|
|
360
|
+
}
|
|
361
|
+
function panIndexRange(current, maxCount, deltaSteps) {
|
|
362
|
+
const base = effectiveIndexRange(current, maxCount);
|
|
363
|
+
if (!base) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
const size = base.endIndex - base.startIndex + 1;
|
|
367
|
+
if (size >= maxCount) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
const start = clamp(base.startIndex + deltaSteps, 0, maxCount - size);
|
|
371
|
+
return { startIndex: start, endIndex: start + size - 1 };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* The value domain for a chart drawn against a zero baseline.
|
|
375
|
+
*
|
|
376
|
+
* A bar, a line, or an area is read against the origin, so the domain has to
|
|
377
|
+
* reach it in both directions: negative data below, positive above. Clamping
|
|
378
|
+
* one end at zero is what pushed negative marks outside the plot.
|
|
379
|
+
*
|
|
380
|
+
* A dataset that is entirely zero would collapse the domain onto a point, which
|
|
381
|
+
* d3 maps to the middle of the range for every value, so it is opened by one
|
|
382
|
+
* unit instead.
|
|
383
|
+
*/
|
|
384
|
+
function zeroBaselineDomain(minValue, maxValue) {
|
|
385
|
+
const min = Math.min(0, minValue);
|
|
386
|
+
const max = Math.max(0, maxValue);
|
|
387
|
+
return min === max ? [min, max + 1] : [min, max];
|
|
388
|
+
}
|
|
389
|
+
function normalizeNumericDomain(a, b) {
|
|
390
|
+
if (a === b) {
|
|
391
|
+
return [a - 1, b + 1];
|
|
392
|
+
}
|
|
393
|
+
return a < b ? [a, b] : [b, a];
|
|
394
|
+
}
|
|
395
|
+
function zoomNumericDomain(current, full, anchor, factor) {
|
|
396
|
+
const currentWidth = current[1] - current[0];
|
|
397
|
+
const fullWidth = full[1] - full[0];
|
|
398
|
+
const nextWidth = clamp(currentWidth * factor, fullWidth / 50, fullWidth);
|
|
399
|
+
if (nextWidth >= fullWidth) {
|
|
400
|
+
return full;
|
|
401
|
+
}
|
|
402
|
+
const ratio = currentWidth === 0 ? 0.5 : (anchor - current[0]) / currentWidth;
|
|
403
|
+
let start = anchor - ratio * nextWidth;
|
|
404
|
+
start = clamp(start, full[0], full[1] - nextWidth);
|
|
405
|
+
return [start, start + nextWidth];
|
|
406
|
+
}
|
|
407
|
+
function panNumericDomain(current, full, delta) {
|
|
408
|
+
const width = current[1] - current[0];
|
|
409
|
+
const start = clamp(current[0] + delta, full[0], full[1] - width);
|
|
410
|
+
return [start, start + width];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Build the shared cartesian scaffolding.
|
|
415
|
+
*
|
|
416
|
+
* MUST be called in an injection context, so a field initializer or the
|
|
417
|
+
* constructor: it injects `ChartContext` and creates one `effect`.
|
|
418
|
+
*/
|
|
419
|
+
function setupCartesianChart(options) {
|
|
420
|
+
const root = inject(ChartContext);
|
|
421
|
+
const innerWidth = computed(() => Math.max(0, root.dimensions().width - options.margin().left - options.margin().right), /* @ts-ignore */
|
|
422
|
+
...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
|
|
423
|
+
const innerHeight = computed(() => Math.max(0, root.dimensions().height - options.margin().top - options.margin().bottom), /* @ts-ignore */
|
|
424
|
+
...(ngDevMode ? [{ debugName: "innerHeight" }] : /* istanbul ignore next */ []));
|
|
425
|
+
const viewBox = computed(() => {
|
|
426
|
+
const { width, height } = root.dimensions();
|
|
427
|
+
return `0 0 ${Math.max(0, width)} ${Math.max(0, height)}`;
|
|
428
|
+
}, /* @ts-ignore */
|
|
429
|
+
...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
|
|
430
|
+
const innerTransform = computed(() => `translate(${options.margin().left},${options.margin().top})`, /* @ts-ignore */
|
|
431
|
+
...(ngDevMode ? [{ debugName: "innerTransform" }] : /* istanbul ignore next */ []));
|
|
432
|
+
// The saved zoom range outlives the data it was drawn on. Renormalizing it
|
|
433
|
+
// against the current row count is what keeps a shrunken dataset from
|
|
434
|
+
// slicing to nothing and reporting a window that no longer exists.
|
|
435
|
+
const windowRange = computed(() => options.viewport
|
|
436
|
+
? effectiveIndexRange(options.viewport.zoomRange(), options.data().length)
|
|
437
|
+
: null, /* @ts-ignore */
|
|
438
|
+
...(ngDevMode ? [{ debugName: "windowRange" }] : /* istanbul ignore next */ []));
|
|
439
|
+
const visibleStartIndex = computed(() => windowRange()?.startIndex ?? 0, /* @ts-ignore */
|
|
440
|
+
...(ngDevMode ? [{ debugName: "visibleStartIndex" }] : /* istanbul ignore next */ []));
|
|
441
|
+
const visibleData = computed(() => options.viewport ? sliceByIndexRange(options.data(), windowRange()) : options.data(), /* @ts-ignore */
|
|
442
|
+
...(ngDevMode ? [{ debugName: "visibleData" }] : /* istanbul ignore next */ []));
|
|
443
|
+
const resolveDatumIndex = (mark) => visibleStartIndex() + mark.datumIndex;
|
|
444
|
+
const seriesLabel = (seriesKey) => root.config()[seriesKey]?.label ?? seriesKey;
|
|
445
|
+
// One effect for all of it. Reading every signal here keeps them tracked
|
|
446
|
+
// together, so a geometry change publishes the layout exactly once.
|
|
447
|
+
effect(() => {
|
|
448
|
+
options.viewport?.dataCount.set(options.data().length);
|
|
449
|
+
const cartesian = options.cartesian;
|
|
450
|
+
if (cartesian) {
|
|
451
|
+
cartesian.context.orientation.set(cartesian.orientation());
|
|
452
|
+
cartesian.context.margin.set(options.margin());
|
|
453
|
+
cartesian.context.innerWidth.set(innerWidth());
|
|
454
|
+
cartesian.context.innerHeight.set(innerHeight());
|
|
455
|
+
}
|
|
456
|
+
options.publishLayout?.({ width: innerWidth(), height: innerHeight() });
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
innerWidth,
|
|
460
|
+
innerHeight,
|
|
461
|
+
viewBox,
|
|
462
|
+
innerTransform,
|
|
463
|
+
visibleStartIndex,
|
|
464
|
+
visibleData,
|
|
465
|
+
resolveDatumIndex,
|
|
466
|
+
seriesLabel,
|
|
467
|
+
pointAriaLabel: (mark) => options.ariaLabel(mark, seriesLabel(mark.seriesKey)),
|
|
468
|
+
emitClick: (mark) => {
|
|
469
|
+
const datumIndex = resolveDatumIndex(mark);
|
|
470
|
+
options.click.emit(options.click.payload(mark, datumIndex, options.data()[datumIndex]));
|
|
471
|
+
},
|
|
472
|
+
setActivePoint: (event, mark) => {
|
|
473
|
+
const center = activationClientPoint(event);
|
|
474
|
+
root.activePoint.set({
|
|
475
|
+
// `index` stays visible-local (it addresses the rendered mark) while
|
|
476
|
+
// `datumIndex` is absolute (it addresses the data row). Both are needed.
|
|
477
|
+
index: mark.datumIndex,
|
|
478
|
+
datumIndex: resolveDatumIndex(mark),
|
|
479
|
+
seriesKey: mark.seriesKey,
|
|
480
|
+
clientX: center?.clientX,
|
|
481
|
+
clientY: center?.clientY,
|
|
482
|
+
});
|
|
483
|
+
},
|
|
484
|
+
clearActivePoint: () => root.activePoint.set(null),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Build the shared polar scaffolding.
|
|
490
|
+
*
|
|
491
|
+
* MUST be called in an injection context, so a field initializer or the
|
|
492
|
+
* constructor: it injects `ChartContext`.
|
|
493
|
+
*/
|
|
494
|
+
function setupPolarChart(options) {
|
|
495
|
+
const root = inject(ChartContext);
|
|
496
|
+
const innerWidth = computed(() => Math.max(0, root.dimensions().width - options.margin().left - options.margin().right), /* @ts-ignore */
|
|
497
|
+
...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
|
|
498
|
+
const innerHeight = computed(() => Math.max(0, root.dimensions().height - options.margin().top - options.margin().bottom), /* @ts-ignore */
|
|
499
|
+
...(ngDevMode ? [{ debugName: "innerHeight" }] : /* istanbul ignore next */ []));
|
|
500
|
+
const viewBox = computed(() => {
|
|
501
|
+
const { width, height } = root.dimensions();
|
|
502
|
+
return `0 0 ${Math.max(0, width)} ${Math.max(0, height)}`;
|
|
503
|
+
}, /* @ts-ignore */
|
|
504
|
+
...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
|
|
505
|
+
const innerTransform = computed(() => `translate(${options.margin().left},${options.margin().top})`, /* @ts-ignore */
|
|
506
|
+
...(ngDevMode ? [{ debugName: "innerTransform" }] : /* istanbul ignore next */ []));
|
|
507
|
+
return {
|
|
508
|
+
innerWidth,
|
|
509
|
+
innerHeight,
|
|
510
|
+
viewBox,
|
|
511
|
+
innerTransform,
|
|
512
|
+
seriesLabel: (seriesKey) => root.config()[seriesKey]?.label ?? seriesKey,
|
|
513
|
+
setActivePoint: (event, mark) => {
|
|
514
|
+
const location = activationClientPoint(event);
|
|
515
|
+
root.activePoint.set({
|
|
516
|
+
// A polar mark addresses one row, so the rendered index and the data
|
|
517
|
+
// index are the same number. Both are set because the tooltip and the
|
|
518
|
+
// crosshair read different ones.
|
|
519
|
+
index: mark.datumIndex,
|
|
520
|
+
datumIndex: mark.datumIndex,
|
|
521
|
+
seriesKey: mark.seriesKey,
|
|
522
|
+
clientX: location?.clientX,
|
|
523
|
+
clientY: location?.clientY,
|
|
524
|
+
});
|
|
525
|
+
},
|
|
526
|
+
clearActivePoint: () => root.activePoint.set(null),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Emits a scoped `<style>` block mapping every series key in the current
|
|
532
|
+
* `ChartConfig` to a `--color-<key>` CSS custom property, scoped to
|
|
533
|
+
* `[data-chart="<id>"]`. Dark-mode values are scoped under `[data-mode="dark"]`.
|
|
534
|
+
*
|
|
535
|
+
* Implemented as an empty component that injects a real `<style>` element
|
|
536
|
+
* into its host via `Renderer2`. We avoid rendering `<style>` directly in a
|
|
537
|
+
* template — Angular hoists those into component CSS and strips
|
|
538
|
+
* interpolations from them.
|
|
539
|
+
*
|
|
540
|
+
* Consumers never instantiate this directly — `ChartContainer` renders it.
|
|
541
|
+
*/
|
|
542
|
+
class ChartStyle {
|
|
543
|
+
ctx = inject(ChartContext);
|
|
544
|
+
host = inject((ElementRef));
|
|
545
|
+
renderer = inject(Renderer2);
|
|
546
|
+
styleEl = null;
|
|
547
|
+
css = computed(() => buildChartCss(this.ctx.id(), this.ctx.config()), /* @ts-ignore */
|
|
548
|
+
...(ngDevMode ? [{ debugName: "css" }] : /* istanbul ignore next */ []));
|
|
549
|
+
constructor() {
|
|
550
|
+
effect(() => {
|
|
551
|
+
const css = this.css();
|
|
552
|
+
if (!this.styleEl) {
|
|
553
|
+
this.styleEl = this.renderer.createElement('style');
|
|
554
|
+
this.renderer.appendChild(this.host.nativeElement, this.styleEl);
|
|
555
|
+
}
|
|
556
|
+
this.styleEl.textContent = css;
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartStyle, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
560
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.4", type: ChartStyle, isStandalone: true, selector: "ChartStyle", ngImport: i0, template: '', isInline: true });
|
|
561
|
+
}
|
|
562
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartStyle, decorators: [{
|
|
563
|
+
type: Component,
|
|
564
|
+
args: [{
|
|
565
|
+
selector: 'ChartStyle',
|
|
566
|
+
template: '',
|
|
567
|
+
}]
|
|
568
|
+
}], ctorParameters: () => [] });
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Resolve the effective chart style from the canonical `styleVariant` input and
|
|
572
|
+
* the deprecated `styles` input.
|
|
573
|
+
*
|
|
574
|
+
* `styleVariant` wins whenever it is bound, which is the only direction that is
|
|
575
|
+
* implementable: `styles` keeps its `'base'` default, so there is no way to tell
|
|
576
|
+
* an explicit `styles="base"` from an unset one. Giving `styleVariant` the
|
|
577
|
+
* `undefined` default leaves the deprecated input's emitted type untouched.
|
|
578
|
+
*
|
|
579
|
+
* The precedence rule lives here alone, so the seven chart families cannot drift
|
|
580
|
+
* from each other. The two `input()` calls themselves have to stay literal in
|
|
581
|
+
* each class body: the compiler discovers signal inputs by looking at the field
|
|
582
|
+
* initializer, and an `input()` hidden behind a helper is never registered.
|
|
583
|
+
*/
|
|
584
|
+
function resolveChartStyle(styleVariant, styles) {
|
|
585
|
+
return computed(() => styleVariant() ?? styles());
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
let chartIdCounter = 0;
|
|
589
|
+
/**
|
|
590
|
+
* Root of every chart. Provides `ChartContext` to descendants, reflects the
|
|
591
|
+
* chart id via `data-chart`, injects the per-instance CSS-variable
|
|
592
|
+
* `<style>` block, and tracks its render-area dimensions via
|
|
593
|
+
* `ResizeObserver`.
|
|
594
|
+
*
|
|
595
|
+
* Usage:
|
|
596
|
+
* ```html
|
|
597
|
+
* <Chart [config]="cfg">
|
|
598
|
+
* <ChartBar [data]="data" />
|
|
599
|
+
* <ChartLegend />
|
|
600
|
+
* </Chart>
|
|
601
|
+
* ```
|
|
602
|
+
*
|
|
603
|
+
* The chart render area is laid out on top; any projected `<ChartLegend>` is
|
|
604
|
+
* rendered as a centered row beneath it. Render-area dimensions are measured on
|
|
605
|
+
* the chart area only, so the legend never shrinks the chart.
|
|
606
|
+
*/
|
|
607
|
+
class ChartContainer {
|
|
608
|
+
ctx = inject(ChartContext);
|
|
609
|
+
zone = inject(NgZone);
|
|
610
|
+
platformId = inject(PLATFORM_ID);
|
|
611
|
+
destroyRef = inject(DestroyRef);
|
|
612
|
+
areaRef = viewChild.required('area', /* @ts-ignore */
|
|
613
|
+
...(ngDevMode ? [{ debugName: "areaRef" }] : /* istanbul ignore next */ []));
|
|
614
|
+
/** Series configuration. Required for color / label resolution. */
|
|
615
|
+
config = input.required(/* @ts-ignore */
|
|
616
|
+
...(ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []));
|
|
617
|
+
/**
|
|
618
|
+
* Tailwind aspect-ratio utility for the chart render area. Defaults to
|
|
619
|
+
* `aspect-video` for cartesian charts; override with `aspect-square` for
|
|
620
|
+
* radial / pie layouts, or e.g. `aspect-auto h-80` for a fixed height.
|
|
621
|
+
*/
|
|
622
|
+
aspect = input('aspect-video', /* @ts-ignore */
|
|
623
|
+
...(ngDevMode ? [{ debugName: "aspect" }] : /* istanbul ignore next */ []));
|
|
624
|
+
areaClass = computed(() => `relative flex ${this.aspect()} w-full justify-center`, /* @ts-ignore */
|
|
625
|
+
...(ngDevMode ? [{ debugName: "areaClass" }] : /* istanbul ignore next */ []));
|
|
626
|
+
/**
|
|
627
|
+
* Optional explicit id override. When omitted, a stable auto-id is
|
|
628
|
+
* generated (`chart-<n>`), unique across the document.
|
|
629
|
+
*/
|
|
630
|
+
chartId = input(null, /* @ts-ignore */
|
|
631
|
+
...(ngDevMode ? [{ debugName: "chartId" }] : /* istanbul ignore next */ []));
|
|
632
|
+
constructor() {
|
|
633
|
+
const autoId = `chart-${++chartIdCounter}`;
|
|
634
|
+
// Sync id + config into the shared context.
|
|
635
|
+
effect(() => {
|
|
636
|
+
this.ctx.id.set(this.chartId() ?? autoId);
|
|
637
|
+
});
|
|
638
|
+
effect(() => {
|
|
639
|
+
this.ctx.config.set(this.config());
|
|
640
|
+
});
|
|
641
|
+
// Observe host size (browser only; client-only is a confirmed constraint).
|
|
642
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
643
|
+
afterNextRender(() => this.observeSize());
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
observeSize() {
|
|
647
|
+
const el = this.areaRef().nativeElement;
|
|
648
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
649
|
+
const rect = el.getBoundingClientRect();
|
|
650
|
+
this.ctx.dimensions.set({ width: rect.width, height: rect.height });
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const observer = new ResizeObserver((entries) => {
|
|
654
|
+
for (const entry of entries) {
|
|
655
|
+
const { width, height } = entry.contentRect;
|
|
656
|
+
// Avoid NgZone churn — dimensions signal drives CD on its own.
|
|
657
|
+
this.zone.run(() => this.ctx.dimensions.set({ width, height }));
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
observer.observe(el);
|
|
661
|
+
this.destroyRef.onDestroy(() => observer.disconnect());
|
|
662
|
+
}
|
|
663
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartContainer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
664
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.4", type: ChartContainer, isStandalone: true, selector: "Chart", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, aspect: { classPropertyName: "aspect", publicName: "aspect", isSignal: true, isRequired: false, transformFunction: null }, chartId: { classPropertyName: "chartId", publicName: "chartId", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-chart": "ctx.id()" }, classAttribute: "relative flex flex-col text-xs" }, providers: [ChartContext], viewQueries: [{ propertyName: "areaRef", first: true, predicate: ["area"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
665
|
+
<ChartStyle />
|
|
666
|
+
<div #area data-chart-area [class]="areaClass()">
|
|
667
|
+
<ng-content />
|
|
668
|
+
</div>
|
|
669
|
+
<ng-content select="ChartLegend" />
|
|
670
|
+
`, isInline: true, dependencies: [{ kind: "component", type: ChartStyle, selector: "ChartStyle" }] });
|
|
671
|
+
}
|
|
672
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartContainer, decorators: [{
|
|
673
|
+
type: Component,
|
|
674
|
+
args: [{
|
|
675
|
+
selector: 'Chart',
|
|
676
|
+
providers: [ChartContext],
|
|
677
|
+
imports: [ChartStyle],
|
|
678
|
+
host: {
|
|
679
|
+
'[attr.data-chart]': 'ctx.id()',
|
|
680
|
+
class: 'relative flex flex-col text-xs',
|
|
681
|
+
},
|
|
682
|
+
template: `
|
|
683
|
+
<ChartStyle />
|
|
684
|
+
<div #area data-chart-area [class]="areaClass()">
|
|
685
|
+
<ng-content />
|
|
686
|
+
</div>
|
|
687
|
+
<ng-content select="ChartLegend" />
|
|
688
|
+
`,
|
|
689
|
+
}]
|
|
690
|
+
}], ctorParameters: () => [], propDecorators: { areaRef: [{ type: i0.ViewChild, args: ['area', { isSignal: true }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], aspect: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspect", required: false }] }], chartId: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartId", required: false }] }] } });
|
|
691
|
+
|
|
692
|
+
/** Screen-reader fallback for charts whose visual SVG is not tabular content. */
|
|
693
|
+
class ChartDataTable {
|
|
694
|
+
data = input.required(/* @ts-ignore */
|
|
695
|
+
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
696
|
+
columns = input.required(/* @ts-ignore */
|
|
697
|
+
...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
698
|
+
label = input('Chart data', /* @ts-ignore */
|
|
699
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
700
|
+
rowHeaderKey = input(null, /* @ts-ignore */
|
|
701
|
+
...(ngDevMode ? [{ debugName: "rowHeaderKey" }] : /* istanbul ignore next */ []));
|
|
702
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartDataTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
703
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: ChartDataTable, isStandalone: true, selector: "ChartDataTable", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, rowHeaderKey: { classPropertyName: "rowHeaderKey", publicName: "rowHeaderKey", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
704
|
+
<table class="sr-only" [attr.aria-label]="label()">
|
|
705
|
+
<thead>
|
|
706
|
+
<tr>
|
|
707
|
+
@for (column of columns(); track column) {
|
|
708
|
+
<th scope="col">{{ column }}</th>
|
|
709
|
+
}
|
|
710
|
+
</tr>
|
|
711
|
+
</thead>
|
|
712
|
+
<tbody>
|
|
713
|
+
@for (row of data(); track $index) {
|
|
714
|
+
<tr>
|
|
715
|
+
@for (column of columns(); track column) {
|
|
716
|
+
@if (column === rowHeaderKey()) {
|
|
717
|
+
<th scope="row">{{ row[column] }}</th>
|
|
718
|
+
} @else {
|
|
719
|
+
<td>{{ row[column] }}</td>
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
</tr>
|
|
723
|
+
}
|
|
724
|
+
</tbody>
|
|
725
|
+
</table>
|
|
726
|
+
`, isInline: true });
|
|
727
|
+
}
|
|
728
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartDataTable, decorators: [{
|
|
729
|
+
type: Component,
|
|
730
|
+
args: [{
|
|
731
|
+
selector: 'ChartDataTable',
|
|
732
|
+
template: `
|
|
733
|
+
<table class="sr-only" [attr.aria-label]="label()">
|
|
734
|
+
<thead>
|
|
735
|
+
<tr>
|
|
736
|
+
@for (column of columns(); track column) {
|
|
737
|
+
<th scope="col">{{ column }}</th>
|
|
738
|
+
}
|
|
739
|
+
</tr>
|
|
740
|
+
</thead>
|
|
741
|
+
<tbody>
|
|
742
|
+
@for (row of data(); track $index) {
|
|
743
|
+
<tr>
|
|
744
|
+
@for (column of columns(); track column) {
|
|
745
|
+
@if (column === rowHeaderKey()) {
|
|
746
|
+
<th scope="row">{{ row[column] }}</th>
|
|
747
|
+
} @else {
|
|
748
|
+
<td>{{ row[column] }}</td>
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
</tr>
|
|
752
|
+
}
|
|
753
|
+
</tbody>
|
|
754
|
+
</table>
|
|
755
|
+
`,
|
|
756
|
+
}]
|
|
757
|
+
}], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], rowHeaderKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeaderKey", required: false }] }] } });
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Keeps dense SVG charts to one tab stop while preserving arrow-key access to
|
|
761
|
+
* every interactive mark. Marks are discovered by their `role="img"` and
|
|
762
|
+
* `tabindex` attributes so chart implementations do not need a shared base
|
|
763
|
+
* class or additional DOM wrappers.
|
|
764
|
+
*/
|
|
765
|
+
class ChartRovingFocus {
|
|
766
|
+
host = inject(ElementRef);
|
|
767
|
+
destroyRef = inject(DestroyRef);
|
|
768
|
+
activeMark = null;
|
|
769
|
+
constructor() {
|
|
770
|
+
afterNextRender(() => {
|
|
771
|
+
this.syncTabStops(this.marks());
|
|
772
|
+
if (typeof MutationObserver === 'undefined') {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const observer = new MutationObserver(() => this.syncTabStops(this.marks()));
|
|
776
|
+
observer.observe(this.host.nativeElement, { childList: true, subtree: true });
|
|
777
|
+
this.destroyRef.onDestroy(() => observer.disconnect());
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
onFocusIn(event) {
|
|
781
|
+
const target = event.target;
|
|
782
|
+
if (!(target instanceof Element) || !this.marks().includes(target)) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
this.activeMark = target;
|
|
786
|
+
this.syncTabStops(this.marks());
|
|
787
|
+
}
|
|
788
|
+
onKeydown(event) {
|
|
789
|
+
const marks = this.marks();
|
|
790
|
+
const target = event.target;
|
|
791
|
+
const currentIndex = target instanceof Element ? marks.indexOf(target) : -1;
|
|
792
|
+
if (currentIndex < 0 || marks.length < 2) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
let targetIndex = null;
|
|
796
|
+
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
|
|
797
|
+
targetIndex = (currentIndex + 1) % marks.length;
|
|
798
|
+
}
|
|
799
|
+
else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
|
|
800
|
+
targetIndex = (currentIndex - 1 + marks.length) % marks.length;
|
|
801
|
+
}
|
|
802
|
+
else if (event.key === 'Home') {
|
|
803
|
+
targetIndex = 0;
|
|
804
|
+
}
|
|
805
|
+
else if (event.key === 'End') {
|
|
806
|
+
targetIndex = marks.length - 1;
|
|
807
|
+
}
|
|
808
|
+
if (targetIndex === null) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
event.preventDefault();
|
|
812
|
+
const next = marks[targetIndex];
|
|
813
|
+
this.activeMark = next;
|
|
814
|
+
this.syncTabStops(marks);
|
|
815
|
+
next.focus();
|
|
816
|
+
}
|
|
817
|
+
marks() {
|
|
818
|
+
return Array.from(this.host.nativeElement.querySelectorAll('[role="img"][tabindex]'));
|
|
819
|
+
}
|
|
820
|
+
syncTabStops(marks) {
|
|
821
|
+
if (marks.length === 0) {
|
|
822
|
+
this.activeMark = null;
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const active = this.activeMark && marks.includes(this.activeMark) ? this.activeMark : marks[0];
|
|
826
|
+
this.activeMark = active;
|
|
827
|
+
for (const mark of marks) {
|
|
828
|
+
mark.setAttribute('tabindex', mark === active ? '0' : '-1');
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartRovingFocus, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
832
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: ChartRovingFocus, isStandalone: true, selector: "svg[ChartRovingFocus]", host: { listeners: { "focusin": "onFocusIn($event)", "keydown": "onKeydown($event)" } }, ngImport: i0 });
|
|
833
|
+
}
|
|
834
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartRovingFocus, decorators: [{
|
|
835
|
+
type: Directive,
|
|
836
|
+
args: [{
|
|
837
|
+
selector: 'svg[ChartRovingFocus]',
|
|
838
|
+
host: {
|
|
839
|
+
'(focusin)': 'onFocusIn($event)',
|
|
840
|
+
'(keydown)': 'onKeydown($event)',
|
|
841
|
+
},
|
|
842
|
+
}]
|
|
843
|
+
}], ctorParameters: () => [] });
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* The focus treatment shared by every interactive chart mark.
|
|
847
|
+
*
|
|
848
|
+
* SVG shapes cannot carry the `ring-*` box-shadow the HTML controls use, so the
|
|
849
|
+
* indicator is painted as a stroke in the theme's dedicated `--ring` token,
|
|
850
|
+
* which is a near-black in light mode and a near-white in dark mode and so
|
|
851
|
+
* separates the focused mark from its own fill on either ground. The class
|
|
852
|
+
* takes precedence over the `stroke` presentation attribute a mark may already
|
|
853
|
+
* carry, which is why the emphasis strokes bound elsewhere do not have to know
|
|
854
|
+
* about focus at all.
|
|
855
|
+
*
|
|
856
|
+
* `outline-none` lives here rather than in the templates so the suppression and
|
|
857
|
+
* its replacement can never drift apart.
|
|
858
|
+
*/
|
|
859
|
+
class ChartMarkFocus {
|
|
860
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartMarkFocus, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
861
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: ChartMarkFocus, isStandalone: true, selector: "[ChartMarkFocus]", host: { classAttribute: "outline-none focus-visible:stroke-ring focus-visible:stroke-2" }, ngImport: i0 });
|
|
862
|
+
}
|
|
863
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartMarkFocus, decorators: [{
|
|
864
|
+
type: Directive,
|
|
865
|
+
args: [{
|
|
866
|
+
selector: '[ChartMarkFocus]',
|
|
867
|
+
host: {
|
|
868
|
+
class: 'outline-none focus-visible:stroke-ring focus-visible:stroke-2',
|
|
869
|
+
},
|
|
870
|
+
}]
|
|
871
|
+
}] });
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Default margins, one per chart geometry. THREE constants, not one: the
|
|
875
|
+
* families disagree on purpose, and collapsing them would silently relayout
|
|
876
|
+
* pie, radial, and radar.
|
|
877
|
+
*
|
|
878
|
+
* Cartesian charts need a bottom gutter for the axis and a wide left gutter for
|
|
879
|
+
* y tick labels. Polar charts have no axes to make room for. Radar needs
|
|
880
|
+
* symmetric room for its outer labels.
|
|
881
|
+
*/
|
|
882
|
+
const DEFAULT_CARTESIAN_MARGIN = { top: 8, right: 8, bottom: 24, left: 40 };
|
|
883
|
+
/** Polar geometry (pie, radial): no axis gutters. */
|
|
884
|
+
const DEFAULT_POLAR_MARGIN = { top: 8, right: 8, bottom: 8, left: 8 };
|
|
885
|
+
/** Radar geometry: symmetric room for the outer axis labels. */
|
|
886
|
+
const DEFAULT_RADAR_MARGIN = { top: 24, right: 24, bottom: 24, left: 24 };
|
|
887
|
+
/**
|
|
888
|
+
* The same three margins expressed in SPACING UNITS rather than pixels.
|
|
889
|
+
*
|
|
890
|
+
* A chart draws its plot area with numbers, so no token can cascade into it.
|
|
891
|
+
* These are what {@link ChartThemeSpace} multiplies by the live spacing unit, so
|
|
892
|
+
* plot gutters tighten along with everything else when the density axis moves.
|
|
893
|
+
*
|
|
894
|
+
* The px constants above stay exactly as they are, and stay exported. They are
|
|
895
|
+
* the published defaults and the SSR fallback, and at the `normal` preset the
|
|
896
|
+
* unit maths reproduces them precisely: 4px per unit gives back 8/8/24/40,
|
|
897
|
+
* 8 all round, and 24 all round.
|
|
898
|
+
*/
|
|
899
|
+
const DEFAULT_CARTESIAN_MARGIN_UNITS = {
|
|
900
|
+
top: 2,
|
|
901
|
+
right: 2,
|
|
902
|
+
bottom: 6,
|
|
903
|
+
left: 10,
|
|
904
|
+
};
|
|
905
|
+
const DEFAULT_POLAR_MARGIN_UNITS = { top: 2, right: 2, bottom: 2, left: 2 };
|
|
906
|
+
const DEFAULT_RADAR_MARGIN_UNITS = {
|
|
907
|
+
top: 6,
|
|
908
|
+
right: 6,
|
|
909
|
+
bottom: 6,
|
|
910
|
+
left: 6,
|
|
911
|
+
};
|
|
912
|
+
/**
|
|
913
|
+
* Cartesian plotting frame shared between a chart type (Bar, Line, Area…)
|
|
914
|
+
* and its axis / grid primitives.
|
|
915
|
+
*
|
|
916
|
+
* The owning chart component provides an instance and publishes its scales;
|
|
917
|
+
* descendants read them via signals and re-render when dimensions or data
|
|
918
|
+
* change.
|
|
919
|
+
*/
|
|
920
|
+
class CartesianContext {
|
|
921
|
+
/** Inner width (outer width − margin.left − margin.right). */
|
|
922
|
+
innerWidth = signal(0, /* @ts-ignore */
|
|
923
|
+
...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
|
|
924
|
+
/** Inner height (outer height − margin.top − margin.bottom). */
|
|
925
|
+
innerHeight = signal(0, /* @ts-ignore */
|
|
926
|
+
...(ngDevMode ? [{ debugName: "innerHeight" }] : /* istanbul ignore next */ []));
|
|
927
|
+
/** Margins around the plotting area. */
|
|
928
|
+
margin = signal(DEFAULT_CARTESIAN_MARGIN, /* @ts-ignore */
|
|
929
|
+
...(ngDevMode ? [{ debugName: "margin" }] : /* istanbul ignore next */ []));
|
|
930
|
+
/** Layout orientation (drives which axis holds the band scale). */
|
|
931
|
+
orientation = signal('vertical', /* @ts-ignore */
|
|
932
|
+
...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
|
|
933
|
+
/** Band (categorical) scale. */
|
|
934
|
+
categoryScale = signal(null, /* @ts-ignore */
|
|
935
|
+
...(ngDevMode ? [{ debugName: "categoryScale" }] : /* istanbul ignore next */ []));
|
|
936
|
+
/** Linear (numeric) scale. */
|
|
937
|
+
valueScale = signal(null, /* @ts-ignore */
|
|
938
|
+
...(ngDevMode ? [{ debugName: "valueScale" }] : /* istanbul ignore next */ []));
|
|
939
|
+
/** Ordered category domain (e.g. x labels for vertical, y labels for horizontal). */
|
|
940
|
+
categories = signal([], /* @ts-ignore */
|
|
941
|
+
...(ngDevMode ? [{ debugName: "categories" }] : /* istanbul ignore next */ []));
|
|
942
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: CartesianContext, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
943
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.4", ngImport: i0, type: CartesianContext, autoProvided: false });
|
|
944
|
+
}
|
|
945
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: CartesianContext, decorators: [{
|
|
946
|
+
type: Service,
|
|
947
|
+
args: [{ autoProvided: false }]
|
|
948
|
+
}] });
|
|
949
|
+
/** Resolve the scale that maps to the X axis for a given orientation. */
|
|
950
|
+
function xScale(ctx) {
|
|
951
|
+
return computed(() => ctx.orientation() === 'vertical' ? ctx.categoryScale() : ctx.valueScale());
|
|
952
|
+
}
|
|
953
|
+
/** Resolve the scale that maps to the Y axis for a given orientation. */
|
|
954
|
+
function yScale(ctx) {
|
|
955
|
+
return computed(() => ctx.orientation() === 'vertical' ? ctx.valueScale() : ctx.categoryScale());
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* The plot margin actually in effect: the consumer's `margin` input when they
|
|
959
|
+
* set one, otherwise the default scaled by the live spacing unit.
|
|
960
|
+
*
|
|
961
|
+
* An explicit `margin` always wins. A consumer who has already tuned their
|
|
962
|
+
* gutters chose those exact numbers, and quietly rescaling them when the
|
|
963
|
+
* density axis moves would override a decision they made on purpose.
|
|
964
|
+
*
|
|
965
|
+
* This is why the `margin` inputs accept `null` and default to it: a chart has
|
|
966
|
+
* to be able to tell "the caller wants 8px here" apart from "nobody said", and
|
|
967
|
+
* an eagerly-defaulted object cannot express the difference.
|
|
968
|
+
*/
|
|
969
|
+
function resolveChartMargin(margin, units, space) {
|
|
970
|
+
return computed(() => {
|
|
971
|
+
const explicit = margin();
|
|
972
|
+
if (explicit) {
|
|
973
|
+
return explicit;
|
|
974
|
+
}
|
|
975
|
+
return {
|
|
976
|
+
top: space.scale(units.top),
|
|
977
|
+
right: space.scale(units.right),
|
|
978
|
+
bottom: space.scale(units.bottom),
|
|
979
|
+
left: space.scale(units.left),
|
|
980
|
+
};
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
class CategoricalViewportContext {
|
|
985
|
+
dataCount = signal(0, /* @ts-ignore */
|
|
986
|
+
...(ngDevMode ? [{ debugName: "dataCount" }] : /* istanbul ignore next */ []));
|
|
987
|
+
brushRange = signal(null, /* @ts-ignore */
|
|
988
|
+
...(ngDevMode ? [{ debugName: "brushRange" }] : /* istanbul ignore next */ []));
|
|
989
|
+
zoomRange = signal(null, /* @ts-ignore */
|
|
990
|
+
...(ngDevMode ? [{ debugName: "zoomRange" }] : /* istanbul ignore next */ []));
|
|
991
|
+
hasZoom = computed(() => {
|
|
992
|
+
const range = this.zoomRange();
|
|
993
|
+
const count = this.dataCount();
|
|
994
|
+
return !!range && count > 0 && (range.startIndex > 0 || range.endIndex < count - 1);
|
|
995
|
+
}, /* @ts-ignore */
|
|
996
|
+
...(ngDevMode ? [{ debugName: "hasZoom" }] : /* istanbul ignore next */ []));
|
|
997
|
+
resetZoom() {
|
|
998
|
+
this.brushRange.set(null);
|
|
999
|
+
this.zoomRange.set(null);
|
|
1000
|
+
}
|
|
1001
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: CategoricalViewportContext, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
1002
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.4", ngImport: i0, type: CategoricalViewportContext, autoProvided: false });
|
|
1003
|
+
}
|
|
1004
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: CategoricalViewportContext, decorators: [{
|
|
1005
|
+
type: Service,
|
|
1006
|
+
args: [{ autoProvided: false }]
|
|
1007
|
+
}] });
|
|
1008
|
+
|
|
1009
|
+
class ScatterViewportContext {
|
|
1010
|
+
innerWidth = signal(0, /* @ts-ignore */
|
|
1011
|
+
...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
|
|
1012
|
+
innerHeight = signal(0, /* @ts-ignore */
|
|
1013
|
+
...(ngDevMode ? [{ debugName: "innerHeight" }] : /* istanbul ignore next */ []));
|
|
1014
|
+
fullXDomain = signal(null, /* @ts-ignore */
|
|
1015
|
+
...(ngDevMode ? [{ debugName: "fullXDomain" }] : /* istanbul ignore next */ []));
|
|
1016
|
+
fullYDomain = signal(null, /* @ts-ignore */
|
|
1017
|
+
...(ngDevMode ? [{ debugName: "fullYDomain" }] : /* istanbul ignore next */ []));
|
|
1018
|
+
zoomXDomain = signal(null, /* @ts-ignore */
|
|
1019
|
+
...(ngDevMode ? [{ debugName: "zoomXDomain" }] : /* istanbul ignore next */ []));
|
|
1020
|
+
zoomYDomain = signal(null, /* @ts-ignore */
|
|
1021
|
+
...(ngDevMode ? [{ debugName: "zoomYDomain" }] : /* istanbul ignore next */ []));
|
|
1022
|
+
xScale = signal(null, /* @ts-ignore */
|
|
1023
|
+
...(ngDevMode ? [{ debugName: "xScale" }] : /* istanbul ignore next */ []));
|
|
1024
|
+
yScale = signal(null, /* @ts-ignore */
|
|
1025
|
+
...(ngDevMode ? [{ debugName: "yScale" }] : /* istanbul ignore next */ []));
|
|
1026
|
+
hasZoom = computed(() => this.zoomXDomain() !== null || this.zoomYDomain() !== null, /* @ts-ignore */
|
|
1027
|
+
...(ngDevMode ? [{ debugName: "hasZoom" }] : /* istanbul ignore next */ []));
|
|
1028
|
+
resetZoom() {
|
|
1029
|
+
this.zoomXDomain.set(null);
|
|
1030
|
+
this.zoomYDomain.set(null);
|
|
1031
|
+
}
|
|
1032
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ScatterViewportContext, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
1033
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.4", ngImport: i0, type: ScatterViewportContext, autoProvided: false });
|
|
1034
|
+
}
|
|
1035
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ScatterViewportContext, decorators: [{
|
|
1036
|
+
type: Service,
|
|
1037
|
+
args: [{ autoProvided: false }]
|
|
1038
|
+
}] });
|
|
1039
|
+
|
|
1040
|
+
/** Produce evenly-spaced ticks for a band (categorical) scale. */
|
|
1041
|
+
function bandTicks(scale) {
|
|
1042
|
+
const half = scale.bandwidth() / 2;
|
|
1043
|
+
return scale.domain().map((value) => ({
|
|
1044
|
+
value,
|
|
1045
|
+
offset: (scale(value) ?? 0) + half,
|
|
1046
|
+
label: value,
|
|
1047
|
+
}));
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Produce ticks for a linear scale.
|
|
1051
|
+
*
|
|
1052
|
+
* @param scale The linear scale.
|
|
1053
|
+
* @param count Approximate number of ticks (hint, d3 may return fewer/more).
|
|
1054
|
+
* @param formatter Label formatter.
|
|
1055
|
+
*/
|
|
1056
|
+
function linearTicks(scale, count = 5, formatter = (v) => String(v)) {
|
|
1057
|
+
return scale.ticks(count).map((value) => ({
|
|
1058
|
+
value,
|
|
1059
|
+
offset: scale(value),
|
|
1060
|
+
label: formatter(value),
|
|
1061
|
+
}));
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Read a numeric value from a datum, tolerating strings.
|
|
1066
|
+
*
|
|
1067
|
+
* Every layout in this library reads its numbers through here. Bar and line
|
|
1068
|
+
* each used to carry their own identical copy while the polar and scatter
|
|
1069
|
+
* layouts reached for a bare `Number(d[key] ?? 0)`, so the same dataset drew
|
|
1070
|
+
* correctly as a bar chart and silently lost marks as a pie: `Number('n/a')`
|
|
1071
|
+
* is `NaN`, and a `NaN` flows straight into the path data.
|
|
1072
|
+
*
|
|
1073
|
+
* Anything that is not a finite number or a parseable numeric string reads as
|
|
1074
|
+
* `0`. That is the behaviour bar and line already shipped, so it is the one the
|
|
1075
|
+
* rest of the family adopts.
|
|
1076
|
+
*/
|
|
1077
|
+
function readNumber(datum, key) {
|
|
1078
|
+
const raw = datum[key];
|
|
1079
|
+
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
|
1080
|
+
return raw;
|
|
1081
|
+
}
|
|
1082
|
+
if (typeof raw === 'string') {
|
|
1083
|
+
const parsed = Number(raw);
|
|
1084
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1085
|
+
}
|
|
1086
|
+
return 0;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function curveFor(curve) {
|
|
1090
|
+
switch (curve) {
|
|
1091
|
+
case 'monotone':
|
|
1092
|
+
return curveMonotoneX;
|
|
1093
|
+
case 'step':
|
|
1094
|
+
return curveStep;
|
|
1095
|
+
case 'linear':
|
|
1096
|
+
default:
|
|
1097
|
+
return curveLinear;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
/** Build category + value scales for point-based charts (line / area). */
|
|
1101
|
+
function buildCartesianScales(input) {
|
|
1102
|
+
const { data, xKey, seriesKeys, orientation, innerWidth, innerHeight } = input;
|
|
1103
|
+
const isVertical = orientation === 'vertical';
|
|
1104
|
+
const categories = data.map((d) => String(d[xKey] ?? ''));
|
|
1105
|
+
const categoryScale = scalePoint()
|
|
1106
|
+
.domain(categories)
|
|
1107
|
+
.range(isVertical ? [0, innerWidth] : [0, innerHeight])
|
|
1108
|
+
.padding(0);
|
|
1109
|
+
const minValue = min(data, (d) => min(seriesKeys, (k) => readNumber(d, k)) ?? 0) ?? 0;
|
|
1110
|
+
const maxValue = max(data, (d) => max(seriesKeys, (k) => readNumber(d, k)) ?? 0) ?? 0;
|
|
1111
|
+
const [domainMin, domainMax] = zeroBaselineDomain(minValue, maxValue);
|
|
1112
|
+
const valueScale = scaleLinear()
|
|
1113
|
+
.domain([domainMin, domainMax])
|
|
1114
|
+
.nice()
|
|
1115
|
+
.range(isVertical ? [innerHeight, 0] : [0, innerWidth]);
|
|
1116
|
+
return { categories, categoryScale, valueScale };
|
|
1117
|
+
}
|
|
1118
|
+
/** Compute line-chart geometry. */
|
|
1119
|
+
function computeLineLayout(input) {
|
|
1120
|
+
const { data, seriesKeys, orientation, curve } = input;
|
|
1121
|
+
const { categories, categoryScale, valueScale } = buildCartesianScales(input);
|
|
1122
|
+
const isVertical = orientation === 'vertical';
|
|
1123
|
+
const allPoints = [];
|
|
1124
|
+
const series = [];
|
|
1125
|
+
for (const seriesKey of seriesKeys) {
|
|
1126
|
+
const points = data.map((d, datumIndex) => {
|
|
1127
|
+
const value = readNumber(d, seriesKey);
|
|
1128
|
+
const category = categories[datumIndex];
|
|
1129
|
+
const c = categoryScale(category) ?? 0;
|
|
1130
|
+
const v = valueScale(value);
|
|
1131
|
+
return {
|
|
1132
|
+
seriesKey,
|
|
1133
|
+
datumIndex,
|
|
1134
|
+
category,
|
|
1135
|
+
value,
|
|
1136
|
+
x: isVertical ? c : v,
|
|
1137
|
+
y: isVertical ? v : c,
|
|
1138
|
+
};
|
|
1139
|
+
});
|
|
1140
|
+
allPoints.push(...points);
|
|
1141
|
+
const generator = line()
|
|
1142
|
+
.x((p) => p.x)
|
|
1143
|
+
.y((p) => p.y)
|
|
1144
|
+
.curve(curveFor(curve));
|
|
1145
|
+
series.push({
|
|
1146
|
+
seriesKey,
|
|
1147
|
+
color: seriesColorVar(seriesKey),
|
|
1148
|
+
linePath: generator(points) ?? '',
|
|
1149
|
+
points,
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
series,
|
|
1154
|
+
points: allPoints,
|
|
1155
|
+
categoryScale,
|
|
1156
|
+
valueScale,
|
|
1157
|
+
categories,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
/** Compute area-chart geometry (single or stacked). */
|
|
1161
|
+
function computeAreaLayout(input) {
|
|
1162
|
+
if (input.stacked && input.seriesKeys.length > 0) {
|
|
1163
|
+
return computeStackedArea(input);
|
|
1164
|
+
}
|
|
1165
|
+
return computeSingleArea(input);
|
|
1166
|
+
}
|
|
1167
|
+
function computeSingleArea(input) {
|
|
1168
|
+
const { data, seriesKeys, orientation, curve } = input;
|
|
1169
|
+
const { categories, categoryScale, valueScale } = buildCartesianScales(input);
|
|
1170
|
+
const isVertical = orientation === 'vertical';
|
|
1171
|
+
const baseline = isVertical ? valueScale(0) : valueScale(0);
|
|
1172
|
+
const allPoints = [];
|
|
1173
|
+
const series = [];
|
|
1174
|
+
for (const seriesKey of seriesKeys) {
|
|
1175
|
+
const points = data.map((d, datumIndex) => {
|
|
1176
|
+
const value = readNumber(d, seriesKey);
|
|
1177
|
+
const category = categories[datumIndex];
|
|
1178
|
+
const c = categoryScale(category) ?? 0;
|
|
1179
|
+
const v = valueScale(value);
|
|
1180
|
+
return {
|
|
1181
|
+
seriesKey,
|
|
1182
|
+
datumIndex,
|
|
1183
|
+
category,
|
|
1184
|
+
value,
|
|
1185
|
+
x: isVertical ? c : v,
|
|
1186
|
+
y: isVertical ? v : c,
|
|
1187
|
+
};
|
|
1188
|
+
});
|
|
1189
|
+
allPoints.push(...points);
|
|
1190
|
+
const lineGen = line()
|
|
1191
|
+
.x((p) => p.x)
|
|
1192
|
+
.y((p) => p.y)
|
|
1193
|
+
.curve(curveFor(curve));
|
|
1194
|
+
const areaGen = isVertical
|
|
1195
|
+
? area()
|
|
1196
|
+
.x((p) => p.x)
|
|
1197
|
+
.y0(baseline)
|
|
1198
|
+
.y1((p) => p.y)
|
|
1199
|
+
.curve(curveFor(curve))
|
|
1200
|
+
: area()
|
|
1201
|
+
.y((p) => p.y)
|
|
1202
|
+
.x0(baseline)
|
|
1203
|
+
.x1((p) => p.x)
|
|
1204
|
+
.curve(curveFor(curve));
|
|
1205
|
+
series.push({
|
|
1206
|
+
seriesKey,
|
|
1207
|
+
color: seriesColorVar(seriesKey),
|
|
1208
|
+
linePath: lineGen(points) ?? '',
|
|
1209
|
+
areaPath: areaGen(points) ?? '',
|
|
1210
|
+
points,
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
return {
|
|
1214
|
+
series,
|
|
1215
|
+
points: allPoints,
|
|
1216
|
+
categoryScale,
|
|
1217
|
+
valueScale,
|
|
1218
|
+
categories,
|
|
1219
|
+
stacked: false,
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
function computeStackedArea(input) {
|
|
1223
|
+
const { data, seriesKeys, orientation, innerWidth, innerHeight, xKey, curve, expanded } = input;
|
|
1224
|
+
const isVertical = orientation === 'vertical';
|
|
1225
|
+
const categories = data.map((d) => String(d[xKey] ?? ''));
|
|
1226
|
+
const categoryScale = scalePoint()
|
|
1227
|
+
.domain(categories)
|
|
1228
|
+
.range(isVertical ? [0, innerWidth] : [0, innerHeight])
|
|
1229
|
+
.padding(0);
|
|
1230
|
+
const normalized = data.map((d) => {
|
|
1231
|
+
const out = {};
|
|
1232
|
+
for (const k of seriesKeys)
|
|
1233
|
+
out[k] = readNumber(d, k);
|
|
1234
|
+
return out;
|
|
1235
|
+
});
|
|
1236
|
+
const stackGenerator = stack().keys(seriesKeys);
|
|
1237
|
+
if (expanded) {
|
|
1238
|
+
stackGenerator.offset(stackOffsetExpand);
|
|
1239
|
+
}
|
|
1240
|
+
const stackSeries = stackGenerator(normalized);
|
|
1241
|
+
// Every layer is measured, not just the last one: a stack that runs below the
|
|
1242
|
+
// baseline puts its extreme on whichever layer carries the negative values.
|
|
1243
|
+
const minTotal = expanded
|
|
1244
|
+
? 0
|
|
1245
|
+
: (min(stackSeries, (layer) => min(layer, (p) => Math.min(p[0], p[1]))) ?? 0);
|
|
1246
|
+
const maxTotal = expanded
|
|
1247
|
+
? 1
|
|
1248
|
+
: (max(stackSeries, (layer) => max(layer, (p) => Math.max(p[0], p[1]))) ?? 0);
|
|
1249
|
+
const [domainMin, domainMax] = zeroBaselineDomain(minTotal, maxTotal);
|
|
1250
|
+
const valueScale = scaleLinear()
|
|
1251
|
+
.domain([domainMin, domainMax])
|
|
1252
|
+
.range(isVertical ? [innerHeight, 0] : [0, innerWidth]);
|
|
1253
|
+
if (!expanded) {
|
|
1254
|
+
valueScale.nice();
|
|
1255
|
+
}
|
|
1256
|
+
const allPoints = [];
|
|
1257
|
+
const series = stackSeries.map((layer) => {
|
|
1258
|
+
const seriesKey = layer.key;
|
|
1259
|
+
const points = layer.map((p, datumIndex) => {
|
|
1260
|
+
const category = categories[datumIndex];
|
|
1261
|
+
const c = categoryScale(category) ?? 0;
|
|
1262
|
+
const upper = valueScale(p[1]);
|
|
1263
|
+
return {
|
|
1264
|
+
seriesKey,
|
|
1265
|
+
datumIndex,
|
|
1266
|
+
category,
|
|
1267
|
+
value: p[1] - p[0],
|
|
1268
|
+
x: isVertical ? c : upper,
|
|
1269
|
+
y: isVertical ? upper : c,
|
|
1270
|
+
};
|
|
1271
|
+
});
|
|
1272
|
+
allPoints.push(...points);
|
|
1273
|
+
const lineGen = line()
|
|
1274
|
+
.x(([x]) => x)
|
|
1275
|
+
.y(([, y]) => y)
|
|
1276
|
+
.curve(curveFor(curve));
|
|
1277
|
+
const areaGen = isVertical
|
|
1278
|
+
? area()
|
|
1279
|
+
.x(([x]) => x)
|
|
1280
|
+
.y0(([, , y0]) => y0)
|
|
1281
|
+
.y1(([, y1]) => y1)
|
|
1282
|
+
.curve(curveFor(curve))
|
|
1283
|
+
: area()
|
|
1284
|
+
.y(([x]) => x)
|
|
1285
|
+
.x0(([, , x0]) => x0)
|
|
1286
|
+
.x1(([, x1]) => x1)
|
|
1287
|
+
.curve(curveFor(curve));
|
|
1288
|
+
const tuples = layer.map((p, i) => {
|
|
1289
|
+
const c = categoryScale(categories[i]) ?? 0;
|
|
1290
|
+
return [c, valueScale(p[1]), valueScale(p[0])];
|
|
1291
|
+
});
|
|
1292
|
+
return {
|
|
1293
|
+
seriesKey,
|
|
1294
|
+
color: seriesColorVar(seriesKey),
|
|
1295
|
+
linePath: lineGen(tuples) ?? '',
|
|
1296
|
+
areaPath: areaGen(tuples) ?? '',
|
|
1297
|
+
points,
|
|
1298
|
+
};
|
|
1299
|
+
});
|
|
1300
|
+
return {
|
|
1301
|
+
series,
|
|
1302
|
+
points: allPoints,
|
|
1303
|
+
categoryScale,
|
|
1304
|
+
valueScale,
|
|
1305
|
+
categories,
|
|
1306
|
+
stacked: true,
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* Publish a `scalePoint` from line/area layouts to the `CartesianContext`,
|
|
1312
|
+
* which expects a `scaleBand`. Line/area points sit on the range edges (the
|
|
1313
|
+
* point scale has zero padding, so the first point touches the y-axis and the
|
|
1314
|
+
* last touches the right edge). We extend the band range by half a step on
|
|
1315
|
+
* each side so band centers — used to position axis ticks — line up exactly
|
|
1316
|
+
* with those edge-touching points.
|
|
1317
|
+
*
|
|
1318
|
+
* This keeps axes/grid primitives agnostic to whether the underlying chart
|
|
1319
|
+
* uses `scaleBand` (bar) or `scalePoint` (line/area).
|
|
1320
|
+
*/
|
|
1321
|
+
function pointToBandAdapter(pointScale, range) {
|
|
1322
|
+
const half = pointScale.step() / 2;
|
|
1323
|
+
const extended = [range[0] - half, range[1] + half];
|
|
1324
|
+
return scaleBand().domain(pointScale.domain()).range(extended).padding(0);
|
|
1325
|
+
}
|
|
1326
|
+
/** Recreate a linear scale with the same domain/range (handy for effects). */
|
|
1327
|
+
function cloneLinear(scale) {
|
|
1328
|
+
return scaleLinear().domain(scale.domain()).range(scale.range());
|
|
1329
|
+
}
|
|
1330
|
+
function provideCartesianFromLineLayout(ctx, layout, orientation, innerWidth, innerHeight) {
|
|
1331
|
+
const range = orientation === 'vertical' ? [0, innerWidth] : [0, innerHeight];
|
|
1332
|
+
ctx.orientation.set(orientation);
|
|
1333
|
+
ctx.innerWidth.set(innerWidth);
|
|
1334
|
+
ctx.innerHeight.set(innerHeight);
|
|
1335
|
+
ctx.categoryScale.set(pointToBandAdapter(layout.categoryScale, range));
|
|
1336
|
+
ctx.valueScale.set(cloneLinear(layout.valueScale));
|
|
1337
|
+
ctx.categories.set(layout.categories);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/** 0.375rem @ 16px root — Tailwind's stock `rounded-md` value. */
|
|
1341
|
+
const FALLBACK_RADIUS_PX = 6;
|
|
1342
|
+
/**
|
|
1343
|
+
* Resolves the live theme corner radius (the `--radius` token) in pixels for SVG
|
|
1344
|
+
* chart geometry, and re-emits when the global `theme-radius` axis switches.
|
|
1345
|
+
*
|
|
1346
|
+
* SVG `rx`/`ry` need px numbers, so this reads the computed `--radius` off
|
|
1347
|
+
* `<html>` and converts rem→px. A `MutationObserver` on the `<html theme-radius>`
|
|
1348
|
+
* attribute keeps the `px` signal in sync with `ThemeRadiusService` at runtime;
|
|
1349
|
+
* charts default their corner radius to this value (still overridable per chart).
|
|
1350
|
+
*
|
|
1351
|
+
* SSR-safe: with no document it returns the `md` fallback and skips observing.
|
|
1352
|
+
*/
|
|
1353
|
+
class ChartThemeRadius {
|
|
1354
|
+
document = inject(DOCUMENT, { optional: true });
|
|
1355
|
+
basePx = signal(this.readBasePx(), /* @ts-ignore */
|
|
1356
|
+
...(ngDevMode ? [{ debugName: "basePx" }] : /* istanbul ignore next */ []));
|
|
1357
|
+
/** Resolved base radius (`--radius`) in px; reactive to the theme-radius axis. */
|
|
1358
|
+
px = this.basePx.asReadonly();
|
|
1359
|
+
constructor() {
|
|
1360
|
+
const root = this.document?.documentElement;
|
|
1361
|
+
const view = this.document?.defaultView;
|
|
1362
|
+
if (!root || !view || typeof view.MutationObserver !== 'function') {
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const observer = new view.MutationObserver(() => this.basePx.set(this.readBasePx()));
|
|
1366
|
+
observer.observe(root, { attributes: true, attributeFilter: ['theme-radius'] });
|
|
1367
|
+
inject(DestroyRef).onDestroy(() => observer.disconnect());
|
|
1368
|
+
}
|
|
1369
|
+
readBasePx() {
|
|
1370
|
+
const root = this.document?.documentElement;
|
|
1371
|
+
const view = this.document?.defaultView;
|
|
1372
|
+
if (!root || !view) {
|
|
1373
|
+
return FALLBACK_RADIUS_PX;
|
|
1374
|
+
}
|
|
1375
|
+
const raw = view.getComputedStyle(root).getPropertyValue('--radius').trim();
|
|
1376
|
+
return this.toPx(raw, view, root) ?? FALLBACK_RADIUS_PX;
|
|
1377
|
+
}
|
|
1378
|
+
toPx(value, view, root) {
|
|
1379
|
+
if (!value) {
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
// Tailwind v4 emits `rounded-full` as `calc(infinity * 1px)`. SVG geometry
|
|
1383
|
+
// still needs a finite number; 9999px preserves the same pill/circle result.
|
|
1384
|
+
if (value.includes('infinity')) {
|
|
1385
|
+
return 9999;
|
|
1386
|
+
}
|
|
1387
|
+
const numeric = Number.parseFloat(value);
|
|
1388
|
+
if (!Number.isFinite(numeric)) {
|
|
1389
|
+
return null;
|
|
1390
|
+
}
|
|
1391
|
+
if (value.endsWith('rem')) {
|
|
1392
|
+
const rootFontSize = Number.parseFloat(view.getComputedStyle(root).fontSize) || 16;
|
|
1393
|
+
return numeric * rootFontSize;
|
|
1394
|
+
}
|
|
1395
|
+
// Concrete px values parse directly.
|
|
1396
|
+
return numeric;
|
|
1397
|
+
}
|
|
1398
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartThemeRadius, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
1399
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.4", ngImport: i0, type: ChartThemeRadius });
|
|
1400
|
+
}
|
|
1401
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartThemeRadius, decorators: [{
|
|
1402
|
+
type: Service
|
|
1403
|
+
}], ctorParameters: () => [] });
|
|
1404
|
+
|
|
1405
|
+
/** 0.25rem @ 16px root — the `--spacing-base` value on the `normal` preset. */
|
|
1406
|
+
const FALLBACK_SPACING_PX = 4;
|
|
1407
|
+
/**
|
|
1408
|
+
* Resolves the live spacing unit in pixels for SVG chart geometry, and re-emits
|
|
1409
|
+
* when the global `theme-space` axis switches.
|
|
1410
|
+
*
|
|
1411
|
+
* Charts are the one part of the library CSS cannot reach: plot margins are
|
|
1412
|
+
* numbers handed to SVG, not properties a token can cascade into. Without a
|
|
1413
|
+
* bridge, choosing `compact` tightens every card and control on a dashboard and
|
|
1414
|
+
* leaves the charts sitting in exactly as much white space as before -- one
|
|
1415
|
+
* island of the old density in the middle of the new one.
|
|
1416
|
+
*
|
|
1417
|
+
* This is deliberately the same shape as {@link ChartThemeRadius}: an attribute
|
|
1418
|
+
* `MutationObserver`, an SSR-safe read, and a fallback constant. Copying a
|
|
1419
|
+
* pattern that already works beats inventing a second one, and it means both
|
|
1420
|
+
* bridges can be tested the same way.
|
|
1421
|
+
*
|
|
1422
|
+
* It reads `--spacing-base`, NOT `--spacing`. They hold the same number, but
|
|
1423
|
+
* `--spacing` is declared inside Tailwind's `@theme inline` block, which is
|
|
1424
|
+
* inlined into utilities at build time and never lands on `<html>` as something
|
|
1425
|
+
* `getComputedStyle` can read back. Reading it would have returned an empty
|
|
1426
|
+
* string, and this bridge would have quietly served its fallback forever while
|
|
1427
|
+
* looking perfectly correct.
|
|
1428
|
+
*
|
|
1429
|
+
* SSR-safe: with no document it returns the fallback and skips observing.
|
|
1430
|
+
*/
|
|
1431
|
+
class ChartThemeSpace {
|
|
1432
|
+
document = inject(DOCUMENT, { optional: true });
|
|
1433
|
+
unitPx = signal(this.readUnitPx(), /* @ts-ignore */
|
|
1434
|
+
...(ngDevMode ? [{ debugName: "unitPx" }] : /* istanbul ignore next */ []));
|
|
1435
|
+
/** Resolved spacing unit (`--spacing-base`) in px; reactive to the theme-space axis. */
|
|
1436
|
+
px = this.unitPx.asReadonly();
|
|
1437
|
+
constructor() {
|
|
1438
|
+
const root = this.document?.documentElement;
|
|
1439
|
+
const view = this.document?.defaultView;
|
|
1440
|
+
if (!root || !view || typeof view.MutationObserver !== 'function') {
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
const observer = new view.MutationObserver(() => this.unitPx.set(this.readUnitPx()));
|
|
1444
|
+
observer.observe(root, { attributes: true, attributeFilter: ['theme-space'] });
|
|
1445
|
+
inject(DestroyRef).onDestroy(() => observer.disconnect());
|
|
1446
|
+
}
|
|
1447
|
+
/** Scale one margin constant, expressed in spacing units, into pixels. */
|
|
1448
|
+
scale(units) {
|
|
1449
|
+
return units * this.unitPx();
|
|
1450
|
+
}
|
|
1451
|
+
readUnitPx() {
|
|
1452
|
+
const root = this.document?.documentElement;
|
|
1453
|
+
const view = this.document?.defaultView;
|
|
1454
|
+
if (!root || !view) {
|
|
1455
|
+
return FALLBACK_SPACING_PX;
|
|
1456
|
+
}
|
|
1457
|
+
const raw = view.getComputedStyle(root).getPropertyValue('--spacing-base').trim();
|
|
1458
|
+
return this.toPx(raw, view, root) ?? FALLBACK_SPACING_PX;
|
|
1459
|
+
}
|
|
1460
|
+
toPx(value, view, root) {
|
|
1461
|
+
if (!value) {
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
const numeric = Number.parseFloat(value);
|
|
1465
|
+
if (!Number.isFinite(numeric)) {
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
if (value.endsWith('rem')) {
|
|
1469
|
+
const rootFontSize = Number.parseFloat(view.getComputedStyle(root).fontSize) || 16;
|
|
1470
|
+
return numeric * rootFontSize;
|
|
1471
|
+
}
|
|
1472
|
+
// Concrete px values parse directly.
|
|
1473
|
+
return numeric;
|
|
1474
|
+
}
|
|
1475
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartThemeSpace, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
1476
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.4", ngImport: i0, type: ChartThemeSpace });
|
|
1477
|
+
}
|
|
1478
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ChartThemeSpace, decorators: [{
|
|
1479
|
+
type: Service
|
|
1480
|
+
}], ctorParameters: () => [] });
|
|
4
1481
|
|
|
5
1482
|
/**
|
|
6
1483
|
* Generated bundle index. Do not edit.
|
|
7
|
-
*/
|
|
1484
|
+
*/
|
|
1485
|
+
|
|
1486
|
+
export { CHART_DATA_ATTRIBUTE, CHART_THEMES, CartesianContext, CategoricalViewportContext, ChartContainer, ChartContext, ChartDataTable, ChartMarkFocus, ChartRovingFocus, ChartStyle, ChartThemeRadius, ChartThemeSpace, DEFAULT_CARTESIAN_MARGIN, DEFAULT_CARTESIAN_MARGIN_UNITS, DEFAULT_POLAR_MARGIN, DEFAULT_POLAR_MARGIN_UNITS, DEFAULT_RADAR_MARGIN, DEFAULT_RADAR_MARGIN_UNITS, ScatterViewportContext, activationClientPoint, bandTicks, buildCartesianScales, buildChartCss, cloneLinear, computeAreaLayout, computeLineLayout, effectiveIndexRange, elementClientCenter, escapeCssIdent, indexRangeSize, isSafeCssColorValue, linearTicks, nearestCategoryIndex, normalizeIndexRange, normalizeNumericDomain, panIndexRange, panNumericDomain, pointToBandAdapter, provideCartesianFromLineLayout, readNumber, resolveChartMargin, resolveChartStyle, resolveSeriesColor, seriesColorVar, setupCartesianChart, setupPolarChart, sliceByIndexRange, xScale, yScale, zeroBaselineDomain, zoomIndexRange, zoomNumericDomain };
|