gloomberb 0.10.0 → 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.
Files changed (353) hide show
  1. package/package.json +8 -1
  2. package/src/api-client/auth.ts +1 -0
  3. package/src/api-client/index.test.ts +359 -108
  4. package/src/api-client/index.ts +24 -13
  5. package/src/api-client/socket.ts +70 -35
  6. package/src/api-client/types.ts +1 -1
  7. package/src/app/language-observer.test.tsx +82 -0
  8. package/src/app/language-observer.tsx +13 -0
  9. package/src/app/pane-runtime/index.tsx +2 -2
  10. package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
  11. package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
  12. package/src/app/pane-runtime/plugin-bindings.ts +2 -2
  13. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
  14. package/src/app/runtime/broker-import.ts +2 -2
  15. package/src/app/runtime/plugin-bindings.ts +2 -2
  16. package/src/app/runtime/startup.ts +2 -2
  17. package/src/app.tsx +16 -7
  18. package/src/brokers/account-cache.ts +6 -6
  19. package/src/brokers/broker-ticker-sync.ts +3 -3
  20. package/src/brokers/remote-broker-adapter.ts +2 -2
  21. package/src/brokers/sync-broker-instance.ts +5 -6
  22. package/src/cli/commands/api.ts +3 -6
  23. package/src/cli/commands/automation.ts +7 -15
  24. package/src/cli/commands/market.ts +26 -61
  25. package/src/cli/commands/overview.ts +34 -45
  26. package/src/cli/commands/search.ts +25 -27
  27. package/src/cli/commands/system.ts +27 -57
  28. package/src/cli/commands/ticker.ts +62 -63
  29. package/src/cli/context.test.ts +63 -0
  30. package/src/cli/context.ts +15 -1
  31. package/src/cli/index.ts +1 -1
  32. package/src/cli/pane-functions/data.ts +2 -19
  33. package/src/cli/pane-functions/index.ts +26 -25
  34. package/src/cli/pane-functions/report.ts +1 -1
  35. package/src/cli/pane-functions/screenshot.ts +3 -3
  36. package/src/cli/scoped-context.ts +41 -0
  37. package/src/components/chart/composite/composite-chart.test.tsx +615 -18
  38. package/src/components/chart/composite/composite-chart.tsx +179 -79
  39. package/src/components/chart/composite/format.test.ts +20 -7
  40. package/src/components/chart/composite/index.ts +0 -21
  41. package/src/components/chart/composite/interactions.test.ts +90 -3
  42. package/src/components/chart/composite/interactions.ts +189 -24
  43. package/src/components/chart/composite/price-series.ts +0 -2
  44. package/src/components/chart/composite/rasterizer.ts +10 -9
  45. package/src/components/chart/composite/renderers.test.ts +108 -2
  46. package/src/components/chart/composite/scene.test.ts +36 -0
  47. package/src/components/chart/composite/scene.ts +5 -2
  48. package/src/components/chart/composite/text-renderer.ts +37 -25
  49. package/src/components/chart/composite/time-axis.test.ts +243 -0
  50. package/src/components/chart/composite/time-axis.ts +608 -0
  51. package/src/components/chart/composite/types.ts +12 -1
  52. package/src/components/chart/core/data.ts +2 -159
  53. package/src/components/chart/core/date-window.ts +6 -225
  54. package/src/components/chart/core/palette.ts +1 -12
  55. package/src/components/chart/core/pointer.ts +0 -142
  56. package/src/components/chart/core/types.ts +1 -58
  57. package/src/components/chart/cursor-motion.ts +2 -4
  58. package/src/components/chart/indicators/types.ts +0 -9
  59. package/src/components/chart/market-session.ts +0 -11
  60. package/src/components/chart/native/kitty/protocol.ts +1 -1
  61. package/src/components/chart/native/raster/layout.ts +3 -10
  62. package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
  63. package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
  64. package/src/components/chart/static/chart/surface.tsx +1 -2
  65. package/src/components/command-bar/action-runtime.ts +2 -2
  66. package/src/components/command-bar/commands/direct/index.ts +1 -1
  67. package/src/components/command-bar/panel/body.tsx +2 -2
  68. package/src/components/command-bar/panel/index.tsx +11 -9
  69. package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
  70. package/src/components/command-bar/surface/index.tsx +2 -2
  71. package/src/components/command-bar/workflow/body.tsx +3 -3
  72. package/src/components/command-bar/workflow/collection-actions.ts +2 -2
  73. package/src/components/command-bar/workflow/coordinator.ts +2 -2
  74. package/src/components/command-bar/workflow/field-row.tsx +25 -11
  75. package/src/components/command-bar/workflow/tickers.ts +2 -2
  76. package/src/components/feed-data-table/stack-view.test.tsx +28 -36
  77. package/src/components/feed-data-table/stack-view.tsx +3 -1
  78. package/src/components/layout/detached-pane-shell.tsx +6 -6
  79. package/src/components/layout/header.tsx +13 -9
  80. package/src/components/layout/pane/content.test.tsx +42 -3
  81. package/src/components/layout/pane/content.tsx +3 -1
  82. package/src/components/layout/pane/footer/index.test.tsx +45 -2
  83. package/src/components/layout/pane/footer/registration.tsx +3 -1
  84. package/src/components/layout/shell/index.tsx +1 -2
  85. package/src/components/layout/shell/native/surfaces.ts +1 -1
  86. package/src/components/layout/shell/native/window-state.ts +1 -1
  87. package/src/components/layout/status-bar.tsx +12 -8
  88. package/src/components/layout/window-controls.tsx +8 -6
  89. package/src/components/onboarding/onboarding-wizard.tsx +3 -1
  90. package/src/components/pane-settings-dialog/desktop.tsx +0 -2
  91. package/src/components/pane-settings-dialog.tsx +0 -2
  92. package/src/components/pane-template-wizard.tsx +10 -9
  93. package/src/components/price-performance.tsx +2 -113
  94. package/src/components/price-sparkline/model.ts +15 -17
  95. package/src/components/price-sparkline/view.tsx +8 -7
  96. package/src/components/speedometer-gauge/desktop.tsx +2 -1
  97. package/src/components/speedometer-gauge/index.tsx +0 -2
  98. package/src/components/speedometer-gauge/terminal.tsx +24 -5
  99. package/src/components/ticker/badge/index.tsx +1 -18
  100. package/src/components/ui/data-table/opentui/index.tsx +3 -3
  101. package/src/components/ui/frame.tsx +1 -2
  102. package/src/components/ui/page-stack-view.test.tsx +126 -0
  103. package/src/components/ui/page-stack-view.tsx +24 -12
  104. package/src/components/ui/tabs.tsx +7 -3
  105. package/src/components/ui/ui.test.tsx +23 -2
  106. package/src/components/use-external-link-footer.ts +4 -2
  107. package/src/core/app-service-ports.ts +46 -0
  108. package/src/core/app-services.test.ts +66 -0
  109. package/src/core/app-services.ts +17 -15
  110. package/src/data/config/chart-settings.ts +3 -10
  111. package/src/data/config/store/index.test.ts +99 -2
  112. package/src/data/config/store/migrations.ts +278 -0
  113. package/src/data/config/store/normalize.ts +44 -186
  114. package/src/data/resource-store.ts +73 -12
  115. package/src/data/sqlite/cache.test.ts +104 -15
  116. package/src/i18n/index.test.ts +17 -0
  117. package/src/i18n/index.ts +23 -6
  118. package/src/i18n/ja.ts +13 -0
  119. package/src/i18n/ko.ts +13 -0
  120. package/src/i18n/react.ts +6 -0
  121. package/src/i18n/zh-cn.ts +13 -0
  122. package/src/i18n/zh-tw.ts +13 -0
  123. package/src/market-data/coordinator/auxiliary.ts +7 -3
  124. package/src/market-data/coordinator/chart.ts +1 -1
  125. package/src/market-data/coordinator/index.test.ts +26 -0
  126. package/src/market-data/coordinator/index.ts +11 -5
  127. package/src/market-data/coordinator/quotes.ts +4 -40
  128. package/src/market-data/coordinator/subscriptions.test.ts +89 -1
  129. package/src/market-data/hooks.tsx +47 -1
  130. package/src/market-data/market/status.ts +6 -6
  131. package/src/market-data/performance.ts +1 -1
  132. package/src/market-data/quote-subscription-target.test.ts +37 -0
  133. package/src/market-data/quote-subscription-target.ts +60 -0
  134. package/src/market-data/quotes/freshness.ts +1 -0
  135. package/src/market-data/quotes/resolution.ts +4 -0
  136. package/src/market-data/request-types.ts +3 -3
  137. package/src/news/aggregator.test.ts +97 -1
  138. package/src/news/aggregator.ts +100 -57
  139. package/src/news/hooks.test.tsx +64 -0
  140. package/src/news/hooks.ts +18 -25
  141. package/src/plugins/builtin/account-management/footer.ts +4 -2
  142. package/src/plugins/builtin/account-management/pane.tsx +9 -4
  143. package/src/plugins/builtin/ai/agent-history.ts +11 -7
  144. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
  145. package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
  146. package/src/plugins/builtin/ai/index.test.ts +2 -2
  147. package/src/plugins/builtin/ai/index.tsx +10 -11
  148. package/src/plugins/builtin/ai/pane-settings.ts +1 -1
  149. package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
  150. package/src/plugins/builtin/ai/providers.ts +1 -9
  151. package/src/plugins/builtin/ai/runner.ts +0 -6
  152. package/src/plugins/builtin/ai/screener/footer.ts +4 -1
  153. package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
  154. package/src/plugins/builtin/ai/workspace/model.ts +2 -27
  155. package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
  156. package/src/plugins/builtin/analytics/metrics.ts +1 -7
  157. package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
  158. package/src/plugins/builtin/broker-manager/index.tsx +4 -2
  159. package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
  160. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
  161. package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
  162. package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
  163. package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
  164. package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
  165. package/src/plugins/builtin/chart-composer/index.tsx +0 -6
  166. package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
  167. package/src/plugins/builtin/chart-composer/presets.ts +1 -19
  168. package/src/plugins/builtin/chart-composer/settings.ts +1 -1
  169. package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
  171. package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
  172. package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
  173. package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
  174. package/src/plugins/builtin/chat/controller/session.ts +2 -0
  175. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  176. package/src/plugins/builtin/chat/controller.test.ts +73 -20
  177. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  178. package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
  179. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
  180. package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
  181. package/src/plugins/builtin/correlation/compute.ts +4 -10
  182. package/src/plugins/builtin/econ/index.tsx +17 -19
  183. package/src/plugins/builtin/holders/types.ts +0 -1
  184. package/src/plugins/builtin/insider/index.tsx +6 -31
  185. package/src/plugins/builtin/news/index.tsx +5 -7
  186. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
  187. package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
  188. package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
  189. package/src/plugins/builtin/news/wire/read-state.ts +18 -28
  190. package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
  191. package/src/plugins/builtin/options/footer.test.ts +115 -0
  192. package/src/plugins/builtin/options/footer.ts +110 -0
  193. package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
  194. package/src/plugins/builtin/options/live-quotes.ts +147 -0
  195. package/src/plugins/builtin/options/view.test.tsx +71 -1
  196. package/src/plugins/builtin/options/view.tsx +39 -24
  197. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
  198. package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
  199. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
  200. package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
  201. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
  202. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
  203. package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
  204. package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
  205. package/src/plugins/builtin/sec/filing-content.ts +94 -0
  206. package/src/plugins/builtin/sec/index.tsx +21 -104
  207. package/src/plugins/builtin/sectors/sector-model.ts +1 -7
  208. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
  209. package/src/plugins/builtin/shared/pane-footer.ts +82 -0
  210. package/src/plugins/builtin/shared/read-state.ts +64 -3
  211. package/src/plugins/builtin/substack/read-state.ts +16 -41
  212. package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
  213. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
  214. package/src/plugins/builtin/yield-curve/index.tsx +11 -9
  215. package/src/plugins/ibkr/dialogs.tsx +3 -3
  216. package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
  217. package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
  218. package/src/plugins/ibkr/gateway/history.ts +2 -2
  219. package/src/plugins/ibkr/gateway/selection.ts +2 -2
  220. package/src/plugins/ibkr/gateway/service/index.ts +2 -2
  221. package/src/plugins/ibkr/gateway/service/native.ts +2 -2
  222. package/src/plugins/plugin-persistence.ts +3 -4
  223. package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
  224. package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
  225. package/src/plugins/prediction-markets/detail/book.tsx +2 -2
  226. package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
  227. package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
  228. package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
  229. package/src/plugins/registry/context.ts +3 -4
  230. package/src/plugins/registry/index.ts +5 -6
  231. package/src/react/input.test.ts +126 -3
  232. package/src/react/input.ts +120 -1
  233. package/src/remote/app-host.tsx +2 -13
  234. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
  235. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
  236. package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
  237. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
  238. package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
  239. package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
  240. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
  241. package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
  242. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
  243. package/src/renderers/electrobun/bun/index.ts +123 -83
  244. package/src/renderers/electrobun/shared/protocol.ts +146 -7
  245. package/src/renderers/electrobun/view/app-services.ts +5 -8
  246. package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
  247. package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
  248. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
  249. package/src/renderers/electrobun/view/config-host.ts +1 -1
  250. package/src/renderers/electrobun/view/context-menu.ts +1 -17
  251. package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
  252. package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
  253. package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
  254. package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
  255. package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
  256. package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
  257. package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
  258. package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
  259. package/src/renderers/electrobun/view/host/box.tsx +0 -1
  260. package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
  261. package/src/renderers/electrobun/view/host/input.tsx +0 -1
  262. package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
  263. package/src/renderers/electrobun/view/host/native.ts +0 -1
  264. package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
  265. package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
  266. package/src/renderers/electrobun/view/host/text.tsx +0 -1
  267. package/src/renderers/electrobun/view/http-fetch.ts +3 -11
  268. package/src/renderers/electrobun/view/input-host.tsx +10 -53
  269. package/src/renderers/electrobun/view/key-event.ts +0 -1
  270. package/src/renderers/electrobun/view/main.tsx +3 -3
  271. package/src/renderers/electrobun/view/native-renderer.ts +0 -1
  272. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
  273. package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
  274. package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
  275. package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
  276. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  277. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  278. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  279. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  280. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  281. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  282. package/src/renderers/electrobun/view/update-host.ts +1 -1
  283. package/src/renderers/opentui/chart-surface.tsx +2 -1
  284. package/src/renderers/opentui/dialog-host.tsx +20 -4
  285. package/src/renderers/opentui/image/loader.ts +12 -2
  286. package/src/renderers/opentui/input-host.tsx +7 -49
  287. package/src/renderers/opentui/start.tsx +2 -0
  288. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  289. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  290. package/src/sources/gloomberb-cloud/index.ts +42 -20
  291. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  292. package/src/sources/provider-router/cache.ts +1 -1
  293. package/src/sources/provider-router/document-routes.ts +10 -5
  294. package/src/sources/provider-router/financials.test.ts +14 -0
  295. package/src/sources/provider-router/financials.ts +6 -1
  296. package/src/sources/provider-router/history.test.ts +163 -1
  297. package/src/sources/provider-router/history.ts +278 -347
  298. package/src/sources/provider-router/index.ts +2 -2
  299. package/src/sources/provider-router/routing.test.ts +39 -0
  300. package/src/sources/provider-router/routing.ts +24 -11
  301. package/src/sources/provider-router/supplemental.ts +37 -17
  302. package/src/sources/sec-edgar/content.ts +2 -12
  303. package/src/sources/sec-edgar.ts +2 -13
  304. package/src/sources/yahoo-finance/history.ts +2 -2
  305. package/src/sources/yahoo-finance/options.ts +5 -0
  306. package/src/sources/yahoo-finance.ts +2 -2
  307. package/src/state/app/bootstrap.ts +2 -2
  308. package/src/state/app/context/index.tsx +2 -2
  309. package/src/state/hooks/quote-streaming.ts +66 -1
  310. package/src/state/hooks/session-snapshot.ts +2 -2
  311. package/src/state/selectors.ts +0 -9
  312. package/src/sync/controller.test.ts +98 -0
  313. package/src/sync/controller.ts +29 -5
  314. package/src/sync/react.ts +2 -2
  315. package/src/sync/types.ts +2 -2
  316. package/src/theme/colors.test.ts +10 -0
  317. package/src/theme/colors.ts +75 -78
  318. package/src/theme/theme-context.test.tsx +67 -0
  319. package/src/theme/theme-context.tsx +9 -5
  320. package/src/tickers/open-target.ts +2 -2
  321. package/src/tickers/search/upsert.ts +2 -2
  322. package/src/time-series/alignment.ts +0 -7
  323. package/src/time-series/chart-data.ts +145 -0
  324. package/src/time-series/date-window.ts +44 -0
  325. package/src/time-series/history-window.ts +21 -0
  326. package/src/time-series/hooks.test.ts +59 -0
  327. package/src/time-series/hooks.ts +33 -12
  328. package/src/time-series/market-resolution.ts +2 -2
  329. package/src/time-series/market.ts +1 -1
  330. package/src/time-series/range.ts +9 -0
  331. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  332. package/src/time-series/resolve.test.ts +203 -1
  333. package/src/time-series/resolve.ts +96 -18
  334. package/src/time-series/spec-studies.test.ts +15 -0
  335. package/src/time-series/spec.ts +2 -2
  336. package/src/time-series/studies.ts +5 -2
  337. package/src/time-series/types.ts +3 -1
  338. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  339. package/src/time-series/use-chart-resolution.ts +46 -4
  340. package/src/types/broker.ts +2 -2
  341. package/src/types/data-provider.ts +3 -3
  342. package/src/types/financials.ts +12 -0
  343. package/src/types/plugin.ts +2 -2
  344. package/src/ui/context-menu.tsx +3 -2
  345. package/src/ui/dialog.tsx +2 -2
  346. package/src/utils/html-entities.ts +3 -5
  347. package/src/utils/main-thread-monitor.ts +1 -1
  348. package/src/utils/price-history.ts +5 -5
  349. package/src/version.ts +3 -1
  350. package/src/components/chart/core/viewport.test.ts +0 -8
  351. package/src/components/chart/core/viewport.ts +0 -24
  352. package/src/time-series/index.ts +0 -14
  353. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
