gloomberb 0.9.3 → 0.9.5
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 +1 -0
- package/package.json +3 -2
- package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
- package/patches/react-reconciler@0.33.0.patch +10 -0
- package/src/app/runtime/desktop-menu.ts +5 -1
- package/src/app/runtime/ticker-refresh.ts +4 -1
- package/src/app.tsx +10 -1
- package/src/brokers/account-cache.ts +1 -1
- package/src/brokers/sync-broker-instance.test.ts +107 -0
- package/src/brokers/sync-broker-instance.ts +22 -10
- package/src/cli/desktop-pane-shot.ts +2 -1
- package/src/cli/entry.ts +8 -1
- package/src/cli/native-smoke.ts +5 -0
- package/src/cli/pane-functions/screenshot.ts +13 -1
- package/src/components/chart/bar-chart-renderer.test.ts +108 -3
- package/src/components/chart/bar-chart-renderer.ts +226 -27
- package/src/components/chart/core/pointer.ts +32 -6
- 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/price-axis-labels.tsx +3 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
- package/src/components/chart/static/bar-chart-surface.tsx +151 -28
- package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
- package/src/components/chart/static/chart/bitmap.ts +16 -9
- package/src/components/chart/static/chart/surface.test.tsx +92 -0
- package/src/components/chart/static/chart/surface.tsx +167 -24
- package/src/components/chart/stock-chart/index.test.ts +25 -0
- package/src/components/command-bar/layout-items/current-layout.ts +5 -1
- package/src/components/command-bar/list/view.tsx +1 -1
- package/src/components/command-bar/theme-picker.tsx +1 -1
- package/src/components/data-table/view.tsx +0 -12
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +6 -5
- package/src/components/layout/pane/content.tsx +20 -8
- package/src/components/layout/pane/footer/registration.tsx +6 -2
- package/src/components/layout/pane/frame.tsx +4 -4
- package/src/components/layout/pane/header.tsx +45 -4
- package/src/components/layout/pane/index.tsx +6 -4
- package/src/components/layout/pane/sizing.test.ts +43 -0
- package/src/components/layout/pane/sizing.ts +13 -4
- package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
- package/src/components/layout/shell/cursor-occlusion.ts +204 -0
- package/src/components/layout/shell/drag/runtime.ts +9 -2
- package/src/components/layout/shell/index.test.tsx +92 -2
- package/src/components/layout/shell/index.tsx +45 -3
- package/src/components/layout/shell/layout-state.test.ts +33 -0
- package/src/components/layout/shell/layout-state.ts +4 -9
- package/src/components/layout/shell/pane/layers.tsx +10 -4
- package/src/components/layout/shell/visible-layout.ts +17 -0
- package/src/components/layout/status-bar.test.tsx +6 -1
- package/src/components/layout/status-bar.tsx +10 -6
- package/src/components/onboarding/onboarding-steps.tsx +8 -28
- package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
- package/src/components/onboarding/onboarding-wizard.tsx +2 -4
- package/src/components/onboarding/portfolio-step/index.tsx +0 -12
- package/src/components/onboarding/portfolio-step/types.ts +1 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -5
- 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 +12 -14
- package/src/components/ticker/list-table-view.tsx +1 -7
- package/src/components/ticker/list-table.test.tsx +0 -4
- package/src/components/ui/data-table/opentui/index.tsx +29 -22
- package/src/components/ui/data-table/types.ts +10 -4
- package/src/components/ui/fields.tsx +10 -3
- package/src/components/ui/list-view.tsx +1 -1
- package/src/components/ui/page-stack-view.tsx +6 -2
- package/src/components/ui/table-layout.test.ts +25 -0
- package/src/components/ui/table-layout.ts +22 -6
- package/src/components/ui/tabs.tsx +27 -12
- package/src/components/ui/ui.test.tsx +87 -9
- package/src/core/state/app/layout-reducer.ts +2 -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/analytics/index.test.tsx +6 -2
- package/src/plugins/builtin/analytics/pane-model.ts +43 -8
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
- 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 +1 -1
- package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
- 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 +1 -1
- package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
- package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
- package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
- package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
- package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
- package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
- package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
- package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
- package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
- package/src/plugins/builtin/layout-manager/index.tsx +5 -1
- package/src/plugins/builtin/options/table.ts +5 -5
- package/src/plugins/builtin/options/view.test.tsx +141 -4
- package/src/plugins/builtin/options/view.tsx +76 -15
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
- package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
- package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/ibkr/broker-adapter.ts +19 -0
- package/src/plugins/ibkr/config.test.ts +31 -2
- package/src/plugins/ibkr/config.ts +9 -2
- package/src/plugins/ibkr/flex/index.test.ts +18 -9
- package/src/plugins/ibkr/flex/index.ts +30 -0
- package/src/plugins/ibkr/flex/statement-client.ts +68 -14
- package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
- package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
- package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
- package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
- package/src/plugins/pane-manager/gridlock.ts +13 -5
- package/src/plugins/pane-manager/layout-state.ts +25 -0
- package/src/plugins/pane-manager.test.ts +33 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +10 -4
- package/src/plugins/prediction-markets/detail/book.tsx +141 -96
- package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
- package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
- package/src/plugins/prediction-markets/detail.test.tsx +50 -0
- package/src/plugins/prediction-markets/pane.test.tsx +43 -0
- package/src/plugins/prediction-markets/pane.tsx +50 -22
- package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
- package/src/renderers/electrobun/view/data-table/index.tsx +19 -12
- package/src/renderers/electrobun/view/data-table/row.tsx +32 -18
- package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
- package/src/renderers/electrobun/view/host/box.tsx +6 -1
- package/src/renderers/electrobun/view/host/input.tsx +115 -18
- package/src/renderers/electrobun/view/host/style.ts +1 -0
- package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
- package/src/renderers/electrobun/view/styles.css +18 -0
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/native-smoke.ts +1 -0
- package/src/renderers/opentui/ui-host.tsx +57 -25
- package/src/sources/provider-router/batches.ts +16 -2
- package/src/sources/provider-router/financial-routes.ts +5 -0
- package/src/sources/provider-router/financials.ts +11 -23
- package/src/sources/provider-router/index.test.ts +189 -0
- package/src/sources/provider-router/primary.ts +3 -2
- package/src/sources/sec-edgar.test.ts +83 -0
- package/src/sources/sec-edgar.ts +231 -0
- package/src/sources/yahoo-finance.test.ts +51 -0
- package/src/sources/yahoo-finance.ts +49 -1
- package/src/state/app/context/index.test.tsx +5 -8
- package/src/types/broker.ts +1 -0
- package/src/types/config.ts +10 -0
- package/src/types/trading.ts +2 -0
- package/src/ui/host.tsx +16 -0
- package/src/ui/index.tsx +2 -0
- package/src/utils/financial-statements.ts +23 -0
- package/src/version.ts +1 -1
- package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { NativeRendererHost, PixelResolution } from "../../../ui";
|
|
2
|
+
import { computeBitmapSize } from "./chart-rasterizer";
|
|
3
|
+
|
|
4
|
+
interface NativeBitmapRendererCapabilities {
|
|
5
|
+
kitty_graphics?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface NativeBitmapSize {
|
|
9
|
+
pixelWidth: number;
|
|
10
|
+
pixelHeight: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasKnownUnsupportedKittyGraphics(renderer: NativeRendererHost): boolean {
|
|
14
|
+
const capabilities = renderer.capabilities as NativeBitmapRendererCapabilities | null | undefined;
|
|
15
|
+
return capabilities?.kitty_graphics === false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function shouldRenderNativeBitmap(renderer: NativeRendererHost, nativeCharts: boolean): boolean {
|
|
19
|
+
return nativeCharts && !hasKnownUnsupportedKittyGraphics(renderer);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveNativeBitmapSize({
|
|
23
|
+
width,
|
|
24
|
+
height,
|
|
25
|
+
resolution,
|
|
26
|
+
terminalWidth,
|
|
27
|
+
terminalHeight,
|
|
28
|
+
cellWidthPx,
|
|
29
|
+
cellHeightPx,
|
|
30
|
+
pixelRatio,
|
|
31
|
+
}: {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
resolution: PixelResolution | null;
|
|
35
|
+
terminalWidth: number;
|
|
36
|
+
terminalHeight: number;
|
|
37
|
+
cellWidthPx: number;
|
|
38
|
+
cellHeightPx: number;
|
|
39
|
+
pixelRatio: number;
|
|
40
|
+
}): NativeBitmapSize {
|
|
41
|
+
if (resolution && terminalWidth > 0 && terminalHeight > 0) {
|
|
42
|
+
return computeBitmapSize(
|
|
43
|
+
{ x: 0, y: 0, width, height },
|
|
44
|
+
resolution,
|
|
45
|
+
terminalWidth,
|
|
46
|
+
terminalHeight,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const scale = Math.max(1, pixelRatio);
|
|
51
|
+
return {
|
|
52
|
+
pixelWidth: Math.max(1, Math.round(width * cellWidthPx * scale)),
|
|
53
|
+
pixelHeight: Math.max(1, Math.round(height * cellHeightPx * scale)),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -143,6 +143,53 @@ describe("resolveNativeSurfaceVisibleRect", () => {
|
|
|
143
143
|
|
|
144
144
|
expect(resolveNativeSurfaceVisibleRect(child, 100, 30)).toBeNull();
|
|
145
145
|
});
|
|
146
|
+
|
|
147
|
+
test("stops at repeated ancestors in cyclic native renderable trees", () => {
|
|
148
|
+
const root: NativeSurfaceRenderableNode = {
|
|
149
|
+
x: 0,
|
|
150
|
+
y: 0,
|
|
151
|
+
width: 100,
|
|
152
|
+
height: 30,
|
|
153
|
+
parent: null,
|
|
154
|
+
};
|
|
155
|
+
const child: NativeSurfaceRenderableNode = {
|
|
156
|
+
x: 2,
|
|
157
|
+
y: 3,
|
|
158
|
+
width: 20,
|
|
159
|
+
height: 8,
|
|
160
|
+
parent: root,
|
|
161
|
+
};
|
|
162
|
+
root.parent = child;
|
|
163
|
+
|
|
164
|
+
expect(resolveNativeSurfaceVisibleRect(child, 100, 30)).toEqual({
|
|
165
|
+
x: 2,
|
|
166
|
+
y: 3,
|
|
167
|
+
width: 20,
|
|
168
|
+
height: 8,
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("fails closed when native renderable ancestors exceed a sane depth", () => {
|
|
173
|
+
let node: NativeSurfaceRenderableNode = {
|
|
174
|
+
x: 0,
|
|
175
|
+
y: 0,
|
|
176
|
+
width: 100,
|
|
177
|
+
height: 30,
|
|
178
|
+
parent: null,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
for (let index = 0; index < 300; index += 1) {
|
|
182
|
+
node = {
|
|
183
|
+
x: 0,
|
|
184
|
+
y: 0,
|
|
185
|
+
width: 100,
|
|
186
|
+
height: 30,
|
|
187
|
+
parent: node,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
expect(resolveNativeSurfaceVisibleRect(node, 100, 30)).toBeNull();
|
|
192
|
+
});
|
|
146
193
|
});
|
|
147
194
|
|
|
148
195
|
describe("renderNativeChartBase", () => {
|
|
@@ -9,6 +9,8 @@ export interface NativeSurfaceRenderableNode {
|
|
|
9
9
|
parent: NativeSurfaceRenderableNode | null;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const MAX_RENDERABLE_ANCESTOR_DEPTH = 256;
|
|
13
|
+
|
|
12
14
|
export function getRenderableCellRect(
|
|
13
15
|
renderable: Pick<NativeSurfaceRenderableNode, "x" | "y" | "width" | "height">,
|
|
14
16
|
): CellRect {
|
|
@@ -34,8 +36,13 @@ export function resolveNativeSurfaceVisibleRect(
|
|
|
34
36
|
height: terminalHeight,
|
|
35
37
|
};
|
|
36
38
|
let current: NativeSurfaceRenderableNode | null = renderable;
|
|
39
|
+
const seen = new Set<NativeSurfaceRenderableNode>();
|
|
40
|
+
let depth = 0;
|
|
37
41
|
|
|
38
|
-
while (current) {
|
|
42
|
+
while (current && !seen.has(current)) {
|
|
43
|
+
if (depth >= MAX_RENDERABLE_ANCESTOR_DEPTH) return null;
|
|
44
|
+
seen.add(current);
|
|
45
|
+
depth += 1;
|
|
39
46
|
if (current.visible === false) return null;
|
|
40
47
|
|
|
41
48
|
const nextVisible = intersectCellRects(visible, getRenderableCellRect(current));
|
|
@@ -12,6 +12,7 @@ interface PriceAxisLabelsProps {
|
|
|
12
12
|
cursorPixelY?: number | null;
|
|
13
13
|
cursorLabel: string | null;
|
|
14
14
|
cursorColor: string;
|
|
15
|
+
axisColor?: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
interface CursorPriceAxisOverlay {
|
|
@@ -61,6 +62,7 @@ export function PriceAxisLabels({
|
|
|
61
62
|
cursorPixelY = null,
|
|
62
63
|
cursorLabel,
|
|
63
64
|
cursorColor,
|
|
65
|
+
axisColor = colors.textDim,
|
|
64
66
|
}: PriceAxisLabelsProps) {
|
|
65
67
|
const { cellHeightPx = 18, fractionalViewport = false } = useUiCapabilities();
|
|
66
68
|
const overlay = useMemo(() => buildCursorPriceAxisOverlay({
|
|
@@ -100,7 +102,7 @@ export function PriceAxisLabels({
|
|
|
100
102
|
const label = isCursorRow ? cursorLabel : (axisLabels.get(row) ?? null);
|
|
101
103
|
return (
|
|
102
104
|
<Box key={row} height={1}>
|
|
103
|
-
{renderAxisLabel(label, isCursorRow ? cursorColor :
|
|
105
|
+
{renderAxisLabel(label, isCursorRow ? cursorColor : axisColor)}
|
|
104
106
|
</Box>
|
|
105
107
|
);
|
|
106
108
|
})}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { Text } from "../../../ui";
|
|
4
|
+
import { testRender } from "../../../renderers/opentui/test-utils";
|
|
5
|
+
import { StaticBarChartSurface } from "./bar-chart-surface";
|
|
6
|
+
|
|
7
|
+
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
if (!testSetup) return;
|
|
11
|
+
await act(async () => {
|
|
12
|
+
testSetup!.renderer.destroy();
|
|
13
|
+
});
|
|
14
|
+
testSetup = undefined;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("StaticBarChartSurface", () => {
|
|
18
|
+
test("shows hover feedback without shifting chart rows", async () => {
|
|
19
|
+
testSetup = await testRender(
|
|
20
|
+
<StaticBarChartSurface
|
|
21
|
+
width={64}
|
|
22
|
+
height={12}
|
|
23
|
+
title="Operating Cash Flow (quarterly)"
|
|
24
|
+
formatValue={(value) => `${Math.round(value / 1000)}k`}
|
|
25
|
+
series={[{
|
|
26
|
+
id: "value",
|
|
27
|
+
label: "Value",
|
|
28
|
+
color: "#00ff66",
|
|
29
|
+
points: [
|
|
30
|
+
{ category: "2025 Q1", value: -271000 },
|
|
31
|
+
{ category: "2025 Q2", value: -138000 },
|
|
32
|
+
{ category: "2025 Q3", value: 653000 },
|
|
33
|
+
],
|
|
34
|
+
}]}
|
|
35
|
+
/>,
|
|
36
|
+
{ width: 66, height: 14 },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
await act(async () => {
|
|
40
|
+
await testSetup!.renderOnce();
|
|
41
|
+
await testSetup!.renderOnce();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const initialFrame = testSetup.captureCharFrame();
|
|
45
|
+
const initialLines = initialFrame.split("\n");
|
|
46
|
+
const initialAxisRow = initialLines.findIndex((line) => line.includes("2025 Q1") && line.includes("2025 Q2"));
|
|
47
|
+
|
|
48
|
+
await act(async () => {
|
|
49
|
+
await testSetup!.mockMouse.moveTo(14, 4);
|
|
50
|
+
await testSetup!.renderOnce();
|
|
51
|
+
await testSetup!.renderOnce();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const hoverFrame = testSetup.captureCharFrame();
|
|
55
|
+
const hoverLines = hoverFrame.split("\n");
|
|
56
|
+
const hoverAxisRow = hoverLines.findIndex((line) => line.includes("2025 Q1") && line.includes("2025 Q2"));
|
|
57
|
+
|
|
58
|
+
expect(hoverFrame).toContain("2025 Q1: -271k");
|
|
59
|
+
expect(hoverFrame).toContain("▓");
|
|
60
|
+
expect(hoverLines).toHaveLength(initialLines.length);
|
|
61
|
+
expect(hoverAxisRow).toBe(initialAxisRow);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("shows a hover readout when a custom header is rendered", async () => {
|
|
65
|
+
testSetup = await testRender(
|
|
66
|
+
<StaticBarChartSurface
|
|
67
|
+
width={64}
|
|
68
|
+
height={12}
|
|
69
|
+
header={<Text>Revenue Gross Profit</Text>}
|
|
70
|
+
formatValue={(value) => `${Math.round(value / 1000)}k`}
|
|
71
|
+
series={[{
|
|
72
|
+
id: "value",
|
|
73
|
+
label: "Value",
|
|
74
|
+
color: "#00ff66",
|
|
75
|
+
points: [
|
|
76
|
+
{ category: "2025 Q1", value: -271000 },
|
|
77
|
+
{ category: "2025 Q2", value: -138000 },
|
|
78
|
+
{ category: "2025 Q3", value: 653000 },
|
|
79
|
+
],
|
|
80
|
+
}]}
|
|
81
|
+
/>,
|
|
82
|
+
{ width: 66, height: 14 },
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await act(async () => {
|
|
86
|
+
await testSetup!.renderOnce();
|
|
87
|
+
await testSetup!.renderOnce();
|
|
88
|
+
await testSetup!.mockMouse.moveTo(14, 4);
|
|
89
|
+
await testSetup!.renderOnce();
|
|
90
|
+
await testSetup!.renderOnce();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const frame = testSetup.captureCharFrame();
|
|
94
|
+
expect(frame).toContain("Revenue Gross Profit");
|
|
95
|
+
expect(frame).toContain("2025 Q1: -271k");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
|
-
import { Box, ChartSurface, Span, Text } from "../../../ui";
|
|
1
|
+
import { useCallback, useMemo, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import { Box, ChartSurface, Span, Text, useNativeRenderer, type BoxRenderable } from "../../../ui";
|
|
3
3
|
import { colors } from "../../../theme/colors";
|
|
4
|
+
import { consumeChartMouseEvent, getLocalPlotPointer, type ChartMouseEvent } from "../core/pointer";
|
|
4
5
|
import type { NativeChartBitmap } from "../native/chart-rasterizer";
|
|
6
|
+
import { truncateWithEllipsis } from "../../../utils/text-wrap";
|
|
5
7
|
import { useStaticChartBitmapSize } from "./chart/bitmap";
|
|
6
8
|
import {
|
|
7
9
|
buildBarChartScene,
|
|
8
10
|
renderBarChart,
|
|
9
11
|
renderBarChartAxis,
|
|
12
|
+
renderBarChartYAxis,
|
|
10
13
|
renderNativeBarChart,
|
|
14
|
+
resolveBarChartHover,
|
|
11
15
|
type BarChartColors,
|
|
16
|
+
type BarChartHover,
|
|
12
17
|
type BarChartSeries,
|
|
13
18
|
} from "../bar-chart-renderer";
|
|
14
19
|
|
|
@@ -17,42 +22,116 @@ export interface StaticBarChartSurfaceProps {
|
|
|
17
22
|
width: number;
|
|
18
23
|
height: number;
|
|
19
24
|
colors?: BarChartColors;
|
|
25
|
+
title?: string;
|
|
26
|
+
header?: ReactNode;
|
|
27
|
+
formatValue?: (value: number) => string;
|
|
28
|
+
onHoverChange?: (hover: BarChartHover | null) => void;
|
|
29
|
+
onMouseDown?: (event: ChartMouseEvent) => void;
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
export function StaticBarChartSurface({
|
|
23
33
|
series,
|
|
24
34
|
width,
|
|
25
35
|
height,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
title,
|
|
37
|
+
header,
|
|
38
|
+
formatValue,
|
|
39
|
+
onHoverChange,
|
|
40
|
+
onMouseDown,
|
|
41
|
+
colors: chartColors,
|
|
31
42
|
}: StaticBarChartSurfaceProps) {
|
|
43
|
+
const renderer = useNativeRenderer();
|
|
44
|
+
const plotRef = useRef<BoxRenderable | null>(null);
|
|
45
|
+
const [hover, setHover] = useState<BarChartHover | null>(null);
|
|
32
46
|
const totalWidth = Math.max(1, Math.floor(width));
|
|
33
47
|
const totalHeight = Math.max(4, Math.floor(height));
|
|
48
|
+
const headerRows = title || header ? 1 : 0;
|
|
34
49
|
const legendRows = series.length > 1 ? 1 : 0;
|
|
35
50
|
const axisRows = 1;
|
|
36
|
-
const
|
|
51
|
+
const yAxisWidth = totalWidth >= 24 ? 8 : 0;
|
|
52
|
+
const plotRightPadding = totalWidth >= 32 ? 1 : 0;
|
|
53
|
+
const plotWidth = Math.max(1, totalWidth - yAxisWidth - plotRightPadding);
|
|
54
|
+
const plotHeight = Math.max(2, totalHeight - headerRows - legendRows - axisRows);
|
|
55
|
+
const resolvedChartColors = useMemo<BarChartColors>(() => ({
|
|
56
|
+
bgColor: chartColors?.bgColor ?? colors.bg,
|
|
57
|
+
gridColor: chartColors?.gridColor ?? colors.border,
|
|
58
|
+
axisColor: chartColors?.axisColor ?? colors.textDim,
|
|
59
|
+
negativeColor: chartColors?.negativeColor ?? colors.negative,
|
|
60
|
+
hoverColor: chartColors?.hoverColor ?? colors.textBright,
|
|
61
|
+
}), [chartColors]);
|
|
37
62
|
const scene = useMemo(() => buildBarChartScene(series, {
|
|
38
|
-
width:
|
|
63
|
+
width: plotWidth,
|
|
39
64
|
height: plotHeight,
|
|
40
|
-
colors:
|
|
41
|
-
}), [
|
|
65
|
+
colors: resolvedChartColors,
|
|
66
|
+
}), [plotHeight, plotWidth, resolvedChartColors, series]);
|
|
42
67
|
|
|
43
|
-
const bitmapSize = useStaticChartBitmapSize(
|
|
68
|
+
const bitmapSize = useStaticChartBitmapSize(plotWidth, plotHeight);
|
|
44
69
|
|
|
45
70
|
const bitmap = useMemo<NativeChartBitmap | null>(() => {
|
|
46
71
|
if (!scene || !bitmapSize) return null;
|
|
47
|
-
return renderNativeBarChart(scene, bitmapSize.pixelWidth, bitmapSize.pixelHeight);
|
|
48
|
-
}, [bitmapSize, scene]);
|
|
72
|
+
return renderNativeBarChart(scene, bitmapSize.pixelWidth, bitmapSize.pixelHeight, hover);
|
|
73
|
+
}, [bitmapSize, hover, scene]);
|
|
49
74
|
|
|
50
|
-
const textLines = useMemo(() => scene ? renderBarChart(scene) : [], [scene]);
|
|
51
|
-
const axis = useMemo(() => scene ? renderBarChartAxis(scene) :
|
|
75
|
+
const textLines = useMemo(() => scene ? renderBarChart(scene, hover) : [], [hover, scene]);
|
|
76
|
+
const axis = useMemo(() => scene ? renderBarChartAxis(scene, axisRows) : [], [axisRows, scene]);
|
|
77
|
+
const yAxis = useMemo(() => scene ? renderBarChartYAxis(scene, yAxisWidth, formatValue) : [], [formatValue, scene, yAxisWidth]);
|
|
78
|
+
const hoverLabel = useMemo(() => {
|
|
79
|
+
if (!hover) return null;
|
|
80
|
+
const value = formatValue ? formatValue(hover.value) : String(hover.value);
|
|
81
|
+
return series.length > 1 ? `${hover.category} ${hover.seriesLabel}: ${value}` : `${hover.category}: ${value}`;
|
|
82
|
+
}, [formatValue, hover, series.length]);
|
|
83
|
+
const readout = useMemo(() => {
|
|
84
|
+
if (!hoverLabel) return title ?? "";
|
|
85
|
+
return title ? `${title} ${hoverLabel}` : hoverLabel;
|
|
86
|
+
}, [hoverLabel, title]);
|
|
87
|
+
const hoverLabelLayout = useMemo(() => {
|
|
88
|
+
if (!hover || !hoverLabel) return null;
|
|
89
|
+
const text = truncateWithEllipsis(hoverLabel, plotWidth);
|
|
90
|
+
const labelWidth = Math.min(plotWidth, Math.max(1, text.length));
|
|
91
|
+
const labelLeft = Math.max(0, Math.min(
|
|
92
|
+
plotWidth - labelWidth,
|
|
93
|
+
hover.x + Math.floor(hover.width / 2) - Math.floor(labelWidth / 2),
|
|
94
|
+
));
|
|
95
|
+
const labelTop = hover.value >= 0
|
|
96
|
+
? Math.max(0, hover.row - 1)
|
|
97
|
+
: Math.min(plotHeight - 1, hover.row + 1);
|
|
98
|
+
return { labelLeft, labelTop, labelWidth, text };
|
|
99
|
+
}, [hover, hoverLabel, plotHeight, plotWidth]);
|
|
100
|
+
|
|
101
|
+
const handleMouseMove = useCallback((event: ChartMouseEvent) => {
|
|
102
|
+
if (!scene) return;
|
|
103
|
+
const pointer = getLocalPlotPointer(event, plotRef.current, renderer);
|
|
104
|
+
if (!pointer) {
|
|
105
|
+
setHover(null);
|
|
106
|
+
onHoverChange?.(null);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const nextHover = resolveBarChartHover(scene, pointer.cellX);
|
|
110
|
+
setHover(nextHover);
|
|
111
|
+
onHoverChange?.(nextHover);
|
|
112
|
+
consumeChartMouseEvent(event);
|
|
113
|
+
}, [onHoverChange, renderer, scene]);
|
|
114
|
+
|
|
115
|
+
const handleMouseOut = useCallback(() => {
|
|
116
|
+
setHover(null);
|
|
117
|
+
onHoverChange?.(null);
|
|
118
|
+
}, [onHoverChange]);
|
|
119
|
+
|
|
120
|
+
const handleMouseDown = useCallback((event: ChartMouseEvent) => {
|
|
121
|
+
onMouseDown?.(event);
|
|
122
|
+
if (!scene) return;
|
|
123
|
+
const pointer = getLocalPlotPointer(event, plotRef.current, renderer);
|
|
124
|
+
if (!pointer) return;
|
|
125
|
+
const nextHover = resolveBarChartHover(scene, pointer.cellX);
|
|
126
|
+
setHover(nextHover);
|
|
127
|
+
onHoverChange?.(nextHover);
|
|
128
|
+
consumeChartMouseEvent(event);
|
|
129
|
+
}, [onHoverChange, onMouseDown, renderer, scene]);
|
|
52
130
|
|
|
53
131
|
if (!scene) {
|
|
54
132
|
return (
|
|
55
|
-
<Box width={totalWidth} height={totalHeight}>
|
|
133
|
+
<Box flexDirection="column" width={totalWidth} height={totalHeight} onMouseDown={onMouseDown}>
|
|
134
|
+
{header ?? (title ? <Text fg={colors.textBright} bold>{truncateWithEllipsis(title, totalWidth)}</Text> : null)}
|
|
56
135
|
<Text fg={colors.textDim}>No chart data</Text>
|
|
57
136
|
</Box>
|
|
58
137
|
);
|
|
@@ -60,6 +139,13 @@ export function StaticBarChartSurface({
|
|
|
60
139
|
|
|
61
140
|
return (
|
|
62
141
|
<Box flexDirection="column" width={totalWidth} height={totalHeight}>
|
|
142
|
+
{headerRows ? (
|
|
143
|
+
header ?? (
|
|
144
|
+
<Text fg={hover ? hover.value < 0 ? colors.negative : colors.textBright : colors.textDim} bold={!!title}>
|
|
145
|
+
{truncateWithEllipsis(readout, totalWidth)}
|
|
146
|
+
</Text>
|
|
147
|
+
)
|
|
148
|
+
) : null}
|
|
63
149
|
{legendRows ? (
|
|
64
150
|
<Text>
|
|
65
151
|
{series.map((item, index) => (
|
|
@@ -69,17 +155,54 @@ export function StaticBarChartSurface({
|
|
|
69
155
|
))}
|
|
70
156
|
</Text>
|
|
71
157
|
) : null}
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
158
|
+
<Box flexDirection="row" width={totalWidth} height={plotHeight}>
|
|
159
|
+
{yAxisWidth ? (
|
|
160
|
+
<Box flexDirection="column" width={yAxisWidth} height={plotHeight}>
|
|
161
|
+
{yAxis.map((line, index) => (
|
|
162
|
+
<Text key={index} fg={colors.textDim}>{line}</Text>
|
|
163
|
+
))}
|
|
164
|
+
</Box>
|
|
165
|
+
) : null}
|
|
166
|
+
<Box position="relative" width={plotWidth} height={plotHeight}>
|
|
167
|
+
<ChartSurface
|
|
168
|
+
ref={plotRef}
|
|
169
|
+
width={plotWidth}
|
|
170
|
+
height={plotHeight}
|
|
171
|
+
flexDirection="column"
|
|
172
|
+
bitmaps={bitmap ? [bitmap] : null}
|
|
173
|
+
onMouseMove={handleMouseMove}
|
|
174
|
+
onMouseDown={handleMouseDown}
|
|
175
|
+
onMouseOut={handleMouseOut}
|
|
176
|
+
cursor="pointer"
|
|
177
|
+
data-gloom-interactive="true"
|
|
178
|
+
data-gloom-role="bar-chart-surface"
|
|
179
|
+
>
|
|
180
|
+
{textLines.map((line, index) => (
|
|
181
|
+
<Text key={index} fg={colors.text}>{line}</Text>
|
|
182
|
+
))}
|
|
183
|
+
</ChartSurface>
|
|
184
|
+
{hoverLabelLayout ? (
|
|
185
|
+
<Box
|
|
186
|
+
position="absolute"
|
|
187
|
+
left={hoverLabelLayout.labelLeft}
|
|
188
|
+
top={hoverLabelLayout.labelTop}
|
|
189
|
+
width={hoverLabelLayout.labelWidth}
|
|
190
|
+
height={1}
|
|
191
|
+
zIndex={1}
|
|
192
|
+
backgroundColor={colors.panel}
|
|
193
|
+
>
|
|
194
|
+
<Text fg={hover && hover.value < 0 ? colors.negative : colors.textBright} bold>
|
|
195
|
+
{hoverLabelLayout.text}
|
|
196
|
+
</Text>
|
|
197
|
+
</Box>
|
|
198
|
+
) : null}
|
|
199
|
+
</Box>
|
|
200
|
+
</Box>
|
|
201
|
+
{axis.map((line, index) => (
|
|
202
|
+
<Text key={index} fg={colors.textDim}>
|
|
203
|
+
{yAxisWidth ? `${" ".repeat(yAxisWidth)}${line}` : line}
|
|
204
|
+
</Text>
|
|
205
|
+
))}
|
|
83
206
|
</Box>
|
|
84
207
|
);
|
|
85
208
|
}
|