gloomberb 0.10.1 → 0.10.2
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/package.json +8 -1
- package/src/api-client/auth.ts +1 -0
- package/src/api-client/index.test.ts +359 -108
- package/src/api-client/index.ts +24 -13
- package/src/api-client/socket.ts +70 -35
- package/src/api-client/types.ts +1 -1
- package/src/app/language-observer.test.tsx +82 -0
- package/src/app/language-observer.tsx +13 -0
- package/src/app/pane-runtime/index.tsx +2 -2
- package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
- package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
- package/src/app/pane-runtime/plugin-bindings.ts +2 -2
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
- package/src/app/runtime/broker-import.ts +2 -2
- package/src/app/runtime/plugin-bindings.ts +2 -2
- package/src/app/runtime/startup.ts +2 -2
- package/src/app.tsx +16 -7
- package/src/brokers/account-cache.ts +6 -6
- package/src/brokers/broker-ticker-sync.ts +3 -3
- package/src/brokers/remote-broker-adapter.ts +2 -2
- package/src/brokers/sync-broker-instance.ts +5 -6
- package/src/cli/commands/api.ts +3 -6
- package/src/cli/commands/automation.ts +7 -15
- package/src/cli/commands/market.ts +26 -61
- package/src/cli/commands/overview.ts +34 -45
- package/src/cli/commands/search.ts +25 -27
- package/src/cli/commands/system.ts +27 -57
- package/src/cli/commands/ticker.ts +62 -63
- package/src/cli/context.test.ts +63 -0
- package/src/cli/context.ts +15 -1
- package/src/cli/index.ts +1 -1
- package/src/cli/pane-functions/data.ts +2 -19
- package/src/cli/pane-functions/index.ts +26 -25
- package/src/cli/pane-functions/report.ts +1 -1
- package/src/cli/pane-functions/screenshot.ts +3 -3
- package/src/cli/scoped-context.ts +41 -0
- package/src/components/chart/composite/composite-chart.test.tsx +615 -18
- package/src/components/chart/composite/composite-chart.tsx +179 -79
- package/src/components/chart/composite/format.test.ts +20 -7
- package/src/components/chart/composite/index.ts +0 -21
- package/src/components/chart/composite/interactions.test.ts +90 -3
- package/src/components/chart/composite/interactions.ts +189 -24
- package/src/components/chart/composite/price-series.ts +0 -2
- package/src/components/chart/composite/rasterizer.ts +10 -9
- package/src/components/chart/composite/renderers.test.ts +108 -2
- package/src/components/chart/composite/scene.test.ts +36 -0
- package/src/components/chart/composite/scene.ts +5 -2
- package/src/components/chart/composite/text-renderer.ts +37 -25
- package/src/components/chart/composite/time-axis.test.ts +243 -0
- package/src/components/chart/composite/time-axis.ts +608 -0
- package/src/components/chart/composite/types.ts +12 -1
- package/src/components/chart/core/data.ts +2 -159
- package/src/components/chart/core/date-window.ts +6 -225
- package/src/components/chart/core/palette.ts +1 -12
- package/src/components/chart/core/pointer.ts +0 -142
- package/src/components/chart/core/types.ts +1 -58
- package/src/components/chart/cursor-motion.ts +2 -4
- package/src/components/chart/indicators/types.ts +0 -9
- package/src/components/chart/market-session.ts +0 -11
- package/src/components/chart/native/kitty/protocol.ts +1 -1
- package/src/components/chart/native/raster/layout.ts +3 -10
- package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
- package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
- package/src/components/chart/static/chart/surface.tsx +1 -2
- package/src/components/command-bar/action-runtime.ts +2 -2
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/panel/body.tsx +2 -2
- package/src/components/command-bar/panel/index.tsx +11 -9
- package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
- package/src/components/command-bar/surface/index.tsx +2 -2
- package/src/components/command-bar/workflow/body.tsx +3 -3
- package/src/components/command-bar/workflow/collection-actions.ts +2 -2
- package/src/components/command-bar/workflow/coordinator.ts +2 -2
- package/src/components/command-bar/workflow/field-row.tsx +25 -11
- package/src/components/command-bar/workflow/tickers.ts +2 -2
- package/src/components/feed-data-table/stack-view.test.tsx +28 -36
- package/src/components/feed-data-table/stack-view.tsx +3 -1
- package/src/components/layout/detached-pane-shell.tsx +6 -6
- package/src/components/layout/header.tsx +13 -9
- package/src/components/layout/pane/content.test.tsx +42 -3
- package/src/components/layout/pane/content.tsx +3 -1
- package/src/components/layout/pane/footer/index.test.tsx +45 -2
- package/src/components/layout/pane/footer/registration.tsx +3 -1
- package/src/components/layout/shell/index.tsx +1 -2
- package/src/components/layout/shell/native/surfaces.ts +1 -1
- package/src/components/layout/shell/native/window-state.ts +1 -1
- package/src/components/layout/status-bar.tsx +12 -8
- package/src/components/layout/window-controls.tsx +8 -6
- package/src/components/onboarding/onboarding-wizard.tsx +3 -1
- package/src/components/pane-settings-dialog/desktop.tsx +0 -2
- package/src/components/pane-settings-dialog.tsx +0 -2
- package/src/components/pane-template-wizard.tsx +10 -9
- package/src/components/price-performance.tsx +2 -113
- package/src/components/price-sparkline/model.ts +15 -17
- package/src/components/price-sparkline/view.tsx +8 -7
- package/src/components/speedometer-gauge/desktop.tsx +2 -1
- package/src/components/speedometer-gauge/index.tsx +0 -2
- package/src/components/speedometer-gauge/terminal.tsx +24 -5
- package/src/components/ticker/badge/index.tsx +1 -18
- package/src/components/ui/data-table/opentui/index.tsx +3 -3
- package/src/components/ui/frame.tsx +1 -2
- package/src/components/ui/page-stack-view.test.tsx +126 -0
- package/src/components/ui/page-stack-view.tsx +24 -12
- package/src/components/ui/tabs.tsx +7 -3
- package/src/components/ui/ui.test.tsx +23 -2
- package/src/components/use-external-link-footer.ts +4 -2
- package/src/core/app-service-ports.ts +46 -0
- package/src/core/app-services.test.ts +66 -0
- package/src/core/app-services.ts +17 -15
- package/src/data/config/chart-settings.ts +3 -10
- package/src/data/config/store/index.test.ts +99 -2
- package/src/data/config/store/migrations.ts +278 -0
- package/src/data/config/store/normalize.ts +44 -186
- package/src/data/resource-store.ts +73 -12
- package/src/data/sqlite/cache.test.ts +104 -15
- package/src/i18n/index.test.ts +17 -0
- package/src/i18n/index.ts +23 -6
- package/src/i18n/ja.ts +13 -0
- package/src/i18n/ko.ts +13 -0
- package/src/i18n/react.ts +6 -0
- package/src/i18n/zh-cn.ts +13 -0
- package/src/i18n/zh-tw.ts +13 -0
- package/src/market-data/coordinator/auxiliary.ts +7 -3
- package/src/market-data/coordinator/chart.ts +1 -1
- package/src/market-data/coordinator/index.test.ts +26 -0
- package/src/market-data/coordinator/index.ts +11 -5
- package/src/market-data/coordinator/quotes.ts +4 -40
- package/src/market-data/coordinator/subscriptions.test.ts +89 -1
- package/src/market-data/hooks.tsx +47 -1
- package/src/market-data/market/status.ts +6 -6
- package/src/market-data/performance.ts +1 -1
- package/src/market-data/quote-subscription-target.test.ts +37 -0
- package/src/market-data/quote-subscription-target.ts +60 -0
- package/src/market-data/quotes/freshness.ts +1 -0
- package/src/market-data/quotes/resolution.ts +4 -0
- package/src/market-data/request-types.ts +3 -3
- package/src/news/aggregator.test.ts +97 -1
- package/src/news/aggregator.ts +100 -57
- package/src/news/hooks.test.tsx +64 -0
- package/src/news/hooks.ts +18 -25
- package/src/plugins/builtin/account-management/footer.ts +4 -2
- package/src/plugins/builtin/account-management/pane.tsx +9 -4
- package/src/plugins/builtin/ai/agent-history.ts +11 -7
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
- package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
- package/src/plugins/builtin/ai/index.test.ts +2 -2
- package/src/plugins/builtin/ai/index.tsx +10 -11
- package/src/plugins/builtin/ai/pane-settings.ts +1 -1
- package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
- package/src/plugins/builtin/ai/providers.ts +1 -9
- package/src/plugins/builtin/ai/runner.ts +0 -6
- package/src/plugins/builtin/ai/screener/footer.ts +4 -1
- package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
- package/src/plugins/builtin/ai/workspace/model.ts +2 -27
- package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
- package/src/plugins/builtin/analytics/metrics.ts +1 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
- package/src/plugins/builtin/broker-manager/index.tsx +4 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
- package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
- package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
- package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
- package/src/plugins/builtin/chart-composer/index.tsx +0 -6
- package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
- package/src/plugins/builtin/chart-composer/presets.ts +1 -19
- package/src/plugins/builtin/chart-composer/settings.ts +1 -1
- package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
- package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
- package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
- package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
- package/src/plugins/builtin/chat/controller/session.ts +2 -0
- package/src/plugins/builtin/chat/controller/state.ts +7 -0
- package/src/plugins/builtin/chat/controller.test.ts +73 -20
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
- package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
- package/src/plugins/builtin/correlation/compute.ts +4 -10
- package/src/plugins/builtin/econ/index.tsx +17 -19
- package/src/plugins/builtin/holders/types.ts +0 -1
- package/src/plugins/builtin/insider/index.tsx +6 -31
- package/src/plugins/builtin/news/index.tsx +5 -7
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
- package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
- package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
- package/src/plugins/builtin/news/wire/read-state.ts +18 -28
- package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
- package/src/plugins/builtin/options/footer.test.ts +115 -0
- package/src/plugins/builtin/options/footer.ts +110 -0
- package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
- package/src/plugins/builtin/options/live-quotes.ts +147 -0
- package/src/plugins/builtin/options/view.test.tsx +71 -1
- package/src/plugins/builtin/options/view.tsx +39 -24
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
- package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
- package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
- package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
- package/src/plugins/builtin/sec/filing-content.ts +94 -0
- package/src/plugins/builtin/sec/index.tsx +21 -104
- package/src/plugins/builtin/sectors/sector-model.ts +1 -7
- package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
- package/src/plugins/builtin/shared/pane-footer.ts +82 -0
- package/src/plugins/builtin/shared/read-state.ts +64 -3
- package/src/plugins/builtin/substack/read-state.ts +16 -41
- package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
- package/src/plugins/builtin/yield-curve/index.tsx +11 -9
- package/src/plugins/ibkr/dialogs.tsx +3 -3
- package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
- package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
- package/src/plugins/ibkr/gateway/history.ts +2 -2
- package/src/plugins/ibkr/gateway/selection.ts +2 -2
- package/src/plugins/ibkr/gateway/service/index.ts +2 -2
- package/src/plugins/ibkr/gateway/service/native.ts +2 -2
- package/src/plugins/plugin-persistence.ts +3 -4
- package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
- package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
- package/src/plugins/prediction-markets/detail/book.tsx +2 -2
- package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
- package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
- package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
- package/src/plugins/registry/context.ts +3 -4
- package/src/plugins/registry/index.ts +5 -6
- package/src/react/input.test.ts +126 -3
- package/src/react/input.ts +120 -1
- package/src/remote/app-host.tsx +2 -13
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
- package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
- package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
- package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
- package/src/renderers/electrobun/bun/index.ts +123 -83
- package/src/renderers/electrobun/shared/protocol.ts +146 -7
- package/src/renderers/electrobun/view/app-services.ts +5 -8
- package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
- package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
- package/src/renderers/electrobun/view/config-host.ts +1 -1
- package/src/renderers/electrobun/view/context-menu.ts +1 -17
- package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
- package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
- package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
- package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
- package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
- package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
- package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
- package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
- package/src/renderers/electrobun/view/host/box.tsx +0 -1
- package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/input.tsx +0 -1
- package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/native.ts +0 -1
- package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
- package/src/renderers/electrobun/view/host/text.tsx +0 -1
- package/src/renderers/electrobun/view/http-fetch.ts +3 -11
- package/src/renderers/electrobun/view/input-host.tsx +10 -53
- package/src/renderers/electrobun/view/key-event.ts +0 -1
- package/src/renderers/electrobun/view/main.tsx +3 -3
- package/src/renderers/electrobun/view/native-renderer.ts +0 -1
- package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
- package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
- package/src/renderers/electrobun/view/toast-host.tsx +0 -1
- package/src/renderers/electrobun/view/ui-host.tsx +1 -2
- package/src/renderers/electrobun/view/update-host.ts +1 -1
- package/src/renderers/opentui/chart-surface.tsx +2 -1
- package/src/renderers/opentui/dialog-host.tsx +20 -4
- package/src/renderers/opentui/image/loader.ts +12 -2
- package/src/renderers/opentui/input-host.tsx +7 -49
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
- package/src/sources/gloomberb-cloud/index.test.ts +173 -0
- package/src/sources/gloomberb-cloud/index.ts +42 -20
- package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
- package/src/sources/provider-router/cache.ts +1 -1
- package/src/sources/provider-router/document-routes.ts +10 -5
- package/src/sources/provider-router/financials.test.ts +14 -0
- package/src/sources/provider-router/financials.ts +6 -1
- package/src/sources/provider-router/history.test.ts +163 -1
- package/src/sources/provider-router/history.ts +278 -347
- package/src/sources/provider-router/index.ts +2 -2
- package/src/sources/provider-router/routing.test.ts +39 -0
- package/src/sources/provider-router/routing.ts +24 -11
- package/src/sources/provider-router/supplemental.ts +37 -17
- package/src/sources/sec-edgar/content.ts +2 -12
- package/src/sources/sec-edgar.ts +2 -13
- package/src/sources/yahoo-finance/history.ts +2 -2
- package/src/sources/yahoo-finance/options.ts +5 -0
- package/src/sources/yahoo-finance.ts +2 -2
- package/src/state/app/bootstrap.ts +2 -2
- package/src/state/app/context/index.tsx +2 -2
- package/src/state/hooks/quote-streaming.ts +66 -1
- package/src/state/hooks/session-snapshot.ts +2 -2
- package/src/state/selectors.ts +0 -9
- package/src/sync/controller.test.ts +98 -0
- package/src/sync/controller.ts +29 -5
- package/src/sync/react.ts +2 -2
- package/src/sync/types.ts +2 -2
- package/src/theme/colors.test.ts +10 -0
- package/src/theme/colors.ts +75 -78
- package/src/theme/theme-context.test.tsx +67 -0
- package/src/theme/theme-context.tsx +9 -5
- package/src/tickers/open-target.ts +2 -2
- package/src/tickers/search/upsert.ts +2 -2
- package/src/time-series/alignment.ts +0 -7
- package/src/time-series/chart-data.ts +145 -0
- package/src/time-series/date-window.ts +44 -0
- package/src/time-series/history-window.ts +21 -0
- package/src/time-series/hooks.test.ts +59 -0
- package/src/time-series/hooks.ts +33 -12
- package/src/time-series/market-resolution.ts +2 -2
- package/src/time-series/market.ts +1 -1
- package/src/time-series/range.ts +9 -0
- package/src/{components/chart/core → time-series}/resolution.ts +3 -102
- package/src/time-series/resolve.test.ts +203 -1
- package/src/time-series/resolve.ts +96 -18
- package/src/time-series/spec-studies.test.ts +15 -0
- package/src/time-series/spec.ts +2 -2
- package/src/time-series/studies.ts +5 -2
- package/src/time-series/types.ts +3 -1
- package/src/time-series/use-chart-resolution.test.tsx +358 -0
- package/src/time-series/use-chart-resolution.ts +46 -4
- package/src/types/broker.ts +2 -2
- package/src/types/data-provider.ts +3 -3
- package/src/types/financials.ts +12 -0
- package/src/types/plugin.ts +2 -2
- package/src/ui/context-menu.tsx +3 -2
- package/src/ui/dialog.tsx +2 -2
- package/src/utils/html-entities.ts +3 -5
- package/src/utils/main-thread-monitor.ts +1 -1
- package/src/utils/price-history.ts +5 -5
- package/src/version.ts +3 -1
- package/src/components/chart/core/viewport.test.ts +0 -8
- package/src/components/chart/core/viewport.ts +0 -24
- package/src/time-series/index.ts +0 -14
- /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
|
@@ -64,8 +64,11 @@ import {
|
|
|
64
64
|
import {
|
|
65
65
|
renderCompositeAxisText,
|
|
66
66
|
renderCompositePanelText,
|
|
67
|
-
renderCompositeTimeAxis,
|
|
68
67
|
} from "./text-renderer";
|
|
68
|
+
import {
|
|
69
|
+
buildCompositeTimeAxisLayout,
|
|
70
|
+
buildCompositeViewportTimeAxisLayout,
|
|
71
|
+
} from "./time-axis";
|
|
69
72
|
import type {
|
|
70
73
|
CompositeChartColors,
|
|
71
74
|
CompositeChartProps,
|
|
@@ -78,6 +81,12 @@ import type {
|
|
|
78
81
|
const DESKTOP_BITMAP_RESIZE_DEBOUNCE_MS = 32;
|
|
79
82
|
const LEGEND_WHEEL_DELTA_PER_CELL = 8;
|
|
80
83
|
|
|
84
|
+
function isVerticalWheelDirection(
|
|
85
|
+
direction: "up" | "down" | "left" | "right",
|
|
86
|
+
): direction is "up" | "down" {
|
|
87
|
+
return direction === "up" || direction === "down";
|
|
88
|
+
}
|
|
89
|
+
|
|
81
90
|
function renderPanelBitmap(
|
|
82
91
|
panel: CompositePanelScene,
|
|
83
92
|
bitmapSize: StaticChartBitmapSize,
|
|
@@ -400,21 +409,13 @@ function CompositePanelSurface({
|
|
|
400
409
|
consumeChartMouseEvent(event);
|
|
401
410
|
const pointerTarget = plotRef.current as unknown as Parameters<typeof getLocalPlotPointer>[1];
|
|
402
411
|
const pointer = getLocalPlotPointer(event, pointerTarget, renderer);
|
|
403
|
-
if (event.modifiers.ctrl && pointer) {
|
|
404
|
-
const zoomIn = direction === "up"
|
|
412
|
+
if (event.modifiers.ctrl && pointer && isVerticalWheelDirection(direction)) {
|
|
413
|
+
const zoomIn = direction === "up";
|
|
405
414
|
const magnitude = Math.min(Math.max(Math.abs(event.scroll?.delta ?? 1), 1), 8);
|
|
406
415
|
const pointerRatio = pointer.cellX / Math.max(plotWidth - 1, 1);
|
|
407
|
-
const anchorDate = resolveCompositeCursorDate(scene, pointer.cellX);
|
|
408
|
-
const viewportSpan = Math.max(viewport.end.getTime() - viewport.start.getTime(), 1);
|
|
409
|
-
const viewportAnchorRatio = anchorDate
|
|
410
|
-
? Math.max(0, Math.min(
|
|
411
|
-
1,
|
|
412
|
-
(anchorDate.getTime() - viewport.start.getTime()) / viewportSpan,
|
|
413
|
-
))
|
|
414
|
-
: pointerRatio;
|
|
415
416
|
onZoomViewport(
|
|
416
417
|
zoomIn ? 1 + magnitude * 0.04 : 1 / (1 + magnitude * 0.04),
|
|
417
|
-
|
|
418
|
+
pointerRatio,
|
|
418
419
|
);
|
|
419
420
|
updateCursor(event);
|
|
420
421
|
return;
|
|
@@ -734,12 +735,14 @@ function CompositeLegend({
|
|
|
734
735
|
export function CompositeChart({
|
|
735
736
|
series,
|
|
736
737
|
legendSeries,
|
|
738
|
+
timelineSeries,
|
|
737
739
|
panels,
|
|
738
740
|
width,
|
|
739
741
|
height,
|
|
740
742
|
focused = false,
|
|
741
743
|
cursorDate,
|
|
742
744
|
viewport,
|
|
745
|
+
viewportResetKey,
|
|
743
746
|
colors,
|
|
744
747
|
interactive = true,
|
|
745
748
|
axisWidth = 9,
|
|
@@ -762,6 +765,12 @@ export function CompositeChart({
|
|
|
762
765
|
const totalWidth = Math.max(1, Math.floor(width));
|
|
763
766
|
const totalHeight = Math.max(1, Math.floor(height));
|
|
764
767
|
const visibleSeries = useMemo(() => series.filter((entry) => entry.points.length > 0), [series]);
|
|
768
|
+
const marketTimelineSeries = useMemo(() => {
|
|
769
|
+
const supplied = timelineSeries?.filter((entry) => entry.points.length > 0) ?? [];
|
|
770
|
+
return supplied.some((entry) => entry.timeBasis?.kind === "market")
|
|
771
|
+
? supplied
|
|
772
|
+
: visibleSeries;
|
|
773
|
+
}, [timelineSeries, visibleSeries]);
|
|
765
774
|
const visibleLegendSeries = useMemo(
|
|
766
775
|
() => (legendSeries ?? visibleSeries).filter((entry) => entry.points.length > 0),
|
|
767
776
|
[legendSeries, visibleSeries],
|
|
@@ -777,9 +786,23 @@ export function CompositeChart({
|
|
|
777
786
|
: Math.min(current, visibleLegendSeries.length - 1)
|
|
778
787
|
));
|
|
779
788
|
}, [visibleLegendSeries.length]);
|
|
789
|
+
const previousAuthoredViewportRef = useRef<CompositeViewportRange | null>(viewport ?? null);
|
|
790
|
+
const previousViewportResetKeyRef = useRef(viewportResetKey);
|
|
791
|
+
const [interactionViewport, setInteractionViewport] = useState<CompositeViewportRange | null>(null);
|
|
792
|
+
const hasViewportResetKey = viewportResetKey !== undefined
|
|
793
|
+
|| previousViewportResetKeyRef.current !== undefined;
|
|
794
|
+
const authoredViewportChanged = hasViewportResetKey
|
|
795
|
+
? previousViewportResetKeyRef.current !== viewportResetKey
|
|
796
|
+
: shouldResetCompositeViewport(
|
|
797
|
+
previousAuthoredViewportRef.current,
|
|
798
|
+
viewport ?? null,
|
|
799
|
+
);
|
|
800
|
+
const navigationAnchorViewport = authoredViewportChanged
|
|
801
|
+
? viewport
|
|
802
|
+
: interactionViewport ?? viewport;
|
|
780
803
|
const navigationBounds = useMemo(
|
|
781
|
-
() => resolveCompositeNavigationBounds(visibleSeries,
|
|
782
|
-
[
|
|
804
|
+
() => resolveCompositeNavigationBounds(visibleSeries, navigationAnchorViewport),
|
|
805
|
+
[navigationAnchorViewport, visibleSeries],
|
|
783
806
|
);
|
|
784
807
|
const initialViewport = useMemo(() => (
|
|
785
808
|
navigationBounds
|
|
@@ -788,82 +811,91 @@ export function CompositeChart({
|
|
|
788
811
|
: navigationBounds
|
|
789
812
|
: null
|
|
790
813
|
), [navigationBounds, viewport]);
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
&& !!navigationBounds
|
|
804
|
-
&& sameCompositeViewport(
|
|
805
|
-
clampCompositeViewport(interactionViewport, navigationBounds),
|
|
806
|
-
interactionViewport,
|
|
807
|
-
);
|
|
808
|
-
const shouldResetInteractionViewport = initialViewportChanged
|
|
809
|
-
|| (navigationBoundsChanged && !interactionViewportStillValid);
|
|
810
|
-
const currentInteractionViewport = shouldResetInteractionViewport ? null : interactionViewport;
|
|
814
|
+
const viewportSeriesKey = visibleLegendSeries
|
|
815
|
+
.map((entry) => `${entry.id}:${entry.label}`)
|
|
816
|
+
.join("|");
|
|
817
|
+
const clampedInteractionViewport = interactionViewport && navigationBounds
|
|
818
|
+
? clampCompositeViewport(interactionViewport, navigationBounds)
|
|
819
|
+
: interactionViewport;
|
|
820
|
+
const interactionViewportNeedsSync = !!interactionViewport
|
|
821
|
+
&& !!clampedInteractionViewport
|
|
822
|
+
&& !sameCompositeViewport(interactionViewport, clampedInteractionViewport);
|
|
823
|
+
const currentInteractionViewport = authoredViewportChanged
|
|
824
|
+
? null
|
|
825
|
+
: clampedInteractionViewport;
|
|
811
826
|
const effectiveViewport = navigationBounds
|
|
812
827
|
? currentInteractionViewport
|
|
813
828
|
? clampCompositeViewport(currentInteractionViewport, navigationBounds)
|
|
814
829
|
: initialViewport
|
|
815
830
|
: null;
|
|
816
|
-
const
|
|
817
|
-
const
|
|
818
|
-
const viewportSeriesKey = visibleLegendSeries
|
|
819
|
-
.map((entry) => `${entry.id}:${entry.label}`)
|
|
820
|
-
.join("|");
|
|
831
|
+
const interactionViewportStart = currentInteractionViewport?.start.getTime() ?? null;
|
|
832
|
+
const interactionViewportEnd = currentInteractionViewport?.end.getTime() ?? null;
|
|
821
833
|
const lastReportedViewportRef = useRef<string | null>(null);
|
|
834
|
+
const viewportInteractionRef = useRef<"pan" | "reset" | "sync" | "zoom">("reset");
|
|
822
835
|
useEffect(() => {
|
|
823
836
|
if (!onViewportChange) return;
|
|
824
|
-
|
|
837
|
+
if (interactionViewportNeedsSync) return;
|
|
838
|
+
const interactionKey = interactionViewportStart === null || interactionViewportEnd === null
|
|
825
839
|
? "none"
|
|
826
|
-
: `${
|
|
827
|
-
const key = `${viewportSeriesKey}|${
|
|
840
|
+
: `${interactionViewportStart}:${interactionViewportEnd}`;
|
|
841
|
+
const key = `${viewportSeriesKey}|${interactionKey}`;
|
|
842
|
+
// The callback drives adaptive data loading. Seed it from the authored
|
|
843
|
+
// viewport without echoing that controlled value back into the loader.
|
|
844
|
+
if (lastReportedViewportRef.current === null) {
|
|
845
|
+
lastReportedViewportRef.current = key;
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
828
848
|
if (lastReportedViewportRef.current === key) return;
|
|
829
849
|
lastReportedViewportRef.current = key;
|
|
830
850
|
onViewportChange(
|
|
831
|
-
|
|
851
|
+
interactionViewportStart === null || interactionViewportEnd === null
|
|
832
852
|
? null
|
|
833
853
|
: {
|
|
834
|
-
start: new Date(
|
|
835
|
-
end: new Date(
|
|
854
|
+
start: new Date(interactionViewportStart),
|
|
855
|
+
end: new Date(interactionViewportEnd),
|
|
836
856
|
},
|
|
857
|
+
viewportInteractionRef.current,
|
|
837
858
|
);
|
|
838
|
-
}, [
|
|
859
|
+
}, [
|
|
860
|
+
interactionViewportEnd,
|
|
861
|
+
interactionViewportNeedsSync,
|
|
862
|
+
interactionViewportStart,
|
|
863
|
+
onViewportChange,
|
|
864
|
+
viewportSeriesKey,
|
|
865
|
+
]);
|
|
839
866
|
const minimumViewportSpanMs = useMemo(
|
|
840
867
|
() => navigationBounds ? resolveCompositeMinimumSpanMs(visibleSeries, navigationBounds) : 1,
|
|
841
868
|
[navigationBounds, visibleSeries],
|
|
842
869
|
);
|
|
843
870
|
|
|
844
871
|
useEffect(() => {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
previousNavigationBoundsRef.current = navigationBounds;
|
|
848
|
-
previousInitialViewportRef.current = initialViewport;
|
|
872
|
+
previousAuthoredViewportRef.current = viewport ?? null;
|
|
873
|
+
previousViewportResetKeyRef.current = viewportResetKey;
|
|
849
874
|
if (
|
|
850
|
-
|
|
851
|
-
|| (
|
|
852
|
-
shouldResetCompositeViewport(previousBounds, navigationBounds)
|
|
853
|
-
&& interactionViewport
|
|
854
|
-
&& navigationBounds
|
|
855
|
-
&& !sameCompositeViewport(
|
|
856
|
-
clampCompositeViewport(interactionViewport, navigationBounds),
|
|
857
|
-
interactionViewport,
|
|
858
|
-
)
|
|
859
|
-
)
|
|
875
|
+
authoredViewportChanged
|
|
876
|
+
|| (interactionViewport && !navigationBounds)
|
|
860
877
|
) {
|
|
878
|
+
viewportInteractionRef.current = "reset";
|
|
861
879
|
setInteractionViewport(null);
|
|
880
|
+
return;
|
|
862
881
|
}
|
|
863
|
-
|
|
882
|
+
if (interactionViewportNeedsSync && clampedInteractionViewport) {
|
|
883
|
+
viewportInteractionRef.current = "sync";
|
|
884
|
+
setInteractionViewport(clampedInteractionViewport);
|
|
885
|
+
}
|
|
886
|
+
}, [
|
|
887
|
+
authoredViewportChanged,
|
|
888
|
+
clampedInteractionViewport,
|
|
889
|
+
interactionViewport,
|
|
890
|
+
interactionViewportNeedsSync,
|
|
891
|
+
navigationBounds,
|
|
892
|
+
viewport,
|
|
893
|
+
viewportResetKey,
|
|
894
|
+
]);
|
|
864
895
|
|
|
865
896
|
const zoomViewport = useCallback((zoomFactor: number, anchorRatio = 1) => {
|
|
866
897
|
if (!navigationBounds || !initialViewport) return;
|
|
898
|
+
viewportInteractionRef.current = "zoom";
|
|
867
899
|
setInteractionViewport((current) => {
|
|
868
900
|
const base = current ?? initialViewport;
|
|
869
901
|
const next = zoomCompositeViewport(
|
|
@@ -872,22 +904,30 @@ export function CompositeChart({
|
|
|
872
904
|
zoomFactor,
|
|
873
905
|
anchorRatio,
|
|
874
906
|
minimumViewportSpanMs,
|
|
907
|
+
marketTimelineSeries,
|
|
875
908
|
);
|
|
909
|
+
if (sameCompositeViewport(next, base)) return current;
|
|
876
910
|
return sameCompositeViewport(next, initialViewport) ? null : next;
|
|
877
911
|
});
|
|
878
|
-
}, [initialViewport, minimumViewportSpanMs, navigationBounds]);
|
|
912
|
+
}, [initialViewport, marketTimelineSeries, minimumViewportSpanMs, navigationBounds]);
|
|
879
913
|
const panViewport = useCallback((
|
|
880
914
|
shiftRatio: number,
|
|
881
915
|
fromViewport?: CompositeViewportRange,
|
|
882
916
|
) => {
|
|
883
917
|
if (!navigationBounds || !initialViewport) return;
|
|
918
|
+
viewportInteractionRef.current = "pan";
|
|
884
919
|
setInteractionViewport((current) => {
|
|
885
920
|
const base = fromViewport ?? current ?? initialViewport;
|
|
886
|
-
const next = panCompositeViewport(base, navigationBounds, shiftRatio);
|
|
921
|
+
const next = panCompositeViewport(base, navigationBounds, shiftRatio, marketTimelineSeries);
|
|
922
|
+
if (sameCompositeViewport(next, base)) {
|
|
923
|
+
if (!fromViewport) return current;
|
|
924
|
+
return sameCompositeViewport(base, initialViewport) ? null : base;
|
|
925
|
+
}
|
|
887
926
|
return sameCompositeViewport(next, initialViewport) ? null : next;
|
|
888
927
|
});
|
|
889
|
-
}, [initialViewport, navigationBounds]);
|
|
928
|
+
}, [initialViewport, marketTimelineSeries, navigationBounds]);
|
|
890
929
|
const resetViewport = useCallback(() => {
|
|
930
|
+
viewportInteractionRef.current = "reset";
|
|
891
931
|
setInteractionViewport(null);
|
|
892
932
|
}, []);
|
|
893
933
|
const hasLeftAxis = visibleSeries.some((entry) => entry.axis === "left");
|
|
@@ -917,8 +957,8 @@ export function CompositeChart({
|
|
|
917
957
|
width: 1,
|
|
918
958
|
height: Math.max(panelCount, 1),
|
|
919
959
|
viewport: effectiveViewport ?? undefined,
|
|
920
|
-
timelineSeries:
|
|
921
|
-
}), [effectiveViewport, panelCount, panels,
|
|
960
|
+
timelineSeries: marketTimelineSeries,
|
|
961
|
+
}), [effectiveViewport, marketTimelineSeries, panelCount, panels, visibleSeries]);
|
|
922
962
|
const layoutPanels = useMemo<CompositePanelScene[] | null>(() => {
|
|
923
963
|
if (!projectedScene) return null;
|
|
924
964
|
const panelSpecById = new Map(panels.map((panel) => [panel.id, panel] as const));
|
|
@@ -955,6 +995,30 @@ export function CompositeChart({
|
|
|
955
995
|
)
|
|
956
996
|
: null
|
|
957
997
|
), [baseScene, normalizedCursorTimestamp]);
|
|
998
|
+
const handleEmptyMouseScroll = useCallback((event: ChartMouseEvent) => {
|
|
999
|
+
const direction = event.scroll?.direction;
|
|
1000
|
+
if (!interactive || !navigationBounds || !direction) return;
|
|
1001
|
+
onActivate?.();
|
|
1002
|
+
consumeChartMouseEvent(event);
|
|
1003
|
+
if (event.modifiers.ctrl && isVerticalWheelDirection(direction)) {
|
|
1004
|
+
const zoomIn = direction === "up";
|
|
1005
|
+
if (!zoomIn) {
|
|
1006
|
+
resetViewport();
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const magnitude = Math.min(Math.max(Math.abs(event.scroll?.delta ?? 1), 1), 8);
|
|
1010
|
+
zoomViewport(1 + magnitude * 0.04, 0.5);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
panViewport(resolveCompositeWheelPanRatio(direction, event.scroll?.delta));
|
|
1014
|
+
}, [
|
|
1015
|
+
interactive,
|
|
1016
|
+
navigationBounds,
|
|
1017
|
+
onActivate,
|
|
1018
|
+
panViewport,
|
|
1019
|
+
resetViewport,
|
|
1020
|
+
zoomViewport,
|
|
1021
|
+
]);
|
|
958
1022
|
const keyboardCursorDateRef = useRef<Date | null>(scene?.cursorDate ?? null);
|
|
959
1023
|
keyboardCursorDateRef.current = scene?.cursorDate ?? null;
|
|
960
1024
|
const lastCursorTimestampRef = useRef<number | null>(normalizedCursorTimestamp);
|
|
@@ -1049,9 +1113,25 @@ export function CompositeChart({
|
|
|
1049
1113
|
}
|
|
1050
1114
|
}, { enabled: focused && interactive });
|
|
1051
1115
|
|
|
1116
|
+
const leftPadding = leftAxisWidth + (leftAxisWidth ? axisGap : 0);
|
|
1117
|
+
const rightPadding = rightAxisWidth + (rightAxisWidth ? axisGap : 0);
|
|
1118
|
+
const timeAxisLayout = scene && showTimeAxis
|
|
1119
|
+
? buildCompositeTimeAxisLayout(scene, plotWidth)
|
|
1120
|
+
: null;
|
|
1121
|
+
const emptyTimeAxisLayout = !scene && showTimeAxis && effectiveViewport
|
|
1122
|
+
? buildCompositeViewportTimeAxisLayout(effectiveViewport, plotWidth)
|
|
1123
|
+
: null;
|
|
1124
|
+
|
|
1052
1125
|
if (!scene) {
|
|
1126
|
+
const emptyPlotHeight = Math.max(0, totalHeight - legendRows - timeAxisRows);
|
|
1053
1127
|
return (
|
|
1054
|
-
<Box
|
|
1128
|
+
<Box
|
|
1129
|
+
flexDirection="column"
|
|
1130
|
+
width={totalWidth}
|
|
1131
|
+
height={totalHeight}
|
|
1132
|
+
overflow="hidden"
|
|
1133
|
+
data-gloom-role="composite-chart"
|
|
1134
|
+
>
|
|
1055
1135
|
{legendRows > 0 && legendAccessory ? (
|
|
1056
1136
|
<CompositeLegend
|
|
1057
1137
|
scene={null}
|
|
@@ -1067,22 +1147,41 @@ export function CompositeChart({
|
|
|
1067
1147
|
keyboardIndex={legendKeyboardIndex}
|
|
1068
1148
|
/>
|
|
1069
1149
|
) : null}
|
|
1070
|
-
{
|
|
1071
|
-
<Box
|
|
1072
|
-
width={
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1150
|
+
{emptyPlotHeight > 0 ? (
|
|
1151
|
+
<Box flexDirection="row" width={totalWidth} height={emptyPlotHeight}>
|
|
1152
|
+
{leftPadding > 0 ? <Box width={leftPadding} /> : null}
|
|
1153
|
+
<ChartSurface
|
|
1154
|
+
width={plotWidth}
|
|
1155
|
+
height={emptyPlotHeight}
|
|
1156
|
+
alignItems="center"
|
|
1157
|
+
justifyContent="center"
|
|
1158
|
+
onMouseScroll={interactive && navigationBounds ? handleEmptyMouseScroll : undefined}
|
|
1159
|
+
cursor={interactive && navigationBounds ? "grab" : undefined}
|
|
1160
|
+
data-gloom-interactive={interactive && navigationBounds ? "true" : undefined}
|
|
1161
|
+
data-gloom-role="composite-chart-empty"
|
|
1162
|
+
data-gloom-label={emptyMessage}
|
|
1163
|
+
>
|
|
1164
|
+
<Text fg={resolvedColors.textDim}>{emptyMessage}</Text>
|
|
1165
|
+
</ChartSurface>
|
|
1166
|
+
{rightPadding > 0 ? <Box width={rightPadding} /> : null}
|
|
1167
|
+
</Box>
|
|
1168
|
+
) : null}
|
|
1169
|
+
{emptyTimeAxisLayout ? (
|
|
1170
|
+
<Box flexDirection="row" width={totalWidth} height={1}>
|
|
1171
|
+
{leftPadding > 0 ? <Box width={leftPadding} /> : null}
|
|
1172
|
+
<StaticXAxisLabels
|
|
1173
|
+
labels={[emptyTimeAxisLayout.text]}
|
|
1174
|
+
positionedLabels={emptyTimeAxisLayout.ticks}
|
|
1175
|
+
width={plotWidth}
|
|
1176
|
+
color={resolvedColors.textDim}
|
|
1177
|
+
/>
|
|
1178
|
+
{rightPadding > 0 ? <Box width={rightPadding} /> : null}
|
|
1078
1179
|
</Box>
|
|
1079
1180
|
) : null}
|
|
1080
1181
|
</Box>
|
|
1081
1182
|
);
|
|
1082
1183
|
}
|
|
1083
1184
|
|
|
1084
|
-
const leftPadding = leftAxisWidth + (leftAxisWidth ? axisGap : 0);
|
|
1085
|
-
const rightPadding = rightAxisWidth + (rightAxisWidth ? axisGap : 0);
|
|
1086
1185
|
const timeAxisCursorColumn = scene.cursorXRatio === null
|
|
1087
1186
|
? null
|
|
1088
1187
|
: scene.cursorXRatio * Math.max(plotWidth - 1, 0);
|
|
@@ -1127,11 +1226,12 @@ export function CompositeChart({
|
|
|
1127
1226
|
onZoomViewport={zoomViewport}
|
|
1128
1227
|
/>
|
|
1129
1228
|
))}
|
|
1130
|
-
{
|
|
1229
|
+
{timeAxisLayout ? (
|
|
1131
1230
|
<Box flexDirection="row" width={totalWidth} height={1}>
|
|
1132
1231
|
{leftPadding > 0 ? <Box width={leftPadding} /> : null}
|
|
1133
1232
|
<StaticXAxisLabels
|
|
1134
|
-
labels={[
|
|
1233
|
+
labels={[timeAxisLayout.text]}
|
|
1234
|
+
positionedLabels={timeAxisLayout.ticks}
|
|
1135
1235
|
width={plotWidth}
|
|
1136
1236
|
color={resolvedColors.textDim}
|
|
1137
1237
|
cursorColumn={timeAxisCursorColumn}
|
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
formatCompositeTimeAxisDate,
|
|
9
9
|
} from "./format";
|
|
10
10
|
import type { ResolvedSeries, TimeSeriesPoint } from "../../../time-series/types";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
renderCompositeTimeAxis,
|
|
13
|
+
renderCompositeViewportTimeAxis,
|
|
14
|
+
} from "./text-renderer";
|
|
12
15
|
|
|
13
16
|
function scene(start: string, end: string): CompositeChartScene {
|
|
14
17
|
const startTime = Date.parse(start);
|
|
@@ -38,7 +41,7 @@ describe("composite chart timestamp formatting", () => {
|
|
|
38
41
|
expect(formatCompositeTimeAxisDate(cursor, intraday.startTime, intraday.endTime))
|
|
39
42
|
.toBe("12:05 UTC");
|
|
40
43
|
expect(renderCompositeTimeAxis(intraday, 60)).toContain("09:30 UTC");
|
|
41
|
-
expect(renderCompositeTimeAxis(intraday, 60)).toContain("12:
|
|
44
|
+
expect(renderCompositeTimeAxis(intraday, 60)).toContain("12:00");
|
|
42
45
|
expect(renderCompositeTimeAxis(intraday, 60)).toContain("16:00 UTC");
|
|
43
46
|
});
|
|
44
47
|
|
|
@@ -51,9 +54,9 @@ describe("composite chart timestamp formatting", () => {
|
|
|
51
54
|
overnight.endTime,
|
|
52
55
|
)).toBe("01-02 00:15 UTC");
|
|
53
56
|
const axis = renderCompositeTimeAxis(overnight, 80);
|
|
54
|
-
expect(axis).toContain("
|
|
55
|
-
expect(axis).toContain("
|
|
56
|
-
expect(axis).toContain("
|
|
57
|
+
expect(axis).toContain("Jan 1 23:30 UTC");
|
|
58
|
+
expect(axis).toContain("Jan 2 00:00");
|
|
59
|
+
expect(axis).toContain("Jan 2 00:30 UTC");
|
|
57
60
|
});
|
|
58
61
|
|
|
59
62
|
test("keeps longer chart spans as concise UTC calendar dates", () => {
|
|
@@ -62,8 +65,18 @@ describe("composite chart timestamp formatting", () => {
|
|
|
62
65
|
|
|
63
66
|
expect(formatCompositeCursorDate(cursor, weekly.startTime, weekly.endTime)).toBe("2025-01-04");
|
|
64
67
|
expect(formatCompositeTimeAxisDate(cursor, weekly.startTime, weekly.endTime)).toBe("2025-01-04");
|
|
65
|
-
expect(renderCompositeTimeAxis(weekly, 60)).toContain("
|
|
66
|
-
expect(renderCompositeTimeAxis(weekly, 60)).toContain("
|
|
68
|
+
expect(renderCompositeTimeAxis(weekly, 60)).toContain("Jan 1");
|
|
69
|
+
expect(renderCompositeTimeAxis(weekly, 60)).toContain("Jan 8");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("renders recovery-shell dates directly from a viewport", () => {
|
|
73
|
+
const axis = renderCompositeViewportTimeAxis({
|
|
74
|
+
start: new Date("2025-01-01T00:00:00.000Z"),
|
|
75
|
+
end: new Date("2025-01-08T00:00:00.000Z"),
|
|
76
|
+
}, 60);
|
|
77
|
+
|
|
78
|
+
expect(axis).toContain("Jan 1");
|
|
79
|
+
expect(axis).toContain("Jan 8");
|
|
67
80
|
});
|
|
68
81
|
});
|
|
69
82
|
|
|
@@ -1,29 +1,8 @@
|
|
|
1
1
|
export { CompositeChart } from "./composite-chart";
|
|
2
|
-
export {
|
|
3
|
-
allocateCompositePanelHeights,
|
|
4
|
-
buildCompositeChartScene,
|
|
5
|
-
projectCompositeValue,
|
|
6
|
-
resolveCompositeCursorDate,
|
|
7
|
-
resolveTimeSeriesPointValue,
|
|
8
|
-
} from "./scene";
|
|
9
|
-
export { renderCompositePanelBitmap } from "./rasterizer";
|
|
10
2
|
export {
|
|
11
3
|
pricePointsToResolvedSeries,
|
|
12
|
-
resolvedPriceSeries,
|
|
13
4
|
type PricePointsToResolvedSeriesOptions,
|
|
14
5
|
} from "./price-series";
|
|
15
|
-
export {
|
|
16
|
-
renderCompositeAxisText,
|
|
17
|
-
renderCompositePanelText,
|
|
18
|
-
renderCompositeTimeAxis,
|
|
19
|
-
} from "./text-renderer";
|
|
20
|
-
export {
|
|
21
|
-
compositeAxisTicks,
|
|
22
|
-
formatCompositeAxisValue,
|
|
23
|
-
formatCompositeCursorDate,
|
|
24
|
-
formatCompositeSeriesValue,
|
|
25
|
-
formatCompositeTimeAxisDate,
|
|
26
|
-
} from "./format";
|
|
27
6
|
export type {
|
|
28
7
|
BuildCompositeChartSceneOptions,
|
|
29
8
|
CompositeAxisDomain,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import type { ResolvedSeries, TimeSeriesPoint } from "../../../time-series/types";
|
|
3
3
|
import {
|
|
4
|
+
compositeViewportHasObservations,
|
|
4
5
|
panCompositeViewport,
|
|
5
6
|
resolveCompositeChartInteraction,
|
|
6
7
|
resolveCompositeMinimumSpanMs,
|
|
@@ -10,6 +11,10 @@ import {
|
|
|
10
11
|
zoomCompositeViewport,
|
|
11
12
|
type CompositeViewportRange,
|
|
12
13
|
} from "./interactions";
|
|
14
|
+
import {
|
|
15
|
+
buildCompositeTimeScale,
|
|
16
|
+
projectCompositeTimestamp,
|
|
17
|
+
} from "./time-scale";
|
|
13
18
|
|
|
14
19
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
15
20
|
|
|
@@ -43,6 +48,27 @@ function viewport(startDay: number, endDay: number): CompositeViewportRange {
|
|
|
43
48
|
};
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
function intradayMarketSeries(): ResolvedSeries {
|
|
52
|
+
const points: TimeSeriesPoint[] = [];
|
|
53
|
+
for (const day of [2, 3]) {
|
|
54
|
+
const sessionStart = Date.UTC(2025, 0, day, 13, 30);
|
|
55
|
+
for (let index = 0; index < 78; index += 1) {
|
|
56
|
+
const date = new Date(sessionStart + index * 5 * 60_000);
|
|
57
|
+
points.push({ date, observedAt: date, value: 100 + points.length });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
...series([]),
|
|
62
|
+
nativeFrequency: "intraday",
|
|
63
|
+
timeBasis: {
|
|
64
|
+
kind: "market",
|
|
65
|
+
timeZone: "America/New_York",
|
|
66
|
+
cadenceMs: 5 * 60_000,
|
|
67
|
+
},
|
|
68
|
+
points,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
46
72
|
describe("composite chart interactions", () => {
|
|
47
73
|
test("zooms around the right edge and clamps zoom-out to loaded bounds", () => {
|
|
48
74
|
const bounds = viewport(1, 11);
|
|
@@ -65,11 +91,34 @@ describe("composite chart interactions", () => {
|
|
|
65
91
|
expect(clampedNewer).toEqual(viewport(7, 11));
|
|
66
92
|
});
|
|
67
93
|
|
|
68
|
-
test("
|
|
69
|
-
const data =
|
|
94
|
+
test("keeps the same market-slot span while panning across a closed session", () => {
|
|
95
|
+
const data = intradayMarketSeries();
|
|
96
|
+
const bounds = resolveCompositeNavigationBounds([data])!;
|
|
97
|
+
const visible = {
|
|
98
|
+
start: new Date("2025-01-03T13:30:00.000Z"),
|
|
99
|
+
end: new Date("2025-01-03T17:00:00.000Z"),
|
|
100
|
+
};
|
|
101
|
+
const panned = panCompositeViewport(visible, bounds, 0.5, [data]);
|
|
102
|
+
const scale = buildCompositeTimeScale(
|
|
103
|
+
[data],
|
|
104
|
+
bounds.start.getTime(),
|
|
105
|
+
bounds.end.getTime(),
|
|
106
|
+
);
|
|
107
|
+
const projectedSpan = (window: CompositeViewportRange) => (
|
|
108
|
+
projectCompositeTimestamp(scale, window.end.getTime())!.ratio
|
|
109
|
+
- projectCompositeTimestamp(scale, window.start.getTime())!.ratio
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(panned.end.getTime() - panned.start.getTime())
|
|
113
|
+
.not.toBe(visible.end.getTime() - visible.start.getTime());
|
|
114
|
+
expect(Math.abs(projectedSpan(panned) - projectedSpan(visible))).toBeLessThan(1e-12);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("uses representative cadence instead of a stray fine gap as the zoom floor", () => {
|
|
118
|
+
const data = series([1, 2, 5, 8, 11]);
|
|
70
119
|
const bounds = resolveCompositeNavigationBounds([data]);
|
|
71
120
|
expect(bounds).toEqual(viewport(1, 11));
|
|
72
|
-
expect(resolveCompositeMinimumSpanMs([data], bounds!)).toBe(DAY_MS);
|
|
121
|
+
expect(resolveCompositeMinimumSpanMs([data], bounds!)).toBe(3 * DAY_MS);
|
|
73
122
|
});
|
|
74
123
|
|
|
75
124
|
test("keeps the selected window separate from wider loaded navigation bounds", () => {
|
|
@@ -77,6 +126,44 @@ describe("composite chart interactions", () => {
|
|
|
77
126
|
expect(resolveCompositeNavigationBounds([data], viewport(5, 9))).toEqual(viewport(1, 9));
|
|
78
127
|
});
|
|
79
128
|
|
|
129
|
+
test("clamps overlapping requested bounds to real data but preserves disjoint requests", () => {
|
|
130
|
+
const data = series([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
131
|
+
|
|
132
|
+
expect(resolveCompositeNavigationBounds([data], viewport(5, 12))).toEqual(viewport(1, 9));
|
|
133
|
+
expect(resolveCompositeNavigationBounds([data], viewport(20, 30))).toEqual(viewport(20, 30));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("requires distinct renderable observations inside a viewport", () => {
|
|
137
|
+
const data = series([1, 2, 3]);
|
|
138
|
+
const emptyPoint = point(4);
|
|
139
|
+
emptyPoint.value = null;
|
|
140
|
+
data.points.push(emptyPoint);
|
|
141
|
+
|
|
142
|
+
expect(compositeViewportHasObservations([data], viewport(1, 2))).toBe(true);
|
|
143
|
+
expect(compositeViewportHasObservations([data], viewport(2, 2))).toBe(false);
|
|
144
|
+
expect(compositeViewportHasObservations([data], viewport(3, 4))).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("does not zoom or pan a populated viewport into fewer than two observations", () => {
|
|
148
|
+
const data = series([1, 5, 10, 11]);
|
|
149
|
+
const bounds = viewport(1, 11);
|
|
150
|
+
const populated = viewport(1, 5);
|
|
151
|
+
|
|
152
|
+
expect(zoomCompositeViewport(populated, bounds, 2, 0.5, 1, [data])).toEqual(populated);
|
|
153
|
+
expect(panCompositeViewport(populated, bounds, -1, [data])).toEqual(populated);
|
|
154
|
+
expect(panCompositeViewport(populated, bounds, -1.5, [data])).toEqual(viewport(7, 11));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("allows zoom-out to recover when refreshed data has already emptied the viewport", () => {
|
|
158
|
+
const data = series([1, 2, 10, 11]);
|
|
159
|
+
const bounds = viewport(1, 11);
|
|
160
|
+
const empty = viewport(5, 6);
|
|
161
|
+
const expanded = zoomCompositeViewport(empty, bounds, 0.5, 0.5, 1, [data]);
|
|
162
|
+
|
|
163
|
+
expect(expanded.end.getTime() - expanded.start.getTime()).toBe(2 * DAY_MS);
|
|
164
|
+
expect(compositeViewportHasObservations([data], expanded)).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
80
167
|
test("keeps live viewport drift but resets deliberate range/window changes", () => {
|
|
81
168
|
const original = viewport(1, 11);
|
|
82
169
|
const oneMinuteLater = {
|