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
package/src/ui/index.tsx CHANGED
@@ -1,6 +1,16 @@
1
- import { createElement, forwardRef, type ComponentProps } from "react";
1
+ import { createElement, forwardRef, useCallback, useRef, type ComponentProps, type ForwardedRef } from "react";
2
2
  import { useForwardedScrollBoxRef, useRegisterPaneScrollBox } from "../state/pane-scroll-registry";
3
3
  import { useUiHost, type UiHost } from "./host";
4
+ import { useRemoteUiNode } from "../remote/semantic-tree";
5
+ import {
6
+ remoteChartEvent,
7
+ remoteEvent,
8
+ remoteMetadataFromProps,
9
+ remoteNumberValue,
10
+ remotePropLabel,
11
+ remotePropRole,
12
+ remoteStringValue,
13
+ } from "../remote/semantic-helpers";
4
14
  export {
5
15
  RGBA,
6
16
  StyledText,
@@ -40,13 +50,50 @@ export type {
40
50
 
41
51
  export const Box = forwardRef<any, ComponentProps<UiHost["Box"]>>((props, ref) => {
42
52
  const { Box: HostBox } = useUiHost();
43
- return createElement(HostBox as any, { ...props, ref });
53
+ const rawProps = props as Record<string, unknown>;
54
+ const onMouseDown = rawProps.onMouseDown as ((event?: unknown) => unknown) | undefined;
55
+ const onMouseUp = rawProps.onMouseUp as ((event?: unknown) => unknown) | undefined;
56
+ const onContextMenu = rawProps.onContextMenu as ((event?: unknown) => unknown) | undefined;
57
+ const remoteNodeId = useRemoteUiNode(
58
+ onMouseDown || onMouseUp || onContextMenu
59
+ ? {
60
+ role: remotePropRole(rawProps, "box"),
61
+ label: remotePropLabel(rawProps),
62
+ actions: {
63
+ press: onMouseDown ? (input) => onMouseDown(remoteEvent(input)) : undefined,
64
+ release: onMouseUp ? (input) => onMouseUp(remoteEvent(input)) : undefined,
65
+ contextMenu: onContextMenu ? (input) => onContextMenu(remoteEvent(input)) : undefined,
66
+ },
67
+ metadata: {
68
+ width: rawProps.width,
69
+ height: rawProps.height,
70
+ },
71
+ }
72
+ : null,
73
+ );
74
+ return createElement(HostBox as any, { ...props, ref, "data-gloom-remote-node-id": remoteNodeId ?? undefined });
44
75
  });
45
76
  Box.displayName = "Box";
46
77
 
47
78
  export const Text = forwardRef<any, ComponentProps<UiHost["Text"]>>((props, ref) => {
48
79
  const { Text: HostText } = useUiHost();
49
- return createElement(HostText as any, { ...props, ref });
80
+ const rawProps = props as Record<string, unknown>;
81
+ const onMouseDown = rawProps.onMouseDown as ((event?: unknown) => unknown) | undefined;
82
+ const label = remotePropLabel(rawProps)
83
+ ?? (typeof rawProps.children === "string" ? rawProps.children : undefined)
84
+ ?? (typeof rawProps.content === "string" ? rawProps.content : undefined);
85
+ const remoteNodeId = useRemoteUiNode(
86
+ onMouseDown
87
+ ? {
88
+ role: remotePropRole(rawProps, "text"),
89
+ label,
90
+ actions: {
91
+ press: (input) => onMouseDown(remoteEvent(input)),
92
+ },
93
+ }
94
+ : null,
95
+ );
96
+ return createElement(HostText as any, { ...props, ref, "data-gloom-remote-node-id": remoteNodeId ?? undefined });
50
97
  });
51
98
  Text.displayName = "Text";
52
99
 
@@ -71,31 +118,160 @@ Underline.displayName = "Underline";
71
118
  export const ScrollBox = forwardRef<any, ComponentProps<UiHost["ScrollBox"]>>((props, ref) => {
72
119
  const { ScrollBox: HostScrollBox } = useUiHost();
73
120
  const localRef = useForwardedScrollBoxRef<any>(ref);
121
+ const rawProps = props as Record<string, unknown>;
122
+ const remoteNodeId = useRemoteUiNode(
123
+ props.scrollY === true
124
+ ? {
125
+ role: remotePropRole(rawProps, "scrollbox"),
126
+ label: remotePropLabel(rawProps),
127
+ actions: {
128
+ scrollTo: (input) => {
129
+ const top = remoteNumberValue(input, ["top", "index"]);
130
+ localRef.current?.scrollTo?.(Math.max(0, Math.round(top)));
131
+ },
132
+ scrollBy: (input) => {
133
+ const currentTop = Math.max(0, Math.round(localRef.current?.scrollTop ?? 0));
134
+ const delta = input && typeof input === "object" && (input as { direction?: unknown }).direction === "up"
135
+ ? remoteNumberValue(input, ["delta"], -1)
136
+ : remoteNumberValue(input, ["delta"], 1);
137
+ localRef.current?.scrollTo?.(Math.max(0, currentTop + Math.round(delta)));
138
+ },
139
+ },
140
+ metadata: {
141
+ ...remoteMetadataFromProps(rawProps),
142
+ width: rawProps.width,
143
+ height: rawProps.height,
144
+ scrollY: props.scrollY === true,
145
+ scrollTop: localRef.current?.scrollTop,
146
+ viewportHeight: localRef.current?.viewport?.height,
147
+ },
148
+ }
149
+ : null,
150
+ );
74
151
  useRegisterPaneScrollBox(localRef, {
75
152
  enabled: props.scrollY === true,
76
153
  onScrollActivity: typeof props.onMouseScroll === "function"
77
154
  ? props.onMouseScroll as (event: { scroll: { direction: "up" | "down"; delta: number } }) => void
78
155
  : undefined,
79
156
  });
80
- return createElement(HostScrollBox as any, { ...props, ref: localRef });
157
+ return createElement(HostScrollBox as any, { ...props, ref: localRef, "data-gloom-remote-node-id": remoteNodeId ?? undefined });
81
158
  });
82
159
  ScrollBox.displayName = "ScrollBox";
83
160
 
84
161
  export const Input = forwardRef<any, ComponentProps<UiHost["Input"]>>((props, ref) => {
85
162
  const { Input: HostInput } = useUiHost();
86
- return createElement(HostInput as any, { ...props, ref });
163
+ const rawProps = props as Record<string, unknown>;
164
+ const onInput = rawProps.onInput as ((value: string) => unknown) | undefined;
165
+ const onChange = rawProps.onChange as ((value: string) => unknown) | undefined;
166
+ const onSubmit = rawProps.onSubmit as ((value?: string) => unknown) | undefined;
167
+ const remoteNodeId = useRemoteUiNode({
168
+ role: remotePropRole(rawProps, "input"),
169
+ label: remotePropLabel(rawProps),
170
+ disabled: rawProps.disabled === true,
171
+ actions: {
172
+ setValue: (input) => {
173
+ const value = remoteStringValue(input);
174
+ onInput?.(value);
175
+ onChange?.(value);
176
+ },
177
+ submit: (input) => {
178
+ const value = remoteStringValue(input, undefined);
179
+ onSubmit?.(value);
180
+ },
181
+ focus: () => (ref && typeof ref === "object" ? ref.current?.focus?.() : undefined),
182
+ },
183
+ metadata: {
184
+ ...remoteMetadataFromProps(rawProps),
185
+ value: rawProps.value,
186
+ placeholder: rawProps.placeholder,
187
+ focused: rawProps.focused,
188
+ },
189
+ });
190
+ return createElement(HostInput as any, { ...props, ref, "data-gloom-remote-node-id": remoteNodeId ?? undefined });
87
191
  });
88
192
  Input.displayName = "Input";
89
193
 
90
194
  export const Textarea = forwardRef<any, ComponentProps<UiHost["Textarea"]>>((props, ref) => {
91
195
  const { Textarea: HostTextarea } = useUiHost();
92
- return createElement(HostTextarea as any, { ...props, ref });
196
+ const rawProps = props as Record<string, unknown>;
197
+ const onInput = rawProps.onInput as ((value: string) => unknown) | undefined;
198
+ const onChange = rawProps.onChange as ((value: string) => unknown) | undefined;
199
+ const onSubmit = rawProps.onSubmit as (() => unknown) | undefined;
200
+ const remoteNodeId = useRemoteUiNode({
201
+ role: remotePropRole(rawProps, "textarea"),
202
+ label: remotePropLabel(rawProps),
203
+ disabled: rawProps.disabled === true,
204
+ actions: {
205
+ setValue: (input) => {
206
+ const value = remoteStringValue(input);
207
+ onInput?.(value);
208
+ onChange?.(value);
209
+ },
210
+ submit: () => onSubmit?.(),
211
+ focus: () => (ref && typeof ref === "object" ? ref.current?.focus?.() : undefined),
212
+ },
213
+ metadata: {
214
+ ...remoteMetadataFromProps(rawProps),
215
+ value: rawProps.value ?? rawProps.initialValue,
216
+ placeholder: rawProps.placeholder,
217
+ focused: rawProps.focused,
218
+ },
219
+ });
220
+ return createElement(HostTextarea as any, { ...props, ref, "data-gloom-remote-node-id": remoteNodeId ?? undefined });
93
221
  });
94
222
  Textarea.displayName = "Textarea";
95
223
 
224
+ function assignForwardedRef(ref: ForwardedRef<any>, value: any): void {
225
+ if (typeof ref === "function") {
226
+ ref(value);
227
+ } else if (ref) {
228
+ ref.current = value;
229
+ }
230
+ }
231
+
96
232
  export const ChartSurface = forwardRef<any, ComponentProps<UiHost["ChartSurface"]>>((props, ref) => {
97
233
  const { ChartSurface: HostChartSurface } = useUiHost();
98
- return createElement(HostChartSurface as any, { ...props, ref });
234
+ const rawProps = props as Record<string, unknown>;
235
+ const localRef = useRef<any>(null);
236
+ const setSurfaceRef = useCallback((value: any) => {
237
+ localRef.current = value;
238
+ assignForwardedRef(ref, value);
239
+ }, [ref]);
240
+ const onMouseMove = rawProps.onMouseMove as ((event?: unknown) => unknown) | undefined;
241
+ const onMouseDown = rawProps.onMouseDown as ((event?: unknown) => unknown) | undefined;
242
+ const onMouseDrag = rawProps.onMouseDrag as ((event?: unknown) => unknown) | undefined;
243
+ const onMouseUp = rawProps.onMouseUp as ((event?: unknown) => unknown) | undefined;
244
+ const onMouseDragEnd = rawProps.onMouseDragEnd as ((event?: unknown) => unknown) | undefined;
245
+ const onMouseScroll = rawProps.onMouseScroll as ((event?: unknown) => unknown) | undefined;
246
+ const width = typeof rawProps.width === "number" ? rawProps.width : undefined;
247
+ const height = typeof rawProps.height === "number" ? rawProps.height : undefined;
248
+ const bitmap = rawProps.bitmap;
249
+ const bitmaps = Array.isArray(rawProps.bitmaps) ? rawProps.bitmaps : null;
250
+ const hasBitmap = !!bitmap || (bitmaps?.length ?? 0) > 0;
251
+ const visualRole = typeof rawProps["data-gloom-role"] === "string" ? rawProps["data-gloom-role"] : undefined;
252
+ const remoteNodeId = useRemoteUiNode({
253
+ role: "chart",
254
+ label: remotePropLabel(rawProps) ?? "Chart",
255
+ actions: {
256
+ moveCursor: onMouseMove ? (input) => onMouseMove(remoteChartEvent(input, localRef.current, width, height)) : undefined,
257
+ press: onMouseDown ? (input) => onMouseDown(remoteChartEvent(input, localRef.current, width, height)) : undefined,
258
+ drag: onMouseDrag ? (input) => onMouseDrag(remoteChartEvent(input, localRef.current, width, height)) : undefined,
259
+ release: onMouseUp ? (input) => onMouseUp(remoteChartEvent(input, localRef.current, width, height)) : undefined,
260
+ endDrag: onMouseDragEnd ? (input) => onMouseDragEnd(remoteChartEvent(input, localRef.current, width, height)) : undefined,
261
+ scroll: onMouseScroll ? (input) => onMouseScroll(remoteChartEvent(input, localRef.current, width, height)) : undefined,
262
+ },
263
+ metadata: {
264
+ ...remoteMetadataFromProps(rawProps),
265
+ width: rawProps.width,
266
+ height: rawProps.height,
267
+ hasBitmap,
268
+ bitmapCount: bitmaps?.length ?? (bitmap ? 1 : 0),
269
+ hasCrosshair: rawProps.crosshair != null,
270
+ interactive: !!(onMouseMove || onMouseDown || onMouseDrag || onMouseUp || onMouseScroll),
271
+ visualRole,
272
+ },
273
+ });
274
+ return createElement(HostChartSurface as any, { ...props, ref: setSurfaceRef, "data-gloom-remote-node-id": remoteNodeId ?? undefined });
99
275
  });
100
276
  ChartSurface.displayName = "ChartSurface";
101
277
 
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.9.4";
1
+ export const VERSION = "0.9.6";
@@ -1,231 +0,0 @@
1
- import type { FloatRect, FloatTileLayout, HolderRow, TileLayout, TreemapGroup, WeightedTreemapItem } from "./types";
2
-
3
- function rowWeight(row: HolderRow): number {
4
- return Math.max(row.value ?? row.shares ?? 0, 0);
5
- }
6
-
7
- function worstAspectRatio(items: WeightedTreemapItem[], sideLength: number): number {
8
- if (items.length === 0 || sideLength <= 0) return Number.POSITIVE_INFINITY;
9
- const areaSum = items.reduce((sum, item) => sum + item.area, 0);
10
- const minArea = Math.min(...items.map((item) => item.area));
11
- const maxArea = Math.max(...items.map((item) => item.area));
12
- if (areaSum <= 0 || minArea <= 0) return Number.POSITIVE_INFINITY;
13
-
14
- const sideSquared = sideLength * sideLength;
15
- return Math.max(
16
- (sideSquared * maxArea) / (areaSum * areaSum),
17
- (areaSum * areaSum) / (sideSquared * minArea),
18
- );
19
- }
20
-
21
- function layoutFloatGroup(items: WeightedTreemapItem[], rect: FloatRect): FloatRect {
22
- const areaSum = items.reduce((sum, item) => sum + item.area, 0);
23
- if (areaSum <= 0 || rect.width <= 0 || rect.height <= 0) return rect;
24
-
25
- if (rect.width >= rect.height) {
26
- const columnWidth = Math.min(rect.width, areaSum / rect.height);
27
- return {
28
- x: rect.x + columnWidth,
29
- y: rect.y,
30
- width: Math.max(0, rect.width - columnWidth),
31
- height: rect.height,
32
- };
33
- }
34
-
35
- const rowHeight = Math.min(rect.height, areaSum / rect.width);
36
- return {
37
- x: rect.x,
38
- y: rect.y + rowHeight,
39
- width: rect.width,
40
- height: Math.max(0, rect.height - rowHeight),
41
- };
42
- }
43
-
44
- function buildSquarifiedGroups(items: WeightedTreemapItem[], width: number, height: number): TreemapGroup[] {
45
- const groups: TreemapGroup[] = [];
46
- let rect: FloatRect = { x: 0, y: 0, width, height };
47
- let index = 0;
48
-
49
- while (index < items.length && rect.width > 0 && rect.height > 0) {
50
- const group: WeightedTreemapItem[] = [];
51
- let currentWorst = Number.POSITIVE_INFINITY;
52
- const sideLength = Math.min(rect.width, rect.height);
53
-
54
- while (index < items.length) {
55
- const candidate = items[index]!;
56
- const nextGroup = [...group, candidate];
57
- const nextWorst = worstAspectRatio(nextGroup, sideLength);
58
- if (group.length > 0 && nextWorst > currentWorst) break;
59
- group.push(candidate);
60
- currentWorst = nextWorst;
61
- index += 1;
62
- }
63
-
64
- groups.push({
65
- items: group,
66
- weight: group.reduce((sum, item) => sum + item.weight, 0),
67
- });
68
- rect = layoutFloatGroup(group, rect);
69
- }
70
-
71
- return groups;
72
- }
73
-
74
- function layoutFloatTiles(groups: TreemapGroup[], width: number, height: number, cellAspect: number): FloatTileLayout[] {
75
- const tiles: FloatTileLayout[] = [];
76
- let rect: FloatRect = { x: 0, y: 0, width, height };
77
-
78
- for (const group of groups) {
79
- const areaSum = group.items.reduce((sum, item) => sum + item.area, 0);
80
- if (areaSum <= 0 || rect.width <= 0 || rect.height <= 0) break;
81
-
82
- if (rect.width >= rect.height) {
83
- const columnWidth = Math.min(rect.width, areaSum / rect.height);
84
- let itemY = rect.y;
85
- for (const item of group.items) {
86
- const itemHeight = Math.min(rect.y + rect.height - itemY, item.area / columnWidth);
87
- tiles.push({
88
- row: item.row,
89
- x: rect.x,
90
- y: itemY / cellAspect,
91
- width: columnWidth,
92
- height: itemHeight / cellAspect,
93
- });
94
- itemY += itemHeight;
95
- }
96
- rect = {
97
- x: rect.x + columnWidth,
98
- y: rect.y,
99
- width: Math.max(0, rect.width - columnWidth),
100
- height: rect.height,
101
- };
102
- } else {
103
- const rowHeight = Math.min(rect.height, areaSum / rect.width);
104
- let itemX = rect.x;
105
- for (const item of group.items) {
106
- const itemWidth = Math.min(rect.x + rect.width - itemX, item.area / rowHeight);
107
- tiles.push({
108
- row: item.row,
109
- x: itemX,
110
- y: rect.y / cellAspect,
111
- width: itemWidth,
112
- height: rowHeight / cellAspect,
113
- });
114
- itemX += itemWidth;
115
- }
116
- rect = {
117
- x: rect.x,
118
- y: rect.y + rowHeight,
119
- width: rect.width,
120
- height: Math.max(0, rect.height - rowHeight),
121
- };
122
- }
123
- }
124
-
125
- return tiles.filter((tile) => tile.width > 0 && tile.height > 0);
126
- }
127
-
128
- export function buildTreemapRects(rows: HolderRow[], width: number, height: number, cellAspect = 1): FloatTileLayout[] {
129
- if (width <= 0 || height <= 0) return [];
130
- const weightedRows = rows
131
- .map((row) => ({ row, weight: rowWeight(row) }))
132
- .filter((item) => item.weight > 0)
133
- .slice(0, Math.max(1, Math.min(80, width * height)));
134
- const totalWeight = weightedRows.reduce((sum, item) => sum + item.weight, 0);
135
- if (totalWeight <= 0) return [];
136
-
137
- const normalizedCellAspect = Math.max(0.25, cellAspect);
138
- const normalizedHeight = height * normalizedCellAspect;
139
- const totalArea = width * normalizedHeight;
140
- const weightedItems: WeightedTreemapItem[] = weightedRows.map((item) => ({
141
- ...item,
142
- area: item.weight / totalWeight * totalArea,
143
- }));
144
- const groups = buildSquarifiedGroups(weightedItems, width, normalizedHeight);
145
- return layoutFloatTiles(groups, width, normalizedHeight, normalizedCellAspect);
146
- }
147
-
148
- function allocateLengths(totalLength: number, weights: number[]): number[] {
149
- if (weights.length === 0 || totalLength <= 0) return [];
150
- const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
151
- let remainingLength = totalLength;
152
- let remainingWeight = totalWeight;
153
-
154
- return weights.map((weight, index) => {
155
- const remainingItems = weights.length - index;
156
- if (remainingItems === 1) return remainingLength;
157
- const ideal = remainingWeight > 0 ? Math.round(remainingLength * weight / remainingWeight) : 1;
158
- const length = Math.max(1, Math.min(remainingLength - (remainingItems - 1), ideal));
159
- remainingLength -= length;
160
- remainingWeight -= weight;
161
- return length;
162
- });
163
- }
164
-
165
- export function buildTreemap(rows: HolderRow[], width: number, height: number, cellAspect = 1): TileLayout[] {
166
- if (width <= 0 || height <= 0) return [];
167
- const weightedRows = rows
168
- .map((row) => ({ row, weight: rowWeight(row) }))
169
- .filter((item) => item.weight > 0)
170
- .slice(0, Math.max(1, Math.min(80, width * height)));
171
- const totalWeight = weightedRows.reduce((sum, item) => sum + item.weight, 0);
172
- if (totalWeight <= 0) return [];
173
-
174
- const normalizedCellAspect = Math.max(0.25, cellAspect);
175
- const normalizedHeight = height * normalizedCellAspect;
176
- const totalArea = width * normalizedHeight;
177
- const weightedItems: WeightedTreemapItem[] = weightedRows.map((item) => ({
178
- ...item,
179
- area: item.weight / totalWeight * totalArea,
180
- }));
181
- const groups = buildSquarifiedGroups(weightedItems, width, normalizedHeight);
182
- const tiles: TileLayout[] = [];
183
-
184
- let x = 0;
185
- let y = 0;
186
- let remainingWidth = width;
187
- let remainingHeight = height;
188
- let remainingWeight = groups.reduce((sum, group) => sum + group.weight, 0);
189
-
190
- groups.forEach((group, groupIndex) => {
191
- if (remainingWidth <= 0 || remainingHeight <= 0) return;
192
- const isLastGroup = groupIndex === groups.length - 1;
193
- const remainingNormalizedHeight = remainingHeight * normalizedCellAspect;
194
-
195
- if (remainingWidth >= remainingNormalizedHeight) {
196
- const columnWidth = isLastGroup
197
- ? remainingWidth
198
- : Math.max(1, Math.min(remainingWidth - 1, Math.round(remainingWidth * group.weight / remainingWeight)));
199
- const heights = allocateLengths(remainingHeight, group.items.map((item) => item.weight));
200
- let tileY = y;
201
- group.items.forEach((item, itemIndex) => {
202
- const tileHeight = heights[itemIndex] ?? 0;
203
- if (tileHeight > 0) {
204
- tiles.push({ row: item.row, x, y: tileY, width: columnWidth, height: tileHeight });
205
- }
206
- tileY += tileHeight;
207
- });
208
- x += columnWidth;
209
- remainingWidth -= columnWidth;
210
- } else {
211
- const rowHeight = isLastGroup
212
- ? remainingHeight
213
- : Math.max(1, Math.min(remainingHeight - 1, Math.round(remainingHeight * group.weight / remainingWeight)));
214
- const widths = allocateLengths(remainingWidth, group.items.map((item) => item.weight));
215
- let tileX = x;
216
- group.items.forEach((item, itemIndex) => {
217
- const tileWidth = widths[itemIndex] ?? 0;
218
- if (tileWidth > 0) {
219
- tiles.push({ row: item.row, x: tileX, y, width: tileWidth, height: rowHeight });
220
- }
221
- tileX += tileWidth;
222
- });
223
- y += rowHeight;
224
- remainingHeight -= rowHeight;
225
- }
226
-
227
- remainingWeight -= group.weight;
228
- });
229
-
230
- return tiles.filter((tile) => tile.width > 0 && tile.height > 0);
231
- }