gloomberb 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (531) hide show
  1. package/README.md +7 -4
  2. package/README.zh-CN.md +4 -2
  3. package/package.json +28 -1
  4. package/src/api-client/auth.ts +10 -0
  5. package/src/api-client/data.ts +108 -1
  6. package/src/api-client/index.test.ts +127 -0
  7. package/src/api-client/index.ts +134 -1
  8. package/src/api-client/normalizers.ts +39 -0
  9. package/src/api-client/paths.ts +85 -1
  10. package/src/api-client/types.ts +220 -0
  11. package/src/app/pane-runtime/index.tsx +15 -12
  12. package/src/app/pane-runtime/layout-placement.test.ts +27 -0
  13. package/src/app/pane-runtime/layout-placement.ts +13 -0
  14. package/src/app/pane-runtime/plugin-bindings.ts +62 -0
  15. package/src/app.tsx +60 -18
  16. package/src/architecture/browser-entry-graph.test.ts +96 -0
  17. package/src/brokers/profile-form.test.ts +25 -27
  18. package/src/brokers/test-broker.ts +68 -0
  19. package/src/capabilities/chart-series.ts +3 -2
  20. package/src/cli/commands/market.ts +2 -2
  21. package/src/cli/commands/plugins.ts +39 -11
  22. package/src/cli/desktop-pane-shot.ts +285 -16
  23. package/src/cli/index.ts +1 -1
  24. package/src/cli/pane-functions/capabilities.ts +61 -31
  25. package/src/cli/pane-functions/catalog.ts +16 -4
  26. package/src/cli/pane-functions/discovery.ts +1 -0
  27. package/src/cli/pane-functions/headless.test.ts +247 -0
  28. package/src/cli/pane-functions/headless.ts +423 -0
  29. package/src/cli/pane-functions/index.test.ts +9 -0
  30. package/src/cli/pane-functions/index.ts +6 -2
  31. package/src/cli/pane-functions/options.ts +27 -2
  32. package/src/cli/pane-functions/report.ts +4 -0
  33. package/src/cli/pane-functions/resolver.ts +26 -8
  34. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  35. package/src/cli/pane-functions/screenshot.ts +236 -20
  36. package/src/cli/registry.ts +0 -22
  37. package/src/cli/restore-plugins.ts +34 -0
  38. package/src/cli/result.ts +7 -10
  39. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  40. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  41. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  42. package/src/components/chart/composite/composite-chart.test.tsx +68 -20
  43. package/src/components/chart/composite/composite-chart.tsx +301 -204
  44. package/src/components/chart/composite/format.ts +8 -2
  45. package/src/components/chart/composite/interactions.test.ts +68 -102
  46. package/src/components/chart/composite/interactions.ts +214 -264
  47. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  48. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  49. package/src/components/chart/composite/scene.ts +57 -3
  50. package/src/components/chart/composite/text-renderer.ts +3 -2
  51. package/src/components/chart/composite/time-scale.ts +138 -57
  52. package/src/components/chart/composite/types.ts +25 -1
  53. package/src/components/chart/core/data.ts +9 -13
  54. package/src/components/chart/core/date-window.ts +7 -1
  55. package/src/components/chart/core/pointer.ts +3 -0
  56. package/src/components/chart/core/types.ts +0 -45
  57. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  58. package/src/components/chart/native/kitty/index.test.ts +0 -188
  59. package/src/components/chart/native/raster/crosshair.ts +6 -5
  60. package/src/components/chart/native/raster/primitives.ts +0 -23
  61. package/src/components/chart/native/renderer-selection.ts +81 -31
  62. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  63. package/src/components/chart/static/chart-surface.tsx +150 -0
  64. package/src/components/chart/static/index.ts +8 -0
  65. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  66. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  67. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  68. package/src/components/chart/static/series.ts +49 -0
  69. package/src/components/command-bar/action-runtime.ts +1 -3
  70. package/src/components/command-bar/assist/model.test.ts +17 -2
  71. package/src/components/command-bar/assist/model.ts +40 -9
  72. package/src/components/command-bar/commands/direct/index.ts +1 -1
  73. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  74. package/src/components/command-bar/commands/registry.ts +0 -11
  75. package/src/components/command-bar/helpers.test.ts +2 -2
  76. package/src/components/command-bar/helpers.ts +2 -6
  77. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  78. package/src/components/command-bar/list/badge.test.ts +54 -0
  79. package/src/components/command-bar/list/badge.ts +62 -0
  80. package/src/components/command-bar/list/model.test.ts +103 -0
  81. package/src/components/command-bar/list/model.ts +61 -4
  82. package/src/components/command-bar/list/view.test.tsx +113 -0
  83. package/src/components/command-bar/list/view.tsx +147 -131
  84. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  85. package/src/components/command-bar/panel/body.tsx +51 -65
  86. package/src/components/command-bar/panel/index.tsx +48 -13
  87. package/src/components/command-bar/panel/layout.test.ts +139 -0
  88. package/src/components/command-bar/panel/layout.ts +65 -28
  89. package/src/components/command-bar/panel/native-surface.ts +24 -0
  90. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  91. package/src/components/command-bar/panel/runtime.ts +6 -3
  92. package/src/components/command-bar/panel/state.ts +41 -7
  93. package/src/components/command-bar/panel/types.ts +7 -1
  94. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  95. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  96. package/src/components/command-bar/routes/root/results.ts +30 -28
  97. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  98. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  99. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  100. package/src/components/command-bar/routes/root/selection.ts +1 -12
  101. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  102. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  103. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  104. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  105. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  106. package/src/components/command-bar/routing/actions.ts +1 -81
  107. package/src/components/command-bar/routing/launch-request.ts +1 -1
  108. package/src/components/command-bar/routing/list-state.ts +6 -21
  109. package/src/components/command-bar/selection-runtime.ts +1 -1
  110. package/src/components/command-bar/surface/assist.test.tsx +14 -10
  111. package/src/components/command-bar/surface/environment.ts +8 -1
  112. package/src/components/command-bar/surface/index.test.tsx +84 -68
  113. package/src/components/command-bar/surface/index.tsx +33 -36
  114. package/src/components/command-bar/surface/test-harness.tsx +15 -3
  115. package/src/components/command-bar/theme-picker.tsx +43 -15
  116. package/src/components/command-bar/view-model.test.ts +53 -8
  117. package/src/components/command-bar/view-model.ts +43 -14
  118. package/src/components/command-bar/workflow/tickers.ts +1 -0
  119. package/src/components/command-bar/workflow/types.ts +1 -1
  120. package/src/components/data-table/export.test.ts +21 -0
  121. package/src/components/data-table/export.ts +21 -0
  122. package/src/components/data-table/view.test.tsx +45 -6
  123. package/src/components/data-table/view.tsx +55 -9
  124. package/src/components/index.ts +10 -2
  125. package/src/components/input-search-bar.test.tsx +36 -2
  126. package/src/components/input-search-bar.tsx +18 -1
  127. package/src/components/layout/header.test.tsx +57 -10
  128. package/src/components/layout/header.tsx +292 -149
  129. package/src/components/layout/market-summary.test.ts +81 -0
  130. package/src/components/layout/market-summary.ts +109 -0
  131. package/src/components/layout/shell/chrome.ts +96 -1
  132. package/src/components/layout/shell/index.tsx +11 -3
  133. package/src/components/layout/shell/menu.ts +9 -0
  134. package/src/components/layout/shell/pane/actions.ts +29 -0
  135. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  136. package/src/components/layout/shell/shortcuts.ts +3 -0
  137. package/src/components/layout/status-bar.test.tsx +9 -8
  138. package/src/components/layout/status-bar.tsx +85 -54
  139. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  140. package/src/components/price-sparkline/index.ts +61 -34
  141. package/src/components/price-sparkline/view.tsx +1 -1
  142. package/src/components/sign-in-gate.test.tsx +128 -0
  143. package/src/components/sign-in-gate.tsx +149 -0
  144. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  145. package/src/components/ticker/badge/list.tsx +17 -3
  146. package/src/components/ticker/list-table-view.tsx +6 -3
  147. package/src/components/ui/data-table/index.tsx +22 -4
  148. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  149. package/src/components/ui/frame.tsx +28 -0
  150. package/src/components/ui/index.ts +3 -0
  151. package/src/components/ui/native-select.tsx +20 -10
  152. package/src/components/ui/select-button.tsx +138 -0
  153. package/src/components/ui/status.tsx +2 -2
  154. package/src/components/ui/table-layout.ts +1 -1
  155. package/src/components/use-double-click-activation.ts +7 -3
  156. package/src/core/app-services.ts +3 -3
  157. package/src/data/config/chart-settings.ts +4 -4
  158. package/src/data/config/layout.ts +12 -6
  159. package/src/data/config/store/index.test.ts +97 -0
  160. package/src/data/config/store/migrations.ts +41 -0
  161. package/src/data/config/store/normalize.ts +5 -3
  162. package/src/i18n/es.ts +6 -2
  163. package/src/i18n/ja.ts +6 -2
  164. package/src/i18n/ko.ts +6 -2
  165. package/src/i18n/zh-cn.ts +6 -2
  166. package/src/i18n/zh-tw.ts +6 -2
  167. package/src/layout-marketplace/gallery-terminal.tsx +126 -96
  168. package/src/layout-marketplace/gallery.test.tsx +65 -1
  169. package/src/layout-marketplace/pane-imagery.ts +1 -0
  170. package/src/layout-marketplace/payload.test.ts +17 -0
  171. package/src/layout-marketplace/payload.ts +40 -5
  172. package/src/market-data/coordinator/index.ts +3 -2
  173. package/src/market-data/coordinator/quotes.ts +173 -66
  174. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  175. package/src/pane-settings.ts +1 -1
  176. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  177. package/src/plugins/builtin/analytics/view.tsx +1 -1
  178. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  179. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  180. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  181. package/src/plugins/builtin/chart-composer/cli-options.ts +12 -9
  182. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  183. package/src/plugins/builtin/chart-composer/settings.ts +2 -13
  184. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  185. package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
  186. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  188. package/src/plugins/builtin/composite-plugins.ts +16 -1
  189. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  190. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  191. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  192. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  193. package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
  194. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  195. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  196. package/src/plugins/builtin/earnings/index.tsx +1 -0
  197. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  198. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  199. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  200. package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
  201. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  202. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  203. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
  204. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  205. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  206. package/src/plugins/builtin/econ/index.tsx +3 -1
  207. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  208. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  209. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  210. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  211. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  212. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  213. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  214. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  215. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  216. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  217. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  218. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  219. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  220. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  221. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  222. package/src/plugins/builtin/futures/index.tsx +1 -0
  223. package/src/plugins/builtin/fx-matrix/index.tsx +1 -0
  224. package/src/plugins/builtin/help/index.tsx +4 -0
  225. package/src/plugins/builtin/holders/index.tsx +1 -0
  226. package/src/plugins/builtin/insider/index.tsx +1 -0
  227. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -0
  228. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  229. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  230. package/src/plugins/builtin/market-halts/index.tsx +1 -0
  231. package/src/plugins/builtin/market-movers/index.tsx +1 -0
  232. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  233. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  234. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  235. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  236. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  237. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  238. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  239. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  240. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  241. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  242. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  243. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  244. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  245. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  246. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  247. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  248. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  249. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  250. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  251. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  252. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  253. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  254. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  255. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  256. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  257. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  258. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  259. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  260. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  261. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  262. package/src/plugins/builtin/options/analytics.test.ts +83 -0
  263. package/src/plugins/builtin/options/analytics.ts +110 -0
  264. package/src/plugins/builtin/options/index.tsx +18 -2
  265. package/src/plugins/builtin/options/table.ts +121 -51
  266. package/src/plugins/builtin/options/types.ts +22 -15
  267. package/src/plugins/builtin/options/view.test.tsx +53 -1
  268. package/src/plugins/builtin/options/view.tsx +92 -41
  269. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  270. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  271. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  272. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  273. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  274. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  275. package/src/plugins/builtin/plugin-module.test.ts +2 -4
  276. package/src/plugins/builtin/plugin-module.ts +9 -8
  277. package/src/plugins/builtin/portfolio-list/index.tsx +1 -0
  278. package/src/plugins/builtin/research/index.tsx +4 -0
  279. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  280. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  281. package/src/plugins/builtin/research-search/data.ts +109 -0
  282. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  283. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  284. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  285. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  286. package/src/plugins/builtin/research-search/index.tsx +85 -0
  287. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  288. package/src/plugins/builtin/research-search/model.ts +291 -0
  289. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  290. package/src/plugins/builtin/research-search/pane.tsx +678 -0
  291. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  292. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  293. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  294. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  295. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  296. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  297. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  298. package/src/plugins/builtin/scanner/index.ts +2 -0
  299. package/src/plugins/builtin/sec/index.tsx +1 -0
  300. package/src/plugins/builtin/sectors/index.tsx +1 -0
  301. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  302. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  303. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  304. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  305. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +3 -5
  306. package/src/plugins/builtin/ticker-detail/index.tsx +2 -14
  307. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  308. package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
  309. package/src/plugins/builtin/volatility/index.tsx +1 -1
  310. package/src/plugins/builtin/world-indices/index.tsx +1 -0
  311. package/src/plugins/builtin/world-venue-map/index.ts +27 -0
  312. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  313. package/src/plugins/builtin/world-venue-map/map.tsx +481 -0
  314. package/src/plugins/builtin/world-venue-map/model.test.ts +85 -0
  315. package/src/plugins/builtin/world-venue-map/model.ts +268 -0
  316. package/src/plugins/builtin/world-venue-map/pane.tsx +300 -0
  317. package/src/plugins/builtin/world-venue-map/world-outlines.ts +2 -0
  318. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  319. package/src/plugins/bundle.test.ts +109 -0
  320. package/src/plugins/bundle.ts +139 -0
  321. package/src/plugins/catalog-backend.ts +6 -44
  322. package/src/plugins/catalog-browser.test.ts +1 -0
  323. package/src/plugins/catalog-browser.ts +12 -0
  324. package/src/plugins/catalog-manifest.test.ts +43 -0
  325. package/src/plugins/catalog-manifest.ts +92 -0
  326. package/src/plugins/catalog-ui.ts +20 -4
  327. package/src/plugins/catalog.ts +9 -2
  328. package/src/plugins/current-target.ts +28 -0
  329. package/src/plugins/host-contract.ts +37 -0
  330. package/src/plugins/host-link.ts +158 -0
  331. package/src/plugins/host-modules.ts +61 -0
  332. package/src/plugins/loader.ts +41 -22
  333. package/src/plugins/ownership.ts +1 -0
  334. package/src/plugins/prediction-markets/index.tsx +1 -0
  335. package/src/plugins/registry/context.ts +3 -0
  336. package/src/plugins/registry/contributions.ts +27 -0
  337. package/src/plugins/registry/index.test.ts +25 -0
  338. package/src/plugins/registry/index.ts +8 -0
  339. package/src/plugins/registry/pane-settings.test.ts +53 -0
  340. package/src/plugins/registry/pane-settings.ts +29 -3
  341. package/src/plugins/seed.test.ts +83 -0
  342. package/src/plugins/seed.ts +80 -0
  343. package/src/public/broker.ts +31 -0
  344. package/src/public/market-data.ts +9 -0
  345. package/src/public/pane-hooks.ts +57 -0
  346. package/src/public/public-api.test.ts +285 -0
  347. package/src/public/react.ts +62 -0
  348. package/src/public/test-support.ts +23 -0
  349. package/src/public/time-series.ts +8 -0
  350. package/src/public/utils.ts +77 -0
  351. package/src/remote/layout-helpers.ts +6 -8
  352. package/src/remote/resources.ts +2 -0
  353. package/src/remote/schema.ts +1 -0
  354. package/src/remote/semantic-tree.test.tsx +105 -0
  355. package/src/remote/semantic-tree.tsx +53 -35
  356. package/src/renderers/browser/main.tsx +6 -0
  357. package/src/renderers/browser/ui-host.tsx +11 -0
  358. package/src/renderers/cloudflare/worker.test.ts +19 -0
  359. package/src/renderers/cloudflare/worker.ts +1 -1
  360. package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -2
  361. package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
  362. package/src/renderers/electrobun/bun/index.ts +6 -2
  363. package/src/renderers/electrobun/shared/protocol.ts +25 -0
  364. package/src/renderers/electrobun/view/build-assets.ts +0 -1
  365. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
  366. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  367. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  368. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  369. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  370. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  371. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  372. package/src/renderers/electrobun/view/main.tsx +27 -2
  373. package/src/renderers/electrobun/view/styles.css +5 -0
  374. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  375. package/src/renderers/electrobun/view/ui-host.tsx +3 -0
  376. package/src/renderers/opentui/chart-surface.tsx +5 -1
  377. package/src/renderers/opentui/host.tsx +20 -2
  378. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  379. package/src/renderers/opentui/interaction-performance.ts +188 -0
  380. package/src/renderers/opentui/start.tsx +26 -1
  381. package/src/renderers/opentui/test-utils.tsx +13 -0
  382. package/src/renderers/opentui/toast-host.tsx +26 -2
  383. package/src/renderers/opentui/ui-host.tsx +1 -0
  384. package/src/sources/provider-router/search.test.ts +191 -0
  385. package/src/sources/provider-router/search.ts +92 -39
  386. package/src/state/app/context/index.tsx +2 -0
  387. package/src/state/app/context/selector.test.tsx +33 -1
  388. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  389. package/src/state/hooks/quote-streaming.ts +106 -30
  390. package/src/state/pane-table-export-registry.ts +50 -0
  391. package/src/theme/color-utils.ts +1 -1
  392. package/src/theme/colors.ts +50 -0
  393. package/src/theme/themes.test.ts +15 -2
  394. package/src/theme/themes.ts +19 -1
  395. package/src/tickers/search/index.test.ts +26 -0
  396. package/src/tickers/search/index.ts +42 -17
  397. package/src/tickers/search/ranking.ts +14 -0
  398. package/src/time-series/chart-data.test.ts +216 -0
  399. package/src/time-series/range.ts +12 -0
  400. package/src/time-series/resolution.test.ts +25 -26
  401. package/src/time-series/resolution.ts +44 -25
  402. package/src/time-series/resolve.ts +7 -0
  403. package/src/time-series/spec.ts +5 -5
  404. package/src/time-series/types.ts +3 -1
  405. package/src/time-series/use-chart-resolution.ts +5 -0
  406. package/src/types/config.ts +123 -3
  407. package/src/types/data-provider.ts +11 -0
  408. package/src/types/headless.ts +161 -0
  409. package/src/types/plugin.ts +104 -20
  410. package/src/ui/host.tsx +19 -1
  411. package/src/ui/index.tsx +1 -0
  412. package/src/ui/toast.tsx +8 -4
  413. package/src/utils/csv.test.ts +13 -0
  414. package/src/utils/csv.ts +37 -0
  415. package/src/utils/format.ts +35 -0
  416. package/src/utils/fuzzy-search.test.ts +94 -0
  417. package/src/utils/fuzzy-search.ts +81 -22
  418. package/src/utils/save-text-file.test.ts +24 -0
  419. package/src/utils/save-text-file.ts +36 -0
  420. package/src/utils/shortcut-labels.ts +14 -0
  421. package/src/components/chart/core/axis-format.ts +0 -157
  422. package/src/components/chart/core/index.test.ts +0 -1075
  423. package/src/components/chart/core/projection.ts +0 -357
  424. package/src/components/chart/core/render-utils.ts +0 -4
  425. package/src/components/chart/core/renderer.ts +0 -244
  426. package/src/components/chart/core/scene.ts +0 -180
  427. package/src/components/chart/core/time-axis.ts +0 -330
  428. package/src/components/chart/cursor-motion.ts +0 -47
  429. package/src/components/chart/indicators/types.ts +0 -26
  430. package/src/components/chart/market-session.ts +0 -176
  431. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  432. package/src/components/chart/multi-line/renderer.ts +0 -161
  433. package/src/components/chart/multi-line/scene.ts +0 -188
  434. package/src/components/chart/native/raster/layout.ts +0 -129
  435. package/src/components/chart/native/raster/price-chart.ts +0 -244
  436. package/src/components/chart/rendering/chart-draw.ts +0 -270
  437. package/src/components/chart/rendering/geometry.ts +0 -95
  438. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  439. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  440. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  441. package/src/components/chart/static/chart/surface.tsx +0 -358
  442. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  443. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  444. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  445. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  446. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  447. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  448. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  449. package/src/plugins/builtin/substack/api/store.ts +0 -186
  450. package/src/plugins/builtin/substack/api/types.ts +0 -42
  451. package/src/plugins/builtin/substack/api.test.ts +0 -298
  452. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  453. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  454. package/src/plugins/builtin/substack/content.ts +0 -478
  455. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  456. package/src/plugins/builtin/substack/index.tsx +0 -49
  457. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  458. package/src/plugins/builtin/substack/model.test.ts +0 -155
  459. package/src/plugins/builtin/substack/normalize.ts +0 -343
  460. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  461. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  462. package/src/plugins/builtin/substack/pane.tsx +0 -547
  463. package/src/plugins/builtin/substack/read-state.ts +0 -30
  464. package/src/plugins/builtin/substack/table.ts +0 -111
  465. package/src/plugins/builtin/substack/types.ts +0 -71
  466. package/src/plugins/builtin/substack/utils.ts +0 -131
  467. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  468. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  469. package/src/plugins/ibkr/account-cache.ts +0 -24
  470. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  471. package/src/plugins/ibkr/config.test.ts +0 -79
  472. package/src/plugins/ibkr/config.ts +0 -250
  473. package/src/plugins/ibkr/dialogs.tsx +0 -40
  474. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  475. package/src/plugins/ibkr/flex/index.ts +0 -367
  476. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  477. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  478. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  479. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  480. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  481. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  482. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  483. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  484. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  485. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  486. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  487. package/src/plugins/ibkr/gateway/history.ts +0 -120
  488. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  489. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  490. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  491. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  492. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  493. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  494. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  495. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  496. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  497. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  498. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  499. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  500. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  501. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  502. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  503. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  504. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  505. package/src/plugins/ibkr/gateway/types.ts +0 -27
  506. package/src/plugins/ibkr/index.test.tsx +0 -162
  507. package/src/plugins/ibkr/index.tsx +0 -159
  508. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  509. package/src/plugins/ibkr/instance-selection.ts +0 -36
  510. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  511. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  512. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  513. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  514. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  515. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  516. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  517. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  518. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  519. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  520. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  521. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  522. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  523. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  524. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  525. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  526. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  527. package/src/plugins/ibkr/trade/utils.ts +0 -144
  528. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  529. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  530. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  531. package/src/plugins/ibkr/trading/state.ts +0 -335
