@eifi1/ui-kit 0.4.1 → 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 +89 -19
- 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 +74 -19
- package/dist/components/dropdown.js +72 -22
- 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 +21 -7
- package/dist/components/file-dropzone.js +28 -7
- 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 +21 -2
- package/dist/components/number-input.js +2 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +25 -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 +5 -3
- package/dist/components/user-avatar.js +4 -2
- 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 +14 -1
- package/dist/theme/palette-presets.js +231 -3
- 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 +66 -13
- 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,262 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
usePlotArea,
|
|
6
|
+
useXAxisInverseScale,
|
|
7
|
+
useYAxisInverseScale
|
|
8
|
+
} from "recharts";
|
|
9
|
+
import { X } from "lucide-react";
|
|
10
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
11
|
+
import { DEFAULT_SERIES_CHART_LABELS } from "./series-chart-labels.js";
|
|
12
|
+
const DEFAULT_Y_AXIS = "y";
|
|
13
|
+
const ZOOM_SQUARE_ENOUGH = 0.5;
|
|
14
|
+
const ZOOM_MIN_DRAG = 0.02;
|
|
15
|
+
function zoomAxesFor(dx, dy, width, height, threshold = ZOOM_SQUARE_ENOUGH) {
|
|
16
|
+
if (!(width > 0) || !(height > 0)) return null;
|
|
17
|
+
const across = Math.abs(dx) / width;
|
|
18
|
+
const down = Math.abs(dy) / height;
|
|
19
|
+
if (across < ZOOM_MIN_DRAG && down < ZOOM_MIN_DRAG) return null;
|
|
20
|
+
const longer = Math.max(across, down);
|
|
21
|
+
const shorter = Math.min(across, down);
|
|
22
|
+
if (shorter / longer >= threshold) return "both";
|
|
23
|
+
return across > down ? "x" : "y";
|
|
24
|
+
}
|
|
25
|
+
function ordered(a, b) {
|
|
26
|
+
return a <= b ? [a, b] : [b, a];
|
|
27
|
+
}
|
|
28
|
+
function selectionFromDrag(drag, plot, xInverse, yInverse, axisIds) {
|
|
29
|
+
const which = zoomAxesFor(drag.toX - drag.fromX, drag.toY - drag.fromY, plot.width, plot.height);
|
|
30
|
+
if (!which) return null;
|
|
31
|
+
const next = { which, y: {} };
|
|
32
|
+
if (which !== "y" && xInverse) {
|
|
33
|
+
const from = Number(xInverse(drag.fromX));
|
|
34
|
+
const to = Number(xInverse(drag.toX));
|
|
35
|
+
if (Number.isFinite(from) && Number.isFinite(to)) next.x = ordered(from, to);
|
|
36
|
+
}
|
|
37
|
+
if (which !== "x") {
|
|
38
|
+
for (const id of axisIds) {
|
|
39
|
+
const inverse = yInverse(id);
|
|
40
|
+
if (!inverse) continue;
|
|
41
|
+
const from = Number(inverse(drag.fromY));
|
|
42
|
+
const to = Number(inverse(drag.toY));
|
|
43
|
+
if (Number.isFinite(from) && Number.isFinite(to)) next.y[id] = ordered(from, to);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return next.x || Object.keys(next.y).length ? next : null;
|
|
47
|
+
}
|
|
48
|
+
const FIT_AIR = 0.05;
|
|
49
|
+
function airy(low, high) {
|
|
50
|
+
const span = high - low;
|
|
51
|
+
const air = span > 0 ? span * FIT_AIR : Math.max(Math.abs(high), 1) * FIT_AIR;
|
|
52
|
+
return [low - air, high + air];
|
|
53
|
+
}
|
|
54
|
+
function widen(span, value) {
|
|
55
|
+
if (!Number.isFinite(value)) return span;
|
|
56
|
+
if (!span) return [value, value];
|
|
57
|
+
if (value < span[0]) span[0] = value;
|
|
58
|
+
else if (value > span[1]) span[1] = value;
|
|
59
|
+
return span;
|
|
60
|
+
}
|
|
61
|
+
function keysOn(source, axisId) {
|
|
62
|
+
return source.series.filter((entry) => (entry.axis ?? DEFAULT_Y_AXIS) === axisId).map((entry) => entry.key);
|
|
63
|
+
}
|
|
64
|
+
function fitYToX(source, window) {
|
|
65
|
+
const [from, to] = window;
|
|
66
|
+
const fitted = {};
|
|
67
|
+
for (const axis of source.axes) {
|
|
68
|
+
const keys = keysOn(source, axis.id);
|
|
69
|
+
let span;
|
|
70
|
+
for (const row of source.rows) {
|
|
71
|
+
const at = row[source.xKey];
|
|
72
|
+
if (!(at >= from && at <= to)) continue;
|
|
73
|
+
for (const key of keys) span = widen(span, row[key]);
|
|
74
|
+
}
|
|
75
|
+
if (span) fitted[axis.id] = airy(span[0], span[1]);
|
|
76
|
+
}
|
|
77
|
+
return fitted;
|
|
78
|
+
}
|
|
79
|
+
function fitXToY(source, windows) {
|
|
80
|
+
let hits;
|
|
81
|
+
for (const [axisId, [low, high]] of Object.entries(windows)) {
|
|
82
|
+
for (const key of keysOn(source, axisId)) {
|
|
83
|
+
for (const row of source.rows) {
|
|
84
|
+
const value = row[key];
|
|
85
|
+
if (Number.isFinite(value) && value >= low && value <= high) {
|
|
86
|
+
hits = widen(hits, row[source.xKey]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return hits ? airy(hits[0], hits[1]) : void 0;
|
|
92
|
+
}
|
|
93
|
+
const NO_ZOOM = { x: void 0, y: null };
|
|
94
|
+
function zoomAfter(state, selection, source) {
|
|
95
|
+
if (selection.which === "both") return { x: selection.x, y: selection.y };
|
|
96
|
+
if (selection.which === "x") return { x: selection.x, y: null };
|
|
97
|
+
return { x: fitXToY(source, selection.y) ?? state.x, y: selection.y };
|
|
98
|
+
}
|
|
99
|
+
function zoomDomains(state, source) {
|
|
100
|
+
return {
|
|
101
|
+
xDomain: state.x,
|
|
102
|
+
yDomains: state.y ?? (state.x ? fitYToX(source, state.x) : {})
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const SharedX = createContext(null);
|
|
106
|
+
function SharedXZoom({ children }) {
|
|
107
|
+
const [window, set] = useState(void 0);
|
|
108
|
+
return /* @__PURE__ */ jsx(SharedX.Provider, { value: { window, set }, children });
|
|
109
|
+
}
|
|
110
|
+
function withChartZoom(Chart) {
|
|
111
|
+
function Zoomable(props) {
|
|
112
|
+
const labels = useKitLabels("seriesChart", DEFAULT_SERIES_CHART_LABELS, props.labels);
|
|
113
|
+
const group = useContext(SharedX);
|
|
114
|
+
const [ownX, setOwnX] = useState(void 0);
|
|
115
|
+
const [ownY, setOwnY] = useState(null);
|
|
116
|
+
const xWindow = group ? group.window : ownX;
|
|
117
|
+
const setXWindow = group ? group.set : setOwnX;
|
|
118
|
+
const axes = props.axes ?? [{ id: DEFAULT_Y_AXIS }];
|
|
119
|
+
const source = {
|
|
120
|
+
rows: props.rows ?? [],
|
|
121
|
+
series: [...props.series ?? []],
|
|
122
|
+
axes: [...axes],
|
|
123
|
+
xKey: props.x?.key ?? "x"
|
|
124
|
+
};
|
|
125
|
+
const state = { x: xWindow, y: ownY };
|
|
126
|
+
const zoomed = xWindow !== void 0 || ownY !== null;
|
|
127
|
+
const clear = () => {
|
|
128
|
+
setXWindow(void 0);
|
|
129
|
+
setOwnY(null);
|
|
130
|
+
};
|
|
131
|
+
const binding = {
|
|
132
|
+
...zoomDomains(state, source),
|
|
133
|
+
layer: /* @__PURE__ */ jsx(
|
|
134
|
+
ZoomLayer,
|
|
135
|
+
{
|
|
136
|
+
axisIds: axes.map((axis) => axis.id),
|
|
137
|
+
label: labels.zoomHint,
|
|
138
|
+
onZoom: (selection) => {
|
|
139
|
+
const next = zoomAfter(state, selection, source);
|
|
140
|
+
if (next.x !== xWindow) setXWindow(next.x);
|
|
141
|
+
setOwnY(next.y);
|
|
142
|
+
},
|
|
143
|
+
onReset: clear
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
};
|
|
147
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
148
|
+
/* @__PURE__ */ jsx(Chart, { ...props, zoom: binding }),
|
|
149
|
+
zoomed && /* @__PURE__ */ jsxs(
|
|
150
|
+
"button",
|
|
151
|
+
{
|
|
152
|
+
type: "button",
|
|
153
|
+
onClick: clear,
|
|
154
|
+
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)]",
|
|
155
|
+
children: [
|
|
156
|
+
/* @__PURE__ */ jsx(X, { "aria-hidden": true, className: "size-3" }),
|
|
157
|
+
" ",
|
|
158
|
+
labels.resetZoom
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
] });
|
|
163
|
+
}
|
|
164
|
+
Zoomable.displayName = `withChartZoom(${Chart.displayName ?? Chart.name ?? "Chart"})`;
|
|
165
|
+
return Zoomable;
|
|
166
|
+
}
|
|
167
|
+
function ZoomLayer({
|
|
168
|
+
axisIds,
|
|
169
|
+
label,
|
|
170
|
+
onZoom,
|
|
171
|
+
onReset
|
|
172
|
+
}) {
|
|
173
|
+
const plot = usePlotArea();
|
|
174
|
+
const xInverse = useXAxisInverseScale();
|
|
175
|
+
const [scales] = useState(() => /* @__PURE__ */ new Map());
|
|
176
|
+
const [drag, setDrag] = useState(null);
|
|
177
|
+
if (!plot || plot.width <= 0 || plot.height <= 0) return null;
|
|
178
|
+
const at = (event) => {
|
|
179
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
180
|
+
return { x: event.clientX - box.left + plot.x, y: event.clientY - box.top + plot.y };
|
|
181
|
+
};
|
|
182
|
+
const finish = () => {
|
|
183
|
+
if (!drag) return;
|
|
184
|
+
setDrag(null);
|
|
185
|
+
const selection = selectionFromDrag(drag, plot, xInverse, (id) => scales.get(id), axisIds);
|
|
186
|
+
if (selection) onZoom(selection);
|
|
187
|
+
};
|
|
188
|
+
const which = drag ? zoomAxesFor(drag.toX - drag.fromX, drag.toY - drag.fromY, plot.width, plot.height) : null;
|
|
189
|
+
return /* @__PURE__ */ jsxs("g", { children: [
|
|
190
|
+
axisIds.map((id) => /* @__PURE__ */ jsx(AxisProbe, { axisId: id, scales }, id)),
|
|
191
|
+
drag && which && /* @__PURE__ */ jsx(
|
|
192
|
+
"rect",
|
|
193
|
+
{
|
|
194
|
+
x: which === "y" ? plot.x : Math.min(drag.fromX, drag.toX),
|
|
195
|
+
y: which === "x" ? plot.y : Math.min(drag.fromY, drag.toY),
|
|
196
|
+
width: which === "y" ? plot.width : Math.abs(drag.toX - drag.fromX),
|
|
197
|
+
height: which === "x" ? plot.height : Math.abs(drag.toY - drag.fromY),
|
|
198
|
+
className: "fill-[var(--brand)]/15 stroke-[var(--brand)]",
|
|
199
|
+
strokeDasharray: "4 3",
|
|
200
|
+
pointerEvents: "none"
|
|
201
|
+
}
|
|
202
|
+
),
|
|
203
|
+
/* @__PURE__ */ jsx(
|
|
204
|
+
"rect",
|
|
205
|
+
{
|
|
206
|
+
x: plot.x,
|
|
207
|
+
y: plot.y,
|
|
208
|
+
width: plot.width,
|
|
209
|
+
height: plot.height,
|
|
210
|
+
fill: "transparent",
|
|
211
|
+
role: "application",
|
|
212
|
+
"aria-label": label,
|
|
213
|
+
style: { cursor: "crosshair" },
|
|
214
|
+
onPointerDown: (event) => {
|
|
215
|
+
if (event.button !== 0) return;
|
|
216
|
+
const point = at(event);
|
|
217
|
+
try {
|
|
218
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
setDrag({ fromX: point.x, fromY: point.y, toX: point.x, toY: point.y });
|
|
222
|
+
},
|
|
223
|
+
onPointerMove: (event) => {
|
|
224
|
+
if (!drag) return;
|
|
225
|
+
const point = at(event);
|
|
226
|
+
setDrag({ ...drag, toX: point.x, toY: point.y });
|
|
227
|
+
},
|
|
228
|
+
onPointerUp: finish,
|
|
229
|
+
onPointerCancel: () => setDrag(null),
|
|
230
|
+
onDoubleClick: onReset
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
] });
|
|
234
|
+
}
|
|
235
|
+
function AxisProbe({
|
|
236
|
+
axisId,
|
|
237
|
+
scales
|
|
238
|
+
}) {
|
|
239
|
+
const inverse = useYAxisInverseScale(axisId);
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
scales.set(axisId, inverse);
|
|
242
|
+
return () => {
|
|
243
|
+
scales.delete(axisId);
|
|
244
|
+
};
|
|
245
|
+
}, [axisId, inverse, scales]);
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
export {
|
|
249
|
+
DEFAULT_Y_AXIS,
|
|
250
|
+
NO_ZOOM,
|
|
251
|
+
SharedXZoom,
|
|
252
|
+
ZOOM_MIN_DRAG,
|
|
253
|
+
ZOOM_SQUARE_ENOUGH,
|
|
254
|
+
fitXToY,
|
|
255
|
+
fitYToX,
|
|
256
|
+
selectionFromDrag,
|
|
257
|
+
withChartZoom,
|
|
258
|
+
zoomAfter,
|
|
259
|
+
zoomAxesFor,
|
|
260
|
+
zoomDomains
|
|
261
|
+
};
|
|
262
|
+
//# sourceMappingURL=chart-zoom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/chart-zoom.tsx"],"sourcesContent":["// Drag-to-zoom for cartesian charts — the HOC, the shared x window, and the arithmetic.\n// Lifted out of lenkbank, where every plot in the app is one `SeriesChart` and zoom is\n// therefore wired in exactly once.\n//\n// **Which axes a drag zooms is read off the drag itself.** A selection that is roughly\n// square is a box: the reader framed a REGION and means both axes. A long thin one is a\n// band, and its own direction says which: a wide flat drag means \"this stretch of the\n// abscissa\", a tall narrow one \"this range of values\". Asking with a modifier key\n// instead would be asking people to remember a convention for something they have\n// already drawn.\n//\n// Squareness is measured **in fractions of the plot**, not pixels. These charts are\n// three times wider than they are tall, so a pixel-square selection is a very wide\n// slice of the data and would read as a band to anybody looking at it.\n//\n// The zoom is VIEW state and lives here, never in a query or a store: it is where\n// somebody is looking, and a chart whose data reloads should not lose it. It is cleared\n// by the reset button, by a double click, and by nothing else.\n//\n// Everything with a decision in it is a pure exported function below (`zoomAxesFor`,\n// `selectionFromDrag`, `zoomAfter`, `zoomDomains`, `fitYToX`, `fitXToY`); the\n// components are plumbing between a pointer and those.\nimport { createContext, useContext, useEffect, useState } from \"react\";\nimport type { ComponentType, Dispatch, ReactNode, SetStateAction } from \"react\";\nimport {\n usePlotArea,\n useXAxisInverseScale,\n useYAxisInverseScale,\n type InverseScaleFunction,\n} from \"recharts\";\nimport { X } from \"lucide-react\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { DEFAULT_SERIES_CHART_LABELS, type SeriesChartLabels } from \"./series-chart-labels\";\n\n/** The y axis a series lands on when it names none — the single shared scale most\n * charts have. `SeriesChart` and the fit below agree on it by importing this. */\nexport const DEFAULT_Y_AXIS = \"y\";\n\n/* ── The arithmetic ─────────────────────────────────────────────────────── */\n\n/**\n * How square a selection has to be before it counts as a box: the shorter side\n * against the longer, both as fractions of the plot. At 0.5 a selection twice as wide\n * as it is tall is still a box and one three times as wide is a band.\n */\nexport const ZOOM_SQUARE_ENOUGH = 0.5;\n\n/** Below this a drag is a click that moved, not a selection. In fractions of the plot,\n * so it is the same gesture on a phone and on a wall display. */\nexport const ZOOM_MIN_DRAG = 0.02;\n\nexport type ZoomAxes = \"both\" | \"x\" | \"y\";\n\n/** Which axes a drag zooms — or `null` for a drag too small to mean one, or a plot\n * that has not been laid out yet. Direction does not matter; proportion does. */\nexport function zoomAxesFor(\n dx: number,\n dy: number,\n width: number,\n height: number,\n threshold: number = ZOOM_SQUARE_ENOUGH,\n): ZoomAxes | null {\n if (!(width > 0) || !(height > 0)) return null;\n const across = Math.abs(dx) / width;\n const down = Math.abs(dy) / height;\n if (across < ZOOM_MIN_DRAG && down < ZOOM_MIN_DRAG) return null;\n const longer = Math.max(across, down);\n const shorter = Math.min(across, down);\n if (shorter / longer >= threshold) return \"both\";\n return across > down ? \"x\" : \"y\";\n}\n\n/** What a drag chose, in DATA coordinates, and which axes it actually named — the\n * caller needs the second to know which of the two it may derive from the other. */\nexport interface ZoomSelection {\n which: ZoomAxes;\n x?: [number, number];\n /** One window per y axis, keyed by axis id: they are in different units. */\n y: Record<string, [number, number]>;\n}\n\n/** A drag, in the chart's own pixel coordinates. */\nexport interface ZoomDrag {\n fromX: number;\n fromY: number;\n toX: number;\n toY: number;\n}\n\n/** A pair as a domain, smallest first. Recharts wants an ascending domain whichever\n * way the drag went, and a y axis counts downward in pixels while its values count\n * upward — so both ends are read and sorted rather than one assumed to be low. */\nfunction ordered(a: number, b: number): [number, number] {\n return a <= b ? [a, b] : [b, a];\n}\n\n/**\n * A finished drag as a selection, or `null` when it selects nothing.\n *\n * `xInverse` and `yInverse(axisId)` are recharts' pixel→value functions; an axis whose\n * function is missing, or whose ends come back non-finite, is left out rather than\n * handed a NaN window.\n */\nexport function selectionFromDrag(\n drag: ZoomDrag,\n plot: { width: number; height: number },\n xInverse: InverseScaleFunction | undefined,\n yInverse: (axisId: string) => InverseScaleFunction | undefined,\n axisIds: readonly string[],\n): ZoomSelection | null {\n const which = zoomAxesFor(drag.toX - drag.fromX, drag.toY - drag.fromY, plot.width, plot.height);\n if (!which) return null;\n const next: ZoomSelection = { which, y: {} };\n if (which !== \"y\" && xInverse) {\n const from = Number(xInverse(drag.fromX));\n const to = Number(xInverse(drag.toX));\n if (Number.isFinite(from) && Number.isFinite(to)) next.x = ordered(from, to);\n }\n if (which !== \"x\") {\n for (const id of axisIds) {\n const inverse = yInverse(id);\n if (!inverse) continue;\n const from = Number(inverse(drag.fromY));\n const to = Number(inverse(drag.toY));\n if (Number.isFinite(from) && Number.isFinite(to)) next.y[id] = ordered(from, to);\n }\n }\n return next.x || Object.keys(next.y).length ? next : null;\n}\n\n/** What a band refit needs to know about the chart — a subset of `SeriesChart`'s own\n * props, so a chart hands its data straight over. */\nexport interface ZoomFitSource {\n rows: Record<string, number>[];\n series: { key: string; axis?: string }[];\n axes: { id: string }[];\n xKey: string;\n}\n\n/** How much of a refitted span to leave as air, top and bottom. Without it the\n * highest sample sits exactly on the frame, where a line is half drawn and reads as\n * clipped rather than as the maximum. */\nconst FIT_AIR = 0.05;\n\n/** A span with air around it — and a real span for a flat channel, which would\n * otherwise be a zero-height window recharts cannot scale. */\nfunction airy(low: number, high: number): [number, number] {\n const span = high - low;\n const air = span > 0 ? span * FIT_AIR : Math.max(Math.abs(high), 1) * FIT_AIR;\n return [low - air, high + air];\n}\n\n/** A running low/high pair. A pair rather than an array to take `Math.min(...)` of:\n * the spread passes one argument per sample, and V8 faults past ~125,000 of them. */\ntype Span = [number, number] | undefined;\n\n/** A pair widened by one value. A non-finite one is not a value — one NaN would\n * otherwise take the whole window with it. */\nfunction widen(span: Span, value: number): Span {\n if (!Number.isFinite(value)) return span;\n if (!span) return [value, value];\n if (value < span[0]) span[0] = value;\n else if (value > span[1]) span[1] = value;\n return span;\n}\n\n/** Which series are measured on an axis. */\nfunction keysOn(source: ZoomFitSource, axisId: string): string[] {\n return source.series\n .filter((entry) => (entry.axis ?? DEFAULT_Y_AXIS) === axisId)\n .map((entry) => entry.key);\n}\n\n/**\n * Each y axis fitted to the samples inside an x window — what a band across x leaves\n * the axes nobody dragged showing. Zoom into a tenth of a sweep and a y axis still\n * scaled to the whole run draws that tenth as a flat line, which is the one thing the\n * zoom was for; so the unnamed axes are refitted, which is what \"zoom\" means everywhere\n * else.\n *\n * An axis with nothing inside the window is left OUT rather than collapsed: a window\n * between two samples is one where the chart should keep drawing the line across.\n */\nexport function fitYToX(\n source: ZoomFitSource,\n window: readonly [number, number],\n): Record<string, [number, number]> {\n const [from, to] = window;\n const fitted: Record<string, [number, number]> = {};\n for (const axis of source.axes) {\n const keys = keysOn(source, axis.id);\n let span: Span;\n for (const row of source.rows) {\n const at = row[source.xKey];\n if (!(at >= from && at <= to)) continue;\n for (const key of keys) span = widen(span, row[key]);\n }\n if (span) fitted[axis.id] = airy(span[0], span[1]);\n }\n return fitted;\n}\n\n/**\n * The x window that holds every sample inside a set of y windows — the other direction\n * of the same idea. A channel crossing its band once gives a tight window; one that\n * oscillates through it gives back most of the sweep, which is the truthful answer.\n * `undefined` when no sample qualifies, rather than a guess.\n */\nexport function fitXToY(\n source: ZoomFitSource,\n windows: Readonly<Record<string, readonly [number, number]>>,\n): [number, number] | undefined {\n let hits: Span;\n for (const [axisId, [low, high]] of Object.entries(windows)) {\n for (const key of keysOn(source, axisId)) {\n for (const row of source.rows) {\n const value = row[key];\n if (Number.isFinite(value) && value >= low && value <= high) {\n hits = widen(hits, row[source.xKey]);\n }\n }\n }\n }\n return hits ? airy(hits[0], hits[1]) : undefined;\n}\n\n/**\n * What a chart remembers about its zoom: the x window, and the y windows the reader\n * DRAGGED. `y: null` means \"derive y from x\" — the split is the whole design: what was\n * dragged is kept, and what follows from it is recomputed on every render, so a band\n * across x refits y here and in every chart sharing the window.\n */\nexport interface ZoomState {\n x?: [number, number];\n y: Record<string, [number, number]> | null;\n}\n\nexport const NO_ZOOM: ZoomState = { x: undefined, y: null };\n\n/**\n * The state after a selection. Two band drags COMPOSE rather than the second undoing\n * the first: a band down y keeps the x window when the band holds no samples, and\n * otherwise pulls x in to where they are.\n */\nexport function zoomAfter(\n state: ZoomState,\n selection: ZoomSelection,\n source: ZoomFitSource,\n): ZoomState {\n if (selection.which === \"both\") return { x: selection.x, y: selection.y };\n // Leave y derived, so it fits itself to what is inside the new window.\n if (selection.which === \"x\") return { x: selection.x, y: null };\n return { x: fitXToY(source, selection.y) ?? state.x, y: selection.y };\n}\n\n/** The domains a chart draws for a state: the x window as is, and each y axis either\n * as dragged or fitted to the x window. `{}` means \"fit your own data\". */\nexport function zoomDomains(\n state: ZoomState,\n source: ZoomFitSource,\n): { xDomain?: [number, number]; yDomains: Record<string, [number, number]> } {\n return {\n xDomain: state.x,\n yDomains: state.y ?? (state.x ? fitYToX(source, state.x) : {}),\n };\n}\n\n/* ── The components ─────────────────────────────────────────────────────── */\n\n/** What the wrapped chart is handed. It applies the domains to its own axes and renders\n * `layer` INSIDE the recharts chart, where recharts' hooks answer. */\nexport interface ZoomBinding {\n xDomain?: [number, number];\n yDomains: Record<string, [number, number]>;\n layer: ReactNode;\n}\n\n/** The props `withChartZoom` reads off the chart it wraps. */\nexport interface ZoomTarget {\n /** Default: the single axis {@link DEFAULT_Y_AXIS}. */\n axes?: readonly { id: string; hide?: boolean }[];\n /** The chart's own data, so a band can refit the axis it did not name. Optional: a\n * chart with nothing plotted has nothing to fit to. */\n rows?: Record<string, number>[];\n series?: readonly { key: string; axis?: string }[];\n x?: { key?: string };\n zoom?: ZoomBinding;\n labels?: Partial<SeriesChartLabels>;\n}\n\nconst SharedX = createContext<{\n window?: [number, number];\n set: Dispatch<SetStateAction<[number, number] | undefined>>;\n} | null>(null);\n\n/**\n * One x window for several charts.\n *\n * A column of charts over one abscissa is one picture, and zooming one of them and not\n * the rest turns it into several pictures of different things. Every zoomable chart\n * inside this shares the x window: any drag in any of them moves all of them.\n *\n * **Only x.** The y axes stay each chart's own — they are in different units — and\n * each refits its own y to what is inside the shared window.\n */\nexport function SharedXZoom({ children }: { children: ReactNode }) {\n const [window, set] = useState<[number, number] | undefined>(undefined);\n return <SharedX.Provider value={{ window, set }}>{children}</SharedX.Provider>;\n}\n\n/**\n * Wrap a chart in drag-to-zoom.\n *\n * The chart receives a `zoom` binding: it applies `xDomain`/`yDomains` to its axes\n * (with `allowDataOverflow`, so recharts clips to the window instead of widening it\n * back out) and renders `layer` as a child of its recharts chart. A reset button\n * appears over the chart's top end corner while it is zoomed — a real button, so the\n * keyboard can undo what the pointer did.\n */\nexport function withChartZoom<P extends ZoomTarget>(Chart: ComponentType<P>) {\n function Zoomable(props: P) {\n const labels = useKitLabels(\"seriesChart\", DEFAULT_SERIES_CHART_LABELS, props.labels);\n const group = useContext(SharedX);\n const [ownX, setOwnX] = useState<[number, number] | undefined>(undefined);\n const [ownY, setOwnY] = useState<Record<string, [number, number]> | null>(null);\n const xWindow = group ? group.window : ownX;\n const setXWindow = group ? group.set : setOwnX;\n const axes = props.axes ?? [{ id: DEFAULT_Y_AXIS }];\n\n // Every axis, hidden ones included: a hidden axis still scales its lines, and a\n // line whose axis did not refit is drawn as a flat streak inside the window.\n const source: ZoomFitSource = {\n rows: props.rows ?? [],\n series: [...(props.series ?? [])],\n axes: [...axes],\n xKey: props.x?.key ?? \"x\",\n };\n const state: ZoomState = { x: xWindow, y: ownY };\n const zoomed = xWindow !== undefined || ownY !== null;\n\n const clear = () => {\n setXWindow(undefined);\n setOwnY(null);\n };\n\n const binding: ZoomBinding = {\n ...zoomDomains(state, source),\n layer: (\n <ZoomLayer\n axisIds={axes.map((axis) => axis.id)}\n label={labels.zoomHint}\n onZoom={(selection) => {\n const next = zoomAfter(state, selection, source);\n if (next.x !== xWindow) setXWindow(next.x);\n setOwnY(next.y);\n }}\n onReset={clear}\n />\n ),\n };\n\n return (\n <div className=\"relative\">\n <Chart {...props} zoom={binding} />\n {zoomed && (\n <button\n type=\"button\"\n onClick={clear}\n 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)]\"\n >\n <X aria-hidden className=\"size-3\" /> {labels.resetZoom}\n </button>\n )}\n </div>\n );\n }\n Zoomable.displayName = `withChartZoom(${Chart.displayName ?? Chart.name ?? \"Chart\"})`;\n return Zoomable;\n}\n\n/**\n * The part that lives INSIDE the recharts chart, where recharts will say where the plot\n * is and what value a pixel is.\n *\n * A transparent rectangle over the plot area that reads the drag. It does NOT stop the\n * event: recharts drives its tooltip from the same pointer move bubbling up to its own\n * wrapper, so swallowing it here would trade the tooltip for the zoom.\n */\nfunction ZoomLayer({\n axisIds,\n label,\n onZoom,\n onReset,\n}: {\n axisIds: string[];\n label: string;\n onZoom: (selection: ZoomSelection) => void;\n onReset: () => void;\n}) {\n const plot = usePlotArea();\n const xInverse = useXAxisInverseScale();\n // One probe COMPONENT per axis rather than a hook in a loop: the number of axes\n // changes as series are toggled, and a hook count that changes with the data is the\n // one thing React will not forgive.\n const [scales] = useState(() => new Map<string, InverseScaleFunction | undefined>());\n const [drag, setDrag] = useState<ZoomDrag | null>(null);\n\n if (!plot || plot.width <= 0 || plot.height <= 0) return null;\n\n const at = (event: { clientX: number; clientY: number; currentTarget: Element }) => {\n // Against the overlay's own screen box, so padding around the container cannot\n // shift every reading — plus the plot's offset, because the scales are in the\n // chart's coordinates rather than the plot's.\n const box = event.currentTarget.getBoundingClientRect();\n return { x: event.clientX - box.left + plot.x, y: event.clientY - box.top + plot.y };\n };\n\n const finish = () => {\n if (!drag) return;\n setDrag(null);\n const selection = selectionFromDrag(drag, plot, xInverse, (id) => scales.get(id), axisIds);\n if (selection) onZoom(selection);\n };\n\n // What the drag would do, shown while it is being made: a band paints the full width\n // or height, so the reader sees it is about to let the other axis refit.\n const which = drag\n ? zoomAxesFor(drag.toX - drag.fromX, drag.toY - drag.fromY, plot.width, plot.height)\n : null;\n\n return (\n <g>\n {axisIds.map((id) => (\n <AxisProbe key={id} axisId={id} scales={scales} />\n ))}\n {drag && which && (\n <rect\n x={which === \"y\" ? plot.x : Math.min(drag.fromX, drag.toX)}\n y={which === \"x\" ? plot.y : Math.min(drag.fromY, drag.toY)}\n width={which === \"y\" ? plot.width : Math.abs(drag.toX - drag.fromX)}\n height={which === \"x\" ? plot.height : Math.abs(drag.toY - drag.fromY)}\n className=\"fill-[var(--brand)]/15 stroke-[var(--brand)]\"\n strokeDasharray=\"4 3\"\n pointerEvents=\"none\"\n />\n )}\n <rect\n x={plot.x}\n y={plot.y}\n width={plot.width}\n height={plot.height}\n // `transparent`, not `none`: an unpainted shape takes no pointer events.\n fill=\"transparent\"\n role=\"application\"\n aria-label={label}\n style={{ cursor: \"crosshair\" }}\n onPointerDown={(event) => {\n if (event.button !== 0) return;\n const point = at(event);\n // Best effort: capture keeps a drag alive past the plot's edge, but a\n // pointer id the environment does not know throws — jsdom refuses them all —\n // and a zoom that threw on pointer-down would be a chart nobody could click.\n try {\n event.currentTarget.setPointerCapture(event.pointerId);\n } catch {\n /* the drag still works; it just stops at the edge of the plot */\n }\n setDrag({ fromX: point.x, fromY: point.y, toX: point.x, toY: point.y });\n }}\n onPointerMove={(event) => {\n if (!drag) return;\n const point = at(event);\n setDrag({ ...drag, toX: point.x, toY: point.y });\n }}\n onPointerUp={finish}\n onPointerCancel={() => setDrag(null)}\n onDoubleClick={onReset}\n />\n </g>\n );\n}\n\n/** One axis's pixel→value function, parked where the pointer handler can reach it. */\nfunction AxisProbe({\n axisId,\n scales,\n}: {\n axisId: string;\n scales: Map<string, InverseScaleFunction | undefined>;\n}) {\n const inverse = useYAxisInverseScale(axisId);\n useEffect(() => {\n scales.set(axisId, inverse);\n return () => {\n scales.delete(axisId);\n };\n }, [axisId, inverse, scales]);\n return null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -30,7 +30,20 @@ interface ChartTooltipContentProps {
|
|
|
30
30
|
indicator?: "dot" | "line";
|
|
31
31
|
hideLabel?: boolean;
|
|
32
32
|
labelFormatter?: (label: string | number) => ReactNode;
|
|
33
|
-
|
|
33
|
+
/** Formats one series' value for display. NOT called when the series has no value at
|
|
34
|
+
* this point — {@link ChartTooltipContentProps.formatValue} is the one that is. */
|
|
35
|
+
valueFormatter?: (value: number) => ReactNode;
|
|
36
|
+
/** Formats one series' value INCLUDING its absence: handed `undefined` for a gap, a
|
|
37
|
+
* projection that has not happened yet, or a field that did not exist at an earlier
|
|
38
|
+
* capture, and whatever it returns is rendered as-is. Only the caller knows whether
|
|
39
|
+
* that absence reads as a dash, a "not yet" or a genuine zero, so this is how it
|
|
40
|
+
* says. Supplying it wins over `valueFormatter` for every value.
|
|
41
|
+
*
|
|
42
|
+
* It sits NEXT to `valueFormatter` rather than widening it because that prop's
|
|
43
|
+
* parameter is contextually typed at every call site there is — `(v) => money(v)` —
|
|
44
|
+
* so widening it to `number | undefined` is a compile error in all three consuming
|
|
45
|
+
* apps, which is a breaking change this module does not get to make by itself. */
|
|
46
|
+
formatValue?: (value: number | undefined) => ReactNode;
|
|
34
47
|
/** Cursor position within the chart, injected by recharts. Used with
|
|
35
48
|
* {@link boundaryRef} to decide whether to flip the tooltip. */
|
|
36
49
|
coordinate?: {
|
|
@@ -42,7 +55,7 @@ interface ChartTooltipContentProps {
|
|
|
42
55
|
* — e.g. a wide chart whose rightmost bars sat off-screen (feedback #77). */
|
|
43
56
|
boundaryRef?: RefObject<HTMLElement | null>;
|
|
44
57
|
}
|
|
45
|
-
declare function ChartTooltipContent({ active, payload, label, indicator, hideLabel, labelFormatter, valueFormatter, coordinate, boundaryRef, }: ChartTooltipContentProps): react.JSX.Element | null;
|
|
58
|
+
declare function ChartTooltipContent({ active, payload, label, indicator, hideLabel, labelFormatter, valueFormatter, formatValue, coordinate, boundaryRef, }: ChartTooltipContentProps): react.JSX.Element | null;
|
|
46
59
|
interface LegendPayloadItem {
|
|
47
60
|
value?: string;
|
|
48
61
|
dataKey?: string | number;
|
|
@@ -51,8 +64,20 @@ interface LegendPayloadItem {
|
|
|
51
64
|
interface ChartLegendContentProps {
|
|
52
65
|
payload?: LegendPayloadItem[];
|
|
53
66
|
onItemClick?: (key: string) => void;
|
|
67
|
+
/**
|
|
68
|
+
* TOGGLE mode (recommended): the series currently switched off. Each entry is a
|
|
69
|
+
* toggle button — pressed while its series is shown — and a hidden one is drawn
|
|
70
|
+
* faded with its label struck through. Several can be off at once; pair with
|
|
71
|
+
* `toggleHidden(hidden, key)` in `onItemClick` and `hide` on the chart's series.
|
|
72
|
+
*/
|
|
73
|
+
hiddenKeys?: ReadonlySet<string> | readonly string[];
|
|
74
|
+
/**
|
|
75
|
+
* ISOLATE mode: the one series shown in full, every other entry dimmed. Kept for
|
|
76
|
+
* the charts built on it; a legend where each entry switches its own series is
|
|
77
|
+
* what readers expect from a legend, so prefer `hiddenKeys`.
|
|
78
|
+
*/
|
|
54
79
|
activeKey?: string | null;
|
|
55
80
|
}
|
|
56
|
-
declare function ChartLegendContent({ payload, onItemClick, activeKey }: ChartLegendContentProps): react.JSX.Element | null;
|
|
81
|
+
declare function ChartLegendContent({ payload, onItemClick, hiddenKeys, activeKey, }: ChartLegendContentProps): react.JSX.Element | null;
|
|
57
82
|
|
|
58
83
|
export { type ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, type ChartSeriesConfig, ChartTooltip, ChartTooltipContent, useChart };
|
package/dist/components/chart.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
3
|
import { createContext, useContext, useId, useRef } from "react";
|
|
3
4
|
import {
|
|
@@ -14,7 +15,7 @@ function useChart() {
|
|
|
14
15
|
}
|
|
15
16
|
function ChartContainer({ id, className, children, config, ...props }) {
|
|
16
17
|
const uid = useId();
|
|
17
|
-
const chartId = `chart-${(id ?? uid).replace(
|
|
18
|
+
const chartId = `chart-${(id ?? uid).replace(/[^A-Za-z0-9_-]/g, "")}`;
|
|
18
19
|
return /* @__PURE__ */ jsx(ChartContext.Provider, { value: config, children: /* @__PURE__ */ jsxs(
|
|
19
20
|
"div",
|
|
20
21
|
{
|
|
@@ -24,12 +25,13 @@ function ChartContainer({ id, className, children, config, ...props }) {
|
|
|
24
25
|
// a horizontal drag is handed to recharts for tooltip scrubbing,
|
|
25
26
|
// instead of the page scrolling underneath the gesture (feedback #292).
|
|
26
27
|
"flex w-full touch-pan-y justify-center text-xs",
|
|
27
|
-
// muted axis labels + gridlines,
|
|
28
|
-
|
|
29
|
-
"[&_.recharts-cartesian-
|
|
28
|
+
// muted axis labels + gridlines; one class each, since the tokens
|
|
29
|
+
// already carry the light/dark values
|
|
30
|
+
"[&_.recharts-cartesian-axis-tick_text]:fill-[var(--text-muted)]",
|
|
31
|
+
"[&_.recharts-cartesian-grid_line]:stroke-[var(--border)]/70",
|
|
30
32
|
// hairline hover cursor instead of the heavy default
|
|
31
|
-
"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-
|
|
32
|
-
"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-
|
|
33
|
+
"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-[var(--border-strong)]",
|
|
34
|
+
"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-[var(--bg-hover)]",
|
|
33
35
|
// kill the focus outlines & white sector strokes Recharts adds
|
|
34
36
|
"[&_.recharts-sector]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent",
|
|
35
37
|
"[&_.recharts-layer]:outline-none [&_.recharts-surface]:outline-none [&_.recharts-dot[stroke='#fff']]:stroke-transparent",
|
|
@@ -43,8 +45,21 @@ function ChartContainer({ id, className, children, config, ...props }) {
|
|
|
43
45
|
}
|
|
44
46
|
) });
|
|
45
47
|
}
|
|
48
|
+
const CSS_IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
49
|
+
const CSS_COLOR = /^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|(?:rgba?|hsla?)\([0-9A-Za-z.,%/ -]*\)|var\(--[A-Za-z0-9_-]+\))$/;
|
|
46
50
|
function ChartStyle({ id, config }) {
|
|
47
|
-
const entries = Object.entries(config).filter(([, c]) =>
|
|
51
|
+
const entries = Object.entries(config).filter(([key, c]) => {
|
|
52
|
+
if (!c.color) return false;
|
|
53
|
+
if (!CSS_IDENT.test(key)) {
|
|
54
|
+
console.warn(`[ui-kit] chart config key is not a CSS identifier, skipped: ${key}`);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (!CSS_COLOR.test(c.color)) {
|
|
58
|
+
console.warn(`[ui-kit] chart config colour for "${key}" is not a colour, skipped: ${c.color}`);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
});
|
|
48
63
|
if (!entries.length) return null;
|
|
49
64
|
const css = `[data-chart=${id}] {
|
|
50
65
|
${entries.map(([key, c]) => ` --color-${key}: ${c.color};`).join("\n")}
|
|
@@ -53,6 +68,12 @@ ${entries.map(([key, c]) => ` --color-${key}: ${c.color};`).join("\n")}
|
|
|
53
68
|
}
|
|
54
69
|
const ChartTooltip = RechartsTooltip;
|
|
55
70
|
const ChartLegend = RechartsLegend;
|
|
71
|
+
const NO_VALUE = "\u2014";
|
|
72
|
+
function tooltipValue(value) {
|
|
73
|
+
if (value == null || value === "") return void 0;
|
|
74
|
+
const n = typeof value === "string" ? Number(value) : value;
|
|
75
|
+
return Number.isFinite(n) ? n : void 0;
|
|
76
|
+
}
|
|
56
77
|
function ChartTooltipContent({
|
|
57
78
|
active,
|
|
58
79
|
payload,
|
|
@@ -61,6 +82,7 @@ function ChartTooltipContent({
|
|
|
61
82
|
hideLabel = false,
|
|
62
83
|
labelFormatter,
|
|
63
84
|
valueFormatter,
|
|
85
|
+
formatValue,
|
|
64
86
|
coordinate,
|
|
65
87
|
boundaryRef
|
|
66
88
|
}) {
|
|
@@ -79,18 +101,22 @@ function ChartTooltipContent({
|
|
|
79
101
|
{
|
|
80
102
|
ref: tipRef,
|
|
81
103
|
style: boundaryRef ? { transform: flip ? "translateX(calc(-100% - 12px))" : "translateX(12px)" } : void 0,
|
|
82
|
-
className: "min-w-[9rem] rounded-lg border border-
|
|
104
|
+
className: "min-w-[9rem] rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] px-2.5 py-2 text-xs shadow-xl",
|
|
83
105
|
children: [
|
|
84
|
-
!hideLabel && label != null && /* @__PURE__ */ jsx("div", { className: "mb-1.5 font-medium text-
|
|
106
|
+
!hideLabel && label != null && /* @__PURE__ */ jsx("div", { className: "mb-1.5 font-medium text-[var(--text-primary)]", children: labelFormatter ? labelFormatter(label) : label }),
|
|
85
107
|
/* @__PURE__ */ jsx("div", { className: "grid gap-1.5", children: payload.map((item, i) => {
|
|
86
108
|
const dk = item.dataKey != null ? String(item.dataKey) : void 0;
|
|
87
109
|
const nm = item.name != null ? String(item.name) : void 0;
|
|
88
110
|
const cfgKey = dk && config[dk] ? dk : nm && config[nm] ? nm : dk ?? nm ?? String(i);
|
|
89
111
|
const series = config[cfgKey];
|
|
90
112
|
const fill = typeof item.payload?.fill === "string" ? item.payload.fill : void 0;
|
|
91
|
-
const color = series?.color ?? item.color ??
|
|
113
|
+
const color = series?.color ?? fill ?? item.color ?? "#64748b";
|
|
92
114
|
const name = series?.label ?? nm ?? cfgKey;
|
|
93
|
-
const raw =
|
|
115
|
+
const raw = tooltipValue(item.value);
|
|
116
|
+
let shown;
|
|
117
|
+
if (formatValue) shown = formatValue(raw);
|
|
118
|
+
else if (raw == null) shown = NO_VALUE;
|
|
119
|
+
else shown = valueFormatter ? valueFormatter(raw) : raw;
|
|
94
120
|
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
95
121
|
/* @__PURE__ */ jsx(
|
|
96
122
|
"span",
|
|
@@ -102,15 +128,21 @@ function ChartTooltipContent({
|
|
|
102
128
|
style: { backgroundColor: color }
|
|
103
129
|
}
|
|
104
130
|
),
|
|
105
|
-
/* @__PURE__ */ jsx("span", { className: "text-
|
|
106
|
-
/* @__PURE__ */ jsx("span", { "data-private": true, className: "ml-auto font-mono font-medium tabular-nums text-
|
|
131
|
+
/* @__PURE__ */ jsx("span", { className: "text-[var(--text-muted)]", children: name }),
|
|
132
|
+
/* @__PURE__ */ jsx("span", { "data-private": true, className: "ml-auto font-mono font-medium tabular-nums text-[var(--text-primary)]", children: shown })
|
|
107
133
|
] }, `${cfgKey}-${i}`);
|
|
108
134
|
}) })
|
|
109
135
|
]
|
|
110
136
|
}
|
|
111
137
|
);
|
|
112
138
|
}
|
|
113
|
-
function ChartLegendContent({
|
|
139
|
+
function ChartLegendContent({
|
|
140
|
+
payload,
|
|
141
|
+
onItemClick,
|
|
142
|
+
hiddenKeys,
|
|
143
|
+
activeKey
|
|
144
|
+
}) {
|
|
145
|
+
const hidden = hiddenKeys ? new Set(hiddenKeys) : null;
|
|
114
146
|
const config = useChart();
|
|
115
147
|
if (!payload?.length) return null;
|
|
116
148
|
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-3", children: payload.map((item, i) => {
|
|
@@ -120,31 +152,38 @@ function ChartLegendContent({ payload, onItemClick, activeKey }) {
|
|
|
120
152
|
const series = config[cfgKey];
|
|
121
153
|
const color = series?.color ?? item.color ?? "#64748b";
|
|
122
154
|
const label = series?.label ?? vv ?? cfgKey;
|
|
123
|
-
const
|
|
124
|
-
|
|
155
|
+
const off = hidden?.has(cfgKey) ?? false;
|
|
156
|
+
const dimmed = off || activeKey != null && activeKey !== cfgKey;
|
|
157
|
+
const className = cn(
|
|
158
|
+
"flex items-center gap-1.5 text-[var(--text-secondary)] transition-opacity",
|
|
159
|
+
onItemClick ? "cursor-pointer hover:opacity-80" : "cursor-default",
|
|
160
|
+
dimmed && "opacity-40"
|
|
161
|
+
);
|
|
162
|
+
const swatch = /* @__PURE__ */ jsx(
|
|
163
|
+
"span",
|
|
164
|
+
{
|
|
165
|
+
"aria-hidden": true,
|
|
166
|
+
className: "h-2.5 w-2.5 shrink-0 rounded-[3px]",
|
|
167
|
+
style: { backgroundColor: color }
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
return onItemClick ? /* @__PURE__ */ jsxs(
|
|
125
171
|
"button",
|
|
126
172
|
{
|
|
127
173
|
type: "button",
|
|
128
|
-
onClick:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
onItemClick && "cursor-pointer hover:opacity-80",
|
|
132
|
-
!onItemClick && "cursor-default",
|
|
133
|
-
dimmed && "opacity-40"
|
|
134
|
-
),
|
|
174
|
+
onClick: () => onItemClick(cfgKey),
|
|
175
|
+
"aria-pressed": hidden ? !off : activeKey === void 0 ? void 0 : activeKey === cfgKey,
|
|
176
|
+
className,
|
|
135
177
|
children: [
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
{
|
|
139
|
-
className: "h-2.5 w-2.5 shrink-0 rounded-[3px]",
|
|
140
|
-
style: { backgroundColor: color }
|
|
141
|
-
}
|
|
142
|
-
),
|
|
143
|
-
label
|
|
178
|
+
swatch,
|
|
179
|
+
/* @__PURE__ */ jsx("span", { className: cn(off && "line-through"), children: label })
|
|
144
180
|
]
|
|
145
181
|
},
|
|
146
182
|
`${cfgKey}-${i}`
|
|
147
|
-
)
|
|
183
|
+
) : /* @__PURE__ */ jsxs("span", { className, children: [
|
|
184
|
+
swatch,
|
|
185
|
+
label
|
|
186
|
+
] }, `${cfgKey}-${i}`);
|
|
148
187
|
}) });
|
|
149
188
|
}
|
|
150
189
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/chart.tsx"],"sourcesContent":["// shadcn/ui chart kit — vendored (MIT) and adapted for this app:\n// - Recharts 3 instead of 2\n// - the original keys its styling off shadcn theme tokens (--muted-foreground,\n// --border, bg-background …). This app has no shadcn token layer, so those\n// are mapped to the existing slate palette + class-based `dark:` variants.\n// The value on show is the *structure*: config-driven colors injected as CSS\n// vars, muted axis/grid styling, and a polished tooltip/legend — the things\n// that make shadcn charts read as \"designed\" rather than \"default Recharts\".\nimport { createContext, useContext, useId, useRef } from \"react\";\nimport type { ComponentProps, CSSProperties, ReactNode, RefObject } from \"react\";\nimport {\n Legend as RechartsLegend,\n ResponsiveContainer,\n Tooltip as RechartsTooltip,\n} from \"recharts\";\nimport { cn } from \"../lib/cn\";\n\nexport interface ChartSeriesConfig {\n label: ReactNode;\n color: string;\n}\n\nexport type ChartConfig = Record<string, ChartSeriesConfig>;\n\nconst ChartContext = createContext<ChartConfig | null>(null);\n\nexport function useChart(): ChartConfig {\n const ctx = useContext(ChartContext);\n if (!ctx) throw new Error(\"useChart must be used within a <ChartContainer />\");\n return ctx;\n}\n\ninterface ChartContainerProps extends ComponentProps<\"div\"> {\n config: ChartConfig;\n children: ComponentProps<typeof ResponsiveContainer>[\"children\"];\n}\n\nexport function ChartContainer({ id, className, children, config, ...props }: ChartContainerProps) {\n const uid = useId();\n const chartId = `chart-${(id ?? uid).replace(/:/g, \"\")}`;\n return (\n <ChartContext.Provider value={config}>\n <div\n data-chart={chartId}\n className={cn(\n // touch-pan-y lets a vertical finger drag still scroll the page while\n // a horizontal drag is handed to recharts for tooltip scrubbing,\n // instead of the page scrolling underneath the gesture (feedback #292).\n \"flex w-full touch-pan-y justify-center text-xs\",\n // muted axis labels + gridlines, light and dark\n \"[&_.recharts-cartesian-axis-tick_text]:fill-slate-500 dark:[&_.recharts-cartesian-axis-tick_text]:fill-slate-400\",\n \"[&_.recharts-cartesian-grid_line]:stroke-slate-200/70 dark:[&_.recharts-cartesian-grid_line]:stroke-slate-700/60\",\n // hairline hover cursor instead of the heavy default\n \"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-slate-300 dark:[&_.recharts-curve.recharts-tooltip-cursor]:stroke-slate-600\",\n \"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-slate-100 dark:[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-slate-800\",\n // kill the focus outlines & white sector strokes Recharts adds\n \"[&_.recharts-sector]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent\",\n \"[&_.recharts-layer]:outline-none [&_.recharts-surface]:outline-none [&_.recharts-dot[stroke='#fff']]:stroke-transparent\",\n className,\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={config} />\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n {children}\n </ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n );\n}\n\n// Inject each series colour as a `--color-<key>` custom property scoped to this\n// chart, so chart JSX can refer to `var(--color-total)` and the legend/tooltip\n// stay in sync from one source of truth.\nfunction ChartStyle({ id, config }: { id: string; config: ChartConfig }) {\n const entries = Object.entries(config).filter(([, c]) => c.color);\n if (!entries.length) return null;\n const css = `[data-chart=${id}] {\\n${entries\n .map(([key, c]) => ` --color-${key}: ${c.color};`)\n .join(\"\\n\")}\\n}`;\n return <style dangerouslySetInnerHTML={{ __html: css }} />;\n}\n\nexport const ChartTooltip = RechartsTooltip;\nexport const ChartLegend = RechartsLegend;\n\ninterface TooltipPayloadItem {\n dataKey?: string | number;\n name?: string | number;\n value?: number | string;\n color?: string;\n payload?: Record<string, unknown>;\n}\n\ninterface ChartTooltipContentProps {\n active?: boolean;\n payload?: TooltipPayloadItem[];\n label?: string | number;\n indicator?: \"dot\" | \"line\";\n hideLabel?: boolean;\n labelFormatter?: (label: string | number) => ReactNode;\n valueFormatter?: (value: number) => string;\n /** Cursor position within the chart, injected by recharts. Used with\n * {@link boundaryRef} to decide whether to flip the tooltip. */\n coordinate?: { x?: number; y?: number };\n /** When set, the tooltip flips to the LEFT of the cursor if it would otherwise\n * spill past the right edge of this (usually horizontally-scrolling) container\n * — e.g. a wide chart whose rightmost bars sat off-screen (feedback #77). */\n boundaryRef?: RefObject<HTMLElement | null>;\n}\n\nexport function ChartTooltipContent({\n active,\n payload,\n label,\n indicator = \"dot\",\n hideLabel = false,\n labelFormatter,\n valueFormatter,\n coordinate,\n boundaryRef,\n}: ChartTooltipContentProps) {\n const config = useChart();\n const tipRef = useRef<HTMLDivElement>(null);\n if (!active || !payload?.length) return null;\n // Recharts anchors the tooltip at coordinate.x inside the full (scrolled) chart\n // width; subtract the container's scrollLeft to get its on-screen x, then flip\n // left if the tooltip's own width would run past the visible right edge.\n let flip = false;\n const boundary = boundaryRef?.current;\n if (boundary && coordinate?.x != null) {\n const visibleX = coordinate.x - boundary.scrollLeft;\n const tipWidth = tipRef.current?.offsetWidth ?? 160;\n flip = visibleX + tipWidth + 12 > boundary.clientWidth;\n }\n return (\n <div\n ref={tipRef}\n style={boundaryRef ? { transform: flip ? \"translateX(calc(-100% - 12px))\" : \"translateX(12px)\" } : undefined}\n className=\"min-w-[9rem] rounded-lg border border-slate-200 bg-white px-2.5 py-2 text-xs shadow-xl dark:border-slate-700 dark:bg-slate-900\"\n >\n {!hideLabel && label != null && (\n <div className=\"mb-1.5 font-medium text-slate-900 dark:text-slate-100\">\n {labelFormatter ? labelFormatter(label) : label}\n </div>\n )}\n <div className=\"grid gap-1.5\">\n {payload.map((item, i) => {\n // Bars/lines/areas identify a series by dataKey; pies/treemaps key off\n // `name` (the nameKey value). Prefer whichever the config knows.\n const dk = item.dataKey != null ? String(item.dataKey) : undefined;\n const nm = item.name != null ? String(item.name) : undefined;\n const cfgKey = dk && config[dk] ? dk : nm && config[nm] ? nm : (dk ?? nm ?? String(i));\n const series = config[cfgKey];\n const fill = typeof item.payload?.fill === \"string\" ? item.payload.fill : undefined;\n const color = series?.color ?? item.color ?? fill ?? \"#64748b\";\n const name = series?.label ?? nm ?? cfgKey;\n const raw = typeof item.value === \"string\" ? Number(item.value) : (item.value ?? 0);\n return (\n <div key={`${cfgKey}-${i}`} className=\"flex items-center gap-2\">\n <span\n className={cn(\n \"shrink-0 rounded-[2px]\",\n indicator === \"dot\" ? \"h-2.5 w-2.5 rounded-full\" : \"h-2.5 w-1\",\n )}\n style={{ backgroundColor: color }}\n />\n <span className=\"text-slate-500 dark:text-slate-400\">{name}</span>\n <span data-private className=\"ml-auto font-mono font-medium tabular-nums text-slate-900 dark:text-slate-100\">\n {valueFormatter ? valueFormatter(raw) : raw}\n </span>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n\ninterface LegendPayloadItem {\n value?: string;\n dataKey?: string | number;\n color?: string;\n}\n\ninterface ChartLegendContentProps {\n payload?: LegendPayloadItem[];\n onItemClick?: (key: string) => void;\n activeKey?: string | null;\n}\n\nexport function ChartLegendContent({ payload, onItemClick, activeKey }: ChartLegendContentProps) {\n const config = useChart();\n if (!payload?.length) return null;\n return (\n <div className=\"flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-3\">\n {payload.map((item, i) => {\n const dk = item.dataKey != null ? String(item.dataKey) : undefined;\n const vv = item.value != null ? String(item.value) : undefined;\n const cfgKey = dk && config[dk] ? dk : vv && config[vv] ? vv : (dk ?? vv ?? String(i));\n const series = config[cfgKey];\n const color = series?.color ?? item.color ?? \"#64748b\";\n const label = series?.label ?? vv ?? cfgKey;\n const dimmed = activeKey != null && activeKey !== cfgKey;\n return (\n <button\n key={`${cfgKey}-${i}`}\n type=\"button\"\n onClick={onItemClick ? () => onItemClick(cfgKey) : undefined}\n className={cn(\n \"flex items-center gap-1.5 text-slate-600 transition-opacity dark:text-slate-300\",\n onItemClick && \"cursor-pointer hover:opacity-80\",\n !onItemClick && \"cursor-default\",\n dimmed && \"opacity-40\",\n )}\n >\n <span\n className=\"h-2.5 w-2.5 shrink-0 rounded-[3px]\"\n style={{ backgroundColor: color } as CSSProperties}\n />\n {label}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/chart.tsx"],"sourcesContent":["// shadcn/ui chart kit — vendored (MIT) and adapted for this app:\n// - Recharts 3 instead of 2\n// - the original keys its styling off shadcn theme tokens (--muted-foreground,\n// --border, bg-background …). This app has no shadcn token layer, so those\n// are mapped onto this kit's own tokens (--text-muted, --border, --bg-surface\n// …), which follow the active palette preset and flip with the theme by\n// themselves — hence no `dark:` variants anywhere below.\n// The value on show is the *structure*: config-driven colors injected as CSS\n// vars, muted axis/grid styling, and a polished tooltip/legend — the things\n// that make shadcn charts read as \"designed\" rather than \"default Recharts\".\nimport { createContext, useContext, useId, useRef } from \"react\";\nimport type { ComponentProps, CSSProperties, ReactNode, RefObject } from \"react\";\nimport {\n Legend as RechartsLegend,\n ResponsiveContainer,\n Tooltip as RechartsTooltip,\n} from \"recharts\";\nimport { cn } from \"../lib/cn\";\n\nexport interface ChartSeriesConfig {\n label: ReactNode;\n color: string;\n}\n\nexport type ChartConfig = Record<string, ChartSeriesConfig>;\n\nconst ChartContext = createContext<ChartConfig | null>(null);\n\nexport function useChart(): ChartConfig {\n const ctx = useContext(ChartContext);\n if (!ctx) throw new Error(\"useChart must be used within a <ChartContainer />\");\n return ctx;\n}\n\ninterface ChartContainerProps extends ComponentProps<\"div\"> {\n config: ChartConfig;\n children: ComponentProps<typeof ResponsiveContainer>[\"children\"];\n}\n\nexport function ChartContainer({ id, className, children, config, ...props }: ChartContainerProps) {\n const uid = useId();\n // The id is pasted into an UNQUOTED `[data-chart=…]` selector below, so it has to be\n // a CSS identifier: one stray space or `#` and the browser discards the whole rule,\n // taking every series colour with it — silently, because an invalid rule is not an\n // error anywhere. `useId()` gives `:r0:`, and a consumer-supplied id gives whatever\n // they typed, so both are filtered rather than just the colons.\n const chartId = `chart-${(id ?? uid).replace(/[^A-Za-z0-9_-]/g, \"\")}`;\n return (\n <ChartContext.Provider value={config}>\n <div\n data-chart={chartId}\n className={cn(\n // touch-pan-y lets a vertical finger drag still scroll the page while\n // a horizontal drag is handed to recharts for tooltip scrubbing,\n // instead of the page scrolling underneath the gesture (feedback #292).\n \"flex w-full touch-pan-y justify-center text-xs\",\n // muted axis labels + gridlines; one class each, since the tokens\n // already carry the light/dark values\n \"[&_.recharts-cartesian-axis-tick_text]:fill-[var(--text-muted)]\",\n \"[&_.recharts-cartesian-grid_line]:stroke-[var(--border)]/70\",\n // hairline hover cursor instead of the heavy default\n \"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-[var(--border-strong)]\",\n \"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-[var(--bg-hover)]\",\n // kill the focus outlines & white sector strokes Recharts adds\n \"[&_.recharts-sector]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent\",\n \"[&_.recharts-layer]:outline-none [&_.recharts-surface]:outline-none [&_.recharts-dot[stroke='#fff']]:stroke-transparent\",\n className,\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={config} />\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n {children}\n </ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n );\n}\n\n/**\n * A config key, which becomes the tail of a custom-property NAME.\n *\n * `dangerouslySetInnerHTML` means what it says: nothing below is escaped by React, and\n * a `ChartConfig` is not necessarily written by hand — kastlan builds one from category\n * rows in its database, so these keys are user data. A key containing `}` closes this\n * rule, and whatever follows it in that key becomes a NEW rule — one that applies to\n * the whole document rather than to this one chart.\n */\nconst CSS_IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\n/**\n * A config colour, which becomes a declaration VALUE.\n *\n * Every form a palette actually produces: both hex spellings, the functional notations\n * — with the argument list restricted to the characters those functions can legally\n * contain, so no `;`, no `}` and no nested `url(` — and `var(--token)`, which is what\n * this kit's own `paletteFor`/`CHART_COLORS` return. `var()`'s optional FALLBACK is\n * deliberately not accepted: its second argument is an arbitrary value, i.e. the same\n * hole in a shape that looks safe.\n */\nconst CSS_COLOR =\n /^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|(?:rgba?|hsla?)\\([0-9A-Za-z.,%/ -]*\\)|var\\(--[A-Za-z0-9_-]+\\))$/;\n\n// Inject each series colour as a `--color-<key>` custom property scoped to this\n// chart, so chart JSX can refer to `var(--color-total)` and the legend/tooltip\n// stay in sync from one source of truth.\nfunction ChartStyle({ id, config }: { id: string; config: ChartConfig }) {\n const entries = Object.entries(config).filter(([key, c]) => {\n if (!c.color) return false;\n // Skipping the entry costs that one series its `var(--color-…)` and leaves the rest\n // of the chart painted; emitting it unchecked costs the page. The warning is so the\n // consumer finds out from their console rather than from a colourless bar.\n if (!CSS_IDENT.test(key)) {\n console.warn(`[ui-kit] chart config key is not a CSS identifier, skipped: ${key}`);\n return false;\n }\n if (!CSS_COLOR.test(c.color)) {\n console.warn(`[ui-kit] chart config colour for \"${key}\" is not a colour, skipped: ${c.color}`);\n return false;\n }\n return true;\n });\n if (!entries.length) return null;\n const css = `[data-chart=${id}] {\\n${entries\n .map(([key, c]) => ` --color-${key}: ${c.color};`)\n .join(\"\\n\")}\\n}`;\n return <style dangerouslySetInnerHTML={{ __html: css }} />;\n}\n\nexport const ChartTooltip = RechartsTooltip;\nexport const ChartLegend = RechartsLegend;\n\ninterface TooltipPayloadItem {\n dataKey?: string | number;\n name?: string | number;\n value?: number | string;\n color?: string;\n payload?: Record<string, unknown>;\n}\n\n/**\n * What a series has no value for at this point: a gap in a history, a projection that\n * has not happened yet, a field that did not exist at an earlier capture.\n *\n * An em dash rather than an empty cell, so the row still lines up with its siblings and\n * the reader can see that the series WAS asked and had nothing, rather than wondering\n * whether it was dropped. It is a mark, not a word, so there is nothing here to\n * translate and no label prop to resolve; a caller who wants words passes\n * `formatValue`, which is handed the `undefined` and decides.\n */\nconst NO_VALUE = \"—\";\n\n/**\n * The number this payload item carries, or `undefined` when it carries none.\n *\n * This was `item.value ?? 0`, so every one of the absences above printed a confident\n * \"0\" that appears nowhere in the data — a fabricated figure in a tooltip, which is the\n * one place a reader goes to check an exact number. keksdose found it and wrapped the\n * component rather than trust it. `\"\"` and a non-numeric string are the same absence\n * spelled differently (recharts hands a string value straight through from the data),\n * and are answered the same way.\n */\nfunction tooltipValue(value: number | string | undefined): number | undefined {\n if (value == null || value === \"\") return undefined;\n const n = typeof value === \"string\" ? Number(value) : value;\n return Number.isFinite(n) ? n : undefined;\n}\n\ninterface ChartTooltipContentProps {\n active?: boolean;\n payload?: TooltipPayloadItem[];\n label?: string | number;\n indicator?: \"dot\" | \"line\";\n hideLabel?: boolean;\n labelFormatter?: (label: string | number) => ReactNode;\n /** Formats one series' value for display. NOT called when the series has no value at\n * this point — {@link ChartTooltipContentProps.formatValue} is the one that is. */\n valueFormatter?: (value: number) => ReactNode;\n /** Formats one series' value INCLUDING its absence: handed `undefined` for a gap, a\n * projection that has not happened yet, or a field that did not exist at an earlier\n * capture, and whatever it returns is rendered as-is. Only the caller knows whether\n * that absence reads as a dash, a \"not yet\" or a genuine zero, so this is how it\n * says. Supplying it wins over `valueFormatter` for every value.\n *\n * It sits NEXT to `valueFormatter` rather than widening it because that prop's\n * parameter is contextually typed at every call site there is — `(v) => money(v)` —\n * so widening it to `number | undefined` is a compile error in all three consuming\n * apps, which is a breaking change this module does not get to make by itself. */\n formatValue?: (value: number | undefined) => ReactNode;\n /** Cursor position within the chart, injected by recharts. Used with\n * {@link boundaryRef} to decide whether to flip the tooltip. */\n coordinate?: { x?: number; y?: number };\n /** When set, the tooltip flips to the LEFT of the cursor if it would otherwise\n * spill past the right edge of this (usually horizontally-scrolling) container\n * — e.g. a wide chart whose rightmost bars sat off-screen (feedback #77). */\n boundaryRef?: RefObject<HTMLElement | null>;\n}\n\nexport function ChartTooltipContent({\n active,\n payload,\n label,\n indicator = \"dot\",\n hideLabel = false,\n labelFormatter,\n valueFormatter,\n formatValue,\n coordinate,\n boundaryRef,\n}: ChartTooltipContentProps) {\n const config = useChart();\n const tipRef = useRef<HTMLDivElement>(null);\n if (!active || !payload?.length) return null;\n // Recharts anchors the tooltip at coordinate.x inside the full (scrolled) chart\n // width; subtract the container's scrollLeft to get its on-screen x, then flip\n // left if the tooltip's own width would run past the visible right edge.\n let flip = false;\n const boundary = boundaryRef?.current;\n if (boundary && coordinate?.x != null) {\n const visibleX = coordinate.x - boundary.scrollLeft;\n const tipWidth = tipRef.current?.offsetWidth ?? 160;\n flip = visibleX + tipWidth + 12 > boundary.clientWidth;\n }\n return (\n <div\n ref={tipRef}\n style={boundaryRef ? { transform: flip ? \"translateX(calc(-100% - 12px))\" : \"translateX(12px)\" } : undefined}\n className=\"min-w-[9rem] rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] px-2.5 py-2 text-xs shadow-xl\"\n >\n {!hideLabel && label != null && (\n <div className=\"mb-1.5 font-medium text-[var(--text-primary)]\">\n {labelFormatter ? labelFormatter(label) : label}\n </div>\n )}\n <div className=\"grid gap-1.5\">\n {payload.map((item, i) => {\n // Bars/lines/areas identify a series by dataKey; pies/treemaps key off\n // `name` (the nameKey value). Prefer whichever the config knows.\n const dk = item.dataKey != null ? String(item.dataKey) : undefined;\n const nm = item.name != null ? String(item.name) : undefined;\n const cfgKey = dk && config[dk] ? dk : nm && config[nm] ? nm : (dk ?? nm ?? String(i));\n const series = config[cfgKey];\n const fill = typeof item.payload?.fill === \"string\" ? item.payload.fill : undefined;\n // The data point's own `fill` before recharts' `item.color`: a treemap tile,\n // or a bar with a per-cell colour, IS the colour the reader is pointing at,\n // while `item.color` is the series-wide one — every tile's swatch came out\n // `--chart-1` whatever colour the tile was.\n const color = series?.color ?? fill ?? item.color ?? \"#64748b\";\n const name = series?.label ?? nm ?? cfgKey;\n const raw = tooltipValue(item.value);\n let shown: ReactNode;\n if (formatValue) shown = formatValue(raw);\n else if (raw == null) shown = NO_VALUE;\n else shown = valueFormatter ? valueFormatter(raw) : raw;\n return (\n <div key={`${cfgKey}-${i}`} className=\"flex items-center gap-2\">\n <span\n className={cn(\n \"shrink-0 rounded-[2px]\",\n indicator === \"dot\" ? \"h-2.5 w-2.5 rounded-full\" : \"h-2.5 w-1\",\n )}\n style={{ backgroundColor: color }}\n />\n <span className=\"text-[var(--text-muted)]\">{name}</span>\n <span data-private className=\"ml-auto font-mono font-medium tabular-nums text-[var(--text-primary)]\">\n {shown}\n </span>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n\ninterface LegendPayloadItem {\n value?: string;\n dataKey?: string | number;\n color?: string;\n}\n\ninterface ChartLegendContentProps {\n payload?: LegendPayloadItem[];\n onItemClick?: (key: string) => void;\n /**\n * TOGGLE mode (recommended): the series currently switched off. Each entry is a\n * toggle button — pressed while its series is shown — and a hidden one is drawn\n * faded with its label struck through. Several can be off at once; pair with\n * `toggleHidden(hidden, key)` in `onItemClick` and `hide` on the chart's series.\n */\n hiddenKeys?: ReadonlySet<string> | readonly string[];\n /**\n * ISOLATE mode: the one series shown in full, every other entry dimmed. Kept for\n * the charts built on it; a legend where each entry switches its own series is\n * what readers expect from a legend, so prefer `hiddenKeys`.\n */\n activeKey?: string | null;\n}\n\nexport function ChartLegendContent({\n payload,\n onItemClick,\n hiddenKeys,\n activeKey,\n}: ChartLegendContentProps) {\n const hidden = hiddenKeys ? new Set(hiddenKeys) : null;\n const config = useChart();\n if (!payload?.length) return null;\n return (\n <div className=\"flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-3\">\n {payload.map((item, i) => {\n const dk = item.dataKey != null ? String(item.dataKey) : undefined;\n const vv = item.value != null ? String(item.value) : undefined;\n const cfgKey = dk && config[dk] ? dk : vv && config[vv] ? vv : (dk ?? vv ?? String(i));\n const series = config[cfgKey];\n const color = series?.color ?? item.color ?? \"#64748b\";\n const label = series?.label ?? vv ?? cfgKey;\n const off = hidden?.has(cfgKey) ?? false;\n const dimmed = off || (activeKey != null && activeKey !== cfgKey);\n const className = cn(\n \"flex items-center gap-1.5 text-[var(--text-secondary)] transition-opacity\",\n onItemClick ? \"cursor-pointer hover:opacity-80\" : \"cursor-default\",\n dimmed && \"opacity-40\",\n );\n const swatch = (\n <span\n aria-hidden\n className=\"h-2.5 w-2.5 shrink-0 rounded-[3px]\"\n style={{ backgroundColor: color } as CSSProperties}\n />\n );\n // A legend with no `onItemClick` is a KEY, not a control — and that is every\n // legend in kastlan, where this rendered N focusable buttons per chart that did\n // nothing when activated. A keyboard user paid the tab stops for all of them,\n // and a screen reader announced each one as a button, promising an action there\n // was none of. The interactive branch is unchanged.\n return onItemClick ? (\n <button\n key={`${cfgKey}-${i}`}\n type=\"button\"\n onClick={() => onItemClick(cfgKey)}\n // Toggles report their state: in toggle mode pressed = the series is shown;\n // in isolate mode pressed = the isolated one. With neither supplied the\n // legend knows nothing about state, and a pressed=\"false\" it cannot vouch\n // for would lie — so it says nothing.\n aria-pressed={\n hidden ? !off : activeKey === undefined ? undefined : activeKey === cfgKey\n }\n className={className}\n >\n {swatch}\n <span className={cn(off && \"line-through\")}>{label}</span>\n </button>\n ) : (\n <span key={`${cfgKey}-${i}`} className={className}>\n {swatch}\n {label}\n </span>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|