gloomberb 0.9.7 → 0.9.9

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 (276) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/auth.ts +20 -0
  3. package/src/api-client/index.test.ts +3 -3
  4. package/src/api-client/index.ts +48 -6
  5. package/src/api-client/request.test.ts +63 -0
  6. package/src/api-client/request.ts +53 -5
  7. package/src/api-client/types.ts +45 -2
  8. package/src/app/pane-runtime/layout-placement.ts +0 -1
  9. package/src/app/runtime/desktop-deeplink.test.ts +87 -0
  10. package/src/app/runtime/desktop-deeplink.ts +496 -0
  11. package/src/app.tsx +34 -0
  12. package/src/brokers/account-cache.ts +2 -10
  13. package/src/brokers/broker-portfolio-sync.ts +0 -1
  14. package/src/brokers/broker-ticker-sync.ts +20 -16
  15. package/src/brokers/remote-broker-adapter.ts +3 -0
  16. package/src/capabilities/registry.test.ts +25 -0
  17. package/src/capabilities/registry.ts +22 -5
  18. package/src/cli/commands/automation.ts +1 -3
  19. package/src/cli/commands/command-utils.ts +0 -7
  20. package/src/cli/commands/overview.ts +2 -2
  21. package/src/cli/commands/plugins.ts +34 -17
  22. package/src/cli/index.ts +2 -2
  23. package/src/cli.test.ts +0 -157
  24. package/src/components/chart/bar-chart-renderer.test.ts +87 -12
  25. package/src/components/chart/bar-chart-renderer.ts +111 -55
  26. package/src/components/chart/comparison/renderer.ts +0 -1
  27. package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
  28. package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
  29. package/src/components/chart/core/index.test.ts +0 -24
  30. package/src/components/chart/core/renderer.ts +0 -4
  31. package/src/components/chart/core/resolution.test.ts +0 -17
  32. package/src/components/chart/core/resolution.ts +0 -26
  33. package/src/components/chart/core/scroll.test.ts +0 -6
  34. package/src/components/chart/core/scroll.ts +0 -22
  35. package/src/components/chart/core/viewport.test.ts +2 -23
  36. package/src/components/chart/core/viewport.ts +0 -27
  37. package/src/components/chart/market-session.ts +1 -1
  38. package/src/components/chart/native/surface/hooks.ts +10 -3
  39. package/src/components/chart/static/bar-chart-surface.tsx +104 -20
  40. package/src/components/chart/stock-chart/data-runtime.ts +0 -1
  41. package/src/components/chart/stock-chart/footer.test.ts +65 -0
  42. package/src/components/chart/stock-chart/footer.ts +68 -13
  43. package/src/components/chart/stock-chart/resolved.tsx +18 -4
  44. package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
  45. package/src/components/command-bar/helpers.test.ts +1 -80
  46. package/src/components/command-bar/helpers.ts +1 -36
  47. package/src/components/command-bar/pane-settings.ts +1 -1
  48. package/src/components/command-bar/pane-templates/items.ts +11 -3
  49. package/src/components/command-bar/panel/runtime.ts +0 -2
  50. package/src/components/command-bar/routes/root/results.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
  52. package/src/components/command-bar/surface/index.test.tsx +52 -16
  53. package/src/components/command-bar/surface/index.tsx +0 -1
  54. package/src/components/command-bar/workflow/ops.ts +81 -20
  55. package/src/components/feed-data-table/stack-view.test.tsx +36 -130
  56. package/src/components/feed-data-table/stack-view.tsx +0 -1
  57. package/src/components/index.ts +1 -0
  58. package/src/components/layout/shell/index.test.tsx +2 -113
  59. package/src/components/layout/shell/index.tsx +0 -1
  60. package/src/components/layout/shell/menu.ts +0 -1
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
  62. package/src/components/onboarding/onboarding-wizard.tsx +3 -3
  63. package/src/components/onboarding/wizard-keyboard.ts +2 -23
  64. package/src/components/price-selector-dialog.tsx +3 -3
  65. package/src/components/ticker/badge/text.tsx +0 -1
  66. package/src/components/toggle-list.tsx +29 -73
  67. package/src/components/ui/button.tsx +4 -1
  68. package/src/components/ui/checkbox.tsx +76 -0
  69. package/src/components/ui/choice-dialog.tsx +15 -3
  70. package/src/components/ui/frame.tsx +1 -1
  71. package/src/components/ui/index.ts +2 -0
  72. package/src/components/ui/message-composer.tsx +4 -0
  73. package/src/components/ui/native-select.tsx +4 -2
  74. package/src/components/ui/page-stack-view.tsx +1 -1
  75. package/src/components/ui/ui.test.tsx +0 -78
  76. package/src/core/state/app/layout-reducer.ts +0 -1
  77. package/src/data/config/layout.ts +3 -3
  78. package/src/data/resource-store.ts +6 -0
  79. package/src/data/sqlite/cache.test.ts +5 -0
  80. package/src/data/ticker-store.ts +0 -7
  81. package/src/market-data/coordinator/auxiliary.ts +1 -44
  82. package/src/market-data/coordinator/entries.ts +0 -1
  83. package/src/market-data/coordinator/events.test.ts +49 -0
  84. package/src/market-data/coordinator/events.ts +25 -4
  85. package/src/market-data/coordinator/index.test.ts +223 -10
  86. package/src/market-data/coordinator/index.ts +41 -21
  87. package/src/market-data/coordinator/quotes.ts +13 -11
  88. package/src/market-data/coordinator/subscriptions.test.ts +1 -0
  89. package/src/market-data/hooks.tsx +17 -13
  90. package/src/market-data/market/freshness.ts +58 -1
  91. package/src/market-data/market/status.ts +19 -1
  92. package/src/market-data/query-store.ts +0 -4
  93. package/src/market-data/quotes/freshness.test.ts +49 -1
  94. package/src/market-data/quotes/freshness.ts +8 -0
  95. package/src/market-data/quotes/resolution.test.ts +185 -0
  96. package/src/market-data/quotes/resolution.ts +96 -10
  97. package/src/market-data/request-types.test.ts +61 -1
  98. package/src/market-data/request-types.ts +42 -8
  99. package/src/market-data/result-types.ts +0 -11
  100. package/src/market-data/selectors.ts +1 -5
  101. package/src/news/aggregator.ts +5 -1
  102. package/src/pane-settings.test.ts +0 -18
  103. package/src/plugins/builtin/account-management/footer.ts +2 -5
  104. package/src/plugins/builtin/account-management/form-components.tsx +175 -59
  105. package/src/plugins/builtin/account-management/keyboard.ts +32 -2
  106. package/src/plugins/builtin/account-management/model.test.ts +99 -4
  107. package/src/plugins/builtin/account-management/model.ts +259 -23
  108. package/src/plugins/builtin/account-management/pane.tsx +784 -155
  109. package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
  110. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  111. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
  112. package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
  113. package/src/plugins/builtin/buildout/format.ts +2 -11
  114. package/src/plugins/builtin/chat/chat.test.tsx +127 -1
  115. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
  116. package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
  117. package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
  118. package/src/plugins/builtin/chat/content/composer.tsx +84 -1
  119. package/src/plugins/builtin/chat/content/index.tsx +153 -16
  120. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
  121. package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
  122. package/src/plugins/builtin/chat/content/mentions.ts +88 -0
  123. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
  124. package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
  125. package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
  126. package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
  127. package/src/plugins/builtin/chat/controller/index.ts +0 -10
  128. package/src/plugins/builtin/chat/controller.test.ts +5 -5
  129. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
  131. package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
  132. package/src/plugins/builtin/chat/sidebar.tsx +26 -2
  133. package/src/plugins/builtin/chat/test-harness.tsx +22 -12
  134. package/src/plugins/builtin/cloud/plugin.tsx +8 -1
  135. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
  136. package/src/plugins/builtin/congress-trades/model.ts +2 -7
  137. package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
  138. package/src/plugins/builtin/correlation/index.tsx +4 -1
  139. package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
  140. package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
  141. package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
  142. package/src/plugins/builtin/earnings/index.tsx +0 -9
  143. package/src/plugins/builtin/econ/index.tsx +1 -5
  144. package/src/plugins/builtin/help/index.tsx +2 -2
  145. package/src/plugins/builtin/holders/pane.tsx +0 -5
  146. package/src/plugins/builtin/insider/index.tsx +1 -8
  147. package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
  148. package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
  149. package/src/plugins/builtin/market-movers/index.tsx +0 -2
  150. package/src/plugins/builtin/news/index.tsx +0 -1
  151. package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
  152. package/src/plugins/builtin/options/view.test.tsx +1 -38
  153. package/src/plugins/builtin/plugin-groups.ts +17 -3
  154. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
  155. package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
  156. package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
  157. package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
  158. package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
  159. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
  160. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
  161. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
  162. package/src/plugins/builtin/sec/filing-documents.ts +0 -11
  163. package/src/plugins/builtin/sec/index.tsx +0 -2
  164. package/src/plugins/builtin/substack/article-detail.tsx +1 -1
  165. package/src/plugins/builtin/thirteenf/api.ts +0 -15
  166. package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
  167. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
  168. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
  169. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
  170. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
  171. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
  172. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
  173. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
  174. package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
  175. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
  176. package/src/plugins/builtin/world-indices/index.tsx +0 -4
  177. package/src/plugins/builtin/world-indices/model.ts +0 -7
  178. package/src/plugins/ibkr/account-cache.ts +1 -1
  179. package/src/plugins/ibkr/gateway/market-data.ts +12 -5
  180. package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
  181. package/src/plugins/ibkr/portfolio-performance.ts +1 -1
  182. package/src/plugins/ibkr/trade/utils.ts +1 -7
  183. package/src/plugins/loader.ts +2 -1
  184. package/src/plugins/prediction-markets/detail.test.tsx +0 -34
  185. package/src/plugins/registry/context.ts +15 -0
  186. package/src/plugins/registry/contributions.ts +22 -11
  187. package/src/plugins/registry/index.test.ts +44 -0
  188. package/src/plugins/registry/index.ts +75 -26
  189. package/src/remote/controller.ts +1 -7
  190. package/src/remote/json-patch.ts +15 -1
  191. package/src/remote/server.ts +16 -6
  192. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  193. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
  194. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
  195. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
  196. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  197. package/src/renderers/electrobun/bun/index.ts +42 -0
  198. package/src/renderers/electrobun/shared/protocol.ts +5 -0
  199. package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
  200. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  201. package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
  202. package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
  203. package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
  204. package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
  205. package/src/renderers/electrobun/view/host/box.tsx +0 -2
  206. package/src/renderers/electrobun/view/host/mouse.ts +0 -11
  207. package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
  208. package/src/renderers/electrobun/view/host/text.tsx +1 -2
  209. package/src/renderers/electrobun/view/http-fetch.ts +15 -2
  210. package/src/renderers/electrobun/view/main.tsx +3 -0
  211. package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
  212. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
  213. package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
  214. package/src/renderers/electrobun/view/ui-host.tsx +2 -0
  215. package/src/renderers/opentui/host.tsx +11 -3
  216. package/src/renderers/opentui/input-host.tsx +5 -1
  217. package/src/sources/gloomberb-cloud/index.test.ts +0 -72
  218. package/src/sources/gloomberb-cloud/index.ts +1 -30
  219. package/src/sources/gloomberb-cloud/news.ts +0 -12
  220. package/src/sources/provider-router/batches.ts +11 -3
  221. package/src/sources/provider-router/cached-financials.test.ts +123 -0
  222. package/src/sources/provider-router/financial-routes.ts +77 -5
  223. package/src/sources/provider-router/financials.test.ts +49 -0
  224. package/src/sources/provider-router/financials.ts +40 -3
  225. package/src/sources/provider-router/index.test.ts +314 -0
  226. package/src/sources/provider-router/primary.ts +17 -3
  227. package/src/sources/provider-router/search.ts +5 -0
  228. package/src/sources/provider-router/streaming.ts +6 -1
  229. package/src/sources/provider-router/supplemental.ts +4 -3
  230. package/src/sources/sec-edgar.test.ts +42 -0
  231. package/src/sources/sec-edgar.ts +1 -2
  232. package/src/sources/yahoo-finance/history.ts +5 -1
  233. package/src/sources/yahoo-finance/mappers.test.ts +29 -0
  234. package/src/sources/yahoo-finance/mappers.ts +2 -1
  235. package/src/sources/yahoo-finance/options.ts +59 -14
  236. package/src/sources/yahoo-finance/quote-summary.ts +4 -1
  237. package/src/sources/yahoo-finance/requests.ts +2 -1
  238. package/src/sources/yahoo-finance/snapshots.ts +13 -2
  239. package/src/sources/yahoo-finance.test.ts +40 -21
  240. package/src/sources/yahoo-finance.ts +19 -10
  241. package/src/state/app/context/index.test.tsx +0 -81
  242. package/src/state/app/context/selector.test.tsx +6 -2
  243. package/src/state/hooks/quote-streaming.test.ts +14 -1
  244. package/src/state/hooks/quote-streaming.ts +5 -4
  245. package/src/sync/controller.test.ts +31 -0
  246. package/src/sync/controller.ts +300 -0
  247. package/src/sync/core-contributors.test.ts +251 -0
  248. package/src/sync/core-contributors.ts +400 -0
  249. package/src/sync/profile-analytics.ts +39 -0
  250. package/src/sync/react.ts +53 -0
  251. package/src/sync/types.ts +77 -0
  252. package/src/test-support/data-provider.ts +0 -1
  253. package/src/types/desktop-deeplink.ts +7 -0
  254. package/src/types/plugin.ts +3 -0
  255. package/src/ui/host.tsx +15 -1
  256. package/src/ui/index.tsx +2 -0
  257. package/src/updater.test.ts +0 -21
  258. package/src/updater.ts +20 -16
  259. package/src/utils/async-deadline.ts +26 -0
  260. package/src/utils/exchanges.test.ts +11 -1
  261. package/src/utils/exchanges.ts +5 -1
  262. package/src/utils/selection-clipboard.ts +2 -2
  263. package/src/version.ts +1 -1
  264. package/src/components/layout/pane/header.test.tsx +0 -39
  265. package/src/plugins/builtin/chat/channels.test.ts +0 -28
  266. package/src/plugins/builtin/chat/index.tsx +0 -4
  267. package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
  268. package/src/plugins/builtin/earnings/index.test.ts +0 -12
  269. package/src/plugins/builtin/help/index.test.tsx +0 -242
  270. package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
  271. package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
  272. package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
  273. package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
  274. package/src/renderers/electrobun/view/host/text.test.ts +0 -22
  275. package/src/renderers/opentui/dialog-host.test.tsx +0 -44
  276. /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
