gloomberb 0.10.4 → 0.11.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 (555) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +30 -3
  3. package/README.zh-CN.md +1 -1
  4. package/package.json +5 -5
  5. package/src/api-client/auth.ts +34 -0
  6. package/src/api-client/data.ts +25 -0
  7. package/src/api-client/index.test.ts +81 -0
  8. package/src/api-client/index.ts +37 -0
  9. package/src/api-client/request.test.ts +30 -0
  10. package/src/api-client/request.ts +44 -20
  11. package/src/api-client/socket-health.test.ts +79 -0
  12. package/src/api-client/socket.ts +108 -5
  13. package/src/api-client/types.ts +139 -0
  14. package/src/app/global-shortcuts.test.tsx +144 -9
  15. package/src/app/global-shortcuts.ts +33 -11
  16. package/src/app/runtime/broker-import.ts +39 -5
  17. package/src/app/runtime/startup.ts +7 -1
  18. package/src/app/runtime/update.ts +26 -1
  19. package/src/app.tsx +36 -19
  20. package/src/brokers/broker-ticker-sync.ts +3 -8
  21. package/src/brokers/sync-broker-instance.test.ts +59 -0
  22. package/src/brokers/sync-broker-instance.ts +37 -13
  23. package/src/capabilities/chart-series.ts +318 -0
  24. package/src/capabilities/factories.ts +46 -0
  25. package/src/capabilities/index.ts +1 -0
  26. package/src/capabilities/registry.test.ts +135 -0
  27. package/src/capabilities/registry.ts +31 -4
  28. package/src/capabilities/types.ts +52 -0
  29. package/src/cli/commands/overview.ts +2 -2
  30. package/src/cli/desktop-pane-shot.ts +2 -0
  31. package/src/cli/entry.ts +11 -1
  32. package/src/cli/native-smoke.ts +5 -0
  33. package/src/cli/pane-functions/catalog.ts +4 -0
  34. package/src/cli/pane-functions/data.ts +6 -1
  35. package/src/cli/pane-functions/discovery.ts +76 -10
  36. package/src/cli/pane-functions/report.test.ts +76 -0
  37. package/src/cli/pane-functions/report.ts +7 -1
  38. package/src/cli/pane-functions/resolver.ts +4 -1
  39. package/src/cli/pane-functions/screenshot.test.ts +56 -3
  40. package/src/cli/pane-functions/screenshot.ts +68 -5
  41. package/src/components/chart/composite/composite-chart.test.tsx +54 -0
  42. package/src/components/chart/composite/composite-chart.tsx +92 -15
  43. package/src/components/chart/composite/downsample.test.ts +136 -0
  44. package/src/components/chart/composite/downsample.ts +208 -0
  45. package/src/components/chart/composite/panel-series.test.ts +66 -0
  46. package/src/components/chart/composite/panel-series.ts +101 -0
  47. package/src/components/chart/multi-line/renderer.ts +1 -0
  48. package/src/components/chart/native/kitty/index.test.ts +25 -3
  49. package/src/components/chart/native/kitty/support.ts +37 -3
  50. package/src/components/chart/native/use-chart-text-fallback.ts +19 -0
  51. package/src/components/chart/static/chart/surface.tsx +43 -12
  52. package/src/components/chart/static/multi-line-chart-surface.tsx +41 -5
  53. package/src/components/command-bar/commands/direct/index.ts +18 -0
  54. package/src/components/command-bar/commands/registry.ts +14 -0
  55. package/src/components/command-bar/routes/root/article-results.test.ts +83 -0
  56. package/src/components/command-bar/routes/root/article-results.ts +55 -0
  57. package/src/components/command-bar/routes/root/command-items.ts +3 -0
  58. package/src/components/command-bar/routes/root/provider-search.ts +7 -3
  59. package/src/components/command-bar/routes/root/results.ts +38 -11
  60. package/src/components/command-bar/routes/root/runtime.ts +6 -2
  61. package/src/components/command-bar/routes/ticker-search/actions.ts +1 -1
  62. package/src/components/command-bar/routes/ticker-search/results.test.ts +34 -0
  63. package/src/components/command-bar/routes/ticker-search/results.ts +4 -6
  64. package/src/components/command-bar/routes/ticker-search/route.ts +2 -2
  65. package/src/components/command-bar/routing/list-state.ts +2 -1
  66. package/src/components/command-bar/surface/assist.test.tsx +13 -6
  67. package/src/components/command-bar/surface/index.test.tsx +65 -0
  68. package/src/components/command-bar/surface/index.tsx +35 -0
  69. package/src/components/command-bar/view-model.test.ts +43 -0
  70. package/src/components/command-bar/view-model.ts +41 -4
  71. package/src/components/command-bar/workflow/collection-actions.test.ts +94 -0
  72. package/src/components/command-bar/workflow/collection-actions.ts +14 -3
  73. package/src/components/command-bar/workflow/ops.test.ts +136 -0
  74. package/src/components/command-bar/workflow/ops.ts +71 -1
  75. package/src/components/index.ts +1 -1
  76. package/src/components/layout/header.test.tsx +35 -0
  77. package/src/components/layout/header.tsx +101 -9
  78. package/src/components/layout/pane/cycle-order.test.ts +0 -1
  79. package/src/components/layout/pane/footer/index.test.tsx +19 -19
  80. package/src/components/layout/pane/header.tsx +2 -13
  81. package/src/components/layout/shell/index.tsx +3 -0
  82. package/src/components/layout/status-bar.test.tsx +36 -1
  83. package/src/components/layout/status-bar.tsx +48 -2
  84. package/src/components/onboarding/account-step/chooser-panel.tsx +27 -50
  85. package/src/components/onboarding/account-step/form-panel.tsx +85 -29
  86. package/src/components/onboarding/account-step/index.tsx +10 -1
  87. package/src/components/onboarding/account-step/model.ts +3 -3
  88. package/src/components/onboarding/account-step/qr-panel.tsx +68 -0
  89. package/src/components/onboarding/account-step/signed-in-panel.tsx +4 -7
  90. package/src/components/onboarding/onboarding-frame.tsx +517 -0
  91. package/src/components/onboarding/onboarding-steps.tsx +0 -264
  92. package/src/components/onboarding/onboarding-wizard.test.tsx +626 -200
  93. package/src/components/onboarding/onboarding-wizard.tsx +848 -266
  94. package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +81 -51
  95. package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +14 -20
  96. package/src/components/onboarding/portfolio-step/broker-sync-panel.tsx +8 -11
  97. package/src/components/onboarding/portfolio-step/choose-panel.tsx +18 -22
  98. package/src/components/onboarding/portfolio-step/index.tsx +3 -0
  99. package/src/components/onboarding/portfolio-step/types.ts +3 -0
  100. package/src/components/onboarding/wizard-account.ts +38 -3
  101. package/src/components/onboarding/wizard-broker-sync.ts +45 -67
  102. package/src/components/onboarding/wizard-model.ts +18 -2
  103. package/src/components/ui/data-table/opentui/index.tsx +29 -9
  104. package/src/components/ui/data-table/types.ts +2 -0
  105. package/src/components/ui/external-link.tsx +4 -2
  106. package/src/components/ui/loading.tsx +1 -1
  107. package/src/components/ui/status.tsx +76 -4
  108. package/src/components/ui/table-layout.test.ts +24 -0
  109. package/src/components/ui/table-layout.ts +90 -23
  110. package/src/components/ui/tabs.tsx +86 -4
  111. package/src/core/app-services.ts +14 -3
  112. package/src/core/connection-health.test.ts +143 -0
  113. package/src/core/connection-health.ts +295 -0
  114. package/src/core/state/app/layout-reducer.ts +34 -0
  115. package/src/core/state/app/layout.ts +21 -0
  116. package/src/core/state/app/state.ts +10 -0
  117. package/src/core/state/app/types.ts +7 -1
  118. package/src/data/config/chart-settings.ts +11 -2
  119. package/src/data/config/store/index.test.ts +83 -9
  120. package/src/data/config/store/migrations.ts +16 -0
  121. package/src/data/config/store/normalize.ts +59 -1
  122. package/src/data/fred-series.ts +8 -2
  123. package/src/i18n/es.ts +126 -0
  124. package/src/i18n/ja.ts +126 -0
  125. package/src/i18n/ko.ts +126 -0
  126. package/src/i18n/zh-cn.ts +126 -0
  127. package/src/i18n/zh-tw.ts +126 -0
  128. package/src/market-data/coordinator/chart.ts +3 -2
  129. package/src/market-data/hooks.tsx +3 -2
  130. package/src/market-data/market/status.test.ts +12 -0
  131. package/src/market-data/market/status.ts +34 -17
  132. package/src/news/aggregator.test.ts +39 -0
  133. package/src/news/aggregator.ts +78 -22
  134. package/src/news/hooks.ts +4 -0
  135. package/src/notifications/app-notifier.test.ts +28 -6
  136. package/src/notifications/app-notifier.ts +24 -5
  137. package/src/plugins/broker-sync/broker-sync.test.ts +130 -0
  138. package/src/plugins/broker-sync/native-loader.ts +10 -0
  139. package/src/plugins/broker-sync/normalize.ts +271 -0
  140. package/src/plugins/broker-sync/public.ts +122 -0
  141. package/src/plugins/broker-sync/robinhood-native.ts +367 -0
  142. package/src/plugins/broker-sync/robinhood.ts +114 -0
  143. package/src/plugins/broker-sync/simplefin-native.ts +201 -0
  144. package/src/plugins/broker-sync/simplefin.ts +70 -0
  145. package/src/plugins/builtin/account-management/footer.ts +8 -19
  146. package/src/plugins/builtin/account-management/pane.tsx +13 -8
  147. package/src/plugins/builtin/ai/pi/host.ts +6 -5
  148. package/src/plugins/builtin/ai/providers.ts +1 -1
  149. package/src/plugins/builtin/ai/screener/footer.ts +0 -7
  150. package/src/plugins/builtin/ai/screener/pane.test.tsx +2 -1
  151. package/src/plugins/builtin/ai/screener/pane.tsx +3 -1
  152. package/src/plugins/builtin/ai/screener/results-view.tsx +8 -0
  153. package/src/plugins/builtin/ai/screener/runner.ts +7 -3
  154. package/src/plugins/builtin/ai/workspace/pane.tsx +13 -2
  155. package/src/plugins/builtin/alerts/alert-engine.ts +13 -0
  156. package/src/plugins/builtin/alerts/constants.ts +2 -1
  157. package/src/plugins/builtin/alerts/index.tsx +67 -36
  158. package/src/plugins/builtin/alerts/pane.tsx +17 -65
  159. package/src/plugins/builtin/analytics/index.tsx +24 -11
  160. package/src/plugins/builtin/analytics/pane-model.ts +48 -14
  161. package/src/plugins/builtin/analytics/portfolio-selection.ts +19 -0
  162. package/src/plugins/builtin/analytics/view.tsx +5 -3
  163. package/src/plugins/builtin/broker-manager/detail.tsx +7 -2
  164. package/src/plugins/builtin/broker-manager/footer.ts +4 -9
  165. package/src/plugins/builtin/broker-manager/index.tsx +1 -6
  166. package/src/plugins/builtin/buildout/detail/ui.tsx +15 -4
  167. package/src/plugins/builtin/buildout/pane/header.tsx +1 -0
  168. package/src/plugins/builtin/changelog/index.tsx +60 -45
  169. package/src/plugins/builtin/chart-composer/catalog-inventory.test.ts +97 -0
  170. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +366 -0
  171. package/src/plugins/builtin/chart-composer/chart-spec.ts +21 -1
  172. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +307 -0
  173. package/src/plugins/builtin/chart-composer/editor/controller.ts +1 -1
  174. package/src/plugins/builtin/chart-composer/index.tsx +45 -5
  175. package/src/plugins/builtin/chart-composer/pane.tsx +7 -88
  176. package/src/plugins/builtin/chart-composer/presets.test.ts +48 -0
  177. package/src/plugins/builtin/chart-composer/presets.ts +69 -3
  178. package/src/plugins/builtin/chart-composer/quick-add.tsx +8 -5
  179. package/src/plugins/builtin/chart-composer/semantic.ts +18 -14
  180. package/src/plugins/builtin/chart-composer/series-catalog.test.ts +39 -0
  181. package/src/plugins/builtin/chart-composer/series-catalog.ts +104 -3
  182. package/src/plugins/builtin/chart-composer/shortcuts.test.ts +10 -10
  183. package/src/plugins/builtin/chart-composer/shortcuts.ts +4 -12
  184. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +230 -23
  185. package/src/plugins/builtin/chat/chat.test.tsx +22 -0
  186. package/src/plugins/builtin/chat/content/composer.tsx +6 -4
  187. package/src/plugins/builtin/chat/content/index.tsx +7 -0
  188. package/src/plugins/builtin/chat/content/snapshot.ts +9 -2
  189. package/src/plugins/builtin/chat/content/transcript.tsx +10 -0
  190. package/src/plugins/builtin/chat/controller/channel-actions.ts +11 -1
  191. package/src/plugins/builtin/chat/controller/channels.ts +13 -3
  192. package/src/plugins/builtin/chat/controller/index.ts +61 -7
  193. package/src/plugins/builtin/chat/controller/lifecycle.ts +3 -0
  194. package/src/plugins/builtin/chat/controller/message-loading.ts +5 -1
  195. package/src/plugins/builtin/chat/controller/messages.ts +3 -1
  196. package/src/plugins/builtin/chat/controller/notifications.ts +32 -11
  197. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  198. package/src/plugins/builtin/chat/controller/view.ts +1 -0
  199. package/src/plugins/builtin/chat/controller.test.ts +117 -28
  200. package/src/plugins/builtin/cloud/auth-commands.ts +18 -0
  201. package/src/plugins/builtin/cloud/device-signin-dialog.tsx +243 -0
  202. package/src/plugins/builtin/cloud/device-signin.test.ts +217 -0
  203. package/src/plugins/builtin/cloud/device-signin.ts +240 -0
  204. package/src/plugins/builtin/cloud/plugin.tsx +7 -1
  205. package/src/plugins/builtin/cloud-tweets/table.tsx +57 -10
  206. package/src/plugins/builtin/composite-plugins.ts +39 -5
  207. package/src/plugins/builtin/congress-trades/detail.tsx +2 -4
  208. package/src/plugins/builtin/congress-trades/footer.ts +7 -16
  209. package/src/plugins/builtin/congress-trades/pane.tsx +12 -20
  210. package/src/plugins/builtin/connections/index.ts +53 -0
  211. package/src/plugins/builtin/connections/pane.test.tsx +79 -0
  212. package/src/plugins/builtin/connections/pane.tsx +258 -0
  213. package/src/plugins/builtin/correlation/index.test.tsx +2 -1
  214. package/src/plugins/builtin/correlation/index.tsx +58 -19
  215. package/src/plugins/builtin/correlation/matrix/model.ts +18 -7
  216. package/src/plugins/builtin/correlation/relationship/pane.tsx +43 -19
  217. package/src/plugins/builtin/correlation/settings.ts +1 -1
  218. package/src/plugins/builtin/credit-conditions/client.test.ts +80 -0
  219. package/src/plugins/builtin/credit-conditions/client.ts +105 -0
  220. package/src/plugins/builtin/credit-conditions/index.test.tsx +111 -0
  221. package/src/plugins/builtin/credit-conditions/index.tsx +227 -0
  222. package/src/plugins/builtin/credit-conditions/model.test.ts +43 -0
  223. package/src/plugins/builtin/credit-conditions/model.ts +64 -0
  224. package/src/plugins/builtin/debug/index.tsx +11 -6
  225. package/src/plugins/builtin/dividend-yield/client.test.ts +58 -0
  226. package/src/plugins/builtin/dividend-yield/client.ts +252 -0
  227. package/src/plugins/builtin/dividend-yield/index.tsx +61 -0
  228. package/src/plugins/builtin/dividend-yield/model.ts +83 -0
  229. package/src/plugins/builtin/dividend-yield/pane.tsx +305 -0
  230. package/src/plugins/builtin/dividend-yield/types.ts +22 -0
  231. package/src/plugins/builtin/earnings/data/cache.test.ts +10 -7
  232. package/src/plugins/builtin/earnings/data/cache.ts +23 -9
  233. package/src/plugins/builtin/earnings/index.tsx +57 -6
  234. package/src/plugins/builtin/econ/calendar-model.test.ts +37 -3
  235. package/src/plugins/builtin/econ/calendar-model.ts +32 -26
  236. package/src/plugins/builtin/econ/fred-cache.test.ts +8 -1
  237. package/src/plugins/builtin/econ/fred-series-map.ts +18 -0
  238. package/src/plugins/builtin/econ/index.test.tsx +105 -0
  239. package/src/plugins/builtin/econ/index.tsx +95 -52
  240. package/src/plugins/builtin/fear-greed/cache.test.ts +18 -3
  241. package/src/plugins/builtin/fear-greed/cache.ts +18 -6
  242. package/src/plugins/builtin/fear-greed/charts.tsx +9 -2
  243. package/src/plugins/builtin/fear-greed/format.ts +0 -9
  244. package/src/plugins/builtin/fear-greed/pane.tsx +21 -21
  245. package/src/plugins/builtin/futures/contracts.ts +100 -0
  246. package/src/plugins/builtin/futures/index.tsx +298 -0
  247. package/src/plugins/builtin/futures/model.test.ts +129 -0
  248. package/src/plugins/builtin/futures/model.ts +136 -0
  249. package/src/plugins/builtin/futures/pane.test.tsx +239 -0
  250. package/src/plugins/builtin/futures/table.test.ts +155 -0
  251. package/src/plugins/builtin/futures/table.tsx +241 -0
  252. package/src/plugins/builtin/fx-matrix/index.tsx +160 -134
  253. package/src/plugins/builtin/fx-matrix/pairs.ts +12 -1
  254. package/src/plugins/builtin/fx-matrix/pane.test.tsx +109 -0
  255. package/src/plugins/builtin/help/components.tsx +1 -30
  256. package/src/plugins/builtin/help/index.tsx +5 -25
  257. package/src/plugins/builtin/holders/pane.tsx +17 -12
  258. package/src/plugins/builtin/holders/treemap.tsx +4 -2
  259. package/src/plugins/builtin/insider/index.tsx +20 -10
  260. package/src/plugins/builtin/ipo-calendar/cache.ts +101 -0
  261. package/src/plugins/builtin/ipo-calendar/client.test.ts +31 -0
  262. package/src/plugins/builtin/ipo-calendar/client.ts +296 -0
  263. package/src/plugins/builtin/ipo-calendar/index.tsx +65 -0
  264. package/src/plugins/builtin/ipo-calendar/model.ts +145 -0
  265. package/src/plugins/builtin/ipo-calendar/pane.tsx +328 -0
  266. package/src/plugins/builtin/ipo-calendar/types.ts +19 -0
  267. package/src/plugins/builtin/kelly-sizer/index.test.tsx +3 -3
  268. package/src/plugins/builtin/kelly-sizer/model.ts +8 -1
  269. package/src/plugins/builtin/kelly-sizer/pane.tsx +33 -15
  270. package/src/plugins/builtin/kelly-sizer/view.tsx +12 -5
  271. package/src/plugins/builtin/market-halts/client.ts +114 -0
  272. package/src/plugins/builtin/market-halts/index.tsx +29 -0
  273. package/src/plugins/builtin/market-halts/model.test.ts +187 -0
  274. package/src/plugins/builtin/market-halts/model.ts +291 -0
  275. package/src/plugins/builtin/market-halts/pane.tsx +256 -0
  276. package/src/plugins/builtin/market-heatmap/data.test.ts +1 -0
  277. package/src/plugins/builtin/market-heatmap/data.ts +12 -3
  278. package/src/plugins/builtin/market-heatmap/index.tsx +131 -46
  279. package/src/plugins/builtin/market-movers/index.tsx +145 -63
  280. package/src/plugins/builtin/market-movers/model.ts +19 -3
  281. package/src/plugins/builtin/market-movers/screener.test.ts +108 -0
  282. package/src/plugins/builtin/market-movers/screener.ts +168 -10
  283. package/src/plugins/builtin/news/index.tsx +78 -99
  284. package/src/plugins/builtin/news/wire/article-search.test.ts +72 -0
  285. package/src/plugins/builtin/news/wire/article-search.ts +140 -0
  286. package/src/plugins/builtin/news/wire/breaking/pane.tsx +18 -8
  287. package/src/plugins/builtin/news/wire/categories.ts +35 -0
  288. package/src/plugins/builtin/news/wire/default-feeds.ts +47 -1
  289. package/src/plugins/builtin/news/wire/feed-config.test.ts +10 -5
  290. package/src/plugins/builtin/news/wire/index.tsx +41 -4
  291. package/src/plugins/builtin/news/wire/industry-pane.tsx +38 -19
  292. package/src/plugins/builtin/news/wire/news/detail-view.tsx +10 -5
  293. package/src/plugins/builtin/news/wire/news/footer.ts +1 -0
  294. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +15 -6
  295. package/src/plugins/builtin/news/wire/news/query-presets.ts +16 -2
  296. package/src/plugins/builtin/news/wire/news/table.test.tsx +54 -0
  297. package/src/plugins/builtin/news/wire/news/table.tsx +111 -17
  298. package/src/plugins/builtin/notes/files.test.ts +18 -0
  299. package/src/plugins/builtin/notes/files.ts +10 -4
  300. package/src/plugins/builtin/notes/index.tsx +4 -1
  301. package/src/plugins/builtin/notes/quick-notes-pane.test.tsx +182 -0
  302. package/src/plugins/builtin/notes/quick-notes-pane.tsx +74 -31
  303. package/src/plugins/builtin/notes/ticker-notes-tab.test.tsx +380 -0
  304. package/src/plugins/builtin/notes/ticker-notes-tab.tsx +97 -40
  305. package/src/plugins/builtin/options/calc-seed.test.ts +118 -0
  306. package/src/plugins/builtin/options/calc-seed.ts +52 -0
  307. package/src/plugins/builtin/options/footer.ts +4 -1
  308. package/src/plugins/builtin/options/index.tsx +22 -1
  309. package/src/plugins/builtin/options/live-quotes.ts +8 -0
  310. package/src/plugins/builtin/options/table.ts +3 -1
  311. package/src/plugins/builtin/options/view.test.tsx +5 -1
  312. package/src/plugins/builtin/options/view.tsx +112 -17
  313. package/src/plugins/builtin/options-calculator/index.tsx +35 -0
  314. package/src/plugins/builtin/options-calculator/model.test.ts +172 -0
  315. package/src/plugins/builtin/options-calculator/model.ts +273 -0
  316. package/src/plugins/builtin/options-calculator/pane.test.tsx +138 -0
  317. package/src/plugins/builtin/options-calculator/pane.tsx +218 -0
  318. package/src/plugins/builtin/plugin-module.test.ts +16 -2
  319. package/src/plugins/builtin/plugin-module.ts +5 -1
  320. package/src/plugins/builtin/polls/client.ts +57 -0
  321. package/src/plugins/builtin/polls/hooks.ts +20 -0
  322. package/src/plugins/builtin/polls/index.tsx +35 -0
  323. package/src/plugins/builtin/polls/normalize.test.ts +230 -0
  324. package/src/plugins/builtin/polls/normalize.ts +319 -0
  325. package/src/plugins/builtin/polls/pane.tsx +722 -0
  326. package/src/plugins/builtin/polls/types.ts +77 -0
  327. package/src/plugins/builtin/portfolio-list/header.tsx +15 -4
  328. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -3
  329. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +1 -1
  330. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -0
  331. package/src/plugins/builtin/portfolio-list/summary/index.tsx +2 -1
  332. package/src/plugins/builtin/research/analyst-pane.tsx +40 -67
  333. package/src/plugins/builtin/research/corporate-actions-pane.tsx +25 -19
  334. package/src/plugins/builtin/research/index.test.ts +6 -25
  335. package/src/plugins/builtin/research/index.tsx +11 -5
  336. package/src/plugins/builtin/research/relative-valuation-pane.tsx +86 -26
  337. package/src/plugins/builtin/scanner/denied.tsx +31 -0
  338. package/src/plugins/builtin/scanner/feed.ts +94 -0
  339. package/src/plugins/builtin/scanner/flow-model.test.ts +85 -0
  340. package/src/plugins/builtin/scanner/flow-model.ts +133 -0
  341. package/src/plugins/builtin/scanner/flow-pane.tsx +197 -0
  342. package/src/plugins/builtin/scanner/hilo-bars.tsx +121 -0
  343. package/src/plugins/builtin/scanner/hilo-model.test.ts +44 -0
  344. package/src/plugins/builtin/scanner/hilo-model.ts +75 -0
  345. package/src/plugins/builtin/scanner/hilo-pane.tsx +165 -0
  346. package/src/plugins/builtin/scanner/index.ts +109 -0
  347. package/src/plugins/builtin/scanner/waiting.tsx +11 -0
  348. package/src/plugins/builtin/sec/index.tsx +11 -7
  349. package/src/plugins/builtin/sectors/index.tsx +105 -81
  350. package/src/plugins/builtin/sectors/move-bar.tsx +43 -0
  351. package/src/plugins/builtin/sectors/sector-data.ts +18 -0
  352. package/src/plugins/builtin/sectors/sector-model.ts +13 -15
  353. package/src/plugins/builtin/shared/auto-refresh.ts +51 -0
  354. package/src/plugins/builtin/shared/cloud-upgrade.ts +29 -5
  355. package/src/plugins/builtin/shared/pane-footer.ts +16 -3
  356. package/src/plugins/builtin/shared/screener-live-quotes.test.ts +91 -0
  357. package/src/plugins/builtin/shared/screener-live-quotes.ts +134 -0
  358. package/src/plugins/builtin/shared/table-pane.ts +1 -5
  359. package/src/plugins/builtin/shared/use-quote-board.test.tsx +211 -0
  360. package/src/plugins/builtin/shared/use-quote-board.ts +195 -0
  361. package/src/plugins/builtin/short-interest/client.ts +124 -0
  362. package/src/plugins/builtin/short-interest/index.tsx +62 -0
  363. package/src/plugins/builtin/short-interest/model.ts +113 -0
  364. package/src/plugins/builtin/short-interest/pane.tsx +252 -0
  365. package/src/plugins/builtin/short-interest/types.ts +9 -0
  366. package/src/plugins/builtin/substack/article-detail.tsx +15 -1
  367. package/src/plugins/builtin/substack/pane-footer.ts +3 -22
  368. package/src/plugins/builtin/substack/pane.tsx +1 -2
  369. package/src/plugins/builtin/thirteenf/api.ts +4 -1
  370. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  371. package/src/plugins/builtin/thirteenf/pane.tsx +6 -2
  372. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +10 -4
  373. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +67 -11
  374. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +15 -8
  375. package/src/plugins/builtin/ticker-detail/index.tsx +5 -21
  376. package/src/plugins/builtin/ticker-detail/overview/components.tsx +72 -32
  377. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -10
  378. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -1
  379. package/src/plugins/builtin/ticker-detail/quote-monitor/card.test.tsx +70 -0
  380. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +34 -26
  381. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +96 -49
  382. package/src/plugins/builtin/ticker-detail/research-tabs.ts +32 -0
  383. package/src/plugins/builtin/ticker-research-backend-plugin.ts +2 -9
  384. package/src/plugins/builtin/ticker-research-plugin.ts +6 -0
  385. package/src/plugins/builtin/treasury-auctions/cache.test.ts +173 -0
  386. package/src/plugins/builtin/treasury-auctions/cache.ts +104 -0
  387. package/src/plugins/builtin/treasury-auctions/client.test.ts +181 -0
  388. package/src/plugins/builtin/treasury-auctions/client.ts +151 -0
  389. package/src/plugins/builtin/treasury-auctions/index.tsx +90 -0
  390. package/src/plugins/builtin/treasury-auctions/model.test.ts +147 -0
  391. package/src/plugins/builtin/treasury-auctions/model.ts +188 -0
  392. package/src/plugins/builtin/treasury-auctions/pane.test.tsx +170 -0
  393. package/src/plugins/builtin/treasury-auctions/pane.tsx +417 -0
  394. package/src/plugins/builtin/treasury-auctions/types.ts +59 -0
  395. package/src/plugins/builtin/tv/pane.tsx +67 -7
  396. package/src/plugins/builtin/tv/youtube-embed.test.ts +84 -0
  397. package/src/plugins/builtin/tv/youtube-embed.ts +173 -0
  398. package/src/plugins/builtin/tv/youtube-stream.ts +27 -2
  399. package/src/plugins/builtin/volatility/client.ts +87 -0
  400. package/src/plugins/builtin/volatility/index.test.tsx +107 -0
  401. package/src/plugins/builtin/volatility/index.tsx +193 -0
  402. package/src/plugins/builtin/volatility/model.test.ts +66 -0
  403. package/src/plugins/builtin/volatility/model.ts +113 -0
  404. package/src/plugins/builtin/world-indices/footer.ts +43 -23
  405. package/src/plugins/builtin/world-indices/index.tsx +44 -93
  406. package/src/plugins/builtin/world-indices/indices.ts +11 -2
  407. package/src/plugins/builtin/world-indices/model.ts +17 -24
  408. package/src/plugins/builtin/world-indices/pane.test.tsx +116 -0
  409. package/src/plugins/builtin/world-indices/table.tsx +82 -37
  410. package/src/plugins/builtin/yield-curve/index.tsx +16 -7
  411. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +25 -0
  412. package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -0
  413. package/src/plugins/catalog-backend.ts +8 -0
  414. package/src/plugins/catalog-ui.ts +8 -0
  415. package/src/plugins/event-bus.ts +1 -0
  416. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  417. package/src/plugins/ibkr/trade/tab/footer.ts +9 -23
  418. package/src/plugins/ibkr/trade/tab/index.tsx +1 -5
  419. package/src/plugins/ibkr/trading/pane-view.tsx +149 -87
  420. package/src/plugins/ibkr/trading/pane.tsx +2 -2
  421. package/src/plugins/ownership.ts +3 -0
  422. package/src/plugins/pane-manager/floating.ts +8 -2
  423. package/src/plugins/pane-manager.test.ts +13 -0
  424. package/src/plugins/prediction-markets/backend-plugin.ts +2 -0
  425. package/src/plugins/prediction-markets/capability.test.ts +281 -0
  426. package/src/plugins/prediction-markets/capability.ts +171 -0
  427. package/src/plugins/prediction-markets/chart.tsx +18 -26
  428. package/src/plugins/prediction-markets/columns.ts +3 -2
  429. package/src/plugins/prediction-markets/controller/catalog.ts +15 -12
  430. package/src/plugins/prediction-markets/controller/detail.ts +4 -0
  431. package/src/plugins/prediction-markets/controller/index.ts +0 -15
  432. package/src/plugins/prediction-markets/detail/book.tsx +3 -23
  433. package/src/plugins/prediction-markets/detail/overview.tsx +0 -18
  434. package/src/plugins/prediction-markets/detail/pane.tsx +36 -12
  435. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  436. package/src/plugins/prediction-markets/index.tsx +2 -0
  437. package/src/plugins/prediction-markets/metrics.ts +28 -18
  438. package/src/plugins/prediction-markets/navigation.ts +1 -0
  439. package/src/plugins/prediction-markets/pane.test.tsx +1 -1
  440. package/src/plugins/prediction-markets/pane.tsx +0 -1
  441. package/src/plugins/prediction-markets/services/fetch.ts +3 -2
  442. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +82 -23
  443. package/src/plugins/prediction-markets/services/kalshi/normalize.ts +3 -1
  444. package/src/plugins/prediction-markets/services/kalshi/types.ts +14 -0
  445. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +26 -14
  446. package/src/plugins/prediction-markets/services/polymarket/detail.ts +67 -18
  447. package/src/plugins/prediction-markets/test-helpers.tsx +0 -1
  448. package/src/plugins/prediction-markets/types.ts +3 -9
  449. package/src/plugins/registry/context.ts +6 -1
  450. package/src/plugins/registry/index.test.ts +2 -0
  451. package/src/plugins/registry/index.ts +53 -3
  452. package/src/plugins/runtime/context.tsx +4 -2
  453. package/src/plugins/runtime/index.tsx +9 -0
  454. package/src/remote/controller.test.ts +17 -4
  455. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  456. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -2
  457. package/src/renderers/electrobun/bun/desktop/capability-bridge.test.ts +64 -0
  458. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +41 -7
  459. package/src/renderers/electrobun/bun/desktop/host-requests.ts +5 -2
  460. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +33 -0
  461. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +53 -6
  462. package/src/renderers/electrobun/bun/desktop/initialization.test.ts +27 -0
  463. package/src/renderers/electrobun/bun/desktop/initialization.ts +6 -1
  464. package/src/renderers/electrobun/bun/index.ts +1 -0
  465. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  466. package/src/renderers/electrobun/view/app-services.ts +25 -2
  467. package/src/renderers/electrobun/view/build-assets.ts +5 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +186 -65
  469. package/src/renderers/electrobun/view/cli-pane-shot-health.test.ts +25 -0
  470. package/src/renderers/electrobun/view/cli-pane-shot-health.ts +31 -0
  471. package/src/renderers/electrobun/view/data-table/index.tsx +10 -11
  472. package/src/renderers/electrobun/view/data-table/row.tsx +2 -0
  473. package/src/renderers/electrobun/view/desktop/controls.test.tsx +68 -0
  474. package/src/renderers/electrobun/view/desktop/controls.tsx +25 -1
  475. package/src/renderers/electrobun/view/host/box.test.tsx +80 -0
  476. package/src/renderers/electrobun/view/host/media-surface.tsx +70 -3
  477. package/src/renderers/electrobun/view/host/tabs.tsx +97 -5
  478. package/src/renderers/electrobun/view/host/text.tsx +5 -2
  479. package/src/renderers/electrobun/view/input-host.test.tsx +140 -0
  480. package/src/renderers/electrobun/view/input-host.tsx +4 -2
  481. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +19 -1
  482. package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +7 -0
  483. package/src/renderers/electrobun/view/native-stubs/chart/kitty-support.ts +10 -0
  484. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +46 -0
  485. package/src/renderers/electrobun/view/remote/capability-invoker.ts +34 -13
  486. package/src/renderers/electrobun/view/remote/connection-health-backend.ts +19 -0
  487. package/src/renderers/electrobun/view/remote/connection-health.test.ts +44 -0
  488. package/src/renderers/electrobun/view/remote/connection-health.ts +49 -0
  489. package/src/renderers/electrobun/view/styles.css +5 -0
  490. package/src/renderers/opentui/dialog-host.tsx +279 -80
  491. package/src/renderers/opentui/host.tsx +15 -1
  492. package/src/renderers/opentui/terminal-media.test.ts +102 -0
  493. package/src/renderers/opentui/terminal-media.ts +142 -0
  494. package/src/renderers/opentui/test-utils.tsx +9 -6
  495. package/src/renderers/opentui/toast-host.tsx +136 -7
  496. package/src/renderers/opentui/ui-host.tsx +19 -3
  497. package/src/sources/provider-router/connection-health.test.ts +28 -0
  498. package/src/sources/provider-router/connection-health.ts +47 -0
  499. package/src/sources/provider-router/history.test.ts +53 -4
  500. package/src/sources/provider-router/history.ts +119 -14
  501. package/src/sources/provider-router/index.ts +12 -1
  502. package/src/sources/yahoo-finance/history.ts +2 -9
  503. package/src/sources/yahoo-finance/http.ts +1 -1
  504. package/src/sources/yahoo-finance/symbols.ts +1 -1
  505. package/src/sources/yahoo-finance/types.ts +19 -0
  506. package/src/state/app/context/index.test.tsx +101 -0
  507. package/src/state/app/context/index.tsx +10 -0
  508. package/src/state/config-save-scheduler.ts +1 -6
  509. package/src/state/persist-scheduler.test.ts +31 -0
  510. package/src/state/persist-scheduler.ts +21 -8
  511. package/src/sync/auth-transition.test.ts +36 -0
  512. package/src/sync/auth-transition.ts +22 -0
  513. package/src/sync/core-contributors.test.ts +58 -1
  514. package/src/sync/core-contributors.ts +39 -2
  515. package/src/sync/react.ts +9 -0
  516. package/src/test-support/plugin-runtime.ts +3 -0
  517. package/src/theme/font-scale.test.ts +14 -0
  518. package/src/theme/font-scale.ts +69 -0
  519. package/src/tickers/search/index.test.ts +284 -2
  520. package/src/tickers/search/index.ts +55 -18
  521. package/src/tickers/search/ranking.ts +143 -18
  522. package/src/tickers/search/types.ts +1 -0
  523. package/src/time-series/alignment.ts +15 -11
  524. package/src/time-series/field-catalog.ts +10 -0
  525. package/src/time-series/fundamentals.ts +19 -11
  526. package/src/time-series/hooks.ts +8 -2
  527. package/src/time-series/parsed-history-cache.ts +35 -0
  528. package/src/time-series/resolution.ts +10 -0
  529. package/src/time-series/resolve.test.ts +332 -3
  530. package/src/time-series/resolve.ts +225 -25
  531. package/src/time-series/spec-studies.test.ts +23 -0
  532. package/src/time-series/spec.ts +21 -2
  533. package/src/time-series/transforms.test.ts +76 -1
  534. package/src/time-series/transforms.ts +39 -8
  535. package/src/time-series/types.ts +9 -2
  536. package/src/time-series/use-chart-resolution.test.tsx +105 -1
  537. package/src/time-series/use-chart-resolution.ts +67 -13
  538. package/src/types/config.ts +30 -27
  539. package/src/types/media.ts +2 -0
  540. package/src/types/plugin.ts +8 -1
  541. package/src/ui/host.tsx +4 -0
  542. package/src/ui/pointer-drag.ts +12 -0
  543. package/src/ui/qr.test.ts +79 -0
  544. package/src/ui/qr.ts +82 -0
  545. package/src/updater/github-releases.test.ts +3 -3
  546. package/src/updater/github-releases.ts +1 -1
  547. package/src/updater.test.ts +117 -16
  548. package/src/updater.ts +32 -5
  549. package/src/utils/exchanges.ts +1 -0
  550. package/src/utils/external-url.test.ts +32 -0
  551. package/src/utils/external-url.ts +23 -0
  552. package/src/utils/sort-values.ts +31 -0
  553. package/src/utils/zoned-date-time.ts +67 -0
  554. package/src/components/onboarding/wizard-keyboard.ts +0 -306
  555. package/src/plugins/builtin/chart-composer/date-window-dialog.tsx +0 -120