@@ -0,0 +1,608 @@
1
+ import type { CompositeViewportRange } from "./interactions";
2
+ import type { CompositeChartScene } from "./types";
3
+
4
+ const MONTHS = [
5
+ "Jan",
6
+ "Feb",
7
+ "Mar",
8
+ "Apr",
9
+ "May",
10
+ "Jun",
11
+ "Jul",
12
+ "Aug",
13
+ "Sep",
14
+ "Oct",
15
+ "Nov",
16
+ "Dec",
17
+ ] as const;
18
+
19
+ const SECOND_MS = 1_000;
20
+ const MINUTE_MS = 60 * SECOND_MS;
21
+ const HOUR_MS = 60 * MINUTE_MS;
22
+ const DAY_MS = 24 * HOUR_MS;
23
+ const WEEK_MS = 7 * DAY_MS;
24
+ const MONTH_MS = 30.4375 * DAY_MS;
25
+ const YEAR_MS = 365.25 * DAY_MS;
26
+ const MONDAY_ANCHOR_MS = Date.UTC(1970, 0, 5);
27
+
28
+ type CompositeTimeAxisUnit =
29
+ | "millisecond"
30
+ | "second"
31
+ | "minute"
32
+ | "hour"
33
+ | "day"
34
+ | "week"
35
+ | "month"
36
+ | "year";
37
+
38
+ interface CompositeTimeAxisInterval {
39
+ unit: CompositeTimeAxisUnit;
40
+ step: number;
41
+ approximateMs: number;
42
+ }
43
+
44
+ interface CompositeTimeAxisSample {
45
+ timestamp: number;
46
+ ratio: number;
47
+ }
48
+
49
+ interface CompositeTimeAxisCandidate extends CompositeTimeAxisSample {
50
+ boundary: boolean;
51
+ }
52
+
53
+ export interface CompositeTimeAxisTick extends CompositeTimeAxisSample {
54
+ label: string;
55
+ start: number;
56
+ end: number;
57
+ }
58
+
59
+ export interface CompositeTimeAxisLayout {
60
+ text: string;
61
+ ticks: CompositeTimeAxisTick[];
62
+ }
63
+
64
+ const TIME_AXIS_INTERVALS: readonly CompositeTimeAxisInterval[] = [
65
+ { unit: "millisecond", step: 1, approximateMs: 1 },
66
+ { unit: "millisecond", step: 5, approximateMs: 5 },
67
+ { unit: "millisecond", step: 10, approximateMs: 10 },
68
+ { unit: "millisecond", step: 50, approximateMs: 50 },
69
+ { unit: "millisecond", step: 100, approximateMs: 100 },
70
+ { unit: "millisecond", step: 250, approximateMs: 250 },
71
+ { unit: "millisecond", step: 500, approximateMs: 500 },
72
+ { unit: "second", step: 1, approximateMs: SECOND_MS },
73
+ { unit: "second", step: 5, approximateMs: 5 * SECOND_MS },
74
+ { unit: "second", step: 15, approximateMs: 15 * SECOND_MS },
75
+ { unit: "second", step: 30, approximateMs: 30 * SECOND_MS },
76
+ { unit: "minute", step: 1, approximateMs: MINUTE_MS },
77
+ { unit: "minute", step: 5, approximateMs: 5 * MINUTE_MS },
78
+ { unit: "minute", step: 15, approximateMs: 15 * MINUTE_MS },
79
+ { unit: "minute", step: 30, approximateMs: 30 * MINUTE_MS },
80
+ { unit: "hour", step: 1, approximateMs: HOUR_MS },
81
+ { unit: "hour", step: 2, approximateMs: 2 * HOUR_MS },
82
+ { unit: "hour", step: 4, approximateMs: 4 * HOUR_MS },
83
+ { unit: "hour", step: 6, approximateMs: 6 * HOUR_MS },
84
+ { unit: "hour", step: 12, approximateMs: 12 * HOUR_MS },
85
+ { unit: "day", step: 1, approximateMs: DAY_MS },
86
+ { unit: "day", step: 2, approximateMs: 2 * DAY_MS },
87
+ { unit: "week", step: 1, approximateMs: WEEK_MS },
88
+ { unit: "week", step: 2, approximateMs: 2 * WEEK_MS },
89
+ { unit: "month", step: 1, approximateMs: MONTH_MS },
90
+ { unit: "month", step: 2, approximateMs: 2 * MONTH_MS },
91
+ { unit: "month", step: 3, approximateMs: 3 * MONTH_MS },
92
+ { unit: "month", step: 6, approximateMs: 6 * MONTH_MS },
93
+ { unit: "year", step: 1, approximateMs: YEAR_MS },
94
+ { unit: "year", step: 2, approximateMs: 2 * YEAR_MS },
95
+ { unit: "year", step: 5, approximateMs: 5 * YEAR_MS },
96
+ { unit: "year", step: 10, approximateMs: 10 * YEAR_MS },
97
+ { unit: "year", step: 20, approximateMs: 20 * YEAR_MS },
98
+ { unit: "year", step: 50, approximateMs: 50 * YEAR_MS },
99
+ ] as const;
100
+
101
+ const clamp = (value: number, min: number, max: number) => (
102
+ Math.max(min, Math.min(max, value))
103
+ );
104
+
105
+ function validTimestamp(value: number): boolean {
106
+ return Number.isFinite(value);
107
+ }
108
+
109
+ function axisLabelWidth(interval: CompositeTimeAxisInterval, startTime: number, endTime: number): number {
110
+ const start = new Date(startTime);
111
+ const end = new Date(endTime);
112
+ const spansDays = start.getUTCFullYear() !== end.getUTCFullYear()
113
+ || start.getUTCMonth() !== end.getUTCMonth()
114
+ || start.getUTCDate() !== end.getUTCDate();
115
+ const spansYears = start.getUTCFullYear() !== end.getUTCFullYear();
116
+
117
+ switch (interval.unit) {
118
+ case "millisecond":
119
+ return spansDays ? 20 : 12;
120
+ case "second":
121
+ return spansDays ? 16 : 8;
122
+ case "minute":
123
+ case "hour":
124
+ return spansDays ? 13 : 5;
125
+ case "day":
126
+ case "week":
127
+ return spansYears ? 11 : 6;
128
+ case "month":
129
+ return 8;
130
+ case "year":
131
+ return 4;
132
+ }
133
+ }
134
+
135
+ function resolveTimeAxisInterval(
136
+ startTime: number,
137
+ endTime: number,
138
+ width: number,
139
+ ): CompositeTimeAxisInterval {
140
+ const span = Math.max(endTime - startTime, 1);
141
+ for (const interval of TIME_AXIS_INTERVALS) {
142
+ const labelWidth = axisLabelWidth(interval, startTime, endTime);
143
+ const maximumLabels = Math.max(2, Math.floor(width / (labelWidth + 2)));
144
+ const estimatedLabels = Math.floor(span / interval.approximateMs) + 1;
145
+ if (estimatedLabels <= maximumLabels) return interval;
146
+ }
147
+ return TIME_AXIS_INTERVALS.at(-1)!;
148
+ }
149
+
150
+ function nextFixedBoundary(
151
+ startTime: number,
152
+ intervalMs: number,
153
+ anchorTime = 0,
154
+ ): number {
155
+ const steps = Math.floor((startTime - anchorTime) / intervalMs) + 1;
156
+ return anchorTime + steps * intervalMs;
157
+ }
158
+
159
+ function nextMonthBoundary(startTime: number, step: number): number {
160
+ const start = new Date(startTime);
161
+ const monthIndex = start.getUTCFullYear() * 12 + start.getUTCMonth();
162
+ let alignedIndex = Math.floor(monthIndex / step) * step;
163
+ let boundary = Date.UTC(
164
+ Math.floor(alignedIndex / 12),
165
+ alignedIndex % 12,
166
+ 1,
167
+ );
168
+ if (boundary <= startTime) {
169
+ alignedIndex += step;
170
+ boundary = Date.UTC(
171
+ Math.floor(alignedIndex / 12),
172
+ alignedIndex % 12,
173
+ 1,
174
+ );
175
+ }
176
+ return boundary;
177
+ }
178
+
179
+ function nextYearBoundary(startTime: number, step: number): number {
180
+ const startYear = new Date(startTime).getUTCFullYear();
181
+ let year = Math.floor(startYear / step) * step;
182
+ let boundary = Date.UTC(year, 0, 1);
183
+ if (boundary <= startTime) {
184
+ year += step;
185
+ boundary = Date.UTC(year, 0, 1);
186
+ }
187
+ return boundary;
188
+ }
189
+
190
+ function calendarBoundaries(
191
+ startTime: number,
192
+ endTime: number,
193
+ interval: CompositeTimeAxisInterval,
194
+ ): number[] {
195
+ let boundary: number;
196
+ let advance: (timestamp: number) => number;
197
+
198
+ switch (interval.unit) {
199
+ case "millisecond":
200
+ boundary = nextFixedBoundary(startTime, interval.step);
201
+ advance = (timestamp) => timestamp + interval.step;
202
+ break;
203
+ case "second":
204
+ boundary = nextFixedBoundary(startTime, interval.step * SECOND_MS);
205
+ advance = (timestamp) => timestamp + interval.step * SECOND_MS;
206
+ break;
207
+ case "minute":
208
+ boundary = nextFixedBoundary(startTime, interval.step * MINUTE_MS);
209
+ advance = (timestamp) => timestamp + interval.step * MINUTE_MS;
210
+ break;
211
+ case "hour":
212
+ boundary = nextFixedBoundary(startTime, interval.step * HOUR_MS);
213
+ advance = (timestamp) => timestamp + interval.step * HOUR_MS;
214
+ break;
215
+ case "day":
216
+ boundary = nextFixedBoundary(startTime, interval.step * DAY_MS);
217
+ advance = (timestamp) => timestamp + interval.step * DAY_MS;
218
+ break;
219
+ case "week":
220
+ boundary = nextFixedBoundary(
221
+ startTime,
222
+ interval.step * WEEK_MS,
223
+ MONDAY_ANCHOR_MS,
224
+ );
225
+ advance = (timestamp) => timestamp + interval.step * WEEK_MS;
226
+ break;
227
+ case "month":
228
+ boundary = nextMonthBoundary(startTime, interval.step);
229
+ advance = (timestamp) => {
230
+ const date = new Date(timestamp);
231
+ return Date.UTC(
232
+ date.getUTCFullYear(),
233
+ date.getUTCMonth() + interval.step,
234
+ 1,
235
+ );
236
+ };
237
+ break;
238
+ case "year":
239
+ boundary = nextYearBoundary(startTime, interval.step);
240
+ advance = (timestamp) => (
241
+ Date.UTC(new Date(timestamp).getUTCFullYear() + interval.step, 0, 1)
242
+ );
243
+ break;
244
+ }
245
+
246
+ const boundaries: number[] = [];
247
+ while (boundary < endTime && boundaries.length < 1_000) {
248
+ boundaries.push(boundary);
249
+ const next = advance(boundary);
250
+ if (!validTimestamp(next) || next <= boundary) break;
251
+ boundary = next;
252
+ }
253
+ return boundaries;
254
+ }
255
+
256
+ function intervalBucketKey(
257
+ timestamp: number,
258
+ interval: CompositeTimeAxisInterval,
259
+ ): number {
260
+ switch (interval.unit) {
261
+ case "millisecond":
262
+ return Math.floor(timestamp / interval.step);
263
+ case "second":
264
+ return Math.floor(timestamp / (interval.step * SECOND_MS));
265
+ case "minute":
266
+ return Math.floor(timestamp / (interval.step * MINUTE_MS));
267
+ case "hour":
268
+ return Math.floor(timestamp / (interval.step * HOUR_MS));
269
+ case "day":
270
+ return Math.floor(timestamp / (interval.step * DAY_MS));
271
+ case "week":
272
+ return Math.floor((timestamp - MONDAY_ANCHOR_MS) / (interval.step * WEEK_MS));
273
+ case "month": {
274
+ const date = new Date(timestamp);
275
+ const monthIndex = date.getUTCFullYear() * 12 + date.getUTCMonth();
276
+ return Math.floor(monthIndex / interval.step);
277
+ }
278
+ case "year":
279
+ return Math.floor(new Date(timestamp).getUTCFullYear() / interval.step);
280
+ }
281
+ }
282
+
283
+ function lowerBoundSample(samples: readonly CompositeTimeAxisSample[], timestamp: number): number {
284
+ let low = 0;
285
+ let high = samples.length;
286
+ while (low < high) {
287
+ const middle = low + Math.floor((high - low) / 2);
288
+ if (samples[middle]!.timestamp < timestamp) low = middle + 1;
289
+ else high = middle;
290
+ }
291
+ return low;
292
+ }
293
+
294
+ function normalizeMarketSamples(scene: CompositeChartScene): CompositeTimeAxisSample[] {
295
+ const samples = scene.dates.flatMap((date, index) => {
296
+ const timestamp = date.getTime();
297
+ const ratio = scene.dateRatios[index];
298
+ return validTimestamp(timestamp) && typeof ratio === "number" && Number.isFinite(ratio)
299
+ ? [{ timestamp, ratio: clamp(ratio, 0, 1) }]
300
+ : [];
301
+ }).sort((left, right) => left.timestamp - right.timestamp || left.ratio - right.ratio);
302
+
303
+ return samples.filter((sample, index) => (
304
+ index === 0
305
+ || sample.timestamp !== samples[index - 1]!.timestamp
306
+ || sample.ratio !== samples[index - 1]!.ratio
307
+ ));
308
+ }
309
+
310
+ function sameUtcDay(left: number, right: number): boolean {
311
+ const leftDate = new Date(left);
312
+ const rightDate = new Date(right);
313
+ return leftDate.getUTCFullYear() === rightDate.getUTCFullYear()
314
+ && leftDate.getUTCMonth() === rightDate.getUTCMonth()
315
+ && leftDate.getUTCDate() === rightDate.getUTCDate();
316
+ }
317
+
318
+ function sameUtcMonth(left: number, right: number): boolean {
319
+ const leftDate = new Date(left);
320
+ const rightDate = new Date(right);
321
+ return leftDate.getUTCFullYear() === rightDate.getUTCFullYear()
322
+ && leftDate.getUTCMonth() === rightDate.getUTCMonth();
323
+ }
324
+
325
+ function clockLabel(timestamp: number, unit: CompositeTimeAxisUnit): string {
326
+ const date = new Date(timestamp);
327
+ const hours = String(date.getUTCHours()).padStart(2, "0");
328
+ const minutes = String(date.getUTCMinutes()).padStart(2, "0");
329
+ const seconds = String(date.getUTCSeconds()).padStart(2, "0");
330
+ const milliseconds = String(date.getUTCMilliseconds()).padStart(3, "0");
331
+ if (unit === "millisecond") return `${hours}:${minutes}:${seconds}.${milliseconds}`;
332
+ if (unit === "second") return `${hours}:${minutes}:${seconds}`;
333
+ return `${hours}:${minutes}`;
334
+ }
335
+
336
+ function calendarLabel(timestamp: number, includeYear: boolean): string {
337
+ const date = new Date(timestamp);
338
+ const base = `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`;
339
+ return includeYear ? `${base} ${date.getUTCFullYear()}` : base;
340
+ }
341
+
342
+ function formatBoundaryLabel(
343
+ timestamp: number,
344
+ counterpart: number,
345
+ interval: CompositeTimeAxisInterval,
346
+ includeTimeZone = true,
347
+ includeBoundaryYear = true,
348
+ ): string {
349
+ const date = new Date(timestamp);
350
+ switch (interval.unit) {
351
+ case "millisecond":
352
+ case "second":
353
+ case "minute":
354
+ case "hour": {
355
+ const clock = clockLabel(timestamp, interval.unit);
356
+ const suffix = includeTimeZone ? " UTC" : "";
357
+ if (sameUtcDay(timestamp, counterpart)) return `${clock}${suffix}`;
358
+ const includeYear = includeBoundaryYear
359
+ && date.getUTCFullYear() !== new Date(counterpart).getUTCFullYear();
360
+ return `${calendarLabel(timestamp, includeYear)} ${clock}${suffix}`;
361
+ }
362
+ case "day":
363
+ case "week":
364
+ return calendarLabel(
365
+ timestamp,
366
+ includeBoundaryYear
367
+ && date.getUTCFullYear() !== new Date(counterpart).getUTCFullYear(),
368
+ );
369
+ case "month":
370
+ return calendarLabel(timestamp, includeBoundaryYear);
371
+ case "year":
372
+ return `${date.getUTCFullYear()}`;
373
+ }
374
+ }
375
+
376
+ function formatInteriorLabel(
377
+ timestamp: number,
378
+ previousTimestamp: number,
379
+ interval: CompositeTimeAxisInterval,
380
+ ): string {
381
+ const date = new Date(timestamp);
382
+ const previous = new Date(previousTimestamp);
383
+ switch (interval.unit) {
384
+ case "millisecond":
385
+ case "second":
386
+ case "minute":
387
+ case "hour": {
388
+ const clock = clockLabel(timestamp, interval.unit);
389
+ if (sameUtcDay(timestamp, previousTimestamp)) return clock;
390
+ return `${calendarLabel(
391
+ timestamp,
392
+ date.getUTCFullYear() !== previous.getUTCFullYear(),
393
+ )} ${clock}`;
394
+ }
395
+ case "day":
396
+ case "week":
397
+ if (sameUtcMonth(timestamp, previousTimestamp)) return `${date.getUTCDate()}`;
398
+ return calendarLabel(
399
+ timestamp,
400
+ date.getUTCFullYear() !== previous.getUTCFullYear(),
401
+ );
402
+ case "month":
403
+ return date.getUTCFullYear() === previous.getUTCFullYear()
404
+ ? MONTHS[date.getUTCMonth()]!
405
+ : `${MONTHS[date.getUTCMonth()]} ${date.getUTCFullYear()}`;
406
+ case "year":
407
+ return `${date.getUTCFullYear()}`;
408
+ }
409
+ }
410
+
411
+ function resolveLabelStart(center: number, label: string, width: number): number {
412
+ return clamp(
413
+ Math.round(center - label.length / 2),
414
+ 0,
415
+ Math.max(width - label.length, 0),
416
+ );
417
+ }
418
+
419
+ function writeLabel(axis: string[], start: number, label: string): void {
420
+ for (let index = 0; index < label.length && start + index < axis.length; index += 1) {
421
+ axis[start + index] = label[index]!;
422
+ }
423
+ }
424
+
425
+ function addCandidate(
426
+ candidates: CompositeTimeAxisCandidate[],
427
+ candidate: CompositeTimeAxisCandidate,
428
+ ): void {
429
+ const duplicate = candidates.some((existing) => (
430
+ Math.abs(existing.ratio - candidate.ratio) < 1e-9
431
+ || existing.timestamp === candidate.timestamp
432
+ ));
433
+ if (!duplicate) candidates.push(candidate);
434
+ }
435
+
436
+ function layoutTimeAxis({
437
+ startTime,
438
+ endTime,
439
+ width,
440
+ samples,
441
+ }: {
442
+ startTime: number;
443
+ endTime: number;
444
+ width: number;
445
+ samples?: readonly CompositeTimeAxisSample[];
446
+ }): CompositeTimeAxisLayout {
447
+ const axisWidth = Math.max(1, Math.floor(width));
448
+ const axis = Array(axisWidth).fill(" ");
449
+ if (!validTimestamp(startTime) || !validTimestamp(endTime) || endTime < startTime) {
450
+ return { text: axis.join(""), ticks: [] };
451
+ }
452
+
453
+ const interval = resolveTimeAxisInterval(startTime, endTime, axisWidth);
454
+ const marketSamples = samples?.length ? [...samples] : null;
455
+ const firstTimestamp = startTime;
456
+ const lastTimestamp = endTime;
457
+ const candidates: CompositeTimeAxisCandidate[] = [{
458
+ timestamp: firstTimestamp,
459
+ ratio: 0,
460
+ boundary: true,
461
+ }];
462
+ const firstBucket = intervalBucketKey(firstTimestamp, interval);
463
+
464
+ for (const boundary of calendarBoundaries(startTime, endTime, interval)) {
465
+ if (marketSamples) {
466
+ const sample = marketSamples[lowerBoundSample(marketSamples, boundary)];
467
+ if (
468
+ sample
469
+ && intervalBucketKey(sample.timestamp, interval) !== firstBucket
470
+ && sample.timestamp < lastTimestamp
471
+ && sample.ratio < 1
472
+ ) {
473
+ addCandidate(candidates, { ...sample, boundary: false });
474
+ }
475
+ continue;
476
+ }
477
+ const bucket = intervalBucketKey(boundary, interval);
478
+ if (bucket === firstBucket) continue;
479
+ const span = Math.max(endTime - startTime, 1);
480
+ addCandidate(candidates, {
481
+ timestamp: boundary,
482
+ ratio: clamp((boundary - startTime) / span, 0, 1),
483
+ boundary: false,
484
+ });
485
+ }
486
+
487
+ addCandidate(candidates, {
488
+ timestamp: lastTimestamp,
489
+ ratio: 1,
490
+ boundary: true,
491
+ });
492
+ candidates.sort((left, right) => left.ratio - right.ratio || left.timestamp - right.timestamp);
493
+
494
+ if (candidates.length === 1 || firstTimestamp === lastTimestamp) {
495
+ const label = formatBoundaryLabel(firstTimestamp, lastTimestamp, interval);
496
+ const visibleLabel = label.slice(0, axisWidth);
497
+ const start = Math.max(Math.floor((axisWidth - visibleLabel.length) / 2), 0);
498
+ writeLabel(axis, start, visibleLabel);
499
+ return {
500
+ text: axis.join(""),
501
+ ticks: [{
502
+ timestamp: firstTimestamp,
503
+ ratio: 0.5,
504
+ label: visibleLabel,
505
+ start,
506
+ end: start + visibleLabel.length - 1,
507
+ }],
508
+ };
509
+ }
510
+
511
+ let startLabel = formatBoundaryLabel(firstTimestamp, lastTimestamp, interval);
512
+ let endLabel = formatBoundaryLabel(lastTimestamp, firstTimestamp, interval);
513
+ if (startLabel.length + endLabel.length + 1 > axisWidth) {
514
+ startLabel = formatBoundaryLabel(firstTimestamp, lastTimestamp, interval, false);
515
+ endLabel = formatBoundaryLabel(lastTimestamp, firstTimestamp, interval, false);
516
+ }
517
+ if (startLabel.length + endLabel.length + 1 > axisWidth) {
518
+ startLabel = formatBoundaryLabel(firstTimestamp, lastTimestamp, interval, false, false);
519
+ endLabel = formatBoundaryLabel(lastTimestamp, firstTimestamp, interval, false, false);
520
+ }
521
+ if (startLabel.length + endLabel.length + 1 > axisWidth) {
522
+ const visibleLabel = startLabel.slice(0, axisWidth);
523
+ writeLabel(axis, 0, visibleLabel);
524
+ return {
525
+ text: axis.join(""),
526
+ ticks: [{
527
+ timestamp: firstTimestamp,
528
+ ratio: 0,
529
+ label: visibleLabel,
530
+ start: 0,
531
+ end: visibleLabel.length - 1,
532
+ }],
533
+ };
534
+ }
535
+
536
+ const placed: CompositeTimeAxisTick[] = [];
537
+ writeLabel(axis, 0, startLabel);
538
+ placed.push({
539
+ timestamp: firstTimestamp,
540
+ ratio: 0,
541
+ label: startLabel,
542
+ start: 0,
543
+ end: startLabel.length - 1,
544
+ });
545
+
546
+ const endPadding = axisWidth > startLabel.length + endLabel.length + 2 ? 1 : 0;
547
+ const endStart = axisWidth - endLabel.length - endPadding;
548
+ const minimumGap = interval.unit === "month" || interval.unit === "year" ? 2 : 1;
549
+ let previousTimestamp = firstTimestamp;
550
+ let previousEnd = startLabel.length - 1;
551
+ for (const candidate of candidates.filter((entry) => !entry.boundary)) {
552
+ const label = formatInteriorLabel(candidate.timestamp, previousTimestamp, interval);
553
+ const center = candidate.ratio * Math.max(axisWidth - 1, 0);
554
+ const start = resolveLabelStart(center, label, axisWidth);
555
+ const end = start + label.length - 1;
556
+ if (start <= previousEnd + minimumGap) continue;
557
+ if (end >= endStart - minimumGap) continue;
558
+ writeLabel(axis, start, label);
559
+ placed.push({
560
+ timestamp: candidate.timestamp,
561
+ ratio: candidate.ratio,
562
+ label,
563
+ start,
564
+ end,
565
+ });
566
+ previousTimestamp = candidate.timestamp;
567
+ previousEnd = end;
568
+ }
569
+
570
+ writeLabel(axis, endStart, endLabel);
571
+ placed.push({
572
+ timestamp: lastTimestamp,
573
+ ratio: 1,
574
+ label: endLabel,
575
+ start: endStart,
576
+ end: endStart + endLabel.length - 1,
577
+ });
578
+
579
+ return {
580
+ text: axis.join(""),
581
+ ticks: placed,
582
+ };
583
+ }
584
+
585
+ export function buildCompositeTimeAxisLayout(
586
+ scene: CompositeChartScene,
587
+ width: number,
588
+ ): CompositeTimeAxisLayout {
589
+ return layoutTimeAxis({
590
+ startTime: scene.startTime,
591
+ endTime: scene.endTime,
592
+ width,
593
+ samples: scene.timeScale.kind === "market"
594
+ ? normalizeMarketSamples(scene)
595
+ : undefined,
596
+ });
597
+ }
598
+
599
+ export function buildCompositeViewportTimeAxisLayout(
600
+ viewport: CompositeViewportRange,
601
+ width: number,
602
+ ): CompositeTimeAxisLayout {
603
+ return layoutTimeAxis({
604
+ startTime: viewport.start.getTime(),
605
+ endTime: viewport.end.getTime(),
606
+ width,
607
+ });
608
+ }
@@ -115,6 +115,8 @@ export interface CompositeChartProps {
115
115
  series: ResolvedSeries[];
116
116
  /** Optional legend model; can include hidden series that are not plotted. */
117
117
  legendSeries?: ResolvedSeries[];
118
+ /** Optional hidden market series used to preserve session-time navigation. */
119
+ timelineSeries?: ResolvedSeries[];
118
120
  panels: ChartPanelSpec[];
119
121
  width: number;
120
122
  height: number;
@@ -125,6 +127,11 @@ export interface CompositeChartProps {
125
127
  start: Date;
126
128
  end: Date;
127
129
  };
130
+ /**
131
+ * Stable identity for the authored viewport. Changing it resets user navigation;
132
+ * viewport updates under the same key are treated as adaptive data refreshes.
133
+ */
134
+ viewportResetKey?: string;
128
135
  colors?: Partial<CompositeChartColors>;
129
136
  axisWidth?: number;
130
137
  showLegend?: boolean;
@@ -134,7 +141,11 @@ export interface CompositeChartProps {
134
141
  emptyMessage?: string;
135
142
  formatValue?: (value: number, series: ResolvedSeries) => string;
136
143
  onCursorDateChange?: (date: Date | null) => void;
137
- onViewportChange?: (viewport: { start: Date; end: Date } | null) => void;
144
+ /** Reports a user-created viewport, or null when the user resets to the authored viewport. */
145
+ onViewportChange?: (
146
+ viewport: { start: Date; end: Date } | null,
147
+ interaction: "pan" | "reset" | "sync" | "zoom",
148
+ ) => void;
138
149
  onActivate?: () => void;
139
150
  onToggleSeries?: (seriesId: string) => void;
140
151
  isSeriesToggleable?: (series: ResolvedSeries) => boolean;