@@ -34,6 +34,9 @@ export interface BarChartBar {
34
34
  category: string;
35
35
  value: number;
36
36
  color: string;
37
+ categoryIndex: number;
38
+ seriesIndex: number;
39
+ seriesCount: number;
37
40
  x: number;
38
41
  width: number;
39
42
  row: number;
@@ -69,17 +72,6 @@ export interface NativeBarChartPixelBounds {
69
72
 
70
73
  const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
71
74
 
72
- function categorySlotBounds(width: number, categoryCount: number, index: number): { left: number; width: number } {
73
- const left = Math.floor((index * width) / categoryCount);
74
- const right = index === categoryCount - 1
75
- ? width
76
- : Math.floor(((index + 1) * width) / categoryCount);
77
- return {
78
- left,
79
- width: Math.max(1, right - left),
80
- };
81
- }
82
-
83
75
  function collectCategories(series: BarChartSeries[]): string[] {
84
76
  const categories: string[] = [];
85
77
  const seen = new Set<string>();
@@ -90,7 +82,10 @@ function collectCategories(series: BarChartSeries[]): string[] {
90
82
  categories.push(point.category);
91
83
  }
92
84
  }
93
- return categories;
85
+ return categories.filter((category) => series.some((item) => {
86
+ const value = item.points.find((point) => point.category === category)?.value;
87
+ return typeof value === "number" && Number.isFinite(value);
88
+ }));
94
89
  }
95
90
 
96
91
  function normalizeRange(values: number[]): { min: number; max: number } {
@@ -133,32 +128,28 @@ export function buildBarChartScene(series: BarChartSeries[], options: BarChartSc
133
128
 
134
129
  const { min, max } = normalizeRange(values);
135
130
  const zeroRow = valueToRow(0, min, max, height);
136
- const seriesCount = Math.max(series.length, 1);
137
- const categorySlots = categories.map((_, index) => categorySlotBounds(width, categories.length, index));
138
- const minSlotWidth = Math.max(1, Math.min(...categorySlots.map((slot) => slot.width)));
139
- const groupedInnerWidth = Math.max(seriesCount, minSlotWidth - (minSlotWidth > seriesCount * 2 ? 1 : 0));
140
- const groupedBarWidth = Math.max(1, Math.floor(groupedInnerWidth / seriesCount));
131
+ const seriesSlotCount = Math.max(series.length, 1);
141
132
  const bars: BarChartBar[] = [];
142
133
 
143
134
  categories.forEach((category, categoryIndex) => {
144
- const group = categorySlots[categoryIndex]!;
145
- const barWidth = seriesCount === 1 ? group.width : groupedBarWidth;
146
- const barGroupWidth = barWidth * seriesCount;
147
- const groupOffset = Math.max(0, Math.floor((group.width - barGroupWidth) / 2));
148
135
  series.forEach((item, seriesIndex) => {
149
136
  const value = item.points.find((point) => point.category === category)?.value;
150
137
  if (typeof value !== "number" || !Number.isFinite(value)) return;
151
138
  const color = value < 0 && options.colors.negativeColor
152
139
  ? options.colors.negativeColor
153
140
  : item.color;
141
+ const barBounds = chartSlotBounds(width, categories.length * seriesSlotCount, categoryIndex * seriesSlotCount + seriesIndex);
154
142
  bars.push({
155
143
  seriesId: item.id,
156
144
  seriesLabel: item.label,
157
145
  category,
158
146
  value,
159
147
  color,
160
- x: clamp(group.left + groupOffset + seriesIndex * barWidth, 0, width - 1),
161
- width: Math.min(barWidth, Math.max(width - (group.left + groupOffset + seriesIndex * barWidth), 1)),
148
+ categoryIndex,
149
+ seriesIndex,
150
+ seriesCount: seriesSlotCount,
151
+ x: barBounds.x,
152
+ width: barBounds.width,
162
153
  row: valueToRow(value, min, max, height),
163
154
  });
164
155
  });
@@ -177,6 +168,47 @@ export function buildBarChartScene(series: BarChartSeries[], options: BarChartSc
177
168
  };
178
169
  }
179
170
 
171
+ function chartSlotBounds(
172
+ width: number,
173
+ slotCount: number,
174
+ slotIndex: number,
175
+ ): { x: number; width: number } {
176
+ const slots = Math.max(1, slotCount);
177
+ if (slots > width) {
178
+ const x = clamp(Math.floor(slotIndex * width / slots), 0, width - 1);
179
+ const right = clamp(Math.floor((slotIndex + 1) * width / slots), x + 1, width);
180
+ return {
181
+ x,
182
+ width: Math.max(1, right - x),
183
+ };
184
+ }
185
+ const gap = slots > 1 && width >= slots * 2 - 1 ? 1 : 0;
186
+ const usableWidth = Math.max(slots, width - gap * (slots - 1));
187
+ const slotWidth = Math.max(1, Math.floor(usableWidth / slots));
188
+ const usedWidth = slotWidth * slots + gap * (slots - 1);
189
+ const offset = Math.max(0, Math.floor((width - usedWidth) / 2));
190
+ const x = clamp(offset + slotIndex * (slotWidth + gap), 0, width - 1);
191
+ return {
192
+ x,
193
+ width: Math.max(1, Math.min(slotWidth, width - x)),
194
+ };
195
+ }
196
+
197
+ function categoryCellBounds(
198
+ width: number,
199
+ categoryCount: number,
200
+ seriesCount: number,
201
+ categoryIndex: number,
202
+ ): { left: number; width: number } {
203
+ const first = chartSlotBounds(width, categoryCount * seriesCount, categoryIndex * seriesCount);
204
+ const last = chartSlotBounds(width, categoryCount * seriesCount, categoryIndex * seriesCount + seriesCount - 1);
205
+ const right = last.x + last.width;
206
+ return {
207
+ left: first.x,
208
+ width: Math.max(1, right - first.x),
209
+ };
210
+ }
211
+
180
212
  function labelAtRow(labels: Array<{ row: number; value: number }>, row: number): number | null {
181
213
  const match = labels.find((label) => label.row === row);
182
214
  return match ? match.value : null;
@@ -227,6 +259,10 @@ export function resolveBarChartHover(scene: BarChartScene, cellX: number): BarCh
227
259
  return Math.abs(currentCenter - x) < Math.abs(closestCenter - x) ? current : closest;
228
260
  }, scene.bars[0]!);
229
261
 
262
+ return barToHover(bar);
263
+ }
264
+
265
+ function barToHover(bar: BarChartBar): BarChartHover {
230
266
  return {
231
267
  seriesId: bar.seriesId,
232
268
  seriesLabel: bar.seriesLabel,
@@ -239,6 +275,29 @@ export function resolveBarChartHover(scene: BarChartScene, cellX: number): BarCh
239
275
  };
240
276
  }
241
277
 
278
+ export function resolveNativeBarChartHover(
279
+ scene: BarChartScene,
280
+ pixelX: number,
281
+ pixelWidth: number,
282
+ ): BarChartHover | null {
283
+ if (!Number.isFinite(pixelX) || scene.bars.length === 0) return null;
284
+ const width = Math.max(1, Math.floor(pixelWidth));
285
+ const x = clamp(Math.floor(pixelX), 0, width - 1);
286
+ const direct = scene.bars.find((bar) => {
287
+ const bounds = resolveNativeBarPixelBounds(scene, bar, width);
288
+ return x >= bounds.left && x < bounds.rightExclusive;
289
+ });
290
+ const bar = direct ?? scene.bars.reduce((closest, current) => {
291
+ const closestBounds = resolveNativeBarPixelBounds(scene, closest, width);
292
+ const currentBounds = resolveNativeBarPixelBounds(scene, current, width);
293
+ const closestCenter = (closestBounds.left + closestBounds.rightExclusive - 1) / 2;
294
+ const currentCenter = (currentBounds.left + currentBounds.rightExclusive - 1) / 2;
295
+ return Math.abs(currentCenter - x) < Math.abs(closestCenter - x) ? current : closest;
296
+ }, scene.bars[0]!);
297
+
298
+ return barToHover(bar);
299
+ }
300
+
242
301
  export function renderBarChart(scene: BarChartScene, hover: BarChartHover | null = null): string[] {
243
302
  const rows = Array.from({ length: scene.height }, () => Array(scene.width).fill(" "));
244
303
  for (let x = 0; x < scene.width; x++) {
@@ -288,7 +347,7 @@ export function renderBarChartAxis(scene: BarChartScene, rowCount = 1): string[]
288
347
  scene.categories.forEach((category, index) => {
289
348
  const label = compactCategoryLabel(category, scene.categories[index - 1], scene.categories.length);
290
349
  if (!label) return;
291
- const group = categorySlotBounds(scene.width, scene.categories.length, index);
350
+ const group = categoryCellBounds(scene.width, scene.categories.length, Math.max(scene.series.length, 1), index);
292
351
  const start = centeredLabelStart(group.left + group.width / 2, label, scene.width, rightInset);
293
352
  const row = rows.find((candidate) => canPlaceLabel(candidate, start, label, minGap));
294
353
  if (!row) return;
@@ -305,42 +364,39 @@ export function resolveNativeBarPixelBounds(
305
364
  pixelWidth: number,
306
365
  ): NativeBarChartPixelBounds {
307
366
  const width = Math.max(1, Math.floor(pixelWidth));
308
-
309
- if (scene.series.length === 1) {
310
- const categoryIndex = scene.categories.indexOf(bar.category);
311
- if (categoryIndex >= 0) {
312
- const separatorWidth = scene.categories.length > 1 && width >= scene.categories.length * 3 ? 1 : 0;
313
- const usableWidth = Math.max(
314
- scene.categories.length,
315
- width - separatorWidth * (scene.categories.length - 1),
316
- );
317
- const left = Math.floor((categoryIndex * usableWidth) / scene.categories.length)
318
- + categoryIndex * separatorWidth;
319
- const rightExclusive = categoryIndex === scene.categories.length - 1
320
- ? width
321
- : Math.floor(((categoryIndex + 1) * usableWidth) / scene.categories.length)
322
- + categoryIndex * separatorWidth;
323
- return {
324
- left,
325
- rightExclusive: Math.max(left + 1, rightExclusive),
326
- };
327
- }
328
- }
329
-
330
- const horizontalInset = 1;
331
- const left = clamp(
332
- Math.floor((bar.x / scene.width) * width + horizontalInset),
333
- 0,
334
- width - 1,
335
- );
336
- const rightExclusive = clamp(
337
- Math.ceil(((bar.x + bar.width) / scene.width) * width - horizontalInset),
338
- left + 1,
367
+ const bounds = nativeSlotBounds(
339
368
  width,
369
+ scene.categories.length * bar.seriesCount,
370
+ bar.categoryIndex * bar.seriesCount + bar.seriesIndex,
340
371
  );
372
+ const left = clamp(bounds.left, 0, width - 1);
373
+ const rightExclusive = clamp(bounds.rightExclusive, left + 1, width);
341
374
  return { left, rightExclusive };
342
375
  }
343
376
 
377
+ function nativeSlotBounds(
378
+ width: number,
379
+ slotCount: number,
380
+ slotIndex: number,
381
+ ): NativeBarChartPixelBounds {
382
+ const slots = Math.max(1, slotCount);
383
+ if (slots > width) {
384
+ const left = clamp(Math.floor(slotIndex * width / slots), 0, width - 1);
385
+ const rightExclusive = clamp(Math.floor((slotIndex + 1) * width / slots), left + 1, width);
386
+ return { left, rightExclusive };
387
+ }
388
+ const gap = slots > 1 && width >= slots * 2 - 1 ? 1 : 0;
389
+ const usableWidth = Math.max(slots, width - gap * (slots - 1));
390
+ const slotWidth = Math.max(1, Math.floor(usableWidth / slots));
391
+ const usedWidth = slotWidth * slots + gap * (slots - 1);
392
+ const offset = Math.max(0, Math.floor((width - usedWidth) / 2));
393
+ const left = offset + slotIndex * (slotWidth + gap);
394
+ return {
395
+ left,
396
+ rightExclusive: Math.max(left + 1, left + slotWidth),
397
+ };
398
+ }
399
+
344
400
  export function renderNativeBarChart(
345
401
  scene: BarChartScene,
346
402
  pixelWidth: number,
@@ -22,7 +22,6 @@ import {
22
22
  getScaledComparisonY,
23
23
  getSelectedComparisonSeries,
24
24
  type ComparisonChartColors,
25
- type ComparisonChartScene,
26
25
  type RenderComparisonChartOptions,
27
26
  } from "./scene";
28
27
 
@@ -1,5 +1,4 @@
1
1
  import { useMemo } from "react";
2
- import type { DisplayCursorState } from "../core/pointer";
3
2
  import type { ComparisonChartProjection } from "../comparison/data";
4
3
  import {
5
4
  buildComparisonChartScene,
@@ -8,16 +7,14 @@ import {
8
7
  type ComparisonChartScene,
9
8
  type RenderComparisonChartResult,
10
9
  } from "../comparison/renderer";
11
- import type { ChartAxisMode, ChartMarketSession, ResolvedChartRenderer } from "../core/types";
10
+ import type { ChartMarketSession, ResolvedChartRenderer } from "../core/types";
12
11
 
13
12
  type ComparisonChartColors = Parameters<typeof buildComparisonChartScene>[1]["colors"];
14
13
 
15
14
  interface UseComparisonChartRenderOutputOptions {
16
- axisMode: ChartAxisMode;
17
15
  chartColors: ComparisonChartColors;
18
16
  chartHeight: number;
19
17
  chartWidth: number;
20
- displayCursor: DisplayCursorState;
21
18
  displayCursorX: number | null;
22
19
  displayCursorY: number | null;
23
20
  effectiveRenderer: ResolvedChartRenderer;
@@ -34,22 +31,20 @@ interface UseComparisonChartRenderOutputResult {
34
31
  cursorRow: number | null;
35
32
  cursorTimeAxisColumn: number | null;
36
33
  cursorTimeAxisDate: Date | null;
37
- displayScene: ComparisonChartScene;
34
+ displayScene: ComparisonChartScene | null;
38
35
  hasDisplayCursor: boolean;
39
36
  legendActiveIndex: number | null;
40
37
  result: RenderComparisonChartResult;
41
38
  staticResult: RenderComparisonChartResult;
42
- staticScene: ComparisonChartScene;
39
+ staticScene: ComparisonChartScene | null;
43
40
  timeAxisLabel: string;
44
41
  visiblePriceRange: number | undefined;
45
42
  }
46
43
 
47
44
  export function useComparisonChartRenderOutput({
48
- axisMode,
49
45
  chartColors,
50
46
  chartHeight,
51
47
  chartWidth,
52
- displayCursor,
53
48
  displayCursorX,
54
49
  displayCursorY,
55
50
  effectiveRenderer,
@@ -402,11 +402,9 @@ function ComparisonStockChartView({
402
402
  staticScene,
403
403
  timeAxisLabel,
404
404
  } = useComparisonChartRenderOutput({
405
- axisMode,
406
405
  chartColors,
407
406
  chartHeight,
408
407
  chartWidth,
409
- displayCursor,
410
408
  displayCursorX,
411
409
  displayCursorY,
412
410
  effectiveRenderer,
@@ -918,30 +918,6 @@ describe("renderChart", () => {
918
918
  }
919
919
  });
920
920
 
921
- test("keeps the default area renderer stable", () => {
922
- const projection = projectChartData(chartFixture, 12, undefined, false);
923
- const result = renderChart(projection.points, {
924
- width: 12,
925
- height: 5,
926
- showVolume: false,
927
- volumeHeight: 0,
928
- cursorX: null,
929
- cursorY: null,
930
- mode: projection.effectiveMode,
931
- colors: palette,
932
- });
933
-
934
- expect(projection.requestedMode).toBe("area");
935
- expect(projection.effectiveMode).toBe("area");
936
- expect(textLines(result)).toEqual([
937
- "⠁ ⡠⠔⢣ ⠁ ⠁ ",
938
- "⠔⠊⣿⣿⠈⢆⠄ ⠄ ",
939
- "⣿⣿⣿⣿⣿⠘⡄ ⡠",
940
- "⣿⣿⣿⣿⣿⣿⠱⡀ ⢠⠊⣿",
941
- "⣿⣿⣿⣿⣿⣿⣿⢣⠔⠁⣿⣿",
942
- ]);
943
- });
944
-
945
921
  test("formats y-axis labels as percent change when requested", () => {
946
922
  const projection = projectChartData(chartFixture, 12, "line", false);
947
923
  const result = renderChart(projection.points, {
@@ -2,20 +2,16 @@ import type { PixelBuffer } from "./types";
2
2
  import type { ProjectedChartPoint } from "./data";
3
3
  import { buildTimeAxis } from "./time-axis";
4
4
  import {
5
- formatAxisCell,
6
5
  formatAxisValue,
7
- formatCursorAxisValue,
8
6
  formatPriceWithCurrency,
9
7
  getAxisFractionDigitFloor,
10
8
  resolveAxisFractionDigits,
11
- resolveChartAxisWidth,
12
9
  } from "./axis-format";
13
10
  import {
14
11
  bufferToBrailleLines,
15
12
  createPixelBuffer,
16
13
  drawCrosshair,
17
14
  drawGridLines,
18
- drawLine,
19
15
  type StyledContent,
20
16
  } from "../rendering/pixel-buffer";
21
17
  import {
@@ -1,8 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  CHART_RESOLUTION_STEP_MS,
4
- clampTimeRangeForResolution,
5
- getActiveRangePreset,
6
4
  getBestSupportedResolutionForVisibleWindow,
7
5
  getExpandedBufferRange,
8
6
  getPresetResolution,
@@ -22,26 +20,11 @@ describe("chart-resolution", () => {
22
20
  expect(getPresetResolution("ALL")).toBe("1mo");
23
21
  });
24
22
 
25
- test("derives the active range preset only for the exact reset preset pair", () => {
26
- expect(getActiveRangePreset("1Y", "1d", 1, 0)).toBe("1Y");
27
- expect(getActiveRangePreset("1Y", "auto", 1, 0)).toBeNull();
28
- expect(getActiveRangePreset("1Y", "1d", 1.2, 0)).toBeNull();
29
- expect(getActiveRangePreset("1Y", "1d", 1, 2)).toBeNull();
30
- });
31
-
32
23
  test("checks whether a range preset is supported by the visible capability set", () => {
33
24
  expect(isRangePresetSupported("1Y", ["1d", "1wk"])).toBe(true);
34
25
  expect(isRangePresetSupported("ALL", ["1d", "1wk"])).toBe(false);
35
26
  });
36
27
 
37
- test("clamps overly-wide ranges for manual intraday resolutions", () => {
38
- expect(clampTimeRangeForResolution("5Y", "5m")).toBe("1W");
39
- expect(clampTimeRangeForResolution("6M", "1h")).toBe("3M");
40
- expect(clampTimeRangeForResolution("1M", "15m")).toBe("1M");
41
- expect(clampTimeRangeForResolution("5Y", "1d")).toBe("5Y");
42
- expect(clampTimeRangeForResolution("5Y", "auto")).toBe("5Y");
43
- });
44
-
45
28
  test("expands buffers only toward wider supported ranges", () => {
46
29
  const support = new Map([
47
30
  ["1h", "3M"],
@@ -239,22 +239,6 @@ export function clampTimeRangeToMaxRange(range: TimeRange, maxRange: TimeRange):
239
239
  return isTimeRangeAtOrBelow(range, maxRange) ? range : maxRange;
240
240
  }
241
241
 
242
- export function clampTimeRangeForResolution(range: TimeRange, resolution: ChartResolution): TimeRange {
243
- if (resolution === "auto") return range;
244
- const maxRange = getSupportMaxRange(
245
- normalizeChartResolutionSupport([{ resolution, maxRange: "ALL" }]),
246
- resolution,
247
- ) ?? "ALL";
248
- const intrinsicMaxRange = resolution === "1m" || resolution === "5m"
249
- ? "1W"
250
- : resolution === "15m"
251
- ? "1M"
252
- : resolution === "30m" || resolution === "45m" || resolution === "1h"
253
- ? "3M"
254
- : "ALL";
255
- return clampTimeRangeToMaxRange(range, minTimeRange(maxRange, intrinsicMaxRange));
256
- }
257
-
258
242
  export function isIntradayResolution(resolution: ManualChartResolution): boolean {
259
243
  return resolution === "1m"
260
244
  || resolution === "5m"
@@ -277,16 +261,6 @@ export function isRangePresetSupported(
277
261
  return maxRange !== null && isTimeRangeAtOrBelow(range, maxRange);
278
262
  }
279
263
 
280
- export function getActiveRangePreset(
281
- range: TimeRange,
282
- resolution: ChartResolution,
283
- zoomLevel: number,
284
- panOffset: number,
285
- ): TimeRange | null {
286
- if (zoomLevel !== 1 || panOffset !== 0) return null;
287
- return RANGE_PRESET_RESOLUTION[range] === resolution ? range : null;
288
- }
289
-
290
264
  export function getWidestPresetForResolution(
291
265
  resolution: ManualChartResolution,
292
266
  maxRange: TimeRange,
@@ -3,10 +3,8 @@ import {
3
3
  applyBufferedPanExpansion,
4
4
  consumeScrollPanCellDelta,
5
5
  consumeScrollPanMovement,
6
- getDragPanPointDelta,
7
6
  getDragPanWindowRatio,
8
7
  getKeyboardPanCellCount,
9
- resolveDragPanOffset,
10
8
  resolveHorizontalScrollPanDirection,
11
9
  } from "./scroll";
12
10
 
@@ -34,11 +32,7 @@ describe("chart-scroll", () => {
34
32
 
35
33
  test("keeps keyboard pan small while matching drag to the visible window", () => {
36
34
  expect(getKeyboardPanCellCount(100)).toBe(2);
37
- expect(getDragPanPointDelta(10, 100, 200)).toBe(20);
38
35
  expect(getDragPanWindowRatio(10, 100)).toBeCloseTo(0.1);
39
- expect(resolveDragPanOffset(20, 10, 100, 200, 40)).toBe(40);
40
- expect(resolveDragPanOffset(20, -10, 100, 200, 40)).toBe(0);
41
- expect(resolveDragPanOffset(38, 10, 100, 200, 40)).toBe(40);
42
36
  });
43
37
 
44
38
  test("resolves wheel direction, cell delta, remainder, and ratio together", () => {
@@ -21,36 +21,14 @@ export interface ScrollPanMovement extends ScrollPanCellDelta {
21
21
  ratio: number;
22
22
  }
23
23
 
24
- function clamp(value: number, min: number, max: number): number {
25
- return Math.max(min, Math.min(max, value));
26
- }
27
-
28
24
  export function getKeyboardPanCellCount(chartWidth: number): number {
29
25
  return Math.max(Math.round(chartWidth * KEYBOARD_PAN_WIDTH_RATIO), 1);
30
26
  }
31
27
 
32
- export function getDragPanPointDelta(deltaCells: number, chartWidth: number, visibleCount: number): number {
33
- return Math.round((deltaCells / Math.max(chartWidth, 1)) * Math.max(visibleCount, 1) * DRAG_PAN_VISIBLE_RATIO);
34
- }
35
-
36
28
  export function getDragPanWindowRatio(deltaCells: number, chartWidth: number): number {
37
29
  return (deltaCells / Math.max(chartWidth, 1)) * DRAG_PAN_VISIBLE_RATIO;
38
30
  }
39
31
 
40
- export function resolveDragPanOffset(
41
- startPanOffset: number,
42
- deltaCells: number,
43
- chartWidth: number,
44
- visibleCount: number,
45
- maxPanOffset: number,
46
- ): number {
47
- return clamp(
48
- startPanOffset + getDragPanPointDelta(deltaCells, chartWidth, visibleCount),
49
- 0,
50
- Math.max(maxPanOffset, 0),
51
- );
52
- }
53
-
54
32
  export function consumeScrollPanCellDelta(
55
33
  chartWidth: number,
56
34
  delta: number | undefined,
@@ -1,28 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { getVisiblePointCount, RIGHT_EDGE_ANCHOR_RATIO, resolveAnchoredChartZoom } from "./viewport";
3
-
4
- describe("resolveAnchoredChartZoom", () => {
5
- test("keeps keyboard zoom pinned to the latest data when anchored to the right edge", () => {
6
- expect(resolveAnchoredChartZoom(252, 1, 0, 2, RIGHT_EDGE_ANCHOR_RATIO)).toEqual({
7
- zoomLevel: 2,
8
- panOffset: 0,
9
- });
10
- });
11
-
12
- test("preserves the current right edge when zooming a panned view", () => {
13
- expect(resolveAnchoredChartZoom(252, 2, 20, 3, RIGHT_EDGE_ANCHOR_RATIO)).toEqual({
14
- zoomLevel: 3,
15
- panOffset: 20,
16
- });
17
- });
18
-
19
- test("still supports center-anchored zoom for pointer-driven interactions", () => {
20
- expect(resolveAnchoredChartZoom(252, 1, 0, 2, 0.5)).toEqual({
21
- zoomLevel: 2,
22
- panOffset: 63,
23
- });
24
- });
2
+ import { getVisiblePointCount } from "./viewport";
25
3
 
4
+ describe("chart viewport", () => {
26
5
  test("allows zooming down to a two-point viewport", () => {
27
6
  expect(getVisiblePointCount(433, 1000)).toBe(2);
28
7
  });
@@ -22,30 +22,3 @@ export function getVisiblePointCount(totalPoints: number, zoomLevel: number): nu
22
22
  const clampedZoom = clampChartZoom(totalPoints, zoomLevel);
23
23
  return Math.min(totalPoints, Math.max(Math.floor(totalPoints / clampedZoom), MIN_VISIBLE_POINTS));
24
24
  }
25
-
26
- export function resolveAnchoredChartZoom(
27
- totalPoints: number,
28
- currentZoomLevel: number,
29
- currentPanOffset: number,
30
- nextZoomLevel: number,
31
- anchorRatio: number,
32
- ): { zoomLevel: number; panOffset: number } {
33
- if (totalPoints <= 0) {
34
- return { zoomLevel: 1, panOffset: 0 };
35
- }
36
-
37
- const clampedZoom = clampChartZoom(totalPoints, nextZoomLevel);
38
- const currentVisibleCount = getVisiblePointCount(totalPoints, currentZoomLevel);
39
- const nextVisibleCount = getVisiblePointCount(totalPoints, clampedZoom);
40
- const currentPan = clamp(currentPanOffset, 0, Math.max(totalPoints - currentVisibleCount, 0));
41
- const ratio = clamp(anchorRatio, 0, 1);
42
- const anchorIndex = totalPoints - currentPan - currentVisibleCount + ratio * Math.max(currentVisibleCount - 1, 0);
43
- const nextStart = Math.round(anchorIndex - ratio * Math.max(nextVisibleCount - 1, 0));
44
- const clampedStart = clamp(nextStart, 0, Math.max(totalPoints - nextVisibleCount, 0));
45
- const nextPanOffset = totalPoints - nextVisibleCount - clampedStart;
46
-
47
- return {
48
- zoomLevel: clampedZoom,
49
- panOffset: clamp(nextPanOffset, 0, Math.max(totalPoints - nextVisibleCount, 0)),
50
- };
51
- }
@@ -148,7 +148,7 @@ export function getChartMarketSessionKey(session: ChartMarketSession | null | un
148
148
  }
149
149
 
150
150
  export function resolveExtendedHoursBackgroundSpans(
151
- values: readonly Array<Date | string | number>,
151
+ values: readonly (Date | string | number)[],
152
152
  session: ChartMarketSession | null | undefined,
153
153
  ): ChartSessionBackgroundSpan[] {
154
154
  if (!session) return [];
@@ -71,8 +71,9 @@ export function useNativeChartSurfaceLifecycle({
71
71
 
72
72
  useEffect(() => {
73
73
  if (effectiveRenderer !== "kitty" || !nativeReady || !plotRef.current) return;
74
- const plot = plotRef.current;
74
+ const plot = plotRef.current as BoxRenderable & { onLifecyclePass?: (() => void) | null };
75
75
  let mountTimer: ReturnType<typeof setTimeout> | null = null;
76
+ const previousLifecyclePass = plot.onLifecyclePass;
76
77
 
77
78
  const syncPlacement = () => {
78
79
  if (effectiveRenderer !== "kitty" || !nativeReady || !plotRef.current) return;
@@ -108,7 +109,11 @@ export function useNativeChartSurfaceLifecycle({
108
109
  );
109
110
  };
110
111
 
111
- plot.onLifecyclePass = syncPlacement;
112
+ const lifecyclePass = () => {
113
+ previousLifecyclePass?.();
114
+ syncPlacement();
115
+ };
116
+ plot.onLifecyclePass = lifecyclePass;
112
117
  renderer.registerLifecyclePass(plot);
113
118
  syncPlacement();
114
119
  mountTimer = setTimeout(() => {
@@ -118,7 +123,9 @@ export function useNativeChartSurfaceLifecycle({
118
123
 
119
124
  return () => {
120
125
  if (mountTimer) clearTimeout(mountTimer);
121
- plot.onLifecyclePass = null;
126
+ if (plot.onLifecyclePass === lifecyclePass) {
127
+ plot.onLifecyclePass = previousLifecyclePass;
128
+ }
122
129
  renderer.unregisterLifecyclePass(plot);
123
130
  lastGeometryRef.current = null;
124
131
  };