@@ -12,11 +12,14 @@ import {
12
12
  type ScrollBoxRenderable,
13
13
  } from "../../../ui";
14
14
  import { useShortcut } from "../../../react/input";
15
+ import { useOptionalPaneInstanceId, usePaneSettingValue } from "../../../state/app/context";
15
16
  import { colors as themeColors, hoverBg } from "../../../theme/colors";
16
17
  import { formatPercentRaw } from "../../../utils/format";
17
18
  import { isPlainKey } from "../../../utils/keyboard";
18
19
  import { truncateWithEllipsis } from "../../../utils/text-wrap";
19
20
  import type { ResolvedSeries } from "../../../time-series/types";
21
+ import { downsampleCompositeChartScene } from "./downsample";
22
+ import { reuseResolvedSeriesList } from "./panel-series";
20
23
  import {
21
24
  consumeChartMouseEvent,
22
25
  getGlobalMouseX,
@@ -24,6 +27,7 @@ import {
24
27
  type ChartMouseEvent,
25
28
  } from "../core/pointer";
26
29
  import type { NativeChartBitmap } from "../native/chart-rasterizer";
30
+ import { useShowChartTextFallback } from "../native/use-chart-text-fallback";
27
31
  import {
28
32
  useStaticChartBitmapSize,
29
33
  type StaticChartBitmapSize,
@@ -611,6 +615,7 @@ interface CompositePanelSurfaceProps {
611
615
  onZoomViewport: (zoomFactor: number, anchorRatio: number) => void;
612
616
  onSetViewport: (range: CompositeViewportRange) => void;
613
617
  onToolSpanChange: (span: ChartToolSpan | null) => void;
618
+ showTextFallback: boolean;
614
619
  }
615
620
 
616
621
  function CompositePanelSurface({
@@ -637,6 +642,7 @@ function CompositePanelSurface({
637
642
  onZoomViewport,
638
643
  onSetViewport,
639
644
  onToolSpanChange,
645
+ showTextFallback,
640
646
  }: CompositePanelSurfaceProps) {
641
647
  const isDesktopWeb = useUiHost().kind === "desktop-web";
642
648
  const { cellHeightPx = 18, cellWidthPx = 8 } = useUiCapabilities();
@@ -787,10 +793,10 @@ function CompositePanelSurface({
787
793
  toolReadout?.direction,
788
794
  ]);
789
795
  const textLines = useMemo(
790
- () => isDesktopWeb
796
+ () => isDesktopWeb || !showTextFallback
791
797
  ? []
792
798
  : renderCompositePanelText(panel, plotWidth, scene.cursorXRatio, activeCursorYRatio),
793
- [activeCursorYRatio, isDesktopWeb, panel, plotWidth, scene.cursorXRatio],
799
+ [activeCursorYRatio, isDesktopWeb, panel, plotWidth, scene.cursorXRatio, showTextFallback],
794
800
  );
795
801
  const leftAxisLabels = useMemo(
796
802
  () => axisLabelRows(
@@ -1364,6 +1370,52 @@ function CompositeLegend({
1364
1370
  );
1365
1371
  }
1366
1372
 
1373
+ const CHART_DRAWINGS_SETTING_KEY = "chartDrawings";
1374
+ const NO_DRAWINGS: readonly ChartDrawing[] = [];
1375
+ /** Coalesces a drag into one write instead of one per pointer move. */
1376
+ const DRAWING_PERSIST_DELAY_MS = 400;
1377
+
1378
+ /**
1379
+ * Drawings are anchored to data, so they outlive the mounted chart: they ride
1380
+ * along with the pane settings that already carry the chart spec. Only mounted
1381
+ * inside a pane, so a standalone chart still renders without app state.
1382
+ */
1383
+ function ChartDrawingStore({
1384
+ paneInstanceId,
1385
+ drawings,
1386
+ onRestore,
1387
+ }: {
1388
+ paneInstanceId: string;
1389
+ drawings: readonly ChartDrawing[];
1390
+ onRestore: (drawings: readonly ChartDrawing[]) => void;
1391
+ }) {
1392
+ const [stored, setStored] = usePaneSettingValue<readonly ChartDrawing[]>(
1393
+ CHART_DRAWINGS_SETTING_KEY,
1394
+ NO_DRAWINGS,
1395
+ paneInstanceId,
1396
+ );
1397
+ const restoredRef = useRef<readonly ChartDrawing[] | null>(null);
1398
+ if (restoredRef.current === null) {
1399
+ restoredRef.current = Array.isArray(stored) ? stored : NO_DRAWINGS;
1400
+ }
1401
+
1402
+ useEffect(() => {
1403
+ const restored = restoredRef.current;
1404
+ if (restored && restored.length > 0) onRestore(restored);
1405
+ // Restoring once on mount: later writes must not scroll back in time.
1406
+ }, []);
1407
+
1408
+ useEffect(() => {
1409
+ const restored = restoredRef.current ?? NO_DRAWINGS;
1410
+ if (drawings === restored) return;
1411
+ if (drawings.length === 0 && restored.length === 0) return;
1412
+ const timer = setTimeout(() => setStored(drawings), DRAWING_PERSIST_DELAY_MS);
1413
+ return () => clearTimeout(timer);
1414
+ }, [drawings, setStored]);
1415
+
1416
+ return null;
1417
+ }
1418
+
1367
1419
  export function CompositeChart({
1368
1420
  series,
1369
1421
  legendSeries,
@@ -1392,15 +1444,15 @@ export function CompositeChart({
1392
1444
  onToggleSeries,
1393
1445
  isSeriesToggleable,
1394
1446
  }: CompositeChartProps) {
1395
- const { cellWidthPx = 8 } = useUiCapabilities();
1447
+ const { cellWidthPx = 8, pixelRatio = 1 } = useUiCapabilities();
1396
1448
  const isDesktopWeb = useUiHost().kind === "desktop-web";
1449
+ const showTextFallback = useShowChartTextFallback();
1397
1450
  const [internalCursorDate, setInternalCursorDate] = useState<Date | null>(null);
1398
1451
  const [legendKeyboardIndex, setLegendKeyboardIndex] = useState<number | null>(null);
1399
1452
  const [toolSpan, setToolSpan] = useState<ChartToolSpan | null>(null);
1400
1453
  const [armedTool, setArmedTool] = useState<ChartToolKind | null>(null);
1401
- // ponytail: drawings live with the mounted chart. Persisting them belongs with
1402
- // pane settings, which is a separate plumbing job.
1403
- const [drawings, setDrawings] = useState<readonly ChartDrawing[]>([]);
1454
+ const paneInstanceId = useOptionalPaneInstanceId();
1455
+ const [drawings, setDrawings] = useState<readonly ChartDrawing[]>(NO_DRAWINGS);
1404
1456
  const [selectedDrawingId, setSelectedDrawingId] = useState<string | null>(null);
1405
1457
  const [drawColor, setDrawColor] = useState<string>(CHART_DRAWING_COLORS[0]);
1406
1458
  const addDrawing = useCallback((drawing: ChartDrawing) => {
@@ -1433,7 +1485,10 @@ export function CompositeChart({
1433
1485
  const resolvedCursorDate = cursorDate === undefined ? internalCursorDate : cursorDate;
1434
1486
  const totalWidth = Math.max(1, Math.floor(width));
1435
1487
  const totalHeight = Math.max(1, Math.floor(height));
1436
- const visibleSeries = useMemo(() => series.filter((entry) => entry.points.length > 0), [series]);
1488
+ const seriesIdentityRef = useRef<ResolvedSeries[]>([]);
1489
+ const stableSeries = reuseResolvedSeriesList(seriesIdentityRef.current, series);
1490
+ seriesIdentityRef.current = stableSeries;
1491
+ const visibleSeries = useMemo(() => stableSeries.filter((entry) => entry.points.length > 0), [stableSeries]);
1437
1492
  const marketTimelineSeries = useMemo(() => {
1438
1493
  const supplied = timelineSeries?.filter((entry) => entry.points.length > 0) ?? [];
1439
1494
  return supplied.some((entry) => entry.timeBasis?.kind === "market")
@@ -1629,6 +1684,11 @@ export function CompositeChart({
1629
1684
  : 0;
1630
1685
  const timeAxisRows = showTimeAxis ? 1 : 0;
1631
1686
  const panelCount = new Set(visibleSeries.map((entry) => entry.panelId)).size;
1687
+ const lastTickKey = visibleSeries.map((entry) => {
1688
+ const last = entry.points.at(-1);
1689
+ if (!last) return entry.id;
1690
+ return `${entry.id}:${last.date.getTime()}:${last.close ?? ""}:${last.value ?? ""}:${entry.latestChangePercent ?? ""}`;
1691
+ }).join("|");
1632
1692
  const plotHeight = Math.max(panelCount, totalHeight - legendRows - timeAxisRows);
1633
1693
  const resolvedColors = useMemo<CompositeChartColors>(() => ({
1634
1694
  background: colors?.background ?? themeColors.bg,
@@ -1638,12 +1698,16 @@ export function CompositeChart({
1638
1698
  textDim: colors?.textDim ?? themeColors.textDim,
1639
1699
  negative: colors?.negative ?? themeColors.negative,
1640
1700
  }), [colors]);
1641
- const projectedScene = useMemo(() => buildCompositeChartScene(visibleSeries, panels, {
1642
- width: 1,
1643
- height: Math.max(panelCount, 1),
1644
- viewport: effectiveViewport ?? undefined,
1645
- timelineSeries: marketTimelineSeries,
1646
- }), [effectiveViewport, marketTimelineSeries, panelCount, panels, visibleSeries]);
1701
+ const projectedScene = useMemo(() => {
1702
+ // lastTickKey busts this memo when a live tick mutates series identity in place.
1703
+ void lastTickKey;
1704
+ return buildCompositeChartScene(visibleSeries, panels, {
1705
+ width: 1,
1706
+ height: Math.max(panelCount, 1),
1707
+ viewport: effectiveViewport ?? undefined,
1708
+ timelineSeries: marketTimelineSeries,
1709
+ });
1710
+ }, [effectiveViewport, lastTickKey, marketTimelineSeries, panelCount, panels, visibleSeries]);
1647
1711
  // Gutters follow the axes the scene actually built. Reading the series list
1648
1712
  // instead drops a gutter the moment its series has no observation in view,
1649
1713
  // which is exactly what a zoom does, taking the axis labels with it.
@@ -1673,6 +1737,10 @@ export function CompositeChart({
1673
1737
  const horizontalReserved = leftAxisWidth + rightAxisWidth
1674
1738
  + axisGap * ((leftAxisWidth ? 1 : 0) + (rightAxisWidth ? 1 : 0));
1675
1739
  const plotWidth = Math.max(1, totalWidth - horizontalReserved);
1740
+ const downsampleWidth = Math.max(
1741
+ 1,
1742
+ Math.round(plotWidth * cellWidthPx * Math.max(1, pixelRatio)),
1743
+ );
1676
1744
  const layoutPanels = useMemo<CompositePanelScene[] | null>(() => {
1677
1745
  if (!projectedScene) return null;
1678
1746
  const panelSpecById = new Map(panels.map((panel) => [panel.id, panel] as const));
@@ -1690,13 +1758,14 @@ export function CompositeChart({
1690
1758
  }, [panels, plotHeight, projectedScene]);
1691
1759
  const baseScene = useMemo<CompositeChartScene | null>(() => {
1692
1760
  if (!projectedScene || !layoutPanels) return null;
1693
- return {
1761
+ const laidOut: CompositeChartScene = {
1694
1762
  ...projectedScene,
1695
1763
  width: plotWidth,
1696
1764
  height: layoutPanels.reduce((sum, panel) => sum + panel.height, 0),
1697
1765
  panels: layoutPanels,
1698
1766
  };
1699
- }, [layoutPanels, plotWidth, projectedScene]);
1767
+ return downsampleCompositeChartScene(laidOut, downsampleWidth);
1768
+ }, [downsampleWidth, layoutPanels, plotWidth, projectedScene]);
1700
1769
  const resolvedCursorTimestamp = resolvedCursorDate?.getTime() ?? null;
1701
1770
  const normalizedCursorTimestamp = resolvedCursorTimestamp !== null && Number.isFinite(resolvedCursorTimestamp)
1702
1771
  ? resolvedCursorTimestamp
@@ -1988,6 +2057,13 @@ export function CompositeChart({
1988
2057
  }}
1989
2058
  />
1990
2059
  ) : null}
2060
+ {paneInstanceId ? (
2061
+ <ChartDrawingStore
2062
+ paneInstanceId={paneInstanceId}
2063
+ drawings={drawings}
2064
+ onRestore={setDrawings}
2065
+ />
2066
+ ) : null}
1991
2067
  {scene.panels.map((panel) => (
1992
2068
  <CompositePanelSurface
1993
2069
  key={panel.id}
@@ -2014,6 +2090,7 @@ export function CompositeChart({
2014
2090
  onZoomViewport={zoomViewport}
2015
2091
  onSetViewport={setViewportRange}
2016
2092
  onToolSpanChange={setToolSpan}
2093
+ showTextFallback={showTextFallback}
2017
2094
  />
2018
2095
  ))}
2019
2096
  {timeAxisLayout ? (
@@ -0,0 +1,136 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ResolvedSeries, TimeSeriesPoint } from "../../../time-series/types";
3
+ import {
4
+ downsampleCompositeChartScene,
5
+ downsampleLttb,
6
+ downsampleOhlcProjectedPoints,
7
+ } from "./downsample";
8
+ import { buildCompositeChartScene } from "./scene";
9
+
10
+ function point(date: string, value: number): TimeSeriesPoint {
11
+ const observedAt = new Date(`${date}T00:00:00.000Z`);
12
+ return { date: observedAt, observedAt, value };
13
+ }
14
+
15
+ function ohlcPoint(
16
+ date: Date,
17
+ open: number,
18
+ high: number,
19
+ low: number,
20
+ close: number,
21
+ ): TimeSeriesPoint {
22
+ return {
23
+ date,
24
+ observedAt: date,
25
+ value: close,
26
+ open,
27
+ high,
28
+ low,
29
+ close,
30
+ volume: 1,
31
+ };
32
+ }
33
+
34
+ function series(
35
+ overrides: Partial<ResolvedSeries> & Pick<ResolvedSeries, "id" | "points">,
36
+ ): ResolvedSeries {
37
+ return {
38
+ id: overrides.id,
39
+ label: overrides.label ?? overrides.id,
40
+ color: overrides.color ?? "#00ff66",
41
+ unit: overrides.unit ?? "USD",
42
+ unitGroup: overrides.unitGroup ?? "currency",
43
+ nativeFrequency: overrides.nativeFrequency ?? "daily",
44
+ dataShape: overrides.dataShape ?? "scalar",
45
+ style: overrides.style ?? "line",
46
+ transform: overrides.transform ?? "raw",
47
+ axis: overrides.axis ?? "left",
48
+ panelId: overrides.panelId ?? "main",
49
+ interpolation: overrides.interpolation ?? "none",
50
+ timestampMode: overrides.timestampMode,
51
+ timeBasis: overrides.timeBasis,
52
+ points: overrides.points,
53
+ warning: overrides.warning,
54
+ };
55
+ }
56
+
57
+ function denseLineSeries(count: number): ResolvedSeries {
58
+ const start = Date.parse("2024-01-01T00:00:00.000Z");
59
+ return series({
60
+ id: "price",
61
+ points: Array.from({ length: count }, (_, index) => {
62
+ const date = new Date(start + index * 86_400_000);
63
+ const value = 100 + Math.sin(index / 8) * 20 + (index % 37 === 0 ? 40 : 0);
64
+ return { date, observedAt: date, value };
65
+ }),
66
+ });
67
+ }
68
+
69
+ describe("composite downsample", () => {
70
+ test("caps line series to the pixel width and keeps the shared date list", () => {
71
+ const scene = buildCompositeChartScene(
72
+ [denseLineSeries(400)],
73
+ [{ id: "main" }],
74
+ { width: 80, height: 12 },
75
+ );
76
+ expect(scene).not.toBeNull();
77
+ expect(scene!.panels[0]!.series[0]!.points.length).toBe(400);
78
+
79
+ const downsampled = downsampleCompositeChartScene(scene!, 64);
80
+ expect(downsampled.dates).toBe(scene!.dates);
81
+ expect(downsampled.dateRatios).toBe(scene!.dateRatios);
82
+ expect(downsampled.dates).toHaveLength(400);
83
+ expect(downsampled.panels[0]!.series[0]!.points.length).toBeLessThanOrEqual(64);
84
+ expect(downsampled.panels[0]!.series[0]!.points.length).toBeGreaterThan(2);
85
+ });
86
+
87
+ test("LTTB never exceeds the requested point budget", () => {
88
+ const scene = buildCompositeChartScene(
89
+ [denseLineSeries(250)],
90
+ [{ id: "main" }],
91
+ { width: 120, height: 10 },
92
+ )!;
93
+ const points = scene.panels[0]!.series[0]!.points;
94
+ expect(downsampleLttb(points, 40)).toHaveLength(40);
95
+ expect(downsampleLttb(points, points.length)).toBe(points);
96
+ });
97
+
98
+ test("OHLC buckets preserve the viewport high and low", () => {
99
+ const start = Date.parse("2024-01-01T00:00:00.000Z");
100
+ const candles = series({
101
+ id: "ohlc",
102
+ dataShape: "ohlcv",
103
+ style: "candles",
104
+ points: Array.from({ length: 180 }, (_, index) => {
105
+ const date = new Date(start + index * 86_400_000);
106
+ const close = 50 + index;
107
+ const high = index === 41 ? 900 : close + 1;
108
+ const low = index === 117 ? 2 : close - 1;
109
+ return ohlcPoint(date, close - 0.5, high, low, close);
110
+ }),
111
+ });
112
+ const scene = buildCompositeChartScene(
113
+ [candles],
114
+ [{ id: "main" }],
115
+ { width: 80, height: 12 },
116
+ )!;
117
+ const source = scene.panels[0]!.series[0]!.points;
118
+ const buckets = downsampleOhlcProjectedPoints(source, 40);
119
+ expect(buckets.length).toBeLessThanOrEqual(40);
120
+ expect(Math.max(...buckets.map((entry) => entry.point.high ?? entry.value))).toBe(900);
121
+ expect(Math.min(...buckets.map((entry) => entry.point.low ?? entry.value))).toBe(2);
122
+ });
123
+
124
+ test("does not downsample when the series already fits the budget", () => {
125
+ const scene = buildCompositeChartScene(
126
+ [series({
127
+ id: "short",
128
+ points: [point("2025-01-01", 10), point("2025-01-02", 12), point("2025-01-03", 11)],
129
+ })],
130
+ [{ id: "main" }],
131
+ { width: 80, height: 8 },
132
+ )!;
133
+ const downsampled = downsampleCompositeChartScene(scene, 64);
134
+ expect(downsampled.panels[0]!.series[0]!.points).toBe(scene.panels[0]!.series[0]!.points);
135
+ });
136
+ });
@@ -0,0 +1,208 @@
1
+ import { isOhlcSeriesStyle } from "../../../time-series/spec";
2
+ import type { TimeSeriesPoint } from "../../../time-series/types";
3
+ import type {
4
+ CompositeChartScene,
5
+ CompositePanelScene,
6
+ CompositeProjectedPoint,
7
+ CompositeProjectedSeries,
8
+ } from "./types";
9
+
10
+ function finiteNumber(value: unknown): value is number {
11
+ return typeof value === "number" && Number.isFinite(value);
12
+ }
13
+
14
+ function splitConnectedSegments(
15
+ points: readonly CompositeProjectedPoint[],
16
+ ): CompositeProjectedPoint[][] {
17
+ const segments: CompositeProjectedPoint[][] = [];
18
+ let current: CompositeProjectedPoint[] = [];
19
+ for (const point of points) {
20
+ if (point.breakBefore && current.length > 0) {
21
+ segments.push(current);
22
+ current = [];
23
+ }
24
+ current.push(point);
25
+ }
26
+ if (current.length > 0) segments.push(current);
27
+ return segments;
28
+ }
29
+
30
+ /**
31
+ * Largest-Triangle-Three-Buckets. Keeps the first and last samples and one
32
+ * representative from each interior bucket so a line still fits the pixel
33
+ * budget without flattening extrema.
34
+ */
35
+ export function downsampleLttb(
36
+ points: readonly CompositeProjectedPoint[],
37
+ threshold: number,
38
+ ): CompositeProjectedPoint[] {
39
+ const count = points.length;
40
+ const budget = Math.max(Math.floor(threshold), 1);
41
+ if (count <= budget) return points as CompositeProjectedPoint[];
42
+ if (budget === 1) return [points[0]!];
43
+ if (budget === 2) return [points[0]!, points[count - 1]!];
44
+
45
+ const sampled: CompositeProjectedPoint[] = [points[0]!];
46
+ const bucketSize = (count - 2) / (budget - 2);
47
+ let previousIndex = 0;
48
+
49
+ for (let bucketIndex = 0; bucketIndex < budget - 2; bucketIndex += 1) {
50
+ const nextStart = Math.floor((bucketIndex + 1) * bucketSize) + 1;
51
+ const nextEnd = Math.min(Math.floor((bucketIndex + 2) * bucketSize) + 1, count);
52
+ let avgX = 0;
53
+ let avgY = 0;
54
+ const avgCount = Math.max(nextEnd - nextStart, 1);
55
+ for (let index = nextStart; index < nextEnd; index += 1) {
56
+ avgX += points[index]!.xRatio;
57
+ avgY += points[index]!.yRatio;
58
+ }
59
+ avgX /= avgCount;
60
+ avgY /= avgCount;
61
+
62
+ const currentStart = Math.floor(bucketIndex * bucketSize) + 1;
63
+ const currentEnd = Math.floor((bucketIndex + 1) * bucketSize) + 1;
64
+ const previous = points[previousIndex]!;
65
+ const prevX = previous.xRatio;
66
+ const prevY = previous.yRatio;
67
+ let maxArea = -1;
68
+ let maxIndex = currentStart;
69
+ for (let index = currentStart; index < currentEnd; index += 1) {
70
+ const point = points[index]!;
71
+ const area = Math.abs(
72
+ (prevX - avgX) * (point.yRatio - prevY) - (prevX - point.xRatio) * (avgY - prevY),
73
+ );
74
+ if (area > maxArea) {
75
+ maxArea = area;
76
+ maxIndex = index;
77
+ }
78
+ }
79
+ sampled.push(points[maxIndex]!);
80
+ previousIndex = maxIndex;
81
+ }
82
+
83
+ sampled.push(points[count - 1]!);
84
+ return sampled;
85
+ }
86
+
87
+ function downsampleLineProjectedPoints(
88
+ points: readonly CompositeProjectedPoint[],
89
+ pixelWidth: number,
90
+ ): CompositeProjectedPoint[] {
91
+ if (points.length <= pixelWidth) return points as CompositeProjectedPoint[];
92
+ const segments = splitConnectedSegments(points);
93
+ if (segments.length === 1) return downsampleLttb(points, pixelWidth);
94
+
95
+ const sampled: CompositeProjectedPoint[] = [];
96
+ for (const segment of segments) {
97
+ const budget = Math.max(2, Math.floor(pixelWidth * (segment.length / points.length)));
98
+ const next = downsampleLttb(segment, Math.min(budget, segment.length));
99
+ if (sampled.length > 0 && next[0] && !next[0].breakBefore) {
100
+ sampled.push({ ...next[0], breakBefore: true }, ...next.slice(1));
101
+ continue;
102
+ }
103
+ sampled.push(...next);
104
+ }
105
+ return sampled.length <= pixelWidth ? sampled : downsampleLttb(sampled, pixelWidth);
106
+ }
107
+
108
+ function sourceValue(point: CompositeProjectedPoint, key: "open" | "high" | "low" | "close"): number {
109
+ const candidate = point.point[key];
110
+ return finiteNumber(candidate) ? candidate : point.value;
111
+ }
112
+
113
+ function aggregateOhlcProjectedBucket(
114
+ bucket: readonly CompositeProjectedPoint[],
115
+ ): CompositeProjectedPoint {
116
+ const first = bucket[0]!;
117
+ const last = bucket[bucket.length - 1]!;
118
+ let high = sourceValue(first, "high");
119
+ let low = sourceValue(first, "low");
120
+ let volume = finiteNumber(first.point.volume) ? first.point.volume : 0;
121
+ let breakBefore = first.breakBefore;
122
+
123
+ for (let index = 1; index < bucket.length; index += 1) {
124
+ const point = bucket[index]!;
125
+ high = Math.max(high, sourceValue(point, "high"));
126
+ low = Math.min(low, sourceValue(point, "low"));
127
+ if (finiteNumber(point.point.volume)) volume += point.point.volume;
128
+ if (point.breakBefore) breakBefore = true;
129
+ }
130
+
131
+ const open = sourceValue(first, "open");
132
+ const close = sourceValue(last, "close");
133
+ const point: TimeSeriesPoint = {
134
+ ...last.point,
135
+ value: close,
136
+ open,
137
+ high,
138
+ low,
139
+ close,
140
+ volume,
141
+ };
142
+ return {
143
+ ...last,
144
+ point,
145
+ value: close,
146
+ breakBefore,
147
+ };
148
+ }
149
+
150
+ export function downsampleOhlcProjectedPoints(
151
+ points: readonly CompositeProjectedPoint[],
152
+ targetWidth: number,
153
+ ): CompositeProjectedPoint[] {
154
+ const budget = Math.max(Math.floor(targetWidth), 1);
155
+ if (points.length <= budget) return points as CompositeProjectedPoint[];
156
+
157
+ const buckets = new Map<number, CompositeProjectedPoint[]>();
158
+ for (const point of points) {
159
+ const index = Math.min(budget - 1, Math.max(0, Math.floor(point.xRatio * budget)));
160
+ const bucket = buckets.get(index);
161
+ if (bucket) bucket.push(point);
162
+ else buckets.set(index, [point]);
163
+ }
164
+ return [...buckets.entries()]
165
+ .sort((left, right) => left[0] - right[0])
166
+ .map(([, bucket]) => aggregateOhlcProjectedBucket(bucket));
167
+ }
168
+
169
+ export function downsampleProjectedSeries(
170
+ series: CompositeProjectedSeries,
171
+ pixelWidth: number,
172
+ ): CompositeProjectedSeries {
173
+ const width = Math.max(Math.floor(pixelWidth), 1);
174
+ if (series.points.length <= 1 || width <= 1) return series;
175
+ const points = isOhlcSeriesStyle(series.source.style)
176
+ ? downsampleOhlcProjectedPoints(series.points, Math.max(Math.floor(width / 2), 1))
177
+ : downsampleLineProjectedPoints(series.points, width);
178
+ return points === series.points ? series : { ...series, points };
179
+ }
180
+
181
+ function downsamplePanel(panel: CompositePanelScene, pixelWidth: number): CompositePanelScene {
182
+ let changed = false;
183
+ const series = panel.series.map((entry) => {
184
+ const next = downsampleProjectedSeries(entry, pixelWidth);
185
+ if (next !== entry) changed = true;
186
+ return next;
187
+ });
188
+ return changed ? { ...panel, series } : panel;
189
+ }
190
+
191
+ /**
192
+ * Caps each panel's projected series to the plot's pixel budget. Cursor
193
+ * snapping still uses the full `dates` / `dateRatios` lists on the scene.
194
+ */
195
+ export function downsampleCompositeChartScene(
196
+ scene: CompositeChartScene,
197
+ pixelWidth: number,
198
+ ): CompositeChartScene {
199
+ const width = Math.max(Math.floor(pixelWidth), 1);
200
+ if (width <= 1) return scene;
201
+ let changed = false;
202
+ const panels = scene.panels.map((panel) => {
203
+ const next = downsamplePanel(panel, width);
204
+ if (next !== panel) changed = true;
205
+ return next;
206
+ });
207
+ return changed ? { ...scene, panels } : scene;
208
+ }
@@ -0,0 +1,66 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ResolvedSeries, TimeSeriesPoint } from "../../../time-series/types";
3
+ import { reuseResolvedSeriesIdentity, reuseResolvedSeriesList } from "./panel-series";
4
+
5
+ function point(): TimeSeriesPoint {
6
+ const date = new Date("2024-01-01T00:00:00.000Z");
7
+ return { date, observedAt: date, value: 1 };
8
+ }
9
+
10
+ function series(id: string, panelId: string): ResolvedSeries {
11
+ return {
12
+ id,
13
+ label: id,
14
+ color: "#0f0",
15
+ unit: "USD",
16
+ unitGroup: "currency",
17
+ nativeFrequency: "daily",
18
+ dataShape: "scalar",
19
+ style: "line",
20
+ transform: "raw",
21
+ axis: "left",
22
+ panelId,
23
+ interpolation: "none",
24
+ points: [point()],
25
+ };
26
+ }
27
+
28
+ describe("reuseResolvedSeriesIdentity", () => {
29
+ test("reuses the previous object when a parent clones identical series data", () => {
30
+ const original = series("a", "main");
31
+ const clone: ResolvedSeries = { ...original, points: original.points.map((entry) => ({ ...entry })) };
32
+ expect(reuseResolvedSeriesIdentity(original, clone)).toBe(original);
33
+ });
34
+
35
+ test("keeps series identity when only the last bar close and time change", () => {
36
+ const start = new Date("2024-01-01T00:00:00.000Z");
37
+ const first: TimeSeriesPoint = { date: start, observedAt: start, value: 10, close: 10 };
38
+ const second: TimeSeriesPoint = {
39
+ date: new Date("2024-01-02T00:00:00.000Z"),
40
+ observedAt: new Date("2024-01-02T00:00:00.000Z"),
41
+ value: 11,
42
+ close: 11,
43
+ };
44
+ const previous = series("a", "main");
45
+ previous.points = [first, second];
46
+ const nextLast: TimeSeriesPoint = {
47
+ date: new Date("2024-01-02T00:00:01.000Z"),
48
+ observedAt: new Date("2024-01-02T00:00:01.000Z"),
49
+ value: 11.5,
50
+ close: 11.5,
51
+ };
52
+ const next: ResolvedSeries = {
53
+ ...previous,
54
+ points: [first, nextLast],
55
+ };
56
+ expect(reuseResolvedSeriesIdentity(previous, next)).toBe(previous);
57
+ expect(previous.points[1]).toBe(nextLast);
58
+ });
59
+
60
+ test("reuses the list identity when every member is reused", () => {
61
+ const a = series("a", "main");
62
+ const first = [a];
63
+ const second = reuseResolvedSeriesList(first, [{ ...a, points: a.points.map((entry) => ({ ...entry })) }]);
64
+ expect(second).toBe(first);
65
+ });
66
+ });