gloomberb 0.9.4 → 0.9.6

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 (138) hide show
  1. package/README.md +49 -8
  2. package/package.json +4 -3
  3. package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
  4. package/patches/react-reconciler@0.33.0.patch +10 -0
  5. package/src/app/global-shortcuts.test.tsx +20 -1
  6. package/src/app/global-shortcuts.ts +2 -0
  7. package/src/app/pane-runtime/index.tsx +11 -4
  8. package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -1
  9. package/src/app.tsx +41 -17
  10. package/src/cli/commands/remote.ts +206 -0
  11. package/src/cli/entry.ts +8 -1
  12. package/src/cli/index.ts +2 -0
  13. package/src/cli/native-smoke.ts +5 -0
  14. package/src/components/chart/core/keyboard.ts +10 -1
  15. package/src/components/chart/native/bitmap-support.test.ts +27 -0
  16. package/src/components/chart/native/bitmap-support.ts +55 -0
  17. package/src/components/chart/native/kitty/index.test.ts +47 -0
  18. package/src/components/chart/native/surface/visibility.ts +8 -1
  19. package/src/components/chart/static/bar-chart-surface.test.tsx +4 -0
  20. package/src/components/chart/static/chart/bitmap.ts +16 -9
  21. package/src/components/chart/static/chart/surface.test.tsx +59 -4
  22. package/src/components/chart/static/chart/surface.tsx +4 -0
  23. package/src/components/chart/stock-chart/index.test.ts +7 -0
  24. package/src/components/command-bar/list/view.tsx +48 -1
  25. package/src/components/command-bar/multi-select-picker.tsx +7 -0
  26. package/src/components/command-bar/routing/navigation-state.ts +10 -1
  27. package/src/components/command-bar/surface/index.test.tsx +14 -0
  28. package/src/components/command-bar/theme-picker.tsx +91 -79
  29. package/src/components/command-bar/workflow/field-row.tsx +30 -5
  30. package/src/components/data-table/view.tsx +0 -12
  31. package/src/components/index.ts +7 -0
  32. package/src/components/layout/floating-pane.tsx +3 -3
  33. package/src/components/layout/pane/footer/index.tsx +23 -0
  34. package/src/components/layout/pane/header.tsx +5 -0
  35. package/src/components/layout/pane/index.tsx +3 -3
  36. package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
  37. package/src/components/layout/shell/cursor-occlusion.ts +9 -1
  38. package/src/components/layout/shell/index.test.tsx +2 -0
  39. package/src/components/layout/shell/index.tsx +16 -4
  40. package/src/components/layout/shell/pane/actions.ts +21 -7
  41. package/src/components/layout/shell/pane/layers.tsx +2 -2
  42. package/src/components/layout/status-bar.tsx +5 -5
  43. package/src/components/metric-treemap/index.tsx +317 -0
  44. package/src/components/metric-treemap/layout.test.ts +77 -0
  45. package/src/components/metric-treemap/layout.ts +476 -0
  46. package/src/components/pane-settings-dialog/desktop.tsx +1 -1
  47. package/src/components/price-performance.tsx +1 -1
  48. package/src/components/price-sparkline/view.tsx +15 -14
  49. package/src/components/speedometer-gauge/terminal.tsx +18 -15
  50. package/src/components/table-view-shared.tsx +0 -10
  51. package/src/components/ticker/list-table-view.tsx +1 -7
  52. package/src/components/ticker/list-table.test.tsx +0 -4
  53. package/src/components/toggle-list.tsx +10 -0
  54. package/src/components/ui/button.tsx +12 -0
  55. package/src/components/ui/data-table/index.tsx +51 -0
  56. package/src/components/ui/data-table/opentui/index.tsx +4 -10
  57. package/src/components/ui/data-table/types.ts +6 -4
  58. package/src/components/ui/fields.tsx +18 -0
  59. package/src/components/ui/list-view.tsx +84 -4
  60. package/src/components/ui/message-composer.tsx +22 -0
  61. package/src/components/ui/tabs.tsx +46 -2
  62. package/src/components/ui/ui.test.tsx +0 -9
  63. package/src/core/state/app/layout-reducer.ts +7 -1
  64. package/src/core/state/app/layout.ts +6 -0
  65. package/src/core/state/app/state.ts +23 -1
  66. package/src/core/state/app/types.ts +4 -2
  67. package/src/plugins/builtin/account-management/form-components.tsx +2 -2
  68. package/src/plugins/builtin/alerts/pane.tsx +1 -1
  69. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  70. package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
  71. package/src/plugins/builtin/chat/channels.ts +5 -18
  72. package/src/plugins/builtin/chat/chat.test.tsx +1 -7
  73. package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
  74. package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
  75. package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
  76. package/src/plugins/builtin/chat/sidebar.tsx +1 -1
  77. package/src/plugins/builtin/chat/status-widget.tsx +2 -8
  78. package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
  79. package/src/plugins/builtin/cloud/plugin.tsx +4 -13
  80. package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
  81. package/src/plugins/builtin/debug/index.tsx +3 -4
  82. package/src/plugins/builtin/help/components.tsx +1 -1
  83. package/src/plugins/builtin/holders/treemap.tsx +28 -297
  84. package/src/plugins/builtin/holders/types.ts +0 -34
  85. package/src/plugins/builtin/market-heatmap/data.test.ts +121 -0
  86. package/src/plugins/builtin/market-heatmap/data.ts +296 -0
  87. package/src/plugins/builtin/market-heatmap/index.tsx +320 -0
  88. package/src/plugins/builtin/options/table.ts +5 -5
  89. package/src/plugins/builtin/plugin-groups.ts +2 -0
  90. package/src/plugins/builtin/portfolio-list/grid.tsx +197 -0
  91. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +41 -0
  92. package/src/plugins/builtin/portfolio-list/pane/index.tsx +78 -22
  93. package/src/plugins/ibkr/gateway/service/index.test.ts +22 -0
  94. package/src/plugins/ibkr/gateway/service/index.ts +6 -9
  95. package/src/plugins/ibkr/gateway/service/native-loader.ts +12 -0
  96. package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
  97. package/src/remote/app-host.tsx +77 -0
  98. package/src/remote/client.ts +34 -0
  99. package/src/remote/command-bar.ts +98 -0
  100. package/src/remote/controller-utils.ts +111 -0
  101. package/src/remote/controller.test.ts +365 -0
  102. package/src/remote/controller.ts +434 -0
  103. package/src/remote/endpoint.ts +41 -0
  104. package/src/remote/json-patch.test.ts +31 -0
  105. package/src/remote/json-patch.ts +89 -0
  106. package/src/remote/layout-helpers.ts +62 -0
  107. package/src/remote/resources.ts +186 -0
  108. package/src/remote/revision.ts +18 -0
  109. package/src/remote/schema.ts +143 -0
  110. package/src/remote/semantic-helpers.ts +161 -0
  111. package/src/remote/semantic-tree.tsx +112 -0
  112. package/src/remote/server.test.ts +50 -0
  113. package/src/remote/server.ts +121 -0
  114. package/src/remote/types.ts +119 -0
  115. package/src/renderers/electrobun/bun/index.ts +55 -1
  116. package/src/renderers/electrobun/shared/protocol.ts +11 -1
  117. package/src/renderers/electrobun/view/backend-rpc.ts +28 -1
  118. package/src/renderers/electrobun/view/build-assets.ts +1 -0
  119. package/src/renderers/electrobun/view/data-table/index.tsx +0 -8
  120. package/src/renderers/electrobun/view/data-table/row.tsx +2 -12
  121. package/src/renderers/electrobun/view/host/box.tsx +11 -1
  122. package/src/renderers/electrobun/view/host/mouse.test.ts +37 -0
  123. package/src/renderers/electrobun/view/host/mouse.ts +2 -0
  124. package/src/renderers/electrobun/view/host/style.ts +1 -0
  125. package/src/renderers/electrobun/view/main.tsx +5 -1
  126. package/src/renderers/electrobun/view/native-stubs/ibkr-native-loader.ts +3 -0
  127. package/src/renderers/electrobun/view/styles.css +7 -0
  128. package/src/renderers/opentui/chart-surface.test.tsx +101 -0
  129. package/src/renderers/opentui/chart-surface.tsx +15 -20
  130. package/src/renderers/opentui/native-smoke.ts +1 -0
  131. package/src/renderers/opentui/start.tsx +32 -0
  132. package/src/sources/yahoo-finance/http.ts +31 -4
  133. package/src/state/app/context/index.test.tsx +47 -0
  134. package/src/state/app/context/index.tsx +48 -18
  135. package/src/ui/host.tsx +1 -0
  136. package/src/ui/index.tsx +183 -7
  137. package/src/version.ts +1 -1
  138. package/src/plugins/builtin/holders/treemap-layout.ts +0 -231
