@eifi1/ui-kit 0.4.0 → 0.5.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/LICENSE +32 -0
- package/README.md +498 -57
- package/dist/chart.d.ts +10 -0
- package/dist/chart.js +10 -0
- package/dist/chart.js.map +1 -0
- package/dist/components/account-settings.d.ts +11 -0
- package/dist/components/account-settings.js +19 -12
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/alert-banner.d.ts +10 -6
- package/dist/components/alert-banner.js +20 -16
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +30 -4
- package/dist/components/amount-input.js +36 -14
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/calculator.d.ts +24 -7
- package/dist/components/calculator.js +37 -20
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.d.ts +156 -0
- package/dist/components/chart-zoom.js +262 -0
- package/dist/components/chart-zoom.js.map +1 -0
- package/dist/components/chart.d.ts +28 -3
- package/dist/components/chart.js +71 -32
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox.d.ts +51 -0
- package/dist/components/checkbox.js +123 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/chip.d.ts +128 -0
- package/dist/components/chip.js +357 -0
- package/dist/components/chip.js.map +1 -0
- package/dist/components/choice-parts.d.ts +24 -0
- package/dist/components/choice-parts.js +18 -0
- package/dist/components/choice-parts.js.map +1 -0
- package/dist/components/combobox-core.d.ts +52 -9
- package/dist/components/combobox-core.js +91 -20
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +43 -33
- package/dist/components/combobox.js +409 -340
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.d.ts +48 -8
- package/dist/components/currency-select.js +107 -65
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +15 -3
- package/dist/components/data-table-filter-popover.js +23 -17
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +3 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +53 -2
- package/dist/components/data-table-labels.js +31 -2
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table-pagination.js +16 -10
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-sort.js +1 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +201 -85
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +25 -9
- package/dist/components/date-picker.js +232 -94
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dropdown.d.ts +96 -20
- package/dist/components/dropdown.js +77 -33
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +5 -4
- package/dist/components/entity-combobox.js +107 -76
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +36 -0
- package/dist/components/facing-pair.js +40 -0
- package/dist/components/facing-pair.js.map +1 -0
- package/dist/components/field-sync.d.ts +164 -0
- package/dist/components/field-sync.js +264 -0
- package/dist/components/field-sync.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +38 -6
- package/dist/components/file-dropzone.js +34 -9
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +37 -26
- package/dist/components/full-bleed-dialog.js +20 -5
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/grouped-picker.d.ts +21 -10
- package/dist/components/grouped-picker.js +66 -52
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +20 -4
- package/dist/components/hover-menu.js +15 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +102 -14
- package/dist/components/mini-calendar.js +256 -70
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +9 -3
- package/dist/components/modal.js +34 -45
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +79 -0
- package/dist/components/month-picker.js +342 -0
- package/dist/components/month-picker.js.map +1 -0
- package/dist/components/multi-entity-combobox.d.ts +7 -6
- package/dist/components/multi-entity-combobox.js +112 -72
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +13 -11
- package/dist/components/multi-select.js +161 -84
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +92 -0
- package/dist/components/number-field.js +111 -0
- package/dist/components/number-field.js.map +1 -0
- package/dist/components/number-input.d.ts +28 -2
- package/dist/components/number-input.js +12 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +32 -4
- package/dist/components/numpad-sheet.js +30 -13
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-contents.d.ts +90 -0
- package/dist/components/page-contents.js +170 -0
- package/dist/components/page-contents.js.map +1 -0
- package/dist/components/password-strength.d.ts +106 -0
- package/dist/components/password-strength.js +132 -0
- package/dist/components/password-strength.js.map +1 -0
- package/dist/components/picker-sheet.d.ts +23 -15
- package/dist/components/picker-sheet.js +34 -14
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.d.ts +62 -4
- package/dist/components/popover.js +52 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/search-field.d.ts +25 -7
- package/dist/components/search-field.js +7 -5
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +14 -0
- package/dist/components/series-chart-labels.js +11 -0
- package/dist/components/series-chart-labels.js.map +1 -0
- package/dist/components/series-chart.d.ts +207 -0
- package/dist/components/series-chart.js +223 -0
- package/dist/components/series-chart.js.map +1 -0
- package/dist/components/settings-fields.d.ts +14 -4
- package/dist/components/settings-fields.js +19 -8
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/signature-pad.d.ts +119 -0
- package/dist/components/signature-pad.js +405 -0
- package/dist/components/signature-pad.js.map +1 -0
- package/dist/components/slider.d.ts +80 -0
- package/dist/components/slider.js +155 -0
- package/dist/components/slider.js.map +1 -0
- package/dist/components/sparkline.d.ts +93 -0
- package/dist/components/sparkline.js +205 -0
- package/dist/components/sparkline.js.map +1 -0
- package/dist/components/stat-tile.d.ts +151 -0
- package/dist/components/stat-tile.js +236 -0
- package/dist/components/stat-tile.js.map +1 -0
- package/dist/components/swipeable-row.d.ts +25 -2
- package/dist/components/swipeable-row.js +31 -0
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +60 -0
- package/dist/components/switch.js +81 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/time-input.d.ts +58 -0
- package/dist/components/time-input.js +50 -0
- package/dist/components/time-input.js.map +1 -0
- package/dist/components/toggle-group.d.ts +18 -3
- package/dist/components/toggle-group.js +10 -6
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +115 -0
- package/dist/components/toggle-legend.js +114 -0
- package/dist/components/toggle-legend.js.map +1 -0
- package/dist/components/tooltip.d.ts +40 -11
- package/dist/components/tooltip.js +77 -20
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/treemap.d.ts +132 -0
- package/dist/components/treemap.js +179 -0
- package/dist/components/treemap.js.map +1 -0
- package/dist/components/ui.d.ts +159 -46
- package/dist/components/ui.js +220 -115
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-mobile-reveal.js +1 -0
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/components/use-table-state.js +38 -17
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.d.ts +15 -4
- package/dist/components/user-avatar.js +5 -4
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/components/wizard-stepper.js +3 -2
- package/dist/components/wizard-stepper.js.map +1 -1
- package/dist/{data-table-filters-aXyf0Xub.d.ts → data-table-filters-DM1auUfH.d.ts} +27 -5
- package/dist/data-table.d.ts +7 -0
- package/dist/data-table.js +12 -0
- package/dist/data-table.js.map +1 -0
- package/dist/feedback/feedback-attachment.js +12 -11
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.js +2 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +33 -7
- package/dist/feedback/feedback-inbox.js +49 -37
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback.d.ts +5 -0
- package/dist/feedback.js +5 -0
- package/dist/feedback.js.map +1 -0
- package/dist/hooks/use-anchored-panel.js +1 -0
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js +1 -0
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-announce.d.ts +46 -0
- package/dist/hooks/use-announce.js +37 -0
- package/dist/hooks/use-announce.js.map +1 -0
- package/dist/hooks/use-body-scroll-lock.js +1 -0
- package/dist/hooks/use-body-scroll-lock.js.map +1 -1
- package/dist/hooks/use-close-transition.js +1 -0
- package/dist/hooks/use-close-transition.js.map +1 -1
- package/dist/hooks/use-dismiss.js +1 -0
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +16 -0
- package/dist/hooks/use-focus-trap.js +67 -0
- package/dist/hooks/use-focus-trap.js.map +1 -0
- package/dist/hooks/use-media-query.js +1 -0
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +27 -1
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.js +1 -0
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.d.ts +31 -0
- package/dist/i18n/defaults.js +63 -0
- package/dist/i18n/defaults.js.map +1 -0
- package/dist/i18n/kit-labels.d.ts +250 -0
- package/dist/i18n/kit-labels.js +184 -0
- package/dist/i18n/kit-labels.js.map +1 -0
- package/dist/index.d.ts +47 -22
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/calc.js +1 -0
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/cn.js +1 -0
- package/dist/lib/cn.js.map +1 -1
- package/dist/lib/dates.js +1 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/logger.js +1 -0
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/safe-storage.js +1 -0
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/search/command-palette.d.ts +11 -1
- package/dist/search/command-palette.js +21 -15
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search.d.ts +2 -0
- package/dist/search.js +3 -0
- package/dist/search.js.map +1 -0
- package/dist/shell/app-shell.d.ts +33 -4
- package/dist/shell/app-shell.js +275 -78
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/option-switcher-menu.js +3 -2
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +6 -0
- package/dist/shell/role-switcher.js +7 -4
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar.d.ts +15 -7
- package/dist/shell/top-bar.js +3 -5
- package/dist/shell/top-bar.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +3 -2
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +5 -2
- package/dist/shell/topbar-controls.js +20 -15
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell.d.ts +13 -0
- package/dist/shell.js +8 -0
- package/dist/shell.js.map +1 -0
- package/dist/theme/chart-palette.d.ts +19 -12
- package/dist/theme/chart-palette.js +24 -30
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/color.d.ts +78 -0
- package/dist/theme/color.js +148 -0
- package/dist/theme/color.js.map +1 -0
- package/dist/theme/palette-derive.d.ts +140 -0
- package/dist/theme/palette-derive.js +376 -0
- package/dist/theme/palette-derive.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +27 -5
- package/dist/theme/palette-presets.js +231 -33
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js +1 -0
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/theme-store.js +1 -0
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour.d.ts +2 -1
- package/dist/tour/tour.js +15 -9
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour.d.ts +2 -0
- package/dist/tour.js +3 -0
- package/dist/tour.js.map +1 -0
- package/dist/wizard/stepper-nav.js +10 -8
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +18 -2
- package/dist/wizard/types.js +4 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +12 -4
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js +1 -0
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js +1 -0
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.js +1 -0
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js +5 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +8 -0
- package/dist/wizard.js +9 -0
- package/dist/wizard.js.map +1 -0
- package/package.json +67 -14
- package/src/chart.ts +22 -0
- package/src/components/account-settings.tsx +275 -0
- package/src/components/alert-banner.tsx +75 -0
- package/src/components/amount-input.tsx +471 -0
- package/src/components/calculator.tsx +252 -0
- package/src/components/chart-zoom.tsx +499 -0
- package/src/components/chart.tsx +363 -0
- package/src/components/checkbox.tsx +222 -0
- package/src/components/chip.tsx +570 -0
- package/src/components/choice-parts.ts +33 -0
- package/src/components/combobox-core.tsx +540 -0
- package/src/components/combobox.tsx +1026 -0
- package/src/components/currency-select.tsx +269 -0
- package/src/components/data-table-filter-popover.tsx +252 -0
- package/src/components/data-table-filters.ts +176 -0
- package/src/components/data-table-labels.ts +190 -0
- package/src/components/data-table-pagination.tsx +126 -0
- package/src/components/data-table-sort.ts +70 -0
- package/src/components/data-table.tsx +1740 -0
- package/src/components/date-picker.tsx +631 -0
- package/src/components/dropdown.tsx +341 -0
- package/src/components/entity-combobox.tsx +215 -0
- package/src/components/facing-pair.ts +86 -0
- package/src/components/field-sync.tsx +472 -0
- package/src/components/file-dropzone.tsx +181 -0
- package/src/components/full-bleed-dialog.tsx +179 -0
- package/src/components/grouped-picker.tsx +157 -0
- package/src/components/hover-menu.tsx +183 -0
- package/src/components/mini-calendar.tsx +554 -0
- package/src/components/modal.tsx +287 -0
- package/src/components/month-picker.tsx +549 -0
- package/src/components/multi-entity-combobox.tsx +226 -0
- package/src/components/multi-select.tsx +303 -0
- package/src/components/number-field.tsx +225 -0
- package/src/components/number-input.tsx +211 -0
- package/src/components/numpad-sheet.tsx +264 -0
- package/src/components/page-contents.tsx +289 -0
- package/src/components/password-strength.tsx +258 -0
- package/src/components/picker-sheet.tsx +251 -0
- package/src/components/popover.tsx +195 -0
- package/src/components/search-field.tsx +146 -0
- package/src/components/series-chart-labels.ts +26 -0
- package/src/components/series-chart.tsx +481 -0
- package/src/components/settings-fields.tsx +68 -0
- package/src/components/signature-pad.tsx +630 -0
- package/src/components/slider.tsx +313 -0
- package/src/components/sparkline.tsx +329 -0
- package/src/components/stat-tile.tsx +448 -0
- package/src/components/swipeable-row.tsx +271 -0
- package/src/components/switch.tsx +170 -0
- package/src/components/time-input.tsx +107 -0
- package/src/components/toggle-group.tsx +153 -0
- package/src/components/toggle-legend.tsx +229 -0
- package/src/components/tooltip.tsx +477 -0
- package/src/components/treemap.tsx +390 -0
- package/src/components/ui.tsx +1284 -0
- package/src/components/use-mobile-reveal.ts +54 -0
- package/src/components/use-table-state.ts +295 -0
- package/src/components/user-avatar.tsx +61 -0
- package/src/components/wizard-stepper.tsx +37 -0
- package/src/data-table.ts +19 -0
- package/src/feedback/feedback-attachment.tsx +246 -0
- package/src/feedback/feedback-dialog.tsx +209 -0
- package/src/feedback/feedback-inbox.tsx +615 -0
- package/src/feedback.ts +8 -0
- package/src/hooks/use-anchored-panel.ts +169 -0
- package/src/hooks/use-anchored-rect.ts +78 -0
- package/src/hooks/use-announce.ts +85 -0
- package/src/hooks/use-body-scroll-lock.ts +94 -0
- package/src/hooks/use-close-transition.ts +99 -0
- package/src/hooks/use-dismiss.ts +77 -0
- package/src/hooks/use-focus-trap.ts +160 -0
- package/src/hooks/use-media-query.ts +23 -0
- package/src/hooks/use-overlay-history.ts +345 -0
- package/src/hooks/use-row-swipe.ts +212 -0
- package/src/i18n/defaults.ts +68 -0
- package/src/i18n/kit-labels.tsx +447 -0
- package/src/index.ts +169 -0
- package/src/lib/calc.ts +309 -0
- package/src/lib/cn.ts +5 -0
- package/src/lib/dates.ts +187 -0
- package/src/lib/logger.ts +73 -0
- package/src/lib/safe-storage.ts +37 -0
- package/src/search/command-palette.tsx +342 -0
- package/src/search.ts +3 -0
- package/src/shell/app-shell.tsx +772 -0
- package/src/shell/option-switcher-menu.tsx +72 -0
- package/src/shell/role-switcher.tsx +46 -0
- package/src/shell/top-bar.tsx +39 -0
- package/src/shell/topbar-action-menu.tsx +91 -0
- package/src/shell/topbar-controls.tsx +229 -0
- package/src/shell.ts +10 -0
- package/src/theme/chart-palette.ts +145 -0
- package/src/theme/color.ts +266 -0
- package/src/theme/palette-derive.ts +681 -0
- package/src/theme/palette-presets.ts +760 -0
- package/src/theme/palette-store.ts +94 -0
- package/src/theme/theme-store.ts +145 -0
- package/src/tour/tour.tsx +586 -0
- package/src/tour.ts +3 -0
- package/src/vite-env.d.ts +26 -0
- package/src/wizard/stepper-nav.tsx +221 -0
- package/src/wizard/types.ts +178 -0
- package/src/wizard/use-wizard.ts +345 -0
- package/src/wizard/validation.ts +37 -0
- package/src/wizard/wizard-context.tsx +29 -0
- package/src/wizard/wizard-step.tsx +14 -0
- package/src/wizard/wizard-summary.tsx +69 -0
- package/src/wizard.ts +14 -0
- package/tokens.css +222 -20
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
// Drag-to-zoom for cartesian charts — the HOC, the shared x window, and the arithmetic.
|
|
2
|
+
// Lifted out of lenkbank, where every plot in the app is one `SeriesChart` and zoom is
|
|
3
|
+
// therefore wired in exactly once.
|
|
4
|
+
//
|
|
5
|
+
// **Which axes a drag zooms is read off the drag itself.** A selection that is roughly
|
|
6
|
+
// square is a box: the reader framed a REGION and means both axes. A long thin one is a
|
|
7
|
+
// band, and its own direction says which: a wide flat drag means "this stretch of the
|
|
8
|
+
// abscissa", a tall narrow one "this range of values". Asking with a modifier key
|
|
9
|
+
// instead would be asking people to remember a convention for something they have
|
|
10
|
+
// already drawn.
|
|
11
|
+
//
|
|
12
|
+
// Squareness is measured **in fractions of the plot**, not pixels. These charts are
|
|
13
|
+
// three times wider than they are tall, so a pixel-square selection is a very wide
|
|
14
|
+
// slice of the data and would read as a band to anybody looking at it.
|
|
15
|
+
//
|
|
16
|
+
// The zoom is VIEW state and lives here, never in a query or a store: it is where
|
|
17
|
+
// somebody is looking, and a chart whose data reloads should not lose it. It is cleared
|
|
18
|
+
// by the reset button, by a double click, and by nothing else.
|
|
19
|
+
//
|
|
20
|
+
// Everything with a decision in it is a pure exported function below (`zoomAxesFor`,
|
|
21
|
+
// `selectionFromDrag`, `zoomAfter`, `zoomDomains`, `fitYToX`, `fitXToY`); the
|
|
22
|
+
// components are plumbing between a pointer and those.
|
|
23
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
24
|
+
import type { ComponentType, Dispatch, ReactNode, SetStateAction } from "react";
|
|
25
|
+
import {
|
|
26
|
+
usePlotArea,
|
|
27
|
+
useXAxisInverseScale,
|
|
28
|
+
useYAxisInverseScale,
|
|
29
|
+
type InverseScaleFunction,
|
|
30
|
+
} from "recharts";
|
|
31
|
+
import { X } from "lucide-react";
|
|
32
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
33
|
+
import { DEFAULT_SERIES_CHART_LABELS, type SeriesChartLabels } from "./series-chart-labels";
|
|
34
|
+
|
|
35
|
+
/** The y axis a series lands on when it names none — the single shared scale most
|
|
36
|
+
* charts have. `SeriesChart` and the fit below agree on it by importing this. */
|
|
37
|
+
export const DEFAULT_Y_AXIS = "y";
|
|
38
|
+
|
|
39
|
+
/* ── The arithmetic ─────────────────────────────────────────────────────── */
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* How square a selection has to be before it counts as a box: the shorter side
|
|
43
|
+
* against the longer, both as fractions of the plot. At 0.5 a selection twice as wide
|
|
44
|
+
* as it is tall is still a box and one three times as wide is a band.
|
|
45
|
+
*/
|
|
46
|
+
export const ZOOM_SQUARE_ENOUGH = 0.5;
|
|
47
|
+
|
|
48
|
+
/** Below this a drag is a click that moved, not a selection. In fractions of the plot,
|
|
49
|
+
* so it is the same gesture on a phone and on a wall display. */
|
|
50
|
+
export const ZOOM_MIN_DRAG = 0.02;
|
|
51
|
+
|
|
52
|
+
export type ZoomAxes = "both" | "x" | "y";
|
|
53
|
+
|
|
54
|
+
/** Which axes a drag zooms — or `null` for a drag too small to mean one, or a plot
|
|
55
|
+
* that has not been laid out yet. Direction does not matter; proportion does. */
|
|
56
|
+
export function zoomAxesFor(
|
|
57
|
+
dx: number,
|
|
58
|
+
dy: number,
|
|
59
|
+
width: number,
|
|
60
|
+
height: number,
|
|
61
|
+
threshold: number = ZOOM_SQUARE_ENOUGH,
|
|
62
|
+
): ZoomAxes | null {
|
|
63
|
+
if (!(width > 0) || !(height > 0)) return null;
|
|
64
|
+
const across = Math.abs(dx) / width;
|
|
65
|
+
const down = Math.abs(dy) / height;
|
|
66
|
+
if (across < ZOOM_MIN_DRAG && down < ZOOM_MIN_DRAG) return null;
|
|
67
|
+
const longer = Math.max(across, down);
|
|
68
|
+
const shorter = Math.min(across, down);
|
|
69
|
+
if (shorter / longer >= threshold) return "both";
|
|
70
|
+
return across > down ? "x" : "y";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** What a drag chose, in DATA coordinates, and which axes it actually named — the
|
|
74
|
+
* caller needs the second to know which of the two it may derive from the other. */
|
|
75
|
+
export interface ZoomSelection {
|
|
76
|
+
which: ZoomAxes;
|
|
77
|
+
x?: [number, number];
|
|
78
|
+
/** One window per y axis, keyed by axis id: they are in different units. */
|
|
79
|
+
y: Record<string, [number, number]>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A drag, in the chart's own pixel coordinates. */
|
|
83
|
+
export interface ZoomDrag {
|
|
84
|
+
fromX: number;
|
|
85
|
+
fromY: number;
|
|
86
|
+
toX: number;
|
|
87
|
+
toY: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A pair as a domain, smallest first. Recharts wants an ascending domain whichever
|
|
91
|
+
* way the drag went, and a y axis counts downward in pixels while its values count
|
|
92
|
+
* upward — so both ends are read and sorted rather than one assumed to be low. */
|
|
93
|
+
function ordered(a: number, b: number): [number, number] {
|
|
94
|
+
return a <= b ? [a, b] : [b, a];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A finished drag as a selection, or `null` when it selects nothing.
|
|
99
|
+
*
|
|
100
|
+
* `xInverse` and `yInverse(axisId)` are recharts' pixel→value functions; an axis whose
|
|
101
|
+
* function is missing, or whose ends come back non-finite, is left out rather than
|
|
102
|
+
* handed a NaN window.
|
|
103
|
+
*/
|
|
104
|
+
export function selectionFromDrag(
|
|
105
|
+
drag: ZoomDrag,
|
|
106
|
+
plot: { width: number; height: number },
|
|
107
|
+
xInverse: InverseScaleFunction | undefined,
|
|
108
|
+
yInverse: (axisId: string) => InverseScaleFunction | undefined,
|
|
109
|
+
axisIds: readonly string[],
|
|
110
|
+
): ZoomSelection | null {
|
|
111
|
+
const which = zoomAxesFor(drag.toX - drag.fromX, drag.toY - drag.fromY, plot.width, plot.height);
|
|
112
|
+
if (!which) return null;
|
|
113
|
+
const next: ZoomSelection = { which, y: {} };
|
|
114
|
+
if (which !== "y" && xInverse) {
|
|
115
|
+
const from = Number(xInverse(drag.fromX));
|
|
116
|
+
const to = Number(xInverse(drag.toX));
|
|
117
|
+
if (Number.isFinite(from) && Number.isFinite(to)) next.x = ordered(from, to);
|
|
118
|
+
}
|
|
119
|
+
if (which !== "x") {
|
|
120
|
+
for (const id of axisIds) {
|
|
121
|
+
const inverse = yInverse(id);
|
|
122
|
+
if (!inverse) continue;
|
|
123
|
+
const from = Number(inverse(drag.fromY));
|
|
124
|
+
const to = Number(inverse(drag.toY));
|
|
125
|
+
if (Number.isFinite(from) && Number.isFinite(to)) next.y[id] = ordered(from, to);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return next.x || Object.keys(next.y).length ? next : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** What a band refit needs to know about the chart — a subset of `SeriesChart`'s own
|
|
132
|
+
* props, so a chart hands its data straight over. */
|
|
133
|
+
export interface ZoomFitSource {
|
|
134
|
+
rows: Record<string, number>[];
|
|
135
|
+
series: { key: string; axis?: string }[];
|
|
136
|
+
axes: { id: string }[];
|
|
137
|
+
xKey: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** How much of a refitted span to leave as air, top and bottom. Without it the
|
|
141
|
+
* highest sample sits exactly on the frame, where a line is half drawn and reads as
|
|
142
|
+
* clipped rather than as the maximum. */
|
|
143
|
+
const FIT_AIR = 0.05;
|
|
144
|
+
|
|
145
|
+
/** A span with air around it — and a real span for a flat channel, which would
|
|
146
|
+
* otherwise be a zero-height window recharts cannot scale. */
|
|
147
|
+
function airy(low: number, high: number): [number, number] {
|
|
148
|
+
const span = high - low;
|
|
149
|
+
const air = span > 0 ? span * FIT_AIR : Math.max(Math.abs(high), 1) * FIT_AIR;
|
|
150
|
+
return [low - air, high + air];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A running low/high pair. A pair rather than an array to take `Math.min(...)` of:
|
|
154
|
+
* the spread passes one argument per sample, and V8 faults past ~125,000 of them. */
|
|
155
|
+
type Span = [number, number] | undefined;
|
|
156
|
+
|
|
157
|
+
/** A pair widened by one value. A non-finite one is not a value — one NaN would
|
|
158
|
+
* otherwise take the whole window with it. */
|
|
159
|
+
function widen(span: Span, value: number): Span {
|
|
160
|
+
if (!Number.isFinite(value)) return span;
|
|
161
|
+
if (!span) return [value, value];
|
|
162
|
+
if (value < span[0]) span[0] = value;
|
|
163
|
+
else if (value > span[1]) span[1] = value;
|
|
164
|
+
return span;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Which series are measured on an axis. */
|
|
168
|
+
function keysOn(source: ZoomFitSource, axisId: string): string[] {
|
|
169
|
+
return source.series
|
|
170
|
+
.filter((entry) => (entry.axis ?? DEFAULT_Y_AXIS) === axisId)
|
|
171
|
+
.map((entry) => entry.key);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Each y axis fitted to the samples inside an x window — what a band across x leaves
|
|
176
|
+
* the axes nobody dragged showing. Zoom into a tenth of a sweep and a y axis still
|
|
177
|
+
* scaled to the whole run draws that tenth as a flat line, which is the one thing the
|
|
178
|
+
* zoom was for; so the unnamed axes are refitted, which is what "zoom" means everywhere
|
|
179
|
+
* else.
|
|
180
|
+
*
|
|
181
|
+
* An axis with nothing inside the window is left OUT rather than collapsed: a window
|
|
182
|
+
* between two samples is one where the chart should keep drawing the line across.
|
|
183
|
+
*/
|
|
184
|
+
export function fitYToX(
|
|
185
|
+
source: ZoomFitSource,
|
|
186
|
+
window: readonly [number, number],
|
|
187
|
+
): Record<string, [number, number]> {
|
|
188
|
+
const [from, to] = window;
|
|
189
|
+
const fitted: Record<string, [number, number]> = {};
|
|
190
|
+
for (const axis of source.axes) {
|
|
191
|
+
const keys = keysOn(source, axis.id);
|
|
192
|
+
let span: Span;
|
|
193
|
+
for (const row of source.rows) {
|
|
194
|
+
const at = row[source.xKey];
|
|
195
|
+
if (!(at >= from && at <= to)) continue;
|
|
196
|
+
for (const key of keys) span = widen(span, row[key]);
|
|
197
|
+
}
|
|
198
|
+
if (span) fitted[axis.id] = airy(span[0], span[1]);
|
|
199
|
+
}
|
|
200
|
+
return fitted;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The x window that holds every sample inside a set of y windows — the other direction
|
|
205
|
+
* of the same idea. A channel crossing its band once gives a tight window; one that
|
|
206
|
+
* oscillates through it gives back most of the sweep, which is the truthful answer.
|
|
207
|
+
* `undefined` when no sample qualifies, rather than a guess.
|
|
208
|
+
*/
|
|
209
|
+
export function fitXToY(
|
|
210
|
+
source: ZoomFitSource,
|
|
211
|
+
windows: Readonly<Record<string, readonly [number, number]>>,
|
|
212
|
+
): [number, number] | undefined {
|
|
213
|
+
let hits: Span;
|
|
214
|
+
for (const [axisId, [low, high]] of Object.entries(windows)) {
|
|
215
|
+
for (const key of keysOn(source, axisId)) {
|
|
216
|
+
for (const row of source.rows) {
|
|
217
|
+
const value = row[key];
|
|
218
|
+
if (Number.isFinite(value) && value >= low && value <= high) {
|
|
219
|
+
hits = widen(hits, row[source.xKey]);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return hits ? airy(hits[0], hits[1]) : undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* What a chart remembers about its zoom: the x window, and the y windows the reader
|
|
229
|
+
* DRAGGED. `y: null` means "derive y from x" — the split is the whole design: what was
|
|
230
|
+
* dragged is kept, and what follows from it is recomputed on every render, so a band
|
|
231
|
+
* across x refits y here and in every chart sharing the window.
|
|
232
|
+
*/
|
|
233
|
+
export interface ZoomState {
|
|
234
|
+
x?: [number, number];
|
|
235
|
+
y: Record<string, [number, number]> | null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export const NO_ZOOM: ZoomState = { x: undefined, y: null };
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The state after a selection. Two band drags COMPOSE rather than the second undoing
|
|
242
|
+
* the first: a band down y keeps the x window when the band holds no samples, and
|
|
243
|
+
* otherwise pulls x in to where they are.
|
|
244
|
+
*/
|
|
245
|
+
export function zoomAfter(
|
|
246
|
+
state: ZoomState,
|
|
247
|
+
selection: ZoomSelection,
|
|
248
|
+
source: ZoomFitSource,
|
|
249
|
+
): ZoomState {
|
|
250
|
+
if (selection.which === "both") return { x: selection.x, y: selection.y };
|
|
251
|
+
// Leave y derived, so it fits itself to what is inside the new window.
|
|
252
|
+
if (selection.which === "x") return { x: selection.x, y: null };
|
|
253
|
+
return { x: fitXToY(source, selection.y) ?? state.x, y: selection.y };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The domains a chart draws for a state: the x window as is, and each y axis either
|
|
257
|
+
* as dragged or fitted to the x window. `{}` means "fit your own data". */
|
|
258
|
+
export function zoomDomains(
|
|
259
|
+
state: ZoomState,
|
|
260
|
+
source: ZoomFitSource,
|
|
261
|
+
): { xDomain?: [number, number]; yDomains: Record<string, [number, number]> } {
|
|
262
|
+
return {
|
|
263
|
+
xDomain: state.x,
|
|
264
|
+
yDomains: state.y ?? (state.x ? fitYToX(source, state.x) : {}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* ── The components ─────────────────────────────────────────────────────── */
|
|
269
|
+
|
|
270
|
+
/** What the wrapped chart is handed. It applies the domains to its own axes and renders
|
|
271
|
+
* `layer` INSIDE the recharts chart, where recharts' hooks answer. */
|
|
272
|
+
export interface ZoomBinding {
|
|
273
|
+
xDomain?: [number, number];
|
|
274
|
+
yDomains: Record<string, [number, number]>;
|
|
275
|
+
layer: ReactNode;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The props `withChartZoom` reads off the chart it wraps. */
|
|
279
|
+
export interface ZoomTarget {
|
|
280
|
+
/** Default: the single axis {@link DEFAULT_Y_AXIS}. */
|
|
281
|
+
axes?: readonly { id: string; hide?: boolean }[];
|
|
282
|
+
/** The chart's own data, so a band can refit the axis it did not name. Optional: a
|
|
283
|
+
* chart with nothing plotted has nothing to fit to. */
|
|
284
|
+
rows?: Record<string, number>[];
|
|
285
|
+
series?: readonly { key: string; axis?: string }[];
|
|
286
|
+
x?: { key?: string };
|
|
287
|
+
zoom?: ZoomBinding;
|
|
288
|
+
labels?: Partial<SeriesChartLabels>;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const SharedX = createContext<{
|
|
292
|
+
window?: [number, number];
|
|
293
|
+
set: Dispatch<SetStateAction<[number, number] | undefined>>;
|
|
294
|
+
} | null>(null);
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* One x window for several charts.
|
|
298
|
+
*
|
|
299
|
+
* A column of charts over one abscissa is one picture, and zooming one of them and not
|
|
300
|
+
* the rest turns it into several pictures of different things. Every zoomable chart
|
|
301
|
+
* inside this shares the x window: any drag in any of them moves all of them.
|
|
302
|
+
*
|
|
303
|
+
* **Only x.** The y axes stay each chart's own — they are in different units — and
|
|
304
|
+
* each refits its own y to what is inside the shared window.
|
|
305
|
+
*/
|
|
306
|
+
export function SharedXZoom({ children }: { children: ReactNode }) {
|
|
307
|
+
const [window, set] = useState<[number, number] | undefined>(undefined);
|
|
308
|
+
return <SharedX.Provider value={{ window, set }}>{children}</SharedX.Provider>;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Wrap a chart in drag-to-zoom.
|
|
313
|
+
*
|
|
314
|
+
* The chart receives a `zoom` binding: it applies `xDomain`/`yDomains` to its axes
|
|
315
|
+
* (with `allowDataOverflow`, so recharts clips to the window instead of widening it
|
|
316
|
+
* back out) and renders `layer` as a child of its recharts chart. A reset button
|
|
317
|
+
* appears over the chart's top end corner while it is zoomed — a real button, so the
|
|
318
|
+
* keyboard can undo what the pointer did.
|
|
319
|
+
*/
|
|
320
|
+
export function withChartZoom<P extends ZoomTarget>(Chart: ComponentType<P>) {
|
|
321
|
+
function Zoomable(props: P) {
|
|
322
|
+
const labels = useKitLabels("seriesChart", DEFAULT_SERIES_CHART_LABELS, props.labels);
|
|
323
|
+
const group = useContext(SharedX);
|
|
324
|
+
const [ownX, setOwnX] = useState<[number, number] | undefined>(undefined);
|
|
325
|
+
const [ownY, setOwnY] = useState<Record<string, [number, number]> | null>(null);
|
|
326
|
+
const xWindow = group ? group.window : ownX;
|
|
327
|
+
const setXWindow = group ? group.set : setOwnX;
|
|
328
|
+
const axes = props.axes ?? [{ id: DEFAULT_Y_AXIS }];
|
|
329
|
+
|
|
330
|
+
// Every axis, hidden ones included: a hidden axis still scales its lines, and a
|
|
331
|
+
// line whose axis did not refit is drawn as a flat streak inside the window.
|
|
332
|
+
const source: ZoomFitSource = {
|
|
333
|
+
rows: props.rows ?? [],
|
|
334
|
+
series: [...(props.series ?? [])],
|
|
335
|
+
axes: [...axes],
|
|
336
|
+
xKey: props.x?.key ?? "x",
|
|
337
|
+
};
|
|
338
|
+
const state: ZoomState = { x: xWindow, y: ownY };
|
|
339
|
+
const zoomed = xWindow !== undefined || ownY !== null;
|
|
340
|
+
|
|
341
|
+
const clear = () => {
|
|
342
|
+
setXWindow(undefined);
|
|
343
|
+
setOwnY(null);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const binding: ZoomBinding = {
|
|
347
|
+
...zoomDomains(state, source),
|
|
348
|
+
layer: (
|
|
349
|
+
<ZoomLayer
|
|
350
|
+
axisIds={axes.map((axis) => axis.id)}
|
|
351
|
+
label={labels.zoomHint}
|
|
352
|
+
onZoom={(selection) => {
|
|
353
|
+
const next = zoomAfter(state, selection, source);
|
|
354
|
+
if (next.x !== xWindow) setXWindow(next.x);
|
|
355
|
+
setOwnY(next.y);
|
|
356
|
+
}}
|
|
357
|
+
onReset={clear}
|
|
358
|
+
/>
|
|
359
|
+
),
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<div className="relative">
|
|
364
|
+
<Chart {...props} zoom={binding} />
|
|
365
|
+
{zoomed && (
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
onClick={clear}
|
|
369
|
+
className="absolute end-1 top-1 z-10 inline-flex items-center gap-1 rounded border border-[var(--border)] bg-[var(--bg-surface)]/90 px-1.5 py-0.5 text-[10px] text-[var(--text-muted)] backdrop-blur transition-colors hover:text-[var(--text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]"
|
|
370
|
+
>
|
|
371
|
+
<X aria-hidden className="size-3" /> {labels.resetZoom}
|
|
372
|
+
</button>
|
|
373
|
+
)}
|
|
374
|
+
</div>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
Zoomable.displayName = `withChartZoom(${Chart.displayName ?? Chart.name ?? "Chart"})`;
|
|
378
|
+
return Zoomable;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* The part that lives INSIDE the recharts chart, where recharts will say where the plot
|
|
383
|
+
* is and what value a pixel is.
|
|
384
|
+
*
|
|
385
|
+
* A transparent rectangle over the plot area that reads the drag. It does NOT stop the
|
|
386
|
+
* event: recharts drives its tooltip from the same pointer move bubbling up to its own
|
|
387
|
+
* wrapper, so swallowing it here would trade the tooltip for the zoom.
|
|
388
|
+
*/
|
|
389
|
+
function ZoomLayer({
|
|
390
|
+
axisIds,
|
|
391
|
+
label,
|
|
392
|
+
onZoom,
|
|
393
|
+
onReset,
|
|
394
|
+
}: {
|
|
395
|
+
axisIds: string[];
|
|
396
|
+
label: string;
|
|
397
|
+
onZoom: (selection: ZoomSelection) => void;
|
|
398
|
+
onReset: () => void;
|
|
399
|
+
}) {
|
|
400
|
+
const plot = usePlotArea();
|
|
401
|
+
const xInverse = useXAxisInverseScale();
|
|
402
|
+
// One probe COMPONENT per axis rather than a hook in a loop: the number of axes
|
|
403
|
+
// changes as series are toggled, and a hook count that changes with the data is the
|
|
404
|
+
// one thing React will not forgive.
|
|
405
|
+
const [scales] = useState(() => new Map<string, InverseScaleFunction | undefined>());
|
|
406
|
+
const [drag, setDrag] = useState<ZoomDrag | null>(null);
|
|
407
|
+
|
|
408
|
+
if (!plot || plot.width <= 0 || plot.height <= 0) return null;
|
|
409
|
+
|
|
410
|
+
const at = (event: { clientX: number; clientY: number; currentTarget: Element }) => {
|
|
411
|
+
// Against the overlay's own screen box, so padding around the container cannot
|
|
412
|
+
// shift every reading — plus the plot's offset, because the scales are in the
|
|
413
|
+
// chart's coordinates rather than the plot's.
|
|
414
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
415
|
+
return { x: event.clientX - box.left + plot.x, y: event.clientY - box.top + plot.y };
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const finish = () => {
|
|
419
|
+
if (!drag) return;
|
|
420
|
+
setDrag(null);
|
|
421
|
+
const selection = selectionFromDrag(drag, plot, xInverse, (id) => scales.get(id), axisIds);
|
|
422
|
+
if (selection) onZoom(selection);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// What the drag would do, shown while it is being made: a band paints the full width
|
|
426
|
+
// or height, so the reader sees it is about to let the other axis refit.
|
|
427
|
+
const which = drag
|
|
428
|
+
? zoomAxesFor(drag.toX - drag.fromX, drag.toY - drag.fromY, plot.width, plot.height)
|
|
429
|
+
: null;
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<g>
|
|
433
|
+
{axisIds.map((id) => (
|
|
434
|
+
<AxisProbe key={id} axisId={id} scales={scales} />
|
|
435
|
+
))}
|
|
436
|
+
{drag && which && (
|
|
437
|
+
<rect
|
|
438
|
+
x={which === "y" ? plot.x : Math.min(drag.fromX, drag.toX)}
|
|
439
|
+
y={which === "x" ? plot.y : Math.min(drag.fromY, drag.toY)}
|
|
440
|
+
width={which === "y" ? plot.width : Math.abs(drag.toX - drag.fromX)}
|
|
441
|
+
height={which === "x" ? plot.height : Math.abs(drag.toY - drag.fromY)}
|
|
442
|
+
className="fill-[var(--brand)]/15 stroke-[var(--brand)]"
|
|
443
|
+
strokeDasharray="4 3"
|
|
444
|
+
pointerEvents="none"
|
|
445
|
+
/>
|
|
446
|
+
)}
|
|
447
|
+
<rect
|
|
448
|
+
x={plot.x}
|
|
449
|
+
y={plot.y}
|
|
450
|
+
width={plot.width}
|
|
451
|
+
height={plot.height}
|
|
452
|
+
// `transparent`, not `none`: an unpainted shape takes no pointer events.
|
|
453
|
+
fill="transparent"
|
|
454
|
+
role="application"
|
|
455
|
+
aria-label={label}
|
|
456
|
+
style={{ cursor: "crosshair" }}
|
|
457
|
+
onPointerDown={(event) => {
|
|
458
|
+
if (event.button !== 0) return;
|
|
459
|
+
const point = at(event);
|
|
460
|
+
// Best effort: capture keeps a drag alive past the plot's edge, but a
|
|
461
|
+
// pointer id the environment does not know throws — jsdom refuses them all —
|
|
462
|
+
// and a zoom that threw on pointer-down would be a chart nobody could click.
|
|
463
|
+
try {
|
|
464
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
465
|
+
} catch {
|
|
466
|
+
/* the drag still works; it just stops at the edge of the plot */
|
|
467
|
+
}
|
|
468
|
+
setDrag({ fromX: point.x, fromY: point.y, toX: point.x, toY: point.y });
|
|
469
|
+
}}
|
|
470
|
+
onPointerMove={(event) => {
|
|
471
|
+
if (!drag) return;
|
|
472
|
+
const point = at(event);
|
|
473
|
+
setDrag({ ...drag, toX: point.x, toY: point.y });
|
|
474
|
+
}}
|
|
475
|
+
onPointerUp={finish}
|
|
476
|
+
onPointerCancel={() => setDrag(null)}
|
|
477
|
+
onDoubleClick={onReset}
|
|
478
|
+
/>
|
|
479
|
+
</g>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** One axis's pixel→value function, parked where the pointer handler can reach it. */
|
|
484
|
+
function AxisProbe({
|
|
485
|
+
axisId,
|
|
486
|
+
scales,
|
|
487
|
+
}: {
|
|
488
|
+
axisId: string;
|
|
489
|
+
scales: Map<string, InverseScaleFunction | undefined>;
|
|
490
|
+
}) {
|
|
491
|
+
const inverse = useYAxisInverseScale(axisId);
|
|
492
|
+
useEffect(() => {
|
|
493
|
+
scales.set(axisId, inverse);
|
|
494
|
+
return () => {
|
|
495
|
+
scales.delete(axisId);
|
|
496
|
+
};
|
|
497
|
+
}, [axisId, inverse, scales]);
|
|
498
|
+
return null;
|
|
499
|
+
}
|