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.
- package/README.md +49 -8
- package/package.json +4 -3
- package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
- package/patches/react-reconciler@0.33.0.patch +10 -0
- package/src/app/global-shortcuts.test.tsx +20 -1
- package/src/app/global-shortcuts.ts +2 -0
- package/src/app/pane-runtime/index.tsx +11 -4
- package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -1
- package/src/app.tsx +41 -17
- package/src/cli/commands/remote.ts +206 -0
- package/src/cli/entry.ts +8 -1
- package/src/cli/index.ts +2 -0
- package/src/cli/native-smoke.ts +5 -0
- package/src/components/chart/core/keyboard.ts +10 -1
- package/src/components/chart/native/bitmap-support.test.ts +27 -0
- package/src/components/chart/native/bitmap-support.ts +55 -0
- package/src/components/chart/native/kitty/index.test.ts +47 -0
- package/src/components/chart/native/surface/visibility.ts +8 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +4 -0
- package/src/components/chart/static/chart/bitmap.ts +16 -9
- package/src/components/chart/static/chart/surface.test.tsx +59 -4
- package/src/components/chart/static/chart/surface.tsx +4 -0
- package/src/components/chart/stock-chart/index.test.ts +7 -0
- package/src/components/command-bar/list/view.tsx +48 -1
- package/src/components/command-bar/multi-select-picker.tsx +7 -0
- package/src/components/command-bar/routing/navigation-state.ts +10 -1
- package/src/components/command-bar/surface/index.test.tsx +14 -0
- package/src/components/command-bar/theme-picker.tsx +91 -79
- package/src/components/command-bar/workflow/field-row.tsx +30 -5
- package/src/components/data-table/view.tsx +0 -12
- package/src/components/index.ts +7 -0
- package/src/components/layout/floating-pane.tsx +3 -3
- package/src/components/layout/pane/footer/index.tsx +23 -0
- package/src/components/layout/pane/header.tsx +5 -0
- package/src/components/layout/pane/index.tsx +3 -3
- package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
- package/src/components/layout/shell/cursor-occlusion.ts +9 -1
- package/src/components/layout/shell/index.test.tsx +2 -0
- package/src/components/layout/shell/index.tsx +16 -4
- package/src/components/layout/shell/pane/actions.ts +21 -7
- package/src/components/layout/shell/pane/layers.tsx +2 -2
- package/src/components/layout/status-bar.tsx +5 -5
- package/src/components/metric-treemap/index.tsx +317 -0
- package/src/components/metric-treemap/layout.test.ts +77 -0
- package/src/components/metric-treemap/layout.ts +476 -0
- package/src/components/pane-settings-dialog/desktop.tsx +1 -1
- package/src/components/price-performance.tsx +1 -1
- package/src/components/price-sparkline/view.tsx +15 -14
- package/src/components/speedometer-gauge/terminal.tsx +18 -15
- package/src/components/table-view-shared.tsx +0 -10
- package/src/components/ticker/list-table-view.tsx +1 -7
- package/src/components/ticker/list-table.test.tsx +0 -4
- package/src/components/toggle-list.tsx +10 -0
- package/src/components/ui/button.tsx +12 -0
- package/src/components/ui/data-table/index.tsx +51 -0
- package/src/components/ui/data-table/opentui/index.tsx +4 -10
- package/src/components/ui/data-table/types.ts +6 -4
- package/src/components/ui/fields.tsx +18 -0
- package/src/components/ui/list-view.tsx +84 -4
- package/src/components/ui/message-composer.tsx +22 -0
- package/src/components/ui/tabs.tsx +46 -2
- package/src/components/ui/ui.test.tsx +0 -9
- package/src/core/state/app/layout-reducer.ts +7 -1
- package/src/core/state/app/layout.ts +6 -0
- package/src/core/state/app/state.ts +23 -1
- package/src/core/state/app/types.ts +4 -2
- package/src/plugins/builtin/account-management/form-components.tsx +2 -2
- package/src/plugins/builtin/alerts/pane.tsx +1 -1
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
- package/src/plugins/builtin/chat/channels.ts +5 -18
- package/src/plugins/builtin/chat/chat.test.tsx +1 -7
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
- package/src/plugins/builtin/chat/sidebar.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +2 -8
- package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
- package/src/plugins/builtin/cloud/plugin.tsx +4 -13
- package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
- package/src/plugins/builtin/debug/index.tsx +3 -4
- package/src/plugins/builtin/help/components.tsx +1 -1
- package/src/plugins/builtin/holders/treemap.tsx +28 -297
- package/src/plugins/builtin/holders/types.ts +0 -34
- package/src/plugins/builtin/market-heatmap/data.test.ts +121 -0
- package/src/plugins/builtin/market-heatmap/data.ts +296 -0
- package/src/plugins/builtin/market-heatmap/index.tsx +320 -0
- package/src/plugins/builtin/options/table.ts +5 -5
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/grid.tsx +197 -0
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +41 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +78 -22
- package/src/plugins/ibkr/gateway/service/index.test.ts +22 -0
- package/src/plugins/ibkr/gateway/service/index.ts +6 -9
- package/src/plugins/ibkr/gateway/service/native-loader.ts +12 -0
- package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
- package/src/remote/app-host.tsx +77 -0
- package/src/remote/client.ts +34 -0
- package/src/remote/command-bar.ts +98 -0
- package/src/remote/controller-utils.ts +111 -0
- package/src/remote/controller.test.ts +365 -0
- package/src/remote/controller.ts +434 -0
- package/src/remote/endpoint.ts +41 -0
- package/src/remote/json-patch.test.ts +31 -0
- package/src/remote/json-patch.ts +89 -0
- package/src/remote/layout-helpers.ts +62 -0
- package/src/remote/resources.ts +186 -0
- package/src/remote/revision.ts +18 -0
- package/src/remote/schema.ts +143 -0
- package/src/remote/semantic-helpers.ts +161 -0
- package/src/remote/semantic-tree.tsx +112 -0
- package/src/remote/server.test.ts +50 -0
- package/src/remote/server.ts +121 -0
- package/src/remote/types.ts +119 -0
- package/src/renderers/electrobun/bun/index.ts +55 -1
- package/src/renderers/electrobun/shared/protocol.ts +11 -1
- package/src/renderers/electrobun/view/backend-rpc.ts +28 -1
- package/src/renderers/electrobun/view/build-assets.ts +1 -0
- package/src/renderers/electrobun/view/data-table/index.tsx +0 -8
- package/src/renderers/electrobun/view/data-table/row.tsx +2 -12
- package/src/renderers/electrobun/view/host/box.tsx +11 -1
- package/src/renderers/electrobun/view/host/mouse.test.ts +37 -0
- package/src/renderers/electrobun/view/host/mouse.ts +2 -0
- package/src/renderers/electrobun/view/host/style.ts +1 -0
- package/src/renderers/electrobun/view/main.tsx +5 -1
- package/src/renderers/electrobun/view/native-stubs/ibkr-native-loader.ts +3 -0
- package/src/renderers/electrobun/view/styles.css +7 -0
- package/src/renderers/opentui/chart-surface.test.tsx +101 -0
- package/src/renderers/opentui/chart-surface.tsx +15 -20
- package/src/renderers/opentui/native-smoke.ts +1 -0
- package/src/renderers/opentui/start.tsx +32 -0
- package/src/sources/yahoo-finance/http.ts +31 -4
- package/src/state/app/context/index.test.tsx +47 -0
- package/src/state/app/context/index.tsx +48 -18
- package/src/ui/host.tsx +1 -0
- package/src/ui/index.tsx +183 -7
- package/src/version.ts +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
}
|