gloomberb 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (489) hide show
  1. package/README.md +6 -4
  2. package/README.zh-CN.md +4 -2
  3. package/package.json +28 -1
  4. package/src/api-client/auth.ts +10 -0
  5. package/src/api-client/data.ts +103 -1
  6. package/src/api-client/index.test.ts +127 -0
  7. package/src/api-client/index.ts +129 -1
  8. package/src/api-client/normalizers.ts +39 -0
  9. package/src/api-client/paths.ts +85 -1
  10. package/src/api-client/types.ts +196 -0
  11. package/src/app/pane-runtime/index.tsx +15 -12
  12. package/src/app/pane-runtime/layout-placement.test.ts +27 -0
  13. package/src/app/pane-runtime/layout-placement.ts +13 -0
  14. package/src/app/pane-runtime/plugin-bindings.ts +62 -0
  15. package/src/app.tsx +60 -18
  16. package/src/architecture/browser-entry-graph.test.ts +96 -0
  17. package/src/brokers/profile-form.test.ts +25 -27
  18. package/src/brokers/test-broker.ts +68 -0
  19. package/src/cli/commands/plugins.ts +39 -11
  20. package/src/cli/desktop-pane-shot.ts +285 -16
  21. package/src/cli/index.ts +1 -1
  22. package/src/cli/pane-functions/capabilities.ts +58 -28
  23. package/src/cli/pane-functions/catalog.ts +16 -4
  24. package/src/cli/pane-functions/discovery.ts +1 -0
  25. package/src/cli/pane-functions/headless.test.ts +247 -0
  26. package/src/cli/pane-functions/headless.ts +423 -0
  27. package/src/cli/pane-functions/index.test.ts +9 -0
  28. package/src/cli/pane-functions/index.ts +6 -2
  29. package/src/cli/pane-functions/options.ts +27 -2
  30. package/src/cli/pane-functions/report.ts +4 -0
  31. package/src/cli/pane-functions/resolver.ts +26 -8
  32. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  33. package/src/cli/pane-functions/screenshot.ts +236 -20
  34. package/src/cli/restore-plugins.ts +34 -0
  35. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  36. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  37. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  38. package/src/components/chart/composite/composite-chart.test.tsx +68 -20
  39. package/src/components/chart/composite/composite-chart.tsx +301 -204
  40. package/src/components/chart/composite/format.ts +8 -2
  41. package/src/components/chart/composite/interactions.test.ts +68 -102
  42. package/src/components/chart/composite/interactions.ts +214 -264
  43. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  44. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  45. package/src/components/chart/composite/scene.ts +57 -3
  46. package/src/components/chart/composite/text-renderer.ts +3 -2
  47. package/src/components/chart/composite/time-scale.ts +138 -57
  48. package/src/components/chart/composite/types.ts +25 -1
  49. package/src/components/chart/core/data.ts +9 -13
  50. package/src/components/chart/core/date-window.ts +7 -1
  51. package/src/components/chart/core/pointer.ts +3 -0
  52. package/src/components/chart/core/types.ts +0 -45
  53. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  54. package/src/components/chart/native/kitty/index.test.ts +0 -188
  55. package/src/components/chart/native/raster/crosshair.ts +6 -5
  56. package/src/components/chart/native/raster/primitives.ts +0 -23
  57. package/src/components/chart/native/renderer-selection.ts +81 -31
  58. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  59. package/src/components/chart/static/chart-surface.tsx +150 -0
  60. package/src/components/chart/static/index.ts +8 -0
  61. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  62. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  63. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  64. package/src/components/chart/static/series.ts +49 -0
  65. package/src/components/command-bar/action-runtime.ts +1 -3
  66. package/src/components/command-bar/assist/model.test.ts +17 -2
  67. package/src/components/command-bar/assist/model.ts +40 -9
  68. package/src/components/command-bar/commands/direct/index.ts +1 -1
  69. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  70. package/src/components/command-bar/commands/registry.ts +0 -11
  71. package/src/components/command-bar/helpers.test.ts +2 -2
  72. package/src/components/command-bar/helpers.ts +2 -6
  73. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  74. package/src/components/command-bar/list/badge.test.ts +54 -0
  75. package/src/components/command-bar/list/badge.ts +62 -0
  76. package/src/components/command-bar/list/model.test.ts +103 -0
  77. package/src/components/command-bar/list/model.ts +61 -4
  78. package/src/components/command-bar/list/view.test.tsx +113 -0
  79. package/src/components/command-bar/list/view.tsx +147 -131
  80. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  81. package/src/components/command-bar/panel/body.tsx +51 -65
  82. package/src/components/command-bar/panel/index.tsx +48 -13
  83. package/src/components/command-bar/panel/layout.test.ts +139 -0
  84. package/src/components/command-bar/panel/layout.ts +65 -28
  85. package/src/components/command-bar/panel/native-surface.ts +24 -0
  86. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  87. package/src/components/command-bar/panel/runtime.ts +6 -3
  88. package/src/components/command-bar/panel/state.ts +41 -7
  89. package/src/components/command-bar/panel/types.ts +7 -1
  90. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  91. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  92. package/src/components/command-bar/routes/root/results.ts +30 -28
  93. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  94. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  95. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  96. package/src/components/command-bar/routes/root/selection.ts +1 -12
  97. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  98. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  99. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  100. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  101. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  102. package/src/components/command-bar/routing/actions.ts +1 -81
  103. package/src/components/command-bar/routing/launch-request.ts +1 -1
  104. package/src/components/command-bar/routing/list-state.ts +6 -21
  105. package/src/components/command-bar/selection-runtime.ts +1 -1
  106. package/src/components/command-bar/surface/assist.test.tsx +14 -10
  107. package/src/components/command-bar/surface/environment.ts +8 -1
  108. package/src/components/command-bar/surface/index.test.tsx +84 -68
  109. package/src/components/command-bar/surface/index.tsx +33 -36
  110. package/src/components/command-bar/surface/test-harness.tsx +15 -3
  111. package/src/components/command-bar/theme-picker.tsx +43 -15
  112. package/src/components/command-bar/view-model.test.ts +53 -8
  113. package/src/components/command-bar/view-model.ts +43 -14
  114. package/src/components/command-bar/workflow/tickers.ts +1 -0
  115. package/src/components/command-bar/workflow/types.ts +1 -1
  116. package/src/components/data-table/view.test.tsx +45 -6
  117. package/src/components/data-table/view.tsx +55 -9
  118. package/src/components/index.ts +10 -2
  119. package/src/components/input-search-bar.test.tsx +36 -2
  120. package/src/components/input-search-bar.tsx +18 -1
  121. package/src/components/layout/header.test.tsx +57 -10
  122. package/src/components/layout/header.tsx +292 -149
  123. package/src/components/layout/market-summary.test.ts +81 -0
  124. package/src/components/layout/market-summary.ts +109 -0
  125. package/src/components/layout/shell/chrome.ts +96 -1
  126. package/src/components/layout/shell/index.tsx +11 -3
  127. package/src/components/layout/shell/menu.ts +9 -0
  128. package/src/components/layout/shell/pane/actions.ts +29 -0
  129. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  130. package/src/components/layout/shell/shortcuts.ts +3 -0
  131. package/src/components/layout/status-bar.test.tsx +9 -8
  132. package/src/components/layout/status-bar.tsx +85 -54
  133. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  134. package/src/components/price-sparkline/index.ts +61 -34
  135. package/src/components/price-sparkline/view.tsx +1 -1
  136. package/src/components/sign-in-gate.test.tsx +128 -0
  137. package/src/components/sign-in-gate.tsx +149 -0
  138. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  139. package/src/components/ticker/badge/list.tsx +17 -3
  140. package/src/components/ticker/list-table-view.tsx +6 -3
  141. package/src/components/ui/data-table/index.tsx +3 -2
  142. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  143. package/src/components/ui/frame.tsx +28 -0
  144. package/src/components/ui/index.ts +3 -0
  145. package/src/components/ui/native-select.tsx +20 -10
  146. package/src/components/ui/select-button.tsx +138 -0
  147. package/src/components/ui/status.tsx +2 -2
  148. package/src/components/ui/table-layout.ts +1 -1
  149. package/src/components/use-double-click-activation.ts +7 -3
  150. package/src/core/app-services.ts +3 -3
  151. package/src/data/config/layout.ts +12 -6
  152. package/src/data/config/store/index.test.ts +97 -0
  153. package/src/data/config/store/migrations.ts +41 -0
  154. package/src/data/config/store/normalize.ts +5 -3
  155. package/src/i18n/es.ts +6 -2
  156. package/src/i18n/ja.ts +6 -2
  157. package/src/i18n/ko.ts +6 -2
  158. package/src/i18n/zh-cn.ts +6 -2
  159. package/src/i18n/zh-tw.ts +6 -2
  160. package/src/layout-marketplace/pane-imagery.ts +1 -0
  161. package/src/layout-marketplace/payload.test.ts +17 -0
  162. package/src/layout-marketplace/payload.ts +40 -5
  163. package/src/market-data/coordinator/index.ts +3 -2
  164. package/src/market-data/coordinator/quotes.ts +173 -66
  165. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  166. package/src/pane-settings.ts +1 -1
  167. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  168. package/src/plugins/builtin/analytics/view.tsx +1 -1
  169. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  170. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  171. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  172. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  173. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  174. package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
  175. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  176. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  177. package/src/plugins/builtin/composite-plugins.ts +14 -1
  178. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  179. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  180. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  181. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  182. package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
  183. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  184. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  185. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  186. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  187. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  188. package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
  189. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  190. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  191. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
  192. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  193. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  194. package/src/plugins/builtin/econ/index.tsx +2 -1
  195. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  196. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  197. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  198. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  199. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  200. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  201. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  202. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  203. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  204. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  205. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  206. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  207. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  208. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  209. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  210. package/src/plugins/builtin/help/index.tsx +4 -0
  211. package/src/plugins/builtin/holders/index.tsx +1 -0
  212. package/src/plugins/builtin/insider/index.tsx +1 -0
  213. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  214. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  215. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  216. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  217. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  218. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  219. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  220. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  221. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  222. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  223. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  224. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  225. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  226. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  227. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  228. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  229. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  230. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  231. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  232. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  233. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  234. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  235. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  236. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  237. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  238. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  239. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  240. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  241. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  242. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  243. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  244. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  245. package/src/plugins/builtin/options/index.tsx +1 -0
  246. package/src/plugins/builtin/options/view.test.tsx +29 -1
  247. package/src/plugins/builtin/options/view.tsx +1 -22
  248. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  249. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  250. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  251. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  252. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  253. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  254. package/src/plugins/builtin/plugin-module.ts +9 -1
  255. package/src/plugins/builtin/research/index.tsx +4 -0
  256. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  257. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  258. package/src/plugins/builtin/research-search/data.ts +109 -0
  259. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  260. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  261. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  262. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  263. package/src/plugins/builtin/research-search/index.tsx +85 -0
  264. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  265. package/src/plugins/builtin/research-search/model.ts +291 -0
  266. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  267. package/src/plugins/builtin/research-search/pane.tsx +678 -0
  268. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  269. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  270. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  271. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  272. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  273. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  274. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  275. package/src/plugins/builtin/sec/index.tsx +1 -0
  276. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  277. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  278. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  279. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  280. package/src/plugins/builtin/ticker-detail/index.tsx +0 -14
  281. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  282. package/src/plugins/builtin/volatility/index.tsx +1 -1
  283. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  284. package/src/plugins/builtin/world-venue-map/map.tsx +213 -69
  285. package/src/plugins/builtin/world-venue-map/model.test.ts +32 -0
  286. package/src/plugins/builtin/world-venue-map/model.ts +134 -13
  287. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  288. package/src/plugins/bundle.test.ts +109 -0
  289. package/src/plugins/bundle.ts +139 -0
  290. package/src/plugins/catalog-browser.test.ts +1 -0
  291. package/src/plugins/catalog-browser.ts +10 -0
  292. package/src/plugins/catalog-manifest.test.ts +43 -0
  293. package/src/plugins/catalog-manifest.ts +92 -0
  294. package/src/plugins/catalog-ui.ts +20 -4
  295. package/src/plugins/catalog.ts +9 -2
  296. package/src/plugins/current-target.ts +28 -0
  297. package/src/plugins/host-contract.ts +37 -0
  298. package/src/plugins/host-link.ts +158 -0
  299. package/src/plugins/host-modules.ts +61 -0
  300. package/src/plugins/loader.ts +41 -22
  301. package/src/plugins/ownership.ts +1 -0
  302. package/src/plugins/prediction-markets/index.tsx +1 -0
  303. package/src/plugins/registry/context.ts +3 -0
  304. package/src/plugins/registry/contributions.ts +27 -0
  305. package/src/plugins/registry/index.test.ts +25 -0
  306. package/src/plugins/registry/index.ts +8 -0
  307. package/src/plugins/registry/pane-settings.ts +6 -13
  308. package/src/plugins/seed.test.ts +83 -0
  309. package/src/plugins/seed.ts +80 -0
  310. package/src/public/broker.ts +31 -0
  311. package/src/public/market-data.ts +9 -0
  312. package/src/public/pane-hooks.ts +57 -0
  313. package/src/public/public-api.test.ts +285 -0
  314. package/src/public/react.ts +62 -0
  315. package/src/public/test-support.ts +23 -0
  316. package/src/public/time-series.ts +8 -0
  317. package/src/public/utils.ts +77 -0
  318. package/src/remote/layout-helpers.ts +6 -8
  319. package/src/remote/resources.ts +2 -0
  320. package/src/remote/schema.ts +1 -0
  321. package/src/remote/semantic-tree.test.tsx +105 -0
  322. package/src/remote/semantic-tree.tsx +53 -35
  323. package/src/renderers/browser/main.tsx +6 -0
  324. package/src/renderers/cloudflare/worker.test.ts +19 -0
  325. package/src/renderers/cloudflare/worker.ts +1 -1
  326. package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
  327. package/src/renderers/electrobun/bun/index.ts +5 -2
  328. package/src/renderers/electrobun/shared/protocol.ts +21 -0
  329. package/src/renderers/electrobun/view/build-assets.ts +0 -1
  330. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
  331. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  332. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  333. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  334. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  335. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  336. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  337. package/src/renderers/electrobun/view/main.tsx +27 -2
  338. package/src/renderers/electrobun/view/styles.css +5 -0
  339. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  340. package/src/renderers/opentui/chart-surface.tsx +5 -1
  341. package/src/renderers/opentui/host.tsx +16 -2
  342. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  343. package/src/renderers/opentui/interaction-performance.ts +188 -0
  344. package/src/renderers/opentui/start.tsx +26 -1
  345. package/src/renderers/opentui/test-utils.tsx +13 -0
  346. package/src/renderers/opentui/toast-host.tsx +26 -2
  347. package/src/renderers/opentui/ui-host.tsx +1 -0
  348. package/src/sources/provider-router/search.test.ts +191 -0
  349. package/src/sources/provider-router/search.ts +92 -39
  350. package/src/state/app/context/index.tsx +2 -0
  351. package/src/state/app/context/selector.test.tsx +33 -1
  352. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  353. package/src/state/hooks/quote-streaming.ts +106 -30
  354. package/src/state/pane-table-export-registry.ts +19 -0
  355. package/src/theme/color-utils.ts +1 -1
  356. package/src/theme/colors.ts +50 -0
  357. package/src/theme/themes.test.ts +15 -2
  358. package/src/theme/themes.ts +19 -1
  359. package/src/tickers/search/index.test.ts +26 -0
  360. package/src/tickers/search/index.ts +42 -17
  361. package/src/tickers/search/ranking.ts +14 -0
  362. package/src/time-series/chart-data.test.ts +216 -0
  363. package/src/time-series/resolution.test.ts +25 -26
  364. package/src/time-series/resolution.ts +41 -10
  365. package/src/time-series/resolve.ts +7 -0
  366. package/src/time-series/types.ts +3 -1
  367. package/src/time-series/use-chart-resolution.ts +5 -0
  368. package/src/types/config.ts +123 -3
  369. package/src/types/data-provider.ts +11 -0
  370. package/src/types/headless.ts +161 -0
  371. package/src/types/plugin.ts +102 -0
  372. package/src/ui/host.tsx +12 -1
  373. package/src/ui/index.tsx +1 -0
  374. package/src/ui/toast.tsx +8 -4
  375. package/src/utils/format.ts +35 -0
  376. package/src/utils/fuzzy-search.test.ts +94 -0
  377. package/src/utils/fuzzy-search.ts +81 -22
  378. package/src/utils/shortcut-labels.ts +14 -0
  379. package/src/components/chart/core/axis-format.ts +0 -157
  380. package/src/components/chart/core/index.test.ts +0 -1075
  381. package/src/components/chart/core/projection.ts +0 -357
  382. package/src/components/chart/core/render-utils.ts +0 -4
  383. package/src/components/chart/core/renderer.ts +0 -244
  384. package/src/components/chart/core/scene.ts +0 -180
  385. package/src/components/chart/core/time-axis.ts +0 -330
  386. package/src/components/chart/cursor-motion.ts +0 -47
  387. package/src/components/chart/indicators/types.ts +0 -26
  388. package/src/components/chart/market-session.ts +0 -176
  389. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  390. package/src/components/chart/multi-line/renderer.ts +0 -161
  391. package/src/components/chart/multi-line/scene.ts +0 -188
  392. package/src/components/chart/native/raster/layout.ts +0 -129
  393. package/src/components/chart/native/raster/price-chart.ts +0 -244
  394. package/src/components/chart/rendering/chart-draw.ts +0 -270
  395. package/src/components/chart/rendering/geometry.ts +0 -95
  396. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  397. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  398. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  399. package/src/components/chart/static/chart/surface.tsx +0 -358
  400. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  401. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  402. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  403. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  404. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  405. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  406. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  407. package/src/plugins/builtin/substack/api/store.ts +0 -186
  408. package/src/plugins/builtin/substack/api/types.ts +0 -42
  409. package/src/plugins/builtin/substack/api.test.ts +0 -298
  410. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  411. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  412. package/src/plugins/builtin/substack/content.ts +0 -478
  413. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  414. package/src/plugins/builtin/substack/index.tsx +0 -49
  415. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  416. package/src/plugins/builtin/substack/model.test.ts +0 -155
  417. package/src/plugins/builtin/substack/normalize.ts +0 -343
  418. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  419. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  420. package/src/plugins/builtin/substack/pane.tsx +0 -547
  421. package/src/plugins/builtin/substack/read-state.ts +0 -30
  422. package/src/plugins/builtin/substack/table.ts +0 -111
  423. package/src/plugins/builtin/substack/types.ts +0 -71
  424. package/src/plugins/builtin/substack/utils.ts +0 -131
  425. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  426. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  427. package/src/plugins/ibkr/account-cache.ts +0 -24
  428. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  429. package/src/plugins/ibkr/config.test.ts +0 -79
  430. package/src/plugins/ibkr/config.ts +0 -250
  431. package/src/plugins/ibkr/dialogs.tsx +0 -40
  432. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  433. package/src/plugins/ibkr/flex/index.ts +0 -367
  434. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  435. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  436. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  437. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  438. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  439. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  440. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  441. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  442. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  443. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  444. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  445. package/src/plugins/ibkr/gateway/history.ts +0 -120
  446. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  447. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  448. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  449. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  450. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  451. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  452. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  453. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  454. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  455. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  456. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  457. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  458. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  459. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  460. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  461. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  462. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  463. package/src/plugins/ibkr/gateway/types.ts +0 -27
  464. package/src/plugins/ibkr/index.test.tsx +0 -162
  465. package/src/plugins/ibkr/index.tsx +0 -159
  466. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  467. package/src/plugins/ibkr/instance-selection.ts +0 -36
  468. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  469. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  470. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  471. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  472. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  473. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  474. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  475. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  476. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  477. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  478. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  479. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  480. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  481. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  482. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  483. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  484. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  485. package/src/plugins/ibkr/trade/utils.ts +0 -144
  486. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  487. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  488. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  489. package/src/plugins/ibkr/trading/state.ts +0 -335