@@ -0,0 +1,77 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ buildMetricTreemap,
4
+ buildMetricTreemapRects,
5
+ findMetricTreemapNeighbor,
6
+ type MetricTreemapItem,
7
+ type MetricTreemapTile,
8
+ } from "./layout";
9
+
10
+ const ITEMS: Array<MetricTreemapItem<string>> = [
11
+ { id: "a", label: "A", weight: 3, data: "A" },
12
+ { id: "b", label: "B", weight: 1, data: "B" },
13
+ { id: "zero", label: "Zero", weight: 0, data: "Zero" },
14
+ ];
15
+
16
+ describe("metric treemap layout", () => {
17
+ test("lays out positive weighted items inside integer bounds", () => {
18
+ const tiles = buildMetricTreemap(ITEMS, 12, 6);
19
+
20
+ expect(tiles.map((tile) => tile.item.id)).toEqual(["a", "b"]);
21
+ for (const tile of tiles) {
22
+ expect(tile.x).toBeGreaterThanOrEqual(0);
23
+ expect(tile.y).toBeGreaterThanOrEqual(0);
24
+ expect(tile.width).toBeGreaterThan(0);
25
+ expect(tile.height).toBeGreaterThan(0);
26
+ expect(tile.x + tile.width).toBeLessThanOrEqual(12);
27
+ expect(tile.y + tile.height).toBeLessThanOrEqual(6);
28
+ }
29
+ });
30
+
31
+ test("produces proportional floating rects for native rendering", () => {
32
+ const tiles = buildMetricTreemapRects(ITEMS, 100, 40);
33
+ const first = tiles[0];
34
+ const second = tiles[1];
35
+
36
+ expect(first?.item.id).toBe("a");
37
+ expect(second?.item.id).toBe("b");
38
+ expect((first?.width ?? 0) * (first?.height ?? 0)).toBeGreaterThan((second?.width ?? 0) * (second?.height ?? 0));
39
+ });
40
+
41
+ test("drops low-ranked float tiles before they render as slivers", () => {
42
+ const denseItems = Array.from({ length: 96 }, (_, index): MetricTreemapItem<number> => ({
43
+ id: `item-${index}`,
44
+ label: `${index}`,
45
+ weight: 100 - index,
46
+ data: index,
47
+ }));
48
+
49
+ const tiles = buildMetricTreemapRects(denseItems, 52, 32, 18 / 8);
50
+
51
+ expect(tiles.length).toBeLessThan(denseItems.length);
52
+ expect(tiles.every((tile) => tile.width >= 3 && tile.height >= 1.8)).toBe(true);
53
+ });
54
+
55
+ test("finds neighbors by rendered geometry instead of source order", () => {
56
+ const tiles: Array<MetricTreemapTile<string>> = [
57
+ { item: { id: "a", label: "A", weight: 1, data: "a" }, x: 0, y: 0, width: 5, height: 5 },
58
+ { item: { id: "below", label: "Below", weight: 1, data: "below" }, x: 0, y: 5, width: 5, height: 5 },
59
+ { item: { id: "right", label: "Right", weight: 1, data: "right" }, x: 5, y: 0, width: 5, height: 5 },
60
+ ];
61
+
62
+ expect(findMetricTreemapNeighbor(tiles, "a", "right")?.item.id).toBe("right");
63
+ expect(findMetricTreemapNeighbor(tiles, "a", "down")?.item.id).toBe("below");
64
+ expect(findMetricTreemapNeighbor(tiles, "right", "left")?.item.id).toBe("a");
65
+ });
66
+
67
+ test("does not choose an overlapping lower tile for right navigation", () => {
68
+ const tiles: Array<MetricTreemapTile<string>> = [
69
+ { item: { id: "current", label: "Current", weight: 1, data: "current" }, x: 33, y: 0, width: 26, height: 10 },
70
+ { item: { id: "lower", label: "Lower", weight: 1, data: "lower" }, x: 49, y: 10, width: 12, height: 6 },
71
+ { item: { id: "right", label: "Right", weight: 1, data: "right" }, x: 59, y: 0, width: 24, height: 10 },
72
+ ];
73
+
74
+ expect(findMetricTreemapNeighbor(tiles, "current", "right")?.item.id).toBe("right");
75
+ expect(findMetricTreemapNeighbor(tiles, "current", "down")?.item.id).toBe("lower");
76
+ });
77
+ });
@@ -0,0 +1,476 @@
1
+ export interface MetricTreemapItem<T = unknown> {
2
+ id: string;
3
+ label: string;
4
+ weight: number | null | undefined;
5
+ colorValue?: number | null;
6
+ primaryText?: string | null;
7
+ secondaryText?: string | null;
8
+ tertiaryText?: string | null;
9
+ data: T;
10
+ }
11
+
12
+ export interface MetricTreemapTile<T = unknown> {
13
+ item: MetricTreemapItem<T>;
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ }
19
+
20
+ export interface FloatMetricTreemapTile<T = unknown> {
21
+ item: MetricTreemapItem<T>;
22
+ x: number;
23
+ y: number;
24
+ width: number;
25
+ height: number;
26
+ }
27
+
28
+ export type MetricTreemapDirection = "left" | "right" | "up" | "down";
29
+ export type MetricTreemapLayoutMode = "integer" | "float";
30
+
31
+ export interface MetricTreemapLayoutOptions {
32
+ maxItems?: number;
33
+ minTileWidth?: number;
34
+ minTileHeight?: number;
35
+ }
36
+
37
+ interface WeightedTreemapItem<T> {
38
+ item: MetricTreemapItem<T>;
39
+ weight: number;
40
+ area: number;
41
+ }
42
+
43
+ interface TreemapGroup<T> {
44
+ items: WeightedTreemapItem<T>[];
45
+ weight: number;
46
+ }
47
+
48
+ interface FloatRect {
49
+ x: number;
50
+ y: number;
51
+ width: number;
52
+ height: number;
53
+ }
54
+
55
+ const MAX_TREEMAP_ITEMS = 160;
56
+ const DEFAULT_MIN_TILE_WIDTH = 3;
57
+ const DEFAULT_MIN_TILE_HEIGHT = 1.8;
58
+
59
+ function itemWeight(item: MetricTreemapItem): number {
60
+ return Math.max(item.weight ?? 0, 0);
61
+ }
62
+
63
+ function worstAspectRatio<T>(items: WeightedTreemapItem<T>[], sideLength: number): number {
64
+ if (items.length === 0 || sideLength <= 0) return Number.POSITIVE_INFINITY;
65
+ const areaSum = items.reduce((sum, item) => sum + item.area, 0);
66
+ const minArea = Math.min(...items.map((item) => item.area));
67
+ const maxArea = Math.max(...items.map((item) => item.area));
68
+ if (areaSum <= 0 || minArea <= 0) return Number.POSITIVE_INFINITY;
69
+
70
+ const sideSquared = sideLength * sideLength;
71
+ return Math.max(
72
+ (sideSquared * maxArea) / (areaSum * areaSum),
73
+ (areaSum * areaSum) / (sideSquared * minArea),
74
+ );
75
+ }
76
+
77
+ function layoutFloatGroup<T>(items: WeightedTreemapItem<T>[], rect: FloatRect): FloatRect {
78
+ const areaSum = items.reduce((sum, item) => sum + item.area, 0);
79
+ if (areaSum <= 0 || rect.width <= 0 || rect.height <= 0) return rect;
80
+
81
+ if (rect.width >= rect.height) {
82
+ const columnWidth = Math.min(rect.width, areaSum / rect.height);
83
+ return {
84
+ x: rect.x + columnWidth,
85
+ y: rect.y,
86
+ width: Math.max(0, rect.width - columnWidth),
87
+ height: rect.height,
88
+ };
89
+ }
90
+
91
+ const rowHeight = Math.min(rect.height, areaSum / rect.width);
92
+ return {
93
+ x: rect.x,
94
+ y: rect.y + rowHeight,
95
+ width: rect.width,
96
+ height: Math.max(0, rect.height - rowHeight),
97
+ };
98
+ }
99
+
100
+ function buildSquarifiedGroups<T>(items: WeightedTreemapItem<T>[], width: number, height: number): TreemapGroup<T>[] {
101
+ const groups: TreemapGroup<T>[] = [];
102
+ let rect: FloatRect = { x: 0, y: 0, width, height };
103
+ let index = 0;
104
+
105
+ while (index < items.length && rect.width > 0 && rect.height > 0) {
106
+ const group: WeightedTreemapItem<T>[] = [];
107
+ let currentWorst = Number.POSITIVE_INFINITY;
108
+ const sideLength = Math.min(rect.width, rect.height);
109
+
110
+ while (index < items.length) {
111
+ const candidate = items[index]!;
112
+ const nextGroup = [...group, candidate];
113
+ const nextWorst = worstAspectRatio(nextGroup, sideLength);
114
+ if (group.length > 0 && nextWorst > currentWorst) break;
115
+ group.push(candidate);
116
+ currentWorst = nextWorst;
117
+ index += 1;
118
+ }
119
+
120
+ groups.push({
121
+ items: group,
122
+ weight: group.reduce((sum, item) => sum + item.weight, 0),
123
+ });
124
+ rect = layoutFloatGroup(group, rect);
125
+ }
126
+
127
+ return groups;
128
+ }
129
+
130
+ function normalizeWeightedItems<T>(
131
+ items: MetricTreemapItem<T>[],
132
+ width: number,
133
+ height: number,
134
+ cellAspect: number,
135
+ maxItems: number,
136
+ ): WeightedTreemapItem<T>[] {
137
+ const weightedItems = items
138
+ .map((item) => ({ item, weight: itemWeight(item) }))
139
+ .filter((item) => item.weight > 0)
140
+ .slice(0, maxItems);
141
+ const totalWeight = weightedItems.reduce((sum, item) => sum + item.weight, 0);
142
+ if (totalWeight <= 0) return [];
143
+
144
+ const normalizedHeight = height * cellAspect;
145
+ const totalArea = width * normalizedHeight;
146
+ return weightedItems.map((item) => ({
147
+ ...item,
148
+ area: item.weight / totalWeight * totalArea,
149
+ }));
150
+ }
151
+
152
+ function maxItemsForLayout(width: number, height: number, options?: MetricTreemapLayoutOptions): number {
153
+ const requested = options?.maxItems ?? width * height;
154
+ return Math.max(1, Math.min(MAX_TREEMAP_ITEMS, Math.floor(requested)));
155
+ }
156
+
157
+ function isTooSmall(
158
+ tile: { width: number; height: number },
159
+ options: MetricTreemapLayoutOptions | undefined,
160
+ defaultMinTileWidth: number,
161
+ defaultMinTileHeight: number,
162
+ ): boolean {
163
+ const minTileWidth = options?.minTileWidth ?? defaultMinTileWidth;
164
+ const minTileHeight = options?.minTileHeight ?? defaultMinTileHeight;
165
+ return tile.width < minTileWidth || tile.height < minTileHeight;
166
+ }
167
+
168
+ function allocateLengths(totalLength: number, weights: number[]): number[] {
169
+ if (weights.length === 0 || totalLength <= 0) return [];
170
+ const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
171
+ let remainingLength = totalLength;
172
+ let remainingWeight = totalWeight;
173
+
174
+ return weights.map((weight, index) => {
175
+ const remainingItems = weights.length - index;
176
+ if (remainingItems === 1) return remainingLength;
177
+ const ideal = remainingWeight > 0 ? Math.round(remainingLength * weight / remainingWeight) : 1;
178
+ const length = Math.max(1, Math.min(remainingLength - (remainingItems - 1), ideal));
179
+ remainingLength -= length;
180
+ remainingWeight -= weight;
181
+ return length;
182
+ });
183
+ }
184
+
185
+ function buildMetricTreemapRectsForLimit<T>(
186
+ items: MetricTreemapItem<T>[],
187
+ width: number,
188
+ height: number,
189
+ cellAspect = 1,
190
+ maxItems = maxItemsForLayout(width, height),
191
+ ): FloatMetricTreemapTile<T>[] {
192
+ if (width <= 0 || height <= 0) return [];
193
+ const normalizedCellAspect = Math.max(0.25, cellAspect);
194
+ const weightedItems = normalizeWeightedItems(items, width, height, normalizedCellAspect, maxItems);
195
+ if (weightedItems.length === 0) return [];
196
+
197
+ const normalizedHeight = height * normalizedCellAspect;
198
+ const groups = buildSquarifiedGroups(weightedItems, width, normalizedHeight);
199
+ const tiles: FloatMetricTreemapTile<T>[] = [];
200
+ let rect: FloatRect = { x: 0, y: 0, width, height: normalizedHeight };
201
+
202
+ for (const group of groups) {
203
+ const areaSum = group.items.reduce((sum, item) => sum + item.area, 0);
204
+ if (areaSum <= 0 || rect.width <= 0 || rect.height <= 0) break;
205
+
206
+ if (rect.width >= rect.height) {
207
+ const columnWidth = Math.min(rect.width, areaSum / rect.height);
208
+ let itemY = rect.y;
209
+ for (const item of group.items) {
210
+ const itemHeight = Math.min(rect.y + rect.height - itemY, item.area / columnWidth);
211
+ tiles.push({
212
+ item: item.item,
213
+ x: rect.x,
214
+ y: itemY / normalizedCellAspect,
215
+ width: columnWidth,
216
+ height: itemHeight / normalizedCellAspect,
217
+ });
218
+ itemY += itemHeight;
219
+ }
220
+ rect = {
221
+ x: rect.x + columnWidth,
222
+ y: rect.y,
223
+ width: Math.max(0, rect.width - columnWidth),
224
+ height: rect.height,
225
+ };
226
+ } else {
227
+ const rowHeight = Math.min(rect.height, areaSum / rect.width);
228
+ let itemX = rect.x;
229
+ for (const item of group.items) {
230
+ const itemWidth = Math.min(rect.x + rect.width - itemX, item.area / rowHeight);
231
+ tiles.push({
232
+ item: item.item,
233
+ x: itemX,
234
+ y: rect.y / normalizedCellAspect,
235
+ width: itemWidth,
236
+ height: rowHeight / normalizedCellAspect,
237
+ });
238
+ itemX += itemWidth;
239
+ }
240
+ rect = {
241
+ x: rect.x,
242
+ y: rect.y + rowHeight,
243
+ width: rect.width,
244
+ height: Math.max(0, rect.height - rowHeight),
245
+ };
246
+ }
247
+ }
248
+
249
+ return tiles.filter((tile) => tile.width > 0 && tile.height > 0);
250
+ }
251
+
252
+ export function buildMetricTreemapRects<T>(
253
+ items: MetricTreemapItem<T>[],
254
+ width: number,
255
+ height: number,
256
+ cellAspect = 1,
257
+ options?: MetricTreemapLayoutOptions,
258
+ ): FloatMetricTreemapTile<T>[] {
259
+ let limit = maxItemsForLayout(width, height, options);
260
+ let lastTiles: FloatMetricTreemapTile<T>[] = [];
261
+
262
+ while (limit > 0) {
263
+ const tiles = buildMetricTreemapRectsForLimit(items, width, height, cellAspect, limit);
264
+ lastTiles = tiles;
265
+ const firstSmallIndex = tiles.findIndex((tile) => isTooSmall(tile, options, DEFAULT_MIN_TILE_WIDTH, DEFAULT_MIN_TILE_HEIGHT));
266
+ if (firstSmallIndex < 0) return tiles;
267
+ limit = Math.min(limit - 1, firstSmallIndex);
268
+ }
269
+
270
+ return lastTiles.filter((tile) => !isTooSmall(tile, options, DEFAULT_MIN_TILE_WIDTH, DEFAULT_MIN_TILE_HEIGHT));
271
+ }
272
+
273
+ function buildMetricTreemapForLimit<T>(
274
+ items: MetricTreemapItem<T>[],
275
+ width: number,
276
+ height: number,
277
+ cellAspect = 1,
278
+ maxItems = maxItemsForLayout(width, height),
279
+ ): MetricTreemapTile<T>[] {
280
+ if (width <= 0 || height <= 0) return [];
281
+ const normalizedCellAspect = Math.max(0.25, cellAspect);
282
+ const weightedItems = normalizeWeightedItems(items, width, height, normalizedCellAspect, maxItems);
283
+ if (weightedItems.length === 0) return [];
284
+
285
+ const normalizedHeight = height * normalizedCellAspect;
286
+ const groups = buildSquarifiedGroups(weightedItems, width, normalizedHeight);
287
+ const tiles: MetricTreemapTile<T>[] = [];
288
+
289
+ let x = 0;
290
+ let y = 0;
291
+ let remainingWidth = width;
292
+ let remainingHeight = height;
293
+ let remainingWeight = groups.reduce((sum, group) => sum + group.weight, 0);
294
+
295
+ groups.forEach((group, groupIndex) => {
296
+ if (remainingWidth <= 0 || remainingHeight <= 0) return;
297
+ const isLastGroup = groupIndex === groups.length - 1;
298
+ const remainingNormalizedHeight = remainingHeight * normalizedCellAspect;
299
+
300
+ if (remainingWidth >= remainingNormalizedHeight) {
301
+ const columnWidth = isLastGroup
302
+ ? remainingWidth
303
+ : Math.max(1, Math.min(remainingWidth - 1, Math.round(remainingWidth * group.weight / remainingWeight)));
304
+ const heights = allocateLengths(remainingHeight, group.items.map((item) => item.weight));
305
+ let tileY = y;
306
+ group.items.forEach((item, itemIndex) => {
307
+ const tileHeight = heights[itemIndex] ?? 0;
308
+ if (tileHeight > 0) {
309
+ tiles.push({ item: item.item, x, y: tileY, width: columnWidth, height: tileHeight });
310
+ }
311
+ tileY += tileHeight;
312
+ });
313
+ x += columnWidth;
314
+ remainingWidth -= columnWidth;
315
+ } else {
316
+ const rowHeight = isLastGroup
317
+ ? remainingHeight
318
+ : Math.max(1, Math.min(remainingHeight - 1, Math.round(remainingHeight * group.weight / remainingWeight)));
319
+ const widths = allocateLengths(remainingWidth, group.items.map((item) => item.weight));
320
+ let tileX = x;
321
+ group.items.forEach((item, itemIndex) => {
322
+ const tileWidth = widths[itemIndex] ?? 0;
323
+ if (tileWidth > 0) {
324
+ tiles.push({ item: item.item, x: tileX, y, width: tileWidth, height: rowHeight });
325
+ }
326
+ tileX += tileWidth;
327
+ });
328
+ y += rowHeight;
329
+ remainingHeight -= rowHeight;
330
+ }
331
+
332
+ remainingWeight -= group.weight;
333
+ });
334
+
335
+ return tiles.filter((tile) => tile.width > 0 && tile.height > 0);
336
+ }
337
+
338
+ export function buildMetricTreemap<T>(
339
+ items: MetricTreemapItem<T>[],
340
+ width: number,
341
+ height: number,
342
+ cellAspect = 1,
343
+ options?: MetricTreemapLayoutOptions,
344
+ ): MetricTreemapTile<T>[] {
345
+ let limit = maxItemsForLayout(width, height, options);
346
+ let lastTiles: MetricTreemapTile<T>[] = [];
347
+
348
+ while (limit > 0) {
349
+ const tiles = buildMetricTreemapForLimit(items, width, height, cellAspect, limit);
350
+ lastTiles = tiles;
351
+ const firstSmallIndex = tiles.findIndex((tile) => isTooSmall(tile, options, 2, 1));
352
+ if (firstSmallIndex < 0) return tiles;
353
+ limit = Math.min(limit - 1, firstSmallIndex);
354
+ }
355
+
356
+ return lastTiles.filter((tile) => !isTooSmall(tile, options, 2, 1));
357
+ }
358
+
359
+ function center(rect: { x: number; y: number; width: number; height: number }) {
360
+ return {
361
+ x: rect.x + rect.width / 2,
362
+ y: rect.y + rect.height / 2,
363
+ };
364
+ }
365
+
366
+ function rangeGap(startA: number, endA: number, startB: number, endB: number): number {
367
+ if (endA < startB) return startB - endA;
368
+ if (endB < startA) return startA - endB;
369
+ return 0;
370
+ }
371
+
372
+ function primaryGap(
373
+ current: { x: number; y: number; width: number; height: number },
374
+ candidate: { x: number; y: number; width: number; height: number },
375
+ direction: MetricTreemapDirection,
376
+ ): number {
377
+ switch (direction) {
378
+ case "left":
379
+ return Math.max(0, current.x - (candidate.x + candidate.width));
380
+ case "right":
381
+ return Math.max(0, candidate.x - (current.x + current.width));
382
+ case "up":
383
+ return Math.max(0, current.y - (candidate.y + candidate.height));
384
+ case "down":
385
+ return Math.max(0, candidate.y - (current.y + current.height));
386
+ }
387
+ }
388
+
389
+ function perpendicularGap(
390
+ current: { x: number; y: number; width: number; height: number },
391
+ candidate: { x: number; y: number; width: number; height: number },
392
+ direction: MetricTreemapDirection,
393
+ ): number {
394
+ if (direction === "left" || direction === "right") {
395
+ return rangeGap(current.y, current.y + current.height, candidate.y, candidate.y + candidate.height);
396
+ }
397
+ return rangeGap(current.x, current.x + current.width, candidate.x, candidate.x + candidate.width);
398
+ }
399
+
400
+ function isEdgeCandidate(
401
+ current: { x: number; y: number; width: number; height: number },
402
+ candidate: { x: number; y: number; width: number; height: number },
403
+ direction: MetricTreemapDirection,
404
+ ): boolean {
405
+ const epsilon = 0.0001;
406
+ switch (direction) {
407
+ case "left":
408
+ return candidate.x + candidate.width <= current.x + epsilon;
409
+ case "right":
410
+ return candidate.x >= current.x + current.width - epsilon;
411
+ case "up":
412
+ return candidate.y + candidate.height <= current.y + epsilon;
413
+ case "down":
414
+ return candidate.y >= current.y + current.height - epsilon;
415
+ }
416
+ }
417
+
418
+ function isCenterCandidate(
419
+ currentCenter: { x: number; y: number },
420
+ candidateCenter: { x: number; y: number },
421
+ direction: MetricTreemapDirection,
422
+ ): boolean {
423
+ switch (direction) {
424
+ case "left":
425
+ return candidateCenter.x < currentCenter.x;
426
+ case "right":
427
+ return candidateCenter.x > currentCenter.x;
428
+ case "up":
429
+ return candidateCenter.y < currentCenter.y;
430
+ case "down":
431
+ return candidateCenter.y > currentCenter.y;
432
+ }
433
+ }
434
+
435
+ export function findMetricTreemapNeighbor<T>(
436
+ tiles: Array<MetricTreemapTile<T> | FloatMetricTreemapTile<T>>,
437
+ selectedId: string | null,
438
+ direction: MetricTreemapDirection,
439
+ ): MetricTreemapTile<T> | FloatMetricTreemapTile<T> | null {
440
+ if (tiles.length === 0) return null;
441
+ const current = tiles.find((tile) => tile.item.id === selectedId) ?? tiles[0]!;
442
+ const currentCenter = center(current);
443
+ let best: { tile: MetricTreemapTile<T> | FloatMetricTreemapTile<T>; score: number } | null = null;
444
+ const hasEdgeCandidate = tiles.some((tile) => tile.item.id !== current.item.id && isEdgeCandidate(current, tile, direction));
445
+
446
+ for (const tile of tiles) {
447
+ if (tile.item.id === current.item.id) continue;
448
+ const candidateCenter = center(tile);
449
+ const isCandidate = hasEdgeCandidate
450
+ ? isEdgeCandidate(current, tile, direction)
451
+ : isCenterCandidate(currentCenter, candidateCenter, direction);
452
+ if (!isCandidate) continue;
453
+
454
+ const centerDistance = direction === "left" || direction === "right"
455
+ ? Math.abs(candidateCenter.x - currentCenter.x)
456
+ : Math.abs(candidateCenter.y - currentCenter.y);
457
+ const score = primaryGap(current, tile, direction) * 1000
458
+ + perpendicularGap(current, tile, direction) * 100
459
+ + centerDistance;
460
+ if (!best || score < best.score) best = { tile, score };
461
+ }
462
+
463
+ return best?.tile ?? null;
464
+ }
465
+
466
+ export function buildMetricTreemapNavigationTiles<T>(
467
+ items: MetricTreemapItem<T>[],
468
+ width: number,
469
+ height: number,
470
+ cellAspect: number,
471
+ mode: MetricTreemapLayoutMode,
472
+ ): Array<MetricTreemapTile<T> | FloatMetricTreemapTile<T>> {
473
+ return mode === "float"
474
+ ? buildMetricTreemapRects(items, width, height, cellAspect)
475
+ : buildMetricTreemap(items, width, height, cellAspect);
476
+ }
@@ -183,7 +183,7 @@ function DesktopSettingsRow({
183
183
  flexDirection="column"
184
184
  minHeight={field.description ? undefined : 2}
185
185
  backgroundColor={hovered || selected ? desktopHoverSurface() : "transparent"}
186
- onMouseMove={onHover}
186
+ onMouseOver={onHover}
187
187
  onMouseDown={field.type === "select" ? undefined : (event: any) => {
188
188
  event.stopPropagation?.();
189
189
  event.preventDefault?.();
@@ -157,7 +157,7 @@ export function PriceReturnTable({
157
157
  height={1}
158
158
  width={width}
159
159
  backgroundColor={row.selected ? blendHex(colors.panel, colors.borderFocused, 0.18) : colors.panel}
160
- onMouseMove={() => onSelectSymbol(row.symbol)}
160
+ onMouseOver={() => onSelectSymbol(row.symbol)}
161
161
  onMouseDown={(event: any) => {
162
162
  onFocusInteraction?.(event);
163
163
  onSelectSymbol(row.symbol);
@@ -10,7 +10,7 @@ import {
10
10
  import { colors } from "../../theme/colors";
11
11
  import { useThemeColors } from "../../theme/theme-context";
12
12
  import type { PricePoint } from "../../types/financials";
13
- import { computeBitmapSize } from "../chart/native/chart-rasterizer";
13
+ import { resolveNativeBitmapSize, shouldRenderNativeBitmap } from "../chart/native/bitmap-support";
14
14
  import { renderSparklineBitmap } from "./bitmap";
15
15
  import {
16
16
  buildSamples,
@@ -90,22 +90,21 @@ function TerminalPriceSparkline({
90
90
  const { nativeCharts, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1 } = useUiCapabilities();
91
91
  const nativeRenderer = useNativeRenderer();
92
92
  const rendererResolution = nativeRenderer.resolution;
93
+ const rendererCapabilities = nativeRenderer.capabilities;
93
94
  const rendererTerminalWidth = nativeRenderer.terminalWidth;
94
95
  const rendererTerminalHeight = nativeRenderer.terminalHeight;
95
96
  const bitmap = useMemo(() => {
96
- if (!nativeCharts || values.length < 2) return null;
97
- const scale = Math.max(1, pixelRatio);
98
- const bitmapSize = rendererResolution && rendererTerminalWidth > 0 && rendererTerminalHeight > 0
99
- ? computeBitmapSize(
100
- { x: 0, y: 0, width, height },
101
- rendererResolution,
102
- rendererTerminalWidth,
103
- rendererTerminalHeight,
104
- )
105
- : {
106
- pixelWidth: Math.max(1, Math.round(width * cellWidthPx * scale)),
107
- pixelHeight: Math.max(1, Math.round(height * cellHeightPx * scale)),
108
- };
97
+ if (values.length < 2 || !shouldRenderNativeBitmap(nativeRenderer, nativeCharts === true)) return null;
98
+ const bitmapSize = resolveNativeBitmapSize({
99
+ width,
100
+ height,
101
+ resolution: rendererResolution,
102
+ terminalWidth: rendererTerminalWidth,
103
+ terminalHeight: rendererTerminalHeight,
104
+ cellWidthPx,
105
+ cellHeightPx,
106
+ pixelRatio,
107
+ });
109
108
  return renderSparklineBitmap(values, bitmapSize.pixelWidth, bitmapSize.pixelHeight, color, { area, compact: height <= 1 });
110
109
  }, [
111
110
  area,
@@ -114,7 +113,9 @@ function TerminalPriceSparkline({
114
113
  color,
115
114
  height,
116
115
  nativeCharts,
116
+ nativeRenderer,
117
117
  pixelRatio,
118
+ rendererCapabilities,
118
119
  rendererResolution,
119
120
  rendererTerminalHeight,
120
121
  rendererTerminalWidth,
@@ -1,6 +1,7 @@
1
1
  import { useMemo } from "react";
2
2
  import { Box, ChartSurface, Span, Text, TextAttributes, useNativeRenderer, useUiCapabilities } from "../../ui";
3
- import { computeBitmapSize, type NativeChartBitmap } from "../chart/native/chart-rasterizer";
3
+ import type { NativeChartBitmap } from "../chart/native/chart-rasterizer";
4
+ import { resolveNativeBitmapSize, shouldRenderNativeBitmap } from "../chart/native/bitmap-support";
4
5
  import { drawCircle, drawLine, parseHex } from "../chart/native/raster/primitives";
5
6
  import { colors } from "../../theme/colors";
6
7
  import {
@@ -203,29 +204,29 @@ export function TerminalSpeedometerGauge({
203
204
  const labelRow = useMemo(() => renderLabelRow(gaugeWidth, min, max, segments), [gaugeWidth, max, min, segments]);
204
205
  const tickRow = useMemo(() => renderTickRow(gaugeWidth, min, max), [gaugeWidth, max, min]);
205
206
  const arcRows = useMemo(() => renderArcRows(value, min, max, gaugeWidth, segments), [gaugeWidth, max, min, segments, value]);
207
+ const rendererCapabilities = nativeRenderer.capabilities;
206
208
  const rendererResolution = nativeRenderer.resolution;
207
209
  const rendererTerminalWidth = nativeRenderer.terminalWidth;
208
210
  const rendererTerminalHeight = nativeRenderer.terminalHeight;
209
211
  const bitmap = useMemo<NativeChartBitmap | null>(() => {
210
- if (!nativeCharts) return null;
211
- const bitmapSize = rendererResolution && rendererTerminalWidth > 0 && rendererTerminalHeight > 0
212
- ? computeBitmapSize(
213
- { x: 0, y: 0, width: gaugeWidth, height: arcRows.length },
214
- rendererResolution,
215
- rendererTerminalWidth,
216
- rendererTerminalHeight,
217
- )
218
- : null;
219
- const scale = Math.max(1, pixelRatio);
220
- const pixelWidth = bitmapSize?.pixelWidth ?? Math.max(1, Math.round(gaugeWidth * cellWidthPx * scale));
221
- const pixelHeight = bitmapSize?.pixelHeight ?? Math.max(1, Math.round(arcRows.length * cellHeightPx * scale));
212
+ if (!shouldRenderNativeBitmap(nativeRenderer, nativeCharts === true)) return null;
213
+ const bitmapSize = resolveNativeBitmapSize({
214
+ width: gaugeWidth,
215
+ height: arcRows.length,
216
+ resolution: rendererResolution,
217
+ terminalWidth: rendererTerminalWidth,
218
+ terminalHeight: rendererTerminalHeight,
219
+ cellWidthPx,
220
+ cellHeightPx,
221
+ pixelRatio,
222
+ });
222
223
  return renderGaugeBitmap(
223
224
  value,
224
225
  min,
225
226
  max,
226
227
  segments,
227
- pixelWidth,
228
- pixelHeight,
228
+ bitmapSize.pixelWidth,
229
+ bitmapSize.pixelHeight,
229
230
  );
230
231
  }, [
231
232
  arcRows.length,
@@ -235,7 +236,9 @@ export function TerminalSpeedometerGauge({
235
236
  max,
236
237
  min,
237
238
  nativeCharts,
239
+ nativeRenderer,
238
240
  pixelRatio,
241
+ rendererCapabilities,
239
242
  rendererResolution,
240
243
  rendererTerminalHeight,
241
244
  rendererTerminalWidth,