@micha.bigler/ui-core-micha 3.14.4 → 4.0.1
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 +29 -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 +31 -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,299 +0,0 @@
|
|
|
1
|
-
import React, { useMemo } from 'react';
|
|
2
|
-
import { Box } from '@mui/material';
|
|
3
|
-
import { useTheme } from '@mui/material/styles';
|
|
4
|
-
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import { ScatterChart as MuiScatterChart } from '@mui/x-charts/ScatterChart';
|
|
6
|
-
import { useXScale, useYScale } from '@mui/x-charts/hooks';
|
|
7
|
-
import { useNeutralChartPalette } from './palette';
|
|
8
|
-
import {
|
|
9
|
-
DEFAULT_LEGEND_POSITION,
|
|
10
|
-
assertRemovedChartProp,
|
|
11
|
-
defaultNumericTickFormatter,
|
|
12
|
-
resolveChartLayout,
|
|
13
|
-
withAxisDefaults,
|
|
14
|
-
withChartSlotDefaults,
|
|
15
|
-
withGridDefaults,
|
|
16
|
-
} from './chartDefaults';
|
|
17
|
-
|
|
18
|
-
// Small enough that hram's largest cloud (~300 points, the tested magnitude -- state it, don't
|
|
19
|
-
// imply it scales past that) doesn't fuse into a solid mass at the default (non-bubble) size.
|
|
20
|
-
const DEFAULT_MARKER_RADIUS = 5;
|
|
21
|
-
const BUBBLE_MIN_RADIUS = 3;
|
|
22
|
-
const BUBBLE_RADIUS_SCALE = 10;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* value -> radius, `max(3, 10 * sqrt(value / maxValue))` -- the formula the access-gap panel
|
|
26
|
-
* already hand-rolls (THEME-10 scope 5). Exported so a consumer can size a legend swatch or a
|
|
27
|
-
* standalone marker the same way the chart does.
|
|
28
|
-
*/
|
|
29
|
-
export function scaleBubbleRadius(value, maxValue, { minRadius = BUBBLE_MIN_RADIUS, scale = BUBBLE_RADIUS_SCALE } = {}) {
|
|
30
|
-
if (!Number.isFinite(value) || !Number.isFinite(maxValue) || maxValue <= 0) return minRadius;
|
|
31
|
-
return Math.max(minRadius, scale * Math.sqrt(Math.max(0, value) / maxValue));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// A custom MUI marker slot -- the only way to vary radius/colour/shape PER POINT: MUI's own
|
|
35
|
-
// Scatter passes a single `markerSize` per whole series (confirmed against
|
|
36
|
-
// @mui/x-charts/ScatterChart/Scatter.js -- `size: series.markerSize`, never derived from a
|
|
37
|
-
// point's own `z`), so bubble sizing and per-point overrides (a hollow status-quo marker, a
|
|
38
|
-
// continuous-scale colour) can only be expressed by replacing the marker itself.
|
|
39
|
-
function makeMarkerSlot(seriesById, { sizeAccessor, chartMax, getPointStyle }) {
|
|
40
|
-
return function PresetScatterMarker({
|
|
41
|
-
seriesId, dataIndex, color, x, y, isHighlighted, isFaded, size, onClick, ...rest
|
|
42
|
-
}) {
|
|
43
|
-
const point = seriesById[seriesId]?.data?.[dataIndex];
|
|
44
|
-
const style = point && getPointStyle ? (getPointStyle(point, { seriesId, dataIndex }) || {}) : {};
|
|
45
|
-
const radius = style.radius
|
|
46
|
-
?? (sizeAccessor && point ? scaleBubbleRadius(sizeAccessor(point), chartMax) : size);
|
|
47
|
-
const r = (isHighlighted ? 1.15 : 1) * radius;
|
|
48
|
-
const fill = style.hollow ? 'none' : (style.color ?? color);
|
|
49
|
-
const stroke = style.color ?? color;
|
|
50
|
-
const shared = {
|
|
51
|
-
onClick,
|
|
52
|
-
opacity: isFaded ? 0.3 : 1,
|
|
53
|
-
stroke,
|
|
54
|
-
strokeWidth: style.hollow ? 2 : (style.strokeWidth ?? 1),
|
|
55
|
-
style: { cursor: onClick ? 'pointer' : 'unset' },
|
|
56
|
-
...rest,
|
|
57
|
-
};
|
|
58
|
-
if (style.shape === 'square') {
|
|
59
|
-
return <rect x={x - r} y={y - r} width={r * 2} height={r * 2} fill={fill} {...shared} />;
|
|
60
|
-
}
|
|
61
|
-
if (style.shape === 'diamond') {
|
|
62
|
-
return <polygon points={`${x},${y - r} ${x + r},${y} ${x},${y + r} ${x - r},${y}`} fill={fill} {...shared} />;
|
|
63
|
-
}
|
|
64
|
-
return <circle cx={x} cy={y} r={r} fill={fill} {...shared} />;
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* A computed curve (allocation's Pareto envelope) drawn inside the chart's own live coordinate
|
|
70
|
-
* space. Render as a `ScatterChart` child -- `useXScale`/`useYScale` only resolve inside the
|
|
71
|
-
* chart's own context, so this can never drift into a second, hand-rolled pixel mapping the way
|
|
72
|
-
* the hand-drawn panels this preset replaces each built their own.
|
|
73
|
-
*/
|
|
74
|
-
export function ScatterReferenceCurve({ points, color, dashed, strokeWidth = 2 }) {
|
|
75
|
-
const xScale = useXScale();
|
|
76
|
-
const yScale = useYScale();
|
|
77
|
-
if (!points || points.length < 2) return null;
|
|
78
|
-
const d = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${xScale(p.x)},${yScale(p.y)}`).join(' ');
|
|
79
|
-
return <path d={d} fill="none" stroke={color} strokeWidth={strokeWidth} strokeDasharray={dashed ? '6 3' : undefined} />;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* A straight reference line with an optional inline label -- the access panel's labelled y=x
|
|
84
|
-
* "no gap" diagonal, or optimization's dashed threshold line. Same live-scale rule as
|
|
85
|
-
* `ScatterReferenceCurve`.
|
|
86
|
-
*/
|
|
87
|
-
export function ScatterReferenceLine({
|
|
88
|
-
from, to, color, dashed, strokeWidth = 1.5, label, labelAt, labelAngle = 0,
|
|
89
|
-
}) {
|
|
90
|
-
const xScale = useXScale();
|
|
91
|
-
const yScale = useYScale();
|
|
92
|
-
const labelX = labelAt ? xScale(labelAt.x) : null;
|
|
93
|
-
const labelY = labelAt ? yScale(labelAt.y) : null;
|
|
94
|
-
return (
|
|
95
|
-
<g>
|
|
96
|
-
<line
|
|
97
|
-
x1={xScale(from.x)}
|
|
98
|
-
y1={yScale(from.y)}
|
|
99
|
-
x2={xScale(to.x)}
|
|
100
|
-
y2={yScale(to.y)}
|
|
101
|
-
stroke={color}
|
|
102
|
-
strokeWidth={strokeWidth}
|
|
103
|
-
strokeDasharray={dashed ? '5 3' : undefined}
|
|
104
|
-
/>
|
|
105
|
-
{label && labelAt && (
|
|
106
|
-
<text
|
|
107
|
-
x={labelX}
|
|
108
|
-
y={labelY}
|
|
109
|
-
fontSize={10}
|
|
110
|
-
fill={color}
|
|
111
|
-
textAnchor="start"
|
|
112
|
-
transform={labelAngle ? `rotate(${labelAngle}, ${labelX}, ${labelY})` : undefined}
|
|
113
|
-
>
|
|
114
|
-
{label}
|
|
115
|
-
</text>
|
|
116
|
-
)}
|
|
117
|
-
</g>
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Responsive MUI X-Charts scatter preset, drawn from three real consumers (THEME-10): a
|
|
123
|
-
* continuously-coloured cost/outcome cloud with an envelope curve and marked points
|
|
124
|
-
* (allocation performance), a bubble cloud with a labelled y=x reference diagonal and
|
|
125
|
-
* categorical per-point colouring (access gap), and a cloud with a dashed reference line and a
|
|
126
|
-
* hollow status-quo marker (optimization results). Reuses BarChart/LineChart's own axis
|
|
127
|
-
* machinery (THEME-9) rather than sizing its own -- a scatter axis is a numeric axis like any
|
|
128
|
-
* other, and a preset that re-derives axis sizing would re-open exactly what THEME-9 closed.
|
|
129
|
-
*
|
|
130
|
-
* `series` follows MUI's own scatter shape (`{id, label, data: [{x,y,z?,id?}], color?}`).
|
|
131
|
-
* Multiple series render as a categorical cloud with a discrete legend, one colour per series
|
|
132
|
-
* -- draw those colours from the app's own family/role register via `palette`; this package
|
|
133
|
-
* does not invent one. A single series with no explicit `color` renders as one neutral mass:
|
|
134
|
-
* undifferentiated data encodes no dimension, so it gets the role register's neutral tone
|
|
135
|
-
* (`palette.js`'s `neutral`), never a series/KPI identity hue.
|
|
136
|
-
*
|
|
137
|
-
* `sizeAccessor(point) => number` turns on bubble mode: marker radius follows
|
|
138
|
-
* `scaleBubbleRadius`, and every series' points are drawn largest-first so a small bubble is
|
|
139
|
-
* never hidden under a large one -- the z-order is the whole correctness of a bubble chart, not
|
|
140
|
-
* a decoration.
|
|
141
|
-
*
|
|
142
|
-
* `getPointStyle(point, {seriesId, dataIndex}) => {hollow?, color?, shape?, radius?,
|
|
143
|
-
* strokeWidth?}` marks individual points -- a status-quo point drawn hollow against filled
|
|
144
|
-
* candidates (allocation, optimization), a `'square'` shape for a source category the data
|
|
145
|
-
* itself marks (allocation's "targeted" points), or a per-point colour from a continuous scale
|
|
146
|
-
* the caller computed (this package does not ship a continuous colour-scale/legend component;
|
|
147
|
-
* the caller already owns that logic and only needs a way to apply the resolved colour per
|
|
148
|
-
* point, which this is it). Read the `point` object itself, not `dataIndex`, to correlate a
|
|
149
|
-
* style back to your own data -- in bubble mode (`sizeAccessor` set) `dataIndex` indexes the
|
|
150
|
-
* chart's internal z-ordered copy of your series, not your original array's order.
|
|
151
|
-
*
|
|
152
|
-
* Reference geometry (a computed curve, a straight line, either with an optional label) is
|
|
153
|
-
* composed as `children` using `ScatterReferenceCurve`/`ScatterReferenceLine` above.
|
|
154
|
-
*
|
|
155
|
-
* `size` (UCM-CHART-12, UCM-CHART-15): `"compact" | "standard" | "tall" | "extra_tall" |
|
|
156
|
-
* "super_tall"`, resolved through the theme's spacing scale by `resolveChartLayout`. `height`
|
|
157
|
-
* (px) is the documented escape for a justified special case -- prefer `size`.
|
|
158
|
-
* `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
|
|
159
|
-
*
|
|
160
|
-
* Tested magnitude: a few hundred marks (hram's largest cloud is ~300 points). Not verified,
|
|
161
|
-
* and not implied to hold, at thousands+.
|
|
162
|
-
*
|
|
163
|
-
* THEME-10 was commissioned before its requirement set closed -- a forthcoming "paired point"
|
|
164
|
-
* shape (optimization's MILP-vs-simulated pair, two marks joined by a connector) is named but
|
|
165
|
-
* explicitly NOT built here. Nothing below assumes a datum produces exactly one mark: a point
|
|
166
|
-
* object may carry arbitrary extra fields beyond `{x,y,z,id}` (as `getPointStyle`'s own status-
|
|
167
|
-
* quo/bubble-radius usage already does), `getPointStyle`/`sizeAccessor` key off that datum, not
|
|
168
|
-
* off a fixed mark count, and reference geometry is composed via `children`, not baked into the
|
|
169
|
-
* per-point contract. A pair variant can therefore layer on later -- extra series entries plus a
|
|
170
|
-
* connector drawn through `children` -- without reshaping `series` or either callback's contract.
|
|
171
|
-
*/
|
|
172
|
-
export function ScatterChart({
|
|
173
|
-
series = [],
|
|
174
|
-
xAxisLabel,
|
|
175
|
-
yAxisLabel,
|
|
176
|
-
xAxis,
|
|
177
|
-
yAxis,
|
|
178
|
-
palette,
|
|
179
|
-
size = 'standard',
|
|
180
|
-
height,
|
|
181
|
-
minHeight,
|
|
182
|
-
aspect,
|
|
183
|
-
margin,
|
|
184
|
-
xLabels = 'auto',
|
|
185
|
-
grid,
|
|
186
|
-
hideLegend = series.length <= 1,
|
|
187
|
-
legendPosition = DEFAULT_LEGEND_POSITION,
|
|
188
|
-
sizeAccessor,
|
|
189
|
-
getPointStyle,
|
|
190
|
-
markerSize = DEFAULT_MARKER_RADIUS,
|
|
191
|
-
slotProps,
|
|
192
|
-
children,
|
|
193
|
-
...chartProps
|
|
194
|
-
}) {
|
|
195
|
-
const theme = useTheme();
|
|
196
|
-
const { i18n } = useTranslation();
|
|
197
|
-
const neutralPalette = useNeutralChartPalette();
|
|
198
|
-
|
|
199
|
-
assertRemovedChartProp('ScatterChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall" | "extra_tall" | "super_tall", or height for the documented escape.');
|
|
200
|
-
assertRemovedChartProp('ScatterChart', 'aspect', aspect, 'Removed with no replacement -- pick a size token; the chart no longer tracks width.');
|
|
201
|
-
assertRemovedChartProp('ScatterChart', 'margin', margin, 'Removed -- the layout model owns margins completely.');
|
|
202
|
-
|
|
203
|
-
const labelledXAxis = withAxisDefaults(
|
|
204
|
-
xAxis,
|
|
205
|
-
xAxisLabel,
|
|
206
|
-
{ scaleType: 'linear', valueFormatter: defaultNumericTickFormatter(i18n.language) },
|
|
207
|
-
theme.typography.caption.fontSize,
|
|
208
|
-
);
|
|
209
|
-
const labelledYAxis = withAxisDefaults(
|
|
210
|
-
yAxis,
|
|
211
|
-
yAxisLabel,
|
|
212
|
-
{ scaleType: 'linear', valueFormatter: defaultNumericTickFormatter(i18n.language) },
|
|
213
|
-
theme.typography.caption.fontSize,
|
|
214
|
-
);
|
|
215
|
-
// `resolveChartLayout`'s own y-axis sizing (via `sizeYAxisForContent`) expects `series[i].data`
|
|
216
|
-
// to be a plain array of numbers -- true for BarChart/LineChart, but a scatter's data is
|
|
217
|
-
// `{x,y,z,id}` points. Adapt AT THIS CALL SITE, not inside the shared resolver -- BarChart/
|
|
218
|
-
// LineChart's own contract must not change for this (reviewer finding, live-caught under
|
|
219
|
-
// THEME-10: the untested case silently found zero candidates and fell back to MUI's flat
|
|
220
|
-
// default at a magnitude that didn't visibly overlap).
|
|
221
|
-
const yValueSeries = useMemo(
|
|
222
|
-
() => series.map((item) => ({ ...item, data: (item.data || []).map((point) => point?.y) })),
|
|
223
|
-
[series],
|
|
224
|
-
);
|
|
225
|
-
const layout = resolveChartLayout({
|
|
226
|
-
size,
|
|
227
|
-
height,
|
|
228
|
-
xAxis: labelledXAxis,
|
|
229
|
-
yAxis: labelledYAxis,
|
|
230
|
-
series: yValueSeries,
|
|
231
|
-
xLabels,
|
|
232
|
-
hideLegend,
|
|
233
|
-
legendPosition,
|
|
234
|
-
slotProps,
|
|
235
|
-
tickFontSize: theme.typography.caption.fontSize,
|
|
236
|
-
spacing: theme.spacing,
|
|
237
|
-
defaultLineHeight: theme.typography.caption.lineHeight,
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
const isNeutral = series.length <= 1 && !series[0]?.color;
|
|
241
|
-
const resolvedPalette = palette || (isNeutral ? [neutralPalette.neutral] : neutralPalette.categorical);
|
|
242
|
-
|
|
243
|
-
// A single chart-wide max, NOT one per series: population (or whatever sizeAccessor reads) is
|
|
244
|
-
// one comparable scale across the whole cloud (matches the access-gap panel's own
|
|
245
|
-
// `maxPop = Math.max(1, ...points.map(p => p.population))`, computed over every point
|
|
246
|
-
// regardless of division/settlement) -- a per-series max would let each series' own largest
|
|
247
|
-
// point always render at full radius, making cross-series bubble size meaningless.
|
|
248
|
-
const chartMax = useMemo(() => {
|
|
249
|
-
if (!sizeAccessor) return 1;
|
|
250
|
-
return Math.max(
|
|
251
|
-
1,
|
|
252
|
-
...series.flatMap((item) => (item.data || []).map((point) => sizeAccessor(point))).filter(Number.isFinite),
|
|
253
|
-
);
|
|
254
|
-
}, [series, sizeAccessor]);
|
|
255
|
-
|
|
256
|
-
// THEME-10 scope 5: explicit z-order -- largest bubbles drawn first so smaller ones are never
|
|
257
|
-
// hidden underneath (SVG paints later elements on top). A no-op outside bubble mode.
|
|
258
|
-
const orderedSeries = useMemo(() => (
|
|
259
|
-
sizeAccessor
|
|
260
|
-
? series.map((item) => ({
|
|
261
|
-
...item,
|
|
262
|
-
data: [...(item.data || [])].sort((a, b) => sizeAccessor(b) - sizeAccessor(a)),
|
|
263
|
-
}))
|
|
264
|
-
: series
|
|
265
|
-
), [series, sizeAccessor]);
|
|
266
|
-
|
|
267
|
-
// The marker's `dataIndex` refers to a position in whatever was actually handed to MUI --
|
|
268
|
-
// `orderedSeries` (post z-order sort), not the caller's original `series` -- so the lookup
|
|
269
|
-
// table the marker closes over must be built from that SAME array, or a sorted bubble series
|
|
270
|
-
// would resolve every point's style/radius against the wrong (pre-sort) index.
|
|
271
|
-
const orderedSeriesById = useMemo(
|
|
272
|
-
() => Object.fromEntries(orderedSeries.map((item) => [item.id, item])),
|
|
273
|
-
[orderedSeries],
|
|
274
|
-
);
|
|
275
|
-
const markerSlot = useMemo(
|
|
276
|
-
() => makeMarkerSlot(orderedSeriesById, { sizeAccessor, chartMax, getPointStyle }),
|
|
277
|
-
[orderedSeriesById, sizeAccessor, chartMax, getPointStyle],
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
return (
|
|
281
|
-
<Box data-testid="scatter-chart-container" sx={layout.sx}>
|
|
282
|
-
<MuiScatterChart
|
|
283
|
-
{...chartProps}
|
|
284
|
-
height={layout.chartHeight}
|
|
285
|
-
series={orderedSeries.map((item) => ({ markerSize, ...item }))}
|
|
286
|
-
xAxis={layout.xAxis}
|
|
287
|
-
yAxis={layout.yAxis}
|
|
288
|
-
colors={resolvedPalette}
|
|
289
|
-
grid={withGridDefaults(grid)}
|
|
290
|
-
hideLegend={hideLegend}
|
|
291
|
-
margin={layout.margin}
|
|
292
|
-
slots={{ marker: markerSlot }}
|
|
293
|
-
slotProps={withChartSlotDefaults(slotProps, legendPosition)}
|
|
294
|
-
>
|
|
295
|
-
{children}
|
|
296
|
-
</MuiScatterChart>
|
|
297
|
-
</Box>
|
|
298
|
-
);
|
|
299
|
-
}
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Box,
|
|
4
|
-
Checkbox,
|
|
5
|
-
FormControlLabel,
|
|
6
|
-
FormGroup,
|
|
7
|
-
Radio,
|
|
8
|
-
RadioGroup,
|
|
9
|
-
} from '@mui/material';
|
|
10
|
-
import { useTranslation } from 'react-i18next';
|
|
11
|
-
import { ChartFrame } from './ChartFrame';
|
|
12
|
-
import { BarChart } from './BarChart';
|
|
13
|
-
import { useNeutralChartPalette } from './palette';
|
|
14
|
-
|
|
15
|
-
// Fixed platform-wide range -> granularity mapping (operator decision, CHART-2 /
|
|
16
|
-
// dcm ACT-1) — the resolution follows the window and is never an independent
|
|
17
|
-
// caller-supplied control. Every preset lands between a dozen and fifty points.
|
|
18
|
-
// Preset default height (CHART-4): MUI X-Charts' responsive container
|
|
19
|
-
// measures its actual parent height via ResizeObserver and draws nothing
|
|
20
|
-
// if that parent has none — ChartFrame's flex-centered content Box does
|
|
21
|
-
// NOT stretch its child to fill available height (alignItems: 'center'),
|
|
22
|
-
// so BarChart needs its own explicit size, not just ChartFrame's.
|
|
23
|
-
// `height` forwards straight to MuiBarChart's own native height prop
|
|
24
|
-
// (bypassing ResizeObserver-based measurement entirely), matching the
|
|
25
|
-
// confirmed-working dev/entries.jsx BarChartEntry reference.
|
|
26
|
-
// Deliberate, known exception to DESIGN.md #14 ("responsive = container-
|
|
27
|
-
// sized, not fixed px"): width stays responsive (unset, still auto-
|
|
28
|
-
// measured), only height is fixed, and only because MUI X-Charts' own
|
|
29
|
-
// ResizeObserver-based height measurement is what silently produced a
|
|
30
|
-
// zero-height chart in the first place (CHART-4). Do not "fix" this back
|
|
31
|
-
// to a responsive/aspect-ratio height without re-verifying the underlying
|
|
32
|
-
// MUI measurement bug is actually resolved.
|
|
33
|
-
//
|
|
34
|
-
// UCM-CHART-12: `CHART_HEIGHT` (320) is the number `"standard"` (BarChart's new `size` token) was
|
|
35
|
-
// deliberately pinned to, specifically so this preset's own default did not silently move under
|
|
36
|
-
// this migration -- still used here as ChartFrame's own `minHeight` floor (a card concern,
|
|
37
|
-
// unaffected by CHART-12), and the inner BarChart now takes `size="standard"` instead of `height`.
|
|
38
|
-
const CHART_HEIGHT = 320;
|
|
39
|
-
|
|
40
|
-
// Dynamic sx factories are outside the registry's documented top-level-object
|
|
41
|
-
// lower bound; the series colour is data-dependent at render time.
|
|
42
|
-
const seriesToggleSx = (colour) => ({
|
|
43
|
-
color: colour,
|
|
44
|
-
'&.Mui-checked': { color: colour },
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// CHART-5: how many x-axis tick labels to show, evenly spread across
|
|
48
|
-
// data.xLabels, regardless of container width or label length. Replaces
|
|
49
|
-
// MUI's own collision-based tick filtering (which, given enough densely
|
|
50
|
-
// packed categories, can end up hiding every single label — confirmed live
|
|
51
|
-
// with 24 hourly buckets) with a deterministic guarantee that some labels
|
|
52
|
-
// always render. See makeTickLabelInterval below.
|
|
53
|
-
const MAX_X_TICKS = 8;
|
|
54
|
-
|
|
55
|
-
function makeTickLabelInterval(labelCount) {
|
|
56
|
-
const step = Math.max(1, Math.ceil(labelCount / MAX_X_TICKS));
|
|
57
|
-
const lastIndex = labelCount - 1;
|
|
58
|
-
// `index % step === 0` alone systematically drops the final bucket unless
|
|
59
|
-
// it happens to land exactly on a step boundary (e.g. 24 labels / step 3
|
|
60
|
-
// covers indices 0..21, never 23) -- the most recent time bucket is
|
|
61
|
-
// usually the one that matters most, so it is always force-included.
|
|
62
|
-
return (_value, index) => index % step === 0 || index === lastIndex;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// CHART-5: a y-axis is "integer" when every value across its currently
|
|
66
|
-
// visible series is a whole number — re-evaluated on every toggle (an axis
|
|
67
|
-
// can flip from fractional to integer, or vice versa, as series are shown/
|
|
68
|
-
// hidden). Empty (no visible series on this axis) is deliberately NOT
|
|
69
|
-
// integer: there's nothing to format, and treating an empty list as
|
|
70
|
-
// vacuously "all integer" would apply the formatter to an axis with no data.
|
|
71
|
-
function isIntegerAxis(seriesList) {
|
|
72
|
-
return seriesList.length > 0 && seriesList.every(
|
|
73
|
-
(series) => (series.data || []).every((value) => Number.isInteger(value)),
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Blanks the tick label for a non-integer value on an axis whose visible
|
|
78
|
-
// series are otherwise all whole numbers -- a deterministic backstop since
|
|
79
|
-
// d3's own "nice tick" choice for a small integer domain can still land on
|
|
80
|
-
// a fractional position (e.g. domain [0,3] ticking at 0, 1.5, 3). Tooltip/
|
|
81
|
-
// legend formatting (a different `location`) is left untouched -- only the
|
|
82
|
-
// on-axis tick text is ever blanked.
|
|
83
|
-
function integerTickFormatter(value, context) {
|
|
84
|
-
if (context?.location !== 'tick') return String(value);
|
|
85
|
-
return Number.isInteger(value) ? String(value) : '';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const RANGE_OPTIONS = [
|
|
89
|
-
{ key: '1d', granularity: 'hour', labelKey: 'TimeSeriesChart.RANGE_1_DAY' },
|
|
90
|
-
{ key: '1w', granularity: '4hour', labelKey: 'TimeSeriesChart.RANGE_1_WEEK' },
|
|
91
|
-
{ key: '1m', granularity: 'day', labelKey: 'TimeSeriesChart.RANGE_1_MONTH' },
|
|
92
|
-
{ key: '1y', granularity: 'month', labelKey: 'TimeSeriesChart.RANGE_1_YEAR' },
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
function SeriesToggleDot({ color, active }) {
|
|
96
|
-
return (
|
|
97
|
-
<Box
|
|
98
|
-
component="span"
|
|
99
|
-
data-testid="series-colour-dot"
|
|
100
|
-
sx={{
|
|
101
|
-
width: (theme) => theme.spacing(1.5),
|
|
102
|
-
height: (theme) => theme.spacing(1.5),
|
|
103
|
-
borderRadius: '50%',
|
|
104
|
-
bgcolor: color,
|
|
105
|
-
opacity: active ? 1 : 0.35,
|
|
106
|
-
}}
|
|
107
|
-
/>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* A time-series preset on ChartFrame + BarChart: a range picker (emits the
|
|
113
|
-
* selected range AND its mapped granularity to the host) and series toggle
|
|
114
|
-
* checkboxes, in the frame's toolbar slot.
|
|
115
|
-
*
|
|
116
|
-
* Presentational only — takes `data` and callbacks, never fetches. The host
|
|
117
|
-
* owns fetching; this component owns which range is selected and which
|
|
118
|
-
* series are visible, telling the host what changed via `onRangeChange`.
|
|
119
|
-
*
|
|
120
|
-
* Second y-axis is opt-in per series (CHART-5, reversing CHART-2's original
|
|
121
|
-
* "toggles only" choice on operator instruction): a series with
|
|
122
|
-
* `axis: 'secondary'` plots against a second, independently-scaled y-axis
|
|
123
|
-
* (requires `secondaryYAxisLabel`). Omit `axis` (or set it to `'primary'`)
|
|
124
|
-
* on every series for today's single-shared-axis behaviour, unchanged.
|
|
125
|
-
*/
|
|
126
|
-
export function TimeSeriesChart({
|
|
127
|
-
title,
|
|
128
|
-
subtitle,
|
|
129
|
-
xAxisLabel,
|
|
130
|
-
yAxisLabel,
|
|
131
|
-
secondaryYAxisLabel,
|
|
132
|
-
data,
|
|
133
|
-
loading = false,
|
|
134
|
-
error = false,
|
|
135
|
-
onRangeChange,
|
|
136
|
-
defaultRange = '1w',
|
|
137
|
-
skipAnimation,
|
|
138
|
-
}) {
|
|
139
|
-
const { t } = useTranslation();
|
|
140
|
-
const palette = useNeutralChartPalette().categorical;
|
|
141
|
-
const seriesConfig = data?.series || [];
|
|
142
|
-
|
|
143
|
-
const [range, setRange] = useState(defaultRange);
|
|
144
|
-
// Series identity (keys) is NOT assumed stable at mount: the host commonly
|
|
145
|
-
// mounts with empty data and fills it asynchronously (fetch-after-mount),
|
|
146
|
-
// so visibleKeys must sync as new keys appear rather than being captured
|
|
147
|
-
// once. seenKeys tracks every key ever observed so that a key present in
|
|
148
|
-
// seriesConfig but absent from visibleKeys is recognized as a deliberate
|
|
149
|
-
// user toggle-off (stays off) rather than treated as new (defaulted
|
|
150
|
-
// visible) on the next data update. Both are plain state (not a ref) and
|
|
151
|
-
// updated together in the render body — React's documented pattern for
|
|
152
|
-
// syncing derived state from props — so an interrupted/discarded render
|
|
153
|
-
// (concurrent features) rolls both back together instead of leaving
|
|
154
|
-
// seenKeys mutated while the paired visibleKeys update is dropped.
|
|
155
|
-
const [seenKeys, setSeenKeys] = useState(() => new Set());
|
|
156
|
-
const [visibleKeys, setVisibleKeys] = useState(() => new Set());
|
|
157
|
-
|
|
158
|
-
const currentKeys = seriesConfig.map((series) => series.key);
|
|
159
|
-
const newKeys = currentKeys.filter((key) => !seenKeys.has(key));
|
|
160
|
-
if (newKeys.length > 0) {
|
|
161
|
-
setSeenKeys((previous) => {
|
|
162
|
-
const next = new Set(previous);
|
|
163
|
-
newKeys.forEach((key) => next.add(key));
|
|
164
|
-
return next;
|
|
165
|
-
});
|
|
166
|
-
setVisibleKeys((previous) => {
|
|
167
|
-
const next = new Set(previous);
|
|
168
|
-
newKeys.forEach((key) => next.add(key));
|
|
169
|
-
return next;
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const handleRangeChange = (event) => {
|
|
174
|
-
const nextRange = event.target.value;
|
|
175
|
-
setRange(nextRange);
|
|
176
|
-
const option = RANGE_OPTIONS.find((candidate) => candidate.key === nextRange);
|
|
177
|
-
onRangeChange?.(nextRange, option?.granularity);
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const toggleSeries = (key) => {
|
|
181
|
-
setVisibleKeys((previous) => {
|
|
182
|
-
const next = new Set(previous);
|
|
183
|
-
if (next.has(key)) next.delete(key);
|
|
184
|
-
else next.add(key);
|
|
185
|
-
return next;
|
|
186
|
-
});
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const visibleSeries = seriesConfig.filter((series) => visibleKeys.has(series.key));
|
|
190
|
-
// Preserve each series' color by its ORIGINAL index, not the filtered
|
|
191
|
-
// position — otherwise toggling one series off shifts every remaining
|
|
192
|
-
// series' color, breaking the swatch-to-series identity the toggles rely on.
|
|
193
|
-
const visiblePalette = visibleSeries.map(
|
|
194
|
-
(series) => palette[seriesConfig.indexOf(series) % palette.length],
|
|
195
|
-
);
|
|
196
|
-
const isDataEmpty = seriesConfig.length === 0 || visibleSeries.length === 0;
|
|
197
|
-
|
|
198
|
-
const hasSecondaryAxis = seriesConfig.some((series) => series.axis === 'secondary');
|
|
199
|
-
if (hasSecondaryAxis && !secondaryYAxisLabel) {
|
|
200
|
-
throw new Error('TimeSeriesChart requires secondaryYAxisLabel when a series uses axis: "secondary".');
|
|
201
|
-
}
|
|
202
|
-
const primaryVisibleSeries = visibleSeries.filter((series) => series.axis !== 'secondary');
|
|
203
|
-
const secondaryVisibleSeries = visibleSeries.filter((series) => series.axis === 'secondary');
|
|
204
|
-
const yAxis = hasSecondaryAxis
|
|
205
|
-
? [
|
|
206
|
-
{
|
|
207
|
-
id: 'primary',
|
|
208
|
-
label: yAxisLabel,
|
|
209
|
-
// MUI defaultizes an axis with no explicit `position` to 'left' ONLY
|
|
210
|
-
// for the first entry in the array -- every subsequent entry
|
|
211
|
-
// defaults to 'none' (never drawn) unless given one explicitly.
|
|
212
|
-
// Both are set here, not just 'secondary', so this stays correct
|
|
213
|
-
// even if the array order ever changes.
|
|
214
|
-
position: 'left',
|
|
215
|
-
...(isIntegerAxis(primaryVisibleSeries) ? { valueFormatter: integerTickFormatter } : {}),
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
id: 'secondary',
|
|
219
|
-
label: secondaryYAxisLabel,
|
|
220
|
-
position: 'right',
|
|
221
|
-
...(isIntegerAxis(secondaryVisibleSeries) ? { valueFormatter: integerTickFormatter } : {}),
|
|
222
|
-
},
|
|
223
|
-
]
|
|
224
|
-
: (isIntegerAxis(primaryVisibleSeries) ? [{ valueFormatter: integerTickFormatter }] : undefined);
|
|
225
|
-
|
|
226
|
-
const toolbar = (
|
|
227
|
-
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3, alignItems: 'center' }}>
|
|
228
|
-
<RadioGroup
|
|
229
|
-
row
|
|
230
|
-
value={range}
|
|
231
|
-
onChange={handleRangeChange}
|
|
232
|
-
aria-label={t('TimeSeriesChart.RANGE_LABEL')}
|
|
233
|
-
>
|
|
234
|
-
{RANGE_OPTIONS.map((option) => (
|
|
235
|
-
<FormControlLabel
|
|
236
|
-
key={option.key}
|
|
237
|
-
value={option.key}
|
|
238
|
-
control={<Radio size="small" />}
|
|
239
|
-
label={t(option.labelKey)}
|
|
240
|
-
/>
|
|
241
|
-
))}
|
|
242
|
-
</RadioGroup>
|
|
243
|
-
{seriesConfig.length > 0 && (
|
|
244
|
-
<FormGroup row aria-label={t('TimeSeriesChart.SERIES_LABEL')}>
|
|
245
|
-
{seriesConfig.map((series, index) => (
|
|
246
|
-
<FormControlLabel
|
|
247
|
-
key={series.key}
|
|
248
|
-
control={(
|
|
249
|
-
<Checkbox
|
|
250
|
-
size="small"
|
|
251
|
-
checked={visibleKeys.has(series.key)}
|
|
252
|
-
onChange={() => toggleSeries(series.key)}
|
|
253
|
-
icon={<SeriesToggleDot color={palette[index % palette.length]} active={false} />}
|
|
254
|
-
checkedIcon={<SeriesToggleDot color={palette[index % palette.length]} active />}
|
|
255
|
-
sx={seriesToggleSx(palette[index % palette.length])}
|
|
256
|
-
/>
|
|
257
|
-
)}
|
|
258
|
-
label={series.label}
|
|
259
|
-
/>
|
|
260
|
-
))}
|
|
261
|
-
</FormGroup>
|
|
262
|
-
)}
|
|
263
|
-
</Box>
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
return (
|
|
267
|
-
<ChartFrame
|
|
268
|
-
title={title}
|
|
269
|
-
subtitle={subtitle}
|
|
270
|
-
toolbar={toolbar}
|
|
271
|
-
loading={loading}
|
|
272
|
-
error={error}
|
|
273
|
-
isEmpty={isDataEmpty}
|
|
274
|
-
minHeight={CHART_HEIGHT}
|
|
275
|
-
>
|
|
276
|
-
{!isDataEmpty && (
|
|
277
|
-
<BarChart
|
|
278
|
-
xAxisLabel={xAxisLabel}
|
|
279
|
-
yAxisLabel={yAxisLabel}
|
|
280
|
-
xAxis={[{
|
|
281
|
-
data: data?.xLabels || [],
|
|
282
|
-
tickLabelInterval: makeTickLabelInterval((data?.xLabels || []).length),
|
|
283
|
-
}]}
|
|
284
|
-
yAxis={yAxis}
|
|
285
|
-
series={visibleSeries.map((series) => ({
|
|
286
|
-
data: series.data,
|
|
287
|
-
label: series.label,
|
|
288
|
-
...(hasSecondaryAxis && series.axis === 'secondary' ? { yAxisId: 'secondary' } : {}),
|
|
289
|
-
}))}
|
|
290
|
-
palette={visiblePalette}
|
|
291
|
-
size="standard"
|
|
292
|
-
hideLegend={seriesConfig.length > 0}
|
|
293
|
-
skipAnimation={skipAnimation}
|
|
294
|
-
/>
|
|
295
|
-
)}
|
|
296
|
-
</ChartFrame>
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export default TimeSeriesChart;
|