@@ -1,1075 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import {
3
- appendLiveQuotePoint,
4
- bucketOhlcSeries,
5
- projectChartData,
6
- resolveRenderMode,
7
- resolveStableOhlcProjectionOptions,
8
- } from "./data";
9
- import { stepCursorTowards } from "../cursor-motion";
10
- import { buildChartScene, buildCursorTimeAxisSegments, buildTimeAxis, formatAxisValue, formatCursorAxisValue, renderChart, resolveChartAxisWidth, resolveChartPalette } from "./renderer";
11
- import { buildCursorPriceAxisOverlay } from "../price-axis-labels";
12
- import { resolveChartMarketSession, resolveExtendedHoursBackgroundSpans } from "../market-session";
13
- import type { PricePoint, Quote } from "../../../types/financials";
14
- import type { ChartRenderMode } from "./types";
15
-
16
- const aggregationFixture: PricePoint[] = [
17
- { date: new Date("2024-01-02"), close: 10, volume: 100 },
18
- { date: new Date("2024-01-03"), open: 11, high: 14, low: 9, close: 13, volume: 150 },
19
- { date: new Date("2024-01-04"), open: 13, high: 15, low: 12, close: 14, volume: 200 },
20
- { date: new Date("2024-01-05"), open: 14, high: 16, low: 10, close: 11, volume: 250 },
21
- ];
22
-
23
- const chartFixture: PricePoint[] = [
24
- { date: new Date("2024-01-02"), open: 10, high: 12, low: 9, close: 11, volume: 100 },
25
- { date: new Date("2024-01-03"), open: 11, high: 13, low: 10, close: 12, volume: 120 },
26
- { date: new Date("2024-01-04"), open: 12, high: 12.5, low: 8, close: 9, volume: 140 },
27
- { date: new Date("2024-01-05"), open: 9, high: 11, low: 8.5, close: 10.5, volume: 160 },
28
- ];
29
-
30
- const palette = resolveChartPalette({
31
- bg: "#000000",
32
- border: "#333333",
33
- borderFocused: "#ffff00",
34
- text: "#ffffff",
35
- textDim: "#777777",
36
- positive: "#00ff00",
37
- negative: "#ff0000",
38
- }, "positive");
39
-
40
- function quoteFixture(overrides: Partial<Quote> = {}): Quote {
41
- return {
42
- symbol: "INTC",
43
- price: 129,
44
- currency: "USD",
45
- change: 0,
46
- changePercent: 0,
47
- lastUpdated: Date.parse("2026-05-15T20:30:00Z"),
48
- listingExchangeName: "NASDAQ",
49
- marketState: "REGULAR",
50
- ...overrides,
51
- };
52
- }
53
-
54
- function textLines(result: ReturnType<typeof renderChart>): string[] {
55
- return result.lines.map((line) => line.chunks.map((chunk) => chunk.text).join(""));
56
- }
57
-
58
- describe("bucketOhlcSeries", () => {
59
- test("preserves bucket open/close/high/low/volume with missing OHLC fields", () => {
60
- const buckets = bucketOhlcSeries(aggregationFixture, 2);
61
-
62
- expect(buckets).toHaveLength(2);
63
- expect(buckets[0]).toEqual({
64
- date: new Date("2024-01-03"),
65
- open: 10,
66
- high: 14,
67
- low: 9,
68
- close: 13,
69
- volume: 250,
70
- });
71
- expect(buckets[1]).toEqual({
72
- date: new Date("2024-01-05"),
73
- open: 13,
74
- high: 16,
75
- low: 10,
76
- close: 11,
77
- volume: 450,
78
- });
79
- });
80
-
81
- test("keeps interior OHLC buckets aligned to source bars while panning", () => {
82
- const source = Array.from({ length: 13 }, (_, index) => ({
83
- date: new Date(Date.UTC(2024, 0, index + 1)),
84
- open: index,
85
- high: index + 10,
86
- low: index - 10,
87
- close: index + 0.5,
88
- volume: index + 1,
89
- })) as PricePoint[];
90
-
91
- const firstView = bucketOhlcSeries(source.slice(0, 12), 3, { sourceIndexOffset: 0 });
92
- const pannedView = bucketOhlcSeries(source.slice(1, 13), 3, { sourceIndexOffset: 1 });
93
- const firstSharedBucket = firstView[1];
94
- const secondSharedBucket = firstView[2];
95
- if (!firstSharedBucket || !secondSharedBucket) {
96
- throw new Error("Expected shared OHLC buckets");
97
- }
98
-
99
- expect(firstSharedBucket).toEqual({
100
- date: source[4]!.date,
101
- open: 1,
102
- high: 14,
103
- low: -9,
104
- close: 4.5,
105
- volume: 2 + 3 + 4 + 5,
106
- });
107
- expect(pannedView[0]).toEqual(firstSharedBucket);
108
- expect(pannedView[1]).toEqual(secondSharedBucket);
109
- });
110
-
111
- test("keeps source-aligned bucket count stable across small pans", () => {
112
- const source: PricePoint[] = [];
113
- let cursor = new Date(Date.UTC(2024, 0, 1));
114
- let sourceIndex = 0;
115
- while (source.length < 220) {
116
- const day = cursor.getUTCDay();
117
- if (day !== 0 && day !== 6) {
118
- source.push({
119
- date: new Date(cursor),
120
- open: sourceIndex,
121
- high: sourceIndex + 1,
122
- low: sourceIndex - 1,
123
- close: sourceIndex + 0.5,
124
- volume: sourceIndex + 1,
125
- });
126
- sourceIndex += 1;
127
- }
128
- cursor = new Date(cursor.getTime() + 24 * 3600_000);
129
- }
130
-
131
- const visibleCount = 160;
132
- const targetWidth = 40;
133
- const projectionOptions = resolveStableOhlcProjectionOptions({
134
- pointCount: visibleCount,
135
- sourceIndexOffset: 0,
136
- bucketWidth: targetWidth * 2,
137
- navigationPointCount: visibleCount,
138
- });
139
- const targetBucketCount = projectionOptions.ohlcTargetBucketCount!;
140
- const projectWindow = (start: number) => bucketOhlcSeries(
141
- source.slice(start, start + visibleCount),
142
- targetWidth,
143
- {
144
- ...projectionOptions,
145
- sourceIndexOffset: start,
146
- },
147
- );
148
-
149
- const first = projectWindow(0);
150
- const pannedOne = projectWindow(1);
151
- const pannedTwo = projectWindow(2);
152
-
153
- expect(first).toHaveLength(targetBucketCount);
154
- expect(pannedOne).toHaveLength(targetBucketCount);
155
- expect(pannedTwo).toHaveLength(targetBucketCount);
156
- expect(pannedOne[1]!).toEqual(first[1]!);
157
- expect(pannedTwo[1]!).toEqual(first[1]!);
158
- });
159
-
160
- test("plans stable OHLC buckets from navigation count when histories are comparable", () => {
161
- expect(resolveStableOhlcProjectionOptions({
162
- pointCount: 158,
163
- sourceIndexOffset: 12,
164
- bucketWidth: 80,
165
- navigationPointCount: 160,
166
- })).toMatchObject({
167
- ohlcBucketWidth: 80,
168
- ohlcSourceBucketSize: 4,
169
- ohlcTargetBucketCount: 40,
170
- sourceIndexOffset: 12,
171
- });
172
- });
173
-
174
- test("plans OHLC buckets from rendered points when navigation count is too different", () => {
175
- expect(resolveStableOhlcProjectionOptions({
176
- pointCount: 84,
177
- sourceIndexOffset: 0,
178
- bucketWidth: 80,
179
- navigationPointCount: 160,
180
- })).toMatchObject({
181
- ohlcSourceBucketSize: 3,
182
- ohlcTargetBucketCount: 28,
183
- });
184
- });
185
- });
186
-
187
- describe("appendLiveQuotePoint", () => {
188
- test("extends coarse chart histories with a fresh quote tail", () => {
189
- const history: PricePoint[] = [
190
- { date: new Date("2026-05-04T00:00:00Z"), close: 56 },
191
- { date: new Date("2026-05-11T00:00:00Z"), close: 68 },
192
- ];
193
-
194
- const extended = appendLiveQuotePoint(
195
- history,
196
- quoteFixture(),
197
- { now: Date.parse("2026-05-15T21:00:00Z") },
198
- );
199
-
200
- expect(extended).toHaveLength(3);
201
- expect(extended.at(-1)).toEqual({
202
- date: new Date("2026-05-15T20:30:00Z"),
203
- close: 129,
204
- });
205
- });
206
-
207
- test("merges a quote into the active OHLC bucket", () => {
208
- const history: PricePoint[] = [
209
- {
210
- date: new Date("2026-05-15T20:25:00Z"),
211
- open: 124,
212
- high: 130,
213
- low: 122,
214
- close: 126,
215
- volume: 1_000,
216
- },
217
- ];
218
-
219
- const extended = appendLiveQuotePoint(
220
- history,
221
- quoteFixture({ lastUpdated: Date.parse("2026-05-15T20:29:00Z") }),
222
- {
223
- now: Date.parse("2026-05-15T20:30:00Z"),
224
- mode: "ohlc",
225
- resolution: "5m",
226
- },
227
- );
228
-
229
- expect(extended).toHaveLength(1);
230
- expect(extended[0]).toEqual({
231
- date: new Date("2026-05-15T20:25:00Z"),
232
- open: 124,
233
- high: 130,
234
- low: 122,
235
- close: 129,
236
- volume: 1_000,
237
- });
238
- });
239
-
240
- test("seeds a new OHLC bucket from the live price instead of prior-bar extremes", () => {
241
- const history: PricePoint[] = [
242
- {
243
- date: new Date("2026-05-15T20:25:00Z"),
244
- open: 124,
245
- high: 180,
246
- low: 80,
247
- close: 126,
248
- volume: 1_000,
249
- },
250
- ];
251
-
252
- const extended = appendLiveQuotePoint(
253
- history,
254
- quoteFixture({ lastUpdated: Date.parse("2026-05-15T20:30:00Z") }),
255
- {
256
- now: Date.parse("2026-05-15T20:31:00Z"),
257
- mode: "ohlc",
258
- resolution: "5m",
259
- },
260
- );
261
-
262
- expect(extended).toHaveLength(2);
263
- expect(extended[1]).toEqual({
264
- date: new Date("2026-05-15T20:30:00Z"),
265
- open: 129,
266
- high: 129,
267
- low: 129,
268
- close: 129,
269
- });
270
- });
271
-
272
- test("treats calendar-month bars as variable-length buckets", () => {
273
- const history: PricePoint[] = [{
274
- date: new Date("2026-01-01T00:00:00Z"),
275
- open: 120,
276
- high: 140,
277
- low: 110,
278
- close: 125,
279
- }];
280
-
281
- const endOfJanuary = appendLiveQuotePoint(
282
- history,
283
- quoteFixture({ lastUpdated: Date.parse("2026-01-31T20:30:00Z") }),
284
- {
285
- now: Date.parse("2026-01-31T20:31:00Z"),
286
- mode: "ohlc",
287
- resolution: "1mo",
288
- },
289
- );
290
- const startOfFebruary = appendLiveQuotePoint(
291
- history,
292
- quoteFixture({ lastUpdated: Date.parse("2026-02-01T20:30:00Z") }),
293
- {
294
- now: Date.parse("2026-02-01T20:31:00Z"),
295
- mode: "ohlc",
296
- resolution: "1mo",
297
- },
298
- );
299
-
300
- expect(endOfJanuary).toHaveLength(1);
301
- expect(endOfJanuary[0]?.close).toBe(129);
302
- expect(startOfFebruary).toHaveLength(2);
303
- expect(startOfFebruary[1]).toMatchObject({
304
- open: 129,
305
- high: 129,
306
- low: 129,
307
- close: 129,
308
- });
309
- });
310
-
311
- test("uses the active extended-hours price for the live tail", () => {
312
- const history: PricePoint[] = [
313
- { date: new Date("2026-05-15T19:30:00Z"), close: 128 },
314
- ];
315
-
316
- const extended = appendLiveQuotePoint(
317
- history,
318
- quoteFixture({
319
- marketState: "POST",
320
- postMarketPrice: 131,
321
- lastUpdated: Date.parse("2026-05-15T21:10:00Z"),
322
- }),
323
- { now: Date.parse("2026-05-15T21:15:00Z") },
324
- );
325
-
326
- expect(extended.at(-1)?.close).toBe(131);
327
- });
328
-
329
- test("does not bridge a missing intraday history window with one synthetic candle", () => {
330
- const history: PricePoint[] = [
331
- { date: new Date("2026-07-22T14:49:00Z"), close: 347.73 },
332
- { date: new Date("2026-07-22T14:50:00Z"), close: 347.76 },
333
- { date: new Date("2026-07-22T14:51:00Z"), close: 347.68 },
334
- ];
335
-
336
- const extended = appendLiveQuotePoint(
337
- history,
338
- quoteFixture({
339
- symbol: "GOOG",
340
- price: 346.27,
341
- lastUpdated: Date.parse("2026-07-22T18:52:00Z"),
342
- }),
343
- Date.parse("2026-07-22T18:53:00Z"),
344
- );
345
-
346
- expect(extended).toBe(history);
347
- });
348
-
349
- test("does not append a live quote tail with a likely unit mismatch", () => {
350
- const history: PricePoint[] = [
351
- { date: new Date("2026-05-18T00:00:00Z"), close: 405 },
352
- { date: new Date("2026-05-22T00:00:00Z"), close: 379 },
353
- ];
354
-
355
- const extended = appendLiveQuotePoint(
356
- history,
357
- quoteFixture({
358
- symbol: "FTC",
359
- currency: "GBP",
360
- price: 3.79,
361
- lastUpdated: Date.parse("2026-05-22T16:39:00Z"),
362
- }),
363
- { now: Date.parse("2026-05-22T16:45:00Z") },
364
- );
365
-
366
- expect(extended).toBe(history);
367
- });
368
-
369
- test("does not append stale quotes from an older active session", () => {
370
- const history: PricePoint[] = [
371
- { date: new Date("2026-05-11T00:00:00Z"), close: 68 },
372
- ];
373
-
374
- const extended = appendLiveQuotePoint(
375
- history,
376
- quoteFixture({
377
- lastUpdated: Date.parse("2026-05-08T20:00:00Z"),
378
- }),
379
- { now: Date.parse("2026-05-15T15:00:00Z") },
380
- );
381
-
382
- expect(extended).toBe(history);
383
- });
384
- });
385
-
386
- describe("resolveRenderMode", () => {
387
- test("applies candle fallback thresholds", () => {
388
- expect(resolveRenderMode("candles", 27)).toMatchObject({ effectiveMode: "line", fallbackMode: "line" });
389
- expect(resolveRenderMode("candles", 28)).toMatchObject({ effectiveMode: "ohlc", fallbackMode: "ohlc" });
390
- expect(resolveRenderMode("candles", 39)).toMatchObject({ effectiveMode: "ohlc", fallbackMode: "ohlc" });
391
- expect(resolveRenderMode("candles", 40)).toMatchObject({ effectiveMode: "candles", fallbackMode: null });
392
- expect(resolveRenderMode("hlc", 27)).toMatchObject({ effectiveMode: "line", fallbackMode: "line" });
393
- expect(resolveRenderMode("hlc", 28)).toMatchObject({ effectiveMode: "hlc", fallbackMode: null });
394
- });
395
-
396
- test("forces compact charts back to area mode", () => {
397
- const projection = projectChartData(chartFixture, 80, "candles", true);
398
- expect(projection.requestedMode).toBe("area");
399
- expect(projection.effectiveMode).toBe("area");
400
- expect(projection.fallbackMode).toBeNull();
401
- });
402
-
403
- test("uses fewer projected buckets for candles and ohlc-style bars to preserve spacing", () => {
404
- const denseSeries = Array.from({ length: 100 }, (_, i) => ({
405
- date: new Date(2024, 0, i + 1),
406
- open: 100 + i,
407
- high: 101 + i,
408
- low: 99 + i,
409
- close: 100.5 + i,
410
- volume: 1_000 + i,
411
- })) as PricePoint[];
412
-
413
- expect(projectChartData(denseSeries, 80, "candles", false).points).toHaveLength(34);
414
- expect(projectChartData(denseSeries, 80, "ohlc", false).points).toHaveLength(34);
415
- expect(projectChartData(denseSeries, 80, "hlc", false).points).toHaveLength(34);
416
- expect(projectChartData(denseSeries, 80, "line", false).points).toHaveLength(80);
417
- });
418
-
419
- test("keeps candle buckets stable when axis width changes the plot by one column", () => {
420
- const denseSeries = Array.from({ length: 121 }, (_, index) => ({
421
- date: new Date(Date.UTC(2024, 0, index + 1)),
422
- open: 100 + index,
423
- high: 101 + index,
424
- low: 99 + index,
425
- close: 100.5 + index,
426
- volume: 1_000 + index,
427
- })) as PricePoint[];
428
-
429
- const wide = projectChartData(denseSeries, 80, "candles", false, { ohlcBucketWidth: 80 }).points;
430
- const narrowed = projectChartData(denseSeries, 78, "candles", false, { ohlcBucketWidth: 80 }).points;
431
-
432
- expect(narrowed.map((point) => point.date.toISOString())).toEqual(wide.map((point) => point.date.toISOString()));
433
- expect(narrowed.map((point) => point.open)).toEqual(wide.map((point) => point.open));
434
- expect(narrowed.map((point) => point.close)).toEqual(wide.map((point) => point.close));
435
- });
436
- });
437
-
438
- describe("renderChart", () => {
439
- test("sizes the y-axis to the visible label width instead of a fixed gutter", () => {
440
- expect(resolveChartAxisWidth(["$18", "$17", "$16", "$14"], 4, 10)).toBe(4);
441
- expect(resolveChartAxisWidth(["$1.1678", "$1.1654"], 4, 10)).toBe(7);
442
- expect(resolveChartAxisWidth(["+0.25%", "-0.10%"], 5, 11)).toBe(6);
443
- });
444
-
445
- test("formats price axes with the instrument currency", () => {
446
- expect(formatAxisValue(21_970, "price", 0, "JPY")).toBe("¥22.0K");
447
- expect(formatAxisValue(12_340, "price", 0, "HKD")).toBe("HK$12.3K");
448
- expect(formatAxisValue(1.17364, "price", 0, "USD", "CURRENCY")).toBe("$1.17364");
449
- });
450
-
451
- test("keeps legend and cursor prices precise while axis ticks stay compact", () => {
452
- expect(formatAxisValue(79_432.18, "price", 0, "USD", "CRYPTO")).toBe("$79.4K");
453
- expect(formatCursorAxisValue(79_432.18, "price", 0, "USD", "CRYPTO")).toBe("$79,432.18");
454
- expect(formatAxisValue(79_432.18, "price", 0, "USD", "STK")).toBe("$79.4K");
455
- expect(formatCursorAxisValue(79_432.18, "price", 0, "USD", "STK")).toBe("$79,432.18");
456
- });
457
-
458
- test("adapts FX axis precision to the visible price range", () => {
459
- expect(formatAxisValue(1.167815, "price", 0, "USD", "CURRENCY", 0.12)).toBe("$1.17");
460
- expect(formatAxisValue(1.167815, "price", 0, "USD", "CURRENCY", 0.0024)).toBe("$1.1678");
461
- });
462
-
463
- test("keeps the active price-axis label more precise than coarse grid ticks", () => {
464
- expect(formatAxisValue(21.184, "price", 0, "USD", "STK", 11)).toBe("$21");
465
- expect(formatCursorAxisValue(21.184, "price", 0, "USD", "STK", 11)).toBe("$21.18");
466
- expect(formatCursorAxisValue(1.167815, "price", 0, "USD", "CURRENCY", 0.12)).toBe("$1.1678");
467
- });
468
-
469
- test("keeps zoomed equity axis labels distinct and decimal-aware", () => {
470
- const narrowRangeFixture: PricePoint[] = [
471
- { date: new Date("2024-01-02T09:30:00Z"), close: 18.02, volume: 100 },
472
- { date: new Date("2024-01-02T09:31:00Z"), close: 17.91, volume: 120 },
473
- { date: new Date("2024-01-02T09:32:00Z"), close: 17.84, volume: 140 },
474
- { date: new Date("2024-01-02T09:33:00Z"), close: 17.73, volume: 160 },
475
- ];
476
-
477
- const result = renderChart(projectChartData(narrowRangeFixture, 32, "area", false).points, {
478
- width: 32,
479
- height: 8,
480
- showVolume: false,
481
- volumeHeight: 0,
482
- cursorX: null,
483
- cursorY: null,
484
- mode: "area",
485
- axisMode: "price",
486
- currency: "USD",
487
- assetCategory: "STK",
488
- colors: palette,
489
- });
490
-
491
- expect(result.axisFractionDigits).toBeGreaterThanOrEqual(1);
492
- expect(new Set(result.axisLabels.map((entry) => entry.label)).size).toBe(result.axisLabels.length);
493
- expect(result.axisLabels.every((entry) => entry.label.includes("."))).toBe(true);
494
- });
495
-
496
- test("keeps indicator overlays out of the chart price range", () => {
497
- const projection = projectChartData(chartFixture, 12, "line", false);
498
- const baseScene = buildChartScene(projection.points, {
499
- width: 12,
500
- height: 6,
501
- showVolume: false,
502
- volumeHeight: 0,
503
- cursorX: null,
504
- cursorY: null,
505
- mode: projection.effectiveMode,
506
- colors: palette,
507
- });
508
- const sceneWithIndicators = buildChartScene(projection.points, {
509
- width: 12,
510
- height: 6,
511
- showVolume: false,
512
- volumeHeight: 0,
513
- cursorX: null,
514
- cursorY: null,
515
- mode: projection.effectiveMode,
516
- colors: palette,
517
- indicators: {
518
- smaLines: [{
519
- period: 2,
520
- color: "#ff00ff",
521
- points: [
522
- { index: 0, value: 6 },
523
- { index: 1, value: 18 },
524
- ],
525
- }],
526
- emaLines: [{
527
- period: 2,
528
- color: "#00ffff",
529
- points: [
530
- { index: 0, value: 4 },
531
- { index: 1, value: 20 },
532
- ],
533
- }],
534
- bollinger: {
535
- color: "#ffaa00",
536
- upper: [
537
- { index: 0, value: 22 },
538
- { index: 1, value: 23 },
539
- ],
540
- middle: [
541
- { index: 0, value: 14 },
542
- { index: 1, value: 15 },
543
- ],
544
- lower: [
545
- { index: 0, value: 3 },
546
- { index: 1, value: 4 },
547
- ],
548
- },
549
- rsi: null,
550
- macd: null,
551
- },
552
- });
553
-
554
- expect(sceneWithIndicators?.min).toBe(baseScene?.min);
555
- expect(sceneWithIndicators?.max).toBe(baseScene?.max);
556
- });
557
-
558
- test("draws indicator overlays below price pixels", () => {
559
- const points: PricePoint[] = [
560
- { date: new Date("2024-01-02"), close: 10, volume: 100 },
561
- { date: new Date("2024-01-03"), close: 11, volume: 120 },
562
- { date: new Date("2024-01-04"), close: 12, volume: 140 },
563
- ];
564
- const result = renderChart(points, {
565
- width: 12,
566
- height: 6,
567
- showVolume: false,
568
- volumeHeight: 0,
569
- cursorX: null,
570
- cursorY: null,
571
- mode: "line",
572
- colors: palette,
573
- indicators: {
574
- smaLines: [{
575
- period: 2,
576
- color: "#ff00ff",
577
- points: [
578
- { index: 0, value: 10 },
579
- { index: 1, value: 11 },
580
- { index: 2, value: 12 },
581
- ],
582
- }],
583
- emaLines: [],
584
- bollinger: null,
585
- rsi: null,
586
- macd: null,
587
- },
588
- });
589
-
590
- const pixels = result.pixelBuffer?.pixels.flat().filter(Boolean) ?? [];
591
- expect(pixels.some((pixel) => pixel?.color === palette.lineColor)).toBe(true);
592
- expect(pixels.some((pixel) => pixel?.color === "#ff00ff")).toBe(false);
593
- });
594
-
595
- test("normalizes fractional web dimensions before drawing volume bars", () => {
596
- const projection = projectChartData(chartFixture, 12, "area", false);
597
- const result = renderChart(projection.points, {
598
- width: 12.5,
599
- height: 7.3,
600
- showVolume: true,
601
- volumeHeight: 3,
602
- cursorX: null,
603
- cursorY: null,
604
- mode: projection.effectiveMode,
605
- colors: palette,
606
- });
607
-
608
- expect(result.lines).toHaveLength(7);
609
- expect(result.pixelBuffer?.width).toBe(24);
610
- expect(result.pixelBuffer?.height).toBe(28);
611
- expect(result.axisLabels.every((entry) => Number.isInteger(entry.row))).toBe(true);
612
- });
613
-
614
- test("keeps terminal chart cells transparent", () => {
615
- const projection = projectChartData(chartFixture, 12, "area", false);
616
- const result = renderChart(projection.points, {
617
- width: 12,
618
- height: 5,
619
- showVolume: false,
620
- volumeHeight: 0,
621
- cursorX: null,
622
- cursorY: null,
623
- mode: projection.effectiveMode,
624
- colors: palette,
625
- });
626
-
627
- const chunks = result.lines.flatMap((line) => line.chunks);
628
- expect(chunks.some((chunk) => chunk.text.trim().length > 0)).toBe(true);
629
- expect(chunks.every((chunk) => chunk.bg === undefined)).toBe(true);
630
- });
631
-
632
- test("keeps one decimal on zoomed equity axes even when whole-dollar ticks are distinct", () => {
633
- const mediumRangeFixture: PricePoint[] = [
634
- { date: new Date("2024-01-02T09:30:00Z"), close: 233.82, volume: 100 },
635
- { date: new Date("2024-01-02T09:45:00Z"), close: 232.14, volume: 120 },
636
- { date: new Date("2024-01-02T10:00:00Z"), close: 230.21, volume: 140 },
637
- { date: new Date("2024-01-02T10:15:00Z"), close: 231.67, volume: 160 },
638
- { date: new Date("2024-01-02T10:30:00Z"), close: 228.94, volume: 180 },
639
- ];
640
-
641
- const result = renderChart(projectChartData(mediumRangeFixture, 28, "area", false).points, {
642
- width: 28,
643
- height: 8,
644
- showVolume: false,
645
- volumeHeight: 0,
646
- cursorX: null,
647
- cursorY: null,
648
- mode: "area",
649
- axisMode: "price",
650
- currency: "USD",
651
- assetCategory: "STK",
652
- colors: palette,
653
- });
654
-
655
- expect(result.axisFractionDigits).toBe(1);
656
- expect(result.axisLabels.every((entry) => entry.label.includes("."))).toBe(true);
657
- });
658
-
659
- test("eases coarse cursor motion quickly and settles without overshooting", () => {
660
- let current: { x: number | null; y: number | null } = { x: 2, y: 1 };
661
- const target = { x: 6, y: 4 };
662
-
663
- const firstStep = stepCursorTowards(current, target);
664
- expect(firstStep.settled).toBe(false);
665
- expect(firstStep.next.x!).toBeGreaterThan(current.x!);
666
- expect(firstStep.next.x!).toBeLessThan(target.x);
667
- expect(firstStep.next.y!).toBeGreaterThan(current.y!);
668
- expect(firstStep.next.y!).toBeLessThan(target.y);
669
-
670
- current = firstStep.next;
671
- for (let iteration = 0; iteration < 8; iteration += 1) {
672
- const nextStep = stepCursorTowards(current, target);
673
- current = nextStep.next;
674
- if (nextStep.settled) break;
675
- }
676
-
677
- expect(current).toEqual(target);
678
- });
679
-
680
- test("snaps directly when the coarse cursor has no active position", () => {
681
- expect(stepCursorTowards({ x: null, y: null }, { x: 8, y: 3 })).toEqual({
682
- next: { x: 8, y: 3 },
683
- settled: true,
684
- });
685
- });
686
-
687
- test("compresses dense short-range x-axis labels without repeating the month on every tick", () => {
688
- const dates = Array.from({ length: 31 }, (_, index) => new Date(2026, 0, index + 1));
689
-
690
- expect(buildTimeAxis(dates, 72)).toBe(
691
- "Jan 1 4 8 11 14 18 21 24 28 Jan 31",
692
- );
693
- });
694
-
695
- test("uses times instead of repeating the same calendar date for intraday ranges", () => {
696
- const dates = Array.from({ length: 13 }, (_, index) => new Date(2026, 0, 5, 9, 30 + index * 30));
697
-
698
- expect(buildTimeAxis(dates, 72)).toBe(
699
- "09:30 10:30 11:00 12:00 13:00 14:00 14:30 15:30",
700
- );
701
- });
702
-
703
- test("overlays the cursor date on the x-axis without changing its width", () => {
704
- const dates = Array.from({ length: 13 }, (_, index) => new Date(2026, 0, 5, 9, 30 + index * 30));
705
- const width = 72;
706
- const segments = buildCursorTimeAxisSegments({
707
- timeLabels: buildTimeAxis(dates, width),
708
- width,
709
- cursorColumn: 36,
710
- cursorDate: dates[6]!,
711
- dates,
712
- });
713
-
714
- expect(segments.map((segment) => segment.text).join("")).toHaveLength(width);
715
- expect(segments.find((segment) => segment.highlighted)?.text).toBe("12:30");
716
- });
717
-
718
- test("keeps day precision for cursor labels when the axis coarsens to months", () => {
719
- const dates = Array.from({ length: 12 }, (_, index) => new Date(2025, index, 1));
720
- const width = 48;
721
- const segments = buildCursorTimeAxisSegments({
722
- timeLabels: buildTimeAxis(dates, width),
723
- width,
724
- cursorColumn: 27,
725
- cursorDate: new Date(2025, 8, 15),
726
- dates,
727
- });
728
-
729
- expect(segments.find((segment) => segment.highlighted)?.text).toBe("Sep 15 2025");
730
- });
731
-
732
- test("positions desktop cursor y-axis overlay from exact pixels", () => {
733
- const height = 8;
734
- const cellHeightPx = 18;
735
- const cursorPixelY = 45.25;
736
- const overlay = buildCursorPriceAxisOverlay({
737
- axisWidth: 7,
738
- axisSectionWidth: 8,
739
- height,
740
- cursorPixelY,
741
- cursorLabel: "$214.03",
742
- cellHeightPx,
743
- });
744
-
745
- expect(overlay.labelText).toBe("$214.03");
746
- expect(overlay.topPercent).toBeCloseTo((cursorPixelY / (height * cellHeightPx - 1)) * 100, 5);
747
- });
748
-
749
- test("shows second precision when the visible window reaches second-level data", () => {
750
- const dates = Array.from({ length: 6 }, (_, index) => new Date(2026, 0, 5, 9, 30, index * 10));
751
-
752
- expect(buildTimeAxis(dates, 60)).toBe(
753
- "09:30:00 09:30:20 09:30:30 09:30:50",
754
- );
755
- });
756
-
757
- test("collapses identical timestamps into a single centered label", () => {
758
- const dates = Array.from({ length: 20 }, () => new Date(2026, 2, 29, 9, 0, 0, 0));
759
-
760
- expect(buildTimeAxis(dates, 96)).toBe(
761
- " 09:00:00.000 ",
762
- );
763
- });
764
-
765
- test("coarsens to month and year labels for longer spans", () => {
766
- const monthlyDates = Array.from({ length: 12 }, (_, index) => new Date(2025, index, 1));
767
- const yearlyDates = Array.from({ length: 6 }, (_, index) => new Date(2020 + index, 0, 1));
768
-
769
- expect(buildTimeAxis(monthlyDates, 48)).toBe("Jan 2025 May Aug Dec 2025");
770
- expect(buildTimeAxis(yearlyDates, 48)).toBe("2020 2021 2022 2023 2024 2025");
771
- });
772
-
773
- test("marks US premarket and postmarket bars for chart backgrounds", () => {
774
- const session = resolveChartMarketSession([{ exchange: "NASDAQ", currency: "USD", assetCategory: "STK" }]);
775
- const dates = [
776
- new Date("2026-05-14T11:00:00Z"),
777
- new Date("2026-05-14T14:00:00Z"),
778
- new Date("2026-05-14T20:30:00Z"),
779
- ];
780
-
781
- expect(resolveExtendedHoursBackgroundSpans(dates, session)).toEqual([
782
- { kind: "pre", startIndex: 0, endIndex: 0 },
783
- { kind: "post", startIndex: 2, endIndex: 2 },
784
- ]);
785
- });
786
-
787
- test("omits extended-hours backgrounds for wide intraday windows", () => {
788
- const session = resolveChartMarketSession([{ exchange: "NASDAQ", currency: "USD", assetCategory: "STK" }]);
789
- const dates = Array.from({ length: 12 }).flatMap((_, dayIndex) => {
790
- const day = 4 + dayIndex;
791
- return [
792
- new Date(Date.UTC(2026, 4, day, 11, 0)),
793
- new Date(Date.UTC(2026, 4, day, 14, 0)),
794
- new Date(Date.UTC(2026, 4, day, 20, 30)),
795
- ];
796
- });
797
-
798
- expect(resolveExtendedHoursBackgroundSpans(dates, session)).toEqual([]);
799
- });
800
-
801
- test("paints extended-hours chart cells as background colors", () => {
802
- const session = resolveChartMarketSession([{ exchange: "NASDAQ", currency: "USD", assetCategory: "STK" }]);
803
- const points: PricePoint[] = [
804
- { date: new Date("2026-05-14T11:00:00Z"), close: 10, volume: 100 },
805
- { date: new Date("2026-05-14T14:00:00Z"), close: 12, volume: 120 },
806
- { date: new Date("2026-05-14T20:30:00Z"), close: 11, volume: 110 },
807
- ];
808
-
809
- const result = renderChart(points, {
810
- width: 12,
811
- height: 4,
812
- showVolume: false,
813
- volumeHeight: 0,
814
- cursorX: null,
815
- cursorY: null,
816
- mode: "line",
817
- colors: palette,
818
- marketSession: session,
819
- });
820
-
821
- const backgrounds = result.lines.flatMap((line) => line.chunks.map((chunk) => chunk.bg));
822
- expect(backgrounds).toContain(palette.preMarketBgColor);
823
- expect(backgrounds).toContain(palette.postMarketBgColor);
824
- });
825
-
826
- test("uses source time-axis dates instead of projected extrema dates when provided", () => {
827
- const sourceHistory = Array.from({ length: 96 }, (_, index) => ({
828
- date: new Date(Date.UTC(2026, 2, 25, index * 3)),
829
- close: index % 2 === 0 ? 0.7 + index * 0.001 : 0.3 + index * 0.001,
830
- volume: 100 + index,
831
- })) as PricePoint[];
832
- const sourceDates = sourceHistory.map((point) => point.date);
833
- const projection = projectChartData(sourceHistory, 48, "area", true);
834
-
835
- const result = renderChart(projection.points, {
836
- width: 48,
837
- height: 8,
838
- showVolume: false,
839
- volumeHeight: 0,
840
- cursorX: null,
841
- cursorY: null,
842
- mode: "area",
843
- axisMode: "price",
844
- currency: "USD",
845
- colors: palette,
846
- timeAxisDates: sourceDates,
847
- });
848
-
849
- expect(result.timeLabels).toBe(buildTimeAxis(sourceDates, 48));
850
- });
851
-
852
- test("maps active bucket metadata across all render modes", () => {
853
- const cases: Array<{ mode: ChartRenderMode; width: number; cursorX: number }> = [
854
- { mode: "area", width: 12, cursorX: 4 },
855
- { mode: "line", width: 12, cursorX: 4 },
856
- { mode: "candles", width: 40, cursorX: 13 },
857
- { mode: "ohlc", width: 28, cursorX: 9 },
858
- { mode: "hlc", width: 28, cursorX: 9 },
859
- ];
860
-
861
- for (const testCase of cases) {
862
- const projection = projectChartData(chartFixture, testCase.width, testCase.mode, false);
863
- const result = renderChart(projection.points, {
864
- width: testCase.width,
865
- height: 6,
866
- showVolume: false,
867
- volumeHeight: 0,
868
- cursorX: testCase.cursorX,
869
- cursorY: null,
870
- mode: projection.effectiveMode,
871
- colors: palette,
872
- });
873
-
874
- expect(result.activePoint?.date.toISOString()).toBe("2024-01-03T00:00:00.000Z");
875
- expect(result.activePoint?.close).toBe(12);
876
- }
877
- });
878
-
879
- test("keeps the visual cursor free-running on both axes", () => {
880
- const cases: Array<{ mode: ChartRenderMode; width: number; height: number; cursorX: number; cursorY: number }> = [
881
- { mode: "area", width: 12, height: 6, cursorX: 5, cursorY: 4 },
882
- { mode: "candles", width: 40, height: 6, cursorX: 13, cursorY: 3 },
883
- { mode: "ohlc", width: 28, height: 6, cursorX: 9, cursorY: 1 },
884
- { mode: "hlc", width: 28, height: 6, cursorX: 9, cursorY: 1 },
885
- ];
886
-
887
- for (const testCase of cases) {
888
- const projection = projectChartData(chartFixture, testCase.width, testCase.mode, false);
889
- const result = renderChart(projection.points, {
890
- width: testCase.width,
891
- height: testCase.height,
892
- showVolume: false,
893
- volumeHeight: 0,
894
- cursorX: testCase.cursorX,
895
- cursorY: testCase.cursorY,
896
- mode: projection.effectiveMode,
897
- colors: palette,
898
- });
899
-
900
- expect(result.cursorColumn).toBe(testCase.cursorX);
901
- expect(result.cursorRow).toBe(testCase.cursorY);
902
- }
903
- });
904
-
905
- test("uses fractional cursor rows for crosshair price readout", () => {
906
- const projection = projectChartData(chartFixture, 12, "line", false);
907
- const result = renderChart(projection.points, {
908
- width: 12,
909
- height: 6,
910
- showVolume: false,
911
- volumeHeight: 0,
912
- cursorX: 5.25,
913
- cursorY: 2.5,
914
- mode: projection.effectiveMode,
915
- colors: palette,
916
- });
917
-
918
- expect(result.cursorColumn).toBe(5);
919
- expect(result.cursorRow).toBe(3);
920
- expect(result.crosshairPrice).toBeCloseTo(10.5, 5);
921
- });
922
-
923
- test("renders stable terminal shapes for each chart mode", () => {
924
- const cases: Array<{ mode: ChartRenderMode; width: number; height: number; lines: string[]; timeLabels: string }> = [
925
- {
926
- mode: "area",
927
- width: 12,
928
- height: 5,
929
- lines: [
930
- "⠁ ⡠⠔⢣ ⠁ ⠁ ",
931
- "⠔⠊⣿⣿⠈⢆⠄ ⠄ ",
932
- "⣿⣿⣿⣿⣿⠘⡄ ⡠",
933
- "⣿⣿⣿⣿⣿⣿⠱⡀ ⢠⠊⣿",
934
- "⣿⣿⣿⣿⣿⣿⣿⢣⠔⠁⣿⣿",
935
- ],
936
- timeLabels: "Jan 2 Jan 5",
937
- },
938
- {
939
- mode: "line",
940
- width: 12,
941
- height: 5,
942
- lines: [
943
- "⠁ ⡠⠔⢣ ⠁ ⠁ ",
944
- "⠔⠊ ⠄⠈⢆⠄ ⠄ ",
945
- " ⠘⡄ ⡠",
946
- "⠂ ⠂ ⠱⡀ ⢠⠊ ",
947
- "⡀ ⡀ ⡀⢣⠔⠁ ",
948
- ],
949
- timeLabels: "Jan 2 Jan 5",
950
- },
951
- {
952
- mode: "candles",
953
- width: 40,
954
- height: 6,
955
- lines: [
956
- "⠁ ⠁ ⠁ ⠁ ⠁ ⡇⠁ ⠁ ⠁ ⠁⢠ ⠁ ⠁ ⠁ ⠁ ⠁",
957
- " ⢰ ⢰⣶⣶⣷⣶⣶ ⣶⣶⣾⣶⣶⡆ ",
958
- "⣶⣶⣾⣶⣶⡆⠁ ⠁ ⠘⠛⠛⡟⠛⠛ ⠁ ⠁ ⣿⣿⣿⣿⣿⡇ ⠁ ⠁ ⠁⡆ ⠁",
959
- "⠿⠿⢿⠿⠿⠇⡀ ⡀ ⡀ ⠇⡀ ⡀ ⡀ ⣿⣿⣿⣿⣿⡇ ⡀ ⡀⢸⣿⣿⣿⣿⣿",
960
- " ⠸ ⠿⠿⢿⠿⠿⠇ ⠸⠿⠿⡿⠿⠿",
961
- "⡀ ⡀ ⡀ ⡀ ⡀ ⡀ ⡀ ⡀ ⡀⢸ ⡀ ⡀ ⡀ ⡀⠃ ⡀",
962
- ],
963
- timeLabels: "Jan 2 3 4 Jan 5",
964
- },
965
- {
966
- mode: "ohlc",
967
- width: 28,
968
- height: 6,
969
- lines: [
970
- "⠁ ⠁ ⠁ ⢸⣿ ⠁ ⠁ ⣤⡄ ⠁ ⠁ ⠁",
971
- " ⢰⣶ ⢸⣿⠒⠂ ⠐⠒⣿⡇ ",
972
- "⠁⢸⣿⠒⠂ ⠁ ⠒⢺⣿ ⠁ ⠁ ⣿⡇ ⠁ ⠁⣶⡆⠁",
973
- "⠤⢼⣿⡀ ⡀ ⠸⠿ ⡀ ⡀ ⣿⡇ ⡀ ⡀⣿⡏⠉",
974
- " ⠸⠿ ⣿⡧⠤ ⠠⠤⣿⡇ ",
975
- "⡀ ⡀ ⡀ ⡀ ⡀ ⡀ ⣿⡇ ⡀ ⡀⠛⠃⡀",
976
- ],
977
- timeLabels: "Jan 2 3 4 Jan 5",
978
- },
979
- {
980
- mode: "hlc",
981
- width: 28,
982
- height: 6,
983
- lines: [
984
- "⠁ ⠁ ⠁ ⢸⣿ ⠁ ⠁ ⣤⡄ ⠁ ⠁ ⠁",
985
- " ⢰⣶ ⢸⣿⠒⠂ ⣿⡇ ",
986
- "⠁⢸⣿⠒⠂ ⠁ ⢸⣿ ⠁ ⠁ ⣿⡇ ⠁ ⠁⣶⡆⠁",
987
- "⡀⢸⣿⡀ ⡀ ⠸⠿ ⡀ ⡀ ⣿⡇ ⡀ ⡀⣿⡏⠉",
988
- " ⠸⠿ ⣿⡧⠤ ⣿⡇ ",
989
- "⡀ ⡀ ⡀ ⡀ ⡀ ⡀ ⣿⡇ ⡀ ⡀⠛⠃⡀",
990
- ],
991
- timeLabels: "Jan 2 3 4 Jan 5",
992
- },
993
- ];
994
-
995
- for (const testCase of cases) {
996
- const projection = projectChartData(chartFixture, testCase.width, testCase.mode, false);
997
- const result = renderChart(projection.points, {
998
- width: testCase.width,
999
- height: testCase.height,
1000
- showVolume: false,
1001
- volumeHeight: 0,
1002
- cursorX: null,
1003
- cursorY: null,
1004
- mode: projection.effectiveMode,
1005
- colors: palette,
1006
- });
1007
-
1008
- expect(textLines(result)).toEqual(testCase.lines);
1009
- expect(result.timeLabels).toBe(testCase.timeLabels);
1010
- }
1011
- });
1012
-
1013
- test("formats y-axis labels as percent change when requested", () => {
1014
- const projection = projectChartData(chartFixture, 12, "line", false);
1015
- const result = renderChart(projection.points, {
1016
- width: 12,
1017
- height: 5,
1018
- showVolume: false,
1019
- volumeHeight: 0,
1020
- cursorX: null,
1021
- cursorY: null,
1022
- mode: projection.effectiveMode,
1023
- axisMode: "percent",
1024
- colors: palette,
1025
- });
1026
-
1027
- expect(result.axisLabels).toEqual([
1028
- { row: 0, label: "+9.09%" },
1029
- { row: 1, label: "0.00%" },
1030
- { row: 3, label: "-9.09%" },
1031
- { row: 4, label: "-18.2%" },
1032
- ]);
1033
- });
1034
-
1035
- test("uses the supplied currency for rendered price-axis labels", () => {
1036
- const projection = projectChartData(chartFixture, 12, "line", false);
1037
- const result = renderChart(projection.points, {
1038
- width: 12,
1039
- height: 5,
1040
- showVolume: false,
1041
- volumeHeight: 0,
1042
- cursorX: null,
1043
- cursorY: null,
1044
- mode: projection.effectiveMode,
1045
- axisMode: "price",
1046
- currency: "JPY",
1047
- colors: palette,
1048
- });
1049
-
1050
- expect(result.axisLabels.some((entry) => entry.label.includes("¥"))).toBe(true);
1051
- expect(result.axisLabels.every((entry) => !entry.label.includes("$"))).toBe(true);
1052
- });
1053
-
1054
- test("accepts serialized string dates from cached chart data", () => {
1055
- const serialized = chartFixture.map((point) => ({
1056
- ...point,
1057
- date: point.date.toISOString(),
1058
- })) as unknown as PricePoint[];
1059
-
1060
- const projection = projectChartData(serialized, 12, "area", false);
1061
- const result = renderChart(projection.points, {
1062
- width: 12,
1063
- height: 5,
1064
- showVolume: false,
1065
- volumeHeight: 0,
1066
- cursorX: null,
1067
- cursorY: null,
1068
- mode: projection.effectiveMode,
1069
- colors: palette,
1070
- });
1071
-
1072
- expect(projection.points[0]?.date instanceof Date).toBe(true);
1073
- expect(result.timeLabels).toBe("Jan 2 Jan 5");
1074
- });
1075
- });