gloomberb 0.9.2 → 0.9.4
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 +24 -2
- package/bin/gloomberb +1 -1
- package/package.json +1 -1
- package/src/app/runtime/desktop-menu.ts +5 -1
- package/src/app/runtime/ticker-refresh.ts +4 -1
- package/src/app.tsx +20 -8
- 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/capabilities/registry.ts +1 -0
- package/src/capabilities/types.ts +23 -0
- package/src/cli/commands/api.ts +119 -0
- package/src/cli/commands/automation.ts +173 -0
- package/src/cli/commands/command-utils.ts +53 -0
- package/src/cli/commands/market.ts +527 -0
- package/src/cli/commands/overview.ts +222 -0
- package/src/cli/commands/search.ts +39 -0
- package/src/cli/commands/system.ts +553 -0
- package/src/cli/commands/ticker.ts +104 -12
- package/src/cli/context.ts +35 -2
- package/src/cli/desktop-pane-shot.ts +2 -1
- package/src/cli/entry.ts +51 -0
- package/src/cli/errors.ts +56 -5
- package/src/cli/index.ts +73 -11
- package/src/cli/options.test.ts +38 -0
- package/src/cli/options.ts +95 -0
- package/src/cli/pane-functions/index.ts +10 -1
- package/src/cli/pane-functions/screenshot.ts +13 -1
- package/src/cli/registry.test.ts +40 -0
- package/src/cli/registry.ts +46 -5
- package/src/cli/result.test.ts +82 -0
- package/src/cli/result.ts +149 -0
- package/src/cli/types.ts +11 -0
- package/src/cli.test.ts +11 -11
- 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/comparison-stock-chart/interactions.ts +7 -1
- package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
- package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
- package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
- package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
- package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
- package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
- package/src/components/chart/core/pane-settings.test.ts +27 -0
- package/src/components/chart/core/pane-settings.ts +60 -1
- package/src/components/chart/core/pointer.ts +34 -6
- 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/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/chart/stock-chart/resolved/runtime.ts +1 -1
- package/src/components/chart/stock-chart/settings.ts +7 -3
- package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
- package/src/components/command-bar/layout-items/current-layout.ts +5 -1
- package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
- package/src/components/command-bar/surface/pane.test.tsx +76 -0
- package/src/components/data-table/view.tsx +13 -2
- package/src/components/feed-data-table/stack-view.tsx +9 -1
- package/src/components/index.ts +1 -1
- package/src/components/input-search-bar.test.tsx +103 -0
- package/src/components/input-search-bar.tsx +16 -0
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +3 -2
- 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 +40 -4
- package/src/components/layout/pane/index.tsx +3 -1
- package/src/components/layout/pane/sizing.test.ts +43 -0
- package/src/components/layout/pane/sizing.ts +13 -4
- package/src/components/layout/shell/active-drag.ts +5 -1
- package/src/components/layout/shell/cursor-occlusion.ts +196 -0
- package/src/components/layout/shell/drag/runtime.ts +28 -3
- package/src/components/layout/shell/fullscreen.ts +10 -0
- package/src/components/layout/shell/index.test.tsx +487 -4
- package/src/components/layout/shell/index.tsx +165 -4
- 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/native/pointer-runtime.ts +10 -4
- package/src/components/layout/shell/pane/layers.tsx +34 -16
- package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
- package/src/components/layout/shell/shortcuts.ts +4 -0
- package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
- package/src/components/layout/shell/visible-layout.ts +17 -0
- package/src/components/layout/shell/window-mode/index.ts +11 -3
- package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
- package/src/components/layout/status-bar.test.tsx +111 -1
- package/src/components/layout/status-bar.tsx +37 -7
- package/src/components/layout/transient-layout.tsx +57 -0
- package/src/components/layout/window-edit/mode.test.ts +76 -5
- package/src/components/layout/window-edit/mode.ts +47 -2
- package/src/components/layout/window-edit/presentation.ts +2 -1
- 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/price-performance.tsx +9 -1
- package/src/components/table-view-shared.tsx +12 -4
- package/src/components/ui/data-table/opentui/index.tsx +25 -12
- package/src/components/ui/data-table/types.ts +4 -0
- package/src/components/ui/fields.tsx +10 -3
- 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 +26 -10
- package/src/components/ui/ui.test.tsx +87 -0
- package/src/core/app-services.ts +7 -4
- package/src/core/state/app/layout-reducer.ts +2 -2
- package/src/data/config/store/index.ts +1 -2
- package/src/plugins/builtin/analytics/index.test.tsx +6 -2
- package/src/plugins/builtin/analytics/pane-model.ts +43 -8
- package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
- package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
- 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/news/wire/read-state.ts +9 -32
- 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/cli/portfolio-command.ts +23 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
- package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +3 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
- 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/shared/read-state.ts +43 -0
- package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
- package/src/plugins/builtin/substack/article-detail.tsx +39 -15
- package/src/plugins/builtin/substack/article-stack.tsx +6 -2
- package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
- package/src/plugins/builtin/substack/pane.tsx +8 -3
- package/src/plugins/builtin/substack/read-state.ts +55 -0
- package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
- 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/pane-manager/docking.ts +9 -2
- package/src/plugins/pane-manager/gridlock.ts +13 -5
- package/src/plugins/pane-manager/layout-state.ts +25 -0
- package/src/plugins/pane-manager/queries.ts +8 -2
- 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/controller/index.ts +2 -0
- package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
- 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 +75 -0
- package/src/plugins/prediction-markets/pane.tsx +71 -23
- package/src/plugins/prediction-markets/test-helpers.tsx +42 -23
- package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/index.ts +13 -0
- package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
- package/src/renderers/electrobun/bun/window/focus.ts +5 -0
- package/src/renderers/electrobun/view/app-services.ts +6 -4
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
- package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
- package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
- package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
- package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
- package/src/renderers/electrobun/view/host/input.tsx +115 -18
- package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
- package/src/renderers/electrobun/view/host/text.test.ts +22 -0
- package/src/renderers/electrobun/view/host/text.tsx +24 -20
- package/src/renderers/electrobun/view/styles.css +11 -0
- package/src/renderers/electrobun/view/ui-host.tsx +1 -1
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/start.tsx +13 -5
- 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/state/app/input-capture.ts +12 -0
- package/src/types/broker.ts +1 -0
- package/src/types/config.ts +10 -0
- package/src/types/plugin.ts +28 -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/cli-output.ts +6 -0
- package/src/utils/financial-statements.ts +23 -0
- package/src/utils/search-focus-navigation.ts +24 -0
- package/src/version.ts +1 -1
- package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { NativeChartBitmap } from "./native/chart-rasterizer";
|
|
2
2
|
import { fillRect, parseHex } from "./native/raster/primitives";
|
|
3
|
+
import { formatCompact } from "../../utils/format";
|
|
3
4
|
|
|
4
5
|
interface BarChartPoint {
|
|
5
6
|
category: string;
|
|
@@ -17,6 +18,8 @@ export interface BarChartColors {
|
|
|
17
18
|
bgColor: string;
|
|
18
19
|
gridColor: string;
|
|
19
20
|
axisColor: string;
|
|
21
|
+
negativeColor?: string;
|
|
22
|
+
hoverColor?: string;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export interface BarChartSceneOptions {
|
|
@@ -25,8 +28,9 @@ export interface BarChartSceneOptions {
|
|
|
25
28
|
colors: BarChartColors;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
interface BarChartBar {
|
|
31
|
+
export interface BarChartBar {
|
|
29
32
|
seriesId: string;
|
|
33
|
+
seriesLabel: string;
|
|
30
34
|
category: string;
|
|
31
35
|
value: number;
|
|
32
36
|
color: string;
|
|
@@ -47,8 +51,35 @@ export interface BarChartScene {
|
|
|
47
51
|
colors: BarChartColors;
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
export interface BarChartHover {
|
|
55
|
+
seriesId: string;
|
|
56
|
+
seriesLabel: string;
|
|
57
|
+
category: string;
|
|
58
|
+
value: number;
|
|
59
|
+
color: string;
|
|
60
|
+
x: number;
|
|
61
|
+
width: number;
|
|
62
|
+
row: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface NativeBarChartPixelBounds {
|
|
66
|
+
left: number;
|
|
67
|
+
rightExclusive: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
|
|
51
71
|
|
|
72
|
+
function categorySlotBounds(width: number, categoryCount: number, index: number): { left: number; width: number } {
|
|
73
|
+
const left = Math.floor((index * width) / categoryCount);
|
|
74
|
+
const right = index === categoryCount - 1
|
|
75
|
+
? width
|
|
76
|
+
: Math.floor(((index + 1) * width) / categoryCount);
|
|
77
|
+
return {
|
|
78
|
+
left,
|
|
79
|
+
width: Math.max(1, right - left),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
52
83
|
function collectCategories(series: BarChartSeries[]): string[] {
|
|
53
84
|
const categories: string[] = [];
|
|
54
85
|
const seen = new Set<string>();
|
|
@@ -64,14 +95,19 @@ function collectCategories(series: BarChartSeries[]): string[] {
|
|
|
64
95
|
|
|
65
96
|
function normalizeRange(values: number[]): { min: number; max: number } {
|
|
66
97
|
if (values.length === 0) return { min: 0, max: 1 };
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
98
|
+
const dataMin = Math.min(...values);
|
|
99
|
+
const dataMax = Math.max(...values);
|
|
100
|
+
if (dataMin === dataMax) {
|
|
101
|
+
if (dataMin === 0) return { min: -1, max: 1 };
|
|
102
|
+
const delta = Math.max(Math.abs(dataMin) * 0.1, 1);
|
|
103
|
+
return dataMin > 0
|
|
104
|
+
? { min: 0, max: dataMax + delta }
|
|
105
|
+
: { min: dataMin - delta, max: 0 };
|
|
72
106
|
}
|
|
73
|
-
const padding = (
|
|
74
|
-
return { min:
|
|
107
|
+
const padding = (dataMax - dataMin) * 0.08;
|
|
108
|
+
if (dataMin >= 0) return { min: 0, max: dataMax + padding };
|
|
109
|
+
if (dataMax <= 0) return { min: dataMin - padding, max: 0 };
|
|
110
|
+
return { min: dataMin - padding, max: dataMax + padding };
|
|
75
111
|
}
|
|
76
112
|
|
|
77
113
|
function valueToRow(value: number, min: number, max: number, height: number): number {
|
|
@@ -79,6 +115,12 @@ function valueToRow(value: number, min: number, max: number, height: number): nu
|
|
|
79
115
|
return clamp(Math.round((1 - t) * (height - 1)), 0, height - 1);
|
|
80
116
|
}
|
|
81
117
|
|
|
118
|
+
function rowToValue(row: number, scene: Pick<BarChartScene, "height" | "min" | "max">): number {
|
|
119
|
+
if (scene.height <= 1) return scene.max;
|
|
120
|
+
const t = 1 - row / Math.max(scene.height - 1, 1);
|
|
121
|
+
return scene.min + t * (scene.max - scene.min);
|
|
122
|
+
}
|
|
123
|
+
|
|
82
124
|
export function buildBarChartScene(series: BarChartSeries[], options: BarChartSceneOptions): BarChartScene | null {
|
|
83
125
|
const width = Math.max(1, Math.floor(options.width));
|
|
84
126
|
const height = Math.max(1, Math.floor(options.height));
|
|
@@ -91,23 +133,32 @@ export function buildBarChartScene(series: BarChartSeries[], options: BarChartSc
|
|
|
91
133
|
|
|
92
134
|
const { min, max } = normalizeRange(values);
|
|
93
135
|
const zeroRow = valueToRow(0, min, max, height);
|
|
94
|
-
const groupWidth = Math.max(1, Math.floor(width / categories.length));
|
|
95
136
|
const seriesCount = Math.max(series.length, 1);
|
|
96
|
-
const
|
|
137
|
+
const categorySlots = categories.map((_, index) => categorySlotBounds(width, categories.length, index));
|
|
138
|
+
const minSlotWidth = Math.max(1, Math.min(...categorySlots.map((slot) => slot.width)));
|
|
139
|
+
const groupedInnerWidth = Math.max(seriesCount, minSlotWidth - (minSlotWidth > seriesCount * 2 ? 1 : 0));
|
|
140
|
+
const groupedBarWidth = Math.max(1, Math.floor(groupedInnerWidth / seriesCount));
|
|
97
141
|
const bars: BarChartBar[] = [];
|
|
98
142
|
|
|
99
143
|
categories.forEach((category, categoryIndex) => {
|
|
100
|
-
const
|
|
144
|
+
const group = categorySlots[categoryIndex]!;
|
|
145
|
+
const barWidth = seriesCount === 1 ? group.width : groupedBarWidth;
|
|
146
|
+
const barGroupWidth = barWidth * seriesCount;
|
|
147
|
+
const groupOffset = Math.max(0, Math.floor((group.width - barGroupWidth) / 2));
|
|
101
148
|
series.forEach((item, seriesIndex) => {
|
|
102
149
|
const value = item.points.find((point) => point.category === category)?.value;
|
|
103
150
|
if (typeof value !== "number" || !Number.isFinite(value)) return;
|
|
151
|
+
const color = value < 0 && options.colors.negativeColor
|
|
152
|
+
? options.colors.negativeColor
|
|
153
|
+
: item.color;
|
|
104
154
|
bars.push({
|
|
105
155
|
seriesId: item.id,
|
|
156
|
+
seriesLabel: item.label,
|
|
106
157
|
category,
|
|
107
158
|
value,
|
|
108
|
-
color
|
|
109
|
-
x: clamp(
|
|
110
|
-
width: Math.min(barWidth, Math.max(width -
|
|
159
|
+
color,
|
|
160
|
+
x: clamp(group.left + groupOffset + seriesIndex * barWidth, 0, width - 1),
|
|
161
|
+
width: Math.min(barWidth, Math.max(width - (group.left + groupOffset + seriesIndex * barWidth), 1)),
|
|
111
162
|
row: valueToRow(value, min, max, height),
|
|
112
163
|
});
|
|
113
164
|
});
|
|
@@ -126,18 +177,81 @@ export function buildBarChartScene(series: BarChartSeries[], options: BarChartSc
|
|
|
126
177
|
};
|
|
127
178
|
}
|
|
128
179
|
|
|
129
|
-
|
|
180
|
+
function labelAtRow(labels: Array<{ row: number; value: number }>, row: number): number | null {
|
|
181
|
+
const match = labels.find((label) => label.row === row);
|
|
182
|
+
return match ? match.value : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function barChartYAxisTicks(scene: BarChartScene): Array<{ row: number; value: number }> {
|
|
186
|
+
const ticksByRow = new Map<number, number>();
|
|
187
|
+
const addTick = (row: number, value: number) => {
|
|
188
|
+
ticksByRow.set(clamp(row, 0, scene.height - 1), value);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
addTick(0, scene.max);
|
|
192
|
+
for (let i = 1; i <= 3; i++) {
|
|
193
|
+
const row = Math.round((scene.height - 1) * (i / 4));
|
|
194
|
+
addTick(row, rowToValue(row, scene));
|
|
195
|
+
}
|
|
196
|
+
addTick(scene.zeroRow, 0);
|
|
197
|
+
addTick(scene.height - 1, scene.min);
|
|
198
|
+
|
|
199
|
+
return [...ticksByRow.entries()]
|
|
200
|
+
.map(([row, value]) => ({ row, value }))
|
|
201
|
+
.sort((left, right) => left.row - right.row);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function renderBarChartYAxis(
|
|
205
|
+
scene: BarChartScene,
|
|
206
|
+
width: number,
|
|
207
|
+
formatValue: (value: number) => string = formatCompact,
|
|
208
|
+
): string[] {
|
|
209
|
+
const axisWidth = Math.max(0, Math.floor(width));
|
|
210
|
+
if (axisWidth === 0) return Array.from({ length: scene.height }, () => "");
|
|
211
|
+
|
|
212
|
+
const ticks = barChartYAxisTicks(scene);
|
|
213
|
+
return Array.from({ length: scene.height }, (_unused, row) => {
|
|
214
|
+
const value = labelAtRow(ticks, row);
|
|
215
|
+
const label = value === null ? "" : formatValue(value);
|
|
216
|
+
return `${label.slice(0, Math.max(axisWidth - 1, 0)).padStart(Math.max(axisWidth - 1, 0))} `;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function resolveBarChartHover(scene: BarChartScene, cellX: number): BarChartHover | null {
|
|
221
|
+
if (!Number.isFinite(cellX) || scene.bars.length === 0) return null;
|
|
222
|
+
const x = clamp(Math.floor(cellX), 0, scene.width - 1);
|
|
223
|
+
const direct = scene.bars.find((bar) => x >= bar.x && x < bar.x + bar.width);
|
|
224
|
+
const bar = direct ?? scene.bars.reduce((closest, current) => {
|
|
225
|
+
const closestCenter = closest.x + closest.width / 2;
|
|
226
|
+
const currentCenter = current.x + current.width / 2;
|
|
227
|
+
return Math.abs(currentCenter - x) < Math.abs(closestCenter - x) ? current : closest;
|
|
228
|
+
}, scene.bars[0]!);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
seriesId: bar.seriesId,
|
|
232
|
+
seriesLabel: bar.seriesLabel,
|
|
233
|
+
category: bar.category,
|
|
234
|
+
value: bar.value,
|
|
235
|
+
color: bar.color,
|
|
236
|
+
x: bar.x,
|
|
237
|
+
width: bar.width,
|
|
238
|
+
row: bar.row,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function renderBarChart(scene: BarChartScene, hover: BarChartHover | null = null): string[] {
|
|
130
243
|
const rows = Array.from({ length: scene.height }, () => Array(scene.width).fill(" "));
|
|
131
244
|
for (let x = 0; x < scene.width; x++) {
|
|
132
245
|
rows[scene.zeroRow]![x] = "─";
|
|
133
246
|
}
|
|
134
247
|
|
|
135
248
|
for (const bar of scene.bars) {
|
|
249
|
+
const fill = hover && hover.seriesId === bar.seriesId && hover.category === bar.category ? "▓" : "█";
|
|
136
250
|
const top = Math.min(bar.row, scene.zeroRow);
|
|
137
251
|
const bottom = Math.max(bar.row, scene.zeroRow);
|
|
138
252
|
for (let y = top; y <= bottom; y++) {
|
|
139
253
|
for (let x = bar.x; x < Math.min(bar.x + bar.width, scene.width); x++) {
|
|
140
|
-
rows[y]![x] =
|
|
254
|
+
rows[y]![x] = fill;
|
|
141
255
|
}
|
|
142
256
|
}
|
|
143
257
|
}
|
|
@@ -145,26 +259,101 @@ export function renderBarChart(scene: BarChartScene): string[] {
|
|
|
145
259
|
return rows.map((row) => row.join(""));
|
|
146
260
|
}
|
|
147
261
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
262
|
+
function centeredLabelStart(center: number, label: string, width: number, rightInset = 0): number {
|
|
263
|
+
return clamp(Math.round(center - label.length / 2), 0, Math.max(0, width - label.length - rightInset));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function compactCategoryLabel(category: string, previous: string | undefined, categoryCount: number): string {
|
|
267
|
+
const quarterMatch = category.match(/^(\d{4}) Q([1-4])$/);
|
|
268
|
+
if (!quarterMatch) return category;
|
|
269
|
+
const year = quarterMatch[1]!;
|
|
270
|
+
const previousYear = previous?.match(/^(\d{4}) Q[1-4]$/)?.[1];
|
|
271
|
+
if (categoryCount > 10) return previousYear === year ? "" : year;
|
|
272
|
+
return category;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function canPlaceLabel(row: string[], start: number, label: string, minGap: number): boolean {
|
|
276
|
+
const left = Math.max(0, start - minGap);
|
|
277
|
+
const right = Math.min(row.length - 1, start + label.length + minGap - 1);
|
|
278
|
+
for (let index = left; index <= right; index++) {
|
|
279
|
+
if (row[index] !== " ") return false;
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function renderBarChartAxis(scene: BarChartScene, rowCount = 1): string[] {
|
|
285
|
+
const rows = Array.from({ length: Math.max(1, Math.floor(rowCount)) }, () => Array(scene.width).fill(" "));
|
|
286
|
+
const minGap = scene.categories.length > 10 ? 3 : 1;
|
|
287
|
+
const rightInset = scene.categories.length > 10 ? 2 : 0;
|
|
151
288
|
scene.categories.forEach((category, index) => {
|
|
152
|
-
const label = category
|
|
153
|
-
|
|
289
|
+
const label = compactCategoryLabel(category, scene.categories[index - 1], scene.categories.length);
|
|
290
|
+
if (!label) return;
|
|
291
|
+
const group = categorySlotBounds(scene.width, scene.categories.length, index);
|
|
292
|
+
const start = centeredLabelStart(group.left + group.width / 2, label, scene.width, rightInset);
|
|
293
|
+
const row = rows.find((candidate) => canPlaceLabel(candidate, start, label, minGap));
|
|
294
|
+
if (!row) return;
|
|
154
295
|
for (let i = 0; i < label.length && start + i < scene.width; i++) {
|
|
155
|
-
|
|
296
|
+
row[start + i] = label[i]!;
|
|
156
297
|
}
|
|
157
298
|
});
|
|
158
|
-
return
|
|
299
|
+
return rows.map((row) => row.join(""));
|
|
159
300
|
}
|
|
160
301
|
|
|
161
|
-
export function
|
|
302
|
+
export function resolveNativeBarPixelBounds(
|
|
303
|
+
scene: BarChartScene,
|
|
304
|
+
bar: BarChartBar,
|
|
305
|
+
pixelWidth: number,
|
|
306
|
+
): NativeBarChartPixelBounds {
|
|
307
|
+
const width = Math.max(1, Math.floor(pixelWidth));
|
|
308
|
+
|
|
309
|
+
if (scene.series.length === 1) {
|
|
310
|
+
const categoryIndex = scene.categories.indexOf(bar.category);
|
|
311
|
+
if (categoryIndex >= 0) {
|
|
312
|
+
const separatorWidth = scene.categories.length > 1 && width >= scene.categories.length * 3 ? 1 : 0;
|
|
313
|
+
const usableWidth = Math.max(
|
|
314
|
+
scene.categories.length,
|
|
315
|
+
width - separatorWidth * (scene.categories.length - 1),
|
|
316
|
+
);
|
|
317
|
+
const left = Math.floor((categoryIndex * usableWidth) / scene.categories.length)
|
|
318
|
+
+ categoryIndex * separatorWidth;
|
|
319
|
+
const rightExclusive = categoryIndex === scene.categories.length - 1
|
|
320
|
+
? width
|
|
321
|
+
: Math.floor(((categoryIndex + 1) * usableWidth) / scene.categories.length)
|
|
322
|
+
+ categoryIndex * separatorWidth;
|
|
323
|
+
return {
|
|
324
|
+
left,
|
|
325
|
+
rightExclusive: Math.max(left + 1, rightExclusive),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const horizontalInset = 1;
|
|
331
|
+
const left = clamp(
|
|
332
|
+
Math.floor((bar.x / scene.width) * width + horizontalInset),
|
|
333
|
+
0,
|
|
334
|
+
width - 1,
|
|
335
|
+
);
|
|
336
|
+
const rightExclusive = clamp(
|
|
337
|
+
Math.ceil(((bar.x + bar.width) / scene.width) * width - horizontalInset),
|
|
338
|
+
left + 1,
|
|
339
|
+
width,
|
|
340
|
+
);
|
|
341
|
+
return { left, rightExclusive };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function renderNativeBarChart(
|
|
345
|
+
scene: BarChartScene,
|
|
346
|
+
pixelWidth: number,
|
|
347
|
+
pixelHeight: number,
|
|
348
|
+
hover: BarChartHover | null = null,
|
|
349
|
+
): NativeChartBitmap {
|
|
162
350
|
const width = Math.max(1, Math.floor(pixelWidth));
|
|
163
351
|
const height = Math.max(1, Math.floor(pixelHeight));
|
|
164
352
|
const data = new Uint8Array(width * height * 4);
|
|
165
353
|
const bg = parseHex(scene.colors.bgColor, 1);
|
|
166
354
|
const grid = parseHex(scene.colors.gridColor, 0.45);
|
|
167
355
|
const axis = parseHex(scene.colors.axisColor, 0.75);
|
|
356
|
+
const hoverColor = parseHex(scene.colors.hoverColor ?? scene.colors.axisColor, 1);
|
|
168
357
|
|
|
169
358
|
fillRect(data, width, height, 0, 0, width - 1, height - 1, bg, 1);
|
|
170
359
|
|
|
@@ -177,13 +366,23 @@ export function renderNativeBarChart(scene: BarChartScene, pixelWidth: number, p
|
|
|
177
366
|
fillRect(data, width, height, 0, zeroY, width - 1, zeroY + 1.1, axis, 0.8);
|
|
178
367
|
|
|
179
368
|
for (const bar of scene.bars) {
|
|
369
|
+
const isHovered = !!hover
|
|
370
|
+
&& hover.seriesId === bar.seriesId
|
|
371
|
+
&& hover.category === bar.category;
|
|
180
372
|
const color = parseHex(bar.color, 0.96);
|
|
181
|
-
const
|
|
182
|
-
const
|
|
373
|
+
const bounds = resolveNativeBarPixelBounds(scene, bar, width);
|
|
374
|
+
const left = bounds.left;
|
|
375
|
+
const right = bounds.rightExclusive - 1;
|
|
183
376
|
const valueY = clamp((bar.row / Math.max(scene.height - 1, 1)) * (height - 1), 0, height - 1);
|
|
184
377
|
const top = Math.min(valueY, zeroY);
|
|
185
378
|
const bottom = Math.max(valueY, zeroY);
|
|
186
|
-
fillRect(data, width, height, left, top,
|
|
379
|
+
fillRect(data, width, height, left, top, right, Math.max(top + 1, bottom), color, 1);
|
|
380
|
+
if (isHovered) {
|
|
381
|
+
fillRect(data, width, height, left, top, right, top + 1.3, hoverColor, 1);
|
|
382
|
+
fillRect(data, width, height, left, bottom - 1.3, right, bottom, hoverColor, 1);
|
|
383
|
+
fillRect(data, width, height, left, top, left + 1.3, bottom, hoverColor, 1);
|
|
384
|
+
fillRect(data, width, height, right - 1.3, top, right, bottom, hoverColor, 1);
|
|
385
|
+
}
|
|
187
386
|
}
|
|
188
387
|
|
|
189
388
|
return { width, height, pixels: data };
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
resolveCursorMotionKind,
|
|
29
29
|
type ChartMouseEvent,
|
|
30
30
|
type DisplayCursorState,
|
|
31
|
+
type MouseInteractionEvent,
|
|
31
32
|
} from "../core/pointer";
|
|
32
33
|
import type {
|
|
33
34
|
ComparisonChartSeries,
|
|
@@ -48,6 +49,7 @@ interface UseComparisonChartPointerInteractionsOptions {
|
|
|
48
49
|
chartWidth: number;
|
|
49
50
|
commitSelectionCursor: (next: { cursorX: number | null; cursorY: number | null }) => void;
|
|
50
51
|
expandBufferRange: (action: PendingExpansionAction) => boolean;
|
|
52
|
+
focusPaneForMouseInteraction: (event: MouseInteractionEvent | null | undefined) => void;
|
|
51
53
|
mouseCrosshairDisabledRef: MutableRefObject<boolean>;
|
|
52
54
|
plotRef: RefObject<BoxRenderable | null>;
|
|
53
55
|
pointCount: number;
|
|
@@ -65,6 +67,7 @@ export function useComparisonChartPointerInteractions({
|
|
|
65
67
|
chartWidth,
|
|
66
68
|
commitSelectionCursor,
|
|
67
69
|
expandBufferRange,
|
|
70
|
+
focusPaneForMouseInteraction,
|
|
68
71
|
mouseCrosshairDisabledRef,
|
|
69
72
|
plotRef,
|
|
70
73
|
pointCount,
|
|
@@ -113,13 +116,14 @@ export function useComparisonChartPointerInteractions({
|
|
|
113
116
|
|
|
114
117
|
const handlePlotDown = useCallback((event: ChartMouseEvent) => {
|
|
115
118
|
consumeChartMouseEvent(event);
|
|
119
|
+
focusPaneForMouseInteraction(null);
|
|
116
120
|
mouseCrosshairDisabledRef.current = false;
|
|
117
121
|
if (!syncPointerCursor(event)) return;
|
|
118
122
|
dragRef.current = {
|
|
119
123
|
startGlobalX: getGlobalMouseX(event, renderer),
|
|
120
124
|
startWindow: visibleDateWindow,
|
|
121
125
|
};
|
|
122
|
-
}, [mouseCrosshairDisabledRef, renderer, syncPointerCursor, visibleDateWindow]);
|
|
126
|
+
}, [focusPaneForMouseInteraction, mouseCrosshairDisabledRef, renderer, syncPointerCursor, visibleDateWindow]);
|
|
123
127
|
|
|
124
128
|
const handlePlotDrag = useCallback((event: ChartMouseEvent) => {
|
|
125
129
|
consumeChartMouseEvent(event);
|
|
@@ -147,6 +151,7 @@ export function useComparisonChartPointerInteractions({
|
|
|
147
151
|
|
|
148
152
|
const handlePlotScroll = useCallback((event: ChartMouseEvent) => {
|
|
149
153
|
consumeChartMouseEvent(event);
|
|
154
|
+
focusPaneForMouseInteraction(null);
|
|
150
155
|
const direction = event.scroll?.direction;
|
|
151
156
|
if (!direction) return;
|
|
152
157
|
const scrollPan = consumeScrollPanMovement(
|
|
@@ -179,6 +184,7 @@ export function useComparisonChartPointerInteractions({
|
|
|
179
184
|
chartWidth,
|
|
180
185
|
dateBounds,
|
|
181
186
|
expandBufferRange,
|
|
187
|
+
focusPaneForMouseInteraction,
|
|
182
188
|
scrollPanCellRemainderRef,
|
|
183
189
|
series,
|
|
184
190
|
seriesDates,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { ComparisonChartProjection } from "../comparison/data";
|
|
2
2
|
import { PriceReturnTable, type PriceReturnTableRow } from "../../price-performance";
|
|
3
3
|
import type { PriceReturnField } from "../../../market-data/performance";
|
|
4
|
+
import type { MouseInteractionEvent } from "../core/pointer";
|
|
4
5
|
|
|
5
6
|
interface ComparisonChartLegendProps {
|
|
6
7
|
legendActiveIndex: number | null;
|
|
7
8
|
legendItemWidth: number;
|
|
8
9
|
legendRows: number;
|
|
10
|
+
onFocusInteraction: (event: MouseInteractionEvent | null | undefined) => void;
|
|
9
11
|
onOpenSymbol: (symbol: string) => void;
|
|
10
12
|
onSelectSymbol: (symbol: string) => void;
|
|
11
13
|
performanceBySymbol: ReadonlyMap<string, PriceReturnField[]>;
|
|
@@ -36,6 +38,7 @@ export function ComparisonChartLegend({
|
|
|
36
38
|
legendActiveIndex,
|
|
37
39
|
legendItemWidth,
|
|
38
40
|
legendRows,
|
|
41
|
+
onFocusInteraction,
|
|
39
42
|
onOpenSymbol,
|
|
40
43
|
onSelectSymbol,
|
|
41
44
|
performanceBySymbol,
|
|
@@ -61,6 +64,7 @@ export function ComparisonChartLegend({
|
|
|
61
64
|
return (
|
|
62
65
|
<PriceReturnTable
|
|
63
66
|
height={legendRows}
|
|
67
|
+
onFocusInteraction={onFocusInteraction}
|
|
64
68
|
onOpenSymbol={onOpenSymbol}
|
|
65
69
|
onSelectSymbol={onSelectSymbol}
|
|
66
70
|
rows={rows}
|
|
@@ -3,7 +3,8 @@ import {
|
|
|
3
3
|
DEFAULT_LIVE_CHART_REFRESH_INTERVAL_MS,
|
|
4
4
|
useChartQueries,
|
|
5
5
|
} from "../../../market-data/hooks";
|
|
6
|
-
import {
|
|
6
|
+
import type { ChartRequest, InstrumentRef } from "../../../market-data/request-types";
|
|
7
|
+
import { buildChartKey, resolveEntryData } from "../../../market-data/selectors";
|
|
7
8
|
import { getSharedMarketData } from "../../../plugins/registry";
|
|
8
9
|
import { blendHex, colors, getComparisonSeriesColor } from "../../../theme/colors";
|
|
9
10
|
import type { BrokerContractRef } from "../../../types/instrument";
|
|
@@ -24,6 +25,8 @@ import type {
|
|
|
24
25
|
ComparisonChartViewState,
|
|
25
26
|
} from "../core/types";
|
|
26
27
|
|
|
28
|
+
const COMPARISON_PERFORMANCE_RANGE = "5Y" as const;
|
|
29
|
+
|
|
27
30
|
export interface ComparisonChartSymbolSource {
|
|
28
31
|
symbol: string;
|
|
29
32
|
currency: string | undefined;
|
|
@@ -40,6 +43,16 @@ interface UseComparisonChartRenderDataOptions {
|
|
|
40
43
|
viewState: ComparisonChartViewState;
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
function instrumentFromSymbolSource(source: ComparisonChartSymbolSource): InstrumentRef {
|
|
47
|
+
return {
|
|
48
|
+
symbol: source.symbol,
|
|
49
|
+
exchange: source.exchange,
|
|
50
|
+
brokerId: source.brokerId,
|
|
51
|
+
brokerInstanceId: source.brokerInstanceId,
|
|
52
|
+
instrument: source.instrument,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
export function useComparisonChartRenderData({
|
|
44
57
|
symbolSources,
|
|
45
58
|
viewState,
|
|
@@ -143,21 +156,25 @@ export function useComparisonChartRenderData({
|
|
|
143
156
|
);
|
|
144
157
|
const chartRequests = useMemo(() => (
|
|
145
158
|
symbolSources.map((source) => ({
|
|
146
|
-
instrument:
|
|
147
|
-
symbol: source.symbol,
|
|
148
|
-
exchange: source.exchange,
|
|
149
|
-
brokerId: source.brokerId,
|
|
150
|
-
brokerInstanceId: source.brokerInstanceId,
|
|
151
|
-
instrument: source.instrument,
|
|
152
|
-
},
|
|
159
|
+
instrument: instrumentFromSymbolSource(source),
|
|
153
160
|
bufferRange: viewState.bufferRange,
|
|
154
161
|
granularity: effectiveResolution === "auto" ? "range" as const : "resolution" as const,
|
|
155
162
|
resolution: effectiveResolution === "auto" ? undefined : effectiveResolution,
|
|
156
163
|
}))
|
|
157
164
|
), [effectiveResolution, symbolSources, viewState.bufferRange]);
|
|
165
|
+
const performanceRequests = useMemo<ChartRequest[]>(() => (
|
|
166
|
+
symbolSources.map((source) => ({
|
|
167
|
+
instrument: instrumentFromSymbolSource(source),
|
|
168
|
+
bufferRange: COMPARISON_PERFORMANCE_RANGE,
|
|
169
|
+
granularity: "range",
|
|
170
|
+
}))
|
|
171
|
+
), [symbolSources]);
|
|
158
172
|
const chartEntries = useChartQueries(chartRequests, {
|
|
159
173
|
refreshIntervalMs: DEFAULT_LIVE_CHART_REFRESH_INTERVAL_MS,
|
|
160
174
|
});
|
|
175
|
+
const performanceEntries = useChartQueries(performanceRequests, {
|
|
176
|
+
refreshIntervalMs: DEFAULT_LIVE_CHART_REFRESH_INTERVAL_MS,
|
|
177
|
+
});
|
|
161
178
|
const entryStates = useMemo(() => chartRequests.map((request) => (
|
|
162
179
|
resolveChartBodyState(chartEntries.get(buildChartKey(request)), (value) => Array.isArray(value) && value.length > 0, "No chart data yet.")
|
|
163
180
|
)), [chartEntries, chartRequests]);
|
|
@@ -182,6 +199,17 @@ export function useComparisonChartRenderData({
|
|
|
182
199
|
points,
|
|
183
200
|
};
|
|
184
201
|
}), [chartEntries, chartRequests, symbolSources]);
|
|
202
|
+
const performanceHistoryBySymbol = useMemo(() => {
|
|
203
|
+
const bySymbol = new Map<string, PricePoint[]>();
|
|
204
|
+
symbolSources.forEach((source, index) => {
|
|
205
|
+
const request = performanceRequests[index];
|
|
206
|
+
const history = request ? resolveEntryData(performanceEntries.get(buildChartKey(request))) : null;
|
|
207
|
+
if (history?.length) {
|
|
208
|
+
bySymbol.set(source.symbol, history);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
return bySymbol;
|
|
212
|
+
}, [performanceEntries, performanceRequests, symbolSources]);
|
|
185
213
|
|
|
186
214
|
return {
|
|
187
215
|
availableManualResolutions,
|
|
@@ -192,6 +220,7 @@ export function useComparisonChartRenderData({
|
|
|
192
220
|
isUpdating,
|
|
193
221
|
resolutionChips,
|
|
194
222
|
selectionSupportMap,
|
|
223
|
+
performanceHistoryBySymbol,
|
|
195
224
|
series,
|
|
196
225
|
supportMap,
|
|
197
226
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { useEffect, useRef, type Dispatch, type MutableRefObject, type SetStateAction } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, type Dispatch, type MutableRefObject, type SetStateAction } from "react";
|
|
2
2
|
import type { BoxRenderable, NativeRendererHost } from "../../../ui";
|
|
3
|
-
import {
|
|
3
|
+
import { useAppDispatch, usePaneInstanceId } from "../../../state/app/context";
|
|
4
|
+
import { buildDisplayCursorState, type MouseInteractionEvent } from "../core/pointer";
|
|
4
5
|
import type { ComparisonChartViewState } from "../core/types";
|
|
5
6
|
import { useChartDisplayCursor, useChartDisplayCursorLayoutRemap } from "../display-cursor";
|
|
6
7
|
import { clamp } from "./helpers";
|
|
@@ -10,6 +11,7 @@ interface UseComparisonChartSelectionRuntimeOptions {
|
|
|
10
11
|
chartWidth: number;
|
|
11
12
|
cursorX: number | null;
|
|
12
13
|
cursorY: number | null;
|
|
14
|
+
focused: boolean;
|
|
13
15
|
renderer: NativeRendererHost;
|
|
14
16
|
setViewState: Dispatch<SetStateAction<ComparisonChartViewState>>;
|
|
15
17
|
symbols: string[];
|
|
@@ -20,6 +22,7 @@ export function useComparisonChartSelectionRuntime({
|
|
|
20
22
|
chartWidth,
|
|
21
23
|
cursorX: viewCursorX,
|
|
22
24
|
cursorY: viewCursorY,
|
|
25
|
+
focused,
|
|
23
26
|
renderer,
|
|
24
27
|
setViewState,
|
|
25
28
|
symbols,
|
|
@@ -28,6 +31,7 @@ export function useComparisonChartSelectionRuntime({
|
|
|
28
31
|
displayCursor: ReturnType<typeof useChartDisplayCursor>["displayCursor"];
|
|
29
32
|
displayCursorX: number | null;
|
|
30
33
|
displayCursorY: number | null;
|
|
34
|
+
focusPaneForMouseInteraction: (event: MouseInteractionEvent | null | undefined) => void;
|
|
31
35
|
mouseCrosshairDisabledRef: MutableRefObject<boolean>;
|
|
32
36
|
plotRef: MutableRefObject<BoxRenderable | null>;
|
|
33
37
|
scrollPanCellRemainderRef: MutableRefObject<number>;
|
|
@@ -35,6 +39,8 @@ export function useComparisonChartSelectionRuntime({
|
|
|
35
39
|
setSelectedSymbol: (symbol: string) => void;
|
|
36
40
|
updateDisplayCursorTarget: ReturnType<typeof useChartDisplayCursor>["updateDisplayCursorTarget"];
|
|
37
41
|
} {
|
|
42
|
+
const dispatch = useAppDispatch();
|
|
43
|
+
const paneId = usePaneInstanceId();
|
|
38
44
|
const plotRef = useRef<BoxRenderable | null>(null);
|
|
39
45
|
const mouseCrosshairDisabledRef = useRef(false);
|
|
40
46
|
const scrollPanCellRemainderRef = useRef(0);
|
|
@@ -52,6 +58,16 @@ export function useComparisonChartSelectionRuntime({
|
|
|
52
58
|
const displayCursorX = displayCursor.cellX !== null ? clamp(displayCursor.cellX, 0, chartWidth - 1) : null;
|
|
53
59
|
const displayCursorY = displayCursor.cellY !== null ? clamp(displayCursor.cellY, 0, chartHeight - 1) : null;
|
|
54
60
|
|
|
61
|
+
const focusPaneForMouseInteraction = useCallback((
|
|
62
|
+
event: MouseInteractionEvent | null | undefined,
|
|
63
|
+
) => {
|
|
64
|
+
event?.stopPropagation?.();
|
|
65
|
+
event?.preventDefault?.();
|
|
66
|
+
if (!focused) {
|
|
67
|
+
dispatch({ type: "FOCUS_PANE", paneId });
|
|
68
|
+
}
|
|
69
|
+
}, [dispatch, focused, paneId]);
|
|
70
|
+
|
|
55
71
|
const setSelectedSymbol = (symbol: string) => {
|
|
56
72
|
setViewState((current) => (
|
|
57
73
|
current.selectedSymbol === symbol
|
|
@@ -101,6 +117,7 @@ export function useComparisonChartSelectionRuntime({
|
|
|
101
117
|
displayCursor,
|
|
102
118
|
displayCursorX,
|
|
103
119
|
displayCursorY,
|
|
120
|
+
focusPaneForMouseInteraction,
|
|
104
121
|
mouseCrosshairDisabledRef,
|
|
105
122
|
plotRef,
|
|
106
123
|
scrollPanCellRemainderRef,
|
|
@@ -24,17 +24,21 @@ export function useComparisonChartSymbolSources(symbols: string[]): {
|
|
|
24
24
|
const marketFinancials = useTickerFinancialsMap(stableTickers);
|
|
25
25
|
const symbolSources = useMemo<ComparisonChartSymbolSource[]>(() => stableSymbols.map((symbol) => {
|
|
26
26
|
const ticker = tickers.get(symbol) ?? null;
|
|
27
|
-
const
|
|
27
|
+
const marketFinancial = marketFinancials.get(symbol) ?? null;
|
|
28
|
+
const storedFinancial = financials.get(symbol) ?? null;
|
|
28
29
|
const instrument = ticker?.metadata.broker_contracts?.[0] ?? null;
|
|
30
|
+
const quote = marketFinancial?.quote ?? storedFinancial?.quote;
|
|
29
31
|
return {
|
|
30
32
|
symbol,
|
|
31
|
-
currency:
|
|
32
|
-
quote
|
|
33
|
+
currency: quote?.currency ?? ticker?.metadata.currency,
|
|
34
|
+
quote,
|
|
33
35
|
exchange: ticker?.metadata.exchange ?? "",
|
|
34
36
|
brokerId: instrument?.brokerId,
|
|
35
37
|
brokerInstanceId: instrument?.brokerInstanceId,
|
|
36
38
|
instrument,
|
|
37
|
-
priceHistory:
|
|
39
|
+
priceHistory: marketFinancial?.priceHistory?.length
|
|
40
|
+
? marketFinancial.priceHistory
|
|
41
|
+
: storedFinancial?.priceHistory ?? [],
|
|
38
42
|
};
|
|
39
43
|
}), [financials, marketFinancials, stableSymbols, tickers]);
|
|
40
44
|
|