@webjsdev/ui 0.3.17 → 0.4.0
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 +66 -7
- package/THIRD_PARTY_NOTICES.md +349 -0
- package/package.json +4 -2
- package/packages/registry/blocks/_shared/placeholder.svg +1 -0
- package/packages/registry/blocks/charts/chart-area-axes.ts +72 -0
- package/packages/registry/blocks/charts/chart-area-default.ts +64 -0
- package/packages/registry/blocks/charts/chart-area-gradient.ts +82 -0
- package/packages/registry/blocks/charts/chart-area-icons.ts +77 -0
- package/packages/registry/blocks/charts/chart-area-interactive.ts +204 -0
- package/packages/registry/blocks/charts/chart-area-legend.ts +72 -0
- package/packages/registry/blocks/charts/chart-area-linear.ts +64 -0
- package/packages/registry/blocks/charts/chart-area-stacked-expand.ts +77 -0
- package/packages/registry/blocks/charts/chart-area-stacked.ts +71 -0
- package/packages/registry/blocks/charts/chart-area-step.ts +68 -0
- package/packages/registry/blocks/charts/chart-bar-active.ts +93 -0
- package/packages/registry/blocks/charts/chart-bar-default.ts +60 -0
- package/packages/registry/blocks/charts/chart-bar-horizontal.ts +62 -0
- package/packages/registry/blocks/charts/chart-bar-interactive.ts +185 -0
- package/packages/registry/blocks/charts/chart-bar-label-custom.ts +78 -0
- package/packages/registry/blocks/charts/chart-bar-label.ts +66 -0
- package/packages/registry/blocks/charts/chart-bar-mixed.ts +87 -0
- package/packages/registry/blocks/charts/chart-bar-multiple.ts +67 -0
- package/packages/registry/blocks/charts/chart-bar-negative.ts +61 -0
- package/packages/registry/blocks/charts/chart-bar-stacked.ts +68 -0
- package/packages/registry/blocks/charts/chart-line-default.ts +61 -0
- package/packages/registry/blocks/charts/chart-line-dots-colors.ts +86 -0
- package/packages/registry/blocks/charts/chart-line-dots-custom.ts +75 -0
- package/packages/registry/blocks/charts/chart-line-dots.ts +65 -0
- package/packages/registry/blocks/charts/chart-line-interactive.ts +185 -0
- package/packages/registry/blocks/charts/chart-line-label-custom.ts +94 -0
- package/packages/registry/blocks/charts/chart-line-label.ts +73 -0
- package/packages/registry/blocks/charts/chart-line-linear.ts +61 -0
- package/packages/registry/blocks/charts/chart-line-multiple.ts +71 -0
- package/packages/registry/blocks/charts/chart-line-step.ts +61 -0
- package/packages/registry/blocks/charts/chart-pie-donut-active.ts +86 -0
- package/packages/registry/blocks/charts/chart-pie-donut-text.ts +89 -0
- package/packages/registry/blocks/charts/chart-pie-donut.ts +77 -0
- package/packages/registry/blocks/charts/chart-pie-interactive.ts +120 -0
- package/packages/registry/blocks/charts/chart-pie-label-custom.ts +82 -0
- package/packages/registry/blocks/charts/chart-pie-label-list.ts +86 -0
- package/packages/registry/blocks/charts/chart-pie-label.ts +77 -0
- package/packages/registry/blocks/charts/chart-pie-legend.ts +79 -0
- package/packages/registry/blocks/charts/chart-pie-separator-none.ts +77 -0
- package/packages/registry/blocks/charts/chart-pie-simple.ts +77 -0
- package/packages/registry/blocks/charts/chart-pie-stacked.ts +99 -0
- package/packages/registry/blocks/charts/chart-radar-default.ts +61 -0
- package/packages/registry/blocks/charts/chart-radar-dots.ts +61 -0
- package/packages/registry/blocks/charts/chart-radar-grid-circle-fill.ts +61 -0
- package/packages/registry/blocks/charts/chart-radar-grid-circle-no-lines.ts +61 -0
- package/packages/registry/blocks/charts/chart-radar-grid-circle.ts +61 -0
- package/packages/registry/blocks/charts/chart-radar-grid-custom.ts +61 -0
- package/packages/registry/blocks/charts/chart-radar-grid-fill.ts +61 -0
- package/packages/registry/blocks/charts/chart-radar-grid-none.ts +60 -0
- package/packages/registry/blocks/charts/chart-radar-icons.ts +76 -0
- package/packages/registry/blocks/charts/chart-radar-label-custom.ts +77 -0
- package/packages/registry/blocks/charts/chart-radar-legend.ts +70 -0
- package/packages/registry/blocks/charts/chart-radar-lines-only.ts +68 -0
- package/packages/registry/blocks/charts/chart-radar-multiple.ts +68 -0
- package/packages/registry/blocks/charts/chart-radar-radius.ts +68 -0
- package/packages/registry/blocks/charts/chart-radial-grid.ts +80 -0
- package/packages/registry/blocks/charts/chart-radial-label.ts +85 -0
- package/packages/registry/blocks/charts/chart-radial-shape.ts +81 -0
- package/packages/registry/blocks/charts/chart-radial-simple.ts +79 -0
- package/packages/registry/blocks/charts/chart-radial-stacked.ts +69 -0
- package/packages/registry/blocks/charts/chart-radial-text.ts +82 -0
- package/packages/registry/blocks/charts/chart-tooltip-advanced.ts +84 -0
- package/packages/registry/blocks/charts/chart-tooltip-default.ts +62 -0
- package/packages/registry/blocks/charts/chart-tooltip-formatter.ts +75 -0
- package/packages/registry/blocks/charts/chart-tooltip-icons.ts +68 -0
- package/packages/registry/blocks/charts/chart-tooltip-indicator-line.ts +62 -0
- package/packages/registry/blocks/charts/chart-tooltip-indicator-none.ts +62 -0
- package/packages/registry/blocks/charts/chart-tooltip-label-custom.ts +65 -0
- package/packages/registry/blocks/charts/chart-tooltip-label-formatter.ts +66 -0
- package/packages/registry/blocks/charts/chart-tooltip-label-none.ts +62 -0
- package/packages/registry/blocks/dashboard-01/components/app-sidebar.ts +108 -0
- package/packages/registry/blocks/dashboard-01/components/chart-area-interactive.ts +276 -0
- package/packages/registry/blocks/dashboard-01/components/dashboard-data.ts +89 -0
- package/packages/registry/blocks/dashboard-01/components/data-table.ts +637 -0
- package/packages/registry/blocks/dashboard-01/components/nav-documents.ts +65 -0
- package/packages/registry/blocks/dashboard-01/components/nav-main.ts +61 -0
- package/packages/registry/blocks/dashboard-01/components/nav-secondary.ts +40 -0
- package/packages/registry/blocks/dashboard-01/components/nav-user.ts +66 -0
- package/packages/registry/blocks/dashboard-01/components/section-cards.ts +59 -0
- package/packages/registry/blocks/dashboard-01/components/site-header.ts +52 -0
- package/packages/registry/blocks/dashboard-01/page.ts +39 -0
- package/packages/registry/blocks/login-01/components/login-form.ts +70 -0
- package/packages/registry/blocks/login-01/page.ts +19 -0
- package/packages/registry/blocks/login-02/components/login-form.ts +68 -0
- package/packages/registry/blocks/login-02/page.ts +36 -0
- package/packages/registry/blocks/login-03/components/login-form.ts +89 -0
- package/packages/registry/blocks/login-03/page.ts +28 -0
- package/packages/registry/blocks/login-04/components/login-form.ts +91 -0
- package/packages/registry/blocks/login-04/page.ts +20 -0
- package/packages/registry/blocks/login-05/components/login-form.ts +75 -0
- package/packages/registry/blocks/login-05/page.ts +19 -0
- package/packages/registry/blocks/preview/cards/activate-agent-dialog.ts +77 -0
- package/packages/registry/blocks/preview/cards/analytics-card.ts +64 -0
- package/packages/registry/blocks/preview/cards/anomaly-alert.ts +35 -0
- package/packages/registry/blocks/preview/cards/bar-chart-card.ts +91 -0
- package/packages/registry/blocks/preview/cards/book-appointment.ts +51 -0
- package/packages/registry/blocks/preview/cards/codespaces-card.ts +172 -0
- package/packages/registry/blocks/preview/cards/contributions-activity.ts +63 -0
- package/packages/registry/blocks/preview/cards/contributors.ts +51 -0
- package/packages/registry/blocks/preview/cards/environment-variables.ts +47 -0
- package/packages/registry/blocks/preview/cards/feedback-form.ts +61 -0
- package/packages/registry/blocks/preview/cards/file-upload.ts +51 -0
- package/packages/registry/blocks/preview/cards/github-profile.ts +66 -0
- package/packages/registry/blocks/preview/cards/icon-preview-grid.ts +47 -0
- package/packages/registry/blocks/preview/cards/invite-team.ts +78 -0
- package/packages/registry/blocks/preview/cards/invoice.ts +91 -0
- package/packages/registry/blocks/preview/cards/live-waveform.ts +330 -0
- package/packages/registry/blocks/preview/cards/no-team-members.ts +61 -0
- package/packages/registry/blocks/preview/cards/not-found.ts +46 -0
- package/packages/registry/blocks/preview/cards/observability-card.ts +44 -0
- package/packages/registry/blocks/preview/cards/pie-chart-card.ts +84 -0
- package/packages/registry/blocks/preview/cards/report-bug.ts +71 -0
- package/packages/registry/blocks/preview/cards/shipping-address.ts +79 -0
- package/packages/registry/blocks/preview/cards/shortcuts.ts +57 -0
- package/packages/registry/blocks/preview/cards/skeleton-loading.ts +37 -0
- package/packages/registry/blocks/preview/cards/sleep-report.ts +85 -0
- package/packages/registry/blocks/preview/cards/style-overview.ts +64 -0
- package/packages/registry/blocks/preview/cards/typography-specimen.ts +107 -0
- package/packages/registry/blocks/preview/cards/ui-elements.ts +164 -0
- package/packages/registry/blocks/preview/cards/usage-card.ts +63 -0
- package/packages/registry/blocks/preview/cards/visitors.ts +71 -0
- package/packages/registry/blocks/preview/cards/weekly-fitness-summary.ts +54 -0
- package/packages/registry/blocks/preview/page.ts +103 -0
- package/packages/registry/blocks/preview-02/cards/account-access.ts +70 -0
- package/packages/registry/blocks/preview-02/cards/card-overview.ts +75 -0
- package/packages/registry/blocks/preview-02/cards/claimable-balance.ts +54 -0
- package/packages/registry/blocks/preview-02/cards/contribution-history.ts +80 -0
- package/packages/registry/blocks/preview-02/cards/cover-art.ts +41 -0
- package/packages/registry/blocks/preview-02/cards/dividend-income.ts +76 -0
- package/packages/registry/blocks/preview-02/cards/empty-connect-bank.ts +38 -0
- package/packages/registry/blocks/preview-02/cards/empty-distribute-track.ts +38 -0
- package/packages/registry/blocks/preview-02/cards/empty-explore-catalog.ts +39 -0
- package/packages/registry/blocks/preview-02/cards/faq.ts +79 -0
- package/packages/registry/blocks/preview-02/cards/front-door.ts +40 -0
- package/packages/registry/blocks/preview-02/cards/index-investing.ts +31 -0
- package/packages/registry/blocks/preview-02/cards/kitchen-island.ts +90 -0
- package/packages/registry/blocks/preview-02/cards/loading-card.ts +34 -0
- package/packages/registry/blocks/preview-02/cards/new-milestone.ts +53 -0
- package/packages/registry/blocks/preview-02/cards/notification-settings.ts +126 -0
- package/packages/registry/blocks/preview-02/cards/payments.ts +88 -0
- package/packages/registry/blocks/preview-02/cards/payout-threshold.ts +92 -0
- package/packages/registry/blocks/preview-02/cards/power-usage.ts +84 -0
- package/packages/registry/blocks/preview-02/cards/preferences.ts +89 -0
- package/packages/registry/blocks/preview-02/cards/qr-connect.ts +54 -0
- package/packages/registry/blocks/preview-02/cards/receiving-method.ts +89 -0
- package/packages/registry/blocks/preview-02/cards/recent-transactions.ts +101 -0
- package/packages/registry/blocks/preview-02/cards/release-catalog.ts +73 -0
- package/packages/registry/blocks/preview-02/cards/roller-shades.ts +110 -0
- package/packages/registry/blocks/preview-02/cards/savings-progress.ts +68 -0
- package/packages/registry/blocks/preview-02/cards/savings-targets.ts +132 -0
- package/packages/registry/blocks/preview-02/cards/sidebar-nav.ts +88 -0
- package/packages/registry/blocks/preview-02/cards/social-links.ts +57 -0
- package/packages/registry/blocks/preview-02/cards/stock-performance.ts +106 -0
- package/packages/registry/blocks/preview-02/cards/syncing-state.ts +37 -0
- package/packages/registry/blocks/preview-02/cards/transfer-funds.ts +111 -0
- package/packages/registry/blocks/preview-02/cards/upcoming-payments.ts +69 -0
- package/packages/registry/blocks/preview-02/page.ts +111 -0
- package/packages/registry/blocks/preview-03/cards/files-chat.ts +75 -0
- package/packages/registry/blocks/preview-03/cards/group-chat.ts +114 -0
- package/packages/registry/blocks/preview-03/cards/reasoning-chat.ts +88 -0
- package/packages/registry/blocks/preview-03/cards/simple-chat.ts +53 -0
- package/packages/registry/blocks/preview-03/components/chat-card.ts +126 -0
- package/packages/registry/blocks/preview-03/components/chat-player.ts +174 -0
- package/packages/registry/blocks/preview-03/components/part-file.ts +77 -0
- package/packages/registry/blocks/preview-03/components/part-reasoning.ts +25 -0
- package/packages/registry/blocks/preview-03/components/part-source.ts +48 -0
- package/packages/registry/blocks/preview-03/components/part-text.ts +69 -0
- package/packages/registry/blocks/preview-03/components/part-tool.ts +71 -0
- package/packages/registry/blocks/preview-03/components/prompt-input.ts +86 -0
- package/packages/registry/blocks/preview-03/page.ts +39 -0
- package/packages/registry/blocks/sidebar-01/components/app-sidebar.ts +113 -0
- package/packages/registry/blocks/sidebar-01/components/search-form.ts +33 -0
- package/packages/registry/blocks/sidebar-01/components/version-switcher.ts +50 -0
- package/packages/registry/blocks/sidebar-01/page.ts +58 -0
- package/packages/registry/blocks/sidebar-02/components/app-sidebar.ts +131 -0
- package/packages/registry/blocks/sidebar-02/components/search-form.ts +33 -0
- package/packages/registry/blocks/sidebar-02/components/version-switcher.ts +50 -0
- package/packages/registry/blocks/sidebar-02/page.ts +53 -0
- package/packages/registry/blocks/sidebar-03/components/app-sidebar.ts +134 -0
- package/packages/registry/blocks/sidebar-03/page.ts +60 -0
- package/packages/registry/blocks/sidebar-04/components/app-sidebar.ts +136 -0
- package/packages/registry/blocks/sidebar-04/page.ts +58 -0
- package/packages/registry/blocks/sidebar-05/components/app-sidebar.ts +147 -0
- package/packages/registry/blocks/sidebar-05/components/search-form.ts +33 -0
- package/packages/registry/blocks/sidebar-05/page.ts +58 -0
- package/packages/registry/blocks/sidebar-06/components/app-sidebar.ts +105 -0
- package/packages/registry/blocks/sidebar-06/components/nav-main.ts +56 -0
- package/packages/registry/blocks/sidebar-06/components/sidebar-opt-in-form.ts +44 -0
- package/packages/registry/blocks/sidebar-06/page.ts +57 -0
- package/packages/registry/blocks/sidebar-07/components/app-sidebar.ts +108 -0
- package/packages/registry/blocks/sidebar-07/components/nav-main.ts +61 -0
- package/packages/registry/blocks/sidebar-07/components/nav-projects.ts +65 -0
- package/packages/registry/blocks/sidebar-07/components/nav-user.ts +70 -0
- package/packages/registry/blocks/sidebar-07/components/team-switcher.ts +66 -0
- package/packages/registry/blocks/sidebar-07/page.ts +59 -0
- package/packages/registry/blocks/sidebar-08/components/app-sidebar.ts +122 -0
- package/packages/registry/blocks/sidebar-08/components/nav-main.ts +68 -0
- package/packages/registry/blocks/sidebar-08/components/nav-projects.ts +65 -0
- package/packages/registry/blocks/sidebar-08/components/nav-secondary.ts +40 -0
- package/packages/registry/blocks/sidebar-08/components/nav-user.ts +70 -0
- package/packages/registry/blocks/sidebar-08/page.ts +59 -0
- package/packages/registry/blocks/sidebar-09/components/app-sidebar.ts +213 -0
- package/packages/registry/blocks/sidebar-09/components/nav-user.ts +70 -0
- package/packages/registry/blocks/sidebar-09/page.ts +52 -0
- package/packages/registry/blocks/sidebar-10/components/app-sidebar.ts +125 -0
- package/packages/registry/blocks/sidebar-10/components/nav-actions.ts +84 -0
- package/packages/registry/blocks/sidebar-10/components/nav-favorites.ts +66 -0
- package/packages/registry/blocks/sidebar-10/components/nav-main.ts +37 -0
- package/packages/registry/blocks/sidebar-10/components/nav-secondary.ts +43 -0
- package/packages/registry/blocks/sidebar-10/components/nav-workspaces.ts +78 -0
- package/packages/registry/blocks/sidebar-10/components/team-switcher.ts +63 -0
- package/packages/registry/blocks/sidebar-10/page.ts +46 -0
- package/packages/registry/blocks/sidebar-11/components/app-sidebar.ts +114 -0
- package/packages/registry/blocks/sidebar-11/page.ts +63 -0
- package/packages/registry/blocks/sidebar-12/components/app-sidebar.ts +63 -0
- package/packages/registry/blocks/sidebar-12/components/calendars.ts +53 -0
- package/packages/registry/blocks/sidebar-12/components/date-picker.ts +26 -0
- package/packages/registry/blocks/sidebar-12/components/nav-user.ts +70 -0
- package/packages/registry/blocks/sidebar-12/page.ts +43 -0
- package/packages/registry/blocks/sidebar-13/components/settings-dialog.ts +114 -0
- package/packages/registry/blocks/sidebar-13/page.ts +21 -0
- package/packages/registry/blocks/sidebar-14/components/app-sidebar.ts +120 -0
- package/packages/registry/blocks/sidebar-14/page.ts +55 -0
- package/packages/registry/blocks/sidebar-15/components/calendars.ts +53 -0
- package/packages/registry/blocks/sidebar-15/components/date-picker.ts +26 -0
- package/packages/registry/blocks/sidebar-15/components/nav-favorites.ts +66 -0
- package/packages/registry/blocks/sidebar-15/components/nav-main.ts +35 -0
- package/packages/registry/blocks/sidebar-15/components/nav-secondary.ts +43 -0
- package/packages/registry/blocks/sidebar-15/components/nav-user.ts +70 -0
- package/packages/registry/blocks/sidebar-15/components/nav-workspaces.ts +75 -0
- package/packages/registry/blocks/sidebar-15/components/sidebar-left.ts +126 -0
- package/packages/registry/blocks/sidebar-15/components/sidebar-right.ts +62 -0
- package/packages/registry/blocks/sidebar-15/components/team-switcher.ts +63 -0
- package/packages/registry/blocks/sidebar-15/page.ts +47 -0
- package/packages/registry/blocks/sidebar-16/components/app-sidebar.ts +124 -0
- package/packages/registry/blocks/sidebar-16/components/nav-main.ts +67 -0
- package/packages/registry/blocks/sidebar-16/components/nav-projects.ts +65 -0
- package/packages/registry/blocks/sidebar-16/components/nav-secondary.ts +40 -0
- package/packages/registry/blocks/sidebar-16/components/nav-user.ts +70 -0
- package/packages/registry/blocks/sidebar-16/components/search-form.ts +27 -0
- package/packages/registry/blocks/sidebar-16/components/site-header.ts +42 -0
- package/packages/registry/blocks/sidebar-16/page.ts +38 -0
- package/packages/registry/blocks/signup-01/components/signup-form.ts +78 -0
- package/packages/registry/blocks/signup-01/page.ts +19 -0
- package/packages/registry/blocks/signup-02/components/signup-form.ts +78 -0
- package/packages/registry/blocks/signup-02/page.ts +36 -0
- package/packages/registry/blocks/signup-03/components/signup-form.ts +79 -0
- package/packages/registry/blocks/signup-03/page.ts +28 -0
- package/packages/registry/blocks/signup-04/components/signup-form.ts +100 -0
- package/packages/registry/blocks/signup-04/page.ts +20 -0
- package/packages/registry/blocks/signup-05/components/signup-form.ts +75 -0
- package/packages/registry/blocks/signup-05/page.ts +20 -0
- package/packages/registry/components/accordion.ts +20 -7
- package/packages/registry/components/alert-dialog.ts +58 -13
- package/packages/registry/components/alert.ts +11 -6
- package/packages/registry/components/attachment.ts +119 -0
- package/packages/registry/components/avatar.ts +17 -8
- package/packages/registry/components/badge.ts +16 -9
- package/packages/registry/components/breadcrumb.ts +6 -6
- package/packages/registry/components/bubble.ts +95 -0
- package/packages/registry/components/button-group.ts +73 -0
- package/packages/registry/components/button.ts +35 -25
- package/packages/registry/components/calendar.ts +566 -0
- package/packages/registry/components/card.ts +8 -8
- package/packages/registry/components/carousel.ts +352 -0
- package/packages/registry/components/chart-element.ts +547 -0
- package/packages/registry/components/chart-geometry.ts +457 -0
- package/packages/registry/components/chart.ts +932 -0
- package/packages/registry/components/checkbox.ts +22 -84
- package/packages/registry/components/code-block.ts +191 -0
- package/packages/registry/components/collapsible.ts +5 -2
- package/packages/registry/components/combobox.ts +802 -0
- package/packages/registry/components/command.ts +618 -0
- package/packages/registry/components/context-menu.ts +302 -0
- package/packages/registry/components/dialog.ts +156 -45
- package/packages/registry/components/direction.ts +43 -0
- package/packages/registry/components/drawer.ts +226 -0
- package/packages/registry/components/dropdown-menu.ts +262 -83
- package/packages/registry/components/empty.ts +8 -8
- package/packages/registry/components/field.ts +20 -18
- package/packages/registry/components/floating.ts +124 -0
- package/packages/registry/components/hover-card.ts +19 -8
- package/packages/registry/components/input-group.ts +118 -0
- package/packages/registry/components/input-otp.ts +365 -0
- package/packages/registry/components/input.ts +3 -3
- package/packages/registry/components/item.ts +134 -0
- package/packages/registry/components/kbd.ts +2 -2
- package/packages/registry/components/label.ts +1 -1
- package/packages/registry/components/marker.ts +63 -0
- package/packages/registry/components/menubar.ts +388 -0
- package/packages/registry/components/message-scroller.ts +470 -0
- package/packages/registry/components/message.ts +69 -0
- package/packages/registry/components/native-select.ts +5 -5
- package/packages/registry/components/navigation-menu.ts +564 -0
- package/packages/registry/components/pagination.ts +5 -5
- package/packages/registry/components/popover.ts +30 -84
- package/packages/registry/components/progress.ts +7 -3
- package/packages/registry/components/questionnaire.ts +840 -0
- package/packages/registry/components/radio-group.ts +18 -73
- package/packages/registry/components/resizable.ts +330 -0
- package/packages/registry/components/scroll-area.ts +46 -0
- package/packages/registry/components/select.ts +650 -0
- package/packages/registry/components/separator.ts +1 -1
- package/packages/registry/components/sheet.ts +128 -0
- package/packages/registry/components/sidebar.ts +457 -0
- package/packages/registry/components/skeleton.ts +1 -1
- package/packages/registry/components/slider.ts +333 -0
- package/packages/registry/components/sonner.ts +74 -7
- package/packages/registry/components/spinner.ts +7 -4
- package/packages/registry/components/switch.ts +117 -9
- package/packages/registry/components/table.ts +11 -9
- package/packages/registry/components/tabs.ts +76 -25
- package/packages/registry/components/textarea.ts +1 -1
- package/packages/registry/components/toggle-group.ts +30 -11
- package/packages/registry/components/toggle.ts +20 -13
- package/packages/registry/components/tooltip.ts +68 -7
- package/packages/registry/components/trace.ts +75 -0
- package/packages/registry/lib/icons-hugeicons.ts +955 -0
- package/packages/registry/lib/icons-phosphor.ts +955 -0
- package/packages/registry/lib/icons-remixicon.ts +955 -0
- package/packages/registry/lib/icons-tabler.ts +955 -0
- package/packages/registry/lib/icons.ts +768 -0
- package/packages/registry/lib/utils.ts +49 -4
- package/packages/registry/registry.json +4056 -72
- package/packages/registry/styles/shadcn-tailwind.css +654 -0
- package/packages/registry/styles/style-luma.css +1987 -0
- package/packages/registry/styles/style-lyra.css +1965 -0
- package/packages/registry/styles/style-maia.css +1987 -0
- package/packages/registry/styles/style-mira.css +1992 -0
- package/packages/registry/styles/style-new-york.css +1597 -0
- package/packages/registry/styles/style-nova.css +1990 -0
- package/packages/registry/styles/style-rhea.css +1987 -0
- package/packages/registry/styles/style-sera.css +1977 -0
- package/packages/registry/styles/style-vega.css +1990 -0
- package/packages/registry/styles/supplement.json +54 -0
- package/packages/registry/styles/tw-animate.css +6 -0
- package/packages/registry/themes/base-colors.js +8 -2
- package/packages/registry/themes/fonts.js +120 -0
- package/packages/registry/themes/index.css +2 -0
- package/packages/registry/themes/items.js +61 -0
- package/packages/registry/themes/preset.js +302 -0
- package/packages/registry/themes/themes.js +1193 -0
- package/src/commands/add.js +236 -12
- package/src/commands/diff.js +34 -20
- package/src/commands/info.js +7 -0
- package/src/commands/init.js +125 -8
- package/src/commands/list.js +43 -11
- package/src/commands/view.js +33 -5
- package/src/registry/extract.js +192 -2
- package/src/registry/icons.js +31 -0
- package/src/registry/local.js +15 -1
- package/src/registry/schema.js +13 -0
- package/src/registry/style-bake.js +506 -0
- package/src/utils/theme.js +110 -21
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart: shadcn's Chart, drawn as plain SVG by the server. Tier-2. shadcn's
|
|
3
|
+
* is Recharts (D10: the kit takes no third-party runtime dependency), so
|
|
4
|
+
* this file ports the parts of Recharts the shadcn charts use: the bar, area,
|
|
5
|
+
* line, pie, radar and radial bar layouts, Recharts' default margins and axis
|
|
6
|
+
* sizes, and shadcn's tooltip and legend markup. Two files come with it and
|
|
7
|
+
* are re-exported here: `chart-geometry.ts`, the maths (scales, nice ticks,
|
|
8
|
+
* the d3 curves, the bar and sector paths), and `chart-element.ts`, the
|
|
9
|
+
* `<ui-chart>` element with the tooltip and legend markup it draws. Each chart function returns the whole `<ui-chart>`
|
|
10
|
+
* (shadcn's ChartContainer plus the chart inside it), so the first paint is
|
|
11
|
+
* the complete chart and it reads with JavaScript off.
|
|
12
|
+
*
|
|
13
|
+
* shadcn parity:
|
|
14
|
+
* ChartContainer config className → the chart function's `config` and `class`
|
|
15
|
+
* <BarChart> / <LineChart> / <AreaChart> → barChart({ ... }) / lineChart / areaChart
|
|
16
|
+
* <PieChart> / <RadarChart> / <RadialBarChart> → pieChart / radarChart / radialBarChart
|
|
17
|
+
* <Bar> / <Line> / <Area> children → `bars` / `lines` / `areas` arrays (same prop names)
|
|
18
|
+
* <CartesianGrid> / <XAxis> / <YAxis> → `grid` / `xAxis` / `yAxis` (same prop names)
|
|
19
|
+
* <ChartTooltip content={<ChartTooltipContent … />} />
|
|
20
|
+
* → `tooltip: { indicator, hideLabel, nameKey, … }`
|
|
21
|
+
* <ChartLegend content={<ChartLegendContent />} /> → `legend: { … }`
|
|
22
|
+
* <LabelList> / <Label> → `labels: [{ position, offset, class, … }]`, `center`
|
|
23
|
+
* shape / dot / label render props → the same options as functions returning `svg` templates
|
|
24
|
+
* ChartConfig → ChartConfig (same shape; `icon` is a function returning a template)
|
|
25
|
+
*
|
|
26
|
+
* The series colours are custom properties in the container's `style`
|
|
27
|
+
* (`--color-desktop: var(--chart-1)`, `light-dark(a, b)` for a `theme`
|
|
28
|
+
* entry), so `fill: 'var(--color-desktop)'` reads exactly as in shadcn.
|
|
29
|
+
* Every part carries a `ui-chart-*` class named after the `recharts-*` class
|
|
30
|
+
* it replaces (`recharts-pie-label-text` → `ui-chart-pie-label-text`), so a
|
|
31
|
+
* shadcn `className` such as `[&_.recharts-text]:fill-background` ports by
|
|
32
|
+
* that one rename.
|
|
33
|
+
*
|
|
34
|
+
* Sizing: the SVG has a `viewBox` of the chart's nominal size (450 by 253 for
|
|
35
|
+
* the default `aspect-video` container, 250 square for the polar charts) and
|
|
36
|
+
* scales to its container. Give `width` / `height` when the container has
|
|
37
|
+
* another shape (`class: 'aspect-auto h-[250px] w-full'` wants a wide box).
|
|
38
|
+
*
|
|
39
|
+
* The element: `<ui-chart>` adds what needs a pointer. Hovering shows the
|
|
40
|
+
* tooltip, the cursor and the active dots for the nearest point; the chart
|
|
41
|
+
* takes focus (Recharts' accessibilityLayer), the arrow keys step through the
|
|
42
|
+
* points, Home / End jump, Escape hides. Every tooltip is resolved by the
|
|
43
|
+
* server, one per point, so a `labelFormatter`, a `formatter` or an `icon`
|
|
44
|
+
* runs once on the server and never ships. A point whose tooltip is all text
|
|
45
|
+
* is served as data and drawn by the element; one with an icon or a custom
|
|
46
|
+
* row is served as markup.
|
|
47
|
+
*
|
|
48
|
+
* Design tokens used: `--chart-1` to `--chart-5` through the config, plus
|
|
49
|
+
* `border`, `muted`, `muted-foreground`, `background` and `foreground`.
|
|
50
|
+
*
|
|
51
|
+
* A11y (handled by the element, plus your part): the SVG is a `role="img"`
|
|
52
|
+
* named by `title`; once the element upgrades and the chart has a tooltip it
|
|
53
|
+
* becomes a focusable `role="application"` whose arrow keys move the tooltip,
|
|
54
|
+
* and each step is announced through a polite live region. Your part: pass a
|
|
55
|
+
* `title` that says what the chart shows, and keep the numbers available in
|
|
56
|
+
* the text around it (the card's description or footer) for a reader who
|
|
57
|
+
* cannot use the chart.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```html
|
|
61
|
+
* ${barChart({
|
|
62
|
+
* config: { desktop: { label: 'Desktop', color: 'var(--chart-1)' } },
|
|
63
|
+
* data: [{ month: 'January', desktop: 186 }, { month: 'February', desktop: 305 }],
|
|
64
|
+
* title: 'Desktop visitors by month',
|
|
65
|
+
* grid: { vertical: false },
|
|
66
|
+
* xAxis: { dataKey: 'month', tickLine: false, axisLine: false, tickMargin: 10, tickFormatter: (v) => String(v).slice(0, 3) },
|
|
67
|
+
* tooltip: { cursor: false, hideLabel: true },
|
|
68
|
+
* bars: [{ dataKey: 'desktop', fill: 'var(--color-desktop)', radius: 8 }],
|
|
69
|
+
* })}
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
import { html, svg, nothing } from '@webjsdev/core';
|
|
73
|
+
import { cn } from '../lib/utils.ts';
|
|
74
|
+
import {
|
|
75
|
+
RADIAN, fmt, formatValue, num, percent, estimateTextWidth, niceTicks, linearTicks, linearScale, bandScale, pointScale,
|
|
76
|
+
curvePath, rectanglePath, polarToCartesian, sectorPath, polygonPath, stackData, visibleTicks, barPositions,
|
|
77
|
+
type CurveType, type Pt, type SectorGeometry, type Tick,
|
|
78
|
+
} from './chart-geometry.ts';
|
|
79
|
+
import {
|
|
80
|
+
UiChart, chartStyle, payloadConfig, chartTooltipView, chartTooltipMarkup, chartTooltipContent, chartLegendContent, isPlainView,
|
|
81
|
+
type ChartConfig, type ChartConfigItem, type ChartDatum, type Margin, type ChartPayloadItem, type ChartTooltipOptions,
|
|
82
|
+
type ChartLegendOptions, type ChartBaseOptions, type ChartTooltipView, type TooltipLook,
|
|
83
|
+
} from './chart-element.ts';
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
fmt, formatValue, estimateTextWidth, niceTicks, linearTicks, linearScale, bandScale, pointScale, curvePath,
|
|
87
|
+
rectanglePath, polarToCartesian, sectorPath, polygonPath, stackData, visibleTicks, barPositions,
|
|
88
|
+
};
|
|
89
|
+
export type { CurveType, SectorGeometry, Tick };
|
|
90
|
+
export { UiChart, chartStyle, payloadConfig, chartTooltipView, chartTooltipMarkup, chartTooltipContent, chartLegendContent };
|
|
91
|
+
export type {
|
|
92
|
+
ChartConfig, ChartConfigItem, ChartDatum, Margin, ChartPayloadItem, ChartTooltipOptions, ChartLegendOptions, ChartBaseOptions,
|
|
93
|
+
ChartTooltipView,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// --------------------------------------------------------------------------
|
|
97
|
+
// Labels (Recharts' Label / LabelList positions)
|
|
98
|
+
// --------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
export type LabelPosition = 'top' | 'bottom' | 'left' | 'right' | 'inside' | 'insideLeft' | 'insideRight' | 'insideTop' | 'insideBottom' | 'center' | 'middle' | 'outside' | 'insideStart' | 'insideEnd' | 'end';
|
|
101
|
+
|
|
102
|
+
/** A LabelList: one label per data point of a series. */
|
|
103
|
+
export interface ChartLabelOptions {
|
|
104
|
+
/** The value to print, a key of the datum. Defaults to the series' dataKey. */
|
|
105
|
+
dataKey?: string;
|
|
106
|
+
position?: LabelPosition;
|
|
107
|
+
offset?: number;
|
|
108
|
+
class?: string;
|
|
109
|
+
fill?: string;
|
|
110
|
+
stroke?: string;
|
|
111
|
+
fontSize?: number;
|
|
112
|
+
formatter?: (value: unknown, datum: ChartDatum, index: number) => unknown;
|
|
113
|
+
/** A custom label: return an `svg` template. */
|
|
114
|
+
content?: (props: { x: number; y: number; width: number; height: number; value: unknown; datum: ChartDatum; index: number }) => unknown;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type VAnchor = 'start' | 'middle' | 'end';
|
|
118
|
+
const DY: Record<VAnchor, string> = { start: '0.71em', middle: '0.355em', end: '0em' };
|
|
119
|
+
|
|
120
|
+
/** A text element as Recharts' `<Text>` writes one: the position on the text, the baseline shift on a tspan. */
|
|
121
|
+
export function chartText(x: number, y: number, value: unknown, o: { anchor?: 'start' | 'middle' | 'end'; vAnchor?: VAnchor; class?: string; fill?: string | null; stroke?: string; fontSize?: number; baseline?: string } = {}) {
|
|
122
|
+
// A size in pixels, as Recharts takes it, written in em of the chart's
|
|
123
|
+
// 12px text so it keeps its size when the chart scales (see `_watchSize`).
|
|
124
|
+
const size = o.fontSize != null ? `${fmt(o.fontSize / 12)}em` : nothing;
|
|
125
|
+
return svg`<text x=${fmt(x)} y=${fmt(y)} text-anchor=${o.anchor ?? 'middle'} fill=${o.fill ?? nothing} stroke=${o.stroke ?? nothing} font-size=${size} alignment-baseline=${o.baseline ?? nothing} class=${cn('ui-chart-text', o.class)}><tspan x=${fmt(x)} dy=${DY[o.vAnchor ?? 'end']}>${value}</tspan></text>`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function cartesianLabelAttrs(box: { x: number; y: number; width: number; height: number }, position: LabelPosition, offset: number): { x: number; y: number; anchor: 'start' | 'middle' | 'end'; vAnchor: VAnchor } {
|
|
129
|
+
const { x, y, width, height } = box;
|
|
130
|
+
const vs = height >= 0 ? 1 : -1;
|
|
131
|
+
const hs = width >= 0 ? 1 : -1;
|
|
132
|
+
switch (position) {
|
|
133
|
+
case 'top': return { x: x + width / 2, y: y - vs * offset, anchor: 'middle', vAnchor: vs > 0 ? 'end' : 'start' };
|
|
134
|
+
case 'bottom': return { x: x + width / 2, y: y + height + vs * offset, anchor: 'middle', vAnchor: vs > 0 ? 'start' : 'end' };
|
|
135
|
+
case 'left': return { x: x - hs * offset, y: y + height / 2, anchor: hs > 0 ? 'end' : 'start', vAnchor: 'middle' };
|
|
136
|
+
case 'right': return { x: x + width + hs * offset, y: y + height / 2, anchor: hs > 0 ? 'start' : 'end', vAnchor: 'middle' };
|
|
137
|
+
case 'insideLeft': return { x: x + hs * offset, y: y + height / 2, anchor: hs > 0 ? 'start' : 'end', vAnchor: 'middle' };
|
|
138
|
+
case 'insideRight': return { x: x + width - hs * offset, y: y + height / 2, anchor: hs > 0 ? 'end' : 'start', vAnchor: 'middle' };
|
|
139
|
+
case 'insideTop': return { x: x + width / 2, y: y + vs * offset, anchor: 'middle', vAnchor: 'start' };
|
|
140
|
+
case 'insideBottom': return { x: x + width / 2, y: y + height - vs * offset, anchor: 'middle', vAnchor: 'end' };
|
|
141
|
+
default: return { x: x + width / 2, y: y + height / 2, anchor: 'middle', vAnchor: 'middle' };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function labelList(labels: ChartLabelOptions[] | undefined, seriesKey: string, items: Array<{ box: { x: number; y: number; width: number; height: number }; datum: ChartDatum; index: number; fill?: string }>, fallbackPosition: LabelPosition) {
|
|
146
|
+
if (!labels?.length) return '';
|
|
147
|
+
return labels.map((l) => svg`<g class="ui-chart-label-list">${items.map(({ box, datum, index, fill }) => {
|
|
148
|
+
const raw = datum[l.dataKey ?? seriesKey];
|
|
149
|
+
const value = l.formatter ? l.formatter(raw, datum, index) : raw;
|
|
150
|
+
if (l.content) return l.content({ ...box, value, datum, index });
|
|
151
|
+
const a = cartesianLabelAttrs(box, l.position ?? fallbackPosition, l.offset ?? 5);
|
|
152
|
+
// A label takes its mark's colour unless it names one, as Recharts' does.
|
|
153
|
+
return chartText(a.x, a.y, value, { anchor: a.anchor, vAnchor: a.vAnchor, class: cn('ui-chart-label', l.class), fill: l.fill ?? fill, stroke: l.stroke, fontSize: l.fontSize });
|
|
154
|
+
})}</g>`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --------------------------------------------------------------------------
|
|
158
|
+
// The container (shadcn's ChartContainer) and the tooltip layer
|
|
159
|
+
// --------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* shadcn's ChartContainer classes, its `[&_.recharts-*]` rules renamed to the
|
|
163
|
+
* `ui-chart-*` parts, plus one of the kit's own: strokes keep their width in
|
|
164
|
+
* pixels when the chart scales, as Recharts draws them at the real size.
|
|
165
|
+
*/
|
|
166
|
+
export function chartContainerClass(): string {
|
|
167
|
+
return "cn-chart flex aspect-video justify-center text-xs [&_.ui-chart-cartesian-axis-tick_text]:fill-muted-foreground [&_.ui-chart-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.ui-chart-curve.ui-chart-tooltip-cursor]:stroke-border [&_.ui-chart-dot[stroke='#fff']]:stroke-transparent [&_.ui-chart-layer]:outline-hidden [&_.ui-chart-polar-grid_[stroke='#ccc']]:stroke-border [&_.ui-chart-radial-bar-background-sector]:fill-muted [&_.ui-chart-rectangle.ui-chart-tooltip-cursor]:fill-muted [&_.ui-chart-reference-line_[stroke='#ccc']]:stroke-border [&_.ui-chart-sector]:outline-hidden [&_.ui-chart-sector[stroke='#fff']]:stroke-transparent [&_.ui-chart-surface]:outline-hidden [&_.ui-chart-surface_*]:[vector-effect:non-scaling-stroke]";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** One tooltip point: its category, its series, and where the tooltip anchors (viewBox units). */
|
|
171
|
+
export interface ChartPoint {
|
|
172
|
+
label: unknown;
|
|
173
|
+
payload: ChartPayloadItem[];
|
|
174
|
+
x: number;
|
|
175
|
+
y: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface Frame {
|
|
179
|
+
width: number;
|
|
180
|
+
height: number;
|
|
181
|
+
/** How the element maps a pointer to a point: along x, along y, or by the hovered shape. */
|
|
182
|
+
hit: 'x' | 'y' | 'target';
|
|
183
|
+
body: unknown;
|
|
184
|
+
points: ChartPoint[];
|
|
185
|
+
legend: ChartPayloadItem[];
|
|
186
|
+
margin: Margin;
|
|
187
|
+
/** The plot area, the box Recharts keeps a tooltip inside. */
|
|
188
|
+
plot: { x: number; y: number; w: number; h: number };
|
|
189
|
+
/** The element may widen the plot to fill its box (a cartesian chart). */
|
|
190
|
+
fit?: boolean;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Recharts sorts a legend by each item's value (its `itemSorter` default),
|
|
194
|
+
// which for a series is its name: a stable sort, so equal names keep order.
|
|
195
|
+
function sortLegend(items: ChartPayloadItem[]): ChartPayloadItem[] {
|
|
196
|
+
return [...items].sort((a, b) => {
|
|
197
|
+
const x = String(a.value);
|
|
198
|
+
const y = String(b.value);
|
|
199
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function legendLayer(o: ChartBaseOptions, f: Frame) {
|
|
204
|
+
if (!o.legend) return '';
|
|
205
|
+
// An HTML row over the chart rather than a foreignObject inside it, as
|
|
206
|
+
// Recharts' legend wrapper is: anchored at the margin, as wide as the area
|
|
207
|
+
// between the side margins, as tall as its content (a wrapping legend grows
|
|
208
|
+
// away from the edge), and in real pixels, since WebKit misplaces the
|
|
209
|
+
// content of a scaled foreignObject and Firefox does not scale its font.
|
|
210
|
+
const pct = (v: number, of: number) => `${fmt((v / of) * 100)}%`;
|
|
211
|
+
const edge = o.legend.verticalAlign === 'top' ? `top: ${pct(f.margin.top, f.height)}` : `bottom: ${pct(f.margin.bottom, f.height)}`;
|
|
212
|
+
return html`<div class="ui-chart-legend-wrapper absolute" style=${`left: ${pct(f.margin.left, f.width)}; right: ${pct(f.margin.right, f.width)}; ${edge}`}>${chartLegendContent(o.config, sortLegend(f.legend), o.legend)}</div>`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function frame(o: ChartBaseOptions, f: Frame) {
|
|
216
|
+
const t = o.tooltip || null;
|
|
217
|
+
const def = t && t.defaultIndex != null ? f.points[t.defaultIndex] : undefined;
|
|
218
|
+
const a11y = o.accessibilityLayer !== false;
|
|
219
|
+
// Each point's tooltip is resolved here, on the server, so a formatter never
|
|
220
|
+
// ships. One that is all strings is served as data (a 90 point chart would
|
|
221
|
+
// otherwise carry 90 copies of the markup); one with an icon or a custom
|
|
222
|
+
// row is served as markup.
|
|
223
|
+
const look: TooltipLook = t ? { indicator: t.indicator, hideIndicator: t.hideIndicator, class: t.class, labelClass: t.labelClass } : {};
|
|
224
|
+
const views = t ? f.points.map((p) => chartTooltipView(o.config, p.label, p.payload, t)) : [];
|
|
225
|
+
// cn() keeps both aspect utilities, so an authored one replaces the default here.
|
|
226
|
+
const base = /(^|\s)aspect-/.test(o.class ?? '') ? chartContainerClass().replace(' aspect-video', '') : chartContainerClass();
|
|
227
|
+
return html`<ui-chart data-slot="chart" data-chart=${o.id ?? nothing} class=${cn(base, o.class)} style=${chartStyle(o.config) || nothing}><div class="ui-chart-wrapper relative h-full w-full min-w-0"><svg class="ui-chart-surface block h-full w-full overflow-visible" xmlns="http://www.w3.org/2000/svg" viewBox=${`0 0 ${fmt(f.width)} ${fmt(f.height)}`} role="img" aria-label=${o.title ?? nothing} data-hit=${t ? f.hit : nothing} data-keyboard=${t && a11y ? 'true' : nothing} data-default-index=${def ? String(t?.defaultIndex) : nothing} data-fit=${f.fit ? 'x' : nothing} data-plot=${t || f.fit ? [f.plot.x, f.plot.y, f.plot.w, f.plot.h].map((v) => fmt(v)).join(' ') : nothing}>${o.defs ? svg`<defs>${o.defs}</defs>` : ''}${f.body}</svg>${legendLayer(o, f)}${t ? html`<div data-slot="chart-tooltip" class="ui-chart-tooltip-wrapper pointer-events-none absolute top-0 left-0 z-10 [transition:transform_400ms]" ?hidden=${!def} style=${def ? `transform: translate(${fmt(def.x + 10)}px, ${fmt(def.y + 10)}px)` : nothing}>${def && t && t.defaultIndex != null ? chartTooltipMarkup(views[t.defaultIndex], look) : ''}</div><div data-slot="chart-tooltips" hidden data-look=${JSON.stringify(look)}>${views.map((v, i) => (isPlainView(v)
|
|
228
|
+
? html`<div data-index=${String(i)} data-x=${fmt(f.points[i].x)} data-y=${fmt(f.points[i].y)} data-tip=${JSON.stringify(v)}></div>`
|
|
229
|
+
: html`<div data-index=${String(i)} data-x=${fmt(f.points[i].x)} data-y=${fmt(f.points[i].y)}>${chartTooltipMarkup(v, look)}</div>`))}</div><span data-slot="chart-live" class="sr-only" aria-live="polite"></span>` : ''}</div></ui-chart>`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The per-point overlay groups: hidden until their point is active (or it is the default). */
|
|
233
|
+
function overlay(slot: 'chart-cursor' | 'chart-active', parts: unknown[], defaultIndex: number | undefined) {
|
|
234
|
+
return svg`<g data-slot=${slot}>${parts.map((p, i) => svg`<g data-index=${String(i)} visibility=${i === defaultIndex ? 'visible' : 'hidden'}>${p}</g>`)}</g>`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function resolveMargin(m: Partial<Margin> | undefined): Margin {
|
|
238
|
+
if (!m) return { top: 5, right: 5, bottom: 5, left: 5 };
|
|
239
|
+
return { top: m.top ?? 0, right: m.right ?? 0, bottom: m.bottom ?? 0, left: m.left ?? 0 };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// --------------------------------------------------------------------------
|
|
243
|
+
// Cartesian charts: bar, line, area
|
|
244
|
+
// --------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
export interface ChartAxisOptions {
|
|
247
|
+
dataKey?: string;
|
|
248
|
+
type?: 'category' | 'number';
|
|
249
|
+
hide?: boolean;
|
|
250
|
+
tick?: boolean;
|
|
251
|
+
tickLine?: boolean;
|
|
252
|
+
axisLine?: boolean;
|
|
253
|
+
tickMargin?: number;
|
|
254
|
+
tickSize?: number;
|
|
255
|
+
tickFormatter?: (value: unknown, index: number) => unknown;
|
|
256
|
+
minTickGap?: number;
|
|
257
|
+
tickCount?: number;
|
|
258
|
+
interval?: number | 'preserveEnd' | 'preserveStart' | 'preserveStartEnd';
|
|
259
|
+
/** The x axis's height, default 30. */
|
|
260
|
+
height?: number;
|
|
261
|
+
/** The y axis's width, default 60. */
|
|
262
|
+
width?: number;
|
|
263
|
+
domain?: [number | 'auto' | 'dataMin' | 'dataMax', number | 'auto' | 'dataMin' | 'dataMax'];
|
|
264
|
+
allowDecimals?: boolean;
|
|
265
|
+
padding?: { left?: number; right?: number; top?: number; bottom?: number };
|
|
266
|
+
/** Run the axis the other way, Recharts' `reversed` (a right-to-left chart's x axis). */
|
|
267
|
+
reversed?: boolean;
|
|
268
|
+
class?: string;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface ChartDotOptions { r?: number; fill?: string; stroke?: string; strokeWidth?: number; fillOpacity?: number; class?: string }
|
|
272
|
+
export type ChartDotRender = (props: { cx: number; cy: number; r: number; fill: string; stroke: string; datum: ChartDatum; index: number; dataKey: string }) => unknown;
|
|
273
|
+
|
|
274
|
+
export interface ChartBarShapeProps {
|
|
275
|
+
x: number; y: number; width: number; height: number;
|
|
276
|
+
radius: number | number[]; fill: string; datum: ChartDatum; index: number; dataKey: string;
|
|
277
|
+
stroke?: string; strokeWidth?: number; fillOpacity?: number;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** A bar as the kit draws one (Recharts' `<Rectangle>`), for a custom `shape` to reuse. */
|
|
281
|
+
export function chartRectangle(p: Omit<ChartBarShapeProps, 'datum' | 'index' | 'dataKey'> & { strokeDasharray?: number | string; strokeDashoffset?: number; class?: string }) {
|
|
282
|
+
return svg`<path class=${cn('ui-chart-rectangle', p.class)} fill=${p.fill} fill-opacity=${p.fillOpacity ?? nothing} stroke=${p.stroke ?? nothing} stroke-width=${p.strokeWidth ?? nothing} stroke-dasharray=${p.strokeDasharray ?? nothing} stroke-dashoffset=${p.strokeDashoffset ?? nothing} d=${rectanglePath(p.x, p.y, p.width, p.height, p.radius)}></path>`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface ChartSeriesOptions {
|
|
286
|
+
dataKey: string;
|
|
287
|
+
name?: string;
|
|
288
|
+
fill?: string;
|
|
289
|
+
stroke?: string;
|
|
290
|
+
strokeWidth?: number;
|
|
291
|
+
fillOpacity?: number;
|
|
292
|
+
strokeDasharray?: string;
|
|
293
|
+
stackId?: string;
|
|
294
|
+
/** Line and area curve, Recharts' `type`. */
|
|
295
|
+
type?: CurveType;
|
|
296
|
+
/** Bar corner radius, one number or `[tl, tr, br, bl]`. */
|
|
297
|
+
radius?: number | number[];
|
|
298
|
+
/** Line dots (default on for lines, off for areas), options or a render function. */
|
|
299
|
+
dot?: boolean | ChartDotOptions | ChartDotRender;
|
|
300
|
+
/** The hover dot (lines and areas), default on. */
|
|
301
|
+
activeDot?: boolean | ChartDotOptions;
|
|
302
|
+
labels?: ChartLabelOptions[];
|
|
303
|
+
/** A bar drawn active from the first paint, and how (Recharts' `activeIndex` + `activeBar`). */
|
|
304
|
+
activeIndex?: number;
|
|
305
|
+
activeBar?: { fill?: string; fillOpacity?: number; stroke?: string; strokeWidth?: number; strokeDasharray?: string; strokeDashoffset?: number };
|
|
306
|
+
/** A custom bar, Recharts' `shape`. */
|
|
307
|
+
shape?: (props: ChartBarShapeProps) => unknown;
|
|
308
|
+
barSize?: number;
|
|
309
|
+
class?: string;
|
|
310
|
+
hide?: boolean;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface CartesianChartOptions extends ChartBaseOptions {
|
|
314
|
+
layout?: 'horizontal' | 'vertical';
|
|
315
|
+
barCategoryGap?: number | string;
|
|
316
|
+
barGap?: number | string;
|
|
317
|
+
maxBarSize?: number;
|
|
318
|
+
stackOffset?: 'none' | 'expand' | 'sign';
|
|
319
|
+
grid?: false | { horizontal?: boolean; vertical?: boolean; strokeDasharray?: string; class?: string };
|
|
320
|
+
xAxis?: ChartAxisOptions | false;
|
|
321
|
+
yAxis?: ChartAxisOptions | false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
type Family = 'bar' | 'line' | 'area';
|
|
325
|
+
|
|
326
|
+
function valueDomain(spec: ChartAxisOptions | undefined, dataMin: number, dataMax: number, expand: boolean): { domain: [number, number]; ticks: number[] } {
|
|
327
|
+
if (expand) return { domain: [0, 1], ticks: niceTicks(0, 1, spec?.tickCount ?? 5) };
|
|
328
|
+
const d = spec?.domain;
|
|
329
|
+
const pick = (v: unknown, auto: number, data: number): number => (typeof v === 'number' ? v : v === 'dataMin' || v === 'dataMax' ? data : auto);
|
|
330
|
+
const lo = pick(d?.[0], Math.min(0, dataMin), dataMin);
|
|
331
|
+
const hi = pick(d?.[1], dataMax, dataMax);
|
|
332
|
+
const ticks = niceTicks(lo, hi, spec?.tickCount ?? 5, spec?.allowDecimals ?? true);
|
|
333
|
+
const [d0, d1] = d ?? [];
|
|
334
|
+
if (typeof d0 === 'number' && typeof d1 === 'number') return { domain: [d0, d1], ticks: ticks.filter((t) => t >= d0 && t <= d1) };
|
|
335
|
+
return { domain: [Math.min(lo, ticks[0]), Math.max(hi, ticks[ticks.length - 1])], ticks };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function dotProps(spec: boolean | ChartDotOptions | ChartDotRender | undefined, fallback: boolean): ChartDotOptions | ChartDotRender | null {
|
|
339
|
+
if (spec === undefined) return fallback ? {} : null;
|
|
340
|
+
if (spec === false) return null;
|
|
341
|
+
if (spec === true) return {};
|
|
342
|
+
return spec;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function drawDot(spec: ChartDotOptions | ChartDotRender, base: { cx: number; cy: number; r: number; fill: string; stroke: string; strokeWidth: number }, datum: ChartDatum, index: number, dataKey: string, cls: string) {
|
|
346
|
+
if (typeof spec === 'function') return spec({ cx: base.cx, cy: base.cy, r: base.r, fill: base.fill, stroke: base.stroke, datum, index, dataKey });
|
|
347
|
+
return svg`<circle cx=${fmt(base.cx)} cy=${fmt(base.cy)} r=${fmt(spec.r ?? base.r)} fill=${spec.fill ?? base.fill} fill-opacity=${spec.fillOpacity ?? nothing} stroke=${spec.stroke ?? base.stroke} stroke-width=${spec.strokeWidth ?? base.strokeWidth} class=${cn('ui-chart-dot', cls, spec.class)}></circle>`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function cartesian(o: CartesianChartOptions, family: Family, all: ChartSeriesOptions[]) {
|
|
351
|
+
const W = o.width ?? 450;
|
|
352
|
+
const H = o.height ?? Math.round((W * 9) / 16);
|
|
353
|
+
const margin = resolveMargin(o.margin);
|
|
354
|
+
const vertical = o.layout === 'vertical';
|
|
355
|
+
const xSpec = o.xAxis || undefined;
|
|
356
|
+
const ySpec = o.yAxis || undefined;
|
|
357
|
+
const catSpec = vertical ? ySpec : xSpec;
|
|
358
|
+
const valSpec = vertical ? xSpec : ySpec;
|
|
359
|
+
const xShown = !!xSpec && !xSpec.hide;
|
|
360
|
+
const yShown = !!ySpec && !ySpec.hide;
|
|
361
|
+
const xH = xShown ? xSpec.height ?? 30 : 0;
|
|
362
|
+
const yW = yShown ? ySpec.width ?? 60 : 0;
|
|
363
|
+
const legendH = o.legend ? o.legend.height ?? 28 : 0;
|
|
364
|
+
const legendTop = !!o.legend && o.legend.verticalAlign === 'top';
|
|
365
|
+
const plot = {
|
|
366
|
+
x: margin.left + yW,
|
|
367
|
+
y: margin.top + (legendTop ? legendH : 0),
|
|
368
|
+
w: W - margin.left - margin.right - yW,
|
|
369
|
+
h: H - margin.top - margin.bottom - xH - legendH,
|
|
370
|
+
};
|
|
371
|
+
const data = o.data;
|
|
372
|
+
const series = all.filter((s) => !s.hide);
|
|
373
|
+
const catKey = catSpec?.dataKey;
|
|
374
|
+
const categories = data.map((d, i) => (catKey ? d[catKey] : i));
|
|
375
|
+
|
|
376
|
+
// Values: each series' [low, high] per row, stacked by stackId.
|
|
377
|
+
const offsetMode = o.stackOffset ?? (family === 'bar' ? 'sign' : 'none');
|
|
378
|
+
const bands: Record<string, Array<[number, number]>> = {};
|
|
379
|
+
const stacks = new Map<string, string[]>();
|
|
380
|
+
for (const s of series) if (s.stackId != null) stacks.set(s.stackId, [...(stacks.get(s.stackId) ?? []), s.dataKey]);
|
|
381
|
+
for (const keys of stacks.values()) Object.assign(bands, stackData(data, keys, offsetMode === 'expand' ? 'expand' : offsetMode === 'sign' ? 'sign' : 'none'));
|
|
382
|
+
for (const s of series) if (s.stackId == null) bands[s.dataKey] = data.map((d) => [0, num(d[s.dataKey])]);
|
|
383
|
+
const flat = series.flatMap((s) => bands[s.dataKey].flat());
|
|
384
|
+
const { domain, ticks: valueTicks } = valueDomain(valSpec, flat.length ? Math.min(...flat) : 0, flat.length ? Math.max(...flat) : 0, offsetMode === 'expand');
|
|
385
|
+
const vRange: [number, number] = vertical ? [plot.x, plot.x + plot.w] : [plot.y + plot.h, plot.y];
|
|
386
|
+
const vScale = linearScale(domain, valSpec?.reversed ? [vRange[1], vRange[0]] : vRange);
|
|
387
|
+
|
|
388
|
+
// Categories: bands under bars, points under lines and areas.
|
|
389
|
+
const pad = catSpec?.padding ?? {};
|
|
390
|
+
const c0 = vertical ? plot.y + (pad.top ?? 0) : plot.x + (pad.left ?? 0);
|
|
391
|
+
const c1 = vertical ? plot.y + plot.h - (pad.bottom ?? 0) : plot.x + plot.w - (pad.right ?? 0);
|
|
392
|
+
// A reversed category axis lays the rows out from the far end.
|
|
393
|
+
const slot = (i: number) => (catSpec?.reversed ? data.length - 1 - i : i);
|
|
394
|
+
const band = bandScale(data.length, c0, c1);
|
|
395
|
+
const point = pointScale(data.length, c0, c1);
|
|
396
|
+
const catAt = (i: number) => (family === 'bar' ? band.at(slot(i)) + band.bandwidth / 2 : point(slot(i)));
|
|
397
|
+
const color = (s: ChartSeriesOptions) => (family === 'bar' ? s.fill : s.stroke ?? s.fill) ?? '#808080';
|
|
398
|
+
const defaultIndex = o.tooltip ? o.tooltip.defaultIndex : undefined;
|
|
399
|
+
|
|
400
|
+
// Grid lines at the value ticks and the category ticks.
|
|
401
|
+
const g = o.grid;
|
|
402
|
+
const gridLine = (x1: number, y1: number, x2: number, y2: number) => svg`<line stroke="#ccc" fill="none" stroke-dasharray=${(g && g.strokeDasharray) || nothing} x1=${fmt(x1)} y1=${fmt(y1)} x2=${fmt(x2)} y2=${fmt(y2)}></line>`;
|
|
403
|
+
const valueCoords = valueTicks.map((t) => vScale(t));
|
|
404
|
+
const catCoords = data.map((_, i) => catAt(i));
|
|
405
|
+
const hLines = vertical ? catCoords : valueCoords;
|
|
406
|
+
const vLines = vertical ? valueCoords : catCoords;
|
|
407
|
+
const grid = g === undefined || g === false ? '' : svg`<g class=${cn('ui-chart-cartesian-grid', g.class)}>${g.horizontal === false ? '' : svg`<g class="ui-chart-cartesian-grid-horizontal">${hLines.map((y) => gridLine(plot.x, y, plot.x + plot.w, y))}</g>`}${g.vertical === false ? '' : svg`<g class="ui-chart-cartesian-grid-vertical">${vLines.map((x) => gridLine(x, plot.y, x, plot.y + plot.h))}</g>`}</g>`;
|
|
408
|
+
|
|
409
|
+
// The hover cursor per point.
|
|
410
|
+
const cursorOn = !!o.tooltip && o.tooltip.cursor !== false;
|
|
411
|
+
const cursors = cursorOn
|
|
412
|
+
? data.map((_, i) => {
|
|
413
|
+
if (family === 'bar') {
|
|
414
|
+
const a = band.at(slot(i));
|
|
415
|
+
return vertical
|
|
416
|
+
? svg`<path class="ui-chart-rectangle ui-chart-tooltip-cursor" fill="#ccc" d=${rectanglePath(plot.x, a, plot.w, band.bandwidth)}></path>`
|
|
417
|
+
: svg`<path class="ui-chart-rectangle ui-chart-tooltip-cursor" fill="#ccc" d=${rectanglePath(a, plot.y, band.bandwidth, plot.h)}></path>`;
|
|
418
|
+
}
|
|
419
|
+
const c = catAt(i);
|
|
420
|
+
return vertical
|
|
421
|
+
? svg`<path class="ui-chart-curve ui-chart-tooltip-cursor" stroke="#ccc" fill="none" d=${`M${fmt(plot.x)},${fmt(c)}L${fmt(plot.x + plot.w)},${fmt(c)}`}></path>`
|
|
422
|
+
: svg`<path class="ui-chart-curve ui-chart-tooltip-cursor" stroke="#ccc" fill="none" d=${`M${fmt(c)},${fmt(plot.y)}L${fmt(c)},${fmt(plot.y + plot.h)}`}></path>`;
|
|
423
|
+
})
|
|
424
|
+
: [];
|
|
425
|
+
|
|
426
|
+
// Marks.
|
|
427
|
+
const slotIds: string[] = [];
|
|
428
|
+
for (const s of series) { const id = s.stackId != null ? `stack:${s.stackId}` : `bar:${s.dataKey}`; if (!slotIds.includes(id)) slotIds.push(id); }
|
|
429
|
+
const slotSpecs = slotIds.map((id) => ({ barSize: series.find((s) => (s.stackId != null ? `stack:${s.stackId}` : `bar:${s.dataKey}`) === id)?.barSize }));
|
|
430
|
+
const positions = family === 'bar' ? barPositions(band.bandwidth, slotSpecs, o.barGap, o.barCategoryGap, o.maxBarSize) : [];
|
|
431
|
+
|
|
432
|
+
const marks = series.map((s) => {
|
|
433
|
+
const sb = bands[s.dataKey];
|
|
434
|
+
if (family === 'bar') {
|
|
435
|
+
const pos = positions[slotIds.indexOf(s.stackId != null ? `stack:${s.stackId}` : `bar:${s.dataKey}`)];
|
|
436
|
+
const boxes = data.map((d, i) => {
|
|
437
|
+
const [lo, hi] = sb[i];
|
|
438
|
+
const a = band.at(slot(i)) + pos.offset;
|
|
439
|
+
const box = vertical
|
|
440
|
+
? { x: vScale(lo), y: a, width: vScale(hi) - vScale(lo), height: pos.size }
|
|
441
|
+
: { x: a, y: vScale(hi), width: pos.size, height: vScale(lo) - vScale(hi) };
|
|
442
|
+
return { box, datum: d, index: i, fill: typeof d.fill === 'string' ? d.fill : s.fill };
|
|
443
|
+
});
|
|
444
|
+
return svg`<g class=${cn('ui-chart-layer ui-chart-bar', s.class)}><g class="ui-chart-layer ui-chart-bar-rectangles">${boxes.map(({ box, datum, index }) => {
|
|
445
|
+
const fill = typeof datum.fill === 'string' ? datum.fill : s.fill ?? '#808080';
|
|
446
|
+
const active = s.activeIndex === index ? s.activeBar ?? {} : null;
|
|
447
|
+
const shape = s.shape
|
|
448
|
+
? s.shape({ ...box, radius: s.radius ?? 0, fill, datum, index, dataKey: s.dataKey, stroke: s.stroke, strokeWidth: s.strokeWidth, fillOpacity: s.fillOpacity })
|
|
449
|
+
: svg`<path class="ui-chart-rectangle" fill=${active?.fill ?? fill} fill-opacity=${active?.fillOpacity ?? s.fillOpacity ?? nothing} stroke=${active?.stroke ?? s.stroke ?? nothing} stroke-width=${active?.strokeWidth ?? s.strokeWidth ?? nothing} stroke-dasharray=${active?.strokeDasharray ?? nothing} stroke-dashoffset=${active?.strokeDashoffset ?? nothing} d=${rectanglePath(box.x, box.y, box.width, box.height, s.radius ?? 0)}></path>`;
|
|
450
|
+
return svg`<g class="ui-chart-layer ui-chart-bar-rectangle" data-index=${String(index)}>${shape}</g>`;
|
|
451
|
+
})}</g>${labelList(s.labels, s.dataKey, boxes, 'center')}</g>`;
|
|
452
|
+
}
|
|
453
|
+
const top = data.map((_, i): Pt => (vertical ? [vScale(sb[i][1]), catAt(i)] : [catAt(i), vScale(sb[i][1])]));
|
|
454
|
+
const curve = s.type ?? 'linear';
|
|
455
|
+
const stroke = s.stroke ?? '#3182bd';
|
|
456
|
+
const pts = top.map((p, i) => ({ box: { x: p[0], y: p[1], width: 0, height: 0 }, datum: data[i], index: i }));
|
|
457
|
+
const dot = dotProps(s.dot, family === 'line');
|
|
458
|
+
const dots = dot
|
|
459
|
+
? svg`<g class="ui-chart-layer ui-chart-dots">${top.map((p, i) => drawDot(dot, { cx: p[0], cy: p[1], r: 3, fill: family === 'line' ? '#fff' : s.fill ?? stroke, stroke, strokeWidth: s.strokeWidth ?? 1 }, data[i], i, s.dataKey, family === 'line' ? 'ui-chart-line-dot' : 'ui-chart-area-dot'))}</g>`
|
|
460
|
+
: '';
|
|
461
|
+
if (family === 'line') {
|
|
462
|
+
return svg`<g class=${cn('ui-chart-layer ui-chart-line', s.class)}><path class="ui-chart-curve ui-chart-line-curve" fill="none" stroke=${stroke} stroke-width=${s.strokeWidth ?? 1} stroke-dasharray=${s.strokeDasharray ?? nothing} d=${curvePath(top, curve)}></path>${dots}${labelList(s.labels, s.dataKey, pts, 'top')}</g>`;
|
|
463
|
+
}
|
|
464
|
+
const bottom = data.map((_, i): Pt => {
|
|
465
|
+
const lo = s.stackId != null ? sb[i][0] : Math.max(domain[0], Math.min(0, domain[1]));
|
|
466
|
+
return vertical ? [vScale(lo), catAt(i)] : [catAt(i), vScale(lo)];
|
|
467
|
+
}).reverse();
|
|
468
|
+
const area = `${curvePath(top, curve)}${curvePath(bottom, curve, 'L')}Z`;
|
|
469
|
+
return svg`<g class=${cn('ui-chart-layer ui-chart-area', s.class)}><path class="ui-chart-curve ui-chart-area-area" fill=${s.fill ?? '#3182bd'} fill-opacity=${s.fillOpacity ?? 0.6} stroke="none" d=${area}></path><path class="ui-chart-curve ui-chart-area-curve" fill="none" stroke=${stroke} stroke-width=${s.strokeWidth ?? 1} stroke-dasharray=${s.strokeDasharray ?? nothing} d=${curvePath(top, curve)}></path>${dots}${labelList(s.labels, s.dataKey, pts, 'top')}</g>`;
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Active dots per point (lines and areas).
|
|
473
|
+
const actives = family === 'bar' || !o.tooltip
|
|
474
|
+
? []
|
|
475
|
+
: data.map((_, i) => series.map((s) => {
|
|
476
|
+
if (s.activeDot === false) return '';
|
|
477
|
+
const sb = bands[s.dataKey];
|
|
478
|
+
const [cx, cy] = vertical ? [vScale(sb[i][1]), catAt(i)] : [catAt(i), vScale(sb[i][1])];
|
|
479
|
+
const spec = typeof s.activeDot === 'object' ? s.activeDot : {};
|
|
480
|
+
return drawDot(spec, { cx, cy, r: 4, fill: s.stroke ?? s.fill ?? '#3182bd', stroke: '#fff', strokeWidth: 2 }, data[i], i, s.dataKey, 'ui-chart-active-dot');
|
|
481
|
+
}));
|
|
482
|
+
|
|
483
|
+
// Axes.
|
|
484
|
+
const axis = (which: 'x' | 'y') => {
|
|
485
|
+
const spec = which === 'x' ? xSpec : ySpec;
|
|
486
|
+
if (!spec || spec.hide) return '';
|
|
487
|
+
const isCat = (which === 'x') !== vertical;
|
|
488
|
+
const fmtTick = (v: unknown, i: number) => (spec.tickFormatter ? spec.tickFormatter(v, i) : v);
|
|
489
|
+
const ticks: Tick[] = isCat
|
|
490
|
+
? categories.map((v, i) => ({ value: v, label: fmtTick(v, i), coord: catAt(i) }))
|
|
491
|
+
: valueTicks.map((v, i) => ({ value: v, label: fmtTick(v, i), coord: vScale(v) }));
|
|
492
|
+
const size = (t: Tick) => (which === 'x' ? estimateTextWidth(String(t.label ?? '')) : 14);
|
|
493
|
+
const interval = spec.interval ?? 'preserveEnd';
|
|
494
|
+
const minTickGap = spec.minTickGap ?? 5;
|
|
495
|
+
// Every tick is served; the ones that do not fit are hidden. The server
|
|
496
|
+
// can only estimate a label's width, so the element measures the real
|
|
497
|
+
// text and thins the ticks again (see `_fitTicks`).
|
|
498
|
+
const shown = new Map(visibleTicks(ticks, size, 0, which === 'x' ? W : H, minTickGap, interval).map((t) => [t.coord, t.at ?? t.coord]));
|
|
499
|
+
const all = spec.tick === false ? [] : ticks;
|
|
500
|
+
const tickSize = spec.tickSize ?? 6;
|
|
501
|
+
const margin2 = spec.tickMargin ?? 2;
|
|
502
|
+
const edge = which === 'x' ? plot.y + plot.h : plot.x;
|
|
503
|
+
const line = spec.axisLine === false ? '' : which === 'x'
|
|
504
|
+
? svg`<line class="ui-chart-cartesian-axis-line" stroke="#666" fill="none" x1=${fmt(plot.x)} y1=${fmt(edge)} x2=${fmt(plot.x + plot.w)} y2=${fmt(edge)}></line>`
|
|
505
|
+
: svg`<line class="ui-chart-cartesian-axis-line" stroke="#666" fill="none" x1=${fmt(edge)} y1=${fmt(plot.y)} x2=${fmt(edge)} y2=${fmt(plot.y + plot.h)}></line>`;
|
|
506
|
+
return svg`<g class=${cn('ui-chart-layer ui-chart-cartesian-axis', which === 'x' ? 'ui-chart-xAxis' : 'ui-chart-yAxis', spec.class)} data-axis=${which} data-gap=${String(minTickGap)} data-interval=${String(interval)} data-extent=${fmt(which === 'x' ? W : H)}>${line}<g class="ui-chart-cartesian-axis-ticks">${all.map((t) => {
|
|
507
|
+
const at = shown.get(t.coord) ?? t.coord;
|
|
508
|
+
const tl = spec.tickLine === false ? '' : which === 'x'
|
|
509
|
+
? svg`<line class="ui-chart-cartesian-axis-tick-line" stroke="#666" fill="none" x1=${fmt(at)} y1=${fmt(edge + tickSize)} x2=${fmt(at)} y2=${fmt(edge)}></line>`
|
|
510
|
+
: svg`<line class="ui-chart-cartesian-axis-tick-line" stroke="#666" fill="none" x1=${fmt(edge - tickSize)} y1=${fmt(at)} x2=${fmt(edge)} y2=${fmt(at)}></line>`;
|
|
511
|
+
const text = which === 'x'
|
|
512
|
+
? chartText(at, edge + tickSize + margin2, t.label, { anchor: 'middle', vAnchor: 'start', fill: '#666', class: 'ui-chart-cartesian-axis-tick-value' })
|
|
513
|
+
: chartText(edge - tickSize - margin2, at, t.label, { anchor: 'end', vAnchor: 'middle', fill: '#666', class: 'ui-chart-cartesian-axis-tick-value' });
|
|
514
|
+
return svg`<g class="ui-chart-layer ui-chart-cartesian-axis-tick" data-coord=${fmt(t.coord)} visibility=${shown.has(t.coord) ? nothing : 'hidden'}>${tl}${text}</g>`;
|
|
515
|
+
})}</g></g>`;
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// Tooltip points: the series at each row, anchored where Recharts puts a
|
|
519
|
+
// tooltip it opens without a pointer (combineCoordinateForDefaultIndex):
|
|
520
|
+
// on the category, halfway between the plot's near edge and the chart's far one.
|
|
521
|
+
const points: ChartPoint[] = data.map((d, i) => ({
|
|
522
|
+
label: categories[i],
|
|
523
|
+
payload: series.map((s) => ({ dataKey: s.dataKey, name: s.name ?? s.dataKey, value: d[s.dataKey], color: color(s), payload: d })),
|
|
524
|
+
x: vertical ? (plot.x + W) / 2 : catAt(i),
|
|
525
|
+
y: vertical ? catAt(i) : (plot.y + H) / 2,
|
|
526
|
+
}));
|
|
527
|
+
|
|
528
|
+
const body = svg`${grid}${overlay('chart-cursor', cursors, defaultIndex)}${marks}${axis('x')}${axis('y')}${actives.length ? overlay('chart-active', actives, defaultIndex) : ''}`;
|
|
529
|
+
return frame(o, {
|
|
530
|
+
width: W, height: H, hit: vertical ? 'y' : 'x', body, points, margin, plot, fit: true,
|
|
531
|
+
legend: series.map((s) => ({ dataKey: s.dataKey, name: s.name ?? s.dataKey, value: s.name ?? s.dataKey, color: color(s), payload: {} })),
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** A bar chart: Recharts' `<BarChart>` with its `<Bar>`s. */
|
|
536
|
+
export function barChart(o: CartesianChartOptions & { bars: ChartSeriesOptions[] }) {
|
|
537
|
+
return cartesian(o, 'bar', o.bars);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** A line chart: Recharts' `<LineChart>` with its `<Line>`s. */
|
|
541
|
+
export function lineChart(o: CartesianChartOptions & { lines: ChartSeriesOptions[] }) {
|
|
542
|
+
return cartesian(o, 'line', o.lines);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** An area chart: Recharts' `<AreaChart>` with its `<Area>`s. */
|
|
546
|
+
export function areaChart(o: CartesianChartOptions & { areas: ChartSeriesOptions[] }) {
|
|
547
|
+
return cartesian(o, 'area', o.areas);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// --------------------------------------------------------------------------
|
|
551
|
+
// Polar charts: pie, radar, radial bar
|
|
552
|
+
// --------------------------------------------------------------------------
|
|
553
|
+
|
|
554
|
+
/** Where a polar chart sits: its centre and radii, what a `center` label receives. */
|
|
555
|
+
export interface ChartPolarBox { cx: number; cy: number; innerRadius: number; outerRadius: number }
|
|
556
|
+
|
|
557
|
+
export interface ChartSectorProps extends SectorGeometry {
|
|
558
|
+
fill: string;
|
|
559
|
+
stroke: string;
|
|
560
|
+
strokeWidth: number;
|
|
561
|
+
datum: ChartDatum;
|
|
562
|
+
index: number;
|
|
563
|
+
value: number;
|
|
564
|
+
midAngle: number;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** A sector as the kit draws one, for a custom `shape` / `activeShape` to reuse. */
|
|
568
|
+
export function chartSector(p: Partial<ChartSectorProps> & SectorGeometry, cls = '') {
|
|
569
|
+
return svg`<path class=${cn('ui-chart-sector', cls)} fill=${p.fill ?? nothing} stroke=${p.stroke ?? nothing} stroke-width=${p.strokeWidth ?? nothing} d=${sectorPath(p)}></path>`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export interface ChartPieOptions {
|
|
573
|
+
dataKey: string;
|
|
574
|
+
nameKey?: string;
|
|
575
|
+
/** This ring's own rows (two rings of a stacked pie), else the chart's `data`. */
|
|
576
|
+
data?: ChartDatum[];
|
|
577
|
+
innerRadius?: number | string;
|
|
578
|
+
outerRadius?: number | string;
|
|
579
|
+
cx?: number | string;
|
|
580
|
+
cy?: number | string;
|
|
581
|
+
startAngle?: number;
|
|
582
|
+
endAngle?: number;
|
|
583
|
+
paddingAngle?: number;
|
|
584
|
+
cornerRadius?: number;
|
|
585
|
+
fill?: string;
|
|
586
|
+
stroke?: string;
|
|
587
|
+
strokeWidth?: number;
|
|
588
|
+
/** Slice labels outside the pie: true, options, or a render function. */
|
|
589
|
+
label?: boolean | { offsetRadius?: number; class?: string; fill?: string; fontSize?: number }
|
|
590
|
+
| ((props: { x: number; y: number; cx: number; cy: number; midAngle: number; innerRadius: number; outerRadius: number; value: unknown; datum: ChartDatum; index: number; fill: string; textAnchor: 'start' | 'middle' | 'end' }) => unknown);
|
|
591
|
+
labelLine?: boolean;
|
|
592
|
+
/** LabelLists on the slices (`position` `inside` by default, or `outside`). */
|
|
593
|
+
labels?: ChartLabelOptions[];
|
|
594
|
+
activeIndex?: number;
|
|
595
|
+
/** How the active slice draws: a render function, or a larger outer radius. */
|
|
596
|
+
activeShape?: ((props: ChartSectorProps) => unknown) | { outerRadius?: number };
|
|
597
|
+
shape?: (props: ChartSectorProps) => unknown;
|
|
598
|
+
/** Content at the centre (a donut's total), shadcn's `<Label content>`. */
|
|
599
|
+
center?: (box: ChartPolarBox) => unknown;
|
|
600
|
+
class?: string;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function polarPlot(o: ChartBaseOptions, W: number, H: number) {
|
|
604
|
+
const m = resolveMargin(o.margin);
|
|
605
|
+
const legendH = o.legend ? o.legend.height ?? 28 : 0;
|
|
606
|
+
const top = m.top + (o.legend && o.legend.verticalAlign === 'top' ? legendH : 0);
|
|
607
|
+
return { m, x: m.left, y: top, w: W - m.left - m.right, h: H - m.top - m.bottom - legendH };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function anchorOf(x: number, cx: number): 'start' | 'middle' | 'end' {
|
|
611
|
+
return x > cx ? 'start' : x < cx ? 'end' : 'middle';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** A pie or donut chart: Recharts' `<PieChart>` with its `<Pie>`s. */
|
|
615
|
+
export function pieChart(o: ChartBaseOptions & { pies: ChartPieOptions[] }) {
|
|
616
|
+
const W = o.width ?? 250;
|
|
617
|
+
const H = o.height ?? W;
|
|
618
|
+
const plot = polarPlot(o, W, H);
|
|
619
|
+
const maxR = Math.min(plot.w, plot.h) / 2;
|
|
620
|
+
const points: ChartPoint[] = [];
|
|
621
|
+
const legend: ChartPayloadItem[] = [];
|
|
622
|
+
const rings = o.pies.map((p) => {
|
|
623
|
+
const rows = p.data ?? o.data;
|
|
624
|
+
const cx = plot.x + percent(p.cx, plot.w, plot.w / 2);
|
|
625
|
+
const cy = plot.y + percent(p.cy, plot.h, plot.h / 2);
|
|
626
|
+
const innerRadius = percent(p.innerRadius, maxR, 0);
|
|
627
|
+
const outerRadius = percent(p.outerRadius, maxR, maxR * 0.8);
|
|
628
|
+
const start = p.startAngle ?? 0;
|
|
629
|
+
const end = p.endAngle ?? 360;
|
|
630
|
+
const delta = Math.sign(end - start) * Math.min(Math.abs(end - start), 360);
|
|
631
|
+
const values = rows.map((d) => num(d[p.dataKey]));
|
|
632
|
+
const nonZero = values.filter((v) => v !== 0).length;
|
|
633
|
+
const padding = rows.length <= 1 ? 0 : p.paddingAngle ?? 0;
|
|
634
|
+
const real = Math.abs(delta) - (Math.abs(delta) >= 360 ? nonZero : nonZero - 1) * padding;
|
|
635
|
+
const sum = values.reduce((s, v) => s + v, 0);
|
|
636
|
+
let prevEnd = start;
|
|
637
|
+
const sectors = rows.map((d, i) => {
|
|
638
|
+
const v = values[i];
|
|
639
|
+
const s0 = i ? prevEnd + Math.sign(delta) * padding * (v !== 0 ? 1 : 0) : start;
|
|
640
|
+
const s1 = s0 + Math.sign(delta) * (sum > 0 ? (v / sum) * real : 0);
|
|
641
|
+
prevEnd = s1;
|
|
642
|
+
const fill = typeof d.fill === 'string' ? d.fill : p.fill ?? '#808080';
|
|
643
|
+
return { cx, cy, innerRadius, outerRadius, startAngle: s0, endAngle: s1, cornerRadius: p.cornerRadius, fill, stroke: p.stroke ?? '#fff', strokeWidth: p.strokeWidth ?? 1, datum: d, index: i, value: v, midAngle: (s0 + s1) / 2 };
|
|
644
|
+
});
|
|
645
|
+
const base = points.length;
|
|
646
|
+
sectors.forEach((s) => {
|
|
647
|
+
const name = String(p.nameKey ? s.datum[p.nameKey] ?? s.index : s.index);
|
|
648
|
+
const item = { dataKey: p.dataKey, name, value: s.datum[p.dataKey], color: s.fill, payload: s.datum };
|
|
649
|
+
const at = polarToCartesian(s.cx, s.cy, (s.innerRadius + s.outerRadius) / 2, s.midAngle);
|
|
650
|
+
points.push({ label: name, payload: [item], x: at.x, y: at.y });
|
|
651
|
+
if (p === o.pies[0]) legend.push({ ...item, value: name });
|
|
652
|
+
});
|
|
653
|
+
const drawn = sectors.map((s) => {
|
|
654
|
+
const isActive = p.activeIndex === s.index && p.activeShape;
|
|
655
|
+
let shape: unknown;
|
|
656
|
+
if (isActive && typeof p.activeShape === 'function') shape = p.activeShape(s);
|
|
657
|
+
else if (isActive && typeof p.activeShape === 'object') shape = chartSector({ ...s, outerRadius: p.activeShape.outerRadius ?? s.outerRadius });
|
|
658
|
+
else shape = p.shape ? p.shape(s) : chartSector(s);
|
|
659
|
+
return svg`<g class="ui-chart-layer ui-chart-pie-sector" data-index=${String(base + s.index)}>${shape}</g>`;
|
|
660
|
+
});
|
|
661
|
+
let labels: unknown = '';
|
|
662
|
+
if (p.label) {
|
|
663
|
+
const lo = typeof p.label === 'object' ? p.label : {};
|
|
664
|
+
const offsetR = lo.offsetRadius ?? 20;
|
|
665
|
+
labels = svg`<g class="ui-chart-layer ui-chart-pie-labels">${sectors.map((s) => {
|
|
666
|
+
const e = polarToCartesian(s.cx, s.cy, s.outerRadius + offsetR, s.midAngle);
|
|
667
|
+
const a = polarToCartesian(s.cx, s.cy, s.outerRadius, s.midAngle);
|
|
668
|
+
const anchor = anchorOf(e.x, s.cx);
|
|
669
|
+
const line = p.labelLine === false ? '' : svg`<path class="ui-chart-curve ui-chart-pie-label-line" fill="none" stroke=${s.fill} d=${`M${fmt(a.x, 3)},${fmt(a.y, 3)}L${fmt(e.x, 3)},${fmt(e.y, 3)}`}></path>`;
|
|
670
|
+
const text = typeof p.label === 'function'
|
|
671
|
+
? p.label({ x: e.x, y: e.y, cx: s.cx, cy: s.cy, midAngle: s.midAngle, innerRadius: s.innerRadius, outerRadius: s.outerRadius, value: s.datum[p.dataKey], datum: s.datum, index: s.index, fill: s.fill, textAnchor: anchor })
|
|
672
|
+
: chartText(e.x, e.y, s.datum[p.dataKey], { anchor, fill: lo.fill ?? s.fill, stroke: 'none', fontSize: lo.fontSize, baseline: 'middle', class: cn('ui-chart-pie-label-text', lo.class) });
|
|
673
|
+
return svg`<g class="ui-chart-layer">${line}${text}</g>`;
|
|
674
|
+
})}</g>`;
|
|
675
|
+
}
|
|
676
|
+
const lists = (p.labels ?? []).map((l) => svg`<g class="ui-chart-label-list">${sectors.map((s) => {
|
|
677
|
+
const raw = s.datum[l.dataKey ?? p.dataKey];
|
|
678
|
+
const value = l.formatter ? l.formatter(raw, s.datum, s.index) : raw;
|
|
679
|
+
const outside = l.position === 'outside';
|
|
680
|
+
const at = polarToCartesian(s.cx, s.cy, outside ? s.outerRadius + (l.offset ?? 5) : (s.innerRadius + s.outerRadius) / 2, s.midAngle);
|
|
681
|
+
if (l.content) return l.content({ x: at.x, y: at.y, width: 0, height: 0, value, datum: s.datum, index: s.index });
|
|
682
|
+
return chartText(at.x, at.y, value, { anchor: outside ? (at.x >= s.cx ? 'start' : 'end') : 'middle', vAnchor: 'middle', class: cn('ui-chart-label', l.class), fill: l.fill, stroke: l.stroke, fontSize: l.fontSize });
|
|
683
|
+
})}</g>`);
|
|
684
|
+
const center = p.center ? p.center({ cx, cy, innerRadius, outerRadius }) : '';
|
|
685
|
+
return svg`<g class=${cn('ui-chart-layer ui-chart-pie', p.class)}><g class="ui-chart-layer">${drawn}</g>${labels}${lists}${center}</g>`;
|
|
686
|
+
});
|
|
687
|
+
return frame(o, { width: W, height: H, hit: 'target', body: rings, points, legend, margin: plot.m, plot });
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export interface ChartRadarOptions {
|
|
691
|
+
dataKey: string;
|
|
692
|
+
name?: string;
|
|
693
|
+
fill?: string;
|
|
694
|
+
fillOpacity?: number;
|
|
695
|
+
stroke?: string;
|
|
696
|
+
strokeWidth?: number;
|
|
697
|
+
dot?: boolean | ChartDotOptions | ChartDotRender;
|
|
698
|
+
activeDot?: boolean | ChartDotOptions;
|
|
699
|
+
labels?: ChartLabelOptions[];
|
|
700
|
+
class?: string;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export interface ChartPolarGridOptions {
|
|
704
|
+
gridType?: 'polygon' | 'circle';
|
|
705
|
+
radialLines?: boolean;
|
|
706
|
+
/** The ring radii, else the radius axis ticks. */
|
|
707
|
+
polarRadius?: number[];
|
|
708
|
+
class?: string;
|
|
709
|
+
fill?: string;
|
|
710
|
+
stroke?: string;
|
|
711
|
+
strokeWidth?: number;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export interface ChartPolarAngleAxisOptions {
|
|
715
|
+
dataKey?: string;
|
|
716
|
+
tickFormatter?: (value: unknown, index: number) => unknown;
|
|
717
|
+
/** false hides the labels; a function draws each one. */
|
|
718
|
+
tick?: boolean | ((props: { x: number; y: number; textAnchor: 'start' | 'middle' | 'end'; value: unknown; label: unknown; index: number; datum: ChartDatum }) => unknown);
|
|
719
|
+
class?: string;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export interface ChartPolarRadiusAxisOptions {
|
|
723
|
+
angle?: number;
|
|
724
|
+
orientation?: 'left' | 'right' | 'middle';
|
|
725
|
+
tickCount?: number;
|
|
726
|
+
domain?: [number, number];
|
|
727
|
+
tick?: boolean;
|
|
728
|
+
tickFormatter?: (value: unknown, index: number) => unknown;
|
|
729
|
+
class?: string;
|
|
730
|
+
stroke?: string;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function polarGrid(g: ChartPolarGridOptions | false | undefined, cx: number, cy: number, radii: number[], angles: number[], outer: number, inner = 0) {
|
|
734
|
+
if (!g) return '';
|
|
735
|
+
const ring = (r: number) => (g.gridType === 'circle'
|
|
736
|
+
? svg`<circle class=${cn('ui-chart-polar-grid-concentric-circle', g.class)} stroke=${g.stroke ?? '#ccc'} stroke-width=${g.strokeWidth ?? nothing} fill=${g.fill ?? 'none'} cx=${fmt(cx)} cy=${fmt(cy)} r=${fmt(r)}></circle>`
|
|
737
|
+
: svg`<path class=${cn('ui-chart-polar-grid-concentric-polygon', g.class)} stroke=${g.stroke ?? '#ccc'} stroke-width=${g.strokeWidth ?? nothing} fill=${g.fill ?? 'none'} d=${polygonPath(angles.map((a) => polarToCartesian(cx, cy, r, a)))}></path>`);
|
|
738
|
+
return svg`<g class="ui-chart-polar-grid"><g class="ui-chart-polar-grid-concentric">${radii.map(ring)}</g>${g.radialLines === false ? '' : svg`<g class="ui-chart-polar-grid-angle">${angles.map((a) => {
|
|
739
|
+
const b = polarToCartesian(cx, cy, inner, a);
|
|
740
|
+
const e = polarToCartesian(cx, cy, outer, a);
|
|
741
|
+
return svg`<line class=${cn('ui-chart-polar-grid-angle-line', g.class)} stroke=${g.stroke ?? '#ccc'} x1=${fmt(b.x)} y1=${fmt(b.y)} x2=${fmt(e.x)} y2=${fmt(e.y)}></line>`;
|
|
742
|
+
})}</g>`}</g>`;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** A radar chart: Recharts' `<RadarChart>` with its `<Radar>`s, grid and axes. */
|
|
746
|
+
export function radarChart(o: ChartBaseOptions & {
|
|
747
|
+
radars: ChartRadarOptions[];
|
|
748
|
+
cx?: number | string;
|
|
749
|
+
cy?: number | string;
|
|
750
|
+
innerRadius?: number | string;
|
|
751
|
+
outerRadius?: number | string;
|
|
752
|
+
startAngle?: number;
|
|
753
|
+
endAngle?: number;
|
|
754
|
+
grid?: ChartPolarGridOptions | false;
|
|
755
|
+
angleAxis?: ChartPolarAngleAxisOptions | false;
|
|
756
|
+
radiusAxis?: ChartPolarRadiusAxisOptions | false;
|
|
757
|
+
}) {
|
|
758
|
+
const W = o.width ?? 250;
|
|
759
|
+
const H = o.height ?? W;
|
|
760
|
+
const plot = polarPlot(o, W, H);
|
|
761
|
+
const maxR = Math.min(plot.w, plot.h) / 2;
|
|
762
|
+
// The centre is a share of the whole chart (Recharts' selectPolarViewBox),
|
|
763
|
+
// while the radius comes from the plot inside the margins.
|
|
764
|
+
const cx = percent(o.cx, W, W / 2);
|
|
765
|
+
const cy = percent(o.cy, H, H / 2);
|
|
766
|
+
const inner = percent(o.innerRadius, maxR, 0);
|
|
767
|
+
const outer = percent(o.outerRadius, maxR, maxR * 0.8);
|
|
768
|
+
const data = o.data;
|
|
769
|
+
const n = data.length;
|
|
770
|
+
const start = o.startAngle ?? 90;
|
|
771
|
+
const end = o.endAngle ?? -270;
|
|
772
|
+
const angles = data.map((_, i) => start + (i * (end - start)) / (n || 1));
|
|
773
|
+
const aSpec = o.angleAxis || undefined;
|
|
774
|
+
const rSpec = o.radiusAxis || undefined;
|
|
775
|
+
const all = o.radars.flatMap((r) => data.map((d) => num(d[r.dataKey])));
|
|
776
|
+
const ticks = rSpec?.domain ? niceTicks(rSpec.domain[0], rSpec.domain[1], rSpec.tickCount ?? 5).filter((t) => t >= rSpec.domain![0] && t <= rSpec.domain![1]) : niceTicks(Math.min(0, ...all), Math.max(...all, 0), rSpec?.tickCount ?? 5);
|
|
777
|
+
const domain: [number, number] = rSpec?.domain ?? [ticks[0], ticks[ticks.length - 1]];
|
|
778
|
+
const rScale = linearScale(domain, [inner, outer]);
|
|
779
|
+
const defaultIndex = o.tooltip ? o.tooltip.defaultIndex : undefined;
|
|
780
|
+
const at = (r: ChartRadarOptions, i: number) => polarToCartesian(cx, cy, rScale(num(data[i][r.dataKey])), angles[i]);
|
|
781
|
+
const colorOf = (r: ChartRadarOptions) => r.stroke ?? r.fill ?? '#808080';
|
|
782
|
+
const grid = polarGrid(o.grid, cx, cy, o.grid && o.grid.polarRadius ? o.grid.polarRadius : ticks.map(rScale), angles, outer);
|
|
783
|
+
const radars = o.radars.map((r) => {
|
|
784
|
+
const pts = data.map((_, i) => at(r, i));
|
|
785
|
+
const dot = dotProps(r.dot, false);
|
|
786
|
+
return svg`<g class=${cn('ui-chart-layer ui-chart-radar', r.class)}><g class="ui-chart-layer ui-chart-radar-polygon"><path class="ui-chart-polygon" fill=${r.fill ?? nothing} fill-opacity=${r.fillOpacity ?? nothing} stroke=${r.stroke ?? nothing} stroke-width=${r.strokeWidth ?? nothing} d=${polygonPath(pts)}></path></g>${dot ? svg`<g class="ui-chart-layer ui-chart-radar-dots">${pts.map((q, i) => drawDot(dot, { cx: q.x, cy: q.y, r: 3, fill: r.fill ?? '#808080', stroke: r.stroke ?? 'none', strokeWidth: r.strokeWidth ?? 1 }, data[i], i, r.dataKey, 'ui-chart-radar-dot'))}</g>` : ''}${labelList(r.labels, r.dataKey, pts.map((q, i) => ({ box: { x: q.x, y: q.y, width: 0, height: 0 }, datum: data[i], index: i })), 'top')}</g>`;
|
|
787
|
+
});
|
|
788
|
+
const angleAxis = aSpec ? svg`<g class=${cn('ui-chart-layer ui-chart-polar-angle-axis', aSpec.class)}>${aSpec.tick === false ? '' : data.map((d, i) => {
|
|
789
|
+
const value = aSpec.dataKey ? d[aSpec.dataKey] : i;
|
|
790
|
+
const label = aSpec.tickFormatter ? aSpec.tickFormatter(value, i) : value;
|
|
791
|
+
const p = polarToCartesian(cx, cy, outer + 8, angles[i]);
|
|
792
|
+
const cos = Math.cos(-RADIAN * angles[i]);
|
|
793
|
+
const anchor = cos > 1e-5 ? 'start' : cos < -1e-5 ? 'end' : 'middle';
|
|
794
|
+
if (typeof aSpec.tick === 'function') return aSpec.tick({ x: p.x, y: p.y, textAnchor: anchor, value, label, index: i, datum: d });
|
|
795
|
+
const sin = Math.sin(-RADIAN * angles[i]);
|
|
796
|
+
const vAnchor: VAnchor = anchor !== 'middle' ? 'middle' : sin < 0 ? 'end' : 'start';
|
|
797
|
+
return svg`<g class="ui-chart-layer ui-chart-polar-angle-axis-tick">${chartText(p.x, p.y, label, { anchor, vAnchor, fill: '#808080', class: 'ui-chart-polar-angle-axis-tick-value' })}</g>`;
|
|
798
|
+
})}</g>` : '';
|
|
799
|
+
const radiusAxis = rSpec && rSpec.tick !== false ? svg`<g class=${cn('ui-chart-layer ui-chart-polar-radius-axis', rSpec.class)}>${ticks.map((t, i) => {
|
|
800
|
+
const p = polarToCartesian(cx, cy, rScale(t), rSpec.angle ?? 0);
|
|
801
|
+
const anchor = rSpec.orientation === 'left' ? 'end' : rSpec.orientation === 'middle' ? 'middle' : 'start';
|
|
802
|
+
return chartText(p.x, p.y, rSpec.tickFormatter ? rSpec.tickFormatter(t, i) : t, { anchor, vAnchor: 'middle', fill: rSpec.stroke ?? '#808080', class: 'ui-chart-polar-radius-axis-tick-value' });
|
|
803
|
+
})}</g>` : '';
|
|
804
|
+
const cursors = o.tooltip && o.tooltip.cursor !== false
|
|
805
|
+
? angles.map((a) => { const e = polarToCartesian(cx, cy, outer, a); return svg`<path class="ui-chart-curve ui-chart-tooltip-cursor" stroke="#ccc" fill="none" d=${`M${fmt(cx)},${fmt(cy)}L${fmt(e.x)},${fmt(e.y)}`}></path>`; })
|
|
806
|
+
: [];
|
|
807
|
+
const actives = o.tooltip
|
|
808
|
+
? data.map((_, i) => o.radars.map((r) => {
|
|
809
|
+
if (r.activeDot === false) return '';
|
|
810
|
+
const q = at(r, i);
|
|
811
|
+
return drawDot(typeof r.activeDot === 'object' ? r.activeDot : {}, { cx: q.x, cy: q.y, r: 4, fill: colorOf(r), stroke: '#fff', strokeWidth: 2 }, data[i], i, r.dataKey, 'ui-chart-active-dot');
|
|
812
|
+
}))
|
|
813
|
+
: [];
|
|
814
|
+
// Hit wedges: each point owns the angles nearer to it than to its neighbours.
|
|
815
|
+
const step = n ? (end - start) / n : 0;
|
|
816
|
+
const hits = o.tooltip ? svg`<g data-slot="chart-hit">${angles.map((a, i) => svg`<path data-index=${String(i)} fill="transparent" d=${sectorPath({ cx, cy, innerRadius: 0, outerRadius: outer + 8, startAngle: a - step / 2, endAngle: a + step / 2 })}></path>`)}</g>` : '';
|
|
817
|
+
const points: ChartPoint[] = data.map((d, i) => {
|
|
818
|
+
const q = o.radars.length ? at(o.radars[0], i) : { x: cx, y: cy };
|
|
819
|
+
return { label: aSpec && aSpec.dataKey ? d[aSpec.dataKey] : i, payload: o.radars.map((r) => ({ dataKey: r.dataKey, name: r.name ?? r.dataKey, value: d[r.dataKey], color: colorOf(r), payload: d })), x: q.x, y: q.y };
|
|
820
|
+
});
|
|
821
|
+
const body = svg`${grid}${overlay('chart-cursor', cursors, defaultIndex)}${radars}${angleAxis}${radiusAxis}${actives.length ? overlay('chart-active', actives, defaultIndex) : ''}${hits}`;
|
|
822
|
+
return frame(o, { width: W, height: H, hit: 'target', body, points, margin: plot.m, plot, legend: o.radars.map((r) => ({ dataKey: r.dataKey, name: r.name ?? r.dataKey, value: r.name ?? r.dataKey, color: colorOf(r), payload: {} })) });
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export interface ChartRadialBarOptions {
|
|
826
|
+
dataKey: string;
|
|
827
|
+
name?: string;
|
|
828
|
+
fill?: string;
|
|
829
|
+
stackId?: string;
|
|
830
|
+
cornerRadius?: number;
|
|
831
|
+
/** The track behind each bar: true, or its fill and classes. */
|
|
832
|
+
background?: boolean | { fill?: string; class?: string };
|
|
833
|
+
/** LabelLists along the bars (`insideStart`, `insideEnd`, `end`, or a cartesian position at the bar's middle). */
|
|
834
|
+
labels?: ChartLabelOptions[];
|
|
835
|
+
class?: string;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
let radialLabelSeq = 0;
|
|
839
|
+
|
|
840
|
+
/** A radial bar chart: Recharts' `<RadialBarChart>` with its `<RadialBar>`s. */
|
|
841
|
+
export function radialBarChart(o: ChartBaseOptions & {
|
|
842
|
+
bars: ChartRadialBarOptions[];
|
|
843
|
+
cx?: number | string;
|
|
844
|
+
cy?: number | string;
|
|
845
|
+
innerRadius?: number | string;
|
|
846
|
+
outerRadius?: number | string;
|
|
847
|
+
startAngle?: number;
|
|
848
|
+
endAngle?: number;
|
|
849
|
+
barSize?: number;
|
|
850
|
+
barGap?: number | string;
|
|
851
|
+
barCategoryGap?: number | string;
|
|
852
|
+
/** The value axis's extent, default `[0, largest value]`. */
|
|
853
|
+
domain?: [number, number];
|
|
854
|
+
grid?: ChartPolarGridOptions | false;
|
|
855
|
+
center?: (box: ChartPolarBox) => unknown;
|
|
856
|
+
}) {
|
|
857
|
+
const W = o.width ?? 250;
|
|
858
|
+
const H = o.height ?? W;
|
|
859
|
+
const plot = polarPlot(o, W, H);
|
|
860
|
+
const maxR = Math.min(plot.w, plot.h) / 2;
|
|
861
|
+
// The centre is a share of the whole chart (Recharts' selectPolarViewBox),
|
|
862
|
+
// while the radius comes from the plot inside the margins.
|
|
863
|
+
const cx = percent(o.cx, W, W / 2);
|
|
864
|
+
const cy = percent(o.cy, H, H / 2);
|
|
865
|
+
const inner = percent(o.innerRadius, maxR, 0);
|
|
866
|
+
const outer = percent(o.outerRadius, maxR, maxR * 0.8);
|
|
867
|
+
const start = o.startAngle ?? 0;
|
|
868
|
+
const end = o.endAngle ?? 360;
|
|
869
|
+
const data = o.data;
|
|
870
|
+
const stacks = new Map<string, string[]>();
|
|
871
|
+
for (const s of o.bars) if (s.stackId != null) stacks.set(s.stackId, [...(stacks.get(s.stackId) ?? []), s.dataKey]);
|
|
872
|
+
const bands: Record<string, Array<[number, number]>> = {};
|
|
873
|
+
for (const keys of stacks.values()) Object.assign(bands, stackData(data, keys));
|
|
874
|
+
for (const s of o.bars) if (s.stackId == null) bands[s.dataKey] = data.map((d) => [0, num(d[s.dataKey])]);
|
|
875
|
+
const max = Math.max(0, ...o.bars.flatMap((s) => bands[s.dataKey].map((b) => b[1])));
|
|
876
|
+
const aScale = linearScale(o.domain ?? [0, max], [start, end]);
|
|
877
|
+
const band = bandScale(data.length, inner, outer);
|
|
878
|
+
const slotIds: string[] = [];
|
|
879
|
+
for (const s of o.bars) { const id = s.stackId != null ? `stack:${s.stackId}` : `bar:${s.dataKey}`; if (!slotIds.includes(id)) slotIds.push(id); }
|
|
880
|
+
const positions = barPositions(band.bandwidth, slotIds.map(() => ({ barSize: o.barSize })), o.barGap, o.barCategoryGap);
|
|
881
|
+
const seq = ++radialLabelSeq;
|
|
882
|
+
const points: ChartPoint[] = data.map((d, i) => ({ label: i, payload: [], x: cx, y: cy }));
|
|
883
|
+
const backgrounds: unknown[] = [];
|
|
884
|
+
const bars = o.bars.map((s, si) => {
|
|
885
|
+
const pos = positions[slotIds.indexOf(s.stackId != null ? `stack:${s.stackId}` : `bar:${s.dataKey}`)];
|
|
886
|
+
const sectors = data.map((d, i) => {
|
|
887
|
+
const r0 = band.at(i) + pos.offset;
|
|
888
|
+
const [lo, hi] = bands[s.dataKey][i];
|
|
889
|
+
const fill = typeof d.fill === 'string' ? d.fill : s.fill ?? '#808080';
|
|
890
|
+
const sec = { cx, cy, innerRadius: r0, outerRadius: r0 + pos.size, startAngle: aScale(lo), endAngle: aScale(hi), cornerRadius: s.cornerRadius, fill, datum: d, index: i };
|
|
891
|
+
points[i].payload.push({ dataKey: s.dataKey, name: s.name ?? s.dataKey, value: d[s.dataKey], color: fill, payload: d });
|
|
892
|
+
if (si === 0) {
|
|
893
|
+
const q = polarToCartesian(cx, cy, r0 + pos.size / 2, (sec.startAngle + sec.endAngle) / 2);
|
|
894
|
+
points[i].x = q.x;
|
|
895
|
+
points[i].y = q.y;
|
|
896
|
+
}
|
|
897
|
+
if (s.background) {
|
|
898
|
+
const bg = typeof s.background === 'object' ? s.background : {};
|
|
899
|
+
backgrounds.push(svg`<g class="ui-chart-layer" data-index=${String(i)}><path class=${cn('ui-chart-sector ui-chart-radial-bar-background-sector', bg.class)} fill=${bg.fill ?? '#eee'} d=${sectorPath({ ...sec, startAngle: start, endAngle: end })}></path></g>`);
|
|
900
|
+
}
|
|
901
|
+
return sec;
|
|
902
|
+
});
|
|
903
|
+
const labels = (s.labels ?? []).map((l, li) => svg`<g class="ui-chart-label-list">${sectors.map((sec) => {
|
|
904
|
+
const raw = sec.datum[l.dataKey ?? s.dataKey];
|
|
905
|
+
const value = l.formatter ? l.formatter(raw, sec.datum, sec.index) : raw;
|
|
906
|
+
const pos2 = l.position ?? 'insideStart';
|
|
907
|
+
const offset = l.offset ?? 5;
|
|
908
|
+
const r = (sec.innerRadius + sec.outerRadius) / 2;
|
|
909
|
+
if (pos2 === 'insideStart' || pos2 === 'insideEnd' || pos2 === 'end') {
|
|
910
|
+
const sign = sec.endAngle - sec.startAngle >= 0 ? 1 : -1;
|
|
911
|
+
const angle = pos2 === 'insideStart' ? sec.startAngle + sign * offset : pos2 === 'insideEnd' ? sec.endAngle - sign * offset : sec.endAngle + sign * offset;
|
|
912
|
+
let dir = pos2 === 'insideEnd';
|
|
913
|
+
dir = sign <= 0 ? dir : !dir;
|
|
914
|
+
const p0 = polarToCartesian(cx, cy, r, angle);
|
|
915
|
+
const p1 = polarToCartesian(cx, cy, r, angle + (dir ? 1 : -1) * 359);
|
|
916
|
+
const id = `ui-chart-radial-label-${seq}-${si}-${li}-${sec.index}`;
|
|
917
|
+
return svg`<text dominant-baseline="central" fill=${l.fill ?? nothing} font-size=${l.fontSize != null ? `${fmt(l.fontSize / 12)}em` : nothing} class=${cn('ui-chart-radial-bar-label', l.class)}><defs><path id=${id} d=${`M${fmt(p0.x)},${fmt(p0.y)} A${fmt(r)},${fmt(r)},0,1,${dir ? 0 : 1},${fmt(p1.x)},${fmt(p1.y)}`}></path></defs><textPath href=${`#${id}`}>${value}</textPath></text>`;
|
|
918
|
+
}
|
|
919
|
+
const q = polarToCartesian(cx, cy, r, (sec.startAngle + sec.endAngle) / 2);
|
|
920
|
+
return chartText(q.x, q.y, value, { anchor: 'middle', vAnchor: 'middle', class: cn('ui-chart-label', l.class), fill: l.fill, fontSize: l.fontSize });
|
|
921
|
+
})}</g>`);
|
|
922
|
+
return svg`<g class=${cn('ui-chart-layer ui-chart-radial-bar', s.class)}><g class="ui-chart-layer ui-chart-radial-bar-sectors">${sectors.map((sec) => svg`<g class="ui-chart-layer" data-index=${String(sec.index)}>${chartSector(sec, 'ui-chart-radial-bar-sector')}</g>`)}</g>${labels}</g>`;
|
|
923
|
+
});
|
|
924
|
+
// The rings sit on the bands, the spokes on the value axis's d3 ticks.
|
|
925
|
+
const spokes = linearTicks(...(o.domain ?? [0, max])).map((t) => aScale(t));
|
|
926
|
+
const grid = polarGrid(o.grid, cx, cy, o.grid && o.grid.polarRadius ? o.grid.polarRadius : data.map((_, i) => band.at(i) + band.bandwidth / 2), spokes, outer, inner);
|
|
927
|
+
const center = o.center ? o.center({ cx, cy, innerRadius: inner, outerRadius: outer }) : '';
|
|
928
|
+
return frame(o, {
|
|
929
|
+
width: W, height: H, hit: 'target', body: svg`${grid}<g class="ui-chart-layer ui-chart-radial-bar-background">${backgrounds}</g>${bars}${center}`, points, margin: plot.m, plot,
|
|
930
|
+
legend: data.map((d, i) => ({ dataKey: o.bars[0]?.dataKey ?? 'value', name: String(i), value: String(i), color: typeof d.fill === 'string' ? d.fill : o.bars[0]?.fill ?? '#808080', payload: d })),
|
|
931
|
+
});
|
|
932
|
+
}
|