@@ -32,9 +32,14 @@ import { useShowChartTextFallback } from "../native/use-chart-text-fallback";
32
32
  import {
33
33
  useStaticChartBitmapSize,
34
34
  type StaticChartBitmapSize,
35
- } from "../static/chart/bitmap";
36
- import { StaticXAxisLabels } from "../static/chart/axis-overlays";
37
- import { PriceAxisLabels } from "../price-axis-labels";
35
+ } from "./bitmap";
36
+ import {
37
+ StaticXAxisLabels,
38
+ StaticXMarkerLabels,
39
+ StaticXMarkerOverlay,
40
+ type StaticChartXMarker,
41
+ } from "./axis-overlays";
42
+ import { PriceAxisLabels } from "./price-axis-labels";
38
43
  import {
39
44
  compositeAxisTicks,
40
45
  formatCompositeAxisValue,
@@ -43,19 +48,24 @@ import {
43
48
  formatCompositePointDetails,
44
49
  formatCompositeSeriesValue,
45
50
  formatCompositeTimeAxisDate,
51
+ type CompositeAxisValueFormatter,
46
52
  } from "./format";
47
53
  import {
48
54
  COMPOSITE_KEYBOARD_PAN_RATIO,
49
55
  COMPOSITE_ZOOM_STEP_FACTOR,
56
+ buildCompositeNavigationFrame,
50
57
  clampCompositeViewport,
58
+ compositeNavigationDataViewport,
59
+ compositeViewportPositions,
60
+ fitCompositeViewport,
51
61
  panCompositeViewport,
52
62
  resolveCompositeChartInteraction,
53
- resolveCompositeMinimumSpanMs,
54
- resolveCompositeNavigationBounds,
55
- resolveCompositeWheelPanRatio,
63
+ resolveCompositeWheelPan,
64
+ resolveCompositeWheelZoom,
56
65
  sameCompositeViewport,
57
66
  shouldResetCompositeViewport,
58
67
  zoomCompositeViewport,
68
+ type CompositeNavigationFrame,
59
69
  type CompositeViewportRange,
60
70
  } from "./interactions";
61
71
  import { buildCompositeColumnLayout, type CompositeColumnLayout } from "./column-layout";
@@ -112,6 +122,8 @@ import type {
112
122
  // live-data paints or depending on a foreground animation frame.
113
123
  const DESKTOP_BITMAP_RESIZE_DEBOUNCE_MS = 32;
114
124
  const LEGEND_WHEEL_DELTA_PER_CELL = 8;
125
+ /** How far past the first loaded observation a backfilling chart may pan, as a fraction of the view. */
126
+ const HISTORICAL_PADDING_RATIO = 0.5;
115
127
 
116
128
  function isVerticalWheelDirection(
117
129
  direction: "up" | "down" | "left" | "right",
@@ -119,6 +131,44 @@ function isVerticalWheelDirection(
119
131
  return direction === "up" || direction === "down";
120
132
  }
121
133
 
134
+ interface PanGesture {
135
+ kind: "pan";
136
+ startGlobalX: number;
137
+ frame: CompositeNavigationFrame;
138
+ startViewport: CompositeViewportRange;
139
+ positionsPerCell: number;
140
+ }
141
+
142
+ /** A drag pans in the frame it started in; see `CompositeNavigationFrame`. */
143
+ function startPanGesture(
144
+ frame: CompositeNavigationFrame,
145
+ viewport: CompositeViewportRange,
146
+ plotWidth: number,
147
+ globalX: number,
148
+ ): PanGesture {
149
+ const positions = compositeViewportPositions(frame, viewport);
150
+ const span = positions ? Math.max(positions.end - positions.start, Number.EPSILON) : 1;
151
+ return {
152
+ kind: "pan",
153
+ startGlobalX: globalX,
154
+ frame,
155
+ startViewport: viewport,
156
+ positionsPerCell: span / Math.max(plotWidth, 1),
157
+ };
158
+ }
159
+
160
+ interface PendingWheel {
161
+ /** Fraction of the visible span, positive toward older observations. */
162
+ panRatio: number;
163
+ zoomLog: number;
164
+ anchorRatio: number;
165
+ }
166
+
167
+ const webFrame = globalThis as typeof globalThis & {
168
+ requestAnimationFrame?: (callback: () => void) => number;
169
+ cancelAnimationFrame?: (handle: number) => void;
170
+ };
171
+
122
172
  function renderPanelBitmap(
123
173
  panel: CompositePanelScene,
124
174
  bitmapSize: StaticChartBitmapSize,
@@ -255,7 +305,7 @@ function resolvePanelCrosshair(
255
305
  cursorYRatio: number | null,
256
306
  color: string,
257
307
  ): ChartSurfaceProps["crosshair"] {
258
- if (!bitmap || cursorXRatio === null || cursorYRatio === null) return null;
308
+ if (!bitmap || cursorXRatio === null) return null;
259
309
  const markers = panel.series.flatMap((series) => {
260
310
  // Column cohorts are drawn at their group center, not each observation's
261
311
  // own timestamp, so match the position the bar actually occupies.
@@ -274,7 +324,7 @@ function resolvePanelCrosshair(
274
324
  });
275
325
  return {
276
326
  pixelX: cursorXRatio * Math.max(bitmap.width - 1, 0),
277
- pixelY: cursorYRatio * Math.max(bitmap.height - 1, 0),
327
+ pixelY: cursorYRatio === null ? null : cursorYRatio * Math.max(bitmap.height - 1, 0),
278
328
  color,
279
329
  markers,
280
330
  };
@@ -291,11 +341,13 @@ function cursorAxisLabel(
291
341
  panel: CompositePanelScene,
292
342
  side: "left" | "right",
293
343
  cursorYRatio: number | null,
344
+ format?: CompositeAxisValueFormatter,
294
345
  ): string | null {
295
346
  const domain = panel.axes[side];
296
347
  if (!domain || cursorYRatio === null) return null;
297
348
  const value = unprojectCompositeValue(cursorYRatio, domain);
298
- return value === null ? null : formatCompositeCursorValue(value, domain);
349
+ if (value === null) return null;
350
+ return format ? format(value, domain) : formatCompositeCursorValue(value, domain);
299
351
  }
300
352
 
301
353
  const MINIMUM_AXIS_LABEL_WIDTH = 3;
@@ -304,10 +356,13 @@ const MINIMUM_AXIS_LABEL_WIDTH = 3;
304
356
  * Cells the gutter needs for its own labels. Ticks cover the axis itself, and a
305
357
  * mid-domain sample covers the wider cursor readout that lands between them.
306
358
  */
307
- function compositeAxisLabelWidth(domain: CompositeAxisDomain | undefined): number {
359
+ function compositeAxisLabelWidth(
360
+ domain: CompositeAxisDomain | undefined,
361
+ format: CompositeAxisValueFormatter = formatCompositeAxisValue,
362
+ ): number {
308
363
  if (!domain) return 0;
309
- const labels = compositeAxisTicks(domain).map((tick) => tick.label);
310
- labels.push(formatCompositeAxisValue((domain.min + domain.max) / 2, domain));
364
+ const labels = compositeAxisTicks(domain, 3, format).map((tick) => tick.label);
365
+ labels.push(format((domain.min + domain.max) / 2, domain));
311
366
  return labels.reduce((widest, label) => Math.max(widest, [...label].length), 0);
312
367
  }
313
368
 
@@ -604,8 +659,12 @@ interface CompositePanelSurfaceProps {
604
659
  axisGap: number;
605
660
  colors: CompositeChartColors;
606
661
  interactive: boolean;
662
+ /** False keeps the hover cursor but removes pan, zoom, and the tools. */
663
+ navigable: boolean;
664
+ formatAxisValue?: CompositeAxisValueFormatter;
665
+ remoteKind?: string;
607
666
  viewport: CompositeViewportRange;
608
- minimumViewportSpanMs: number;
667
+ frame: CompositeNavigationFrame;
609
668
  armedTool: ChartToolKind | null;
610
669
  drawings: readonly ChartDrawing[];
611
670
  selectedDrawingId: string | null;
@@ -615,7 +674,11 @@ interface CompositePanelSurfaceProps {
615
674
  onSelectDrawing: (id: string | null) => void;
616
675
  onActivate?: () => void;
617
676
  onCursorDateChange: (date: Date | null) => void;
618
- onPanViewport: (shiftRatio: number, fromViewport?: CompositeViewportRange) => void;
677
+ /** Positive positions move toward older observations. A gesture supplies the frame and origin it started from. */
678
+ onPanViewport: (
679
+ deltaPositions: number,
680
+ gesture?: { frame: CompositeNavigationFrame; from: CompositeViewportRange },
681
+ ) => void;
619
682
  onZoomViewport: (zoomFactor: number, anchorRatio: number) => void;
620
683
  onSetViewport: (range: CompositeViewportRange) => void;
621
684
  onToolSpanChange: (span: ChartToolSpan | null) => void;
@@ -631,8 +694,11 @@ function CompositePanelSurface({
631
694
  axisGap,
632
695
  colors,
633
696
  interactive,
697
+ navigable,
698
+ formatAxisValue,
699
+ remoteKind,
634
700
  viewport,
635
- minimumViewportSpanMs,
701
+ frame,
636
702
  armedTool,
637
703
  drawings,
638
704
  selectedDrawingId,
@@ -661,7 +727,7 @@ function CompositePanelSurface({
661
727
  ? null
662
728
  : cursorYRatio ?? seriesCursorYRatio;
663
729
  const dragRef = useRef<
664
- | { kind: "pan"; startGlobalX: number; startViewport: CompositeViewportRange }
730
+ | PanGesture
665
731
  | {
666
732
  kind: "edit";
667
733
  drawingId: string;
@@ -677,9 +743,12 @@ function CompositePanelSurface({
677
743
  const bitmapSize = useStaticChartBitmapSize(plotWidth, panel.height);
678
744
  const bitmap = useCompositePanelBitmap({ panel, bitmapSize, colors, isDesktopWeb });
679
745
  const columnLayout = useMemo(() => buildCompositeColumnLayout(panel), [panel]);
746
+ // The level line follows the pointer only. A keyboard or shared cursor knows
747
+ // its column, and the series markers and axis readout already say the value.
748
+ const pointerCursorYRatio = scene.cursorXRatio === null ? null : cursorYRatio;
680
749
  const crosshair = useMemo(
681
- () => resolvePanelCrosshair(panel, columnLayout, bitmap, scene.cursorXRatio, activeCursorYRatio, colors.crosshair),
682
- [activeCursorYRatio, bitmap, colors.crosshair, columnLayout, panel, scene.cursorXRatio],
750
+ () => resolvePanelCrosshair(panel, columnLayout, bitmap, scene.cursorXRatio, pointerCursorYRatio, colors.crosshair),
751
+ [bitmap, colors.crosshair, columnLayout, panel, pointerCursorYRatio, scene.cursorXRatio],
683
752
  );
684
753
  const measureDomain = useMemo(() => resolveMeasureAxisDomain(panel), [panel]);
685
754
  const toolReadout = useMemo(() => {
@@ -799,20 +868,20 @@ function CompositePanelSurface({
799
868
  const textLines = useMemo(
800
869
  () => isDesktopWeb || !showTextFallback
801
870
  ? []
802
- : renderCompositePanelText(panel, plotWidth, scene.cursorXRatio, activeCursorYRatio),
803
- [activeCursorYRatio, isDesktopWeb, panel, plotWidth, scene.cursorXRatio, showTextFallback],
871
+ : renderCompositePanelText(panel, plotWidth, scene.cursorXRatio, pointerCursorYRatio),
872
+ [isDesktopWeb, panel, plotWidth, pointerCursorYRatio, scene.cursorXRatio, showTextFallback],
804
873
  );
805
874
  const leftAxisLabels = useMemo(
806
875
  () => axisLabelRows(
807
- renderCompositeAxisText(panel.axes.left, panel.height, leftAxisWidth, "left"),
876
+ renderCompositeAxisText(panel.axes.left, panel.height, leftAxisWidth, "left", formatAxisValue),
808
877
  ),
809
- [leftAxisWidth, panel],
878
+ [formatAxisValue, leftAxisWidth, panel],
810
879
  );
811
880
  const rightAxisLabels = useMemo(
812
881
  () => axisLabelRows(
813
- renderCompositeAxisText(panel.axes.right, panel.height, rightAxisWidth, "right"),
882
+ renderCompositeAxisText(panel.axes.right, panel.height, rightAxisWidth, "right", formatAxisValue),
814
883
  ),
815
- [panel, rightAxisWidth],
884
+ [formatAxisValue, panel, rightAxisWidth],
816
885
  );
817
886
  const cursorRow = activeCursorYRatio === null
818
887
  ? null
@@ -820,8 +889,8 @@ function CompositePanelSurface({
820
889
  const cursorPixelY = activeCursorYRatio === null
821
890
  ? null
822
891
  : activeCursorYRatio * Math.max(panel.height * cellHeightPx - 1, 0);
823
- const leftCursorLabel = cursorAxisLabel(panel, "left", activeCursorYRatio);
824
- const rightCursorLabel = cursorAxisLabel(panel, "right", activeCursorYRatio);
892
+ const leftCursorLabel = cursorAxisLabel(panel, "left", activeCursorYRatio, formatAxisValue);
893
+ const rightCursorLabel = cursorAxisLabel(panel, "right", activeCursorYRatio, formatAxisValue);
825
894
  const measureReadout = useMemo(() => {
826
895
  if (!toolDrag || !toolReadout?.summary) return null;
827
896
  const text = toolReadout.summary;
@@ -944,24 +1013,26 @@ function CompositePanelSurface({
944
1013
  return;
945
1014
  }
946
1015
  if (!updateCursor(event)) return;
947
- dragRef.current = {
948
- kind: "pan",
949
- startGlobalX: getGlobalMouseX(event, renderer),
950
- startViewport: viewport,
951
- };
1016
+ dragRef.current = startPanGesture(frame, viewport, plotWidth, getGlobalMouseX(event, renderer));
952
1017
  }, [
953
1018
  armedTool,
954
1019
  drawings,
1020
+ frame,
955
1021
  onActivate,
956
1022
  onSelectDrawing,
957
1023
  panel,
958
1024
  plotAspect,
1025
+ plotWidth,
959
1026
  pointerRatios,
960
1027
  renderer,
961
1028
  scene,
962
1029
  updateCursor,
963
1030
  viewport,
964
1031
  ]);
1032
+ const pressCursor = useCallback((event: ChartMouseEvent) => {
1033
+ onActivate?.();
1034
+ updateCursor(event);
1035
+ }, [onActivate, updateCursor]);
965
1036
  const dragViewport = useCallback((event: ChartMouseEvent) => {
966
1037
  const drag = dragRef.current;
967
1038
  if (!drag) return;
@@ -992,9 +1063,18 @@ function CompositePanelSurface({
992
1063
  setToolDrag({ ...drag, path: [...drag.path] });
993
1064
  return;
994
1065
  }
995
- const deltaCells = getGlobalMouseX(event, renderer) - drag.startGlobalX;
996
- onPanViewport(deltaCells / Math.max(plotWidth, 1), drag.startViewport);
1066
+ const globalX = getGlobalMouseX(event, renderer);
1067
+ if (drag.frame !== frame) {
1068
+ // Data refreshed mid-drag. The new frame maps positions differently, so
1069
+ // continue from where the plot is now instead of replaying the drag.
1070
+ Object.assign(drag, startPanGesture(frame, viewport, plotWidth, globalX));
1071
+ }
1072
+ onPanViewport(
1073
+ (globalX - drag.startGlobalX) * drag.positionsPerCell,
1074
+ { frame: drag.frame, from: drag.startViewport },
1075
+ );
997
1076
  }, [
1077
+ frame,
998
1078
  onEditDrawing,
999
1079
  onPanViewport,
1000
1080
  panel,
@@ -1003,6 +1083,7 @@ function CompositePanelSurface({
1003
1083
  renderer,
1004
1084
  scene,
1005
1085
  updateCursor,
1086
+ viewport,
1006
1087
  ]);
1007
1088
  const finishToolDrag = useCallback(() => {
1008
1089
  const drag = dragRef.current;
@@ -1016,9 +1097,9 @@ function CompositePanelSurface({
1016
1097
  return;
1017
1098
  }
1018
1099
  if (drag.kind !== "zoom") return;
1019
- const range = resolveZoomBoxRange(scene, drag, minimumViewportSpanMs);
1100
+ const range = resolveZoomBoxRange(scene, drag, frame.minimumSpanMs);
1020
1101
  if (range) onSetViewport(range);
1021
- }, [drawColor, minimumViewportSpanMs, onDraw, onSetViewport, panel, scene]);
1102
+ }, [drawColor, frame.minimumSpanMs, onDraw, onSetViewport, panel, scene]);
1022
1103
  const resetDrag = useCallback(() => {
1023
1104
  if (dragRef.current?.kind === "pan") {
1024
1105
  dragRef.current = null;
@@ -1032,27 +1113,58 @@ function CompositePanelSurface({
1032
1113
  finishToolDrag();
1033
1114
  updateCursor(event);
1034
1115
  }, [finishToolDrag, updateCursor]);
1116
+ // Trackpads fire wheel events faster than the plot can paint, so one frame
1117
+ // absorbs every event that arrived since the last one.
1118
+ const pendingWheelRef = useRef<PendingWheel | null>(null);
1119
+ const wheelFrameRef = useRef<number | null>(null);
1120
+ const latestWheelStateRef = useRef({ frame, viewport, onPanViewport, onZoomViewport });
1121
+ latestWheelStateRef.current = { frame, viewport, onPanViewport, onZoomViewport };
1122
+ const flushWheel = useCallback(() => {
1123
+ wheelFrameRef.current = null;
1124
+ const pending = pendingWheelRef.current;
1125
+ pendingWheelRef.current = null;
1126
+ if (!pending) return;
1127
+ const latest = latestWheelStateRef.current;
1128
+ if (pending.panRatio !== 0) {
1129
+ const positions = compositeViewportPositions(latest.frame, latest.viewport);
1130
+ const span = positions ? Math.max(positions.end - positions.start, Number.EPSILON) : 0;
1131
+ latest.onPanViewport(pending.panRatio * span);
1132
+ }
1133
+ if (pending.zoomLog !== 0) {
1134
+ latest.onZoomViewport(Math.exp(pending.zoomLog), pending.anchorRatio);
1135
+ }
1136
+ }, []);
1137
+ useEffect(() => () => {
1138
+ pendingWheelRef.current = null;
1139
+ if (wheelFrameRef.current !== null) {
1140
+ webFrame.cancelAnimationFrame?.(wheelFrameRef.current);
1141
+ wheelFrameRef.current = null;
1142
+ }
1143
+ }, []);
1035
1144
  const panFromWheel = useCallback((event: ChartMouseEvent) => {
1036
- const direction = event.scroll?.direction;
1037
- if (!direction) return;
1145
+ const scroll = event.scroll;
1146
+ if (!scroll) return;
1038
1147
  onActivate?.();
1039
1148
  consumeChartMouseEvent(event);
1040
1149
  const pointerTarget = plotRef.current as unknown as Parameters<typeof getLocalPlotPointer>[1];
1041
1150
  const pointer = getLocalPlotPointer(event, pointerTarget, renderer);
1042
- if (event.modifiers.ctrl && pointer && isVerticalWheelDirection(direction)) {
1043
- const zoomIn = direction === "up";
1044
- const magnitude = Math.min(Math.max(Math.abs(event.scroll?.delta ?? 1), 1), 8);
1045
- const pointerRatio = pointer.cellX / Math.max(plotWidth - 1, 1);
1046
- onZoomViewport(
1047
- zoomIn ? 1 + magnitude * 0.04 : 1 / (1 + magnitude * 0.04),
1048
- pointerRatio,
1049
- );
1050
- updateCursor(event);
1151
+ updateCursor(event);
1152
+ const pending = pendingWheelRef.current ?? { panRatio: 0, zoomLog: 0, anchorRatio: 0.5 };
1153
+ if (event.modifiers.ctrl && pointer && isVerticalWheelDirection(scroll.direction)) {
1154
+ pending.zoomLog += Math.log(resolveCompositeWheelZoom(scroll));
1155
+ pending.anchorRatio = pointer.cellX / Math.max(plotWidth - 1, 1);
1156
+ } else {
1157
+ pending.panRatio += resolveCompositeWheelPan(scroll, plotWidth * cellWidthPx);
1158
+ }
1159
+ pendingWheelRef.current = pending;
1160
+ if (!isDesktopWeb || typeof webFrame.requestAnimationFrame !== "function") {
1161
+ flushWheel();
1051
1162
  return;
1052
1163
  }
1053
- updateCursor(event);
1054
- onPanViewport(resolveCompositeWheelPanRatio(direction, event.scroll?.delta));
1055
- }, [onActivate, onPanViewport, onZoomViewport, plotWidth, renderer, updateCursor]);
1164
+ if (wheelFrameRef.current === null) {
1165
+ wheelFrameRef.current = webFrame.requestAnimationFrame(flushWheel);
1166
+ }
1167
+ }, [cellWidthPx, flushWheel, isDesktopWeb, onActivate, plotWidth, renderer, updateCursor]);
1056
1168
 
1057
1169
  return (
1058
1170
  <Box
@@ -1111,15 +1223,16 @@ function CompositePanelSurface({
1111
1223
  crosshair={crosshair}
1112
1224
  vectors={vectors}
1113
1225
  onMouseMove={interactive ? handleMouseMove : undefined}
1114
- onMouseDown={interactive ? startDrag : undefined}
1115
- onMouseDrag={interactive ? dragViewport : undefined}
1116
- onMouseUp={interactive ? resetDrag : undefined}
1117
- onMouseDragEnd={interactive ? resetDrag : undefined}
1118
- onMouseScroll={interactive ? panFromWheel : undefined}
1226
+ onMouseDown={interactive ? navigable ? startDrag : pressCursor : undefined}
1227
+ onMouseDrag={interactive && navigable ? dragViewport : undefined}
1228
+ onMouseUp={interactive && navigable ? resetDrag : undefined}
1229
+ onMouseDragEnd={interactive && navigable ? resetDrag : undefined}
1230
+ onMouseScroll={interactive && navigable ? panFromWheel : undefined}
1119
1231
  onMouseOut={interactive ? clearCursor : undefined}
1120
- cursor={interactive ? toolDrag ? "crosshair" : "grab" : undefined}
1232
+ cursor={interactive ? toolDrag || !navigable ? "crosshair" : "grab" : undefined}
1121
1233
  data-gloom-interactive={interactive ? "true" : undefined}
1122
1234
  data-gloom-role={COMPOSITE_PANEL_ROLE}
1235
+ data-gloom-remote-kind={remoteKind}
1123
1236
  data-gloom-label={panel.label ?? panel.id}
1124
1237
  >
1125
1238
  {textLines.map((line, index) => <Text key={index} fg={colors.text}>{line}</Text>)}
@@ -1375,6 +1488,7 @@ function CompositeLegend({
1375
1488
  }
1376
1489
 
1377
1490
  const NO_DRAWINGS: readonly ChartDrawing[] = [];
1491
+ const NO_X_MARKERS: readonly StaticChartXMarker[] = [];
1378
1492
  /** Coalesces a drag into one write instead of one per pointer move. */
1379
1493
  const DRAWING_PERSIST_DELAY_MS = 400;
1380
1494
 
@@ -1432,6 +1546,10 @@ export function CompositeChart({
1432
1546
  viewportResetKey,
1433
1547
  colors,
1434
1548
  interactive = true,
1549
+ navigable = true,
1550
+ formatAxisValue,
1551
+ xAxis,
1552
+ remoteKind,
1435
1553
  allowHistoricalBackfill = false,
1436
1554
  axisWidth = 9,
1437
1555
  showLegend = true,
@@ -1519,7 +1637,9 @@ export function CompositeChart({
1519
1637
  }, [visibleLegendSeries.length]);
1520
1638
  const previousAuthoredViewportRef = useRef<CompositeViewportRange | null>(viewport ?? null);
1521
1639
  const previousViewportResetKeyRef = useRef(viewportResetKey);
1522
- const [interactionViewport, setInteractionViewport] = useState<CompositeViewportRange | null>(null);
1640
+ // The user owns this once they navigate. Data refreshes never rewrite it;
1641
+ // only the authored viewport changing or an explicit reset clears it.
1642
+ const [userViewport, setUserViewport] = useState<CompositeViewportRange | null>(null);
1523
1643
  const hasViewportResetKey = viewportResetKey !== undefined
1524
1644
  || previousViewportResetKeyRef.current !== undefined;
1525
1645
  const authoredViewportChanged = hasViewportResetKey
@@ -1528,52 +1648,29 @@ export function CompositeChart({
1528
1648
  previousAuthoredViewportRef.current,
1529
1649
  viewport ?? null,
1530
1650
  );
1531
- const navigationAnchorViewport = authoredViewportChanged
1532
- ? viewport
1533
- : interactionViewport ?? viewport;
1534
- const navigationBounds = useMemo(
1535
- () => resolveCompositeNavigationBounds(
1536
- visibleSeries,
1537
- navigationAnchorViewport,
1538
- { historicalPaddingRatio: allowHistoricalBackfill ? 1 : 0 },
1539
- ),
1540
- [allowHistoricalBackfill, navigationAnchorViewport, visibleSeries],
1651
+ const navigationFrame = useMemo(
1652
+ () => buildCompositeNavigationFrame(visibleSeries, marketTimelineSeries, {
1653
+ historicalPaddingRatio: allowHistoricalBackfill ? HISTORICAL_PADDING_RATIO : 0,
1654
+ }),
1655
+ [allowHistoricalBackfill, marketTimelineSeries, visibleSeries],
1541
1656
  );
1542
- const initialViewport = useMemo(() => (
1543
- navigationBounds
1544
- ? viewport
1545
- ? clampCompositeViewport(viewport, navigationBounds)
1546
- : navigationBounds
1547
- : null
1548
- ), [navigationBounds, viewport]);
1549
- const viewportSeriesKey = visibleLegendSeries
1550
- .map((entry) => `${entry.id}:${entry.label}`)
1551
- .join("|");
1552
- const clampedInteractionViewport = interactionViewport && navigationBounds
1553
- ? clampCompositeViewport(interactionViewport, navigationBounds)
1554
- : interactionViewport;
1555
- const interactionViewportNeedsSync = !!interactionViewport
1556
- && !!clampedInteractionViewport
1557
- && !sameCompositeViewport(interactionViewport, clampedInteractionViewport);
1558
- const currentInteractionViewport = authoredViewportChanged
1559
- ? null
1560
- : clampedInteractionViewport;
1561
- const effectiveViewport = navigationBounds
1562
- ? currentInteractionViewport
1563
- ? clampCompositeViewport(currentInteractionViewport, navigationBounds)
1564
- : initialViewport
1565
- : null;
1566
- const interactionViewportStart = currentInteractionViewport?.start.getTime() ?? null;
1567
- const interactionViewportEnd = currentInteractionViewport?.end.getTime() ?? null;
1657
+ const authoredViewport = useMemo(() => {
1658
+ if (!navigationFrame) return viewport ?? null;
1659
+ return viewport
1660
+ ? clampCompositeViewport(navigationFrame, viewport)
1661
+ : compositeNavigationDataViewport(navigationFrame);
1662
+ }, [navigationFrame, viewport]);
1663
+ const activeUserViewport = authoredViewportChanged ? null : userViewport;
1664
+ const effectiveViewport = activeUserViewport ?? authoredViewport;
1665
+ const userViewportStart = activeUserViewport?.start.getTime() ?? null;
1666
+ const userViewportEnd = activeUserViewport?.end.getTime() ?? null;
1568
1667
  const lastReportedViewportRef = useRef<string | null>(null);
1569
- const viewportInteractionRef = useRef<"pan" | "reset" | "sync" | "zoom">("reset");
1668
+ const viewportInteractionRef = useRef<"pan" | "reset" | "zoom">("reset");
1570
1669
  useEffect(() => {
1571
1670
  if (!onViewportChange) return;
1572
- if (interactionViewportNeedsSync) return;
1573
- const interactionKey = interactionViewportStart === null || interactionViewportEnd === null
1671
+ const key = userViewportStart === null || userViewportEnd === null
1574
1672
  ? "none"
1575
- : `${interactionViewportStart}:${interactionViewportEnd}`;
1576
- const key = `${viewportSeriesKey}|${interactionKey}`;
1673
+ : `${userViewportStart}:${userViewportEnd}`;
1577
1674
  // The callback drives adaptive data loading. Seed it from the authored
1578
1675
  // viewport without echoing that controlled value back into the loader.
1579
1676
  if (lastReportedViewportRef.current === null) {
@@ -1583,99 +1680,62 @@ export function CompositeChart({
1583
1680
  if (lastReportedViewportRef.current === key) return;
1584
1681
  lastReportedViewportRef.current = key;
1585
1682
  onViewportChange(
1586
- interactionViewportStart === null || interactionViewportEnd === null
1683
+ userViewportStart === null || userViewportEnd === null
1587
1684
  ? null
1588
- : {
1589
- start: new Date(interactionViewportStart),
1590
- end: new Date(interactionViewportEnd),
1591
- },
1685
+ : { start: new Date(userViewportStart), end: new Date(userViewportEnd) },
1592
1686
  viewportInteractionRef.current,
1593
1687
  );
1594
- }, [
1595
- interactionViewportEnd,
1596
- interactionViewportNeedsSync,
1597
- interactionViewportStart,
1598
- onViewportChange,
1599
- viewportSeriesKey,
1600
- ]);
1601
- const minimumViewportSpanMs = useMemo(
1602
- () => navigationBounds ? resolveCompositeMinimumSpanMs(visibleSeries, navigationBounds) : 1,
1603
- [navigationBounds, visibleSeries],
1604
- );
1688
+ }, [onViewportChange, userViewportEnd, userViewportStart]);
1605
1689
 
1606
1690
  useEffect(() => {
1607
1691
  previousAuthoredViewportRef.current = viewport ?? null;
1608
1692
  previousViewportResetKeyRef.current = viewportResetKey;
1609
- if (
1610
- authoredViewportChanged
1611
- || (interactionViewport && !navigationBounds)
1612
- ) {
1693
+ if (authoredViewportChanged && userViewport) {
1613
1694
  viewportInteractionRef.current = "reset";
1614
- setInteractionViewport(null);
1615
- return;
1616
- }
1617
- if (interactionViewportNeedsSync && clampedInteractionViewport) {
1618
- viewportInteractionRef.current = "sync";
1619
- setInteractionViewport(clampedInteractionViewport);
1695
+ setUserViewport(null);
1620
1696
  }
1621
- }, [
1622
- authoredViewportChanged,
1623
- clampedInteractionViewport,
1624
- interactionViewport,
1625
- interactionViewportNeedsSync,
1626
- navigationBounds,
1627
- viewport,
1628
- viewportResetKey,
1629
- ]);
1697
+ }, [authoredViewportChanged, userViewport, viewport, viewportResetKey]);
1630
1698
 
1631
- const zoomViewport = useCallback((zoomFactor: number, anchorRatio = 1) => {
1632
- if (!navigationBounds || !initialViewport) return;
1633
- viewportInteractionRef.current = "zoom";
1634
- setInteractionViewport((current) => {
1635
- const base = current ?? initialViewport;
1636
- const next = zoomCompositeViewport(
1637
- base,
1638
- navigationBounds,
1639
- zoomFactor,
1640
- anchorRatio,
1641
- minimumViewportSpanMs,
1642
- marketTimelineSeries,
1643
- );
1644
- if (sameCompositeViewport(next, base)) return current;
1645
- return sameCompositeViewport(next, initialViewport) ? null : next;
1699
+ const navigate = useCallback((
1700
+ kind: "pan" | "zoom",
1701
+ compute: (base: CompositeViewportRange, frame: CompositeNavigationFrame) => CompositeViewportRange,
1702
+ gesture?: { frame: CompositeNavigationFrame; from: CompositeViewportRange },
1703
+ ) => {
1704
+ const frame = gesture?.frame ?? navigationFrame;
1705
+ if (!frame) return;
1706
+ viewportInteractionRef.current = kind;
1707
+ setUserViewport((current) => {
1708
+ const base = gesture?.from ?? current ?? authoredViewport;
1709
+ if (!base) return current;
1710
+ const next = compute(base, frame);
1711
+ // Once navigated, the window is the user's until they reset it, even if
1712
+ // a gesture happens to land back on the authored range: the owner may
1713
+ // echo a navigated range back as the authored one, and dropping to null
1714
+ // there would reload the original range under the pointer.
1715
+ return sameCompositeViewport(next, current ?? base) ? current : next;
1646
1716
  });
1647
- }, [initialViewport, marketTimelineSeries, minimumViewportSpanMs, navigationBounds]);
1717
+ }, [authoredViewport, navigationFrame]);
1648
1718
  const panViewport = useCallback((
1649
- shiftRatio: number,
1650
- fromViewport?: CompositeViewportRange,
1719
+ deltaPositions: number,
1720
+ gesture?: { frame: CompositeNavigationFrame; from: CompositeViewportRange },
1651
1721
  ) => {
1652
- if (!navigationBounds || !initialViewport) return;
1653
- viewportInteractionRef.current = "pan";
1654
- setInteractionViewport((current) => {
1655
- const base = fromViewport ?? current ?? initialViewport;
1656
- const next = panCompositeViewport(
1657
- base,
1658
- navigationBounds,
1659
- shiftRatio,
1660
- marketTimelineSeries,
1661
- allowHistoricalBackfill,
1662
- );
1663
- if (sameCompositeViewport(next, base)) {
1664
- if (!fromViewport) return current;
1665
- return sameCompositeViewport(base, initialViewport) ? null : base;
1666
- }
1667
- return sameCompositeViewport(next, initialViewport) ? null : next;
1668
- });
1669
- }, [allowHistoricalBackfill, initialViewport, marketTimelineSeries, navigationBounds]);
1722
+ navigate("pan", (base, frame) => panCompositeViewport(frame, base, deltaPositions), gesture);
1723
+ }, [navigate]);
1724
+ const panViewportByRatio = useCallback((shiftRatio: number) => {
1725
+ if (!navigationFrame || !effectiveViewport) return;
1726
+ const positions = compositeViewportPositions(navigationFrame, effectiveViewport);
1727
+ if (!positions) return;
1728
+ panViewport(shiftRatio * Math.max(positions.end - positions.start, Number.EPSILON));
1729
+ }, [effectiveViewport, navigationFrame, panViewport]);
1730
+ const zoomViewport = useCallback((zoomFactor: number, anchorRatio = 1) => {
1731
+ navigate("zoom", (base, frame) => zoomCompositeViewport(frame, base, zoomFactor, anchorRatio));
1732
+ }, [navigate]);
1670
1733
  const setViewportRange = useCallback((range: CompositeViewportRange) => {
1671
- if (!navigationBounds || !initialViewport) return;
1672
- viewportInteractionRef.current = "zoom";
1673
- const next = clampCompositeViewport(range, navigationBounds);
1674
- setInteractionViewport(sameCompositeViewport(next, initialViewport) ? null : next);
1675
- }, [initialViewport, navigationBounds]);
1734
+ navigate("zoom", (_base, frame) => fitCompositeViewport(frame, range));
1735
+ }, [navigate]);
1676
1736
  const resetViewport = useCallback(() => {
1677
1737
  viewportInteractionRef.current = "reset";
1678
- setInteractionViewport(null);
1738
+ setUserViewport(null);
1679
1739
  }, []);
1680
1740
  // Sticky while armed: the toolbar chip shows which tool owns the drag, and a
1681
1741
  // one-shot tool would blink off before the user could see it.
@@ -1687,13 +1747,15 @@ export function CompositeChart({
1687
1747
  ? 1
1688
1748
  : 0;
1689
1749
  const timeAxisRows = showTimeAxis ? 1 : 0;
1750
+ const xMarkers = xAxis?.markers ?? NO_X_MARKERS;
1751
+ const xMarkerRows = xMarkers.some((marker) => marker.label) ? 1 : 0;
1690
1752
  const panelCount = new Set(visibleSeries.map((entry) => entry.panelId)).size;
1691
1753
  const lastTickKey = visibleSeries.map((entry) => {
1692
1754
  const last = entry.points.at(-1);
1693
1755
  if (!last) return entry.id;
1694
1756
  return `${entry.id}:${last.date.getTime()}:${last.close ?? ""}:${last.value ?? ""}:${entry.latestChangePercent ?? ""}`;
1695
1757
  }).join("|");
1696
- const plotHeight = Math.max(panelCount, totalHeight - legendRows - timeAxisRows);
1758
+ const plotHeight = Math.max(panelCount, totalHeight - legendRows - timeAxisRows - xMarkerRows);
1697
1759
  const resolvedColors = useMemo<CompositeChartColors>(() => ({
1698
1760
  background: colors?.background ?? activeThemeColors.bg,
1699
1761
  grid: colors?.grid ?? activeThemeColors.border,
@@ -1730,11 +1792,11 @@ export function CompositeChart({
1730
1792
  Math.max(
1731
1793
  MINIMUM_AXIS_LABEL_WIDTH,
1732
1794
  ...(projectedScene?.panels ?? []).flatMap((panel) => [
1733
- compositeAxisLabelWidth(panel.axes.left),
1734
- compositeAxisLabelWidth(panel.axes.right),
1795
+ compositeAxisLabelWidth(panel.axes.left, formatAxisValue),
1796
+ compositeAxisLabelWidth(panel.axes.right, formatAxisValue),
1735
1797
  ]),
1736
1798
  ),
1737
- ), [maximumAxisWidth, projectedScene]);
1799
+ ), [formatAxisValue, maximumAxisWidth, projectedScene]);
1738
1800
  const leftAxisWidth = hasLeftAxis ? resolvedAxisWidth : 0;
1739
1801
  const rightAxisWidth = hasRightAxis ? resolvedAxisWidth : 0;
1740
1802
  const axisGap = resolvedAxisWidth > 0 ? 1 : 0;
@@ -1783,26 +1845,27 @@ export function CompositeChart({
1783
1845
  : null
1784
1846
  ), [baseScene, normalizedCursorTimestamp]);
1785
1847
  const handleEmptyMouseScroll = useCallback((event: ChartMouseEvent) => {
1786
- const direction = event.scroll?.direction;
1787
- if (!interactive || !navigationBounds || !direction) return;
1848
+ const scroll = event.scroll;
1849
+ if (!interactive || !navigationFrame || !scroll) return;
1788
1850
  onActivate?.();
1789
1851
  consumeChartMouseEvent(event);
1790
- if (event.modifiers.ctrl && isVerticalWheelDirection(direction)) {
1791
- const zoomIn = direction === "up";
1792
- if (!zoomIn) {
1852
+ if (event.modifiers.ctrl && isVerticalWheelDirection(scroll.direction)) {
1853
+ const factor = resolveCompositeWheelZoom(scroll);
1854
+ if (factor < 1) {
1793
1855
  resetViewport();
1794
1856
  return;
1795
1857
  }
1796
- const magnitude = Math.min(Math.max(Math.abs(event.scroll?.delta ?? 1), 1), 8);
1797
- zoomViewport(1 + magnitude * 0.04, 0.5);
1858
+ zoomViewport(factor, 0.5);
1798
1859
  return;
1799
1860
  }
1800
- panViewport(resolveCompositeWheelPanRatio(direction, event.scroll?.delta));
1861
+ panViewportByRatio(resolveCompositeWheelPan(scroll, plotWidth * cellWidthPx));
1801
1862
  }, [
1863
+ cellWidthPx,
1802
1864
  interactive,
1803
- navigationBounds,
1865
+ navigationFrame,
1804
1866
  onActivate,
1805
- panViewport,
1867
+ panViewportByRatio,
1868
+ plotWidth,
1806
1869
  resetViewport,
1807
1870
  zoomViewport,
1808
1871
  ]);
@@ -1825,7 +1888,7 @@ export function CompositeChart({
1825
1888
  }, [cursorDate, onCursorDateChange]);
1826
1889
 
1827
1890
  useShortcut((event) => {
1828
- if (!focused || !interactive) return;
1891
+ if (!focused || !interactive || !navigable) return;
1829
1892
  if (isPlainKey(event, "[", "]") && visibleLegendSeries.length > 0) {
1830
1893
  event.preventDefault();
1831
1894
  event.stopPropagation();
@@ -1874,7 +1937,7 @@ export function CompositeChart({
1874
1937
  || ((interaction === "zoom-in"
1875
1938
  || interaction === "zoom-out"
1876
1939
  || interaction === "pan-left"
1877
- || interaction === "pan-right") && !navigationBounds)
1940
+ || interaction === "pan-right") && !navigationFrame)
1878
1941
  ) {
1879
1942
  return;
1880
1943
  }
@@ -1910,10 +1973,10 @@ export function CompositeChart({
1910
1973
  return;
1911
1974
  }
1912
1975
  case "pan-left":
1913
- panViewport(COMPOSITE_KEYBOARD_PAN_RATIO);
1976
+ panViewportByRatio(COMPOSITE_KEYBOARD_PAN_RATIO);
1914
1977
  return;
1915
1978
  case "pan-right":
1916
- panViewport(-COMPOSITE_KEYBOARD_PAN_RATIO);
1979
+ panViewportByRatio(-COMPOSITE_KEYBOARD_PAN_RATIO);
1917
1980
  return;
1918
1981
  case "reset":
1919
1982
  resetViewport();
@@ -1924,7 +1987,7 @@ export function CompositeChart({
1924
1987
  case "zoom-out":
1925
1988
  zoomViewport(1 / COMPOSITE_ZOOM_STEP_FACTOR);
1926
1989
  }
1927
- }, { enabled: focused && interactive });
1990
+ }, { enabled: focused && interactive && navigable });
1928
1991
 
1929
1992
  const leftPadding = leftAxisWidth + (leftAxisWidth ? axisGap : 0);
1930
1993
  const rightPadding = rightAxisWidth + (rightAxisWidth ? axisGap : 0);
@@ -1936,7 +1999,7 @@ export function CompositeChart({
1936
1999
  : null;
1937
2000
 
1938
2001
  if (!scene) {
1939
- const emptyPlotHeight = Math.max(0, totalHeight - legendRows - timeAxisRows);
2002
+ const emptyPlotHeight = Math.max(0, totalHeight - legendRows - timeAxisRows - xMarkerRows);
1940
2003
  return (
1941
2004
  <Box
1942
2005
  flexDirection="column"
@@ -1969,9 +2032,9 @@ export function CompositeChart({
1969
2032
  height={emptyPlotHeight}
1970
2033
  alignItems="center"
1971
2034
  justifyContent="center"
1972
- onMouseScroll={interactive && navigationBounds ? handleEmptyMouseScroll : undefined}
1973
- cursor={interactive && navigationBounds ? "grab" : undefined}
1974
- data-gloom-interactive={interactive && navigationBounds ? "true" : undefined}
2035
+ onMouseScroll={interactive && navigable && navigationFrame ? handleEmptyMouseScroll : undefined}
2036
+ cursor={interactive && navigable && navigationFrame ? "grab" : undefined}
2037
+ data-gloom-interactive={interactive && navigable && navigationFrame ? "true" : undefined}
1975
2038
  data-gloom-role="composite-chart-empty"
1976
2039
  data-gloom-label={emptyMessage}
1977
2040
  >
@@ -2002,9 +2065,11 @@ export function CompositeChart({
2002
2065
  const timeAxisCursorPixelX = timeAxisCursorColumn === null
2003
2066
  ? null
2004
2067
  : timeAxisCursorColumn * cellWidthPx;
2005
- const timeAxisCursorLabel = scene.cursorDate
2006
- ? formatCompositeTimeAxisDate(scene.cursorDate, scene.startTime, scene.endTime)
2007
- : null;
2068
+ const timeAxisCursorLabel = xAxis?.formatCursor && scene.cursorXRatio !== null
2069
+ ? xAxis.formatCursor(scene.cursorXRatio)
2070
+ : scene.cursorDate
2071
+ ? formatCompositeTimeAxisDate(scene.cursorDate, scene.startTime, scene.endTime)
2072
+ : null;
2008
2073
  // The crosshair labels the moving end, so the axis only adds the anchor.
2009
2074
  const timeAxisMarkers = toolSpan
2010
2075
  ? [{
@@ -2043,7 +2108,7 @@ export function CompositeChart({
2043
2108
  keyboardIndex={legendKeyboardIndex}
2044
2109
  />
2045
2110
  ) : null}
2046
- {interactive && plotWidth > CHART_TOOLBAR_WIDTH + 4 ? (
2111
+ {interactive && navigable && plotWidth > CHART_TOOLBAR_WIDTH + 4 ? (
2047
2112
  <ChartToolbar
2048
2113
  armedTool={armedTool}
2049
2114
  isDesktopWeb={isDesktopWeb}
@@ -2079,8 +2144,11 @@ export function CompositeChart({
2079
2144
  axisGap={axisGap}
2080
2145
  colors={resolvedColors}
2081
2146
  interactive={interactive}
2147
+ navigable={navigable}
2148
+ formatAxisValue={formatAxisValue}
2149
+ remoteKind={remoteKind}
2082
2150
  viewport={effectiveViewport!}
2083
- minimumViewportSpanMs={minimumViewportSpanMs}
2151
+ frame={navigationFrame!}
2084
2152
  armedTool={armedTool}
2085
2153
  drawings={drawings}
2086
2154
  selectedDrawingId={selectedDrawingId}
@@ -2097,12 +2165,30 @@ export function CompositeChart({
2097
2165
  showTextFallback={showTextFallback}
2098
2166
  />
2099
2167
  ))}
2168
+ {xMarkers.length > 0 ? (
2169
+ <Box
2170
+ position="absolute"
2171
+ left={leftPadding}
2172
+ top={legendRows}
2173
+ width={plotWidth}
2174
+ height={plotHeight}
2175
+ zIndex={12}
2176
+ style={isDesktopWeb ? { pointerEvents: "none" } : undefined}
2177
+ >
2178
+ <StaticXMarkerOverlay
2179
+ markers={xMarkers}
2180
+ width={plotWidth}
2181
+ height={plotHeight}
2182
+ fallbackColor={resolvedColors.textDim}
2183
+ />
2184
+ </Box>
2185
+ ) : null}
2100
2186
  {timeAxisLayout ? (
2101
2187
  <Box flexDirection="row" width={totalWidth} height={1}>
2102
2188
  {leftPadding > 0 ? <Box width={leftPadding} /> : null}
2103
2189
  <StaticXAxisLabels
2104
- labels={[timeAxisLayout.text]}
2105
- positionedLabels={timeAxisLayout.ticks}
2190
+ labels={xAxis?.labels ? [...xAxis.labels] : [timeAxisLayout.text]}
2191
+ positionedLabels={xAxis?.labels ? undefined : timeAxisLayout.ticks}
2106
2192
  width={plotWidth}
2107
2193
  color={resolvedColors.textDim}
2108
2194
  cursorColumn={timeAxisCursorColumn}
@@ -2115,6 +2201,17 @@ export function CompositeChart({
2115
2201
  {rightPadding > 0 ? <Box width={rightPadding} /> : null}
2116
2202
  </Box>
2117
2203
  ) : null}
2204
+ {xMarkerRows > 0 ? (
2205
+ <Box flexDirection="row" width={totalWidth} height={1}>
2206
+ {leftPadding > 0 ? <Box width={leftPadding} /> : null}
2207
+ <StaticXMarkerLabels
2208
+ markers={xMarkers}
2209
+ width={plotWidth}
2210
+ fallbackColor={resolvedColors.textDim}
2211
+ />
2212
+ {rightPadding > 0 ? <Box width={rightPadding} /> : null}
2213
+ </Box>
2214
+ ) : null}
2118
2215
  </Box>
2119
2216
  );
2120
2217
  }