gloomberb 0.7.5 → 0.8.1

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 (1292) hide show
  1. package/README.md +27 -6
  2. package/package.json +2 -3
  3. package/src/api-client/auth.ts +167 -0
  4. package/src/api-client/chat.ts +116 -0
  5. package/src/api-client/data.ts +177 -0
  6. package/src/api-client/errors.ts +28 -0
  7. package/src/api-client/index.test.ts +675 -0
  8. package/src/api-client/index.ts +384 -0
  9. package/src/api-client/normalizers.ts +61 -0
  10. package/src/api-client/paths.ts +173 -0
  11. package/src/api-client/request.ts +124 -0
  12. package/src/api-client/socket.ts +483 -0
  13. package/src/api-client/types.ts +490 -0
  14. package/src/app/app-bootstrap-state.test.ts +157 -0
  15. package/src/app/app-bootstrap-state.ts +134 -0
  16. package/src/app/global-shortcuts.test.tsx +166 -0
  17. package/src/app/global-shortcuts.ts +126 -0
  18. package/src/app/pane-runtime/index.tsx +297 -0
  19. package/src/app/pane-runtime/layout-placement.ts +73 -0
  20. package/src/app/pane-runtime/pane-settings-runtime.tsx +67 -0
  21. package/src/app/pane-runtime/pane-template-runtime.ts +110 -0
  22. package/src/app/pane-runtime/plugin-bindings.ts +231 -0
  23. package/src/app/pane-runtime/ticker-inspector-runtime.ts +167 -0
  24. package/src/app/pane-runtime/ticker-open-runtime.ts +144 -0
  25. package/src/app/pane-template-dialog-wizard.tsx +52 -0
  26. package/src/app/runtime/broker-import.ts +109 -0
  27. package/src/app/runtime/desktop-menu.ts +74 -0
  28. package/src/app/runtime/plugin-bindings.ts +246 -0
  29. package/src/app/runtime/startup.ts +126 -0
  30. package/src/app/runtime/ticker-refresh.ts +249 -0
  31. package/src/app/runtime/update.ts +86 -0
  32. package/src/app.tsx +107 -1487
  33. package/src/brokers/broker-portfolio-sync.ts +186 -0
  34. package/src/brokers/broker-ticker-sync.ts +141 -0
  35. package/src/brokers/profile-form.ts +2 -2
  36. package/src/brokers/remote-broker-adapter.ts +3 -2
  37. package/src/brokers/sync-broker-instance.test.ts +140 -0
  38. package/src/brokers/sync-broker-instance.ts +60 -181
  39. package/src/capabilities/types.ts +4 -4
  40. package/src/cli/commands/search.ts +1 -1
  41. package/src/cli/commands/ticker.ts +3 -3
  42. package/src/cli/context.ts +1 -1
  43. package/src/cli/desktop-pane-shot.ts +305 -0
  44. package/src/cli/helpers.ts +1 -1
  45. package/src/cli/index.ts +34 -0
  46. package/src/cli/pane-functions/catalog.ts +243 -0
  47. package/src/cli/pane-functions/data.ts +78 -0
  48. package/src/cli/pane-functions/discovery.ts +113 -0
  49. package/src/cli/pane-functions/index.test.ts +114 -0
  50. package/src/cli/pane-functions/index.ts +112 -0
  51. package/src/cli/pane-functions/options.ts +249 -0
  52. package/src/cli/pane-functions/report.ts +180 -0
  53. package/src/cli/pane-functions/resolver.ts +96 -0
  54. package/src/cli/pane-functions/screenshot.ts +113 -0
  55. package/src/cli/registry.ts +3 -2
  56. package/src/cli/ticker-resolution.ts +1 -1
  57. package/src/cli.test.ts +1 -1
  58. package/src/components/chart/bar-chart-renderer.test.ts +53 -0
  59. package/src/components/chart/bar-chart-renderer.ts +190 -0
  60. package/src/components/chart/comparison/data.test.ts +105 -0
  61. package/src/components/chart/comparison/data.ts +279 -0
  62. package/src/components/chart/comparison/renderer.test.ts +187 -0
  63. package/src/components/chart/comparison/renderer.ts +283 -0
  64. package/src/components/chart/comparison/scene.ts +178 -0
  65. package/src/components/chart/comparison-stock-chart/axis-width.ts +99 -0
  66. package/src/components/chart/comparison-stock-chart/canvas.ts +148 -0
  67. package/src/components/chart/comparison-stock-chart/controls.ts +252 -0
  68. package/src/components/chart/comparison-stock-chart/helpers.ts +133 -0
  69. package/src/components/chart/comparison-stock-chart/interactions.ts +202 -0
  70. package/src/components/chart/comparison-stock-chart/layout-metrics.ts +62 -0
  71. package/src/components/chart/comparison-stock-chart/layout.tsx +155 -0
  72. package/src/components/chart/comparison-stock-chart/legend.tsx +73 -0
  73. package/src/components/chart/comparison-stock-chart/native-surfaces.ts +89 -0
  74. package/src/components/chart/comparison-stock-chart/render-data.ts +198 -0
  75. package/src/components/chart/comparison-stock-chart/render-output.ts +145 -0
  76. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +111 -0
  77. package/src/components/chart/comparison-stock-chart/sources.ts +47 -0
  78. package/src/components/chart/comparison-stock-chart/surface-runtime.ts +159 -0
  79. package/src/components/chart/comparison-stock-chart/toolbar.tsx +107 -0
  80. package/src/components/chart/comparison-stock-chart/types.ts +22 -0
  81. package/src/components/chart/comparison-stock-chart/view.tsx +524 -0
  82. package/src/components/chart/comparison-stock-chart/viewport-runtime.ts +239 -0
  83. package/src/components/chart/comparison-stock-chart.tsx +12 -2028
  84. package/src/components/chart/core/axis-format.ts +147 -0
  85. package/src/components/chart/core/axis-measure.ts +33 -0
  86. package/src/components/chart/core/controller.test.ts +157 -0
  87. package/src/components/chart/core/controller.ts +349 -0
  88. package/src/components/chart/core/data.ts +94 -0
  89. package/src/components/chart/core/date-window.ts +392 -0
  90. package/src/components/chart/core/index.test.ts +1007 -0
  91. package/src/components/chart/core/indicator-selector.tsx +75 -0
  92. package/src/components/chart/core/keyboard.ts +15 -0
  93. package/src/components/chart/core/palette.ts +60 -0
  94. package/src/components/chart/core/pane-settings.test.ts +25 -0
  95. package/src/components/chart/core/pane-settings.ts +82 -0
  96. package/src/components/chart/core/pointer.ts +292 -0
  97. package/src/components/chart/core/projection.ts +333 -0
  98. package/src/components/chart/core/render-utils.ts +4 -0
  99. package/src/components/chart/core/renderer.ts +249 -0
  100. package/src/components/chart/core/resolution.test.ts +99 -0
  101. package/src/components/chart/core/resolution.ts +380 -0
  102. package/src/components/chart/core/scene.ts +180 -0
  103. package/src/components/chart/core/scroll.test.ts +70 -0
  104. package/src/components/chart/core/scroll.ts +104 -0
  105. package/src/components/chart/core/time-axis.ts +330 -0
  106. package/src/components/chart/core/types.ts +124 -0
  107. package/src/components/chart/core/viewport.test.ts +29 -0
  108. package/src/components/chart/core/viewport.ts +51 -0
  109. package/src/components/chart/core/window-viewport.test.ts +69 -0
  110. package/src/components/chart/core/window-viewport.ts +162 -0
  111. package/src/components/chart/display-cursor.ts +153 -0
  112. package/src/components/chart/market-session.ts +1 -1
  113. package/src/components/chart/multi-line/renderer.test.ts +83 -0
  114. package/src/components/chart/multi-line/renderer.ts +160 -0
  115. package/src/components/chart/multi-line/scene.ts +188 -0
  116. package/src/components/chart/native/canvas-bitmaps.ts +140 -0
  117. package/src/components/chart/native/chart-rasterizer.ts +18 -865
  118. package/src/components/chart/native/kitty/adapter.ts +9 -0
  119. package/src/components/chart/native/kitty/index.test.ts +752 -0
  120. package/src/components/chart/native/kitty/manager.ts +142 -0
  121. package/src/components/chart/native/kitty/support.ts +70 -0
  122. package/src/components/chart/native/raster/comparison-chart.ts +162 -0
  123. package/src/components/chart/native/raster/crosshair.ts +51 -0
  124. package/src/components/chart/native/raster/layout.ts +136 -0
  125. package/src/components/chart/native/raster/placement.ts +132 -0
  126. package/src/components/chart/native/raster/price-chart.ts +244 -0
  127. package/src/components/chart/native/raster/primitives.ts +154 -0
  128. package/src/components/chart/native/raster/types.ts +41 -0
  129. package/src/components/chart/native/renderer-selection.ts +81 -1
  130. package/src/components/chart/native/surface/hooks.ts +137 -0
  131. package/src/components/chart/native/surface/manager.ts +225 -0
  132. package/src/components/chart/native/surface/rendering.ts +211 -0
  133. package/src/components/chart/native/surface/sync.ts +34 -0
  134. package/src/components/chart/native/surface/visibility.ts +48 -0
  135. package/src/components/chart/price-axis-labels.tsx +4 -4
  136. package/src/components/chart/rendering/chart-draw.ts +270 -0
  137. package/src/components/chart/rendering/geometry.ts +95 -0
  138. package/src/components/chart/rendering/pixel-buffer.ts +267 -0
  139. package/src/components/chart/scatter-chart-renderer.test.ts +31 -0
  140. package/src/components/chart/scatter-chart-renderer.ts +223 -0
  141. package/src/components/chart/static/bar-chart-surface.tsx +85 -0
  142. package/src/components/chart/static/chart/bitmap.ts +50 -0
  143. package/src/components/chart/static/chart/surface.test.tsx +51 -0
  144. package/src/components/chart/static/chart/surface.tsx +181 -0
  145. package/src/components/chart/static/multi-line-chart-surface.tsx +180 -0
  146. package/src/components/chart/static/scatter-chart-surface.tsx +83 -0
  147. package/src/components/chart/stock-chart/auto.ts +216 -0
  148. package/src/components/chart/stock-chart/axis-width.ts +146 -0
  149. package/src/components/chart/stock-chart/bitmaps.ts +22 -0
  150. package/src/components/chart/stock-chart/canvas.ts +202 -0
  151. package/src/components/chart/stock-chart/controls.tsx +149 -0
  152. package/src/components/chart/stock-chart/cursor.ts +60 -0
  153. package/src/components/chart/stock-chart/data-runtime.ts +129 -0
  154. package/src/components/chart/stock-chart/footer.ts +248 -0
  155. package/src/components/chart/stock-chart/geometry.ts +160 -0
  156. package/src/components/chart/stock-chart/index.test.ts +500 -0
  157. package/src/components/chart/stock-chart/index.tsx +29 -0
  158. package/src/components/chart/stock-chart/indicators.ts +226 -0
  159. package/src/components/chart/stock-chart/interaction-runtime.ts +44 -0
  160. package/src/components/chart/stock-chart/interactions.ts +300 -0
  161. package/src/components/chart/stock-chart/keyboard.ts +373 -0
  162. package/src/components/chart/stock-chart/layout-metrics.ts +62 -0
  163. package/src/components/chart/stock-chart/layout.tsx +177 -0
  164. package/src/components/chart/stock-chart/native-surfaces.ts +121 -0
  165. package/src/components/chart/stock-chart/presentation.ts +67 -0
  166. package/src/components/chart/stock-chart/renderer-switch.test.tsx +263 -0
  167. package/src/components/chart/stock-chart/rendering/bounds.ts +116 -0
  168. package/src/components/chart/stock-chart/rendering/candidates.ts +145 -0
  169. package/src/components/chart/stock-chart/rendering/data.ts +370 -0
  170. package/src/components/chart/stock-chart/rendering/invalidation.ts +68 -0
  171. package/src/components/chart/stock-chart/rendering/output.ts +304 -0
  172. package/src/components/chart/stock-chart/rendering/planning.ts +122 -0
  173. package/src/components/chart/stock-chart/rendering/projection.test.tsx +98 -0
  174. package/src/components/chart/stock-chart/rendering/projection.ts +112 -0
  175. package/src/components/chart/stock-chart/rendering/state.test.ts +70 -0
  176. package/src/components/chart/stock-chart/rendering/state.ts +283 -0
  177. package/src/components/chart/stock-chart/requests.ts +337 -0
  178. package/src/components/chart/stock-chart/resolution-support.ts +96 -0
  179. package/src/components/chart/stock-chart/resolved/runtime.ts +289 -0
  180. package/src/components/chart/stock-chart/resolved.tsx +345 -0
  181. package/src/components/chart/stock-chart/settings.ts +77 -0
  182. package/src/components/chart/stock-chart/surface-runtime.ts +62 -0
  183. package/src/components/chart/stock-chart/types.ts +25 -0
  184. package/src/components/chart/stock-chart/view.tsx +155 -0
  185. package/src/components/chart/stock-chart/viewport/effects.ts +296 -0
  186. package/src/components/chart/stock-chart/viewport/index.ts +144 -0
  187. package/src/components/chart/stock-chart/viewport/runtime.ts +375 -0
  188. package/src/components/chart/time-axis-label.tsx +4 -3
  189. package/src/components/command-bar/action-runtime.ts +326 -0
  190. package/src/components/command-bar/commands/collection.ts +214 -0
  191. package/src/components/command-bar/commands/direct/index.ts +266 -0
  192. package/src/components/command-bar/commands/direct/runtime.ts +168 -0
  193. package/src/components/command-bar/commands/plugin/actions.ts +171 -0
  194. package/src/components/command-bar/commands/plugin/items.ts +166 -0
  195. package/src/components/command-bar/commands/registry.ts +263 -0
  196. package/src/components/command-bar/helpers.test.ts +13 -21
  197. package/src/components/command-bar/helpers.ts +51 -272
  198. package/src/components/command-bar/keyboard-handlers.ts +306 -0
  199. package/src/components/command-bar/keyboard-shortcuts.ts +204 -0
  200. package/src/components/command-bar/layout-items/current-layout.ts +120 -0
  201. package/src/components/command-bar/layout-items/focused-pane.ts +143 -0
  202. package/src/components/command-bar/layout-items/types.ts +25 -0
  203. package/src/components/command-bar/layout-items/window-mode.ts +72 -0
  204. package/src/components/command-bar/layout-items.ts +93 -0
  205. package/src/components/command-bar/list/model.ts +81 -0
  206. package/src/components/command-bar/list/navigation.ts +125 -0
  207. package/src/components/command-bar/list/view.tsx +316 -0
  208. package/src/components/command-bar/multi-select-picker.tsx +219 -0
  209. package/src/components/command-bar/multi-select-runtime.ts +91 -0
  210. package/src/components/command-bar/pane-actions.ts +124 -0
  211. package/src/components/command-bar/pane-settings.ts +227 -0
  212. package/src/components/command-bar/pane-templates/items.ts +203 -0
  213. package/src/components/command-bar/pane-templates/workflow-route.ts +81 -0
  214. package/src/components/command-bar/pane-templates/workflow.ts +282 -0
  215. package/src/components/command-bar/panel/body.tsx +311 -0
  216. package/src/components/command-bar/panel/index.tsx +196 -0
  217. package/src/components/command-bar/panel/layout.ts +117 -0
  218. package/src/components/command-bar/panel/runtime.ts +280 -0
  219. package/src/components/command-bar/panel/state.ts +138 -0
  220. package/src/components/command-bar/panel/types.ts +78 -0
  221. package/src/components/command-bar/picker-activation.ts +126 -0
  222. package/src/components/command-bar/routes/root/command-items.ts +176 -0
  223. package/src/components/command-bar/routes/root/provider-search.ts +200 -0
  224. package/src/components/command-bar/routes/root/results.ts +233 -0
  225. package/src/components/command-bar/routes/root/runtime.ts +315 -0
  226. package/src/components/command-bar/routes/root/selection.ts +228 -0
  227. package/src/components/command-bar/routes/root/shortcut-feedback.ts +108 -0
  228. package/src/components/command-bar/routes/root/shortcut-items.ts +159 -0
  229. package/src/components/command-bar/routes/root/shortcuts.test.ts +151 -0
  230. package/src/components/command-bar/routes/root/shortcuts.ts +188 -0
  231. package/src/components/command-bar/routes/ticker-search/actions.ts +142 -0
  232. package/src/components/command-bar/routes/ticker-search/results.ts +94 -0
  233. package/src/components/command-bar/routes/ticker-search/route.ts +151 -0
  234. package/src/components/command-bar/routing/actions.ts +299 -0
  235. package/src/components/command-bar/routing/confirm.ts +91 -0
  236. package/src/components/command-bar/routing/effects.ts +82 -0
  237. package/src/components/command-bar/routing/launch-request.ts +59 -0
  238. package/src/components/command-bar/routing/list-state.ts +242 -0
  239. package/src/components/command-bar/routing/navigation-state.ts +147 -0
  240. package/src/components/command-bar/selection-runtime.ts +291 -0
  241. package/src/components/command-bar/surface/environment.ts +124 -0
  242. package/src/components/command-bar/surface/index.test.tsx +864 -0
  243. package/src/components/command-bar/surface/index.tsx +344 -0
  244. package/src/components/command-bar/surface/pane.test.tsx +703 -0
  245. package/src/components/command-bar/surface/portfolio.test.tsx +452 -0
  246. package/src/components/command-bar/surface/test-harness.tsx +403 -0
  247. package/src/components/command-bar/theme-preview.ts +84 -0
  248. package/src/components/command-bar/view-model.test.ts +3 -3
  249. package/src/components/command-bar/view-model.ts +7 -6
  250. package/src/components/command-bar/workflow/alert.ts +164 -0
  251. package/src/components/command-bar/workflow/body.tsx +179 -0
  252. package/src/components/command-bar/workflow/broker.ts +158 -0
  253. package/src/components/command-bar/workflow/builtin.ts +134 -0
  254. package/src/components/command-bar/workflow/collection-actions.ts +257 -0
  255. package/src/components/command-bar/workflow/coordinator.ts +193 -0
  256. package/src/components/command-bar/workflow/field-row.tsx +214 -0
  257. package/src/components/command-bar/workflow/fields.ts +230 -0
  258. package/src/components/command-bar/workflow/ops.test.ts +197 -0
  259. package/src/components/command-bar/workflow/ops.ts +268 -0
  260. package/src/components/command-bar/workflow/route-actions.ts +232 -0
  261. package/src/components/command-bar/workflow/route-builder.ts +56 -0
  262. package/src/components/command-bar/workflow/runtime.ts +234 -0
  263. package/src/components/command-bar/workflow/submit.ts +213 -0
  264. package/src/components/command-bar/workflow/tickers.ts +214 -0
  265. package/src/components/command-bar/workflow/types.ts +127 -0
  266. package/src/components/data-table/stack-view.test.tsx +218 -0
  267. package/src/components/data-table/stack-view.tsx +59 -0
  268. package/src/components/data-table/view.test.tsx +254 -0
  269. package/src/components/data-table/view.tsx +306 -0
  270. package/src/components/feed-data-table/stack-view.test.tsx +148 -0
  271. package/src/components/feed-data-table/stack-view.tsx +359 -0
  272. package/src/components/index.ts +14 -14
  273. package/src/components/input-search-bar.tsx +94 -0
  274. package/src/components/layout/detached-pane-shell.tsx +22 -16
  275. package/src/components/layout/floating-pane.tsx +19 -24
  276. package/src/components/layout/header.tsx +9 -21
  277. package/src/components/layout/pane/content.test.tsx +64 -0
  278. package/src/components/layout/pane/content.tsx +44 -0
  279. package/src/components/layout/pane/cycle-order.test.ts +30 -0
  280. package/src/components/layout/pane/cycle-order.ts +32 -0
  281. package/src/components/layout/pane/footer/index.test.tsx +131 -0
  282. package/src/components/layout/pane/footer/index.tsx +246 -0
  283. package/src/components/layout/pane/footer/model.ts +99 -0
  284. package/src/components/layout/pane/footer/registration.tsx +114 -0
  285. package/src/components/layout/pane/frame.tsx +55 -0
  286. package/src/components/layout/pane/header.test.tsx +39 -0
  287. package/src/components/layout/pane/header.tsx +252 -0
  288. package/src/components/layout/pane/index.tsx +105 -0
  289. package/src/components/layout/pane/keyboard-scroll.test.tsx +146 -0
  290. package/src/components/layout/pane/sizing.ts +66 -0
  291. package/src/components/layout/pane/title.ts +38 -0
  292. package/src/components/layout/shell/action-menu-overlay.tsx +72 -0
  293. package/src/components/layout/shell/active-drag.ts +221 -0
  294. package/src/components/layout/shell/chrome.ts +8 -0
  295. package/src/components/layout/shell/drag/index.ts +432 -0
  296. package/src/components/layout/shell/drag/overlays.tsx +78 -0
  297. package/src/components/layout/shell/drag/runtime.ts +264 -0
  298. package/src/components/layout/shell/index.test.tsx +788 -0
  299. package/src/components/layout/shell/index.tsx +451 -0
  300. package/src/components/layout/shell/layout-state.ts +97 -0
  301. package/src/components/layout/shell/menu.ts +152 -0
  302. package/src/components/layout/shell/native/pointer-runtime.ts +184 -0
  303. package/src/components/layout/shell/native/surfaces.ts +127 -0
  304. package/src/components/layout/shell/native/window-state.ts +208 -0
  305. package/src/components/layout/shell/pane/actions.ts +129 -0
  306. package/src/components/layout/shell/pane/layers.tsx +234 -0
  307. package/src/components/layout/shell/pane/management-shortcuts.ts +114 -0
  308. package/src/components/layout/shell/shortcuts.ts +47 -0
  309. package/src/components/layout/shell/terminal-pointer-runtime.ts +326 -0
  310. package/src/components/layout/shell/window-mode/index.ts +253 -0
  311. package/src/components/layout/shell/window-mode/overlays.tsx +208 -0
  312. package/src/components/layout/status-bar.test.tsx +1 -101
  313. package/src/components/layout/status-bar.tsx +1 -1
  314. package/src/components/layout/window-edit/mode.test.ts +55 -0
  315. package/src/components/layout/window-edit/mode.ts +422 -0
  316. package/src/components/layout/window-edit/presentation.ts +123 -0
  317. package/src/components/layout/window-edit/status.tsx +101 -0
  318. package/src/components/markdown-text.tsx +3 -3
  319. package/src/components/onboarding/onboarding-steps.tsx +248 -0
  320. package/src/components/onboarding/onboarding-wizard.tsx +97 -1035
  321. package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +144 -0
  322. package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +97 -0
  323. package/src/components/onboarding/portfolio-step/broker-sync-panel.tsx +42 -0
  324. package/src/components/onboarding/portfolio-step/choose-panel.tsx +42 -0
  325. package/src/components/onboarding/portfolio-step/index.tsx +70 -0
  326. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +62 -0
  327. package/src/components/onboarding/portfolio-step/types.ts +25 -0
  328. package/src/components/onboarding/portfolio-step/utils.ts +15 -0
  329. package/src/components/onboarding/wizard-broker-sync.ts +230 -0
  330. package/src/components/onboarding/wizard-keyboard.ts +264 -0
  331. package/src/components/onboarding/wizard-model.ts +21 -0
  332. package/src/components/pane-settings-dialog/desktop.tsx +315 -0
  333. package/src/components/pane-settings-dialog/field-dialogs.tsx +213 -0
  334. package/src/components/pane-settings-dialog/tui.tsx +73 -0
  335. package/src/components/pane-settings-dialog/value.ts +36 -0
  336. package/src/components/pane-settings-dialog.tsx +18 -622
  337. package/src/components/pane-template-wizard.tsx +1 -1
  338. package/src/components/price-selector-dialog.tsx +2 -2
  339. package/src/components/price-sparkline/bitmap.ts +150 -0
  340. package/src/components/price-sparkline/index.ts +46 -0
  341. package/src/components/price-sparkline/model.ts +119 -0
  342. package/src/components/price-sparkline/view.tsx +214 -0
  343. package/src/components/quote-flash.ts +103 -0
  344. package/src/components/speedometer-gauge/desktop.tsx +151 -0
  345. package/src/components/speedometer-gauge/index.tsx +44 -0
  346. package/src/components/speedometer-gauge/model.ts +60 -0
  347. package/src/components/speedometer-gauge/terminal.tsx +261 -0
  348. package/src/components/ticker/badge/format.ts +42 -0
  349. package/src/components/ticker/badge/index.tsx +100 -0
  350. package/src/components/ticker/badge/list.tsx +57 -0
  351. package/src/components/ticker/badge/text.test.tsx +223 -0
  352. package/src/components/ticker/badge/text.tsx +156 -0
  353. package/src/components/ticker/list-table-view.tsx +297 -0
  354. package/src/components/ticker/list-table.test.tsx +177 -0
  355. package/src/components/toggle-list.tsx +1 -1
  356. package/src/components/ui/data-table/index.tsx +26 -0
  357. package/src/components/ui/data-table/opentui/index.tsx +467 -0
  358. package/src/components/ui/data-table/opentui/model.test.ts +47 -0
  359. package/src/components/ui/data-table/opentui/model.ts +66 -0
  360. package/src/components/ui/data-table/types.ts +81 -0
  361. package/src/components/ui/index.ts +1 -3
  362. package/src/components/ui/multi-select/dialog.tsx +336 -0
  363. package/src/components/ui/native-select.tsx +1 -1
  364. package/src/components/ui/shortcut-hint.tsx +1 -1
  365. package/src/components/ui/tabs.tsx +11 -5
  366. package/src/components/ui/toggle.tsx +1 -1
  367. package/src/components/ui/ui.test.tsx +3 -75
  368. package/src/components/use-external-link-footer.ts +1 -1
  369. package/src/core/app-services.ts +3 -0
  370. package/src/core/state/app/layout-reducer.ts +196 -0
  371. package/src/core/state/app/layout.ts +406 -0
  372. package/src/core/state/app/state.ts +340 -0
  373. package/src/core/state/app/types.ts +116 -0
  374. package/src/core/state/session-persistence.ts +1 -1
  375. package/src/data/app-persistence.ts +1 -1
  376. package/src/data/config/layout.ts +227 -0
  377. package/src/data/config/store/index.test.ts +500 -0
  378. package/src/data/config/store/index.ts +64 -0
  379. package/src/data/config/store/node.ts +101 -0
  380. package/src/data/config/store/normalize.ts +403 -0
  381. package/src/data/plugin-state-store.ts +3 -3
  382. package/src/data/resource-store.ts +3 -3
  383. package/src/data/session-store.ts +3 -3
  384. package/src/data/sqlite/cache.test.ts +151 -0
  385. package/src/data/sqlite/database.ts +68 -0
  386. package/src/data/sqlite/json.ts +16 -0
  387. package/src/data/sqlite/retry.test.ts +63 -0
  388. package/src/data/ticker-repository.ts +1 -1
  389. package/src/data/ticker-store.ts +2 -2
  390. package/src/market-data/coordinator/auxiliary.ts +252 -0
  391. package/src/market-data/coordinator/chart.ts +141 -0
  392. package/src/market-data/coordinator/entries.ts +129 -0
  393. package/src/market-data/coordinator/events.ts +74 -0
  394. package/src/market-data/coordinator/financials.ts +281 -0
  395. package/src/market-data/coordinator/index.test.ts +840 -0
  396. package/src/market-data/coordinator/index.ts +388 -0
  397. package/src/market-data/coordinator/quotes.ts +329 -0
  398. package/src/market-data/coordinator/subscriptions.test.ts +209 -0
  399. package/src/market-data/hooks.test.tsx +42 -1
  400. package/src/market-data/hooks.tsx +83 -18
  401. package/src/market-data/market/format.test.ts +109 -0
  402. package/src/market-data/market/format.ts +293 -0
  403. package/src/market-data/market/freshness.ts +140 -0
  404. package/src/market-data/market/status.ts +112 -0
  405. package/src/market-data/quotes/contributions.ts +194 -0
  406. package/src/market-data/quotes/freshness.test.ts +150 -0
  407. package/src/market-data/quotes/freshness.ts +36 -0
  408. package/src/market-data/quotes/resolution.test.ts +277 -0
  409. package/src/market-data/quotes/resolution.ts +468 -0
  410. package/src/market-data/quotes/time.test.ts +34 -0
  411. package/src/market-data/quotes/time.ts +59 -0
  412. package/src/market-data/request-types.ts +10 -3
  413. package/src/market-data/result-types.ts +3 -3
  414. package/src/market-data/selectors.ts +2 -2
  415. package/src/news/aggregator.ts +29 -219
  416. package/src/news/news-model.ts +208 -0
  417. package/src/news/ticker-symbols.ts +1 -1
  418. package/src/news/types.ts +3 -3
  419. package/src/plugins/builtin/account-management/footer.ts +39 -0
  420. package/src/plugins/builtin/account-management/form-components.tsx +162 -0
  421. package/src/plugins/builtin/account-management/keyboard.ts +90 -0
  422. package/src/plugins/builtin/account-management/model.ts +97 -0
  423. package/src/plugins/builtin/account-management/pane.tsx +389 -0
  424. package/src/plugins/builtin/account-management/password-dialog.tsx +125 -0
  425. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +6 -6
  426. package/src/plugins/builtin/ai/ask-ai.test.tsx +305 -0
  427. package/src/plugins/builtin/ai/index.tsx +4 -4
  428. package/src/plugins/builtin/ai/providers.ts +1 -1
  429. package/src/plugins/builtin/ai/runner.ts +1 -0
  430. package/src/plugins/builtin/ai/screener/action-bar.tsx +58 -0
  431. package/src/plugins/builtin/ai/screener/contract.test.ts +117 -0
  432. package/src/plugins/builtin/ai/screener/contract.ts +174 -0
  433. package/src/plugins/builtin/ai/screener/editor-runtime.ts +110 -0
  434. package/src/plugins/builtin/ai/screener/editor.tsx +125 -0
  435. package/src/plugins/builtin/ai/screener/footer.ts +93 -0
  436. package/src/plugins/builtin/ai/screener/keyboard.ts +90 -0
  437. package/src/plugins/builtin/ai/screener/market-runtime.ts +99 -0
  438. package/src/plugins/builtin/ai/screener/model.ts +107 -0
  439. package/src/plugins/builtin/ai/screener/pane.test.tsx +348 -0
  440. package/src/plugins/builtin/ai/screener/pane.tsx +412 -0
  441. package/src/plugins/builtin/ai/screener/results-view.tsx +169 -0
  442. package/src/plugins/builtin/ai/screener/results.ts +139 -0
  443. package/src/plugins/builtin/ai/screener/runner.ts +144 -0
  444. package/src/plugins/builtin/ai/screener/tabs-bar.tsx +65 -0
  445. package/src/plugins/builtin/ai/settings.ts +3 -3
  446. package/src/plugins/builtin/ai/ticker-context.ts +1 -1
  447. package/src/plugins/builtin/alerts/command.ts +86 -0
  448. package/src/plugins/builtin/alerts/constants.ts +3 -0
  449. package/src/plugins/builtin/alerts/format.ts +68 -0
  450. package/src/plugins/builtin/alerts/index.test.tsx +5 -4
  451. package/src/plugins/builtin/alerts/index.tsx +18 -473
  452. package/src/plugins/builtin/alerts/pane.tsx +304 -0
  453. package/src/plugins/builtin/alerts/quotes.ts +57 -0
  454. package/src/plugins/builtin/alerts/storage.ts +20 -0
  455. package/src/plugins/builtin/analytics/broker-performance.ts +142 -0
  456. package/src/plugins/builtin/analytics/display.ts +44 -0
  457. package/src/plugins/builtin/analytics/index.test.tsx +144 -5
  458. package/src/plugins/builtin/analytics/index.tsx +108 -468
  459. package/src/plugins/builtin/analytics/metrics.ts +1 -1
  460. package/src/plugins/builtin/analytics/pane-model.ts +297 -0
  461. package/src/plugins/builtin/analytics/portfolio-selection.ts +10 -0
  462. package/src/plugins/builtin/analytics/sector-model.ts +172 -0
  463. package/src/plugins/builtin/analytics/view.tsx +207 -0
  464. package/src/plugins/builtin/broker-manager/detail.tsx +232 -0
  465. package/src/plugins/builtin/broker-manager/footer.ts +62 -0
  466. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -3
  467. package/src/plugins/builtin/broker-manager/index.tsx +125 -527
  468. package/src/plugins/builtin/broker-manager/keyboard.ts +104 -0
  469. package/src/plugins/builtin/broker-manager/model.test.ts +39 -1
  470. package/src/plugins/builtin/broker-manager/model.ts +14 -8
  471. package/src/plugins/builtin/broker-manager/pane-actions.tsx +181 -0
  472. package/src/plugins/builtin/broker-manager/table.ts +86 -0
  473. package/src/plugins/builtin/buildout/cells.tsx +203 -0
  474. package/src/plugins/builtin/buildout/data-runtime.ts +238 -0
  475. package/src/plugins/builtin/buildout/detail/company.tsx +219 -0
  476. package/src/plugins/builtin/buildout/detail/index.tsx +105 -0
  477. package/src/plugins/builtin/buildout/detail/intel.tsx +61 -0
  478. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +80 -0
  479. package/src/plugins/builtin/buildout/detail/site.tsx +142 -0
  480. package/src/plugins/builtin/buildout/detail/ui.tsx +355 -0
  481. package/src/plugins/builtin/buildout/detail/values.ts +67 -0
  482. package/src/plugins/builtin/buildout/format.ts +163 -0
  483. package/src/plugins/builtin/buildout/model/index.ts +135 -0
  484. package/src/plugins/builtin/buildout/model/normalizers.ts +396 -0
  485. package/src/plugins/builtin/buildout/model/types.ts +318 -0
  486. package/src/plugins/builtin/buildout/pane/header.tsx +55 -0
  487. package/src/plugins/builtin/buildout/pane/index.tsx +405 -0
  488. package/src/plugins/builtin/buildout/pane/status.tsx +84 -0
  489. package/src/plugins/builtin/buildout/table-model.ts +367 -0
  490. package/src/plugins/builtin/changelog/index.tsx +370 -0
  491. package/src/plugins/builtin/chat/channels.ts +157 -0
  492. package/src/plugins/builtin/chat/chat.test.tsx +1048 -0
  493. package/src/plugins/builtin/chat/composer-commands.test.ts +30 -0
  494. package/src/plugins/builtin/chat/composer-commands.ts +34 -0
  495. package/src/plugins/builtin/chat/content/channel-navigation.ts +157 -0
  496. package/src/plugins/builtin/chat/content/composer-runtime.ts +251 -0
  497. package/src/plugins/builtin/chat/content/composer.tsx +148 -0
  498. package/src/plugins/builtin/chat/content/index.tsx +407 -0
  499. package/src/plugins/builtin/chat/content/layout-metrics.ts +72 -0
  500. package/src/plugins/builtin/chat/content/scroll.ts +278 -0
  501. package/src/plugins/builtin/chat/content/selection-runtime.ts +66 -0
  502. package/src/plugins/builtin/chat/content/shortcuts.ts +212 -0
  503. package/src/plugins/builtin/chat/content/snapshot.ts +154 -0
  504. package/src/plugins/builtin/chat/content/transcript.tsx +155 -0
  505. package/src/plugins/builtin/chat/content/types.ts +26 -0
  506. package/src/plugins/builtin/chat/content/user-map.ts +21 -0
  507. package/src/plugins/builtin/chat/controller/channel-actions.ts +87 -0
  508. package/src/plugins/builtin/chat/controller/channels.ts +159 -0
  509. package/src/plugins/builtin/chat/controller/connections.ts +90 -0
  510. package/src/plugins/builtin/chat/controller/draft-sync.ts +41 -0
  511. package/src/plugins/builtin/chat/controller/fetch.ts +83 -0
  512. package/src/plugins/builtin/chat/controller/index.ts +488 -0
  513. package/src/plugins/builtin/chat/controller/lifecycle.ts +141 -0
  514. package/src/plugins/builtin/chat/controller/message-loading.ts +97 -0
  515. package/src/plugins/builtin/chat/controller/messages.ts +173 -0
  516. package/src/plugins/builtin/chat/controller/notifications.ts +56 -0
  517. package/src/plugins/builtin/chat/controller/persistence.ts +166 -0
  518. package/src/plugins/builtin/chat/controller/realtime.ts +91 -0
  519. package/src/plugins/builtin/chat/controller/send.ts +89 -0
  520. package/src/plugins/builtin/chat/controller/session-runtime.ts +156 -0
  521. package/src/plugins/builtin/chat/controller/session.ts +39 -0
  522. package/src/plugins/builtin/chat/controller/state.ts +162 -0
  523. package/src/plugins/builtin/chat/controller/storage.ts +136 -0
  524. package/src/plugins/builtin/chat/controller/utils.ts +75 -0
  525. package/src/plugins/builtin/chat/controller/view.ts +96 -0
  526. package/src/plugins/builtin/chat/controller.test.ts +1302 -0
  527. package/src/plugins/builtin/chat/index.tsx +4 -0
  528. package/src/plugins/builtin/chat/layout.ts +330 -0
  529. package/src/plugins/builtin/chat/message/action-chip.tsx +38 -0
  530. package/src/plugins/builtin/chat/message/desktop.tsx +158 -0
  531. package/src/plugins/builtin/chat/message/inline-tokens.tsx +118 -0
  532. package/src/plugins/builtin/chat/message/profile-popover.tsx +72 -0
  533. package/src/plugins/builtin/chat/message/render-state.ts +67 -0
  534. package/src/plugins/builtin/chat/message/terminal.tsx +132 -0
  535. package/src/plugins/builtin/chat/message/types.ts +18 -0
  536. package/src/plugins/builtin/chat/pane.tsx +100 -0
  537. package/src/plugins/builtin/chat/profile-popover.ts +49 -0
  538. package/src/plugins/builtin/chat/sidebar.test.tsx +455 -0
  539. package/src/plugins/builtin/chat/sidebar.tsx +296 -0
  540. package/src/plugins/builtin/chat/status-widget.tsx +103 -0
  541. package/src/plugins/builtin/chat/test-harness.tsx +212 -0
  542. package/src/plugins/builtin/cloud/auth-actions.tsx +72 -0
  543. package/src/plugins/builtin/cloud/auth-commands.ts +115 -0
  544. package/src/plugins/builtin/cloud/index.tsx +11 -0
  545. package/src/plugins/builtin/cloud/plugin.tsx +185 -0
  546. package/src/plugins/builtin/cloud-tweets/footer.ts +40 -0
  547. package/src/plugins/builtin/cloud-tweets/index.ts +1 -0
  548. package/src/plugins/builtin/cloud-tweets/keyboard.ts +65 -0
  549. package/src/plugins/builtin/cloud-tweets/model.ts +257 -0
  550. package/src/plugins/builtin/cloud-tweets/pane.tsx +318 -0
  551. package/src/plugins/builtin/cloud-tweets/registration.ts +87 -0
  552. package/src/plugins/builtin/cloud-tweets/search-bar.tsx +49 -0
  553. package/src/plugins/builtin/cloud-tweets/table.tsx +331 -0
  554. package/src/plugins/builtin/comparison-chart/index.test.tsx +645 -0
  555. package/src/plugins/builtin/comparison-chart/index.tsx +194 -0
  556. package/src/plugins/builtin/congress-trades/detail.tsx +154 -0
  557. package/src/plugins/builtin/congress-trades/footer.ts +67 -0
  558. package/src/plugins/builtin/congress-trades/keyboard.ts +112 -0
  559. package/src/plugins/builtin/congress-trades/model.ts +224 -0
  560. package/src/plugins/builtin/congress-trades/pane.tsx +331 -0
  561. package/src/plugins/builtin/congress-trades/table.tsx +84 -0
  562. package/src/plugins/builtin/correlation/compute.ts +0 -7
  563. package/src/plugins/builtin/correlation/index.test.tsx +5 -5
  564. package/src/plugins/builtin/correlation/index.tsx +36 -208
  565. package/src/plugins/builtin/correlation/matrix/model.ts +155 -0
  566. package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +56 -0
  567. package/src/plugins/builtin/correlation/relationship/controls.tsx +53 -0
  568. package/src/plugins/builtin/correlation/relationship/history.ts +72 -0
  569. package/src/plugins/builtin/correlation/relationship/model.ts +279 -0
  570. package/src/plugins/builtin/correlation/relationship/pane.tsx +359 -0
  571. package/src/plugins/builtin/correlation/relationship/view-model.ts +120 -0
  572. package/src/plugins/builtin/correlation/settings.ts +6 -6
  573. package/src/plugins/builtin/debug/index.tsx +365 -0
  574. package/src/plugins/builtin/earnings/data/cache.test.ts +94 -0
  575. package/src/plugins/builtin/earnings/data/cache.ts +145 -0
  576. package/src/plugins/builtin/earnings/data/data.test.ts +90 -0
  577. package/src/plugins/builtin/earnings/data/data.ts +115 -0
  578. package/src/plugins/builtin/earnings/index.test.ts +12 -0
  579. package/src/plugins/builtin/earnings/index.tsx +74 -266
  580. package/src/plugins/builtin/earnings/model.ts +82 -0
  581. package/src/plugins/builtin/earnings/table.ts +206 -0
  582. package/src/plugins/builtin/econ/calendar-model.test.ts +48 -0
  583. package/src/plugins/builtin/econ/calendar-model.ts +217 -0
  584. package/src/plugins/builtin/econ/calendar-source.ts +1 -1
  585. package/src/plugins/builtin/econ/detail-view.tsx +319 -0
  586. package/src/plugins/builtin/econ/fred-cache.test.ts +58 -0
  587. package/src/plugins/builtin/econ/fred-cache.ts +106 -0
  588. package/src/plugins/builtin/econ/fred-series-map.ts +0 -4
  589. package/src/plugins/builtin/econ/index.tsx +46 -474
  590. package/src/plugins/builtin/fear-greed/cache.test.ts +75 -0
  591. package/src/plugins/builtin/fear-greed/cache.ts +142 -0
  592. package/src/plugins/builtin/fear-greed/charts.tsx +305 -0
  593. package/src/plugins/builtin/fear-greed/data.test.ts +72 -0
  594. package/src/plugins/builtin/fear-greed/data.ts +370 -0
  595. package/src/plugins/builtin/fear-greed/format.ts +96 -0
  596. package/src/plugins/builtin/fear-greed/index.tsx +13 -555
  597. package/src/plugins/builtin/fear-greed/pane.tsx +169 -0
  598. package/src/plugins/builtin/fx-matrix/index.tsx +1 -1
  599. package/src/plugins/builtin/fx-matrix/pairs.ts +0 -22
  600. package/src/plugins/builtin/help/components.tsx +92 -0
  601. package/src/plugins/builtin/help/index.test.tsx +167 -0
  602. package/src/plugins/builtin/help/index.tsx +256 -0
  603. package/src/plugins/builtin/help/shortcut-model.ts +145 -0
  604. package/src/plugins/builtin/holders/format.ts +44 -0
  605. package/src/plugins/builtin/holders/index.test.ts +2 -76
  606. package/src/plugins/builtin/holders/index.tsx +3 -959
  607. package/src/plugins/builtin/holders/pane.tsx +331 -0
  608. package/src/plugins/builtin/holders/table-model.ts +84 -0
  609. package/src/plugins/builtin/holders/thirteenf-match.ts +76 -0
  610. package/src/plugins/builtin/holders/treemap-layout.ts +231 -0
  611. package/src/plugins/builtin/holders/treemap.tsx +318 -0
  612. package/src/plugins/builtin/holders/types.ts +51 -0
  613. package/src/plugins/builtin/insider/index.tsx +5 -5
  614. package/src/plugins/builtin/layout-manager/index.tsx +247 -0
  615. package/src/plugins/builtin/market-movers/index.tsx +44 -282
  616. package/src/plugins/builtin/market-movers/model.ts +146 -0
  617. package/src/plugins/builtin/market-movers/screener.test.ts +31 -1
  618. package/src/plugins/builtin/market-movers/screener.ts +106 -12
  619. package/src/plugins/builtin/market-movers/table.ts +95 -0
  620. package/src/plugins/builtin/news/index.tsx +194 -0
  621. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +98 -0
  622. package/src/plugins/builtin/news/wire/breaking/notifications.ts +110 -0
  623. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +156 -0
  624. package/src/plugins/builtin/news/wire/breaking/pane.tsx +68 -0
  625. package/src/plugins/builtin/news/wire/categories.ts +114 -0
  626. package/src/plugins/builtin/news/wire/default-feeds.ts +3 -0
  627. package/src/plugins/builtin/news/wire/feed-config.test.ts +90 -0
  628. package/src/plugins/builtin/news/wire/feed-config.ts +216 -0
  629. package/src/plugins/builtin/news/wire/index.tsx +129 -0
  630. package/src/plugins/builtin/news/wire/industry-pane.tsx +126 -0
  631. package/src/plugins/builtin/news/wire/news/detail-view.tsx +306 -0
  632. package/src/plugins/builtin/news/wire/news/footer.ts +77 -0
  633. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +98 -0
  634. package/src/plugins/builtin/news/wire/news/query-presets.ts +40 -0
  635. package/src/plugins/builtin/news/wire/news/table.test.tsx +157 -0
  636. package/src/plugins/builtin/news/wire/news/table.tsx +270 -0
  637. package/src/plugins/builtin/news/wire/persisted-articles.ts +98 -0
  638. package/src/plugins/builtin/news/wire/read-state.ts +76 -0
  639. package/src/plugins/builtin/news/wire/rss/parser.test.ts +116 -0
  640. package/src/plugins/builtin/news/wire/rss/parser.ts +213 -0
  641. package/src/plugins/builtin/news/wire/rss/source.test.ts +104 -0
  642. package/src/plugins/builtin/news/wire/rss/source.ts +181 -0
  643. package/src/plugins/builtin/notes/index.tsx +49 -0
  644. package/src/plugins/builtin/notes/markdown-note-preview.tsx +39 -0
  645. package/src/plugins/builtin/notes/model.ts +36 -0
  646. package/src/plugins/builtin/notes/quick-notes-pane.tsx +334 -0
  647. package/src/plugins/builtin/notes/text-state.ts +11 -0
  648. package/src/plugins/builtin/notes/ticker-notes-tab.tsx +112 -0
  649. package/src/plugins/builtin/options/index.tsx +44 -0
  650. package/src/plugins/builtin/options/table.ts +209 -0
  651. package/src/plugins/builtin/options/types.ts +33 -0
  652. package/src/plugins/builtin/options/view.test.tsx +185 -0
  653. package/src/plugins/builtin/options/view.tsx +264 -0
  654. package/src/plugins/builtin/plugin-groups.ts +25 -5
  655. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +50 -0
  656. package/src/plugins/builtin/portfolio-list/account-metrics.ts +46 -0
  657. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +224 -0
  658. package/src/plugins/builtin/portfolio-list/cli/render.ts +203 -0
  659. package/src/plugins/builtin/portfolio-list/cli/shared.ts +21 -0
  660. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +224 -0
  661. package/src/plugins/builtin/portfolio-list/column-values.ts +284 -0
  662. package/src/plugins/builtin/portfolio-list/command-bar.ts +1 -1
  663. package/src/plugins/builtin/portfolio-list/header.tsx +7 -98
  664. package/src/plugins/builtin/portfolio-list/index.test.ts +133 -0
  665. package/src/plugins/builtin/portfolio-list/index.tsx +2 -1
  666. package/src/plugins/builtin/portfolio-list/metrics.test.ts +242 -0
  667. package/src/plugins/builtin/portfolio-list/metrics.ts +5 -462
  668. package/src/plugins/builtin/portfolio-list/pane/data.ts +171 -0
  669. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1267 -0
  670. package/src/plugins/builtin/portfolio-list/pane/index.tsx +385 -0
  671. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +159 -0
  672. package/src/plugins/builtin/portfolio-list/position-metrics.ts +123 -0
  673. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +326 -0
  674. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +135 -0
  675. package/src/plugins/builtin/portfolio-list/settings.ts +5 -3
  676. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +126 -0
  677. package/src/plugins/builtin/portfolio-list/summary/index.tsx +392 -0
  678. package/src/plugins/builtin/portfolio-list/summary/totals.ts +86 -0
  679. package/src/plugins/builtin/portfolio-list/table.tsx +1 -1
  680. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  681. package/src/plugins/builtin/research/analyst-pane.tsx +383 -0
  682. package/src/plugins/builtin/research/corporate-actions-pane.tsx +413 -0
  683. package/src/plugins/builtin/research/index.test.ts +248 -2
  684. package/src/plugins/builtin/research/index.tsx +30 -760
  685. package/src/plugins/builtin/research/relative-valuation-pane.tsx +189 -0
  686. package/src/plugins/builtin/sec/filing-display.tsx +84 -0
  687. package/src/plugins/builtin/sec/index.tsx +342 -0
  688. package/src/plugins/builtin/sectors/index.tsx +27 -234
  689. package/src/plugins/builtin/sectors/sector-data.ts +0 -2
  690. package/src/plugins/builtin/sectors/sector-model.ts +221 -0
  691. package/src/plugins/builtin/shared/table-pane.ts +31 -0
  692. package/src/plugins/builtin/shared/ticker-request.ts +70 -0
  693. package/src/plugins/builtin/shared/ticker-surface.test.ts +57 -0
  694. package/src/plugins/builtin/shared/ticker-surface.ts +113 -0
  695. package/src/plugins/builtin/thirteenf/api.test.ts +58 -0
  696. package/src/plugins/builtin/thirteenf/api.ts +375 -0
  697. package/src/plugins/builtin/thirteenf/data.test.ts +95 -0
  698. package/src/plugins/builtin/thirteenf/data.ts +210 -0
  699. package/src/plugins/builtin/thirteenf/format.ts +55 -0
  700. package/src/plugins/builtin/thirteenf/index.tsx +80 -0
  701. package/src/plugins/builtin/thirteenf/model.test.ts +156 -0
  702. package/src/plugins/builtin/thirteenf/model.ts +618 -0
  703. package/src/plugins/builtin/thirteenf/pane.test.tsx +328 -0
  704. package/src/plugins/builtin/thirteenf/pane.tsx +826 -0
  705. package/src/plugins/builtin/thirteenf/table.ts +212 -0
  706. package/src/plugins/builtin/thirteenf/types.ts +187 -0
  707. package/src/plugins/builtin/ticker-detail/chart-pane.tsx +91 -0
  708. package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +282 -0
  709. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -5
  710. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +158 -0
  711. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/data.ts +62 -0
  712. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +234 -0
  713. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +44 -0
  714. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +51 -0
  715. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +135 -0
  716. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +192 -0
  717. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +172 -0
  718. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +110 -0
  719. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +203 -0
  720. package/src/plugins/builtin/ticker-detail/financials/model.test.ts +84 -0
  721. package/src/plugins/builtin/ticker-detail/financials/model.ts +364 -0
  722. package/src/plugins/builtin/ticker-detail/financials/pane.tsx +27 -0
  723. package/src/plugins/builtin/ticker-detail/financials/schema.ts +392 -0
  724. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +438 -0
  725. package/src/plugins/builtin/ticker-detail/index.test.tsx +780 -0
  726. package/src/plugins/builtin/ticker-detail/index.tsx +174 -17
  727. package/src/plugins/builtin/ticker-detail/overview/components.tsx +240 -0
  728. package/src/plugins/builtin/ticker-detail/overview/model.ts +168 -0
  729. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +14 -0
  730. package/src/plugins/builtin/ticker-detail/overview/types.ts +17 -0
  731. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +27 -392
  732. package/src/plugins/builtin/ticker-detail/pane.tsx +24 -136
  733. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +330 -0
  734. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +292 -0
  735. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +180 -0
  736. package/src/plugins/builtin/ticker-detail/settings.ts +39 -33
  737. package/src/plugins/builtin/world-indices/footer.ts +28 -0
  738. package/src/plugins/builtin/world-indices/index.tsx +25 -218
  739. package/src/plugins/builtin/world-indices/model.ts +120 -0
  740. package/src/plugins/builtin/world-indices/table.tsx +96 -0
  741. package/src/plugins/builtin/yahoo/index.ts +72 -0
  742. package/src/plugins/builtin/yield-curve/index.tsx +5 -5
  743. package/src/plugins/builtin/yield-curve/treasury-data.ts +1 -1
  744. package/src/plugins/catalog-ui.ts +7 -9
  745. package/src/plugins/catalog.ts +1 -1
  746. package/src/plugins/ibkr/account-cache.ts +2 -10
  747. package/src/plugins/ibkr/broker-adapter.ts +10 -4
  748. package/src/plugins/ibkr/config.ts +5 -24
  749. package/src/plugins/ibkr/dialogs.tsx +0 -1
  750. package/src/plugins/ibkr/flex/index.test.ts +236 -0
  751. package/src/plugins/ibkr/flex/index.ts +337 -0
  752. package/src/plugins/ibkr/flex/statement-client.ts +181 -0
  753. package/src/plugins/ibkr/gateway/account-loaders.ts +266 -0
  754. package/src/plugins/ibkr/gateway/account-summary.ts +163 -0
  755. package/src/plugins/ibkr/gateway/client-lock.ts +154 -0
  756. package/src/plugins/ibkr/gateway/connection-events.ts +114 -0
  757. package/src/plugins/ibkr/gateway/connection.ts +152 -0
  758. package/src/plugins/ibkr/gateway/contracts.ts +132 -0
  759. package/src/plugins/ibkr/gateway/fundamentals.ts +83 -0
  760. package/src/plugins/ibkr/gateway/helpers.ts +33 -0
  761. package/src/plugins/ibkr/gateway/history-requests.ts +158 -0
  762. package/src/plugins/ibkr/gateway/history.ts +120 -0
  763. package/src/plugins/ibkr/gateway/instrument-lookup.ts +117 -0
  764. package/src/plugins/ibkr/gateway/market-data.ts +338 -0
  765. package/src/plugins/ibkr/gateway/order-execution.ts +218 -0
  766. package/src/plugins/ibkr/gateway/orders.ts +63 -0
  767. package/src/plugins/ibkr/gateway/price-normalization.test.ts +44 -0
  768. package/src/plugins/ibkr/gateway/price-normalization.ts +50 -0
  769. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +203 -0
  770. package/src/plugins/ibkr/gateway/quote-stream.ts +171 -0
  771. package/src/plugins/ibkr/gateway/selection.ts +51 -0
  772. package/src/plugins/ibkr/gateway/service/index.test.ts +614 -0
  773. package/src/plugins/ibkr/gateway/service/index.ts +432 -0
  774. package/src/plugins/ibkr/gateway/service/lifecycle.ts +191 -0
  775. package/src/plugins/ibkr/gateway/service/manager.ts +52 -0
  776. package/src/plugins/ibkr/gateway/service/market-data.ts +48 -0
  777. package/src/plugins/ibkr/gateway/service/native.ts +543 -0
  778. package/src/plugins/ibkr/gateway/timeouts.ts +13 -0
  779. package/src/plugins/ibkr/gateway/types.ts +27 -0
  780. package/src/plugins/ibkr/hash.ts +8 -0
  781. package/src/plugins/ibkr/index.test.tsx +1 -12
  782. package/src/plugins/ibkr/index.tsx +8 -9
  783. package/src/plugins/ibkr/instance-selection.test.ts +2 -2
  784. package/src/plugins/ibkr/instance-selection.ts +1 -1
  785. package/src/plugins/ibkr/portfolio-performance.test.ts +32 -0
  786. package/src/plugins/ibkr/portfolio-performance.ts +129 -0
  787. package/src/plugins/ibkr/trade/badge.tsx +28 -0
  788. package/src/plugins/ibkr/trade/dialogs.tsx +44 -0
  789. package/src/plugins/ibkr/trade/preview-panel.tsx +130 -0
  790. package/src/plugins/ibkr/trade/tab/actions.tsx +443 -0
  791. package/src/plugins/ibkr/trade/tab/field-editors.tsx +79 -0
  792. package/src/plugins/ibkr/trade/tab/footer.ts +67 -0
  793. package/src/plugins/ibkr/trade/tab/header.tsx +167 -0
  794. package/src/plugins/ibkr/trade/tab/index.test.tsx +178 -0
  795. package/src/plugins/ibkr/trade/tab/index.tsx +302 -0
  796. package/src/plugins/ibkr/trade/tab/model.ts +159 -0
  797. package/src/plugins/ibkr/trade/tab/order-request.ts +64 -0
  798. package/src/plugins/ibkr/trade/tab/shortcuts.ts +89 -0
  799. package/src/plugins/ibkr/trade/tab/state-sync.ts +86 -0
  800. package/src/plugins/ibkr/trade/ticket-panel.tsx +301 -0
  801. package/src/plugins/ibkr/trade/utils.test.ts +175 -0
  802. package/src/plugins/ibkr/trade/utils.ts +150 -0
  803. package/src/plugins/ibkr/trading/pane-view.tsx +175 -0
  804. package/src/plugins/ibkr/trading/pane.tsx +332 -0
  805. package/src/plugins/ibkr/trading/state.test.ts +109 -0
  806. package/src/plugins/ibkr/trading/state.ts +335 -0
  807. package/src/plugins/pane-manager/dock-tree.ts +282 -0
  808. package/src/plugins/pane-manager/docking.ts +202 -0
  809. package/src/plugins/pane-manager/floating-actions.ts +158 -0
  810. package/src/plugins/pane-manager/floating.ts +76 -0
  811. package/src/plugins/pane-manager/gridlock-inference.ts +126 -0
  812. package/src/plugins/pane-manager/gridlock.ts +34 -0
  813. package/src/plugins/pane-manager/layout-state.ts +160 -0
  814. package/src/plugins/pane-manager/queries.ts +75 -0
  815. package/src/plugins/pane-manager/types.ts +24 -0
  816. package/src/plugins/pane-manager.test.ts +79 -4
  817. package/src/plugins/pane-manager.ts +57 -1061
  818. package/src/plugins/prediction-markets/chart.test.tsx +2 -2
  819. package/src/plugins/prediction-markets/controller/catalog.ts +317 -0
  820. package/src/plugins/prediction-markets/controller/data.ts +171 -0
  821. package/src/plugins/prediction-markets/controller/detail.ts +243 -0
  822. package/src/plugins/prediction-markets/controller/effects.ts +253 -0
  823. package/src/plugins/prediction-markets/controller/index.ts +374 -0
  824. package/src/plugins/prediction-markets/controller/keyboard.ts +220 -0
  825. package/src/plugins/prediction-markets/controller/live-updates.ts +85 -0
  826. package/src/plugins/prediction-markets/controller/selection.ts +59 -0
  827. package/src/plugins/prediction-markets/controller/status.ts +104 -0
  828. package/src/plugins/prediction-markets/detail/outcomes.tsx +1 -1
  829. package/src/plugins/prediction-markets/metrics.ts +4 -14
  830. package/src/plugins/prediction-markets/navigation.ts +0 -13
  831. package/src/plugins/prediction-markets/pane.test.tsx +2 -198
  832. package/src/plugins/prediction-markets/plugin.test.tsx +2 -2
  833. package/src/plugins/prediction-markets/services/fetch.ts +1 -5
  834. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +299 -0
  835. package/src/plugins/prediction-markets/services/kalshi/normalize.ts +209 -0
  836. package/src/plugins/prediction-markets/services/kalshi/types.ts +89 -0
  837. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +134 -0
  838. package/src/plugins/prediction-markets/services/polymarket/detail.ts +307 -0
  839. package/src/plugins/prediction-markets/services/polymarket/normalize.ts +239 -0
  840. package/src/plugins/prediction-markets/services/polymarket/types.ts +66 -0
  841. package/src/plugins/prediction-markets/services/polymarket/ws.ts +128 -0
  842. package/src/plugins/prediction-markets/test-helpers.tsx +12 -76
  843. package/src/plugins/prediction-markets/types.ts +2 -2
  844. package/src/plugins/registry/context-menu.ts +36 -0
  845. package/src/plugins/registry/context.ts +196 -0
  846. package/src/plugins/registry/contributions.ts +174 -0
  847. package/src/plugins/registry/index.test.ts +362 -0
  848. package/src/plugins/registry/index.ts +474 -0
  849. package/src/plugins/registry/pane-settings.ts +105 -0
  850. package/src/plugins/registry/plugin-state.ts +123 -0
  851. package/src/plugins/registry/shared.ts +34 -0
  852. package/src/plugins/registry/slots.ts +70 -0
  853. package/src/plugins/runtime/context.tsx +115 -0
  854. package/src/plugins/runtime/index.test.tsx +328 -0
  855. package/src/plugins/runtime/index.tsx +111 -0
  856. package/src/plugins/runtime/state.ts +275 -0
  857. package/src/plugins/ticker-navigation.test.ts +23 -2
  858. package/src/plugins/ticker-navigation.ts +9 -3
  859. package/src/react/input.test.ts +29 -0
  860. package/src/react/input.ts +9 -0
  861. package/src/renderers/electrobun/bun/application-menu/click.test.ts +50 -0
  862. package/src/renderers/electrobun/bun/application-menu/click.ts +58 -0
  863. package/src/renderers/electrobun/bun/application-menu/index.ts +123 -0
  864. package/src/renderers/electrobun/bun/context-menu/click.test.ts +67 -0
  865. package/src/renderers/electrobun/bun/context-menu/normalize.test.ts +52 -0
  866. package/src/renderers/electrobun/bun/context-menu/normalize.ts +67 -0
  867. package/src/renderers/electrobun/bun/core-capabilities.ts +3 -0
  868. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +163 -0
  869. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +93 -0
  870. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +263 -0
  871. package/src/renderers/electrobun/bun/desktop/host-requests.ts +114 -0
  872. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +56 -0
  873. package/src/renderers/electrobun/bun/desktop/initialization.ts +139 -0
  874. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +92 -0
  875. package/src/renderers/electrobun/bun/desktop/relaunch.ts +48 -0
  876. package/src/renderers/electrobun/bun/desktop/rpc-registry.ts +43 -0
  877. package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +76 -0
  878. package/src/renderers/electrobun/bun/desktop/update.ts +162 -0
  879. package/src/renderers/electrobun/bun/desktop/window-events.ts +49 -0
  880. package/src/renderers/electrobun/bun/desktop/workspace/index.test.ts +63 -0
  881. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +180 -0
  882. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +97 -0
  883. package/src/renderers/electrobun/bun/index.ts +165 -1001
  884. package/src/renderers/electrobun/bun/tui-entry.ts +6 -0
  885. package/src/renderers/electrobun/bun/window/focus.ts +37 -0
  886. package/src/renderers/electrobun/bun/window/frame.test.ts +40 -0
  887. package/src/renderers/electrobun/shared/protocol.ts +1 -1
  888. package/src/renderers/electrobun/view/app-services.ts +4 -375
  889. package/src/renderers/electrobun/view/build-assets.ts +117 -0
  890. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +398 -0
  891. package/src/renderers/electrobun/view/config-host.ts +1 -1
  892. package/src/renderers/electrobun/view/context-menu.test.ts +10 -4
  893. package/src/renderers/electrobun/view/context-menu.ts +1 -1
  894. package/src/renderers/electrobun/view/data-table/dom.ts +211 -0
  895. package/src/renderers/electrobun/view/data-table/index.tsx +358 -0
  896. package/src/renderers/electrobun/view/data-table/row.tsx +335 -0
  897. package/src/renderers/electrobun/view/desktop/control-styles.ts +77 -0
  898. package/src/renderers/electrobun/view/desktop/controls.tsx +421 -0
  899. package/src/renderers/electrobun/view/desktop/list-view.tsx +200 -0
  900. package/src/renderers/electrobun/view/desktop/window/bridge.ts +70 -0
  901. package/src/renderers/electrobun/view/desktop/window/snapshot.test.ts +57 -0
  902. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +41 -0
  903. package/src/renderers/electrobun/view/fatal-screen.tsx +2 -2
  904. package/src/renderers/electrobun/view/host/box.tsx +160 -0
  905. package/src/renderers/electrobun/view/host/chart-surface.tsx +142 -0
  906. package/src/renderers/electrobun/view/host/input.tsx +294 -0
  907. package/src/renderers/electrobun/view/host/mouse.ts +139 -0
  908. package/src/renderers/electrobun/view/host/native.ts +49 -0
  909. package/src/renderers/electrobun/view/host/scroll-box.tsx +276 -0
  910. package/src/renderers/electrobun/view/host/style.ts +124 -0
  911. package/src/renderers/electrobun/view/host/tabs.tsx +255 -0
  912. package/src/renderers/electrobun/view/host/text.tsx +121 -0
  913. package/src/renderers/electrobun/view/http-fetch.ts +1 -1
  914. package/src/renderers/electrobun/view/input-host.tsx +12 -1
  915. package/src/renderers/electrobun/view/main.tsx +8 -3
  916. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +59 -0
  917. package/src/renderers/electrobun/view/native-stubs/chart/kitty-support.ts +9 -0
  918. package/src/renderers/electrobun/view/remote/asset-data-client.ts +201 -0
  919. package/src/renderers/electrobun/view/remote/persistence.ts +158 -0
  920. package/src/renderers/electrobun/view/remote/ticker-repository.ts +26 -0
  921. package/src/renderers/electrobun/view/styles.css +43 -0
  922. package/src/renderers/electrobun/view/ui-host.tsx +22 -1469
  923. package/src/renderers/opentui/chart-surface.tsx +4 -5
  924. package/src/renderers/opentui/host.tsx +30 -4
  925. package/src/renderers/opentui/image/loader.test.ts +25 -0
  926. package/src/renderers/opentui/image/loader.ts +105 -0
  927. package/src/renderers/opentui/image/surface.tsx +266 -0
  928. package/src/renderers/opentui/input-host.tsx +12 -1
  929. package/src/renderers/opentui/start.tsx +2 -2
  930. package/src/renderers/opentui/test-utils.tsx +2 -2
  931. package/src/renderers/opentui/ui-host.tsx +1 -1
  932. package/src/sources/gloomberb-cloud/index.test.ts +660 -0
  933. package/src/sources/gloomberb-cloud/index.ts +444 -0
  934. package/src/sources/gloomberb-cloud/news.ts +127 -0
  935. package/src/sources/gloomberb-cloud/normalizers.ts +354 -0
  936. package/src/sources/provider-errors.ts +8 -2
  937. package/src/sources/provider-router/batches.ts +170 -0
  938. package/src/sources/provider-router/brokers.ts +114 -0
  939. package/src/sources/provider-router/cache.ts +171 -0
  940. package/src/sources/provider-router/cached-financials.test.ts +498 -0
  941. package/src/sources/provider-router/document-routes.ts +153 -0
  942. package/src/sources/provider-router/financial-routes.ts +225 -0
  943. package/src/sources/provider-router/financials.ts +259 -0
  944. package/src/sources/provider-router/history.test.ts +383 -0
  945. package/src/sources/provider-router/history.ts +448 -0
  946. package/src/sources/provider-router/index.test.ts +1438 -0
  947. package/src/sources/provider-router/index.ts +389 -0
  948. package/src/sources/provider-router/news.ts +36 -0
  949. package/src/sources/provider-router/primary.ts +164 -0
  950. package/src/sources/provider-router/route-types.ts +34 -0
  951. package/src/sources/provider-router/routing.ts +58 -0
  952. package/src/sources/provider-router/search.ts +183 -0
  953. package/src/sources/provider-router/sources.ts +55 -0
  954. package/src/sources/provider-router/streaming.ts +92 -0
  955. package/src/sources/provider-router/supplemental.ts +373 -0
  956. package/src/sources/provider-router/test-support.ts +136 -0
  957. package/src/sources/sec-edgar/content.ts +211 -0
  958. package/src/sources/sec-edgar.ts +9 -217
  959. package/src/sources/yahoo-finance/financials.ts +339 -0
  960. package/src/sources/yahoo-finance/history.ts +106 -0
  961. package/src/sources/yahoo-finance/http.ts +100 -0
  962. package/src/sources/yahoo-finance/mappers.ts +409 -0
  963. package/src/sources/yahoo-finance/options.ts +134 -0
  964. package/src/sources/yahoo-finance/quote-summary.ts +210 -0
  965. package/src/sources/yahoo-finance/requests.ts +192 -0
  966. package/src/sources/yahoo-finance/snapshots.ts +236 -0
  967. package/src/sources/yahoo-finance/symbols.ts +102 -0
  968. package/src/sources/yahoo-finance/types.ts +167 -0
  969. package/src/sources/yahoo-finance.test.ts +116 -12
  970. package/src/sources/yahoo-finance.ts +115 -1408
  971. package/src/state/app/activity.test.ts +41 -0
  972. package/src/state/app/activity.ts +106 -0
  973. package/src/state/app/bootstrap.test.ts +498 -0
  974. package/src/state/app/bootstrap.ts +412 -0
  975. package/src/state/app/context/index.test.ts +564 -0
  976. package/src/state/app/context/index.test.tsx +193 -0
  977. package/src/state/app/context/index.tsx +476 -0
  978. package/src/state/app/context/selector.test.tsx +291 -0
  979. package/src/state/config-save-scheduler.ts +2 -6
  980. package/src/state/hooks/inline-tickers.test.tsx +123 -0
  981. package/src/state/hooks/inline-tickers.ts +212 -0
  982. package/src/state/hooks/quote-streaming.test.ts +31 -0
  983. package/src/state/hooks/quote-streaming.test.tsx +70 -0
  984. package/src/state/hooks/quote-streaming.ts +92 -0
  985. package/src/state/hooks/session-snapshot.ts +99 -0
  986. package/src/state/pane-scroll-registry.tsx +1 -1
  987. package/src/state/selectors-ui.ts +1 -1
  988. package/src/state/selectors.ts +1 -1
  989. package/src/test-support/plugin-persistence.ts +112 -0
  990. package/src/test-support/plugin-runtime.ts +2 -1
  991. package/src/theme/colors.ts +23 -9
  992. package/src/theme/themes.test.ts +15 -0
  993. package/src/tickers/list.test.ts +16 -0
  994. package/src/tickers/open-target.ts +77 -0
  995. package/src/tickers/search/index.test.ts +252 -0
  996. package/src/tickers/search/index.ts +367 -0
  997. package/src/tickers/search/ranking.ts +387 -0
  998. package/src/tickers/search/result.ts +27 -0
  999. package/src/tickers/search/types.ts +41 -0
  1000. package/src/tickers/search/upsert.ts +91 -0
  1001. package/src/tickers/tokenizer.test.ts +31 -0
  1002. package/src/tickers/tokenizer.ts +37 -0
  1003. package/src/types/broker.ts +12 -4
  1004. package/src/types/config.ts +42 -23
  1005. package/src/types/context-menu.ts +1 -1
  1006. package/src/types/data-provider.ts +2 -2
  1007. package/src/types/desktop-window.ts +1 -1
  1008. package/src/types/financials.ts +109 -4
  1009. package/src/types/news-source.ts +0 -1
  1010. package/src/types/persistence.ts +1 -1
  1011. package/src/types/plugin.ts +37 -21
  1012. package/src/types/ticker.ts +1 -0
  1013. package/src/types/trading.ts +23 -1
  1014. package/src/ui/context-menu.tsx +6 -5
  1015. package/src/ui/dialog.tsx +6 -3
  1016. package/src/ui/host.tsx +10 -10
  1017. package/src/ui/index.tsx +14 -33
  1018. package/src/updater/github-releases.test.ts +41 -0
  1019. package/src/updater/github-releases.ts +73 -0
  1020. package/src/updater.test.ts +21 -0
  1021. package/src/updater.ts +10 -3
  1022. package/src/utils/back-navigation.ts +2 -2
  1023. package/src/utils/cli-output.ts +4 -4
  1024. package/src/utils/currency-units.test.ts +23 -1
  1025. package/src/utils/currency-units.ts +56 -0
  1026. package/src/utils/datetime-format.ts +40 -0
  1027. package/src/utils/dom-screenshot.ts +2 -2
  1028. package/src/utils/double-escape-close.ts +1 -1
  1029. package/src/utils/exchanges.ts +2 -2
  1030. package/src/utils/format.ts +0 -7
  1031. package/src/utils/fuzzy-search.ts +1 -1
  1032. package/src/utils/inline-content-tokenizer.test.ts +11 -2
  1033. package/src/utils/inline-content-tokenizer.ts +33 -12
  1034. package/src/utils/keyboard.ts +1 -1
  1035. package/src/utils/link-tokenizer.ts +2 -2
  1036. package/src/utils/options.ts +1 -1
  1037. package/src/utils/price-history.test.ts +20 -0
  1038. package/src/utils/price-history.ts +24 -2
  1039. package/src/utils/relative-time.ts +8 -0
  1040. package/src/utils/selection-clipboard.ts +1 -1
  1041. package/src/utils/shortcut-labels.ts +2 -2
  1042. package/src/utils/sort-values.ts +17 -0
  1043. package/src/utils/terminal-input-reset.ts +1 -1
  1044. package/src/utils/text-wrap.ts +20 -0
  1045. package/src/version.ts +1 -1
  1046. package/src/components/chart/chart-controller.test.ts +0 -110
  1047. package/src/components/chart/chart-controller.ts +0 -650
  1048. package/src/components/chart/chart-data.ts +0 -350
  1049. package/src/components/chart/chart-indicator-selector.tsx +0 -75
  1050. package/src/components/chart/chart-pane-settings.ts +0 -37
  1051. package/src/components/chart/chart-renderer.ts +0 -1562
  1052. package/src/components/chart/chart-resolution.test.ts +0 -87
  1053. package/src/components/chart/chart-resolution.ts +0 -405
  1054. package/src/components/chart/chart-scroll.test.ts +0 -70
  1055. package/src/components/chart/chart-scroll.ts +0 -104
  1056. package/src/components/chart/chart-types.ts +0 -119
  1057. package/src/components/chart/chart-viewport.test.ts +0 -29
  1058. package/src/components/chart/chart-viewport.ts +0 -50
  1059. package/src/components/chart/chart.test.ts +0 -922
  1060. package/src/components/chart/comparison-chart-data.test.ts +0 -105
  1061. package/src/components/chart/comparison-chart-data.ts +0 -292
  1062. package/src/components/chart/comparison-chart-renderer.test.ts +0 -187
  1063. package/src/components/chart/comparison-chart-renderer.ts +0 -437
  1064. package/src/components/chart/native/kitty-adapter.ts +0 -9
  1065. package/src/components/chart/native/kitty-manager.ts +0 -142
  1066. package/src/components/chart/native/kitty-support.ts +0 -71
  1067. package/src/components/chart/native/kitty.test.ts +0 -728
  1068. package/src/components/chart/native/surface-manager.ts +0 -225
  1069. package/src/components/chart/native/surface-sync.ts +0 -34
  1070. package/src/components/chart/native/surface-visibility.ts +0 -48
  1071. package/src/components/chart/static-chart-surface.test.tsx +0 -51
  1072. package/src/components/chart/static-chart-surface.tsx +0 -217
  1073. package/src/components/chart/stock-chart-renderer-switch.test.tsx +0 -1323
  1074. package/src/components/chart/stock-chart.test.ts +0 -330
  1075. package/src/components/chart/stock-chart.tsx +0 -4148
  1076. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -109
  1077. package/src/components/command-bar/command-bar.test.tsx +0 -3244
  1078. package/src/components/command-bar/command-bar.tsx +0 -5886
  1079. package/src/components/command-bar/command-registry.ts +0 -267
  1080. package/src/components/command-bar/confirm-body.tsx +0 -51
  1081. package/src/components/command-bar/list-model.ts +0 -80
  1082. package/src/components/command-bar/pane-template-display.ts +0 -8
  1083. package/src/components/command-bar/root-shortcuts.ts +0 -188
  1084. package/src/components/command-bar/workflow-ops.test.ts +0 -144
  1085. package/src/components/command-bar/workflow-ops.ts +0 -459
  1086. package/src/components/command-bar/workflow-types.ts +0 -127
  1087. package/src/components/data-table-stack-view.test.tsx +0 -241
  1088. package/src/components/data-table-stack-view.tsx +0 -59
  1089. package/src/components/data-table-view.test.tsx +0 -211
  1090. package/src/components/data-table-view.tsx +0 -244
  1091. package/src/components/feed-data-table-stack-view.test.tsx +0 -225
  1092. package/src/components/feed-data-table-stack-view.tsx +0 -359
  1093. package/src/components/layout/pane-content.test.tsx +0 -64
  1094. package/src/components/layout/pane-content.tsx +0 -44
  1095. package/src/components/layout/pane-cycle-order.test.ts +0 -30
  1096. package/src/components/layout/pane-cycle-order.ts +0 -32
  1097. package/src/components/layout/pane-footer.test.tsx +0 -195
  1098. package/src/components/layout/pane-footer.tsx +0 -432
  1099. package/src/components/layout/pane-header.test.tsx +0 -39
  1100. package/src/components/layout/pane-header.tsx +0 -248
  1101. package/src/components/layout/pane-keyboard-scroll.test.tsx +0 -226
  1102. package/src/components/layout/pane-sizing.ts +0 -34
  1103. package/src/components/layout/pane-title.ts +0 -38
  1104. package/src/components/layout/pane.tsx +0 -129
  1105. package/src/components/layout/shell.test.tsx +0 -1453
  1106. package/src/components/layout/shell.tsx +0 -1960
  1107. package/src/components/price-sparkline-view.tsx +0 -463
  1108. package/src/components/price-sparkline.ts +0 -46
  1109. package/src/components/speedometer-gauge.test.tsx +0 -45
  1110. package/src/components/speedometer-gauge.tsx +0 -562
  1111. package/src/components/ticker-badge-list.tsx +0 -53
  1112. package/src/components/ticker-badge-text.test.tsx +0 -253
  1113. package/src/components/ticker-badge-text.tsx +0 -113
  1114. package/src/components/ticker-badge.tsx +0 -109
  1115. package/src/components/ticker-list-table-view.tsx +0 -233
  1116. package/src/components/ticker-list-table.test.tsx +0 -210
  1117. package/src/components/ticker-list-table.tsx +0 -458
  1118. package/src/components/ui/data-table.tsx +0 -522
  1119. package/src/components/ui/multi-select-dialog.tsx +0 -336
  1120. package/src/core/state/app-state.ts +0 -1009
  1121. package/src/data/config-layout.ts +0 -225
  1122. package/src/data/config-store-node.ts +0 -478
  1123. package/src/data/config-store.test.ts +0 -481
  1124. package/src/data/config-store.ts +0 -64
  1125. package/src/data/sqlite-cache.test.ts +0 -151
  1126. package/src/data/sqlite-database.ts +0 -68
  1127. package/src/data/sqlite-json.ts +0 -16
  1128. package/src/data/sqlite-retry.test.ts +0 -63
  1129. package/src/market-data/coordinator-subscriptions.test.ts +0 -209
  1130. package/src/market-data/coordinator.test.ts +0 -830
  1131. package/src/market-data/coordinator.ts +0 -1155
  1132. package/src/plugins/builtin/ai/screener-contract.test.ts +0 -117
  1133. package/src/plugins/builtin/ai/screener-contract.ts +0 -174
  1134. package/src/plugins/builtin/ai/screener-pane.test.tsx +0 -348
  1135. package/src/plugins/builtin/ai/screener-pane.tsx +0 -1118
  1136. package/src/plugins/builtin/ai/utils.ts +0 -1
  1137. package/src/plugins/builtin/ask-ai.test.tsx +0 -348
  1138. package/src/plugins/builtin/chat-controller.test.ts +0 -1376
  1139. package/src/plugins/builtin/chat-controller.ts +0 -1248
  1140. package/src/plugins/builtin/chat.test.tsx +0 -2245
  1141. package/src/plugins/builtin/chat.tsx +0 -2267
  1142. package/src/plugins/builtin/cloud-auth-actions.tsx +0 -72
  1143. package/src/plugins/builtin/cloud-tweets.tsx +0 -896
  1144. package/src/plugins/builtin/comparison-chart.test.tsx +0 -532
  1145. package/src/plugins/builtin/comparison-chart.tsx +0 -194
  1146. package/src/plugins/builtin/debug.tsx +0 -365
  1147. package/src/plugins/builtin/earnings/earnings-cache.test.ts +0 -181
  1148. package/src/plugins/builtin/earnings/earnings-cache.ts +0 -145
  1149. package/src/plugins/builtin/earnings/earnings-data.test.ts +0 -90
  1150. package/src/plugins/builtin/earnings/earnings-data.ts +0 -115
  1151. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +0 -72
  1152. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +0 -366
  1153. package/src/plugins/builtin/help.test.tsx +0 -166
  1154. package/src/plugins/builtin/help.tsx +0 -450
  1155. package/src/plugins/builtin/layout-manager.tsx +0 -247
  1156. package/src/plugins/builtin/manual-entry.tsx +0 -25
  1157. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +0 -98
  1158. package/src/plugins/builtin/news-wire/breaking-notifications.ts +0 -110
  1159. package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +0 -156
  1160. package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -68
  1161. package/src/plugins/builtin/news-wire/categories.ts +0 -114
  1162. package/src/plugins/builtin/news-wire/default-feeds.ts +0 -3
  1163. package/src/plugins/builtin/news-wire/feed-config.test.ts +0 -90
  1164. package/src/plugins/builtin/news-wire/feed-config.ts +0 -216
  1165. package/src/plugins/builtin/news-wire/index.tsx +0 -129
  1166. package/src/plugins/builtin/news-wire/industry-pane.tsx +0 -126
  1167. package/src/plugins/builtin/news-wire/news-detail-view.tsx +0 -299
  1168. package/src/plugins/builtin/news-wire/news-footer.ts +0 -28
  1169. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +0 -98
  1170. package/src/plugins/builtin/news-wire/news-query-presets.ts +0 -40
  1171. package/src/plugins/builtin/news-wire/news-table.test.tsx +0 -206
  1172. package/src/plugins/builtin/news-wire/news-table.tsx +0 -296
  1173. package/src/plugins/builtin/news-wire/persisted-articles.ts +0 -98
  1174. package/src/plugins/builtin/news-wire/read-state.ts +0 -76
  1175. package/src/plugins/builtin/news-wire/rss-parser.test.ts +0 -184
  1176. package/src/plugins/builtin/news-wire/rss-parser.ts +0 -213
  1177. package/src/plugins/builtin/news-wire/rss-source.test.ts +0 -159
  1178. package/src/plugins/builtin/news-wire/rss-source.ts +0 -181
  1179. package/src/plugins/builtin/news.tsx +0 -194
  1180. package/src/plugins/builtin/notes.tsx +0 -561
  1181. package/src/plugins/builtin/options.test.tsx +0 -241
  1182. package/src/plugins/builtin/options.tsx +0 -542
  1183. package/src/plugins/builtin/portfolio-list/cli/commands.ts +0 -657
  1184. package/src/plugins/builtin/portfolio-list/pane.tsx +0 -724
  1185. package/src/plugins/builtin/portfolio-list/quick-add.tsx +0 -422
  1186. package/src/plugins/builtin/portfolio-list/summary.tsx +0 -275
  1187. package/src/plugins/builtin/portfolio-list.test.ts +0 -133
  1188. package/src/plugins/builtin/portfolio-list.test.tsx +0 -1535
  1189. package/src/plugins/builtin/portfolio-metrics.test.ts +0 -236
  1190. package/src/plugins/builtin/quote-monitor.test.tsx +0 -222
  1191. package/src/plugins/builtin/sec-filing-display.tsx +0 -84
  1192. package/src/plugins/builtin/sec.tsx +0 -342
  1193. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +0 -461
  1194. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +0 -479
  1195. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +0 -269
  1196. package/src/plugins/builtin/ticker-detail.test.tsx +0 -888
  1197. package/src/plugins/builtin/ticker-surface.ts +0 -68
  1198. package/src/plugins/builtin/yahoo.ts +0 -72
  1199. package/src/plugins/catalog-ui.test.ts +0 -24
  1200. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +0 -35
  1201. package/src/plugins/ibkr/flex.test.ts +0 -162
  1202. package/src/plugins/ibkr/flex.ts +0 -371
  1203. package/src/plugins/ibkr/gateway-helpers.ts +0 -33
  1204. package/src/plugins/ibkr/gateway-selection.ts +0 -51
  1205. package/src/plugins/ibkr/gateway-service-native.ts +0 -2377
  1206. package/src/plugins/ibkr/gateway-service.test.ts +0 -584
  1207. package/src/plugins/ibkr/gateway-service.ts +0 -433
  1208. package/src/plugins/ibkr/gateway-types.ts +0 -27
  1209. package/src/plugins/ibkr/price-normalization.test.ts +0 -33
  1210. package/src/plugins/ibkr/price-normalization.ts +0 -60
  1211. package/src/plugins/ibkr/trade-badge.tsx +0 -28
  1212. package/src/plugins/ibkr/trade-tab.test.tsx +0 -276
  1213. package/src/plugins/ibkr/trade-tab.tsx +0 -1117
  1214. package/src/plugins/ibkr/trade-utils.test.ts +0 -175
  1215. package/src/plugins/ibkr/trade-utils.ts +0 -150
  1216. package/src/plugins/ibkr/trading-pane.tsx +0 -474
  1217. package/src/plugins/ibkr/trading-state.test.ts +0 -109
  1218. package/src/plugins/ibkr/trading-state.ts +0 -339
  1219. package/src/plugins/plugin-runtime.test.tsx +0 -323
  1220. package/src/plugins/plugin-runtime.tsx +0 -433
  1221. package/src/plugins/prediction-markets/controller-data.ts +0 -785
  1222. package/src/plugins/prediction-markets/controller.ts +0 -659
  1223. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +0 -572
  1224. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +0 -696
  1225. package/src/plugins/prediction-markets/services/polymarket-ws.ts +0 -128
  1226. package/src/plugins/registry.test.ts +0 -362
  1227. package/src/plugins/registry.ts +0 -854
  1228. package/src/renderers/electrobun/bun/application-menu-click.test.ts +0 -50
  1229. package/src/renderers/electrobun/bun/application-menu-click.ts +0 -58
  1230. package/src/renderers/electrobun/bun/application-menu.ts +0 -123
  1231. package/src/renderers/electrobun/bun/context-menu-click.test.ts +0 -67
  1232. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +0 -52
  1233. package/src/renderers/electrobun/bun/context-menu-normalize.ts +0 -67
  1234. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +0 -63
  1235. package/src/renderers/electrobun/bun/desktop-workspace.ts +0 -180
  1236. package/src/renderers/electrobun/bun/window-focus.ts +0 -37
  1237. package/src/renderers/electrobun/bun/window-frame.test.ts +0 -40
  1238. package/src/renderers/electrobun/view/data-table.tsx +0 -810
  1239. package/src/renderers/electrobun/view/desktop-controls.tsx +0 -673
  1240. package/src/renderers/electrobun/view/desktop-window-bridge.ts +0 -70
  1241. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +0 -57
  1242. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +0 -41
  1243. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +0 -9
  1244. package/src/renderers/opentui/image-loader.test.ts +0 -25
  1245. package/src/renderers/opentui/image-loader.ts +0 -105
  1246. package/src/renderers/opentui/image-surface.tsx +0 -266
  1247. package/src/sources/gloomberb-cloud.test.ts +0 -637
  1248. package/src/sources/gloomberb-cloud.ts +0 -828
  1249. package/src/sources/provider-router.test.ts +0 -2600
  1250. package/src/sources/provider-router.ts +0 -2231
  1251. package/src/state/app-activity.test.ts +0 -41
  1252. package/src/state/app-activity.ts +0 -106
  1253. package/src/state/app-bootstrap.test.ts +0 -498
  1254. package/src/state/app-bootstrap.ts +0 -411
  1255. package/src/state/app-context-selector.test.tsx +0 -369
  1256. package/src/state/app-context.test.ts +0 -670
  1257. package/src/state/app-context.test.tsx +0 -193
  1258. package/src/state/app-context.tsx +0 -485
  1259. package/src/state/use-inline-tickers.test.tsx +0 -73
  1260. package/src/state/use-inline-tickers.ts +0 -188
  1261. package/src/state/use-quote-streaming.test.ts +0 -31
  1262. package/src/state/use-quote-streaming.test.tsx +0 -70
  1263. package/src/state/use-quote-streaming.ts +0 -92
  1264. package/src/state/use-session-snapshot.ts +0 -99
  1265. package/src/utils/api-client.test.ts +0 -603
  1266. package/src/utils/api-client.ts +0 -1379
  1267. package/src/utils/market-format.test.ts +0 -109
  1268. package/src/utils/market-format.ts +0 -299
  1269. package/src/utils/market-status.ts +0 -141
  1270. package/src/utils/quote-freshness.test.ts +0 -150
  1271. package/src/utils/quote-freshness.ts +0 -145
  1272. package/src/utils/quote-resolution.test.ts +0 -277
  1273. package/src/utils/quote-resolution.ts +0 -642
  1274. package/src/utils/quote-time.test.ts +0 -34
  1275. package/src/utils/quote-time.ts +0 -59
  1276. package/src/utils/ticker-list.test.ts +0 -16
  1277. package/src/utils/ticker-search.test.ts +0 -227
  1278. package/src/utils/ticker-search.ts +0 -926
  1279. package/src/utils/ticker-tokenizer.test.ts +0 -31
  1280. package/src/utils/ticker-tokenizer.ts +0 -37
  1281. /package/src/components/chart/native/{kitty-protocol.ts → kitty/protocol.ts} +0 -0
  1282. /package/src/components/ui/{multi-select.ts → multi-select/index.ts} +0 -0
  1283. /package/src/data/{sqlite-retry.ts → sqlite/retry.ts} +0 -0
  1284. /package/src/plugins/builtin/{news-wire → news/wire}/hash.ts +0 -0
  1285. /package/src/plugins/builtin/{news-wire → news/wire}/read-state.test.ts +0 -0
  1286. /package/src/plugins/builtin/{notes-files.ts → notes/files.ts} +0 -0
  1287. /package/src/renderers/electrobun/bun/{context-menu-click.ts → context-menu/click.ts} +0 -0
  1288. /package/src/renderers/electrobun/bun/{window-frame.ts → window/frame.ts} +0 -0
  1289. /package/src/renderers/electrobun/view/native-stubs/{chart-surface-manager.ts → chart/surface-manager.ts} +0 -0
  1290. /package/src/renderers/electrobun/view/native-stubs/{chart-surface-sync.ts → chart/surface-sync.ts} +0 -0
  1291. /package/src/{utils/ticker-list.ts → tickers/list.ts} +0 -0
  1292. /package/src/{utils/ticker-metadata.ts → tickers/metadata.ts} +0 -0
