@micha.bigler/ui-core-micha 3.14.4 → 4.0.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/CHANGELOG.md +25 -0
- package/README.md +7 -0
- package/dist/chat/kitSxRegistry.js +9 -0
- package/dist/components/charts/kitSxRegistry.js +6 -0
- package/dist/entries/charts.js +6 -0
- package/dist/entries/chat.js +8 -0
- package/dist/entries/markdown.js +1 -0
- package/dist/index.js +0 -14
- package/dist/pages/AccountPage.js +1 -2
- package/dist/pages/LoginPage.js +1 -2
- package/dist/pages/PasswordChangePage.js +1 -2
- package/dist/pages/PasswordInvitePage.js +1 -2
- package/dist/pages/PasswordResetRequestPage.js +1 -2
- package/dist/pages/SignUpPage.js +1 -2
- package/dist/pages/SignupConfirmPage.js +1 -2
- package/dist/theme/kitSxEntry.js +11 -0
- package/dist/theme/kitSxRegistry.js +2 -15
- package/package.json +32 -7
- package/.github/workflows/ci.yml +0 -35
- package/.github/workflows/publish.yml +0 -126
- package/DESIGN.md +0 -60
- package/WORK_ORDERS.md +0 -150
- package/dev/entries.jsx +0 -721
- package/dev/fixtures.js +0 -26
- package/dev/main.jsx +0 -75
- package/dev/mockTransport.jsx +0 -128
- package/docs/CHART-LAYOUT.md +0 -157
- package/docs/DARK-TOKENS.md +0 -140
- package/docs/TYPE-SCALE.md +0 -111
- package/docs/messaging-deviations.md +0 -118
- package/index.html +0 -12
- package/renovate.json +0 -10
- package/scripts/chart-api-census.mjs +0 -415
- package/src/auth/AuthContext.jsx +0 -158
- package/src/auth/ReauthModal.jsx +0 -82
- package/src/auth/UserMenu.jsx +0 -121
- package/src/auth/apiClient.jsx +0 -232
- package/src/auth/authApi.jsx +0 -539
- package/src/auth/authConfig.jsx +0 -34
- package/src/auth/reauth.js +0 -47
- package/src/chat/ChatComposer.jsx +0 -36
- package/src/chat/ChatList.jsx +0 -87
- package/src/chat/ChatThread.jsx +0 -128
- package/src/chat/ChatWorkspace.jsx +0 -244
- package/src/chat/MarkdownReply.jsx +0 -22
- package/src/chat/ModelPicker.jsx +0 -85
- package/src/chat/api.js +0 -18
- package/src/chat/useChatSocket.js +0 -32
- package/src/chat/wire.js +0 -7
- package/src/components/AccessCodeManager.jsx +0 -288
- package/src/components/AccessCodeSingleUseToggle.jsx +0 -92
- package/src/components/AllowedEmailDomainsManager.jsx +0 -92
- package/src/components/AuthFactorRequirementCard.jsx +0 -95
- package/src/components/BulkInviteCsvTab.jsx +0 -198
- package/src/components/EmptyState.jsx +0 -38
- package/src/components/LoginForm.jsx +0 -159
- package/src/components/MFAComponent.jsx +0 -350
- package/src/components/MfaLoginComponent.jsx +0 -252
- package/src/components/MobileBottomNav.jsx +0 -82
- package/src/components/PasskeysComponent.jsx +0 -252
- package/src/components/PasswordChangeForm.jsx +0 -87
- package/src/components/PasswordResetRequestForm.jsx +0 -43
- package/src/components/PasswordSetForm.jsx +0 -97
- package/src/components/ProfileComponent.jsx +0 -348
- package/src/components/QrSignupManager.jsx +0 -404
- package/src/components/QrSignupValidityManager.jsx +0 -106
- package/src/components/RegistrationMethodsManager.jsx +0 -150
- package/src/components/SecurityComponent.jsx +0 -141
- package/src/components/SocialLoginButtons.jsx +0 -83
- package/src/components/SoftChip.jsx +0 -93
- package/src/components/StatTile.jsx +0 -39
- package/src/components/SupportRecoveryRequestsTab.jsx +0 -482
- package/src/components/TurnstileWidget.jsx +0 -99
- package/src/components/UserInviteComponent.jsx +0 -76
- package/src/components/UserListComponent.jsx +0 -618
- package/src/components/charts/BarChart.jsx +0 -102
- package/src/components/charts/ChartFrame.jsx +0 -214
- package/src/components/charts/LineChart.jsx +0 -126
- package/src/components/charts/ScatterChart.jsx +0 -299
- package/src/components/charts/TimeSeriesChart.jsx +0 -300
- package/src/components/charts/chartDefaults.js +0 -644
- package/src/components/charts/chartLabels.js +0 -36
- package/src/components/charts/exportChart.js +0 -258
- package/src/components/charts/formatters.js +0 -39
- package/src/components/charts/palette.js +0 -33
- package/src/components/charts/yearTickInterval.js +0 -7
- package/src/i18n/authTranslations.ts +0 -2651
- package/src/i18n/chartsTranslations.ts +0 -91
- package/src/i18n/chatTranslations.ts +0 -55
- package/src/i18n/messagingTranslations.ts +0 -120
- package/src/i18n/notificationsTranslations.ts +0 -32
- package/src/i18n/onboardingTranslations.ts +0 -29
- package/src/i18n/sectionNavTranslations.ts +0 -20
- package/src/i18n/uiCoreTranslations.js +0 -29
- package/src/i18n/userMenuTranslations.ts +0 -5
- package/src/index.js +0 -158
- package/src/layout/PageLayout.jsx +0 -36
- package/src/layout/SectionNav.jsx +0 -320
- package/src/messaging/AttachmentList.jsx +0 -145
- package/src/messaging/Composer.jsx +0 -197
- package/src/messaging/ConversationLaunchers.jsx +0 -51
- package/src/messaging/ConversationList.jsx +0 -109
- package/src/messaging/DirectMessageLauncher.jsx +0 -112
- package/src/messaging/MessageBubble.jsx +0 -183
- package/src/messaging/MessagingProvider.jsx +0 -0
- package/src/messaging/MessagingScopeConfig.jsx +0 -24
- package/src/messaging/PollCard.jsx +0 -163
- package/src/messaging/ReactionBar.jsx +0 -43
- package/src/messaging/ReadTicks.jsx +0 -82
- package/src/messaging/Thread.jsx +0 -195
- package/src/messaging/api.js +0 -48
- package/src/messaging/composerImageCompression.js +0 -35
- package/src/messaging/linkifyText.jsx +0 -37
- package/src/notifications/NotificationBell.jsx +0 -84
- package/src/notifications/NotificationSettings.jsx +0 -330
- package/src/notifications/NotificationsProvider.jsx +0 -224
- package/src/notifications/PopupSurface.jsx +0 -105
- package/src/notifications/api.js +0 -55
- package/src/notifications/feedApi.js +0 -18
- package/src/notifications/realtime.jsx +0 -138
- package/src/notifications/serviceWorker/sw.js +0 -50
- package/src/onboarding/OnboardingProvider.jsx +0 -326
- package/src/onboarding/OnboardingWizard.jsx +0 -61
- package/src/onboarding/WizardDialogShell.jsx +0 -51
- package/src/onboarding/api.js +0 -13
- package/src/onboarding/stepSelection.js +0 -16
- package/src/onboarding/steps/BrowserPushStep.jsx +0 -121
- package/src/onboarding/steps/CompleteNameStep.jsx +0 -46
- package/src/onboarding/steps/CookieConsentStep.jsx +0 -133
- package/src/onboarding/steps/PwaInstallStep.jsx +0 -47
- package/src/pages/AccountPage.jsx +0 -394
- package/src/pages/LoginPage.jsx +0 -343
- package/src/pages/PasswordChangePage.jsx +0 -62
- package/src/pages/PasswordInvitePage.jsx +0 -177
- package/src/pages/PasswordResetRequestPage.jsx +0 -68
- package/src/pages/SignUpPage.jsx +0 -268
- package/src/pages/SignupConfirmPage.jsx +0 -155
- package/src/testing/checkKitIntegration.js +0 -233
- package/src/theme/createAppTheme.js +0 -423
- package/src/theme/fonts.js +0 -3
- package/src/theme/index.js +0 -21
- package/src/theme/kitSxRegistry.js +0 -285
- package/src/theme/themeCompleteness.js +0 -1045
- package/src/theme/tokens.js +0 -478
- package/src/utils/auth-errors.js +0 -76
- package/src/utils/authService.js +0 -223
- package/src/utils/resolveErrorText.js +0 -28
- package/src/utils/webauthn.js +0 -51
- package/tests/AccountPage.profileStatements.test.jsx +0 -162
- package/tests/AccountPage.test.jsx +0 -200
- package/tests/AuthContext.test.jsx +0 -129
- package/tests/BarChart.test.jsx +0 -112
- package/tests/BrowserPushStep.test.jsx +0 -110
- package/tests/ChartFrame.test.jsx +0 -336
- package/tests/ConversationList.test.jsx +0 -144
- package/tests/CookieConsentStep.test.jsx +0 -69
- package/tests/EmptyState.test.jsx +0 -62
- package/tests/LineChart.test.jsx +0 -80
- package/tests/MessagingProvider.test.jsx +0 -50
- package/tests/MobileBottomNav.test.jsx +0 -319
- package/tests/NotificationBell.test.jsx +0 -60
- package/tests/NotificationSettings.test.jsx +0 -276
- package/tests/NotificationsProvider.test.jsx +0 -240
- package/tests/OnboardingProvider.test.jsx +0 -294
- package/tests/OnboardingWizard.test.jsx +0 -143
- package/tests/PasswordChangeForm.test.jsx +0 -70
- package/tests/PasswordInvitePage.test.jsx +0 -142
- package/tests/PasswordSetForm.test.jsx +0 -70
- package/tests/PopupSurface.test.jsx +0 -142
- package/tests/ProfileComponent.refresh.test.jsx +0 -194
- package/tests/QrSignupManager.registrationContext.test.jsx +0 -106
- package/tests/QrSignupManagerInfoBox.test.jsx +0 -49
- package/tests/ScatterChart.test.jsx +0 -270
- package/tests/SectionNav.test.jsx +0 -363
- package/tests/SignUpPage.test.jsx +0 -208
- package/tests/SoftChip.test.jsx +0 -105
- package/tests/StatTile.test.jsx +0 -57
- package/tests/Thread.test.jsx +0 -151
- package/tests/TimeSeriesChart.test.jsx +0 -367
- package/tests/UserListComponent.test.jsx +0 -192
- package/tests/UserMenu.test.jsx +0 -171
- package/tests/apiClientLanguage.test.js +0 -33
- package/tests/apiClientPublicPaths.test.js +0 -178
- package/tests/authApiLogin.test.js +0 -42
- package/tests/authApiRegistration.test.js +0 -31
- package/tests/authErrorCodeRendering.test.jsx +0 -77
- package/tests/authErrors.test.js +0 -74
- package/tests/authTranslations.test.js +0 -74
- package/tests/chart-api-census.test.js +0 -366
- package/tests/chartAxisTickBand.test.jsx +0 -95
- package/tests/chartDefaults.test.js +0 -902
- package/tests/chartLabels.test.js +0 -50
- package/tests/chartsPalette.test.js +0 -99
- package/tests/chatApi.test.js +0 -48
- package/tests/chatDriverNames.test.jsx +0 -71
- package/tests/chatExports.test.js +0 -3
- package/tests/chatList.test.jsx +0 -84
- package/tests/chatPicker.test.jsx +0 -82
- package/tests/chatSocket.test.jsx +0 -26
- package/tests/chatThread.test.jsx +0 -116
- package/tests/chatTranslations.test.js +0 -47
- package/tests/chatWorkspace.test.jsx +0 -234
- package/tests/checkKitIntegration.test.jsx +0 -219
- package/tests/createAppTheme.test.js +0 -132
- package/tests/createAppThemeDark.test.js +0 -127
- package/tests/createAppThemeLightGuard.test.js +0 -40
- package/tests/createAppThemePageContrast.test.js +0 -108
- package/tests/createAppThemeTypography.test.js +0 -53
- package/tests/errorCallSiteGuard.test.js +0 -32
- package/tests/exportChart.test.js +0 -313
- package/tests/feedApi.test.js +0 -46
- package/tests/fixtures/light-theme-baseline.json +0 -2234
- package/tests/i18nAggregate.test.js +0 -120
- package/tests/i18nGermanVariant.test.js +0 -65
- package/tests/loginPageErrorResolution.test.jsx +0 -102
- package/tests/messagingApi.test.js +0 -32
- package/tests/messagingChunk6.test.jsx +0 -116
- package/tests/messagingCompactBubble.test.jsx +0 -64
- package/tests/messagingComposer.test.jsx +0 -136
- package/tests/messagingComposerChunk5.test.jsx +0 -113
- package/tests/messagingContractConformance.test.js +0 -69
- package/tests/messagingDirectMessageLauncher.test.jsx +0 -151
- package/tests/messagingExports.test.js +0 -21
- package/tests/messagingInteractions.test.jsx +0 -136
- package/tests/messagingLastMessagePreview.test.jsx +0 -93
- package/tests/messagingLinkify.test.jsx +0 -51
- package/tests/messagingMessageActions.test.jsx +0 -163
- package/tests/messagingMsg6.test.jsx +0 -105
- package/tests/messagingMsg6f.test.jsx +0 -155
- package/tests/messagingMsg6g.test.jsx +0 -166
- package/tests/messagingMsg6h.test.jsx +0 -136
- package/tests/messagingMsg6i.test.jsx +0 -129
- package/tests/messagingPollRendering.test.jsx +0 -142
- package/tests/messagingReactionMerge.test.jsx +0 -77
- package/tests/messagingReadTicksTooltip.test.jsx +0 -36
- package/tests/messagingThreadReceipts.test.jsx +0 -178
- package/tests/messagingThreadingQuoting.test.jsx +0 -87
- package/tests/messagingUnreadLifecycle.test.jsx +0 -78
- package/tests/notificationsApi.test.js +0 -41
- package/tests/notificationsExports.test.js +0 -34
- package/tests/onboardingDescriptors.test.js +0 -132
- package/tests/packageTreeShaking.test.js +0 -126
- package/tests/realtime.test.jsx +0 -219
- package/tests/resolveErrorText.test.js +0 -72
- package/tests/stepSelection.test.js +0 -53
- package/tests/themeCompleteness.test.js +0 -626
- package/tests/timeSeriesLegend.test.js +0 -99
- package/tests/userScreenTranslations.test.jsx +0 -218
- package/tsconfig.build.json +0 -18
- package/vite.config.mjs +0 -6
- package/work-orders/AUTH-1.md +0 -74
- package/work-orders/AUTH-2.md +0 -74
- package/work-orders/AUTH-4.md +0 -183
- package/work-orders/AUTH-5.md +0 -154
- package/work-orders/AUTH-7.md +0 -181
- package/work-orders/AUTH-8.md +0 -184
- package/work-orders/CHART-1.md +0 -285
- package/work-orders/CHART-2.md +0 -222
- package/work-orders/CHART-3.md +0 -163
- package/work-orders/CHART-4.md +0 -203
- package/work-orders/CHART-5.md +0 -226
- package/work-orders/CHART-6.md +0 -428
- package/work-orders/CHART-7.md +0 -175
- package/work-orders/DX-1.md +0 -169
- package/work-orders/DX-2.md +0 -188
- package/work-orders/DX-3.md +0 -195
- package/work-orders/I18N-1.md +0 -254
- package/work-orders/I18N-3.md +0 -207
- package/work-orders/MSG-3.md +0 -375
- package/work-orders/MSG-3b.md +0 -1002
- package/work-orders/MSG-3c.md +0 -378
- package/work-orders/MSG-3d.md +0 -268
- package/work-orders/MSG-3e.md +0 -164
- package/work-orders/MSG-3f.md +0 -391
- package/work-orders/MSG-6e.md +0 -308
- package/work-orders/MSG-6f.md +0 -214
- package/work-orders/MSG-6g.md +0 -244
- package/work-orders/MSG-6h.md +0 -238
- package/work-orders/MSG-6i.md +0 -271
- package/work-orders/MSG-6j.md +0 -117
- package/work-orders/NOTIF-12.md +0 -214
- package/work-orders/NOTIF-13.md +0 -142
- package/work-orders/NOTIF-14.md +0 -151
- package/work-orders/PRIM-1.md +0 -177
- package/work-orders/SHELL-1.md +0 -255
- package/work-orders/SHELL-2.md +0 -197
- package/work-orders/SHELL-3.md +0 -359
- package/work-orders/SHELL-4.md +0 -351
- package/work-orders/SHELL-5.md +0 -345
- package/work-orders/SHELL-6.md +0 -210
- package/work-orders/THEME-1.md +0 -587
- package/work-orders/THEME-10.md +0 -303
- package/work-orders/THEME-11.md +0 -208
- package/work-orders/THEME-2.md +0 -339
- package/work-orders/THEME-3.md +0 -210
- package/work-orders/THEME-4.md +0 -271
- package/work-orders/THEME-5.md +0 -341
- package/work-orders/THEME-6.md +0 -247
- package/work-orders/THEME-7.md +0 -82
- package/work-orders/THEME-8.md +0 -211
- package/work-orders/THEME-9.md +0 -194
- package/work-orders/UCM-AUTH-10.md +0 -490
- package/work-orders/UCM-AUTH-11.md +0 -185
- package/work-orders/UCM-AUTH-12.md +0 -98
- package/work-orders/UCM-AUTH-13.md +0 -47
- package/work-orders/UCM-AUTH-9.md +0 -207
- package/work-orders/UCM-CHART-10.md +0 -150
- package/work-orders/UCM-CHART-11.md +0 -132
- package/work-orders/UCM-CHART-12.md +0 -348
- package/work-orders/UCM-CHART-13.md +0 -122
- package/work-orders/UCM-CHART-14.md +0 -198
- package/work-orders/UCM-CHART-15.md +0 -187
- package/work-orders/UCM-CHART-16.md +0 -169
- package/work-orders/UCM-CHART-17.md +0 -195
- package/work-orders/UCM-CHART-18.md +0 -200
- package/work-orders/UCM-CHART-8.md +0 -147
- package/work-orders/UCM-CHART-9.md +0 -265
- package/work-orders/UCM-CHAT-1.md +0 -190
- package/work-orders/UCM-CHAT-2.md +0 -75
- package/work-orders/UCM-CI-1.md +0 -65
- package/work-orders/UCM-DEP-1.md +0 -63
- package/work-orders/UCM-I18N-4.md +0 -226
- package/work-orders/UCM-I18N-5.md +0 -211
- package/work-orders/UCM-MSG-20.md +0 -192
- package/work-orders/UCM-PRIM-2.md +0 -114
- package/work-orders/UCM-SHELL-7.md +0 -134
- package/work-orders/UCM-SHELL-8.md +0 -125
- package/work-orders/UCM-TEST-1.md +0 -304
- package/work-orders/UCM-THEME-13.md +0 -69
- package/work-orders/UCM-THEME-14.md +0 -312
- package/work-orders/UCM-THEME-15.md +0 -356
- package/work-orders/UCM-THEME-16.md +0 -80
- package/work-orders/UCM-THEME-17.md +0 -111
- package/work-orders/UCM-THEME-18.md +0 -184
- package/work-orders/assets/AUTH-4-user-list-card.html +0 -407
- package/work-orders/assets/PRIM-1-dashboard-primitives.html +0 -624
- package/work-orders/assets/SHELL-2-account-navigation.html +0 -1159
- package/work-orders/assets/THEME-1-baseline-reference-sheet.html +0 -1352
- package/work-orders/assets/UCM-CHAT-1-ai-chat.html +0 -447
- package/work-orders/assets/UCM-THEME-13-dark-prototype.html +0 -390
- package/work-orders/assets/UCM-THEME-17-type-scale.html +0 -391
|
@@ -1,644 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_LEGEND_POSITION = {
|
|
2
|
-
vertical: 'bottom',
|
|
3
|
-
horizontal: 'start',
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
export function withGridDefaults(grid) {
|
|
7
|
-
return { horizontal: true, ...grid };
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function withAxisDefaults(axes, label, defaults, tickFontSize) {
|
|
11
|
-
const values = axes?.length ? axes : [defaults];
|
|
12
|
-
return values.map((axis, index) => ({
|
|
13
|
-
// `defaults.scaleType` (and any other scale-defining default) must
|
|
14
|
-
// survive even when the caller passes their OWN xAxis/yAxis array --
|
|
15
|
-
// confirmed live in the dev harness: a caller-supplied axis with `data`
|
|
16
|
-
// but no `scaleType` previously lost 'point'/'band' entirely (only
|
|
17
|
-
// applied when the whole array was absent), leaving MUI's scale
|
|
18
|
-
// undefined and every point position NaN (`<path> attribute d:
|
|
19
|
-
// Expected number, "MNaN,..."` in the real rendered SVG -- a mocked
|
|
20
|
-
// prop-assertion test cannot catch this, it only reproduces on an
|
|
21
|
-
// actual MUI scale computation).
|
|
22
|
-
...defaults,
|
|
23
|
-
...axis,
|
|
24
|
-
tickLabelStyle: {
|
|
25
|
-
fontSize: tickFontSize,
|
|
26
|
-
...axis.tickLabelStyle,
|
|
27
|
-
},
|
|
28
|
-
// THEME-9: the single `label` argument is the caller's ONE string for
|
|
29
|
-
// this whole axis family (xAxisLabel/yAxisLabel) -- it may only ever
|
|
30
|
-
// fall back onto ONE axis. Applying it via .map() to every entry
|
|
31
|
-
// broadcast the identical label onto every axis in a multi-axis array
|
|
32
|
-
// (measured live: hram's dual-axis chart carried the same 43-character
|
|
33
|
-
// label on both y-axes). Only the first entry -- the common single-axis
|
|
34
|
-
// case is index 0 -- gets the fallback; a caller building a genuine
|
|
35
|
-
// multi-axis array must label each entry itself (as TimeSeriesChart's
|
|
36
|
-
// own secondary-axis support already does), exactly like MUI's own
|
|
37
|
-
// default `position: 'left'` (only the first entry, MUI docs).
|
|
38
|
-
label: axis.label || (index === 0 ? label : undefined),
|
|
39
|
-
}));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function cssLengthToPixels(value) {
|
|
43
|
-
if (typeof value === 'number') return value;
|
|
44
|
-
if (typeof value !== 'string') return 0;
|
|
45
|
-
|
|
46
|
-
const parsed = Number.parseFloat(value);
|
|
47
|
-
if (!Number.isFinite(parsed)) return 0;
|
|
48
|
-
if (value.endsWith('rem') || value.endsWith('em')) return parsed * 16;
|
|
49
|
-
return parsed;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const MUI_LABELLED_X_AXIS_HEIGHT = 45;
|
|
53
|
-
// Still used by `sizeYAxisForContent` (THEME-9, y-axis width) below -- unrelated to CHART-11,
|
|
54
|
-
// which only replaces how the ROTATED x-axis path (`rotatedTickMetrics`) estimates width.
|
|
55
|
-
const AVERAGE_GLYPH_WIDTH_EM = 0.6;
|
|
56
|
-
|
|
57
|
-
// CHART-11: per-character width table (em units), the ESTIMATE fallback used only when no DOM is
|
|
58
|
-
// available to actually measure a tick label's rendered extent (SSR, or a test environment where
|
|
59
|
-
// `measureTickTextWidthPx` returns null -- jsdom does not implement real SVG text layout). A
|
|
60
|
-
// single flat multiplier (the pre-CHART-11 constant, `AVERAGE_GLYPH_WIDTH_EM = 0.6`) systematically
|
|
61
|
-
// overestimated real ward-name-shaped strings: they mix spaces, apostrophes and narrow lowercase
|
|
62
|
-
// letters ("Mang'ula", "Msolwa Station") with a few wide/uppercase ones, and a flat multiplier
|
|
63
|
-
// prices every character at the wide end. Three tiers, not a full per-glyph font metrics table --
|
|
64
|
-
// proportionate to "an estimate", not a replacement for real measurement.
|
|
65
|
-
const NARROW_GLYPH_WIDTH_EM = 0.28; // i l j I ' . , : ; ! | and space
|
|
66
|
-
const WIDE_GLYPH_WIDTH_EM = 0.78; // uppercase (except I), plus lowercase m/w
|
|
67
|
-
const DEFAULT_GLYPH_WIDTH_EM = 0.52; // everything else -- most lowercase letters and digits
|
|
68
|
-
const NARROW_GLYPH_CHARS = new Set(["i", "l", "j", "I", "'", ".", ",", ":", ";", "!", "|", " "]);
|
|
69
|
-
const WIDE_GLYPH_CHARS = new Set(["m", "w"]);
|
|
70
|
-
|
|
71
|
-
function glyphWidthEm(char) {
|
|
72
|
-
if (NARROW_GLYPH_CHARS.has(char)) return NARROW_GLYPH_WIDTH_EM;
|
|
73
|
-
if (WIDE_GLYPH_CHARS.has(char) || (char >= 'A' && char <= 'Z')) return WIDE_GLYPH_WIDTH_EM;
|
|
74
|
-
return DEFAULT_GLYPH_WIDTH_EM;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* CHART-11 ESTIMATE fallback (declared explicitly, not dressed up as a measurement): sums a
|
|
79
|
-
* per-character width table instead of applying one flat multiplier to the whole string. Only
|
|
80
|
-
* reached when `measureTickTextWidthPx` returns null.
|
|
81
|
-
*/
|
|
82
|
-
function estimateTextWidthPx(text, fontSizePx) {
|
|
83
|
-
let widthEm = 0;
|
|
84
|
-
for (const char of text) widthEm += glyphWidthEm(char);
|
|
85
|
-
return widthEm * fontSizePx;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let hiddenMeasurementTextNode;
|
|
89
|
-
|
|
90
|
-
function getHiddenMeasurementTextNode() {
|
|
91
|
-
if (typeof document === 'undefined') return null;
|
|
92
|
-
if (hiddenMeasurementTextNode && hiddenMeasurementTextNode.isConnected) {
|
|
93
|
-
return hiddenMeasurementTextNode;
|
|
94
|
-
}
|
|
95
|
-
const svgNS = 'http://www.w3.org/2000/svg';
|
|
96
|
-
const svg = document.createElementNS(svgNS, 'svg');
|
|
97
|
-
svg.setAttribute('aria-hidden', 'true');
|
|
98
|
-
svg.style.position = 'absolute';
|
|
99
|
-
svg.style.width = '0';
|
|
100
|
-
svg.style.height = '0';
|
|
101
|
-
svg.style.overflow = 'hidden';
|
|
102
|
-
svg.style.pointerEvents = 'none';
|
|
103
|
-
const text = document.createElementNS(svgNS, 'text');
|
|
104
|
-
svg.appendChild(text);
|
|
105
|
-
(document.body || document.documentElement).appendChild(svg);
|
|
106
|
-
hiddenMeasurementTextNode = text;
|
|
107
|
-
return text;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* CHART-11: measures a tick label's real rendered width via a transient, invisible, shared SVG
|
|
112
|
-
* `<text>` node -- the same element type MUI's own axis ticks render as, so glyph metrics match
|
|
113
|
-
* whatever font actually cascades to it (this package has no access to the consumer's theme font
|
|
114
|
-
* at this call site; the transient node inherits the page's own cascade, same as a real tick
|
|
115
|
-
* would). Returns `null` (never throws) when no DOM is available (SSR) or when
|
|
116
|
-
* `getComputedTextLength` is unsupported/unreliable there (jsdom does not implement real SVG text
|
|
117
|
-
* layout) -- callers fall back to `estimateTextWidthPx` in that case, and tests inject their own
|
|
118
|
-
* stub via `spaceForRotatedTicks`'s optional `measureTextWidth` parameter for determinism.
|
|
119
|
-
*/
|
|
120
|
-
function measureTickTextWidthPx(text, fontSizePx) {
|
|
121
|
-
try {
|
|
122
|
-
const node = getHiddenMeasurementTextNode();
|
|
123
|
-
if (!node) return null;
|
|
124
|
-
node.setAttribute('font-size', String(fontSizePx));
|
|
125
|
-
node.textContent = text;
|
|
126
|
-
const length = node.getComputedTextLength();
|
|
127
|
-
return Number.isFinite(length) && length > 0 ? length : null;
|
|
128
|
-
} catch {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// THEME-11: this package never formed an opinion on chart margins, so every chart fell back to
|
|
134
|
-
// MUI's own DEFAULT_MARGINS (20px on all four sides) -- measured live (hram's KPI cards) as 167
|
|
135
|
-
// of a 270px-tall chart being furniture. Per-side, not a uniform trim, because each side is for
|
|
136
|
-
// something different: `left`/`bottom` are pure padding (the tick labels already live inside
|
|
137
|
-
// axisSizeLeft / DEFAULT_AXIS_SIZE_HEIGHT), `top` keeps room for the topmost y-tick label's own
|
|
138
|
-
// ~9px overhang past the plot (it is centred on its gridline, not below it), and `right` stays
|
|
139
|
-
// less trimmed than `left` because a LINEAR x-axis's last tick label sits AT the edge and
|
|
140
|
-
// overhangs by about half its width (~13px at caption size) -- a band axis wouldn't need it, but
|
|
141
|
-
// making this conditional on scale type would be inferred behaviour with no named consumer.
|
|
142
|
-
//
|
|
143
|
-
// UCM-CHART-12 Rule 3: no caller-supplied `margin` exists any more (the model owns spacing
|
|
144
|
-
// completely, `resolveChartLayout` builds the final margin object directly) -- this is now only
|
|
145
|
-
// the base every side starts from before `resolveChartLayout` adds the legend band to one side.
|
|
146
|
-
export const PACKAGE_DEFAULT_MARGIN = {
|
|
147
|
-
top: 10, bottom: 8, left: 8, right: 16,
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// CHART-11: small clearance between the rotated text block's own projected extent and the
|
|
151
|
-
// bottom of the axis's reserved band -- the same order of magnitude as the package's own
|
|
152
|
-
// PACKAGE_DEFAULT_MARGIN breathing-room, applied here for the same reason (a hard 0px gap reads
|
|
153
|
-
// as clipped even when nothing is actually cut off).
|
|
154
|
-
const ROTATED_TICK_CLEARANCE_PX = 8;
|
|
155
|
-
|
|
156
|
-
// UCM-CHART-16: the FLAT (angle 0) tick band used to be the unconditional TICK_BAND_BASE_PX
|
|
157
|
-
// regardless of the actual tick font size -- fine for MUI's own generic default, but MUI's
|
|
158
|
-
// internal fit check (`shortenLabels`, against `tickLabelsMaxHeight`) blanks EVERY tick label the
|
|
159
|
-
// moment the real rendered text is even slightly taller than what that flat floor leaves room
|
|
160
|
-
// for once MUI's own tick/gap overhead (and, when an axis title shares the band, its own gap) is
|
|
161
|
-
// subtracted. `TICK_BAND_BASE_PX` alone was a near-miss with essentially no margin at any font
|
|
162
|
-
// size above whatever MUI itself implicitly assumed -- two hram consumers independently patched
|
|
163
|
-
// around it with the same empirically-measured `height: 56`, and a third, titleless case broke at
|
|
164
|
-
// a tick font just 1px above the untitled default.
|
|
165
|
-
//
|
|
166
|
-
// These three constants are DERIVED, not guessed: binary-searched against the real, unmocked
|
|
167
|
-
// `BarChart` rendered in this repo's own dev harness (`dev/entries.jsx`'s `chart-probe` entry,
|
|
168
|
-
// Chrome, this package's own DM Sans default -- the taller of the two fonts checked; MUI's Roboto
|
|
169
|
-
// fallback needs visibly less and was never the trigger) at tick font sizes 11-18px, with and
|
|
170
|
-
// without an axis title:
|
|
171
|
-
// required flat tickBand ~= ceil(fontSize * TICK_TEXT_HEIGHT_FACTOR) + TICK_BAND_MUI_OVERHEAD_PX
|
|
172
|
-
// required tickBand+title ~= that, plus TICK_BAND_TITLE_EXTRA_PX, plus AXIS_TITLE_BAND_PX
|
|
173
|
-
// matched every measured threshold exactly or over by at most 1px, never under, across ten
|
|
174
|
-
// font-size/title combinations -- see UCM-CHART-16's work order for the full measurement table.
|
|
175
|
-
const TICK_TEXT_HEIGHT_FACTOR = 1.3;
|
|
176
|
-
const TICK_BAND_MUI_OVERHEAD_PX = 9;
|
|
177
|
-
const TICK_BAND_TITLE_EXTRA_PX = 2;
|
|
178
|
-
|
|
179
|
-
// THEME-9: MUI X-Charts' own default y-axis width is a flat 45px (ticks) +
|
|
180
|
-
// 20px (label, if present) = 65px regardless of what the ticks actually say
|
|
181
|
-
// -- confirmed against @mui/x-charts' own DEFAULT_AXIS_SIZE_WIDTH /
|
|
182
|
-
// AXIS_LABEL_DEFAULT_HEIGHT constants and its `useChartDimensions` selector
|
|
183
|
-
// (drawingArea.left = margin.left + axisSizeLeft, i.e. margin and axis size
|
|
184
|
-
// are ADDITIVE, not one containing the other). That flat default is both too
|
|
185
|
-
// generous for short ticks ("0%"/"50%"/"100%") and too stingy for wide ones
|
|
186
|
-
// ("50.0 min"), which is exactly the measured overlap on the Accessibility
|
|
187
|
-
// y-axis. Mirrors spaceForRotatedTicks' existing text-width heuristic below.
|
|
188
|
-
const Y_AXIS_TICK_SIZE = 6;
|
|
189
|
-
const Y_AXIS_TICK_LABEL_GAP = 2;
|
|
190
|
-
const Y_AXIS_LABEL_TICK_GAP = 2;
|
|
191
|
-
// A rotated (90deg) axis-label's line-box "thickness" -- what it costs in
|
|
192
|
-
// WIDTH once rotated -- runs a bit past its raw font size once ascender/
|
|
193
|
-
// descender room is included; matches the ~18px measured live for a 14px
|
|
194
|
-
// label (14 * 1.3 ~= 18).
|
|
195
|
-
const Y_AXIS_LABEL_THICKNESS_FACTOR = 1.3;
|
|
196
|
-
// A small safety margin on the estimate: `spaceForRotatedTicks`'s own risk
|
|
197
|
-
// note applies here too -- a too-tight cut recreates the overlap it was
|
|
198
|
-
// meant to fix, and glyph-width heuristics are approximate by nature.
|
|
199
|
-
const Y_AXIS_WIDTH_SAFETY_FACTOR = 1.1;
|
|
200
|
-
// MUI X-Charts' ChartsYAxisImpl hardcodes the axis-label font at 14px
|
|
201
|
-
// (`additionalProps.style: {...theme.typography.body1, fontSize: 14, ...}`
|
|
202
|
-
// -- fontSize:14 is set AFTER the theme.typography.body1 spread, so it wins
|
|
203
|
-
// regardless of what body1 actually resolves to). Matching that literal
|
|
204
|
-
// constant here -- rather than reading it off `theme.typography.body1` --
|
|
205
|
-
// keeps this sizing correct even if this package's body1 size ever changes;
|
|
206
|
-
// coupling it to body1 was only ever coincidentally right because body1
|
|
207
|
-
// happens to be 14px today.
|
|
208
|
-
const MUI_AXIS_LABEL_FONT_SIZE_PX = 14;
|
|
209
|
-
|
|
210
|
-
function longestFormattedTick(values, valueFormatter) {
|
|
211
|
-
return values.reduce((longest, value) => {
|
|
212
|
-
let text;
|
|
213
|
-
try {
|
|
214
|
-
text = valueFormatter ? String(valueFormatter(value, { location: 'tick' })) : String(value);
|
|
215
|
-
} catch {
|
|
216
|
-
// Some formatters require MUI's complete scale context; falling back
|
|
217
|
-
// to the raw value is conservative -- it reserves at least as much
|
|
218
|
-
// space as the unformatted number would need.
|
|
219
|
-
text = String(value);
|
|
220
|
-
}
|
|
221
|
-
return text.length > longest.length ? text : longest;
|
|
222
|
-
}, '');
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Sizes a linear y-axis to its actual tick content instead of MUI's flat
|
|
227
|
-
* default. Only touches axes the caller left unsized (`axis.width == null`)
|
|
228
|
-
* and only linear/numeric ones -- a categorical axis's ticks are names, not
|
|
229
|
-
* numbers, and sizing those is not this function's job. Candidate values come
|
|
230
|
-
* from the axis' own `min`/`max` when the caller set them (as every current
|
|
231
|
-
* consumer does), falling back to the series actually plotted against this
|
|
232
|
-
* axis; if neither yields a candidate, the axis is left untouched rather than
|
|
233
|
-
* guessed at.
|
|
234
|
-
*
|
|
235
|
-
* `tickFontSize` is only the FALLBACK -- an axis' own `tickLabelStyle.fontSize`
|
|
236
|
-
* (set by `withAxisDefaults`, itself overridable per-axis by the caller) is
|
|
237
|
-
* read first, so a caller-tuned tick font is sized correctly rather than
|
|
238
|
-
* against the package-wide default. The axis LABEL's font is not a caller
|
|
239
|
-
* knob at all (`MUI_AXIS_LABEL_FONT_SIZE_PX` -- MUI hardcodes it).
|
|
240
|
-
*
|
|
241
|
-
* The series-to-axis match for the fallback deliberately mirrors MUI's own
|
|
242
|
-
* default-assignment rule, not a naive `undefined === undefined`: a series
|
|
243
|
-
* with no explicit `yAxisId` is assigned to the FIRST axis in the array
|
|
244
|
-
* (`yAxisIds[0]` in MUI's own selector), never to "no id". Matching only
|
|
245
|
-
* `undefined === undefined` left the primary axis of a real dual-axis chart
|
|
246
|
-
* (id `'primary'`, its series never set `yAxisId`) with zero matched series
|
|
247
|
-
* and therefore MUI's untouched flat default -- a silent no-op on exactly
|
|
248
|
-
* the dual-axis case this WO was measured against.
|
|
249
|
-
*/
|
|
250
|
-
export function sizeYAxisForContent(yAxis, series, tickFontSize) {
|
|
251
|
-
const fallbackTickFontSizePx = cssLengthToPixels(tickFontSize);
|
|
252
|
-
const defaultAxisId = yAxis[0]?.id;
|
|
253
|
-
return yAxis.map((axis) => {
|
|
254
|
-
if (axis.width != null) return axis;
|
|
255
|
-
if (axis.scaleType && axis.scaleType !== 'linear') return axis;
|
|
256
|
-
|
|
257
|
-
const tickFontSizePx = cssLengthToPixels(axis.tickLabelStyle?.fontSize) || fallbackTickFontSizePx;
|
|
258
|
-
if (tickFontSizePx === 0) return axis;
|
|
259
|
-
|
|
260
|
-
const candidates = [];
|
|
261
|
-
if (typeof axis.min === 'number') candidates.push(axis.min);
|
|
262
|
-
if (typeof axis.max === 'number') candidates.push(axis.max);
|
|
263
|
-
if (candidates.length === 0) {
|
|
264
|
-
(series || [])
|
|
265
|
-
.filter((item) => (item.yAxisId ?? defaultAxisId) === axis.id)
|
|
266
|
-
.forEach((item) => (item.data || []).forEach((value) => {
|
|
267
|
-
if (typeof value === 'number') candidates.push(value);
|
|
268
|
-
}));
|
|
269
|
-
}
|
|
270
|
-
if (candidates.length === 0) return axis;
|
|
271
|
-
|
|
272
|
-
const longestTick = longestFormattedTick(candidates, axis.valueFormatter);
|
|
273
|
-
// UCM-CHART-12 Rule 2 (codex second-pass review finding): candidates existed, but a caller
|
|
274
|
-
// `valueFormatter` reduced every one of them to a blank string -- this is the y-axis analogue
|
|
275
|
-
// of `xAxisHasVisibleTicks`'s Access-scatter case, and must collapse the same way. Distinct
|
|
276
|
-
// from `candidates.length === 0` above (nothing to measure at all, MUI's own default applies,
|
|
277
|
-
// untouched): here there WAS content, it just renders as nothing. With no axis title either,
|
|
278
|
-
// the axis has genuinely nothing to show, so the whole band collapses to 0 -- not merely the
|
|
279
|
-
// text-width term floored, matching the x-axis band's own all-or-nothing zero.
|
|
280
|
-
if (longestTick.length === 0 && !axis.label) return { ...axis, width: 0 };
|
|
281
|
-
const tickTextWidth = longestTick.length === 0 ? 0 : Math.max(
|
|
282
|
-
tickFontSizePx,
|
|
283
|
-
longestTick.length * tickFontSizePx * AVERAGE_GLYPH_WIDTH_EM,
|
|
284
|
-
);
|
|
285
|
-
const labelThickness = axis.label
|
|
286
|
-
? Math.ceil(MUI_AXIS_LABEL_FONT_SIZE_PX * Y_AXIS_LABEL_THICKNESS_FACTOR) + Y_AXIS_LABEL_TICK_GAP
|
|
287
|
-
: 0;
|
|
288
|
-
const width = Math.ceil(
|
|
289
|
-
(tickTextWidth + Y_AXIS_TICK_SIZE + Y_AXIS_TICK_LABEL_GAP + labelThickness)
|
|
290
|
-
* Y_AXIS_WIDTH_SAFETY_FACTOR,
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
return { ...axis, width };
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Locale-correct, unit-free tick numbers as the default -- BarChart set none
|
|
299
|
-
* at all (raw numbers straight from the domain), and a hand-rolled `Intl`
|
|
300
|
-
* compact-notation default was measured to be actively wrong: German does
|
|
301
|
-
* not compact thousands at all (`12'500` stays `12'500`) and Swahili prepends
|
|
302
|
-
* a word that is WIDER than the raw number (`elfu 12.5`) -- compact notation
|
|
303
|
-
* does not solve the width problem and worsens it in one locale. Plain
|
|
304
|
-
* grouped formatting (no `notation: 'compact'`) is safe across all four
|
|
305
|
-
* locales this package's consumers use. Ticks get a short, rounded form;
|
|
306
|
-
* tooltip/legend keep full precision -- teaching screens must not round away
|
|
307
|
-
* a meaningful difference. A caller's own `valueFormatter` always wins (this
|
|
308
|
-
* is only ever used as a default, per `withAxisDefaults`' `{...defaults,
|
|
309
|
-
* ...axis}` merge).
|
|
310
|
-
*/
|
|
311
|
-
export function defaultNumericTickFormatter(locale) {
|
|
312
|
-
return (value, context) => (context?.location === 'tick'
|
|
313
|
-
? new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value)
|
|
314
|
-
: new Intl.NumberFormat(locale).format(value));
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// UCM-CHART-15: named size tokens, resolved through the theme's 8px spacing unit
|
|
318
|
-
// (`src/theme/tokens.js`'s `spacing: 8`). Through UCM-CHART-12/14, `standard` stayed pinned to
|
|
319
|
-
// the pre-existing deployed default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so that
|
|
320
|
-
// migration did not also silently redraw every already-shipped default-sized chart -- that pin
|
|
321
|
-
// was a migration guardrail, not a design verdict. The migration it protected is complete, and
|
|
322
|
-
// the operator judged the whole scale too cramped (`tall` had already forced hram to use the
|
|
323
|
-
// `height` px escape to get past the old ceiling), so this WO shifts every token up one step and
|
|
324
|
-
// adds two steps above the old `tall`, chosen rather than inherited. Documented plainly in
|
|
325
|
-
// `docs/CHART-LAYOUT.md` -- these are visible product decisions, not internal plumbing.
|
|
326
|
-
export const CHART_SIZE_SPACING_UNITS = {
|
|
327
|
-
compact: 40, standard: 50, tall: 60, extra_tall: 70, super_tall: 80,
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
// UCM-CHART-12 Rule 2: every band is its own measured content, or zero -- no band is ever a
|
|
331
|
-
// constant taken on faith. These two ARE constants, but only for the "content is present" case;
|
|
332
|
-
// each one collapses to 0 the moment its content is absent (`resolveXAxisGeometry`,
|
|
333
|
-
// `resolveChartLayout` below).
|
|
334
|
-
//
|
|
335
|
-
// ui_reviewer finding U1: before this WO, a plain (unrotated, untitled) axis got NO explicit
|
|
336
|
-
// `axis.height` at all -- MUI computed its own default internally. This resolver now always sets
|
|
337
|
-
// `axis.height` explicitly (Rule 1: nothing outside the resolver does chart arithmetic), which
|
|
338
|
-
// looked like a silent behaviour change until checked against MUI's own source
|
|
339
|
-
// (`@mui/x-charts/constants`): `DEFAULT_AXIS_SIZE_HEIGHT = 25` and `AXIS_LABEL_DEFAULT_HEIGHT =
|
|
340
|
-
// 20` -- i.e. `TICK_BAND_BASE_PX`/`AXIS_TITLE_BAND_PX` below are not an approximation, they are
|
|
341
|
-
// MUI's own constants, confirmed pixel-for-pixel. Making them explicit changes nothing about what
|
|
342
|
-
// renders for that common case; `TICK_BAND_BASE_PX + AXIS_TITLE_BAND_PX` = 45 is also exactly the
|
|
343
|
-
// pre-CHART-12 `MUI_LABELLED_X_AXIS_HEIGHT`, decomposed into the two bands Rule 2 requires instead
|
|
344
|
-
// of one opaque "labelled axis height" figure.
|
|
345
|
-
const TICK_BAND_BASE_PX = 25;
|
|
346
|
-
const AXIS_TITLE_BAND_PX = MUI_LABELLED_X_AXIS_HEIGHT - TICK_BAND_BASE_PX;
|
|
347
|
-
// One row of legend chips + gap -- MUI does not reserve this space itself (confirmed: nothing in
|
|
348
|
-
// this package's pre-CHART-12 margin/axis math ever accounted for the legend), so a visible
|
|
349
|
-
// legend growing the SVG uncounted was the sixth defect this WO's Rule 2 also closes.
|
|
350
|
-
const LEGEND_BAND_PX = 32;
|
|
351
|
-
|
|
352
|
-
// UCM-CHART-12 `xLabels: "auto"`: with no real container-width measurement available before
|
|
353
|
-
// render (the presets stay responsive -- MUI measures actual width via ResizeObserver, which this
|
|
354
|
-
// pure function cannot see), "auto" is a documented HEURISTIC, not a true fit computation: rotate
|
|
355
|
-
// once there are enough categories AND the longest one, MEASURED (same mechanism as the rotated
|
|
356
|
-
// projection below -- real DOM measurement when available, the per-glyph estimate otherwise), is
|
|
357
|
-
// wide enough that a horizontal row would plausibly collide. Codex second-pass review finding:
|
|
358
|
-
// the first cut of this heuristic used a raw character-count threshold instead of the measured
|
|
359
|
-
// width this function already has available -- replaced, since "decide from the measured labels"
|
|
360
|
-
// (Rule 3) is achievable even without a real container width to compare against. The count
|
|
361
|
-
// threshold stays as the OTHER half of the heuristic: a handful of wide labels can still fit
|
|
362
|
-
// spread across a wide chart, so width alone should not trigger rotation either.
|
|
363
|
-
const AUTO_ROTATE_TICK_COUNT = 6;
|
|
364
|
-
const AUTO_ROTATE_LABEL_WIDTH_PX = 50;
|
|
365
|
-
|
|
366
|
-
function xAxisTickContentText(axis) {
|
|
367
|
-
const data = axis?.data;
|
|
368
|
-
if (!Array.isArray(data)) return null; // no data array -- can't tell (e.g. a numeric scale); assume content
|
|
369
|
-
return longestFormattedTick(data, axis.valueFormatter);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// The Access-scatter case (WO Part A): an axis whose tick labels are all blank/absent must
|
|
373
|
-
// collapse its reserved band to zero, not keep the package's floor. `null` (no data array at
|
|
374
|
-
// all -- a linear numeric axis, whose ticks come from the scale, not a caller-supplied list)
|
|
375
|
-
// is treated as "assume content", since there is nothing here to measure as empty.
|
|
376
|
-
function xAxisHasVisibleTicks(axis) {
|
|
377
|
-
const text = xAxisTickContentText(axis);
|
|
378
|
-
return text === null || text.length > 0;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* UCM-CHART-12: decides the x-axis tick band's height and rotation angle. Replaces CHART-10/11's
|
|
383
|
-
* `spaceForRotatedTicks`/`rotatedTickMetrics` -- same measured-text-width mechanism
|
|
384
|
-
* (`measureTickTextWidthPx`, with `estimateTextWidthPx` as the no-DOM fallback and
|
|
385
|
-
* `measureTextWidth` as the test injection point), but now driven by `xLabels`
|
|
386
|
-
* ("auto"|"horizontal"|"angled") instead of a caller-supplied `tickLabelStyle.angle`, and
|
|
387
|
-
* returning 0 outright when the axis has no visible tick content (Rule 2).
|
|
388
|
-
*/
|
|
389
|
-
function resolveXAxisGeometry(axis, xLabelsMode, defaultLineHeight, measureTextWidth, fallbackTickFontSize) {
|
|
390
|
-
if (!xAxisHasVisibleTicks(axis)) return { tickBand: 0, angle: 0 };
|
|
391
|
-
|
|
392
|
-
const fontSize = cssLengthToPixels(axis?.tickLabelStyle?.fontSize) || cssLengthToPixels(fallbackTickFontSize);
|
|
393
|
-
const longestLabel = xAxisTickContentText(axis) || '';
|
|
394
|
-
const dataCount = Array.isArray(axis?.data) ? axis.data.length : 0;
|
|
395
|
-
const measure = measureTextWidth || measureTickTextWidthPx;
|
|
396
|
-
|
|
397
|
-
// Measured once, up front -- both "auto"'s rotation DECISION and the rotated-band PROJECTION
|
|
398
|
-
// below need the label's real width, and must agree on what "real" means (same measurement,
|
|
399
|
-
// same estimate fallback) or the two could disagree about the same label.
|
|
400
|
-
const labelWidthPx = fontSize > 0
|
|
401
|
-
? Math.max(fontSize, (() => {
|
|
402
|
-
const measured = measure(longestLabel, fontSize);
|
|
403
|
-
return Number.isFinite(measured) && measured > 0 ? measured : estimateTextWidthPx(longestLabel, fontSize);
|
|
404
|
-
})())
|
|
405
|
-
: 0;
|
|
406
|
-
|
|
407
|
-
let angle = 0;
|
|
408
|
-
if (xLabelsMode === 'angled') {
|
|
409
|
-
angle = Number(axis?.tickLabelStyle?.angle) || -45;
|
|
410
|
-
} else if (xLabelsMode === 'auto') {
|
|
411
|
-
const needsRotation = dataCount > AUTO_ROTATE_TICK_COUNT && labelWidthPx > AUTO_ROTATE_LABEL_WIDTH_PX;
|
|
412
|
-
angle = needsRotation ? -45 : 0;
|
|
413
|
-
} // "horizontal" always stays at angle 0
|
|
414
|
-
|
|
415
|
-
if (angle === 0) {
|
|
416
|
-
// UCM-CHART-16: derive the flat band from the actual tick font size instead of the
|
|
417
|
-
// unconditional floor -- `axis?.label` is the SAME signal `resolveChartLayout` uses to decide
|
|
418
|
-
// whether an axis title also shares this band (`xTitleBand`), so a titled axis reserves the
|
|
419
|
-
// extra room its own gap costs, precisely where the near-miss was.
|
|
420
|
-
const hasTitle = Boolean(axis?.label);
|
|
421
|
-
const flatTickBand = fontSize > 0
|
|
422
|
-
? Math.ceil(fontSize * TICK_TEXT_HEIGHT_FACTOR) + TICK_BAND_MUI_OVERHEAD_PX
|
|
423
|
-
+ (hasTitle ? TICK_BAND_TITLE_EXTRA_PX : 0)
|
|
424
|
-
: TICK_BAND_BASE_PX;
|
|
425
|
-
return { tickBand: Math.max(TICK_BAND_BASE_PX, flatTickBand), angle: 0 };
|
|
426
|
-
}
|
|
427
|
-
// Rotation is a visual instruction independent of whether a font size is knowable here -- an
|
|
428
|
-
// "angled" axis still rotates even when `fontSize` can't be resolved (no theme fallback given),
|
|
429
|
-
// it just can't project a tighter band than the floor without one.
|
|
430
|
-
if (fontSize === 0) return { tickBand: TICK_BAND_BASE_PX, angle };
|
|
431
|
-
|
|
432
|
-
const lineHeight = Number(axis?.tickLabelStyle?.lineHeight) || defaultLineHeight;
|
|
433
|
-
const textWidth = labelWidthPx;
|
|
434
|
-
const radians = (angle * Math.PI) / 180;
|
|
435
|
-
const projectedHeight = Math.abs(Math.cos(radians)) * fontSize * lineHeight
|
|
436
|
-
+ Math.abs(Math.sin(radians)) * textWidth;
|
|
437
|
-
const tickBand = Math.max(TICK_BAND_BASE_PX, Math.ceil(projectedHeight) + ROTATED_TICK_CLEARANCE_PX);
|
|
438
|
-
return { tickBand, angle };
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function resolveChartHeightPx(size, height, spacing) {
|
|
442
|
-
if (height != null) return height;
|
|
443
|
-
const units = CHART_SIZE_SPACING_UNITS[size];
|
|
444
|
-
if (units == null) {
|
|
445
|
-
throw new Error(
|
|
446
|
-
`[ui-core-micha] Unknown chart size="${size}". Use "compact" | "standard" | "tall" | `
|
|
447
|
-
+ '"extra_tall" | "super_tall", or pass height (in px) for the documented escape.',
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
// MUI's `theme.spacing(n)` returns a CSS length STRING ("320px"), not a number -- the chart
|
|
451
|
-
// height feeds straight into MUI X-Charts' own numeric `height` prop, so it must be unwrapped
|
|
452
|
-
// back to a px number via the same `cssLengthToPixels` the rest of this file already uses.
|
|
453
|
-
return typeof spacing === 'function' ? cssLengthToPixels(spacing(units)) : units * 8;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* UCM-CHART-12: the one function that owns a chart's geometry (Rule 1-3), replacing
|
|
458
|
-
* `resolveChartHeight` + `spaceForRotatedTicks` + `warnOnHeightMismatch`. Returns the COMPLETE
|
|
459
|
-
* geometry a preset needs -- wrapper `sx`, chart height, margins, and both resolved axis arrays --
|
|
460
|
-
* plus the named `bands` the composition invariant is asserted against:
|
|
461
|
-
*
|
|
462
|
-
* chartHeight === bands.plotHeight + bands.xAxisBand + bands.xTitleBand + bands.legendBand
|
|
463
|
-
* containerWidth === bands.yAxisBand + bands.plotWidth + bands.rightPad (when `containerWidth` is given)
|
|
464
|
-
*
|
|
465
|
-
* `plotHeight`/`plotWidth` are the RESIDUAL of `chartHeight`/`containerWidth` minus the OTHER
|
|
466
|
-
* fields this function itself returns (`margin.top/bottom`, `xAxis[0].height`, `yAxisBand`) --
|
|
467
|
-
* by construction, summing them back up can never fail to balance, so that arithmetic identity
|
|
468
|
-
* ALONE proves nothing about a `UCM-CHART-10`-shaped bug (a term double-counted between
|
|
469
|
-
* `xAxis.height` and `margin.bottom`). What actually catches that class of bug is asserting each
|
|
470
|
-
* individual band lands in exactly one place downstream, independently of the others:
|
|
471
|
-
* `xAxis[0].height === xAxisBand + xTitleBand` (nothing else may also carry the tick/title
|
|
472
|
-
* allowance) and `margin.top + margin.bottom - PACKAGE_DEFAULT_MARGIN.top -
|
|
473
|
-
* PACKAGE_DEFAULT_MARGIN.bottom === legendBand` (the ONLY thing margin grows for, over its own
|
|
474
|
-
* package baseline, is the legend). The test suite asserts both forms side by side.
|
|
475
|
-
*
|
|
476
|
-
* `plotHeight` deliberately does NOT subtract the base `PACKAGE_DEFAULT_MARGIN` breathing room --
|
|
477
|
-
* that margin is intentional spacing around the plot (THEME-11), not one of the three itemized
|
|
478
|
-
* "furniture" bands (tick/title/legend) this WO exists to zero out, so it is treated as part of
|
|
479
|
-
* "the rest of the chart", the same as `plotHeight` itself.
|
|
480
|
-
*
|
|
481
|
-
* `containerWidth` is optional and only meaningful for tests / the browser-level check -- the
|
|
482
|
-
* presets themselves stay responsive (no explicit width) and never pass it.
|
|
483
|
-
*/
|
|
484
|
-
export function resolveChartLayout({
|
|
485
|
-
size = 'standard',
|
|
486
|
-
height,
|
|
487
|
-
xAxis,
|
|
488
|
-
yAxis,
|
|
489
|
-
series = [],
|
|
490
|
-
xLabels = 'auto',
|
|
491
|
-
hideLegend = false,
|
|
492
|
-
legendPosition = DEFAULT_LEGEND_POSITION,
|
|
493
|
-
slotProps,
|
|
494
|
-
tickFontSize,
|
|
495
|
-
spacing,
|
|
496
|
-
defaultLineHeight = 1,
|
|
497
|
-
measureTextWidth,
|
|
498
|
-
containerWidth,
|
|
499
|
-
} = {}) {
|
|
500
|
-
const chartHeight = resolveChartHeightPx(size, height, spacing);
|
|
501
|
-
|
|
502
|
-
const xAxisList = xAxis?.length ? xAxis : [{}];
|
|
503
|
-
const primaryXAxis = xAxisList[0];
|
|
504
|
-
const { tickBand: xAxisBand, angle } = resolveXAxisGeometry(
|
|
505
|
-
primaryXAxis, xLabels, defaultLineHeight, measureTextWidth, tickFontSize,
|
|
506
|
-
);
|
|
507
|
-
const xTitleBand = primaryXAxis?.label ? AXIS_TITLE_BAND_PX : 0;
|
|
508
|
-
|
|
509
|
-
// Codex second-pass review finding: a caller-set `slotProps.legend.position` overrides
|
|
510
|
-
// `legendPosition` at RENDER time (`withChartSlotDefaults`) but was invisible here, so the
|
|
511
|
-
// margin/height this function reserved could disagree with where the legend actually rendered.
|
|
512
|
-
// `resolveLegendPosition` is the one merge both call sites now share.
|
|
513
|
-
const effectiveLegendPosition = resolveLegendPosition(legendPosition, slotProps);
|
|
514
|
-
|
|
515
|
-
// A `vertical: 'middle'` legend renders on the SIDE (consuming width, not height) -- reviewer
|
|
516
|
-
// finding: only branching on 'top' vs. not-'top' always reserved the legend band vertically,
|
|
517
|
-
// over-reserving height and under-reserving width for a side-placed legend. This WO's width
|
|
518
|
-
// invariant does not itemize a legend-width band, so a side legend simply gets none of the
|
|
519
|
-
// height reservation here rather than a wrong one -- consistent with the WO's own text ("0 when
|
|
520
|
-
// legend is off or horizontal").
|
|
521
|
-
const legendVertical = effectiveLegendPosition.vertical;
|
|
522
|
-
const legendIsSidePlaced = legendVertical === 'middle';
|
|
523
|
-
const legendHeightBand = hideLegend || legendIsSidePlaced ? 0 : LEGEND_BAND_PX;
|
|
524
|
-
const legendOnTop = legendHeightBand > 0 && legendVertical === 'top';
|
|
525
|
-
|
|
526
|
-
const sizedYAxis = sizeYAxisForContent(yAxis?.length ? yAxis : [{}], series, tickFontSize);
|
|
527
|
-
// Codex second-pass review finding: a dual-axis chart (TimeSeriesChart's `axis: 'secondary'`
|
|
528
|
-
// feature, CHART-5) has a SECOND y-axis on the right, consuming its own width -- the resolver
|
|
529
|
-
// only ever accounted for `sizedYAxis[0]`. `position` defaults to `'left'` for an unmarked axis
|
|
530
|
-
// (matching MUI's own default-assignment rule, same as `sizeYAxisForContent`'s own series-match
|
|
531
|
-
// logic above), so only an explicit `'right'` counts as the secondary axis.
|
|
532
|
-
const primaryYAxis = sizedYAxis.find((axisEntry) => axisEntry.position !== 'right') ?? sizedYAxis[0];
|
|
533
|
-
const secondaryYAxisEntry = sizedYAxis.find((axisEntry) => axisEntry.position === 'right');
|
|
534
|
-
const yAxisBand = primaryYAxis?.width ?? 0;
|
|
535
|
-
const secondaryYAxisBand = secondaryYAxisEntry?.width ?? 0;
|
|
536
|
-
const rightPad = PACKAGE_DEFAULT_MARGIN.right;
|
|
537
|
-
|
|
538
|
-
const resolvedXAxis = xAxisList.map((axisEntry, index) => {
|
|
539
|
-
if (index !== 0) return axisEntry;
|
|
540
|
-
// `xLabels` is the SOLE source of truth for rotation (Rule 3) -- `angle` is always written
|
|
541
|
-
// explicitly here, even when 0, rather than only overriding a truthy value, and regardless of
|
|
542
|
-
// whether the caller also set an explicit `height` (own `height` bypasses the model's SIZE,
|
|
543
|
-
// never its ROTATION -- codex second-pass review finding: the two were previously coupled,
|
|
544
|
-
// so a caller-set `height` could silently keep a stale rotated `tickLabelStyle.angle` even
|
|
545
|
-
// under `xLabels="horizontal"`). Leaving a stale caller-supplied angle in place when `xLabels`
|
|
546
|
-
// resolved to no rotation would let MUI still render a rotated label whose band this resolver
|
|
547
|
-
// reserved as if it were flat -- exactly the space/render mismatch this WO exists to close,
|
|
548
|
-
// just inverted (under-reservation instead of CHART-10's over-reservation).
|
|
549
|
-
const { angle: _callerAngle, ...restTickLabelStyle } = axisEntry.tickLabelStyle || {};
|
|
550
|
-
return {
|
|
551
|
-
...axisEntry,
|
|
552
|
-
height: axisEntry.height ?? (xAxisBand + xTitleBand),
|
|
553
|
-
tickLabelStyle: { ...restTickLabelStyle, angle },
|
|
554
|
-
};
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
const margin = {
|
|
558
|
-
top: PACKAGE_DEFAULT_MARGIN.top + (legendOnTop ? legendHeightBand : 0),
|
|
559
|
-
bottom: PACKAGE_DEFAULT_MARGIN.bottom + (legendOnTop ? 0 : legendHeightBand),
|
|
560
|
-
left: PACKAGE_DEFAULT_MARGIN.left,
|
|
561
|
-
right: rightPad + secondaryYAxisBand,
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
// `plotHeight`/`plotWidth`: the residual of `chartHeight`/`containerWidth` minus the three named
|
|
565
|
-
// furniture bands (see this function's own docblock for why this alone does not prove the
|
|
566
|
-
// absence of a double-counted term, and what does).
|
|
567
|
-
const plotHeight = chartHeight - xAxisBand - xTitleBand - legendHeightBand;
|
|
568
|
-
const plotWidth = containerWidth != null
|
|
569
|
-
? containerWidth - yAxisBand - secondaryYAxisBand - rightPad
|
|
570
|
-
: undefined;
|
|
571
|
-
|
|
572
|
-
return {
|
|
573
|
-
chartHeight,
|
|
574
|
-
sx: { width: '100%', height: chartHeight },
|
|
575
|
-
margin,
|
|
576
|
-
xAxis: resolvedXAxis,
|
|
577
|
-
yAxis: sizedYAxis,
|
|
578
|
-
bands: {
|
|
579
|
-
plotHeight,
|
|
580
|
-
xAxisBand,
|
|
581
|
-
secondaryYAxisBand,
|
|
582
|
-
xTitleBand,
|
|
583
|
-
legendBand: legendHeightBand,
|
|
584
|
-
yAxisBand,
|
|
585
|
-
rightPad,
|
|
586
|
-
...(containerWidth != null ? { plotWidth, containerWidth } : {}),
|
|
587
|
-
},
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* UCM-CHART-12 Rule 1/3: `minHeight`, `aspect`, and `margin` are gone from the four chart presets
|
|
593
|
-
* -- the model owns sizing and spacing completely. Passing one is a dev-mode error naming its
|
|
594
|
-
* replacement (never a silent ignore, so the 38 existing call sites this breaks get a clear
|
|
595
|
-
* migration path instead of a layout that quietly stops matching what the prop says) -- gated on
|
|
596
|
-
* `NODE_ENV !== 'production'` like the deleted `warnOnHeightMismatch` was, so a shared package
|
|
597
|
-
* cannot crash a consumer's production page over a layout prop; loud in dev, inert in prod.
|
|
598
|
-
*
|
|
599
|
-
* `removedIn` (codex review finding, UCM-CHART-13): a shared helper hardcoding ONE WO/version
|
|
600
|
-
* string would misattribute the removal for every OTHER call site that reuses it -- `ChartFrame`
|
|
601
|
-
* (UCM-CHART-13, v3.1.0) removes `height`/`aspect` for a different reason, in a different WO, than
|
|
602
|
-
* the four presets' own `minHeight`/`aspect`/`margin` (UCM-CHART-12, v3.0.0). The default keeps
|
|
603
|
-
* every existing call site's message byte-identical; only a NEW call site needs to override it.
|
|
604
|
-
*/
|
|
605
|
-
export function assertRemovedChartProp(componentName, propName, value, replacement, removedIn = 'UCM-CHART-12, v3.0.0') {
|
|
606
|
-
if (value === undefined || process.env.NODE_ENV === 'production') return;
|
|
607
|
-
throw new Error(
|
|
608
|
-
`[ui-core-micha] <${componentName}> no longer accepts "${propName}" (removed in ${removedIn} `
|
|
609
|
-
+ `-- see docs/CHART-LAYOUT.md). ${replacement}`,
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* UCM-CHART-12 (codex second-pass review finding): the single source of truth for where the
|
|
615
|
-
* legend actually renders -- `legendPosition` merged with a `slotProps.legend.position` override,
|
|
616
|
-
* same precedence `withChartSlotDefaults` already applied. Before this fix, `resolveChartLayout`
|
|
617
|
-
* only ever looked at `legendPosition` when deciding how much space to reserve and on which side,
|
|
618
|
-
* while `withChartSlotDefaults` (below) separately let `slotProps.legend.position` win at RENDER
|
|
619
|
-
* time -- a caller passing both could get a legend rendered top/side while the resolver had
|
|
620
|
-
* reserved space for bottom (or none), the exact reserved-vs-rendered mismatch this WO exists to
|
|
621
|
-
* close. Both call sites now merge through this one function.
|
|
622
|
-
*/
|
|
623
|
-
export function resolveLegendPosition(legendPosition, slotProps) {
|
|
624
|
-
return {
|
|
625
|
-
...DEFAULT_LEGEND_POSITION,
|
|
626
|
-
...legendPosition,
|
|
627
|
-
...slotProps?.legend?.position,
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
export function withChartSlotDefaults(slotProps, legendPosition) {
|
|
632
|
-
const position = resolveLegendPosition(legendPosition, slotProps);
|
|
633
|
-
return {
|
|
634
|
-
...slotProps,
|
|
635
|
-
tooltip: {
|
|
636
|
-
trigger: 'axis',
|
|
637
|
-
...slotProps?.tooltip,
|
|
638
|
-
},
|
|
639
|
-
legend: {
|
|
640
|
-
...slotProps?.legend,
|
|
641
|
-
position,
|
|
642
|
-
},
|
|
643
|
-
};
|
|
644
|
-
}
|