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
@@ -4,6 +4,7 @@ import {
4
4
  createElement,
5
5
  forwardRef,
6
6
  useMemo,
7
+ useState,
7
8
  type ForwardedRef,
8
9
  type ReactNode,
9
10
  } from "react";
@@ -24,6 +25,10 @@ import {
24
25
  type KeyEventLike,
25
26
  } from "../../../react/input";
26
27
  import { CompositeChart } from "./composite-chart";
28
+ import {
29
+ resolveCompositeMinimumSpanMs,
30
+ zoomCompositeViewport,
31
+ } from "./interactions";
27
32
 
28
33
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
29
34
  let chartShortcut: ((event: KeyEventLike) => void) | null = null;
@@ -154,6 +159,42 @@ function series(id: string, panelId: string, axis: ResolvedSeries["axis"], unit:
154
159
  };
155
160
  }
156
161
 
162
+ function twoSessionCandles(): ResolvedSeries {
163
+ const points = [
164
+ ...Array.from({ length: 7 }, (_, index) => (
165
+ Date.UTC(2025, 0, 2, 17, index * 30)
166
+ )),
167
+ ...Array.from({ length: 8 }, (_, index) => (
168
+ Date.UTC(2025, 0, 3, 13, 30 + index * 30)
169
+ )),
170
+ ].map((timestamp, index) => {
171
+ const date = new Date(timestamp);
172
+ const value = 100 + index * 0.1;
173
+ return {
174
+ date,
175
+ observedAt: date,
176
+ value,
177
+ open: value,
178
+ high: value + 1,
179
+ low: value - 1,
180
+ close: value,
181
+ volume: 100,
182
+ };
183
+ });
184
+ return {
185
+ ...series("price", "main", "left", "USD", []),
186
+ nativeFrequency: "intraday",
187
+ dataShape: "ohlc",
188
+ style: "candles",
189
+ timeBasis: {
190
+ kind: "market",
191
+ timeZone: "America/New_York",
192
+ cadenceMs: 30 * 60_000,
193
+ },
194
+ points,
195
+ };
196
+ }
197
+
157
198
  describe("CompositeChart", () => {
158
199
  test("lays out mixed panels with one shared legend and time axis", async () => {
159
200
  testSetup = await testRender(
@@ -180,8 +221,8 @@ describe("CompositeChart", () => {
180
221
  expect(frame).toContain("ACME Price");
181
222
  expect(frame).toContain("OTHER Revenue");
182
223
  expect(frame).toContain("$103");
183
- expect(frame.match(/2025-01-01/g)).toHaveLength(1);
184
- expect(frame.match(/2025-01-03/g)).toHaveLength(1);
224
+ expect(frame.match(/Jan 1/g)).toHaveLength(1);
225
+ expect(frame.match(/Jan 3/g)).toHaveLength(1);
185
226
  });
186
227
 
187
228
  test("keeps legend items compact and anchors an accessory to the right edge", async () => {
@@ -244,6 +285,42 @@ describe("CompositeChart", () => {
244
285
  expect(testSetup.captureCharFrame()).toContain("OTHER Revenue");
245
286
  });
246
287
 
288
+ test("uses buffered market anchors so closed sessions do not create chart holes", async () => {
289
+ const candles = twoSessionCandles();
290
+ const currentSession = {
291
+ ...candles,
292
+ points: candles.points.filter((point) => point.date.getUTCDate() === 3),
293
+ };
294
+ testSetup = await testRender(
295
+ <CompositeChart
296
+ width={72}
297
+ height={12}
298
+ series={[candles]}
299
+ legendSeries={[currentSession]}
300
+ panels={[{ id: "main" }]}
301
+ viewport={{
302
+ start: new Date("2025-01-02T17:00:00.000Z"),
303
+ end: new Date("2025-01-03T17:00:00.000Z"),
304
+ }}
305
+ showLegend={false}
306
+ />,
307
+ { width: 74, height: 14 },
308
+ );
309
+ await act(async () => testSetup!.renderOnce());
310
+
311
+ const bodyColumns = [...new Set(testSetup.captureCharFrame()
312
+ .split("\n")
313
+ .flatMap((line) => [...line].flatMap((cell, index) => cell === "█" ? [index] : [])))]
314
+ .sort((left, right) => left - right);
315
+ const largestGap = bodyColumns.slice(1).reduce(
316
+ (largest, column, index) => Math.max(largest, column - bodyColumns[index]!),
317
+ 0,
318
+ );
319
+
320
+ expect(bodyColumns.length).toBeGreaterThan(10);
321
+ expect(largestGap).toBeLessThan(10);
322
+ });
323
+
247
324
  test("shows an explicit legend toggle action on hover", async () => {
248
325
  const toggled: string[] = [];
249
326
  testSetup = await testRender(
@@ -443,6 +520,7 @@ describe("CompositeChart", () => {
443
520
 
444
521
  test("zooms with plus and resets the interaction viewport with zero", async () => {
445
522
  const viewportChanges: Array<{ start: string; end: string } | null> = [];
523
+ const viewportInteractions: string[] = [];
446
524
  testSetup = await testRender(
447
525
  <InputHostProvider host={chartInputHost}>
448
526
  <CompositeChart
@@ -455,20 +533,20 @@ describe("CompositeChart", () => {
455
533
  start: new Date("2025-01-01T00:00:00.000Z"),
456
534
  end: new Date("2025-01-09T00:00:00.000Z"),
457
535
  }}
458
- onViewportChange={(next) => viewportChanges.push(next
459
- ? { start: next.start.toISOString(), end: next.end.toISOString() }
460
- : null)}
536
+ onViewportChange={(next, interaction) => {
537
+ viewportInteractions.push(interaction);
538
+ viewportChanges.push(next
539
+ ? { start: next.start.toISOString(), end: next.end.toISOString() }
540
+ : null);
541
+ }}
461
542
  />
462
543
  </InputHostProvider>,
463
544
  { width: 62, height: 14 },
464
545
  );
465
546
 
466
547
  await act(async () => testSetup!.renderOnce());
467
- expect(testSetup.captureCharFrame()).toContain("2025-01-01");
468
- expect(viewportChanges).toEqual([{
469
- start: "2025-01-01T00:00:00.000Z",
470
- end: "2025-01-09T00:00:00.000Z",
471
- }]);
548
+ expect(testSetup.captureCharFrame()).toContain("Jan 1");
549
+ expect(viewportChanges).toEqual([]);
472
550
 
473
551
  const zoomIn = keyEvent("=");
474
552
  zoomIn.sequence = "+";
@@ -477,16 +555,322 @@ describe("CompositeChart", () => {
477
555
  await act(async () => testSetup!.renderOnce());
478
556
 
479
557
  expect(zoomIn.defaultPrevented).toBe(true);
480
- expect(testSetup.captureCharFrame()).not.toContain("2025-01-01");
558
+ expect(testSetup.captureCharFrame()).not.toContain("Jan 1");
481
559
  expect(viewportChanges.at(-1)?.start).not.toBe("2025-01-01T00:00:00.000Z");
560
+ expect(viewportInteractions.at(-1)).toBe("zoom");
482
561
 
483
562
  await act(async () => chartShortcut?.(keyEvent("0")));
484
563
  await act(async () => testSetup!.renderOnce());
485
- expect(testSetup.captureCharFrame()).toContain("2025-01-01");
486
- expect(viewportChanges.at(-1)).toEqual({
564
+ expect(testSetup.captureCharFrame()).toContain("Jan 1");
565
+ expect(viewportChanges.at(-1)).toBeNull();
566
+ expect(viewportInteractions.at(-1)).toBe("reset");
567
+ });
568
+
569
+ test("clears the interaction when zoom returns to the authored viewport", async () => {
570
+ const viewportChanges: Array<{ start: string; end: string } | null> = [];
571
+ testSetup = await testRender(
572
+ <InputHostProvider host={chartInputHost}>
573
+ <CompositeChart
574
+ width={60}
575
+ height={12}
576
+ focused
577
+ series={[series("price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108])]}
578
+ panels={[{ id: "main" }]}
579
+ viewport={{
580
+ start: new Date("2025-01-01T00:00:00.000Z"),
581
+ end: new Date("2025-01-09T00:00:00.000Z"),
582
+ }}
583
+ onViewportChange={(next) => viewportChanges.push(next
584
+ ? { start: next.start.toISOString(), end: next.end.toISOString() }
585
+ : null)}
586
+ />
587
+ </InputHostProvider>,
588
+ { width: 62, height: 14 },
589
+ );
590
+
591
+ await act(async () => testSetup!.renderOnce());
592
+ const zoomIn = keyEvent("=");
593
+ zoomIn.sequence = "+";
594
+ zoomIn.shift = true;
595
+ await act(async () => chartShortcut?.(zoomIn));
596
+ await act(async () => testSetup!.renderOnce());
597
+ expect(viewportChanges.at(-1)).not.toBeNull();
598
+
599
+ await act(async () => chartShortcut?.(keyEvent("-")));
600
+ await act(async () => testSetup!.renderOnce());
601
+
602
+ expect(viewportChanges.at(-1)).toBeNull();
603
+ });
604
+
605
+ test("preserves a zoomed viewport when adaptive data refreshes its buffer", async () => {
606
+ const viewportChanges: Array<{ start: string; end: string } | null> = [];
607
+ let replacePoints: ((points: TimeSeriesPoint[]) => void) | null = null;
608
+ const initialPoints = series(
609
+ "price",
610
+ "main",
611
+ "left",
612
+ "USD",
613
+ [100, 101, 102, 103, 104, 105, 106, 107, 108],
614
+ ).points;
615
+ function Harness() {
616
+ const [points, setPoints] = useState(initialPoints);
617
+ replacePoints = setPoints;
618
+ return (
619
+ <InputHostProvider host={chartInputHost}>
620
+ <CompositeChart
621
+ width={60}
622
+ height={12}
623
+ focused
624
+ series={[{
625
+ ...series("price", "main", "left", "USD", []),
626
+ points,
627
+ }]}
628
+ panels={[{ id: "main" }]}
629
+ viewport={{
630
+ start: new Date("2025-01-01T00:00:00.000Z"),
631
+ end: new Date("2025-01-09T00:00:00.000Z"),
632
+ }}
633
+ onViewportChange={(next) => viewportChanges.push(next
634
+ ? { start: next.start.toISOString(), end: next.end.toISOString() }
635
+ : null)}
636
+ />
637
+ </InputHostProvider>
638
+ );
639
+ }
640
+
641
+ testSetup = await testRender(
642
+ <Harness />,
643
+ { width: 62, height: 14 },
644
+ );
645
+ await act(async () => testSetup!.renderOnce());
646
+
647
+ for (let index = 0; index < 2; index += 1) {
648
+ const zoomIn = keyEvent("=");
649
+ zoomIn.sequence = "+";
650
+ zoomIn.shift = true;
651
+ await act(async () => chartShortcut?.(zoomIn));
652
+ await act(async () => testSetup!.renderOnce());
653
+ }
654
+ const zoomedViewport = viewportChanges.at(-1);
655
+ expect(zoomedViewport?.start).not.toBe("2025-01-01T00:00:00.000Z");
656
+ const changeCount = viewportChanges.length;
657
+
658
+ await act(async () => replacePoints?.(initialPoints.slice(2)));
659
+ await act(async () => {
660
+ await testSetup!.renderOnce();
661
+ await testSetup!.renderOnce();
662
+ });
663
+
664
+ expect(viewportChanges).toHaveLength(changeCount);
665
+ expect(viewportChanges.at(-1)).toEqual(zoomedViewport);
666
+ });
667
+
668
+ test("keeps a panned viewport when backfill moves beyond the authored range", async () => {
669
+ const viewportChanges: Array<{ start: string; end: string } | null> = [];
670
+ const viewportInteractions: string[] = [];
671
+ let replacePoints: ((points: TimeSeriesPoint[]) => void) | null = null;
672
+ const initialPoints = series(
673
+ "price",
674
+ "main",
675
+ "left",
676
+ "USD",
677
+ [100, 101, 102, 103, 104, 105, 106, 107, 108],
678
+ ).points;
679
+ const olderPoints = Array.from({ length: 11 }, (_, index) => {
680
+ const date = new Date(
681
+ Date.UTC(2024, 11, 25 + index) - (index === 10 ? 60_000 : 0),
682
+ );
683
+ return { date, observedAt: date, value: 90 + index };
684
+ });
685
+ function Harness() {
686
+ const [points, setPoints] = useState(initialPoints);
687
+ replacePoints = setPoints;
688
+ return (
689
+ <InputHostProvider host={chartInputHost}>
690
+ <CompositeChart
691
+ width={60}
692
+ height={12}
693
+ focused
694
+ series={[{
695
+ ...series("price", "main", "left", "USD", []),
696
+ points,
697
+ }]}
698
+ panels={[{ id: "main" }]}
699
+ viewport={{
700
+ start: new Date("2025-01-06T00:00:00.000Z"),
701
+ end: new Date("2025-01-09T00:00:00.000Z"),
702
+ }}
703
+ onViewportChange={(next, interaction) => {
704
+ viewportInteractions.push(interaction);
705
+ viewportChanges.push(next
706
+ ? { start: next.start.toISOString(), end: next.end.toISOString() }
707
+ : null);
708
+ }}
709
+ />
710
+ </InputHostProvider>
711
+ );
712
+ }
713
+
714
+ testSetup = await testRender(<Harness />, { width: 62, height: 14 });
715
+ await act(async () => testSetup!.renderOnce());
716
+ for (let index = 0; index < 120; index += 1) {
717
+ const panOlder = keyEvent("left");
718
+ panOlder.shift = true;
719
+ await act(async () => chartShortcut?.(panOlder));
720
+ await act(async () => testSetup!.renderOnce());
721
+ }
722
+ const pannedViewport = viewportChanges.at(-1);
723
+ expect(pannedViewport).toEqual({
487
724
  start: "2025-01-01T00:00:00.000Z",
488
- end: "2025-01-09T00:00:00.000Z",
725
+ end: "2025-01-04T00:00:00.000Z",
489
726
  });
727
+
728
+ await act(async () => replacePoints?.(olderPoints));
729
+ await act(async () => {
730
+ await testSetup!.renderOnce();
731
+ await testSetup!.renderOnce();
732
+ });
733
+
734
+ expect(viewportChanges.at(-1)).toEqual({
735
+ start: "2024-12-31T23:59:00.000Z",
736
+ end: "2025-01-03T23:59:00.000Z",
737
+ });
738
+ expect(viewportInteractions.at(-1)).toBe("sync");
739
+ expect(testSetup.captureCharFrame()).not.toContain("No chart data");
740
+ expect(testSetup.captureCharFrame()).not.toContain("Jan 9");
741
+ });
742
+
743
+ test("keeps the latest pan through delayed adaptive viewport and series echoes", async () => {
744
+ const viewportChanges: Array<{ start: string; end: string } | null> = [];
745
+ let publishViewport: ((
746
+ next: { start: string; end: string },
747
+ showSecondary?: boolean,
748
+ ) => void) | null = null;
749
+ let publishExternalViewport: (() => void) | null = null;
750
+ function Harness() {
751
+ const [viewport, setViewport] = useState({
752
+ start: new Date("2025-01-05T00:00:00.000Z"),
753
+ end: new Date("2025-01-09T00:00:00.000Z"),
754
+ });
755
+ const [showSecondary, setShowSecondary] = useState(false);
756
+ const [viewportResetKey, setViewportResetKey] = useState("price:1D:auto");
757
+ publishViewport = (next, nextShowSecondary = showSecondary) => {
758
+ setViewport({
759
+ start: new Date(next.start),
760
+ end: new Date(next.end),
761
+ });
762
+ setShowSecondary(nextShowSecondary);
763
+ };
764
+ publishExternalViewport = () => {
765
+ setViewport({
766
+ start: new Date("2025-01-01T00:00:00.000Z"),
767
+ end: new Date("2025-01-09T00:00:00.000Z"),
768
+ });
769
+ setViewportResetKey("price:1W:auto");
770
+ };
771
+ return (
772
+ <CompositeChart
773
+ width={60}
774
+ height={12}
775
+ interactive
776
+ series={[
777
+ series("price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108]),
778
+ series("secondary", "main", "left", "USD", showSecondary ? [90, 91, 92] : []),
779
+ ]}
780
+ panels={[{ id: "main" }]}
781
+ viewport={viewport}
782
+ viewportResetKey={viewportResetKey}
783
+ onViewportChange={(next) => {
784
+ viewportChanges.push(next
785
+ ? { start: next.start.toISOString(), end: next.end.toISOString() }
786
+ : null);
787
+ }}
788
+ />
789
+ );
790
+ }
791
+
792
+ testSetup = await testRender(
793
+ <CaptureChartSurfaceProvider>
794
+ <Harness />
795
+ </CaptureChartSurfaceProvider>,
796
+ { width: 62, height: 14 },
797
+ );
798
+ await act(async () => testSetup!.renderOnce());
799
+
800
+ for (let index = 0; index < 10; index += 1) {
801
+ await act(async () => {
802
+ capturedSurfaceProps!.onMouseScroll(pointerEvent(25, 3, {
803
+ scroll: { direction: "up", delta: 1 },
804
+ }));
805
+ });
806
+ await act(async () => {
807
+ await testSetup!.renderOnce();
808
+ await testSetup!.renderOnce();
809
+ });
810
+ }
811
+ const firstPan = viewportChanges[0]!;
812
+ const latestPan = viewportChanges.at(-1)!;
813
+ expect(firstPan).not.toBeNull();
814
+ expect(latestPan).not.toBeNull();
815
+ expect(latestPan).not.toEqual(firstPan);
816
+
817
+ await act(async () => publishViewport!(firstPan, true));
818
+ await act(async () => {
819
+ await testSetup!.renderOnce();
820
+ await testSetup!.renderOnce();
821
+ await testSetup!.renderOnce();
822
+ });
823
+ expect(viewportChanges).not.toContain(null);
824
+ expect(viewportChanges.at(-1)).toEqual(latestPan);
825
+
826
+ await act(async () => publishViewport!(latestPan));
827
+ await act(async () => {
828
+ await testSetup!.renderOnce();
829
+ await testSetup!.renderOnce();
830
+ await testSetup!.renderOnce();
831
+ });
832
+ expect(viewportChanges).not.toContain(null);
833
+ expect(viewportChanges.at(-1)).toEqual(latestPan);
834
+
835
+ for (let index = 0; index < 32; index += 1) {
836
+ await act(async () => {
837
+ capturedSurfaceProps!.onMouseScroll(pointerEvent(25, 3, {
838
+ scroll: { direction: "up", delta: 100 },
839
+ }));
840
+ });
841
+ await act(async () => {
842
+ await testSetup!.renderOnce();
843
+ await testSetup!.renderOnce();
844
+ });
845
+ }
846
+ const boundaryViewport = viewportChanges.at(-1)!;
847
+ expect(boundaryViewport).not.toBeNull();
848
+
849
+ await act(async () => publishViewport!(boundaryViewport));
850
+ await act(async () => {
851
+ await testSetup!.renderOnce();
852
+ await testSetup!.renderOnce();
853
+ });
854
+ const boundaryChangeCount = viewportChanges.length;
855
+
856
+ await act(async () => {
857
+ capturedSurfaceProps!.onMouseScroll(pointerEvent(25, 3, {
858
+ scroll: { direction: "up", delta: 100 },
859
+ }));
860
+ });
861
+ await act(async () => {
862
+ await testSetup!.renderOnce();
863
+ await testSetup!.renderOnce();
864
+ });
865
+ expect(viewportChanges).toHaveLength(boundaryChangeCount);
866
+ expect(viewportChanges).not.toContain(null);
867
+
868
+ await act(async () => publishExternalViewport!());
869
+ await act(async () => {
870
+ await testSetup!.renderOnce();
871
+ await testSetup!.renderOnce();
872
+ });
873
+ expect(viewportChanges.at(-1)).toBeNull();
490
874
  });
491
875
 
492
876
  test("activates and navigates a buffered viewport from the first mouse gesture", async () => {
@@ -515,7 +899,7 @@ describe("CompositeChart", () => {
515
899
  );
516
900
 
517
901
  await act(async () => testSetup!.renderOnce());
518
- expect(testSetup.captureCharFrame()).toContain("2025-01-05");
902
+ expect(testSetup.captureCharFrame()).toContain("Jan 5");
519
903
 
520
904
  await act(async () => {
521
905
  capturedSurfaceProps!.onMouseDown(pointerEvent(10, 3));
@@ -526,7 +910,7 @@ describe("CompositeChart", () => {
526
910
 
527
911
  expect(activations).toBe(1);
528
912
  expect(cursorChanges.length).toBeGreaterThan(0);
529
- expect(testSetup.captureCharFrame()).toContain("2025-01-03");
913
+ expect(testSetup.captureCharFrame()).toContain("Jan 3");
530
914
  });
531
915
 
532
916
  test("maps the pointer crosshair level through both axes and labels its date", async () => {
@@ -584,7 +968,7 @@ describe("CompositeChart", () => {
584
968
  );
585
969
 
586
970
  await act(async () => testSetup!.renderOnce());
587
- expect(testSetup.captureCharFrame()).toContain("2025-01-09");
971
+ expect(testSetup.captureCharFrame()).toContain("Jan 9");
588
972
 
589
973
  await act(async () => {
590
974
  capturedSurfaceProps!.onMouseScroll(pointerEvent(25, 3, {
@@ -594,7 +978,220 @@ describe("CompositeChart", () => {
594
978
  });
595
979
  await act(async () => testSetup!.renderOnce());
596
980
 
597
- expect(testSetup.captureCharFrame()).not.toContain("2025-01-09");
981
+ expect(testSetup.captureCharFrame()).not.toContain("Jan 9");
982
+ });
983
+
984
+ test("uses a supplied market timeline to keep control-wheel zoom under the pointer", async () => {
985
+ const anchor = twoSessionCandles();
986
+ const display = {
987
+ ...anchor,
988
+ id: "secondary",
989
+ label: "Secondary",
990
+ timeBasis: undefined,
991
+ };
992
+ const viewport = {
993
+ start: new Date(anchor.points[0]!.date.getTime()),
994
+ end: new Date(anchor.points.at(-1)!.date.getTime()),
995
+ };
996
+ const viewportChanges: Array<{ start: string; end: string } | null> = [];
997
+ testSetup = await testRender(
998
+ <CaptureChartSurfaceProvider>
999
+ <CompositeChart
1000
+ width={60}
1001
+ height={12}
1002
+ interactive
1003
+ series={[display]}
1004
+ timelineSeries={[anchor]}
1005
+ panels={[{ id: "main" }]}
1006
+ viewport={viewport}
1007
+ onViewportChange={(next) => viewportChanges.push(next
1008
+ ? { start: next.start.toISOString(), end: next.end.toISOString() }
1009
+ : null)}
1010
+ />
1011
+ </CaptureChartSurfaceProvider>,
1012
+ { width: 62, height: 14 },
1013
+ );
1014
+
1015
+ await act(async () => testSetup!.renderOnce());
1016
+ const pointerX = 35;
1017
+ const plotWidth = capturedSurfaceNode!.width as number;
1018
+ const minimumSpan = resolveCompositeMinimumSpanMs([display], viewport);
1019
+ const expected = zoomCompositeViewport(
1020
+ viewport,
1021
+ viewport,
1022
+ 1 + 4 * 0.04,
1023
+ pointerX / Math.max(plotWidth - 1, 1),
1024
+ minimumSpan,
1025
+ [anchor],
1026
+ );
1027
+
1028
+ await act(async () => {
1029
+ capturedSurfaceProps!.onMouseScroll(pointerEvent(pointerX, 3, {
1030
+ ctrl: true,
1031
+ scroll: { direction: "up", delta: 4 },
1032
+ }));
1033
+ });
1034
+ await act(async () => {
1035
+ await testSetup!.renderOnce();
1036
+ await testSetup!.renderOnce();
1037
+ });
1038
+
1039
+ expect(viewportChanges.at(-1)).toEqual({
1040
+ start: expected.start.toISOString(),
1041
+ end: expected.end.toISOString(),
1042
+ });
1043
+ });
1044
+
1045
+ test("clears a drag interaction when the pointer returns to the authored viewport", async () => {
1046
+ const viewportChanges: Array<{ start: string; end: string } | null> = [];
1047
+ const interactions: string[] = [];
1048
+ testSetup = await testRender(
1049
+ <CaptureChartSurfaceProvider>
1050
+ <CompositeChart
1051
+ width={60}
1052
+ height={12}
1053
+ interactive
1054
+ series={[series("price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108])]}
1055
+ panels={[{ id: "main" }]}
1056
+ viewport={{
1057
+ start: new Date("2025-01-05T00:00:00.000Z"),
1058
+ end: new Date("2025-01-09T00:00:00.000Z"),
1059
+ }}
1060
+ onViewportChange={(next, interaction) => {
1061
+ interactions.push(interaction);
1062
+ viewportChanges.push(next
1063
+ ? { start: next.start.toISOString(), end: next.end.toISOString() }
1064
+ : null);
1065
+ }}
1066
+ />
1067
+ </CaptureChartSurfaceProvider>,
1068
+ { width: 62, height: 14 },
1069
+ );
1070
+
1071
+ await act(async () => testSetup!.renderOnce());
1072
+ await act(async () => {
1073
+ capturedSurfaceProps!.onMouseDown(pointerEvent(20, 3));
1074
+ capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 3));
1075
+ });
1076
+ await act(async () => testSetup!.renderOnce());
1077
+ expect(viewportChanges.at(-1)).not.toBeNull();
1078
+
1079
+ await act(async () => {
1080
+ capturedSurfaceProps!.onMouseDrag(pointerEvent(20, 3));
1081
+ capturedSurfaceProps!.onMouseUp(pointerEvent(20, 3));
1082
+ });
1083
+ await act(async () => {
1084
+ await testSetup!.renderOnce();
1085
+ await testSetup!.renderOnce();
1086
+ });
1087
+
1088
+ expect(viewportChanges.at(-1)).toBeNull();
1089
+ expect(interactions.at(-1)).toBe("pan");
1090
+ });
1091
+
1092
+ test("always treats horizontal control-wheel movement as a pan", async () => {
1093
+ const interactions: string[] = [];
1094
+ testSetup = await testRender(
1095
+ <CaptureChartSurfaceProvider>
1096
+ <CompositeChart
1097
+ width={60}
1098
+ height={12}
1099
+ interactive
1100
+ series={[twoSessionCandles()]}
1101
+ panels={[{ id: "main" }]}
1102
+ viewport={{
1103
+ start: new Date("2025-01-03T13:30:00.000Z"),
1104
+ end: new Date("2025-01-03T17:00:00.000Z"),
1105
+ }}
1106
+ onViewportChange={(_next, interaction) => interactions.push(interaction)}
1107
+ />
1108
+ </CaptureChartSurfaceProvider>,
1109
+ { width: 62, height: 14 },
1110
+ );
1111
+
1112
+ await act(async () => testSetup!.renderOnce());
1113
+ await act(async () => {
1114
+ capturedSurfaceProps!.onMouseScroll(pointerEvent(25, 3, {
1115
+ ctrl: true,
1116
+ scroll: { direction: "left", delta: 4 },
1117
+ }));
1118
+ });
1119
+ await act(async () => {
1120
+ await testSetup!.renderOnce();
1121
+ await testSetup!.renderOnce();
1122
+ });
1123
+
1124
+ expect(interactions.at(-1)).toBe("pan");
1125
+ });
1126
+
1127
+ test("keeps the date axis and mouse recovery when refreshed data misses the zoomed window", async () => {
1128
+ let replacePoints: ((points: TimeSeriesPoint[]) => void) | null = null;
1129
+ function Harness() {
1130
+ const [points, setPoints] = useState(
1131
+ series("price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108]).points,
1132
+ );
1133
+ replacePoints = setPoints;
1134
+ return (
1135
+ <CompositeChart
1136
+ width={60}
1137
+ height={12}
1138
+ interactive
1139
+ series={[{
1140
+ ...series("price", "main", "left", "USD", []),
1141
+ points,
1142
+ }]}
1143
+ panels={[{ id: "main" }]}
1144
+ viewport={{
1145
+ start: new Date("2025-01-01T00:00:00.000Z"),
1146
+ end: new Date("2025-01-09T00:00:00.000Z"),
1147
+ }}
1148
+ />
1149
+ );
1150
+ }
1151
+
1152
+ testSetup = await testRender(
1153
+ <CaptureChartSurfaceProvider>
1154
+ <Harness />
1155
+ </CaptureChartSurfaceProvider>,
1156
+ { width: 62, height: 14 },
1157
+ );
1158
+ await act(async () => testSetup!.renderOnce());
1159
+
1160
+ for (let index = 0; index < 8; index += 1) {
1161
+ await act(async () => {
1162
+ capturedSurfaceProps!.onMouseScroll(pointerEvent(25, 3, {
1163
+ ctrl: true,
1164
+ scroll: { direction: "up", delta: 8 },
1165
+ }));
1166
+ });
1167
+ await act(async () => testSetup!.renderOnce());
1168
+ }
1169
+
1170
+ await act(async () => {
1171
+ replacePoints?.([
1172
+ point("2025-01-01", 100),
1173
+ point("2025-01-09", 108),
1174
+ ]);
1175
+ });
1176
+ await act(async () => testSetup!.renderOnce());
1177
+
1178
+ const emptyFrame = testSetup.captureCharFrame();
1179
+ expect(emptyFrame).toContain("No chart data");
1180
+ expect(emptyFrame).toContain("Jan ");
1181
+ expect(typeof capturedSurfaceProps!.onMouseScroll).toBe("function");
1182
+
1183
+ await act(async () => {
1184
+ capturedSurfaceProps!.onMouseScroll(pointerEvent(25, 3, {
1185
+ ctrl: true,
1186
+ scroll: { direction: "down", delta: 4 },
1187
+ }));
1188
+ });
1189
+ await act(async () => testSetup!.renderOnce());
1190
+
1191
+ const recoveredFrame = testSetup.captureCharFrame();
1192
+ expect(recoveredFrame).not.toContain("No chart data");
1193
+ expect(recoveredFrame).toContain("2025-01-01");
1194
+ expect(recoveredFrame).toContain("Jan 9");
598
1195
  });
599
1196
 
600
1197
  test("shows useful UTC times for an intraday shared cursor and time axis", async () => {