@@ -1,4148 +0,0 @@
1
- import { Box, ChartSurface, Text } from "../../ui";
2
- import { memo, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
3
- import { TextAttributes, type BoxRenderable, type NativeRendererHost as CliRenderer } from "../../ui";
4
- import { useNativeRenderer, useUiCapabilities } from "../../ui";
5
- import { useShortcut } from "../../react/input";
6
- import { useAppDispatch, useAppSelector, usePaneInstance, usePaneInstanceId, usePaneSettingValue, usePaneTicker } from "../../state/app-context";
7
- import { usePaneFooter, type PaneFooterSegment, type PaneHint } from "../layout/pane-footer";
8
- import { ShortcutHint } from "../ui";
9
- import { getSharedMarketData } from "../../plugins/registry";
10
- import { colors } from "../../theme/colors";
11
- import { useThemeColors } from "../../theme/theme-context";
12
- import { formatCompact } from "../../utils/format";
13
- import { formatMarketPriceWithCurrency } from "../../utils/market-format";
14
- import { useChartQueries, useChartQuery } from "../../market-data/hooks";
15
- import { instrumentFromTicker, type ChartRequest } from "../../market-data/request-types";
16
- import { buildChartKey } from "../../market-data/selectors";
17
- import { usePersistChartControlSelection } from "./chart-pane-settings";
18
- import { computeSMA, computeEMA } from "./indicators/moving-averages";
19
- import { computeRSI, computeMACD } from "./indicators/oscillators";
20
- import { computeBollingerBands } from "./indicators/bands";
21
- import type { IndicatorConfig, MacdResult, OscillatorPoint, OverlayPoint } from "./indicators/types";
22
- import type { ChartIndicatorOverlays } from "./chart-types";
23
- import {
24
- applyBufferedPanExpansion,
25
- consumeScrollPanMovement,
26
- getDragPanWindowRatio,
27
- getKeyboardPanCellCount,
28
- resolveDragPanOffset,
29
- } from "./chart-scroll";
30
- import { projectChartData, resolveBarSize, resolveStableOhlcProjectionOptions, type ProjectedChartPoint } from "./chart-data";
31
- import {
32
- buildPresetDateWindow,
33
- buildVisibleDateWindow,
34
- buildVisibleDateWindowFromRange,
35
- clampDateWindowToBounds,
36
- clearActivePreset,
37
- getDateWindowBounds,
38
- getMinimumDateStepMs,
39
- getVisibleWindowForDateRange,
40
- getPointDates,
41
- needsCanonicalPresetViewportReset,
42
- resolveChartBodyState,
43
- resolveCanonicalPresetViewport,
44
- resolvePresetSelection,
45
- resolvePresetRangeViewport,
46
- resolveResolutionSelection,
47
- resolveStoredChartSelection,
48
- resolveVisibleActivePreset,
49
- sameDateWindow,
50
- shiftDateWindow,
51
- type ChartBodyState,
52
- type DateWindowRange,
53
- } from "./chart-controller";
54
- import {
55
- buildChartResolutionSupportMap,
56
- CHART_RESOLUTION_STEP_MS,
57
- clampTimeRangeToMaxRange,
58
- DEFAULT_TICKER_CHART_RANGE_PRESET,
59
- DEFAULT_TICKER_CHART_RESOLUTION,
60
- DEFAULT_VISIBLE_MANUAL_CHART_RESOLUTIONS,
61
- getBestSupportedResolutionForVisibleWindow,
62
- getChartResolutionLabel,
63
- getNextBufferRange,
64
- getNextFallbackResolution,
65
- getPresetResolution,
66
- getSupportMaxRange,
67
- getTimeRangeForDateWindow,
68
- isIntradayResolution,
69
- isRangePresetSupported,
70
- maxTimeRange,
71
- normalizeChartResolutionSupport,
72
- sortChartResolutions,
73
- subtractTimeRange,
74
- TIME_RANGE_ORDER,
75
- type ChartResolutionSupport,
76
- type ManualChartResolution,
77
- } from "./chart-resolution";
78
- import {
79
- getVisiblePointCount,
80
- RIGHT_EDGE_ANCHOR_RATIO,
81
- resolveAnchoredChartZoom,
82
- } from "./chart-viewport";
83
- import {
84
- buildChartScene,
85
- formatCursorAxisValue,
86
- getActivePointIndex,
87
- getPointTerminalColumn,
88
- renderChart,
89
- resolveChartPalette,
90
- resolveChartAxisWidth,
91
- type StyledContent,
92
- } from "./chart-renderer";
93
- import {
94
- CELL_CURSOR_SNAP_DISTANCE,
95
- sameCursorPosition,
96
- stepCursorTowards,
97
- type ChartCursorMotionKind,
98
- } from "./cursor-motion";
99
- import {
100
- CHART_RENDER_MODES,
101
- RANGE_DAYS,
102
- TIME_RANGES,
103
- type ChartAxisMode,
104
- type ChartRenderMode,
105
- type ChartResolution,
106
- type TimeRange,
107
- type ChartViewState,
108
- type ResolvedChartRenderer,
109
- } from "./chart-types";
110
- import {
111
- computeBitmapSize,
112
- renderNativeChartBase,
113
- renderNativeCrosshairOverlay,
114
- type CellRect,
115
- type NativeChartBitmap,
116
- type NativeCrosshairOverlay,
117
- } from "./native/chart-rasterizer";
118
- import { ensureKittySupport, getCachedKittySupport } from "./native/kitty-support";
119
- import { resolveChartRendererState } from "./native/renderer-selection";
120
- import { getNativeSurfaceManager } from "./native/surface-manager";
121
- import { syncCachedNativeSurface } from "./native/surface-sync";
122
- import {
123
- getRenderableCellRect,
124
- resolveNativeSurfaceVisibleRect,
125
- type NativeSurfaceRenderableNode,
126
- } from "./native/surface-visibility";
127
- import type { PricePoint, TickerFinancials } from "../../types/financials";
128
- import type { TickerRecord } from "../../types/ticker";
129
- import { TimeAxisLabel } from "./time-axis-label";
130
- import { PriceAxisLabels } from "./price-axis-labels";
131
- import { getChartMarketSessionKey, resolveChartMarketSession } from "./market-session";
132
-
133
- const MODE_CHIPS: Record<ChartRenderMode, string> = {
134
- area: "A",
135
- line: "L",
136
- candles: "C",
137
- ohlc: "O",
138
- hlc: "H",
139
- };
140
-
141
- const MODE_LABELS: Record<ChartRenderMode, string> = {
142
- area: "AREA",
143
- line: "LINE",
144
- candles: "CANDLES",
145
- ohlc: "OHLC",
146
- hlc: "HLC",
147
- };
148
-
149
- const AXIS_MEASURE_PALETTE = resolveChartPalette({
150
- bg: colors.bg,
151
- border: colors.border,
152
- borderFocused: colors.borderFocused,
153
- text: colors.text,
154
- textDim: colors.textDim,
155
- positive: colors.positive,
156
- negative: colors.negative,
157
- }, "neutral");
158
-
159
- const EMPTY_INDICATOR_CONFIG: IndicatorConfig = {};
160
- const INDICATOR_RENDER_DEBOUNCE_MS = 120;
161
- const CHART_DETAIL_REQUEST_DEBOUNCE_MS = 160;
162
-
163
- interface StockChartProps {
164
- width: number;
165
- height: number;
166
- focused: boolean;
167
- interactive?: boolean;
168
- onActivate?: () => void;
169
- compact?: boolean;
170
- axisMode?: ChartAxisMode;
171
- historyOverride?: PricePoint[] | null;
172
- currencyOverride?: string | null;
173
- indicatorConfig?: IndicatorConfig;
174
- showVolume?: boolean;
175
- footerControls?: ReactNode;
176
- footerHints?: PaneHint[];
177
- }
178
-
179
- interface ResolvedStockChartProps extends StockChartProps {
180
- ticker: TickerRecord | null;
181
- financials: TickerFinancials | null;
182
- }
183
-
184
- interface DragState {
185
- startGlobalX: number;
186
- startPanOffset: number;
187
- startWindow: DateWindowRange | null;
188
- }
189
-
190
- interface ResolvedChartRequestPlan {
191
- effectiveResolution: ManualChartResolution | null;
192
- requestedWindow: DateWindowRange | null;
193
- resolutionRequest: ChartRequest | null;
194
- detailRequest: ChartRequest | null;
195
- unsupportedMessage: string | null;
196
- }
197
-
198
- interface ResolvedRenderCandidate {
199
- resolution: ManualChartResolution;
200
- plan: ResolvedChartRequestPlan;
201
- resolutionRequestKey: string | null;
202
- detailRequestKey: string | null;
203
- }
204
-
205
- interface AutoRenderedView {
206
- window: DateWindowRange;
207
- resolution: ManualChartResolution;
208
- data: PricePoint[];
209
- }
210
-
211
- interface CachedRenderedView {
212
- window: DateWindowRange | null;
213
- resolution: ManualChartResolution | null;
214
- data: PricePoint[];
215
- }
216
-
217
- interface AutoDisplayState {
218
- bodyState: ChartBodyState<PricePoint[]>;
219
- resolution: ManualChartResolution | null;
220
- window: DateWindowRange | null;
221
- }
222
-
223
- type StockChartViewportState = Omit<ChartViewState, "resolution">;
224
- type ResolutionAwareViewportState = StockChartViewportState & { resolution: ChartResolution };
225
-
226
- type PendingExpansionAction =
227
- | { kind: "zoom-out"; targetVisibleCount: number; anchorRatio: number }
228
- | { kind: "pan-left"; targetPanOffset: number }
229
- | null;
230
-
231
- interface DeferredIndicatorState {
232
- key: string;
233
- overlays: ChartIndicatorOverlays;
234
- }
235
-
236
- interface ChartMouseEvent {
237
- x: number;
238
- y: number;
239
- pixelX?: number;
240
- pixelY?: number;
241
- stopPropagation?: () => void;
242
- preventDefault?: () => void;
243
- modifiers: {
244
- shift: boolean;
245
- alt: boolean;
246
- ctrl: boolean;
247
- };
248
- scroll?: {
249
- direction: "up" | "down" | "left" | "right";
250
- delta: number;
251
- };
252
- }
253
-
254
- export function resolveChartKeyboardKey(event: { name?: string; sequence?: string }): string {
255
- const name = event.name ?? "";
256
- const sequence = event.sequence ?? "";
257
- const candidates = [name, sequence];
258
-
259
- if (candidates.some((key) => key === "=" || key === "+" || key === "plus")) {
260
- return "zoom-in";
261
- }
262
- if (candidates.some((key) => key === "-" || key === "_" || key === "minus")) {
263
- return "zoom-out";
264
- }
265
-
266
- const key = name || sequence;
267
- return key.length === 1 ? key.toLowerCase() : key;
268
- }
269
-
270
- export interface LocalPlotPointer {
271
- cellX: number;
272
- cellY: number;
273
- pixelX: number | null;
274
- pixelY: number | null;
275
- hasPixelPrecision: boolean;
276
- }
277
-
278
- interface DisplayCursorState {
279
- cellX: number | null;
280
- cellY: number | null;
281
- pixelX: number | null;
282
- pixelY: number | null;
283
- }
284
-
285
- const EMPTY_DISPLAY_CURSOR: DisplayCursorState = {
286
- cellX: null,
287
- cellY: null,
288
- pixelX: null,
289
- pixelY: null,
290
- };
291
-
292
- const PIXEL_CURSOR_SNAP_DISTANCE = 0.5;
293
- const globalAnimationFrame = globalThis as typeof globalThis & {
294
- requestAnimationFrame?: (callback: (timestamp: number) => void) => number;
295
- cancelAnimationFrame?: (handle: number) => void;
296
- };
297
-
298
- function clamp(value: number, min: number, max: number): number {
299
- return Math.max(min, Math.min(max, value));
300
- }
301
-
302
- function requestAnimationFrameSafe(callback: (timestamp: number) => void): number {
303
- if (typeof globalAnimationFrame.requestAnimationFrame === "function") {
304
- return globalAnimationFrame.requestAnimationFrame(callback);
305
- }
306
- return globalThis.setTimeout(() => callback(Date.now()), 16) as unknown as number;
307
- }
308
-
309
- function cancelAnimationFrameSafe(handle: number) {
310
- if (typeof globalAnimationFrame.cancelAnimationFrame === "function") {
311
- globalAnimationFrame.cancelAnimationFrame(handle);
312
- return;
313
- }
314
- clearTimeout(handle);
315
- }
316
-
317
- function coerceChartDate(value: Date | string | number): Date {
318
- return value instanceof Date ? value : new Date(value);
319
- }
320
-
321
- function getRendererCellMetrics(renderer: Pick<CliRenderer, "resolution" | "terminalWidth" | "terminalHeight">) {
322
- if (!renderer.resolution) return null;
323
- const cellWidth = renderer.resolution.width / Math.max(renderer.terminalWidth, 1);
324
- const cellHeight = renderer.resolution.height / Math.max(renderer.terminalHeight, 1);
325
- if (!(cellWidth > 0) || !(cellHeight > 0)) return null;
326
- return { cellWidth, cellHeight };
327
- }
328
-
329
- function getRenderablePixelSize(
330
- renderable: Pick<RenderableNode, "width" | "height"> | null,
331
- renderer: Pick<CliRenderer, "resolution" | "terminalWidth" | "terminalHeight">,
332
- ) {
333
- const metrics = getRendererCellMetrics(renderer);
334
- if (!renderable || !metrics) return null;
335
- return {
336
- pixelWidth: Math.max(renderable.width * metrics.cellWidth, 1),
337
- pixelHeight: Math.max(renderable.height * metrics.cellHeight, 1),
338
- };
339
- }
340
-
341
- export function projectCellCursorToLocalPixels(
342
- cellX: number,
343
- cellY: number,
344
- renderable: Pick<RenderableNode, "width" | "height"> | null,
345
- renderer: Pick<CliRenderer, "resolution" | "terminalWidth" | "terminalHeight">,
346
- ): { pixelX: number; pixelY: number } | null {
347
- const pixelSize = getRenderablePixelSize(renderable, renderer);
348
- if (!renderable || !pixelSize) return null;
349
-
350
- return {
351
- pixelX: renderable.width <= 1
352
- ? 0
353
- : clamp(
354
- (cellX / Math.max(renderable.width - 1, 1)) * Math.max(pixelSize.pixelWidth - 1, 0),
355
- 0,
356
- Math.max(pixelSize.pixelWidth - 1, 0),
357
- ),
358
- pixelY: renderable.height <= 1
359
- ? 0
360
- : clamp(
361
- (cellY / Math.max(renderable.height - 1, 1)) * Math.max(pixelSize.pixelHeight - 1, 0),
362
- 0,
363
- Math.max(pixelSize.pixelHeight - 1, 0),
364
- ),
365
- };
366
- }
367
-
368
- function scaleLocalPixelCoordinate(value: number | null, sourceExtent: number, targetExtent: number): number | null {
369
- if (value === null) return null;
370
- if (targetExtent <= 1) return 0;
371
- if (!(sourceExtent > 1)) {
372
- return clamp(value, 0, Math.max(targetExtent - 1, 0));
373
- }
374
- return clamp(
375
- (value / Math.max(sourceExtent - 1, 1)) * Math.max(targetExtent - 1, 0),
376
- 0,
377
- Math.max(targetExtent - 1, 0),
378
- );
379
- }
380
-
381
- function toCursorPosition(x: number | null, y: number | null) {
382
- return { x, y };
383
- }
384
-
385
- function sameDisplayCursorState(
386
- left: DisplayCursorState,
387
- right: DisplayCursorState,
388
- cellEpsilon = 0,
389
- pixelEpsilon = 0,
390
- ): boolean {
391
- return sameCursorPosition(toCursorPosition(left.cellX, left.cellY), toCursorPosition(right.cellX, right.cellY), cellEpsilon)
392
- && sameCursorPosition(toCursorPosition(left.pixelX, left.pixelY), toCursorPosition(right.pixelX, right.pixelY), pixelEpsilon);
393
- }
394
-
395
- function buildDisplayCursorState(
396
- cellX: number | null,
397
- cellY: number | null,
398
- renderable: BoxRenderable | null,
399
- renderer: Pick<CliRenderer, "resolution" | "terminalWidth" | "terminalHeight">,
400
- pixelX: number | null = null,
401
- pixelY: number | null = null,
402
- ): DisplayCursorState {
403
- if (cellX === null || cellY === null) return EMPTY_DISPLAY_CURSOR;
404
- if (pixelX !== null && pixelY !== null) {
405
- return { cellX, cellY, pixelX, pixelY };
406
- }
407
- const derivedPixels = projectCellCursorToLocalPixels(cellX, cellY, renderable, renderer);
408
- return {
409
- cellX,
410
- cellY,
411
- pixelX: derivedPixels?.pixelX ?? null,
412
- pixelY: derivedPixels?.pixelY ?? null,
413
- };
414
- }
415
-
416
- export function resolveSelectionDisplayCursorState(
417
- cursorX: number | null,
418
- cursorY: number | null,
419
- fallbackCursorX: number | null,
420
- fallbackCursorY: number | null,
421
- renderable: BoxRenderable | null,
422
- renderer: Pick<CliRenderer, "resolution" | "terminalWidth" | "terminalHeight">,
423
- ): DisplayCursorState {
424
- const resolvedCursorX = cursorY === null ? (fallbackCursorX ?? cursorX) : cursorX;
425
- const resolvedCursorY = cursorY ?? fallbackCursorY;
426
- if (resolvedCursorX === null) return EMPTY_DISPLAY_CURSOR;
427
- return buildDisplayCursorState(resolvedCursorX, resolvedCursorY, renderable, renderer);
428
- }
429
-
430
- export function resolveAdjacentSelectionCursorX(
431
- cursorX: number | null,
432
- step: -1 | 1,
433
- pointCount: number,
434
- width: number,
435
- mode: ChartRenderMode,
436
- ): number | null {
437
- if (pointCount <= 0 || width <= 0) return null;
438
- const anchorX = cursorX ?? (step < 0 ? width - 1 : 0);
439
- const currentIndex = getActivePointIndex(pointCount, width, anchorX, mode);
440
- const nextIndex = clamp(currentIndex + step, 0, pointCount - 1);
441
- return getPointTerminalColumn(nextIndex, pointCount, width, mode);
442
- }
443
-
444
- export function getLocalPlotPointer(
445
- event: ChartMouseEvent,
446
- renderable: BoxRenderable | null,
447
- renderer: Pick<CliRenderer, "resolution" | "terminalWidth" | "terminalHeight">,
448
- ): LocalPlotPointer | null {
449
- if (!renderable) return null;
450
-
451
- const localCellX = event.x - renderable.x;
452
- const localCellY = event.y - renderable.y;
453
- if (localCellX < 0 || localCellX >= renderable.width || localCellY < 0 || localCellY >= renderable.height) {
454
- return null;
455
- }
456
-
457
- const metrics = getRendererCellMetrics(renderer);
458
- if (event.pixelX === undefined || event.pixelY === undefined || !metrics) {
459
- return {
460
- cellX: localCellX,
461
- cellY: localCellY,
462
- pixelX: null,
463
- pixelY: null,
464
- hasPixelPrecision: false,
465
- };
466
- }
467
-
468
- const pixelLeft = renderable.x * metrics.cellWidth;
469
- const pixelTop = renderable.y * metrics.cellHeight;
470
- const pixelWidth = renderable.width * metrics.cellWidth;
471
- const pixelHeight = renderable.height * metrics.cellHeight;
472
- const localPixelX = event.pixelX - pixelLeft;
473
- const localPixelY = event.pixelY - pixelTop;
474
-
475
- if (localPixelX < 0 || localPixelY < 0 || localPixelX > pixelWidth || localPixelY > pixelHeight) {
476
- return {
477
- cellX: localCellX,
478
- cellY: localCellY,
479
- pixelX: null,
480
- pixelY: null,
481
- hasPixelPrecision: false,
482
- };
483
- }
484
-
485
- return {
486
- cellX: renderable.width <= 1
487
- ? 0
488
- : clamp(
489
- (localPixelX / Math.max(pixelWidth - 1, 1)) * Math.max(renderable.width - 1, 0),
490
- 0,
491
- Math.max(renderable.width - 1, 0),
492
- ),
493
- cellY: renderable.height <= 1
494
- ? 0
495
- : clamp(
496
- (localPixelY / Math.max(pixelHeight - 1, 1)) * Math.max(renderable.height - 1, 0),
497
- 0,
498
- Math.max(renderable.height - 1, 0),
499
- ),
500
- pixelX: clamp(localPixelX, 0, Math.max(pixelWidth - 1, 0)),
501
- pixelY: clamp(localPixelY, 0, Math.max(pixelHeight - 1, 0)),
502
- hasPixelPrecision: true,
503
- };
504
- }
505
-
506
- function getGlobalMouseX(
507
- event: ChartMouseEvent,
508
- renderer: Pick<CliRenderer, "resolution" | "terminalWidth" | "terminalHeight">,
509
- ): number {
510
- const metrics = getRendererCellMetrics(renderer);
511
- if (event.pixelX === undefined || !metrics) return event.x;
512
- return event.pixelX / metrics.cellWidth;
513
- }
514
-
515
- function resolveCursorMotionKind(
516
- event: ChartMouseEvent,
517
- renderer: Pick<CliRenderer, "resolution" | "terminalWidth" | "terminalHeight">,
518
- ): ChartCursorMotionKind {
519
- return event.pixelX !== undefined && event.pixelY !== undefined && getRendererCellMetrics(renderer)
520
- ? "pixel"
521
- : "cell";
522
- }
523
-
524
- function buildBlankPlotLines(width: number, height: number): string[] {
525
- return Array.from({ length: height }, () => " ".repeat(width));
526
- }
527
-
528
- function getMaxPanOffset(history: PricePoint[], zoomLevel: number): number {
529
- const visibleCount = getVisiblePointCount(history.length, zoomLevel);
530
- return Math.max(history.length - visibleCount, 0);
531
- }
532
-
533
- function attachViewportResolution(
534
- view: StockChartViewportState,
535
- resolution: ChartResolution,
536
- ): ResolutionAwareViewportState {
537
- return {
538
- ...view,
539
- resolution,
540
- };
541
- }
542
-
543
- function stripViewportResolution(view: ResolutionAwareViewportState): StockChartViewportState {
544
- const { resolution: _resolution, ...viewport } = view;
545
- return viewport;
546
- }
547
-
548
- function resolveViewportPresetSelection(
549
- view: StockChartViewportState,
550
- presetRange: TimeRange,
551
- supportMaxRange: TimeRange | null = null,
552
- ): StockChartViewportState {
553
- return stripViewportResolution(
554
- resolvePresetSelection(attachViewportResolution(view, "auto"), presetRange, supportMaxRange),
555
- );
556
- }
557
-
558
- function resolveViewportResolutionSelection(
559
- view: StockChartViewportState,
560
- resolution: ChartResolution,
561
- support: ReadonlyMap<ManualChartResolution, TimeRange>,
562
- visibleWindow: DateWindowRange | null,
563
- ): StockChartViewportState | null {
564
- const nextView = resolveResolutionSelection(
565
- attachViewportResolution(view, resolution),
566
- resolution,
567
- support,
568
- visibleWindow,
569
- );
570
- return nextView ? stripViewportResolution(nextView) : null;
571
- }
572
-
573
- function resolveViewportStoredSelection(
574
- view: StockChartViewportState,
575
- presetRange: TimeRange,
576
- resolution: ChartResolution,
577
- support: ReadonlyMap<ManualChartResolution, TimeRange>,
578
- ): StockChartViewportState {
579
- return stripViewportResolution(
580
- resolveStoredChartSelection(attachViewportResolution(view, resolution), presetRange, resolution, support),
581
- );
582
- }
583
-
584
- function clearAutoViewportState(view: StockChartViewportState): StockChartViewportState {
585
- const clearedPreset = clearActivePreset(view);
586
- if (clearedPreset.panOffset === 0 && clearedPreset.zoomLevel === 1) {
587
- return clearedPreset;
588
- }
589
- return {
590
- ...clearedPreset,
591
- panOffset: 0,
592
- zoomLevel: 1,
593
- };
594
- }
595
-
596
- function buildBufferedDetailWindow(
597
- requestedWindow: DateWindowRange | null | undefined,
598
- maxRange: TimeRange,
599
- bounds: DateWindowRange | null | undefined,
600
- minimumSpanMs: number,
601
- minimumBufferRange: TimeRange | null = null,
602
- ): DateWindowRange | null {
603
- if (!requestedWindow?.end) return null;
604
-
605
- let bufferRange = maxRange;
606
- const maxRangeIndex = TIME_RANGE_ORDER.indexOf(maxRange);
607
- if (maxRangeIndex >= 0) {
608
- for (const candidate of TIME_RANGE_ORDER.slice(0, maxRangeIndex + 1)) {
609
- if (requestedWindow.start && subtractTimeRange(requestedWindow.end, candidate).getTime() <= requestedWindow.start.getTime()) {
610
- bufferRange = candidate;
611
- break;
612
- }
613
- }
614
- }
615
- if (minimumBufferRange) {
616
- bufferRange = maxTimeRange(bufferRange, clampTimeRangeToMaxRange(minimumBufferRange, maxRange));
617
- }
618
-
619
- return clampDateWindowToBounds(
620
- {
621
- start: subtractTimeRange(requestedWindow.end, bufferRange),
622
- end: requestedWindow.end,
623
- },
624
- bounds,
625
- minimumSpanMs,
626
- );
627
- }
628
-
629
- function getWindowPoints(points: readonly PricePoint[], requestedWindow: DateWindowRange | null | undefined): PricePoint[] {
630
- if (!requestedWindow?.start || !requestedWindow.end || points.length === 0) return [...points];
631
- const startMs = requestedWindow.start.getTime();
632
- const endMs = requestedWindow.end.getTime();
633
- return points.filter((point) => {
634
- const pointMs = coerceChartDate(point.date as Date | string | number).getTime();
635
- return pointMs >= startMs && pointMs <= endMs;
636
- });
637
- }
638
-
639
- function getMaximumDateGapMs(dates: readonly Date[]): number {
640
- if (dates.length < 2) return 0;
641
-
642
- let maximumGapMs = 0;
643
- for (let index = 1; index < dates.length; index += 1) {
644
- const previousDate = dates[index - 1];
645
- const currentDate = dates[index];
646
- if (!previousDate || !currentDate) continue;
647
- maximumGapMs = Math.max(maximumGapMs, currentDate.getTime() - previousDate.getTime());
648
- }
649
-
650
- return maximumGapMs;
651
- }
652
-
653
- function isSeriesCompatibleWithRequest(
654
- points: readonly PricePoint[] | null | undefined,
655
- requestedWindow: DateWindowRange | null | undefined,
656
- resolution: ManualChartResolution | null,
657
- ): boolean {
658
- if (!requestedWindow?.start || !requestedWindow.end || !resolution || !points?.length) return false;
659
-
660
- const windowPoints = getWindowPoints(points, requestedWindow);
661
- if (windowPoints.length === 0) return false;
662
-
663
- const expectedStepMs = CHART_RESOLUTION_STEP_MS[resolution];
664
- if (!expectedStepMs) return false;
665
-
666
- const windowStartMs = requestedWindow.start.getTime();
667
- const windowEndMs = requestedWindow.end.getTime();
668
- const requestedSpanMs = Math.max(windowEndMs - windowStartMs, 0);
669
- const allDates = getPointDates(points);
670
- const windowDates = getPointDates(windowPoints);
671
- const firstPointMs = windowDates[0]!.getTime();
672
- const lastPointMs = windowDates[windowDates.length - 1]!.getTime();
673
- const startGapMs = Math.max(firstPointMs - windowStartMs, 0);
674
- const endGapMs = Math.max(windowEndMs - lastPointMs, 0);
675
- const sessionGapAllowanceMs = isIntradayResolution(resolution)
676
- ? Math.max(expectedStepMs * 8, getMaximumDateGapMs(allDates) + expectedStepMs)
677
- : expectedStepMs * 8;
678
-
679
- if (windowPoints.length === 1) {
680
- return requestedSpanMs <= expectedStepMs * 2
681
- && startGapMs <= sessionGapAllowanceMs
682
- && endGapMs <= sessionGapAllowanceMs;
683
- }
684
-
685
- const actualStepMs = getMinimumDateStepMs(windowDates);
686
- return actualStepMs <= expectedStepMs * 4
687
- && startGapMs <= sessionGapAllowanceMs
688
- && endGapMs <= sessionGapAllowanceMs;
689
- }
690
-
691
- function getExpectedPointCountForWindow(
692
- requestedWindow: DateWindowRange | null | undefined,
693
- resolution: ManualChartResolution | null,
694
- ): number {
695
- if (!requestedWindow?.start || !requestedWindow.end || !resolution) return 0;
696
- const expectedStepMs = CHART_RESOLUTION_STEP_MS[resolution];
697
- if (!expectedStepMs) return 0;
698
-
699
- const spanMs = Math.max(requestedWindow.end.getTime() - requestedWindow.start.getTime(), 0);
700
- return Math.max(spanMs / expectedStepMs, 1);
701
- }
702
-
703
- function getMinimumAutoRenderablePointCount(
704
- requestedWindow: DateWindowRange | null | undefined,
705
- targetResolution: ManualChartResolution | null,
706
- ): number {
707
- const expectedPointCount = getExpectedPointCountForWindow(requestedWindow, targetResolution);
708
- if (!Number.isFinite(expectedPointCount) || expectedPointCount <= 0) return 2;
709
- return clamp(Math.ceil(expectedPointCount / 24), 2, 8);
710
- }
711
-
712
- function isSeriesAcceptedForRequest(
713
- points: readonly PricePoint[] | null | undefined,
714
- requestedWindow: DateWindowRange | null | undefined,
715
- resolution: ManualChartResolution | null,
716
- options?: {
717
- requireAutoDensity?: boolean;
718
- targetResolution?: ManualChartResolution | null;
719
- },
720
- ): boolean {
721
- if (!isSeriesCompatibleWithRequest(points, requestedWindow, resolution)) return false;
722
- if (!options?.requireAutoDensity) return true;
723
-
724
- const windowPoints = getWindowPoints(points ?? [], requestedWindow);
725
- if (windowPoints.length === 0) return false;
726
-
727
- const targetResolution = options.targetResolution ?? resolution;
728
- const minimumPointCount = getMinimumAutoRenderablePointCount(requestedWindow, targetResolution);
729
- return windowPoints.length >= minimumPointCount;
730
- }
731
-
732
- export function resolveAutoDisplayState(options: {
733
- shouldUseRenderedAutoView: boolean;
734
- renderedAutoView: AutoRenderedView | null;
735
- isRenderedAutoViewUpdating: boolean;
736
- plannedRenderBodyState: ChartBodyState<PricePoint[]>;
737
- plannedResolvedManualResolution: ManualChartResolution | null;
738
- plannedDateWindow: DateWindowRange | null;
739
- }): AutoDisplayState {
740
- const {
741
- shouldUseRenderedAutoView,
742
- renderedAutoView,
743
- isRenderedAutoViewUpdating,
744
- plannedRenderBodyState,
745
- plannedResolvedManualResolution,
746
- plannedDateWindow,
747
- } = options;
748
-
749
- if (shouldUseRenderedAutoView && renderedAutoView) {
750
- return {
751
- bodyState: {
752
- data: renderedAutoView.data,
753
- blocking: false,
754
- updating: isRenderedAutoViewUpdating,
755
- emptyMessage: null,
756
- errorMessage: null,
757
- },
758
- resolution: renderedAutoView.resolution,
759
- window: renderedAutoView.window,
760
- };
761
- }
762
-
763
- return {
764
- bodyState: plannedRenderBodyState,
765
- resolution: plannedResolvedManualResolution,
766
- window: plannedDateWindow,
767
- };
768
- }
769
-
770
- function buildDateWindowFromIndices(dates: readonly Date[], startIdx: number, endIdx: number): DateWindowRange | null {
771
- if (dates.length === 0 || endIdx <= startIdx) return null;
772
- return {
773
- start: dates[startIdx] ?? null,
774
- end: dates[endIdx - 1] ?? null,
775
- };
776
- }
777
-
778
- function inferAutoZoomStepMs(
779
- dates: readonly Date[],
780
- window: Pick<ReturnType<typeof buildVisibleDateWindowFromRange>, "startIdx" | "endIdx">,
781
- fallbackWindow: DateWindowRange,
782
- ): number {
783
- const visibleCount = window.endIdx - window.startIdx;
784
- if (visibleCount >= 2) {
785
- const start = dates[window.startIdx]?.getTime() ?? null;
786
- const end = dates[window.endIdx - 1]?.getTime() ?? null;
787
- if (start !== null && end !== null && end > start) {
788
- return Math.max((end - start) / Math.max(visibleCount - 1, 1), 1);
789
- }
790
- }
791
-
792
- if (dates.length >= 2) {
793
- return Math.max(getMinimumDateStepMs(dates), 1);
794
- }
795
-
796
- if (fallbackWindow.start && fallbackWindow.end) {
797
- return Math.max(fallbackWindow.end.getTime() - fallbackWindow.start.getTime(), 1);
798
- }
799
-
800
- return 1;
801
- }
802
-
803
- export function resolveAutoZoomWindow(options: {
804
- historyPoints: readonly PricePoint[];
805
- boundsDates: readonly Date[];
806
- currentWindow: DateWindowRange | null | undefined;
807
- direction: "in" | "out";
808
- anchorRatio: number;
809
- zoomFactor?: number;
810
- }): DateWindowRange | null {
811
- const {
812
- historyPoints,
813
- boundsDates,
814
- currentWindow,
815
- direction,
816
- anchorRatio,
817
- zoomFactor = 1.5,
818
- } = options;
819
-
820
- if (!currentWindow?.start || !currentWindow.end) return currentWindow ?? null;
821
-
822
- const historyDates = getPointDates(historyPoints);
823
- if (historyDates.length === 0) return currentWindow;
824
-
825
- const currentHistoryWindow = buildVisibleDateWindowFromRange(historyDates, currentWindow, 0);
826
- const canExpandWithinHistory = direction === "out"
827
- && (
828
- currentHistoryWindow.startIdx > 0
829
- || currentHistoryWindow.endIdx < historyDates.length
830
- );
831
-
832
- const navigationDates = direction === "out" && !canExpandWithinHistory && boundsDates.length > 0
833
- ? boundsDates
834
- : historyDates;
835
- const currentNavigationWindow = buildVisibleDateWindowFromRange(navigationDates, currentWindow, 0);
836
- const currentVisibleCount = currentNavigationWindow.endIdx - currentNavigationWindow.startIdx;
837
-
838
- if (currentVisibleCount <= 0) {
839
- return currentWindow;
840
- }
841
-
842
- const ratio = clamp(anchorRatio, 0, 1);
843
- let targetVisibleCount: number;
844
- if (direction === "in") {
845
- if (currentVisibleCount <= 2) {
846
- const currentSpanMs = Math.max(currentWindow.end.getTime() - currentWindow.start.getTime(), 1);
847
- const targetSpanMs = Math.max(currentSpanMs / zoomFactor, 1);
848
- const anchorMs = currentWindow.start.getTime() + currentSpanMs * ratio;
849
- const nextStartMs = anchorMs - targetSpanMs * ratio;
850
- return {
851
- start: new Date(nextStartMs),
852
- end: new Date(nextStartMs + targetSpanMs),
853
- };
854
- }
855
- targetVisibleCount = Math.max(2, Math.floor(currentVisibleCount / zoomFactor));
856
- if (targetVisibleCount >= currentVisibleCount) {
857
- targetVisibleCount = currentVisibleCount - 1;
858
- }
859
- } else {
860
- targetVisibleCount = Math.ceil(currentVisibleCount * zoomFactor);
861
- if (targetVisibleCount <= currentVisibleCount) {
862
- targetVisibleCount = currentVisibleCount + 1;
863
- }
864
-
865
- if (targetVisibleCount > navigationDates.length) {
866
- const stepMs = inferAutoZoomStepMs(navigationDates, currentNavigationWindow, currentWindow);
867
- const targetSpanMs = Math.max(stepMs * Math.max(targetVisibleCount - 1, 1), stepMs);
868
- const currentSpanMs = Math.max(currentWindow.end.getTime() - currentWindow.start.getTime(), stepMs);
869
- const anchorMs = currentWindow.start.getTime() + currentSpanMs * ratio;
870
- const nextStartMs = anchorMs - targetSpanMs * ratio;
871
-
872
- return {
873
- start: new Date(nextStartMs),
874
- end: new Date(nextStartMs + targetSpanMs),
875
- };
876
- }
877
- }
878
-
879
- const anchorIndex = currentNavigationWindow.startIdx + ratio * Math.max(currentVisibleCount - 1, 0);
880
- let nextStartIdx = clamp(
881
- Math.round(anchorIndex - ratio * Math.max(targetVisibleCount - 1, 0)),
882
- 0,
883
- Math.max(navigationDates.length - targetVisibleCount, 0),
884
- );
885
- let nextEndIdx = Math.min(nextStartIdx + targetVisibleCount, navigationDates.length);
886
-
887
- if (nextStartIdx === currentNavigationWindow.startIdx && nextEndIdx === currentNavigationWindow.endIdx) {
888
- if (direction === "in" && currentVisibleCount > 2) {
889
- nextStartIdx = Math.min(currentNavigationWindow.startIdx + 1, currentNavigationWindow.endIdx - 2);
890
- nextEndIdx = currentNavigationWindow.endIdx;
891
- } else if (direction === "out" && currentNavigationWindow.startIdx > 0) {
892
- nextStartIdx = currentNavigationWindow.startIdx - 1;
893
- nextEndIdx = currentNavigationWindow.endIdx;
894
- } else if (direction === "out" && currentNavigationWindow.endIdx < navigationDates.length) {
895
- nextStartIdx = currentNavigationWindow.startIdx;
896
- nextEndIdx = currentNavigationWindow.endIdx + 1;
897
- }
898
- }
899
-
900
- return buildDateWindowFromIndices(navigationDates, nextStartIdx, nextEndIdx) ?? currentWindow;
901
- }
902
-
903
- function isDateWindowReachableByAnchoredRange(
904
- window: DateWindowRange | null | undefined,
905
- latestDate: Date | null | undefined,
906
- range: TimeRange,
907
- ): boolean {
908
- if (!window?.start || !window.end || !latestDate) return false;
909
- const threshold = subtractTimeRange(latestDate, range).getTime();
910
- return window.start.getTime() >= threshold && window.end.getTime() <= latestDate.getTime();
911
- }
912
-
913
- function getMinimumAnchoredBufferRange(
914
- window: DateWindowRange | null | undefined,
915
- latestDate: Date | null | undefined,
916
- maxRange: TimeRange,
917
- ): TimeRange | null {
918
- for (const candidate of TIME_RANGE_ORDER) {
919
- if (candidate === "ALL" || TIME_RANGE_ORDER.indexOf(candidate) > TIME_RANGE_ORDER.indexOf(maxRange)) break;
920
- if (isDateWindowReachableByAnchoredRange(window, latestDate, candidate)) {
921
- return candidate;
922
- }
923
- }
924
-
925
- return isDateWindowReachableByAnchoredRange(window, latestDate, maxRange)
926
- ? maxRange
927
- : null;
928
- }
929
-
930
- function buildResolvedChartRequestPlan(options: {
931
- compact?: boolean;
932
- historyOverride?: PricePoint[] | null;
933
- instrumentRef: ReturnType<typeof instrumentFromTicker>;
934
- requestedWindow: DateWindowRange | null;
935
- effectiveResolution: ChartResolution;
936
- effectiveManualResolution: ManualChartResolution | null;
937
- bounds: DateWindowRange | null;
938
- bufferRange: TimeRange;
939
- minimumBufferRange?: TimeRange | null;
940
- support: ReadonlyMap<ManualChartResolution, TimeRange>;
941
- hasResolutionHistoryApi: boolean;
942
- hasDetailedHistoryApi: boolean;
943
- minimumSpanMs: number;
944
- }): ResolvedChartRequestPlan {
945
- const {
946
- compact,
947
- historyOverride,
948
- instrumentRef,
949
- requestedWindow,
950
- effectiveResolution,
951
- effectiveManualResolution,
952
- bounds,
953
- bufferRange,
954
- minimumBufferRange = null,
955
- support,
956
- hasResolutionHistoryApi,
957
- hasDetailedHistoryApi,
958
- minimumSpanMs,
959
- } = options;
960
-
961
- if (historyOverride) {
962
- return {
963
- effectiveResolution: effectiveManualResolution,
964
- requestedWindow,
965
- resolutionRequest: null,
966
- detailRequest: null,
967
- unsupportedMessage: null,
968
- };
969
- }
970
-
971
- if (compact || !instrumentRef || !requestedWindow?.start || !requestedWindow.end || !effectiveManualResolution) {
972
- return {
973
- effectiveResolution: effectiveManualResolution,
974
- requestedWindow,
975
- resolutionRequest: null,
976
- detailRequest: null,
977
- unsupportedMessage: null,
978
- };
979
- }
980
-
981
- const maxRange = getSupportMaxRange(support, effectiveManualResolution);
982
- const latestDate = bounds?.end ?? null;
983
- let resolutionRequest: ChartRequest | null = null;
984
- if (hasResolutionHistoryApi && maxRange && latestDate) {
985
- const baseAnchoredRange = getMinimumAnchoredBufferRange(requestedWindow, latestDate, maxRange);
986
- const minimumAnchoredRange = minimumBufferRange
987
- ? clampTimeRangeToMaxRange(minimumBufferRange, maxRange)
988
- : null;
989
- const anchoredRange = baseAnchoredRange && minimumAnchoredRange
990
- ? maxTimeRange(baseAnchoredRange, minimumAnchoredRange)
991
- : baseAnchoredRange;
992
- if (anchoredRange) {
993
- resolutionRequest = {
994
- instrument: instrumentRef,
995
- bufferRange: anchoredRange,
996
- granularity: "resolution",
997
- resolution: effectiveManualResolution,
998
- };
999
- }
1000
- }
1001
-
1002
- let detailRequest: ChartRequest | null = null;
1003
- if (hasDetailedHistoryApi && maxRange) {
1004
- const bufferedWindow = buildBufferedDetailWindow(requestedWindow, maxRange, bounds, minimumSpanMs, minimumBufferRange);
1005
- if (bufferedWindow?.start && bufferedWindow.end) {
1006
- detailRequest = {
1007
- instrument: instrumentRef,
1008
- bufferRange,
1009
- granularity: "detail",
1010
- startDate: bufferedWindow.start,
1011
- endDate: bufferedWindow.end,
1012
- barSize: effectiveManualResolution,
1013
- };
1014
- }
1015
- }
1016
-
1017
- return {
1018
- effectiveResolution: effectiveManualResolution,
1019
- requestedWindow,
1020
- resolutionRequest,
1021
- detailRequest,
1022
- unsupportedMessage: effectiveResolution !== "auto" && !resolutionRequest && !detailRequest
1023
- ? `No ${getChartResolutionLabel(effectiveManualResolution)} history available for this window.`
1024
- : null,
1025
- };
1026
- }
1027
-
1028
- function buildResolutionFallbackChain(
1029
- startResolution: ManualChartResolution | null,
1030
- range: TimeRange,
1031
- support: ReadonlyMap<ManualChartResolution, TimeRange>,
1032
- ): ManualChartResolution[] {
1033
- if (!startResolution) return [];
1034
-
1035
- const chain: ManualChartResolution[] = [];
1036
- let current: ManualChartResolution | null = startResolution;
1037
- while (current && !chain.includes(current)) {
1038
- chain.push(current);
1039
- const nextFallback = getNextFallbackResolution(range, current, support);
1040
- current = nextFallback && nextFallback !== "auto" ? nextFallback : null;
1041
- }
1042
-
1043
- return chain;
1044
- }
1045
-
1046
- function dedupeChartRequests(requests: Array<ChartRequest | null | undefined>): ChartRequest[] {
1047
- const uniqueRequests: ChartRequest[] = [];
1048
- const seenKeys = new Set<string>();
1049
- for (const request of requests) {
1050
- if (!request) continue;
1051
- const key = buildChartKey(request);
1052
- if (seenKeys.has(key)) continue;
1053
- seenKeys.add(key);
1054
- uniqueRequests.push(request);
1055
- }
1056
- return uniqueRequests;
1057
- }
1058
-
1059
- function applyZoomAroundAnchor(
1060
- view: StockChartViewportState,
1061
- nextZoomLevel: number,
1062
- anchorRatio: number,
1063
- history: PricePoint[],
1064
- ): StockChartViewportState {
1065
- if (history.length === 0) return view;
1066
-
1067
- const nextZoom = resolveAnchoredChartZoom(
1068
- history.length,
1069
- view.zoomLevel,
1070
- view.panOffset,
1071
- nextZoomLevel,
1072
- anchorRatio,
1073
- );
1074
-
1075
- return {
1076
- ...clearActivePreset(view),
1077
- ...nextZoom,
1078
- };
1079
- }
1080
-
1081
- function buildNativeBitmapKey(
1082
- pointCount: number,
1083
- points: PricePoint[],
1084
- pixelWidth: number,
1085
- pixelHeight: number,
1086
- mode: ChartRenderMode,
1087
- showVolume: boolean,
1088
- paletteId: string,
1089
- indicatorKey: string,
1090
- marketSessionKey: string,
1091
- ): string {
1092
- const fingerprint = points
1093
- .map((point) => {
1094
- const date = point.date instanceof Date ? point.date.getTime() : new Date(point.date).getTime();
1095
- return `${date}:${point.open}:${point.high}:${point.low}:${point.close}:${point.volume ?? 0}`;
1096
- })
1097
- .join("|");
1098
- return [pointCount, pixelWidth, pixelHeight, mode, showVolume ? "1" : "0", paletteId, indicatorKey, marketSessionKey, fingerprint].join("::");
1099
- }
1100
-
1101
- function buildNativeCrosshairBitmapKey(
1102
- pixelWidth: number,
1103
- pixelHeight: number,
1104
- chartHeight: number,
1105
- chartRows: number,
1106
- crosshairColor: string,
1107
- pixelX: number | null,
1108
- pixelY: number | null,
1109
- ): string {
1110
- const cursorKey = pixelX === null || pixelY === null
1111
- ? "cursor:none"
1112
- : `cursor:${pixelX.toFixed(3)}:${pixelY.toFixed(3)}`;
1113
- return [pixelWidth, pixelHeight, chartHeight, chartRows, crosshairColor, cursorKey].join("::");
1114
- }
1115
-
1116
- function resolveSelectionCursorX(
1117
- cellX: number,
1118
- pointCount: number,
1119
- width: number,
1120
- mode: ChartRenderMode,
1121
- ): number | null {
1122
- if (pointCount <= 0 || width <= 0) return null;
1123
-
1124
- let bestIndex = pointCount - 1;
1125
- let bestDistance = Number.POSITIVE_INFINITY;
1126
- for (let index = 0; index < pointCount; index += 1) {
1127
- const pointColumn = getPointTerminalColumn(index, pointCount, width, mode);
1128
- const distance = Math.abs(pointColumn - cellX);
1129
- if (distance <= bestDistance) {
1130
- bestDistance = distance;
1131
- bestIndex = index;
1132
- }
1133
- }
1134
-
1135
- return getPointTerminalColumn(bestIndex, pointCount, width, mode);
1136
- }
1137
-
1138
- function resolveSelectionCursor(
1139
- pointer: LocalPlotPointer,
1140
- pointCount: number,
1141
- width: number,
1142
- mode: ChartRenderMode,
1143
- ): { cursorX: number | null; cursorY: number | null } {
1144
- if (!pointer.hasPixelPrecision) {
1145
- return {
1146
- cursorX: pointer.cellX,
1147
- cursorY: pointer.cellY,
1148
- };
1149
- }
1150
-
1151
- return {
1152
- cursorX: resolveSelectionCursorX(pointer.cellX, pointCount, width, mode),
1153
- cursorY: null,
1154
- };
1155
- }
1156
-
1157
- function getIndicatorColor(index: number): string {
1158
- const indicatorColors = [
1159
- colors.negative,
1160
- colors.positive,
1161
- colors.borderFocused,
1162
- colors.warning,
1163
- colors.textBright,
1164
- colors.neutral,
1165
- colors.textMuted,
1166
- ];
1167
- return indicatorColors[((index % indicatorColors.length) + indicatorColors.length) % indicatorColors.length]!;
1168
- }
1169
-
1170
- function getIndicatorWarmupPeriod(config: IndicatorConfig): number {
1171
- const periods = [
1172
- ...(config.sma ?? []),
1173
- ...(config.ema ?? []),
1174
- config.bollinger?.period,
1175
- config.rsi ?? undefined,
1176
- config.macd ? config.macd.slow + config.macd.signal : undefined,
1177
- ].filter((period): period is number => typeof period === "number" && Number.isFinite(period) && period > 0);
1178
-
1179
- return periods.length > 0 ? Math.max(...periods) : 0;
1180
- }
1181
-
1182
- export function resolveIndicatorBufferRange(
1183
- visibleRange: TimeRange,
1184
- currentBufferRange: TimeRange,
1185
- config: IndicatorConfig,
1186
- ): TimeRange {
1187
- const warmupPeriod = getIndicatorWarmupPeriod(config);
1188
- if (warmupPeriod <= 0) return currentBufferRange;
1189
-
1190
- const visibleDays = RANGE_DAYS[visibleRange];
1191
- if (!Number.isFinite(visibleDays)) return currentBufferRange;
1192
-
1193
- const targetDays = visibleDays + warmupPeriod;
1194
- const warmupRange = TIME_RANGE_ORDER.find((range) => RANGE_DAYS[range] >= targetDays) ?? "ALL";
1195
- return maxTimeRange(currentBufferRange, warmupRange);
1196
- }
1197
-
1198
- function getPricePointTime(point: Pick<PricePoint, "date"> | Pick<ProjectedChartPoint, "date">): number {
1199
- return coerceChartDate(point.date as Date | string | number).getTime();
1200
- }
1201
-
1202
- function buildProjectedSourceIndexMap(
1203
- sourcePoints: readonly PricePoint[],
1204
- projectedPoints: readonly ProjectedChartPoint[],
1205
- sourceIndexOffset = 0,
1206
- ): Map<number, number> {
1207
- const sourceIndexByTime = new Map<number, number>();
1208
- sourcePoints.forEach((point, index) => {
1209
- sourceIndexByTime.set(getPricePointTime(point), sourceIndexOffset + index);
1210
- });
1211
-
1212
- const projectedIndexBySourceIndex = new Map<number, number>();
1213
- projectedPoints.forEach((point, projectedIndex) => {
1214
- const sourceIndex = sourceIndexByTime.get(getPricePointTime(point));
1215
- if (sourceIndex !== undefined) {
1216
- projectedIndexBySourceIndex.set(sourceIndex, projectedIndex);
1217
- }
1218
- });
1219
- return projectedIndexBySourceIndex;
1220
- }
1221
-
1222
- function findPointBySourceIndex<TPoint extends { index: number }>(
1223
- points: readonly TPoint[],
1224
- sourceIndex: number,
1225
- ): TPoint | null {
1226
- let low = 0;
1227
- let high = points.length - 1;
1228
-
1229
- while (low <= high) {
1230
- const mid = Math.floor((low + high) / 2);
1231
- const point = points[mid]!;
1232
- if (point.index === sourceIndex) return point;
1233
- if (point.index < sourceIndex) {
1234
- low = mid + 1;
1235
- } else {
1236
- high = mid - 1;
1237
- }
1238
- }
1239
-
1240
- return null;
1241
- }
1242
-
1243
- function reindexOverlayPoints(
1244
- points: readonly OverlayPoint[],
1245
- projectedIndexBySourceIndex: ReadonlyMap<number, number>,
1246
- ): OverlayPoint[] {
1247
- const reindexed: OverlayPoint[] = [];
1248
- projectedIndexBySourceIndex.forEach((projectedIndex, sourceIndex) => {
1249
- const point = findPointBySourceIndex(points, sourceIndex);
1250
- if (point) reindexed.push({ index: projectedIndex, value: point.value });
1251
- });
1252
- return reindexed;
1253
- }
1254
-
1255
- function reindexOscillatorPoints(
1256
- points: readonly OscillatorPoint[],
1257
- projectedIndexBySourceIndex: ReadonlyMap<number, number>,
1258
- ): OscillatorPoint[] {
1259
- const reindexed: OscillatorPoint[] = [];
1260
- projectedIndexBySourceIndex.forEach((projectedIndex, sourceIndex) => {
1261
- const point = findPointBySourceIndex(points, sourceIndex);
1262
- if (point) reindexed.push({ index: projectedIndex, value: point.value });
1263
- });
1264
- return reindexed;
1265
- }
1266
-
1267
- function reindexMacdResult(
1268
- result: MacdResult | null,
1269
- projectedIndexBySourceIndex: ReadonlyMap<number, number>,
1270
- ): MacdResult | null {
1271
- if (!result) return null;
1272
- return {
1273
- macd: reindexOscillatorPoints(result.macd, projectedIndexBySourceIndex),
1274
- signal: reindexOscillatorPoints(result.signal, projectedIndexBySourceIndex),
1275
- histogram: reindexOscillatorPoints(result.histogram, projectedIndexBySourceIndex),
1276
- };
1277
- }
1278
-
1279
- function computeIndicatorOverlays(
1280
- closes: number[],
1281
- config: IndicatorConfig,
1282
- ): ChartIndicatorOverlays {
1283
- let colorIdx = 0;
1284
- const nextColor = () => getIndicatorColor(colorIdx++);
1285
-
1286
- const smaLines = (config.sma ?? []).map((period) => ({
1287
- period,
1288
- points: computeSMA(closes, period),
1289
- color: nextColor(),
1290
- }));
1291
-
1292
- const emaLines = (config.ema ?? []).map((period) => ({
1293
- period,
1294
- points: computeEMA(closes, period),
1295
- color: nextColor(),
1296
- }));
1297
-
1298
- const bollinger = config.bollinger
1299
- ? { ...computeBollingerBands(closes, config.bollinger.period, config.bollinger.stdDev), color: nextColor() }
1300
- : null;
1301
-
1302
- const rsi = config.rsi ? computeRSI(closes, config.rsi) : null;
1303
- const macd = config.macd
1304
- ? computeMACD(closes, config.macd.fast, config.macd.slow, config.macd.signal)
1305
- : null;
1306
-
1307
- return { smaLines, emaLines, bollinger, rsi, macd };
1308
- }
1309
-
1310
- export function computeProjectedIndicatorOverlays(
1311
- sourcePoints: readonly PricePoint[],
1312
- projectedPoints: readonly ProjectedChartPoint[],
1313
- config: IndicatorConfig,
1314
- ): ChartIndicatorOverlays {
1315
- const closes = sourcePoints.map((point) => point.close);
1316
- const overlays = computeIndicatorOverlays(closes, config);
1317
- return reindexIndicatorOverlaysForProjection(overlays, sourcePoints, projectedPoints);
1318
- }
1319
-
1320
- function reindexIndicatorOverlaysForProjection(
1321
- overlays: ChartIndicatorOverlays,
1322
- sourcePoints: readonly PricePoint[],
1323
- projectedPoints: readonly ProjectedChartPoint[],
1324
- sourceIndexOffset = 0,
1325
- ): ChartIndicatorOverlays {
1326
- const projectedIndexBySourceIndex = buildProjectedSourceIndexMap(
1327
- sourcePoints,
1328
- projectedPoints,
1329
- sourceIndexOffset,
1330
- );
1331
-
1332
- return {
1333
- smaLines: overlays.smaLines.map((line) => ({
1334
- ...line,
1335
- points: reindexOverlayPoints(line.points, projectedIndexBySourceIndex),
1336
- })),
1337
- emaLines: overlays.emaLines.map((line) => ({
1338
- ...line,
1339
- points: reindexOverlayPoints(line.points, projectedIndexBySourceIndex),
1340
- })),
1341
- bollinger: overlays.bollinger
1342
- ? {
1343
- ...overlays.bollinger,
1344
- upper: reindexOverlayPoints(overlays.bollinger.upper, projectedIndexBySourceIndex),
1345
- middle: reindexOverlayPoints(overlays.bollinger.middle, projectedIndexBySourceIndex),
1346
- lower: reindexOverlayPoints(overlays.bollinger.lower, projectedIndexBySourceIndex),
1347
- }
1348
- : null,
1349
- rsi: overlays.rsi ? reindexOscillatorPoints(overlays.rsi, projectedIndexBySourceIndex) : null,
1350
- macd: reindexMacdResult(overlays.macd, projectedIndexBySourceIndex),
1351
- };
1352
- }
1353
-
1354
- function buildIndicatorRenderKey(indicators: ChartIndicatorOverlays | null): string {
1355
- if (!indicators) return "none";
1356
-
1357
- const pointKey = (points: readonly OverlayPoint[]) => {
1358
- const first = points[0];
1359
- const last = points[points.length - 1];
1360
- return first && last
1361
- ? `${points.length}:${first.index}:${first.value.toFixed(6)}:${last.index}:${last.value.toFixed(6)}`
1362
- : "0";
1363
- };
1364
-
1365
- return [
1366
- indicators.smaLines.map((line) => `sma:${line.period}:${line.color}:${pointKey(line.points)}`).join(","),
1367
- indicators.emaLines.map((line) => `ema:${line.period}:${line.color}:${pointKey(line.points)}`).join(","),
1368
- indicators.bollinger
1369
- ? [
1370
- `bb:${indicators.bollinger.color}`,
1371
- pointKey(indicators.bollinger.upper),
1372
- pointKey(indicators.bollinger.middle),
1373
- pointKey(indicators.bollinger.lower),
1374
- ].join(":")
1375
- : "",
1376
- ].join("|");
1377
- }
1378
-
1379
- function buildIndicatorConfigKey(config: IndicatorConfig): string {
1380
- return [
1381
- `sma:${(config.sma ?? []).join(",")}`,
1382
- `ema:${(config.ema ?? []).join(",")}`,
1383
- config.bollinger ? `bb:${config.bollinger.period}:${config.bollinger.stdDev}` : "bb:",
1384
- `rsi:${config.rsi ?? ""}`,
1385
- config.macd ? `macd:${config.macd.fast}:${config.macd.slow}:${config.macd.signal}` : "macd:",
1386
- ].join("|");
1387
- }
1388
-
1389
- function buildIndicatorSourceKey(
1390
- sourcePoints: readonly PricePoint[],
1391
- config: IndicatorConfig,
1392
- ): string {
1393
- const first = sourcePoints[0];
1394
- const last = sourcePoints[sourcePoints.length - 1];
1395
- return [
1396
- buildIndicatorConfigKey(config),
1397
- sourcePoints.length,
1398
- first ? getPricePointTime(first) : "",
1399
- first?.close ?? "",
1400
- last ? getPricePointTime(last) : "",
1401
- last?.close ?? "",
1402
- ].join(":");
1403
- }
1404
-
1405
- function buildIndicatorProjectionKey(options: {
1406
- sourceKey: string;
1407
- sourcePoints: readonly PricePoint[];
1408
- sourceIndexOffset: number;
1409
- projectedPoints: readonly ProjectedChartPoint[];
1410
- mode: ChartRenderMode;
1411
- }): string {
1412
- const firstSource = options.sourcePoints[0];
1413
- const lastSource = options.sourcePoints[options.sourcePoints.length - 1];
1414
- const firstProjected = options.projectedPoints[0];
1415
- const lastProjected = options.projectedPoints[options.projectedPoints.length - 1];
1416
-
1417
- return [
1418
- options.sourceKey,
1419
- options.sourceIndexOffset,
1420
- options.sourcePoints.length,
1421
- firstSource ? getPricePointTime(firstSource) : "",
1422
- lastSource ? getPricePointTime(lastSource) : "",
1423
- options.projectedPoints.length,
1424
- firstProjected ? getPricePointTime(firstProjected) : "",
1425
- lastProjected ? getPricePointTime(lastProjected) : "",
1426
- options.mode,
1427
- ].join(":");
1428
- }
1429
-
1430
- export function StockChart(props: StockChartProps) {
1431
- const { ticker, financials } = usePaneTicker();
1432
- return <ResolvedStockChart {...props} ticker={ticker} financials={financials} />;
1433
- }
1434
-
1435
- export const ResolvedStockChart = memo(function ResolvedStockChart({
1436
- width,
1437
- height,
1438
- focused,
1439
- interactive,
1440
- onActivate,
1441
- compact,
1442
- axisMode = "price",
1443
- historyOverride = null,
1444
- currencyOverride = null,
1445
- indicatorConfig: indicatorConfigOverride,
1446
- showVolume: showVolumeOverride,
1447
- footerControls,
1448
- footerHints,
1449
- ticker,
1450
- financials,
1451
- }: ResolvedStockChartProps) {
1452
- const themeColors = useThemeColors();
1453
- const renderer = useNativeRenderer();
1454
- const { canvasCharts, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1, fractionalViewport = false } = useUiCapabilities();
1455
- const dispatch = useAppDispatch();
1456
- const config = useAppSelector((state) => state.config);
1457
- const paneId = usePaneInstanceId();
1458
- const pane = usePaneInstance();
1459
- const nativeSurfaceManager = useMemo(() => getNativeSurfaceManager(renderer), [renderer]);
1460
- const defaultRenderMode = config.chartPreferences.defaultRenderMode;
1461
- const preferredRenderer = config.chartPreferences.renderer;
1462
- const [storedRangePreset] = usePaneSettingValue("chartRangePreset", DEFAULT_TICKER_CHART_RANGE_PRESET);
1463
- const [storedResolution] = usePaneSettingValue<ChartResolution>("chartResolution", DEFAULT_TICKER_CHART_RESOLUTION);
1464
- const [storedRenderMode, setStoredRenderMode] = usePaneSettingValue<ChartRenderMode>("chartRenderMode", defaultRenderMode);
1465
- const persistChartControls = usePersistChartControlSelection("chartRangePreset");
1466
- const [viewState, setViewState] = useState<StockChartViewportState>({
1467
- presetRange: compact ? "1Y" : storedRangePreset,
1468
- bufferRange: compact ? "1Y" : storedRangePreset,
1469
- activePreset: compact ? null : storedRangePreset,
1470
- panOffset: 0,
1471
- zoomLevel: 1,
1472
- cursorX: null,
1473
- cursorY: null,
1474
- renderMode: storedRenderMode,
1475
- });
1476
- // Read indicator config from the chart caller, falling back to legacy pane settings.
1477
- const indicatorConfig: IndicatorConfig = indicatorConfigOverride ?? ((pane?.settings?.indicators as IndicatorConfig) ?? EMPTY_INDICATOR_CONFIG);
1478
- const hasIndicators = !!(indicatorConfig.sma?.length || indicatorConfig.ema?.length || indicatorConfig.rsi || indicatorConfig.macd || indicatorConfig.bollinger);
1479
- const indicatorBufferRange = useMemo(
1480
- () => resolveIndicatorBufferRange(viewState.presetRange, viewState.bufferRange, indicatorConfig),
1481
- [indicatorConfig, viewState.bufferRange, viewState.presetRange],
1482
- );
1483
- const [requestedResolution, setRequestedResolution] = useState<ChartResolution>(
1484
- compact ? "auto" : storedResolution,
1485
- );
1486
- const [resolutionSupport, setResolutionSupport] = useState<ChartResolutionSupport[] | null>(null);
1487
- const supportMap = useMemo(() => buildChartResolutionSupportMap(resolutionSupport ?? []), [resolutionSupport]);
1488
- const [renderedAutoView, setRenderedAutoView] = useState<AutoRenderedView | null>(null);
1489
- const [pendingAutoWindowOverride, setPendingAutoWindowOverride] = useState<DateWindowRange | null>(null);
1490
- const [lastReadyRenderView, setLastReadyRenderView] = useState<CachedRenderedView | null>(null);
1491
- const showVolume = showVolumeOverride ?? !compact;
1492
- const [kittySupport, setKittySupport] = useState<boolean | null>(() => getCachedKittySupport(renderer));
1493
- const rendererState = resolveChartRendererState(preferredRenderer, kittySupport, renderer.resolution);
1494
- const effectiveRenderer: ResolvedChartRenderer = rendererState.renderer;
1495
- const useCanvasChart = canvasCharts && effectiveRenderer !== "kitty";
1496
- const showNativeUnavailable = rendererState.nativeUnavailable && !useCanvasChart;
1497
- const [displayCursor, setDisplayCursor] = useState<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
1498
- const [canvasBaseBitmapState, setCanvasBaseBitmapState] = useState<{ key: string; bitmap: NativeChartBitmap } | null>(null);
1499
- const plotRef = useRef<BoxRenderable | null>(null);
1500
- const nativeSurfaceScope = compact ? "compact" : "full";
1501
- const nativeBaseSurfaceId = useMemo(
1502
- () => `chart-surface:${paneId}:${nativeSurfaceScope}:base`,
1503
- [nativeSurfaceScope, paneId],
1504
- );
1505
- const nativeCrosshairSurfaceId = useMemo(
1506
- () => `chart-surface:${paneId}:${nativeSurfaceScope}:crosshair`,
1507
- [nativeSurfaceScope, paneId],
1508
- );
1509
- const dragRef = useRef<DragState | null>(null);
1510
- const lastNativeGeometryRef = useRef<{ rect: CellRect; visibleRect: CellRect | null } | null>(null);
1511
- const lastNativeBaseBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
1512
- const lastNativeCrosshairBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
1513
- const lastCanvasBaseBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
1514
- const displayCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
1515
- const targetCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
1516
- const mouseCrosshairDisabledRef = useRef(false);
1517
- const cursorMotionKindRef = useRef<ChartCursorMotionKind>("discrete");
1518
- const animationFrameRef = useRef<number | null>(null);
1519
- const pendingCanonicalResetRef = useRef(1);
1520
- const appliedCanonicalResetRef = useRef(0);
1521
- const pendingExpansionRef = useRef<PendingExpansionAction>(null);
1522
- const pendingAutoWindowRef = useRef<DateWindowRange | null>(null);
1523
- const scrollPanCellRemainderRef = useRef(0);
1524
- const lastAppliedStoredSelectionKeyRef = useRef<string | null>(null);
1525
-
1526
- const commitDisplayCursor = (next: DisplayCursorState) => {
1527
- displayCursorRef.current = next;
1528
- setDisplayCursor((current) => (sameDisplayCursorState(current, next) ? current : next));
1529
- };
1530
-
1531
- const stopDisplayCursorAnimation = () => {
1532
- if (animationFrameRef.current !== null) {
1533
- cancelAnimationFrameSafe(animationFrameRef.current);
1534
- animationFrameRef.current = null;
1535
- }
1536
- };
1537
-
1538
- const startDisplayCursorAnimation = () => {
1539
- if (animationFrameRef.current !== null) return;
1540
-
1541
- const tick = () => {
1542
- animationFrameRef.current = null;
1543
- const target = targetCursorRef.current;
1544
-
1545
- if (cursorMotionKindRef.current !== "cell") {
1546
- commitDisplayCursor(target);
1547
- return;
1548
- }
1549
-
1550
- const currentCell = toCursorPosition(displayCursorRef.current.cellX, displayCursorRef.current.cellY);
1551
- const targetCell = toCursorPosition(target.cellX, target.cellY);
1552
- const currentPixel = toCursorPosition(displayCursorRef.current.pixelX, displayCursorRef.current.pixelY);
1553
- const targetPixel = toCursorPosition(target.pixelX, target.pixelY);
1554
- const cellStep = stepCursorTowards(currentCell, targetCell);
1555
- const pixelStep = stepCursorTowards(currentPixel, targetPixel, undefined, PIXEL_CURSOR_SNAP_DISTANCE);
1556
- const next: DisplayCursorState = {
1557
- cellX: cellStep.next.x,
1558
- cellY: cellStep.next.y,
1559
- pixelX: pixelStep.next.x,
1560
- pixelY: pixelStep.next.y,
1561
- };
1562
- const settled = cellStep.settled && pixelStep.settled;
1563
- commitDisplayCursor(next);
1564
- if (!settled) {
1565
- animationFrameRef.current = requestAnimationFrameSafe(tick);
1566
- }
1567
- };
1568
-
1569
- animationFrameRef.current = requestAnimationFrameSafe(tick);
1570
- };
1571
-
1572
- const updateDisplayCursorTarget = (next: DisplayCursorState, motionKind: ChartCursorMotionKind) => {
1573
- cursorMotionKindRef.current = motionKind;
1574
- targetCursorRef.current = next;
1575
-
1576
- if (motionKind !== "cell" || next.cellX === null || next.cellY === null) {
1577
- stopDisplayCursorAnimation();
1578
- commitDisplayCursor(next);
1579
- return;
1580
- }
1581
-
1582
- if (displayCursorRef.current.cellX === null || displayCursorRef.current.cellY === null) {
1583
- commitDisplayCursor(next);
1584
- return;
1585
- }
1586
-
1587
- if (sameDisplayCursorState(displayCursorRef.current, next, CELL_CURSOR_SNAP_DISTANCE, PIXEL_CURSOR_SNAP_DISTANCE)) {
1588
- stopDisplayCursorAnimation();
1589
- commitDisplayCursor(next);
1590
- return;
1591
- }
1592
-
1593
- startDisplayCursorAnimation();
1594
- };
1595
-
1596
- useEffect(() => (
1597
- () => {
1598
- stopDisplayCursorAnimation();
1599
- lastNativeBaseBitmapRef.current = null;
1600
- lastNativeCrosshairBitmapRef.current = null;
1601
- nativeSurfaceManager.removeSurface(nativeBaseSurfaceId);
1602
- nativeSurfaceManager.removeSurface(nativeCrosshairSurfaceId);
1603
- }
1604
- ), [nativeBaseSurfaceId, nativeCrosshairSurfaceId, nativeSurfaceManager]);
1605
-
1606
- const instrumentRef = useMemo(
1607
- () => instrumentFromTicker(ticker, ticker?.metadata.ticker ?? null),
1608
- [ticker],
1609
- );
1610
- const dataProvider = getSharedMarketData();
1611
- const capabilityKey = instrumentRef ? [
1612
- instrumentRef.symbol,
1613
- instrumentRef.exchange ?? "",
1614
- instrumentRef.brokerId ?? "",
1615
- instrumentRef.brokerInstanceId ?? "",
1616
- instrumentRef.instrument?.conId ?? "",
1617
- ].join("|") : null;
1618
-
1619
- useEffect(() => {
1620
- if (compact || !instrumentRef) {
1621
- setResolutionSupport(null);
1622
- return;
1623
- }
1624
- if (!dataProvider?.getChartResolutionSupport && !dataProvider?.getChartResolutionCapabilities) {
1625
- setResolutionSupport(null);
1626
- return;
1627
- }
1628
-
1629
- let cancelled = false;
1630
- setResolutionSupport(null);
1631
- Promise.resolve(dataProvider.getChartResolutionSupport
1632
- ? dataProvider.getChartResolutionSupport(
1633
- instrumentRef.symbol,
1634
- instrumentRef.exchange ?? "",
1635
- {
1636
- brokerId: instrumentRef.brokerId,
1637
- brokerInstanceId: instrumentRef.brokerInstanceId,
1638
- instrument: instrumentRef.instrument ?? null,
1639
- },
1640
- )
1641
- : Promise.resolve(dataProvider.getChartResolutionCapabilities?.(
1642
- instrumentRef.symbol,
1643
- instrumentRef.exchange ?? "",
1644
- {
1645
- brokerId: instrumentRef.brokerId,
1646
- brokerInstanceId: instrumentRef.brokerInstanceId,
1647
- instrument: instrumentRef.instrument ?? null,
1648
- },
1649
- ) ?? []).then((capabilities) => normalizeChartResolutionSupport(
1650
- capabilities.map((resolution) => ({ resolution, maxRange: "ALL" })),
1651
- ))
1652
- ).then((support) => {
1653
- if (!cancelled) {
1654
- setResolutionSupport(support);
1655
- }
1656
- }).catch(() => {
1657
- if (!cancelled) {
1658
- setResolutionSupport(null);
1659
- }
1660
- });
1661
-
1662
- return () => {
1663
- cancelled = true;
1664
- };
1665
- }, [capabilityKey, compact, dataProvider, instrumentRef]);
1666
-
1667
- const effectiveResolutionSupport = useMemo<ChartResolutionSupport[]>(() => (
1668
- resolutionSupport ?? DEFAULT_VISIBLE_MANUAL_CHART_RESOLUTIONS.map((resolution) => ({ resolution, maxRange: "ALL" as const }))
1669
- ), [resolutionSupport]);
1670
- const selectionSupportMap = useMemo(
1671
- () => buildChartResolutionSupportMap(effectiveResolutionSupport),
1672
- [effectiveResolutionSupport],
1673
- );
1674
- const availableManualResolutions = resolutionSupport?.map((entry) => entry.resolution) ?? DEFAULT_VISIBLE_MANUAL_CHART_RESOLUTIONS;
1675
- useEffect(() => {
1676
- if (compact) return;
1677
- const storedSelectionKey = `${storedRangePreset}:${storedResolution}`;
1678
- if (lastAppliedStoredSelectionKeyRef.current === storedSelectionKey) return;
1679
-
1680
- lastAppliedStoredSelectionKeyRef.current = storedSelectionKey;
1681
- setRequestedResolution(storedResolution);
1682
- pendingCanonicalResetRef.current += 1;
1683
- pendingAutoWindowRef.current = null;
1684
- setPendingAutoWindowOverride(null);
1685
- if (storedResolution === "auto") {
1686
- setRenderedAutoView(null);
1687
- }
1688
- setViewState((current) => {
1689
- return storedResolution === "auto"
1690
- ? resolveViewportStoredSelection(current, storedRangePreset, storedResolution, selectionSupportMap)
1691
- : resolveViewportPresetSelection(
1692
- current,
1693
- storedRangePreset,
1694
- getSupportMaxRange(selectionSupportMap, storedResolution),
1695
- );
1696
- });
1697
- updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
1698
- }, [compact, selectionSupportMap, storedRangePreset, storedResolution]);
1699
- useEffect(() => {
1700
- if (compact) return;
1701
- setViewState((current) => (
1702
- current.renderMode === storedRenderMode ? current : { ...current, renderMode: storedRenderMode }
1703
- ));
1704
- }, [compact, storedRenderMode]);
1705
- const effectiveResolution: ChartResolution = !compact
1706
- && requestedResolution !== "auto"
1707
- && resolutionSupport !== null
1708
- && !supportMap.has(requestedResolution)
1709
- ? "auto"
1710
- : requestedResolution;
1711
- const resolutionChips = useMemo(
1712
- () => sortChartResolutions(["auto", ...availableManualResolutions] as ChartResolution[]),
1713
- [availableManualResolutions],
1714
- );
1715
-
1716
- const boundsChartEntry = useChartQuery(
1717
- !compact && instrumentRef
1718
- ? {
1719
- instrument: instrumentRef,
1720
- bufferRange: viewState.bufferRange,
1721
- granularity: "range",
1722
- }
1723
- : null,
1724
- );
1725
- const fallbackPriceHistory = historyOverride ?? financials?.priceHistory ?? [];
1726
- const rawBoundsBodyState = resolveChartBodyState(
1727
- boundsChartEntry,
1728
- (value) => Array.isArray(value) && value.length > 0,
1729
- "No price history available.",
1730
- );
1731
- const shouldUseBoundsFallback = !compact
1732
- && fallbackPriceHistory.length > 0
1733
- && (!boundsChartEntry || (rawBoundsBodyState.blocking && !rawBoundsBodyState.data?.length));
1734
- const boundsBodyState = shouldUseBoundsFallback
1735
- ? {
1736
- data: fallbackPriceHistory,
1737
- blocking: false,
1738
- updating: !!boundsChartEntry && rawBoundsBodyState.blocking,
1739
- emptyMessage: null,
1740
- errorMessage: null,
1741
- }
1742
- : rawBoundsBodyState;
1743
- const axisSectionWidthBudget = compact
1744
- ? axisMode === "percent" ? 11 : 8
1745
- : axisMode === "percent" ? 11 : 10;
1746
- const axisRightPadding = compact || fractionalViewport ? 0 : 1;
1747
- const minimumAxisWidth = axisMode === "percent" ? 5 : 4;
1748
- const axisGap = axisSectionWidthBudget > 0 ? fractionalViewport ? 0.5 : 1 : 0;
1749
- const minChartWidth = compact ? 12 : 20;
1750
- const measurementChartWidth = Math.max(width - axisSectionWidthBudget - axisGap, minChartWidth);
1751
- const headerRows = compact ? 0 : 2;
1752
- const helpRow = compact ? 1 : 0;
1753
- const timeAxisRow = 1;
1754
- // Native/canvas surfaces can cover the last physical row after pixel rounding;
1755
- // keep the text x-axis one row clear of the pane footer.
1756
- const nativeSurfaceGuardRow = !compact && fractionalViewport && (useCanvasChart || effectiveRenderer === "kitty") ? 1 : 0;
1757
- const volumeHeight = showVolume && !compact ? 3 : 0;
1758
- const chartHeight = Math.max(height - headerRows - helpRow - timeAxisRow - nativeSurfaceGuardRow, 4);
1759
- const chartCurrency = currencyOverride ?? financials?.quote?.currency ?? ticker?.metadata.currency ?? "USD";
1760
- const chartAssetCategory = ticker?.metadata.assetCategory;
1761
- const boundsHistory = compact
1762
- ? fallbackPriceHistory
1763
- : (boundsBodyState.data ?? []);
1764
- const boundsHistoryDates = useMemo(() => getPointDates(boundsHistory), [boundsHistory]);
1765
- const baseDateBounds = useMemo(() => getDateWindowBounds(boundsHistoryDates), [boundsHistoryDates]);
1766
- const manualVisibleDateWindow = useMemo(
1767
- () => buildVisibleDateWindow(boundsHistoryDates, viewState.panOffset, viewState.zoomLevel),
1768
- [boundsHistoryDates, viewState.panOffset, viewState.zoomLevel],
1769
- );
1770
- const canonicalAutoWindow = useMemo(
1771
- () => buildPresetDateWindow(boundsHistoryDates, viewState.presetRange),
1772
- [boundsHistoryDates, viewState.presetRange],
1773
- );
1774
- const autoMinimumSpanMs = useMemo(() => {
1775
- const finestSupportedResolution = effectiveResolutionSupport[0]?.resolution;
1776
- return finestSupportedResolution
1777
- ? CHART_RESOLUTION_STEP_MS[finestSupportedResolution]
1778
- : getMinimumDateStepMs(boundsHistoryDates);
1779
- }, [boundsHistoryDates, effectiveResolutionSupport]);
1780
- const renderedAutoWindow = useMemo(() => (
1781
- effectiveResolution !== "auto"
1782
- ? null
1783
- : clampDateWindowToBounds(renderedAutoView?.window ?? canonicalAutoWindow, baseDateBounds, autoMinimumSpanMs)
1784
- ), [autoMinimumSpanMs, baseDateBounds, canonicalAutoWindow, effectiveResolution, renderedAutoView]);
1785
- const plannedAutoWindow = useMemo(() => (
1786
- effectiveResolution !== "auto"
1787
- ? null
1788
- : clampDateWindowToBounds(
1789
- pendingAutoWindowOverride ?? renderedAutoWindow ?? canonicalAutoWindow,
1790
- baseDateBounds,
1791
- autoMinimumSpanMs,
1792
- )
1793
- ), [
1794
- autoMinimumSpanMs,
1795
- baseDateBounds,
1796
- canonicalAutoWindow,
1797
- effectiveResolution,
1798
- pendingAutoWindowOverride,
1799
- renderedAutoWindow,
1800
- ]);
1801
- const hasResolutionSupportApi = !!dataProvider?.getChartResolutionSupport || !!dataProvider?.getChartResolutionCapabilities;
1802
- const plannedAutoResolution = useMemo<ManualChartResolution | null>(() => {
1803
- if (
1804
- compact
1805
- || effectiveResolution !== "auto"
1806
- || !plannedAutoWindow?.start
1807
- || !plannedAutoWindow.end
1808
- ) {
1809
- return null;
1810
- }
1811
-
1812
- const spanMs = plannedAutoWindow.end.getTime() - plannedAutoWindow.start.getTime();
1813
- if (!Number.isFinite(spanMs) || spanMs < 0) return null;
1814
- if (hasResolutionSupportApi && resolutionSupport === null) return null;
1815
- return hasResolutionSupportApi
1816
- ? getBestSupportedResolutionForVisibleWindow(plannedAutoWindow, supportMap, measurementChartWidth)
1817
- : (resolveBarSize(spanMs) as ManualChartResolution | null);
1818
- }, [compact, plannedAutoWindow, effectiveResolution, hasResolutionSupportApi, measurementChartWidth, resolutionSupport, supportMap]);
1819
- const plannedDateWindow = effectiveResolution === "auto"
1820
- ? plannedAutoWindow
1821
- : manualVisibleDateWindow;
1822
- const plannedManualResolution = effectiveResolution === "auto"
1823
- ? plannedAutoResolution
1824
- : effectiveResolution;
1825
- const plannedWindowRange = useMemo(
1826
- () => getTimeRangeForDateWindow(plannedDateWindow),
1827
- [plannedDateWindow],
1828
- );
1829
- const renderCandidates = useMemo<ResolvedRenderCandidate[]>(() => (
1830
- buildResolutionFallbackChain(plannedManualResolution, plannedWindowRange, supportMap).map((candidateResolution) => {
1831
- const plan = buildResolvedChartRequestPlan({
1832
- compact,
1833
- historyOverride,
1834
- instrumentRef,
1835
- requestedWindow: plannedDateWindow,
1836
- effectiveResolution,
1837
- effectiveManualResolution: candidateResolution,
1838
- bounds: baseDateBounds,
1839
- bufferRange: indicatorBufferRange,
1840
- minimumBufferRange: hasIndicators ? indicatorBufferRange : null,
1841
- support: supportMap,
1842
- hasResolutionHistoryApi: !!dataProvider?.getPriceHistoryForResolution,
1843
- hasDetailedHistoryApi: !!dataProvider?.getDetailedPriceHistory,
1844
- minimumSpanMs: autoMinimumSpanMs,
1845
- });
1846
- return {
1847
- resolution: candidateResolution,
1848
- plan,
1849
- resolutionRequestKey: plan.resolutionRequest ? buildChartKey(plan.resolutionRequest) : null,
1850
- detailRequestKey: plan.detailRequest ? buildChartKey(plan.detailRequest) : null,
1851
- };
1852
- })
1853
- ), [
1854
- autoMinimumSpanMs,
1855
- baseDateBounds,
1856
- compact,
1857
- dataProvider,
1858
- plannedManualResolution,
1859
- effectiveResolution,
1860
- historyOverride,
1861
- instrumentRef,
1862
- plannedDateWindow,
1863
- plannedWindowRange,
1864
- supportMap,
1865
- indicatorBufferRange,
1866
- hasIndicators,
1867
- ]);
1868
- const candidateResolutionRequests = useMemo(
1869
- () => dedupeChartRequests(renderCandidates.map((candidate) => candidate.plan.resolutionRequest)),
1870
- [renderCandidates],
1871
- );
1872
- const candidateResolutionEntries = useChartQueries(candidateResolutionRequests);
1873
- const candidateDetailRequests = useMemo(() => (
1874
- dedupeChartRequests(renderCandidates.flatMap((candidate) => {
1875
- if (!candidate.plan.detailRequest) return [];
1876
- if (!candidate.plan.resolutionRequest) return [candidate.plan.detailRequest];
1877
-
1878
- const resolutionBodyState = resolveChartBodyState(
1879
- candidateResolutionEntries.get(candidate.resolutionRequestKey!),
1880
- (value) => Array.isArray(value) && value.length > 0,
1881
- "No price history available.",
1882
- );
1883
- const resolutionAccepted = isSeriesAcceptedForRequest(
1884
- resolutionBodyState.data ?? [],
1885
- plannedDateWindow,
1886
- candidate.resolution,
1887
- {
1888
- requireAutoDensity: effectiveResolution === "auto",
1889
- targetResolution: plannedManualResolution,
1890
- },
1891
- );
1892
-
1893
- return !resolutionBodyState.blocking && !resolutionAccepted
1894
- ? [candidate.plan.detailRequest]
1895
- : [];
1896
- }))
1897
- ), [candidateResolutionEntries, effectiveResolution, plannedDateWindow, plannedManualResolution, renderCandidates]);
1898
- const candidateDetailEntries = useChartQueries(candidateDetailRequests, {
1899
- debounceMs: CHART_DETAIL_REQUEST_DEBOUNCE_MS,
1900
- });
1901
- const boundsHistoryCompatible = useMemo(() => (
1902
- isSeriesAcceptedForRequest(boundsHistory, plannedDateWindow, plannedManualResolution, {
1903
- requireAutoDensity: effectiveResolution === "auto",
1904
- targetResolution: plannedManualResolution,
1905
- })
1906
- ), [boundsHistory, effectiveResolution, plannedManualResolution, plannedDateWindow]);
1907
- const renderedAutoViewAccepted = useMemo(() => (
1908
- effectiveResolution === "auto"
1909
- && !!renderedAutoView
1910
- && !!plannedDateWindow?.start
1911
- && !!plannedDateWindow.end
1912
- && !!plannedManualResolution
1913
- && CHART_RESOLUTION_STEP_MS[renderedAutoView.resolution] <= CHART_RESOLUTION_STEP_MS[plannedManualResolution]
1914
- && isSeriesAcceptedForRequest(renderedAutoView.data, plannedDateWindow, renderedAutoView.resolution, {
1915
- requireAutoDensity: true,
1916
- targetResolution: plannedManualResolution,
1917
- })
1918
- ), [effectiveResolution, plannedDateWindow, plannedManualResolution, renderedAutoView]);
1919
- const resolvedRender = useMemo(() => {
1920
- const overrideBodyState = {
1921
- data: fallbackPriceHistory,
1922
- blocking: false,
1923
- updating: false,
1924
- emptyMessage: null,
1925
- errorMessage: null,
1926
- };
1927
-
1928
- if (historyOverride || compact) {
1929
- return {
1930
- bodyState: overrideBodyState,
1931
- resolvedManualResolution: plannedManualResolution,
1932
- };
1933
- }
1934
-
1935
- if (!plannedDateWindow?.start || !plannedDateWindow.end || !plannedManualResolution) {
1936
- if (boundsBodyState.errorMessage) {
1937
- return {
1938
- bodyState: {
1939
- data: null,
1940
- blocking: false,
1941
- updating: false,
1942
- emptyMessage: null,
1943
- errorMessage: boundsBodyState.errorMessage,
1944
- },
1945
- resolvedManualResolution: plannedManualResolution,
1946
- };
1947
- }
1948
- if (boundsBodyState.emptyMessage) {
1949
- return {
1950
- bodyState: {
1951
- data: null,
1952
- blocking: false,
1953
- updating: false,
1954
- emptyMessage: boundsBodyState.emptyMessage,
1955
- errorMessage: null,
1956
- },
1957
- resolvedManualResolution: plannedManualResolution,
1958
- };
1959
- }
1960
- return {
1961
- bodyState: {
1962
- data: null,
1963
- blocking: true,
1964
- updating: false,
1965
- emptyMessage: null,
1966
- errorMessage: null,
1967
- },
1968
- resolvedManualResolution: plannedManualResolution,
1969
- };
1970
- }
1971
-
1972
- if (renderedAutoViewAccepted && renderedAutoView) {
1973
- return {
1974
- bodyState: {
1975
- data: renderedAutoView.data,
1976
- blocking: false,
1977
- updating: false,
1978
- emptyMessage: null,
1979
- errorMessage: null,
1980
- },
1981
- resolvedManualResolution: renderedAutoView.resolution,
1982
- };
1983
- }
1984
-
1985
- let firstBlockingState: ReturnType<typeof resolveChartBodyState<PricePoint[]>> | null = null;
1986
- let firstBlockingResolution: ManualChartResolution | null = null;
1987
- let firstCompatibleState: ReturnType<typeof resolveChartBodyState<PricePoint[]>> | null = null;
1988
- let firstCompatibleResolution: ManualChartResolution | null = null;
1989
- let lastFailureState: ReturnType<typeof resolveChartBodyState<PricePoint[]>> | null = null;
1990
-
1991
- for (const candidate of renderCandidates) {
1992
- const resolutionBodyState = candidate.plan.resolutionRequest
1993
- ? resolveChartBodyState(
1994
- candidateResolutionEntries.get(candidate.resolutionRequestKey!),
1995
- (value) => Array.isArray(value) && value.length > 0,
1996
- "No price history available.",
1997
- )
1998
- : null;
1999
- const resolutionCompatible = candidate.plan.resolutionRequest !== null
2000
- && isSeriesAcceptedForRequest(
2001
- resolutionBodyState?.data ?? [],
2002
- plannedDateWindow,
2003
- candidate.resolution,
2004
- {
2005
- requireAutoDensity: effectiveResolution === "auto",
2006
- targetResolution: plannedManualResolution,
2007
- },
2008
- );
2009
- if (resolutionCompatible) {
2010
- if (!firstBlockingState) {
2011
- return {
2012
- bodyState: resolutionBodyState!,
2013
- resolvedManualResolution: candidate.resolution,
2014
- };
2015
- }
2016
- firstCompatibleState ??= resolutionBodyState!;
2017
- firstCompatibleResolution ??= candidate.resolution;
2018
- continue;
2019
- }
2020
-
2021
- const detailBodyState = candidate.plan.detailRequest
2022
- ? resolveChartBodyState(
2023
- candidateDetailEntries.get(candidate.detailRequestKey!),
2024
- (value) => Array.isArray(value) && value.length > 0,
2025
- "No price history available.",
2026
- )
2027
- : null;
2028
- const detailCompatible = candidate.plan.detailRequest !== null
2029
- && isSeriesAcceptedForRequest(
2030
- detailBodyState?.data ?? [],
2031
- plannedDateWindow,
2032
- candidate.resolution,
2033
- {
2034
- requireAutoDensity: effectiveResolution === "auto",
2035
- targetResolution: plannedManualResolution,
2036
- },
2037
- );
2038
- if (detailCompatible) {
2039
- if (!firstBlockingState) {
2040
- return {
2041
- bodyState: detailBodyState!,
2042
- resolvedManualResolution: candidate.resolution,
2043
- };
2044
- }
2045
- firstCompatibleState ??= detailBodyState!;
2046
- firstCompatibleResolution ??= candidate.resolution;
2047
- continue;
2048
- }
2049
-
2050
- if (!firstBlockingState) {
2051
- if (resolutionBodyState?.blocking) {
2052
- firstBlockingState = resolutionBodyState;
2053
- firstBlockingResolution = candidate.resolution;
2054
- } else if (detailBodyState?.blocking) {
2055
- firstBlockingState = detailBodyState;
2056
- firstBlockingResolution = candidate.resolution;
2057
- }
2058
- }
2059
-
2060
- if (detailBodyState?.errorMessage || detailBodyState?.emptyMessage) {
2061
- lastFailureState = detailBodyState;
2062
- } else if (resolutionBodyState?.errorMessage || resolutionBodyState?.emptyMessage) {
2063
- lastFailureState = resolutionBodyState;
2064
- }
2065
- }
2066
-
2067
- if (effectiveResolution === "auto" && boundsHistoryCompatible) {
2068
- return {
2069
- bodyState: {
2070
- data: boundsHistory,
2071
- blocking: false,
2072
- updating: boundsBodyState.updating,
2073
- emptyMessage: null,
2074
- errorMessage: null,
2075
- },
2076
- resolvedManualResolution: plannedManualResolution,
2077
- };
2078
- }
2079
-
2080
- if (firstCompatibleState) {
2081
- return {
2082
- bodyState: firstBlockingState
2083
- ? {
2084
- ...firstCompatibleState,
2085
- blocking: false,
2086
- updating: true,
2087
- emptyMessage: null,
2088
- errorMessage: null,
2089
- }
2090
- : firstCompatibleState,
2091
- resolvedManualResolution: firstCompatibleResolution ?? plannedManualResolution,
2092
- };
2093
- }
2094
-
2095
- if (effectiveResolution === "auto" && firstBlockingState && boundsHistory.length > 0) {
2096
- return {
2097
- bodyState: {
2098
- data: boundsHistory,
2099
- blocking: false,
2100
- updating: true,
2101
- emptyMessage: null,
2102
- errorMessage: null,
2103
- },
2104
- resolvedManualResolution: plannedManualResolution,
2105
- };
2106
- }
2107
-
2108
- if (effectiveResolution !== "auto" && firstBlockingState && boundsHistoryCompatible) {
2109
- return {
2110
- bodyState: {
2111
- data: boundsHistory,
2112
- blocking: false,
2113
- updating: true,
2114
- emptyMessage: null,
2115
- errorMessage: null,
2116
- },
2117
- resolvedManualResolution: plannedManualResolution,
2118
- };
2119
- }
2120
-
2121
- if (firstBlockingState) {
2122
- return {
2123
- bodyState: firstBlockingState,
2124
- resolvedManualResolution: firstBlockingResolution ?? plannedManualResolution,
2125
- };
2126
- }
2127
-
2128
- if (renderCandidates[0]?.plan.unsupportedMessage) {
2129
- return {
2130
- bodyState: {
2131
- data: null,
2132
- blocking: false,
2133
- updating: false,
2134
- emptyMessage: null,
2135
- errorMessage: renderCandidates[0].plan.unsupportedMessage,
2136
- },
2137
- resolvedManualResolution: renderCandidates[0].resolution,
2138
- };
2139
- }
2140
-
2141
- return {
2142
- bodyState: lastFailureState ?? {
2143
- data: null,
2144
- blocking: false,
2145
- updating: false,
2146
- emptyMessage: boundsBodyState.emptyMessage ?? "No price history available.",
2147
- errorMessage: boundsBodyState.errorMessage,
2148
- },
2149
- resolvedManualResolution: plannedManualResolution,
2150
- };
2151
- }, [
2152
- boundsBodyState.emptyMessage,
2153
- boundsBodyState.errorMessage,
2154
- boundsBodyState.updating,
2155
- boundsHistory,
2156
- boundsHistoryCompatible,
2157
- candidateDetailEntries,
2158
- candidateResolutionEntries,
2159
- compact,
2160
- effectiveResolution,
2161
- financials?.priceHistory,
2162
- historyOverride,
2163
- plannedDateWindow,
2164
- plannedManualResolution,
2165
- renderedAutoView,
2166
- renderedAutoViewAccepted,
2167
- renderCandidates,
2168
- ]);
2169
- const plannedRenderBodyState = resolvedRender.bodyState;
2170
- const plannedResolvedManualResolution = resolvedRender.resolvedManualResolution;
2171
- const canCommitPlannedAutoView = effectiveResolution === "auto"
2172
- && !!plannedDateWindow?.start
2173
- && !!plannedDateWindow.end
2174
- && !!plannedResolvedManualResolution
2175
- && isSeriesAcceptedForRequest(
2176
- plannedRenderBodyState.data ?? [],
2177
- plannedDateWindow,
2178
- plannedResolvedManualResolution,
2179
- {
2180
- requireAutoDensity: true,
2181
- targetResolution: plannedManualResolution,
2182
- },
2183
- );
2184
- useEffect(() => {
2185
- if (!canCommitPlannedAutoView || !plannedDateWindow?.start || !plannedDateWindow.end || !plannedResolvedManualResolution || !plannedRenderBodyState.data?.length) {
2186
- return;
2187
- }
2188
-
2189
- const nextRenderedAutoView: AutoRenderedView = {
2190
- window: plannedDateWindow,
2191
- resolution: plannedResolvedManualResolution,
2192
- data: plannedRenderBodyState.data,
2193
- };
2194
- setRenderedAutoView((current) => (
2195
- current
2196
- && sameDateWindow(current.window, nextRenderedAutoView.window)
2197
- && current.resolution === nextRenderedAutoView.resolution
2198
- && current.data === nextRenderedAutoView.data
2199
- ? current
2200
- : nextRenderedAutoView
2201
- ));
2202
- setPendingAutoWindowOverride((current) => (
2203
- sameDateWindow(current, plannedDateWindow) ? null : current
2204
- ));
2205
- }, [
2206
- canCommitPlannedAutoView,
2207
- plannedDateWindow,
2208
- plannedRenderBodyState.data,
2209
- plannedResolvedManualResolution,
2210
- ]);
2211
- const shouldRejectPendingAutoView = effectiveResolution === "auto"
2212
- && pendingAutoWindowOverride !== null
2213
- && !plannedRenderBodyState.blocking
2214
- && !plannedRenderBodyState.updating
2215
- && !canCommitPlannedAutoView;
2216
- useEffect(() => {
2217
- if (!shouldRejectPendingAutoView || !plannedDateWindow?.start || !plannedDateWindow.end) {
2218
- return;
2219
- }
2220
-
2221
- setPendingAutoWindowOverride((current) => (
2222
- sameDateWindow(current, plannedDateWindow) ? null : current
2223
- ));
2224
- }, [
2225
- pendingAutoWindowOverride,
2226
- plannedDateWindow,
2227
- plannedRenderBodyState.blocking,
2228
- plannedRenderBodyState.emptyMessage,
2229
- plannedRenderBodyState.errorMessage,
2230
- plannedRenderBodyState.updating,
2231
- shouldRejectPendingAutoView,
2232
- ]);
2233
- useEffect(() => {
2234
- setPendingAutoWindowOverride(null);
2235
- setRenderedAutoView(null);
2236
- setLastReadyRenderView(null);
2237
- }, [instrumentRef?.exchange, instrumentRef?.symbol]);
2238
- const hasPendingAutoProposal = effectiveResolution === "auto" && pendingAutoWindowOverride !== null;
2239
- const shouldUseRenderedAutoView = effectiveResolution === "auto"
2240
- && !!renderedAutoView
2241
- && (
2242
- plannedRenderBodyState.blocking
2243
- || !plannedRenderBodyState.data?.length
2244
- || !!plannedRenderBodyState.emptyMessage
2245
- || !!plannedRenderBodyState.errorMessage
2246
- );
2247
- const isRenderedAutoViewUpdating = hasPendingAutoProposal
2248
- || plannedRenderBodyState.blocking
2249
- || plannedRenderBodyState.updating;
2250
- const autoDisplayState = useMemo(() => resolveAutoDisplayState({
2251
- shouldUseRenderedAutoView,
2252
- renderedAutoView,
2253
- isRenderedAutoViewUpdating,
2254
- plannedRenderBodyState,
2255
- plannedResolvedManualResolution,
2256
- plannedDateWindow,
2257
- }), [
2258
- isRenderedAutoViewUpdating,
2259
- plannedDateWindow,
2260
- plannedRenderBodyState,
2261
- plannedResolvedManualResolution,
2262
- renderedAutoView,
2263
- shouldUseRenderedAutoView,
2264
- ]);
2265
- const baseRenderBodyState = effectiveResolution === "auto"
2266
- ? autoDisplayState.bodyState
2267
- : plannedRenderBodyState;
2268
- const baseResolvedManualResolution = effectiveResolution === "auto"
2269
- ? autoDisplayState.resolution
2270
- : plannedResolvedManualResolution;
2271
- const baseDisplayedDateWindow = effectiveResolution === "auto"
2272
- ? autoDisplayState.window
2273
- : manualVisibleDateWindow;
2274
- useEffect(() => {
2275
- if (compact || baseRenderBodyState.blocking || !baseRenderBodyState.data?.length) return;
2276
-
2277
- const nextView: CachedRenderedView = {
2278
- window: baseDisplayedDateWindow,
2279
- resolution: baseResolvedManualResolution,
2280
- data: baseRenderBodyState.data,
2281
- };
2282
- setLastReadyRenderView((current) => (
2283
- current
2284
- && current.data === nextView.data
2285
- && current.resolution === nextView.resolution
2286
- && sameDateWindow(current.window, nextView.window)
2287
- ? current
2288
- : nextView
2289
- ));
2290
- }, [
2291
- baseDisplayedDateWindow,
2292
- baseRenderBodyState.blocking,
2293
- baseRenderBodyState.data,
2294
- baseResolvedManualResolution,
2295
- compact,
2296
- ]);
2297
- const shouldUseLastReadyRenderView = !compact && baseRenderBodyState.blocking && !!lastReadyRenderView?.data.length;
2298
- const renderBodyState = shouldUseLastReadyRenderView
2299
- ? {
2300
- data: lastReadyRenderView!.data,
2301
- blocking: false,
2302
- updating: true,
2303
- emptyMessage: null,
2304
- errorMessage: null,
2305
- }
2306
- : baseRenderBodyState;
2307
- const resolvedManualResolution = shouldUseLastReadyRenderView
2308
- ? lastReadyRenderView!.resolution
2309
- : baseResolvedManualResolution;
2310
- const displayedDateWindow = shouldUseLastReadyRenderView
2311
- ? lastReadyRenderView!.window
2312
- : baseDisplayedDateWindow;
2313
- const renderedResolution: ChartResolution = effectiveResolution === "auto"
2314
- ? "auto"
2315
- : (resolvedManualResolution ?? effectiveResolution);
2316
- const selectedResolution: ChartResolution = requestedResolution === "auto"
2317
- ? "auto"
2318
- : (resolutionChips.includes(requestedResolution) ? requestedResolution : effectiveResolution);
2319
- const fallbackResolutionLabel = selectedResolution !== "auto" && renderedResolution !== selectedResolution
2320
- ? `showing ${getChartResolutionLabel(renderedResolution)}`
2321
- : null;
2322
- const history = compact
2323
- ? fallbackPriceHistory
2324
- : (renderBodyState.data ?? []);
2325
- const visibleDateWindow = displayedDateWindow;
2326
- const navigableDateWindow = effectiveResolution === "auto"
2327
- ? (pendingAutoWindowOverride ?? displayedDateWindow ?? plannedDateWindow)
2328
- : visibleDateWindow;
2329
- const navigationOhlcPointCount = !compact && effectiveResolution !== "auto"
2330
- ? manualVisibleDateWindow.dates.length
2331
- : 0;
2332
- const resolveOhlcProjectionOptions = useCallback((
2333
- pointCount: number,
2334
- sourceIndexOffset: number,
2335
- ) => (
2336
- resolveStableOhlcProjectionOptions({
2337
- pointCount,
2338
- sourceIndexOffset,
2339
- bucketWidth: measurementChartWidth,
2340
- navigationPointCount: navigationOhlcPointCount,
2341
- })
2342
- ), [measurementChartWidth, navigationOhlcPointCount]);
2343
- const axisWidth = useMemo(() => {
2344
- const measureAxisWidth = (targetWidth: number) => {
2345
- const measuredWindow = historyOverride || !displayedDateWindow?.start || !displayedDateWindow.end
2346
- ? { points: history, startIdx: 0, endIdx: history.length }
2347
- : getVisibleWindowForDateRange(history, displayedDateWindow, 0);
2348
- const measuredTimeAxisDates = measuredWindow.points.map((point) => point.date);
2349
- const measuredProjection = projectChartData(
2350
- measuredWindow.points,
2351
- targetWidth,
2352
- viewState.renderMode,
2353
- !!compact,
2354
- resolveOhlcProjectionOptions(measuredWindow.points.length, measuredWindow.startIdx),
2355
- );
2356
- const measuredResult = renderChart(measuredProjection.points, {
2357
- width: targetWidth,
2358
- height: chartHeight,
2359
- showVolume: showVolume && !compact,
2360
- volumeHeight,
2361
- cursorX: null,
2362
- cursorY: null,
2363
- mode: measuredProjection.effectiveMode,
2364
- axisMode,
2365
- currency: chartCurrency,
2366
- assetCategory: chartAssetCategory,
2367
- colors: AXIS_MEASURE_PALETTE,
2368
- timeAxisDates: measuredTimeAxisDates,
2369
- });
2370
- const measuredScene = buildChartScene(measuredProjection.points, {
2371
- width: targetWidth,
2372
- height: chartHeight,
2373
- showVolume: showVolume && !compact,
2374
- volumeHeight,
2375
- cursorX: null,
2376
- cursorY: null,
2377
- mode: measuredProjection.effectiveMode,
2378
- axisMode,
2379
- colors: AXIS_MEASURE_PALETTE,
2380
- timeAxisDates: measuredTimeAxisDates,
2381
- });
2382
- const cursorSamples = !compact && measuredScene
2383
- ? [
2384
- formatCursorAxisValue(
2385
- measuredScene.min,
2386
- axisMode,
2387
- measuredProjection.points[0]?.close ?? 0,
2388
- chartCurrency,
2389
- chartAssetCategory,
2390
- measuredResult.priceRange ?? undefined,
2391
- ),
2392
- formatCursorAxisValue(
2393
- measuredScene.max,
2394
- axisMode,
2395
- measuredProjection.points[0]?.close ?? 0,
2396
- chartCurrency,
2397
- chartAssetCategory,
2398
- measuredResult.priceRange ?? undefined,
2399
- ),
2400
- ]
2401
- : [];
2402
-
2403
- return resolveChartAxisWidth(
2404
- [...measuredResult.axisLabels.map((entry) => entry.label), ...cursorSamples],
2405
- minimumAxisWidth,
2406
- Math.max(axisSectionWidthBudget - axisRightPadding, minimumAxisWidth),
2407
- );
2408
- };
2409
-
2410
- const firstPassWidth = measureAxisWidth(measurementChartWidth);
2411
- const refinedChartWidth = Math.max(width - firstPassWidth - axisRightPadding - axisGap, minChartWidth);
2412
- return refinedChartWidth === measurementChartWidth ? firstPassWidth : measureAxisWidth(refinedChartWidth);
2413
- }, [
2414
- axisGap,
2415
- axisMode,
2416
- axisRightPadding,
2417
- axisSectionWidthBudget,
2418
- chartAssetCategory,
2419
- chartCurrency,
2420
- chartHeight,
2421
- compact,
2422
- displayedDateWindow,
2423
- fractionalViewport,
2424
- history,
2425
- historyOverride,
2426
- minChartWidth,
2427
- minimumAxisWidth,
2428
- resolveOhlcProjectionOptions,
2429
- showVolume,
2430
- viewState.renderMode,
2431
- volumeHeight,
2432
- width,
2433
- ]);
2434
- const axisSectionWidth = axisWidth + axisRightPadding;
2435
- const chartWidth = Math.max(width - axisSectionWidth - axisGap, minChartWidth);
2436
- const maxCursorX = chartWidth - 1;
2437
- const panStep = getKeyboardPanCellCount(chartWidth);
2438
- const activePreset = compact
2439
- ? null
2440
- : effectiveResolution === "auto"
2441
- ? (canonicalAutoWindow && displayedDateWindow && sameDateWindow(displayedDateWindow, canonicalAutoWindow)
2442
- ? viewState.presetRange
2443
- : null)
2444
- : resolveVisibleActivePreset(boundsHistoryDates, {
2445
- presetRange: viewState.presetRange,
2446
- activePreset: viewState.activePreset,
2447
- panOffset: viewState.panOffset,
2448
- zoomLevel: viewState.zoomLevel,
2449
- resolution: effectiveResolution,
2450
- });
2451
-
2452
- useEffect(() => {
2453
- if (compact || effectiveResolution === "auto" || boundsHistoryDates.length === 0) return;
2454
-
2455
- const hasPendingCanonicalReset = appliedCanonicalResetRef.current < pendingCanonicalResetRef.current;
2456
- const shouldReconcileActivePreset = !hasPendingCanonicalReset
2457
- && needsCanonicalPresetViewportReset(boundsHistoryDates, viewState);
2458
- if (hasPendingCanonicalReset || shouldReconcileActivePreset) {
2459
- if (hasPendingCanonicalReset) {
2460
- appliedCanonicalResetRef.current = pendingCanonicalResetRef.current;
2461
- }
2462
- setViewState((current) => (
2463
- hasPendingCanonicalReset
2464
- ? resolvePresetRangeViewport(current, boundsHistoryDates)
2465
- : resolveCanonicalPresetViewport(current, boundsHistoryDates)
2466
- ));
2467
- return;
2468
- }
2469
-
2470
- if (!pendingExpansionRef.current) return;
2471
- const pendingExpansion = pendingExpansionRef.current;
2472
- pendingExpansionRef.current = null;
2473
- setViewState((current) => {
2474
- if (pendingExpansion.kind === "zoom-out") {
2475
- const nextVisibleCount = Math.min(boundsHistoryDates.length, Math.max(pendingExpansion.targetVisibleCount, 1));
2476
- return {
2477
- ...current,
2478
- ...resolveAnchoredChartZoom(
2479
- boundsHistoryDates.length,
2480
- 1,
2481
- 0,
2482
- boundsHistoryDates.length / nextVisibleCount,
2483
- pendingExpansion.anchorRatio,
2484
- ),
2485
- };
2486
- }
2487
- return {
2488
- ...current,
2489
- panOffset: clamp(pendingExpansion.targetPanOffset, 0, getMaxPanOffset(boundsHistory, current.zoomLevel)),
2490
- };
2491
- });
2492
- }, [
2493
- boundsHistory,
2494
- boundsHistoryDates,
2495
- compact,
2496
- effectiveResolution,
2497
- viewState.activePreset,
2498
- viewState.panOffset,
2499
- viewState.presetRange,
2500
- viewState.zoomLevel,
2501
- ]);
2502
-
2503
- useEffect(() => {
2504
- if (compact || effectiveResolution !== "auto" || !baseDateBounds) return;
2505
-
2506
- const pendingAutoWindow = pendingAutoWindowRef.current;
2507
- if (pendingAutoWindow) {
2508
- const clampedPendingWindow = clampDateWindowToBounds(pendingAutoWindow, baseDateBounds, autoMinimumSpanMs);
2509
- if (clampedPendingWindow && sameDateWindow(clampedPendingWindow, pendingAutoWindow)) {
2510
- pendingAutoWindowRef.current = null;
2511
- const normalizedPendingWindow = canonicalAutoWindow && sameDateWindow(clampedPendingWindow, canonicalAutoWindow)
2512
- ? null
2513
- : clampedPendingWindow;
2514
- setPendingAutoWindowOverride((current) => (
2515
- sameDateWindow(current, normalizedPendingWindow) ? current : normalizedPendingWindow
2516
- ));
2517
- }
2518
- return;
2519
- }
2520
-
2521
- if (!pendingAutoWindowOverride) return;
2522
- const clampedOverride = clampDateWindowToBounds(pendingAutoWindowOverride, baseDateBounds, autoMinimumSpanMs);
2523
- if (!clampedOverride) return;
2524
- const normalizedOverride = canonicalAutoWindow && sameDateWindow(clampedOverride, canonicalAutoWindow)
2525
- ? null
2526
- : clampedOverride;
2527
- if (!sameDateWindow(pendingAutoWindowOverride, normalizedOverride)) {
2528
- setPendingAutoWindowOverride(normalizedOverride);
2529
- }
2530
- }, [autoMinimumSpanMs, baseDateBounds, canonicalAutoWindow, compact, effectiveResolution, pendingAutoWindowOverride]);
2531
-
2532
- useEffect(() => {
2533
- if (interactive) {
2534
- cursorMotionKindRef.current = "discrete";
2535
- setViewState((current) => (current.cursorX === null ? { ...current, cursorX: chartWidth - 1 } : current));
2536
- } else {
2537
- updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
2538
- setViewState((current) => (current.cursorX !== null || current.cursorY !== null
2539
- ? { ...current, cursorX: null, cursorY: null }
2540
- : current));
2541
- }
2542
- }, [interactive, chartWidth]);
2543
-
2544
- useEffect(() => {
2545
- const refreshSupport = () => setKittySupport(getCachedKittySupport(renderer));
2546
- refreshSupport();
2547
- renderer.on("capabilities", refreshSupport);
2548
- return () => {
2549
- renderer.off("capabilities", refreshSupport);
2550
- };
2551
- }, [renderer]);
2552
-
2553
- useEffect(() => {
2554
- let cancelled = false;
2555
- const known = getCachedKittySupport(renderer);
2556
- setKittySupport(known);
2557
- if (preferredRenderer === "braille" || known !== null) return;
2558
- ensureKittySupport(renderer).then((supported) => {
2559
- if (!cancelled) setKittySupport(supported);
2560
- }).catch(() => {
2561
- if (!cancelled) setKittySupport(false);
2562
- });
2563
- return () => {
2564
- cancelled = true;
2565
- };
2566
- }, [preferredRenderer, renderer]);
2567
-
2568
- const persistRenderMode = useCallback((nextMode: ChartRenderMode) => {
2569
- if (!compact && nextMode !== storedRenderMode) {
2570
- setStoredRenderMode(nextMode);
2571
- }
2572
- }, [compact, setStoredRenderMode, storedRenderMode]);
2573
-
2574
- const expandBufferRange = (action: PendingExpansionAction): boolean => {
2575
- if (compact) return false;
2576
- const nextCandidate = getNextBufferRange(viewState.bufferRange);
2577
- const nextBufferRange = effectiveResolution === "auto"
2578
- ? nextCandidate
2579
- : clampTimeRangeToMaxRange(nextCandidate, supportMap.get(effectiveResolution) ?? viewState.bufferRange);
2580
- if (nextBufferRange === viewState.bufferRange) return false;
2581
- pendingExpansionRef.current = action;
2582
- setViewState((current) => applyBufferedPanExpansion(current, nextBufferRange));
2583
- return true;
2584
- };
2585
-
2586
- const requestAutoWindow = (nextWindow: DateWindowRange | null | undefined): boolean => {
2587
- if (compact || effectiveResolution !== "auto" || !nextWindow?.start || !nextWindow.end || !baseDateBounds?.start || !baseDateBounds.end) {
2588
- return false;
2589
- }
2590
-
2591
- if (nextWindow.start.getTime() < baseDateBounds.start.getTime()) {
2592
- const nextBufferRange = getNextBufferRange(viewState.bufferRange);
2593
- if (nextBufferRange !== viewState.bufferRange) {
2594
- pendingAutoWindowRef.current = nextWindow;
2595
- setViewState((current) => {
2596
- const nextState = clearAutoViewportState(current);
2597
- return nextState.bufferRange === nextBufferRange
2598
- ? nextState
2599
- : { ...nextState, bufferRange: nextBufferRange };
2600
- });
2601
- return true;
2602
- }
2603
- }
2604
-
2605
- const clampedWindow = clampDateWindowToBounds(nextWindow, baseDateBounds, autoMinimumSpanMs);
2606
- if (!clampedWindow) {
2607
- return false;
2608
- }
2609
- const normalizedWindow = canonicalAutoWindow && sameDateWindow(clampedWindow, canonicalAutoWindow)
2610
- ? null
2611
- : clampedWindow;
2612
- if (sameDateWindow(navigableDateWindow, normalizedWindow)) {
2613
- return false;
2614
- }
2615
- pendingAutoWindowRef.current = null;
2616
- setPendingAutoWindowOverride((current) => (sameDateWindow(current, normalizedWindow) ? current : normalizedWindow));
2617
- setViewState((current) => clearAutoViewportState(current));
2618
- return true;
2619
- };
2620
-
2621
- const setRange = (range: TimeRange) => {
2622
- if (!compact && !isRangePresetSupported(range, effectiveResolutionSupport)) return;
2623
- const supportMaxRange = getSupportMaxRange(effectiveResolutionSupport, getPresetResolution(range));
2624
- const nextResolution = getPresetResolution(range);
2625
- setRequestedResolution(nextResolution);
2626
- if (!compact) {
2627
- persistChartControls(range, nextResolution);
2628
- }
2629
- pendingCanonicalResetRef.current += 1;
2630
- pendingAutoWindowRef.current = null;
2631
- setPendingAutoWindowOverride(null);
2632
- updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
2633
- setViewState((current) => resolveViewportPresetSelection(current, range, supportMaxRange));
2634
- };
2635
-
2636
- const setResolution = (resolution: ChartResolution) => {
2637
- if (compact) return;
2638
- if (resolution !== "auto" && !availableManualResolutions.includes(resolution)) return;
2639
-
2640
- if (resolution === "auto") {
2641
- const preservedWindow = effectiveResolution === "auto" ? navigableDateWindow : manualVisibleDateWindow;
2642
- const nextAutoWindow = clampDateWindowToBounds(preservedWindow, baseDateBounds, autoMinimumSpanMs);
2643
- pendingAutoWindowRef.current = null;
2644
- setPendingAutoWindowOverride(
2645
- canonicalAutoWindow && nextAutoWindow && sameDateWindow(nextAutoWindow, canonicalAutoWindow)
2646
- ? null
2647
- : nextAutoWindow,
2648
- );
2649
- if (effectiveResolution !== "auto") {
2650
- setRenderedAutoView(null);
2651
- }
2652
- setRequestedResolution(resolution);
2653
- persistChartControls(viewState.presetRange, resolution);
2654
- updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
2655
- setViewState((current) => ({
2656
- ...clearAutoViewportState(current),
2657
- bufferRange: getNextBufferRange(current.presetRange),
2658
- }));
2659
- return;
2660
- }
2661
-
2662
- const nextState = resolveViewportResolutionSelection(viewState, resolution, selectionSupportMap, visibleDateWindow);
2663
- if (!nextState) return;
2664
- setRequestedResolution(resolution);
2665
- pendingAutoWindowRef.current = null;
2666
- setPendingAutoWindowOverride(null);
2667
- pendingCanonicalResetRef.current += 1;
2668
- persistChartControls(nextState.presetRange, resolution);
2669
- updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
2670
- setViewState(nextState);
2671
- };
2672
-
2673
- const setRenderMode = (mode: ChartRenderMode) => {
2674
- persistRenderMode(mode);
2675
- setViewState((current) => ({ ...current, renderMode: mode }));
2676
- };
2677
-
2678
- const focusPaneForMouseInteraction = (event: { stopPropagation?: () => void; preventDefault?: () => void } | null | undefined) => {
2679
- event?.stopPropagation?.();
2680
- event?.preventDefault?.();
2681
- if (!focused) {
2682
- dispatch({ type: "FOCUS_PANE", paneId });
2683
- }
2684
- };
2685
-
2686
- const chartWindow = useMemo(() => {
2687
- if (historyOverride || !displayedDateWindow?.start || !displayedDateWindow.end) {
2688
- return { points: history, startIdx: 0, endIdx: history.length };
2689
- }
2690
- return getVisibleWindowForDateRange(history, displayedDateWindow, 0);
2691
- }, [displayedDateWindow, history, historyOverride]);
2692
- const historyRenderKey = chartWindow.points.length === 0
2693
- ? "empty"
2694
- : [
2695
- chartWindow.points.length,
2696
- new Date(chartWindow.points[0]!.date).getTime(),
2697
- new Date(chartWindow.points[chartWindow.points.length - 1]!.date).getTime(),
2698
- chartWindow.points[chartWindow.points.length - 1]!.close,
2699
- ].join(":");
2700
-
2701
- useEffect(() => {
2702
- queueMicrotask(() => renderer.requestRender());
2703
- }, [chartHeight, chartWidth, compact, historyRenderKey, renderer, ticker?.metadata.ticker, viewState.renderMode]);
2704
-
2705
- const timeAxisDates = useMemo(
2706
- () => chartWindow.points.map((point) => point.date),
2707
- [chartWindow.points],
2708
- );
2709
-
2710
- const projection = useMemo(() => (
2711
- projectChartData(
2712
- chartWindow.points,
2713
- chartWidth,
2714
- viewState.renderMode,
2715
- !!compact,
2716
- resolveOhlcProjectionOptions(chartWindow.points.length, chartWindow.startIdx),
2717
- )
2718
- ), [
2719
- chartWindow.points,
2720
- chartWindow.startIdx,
2721
- chartWidth,
2722
- compact,
2723
- resolveOhlcProjectionOptions,
2724
- viewState.renderMode,
2725
- ]);
2726
-
2727
- const sourceIndicatorOverlays = useMemo(() => {
2728
- if (!hasIndicators || !history.length) return null;
2729
- return computeIndicatorOverlays(history.map((point) => point.close), indicatorConfig);
2730
- }, [history, hasIndicators, indicatorConfig]);
2731
-
2732
- const indicatorSourceKey = useMemo(() => (
2733
- sourceIndicatorOverlays ? buildIndicatorSourceKey(history, indicatorConfig) : "none"
2734
- ), [history, indicatorConfig, sourceIndicatorOverlays]);
2735
- const indicatorProjectionKey = useMemo(() => {
2736
- if (!sourceIndicatorOverlays || !chartWindow.points.length || !projection.points.length) return "none";
2737
- return buildIndicatorProjectionKey({
2738
- sourceKey: indicatorSourceKey,
2739
- sourcePoints: chartWindow.points,
2740
- sourceIndexOffset: chartWindow.startIdx,
2741
- projectedPoints: projection.points,
2742
- mode: projection.effectiveMode,
2743
- });
2744
- }, [
2745
- chartWindow.points,
2746
- chartWindow.startIdx,
2747
- indicatorSourceKey,
2748
- projection.effectiveMode,
2749
- projection.points,
2750
- sourceIndicatorOverlays,
2751
- ]);
2752
- const [deferredIndicators, setDeferredIndicators] = useState<DeferredIndicatorState | null>(null);
2753
-
2754
- useEffect(() => {
2755
- if (!sourceIndicatorOverlays || indicatorProjectionKey === "none" || !chartWindow.points.length || !projection.points.length) {
2756
- setDeferredIndicators((current) => (current === null ? current : null));
2757
- return;
2758
- }
2759
-
2760
- const timeout = setTimeout(() => {
2761
- setDeferredIndicators({
2762
- key: indicatorProjectionKey,
2763
- overlays: reindexIndicatorOverlaysForProjection(
2764
- sourceIndicatorOverlays,
2765
- chartWindow.points,
2766
- projection.points,
2767
- chartWindow.startIdx,
2768
- ),
2769
- });
2770
- }, INDICATOR_RENDER_DEBOUNCE_MS);
2771
-
2772
- return () => clearTimeout(timeout);
2773
- }, [
2774
- chartWindow.points,
2775
- chartWindow.startIdx,
2776
- indicatorProjectionKey,
2777
- projection.points,
2778
- sourceIndicatorOverlays,
2779
- ]);
2780
-
2781
- const indicators = deferredIndicators?.key === indicatorProjectionKey ? deferredIndicators.overlays : null;
2782
- const indicatorRenderKey = useMemo(() => {
2783
- return buildIndicatorRenderKey(indicators);
2784
- }, [indicators]);
2785
-
2786
- useShortcut((event) => {
2787
- if (!focused || compact) return;
2788
- const key = resolveChartKeyboardKey(event);
2789
-
2790
- switch (key) {
2791
- case "zoom-in":
2792
- if (effectiveResolution === "auto") {
2793
- requestAutoWindow(resolveAutoZoomWindow({
2794
- historyPoints: history,
2795
- boundsDates: boundsHistoryDates,
2796
- currentWindow: navigableDateWindow,
2797
- direction: "in",
2798
- anchorRatio: RIGHT_EDGE_ANCHOR_RATIO,
2799
- }));
2800
- return;
2801
- }
2802
- setViewState((current) => applyZoomAroundAnchor(current, current.zoomLevel * 1.5, RIGHT_EDGE_ANCHOR_RATIO, boundsHistory));
2803
- return;
2804
- case "zoom-out":
2805
- if (effectiveResolution === "auto") {
2806
- requestAutoWindow(resolveAutoZoomWindow({
2807
- historyPoints: history,
2808
- boundsDates: boundsHistoryDates,
2809
- currentWindow: navigableDateWindow,
2810
- direction: "out",
2811
- anchorRatio: RIGHT_EDGE_ANCHOR_RATIO,
2812
- }));
2813
- return;
2814
- }
2815
- if (viewState.zoomLevel <= 1.001 && expandBufferRange({
2816
- kind: "zoom-out",
2817
- targetVisibleCount: Math.round(boundsHistory.length * 1.5),
2818
- anchorRatio: RIGHT_EDGE_ANCHOR_RATIO,
2819
- })) {
2820
- return;
2821
- }
2822
- setViewState((current) => applyZoomAroundAnchor(
2823
- current,
2824
- current.zoomLevel / 1.5,
2825
- RIGHT_EDGE_ANCHOR_RATIO,
2826
- boundsHistory,
2827
- ));
2828
- return;
2829
- case "0":
2830
- if (effectiveResolution === "auto") {
2831
- pendingAutoWindowRef.current = null;
2832
- setPendingAutoWindowOverride(null);
2833
- setRenderedAutoView(null);
2834
- updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
2835
- setViewState((current) => clearAutoViewportState(current));
2836
- return;
2837
- }
2838
- pendingCanonicalResetRef.current += 1;
2839
- updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
2840
- setViewState((current) => {
2841
- const nextState = resolveViewportResolutionSelection(
2842
- current,
2843
- effectiveResolution,
2844
- selectionSupportMap,
2845
- visibleDateWindow,
2846
- ) ?? current;
2847
- return {
2848
- ...nextState,
2849
- panOffset: 0,
2850
- zoomLevel: 1,
2851
- cursorX: null,
2852
- cursorY: null,
2853
- };
2854
- });
2855
- return;
2856
- case "a":
2857
- if (effectiveResolution === "auto") {
2858
- requestAutoWindow(shiftDateWindow(navigableDateWindow, panStep / Math.max(chartWidth, 1)));
2859
- return;
2860
- }
2861
- if (viewState.panOffset >= getMaxPanOffset(boundsHistory, viewState.zoomLevel) && expandBufferRange({
2862
- kind: "pan-left",
2863
- targetPanOffset: viewState.panOffset + panStep,
2864
- })) {
2865
- return;
2866
- }
2867
- setViewState((current) => {
2868
- const cleared = clearActivePreset(current);
2869
- return { ...cleared, panOffset: current.panOffset + panStep };
2870
- });
2871
- return;
2872
- case "d":
2873
- if (effectiveResolution === "auto") {
2874
- requestAutoWindow(shiftDateWindow(navigableDateWindow, -panStep / Math.max(chartWidth, 1)));
2875
- return;
2876
- }
2877
- setViewState((current) => {
2878
- const cleared = clearActivePreset(current);
2879
- const nextPanOffset = Math.max(current.panOffset - panStep, 0);
2880
- return cleared.panOffset === nextPanOffset ? cleared : { ...cleared, panOffset: nextPanOffset };
2881
- });
2882
- return;
2883
- case "m":
2884
- setViewState((current) => {
2885
- const activeMode = current.renderMode ?? "area";
2886
- const index = CHART_RENDER_MODES.indexOf(activeMode);
2887
- const nextMode = CHART_RENDER_MODES[(index + 1) % CHART_RENDER_MODES.length]!;
2888
- persistRenderMode(nextMode);
2889
- return { ...current, renderMode: nextMode };
2890
- });
2891
- return;
2892
- case "r": {
2893
- const currentIndex = resolutionChips.indexOf(selectedResolution);
2894
- const nextResolution = resolutionChips[(currentIndex + 1) % resolutionChips.length] ?? "auto";
2895
- setResolution(nextResolution);
2896
- return;
2897
- }
2898
- }
2899
-
2900
- if (key >= "1" && key <= "7") {
2901
- const index = parseInt(key) - 1;
2902
- if (index < TIME_RANGES.length) setRange(TIME_RANGES[index]!);
2903
- return;
2904
- }
2905
-
2906
- if (!interactive) return;
2907
-
2908
- switch (key) {
2909
- case "escape":
2910
- mouseCrosshairDisabledRef.current = true;
2911
- updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
2912
- commitSelectionCursor({ cursorX: null, cursorY: null });
2913
- return;
2914
- case "left":
2915
- if (event.shift) {
2916
- if (effectiveResolution === "auto") {
2917
- requestAutoWindow(shiftDateWindow(navigableDateWindow, panStep / Math.max(chartWidth, 1)));
2918
- return;
2919
- }
2920
- if (viewState.panOffset >= getMaxPanOffset(boundsHistory, viewState.zoomLevel) && expandBufferRange({
2921
- kind: "pan-left",
2922
- targetPanOffset: viewState.panOffset + panStep,
2923
- })) {
2924
- return;
2925
- }
2926
- setViewState((current) => {
2927
- const cleared = clearActivePreset(current);
2928
- return { ...cleared, panOffset: current.panOffset + panStep };
2929
- });
2930
- } else {
2931
- mouseCrosshairDisabledRef.current = false;
2932
- cursorMotionKindRef.current = "discrete";
2933
- const pointCount = projection.points.length;
2934
- const currentIndex = pointCount <= 0
2935
- ? -1
2936
- : getActivePointIndex(
2937
- pointCount,
2938
- chartWidth,
2939
- viewState.cursorX ?? maxCursorX,
2940
- projection.effectiveMode,
2941
- );
2942
- const nextCursor = resolveAdjacentSelectionCursorX(
2943
- viewState.cursorX,
2944
- -1,
2945
- pointCount,
2946
- chartWidth,
2947
- projection.effectiveMode,
2948
- );
2949
- if (effectiveResolution === "auto" && currentIndex <= 0) {
2950
- requestAutoWindow(shiftDateWindow(
2951
- navigableDateWindow,
2952
- 1 / Math.max(pointCount - 1, 1),
2953
- ));
2954
- commitSelectionCursor({ cursorX: nextCursor, cursorY: null });
2955
- return;
2956
- }
2957
- setViewState((current) => {
2958
- const maxPan = getMaxPanOffset(boundsHistory, current.zoomLevel);
2959
- if (currentIndex <= 0) {
2960
- return {
2961
- ...clearActivePreset(current),
2962
- cursorX: nextCursor,
2963
- cursorY: null,
2964
- panOffset: clamp(current.panOffset + 1, 0, maxPan),
2965
- };
2966
- }
2967
- return { ...current, cursorX: nextCursor, cursorY: null };
2968
- });
2969
- }
2970
- return;
2971
- case "right":
2972
- if (event.shift) {
2973
- if (effectiveResolution === "auto") {
2974
- requestAutoWindow(shiftDateWindow(navigableDateWindow, -panStep / Math.max(chartWidth, 1)));
2975
- return;
2976
- }
2977
- setViewState((current) => {
2978
- const cleared = clearActivePreset(current);
2979
- const nextPanOffset = Math.max(current.panOffset - panStep, 0);
2980
- return cleared.panOffset === nextPanOffset ? cleared : { ...cleared, panOffset: nextPanOffset };
2981
- });
2982
- } else {
2983
- mouseCrosshairDisabledRef.current = false;
2984
- cursorMotionKindRef.current = "discrete";
2985
- const pointCount = projection.points.length;
2986
- const currentIndex = pointCount <= 0
2987
- ? -1
2988
- : getActivePointIndex(
2989
- pointCount,
2990
- chartWidth,
2991
- viewState.cursorX ?? 0,
2992
- projection.effectiveMode,
2993
- );
2994
- const nextCursor = resolveAdjacentSelectionCursorX(
2995
- viewState.cursorX,
2996
- 1,
2997
- pointCount,
2998
- chartWidth,
2999
- projection.effectiveMode,
3000
- );
3001
- if (effectiveResolution === "auto" && currentIndex >= pointCount - 1) {
3002
- requestAutoWindow(shiftDateWindow(
3003
- navigableDateWindow,
3004
- -1 / Math.max(pointCount - 1, 1),
3005
- ));
3006
- commitSelectionCursor({ cursorX: nextCursor, cursorY: null });
3007
- return;
3008
- }
3009
- setViewState((current) => {
3010
- if (currentIndex >= pointCount - 1) {
3011
- return {
3012
- ...clearActivePreset(current),
3013
- cursorX: nextCursor,
3014
- cursorY: null,
3015
- panOffset: Math.max(current.panOffset - 1, 0),
3016
- };
3017
- }
3018
- return { ...current, cursorX: nextCursor, cursorY: null };
3019
- });
3020
- }
3021
- return;
3022
- }
3023
- });
3024
-
3025
- const chartColors = useMemo(() => {
3026
- const rawChange = chartWindow.points.length >= 2
3027
- ? chartWindow.points[chartWindow.points.length - 1]!.close - chartWindow.points[0]!.close
3028
- : 0;
3029
- const trend = rawChange < 0 ? "negative" : rawChange > 0 ? "positive" : "neutral";
3030
- return resolveChartPalette({
3031
- bg: themeColors.bg,
3032
- border: themeColors.border,
3033
- borderFocused: themeColors.borderFocused,
3034
- text: themeColors.text,
3035
- textDim: themeColors.textDim,
3036
- positive: themeColors.positive,
3037
- negative: themeColors.negative,
3038
- }, trend);
3039
- }, [
3040
- chartWindow.points,
3041
- themeColors.bg,
3042
- themeColors.border,
3043
- themeColors.borderFocused,
3044
- themeColors.negative,
3045
- themeColors.positive,
3046
- themeColors.text,
3047
- themeColors.textDim,
3048
- ]);
3049
- const marketSession = useMemo(() => resolveChartMarketSession(ticker
3050
- ? [{
3051
- exchange: ticker.metadata.exchange,
3052
- currency: ticker.metadata.currency,
3053
- assetCategory: ticker.metadata.assetCategory,
3054
- }]
3055
- : []), [ticker]);
3056
- const marketSessionKey = useMemo(() => getChartMarketSessionKey(marketSession), [marketSession]);
3057
-
3058
- const cursorX = viewState.cursorX !== null ? clamp(viewState.cursorX, 0, chartWidth - 1) : null;
3059
- const cursorY = viewState.cursorY !== null ? clamp(viewState.cursorY, 0, chartHeight - 1) : null;
3060
- const selectionCursorX = interactive ? cursorX : null;
3061
- const selectionCursorY = interactive ? cursorY : null;
3062
- const displayCursorX = interactive && displayCursor.cellX !== null ? clamp(displayCursor.cellX, 0, chartWidth - 1) : null;
3063
- const displayCursorY = interactive && displayCursor.cellY !== null ? clamp(displayCursor.cellY, 0, chartHeight - 1) : null;
3064
-
3065
- const commitSelectionCursor = (next: { cursorX: number | null; cursorY: number | null }) => {
3066
- setViewState((current) => (
3067
- current.cursorX === next.cursorX && current.cursorY === next.cursorY
3068
- ? current
3069
- : { ...current, cursorX: next.cursorX, cursorY: next.cursorY }
3070
- ));
3071
- };
3072
-
3073
- const selectionScene = useMemo(() => buildChartScene(projection.points, {
3074
- width: chartWidth,
3075
- height: chartHeight,
3076
- showVolume: showVolume && !compact,
3077
- volumeHeight,
3078
- cursorX: selectionCursorX,
3079
- cursorY: selectionCursorY,
3080
- mode: projection.effectiveMode,
3081
- axisMode,
3082
- colors: chartColors,
3083
- timeAxisDates,
3084
- marketSession,
3085
- }), [axisMode, chartColors, chartHeight, chartWidth, compact, marketSession, projection.effectiveMode, projection.points, selectionCursorX, selectionCursorY, showVolume, timeAxisDates, volumeHeight]);
3086
- const displayScene = useMemo(() => buildChartScene(projection.points, {
3087
- width: chartWidth,
3088
- height: chartHeight,
3089
- showVolume: showVolume && !compact,
3090
- volumeHeight,
3091
- cursorX: displayCursorX,
3092
- cursorY: displayCursorY,
3093
- mode: projection.effectiveMode,
3094
- axisMode,
3095
- colors: chartColors,
3096
- timeAxisDates,
3097
- indicators,
3098
- marketSession,
3099
- }), [axisMode, chartColors, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, indicators, marketSession, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
3100
- const snappedSelectionCursorX = selectionScene
3101
- ? getPointTerminalColumn(selectionScene.activeIdx, projection.points.length, chartWidth, projection.effectiveMode)
3102
- : null;
3103
-
3104
- useEffect(() => {
3105
- const remapCursor = (cursor: DisplayCursorState) => buildDisplayCursorState(
3106
- cursor.cellX,
3107
- cursor.cellY,
3108
- plotRef.current,
3109
- renderer,
3110
- );
3111
- const nextDisplay = remapCursor(displayCursorRef.current);
3112
- const nextTarget = remapCursor(targetCursorRef.current);
3113
- displayCursorRef.current = nextDisplay;
3114
- targetCursorRef.current = nextTarget;
3115
- setDisplayCursor((current) => (sameDisplayCursorState(current, nextDisplay) ? current : nextDisplay));
3116
- }, [chartHeight, chartWidth, renderer.resolution?.height, renderer.resolution?.width]);
3117
-
3118
- useEffect(() => {
3119
- if (cursorMotionKindRef.current === "pixel") return;
3120
- updateDisplayCursorTarget(
3121
- resolveSelectionDisplayCursorState(
3122
- selectionCursorX,
3123
- selectionCursorY,
3124
- cursorMotionKindRef.current === "discrete" ? snappedSelectionCursorX : null,
3125
- selectionScene?.cursorY ?? null,
3126
- plotRef.current,
3127
- renderer,
3128
- ),
3129
- cursorMotionKindRef.current,
3130
- );
3131
- }, [renderer, selectionCursorX, selectionCursorY, selectionScene?.cursorY, snappedSelectionCursorX]);
3132
-
3133
- const nativeBaseScene = useMemo(() => buildChartScene(projection.points, {
3134
- width: chartWidth,
3135
- height: chartHeight,
3136
- showVolume: showVolume && !compact,
3137
- volumeHeight,
3138
- cursorX: null,
3139
- cursorY: null,
3140
- mode: projection.effectiveMode,
3141
- axisMode,
3142
- colors: chartColors,
3143
- timeAxisDates,
3144
- indicators,
3145
- marketSession,
3146
- }), [axisMode, chartColors, chartHeight, chartWidth, compact, indicators, marketSession, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
3147
-
3148
- const staticResult = useMemo(() => renderChart(projection.points, {
3149
- width: chartWidth,
3150
- height: chartHeight,
3151
- showVolume: showVolume && !compact,
3152
- volumeHeight,
3153
- cursorX: null,
3154
- cursorY: null,
3155
- mode: projection.effectiveMode,
3156
- axisMode,
3157
- currency: chartCurrency,
3158
- assetCategory: chartAssetCategory,
3159
- colors: chartColors,
3160
- timeAxisDates,
3161
- indicators,
3162
- marketSession,
3163
- }), [axisMode, chartAssetCategory, chartColors, chartCurrency, chartHeight, chartWidth, compact, indicators, marketSession, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
3164
-
3165
- const interactiveResult = useMemo(() => (
3166
- effectiveRenderer === "kitty" || useCanvasChart
3167
- ? null
3168
- : renderChart(projection.points, {
3169
- width: chartWidth,
3170
- height: chartHeight,
3171
- showVolume: showVolume && !compact,
3172
- volumeHeight,
3173
- cursorX: displayCursorX,
3174
- cursorY: displayCursorY,
3175
- mode: projection.effectiveMode,
3176
- axisMode,
3177
- currency: chartCurrency,
3178
- assetCategory: chartAssetCategory,
3179
- colors: chartColors,
3180
- timeAxisDates,
3181
- indicators,
3182
- marketSession,
3183
- })
3184
- ), [axisMode, chartAssetCategory, chartColors, chartCurrency, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, effectiveRenderer, indicators, marketSession, projection.effectiveMode, projection.points, showVolume, timeAxisDates, useCanvasChart, volumeHeight]);
3185
-
3186
- const result = effectiveRenderer === "kitty" || useCanvasChart ? staticResult : interactiveResult!;
3187
-
3188
- const rasterCursorRow = (effectiveRenderer === "kitty" || useCanvasChart) && displayCursorY !== null && nativeBaseScene
3189
- ? Math.round(clamp(displayCursorY, 0, Math.max(nativeBaseScene.chartRows - 1, 0)))
3190
- : null;
3191
- const rasterCrosshairPrice = (effectiveRenderer === "kitty" || useCanvasChart) && displayCursorY !== null && nativeBaseScene
3192
- ? nativeBaseScene.max
3193
- - (clamp(displayCursorY, 0, Math.max(nativeBaseScene.chartRows - 1, 0)) / Math.max(nativeBaseScene.chartRows - 1, 1))
3194
- * (nativeBaseScene.max - nativeBaseScene.min)
3195
- : null;
3196
- const cursorRow = effectiveRenderer === "kitty" || useCanvasChart ? rasterCursorRow : result.cursorRow;
3197
- const crosshairPrice = effectiveRenderer === "kitty" || useCanvasChart ? rasterCrosshairPrice : result.crosshairPrice;
3198
-
3199
- const nativeCrosshair = useMemo<NativeCrosshairOverlay | null>(() => {
3200
- if (!interactive || displayCursor.cellX === null || displayCursor.cellY === null) return null;
3201
- return {
3202
- width: chartWidth,
3203
- height: chartHeight,
3204
- chartRows: chartHeight - (showVolume && !compact ? volumeHeight : 0),
3205
- pixelX: displayCursor.pixelX,
3206
- pixelY: displayCursor.pixelY,
3207
- colors: {
3208
- crosshairColor: chartColors.crosshairColor,
3209
- },
3210
- };
3211
- }, [
3212
- chartColors.crosshairColor,
3213
- chartHeight,
3214
- chartWidth,
3215
- compact,
3216
- displayCursor.cellX,
3217
- displayCursor.cellY,
3218
- displayCursor.pixelX,
3219
- displayCursor.pixelY,
3220
- interactive,
3221
- showVolume,
3222
- volumeHeight,
3223
- ]);
3224
- const blankPlotLines = useMemo(() => buildBlankPlotLines(chartWidth, chartHeight), [chartHeight, chartWidth]);
3225
-
3226
- useEffect(() => {
3227
- if (effectiveRenderer === "kitty") return;
3228
- lastNativeBaseBitmapRef.current = null;
3229
- lastNativeCrosshairBitmapRef.current = null;
3230
- nativeSurfaceManager.removeSurface(nativeBaseSurfaceId);
3231
- nativeSurfaceManager.removeSurface(nativeCrosshairSurfaceId);
3232
- lastNativeGeometryRef.current = null;
3233
- }, [effectiveRenderer, nativeBaseSurfaceId, nativeCrosshairSurfaceId, nativeSurfaceManager]);
3234
-
3235
- useEffect(() => {
3236
- if (effectiveRenderer !== "kitty" || !rendererState.nativeReady || !plotRef.current) return;
3237
- const plot = plotRef.current;
3238
- let mountTimer: ReturnType<typeof setTimeout> | null = null;
3239
-
3240
- const syncPlacement = () => {
3241
- if (effectiveRenderer !== "kitty" || !rendererState.nativeReady || !plotRef.current) return;
3242
- const plot = plotRef.current as NativeSurfaceRenderableNode;
3243
- const rect = getRenderableCellRect(plot);
3244
- const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
3245
- const previous = lastNativeGeometryRef.current;
3246
- if (previous
3247
- && previous.rect.x === rect.x
3248
- && previous.rect.y === rect.y
3249
- && previous.rect.width === rect.width
3250
- && previous.rect.height === rect.height
3251
- && previous.visibleRect?.x === visibleRect?.x
3252
- && previous.visibleRect?.y === visibleRect?.y
3253
- && previous.visibleRect?.width === visibleRect?.width
3254
- && previous.visibleRect?.height === visibleRect?.height) {
3255
- return;
3256
- }
3257
- lastNativeGeometryRef.current = { rect, visibleRect };
3258
- const geometry = {
3259
- paneId,
3260
- rect,
3261
- visibleRect,
3262
- };
3263
- syncCachedNativeSurface(
3264
- nativeSurfaceManager,
3265
- nativeBaseSurfaceId,
3266
- geometry,
3267
- lastNativeBaseBitmapRef.current,
3268
- );
3269
- syncCachedNativeSurface(
3270
- nativeSurfaceManager,
3271
- nativeCrosshairSurfaceId,
3272
- geometry,
3273
- lastNativeCrosshairBitmapRef.current,
3274
- );
3275
- };
3276
-
3277
- plot.onLifecyclePass = syncPlacement;
3278
- renderer.registerLifecyclePass(plot);
3279
- syncPlacement();
3280
- mountTimer = setTimeout(() => {
3281
- syncPlacement();
3282
- renderer.requestRender();
3283
- }, 0);
3284
- return () => {
3285
- if (mountTimer) clearTimeout(mountTimer);
3286
- plot.onLifecyclePass = null;
3287
- renderer.unregisterLifecyclePass(plot);
3288
- lastNativeGeometryRef.current = null;
3289
- };
3290
- }, [effectiveRenderer, nativeBaseSurfaceId, nativeCrosshairSurfaceId, nativeSurfaceManager, paneId, renderer, rendererState.nativeReady]);
3291
-
3292
- useEffect(() => {
3293
- if (effectiveRenderer !== "kitty" || !rendererState.nativeReady || !renderer.resolution || !plotRef.current || !nativeBaseScene) {
3294
- lastNativeBaseBitmapRef.current = null;
3295
- nativeSurfaceManager.removeSurface(nativeBaseSurfaceId);
3296
- return;
3297
- }
3298
-
3299
- const plot = plotRef.current as NativeSurfaceRenderableNode;
3300
- const plotRect = getRenderableCellRect(plot);
3301
- const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
3302
- const bitmapSize = computeBitmapSize(plotRect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
3303
- const bitmapKey = buildNativeBitmapKey(
3304
- projection.points.length,
3305
- projection.points,
3306
- bitmapSize.pixelWidth,
3307
- bitmapSize.pixelHeight,
3308
- projection.effectiveMode,
3309
- showVolume && !compact,
3310
- [
3311
- chartColors.lineColor,
3312
- chartColors.fillColor,
3313
- chartColors.gridColor,
3314
- chartColors.volumeUp,
3315
- chartColors.volumeDown,
3316
- chartColors.candleUp,
3317
- chartColors.candleDown,
3318
- chartColors.preMarketBgColor,
3319
- chartColors.postMarketBgColor,
3320
- ].join(","),
3321
- indicatorRenderKey,
3322
- marketSessionKey,
3323
- );
3324
- const cachedBitmap = lastNativeBaseBitmapRef.current?.key === bitmapKey
3325
- ? lastNativeBaseBitmapRef.current.bitmap
3326
- : null;
3327
- const bitmap = cachedBitmap ?? renderNativeChartBase(nativeBaseScene, bitmapSize.pixelWidth, bitmapSize.pixelHeight);
3328
- if (!cachedBitmap) {
3329
- lastNativeBaseBitmapRef.current = { key: bitmapKey, bitmap };
3330
- }
3331
-
3332
- if (!visibleRect) {
3333
- nativeSurfaceManager.removeSurface(nativeBaseSurfaceId);
3334
- return;
3335
- }
3336
-
3337
- nativeSurfaceManager.upsertSurface({
3338
- id: nativeBaseSurfaceId,
3339
- paneId,
3340
- rect: plotRect,
3341
- visibleRect,
3342
- bitmap,
3343
- bitmapKey,
3344
- });
3345
- renderer.requestRender();
3346
- }, [
3347
- chartColors.candleDown,
3348
- chartColors.candleUp,
3349
- chartColors.fillColor,
3350
- chartColors.gridColor,
3351
- chartColors.lineColor,
3352
- chartColors.postMarketBgColor,
3353
- chartColors.preMarketBgColor,
3354
- chartColors.volumeDown,
3355
- chartColors.volumeUp,
3356
- compact,
3357
- effectiveRenderer,
3358
- indicatorRenderKey,
3359
- marketSessionKey,
3360
- nativeBaseScene,
3361
- nativeBaseSurfaceId,
3362
- nativeSurfaceManager,
3363
- paneId,
3364
- projection.effectiveMode,
3365
- projection.points,
3366
- renderer,
3367
- rendererState.nativeReady,
3368
- showVolume,
3369
- ]);
3370
-
3371
- useEffect(() => {
3372
- if (effectiveRenderer !== "kitty" || !rendererState.nativeReady || !renderer.resolution || !plotRef.current || !nativeCrosshair) {
3373
- lastNativeCrosshairBitmapRef.current = null;
3374
- nativeSurfaceManager.removeSurface(nativeCrosshairSurfaceId);
3375
- return;
3376
- }
3377
-
3378
- const plot = plotRef.current as NativeSurfaceRenderableNode;
3379
- const plotRect = getRenderableCellRect(plot);
3380
- const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
3381
- const bitmapSize = computeBitmapSize(plotRect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
3382
- const renderablePixelSize = getRenderablePixelSize(plotRef.current, renderer);
3383
- const overlayPixelX = scaleLocalPixelCoordinate(
3384
- nativeCrosshair.pixelX,
3385
- renderablePixelSize?.pixelWidth ?? bitmapSize.pixelWidth,
3386
- bitmapSize.pixelWidth,
3387
- );
3388
- const overlayPixelY = scaleLocalPixelCoordinate(
3389
- nativeCrosshair.pixelY,
3390
- renderablePixelSize?.pixelHeight ?? bitmapSize.pixelHeight,
3391
- bitmapSize.pixelHeight,
3392
- );
3393
-
3394
- if (overlayPixelX === null || overlayPixelY === null) {
3395
- lastNativeCrosshairBitmapRef.current = null;
3396
- nativeSurfaceManager.removeSurface(nativeCrosshairSurfaceId);
3397
- return;
3398
- }
3399
-
3400
- const overlay = {
3401
- ...nativeCrosshair,
3402
- pixelX: overlayPixelX,
3403
- pixelY: overlayPixelY,
3404
- };
3405
- const bitmapKey = buildNativeCrosshairBitmapKey(
3406
- bitmapSize.pixelWidth,
3407
- bitmapSize.pixelHeight,
3408
- overlay.height,
3409
- overlay.chartRows,
3410
- overlay.colors.crosshairColor,
3411
- overlay.pixelX,
3412
- overlay.pixelY,
3413
- );
3414
- const cachedBitmap = lastNativeCrosshairBitmapRef.current?.key === bitmapKey
3415
- ? lastNativeCrosshairBitmapRef.current.bitmap
3416
- : null;
3417
- const bitmap = cachedBitmap ?? renderNativeCrosshairOverlay(overlay, bitmapSize.pixelWidth, bitmapSize.pixelHeight);
3418
- if (!cachedBitmap) {
3419
- lastNativeCrosshairBitmapRef.current = { key: bitmapKey, bitmap };
3420
- }
3421
-
3422
- if (!visibleRect) {
3423
- nativeSurfaceManager.removeSurface(nativeCrosshairSurfaceId);
3424
- return;
3425
- }
3426
-
3427
- nativeSurfaceManager.upsertSurface({
3428
- id: nativeCrosshairSurfaceId,
3429
- paneId,
3430
- rect: plotRect,
3431
- visibleRect,
3432
- bitmap,
3433
- bitmapKey,
3434
- });
3435
- }, [
3436
- effectiveRenderer,
3437
- nativeCrosshair,
3438
- nativeCrosshairSurfaceId,
3439
- nativeSurfaceManager,
3440
- paneId,
3441
- renderer,
3442
- rendererState.nativeReady,
3443
- ]);
3444
-
3445
- const hasHistory = chartWindow.points.length > 0;
3446
- const requestedMode = projection.requestedMode;
3447
- const showOhlcSummary = projection.effectiveMode === "candles" || projection.effectiveMode === "ohlc" || projection.effectiveMode === "hlc";
3448
- const hasDisplayCursor = displayCursorX !== null && displayCursorY !== null;
3449
- const activePoint = showOhlcSummary ? (selectionScene?.activePoint ?? null) : null;
3450
- const visiblePriceRange = selectionScene
3451
- ? Math.max(selectionScene.max - selectionScene.min, 0)
3452
- : (staticResult.priceRange ?? undefined);
3453
- const axisLabels = new Map(staticResult.axisLabels.map((entry) => [entry.row, entry.label]));
3454
- const cursorAxisLabel = hasDisplayCursor && cursorRow !== null && crosshairPrice !== null
3455
- ? formatCursorAxisValue(
3456
- crosshairPrice,
3457
- axisMode,
3458
- projection.points[0]?.close ?? 0,
3459
- chartCurrency,
3460
- chartAssetCategory,
3461
- visiblePriceRange,
3462
- )
3463
- : null;
3464
- const isBlockingBody = !compact && renderBodyState.blocking;
3465
- const bodyMessage = !compact
3466
- ? (renderBodyState.errorMessage ?? renderBodyState.emptyMessage)
3467
- : null;
3468
- const isUpdating = !compact && (renderBodyState.updating || boundsBodyState.updating);
3469
- const timeAxisLabel = selectionScene?.timeLabels ?? staticResult.timeLabels;
3470
- const cursorTimeAxisColumn = hasDisplayCursor ? displayScene?.cursorColumn ?? null : null;
3471
- const cursorTimeAxisDate = hasDisplayCursor ? displayScene?.dateAtCursor ?? null : null;
3472
- const timeAxisBox = (
3473
- <Box
3474
- height={1}
3475
- width={chartWidth}
3476
- overflow="hidden"
3477
- data-gloom-role="chart-time-axis"
3478
- >
3479
- <TimeAxisLabel
3480
- timeLabels={timeAxisLabel}
3481
- width={chartWidth}
3482
- cursorColumn={cursorTimeAxisColumn}
3483
- cursorPixelX={hasDisplayCursor ? displayCursor.pixelX : null}
3484
- cursorDate={cursorTimeAxisDate}
3485
- dates={timeAxisDates}
3486
- cursorColor={chartColors.crosshairColor}
3487
- />
3488
- </Box>
3489
- );
3490
- const chartFooterHints = useMemo<PaneHint[]>(() => {
3491
- if (compact) return [];
3492
-
3493
- const cycleMode = () => {
3494
- setViewState((current) => {
3495
- const activeMode = current.renderMode ?? "area";
3496
- const index = CHART_RENDER_MODES.indexOf(activeMode);
3497
- const nextMode = CHART_RENDER_MODES[(index + 1) % CHART_RENDER_MODES.length]!;
3498
- persistRenderMode(nextMode);
3499
- return { ...current, renderMode: nextMode };
3500
- });
3501
- };
3502
- const cycleResolution = () => {
3503
- const currentIndex = resolutionChips.indexOf(selectedResolution);
3504
- const nextResolution = resolutionChips[(currentIndex + 1) % resolutionChips.length] ?? "auto";
3505
- setResolution(nextResolution);
3506
- };
3507
- const zoomIn = () => {
3508
- if (effectiveResolution === "auto") {
3509
- requestAutoWindow(resolveAutoZoomWindow({
3510
- historyPoints: history,
3511
- boundsDates: boundsHistoryDates,
3512
- currentWindow: navigableDateWindow,
3513
- direction: "in",
3514
- anchorRatio: RIGHT_EDGE_ANCHOR_RATIO,
3515
- }));
3516
- return;
3517
- }
3518
- setViewState((current) => applyZoomAroundAnchor(current, current.zoomLevel * 1.5, RIGHT_EDGE_ANCHOR_RATIO, boundsHistory));
3519
- };
3520
- const resetView = () => {
3521
- if (effectiveResolution === "auto") {
3522
- pendingAutoWindowRef.current = null;
3523
- setPendingAutoWindowOverride(null);
3524
- setRenderedAutoView(null);
3525
- updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
3526
- setViewState((current) => clearAutoViewportState(current));
3527
- return;
3528
- }
3529
- pendingCanonicalResetRef.current += 1;
3530
- updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
3531
- setViewState((current) => {
3532
- const nextState = resolveViewportResolutionSelection(
3533
- current,
3534
- effectiveResolution,
3535
- selectionSupportMap,
3536
- visibleDateWindow,
3537
- ) ?? current;
3538
- return {
3539
- ...nextState,
3540
- panOffset: 0,
3541
- zoomLevel: 1,
3542
- cursorX: null,
3543
- cursorY: null,
3544
- };
3545
- });
3546
- };
3547
- const cycleRange = () => {
3548
- const currentIndex = activePreset ? TIME_RANGES.indexOf(activePreset) : -1;
3549
- const nextRange = TIME_RANGES[(currentIndex + 1) % TIME_RANGES.length] ?? TIME_RANGES[0]!;
3550
- setRange(nextRange);
3551
- };
3552
-
3553
- return [
3554
- { id: "mode", key: "m", label: "ode", onPress: cycleMode },
3555
- ...(footerHints ?? []),
3556
- { id: "resolution", key: "r", label: "es", onPress: cycleResolution },
3557
- { id: "zoom", key: "+/-", label: "zoom", onPress: zoomIn },
3558
- { id: "reset", key: "0", label: "reset", onPress: resetView },
3559
- ...(width >= 72 ? [{ id: "range", key: "1-7", label: "range", onPress: cycleRange }] : []),
3560
- ];
3561
- }, [
3562
- activePreset,
3563
- boundsHistory,
3564
- boundsHistoryDates,
3565
- compact,
3566
- effectiveResolution,
3567
- footerHints,
3568
- history,
3569
- navigableDateWindow,
3570
- persistRenderMode,
3571
- resolutionChips,
3572
- selectedResolution,
3573
- selectionSupportMap,
3574
- visibleDateWindow,
3575
- width,
3576
- ]);
3577
-
3578
- const chartFooterInfo = useMemo<PaneFooterSegment[]>(() => {
3579
- if (compact || !showOhlcSummary || !activePoint) return [];
3580
-
3581
- const formatPrice = (value: number) => formatMarketPriceWithCurrency(value, chartCurrency, {
3582
- assetCategory: chartAssetCategory,
3583
- minimumFractionDigits: 2,
3584
- precisionOffset: 1,
3585
- priceRange: visiblePriceRange,
3586
- });
3587
-
3588
- const parts: PaneFooterSegment["parts"] = [
3589
- ...(projection.effectiveMode === "hlc"
3590
- ? []
3591
- : [
3592
- { text: "O", tone: "label" as const },
3593
- { text: formatPrice(activePoint.open), tone: "value" as const },
3594
- ]),
3595
- { text: "H", tone: "label" },
3596
- { text: formatPrice(activePoint.high), tone: "value" },
3597
- { text: "L", tone: "label" },
3598
- { text: formatPrice(activePoint.low), tone: "value" },
3599
- { text: "C", tone: "label" },
3600
- { text: formatPrice(activePoint.close), tone: "value" },
3601
- { text: "V", tone: "label" },
3602
- { text: formatCompact(activePoint.volume), tone: "value" },
3603
- ];
3604
-
3605
- return [{
3606
- id: projection.effectiveMode === "hlc" ? "hlc" : "ohlc",
3607
- parts,
3608
- }];
3609
- }, [
3610
- activePoint,
3611
- chartAssetCategory,
3612
- chartCurrency,
3613
- compact,
3614
- projection.effectiveMode,
3615
- showOhlcSummary,
3616
- visiblePriceRange,
3617
- ]);
3618
-
3619
- usePaneFooter("stock-chart", () => (
3620
- compact
3621
- ? null
3622
- : {
3623
- order: 10,
3624
- info: chartFooterInfo,
3625
- hints: chartFooterHints,
3626
- }
3627
- ), [chartFooterHints, chartFooterInfo, compact]);
3628
-
3629
- useEffect(() => {
3630
- queueMicrotask(() => renderer.requestRender());
3631
- }, [
3632
- activePreset,
3633
- bodyMessage,
3634
- fallbackResolutionLabel,
3635
- isUpdating,
3636
- renderer,
3637
- selectedResolution,
3638
- ]);
3639
-
3640
- const handlePlotMove = (event: ChartMouseEvent) => {
3641
- if (!interactive || compact) return;
3642
- if (mouseCrosshairDisabledRef.current) return;
3643
- const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
3644
- if (!localPointer) return;
3645
- const selectionCursor = resolveSelectionCursor(localPointer, projection.points.length, chartWidth, projection.effectiveMode);
3646
- updateDisplayCursorTarget(
3647
- buildDisplayCursorState(
3648
- localPointer.cellX,
3649
- localPointer.cellY,
3650
- plotRef.current,
3651
- renderer,
3652
- localPointer.pixelX,
3653
- localPointer.pixelY,
3654
- ),
3655
- resolveCursorMotionKind(event, renderer),
3656
- );
3657
- commitSelectionCursor(selectionCursor);
3658
- };
3659
-
3660
- const handlePlotDown = (event: ChartMouseEvent) => {
3661
- if (compact) return;
3662
- if (!interactive) onActivate?.();
3663
- focusPaneForMouseInteraction(event);
3664
- mouseCrosshairDisabledRef.current = false;
3665
- const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
3666
- if (!localPointer) return;
3667
- const selectionCursor = resolveSelectionCursor(localPointer, projection.points.length, chartWidth, projection.effectiveMode);
3668
- updateDisplayCursorTarget(
3669
- buildDisplayCursorState(
3670
- localPointer.cellX,
3671
- localPointer.cellY,
3672
- plotRef.current,
3673
- renderer,
3674
- localPointer.pixelX,
3675
- localPointer.pixelY,
3676
- ),
3677
- resolveCursorMotionKind(event, renderer),
3678
- );
3679
- dragRef.current = {
3680
- startGlobalX: getGlobalMouseX(event, renderer),
3681
- startPanOffset: viewState.panOffset,
3682
- startWindow: navigableDateWindow,
3683
- };
3684
- commitSelectionCursor(selectionCursor);
3685
- };
3686
-
3687
- const handlePlotDrag = (event: ChartMouseEvent) => {
3688
- if (compact || (!interactive && !dragRef.current)) return;
3689
- if (mouseCrosshairDisabledRef.current && !dragRef.current) return;
3690
- const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
3691
- if (localPointer) {
3692
- const selectionCursor = resolveSelectionCursor(localPointer, projection.points.length, chartWidth, projection.effectiveMode);
3693
- updateDisplayCursorTarget(
3694
- buildDisplayCursorState(
3695
- localPointer.cellX,
3696
- localPointer.cellY,
3697
- plotRef.current,
3698
- renderer,
3699
- localPointer.pixelX,
3700
- localPointer.pixelY,
3701
- ),
3702
- resolveCursorMotionKind(event, renderer),
3703
- );
3704
- commitSelectionCursor(selectionCursor);
3705
- }
3706
- if (!dragRef.current) return;
3707
-
3708
- if (effectiveResolution === "auto" && dragRef.current.startWindow) {
3709
- const deltaCells = getGlobalMouseX(event, renderer) - dragRef.current.startGlobalX;
3710
- requestAutoWindow(shiftDateWindow(
3711
- dragRef.current.startWindow,
3712
- getDragPanWindowRatio(deltaCells, chartWidth),
3713
- ));
3714
- return;
3715
- }
3716
-
3717
- const visibleCount = getVisiblePointCount(boundsHistory.length, viewState.zoomLevel);
3718
- const deltaCells = getGlobalMouseX(event, renderer) - dragRef.current.startGlobalX;
3719
- const nextPan = resolveDragPanOffset(
3720
- dragRef.current.startPanOffset,
3721
- deltaCells,
3722
- chartWidth,
3723
- visibleCount,
3724
- boundsHistory.length - visibleCount,
3725
- );
3726
- setViewState((current) => {
3727
- const cleared = clearActivePreset(current);
3728
- return cleared.panOffset === nextPan ? cleared : { ...cleared, panOffset: nextPan };
3729
- });
3730
- };
3731
-
3732
- const handlePlotScroll = (event: ChartMouseEvent) => {
3733
- if (compact) return;
3734
- event.stopPropagation?.();
3735
- event.preventDefault?.();
3736
- focusPaneForMouseInteraction(event);
3737
- const direction = event.scroll?.direction;
3738
- if (!direction) return;
3739
- const scrollPan = consumeScrollPanMovement(
3740
- chartWidth,
3741
- event.scroll?.delta,
3742
- direction,
3743
- scrollPanCellRemainderRef.current,
3744
- );
3745
- scrollPanCellRemainderRef.current = scrollPan.remainder;
3746
- const scrollPanCells = scrollPan.cells;
3747
- const scrollPanRatio = scrollPan.ratio;
3748
-
3749
- const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
3750
-
3751
- if (localPointer) {
3752
- const selectionCursor = resolveSelectionCursor(localPointer, projection.points.length, chartWidth, projection.effectiveMode);
3753
- updateDisplayCursorTarget(
3754
- buildDisplayCursorState(
3755
- localPointer.cellX,
3756
- localPointer.cellY,
3757
- plotRef.current,
3758
- renderer,
3759
- localPointer.pixelX,
3760
- localPointer.pixelY,
3761
- ),
3762
- resolveCursorMotionKind(event, renderer),
3763
- );
3764
- commitSelectionCursor(selectionCursor);
3765
- }
3766
-
3767
- if (scrollPanCells === 0) return;
3768
-
3769
- if (effectiveResolution === "auto") {
3770
- requestAutoWindow(shiftDateWindow(navigableDateWindow, scrollPanRatio));
3771
- return;
3772
- }
3773
-
3774
- const targetPanOffset = viewState.panOffset + scrollPanCells;
3775
- if (scrollPanCells > 0 && viewState.panOffset >= getMaxPanOffset(boundsHistory, viewState.zoomLevel) && expandBufferRange({
3776
- kind: "pan-left",
3777
- targetPanOffset,
3778
- })) {
3779
- return;
3780
- }
3781
- setViewState((current) => ({
3782
- ...clearActivePreset(current),
3783
- panOffset: clamp(
3784
- current.panOffset + scrollPanCells,
3785
- 0,
3786
- getMaxPanOffset(boundsHistory, current.zoomLevel),
3787
- ),
3788
- }));
3789
- };
3790
-
3791
- const canvasBitmapSize = useMemo(() => {
3792
- if (!canvasCharts) return null;
3793
- const resolutionScale = Math.max(1, pixelRatio);
3794
- return {
3795
- pixelWidth: Math.max(1, Math.round(chartWidth * cellWidthPx * resolutionScale)),
3796
- pixelHeight: Math.max(1, Math.round(chartHeight * cellHeightPx * resolutionScale)),
3797
- };
3798
- }, [canvasCharts, cellHeightPx, cellWidthPx, chartHeight, chartWidth, pixelRatio]);
3799
-
3800
- const canvasProjection = useMemo(() => {
3801
- if (!canvasBitmapSize) return null;
3802
- const canvasWidth = Math.max(chartWidth, canvasBitmapSize.pixelWidth);
3803
- const canvasOhlcOptions = resolveStableOhlcProjectionOptions({
3804
- pointCount: chartWindow.points.length,
3805
- sourceIndexOffset: chartWindow.startIdx,
3806
- bucketWidth: canvasWidth,
3807
- navigationPointCount: navigationOhlcPointCount,
3808
- });
3809
- return projectChartData(
3810
- chartWindow.points,
3811
- canvasWidth,
3812
- viewState.renderMode,
3813
- !!compact,
3814
- canvasOhlcOptions,
3815
- );
3816
- }, [
3817
- canvasBitmapSize,
3818
- chartWidth,
3819
- chartWindow.points,
3820
- chartWindow.startIdx,
3821
- compact,
3822
- navigationOhlcPointCount,
3823
- viewState.renderMode,
3824
- ]);
3825
-
3826
- const canvasIndicators = useMemo(() => (
3827
- sourceIndicatorOverlays && chartWindow.points.length && canvasProjection?.points.length
3828
- ? reindexIndicatorOverlaysForProjection(
3829
- sourceIndicatorOverlays,
3830
- chartWindow.points,
3831
- canvasProjection.points,
3832
- chartWindow.startIdx,
3833
- )
3834
- : null
3835
- ), [canvasProjection?.points, chartWindow.points, chartWindow.startIdx, sourceIndicatorOverlays]);
3836
-
3837
- const canvasIndicatorRenderKey = useMemo(() => buildIndicatorRenderKey(canvasIndicators), [canvasIndicators]);
3838
-
3839
- const canvasBaseScene = useMemo(() => (
3840
- canvasProjection
3841
- ? buildChartScene(canvasProjection.points, {
3842
- width: canvasProjection.points.length,
3843
- height: chartHeight,
3844
- showVolume: showVolume && !compact,
3845
- volumeHeight,
3846
- cursorX: null,
3847
- cursorY: null,
3848
- mode: canvasProjection.effectiveMode,
3849
- axisMode,
3850
- colors: chartColors,
3851
- timeAxisDates,
3852
- indicators: canvasIndicators,
3853
- marketSession,
3854
- })
3855
- : null
3856
- ), [axisMode, canvasIndicators, canvasProjection, chartColors, chartHeight, compact, marketSession, showVolume, timeAxisDates, volumeHeight]);
3857
-
3858
- const canvasBaseBitmapKey = useMemo(() => {
3859
- if (!canvasBitmapSize || !canvasProjection || !hasHistory || isBlockingBody || bodyMessage) return null;
3860
- return buildNativeBitmapKey(
3861
- canvasProjection.points.length,
3862
- canvasProjection.points,
3863
- canvasBitmapSize.pixelWidth,
3864
- canvasBitmapSize.pixelHeight,
3865
- canvasProjection.effectiveMode,
3866
- showVolume && !compact,
3867
- [
3868
- chartColors.lineColor,
3869
- chartColors.fillColor,
3870
- chartColors.gridColor,
3871
- chartColors.volumeUp,
3872
- chartColors.volumeDown,
3873
- chartColors.candleUp,
3874
- chartColors.candleDown,
3875
- chartColors.preMarketBgColor,
3876
- chartColors.postMarketBgColor,
3877
- ].join(","),
3878
- canvasIndicatorRenderKey,
3879
- marketSessionKey,
3880
- );
3881
- }, [
3882
- bodyMessage,
3883
- canvasBitmapSize,
3884
- canvasIndicatorRenderKey,
3885
- canvasProjection,
3886
- chartColors.candleDown,
3887
- chartColors.candleUp,
3888
- chartColors.fillColor,
3889
- chartColors.gridColor,
3890
- chartColors.lineColor,
3891
- chartColors.postMarketBgColor,
3892
- chartColors.preMarketBgColor,
3893
- chartColors.volumeDown,
3894
- chartColors.volumeUp,
3895
- compact,
3896
- hasHistory,
3897
- isBlockingBody,
3898
- marketSessionKey,
3899
- showVolume,
3900
- ]);
3901
-
3902
- const canvasBaseBitmap = useMemo<NativeChartBitmap | null>(() => {
3903
- return canvasBaseBitmapKey && canvasBaseBitmapState?.key === canvasBaseBitmapKey
3904
- ? canvasBaseBitmapState.bitmap
3905
- : null;
3906
- }, [canvasBaseBitmapKey, canvasBaseBitmapState]);
3907
-
3908
- const visibleCanvasBaseBitmap = useMemo<NativeChartBitmap | null>(() => {
3909
- if (canvasBaseBitmap) return canvasBaseBitmap;
3910
- if (!canvasBaseBitmapKey || !canvasBitmapSize || !canvasBaseBitmapState) return null;
3911
- return canvasBaseBitmapState.bitmap.width === canvasBitmapSize.pixelWidth
3912
- && canvasBaseBitmapState.bitmap.height === canvasBitmapSize.pixelHeight
3913
- ? canvasBaseBitmapState.bitmap
3914
- : null;
3915
- }, [canvasBaseBitmap, canvasBaseBitmapKey, canvasBaseBitmapState, canvasBitmapSize]);
3916
-
3917
- useEffect(() => {
3918
- if (!canvasBitmapSize || !canvasBaseBitmapKey || !canvasBaseScene) {
3919
- lastCanvasBaseBitmapRef.current = null;
3920
- setCanvasBaseBitmapState((current) => (current === null ? current : null));
3921
- return;
3922
- }
3923
-
3924
- const cachedBitmap = lastCanvasBaseBitmapRef.current?.key === canvasBaseBitmapKey
3925
- ? lastCanvasBaseBitmapRef.current.bitmap
3926
- : null;
3927
- if (cachedBitmap) {
3928
- setCanvasBaseBitmapState((current) => (
3929
- current?.key === canvasBaseBitmapKey ? current : { key: canvasBaseBitmapKey, bitmap: cachedBitmap }
3930
- ));
3931
- return;
3932
- }
3933
-
3934
- let cancelled = false;
3935
- const frame = requestAnimationFrameSafe(() => {
3936
- const bitmap = renderNativeChartBase(canvasBaseScene, canvasBitmapSize.pixelWidth, canvasBitmapSize.pixelHeight);
3937
- if (cancelled) return;
3938
- lastCanvasBaseBitmapRef.current = { key: canvasBaseBitmapKey, bitmap };
3939
- setCanvasBaseBitmapState((current) => (
3940
- current?.key === canvasBaseBitmapKey ? current : { key: canvasBaseBitmapKey, bitmap }
3941
- ));
3942
- });
3943
-
3944
- return () => {
3945
- cancelled = true;
3946
- cancelAnimationFrameSafe(frame);
3947
- };
3948
- }, [canvasBaseBitmapKey, canvasBaseScene, canvasBitmapSize]);
3949
-
3950
- const canvasCrosshair = useMemo(() => {
3951
- if (!canvasBitmapSize || !visibleCanvasBaseBitmap || !nativeCrosshair) return null;
3952
- const renderablePixelSize = getRenderablePixelSize(plotRef.current, renderer);
3953
- const overlayPixelX = scaleLocalPixelCoordinate(
3954
- nativeCrosshair.pixelX,
3955
- renderablePixelSize?.pixelWidth ?? canvasBitmapSize.pixelWidth,
3956
- canvasBitmapSize.pixelWidth,
3957
- );
3958
- const overlayPixelY = scaleLocalPixelCoordinate(
3959
- nativeCrosshair.pixelY,
3960
- renderablePixelSize?.pixelHeight ?? canvasBitmapSize.pixelHeight,
3961
- canvasBitmapSize.pixelHeight,
3962
- );
3963
- if (overlayPixelX === null || overlayPixelY === null) return null;
3964
- return {
3965
- pixelX: overlayPixelX,
3966
- pixelY: overlayPixelY,
3967
- color: nativeCrosshair.colors.crosshairColor,
3968
- };
3969
- }, [canvasBitmapSize, nativeCrosshair, renderer, visibleCanvasBaseBitmap]);
3970
-
3971
- const plotBitmaps = useMemo(() => {
3972
- if (!visibleCanvasBaseBitmap) return null;
3973
- return [visibleCanvasBaseBitmap];
3974
- }, [visibleCanvasBaseBitmap]);
3975
-
3976
- const plotLines: Array<string | StyledContent> = effectiveRenderer === "kitty"
3977
- ? blankPlotLines
3978
- : result.lines;
3979
- const plotContent = plotBitmaps || (useCanvasChart && canvasBaseBitmapKey)
3980
- ? null
3981
- : plotLines.map((line, index) => (
3982
- <Text key={index} content={line as unknown as string} />
3983
- ));
3984
-
3985
- const plotBox = (
3986
- <ChartSurface
3987
- ref={plotRef}
3988
- width={chartWidth}
3989
- height={chartHeight}
3990
- flexDirection="column"
3991
- bitmaps={plotBitmaps}
3992
- crosshair={canvasCrosshair}
3993
- onMouseMove={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotMove}
3994
- onMouseDown={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotDown}
3995
- onMouseUp={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : () => { dragRef.current = null; }}
3996
- onMouseDrag={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotDrag}
3997
- onMouseDragEnd={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : () => { dragRef.current = null; }}
3998
- onMouseScroll={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotScroll}
3999
- >
4000
- {isBlockingBody
4001
- ? (
4002
- <Box flexGrow={1} alignItems="center" justifyContent="center">
4003
- <Text fg={colors.textDim}>Loading chart...</Text>
4004
- </Box>
4005
- )
4006
- : bodyMessage
4007
- ? (
4008
- <Box flexGrow={1} alignItems="center" justifyContent="center">
4009
- <Text fg={colors.textDim}>{bodyMessage}</Text>
4010
- </Box>
4011
- )
4012
- : plotContent}
4013
- </ChartSurface>
4014
- );
4015
-
4016
- const axisBox = (
4017
- <PriceAxisLabels
4018
- axisLabels={axisLabels}
4019
- axisWidth={axisWidth}
4020
- axisSectionWidth={axisSectionWidth}
4021
- height={chartHeight}
4022
- cursorRow={cursorRow}
4023
- cursorPixelY={hasDisplayCursor ? displayCursor.pixelY : null}
4024
- cursorLabel={cursorAxisLabel}
4025
- cursorColor={chartColors.crosshairColor}
4026
- />
4027
- );
4028
-
4029
- if (compact) {
4030
- return (
4031
- <Box flexDirection="column">
4032
- <Box flexDirection="row" height={chartHeight} gap={axisGap}>
4033
- {plotBox}
4034
- {axisBox}
4035
- </Box>
4036
- {timeAxisBox}
4037
- </Box>
4038
- );
4039
- }
4040
-
4041
- return (
4042
- <Box
4043
- flexDirection="column"
4044
- flexGrow={1}
4045
- onMouseScroll={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotScroll}
4046
- >
4047
- <Box flexDirection="row" height={1}>
4048
- <Box flexDirection="row" gap={1}>
4049
- {TIME_RANGES.map((range, index) => (
4050
- <Text
4051
- key={range}
4052
- fg={activePreset === range ? chartColors.activeRangeColor : (isRangePresetSupported(range, availableManualResolutions) ? chartColors.inactiveRangeColor : colors.textMuted)}
4053
- attributes={activePreset === range ? TextAttributes.BOLD : 0}
4054
- onMouseDown={(event: any) => {
4055
- focusPaneForMouseInteraction(event);
4056
- if (isRangePresetSupported(range, availableManualResolutions)) setRange(range);
4057
- }}
4058
- >
4059
- {`${index + 1}:${range}`}
4060
- </Text>
4061
- ))}
4062
- </Box>
4063
- </Box>
4064
-
4065
- <Box flexDirection="row" height={1}>
4066
- <Box flexDirection="row" gap={1}>
4067
- {resolutionChips.map((resolution) => (
4068
- <Text
4069
- key={resolution}
4070
- fg={selectedResolution === resolution ? chartColors.activeRangeColor : chartColors.inactiveRangeColor}
4071
- attributes={selectedResolution === resolution ? TextAttributes.BOLD : 0}
4072
- onMouseDown={(event: any) => {
4073
- focusPaneForMouseInteraction(event);
4074
- setResolution(resolution);
4075
- }}
4076
- >
4077
- {getChartResolutionLabel(resolution)}
4078
- </Text>
4079
- ))}
4080
- {isUpdating && (
4081
- <Text fg={colors.textDim}>updating</Text>
4082
- )}
4083
- {fallbackResolutionLabel && (
4084
- <Text fg={colors.textDim}>{fallbackResolutionLabel}</Text>
4085
- )}
4086
- </Box>
4087
- <Box flexGrow={1} />
4088
- {chartWidth >= 72 ? (
4089
- <Box flexDirection="row" gap={1}>
4090
- {CHART_RENDER_MODES.map((mode) => (
4091
- <Text
4092
- key={mode}
4093
- fg={requestedMode === mode ? chartColors.activeRangeColor : chartColors.inactiveRangeColor}
4094
- attributes={requestedMode === mode ? TextAttributes.BOLD : 0}
4095
- onMouseDown={(event: any) => {
4096
- focusPaneForMouseInteraction(event);
4097
- setRenderMode(mode);
4098
- }}
4099
- >
4100
- {MODE_CHIPS[mode]}
4101
- </Text>
4102
- ))}
4103
- {projection.fallbackMode && (
4104
- <Text fg={colors.textDim}>auto:{MODE_LABELS[projection.fallbackMode]}</Text>
4105
- )}
4106
- {showNativeUnavailable && (
4107
- <Text fg={colors.textDim}>native unavailable</Text>
4108
- )}
4109
- </Box>
4110
- ) : (
4111
- <Box flexDirection="row" gap={1}>
4112
- {projection.fallbackMode && (
4113
- <Text fg={colors.textDim}>auto:{MODE_LABELS[projection.fallbackMode]}</Text>
4114
- )}
4115
- {showNativeUnavailable && (
4116
- <Text fg={colors.textDim}>native unavailable</Text>
4117
- )}
4118
- </Box>
4119
- )}
4120
- </Box>
4121
-
4122
- <Box
4123
- flexDirection="row"
4124
- height={chartHeight}
4125
- gap={axisGap}
4126
- onMouseScroll={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotScroll}
4127
- >
4128
- {plotBox}
4129
- {axisBox}
4130
- </Box>
4131
-
4132
- {timeAxisBox}
4133
-
4134
- {compact && (
4135
- <>
4136
- <Box height={1} flexDirection="row" gap={1}>
4137
- <ShortcutHint hotkey="m" label="ode" />
4138
- {footerControls}
4139
- <ShortcutHint hotkey="r" label="es" />
4140
- <ShortcutHint hotkey="+/-" label="zoom" />
4141
- <ShortcutHint hotkey="0" label="reset" />
4142
- {width >= 72 && <ShortcutHint hotkey="1-7" label="range" />}
4143
- </Box>
4144
- </>
4145
- )}
4146
- </Box>
4147
- );
4148
- });