gloomberb 0.4.1 → 0.5.1
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 +2 -2
- package/package.json +1 -1
- package/src/app.tsx +168 -65
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +292 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/floating-pane.tsx +3 -2
- package/src/components/layout/header.tsx +2 -1
- package/src/components/layout/pane-sizing.ts +10 -0
- package/src/components/layout/pane.tsx +9 -2
- package/src/components/layout/shell.test.tsx +282 -28
- package/src/components/layout/shell.tsx +10 -7
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +54 -1
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +60 -3
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +83 -18
- package/src/plugins/builtin/chat-controller.ts +26 -14
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +424 -120
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +24 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +1 -1
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +8 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +13 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +2 -0
- package/src/plugins/registry.ts +22 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/utils/api-client.test.ts +90 -0
- package/src/utils/api-client.ts +56 -16
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
|
@@ -1,43 +1,73 @@
|
|
|
1
|
-
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { memo, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { TextAttributes, type BoxRenderable, type CliRenderer } from "@opentui/core";
|
|
3
3
|
import { useKeyboard, useRenderer } from "@opentui/react";
|
|
4
|
-
import { useAppDispatch, useAppSelector, usePaneInstanceId, usePaneSettingValue, usePaneTicker } from "../../state/app-context";
|
|
4
|
+
import { useAppDispatch, useAppSelector, usePaneInstance, usePaneInstanceId, usePaneSettingValue, usePaneTicker } from "../../state/app-context";
|
|
5
5
|
import { saveConfig } from "../../data/config-store";
|
|
6
6
|
import { getSharedDataProvider } from "../../plugins/registry";
|
|
7
|
-
import { colors
|
|
8
|
-
import { formatCompact
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
7
|
+
import { colors } from "../../theme/colors";
|
|
8
|
+
import { formatCompact } from "../../utils/format";
|
|
9
|
+
import { formatMarketPriceWithCurrency } from "../../utils/market-format";
|
|
10
|
+
import { useChartQueries, useChartQuery } from "../../market-data/hooks";
|
|
11
|
+
import { instrumentFromTicker, type ChartRequest } from "../../market-data/request-types";
|
|
12
|
+
import { buildChartKey } from "../../market-data/selectors";
|
|
11
13
|
import { usePersistChartControlSelection } from "./chart-pane-settings";
|
|
12
|
-
import {
|
|
14
|
+
import { computeSMA, computeEMA } from "./indicators/moving-averages";
|
|
15
|
+
import { computeRSI, computeMACD } from "./indicators/oscillators";
|
|
16
|
+
import { computeBollingerBands } from "./indicators/bands";
|
|
17
|
+
import type { IndicatorConfig, MacdResult, OscillatorPoint, OverlayPoint } from "./indicators/types";
|
|
18
|
+
import type { ChartIndicatorOverlays } from "./chart-types";
|
|
13
19
|
import {
|
|
20
|
+
applyBufferedPanExpansion,
|
|
21
|
+
consumeScrollPanMovement,
|
|
22
|
+
getDragPanWindowRatio,
|
|
23
|
+
getKeyboardPanCellCount,
|
|
24
|
+
resolveDragPanOffset,
|
|
25
|
+
} from "./chart-scroll";
|
|
26
|
+
import { getVisibleWindow, projectChartData, resolveBarSize, resolveStableOhlcProjectionOptions, type ProjectedChartPoint } from "./chart-data";
|
|
27
|
+
import {
|
|
28
|
+
buildPresetDateWindow,
|
|
14
29
|
buildVisibleDateWindow,
|
|
30
|
+
buildVisibleDateWindowFromRange,
|
|
31
|
+
clampDateWindowToBounds,
|
|
15
32
|
clearActivePreset,
|
|
16
|
-
|
|
17
|
-
|
|
33
|
+
getDateWindowBounds,
|
|
34
|
+
getMinimumDateStepMs,
|
|
35
|
+
getVisibleWindowForDateRange,
|
|
18
36
|
getPointDates,
|
|
19
|
-
|
|
37
|
+
needsCanonicalPresetViewportReset,
|
|
20
38
|
resolveChartBodyState,
|
|
39
|
+
resolveCanonicalPresetViewport,
|
|
21
40
|
resolvePresetSelection,
|
|
22
|
-
|
|
41
|
+
resolvePresetRangeViewport,
|
|
23
42
|
resolveResolutionSelection,
|
|
24
43
|
resolveStoredChartSelection,
|
|
44
|
+
resolveVisibleActivePreset,
|
|
45
|
+
sameDateWindow,
|
|
46
|
+
shiftDateWindow,
|
|
47
|
+
type ChartBodyState,
|
|
48
|
+
type DateWindowRange,
|
|
25
49
|
} from "./chart-controller";
|
|
26
50
|
import {
|
|
27
51
|
buildChartResolutionSupportMap,
|
|
52
|
+
CHART_RESOLUTION_STEP_MS,
|
|
28
53
|
clampTimeRangeToMaxRange,
|
|
29
54
|
DEFAULT_TICKER_CHART_RANGE_PRESET,
|
|
30
55
|
DEFAULT_TICKER_CHART_RESOLUTION,
|
|
31
56
|
DEFAULT_VISIBLE_MANUAL_CHART_RESOLUTIONS,
|
|
32
|
-
|
|
57
|
+
getBestSupportedResolutionForVisibleWindow,
|
|
33
58
|
getChartResolutionLabel,
|
|
34
|
-
getNextFallbackResolution,
|
|
35
59
|
getNextBufferRange,
|
|
60
|
+
getNextFallbackResolution,
|
|
36
61
|
getPresetResolution,
|
|
37
62
|
getSupportMaxRange,
|
|
63
|
+
getTimeRangeForDateWindow,
|
|
64
|
+
isIntradayResolution,
|
|
38
65
|
isRangePresetSupported,
|
|
66
|
+
maxTimeRange,
|
|
39
67
|
normalizeChartResolutionSupport,
|
|
40
68
|
sortChartResolutions,
|
|
69
|
+
subtractTimeRange,
|
|
70
|
+
TIME_RANGE_ORDER,
|
|
41
71
|
type ChartResolutionSupport,
|
|
42
72
|
type ManualChartResolution,
|
|
43
73
|
} from "./chart-resolution";
|
|
@@ -48,12 +78,14 @@ import {
|
|
|
48
78
|
} from "./chart-viewport";
|
|
49
79
|
import {
|
|
50
80
|
buildChartScene,
|
|
81
|
+
formatAxisCell,
|
|
51
82
|
formatDateShort,
|
|
52
|
-
|
|
83
|
+
formatCursorAxisValue,
|
|
53
84
|
getActivePointIndex,
|
|
54
85
|
getPointTerminalColumn,
|
|
55
86
|
renderChart,
|
|
56
87
|
resolveChartPalette,
|
|
88
|
+
resolveChartAxisWidth,
|
|
57
89
|
type StyledContent,
|
|
58
90
|
} from "./chart-renderer";
|
|
59
91
|
import {
|
|
@@ -64,13 +96,16 @@ import {
|
|
|
64
96
|
} from "./cursor-motion";
|
|
65
97
|
import {
|
|
66
98
|
CHART_RENDER_MODES,
|
|
99
|
+
RANGE_DAYS,
|
|
67
100
|
TIME_RANGES,
|
|
68
101
|
type ChartAxisMode,
|
|
69
102
|
type ChartRenderMode,
|
|
70
103
|
type ChartResolution,
|
|
104
|
+
type TimeRange,
|
|
71
105
|
type ChartViewState,
|
|
72
106
|
type ResolvedChartRenderer,
|
|
73
107
|
} from "./chart-types";
|
|
108
|
+
import { ChartControlHint } from "./chart-control-hint";
|
|
74
109
|
import {
|
|
75
110
|
computeBitmapSize,
|
|
76
111
|
intersectCellRects,
|
|
@@ -101,6 +136,20 @@ const MODE_LABELS: Record<ChartRenderMode, string> = {
|
|
|
101
136
|
ohlc: "OHLC",
|
|
102
137
|
};
|
|
103
138
|
|
|
139
|
+
const AXIS_MEASURE_PALETTE = resolveChartPalette({
|
|
140
|
+
bg: colors.bg,
|
|
141
|
+
border: colors.border,
|
|
142
|
+
borderFocused: colors.borderFocused,
|
|
143
|
+
text: colors.text,
|
|
144
|
+
textDim: colors.textDim,
|
|
145
|
+
positive: colors.positive,
|
|
146
|
+
negative: colors.negative,
|
|
147
|
+
}, "neutral");
|
|
148
|
+
|
|
149
|
+
const EMPTY_INDICATOR_CONFIG: IndicatorConfig = {};
|
|
150
|
+
const INDICATOR_RENDER_DEBOUNCE_MS = 120;
|
|
151
|
+
const CHART_DETAIL_REQUEST_DEBOUNCE_MS = 160;
|
|
152
|
+
|
|
104
153
|
interface StockChartProps {
|
|
105
154
|
width: number;
|
|
106
155
|
height: number;
|
|
@@ -110,6 +159,9 @@ interface StockChartProps {
|
|
|
110
159
|
axisMode?: ChartAxisMode;
|
|
111
160
|
historyOverride?: PricePoint[] | null;
|
|
112
161
|
currencyOverride?: string | null;
|
|
162
|
+
indicatorConfig?: IndicatorConfig;
|
|
163
|
+
showVolume?: boolean;
|
|
164
|
+
footerControls?: ReactNode;
|
|
113
165
|
}
|
|
114
166
|
|
|
115
167
|
interface ResolvedStockChartProps extends StockChartProps {
|
|
@@ -121,18 +173,62 @@ interface ResolvedStockChartProps extends StockChartProps {
|
|
|
121
173
|
interface DragState {
|
|
122
174
|
startGlobalX: number;
|
|
123
175
|
startPanOffset: number;
|
|
176
|
+
startWindow: DateWindowRange | null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface ResolvedChartRequestPlan {
|
|
180
|
+
effectiveResolution: ManualChartResolution | null;
|
|
181
|
+
requestedWindow: DateWindowRange | null;
|
|
182
|
+
resolutionRequest: ChartRequest | null;
|
|
183
|
+
detailRequest: ChartRequest | null;
|
|
184
|
+
unsupportedMessage: string | null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
interface ResolvedRenderCandidate {
|
|
188
|
+
resolution: ManualChartResolution;
|
|
189
|
+
plan: ResolvedChartRequestPlan;
|
|
190
|
+
resolutionRequestKey: string | null;
|
|
191
|
+
detailRequestKey: string | null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface AutoRenderedView {
|
|
195
|
+
window: DateWindowRange;
|
|
196
|
+
resolution: ManualChartResolution;
|
|
197
|
+
data: PricePoint[];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
interface CachedRenderedView {
|
|
201
|
+
window: DateWindowRange | null;
|
|
202
|
+
resolution: ManualChartResolution | null;
|
|
203
|
+
data: PricePoint[];
|
|
124
204
|
}
|
|
125
205
|
|
|
206
|
+
interface AutoDisplayState {
|
|
207
|
+
bodyState: ChartBodyState<PricePoint[]>;
|
|
208
|
+
resolution: ManualChartResolution | null;
|
|
209
|
+
window: DateWindowRange | null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
type StockChartViewportState = Omit<ChartViewState, "resolution">;
|
|
213
|
+
type ResolutionAwareViewportState = StockChartViewportState & { resolution: ChartResolution };
|
|
214
|
+
|
|
126
215
|
type PendingExpansionAction =
|
|
127
216
|
| { kind: "zoom-out"; targetVisibleCount: number; anchorRatio: number }
|
|
128
217
|
| { kind: "pan-left"; targetPanOffset: number }
|
|
129
218
|
| null;
|
|
130
219
|
|
|
220
|
+
interface DeferredIndicatorState {
|
|
221
|
+
key: string;
|
|
222
|
+
overlays: ChartIndicatorOverlays;
|
|
223
|
+
}
|
|
224
|
+
|
|
131
225
|
interface ChartMouseEvent {
|
|
132
226
|
x: number;
|
|
133
227
|
y: number;
|
|
134
228
|
pixelX?: number;
|
|
135
229
|
pixelY?: number;
|
|
230
|
+
stopPropagation?: () => void;
|
|
231
|
+
preventDefault?: () => void;
|
|
136
232
|
modifiers: {
|
|
137
233
|
shift: boolean;
|
|
138
234
|
alt: boolean;
|
|
@@ -144,6 +240,22 @@ interface ChartMouseEvent {
|
|
|
144
240
|
};
|
|
145
241
|
}
|
|
146
242
|
|
|
243
|
+
export function resolveChartKeyboardKey(event: { name?: string; sequence?: string }): string {
|
|
244
|
+
const name = event.name ?? "";
|
|
245
|
+
const sequence = event.sequence ?? "";
|
|
246
|
+
const candidates = [name, sequence];
|
|
247
|
+
|
|
248
|
+
if (candidates.some((key) => key === "=" || key === "+" || key === "plus")) {
|
|
249
|
+
return "zoom-in";
|
|
250
|
+
}
|
|
251
|
+
if (candidates.some((key) => key === "-" || key === "_" || key === "minus")) {
|
|
252
|
+
return "zoom-out";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const key = name || sequence;
|
|
256
|
+
return key.length === 1 ? key.toLowerCase() : key;
|
|
257
|
+
}
|
|
258
|
+
|
|
147
259
|
export interface LocalPlotPointer {
|
|
148
260
|
cellX: number;
|
|
149
261
|
cellY: number;
|
|
@@ -445,22 +557,558 @@ function buildBlankPlotLines(width: number, height: number): string[] {
|
|
|
445
557
|
return Array.from({ length: height }, () => " ".repeat(width));
|
|
446
558
|
}
|
|
447
559
|
|
|
448
|
-
function formatAxisCell(label: string | null, width: number): string {
|
|
449
|
-
if (!label) return " ".repeat(width);
|
|
450
|
-
return label.length >= width ? label.slice(0, width) : label.padStart(width);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
560
|
function getMaxPanOffset(history: PricePoint[], zoomLevel: number): number {
|
|
454
561
|
const visibleCount = getVisiblePointCount(history.length, zoomLevel);
|
|
455
562
|
return Math.max(history.length - visibleCount, 0);
|
|
456
563
|
}
|
|
457
564
|
|
|
565
|
+
function attachViewportResolution(
|
|
566
|
+
view: StockChartViewportState,
|
|
567
|
+
resolution: ChartResolution,
|
|
568
|
+
): ResolutionAwareViewportState {
|
|
569
|
+
return {
|
|
570
|
+
...view,
|
|
571
|
+
resolution,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function stripViewportResolution(view: ResolutionAwareViewportState): StockChartViewportState {
|
|
576
|
+
const { resolution: _resolution, ...viewport } = view;
|
|
577
|
+
return viewport;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function resolveViewportPresetSelection(
|
|
581
|
+
view: StockChartViewportState,
|
|
582
|
+
presetRange: TimeRange,
|
|
583
|
+
supportMaxRange: TimeRange | null = null,
|
|
584
|
+
): StockChartViewportState {
|
|
585
|
+
return stripViewportResolution(
|
|
586
|
+
resolvePresetSelection(attachViewportResolution(view, "auto"), presetRange, supportMaxRange),
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function resolveViewportResolutionSelection(
|
|
591
|
+
view: StockChartViewportState,
|
|
592
|
+
resolution: ChartResolution,
|
|
593
|
+
support: ReadonlyMap<ManualChartResolution, TimeRange>,
|
|
594
|
+
visibleWindow: DateWindowRange | null,
|
|
595
|
+
): StockChartViewportState | null {
|
|
596
|
+
const nextView = resolveResolutionSelection(
|
|
597
|
+
attachViewportResolution(view, resolution),
|
|
598
|
+
resolution,
|
|
599
|
+
support,
|
|
600
|
+
visibleWindow,
|
|
601
|
+
);
|
|
602
|
+
return nextView ? stripViewportResolution(nextView) : null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function resolveViewportStoredSelection(
|
|
606
|
+
view: StockChartViewportState,
|
|
607
|
+
presetRange: TimeRange,
|
|
608
|
+
resolution: ChartResolution,
|
|
609
|
+
support: ReadonlyMap<ManualChartResolution, TimeRange>,
|
|
610
|
+
): StockChartViewportState {
|
|
611
|
+
return stripViewportResolution(
|
|
612
|
+
resolveStoredChartSelection(attachViewportResolution(view, resolution), presetRange, resolution, support),
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function clearAutoViewportState(view: StockChartViewportState): StockChartViewportState {
|
|
617
|
+
const clearedPreset = clearActivePreset(view);
|
|
618
|
+
if (clearedPreset.panOffset === 0 && clearedPreset.zoomLevel === 1) {
|
|
619
|
+
return clearedPreset;
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
...clearedPreset,
|
|
623
|
+
panOffset: 0,
|
|
624
|
+
zoomLevel: 1,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function buildBufferedDetailWindow(
|
|
629
|
+
requestedWindow: DateWindowRange | null | undefined,
|
|
630
|
+
maxRange: TimeRange,
|
|
631
|
+
bounds: DateWindowRange | null | undefined,
|
|
632
|
+
minimumSpanMs: number,
|
|
633
|
+
minimumBufferRange: TimeRange | null = null,
|
|
634
|
+
): DateWindowRange | null {
|
|
635
|
+
if (!requestedWindow?.end) return null;
|
|
636
|
+
|
|
637
|
+
let bufferRange = maxRange;
|
|
638
|
+
const maxRangeIndex = TIME_RANGE_ORDER.indexOf(maxRange);
|
|
639
|
+
if (maxRangeIndex >= 0) {
|
|
640
|
+
for (const candidate of TIME_RANGE_ORDER.slice(0, maxRangeIndex + 1)) {
|
|
641
|
+
if (requestedWindow.start && subtractTimeRange(requestedWindow.end, candidate).getTime() <= requestedWindow.start.getTime()) {
|
|
642
|
+
bufferRange = candidate;
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (minimumBufferRange) {
|
|
648
|
+
bufferRange = maxTimeRange(bufferRange, clampTimeRangeToMaxRange(minimumBufferRange, maxRange));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return clampDateWindowToBounds(
|
|
652
|
+
{
|
|
653
|
+
start: subtractTimeRange(requestedWindow.end, bufferRange),
|
|
654
|
+
end: requestedWindow.end,
|
|
655
|
+
},
|
|
656
|
+
bounds,
|
|
657
|
+
minimumSpanMs,
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function getWindowPoints(points: readonly PricePoint[], requestedWindow: DateWindowRange | null | undefined): PricePoint[] {
|
|
662
|
+
if (!requestedWindow?.start || !requestedWindow.end || points.length === 0) return [...points];
|
|
663
|
+
const startMs = requestedWindow.start.getTime();
|
|
664
|
+
const endMs = requestedWindow.end.getTime();
|
|
665
|
+
return points.filter((point) => {
|
|
666
|
+
const pointMs = coerceChartDate(point.date as Date | string | number).getTime();
|
|
667
|
+
return pointMs >= startMs && pointMs <= endMs;
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function getMaximumDateGapMs(dates: readonly Date[]): number {
|
|
672
|
+
if (dates.length < 2) return 0;
|
|
673
|
+
|
|
674
|
+
let maximumGapMs = 0;
|
|
675
|
+
for (let index = 1; index < dates.length; index += 1) {
|
|
676
|
+
const previousDate = dates[index - 1];
|
|
677
|
+
const currentDate = dates[index];
|
|
678
|
+
if (!previousDate || !currentDate) continue;
|
|
679
|
+
maximumGapMs = Math.max(maximumGapMs, currentDate.getTime() - previousDate.getTime());
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return maximumGapMs;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function isSeriesCompatibleWithRequest(
|
|
686
|
+
points: readonly PricePoint[] | null | undefined,
|
|
687
|
+
requestedWindow: DateWindowRange | null | undefined,
|
|
688
|
+
resolution: ManualChartResolution | null,
|
|
689
|
+
): boolean {
|
|
690
|
+
if (!requestedWindow?.start || !requestedWindow.end || !resolution || !points?.length) return false;
|
|
691
|
+
|
|
692
|
+
const windowPoints = getWindowPoints(points, requestedWindow);
|
|
693
|
+
if (windowPoints.length === 0) return false;
|
|
694
|
+
|
|
695
|
+
const expectedStepMs = CHART_RESOLUTION_STEP_MS[resolution];
|
|
696
|
+
if (!expectedStepMs) return false;
|
|
697
|
+
|
|
698
|
+
const windowStartMs = requestedWindow.start.getTime();
|
|
699
|
+
const windowEndMs = requestedWindow.end.getTime();
|
|
700
|
+
const requestedSpanMs = Math.max(windowEndMs - windowStartMs, 0);
|
|
701
|
+
const allDates = getPointDates(points);
|
|
702
|
+
const windowDates = getPointDates(windowPoints);
|
|
703
|
+
const firstPointMs = windowDates[0]!.getTime();
|
|
704
|
+
const lastPointMs = windowDates[windowDates.length - 1]!.getTime();
|
|
705
|
+
const startGapMs = Math.max(firstPointMs - windowStartMs, 0);
|
|
706
|
+
const endGapMs = Math.max(windowEndMs - lastPointMs, 0);
|
|
707
|
+
const sessionGapAllowanceMs = isIntradayResolution(resolution)
|
|
708
|
+
? Math.max(expectedStepMs * 8, getMaximumDateGapMs(allDates) + expectedStepMs)
|
|
709
|
+
: expectedStepMs * 8;
|
|
710
|
+
|
|
711
|
+
if (windowPoints.length === 1) {
|
|
712
|
+
return requestedSpanMs <= expectedStepMs * 2
|
|
713
|
+
&& startGapMs <= sessionGapAllowanceMs
|
|
714
|
+
&& endGapMs <= sessionGapAllowanceMs;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const actualStepMs = getMinimumDateStepMs(windowDates);
|
|
718
|
+
return actualStepMs <= expectedStepMs * 4
|
|
719
|
+
&& startGapMs <= sessionGapAllowanceMs
|
|
720
|
+
&& endGapMs <= sessionGapAllowanceMs;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function getExpectedPointCountForWindow(
|
|
724
|
+
requestedWindow: DateWindowRange | null | undefined,
|
|
725
|
+
resolution: ManualChartResolution | null,
|
|
726
|
+
): number {
|
|
727
|
+
if (!requestedWindow?.start || !requestedWindow.end || !resolution) return 0;
|
|
728
|
+
const expectedStepMs = CHART_RESOLUTION_STEP_MS[resolution];
|
|
729
|
+
if (!expectedStepMs) return 0;
|
|
730
|
+
|
|
731
|
+
const spanMs = Math.max(requestedWindow.end.getTime() - requestedWindow.start.getTime(), 0);
|
|
732
|
+
return Math.max(spanMs / expectedStepMs, 1);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function getMinimumAutoRenderablePointCount(
|
|
736
|
+
requestedWindow: DateWindowRange | null | undefined,
|
|
737
|
+
targetResolution: ManualChartResolution | null,
|
|
738
|
+
): number {
|
|
739
|
+
const expectedPointCount = getExpectedPointCountForWindow(requestedWindow, targetResolution);
|
|
740
|
+
if (!Number.isFinite(expectedPointCount) || expectedPointCount <= 0) return 2;
|
|
741
|
+
return clamp(Math.ceil(expectedPointCount / 24), 2, 8);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function isSeriesAcceptedForRequest(
|
|
745
|
+
points: readonly PricePoint[] | null | undefined,
|
|
746
|
+
requestedWindow: DateWindowRange | null | undefined,
|
|
747
|
+
resolution: ManualChartResolution | null,
|
|
748
|
+
options?: {
|
|
749
|
+
requireAutoDensity?: boolean;
|
|
750
|
+
targetResolution?: ManualChartResolution | null;
|
|
751
|
+
},
|
|
752
|
+
): boolean {
|
|
753
|
+
if (!isSeriesCompatibleWithRequest(points, requestedWindow, resolution)) return false;
|
|
754
|
+
if (!options?.requireAutoDensity) return true;
|
|
755
|
+
|
|
756
|
+
const windowPoints = getWindowPoints(points ?? [], requestedWindow);
|
|
757
|
+
if (windowPoints.length === 0) return false;
|
|
758
|
+
|
|
759
|
+
const targetResolution = options.targetResolution ?? resolution;
|
|
760
|
+
const minimumPointCount = getMinimumAutoRenderablePointCount(requestedWindow, targetResolution);
|
|
761
|
+
return windowPoints.length >= minimumPointCount;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function canRenderAutoViewForWindow(
|
|
765
|
+
render: AutoRenderedView | null,
|
|
766
|
+
requestedWindow: DateWindowRange | null | undefined,
|
|
767
|
+
): render is AutoRenderedView {
|
|
768
|
+
if (!render || !requestedWindow?.start || !requestedWindow.end) return false;
|
|
769
|
+
return getWindowPoints(render.data, requestedWindow).length > 0;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export function resolveAutoDisplayState(options: {
|
|
773
|
+
shouldUseRenderedAutoView: boolean;
|
|
774
|
+
renderedAutoView: AutoRenderedView | null;
|
|
775
|
+
isRenderedAutoViewUpdating: boolean;
|
|
776
|
+
plannedRenderBodyState: ChartBodyState<PricePoint[]>;
|
|
777
|
+
plannedResolvedManualResolution: ManualChartResolution | null;
|
|
778
|
+
plannedDateWindow: DateWindowRange | null;
|
|
779
|
+
}): AutoDisplayState {
|
|
780
|
+
const {
|
|
781
|
+
shouldUseRenderedAutoView,
|
|
782
|
+
renderedAutoView,
|
|
783
|
+
isRenderedAutoViewUpdating,
|
|
784
|
+
plannedRenderBodyState,
|
|
785
|
+
plannedResolvedManualResolution,
|
|
786
|
+
plannedDateWindow,
|
|
787
|
+
} = options;
|
|
788
|
+
|
|
789
|
+
if (shouldUseRenderedAutoView && renderedAutoView) {
|
|
790
|
+
return {
|
|
791
|
+
bodyState: {
|
|
792
|
+
data: renderedAutoView.data,
|
|
793
|
+
blocking: false,
|
|
794
|
+
updating: isRenderedAutoViewUpdating,
|
|
795
|
+
emptyMessage: null,
|
|
796
|
+
errorMessage: null,
|
|
797
|
+
},
|
|
798
|
+
resolution: renderedAutoView.resolution,
|
|
799
|
+
window: renderedAutoView.window,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return {
|
|
804
|
+
bodyState: plannedRenderBodyState,
|
|
805
|
+
resolution: plannedResolvedManualResolution,
|
|
806
|
+
window: plannedDateWindow,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
export function resolveVisibleChartDateWindow(
|
|
811
|
+
points: readonly Pick<PricePoint, "date">[],
|
|
812
|
+
preferredWindow: DateWindowRange | null | undefined,
|
|
813
|
+
): DateWindowRange | null {
|
|
814
|
+
return getDateWindowBounds(getPointDates(points)) ?? preferredWindow ?? null;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function buildDateWindowFromIndices(dates: readonly Date[], startIdx: number, endIdx: number): DateWindowRange | null {
|
|
818
|
+
if (dates.length === 0 || endIdx <= startIdx) return null;
|
|
819
|
+
return {
|
|
820
|
+
start: dates[startIdx] ?? null,
|
|
821
|
+
end: dates[endIdx - 1] ?? null,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function inferAutoZoomStepMs(
|
|
826
|
+
dates: readonly Date[],
|
|
827
|
+
window: Pick<ReturnType<typeof buildVisibleDateWindowFromRange>, "startIdx" | "endIdx">,
|
|
828
|
+
fallbackWindow: DateWindowRange,
|
|
829
|
+
): number {
|
|
830
|
+
const visibleCount = window.endIdx - window.startIdx;
|
|
831
|
+
if (visibleCount >= 2) {
|
|
832
|
+
const start = dates[window.startIdx]?.getTime() ?? null;
|
|
833
|
+
const end = dates[window.endIdx - 1]?.getTime() ?? null;
|
|
834
|
+
if (start !== null && end !== null && end > start) {
|
|
835
|
+
return Math.max((end - start) / Math.max(visibleCount - 1, 1), 1);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (dates.length >= 2) {
|
|
840
|
+
return Math.max(getMinimumDateStepMs(dates), 1);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (fallbackWindow.start && fallbackWindow.end) {
|
|
844
|
+
return Math.max(fallbackWindow.end.getTime() - fallbackWindow.start.getTime(), 1);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return 1;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export function resolveAutoZoomWindow(options: {
|
|
851
|
+
historyPoints: readonly PricePoint[];
|
|
852
|
+
boundsDates: readonly Date[];
|
|
853
|
+
currentWindow: DateWindowRange | null | undefined;
|
|
854
|
+
direction: "in" | "out";
|
|
855
|
+
anchorRatio: number;
|
|
856
|
+
zoomFactor?: number;
|
|
857
|
+
}): DateWindowRange | null {
|
|
858
|
+
const {
|
|
859
|
+
historyPoints,
|
|
860
|
+
boundsDates,
|
|
861
|
+
currentWindow,
|
|
862
|
+
direction,
|
|
863
|
+
anchorRatio,
|
|
864
|
+
zoomFactor = 1.5,
|
|
865
|
+
} = options;
|
|
866
|
+
|
|
867
|
+
if (!currentWindow?.start || !currentWindow.end) return currentWindow ?? null;
|
|
868
|
+
|
|
869
|
+
const historyDates = getPointDates(historyPoints);
|
|
870
|
+
if (historyDates.length === 0) return currentWindow;
|
|
871
|
+
|
|
872
|
+
const currentHistoryWindow = buildVisibleDateWindowFromRange(historyDates, currentWindow, 0);
|
|
873
|
+
const canExpandWithinHistory = direction === "out"
|
|
874
|
+
&& (
|
|
875
|
+
currentHistoryWindow.startIdx > 0
|
|
876
|
+
|| currentHistoryWindow.endIdx < historyDates.length
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
const navigationDates = direction === "out" && !canExpandWithinHistory && boundsDates.length > 0
|
|
880
|
+
? boundsDates
|
|
881
|
+
: historyDates;
|
|
882
|
+
const currentNavigationWindow = buildVisibleDateWindowFromRange(navigationDates, currentWindow, 0);
|
|
883
|
+
const currentVisibleCount = currentNavigationWindow.endIdx - currentNavigationWindow.startIdx;
|
|
884
|
+
|
|
885
|
+
if (currentVisibleCount <= 0) {
|
|
886
|
+
return currentWindow;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const ratio = clamp(anchorRatio, 0, 1);
|
|
890
|
+
let targetVisibleCount: number;
|
|
891
|
+
if (direction === "in") {
|
|
892
|
+
if (currentVisibleCount <= 2) {
|
|
893
|
+
const currentSpanMs = Math.max(currentWindow.end.getTime() - currentWindow.start.getTime(), 1);
|
|
894
|
+
const targetSpanMs = Math.max(currentSpanMs / zoomFactor, 1);
|
|
895
|
+
const anchorMs = currentWindow.start.getTime() + currentSpanMs * ratio;
|
|
896
|
+
const nextStartMs = anchorMs - targetSpanMs * ratio;
|
|
897
|
+
return {
|
|
898
|
+
start: new Date(nextStartMs),
|
|
899
|
+
end: new Date(nextStartMs + targetSpanMs),
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
targetVisibleCount = Math.max(2, Math.floor(currentVisibleCount / zoomFactor));
|
|
903
|
+
if (targetVisibleCount >= currentVisibleCount) {
|
|
904
|
+
targetVisibleCount = currentVisibleCount - 1;
|
|
905
|
+
}
|
|
906
|
+
} else {
|
|
907
|
+
targetVisibleCount = Math.ceil(currentVisibleCount * zoomFactor);
|
|
908
|
+
if (targetVisibleCount <= currentVisibleCount) {
|
|
909
|
+
targetVisibleCount = currentVisibleCount + 1;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (targetVisibleCount > navigationDates.length) {
|
|
913
|
+
const stepMs = inferAutoZoomStepMs(navigationDates, currentNavigationWindow, currentWindow);
|
|
914
|
+
const targetSpanMs = Math.max(stepMs * Math.max(targetVisibleCount - 1, 1), stepMs);
|
|
915
|
+
const currentSpanMs = Math.max(currentWindow.end.getTime() - currentWindow.start.getTime(), stepMs);
|
|
916
|
+
const anchorMs = currentWindow.start.getTime() + currentSpanMs * ratio;
|
|
917
|
+
const nextStartMs = anchorMs - targetSpanMs * ratio;
|
|
918
|
+
|
|
919
|
+
return {
|
|
920
|
+
start: new Date(nextStartMs),
|
|
921
|
+
end: new Date(nextStartMs + targetSpanMs),
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const anchorIndex = currentNavigationWindow.startIdx + ratio * Math.max(currentVisibleCount - 1, 0);
|
|
927
|
+
let nextStartIdx = clamp(
|
|
928
|
+
Math.round(anchorIndex - ratio * Math.max(targetVisibleCount - 1, 0)),
|
|
929
|
+
0,
|
|
930
|
+
Math.max(navigationDates.length - targetVisibleCount, 0),
|
|
931
|
+
);
|
|
932
|
+
let nextEndIdx = Math.min(nextStartIdx + targetVisibleCount, navigationDates.length);
|
|
933
|
+
|
|
934
|
+
if (nextStartIdx === currentNavigationWindow.startIdx && nextEndIdx === currentNavigationWindow.endIdx) {
|
|
935
|
+
if (direction === "in" && currentVisibleCount > 2) {
|
|
936
|
+
nextStartIdx = Math.min(currentNavigationWindow.startIdx + 1, currentNavigationWindow.endIdx - 2);
|
|
937
|
+
nextEndIdx = currentNavigationWindow.endIdx;
|
|
938
|
+
} else if (direction === "out" && currentNavigationWindow.startIdx > 0) {
|
|
939
|
+
nextStartIdx = currentNavigationWindow.startIdx - 1;
|
|
940
|
+
nextEndIdx = currentNavigationWindow.endIdx;
|
|
941
|
+
} else if (direction === "out" && currentNavigationWindow.endIdx < navigationDates.length) {
|
|
942
|
+
nextStartIdx = currentNavigationWindow.startIdx;
|
|
943
|
+
nextEndIdx = currentNavigationWindow.endIdx + 1;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return buildDateWindowFromIndices(navigationDates, nextStartIdx, nextEndIdx) ?? currentWindow;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function isDateWindowReachableByAnchoredRange(
|
|
951
|
+
window: DateWindowRange | null | undefined,
|
|
952
|
+
latestDate: Date | null | undefined,
|
|
953
|
+
range: TimeRange,
|
|
954
|
+
): boolean {
|
|
955
|
+
if (!window?.start || !window.end || !latestDate) return false;
|
|
956
|
+
const threshold = subtractTimeRange(latestDate, range).getTime();
|
|
957
|
+
return window.start.getTime() >= threshold && window.end.getTime() <= latestDate.getTime();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function getMinimumAnchoredBufferRange(
|
|
961
|
+
window: DateWindowRange | null | undefined,
|
|
962
|
+
latestDate: Date | null | undefined,
|
|
963
|
+
maxRange: TimeRange,
|
|
964
|
+
): TimeRange | null {
|
|
965
|
+
for (const candidate of TIME_RANGE_ORDER) {
|
|
966
|
+
if (candidate === "ALL" || TIME_RANGE_ORDER.indexOf(candidate) > TIME_RANGE_ORDER.indexOf(maxRange)) break;
|
|
967
|
+
if (isDateWindowReachableByAnchoredRange(window, latestDate, candidate)) {
|
|
968
|
+
return candidate;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
return isDateWindowReachableByAnchoredRange(window, latestDate, maxRange)
|
|
973
|
+
? maxRange
|
|
974
|
+
: null;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function buildResolvedChartRequestPlan(options: {
|
|
978
|
+
compact?: boolean;
|
|
979
|
+
historyOverride?: PricePoint[] | null;
|
|
980
|
+
instrumentRef: ReturnType<typeof instrumentFromTicker>;
|
|
981
|
+
requestedWindow: DateWindowRange | null;
|
|
982
|
+
effectiveResolution: ChartResolution;
|
|
983
|
+
effectiveManualResolution: ManualChartResolution | null;
|
|
984
|
+
bounds: DateWindowRange | null;
|
|
985
|
+
bufferRange: TimeRange;
|
|
986
|
+
minimumBufferRange?: TimeRange | null;
|
|
987
|
+
support: ReadonlyMap<ManualChartResolution, TimeRange>;
|
|
988
|
+
hasResolutionHistoryApi: boolean;
|
|
989
|
+
hasDetailedHistoryApi: boolean;
|
|
990
|
+
minimumSpanMs: number;
|
|
991
|
+
}): ResolvedChartRequestPlan {
|
|
992
|
+
const {
|
|
993
|
+
compact,
|
|
994
|
+
historyOverride,
|
|
995
|
+
instrumentRef,
|
|
996
|
+
requestedWindow,
|
|
997
|
+
effectiveResolution,
|
|
998
|
+
effectiveManualResolution,
|
|
999
|
+
bounds,
|
|
1000
|
+
bufferRange,
|
|
1001
|
+
minimumBufferRange = null,
|
|
1002
|
+
support,
|
|
1003
|
+
hasResolutionHistoryApi,
|
|
1004
|
+
hasDetailedHistoryApi,
|
|
1005
|
+
minimumSpanMs,
|
|
1006
|
+
} = options;
|
|
1007
|
+
|
|
1008
|
+
if (historyOverride) {
|
|
1009
|
+
return {
|
|
1010
|
+
effectiveResolution: effectiveManualResolution,
|
|
1011
|
+
requestedWindow,
|
|
1012
|
+
resolutionRequest: null,
|
|
1013
|
+
detailRequest: null,
|
|
1014
|
+
unsupportedMessage: null,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (compact || !instrumentRef || !requestedWindow?.start || !requestedWindow.end || !effectiveManualResolution) {
|
|
1019
|
+
return {
|
|
1020
|
+
effectiveResolution: effectiveManualResolution,
|
|
1021
|
+
requestedWindow,
|
|
1022
|
+
resolutionRequest: null,
|
|
1023
|
+
detailRequest: null,
|
|
1024
|
+
unsupportedMessage: null,
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const maxRange = getSupportMaxRange(support, effectiveManualResolution);
|
|
1029
|
+
const latestDate = bounds?.end ?? null;
|
|
1030
|
+
let resolutionRequest: ChartRequest | null = null;
|
|
1031
|
+
if (hasResolutionHistoryApi && maxRange && latestDate) {
|
|
1032
|
+
const baseAnchoredRange = getMinimumAnchoredBufferRange(requestedWindow, latestDate, maxRange);
|
|
1033
|
+
const minimumAnchoredRange = minimumBufferRange
|
|
1034
|
+
? clampTimeRangeToMaxRange(minimumBufferRange, maxRange)
|
|
1035
|
+
: null;
|
|
1036
|
+
const anchoredRange = baseAnchoredRange && minimumAnchoredRange
|
|
1037
|
+
? maxTimeRange(baseAnchoredRange, minimumAnchoredRange)
|
|
1038
|
+
: baseAnchoredRange;
|
|
1039
|
+
if (anchoredRange) {
|
|
1040
|
+
resolutionRequest = {
|
|
1041
|
+
instrument: instrumentRef,
|
|
1042
|
+
bufferRange: anchoredRange,
|
|
1043
|
+
granularity: "resolution",
|
|
1044
|
+
resolution: effectiveManualResolution,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
let detailRequest: ChartRequest | null = null;
|
|
1050
|
+
if (hasDetailedHistoryApi && maxRange) {
|
|
1051
|
+
const bufferedWindow = buildBufferedDetailWindow(requestedWindow, maxRange, bounds, minimumSpanMs, minimumBufferRange);
|
|
1052
|
+
if (bufferedWindow?.start && bufferedWindow.end) {
|
|
1053
|
+
detailRequest = {
|
|
1054
|
+
instrument: instrumentRef,
|
|
1055
|
+
bufferRange,
|
|
1056
|
+
granularity: "detail",
|
|
1057
|
+
startDate: bufferedWindow.start,
|
|
1058
|
+
endDate: bufferedWindow.end,
|
|
1059
|
+
barSize: effectiveManualResolution,
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
return {
|
|
1065
|
+
effectiveResolution: effectiveManualResolution,
|
|
1066
|
+
requestedWindow,
|
|
1067
|
+
resolutionRequest,
|
|
1068
|
+
detailRequest,
|
|
1069
|
+
unsupportedMessage: effectiveResolution !== "auto" && !resolutionRequest && !detailRequest
|
|
1070
|
+
? `No ${getChartResolutionLabel(effectiveManualResolution)} history available for this window.`
|
|
1071
|
+
: null,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function buildResolutionFallbackChain(
|
|
1076
|
+
startResolution: ManualChartResolution | null,
|
|
1077
|
+
range: TimeRange,
|
|
1078
|
+
support: ReadonlyMap<ManualChartResolution, TimeRange>,
|
|
1079
|
+
): ManualChartResolution[] {
|
|
1080
|
+
if (!startResolution) return [];
|
|
1081
|
+
|
|
1082
|
+
const chain: ManualChartResolution[] = [];
|
|
1083
|
+
let current: ManualChartResolution | null = startResolution;
|
|
1084
|
+
while (current && !chain.includes(current)) {
|
|
1085
|
+
chain.push(current);
|
|
1086
|
+
const nextFallback = getNextFallbackResolution(range, current, support);
|
|
1087
|
+
current = nextFallback && nextFallback !== "auto" ? nextFallback : null;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
return chain;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function dedupeChartRequests(requests: Array<ChartRequest | null | undefined>): ChartRequest[] {
|
|
1094
|
+
const uniqueRequests: ChartRequest[] = [];
|
|
1095
|
+
const seenKeys = new Set<string>();
|
|
1096
|
+
for (const request of requests) {
|
|
1097
|
+
if (!request) continue;
|
|
1098
|
+
const key = buildChartKey(request);
|
|
1099
|
+
if (seenKeys.has(key)) continue;
|
|
1100
|
+
seenKeys.add(key);
|
|
1101
|
+
uniqueRequests.push(request);
|
|
1102
|
+
}
|
|
1103
|
+
return uniqueRequests;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
458
1106
|
function applyZoomAroundAnchor(
|
|
459
|
-
view:
|
|
1107
|
+
view: StockChartViewportState,
|
|
460
1108
|
nextZoomLevel: number,
|
|
461
1109
|
anchorRatio: number,
|
|
462
1110
|
history: PricePoint[],
|
|
463
|
-
):
|
|
1111
|
+
): StockChartViewportState {
|
|
464
1112
|
if (history.length === 0) return view;
|
|
465
1113
|
|
|
466
1114
|
const nextZoom = resolveAnchoredChartZoom(
|
|
@@ -485,6 +1133,7 @@ function buildNativeBitmapKey(
|
|
|
485
1133
|
mode: ChartRenderMode,
|
|
486
1134
|
showVolume: boolean,
|
|
487
1135
|
paletteId: string,
|
|
1136
|
+
indicatorKey: string,
|
|
488
1137
|
): string {
|
|
489
1138
|
const fingerprint = points
|
|
490
1139
|
.map((point) => {
|
|
@@ -492,7 +1141,7 @@ function buildNativeBitmapKey(
|
|
|
492
1141
|
return `${date}:${point.open}:${point.high}:${point.low}:${point.close}:${point.volume ?? 0}`;
|
|
493
1142
|
})
|
|
494
1143
|
.join("|");
|
|
495
|
-
return [pointCount, pixelWidth, pixelHeight, mode, showVolume ? "1" : "0", paletteId, fingerprint].join("::");
|
|
1144
|
+
return [pointCount, pixelWidth, pixelHeight, mode, showVolume ? "1" : "0", paletteId, indicatorKey, fingerprint].join("::");
|
|
496
1145
|
}
|
|
497
1146
|
|
|
498
1147
|
function buildNativeCrosshairBitmapKey(
|
|
@@ -551,6 +1200,270 @@ function resolveSelectionCursor(
|
|
|
551
1200
|
};
|
|
552
1201
|
}
|
|
553
1202
|
|
|
1203
|
+
const INDICATOR_COLORS = [
|
|
1204
|
+
"#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7", "#DDA0DD", "#98D8C8",
|
|
1205
|
+
];
|
|
1206
|
+
|
|
1207
|
+
export function getIndicatorWarmupPeriod(config: IndicatorConfig): number {
|
|
1208
|
+
const periods = [
|
|
1209
|
+
...(config.sma ?? []),
|
|
1210
|
+
...(config.ema ?? []),
|
|
1211
|
+
config.bollinger?.period,
|
|
1212
|
+
config.rsi ?? undefined,
|
|
1213
|
+
config.macd ? config.macd.slow + config.macd.signal : undefined,
|
|
1214
|
+
].filter((period): period is number => typeof period === "number" && Number.isFinite(period) && period > 0);
|
|
1215
|
+
|
|
1216
|
+
return periods.length > 0 ? Math.max(...periods) : 0;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
export function resolveIndicatorBufferRange(
|
|
1220
|
+
visibleRange: TimeRange,
|
|
1221
|
+
currentBufferRange: TimeRange,
|
|
1222
|
+
config: IndicatorConfig,
|
|
1223
|
+
): TimeRange {
|
|
1224
|
+
const warmupPeriod = getIndicatorWarmupPeriod(config);
|
|
1225
|
+
if (warmupPeriod <= 0) return currentBufferRange;
|
|
1226
|
+
|
|
1227
|
+
const visibleDays = RANGE_DAYS[visibleRange];
|
|
1228
|
+
if (!Number.isFinite(visibleDays)) return currentBufferRange;
|
|
1229
|
+
|
|
1230
|
+
const targetDays = visibleDays + warmupPeriod;
|
|
1231
|
+
const warmupRange = TIME_RANGE_ORDER.find((range) => RANGE_DAYS[range] >= targetDays) ?? "ALL";
|
|
1232
|
+
return maxTimeRange(currentBufferRange, warmupRange);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function getPricePointTime(point: Pick<PricePoint, "date"> | Pick<ProjectedChartPoint, "date">): number {
|
|
1236
|
+
return coerceChartDate(point.date as Date | string | number).getTime();
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function buildProjectedSourceIndexMap(
|
|
1240
|
+
sourcePoints: readonly PricePoint[],
|
|
1241
|
+
projectedPoints: readonly ProjectedChartPoint[],
|
|
1242
|
+
sourceIndexOffset = 0,
|
|
1243
|
+
): Map<number, number> {
|
|
1244
|
+
const sourceIndexByTime = new Map<number, number>();
|
|
1245
|
+
sourcePoints.forEach((point, index) => {
|
|
1246
|
+
sourceIndexByTime.set(getPricePointTime(point), sourceIndexOffset + index);
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
const projectedIndexBySourceIndex = new Map<number, number>();
|
|
1250
|
+
projectedPoints.forEach((point, projectedIndex) => {
|
|
1251
|
+
const sourceIndex = sourceIndexByTime.get(getPricePointTime(point));
|
|
1252
|
+
if (sourceIndex !== undefined) {
|
|
1253
|
+
projectedIndexBySourceIndex.set(sourceIndex, projectedIndex);
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
return projectedIndexBySourceIndex;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function findPointBySourceIndex<TPoint extends { index: number }>(
|
|
1260
|
+
points: readonly TPoint[],
|
|
1261
|
+
sourceIndex: number,
|
|
1262
|
+
): TPoint | null {
|
|
1263
|
+
let low = 0;
|
|
1264
|
+
let high = points.length - 1;
|
|
1265
|
+
|
|
1266
|
+
while (low <= high) {
|
|
1267
|
+
const mid = Math.floor((low + high) / 2);
|
|
1268
|
+
const point = points[mid]!;
|
|
1269
|
+
if (point.index === sourceIndex) return point;
|
|
1270
|
+
if (point.index < sourceIndex) {
|
|
1271
|
+
low = mid + 1;
|
|
1272
|
+
} else {
|
|
1273
|
+
high = mid - 1;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function reindexOverlayPoints(
|
|
1281
|
+
points: readonly OverlayPoint[],
|
|
1282
|
+
projectedIndexBySourceIndex: ReadonlyMap<number, number>,
|
|
1283
|
+
): OverlayPoint[] {
|
|
1284
|
+
const reindexed: OverlayPoint[] = [];
|
|
1285
|
+
projectedIndexBySourceIndex.forEach((projectedIndex, sourceIndex) => {
|
|
1286
|
+
const point = findPointBySourceIndex(points, sourceIndex);
|
|
1287
|
+
if (point) reindexed.push({ index: projectedIndex, value: point.value });
|
|
1288
|
+
});
|
|
1289
|
+
return reindexed;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function reindexOscillatorPoints(
|
|
1293
|
+
points: readonly OscillatorPoint[],
|
|
1294
|
+
projectedIndexBySourceIndex: ReadonlyMap<number, number>,
|
|
1295
|
+
): OscillatorPoint[] {
|
|
1296
|
+
const reindexed: OscillatorPoint[] = [];
|
|
1297
|
+
projectedIndexBySourceIndex.forEach((projectedIndex, sourceIndex) => {
|
|
1298
|
+
const point = findPointBySourceIndex(points, sourceIndex);
|
|
1299
|
+
if (point) reindexed.push({ index: projectedIndex, value: point.value });
|
|
1300
|
+
});
|
|
1301
|
+
return reindexed;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function reindexMacdResult(
|
|
1305
|
+
result: MacdResult | null,
|
|
1306
|
+
projectedIndexBySourceIndex: ReadonlyMap<number, number>,
|
|
1307
|
+
): MacdResult | null {
|
|
1308
|
+
if (!result) return null;
|
|
1309
|
+
return {
|
|
1310
|
+
macd: reindexOscillatorPoints(result.macd, projectedIndexBySourceIndex),
|
|
1311
|
+
signal: reindexOscillatorPoints(result.signal, projectedIndexBySourceIndex),
|
|
1312
|
+
histogram: reindexOscillatorPoints(result.histogram, projectedIndexBySourceIndex),
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function computeIndicatorOverlays(
|
|
1317
|
+
closes: number[],
|
|
1318
|
+
config: IndicatorConfig,
|
|
1319
|
+
): ChartIndicatorOverlays {
|
|
1320
|
+
let colorIdx = 0;
|
|
1321
|
+
const nextColor = () => INDICATOR_COLORS[colorIdx++ % INDICATOR_COLORS.length]!;
|
|
1322
|
+
|
|
1323
|
+
const smaLines = (config.sma ?? []).map((period) => ({
|
|
1324
|
+
period,
|
|
1325
|
+
points: computeSMA(closes, period),
|
|
1326
|
+
color: nextColor(),
|
|
1327
|
+
}));
|
|
1328
|
+
|
|
1329
|
+
const emaLines = (config.ema ?? []).map((period) => ({
|
|
1330
|
+
period,
|
|
1331
|
+
points: computeEMA(closes, period),
|
|
1332
|
+
color: nextColor(),
|
|
1333
|
+
}));
|
|
1334
|
+
|
|
1335
|
+
const bollinger = config.bollinger
|
|
1336
|
+
? { ...computeBollingerBands(closes, config.bollinger.period, config.bollinger.stdDev), color: nextColor() }
|
|
1337
|
+
: null;
|
|
1338
|
+
|
|
1339
|
+
const rsi = config.rsi ? computeRSI(closes, config.rsi) : null;
|
|
1340
|
+
const macd = config.macd
|
|
1341
|
+
? computeMACD(closes, config.macd.fast, config.macd.slow, config.macd.signal)
|
|
1342
|
+
: null;
|
|
1343
|
+
|
|
1344
|
+
return { smaLines, emaLines, bollinger, rsi, macd };
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
export function computeProjectedIndicatorOverlays(
|
|
1348
|
+
sourcePoints: readonly PricePoint[],
|
|
1349
|
+
projectedPoints: readonly ProjectedChartPoint[],
|
|
1350
|
+
config: IndicatorConfig,
|
|
1351
|
+
): ChartIndicatorOverlays {
|
|
1352
|
+
const closes = sourcePoints.map((point) => point.close);
|
|
1353
|
+
const overlays = computeIndicatorOverlays(closes, config);
|
|
1354
|
+
return reindexIndicatorOverlaysForProjection(overlays, sourcePoints, projectedPoints);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
export function reindexIndicatorOverlaysForProjection(
|
|
1358
|
+
overlays: ChartIndicatorOverlays,
|
|
1359
|
+
sourcePoints: readonly PricePoint[],
|
|
1360
|
+
projectedPoints: readonly ProjectedChartPoint[],
|
|
1361
|
+
sourceIndexOffset = 0,
|
|
1362
|
+
): ChartIndicatorOverlays {
|
|
1363
|
+
const projectedIndexBySourceIndex = buildProjectedSourceIndexMap(
|
|
1364
|
+
sourcePoints,
|
|
1365
|
+
projectedPoints,
|
|
1366
|
+
sourceIndexOffset,
|
|
1367
|
+
);
|
|
1368
|
+
|
|
1369
|
+
return {
|
|
1370
|
+
smaLines: overlays.smaLines.map((line) => ({
|
|
1371
|
+
...line,
|
|
1372
|
+
points: reindexOverlayPoints(line.points, projectedIndexBySourceIndex),
|
|
1373
|
+
})),
|
|
1374
|
+
emaLines: overlays.emaLines.map((line) => ({
|
|
1375
|
+
...line,
|
|
1376
|
+
points: reindexOverlayPoints(line.points, projectedIndexBySourceIndex),
|
|
1377
|
+
})),
|
|
1378
|
+
bollinger: overlays.bollinger
|
|
1379
|
+
? {
|
|
1380
|
+
...overlays.bollinger,
|
|
1381
|
+
upper: reindexOverlayPoints(overlays.bollinger.upper, projectedIndexBySourceIndex),
|
|
1382
|
+
middle: reindexOverlayPoints(overlays.bollinger.middle, projectedIndexBySourceIndex),
|
|
1383
|
+
lower: reindexOverlayPoints(overlays.bollinger.lower, projectedIndexBySourceIndex),
|
|
1384
|
+
}
|
|
1385
|
+
: null,
|
|
1386
|
+
rsi: overlays.rsi ? reindexOscillatorPoints(overlays.rsi, projectedIndexBySourceIndex) : null,
|
|
1387
|
+
macd: reindexMacdResult(overlays.macd, projectedIndexBySourceIndex),
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function buildIndicatorRenderKey(indicators: ChartIndicatorOverlays | null): string {
|
|
1392
|
+
if (!indicators) return "none";
|
|
1393
|
+
|
|
1394
|
+
const pointKey = (points: readonly OverlayPoint[]) => {
|
|
1395
|
+
const first = points[0];
|
|
1396
|
+
const last = points[points.length - 1];
|
|
1397
|
+
return first && last
|
|
1398
|
+
? `${points.length}:${first.index}:${first.value.toFixed(6)}:${last.index}:${last.value.toFixed(6)}`
|
|
1399
|
+
: "0";
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
return [
|
|
1403
|
+
indicators.smaLines.map((line) => `sma:${line.period}:${line.color}:${pointKey(line.points)}`).join(","),
|
|
1404
|
+
indicators.emaLines.map((line) => `ema:${line.period}:${line.color}:${pointKey(line.points)}`).join(","),
|
|
1405
|
+
indicators.bollinger
|
|
1406
|
+
? [
|
|
1407
|
+
`bb:${indicators.bollinger.color}`,
|
|
1408
|
+
pointKey(indicators.bollinger.upper),
|
|
1409
|
+
pointKey(indicators.bollinger.middle),
|
|
1410
|
+
pointKey(indicators.bollinger.lower),
|
|
1411
|
+
].join(":")
|
|
1412
|
+
: "",
|
|
1413
|
+
].join("|");
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function buildIndicatorConfigKey(config: IndicatorConfig): string {
|
|
1417
|
+
return [
|
|
1418
|
+
`sma:${(config.sma ?? []).join(",")}`,
|
|
1419
|
+
`ema:${(config.ema ?? []).join(",")}`,
|
|
1420
|
+
config.bollinger ? `bb:${config.bollinger.period}:${config.bollinger.stdDev}` : "bb:",
|
|
1421
|
+
`rsi:${config.rsi ?? ""}`,
|
|
1422
|
+
config.macd ? `macd:${config.macd.fast}:${config.macd.slow}:${config.macd.signal}` : "macd:",
|
|
1423
|
+
].join("|");
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function buildIndicatorSourceKey(
|
|
1427
|
+
sourcePoints: readonly PricePoint[],
|
|
1428
|
+
config: IndicatorConfig,
|
|
1429
|
+
): string {
|
|
1430
|
+
const first = sourcePoints[0];
|
|
1431
|
+
const last = sourcePoints[sourcePoints.length - 1];
|
|
1432
|
+
return [
|
|
1433
|
+
buildIndicatorConfigKey(config),
|
|
1434
|
+
sourcePoints.length,
|
|
1435
|
+
first ? getPricePointTime(first) : "",
|
|
1436
|
+
first?.close ?? "",
|
|
1437
|
+
last ? getPricePointTime(last) : "",
|
|
1438
|
+
last?.close ?? "",
|
|
1439
|
+
].join(":");
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function buildIndicatorProjectionKey(options: {
|
|
1443
|
+
sourceKey: string;
|
|
1444
|
+
sourcePoints: readonly PricePoint[];
|
|
1445
|
+
sourceIndexOffset: number;
|
|
1446
|
+
projectedPoints: readonly ProjectedChartPoint[];
|
|
1447
|
+
mode: ChartRenderMode;
|
|
1448
|
+
}): string {
|
|
1449
|
+
const firstSource = options.sourcePoints[0];
|
|
1450
|
+
const lastSource = options.sourcePoints[options.sourcePoints.length - 1];
|
|
1451
|
+
const firstProjected = options.projectedPoints[0];
|
|
1452
|
+
const lastProjected = options.projectedPoints[options.projectedPoints.length - 1];
|
|
1453
|
+
|
|
1454
|
+
return [
|
|
1455
|
+
options.sourceKey,
|
|
1456
|
+
options.sourceIndexOffset,
|
|
1457
|
+
options.sourcePoints.length,
|
|
1458
|
+
firstSource ? getPricePointTime(firstSource) : "",
|
|
1459
|
+
lastSource ? getPricePointTime(lastSource) : "",
|
|
1460
|
+
options.projectedPoints.length,
|
|
1461
|
+
firstProjected ? getPricePointTime(firstProjected) : "",
|
|
1462
|
+
lastProjected ? getPricePointTime(lastProjected) : "",
|
|
1463
|
+
options.mode,
|
|
1464
|
+
].join(":");
|
|
1465
|
+
}
|
|
1466
|
+
|
|
554
1467
|
export function StockChart(props: StockChartProps) {
|
|
555
1468
|
const { symbol, ticker, financials } = usePaneTicker();
|
|
556
1469
|
return <ResolvedStockChart {...props} symbol={symbol} ticker={ticker} financials={financials} />;
|
|
@@ -565,6 +1478,9 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
565
1478
|
axisMode = "price",
|
|
566
1479
|
historyOverride = null,
|
|
567
1480
|
currencyOverride = null,
|
|
1481
|
+
indicatorConfig: indicatorConfigOverride,
|
|
1482
|
+
showVolume: showVolumeOverride,
|
|
1483
|
+
footerControls,
|
|
568
1484
|
symbol,
|
|
569
1485
|
ticker,
|
|
570
1486
|
financials,
|
|
@@ -573,30 +1489,51 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
573
1489
|
const dispatch = useAppDispatch();
|
|
574
1490
|
const config = useAppSelector((state) => state.config);
|
|
575
1491
|
const paneId = usePaneInstanceId();
|
|
1492
|
+
const pane = usePaneInstance();
|
|
576
1493
|
const nativeSurfaceManager = useMemo(() => getNativeSurfaceManager(renderer), [renderer]);
|
|
577
1494
|
const defaultRenderMode = config.chartPreferences.defaultRenderMode;
|
|
578
1495
|
const preferredRenderer = config.chartPreferences.renderer;
|
|
579
1496
|
const [storedRangePreset] = usePaneSettingValue("chartRangePreset", DEFAULT_TICKER_CHART_RANGE_PRESET);
|
|
580
1497
|
const [storedResolution] = usePaneSettingValue<ChartResolution>("chartResolution", DEFAULT_TICKER_CHART_RESOLUTION);
|
|
581
1498
|
const persistChartControls = usePersistChartControlSelection("chartRangePreset");
|
|
582
|
-
const [viewState, setViewState] = useState<
|
|
1499
|
+
const [viewState, setViewState] = useState<StockChartViewportState>({
|
|
583
1500
|
presetRange: compact ? "1Y" : storedRangePreset,
|
|
584
1501
|
bufferRange: compact ? "1Y" : storedRangePreset,
|
|
585
1502
|
activePreset: compact ? null : storedRangePreset,
|
|
586
|
-
resolution: compact ? "auto" : storedResolution,
|
|
587
1503
|
panOffset: 0,
|
|
588
1504
|
zoomLevel: 1,
|
|
589
1505
|
cursorX: null,
|
|
590
1506
|
cursorY: null,
|
|
591
1507
|
renderMode: defaultRenderMode,
|
|
592
1508
|
});
|
|
1509
|
+
// Read indicator config from the chart caller, falling back to legacy pane settings.
|
|
1510
|
+
const indicatorConfig: IndicatorConfig = indicatorConfigOverride ?? ((pane?.settings?.indicators as IndicatorConfig) ?? EMPTY_INDICATOR_CONFIG);
|
|
1511
|
+
const hasIndicators = !!(indicatorConfig.sma?.length || indicatorConfig.ema?.length || indicatorConfig.rsi || indicatorConfig.macd || indicatorConfig.bollinger);
|
|
1512
|
+
const indicatorBufferRange = useMemo(
|
|
1513
|
+
() => resolveIndicatorBufferRange(viewState.presetRange, viewState.bufferRange, indicatorConfig),
|
|
1514
|
+
[indicatorConfig, viewState.bufferRange, viewState.presetRange],
|
|
1515
|
+
);
|
|
1516
|
+
const [requestedResolution, setRequestedResolution] = useState<ChartResolution>(
|
|
1517
|
+
compact ? "auto" : storedResolution,
|
|
1518
|
+
);
|
|
593
1519
|
const [resolutionSupport, setResolutionSupport] = useState<ChartResolutionSupport[] | null>(null);
|
|
594
|
-
const
|
|
1520
|
+
const supportMap = useMemo(() => buildChartResolutionSupportMap(resolutionSupport ?? []), [resolutionSupport]);
|
|
1521
|
+
const [renderedAutoView, setRenderedAutoView] = useState<AutoRenderedView | null>(null);
|
|
1522
|
+
const [pendingAutoWindowOverride, setPendingAutoWindowOverride] = useState<DateWindowRange | null>(null);
|
|
1523
|
+
const [lastReadyRenderView, setLastReadyRenderView] = useState<CachedRenderedView | null>(null);
|
|
1524
|
+
const showVolume = showVolumeOverride ?? !compact;
|
|
595
1525
|
const [kittySupport, setKittySupport] = useState<boolean | null>(() => getCachedKittySupport(renderer));
|
|
596
1526
|
const [displayCursor, setDisplayCursor] = useState<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
|
|
597
1527
|
const plotRef = useRef<BoxRenderable | null>(null);
|
|
598
|
-
const
|
|
599
|
-
const
|
|
1528
|
+
const nativeSurfaceScope = compact ? "compact" : "full";
|
|
1529
|
+
const nativeBaseSurfaceId = useMemo(
|
|
1530
|
+
() => `chart-surface:${paneId}:${nativeSurfaceScope}:base`,
|
|
1531
|
+
[nativeSurfaceScope, paneId],
|
|
1532
|
+
);
|
|
1533
|
+
const nativeCrosshairSurfaceId = useMemo(
|
|
1534
|
+
() => `chart-surface:${paneId}:${nativeSurfaceScope}:crosshair`,
|
|
1535
|
+
[nativeSurfaceScope, paneId],
|
|
1536
|
+
);
|
|
600
1537
|
const dragRef = useRef<DragState | null>(null);
|
|
601
1538
|
const lastNativeGeometryRef = useRef<{ rect: CellRect; visibleRect: CellRect | null } | null>(null);
|
|
602
1539
|
const lastNativeBaseBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
|
|
@@ -608,6 +1545,9 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
608
1545
|
const pendingCanonicalResetRef = useRef(1);
|
|
609
1546
|
const appliedCanonicalResetRef = useRef(0);
|
|
610
1547
|
const pendingExpansionRef = useRef<PendingExpansionAction>(null);
|
|
1548
|
+
const pendingAutoWindowRef = useRef<DateWindowRange | null>(null);
|
|
1549
|
+
const scrollPanCellRemainderRef = useRef(0);
|
|
1550
|
+
const lastAppliedStoredSelectionKeyRef = useRef<string | null>(null);
|
|
611
1551
|
|
|
612
1552
|
const commitDisplayCursor = (next: DisplayCursorState) => {
|
|
613
1553
|
displayCursorRef.current = next;
|
|
@@ -684,15 +1624,16 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
684
1624
|
stopDisplayCursorAnimation();
|
|
685
1625
|
lastNativeBaseBitmapRef.current = null;
|
|
686
1626
|
lastNativeCrosshairBitmapRef.current = null;
|
|
687
|
-
nativeSurfaceManager.removeSurface(
|
|
688
|
-
nativeSurfaceManager.removeSurface(
|
|
1627
|
+
nativeSurfaceManager.removeSurface(nativeBaseSurfaceId);
|
|
1628
|
+
nativeSurfaceManager.removeSurface(nativeCrosshairSurfaceId);
|
|
689
1629
|
}
|
|
690
|
-
), [nativeSurfaceManager]);
|
|
1630
|
+
), [nativeBaseSurfaceId, nativeCrosshairSurfaceId, nativeSurfaceManager]);
|
|
691
1631
|
|
|
692
1632
|
const instrumentRef = useMemo(
|
|
693
1633
|
() => instrumentFromTicker(ticker, ticker?.metadata.ticker ?? null),
|
|
694
1634
|
[ticker],
|
|
695
1635
|
);
|
|
1636
|
+
const dataProvider = getSharedDataProvider();
|
|
696
1637
|
const capabilityKey = instrumentRef ? [
|
|
697
1638
|
instrumentRef.symbol,
|
|
698
1639
|
instrumentRef.exchange ?? "",
|
|
@@ -701,38 +1642,20 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
701
1642
|
instrumentRef.instrument?.conId ?? "",
|
|
702
1643
|
].join("|") : null;
|
|
703
1644
|
|
|
704
|
-
useEffect(() => {
|
|
705
|
-
if (compact) return;
|
|
706
|
-
pendingCanonicalResetRef.current += 1;
|
|
707
|
-
setViewState((current) => {
|
|
708
|
-
const nextState = resolvePresetSelection(
|
|
709
|
-
{
|
|
710
|
-
...current,
|
|
711
|
-
resolution: storedResolution,
|
|
712
|
-
},
|
|
713
|
-
storedRangePreset,
|
|
714
|
-
storedResolution === "auto" ? null : getSupportMaxRange(resolutionSupport ?? [], storedResolution),
|
|
715
|
-
);
|
|
716
|
-
return nextState;
|
|
717
|
-
});
|
|
718
|
-
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
719
|
-
}, [compact, resolutionSupport, storedRangePreset, storedResolution]);
|
|
720
|
-
|
|
721
1645
|
useEffect(() => {
|
|
722
1646
|
if (compact || !instrumentRef) {
|
|
723
1647
|
setResolutionSupport(null);
|
|
724
1648
|
return;
|
|
725
1649
|
}
|
|
726
|
-
|
|
727
|
-
if (!provider?.getChartResolutionSupport && !provider?.getChartResolutionCapabilities) {
|
|
1650
|
+
if (!dataProvider?.getChartResolutionSupport && !dataProvider?.getChartResolutionCapabilities) {
|
|
728
1651
|
setResolutionSupport(null);
|
|
729
1652
|
return;
|
|
730
1653
|
}
|
|
731
1654
|
|
|
732
1655
|
let cancelled = false;
|
|
733
1656
|
setResolutionSupport(null);
|
|
734
|
-
Promise.resolve(
|
|
735
|
-
?
|
|
1657
|
+
Promise.resolve(dataProvider.getChartResolutionSupport
|
|
1658
|
+
? dataProvider.getChartResolutionSupport(
|
|
736
1659
|
instrumentRef.symbol,
|
|
737
1660
|
instrumentRef.exchange ?? "",
|
|
738
1661
|
{
|
|
@@ -741,8 +1664,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
741
1664
|
instrument: instrumentRef.instrument ?? null,
|
|
742
1665
|
},
|
|
743
1666
|
)
|
|
744
|
-
:
|
|
745
|
-
(provider.getChartResolutionCapabilities?.(
|
|
1667
|
+
: Promise.resolve(dataProvider.getChartResolutionCapabilities?.(
|
|
746
1668
|
instrumentRef.symbol,
|
|
747
1669
|
instrumentRef.exchange ?? "",
|
|
748
1670
|
{
|
|
@@ -750,8 +1672,9 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
750
1672
|
brokerInstanceId: instrumentRef.brokerInstanceId,
|
|
751
1673
|
instrument: instrumentRef.instrument ?? null,
|
|
752
1674
|
},
|
|
753
|
-
) ?? []).
|
|
754
|
-
|
|
1675
|
+
) ?? []).then((capabilities) => normalizeChartResolutionSupport(
|
|
1676
|
+
capabilities.map((resolution) => ({ resolution, maxRange: "ALL" })),
|
|
1677
|
+
))
|
|
755
1678
|
).then((support) => {
|
|
756
1679
|
if (!cancelled) {
|
|
757
1680
|
setResolutionSupport(support);
|
|
@@ -762,99 +1685,797 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
762
1685
|
}
|
|
763
1686
|
});
|
|
764
1687
|
|
|
765
|
-
return () => {
|
|
766
|
-
cancelled = true;
|
|
1688
|
+
return () => {
|
|
1689
|
+
cancelled = true;
|
|
1690
|
+
};
|
|
1691
|
+
}, [capabilityKey, compact, dataProvider, instrumentRef]);
|
|
1692
|
+
|
|
1693
|
+
const effectiveResolutionSupport = useMemo<ChartResolutionSupport[]>(() => (
|
|
1694
|
+
resolutionSupport ?? DEFAULT_VISIBLE_MANUAL_CHART_RESOLUTIONS.map((resolution) => ({ resolution, maxRange: "ALL" as const }))
|
|
1695
|
+
), [resolutionSupport]);
|
|
1696
|
+
const selectionSupportMap = useMemo(
|
|
1697
|
+
() => buildChartResolutionSupportMap(effectiveResolutionSupport),
|
|
1698
|
+
[effectiveResolutionSupport],
|
|
1699
|
+
);
|
|
1700
|
+
const availableManualResolutions = resolutionSupport?.map((entry) => entry.resolution) ?? DEFAULT_VISIBLE_MANUAL_CHART_RESOLUTIONS;
|
|
1701
|
+
useEffect(() => {
|
|
1702
|
+
if (compact) return;
|
|
1703
|
+
const storedSelectionKey = `${storedRangePreset}:${storedResolution}`;
|
|
1704
|
+
if (lastAppliedStoredSelectionKeyRef.current === storedSelectionKey) return;
|
|
1705
|
+
|
|
1706
|
+
lastAppliedStoredSelectionKeyRef.current = storedSelectionKey;
|
|
1707
|
+
setRequestedResolution(storedResolution);
|
|
1708
|
+
pendingCanonicalResetRef.current += 1;
|
|
1709
|
+
pendingAutoWindowRef.current = null;
|
|
1710
|
+
setPendingAutoWindowOverride(null);
|
|
1711
|
+
if (storedResolution === "auto") {
|
|
1712
|
+
setRenderedAutoView(null);
|
|
1713
|
+
}
|
|
1714
|
+
setViewState((current) => {
|
|
1715
|
+
return storedResolution === "auto"
|
|
1716
|
+
? resolveViewportStoredSelection(current, storedRangePreset, storedResolution, selectionSupportMap)
|
|
1717
|
+
: resolveViewportPresetSelection(
|
|
1718
|
+
current,
|
|
1719
|
+
storedRangePreset,
|
|
1720
|
+
getSupportMaxRange(selectionSupportMap, storedResolution),
|
|
1721
|
+
);
|
|
1722
|
+
});
|
|
1723
|
+
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
1724
|
+
}, [compact, selectionSupportMap, storedRangePreset, storedResolution]);
|
|
1725
|
+
const effectiveResolution: ChartResolution = !compact
|
|
1726
|
+
&& requestedResolution !== "auto"
|
|
1727
|
+
&& resolutionSupport !== null
|
|
1728
|
+
&& !supportMap.has(requestedResolution)
|
|
1729
|
+
? "auto"
|
|
1730
|
+
: requestedResolution;
|
|
1731
|
+
const resolutionChips = useMemo(
|
|
1732
|
+
() => sortChartResolutions(["auto", ...availableManualResolutions] as ChartResolution[]),
|
|
1733
|
+
[availableManualResolutions],
|
|
1734
|
+
);
|
|
1735
|
+
|
|
1736
|
+
const boundsChartEntry = useChartQuery(
|
|
1737
|
+
!compact && instrumentRef
|
|
1738
|
+
? {
|
|
1739
|
+
instrument: instrumentRef,
|
|
1740
|
+
bufferRange: indicatorBufferRange,
|
|
1741
|
+
granularity: "range",
|
|
1742
|
+
}
|
|
1743
|
+
: null,
|
|
1744
|
+
);
|
|
1745
|
+
const fallbackPriceHistory = historyOverride ?? financials?.priceHistory ?? [];
|
|
1746
|
+
const rawBoundsBodyState = resolveChartBodyState(
|
|
1747
|
+
boundsChartEntry,
|
|
1748
|
+
(value) => Array.isArray(value) && value.length > 0,
|
|
1749
|
+
"No price history available.",
|
|
1750
|
+
);
|
|
1751
|
+
const boundsBodyState = !compact && !boundsChartEntry && fallbackPriceHistory.length > 0
|
|
1752
|
+
? {
|
|
1753
|
+
data: fallbackPriceHistory,
|
|
1754
|
+
blocking: false,
|
|
1755
|
+
updating: false,
|
|
1756
|
+
emptyMessage: null,
|
|
1757
|
+
errorMessage: null,
|
|
1758
|
+
}
|
|
1759
|
+
: rawBoundsBodyState;
|
|
1760
|
+
const axisSectionWidthBudget = compact
|
|
1761
|
+
? axisMode === "percent" ? 11 : 8
|
|
1762
|
+
: axisMode === "percent" ? 11 : 10;
|
|
1763
|
+
const axisRightPadding = 1;
|
|
1764
|
+
const minimumAxisWidth = axisMode === "percent" ? 5 : 4;
|
|
1765
|
+
const axisGap = axisSectionWidthBudget > 0 ? 1 : 0;
|
|
1766
|
+
const minChartWidth = compact ? 12 : 20;
|
|
1767
|
+
const measurementChartWidth = Math.max(width - axisSectionWidthBudget - axisGap, minChartWidth);
|
|
1768
|
+
const headerRows = compact ? 0 : 4;
|
|
1769
|
+
const helpRow = compact ? 0 : 1;
|
|
1770
|
+
const timeAxisRow = 1;
|
|
1771
|
+
const volumeHeight = showVolume && !compact ? 3 : 0;
|
|
1772
|
+
const chartHeight = Math.max(height - headerRows - helpRow - timeAxisRow, 4);
|
|
1773
|
+
const chartCurrency = currencyOverride ?? financials?.quote?.currency ?? ticker?.metadata.currency ?? "USD";
|
|
1774
|
+
const chartAssetCategory = ticker?.metadata.assetCategory;
|
|
1775
|
+
const boundsHistory = compact
|
|
1776
|
+
? fallbackPriceHistory
|
|
1777
|
+
: (boundsBodyState.data ?? []);
|
|
1778
|
+
const boundsHistoryDates = useMemo(() => getPointDates(boundsHistory), [boundsHistory]);
|
|
1779
|
+
const baseDateBounds = useMemo(() => getDateWindowBounds(boundsHistoryDates), [boundsHistoryDates]);
|
|
1780
|
+
const manualVisibleDateWindow = useMemo(
|
|
1781
|
+
() => buildVisibleDateWindow(boundsHistoryDates, viewState.panOffset, viewState.zoomLevel),
|
|
1782
|
+
[boundsHistoryDates, viewState.panOffset, viewState.zoomLevel],
|
|
1783
|
+
);
|
|
1784
|
+
const canonicalAutoWindow = useMemo(
|
|
1785
|
+
() => buildPresetDateWindow(boundsHistoryDates, viewState.presetRange),
|
|
1786
|
+
[boundsHistoryDates, viewState.presetRange],
|
|
1787
|
+
);
|
|
1788
|
+
const autoMinimumSpanMs = useMemo(() => {
|
|
1789
|
+
const finestSupportedResolution = effectiveResolutionSupport[0]?.resolution;
|
|
1790
|
+
return finestSupportedResolution
|
|
1791
|
+
? CHART_RESOLUTION_STEP_MS[finestSupportedResolution]
|
|
1792
|
+
: getMinimumDateStepMs(boundsHistoryDates);
|
|
1793
|
+
}, [boundsHistoryDates, effectiveResolutionSupport]);
|
|
1794
|
+
const renderedAutoWindow = useMemo(() => (
|
|
1795
|
+
effectiveResolution !== "auto"
|
|
1796
|
+
? null
|
|
1797
|
+
: clampDateWindowToBounds(renderedAutoView?.window ?? canonicalAutoWindow, baseDateBounds, autoMinimumSpanMs)
|
|
1798
|
+
), [autoMinimumSpanMs, baseDateBounds, canonicalAutoWindow, effectiveResolution, renderedAutoView]);
|
|
1799
|
+
const plannedAutoWindow = useMemo(() => (
|
|
1800
|
+
effectiveResolution !== "auto"
|
|
1801
|
+
? null
|
|
1802
|
+
: clampDateWindowToBounds(
|
|
1803
|
+
pendingAutoWindowOverride ?? renderedAutoWindow ?? canonicalAutoWindow,
|
|
1804
|
+
baseDateBounds,
|
|
1805
|
+
autoMinimumSpanMs,
|
|
1806
|
+
)
|
|
1807
|
+
), [
|
|
1808
|
+
autoMinimumSpanMs,
|
|
1809
|
+
baseDateBounds,
|
|
1810
|
+
canonicalAutoWindow,
|
|
1811
|
+
effectiveResolution,
|
|
1812
|
+
pendingAutoWindowOverride,
|
|
1813
|
+
renderedAutoWindow,
|
|
1814
|
+
]);
|
|
1815
|
+
const hasResolutionSupportApi = !!dataProvider?.getChartResolutionSupport || !!dataProvider?.getChartResolutionCapabilities;
|
|
1816
|
+
const plannedAutoResolution = useMemo<ManualChartResolution | null>(() => {
|
|
1817
|
+
if (
|
|
1818
|
+
compact
|
|
1819
|
+
|| effectiveResolution !== "auto"
|
|
1820
|
+
|| !plannedAutoWindow?.start
|
|
1821
|
+
|| !plannedAutoWindow.end
|
|
1822
|
+
) {
|
|
1823
|
+
return null;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
const spanMs = plannedAutoWindow.end.getTime() - plannedAutoWindow.start.getTime();
|
|
1827
|
+
if (!Number.isFinite(spanMs) || spanMs < 0) return null;
|
|
1828
|
+
if (hasResolutionSupportApi && resolutionSupport === null) return null;
|
|
1829
|
+
return hasResolutionSupportApi
|
|
1830
|
+
? getBestSupportedResolutionForVisibleWindow(plannedAutoWindow, supportMap, measurementChartWidth)
|
|
1831
|
+
: (resolveBarSize(spanMs) as ManualChartResolution | null);
|
|
1832
|
+
}, [compact, plannedAutoWindow, effectiveResolution, hasResolutionSupportApi, measurementChartWidth, resolutionSupport, supportMap]);
|
|
1833
|
+
const plannedDateWindow = effectiveResolution === "auto"
|
|
1834
|
+
? plannedAutoWindow
|
|
1835
|
+
: manualVisibleDateWindow;
|
|
1836
|
+
const plannedManualResolution = effectiveResolution === "auto"
|
|
1837
|
+
? plannedAutoResolution
|
|
1838
|
+
: effectiveResolution;
|
|
1839
|
+
const plannedWindowRange = useMemo(
|
|
1840
|
+
() => getTimeRangeForDateWindow(plannedDateWindow),
|
|
1841
|
+
[plannedDateWindow],
|
|
1842
|
+
);
|
|
1843
|
+
const renderCandidates = useMemo<ResolvedRenderCandidate[]>(() => (
|
|
1844
|
+
buildResolutionFallbackChain(plannedManualResolution, plannedWindowRange, supportMap).map((candidateResolution) => {
|
|
1845
|
+
const plan = buildResolvedChartRequestPlan({
|
|
1846
|
+
compact,
|
|
1847
|
+
historyOverride,
|
|
1848
|
+
instrumentRef,
|
|
1849
|
+
requestedWindow: plannedDateWindow,
|
|
1850
|
+
effectiveResolution,
|
|
1851
|
+
effectiveManualResolution: candidateResolution,
|
|
1852
|
+
bounds: baseDateBounds,
|
|
1853
|
+
bufferRange: indicatorBufferRange,
|
|
1854
|
+
minimumBufferRange: hasIndicators ? indicatorBufferRange : null,
|
|
1855
|
+
support: supportMap,
|
|
1856
|
+
hasResolutionHistoryApi: !!dataProvider?.getPriceHistoryForResolution,
|
|
1857
|
+
hasDetailedHistoryApi: !!dataProvider?.getDetailedPriceHistory,
|
|
1858
|
+
minimumSpanMs: autoMinimumSpanMs,
|
|
1859
|
+
});
|
|
1860
|
+
return {
|
|
1861
|
+
resolution: candidateResolution,
|
|
1862
|
+
plan,
|
|
1863
|
+
resolutionRequestKey: plan.resolutionRequest ? buildChartKey(plan.resolutionRequest) : null,
|
|
1864
|
+
detailRequestKey: plan.detailRequest ? buildChartKey(plan.detailRequest) : null,
|
|
1865
|
+
};
|
|
1866
|
+
})
|
|
1867
|
+
), [
|
|
1868
|
+
autoMinimumSpanMs,
|
|
1869
|
+
baseDateBounds,
|
|
1870
|
+
compact,
|
|
1871
|
+
dataProvider,
|
|
1872
|
+
plannedManualResolution,
|
|
1873
|
+
effectiveResolution,
|
|
1874
|
+
historyOverride,
|
|
1875
|
+
instrumentRef,
|
|
1876
|
+
plannedDateWindow,
|
|
1877
|
+
plannedWindowRange,
|
|
1878
|
+
supportMap,
|
|
1879
|
+
indicatorBufferRange,
|
|
1880
|
+
hasIndicators,
|
|
1881
|
+
]);
|
|
1882
|
+
const candidateResolutionRequests = useMemo(
|
|
1883
|
+
() => dedupeChartRequests(renderCandidates.map((candidate) => candidate.plan.resolutionRequest)),
|
|
1884
|
+
[renderCandidates],
|
|
1885
|
+
);
|
|
1886
|
+
const candidateResolutionEntries = useChartQueries(candidateResolutionRequests);
|
|
1887
|
+
const candidateDetailRequests = useMemo(() => (
|
|
1888
|
+
dedupeChartRequests(renderCandidates.flatMap((candidate) => {
|
|
1889
|
+
if (!candidate.plan.detailRequest) return [];
|
|
1890
|
+
if (!candidate.plan.resolutionRequest) return [candidate.plan.detailRequest];
|
|
1891
|
+
|
|
1892
|
+
const resolutionBodyState = resolveChartBodyState(
|
|
1893
|
+
candidateResolutionEntries.get(candidate.resolutionRequestKey!),
|
|
1894
|
+
(value) => Array.isArray(value) && value.length > 0,
|
|
1895
|
+
"No price history available.",
|
|
1896
|
+
);
|
|
1897
|
+
const resolutionAccepted = isSeriesAcceptedForRequest(
|
|
1898
|
+
resolutionBodyState.data ?? [],
|
|
1899
|
+
plannedDateWindow,
|
|
1900
|
+
candidate.resolution,
|
|
1901
|
+
{
|
|
1902
|
+
requireAutoDensity: effectiveResolution === "auto",
|
|
1903
|
+
targetResolution: plannedManualResolution,
|
|
1904
|
+
},
|
|
1905
|
+
);
|
|
1906
|
+
|
|
1907
|
+
return !resolutionBodyState.blocking && !resolutionAccepted
|
|
1908
|
+
? [candidate.plan.detailRequest]
|
|
1909
|
+
: [];
|
|
1910
|
+
}))
|
|
1911
|
+
), [candidateResolutionEntries, effectiveResolution, plannedDateWindow, plannedManualResolution, renderCandidates]);
|
|
1912
|
+
const candidateDetailEntries = useChartQueries(candidateDetailRequests, {
|
|
1913
|
+
debounceMs: CHART_DETAIL_REQUEST_DEBOUNCE_MS,
|
|
1914
|
+
});
|
|
1915
|
+
const boundsHistoryCompatible = useMemo(() => (
|
|
1916
|
+
isSeriesAcceptedForRequest(boundsHistory, plannedDateWindow, plannedManualResolution, {
|
|
1917
|
+
requireAutoDensity: effectiveResolution === "auto",
|
|
1918
|
+
targetResolution: plannedManualResolution,
|
|
1919
|
+
})
|
|
1920
|
+
), [boundsHistory, effectiveResolution, plannedManualResolution, plannedDateWindow]);
|
|
1921
|
+
const renderedAutoViewAccepted = useMemo(() => (
|
|
1922
|
+
effectiveResolution === "auto"
|
|
1923
|
+
&& !!renderedAutoView
|
|
1924
|
+
&& !!plannedDateWindow?.start
|
|
1925
|
+
&& !!plannedDateWindow.end
|
|
1926
|
+
&& !!plannedManualResolution
|
|
1927
|
+
&& CHART_RESOLUTION_STEP_MS[renderedAutoView.resolution] <= CHART_RESOLUTION_STEP_MS[plannedManualResolution]
|
|
1928
|
+
&& isSeriesAcceptedForRequest(renderedAutoView.data, plannedDateWindow, renderedAutoView.resolution, {
|
|
1929
|
+
requireAutoDensity: true,
|
|
1930
|
+
targetResolution: plannedManualResolution,
|
|
1931
|
+
})
|
|
1932
|
+
), [effectiveResolution, plannedDateWindow, plannedManualResolution, renderedAutoView]);
|
|
1933
|
+
const resolvedRender = useMemo(() => {
|
|
1934
|
+
const overrideBodyState = {
|
|
1935
|
+
data: fallbackPriceHistory,
|
|
1936
|
+
blocking: false,
|
|
1937
|
+
updating: false,
|
|
1938
|
+
emptyMessage: null,
|
|
1939
|
+
errorMessage: null,
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
if (historyOverride || compact) {
|
|
1943
|
+
return {
|
|
1944
|
+
bodyState: overrideBodyState,
|
|
1945
|
+
resolvedManualResolution: plannedManualResolution,
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
if (!plannedDateWindow?.start || !plannedDateWindow.end || !plannedManualResolution) {
|
|
1950
|
+
if (boundsBodyState.errorMessage) {
|
|
1951
|
+
return {
|
|
1952
|
+
bodyState: {
|
|
1953
|
+
data: null,
|
|
1954
|
+
blocking: false,
|
|
1955
|
+
updating: false,
|
|
1956
|
+
emptyMessage: null,
|
|
1957
|
+
errorMessage: boundsBodyState.errorMessage,
|
|
1958
|
+
},
|
|
1959
|
+
resolvedManualResolution: plannedManualResolution,
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
if (boundsBodyState.emptyMessage) {
|
|
1963
|
+
return {
|
|
1964
|
+
bodyState: {
|
|
1965
|
+
data: null,
|
|
1966
|
+
blocking: false,
|
|
1967
|
+
updating: false,
|
|
1968
|
+
emptyMessage: boundsBodyState.emptyMessage,
|
|
1969
|
+
errorMessage: null,
|
|
1970
|
+
},
|
|
1971
|
+
resolvedManualResolution: plannedManualResolution,
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
return {
|
|
1975
|
+
bodyState: {
|
|
1976
|
+
data: null,
|
|
1977
|
+
blocking: true,
|
|
1978
|
+
updating: false,
|
|
1979
|
+
emptyMessage: null,
|
|
1980
|
+
errorMessage: null,
|
|
1981
|
+
},
|
|
1982
|
+
resolvedManualResolution: plannedManualResolution,
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
if (renderedAutoViewAccepted && renderedAutoView) {
|
|
1987
|
+
return {
|
|
1988
|
+
bodyState: {
|
|
1989
|
+
data: renderedAutoView.data,
|
|
1990
|
+
blocking: false,
|
|
1991
|
+
updating: false,
|
|
1992
|
+
emptyMessage: null,
|
|
1993
|
+
errorMessage: null,
|
|
1994
|
+
},
|
|
1995
|
+
resolvedManualResolution: renderedAutoView.resolution,
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
let firstBlockingState: ReturnType<typeof resolveChartBodyState<PricePoint[]>> | null = null;
|
|
2000
|
+
let firstBlockingResolution: ManualChartResolution | null = null;
|
|
2001
|
+
let firstCompatibleState: ReturnType<typeof resolveChartBodyState<PricePoint[]>> | null = null;
|
|
2002
|
+
let firstCompatibleResolution: ManualChartResolution | null = null;
|
|
2003
|
+
let lastFailureState: ReturnType<typeof resolveChartBodyState<PricePoint[]>> | null = null;
|
|
2004
|
+
|
|
2005
|
+
for (const candidate of renderCandidates) {
|
|
2006
|
+
const resolutionBodyState = candidate.plan.resolutionRequest
|
|
2007
|
+
? resolveChartBodyState(
|
|
2008
|
+
candidateResolutionEntries.get(candidate.resolutionRequestKey!),
|
|
2009
|
+
(value) => Array.isArray(value) && value.length > 0,
|
|
2010
|
+
"No price history available.",
|
|
2011
|
+
)
|
|
2012
|
+
: null;
|
|
2013
|
+
const resolutionCompatible = candidate.plan.resolutionRequest !== null
|
|
2014
|
+
&& isSeriesAcceptedForRequest(
|
|
2015
|
+
resolutionBodyState?.data ?? [],
|
|
2016
|
+
plannedDateWindow,
|
|
2017
|
+
candidate.resolution,
|
|
2018
|
+
{
|
|
2019
|
+
requireAutoDensity: effectiveResolution === "auto",
|
|
2020
|
+
targetResolution: plannedManualResolution,
|
|
2021
|
+
},
|
|
2022
|
+
);
|
|
2023
|
+
if (resolutionCompatible) {
|
|
2024
|
+
if (!firstBlockingState) {
|
|
2025
|
+
return {
|
|
2026
|
+
bodyState: resolutionBodyState!,
|
|
2027
|
+
resolvedManualResolution: candidate.resolution,
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
firstCompatibleState ??= resolutionBodyState!;
|
|
2031
|
+
firstCompatibleResolution ??= candidate.resolution;
|
|
2032
|
+
continue;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
const detailBodyState = candidate.plan.detailRequest
|
|
2036
|
+
? resolveChartBodyState(
|
|
2037
|
+
candidateDetailEntries.get(candidate.detailRequestKey!),
|
|
2038
|
+
(value) => Array.isArray(value) && value.length > 0,
|
|
2039
|
+
"No price history available.",
|
|
2040
|
+
)
|
|
2041
|
+
: null;
|
|
2042
|
+
const detailCompatible = candidate.plan.detailRequest !== null
|
|
2043
|
+
&& isSeriesAcceptedForRequest(
|
|
2044
|
+
detailBodyState?.data ?? [],
|
|
2045
|
+
plannedDateWindow,
|
|
2046
|
+
candidate.resolution,
|
|
2047
|
+
{
|
|
2048
|
+
requireAutoDensity: effectiveResolution === "auto",
|
|
2049
|
+
targetResolution: plannedManualResolution,
|
|
2050
|
+
},
|
|
2051
|
+
);
|
|
2052
|
+
if (detailCompatible) {
|
|
2053
|
+
if (!firstBlockingState) {
|
|
2054
|
+
return {
|
|
2055
|
+
bodyState: detailBodyState!,
|
|
2056
|
+
resolvedManualResolution: candidate.resolution,
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
firstCompatibleState ??= detailBodyState!;
|
|
2060
|
+
firstCompatibleResolution ??= candidate.resolution;
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
if (!firstBlockingState) {
|
|
2065
|
+
if (resolutionBodyState?.blocking) {
|
|
2066
|
+
firstBlockingState = resolutionBodyState;
|
|
2067
|
+
firstBlockingResolution = candidate.resolution;
|
|
2068
|
+
} else if (detailBodyState?.blocking) {
|
|
2069
|
+
firstBlockingState = detailBodyState;
|
|
2070
|
+
firstBlockingResolution = candidate.resolution;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
if (detailBodyState?.errorMessage || detailBodyState?.emptyMessage) {
|
|
2075
|
+
lastFailureState = detailBodyState;
|
|
2076
|
+
} else if (resolutionBodyState?.errorMessage || resolutionBodyState?.emptyMessage) {
|
|
2077
|
+
lastFailureState = resolutionBodyState;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
if (effectiveResolution === "auto" && boundsHistoryCompatible) {
|
|
2082
|
+
return {
|
|
2083
|
+
bodyState: {
|
|
2084
|
+
data: boundsHistory,
|
|
2085
|
+
blocking: false,
|
|
2086
|
+
updating: boundsBodyState.updating,
|
|
2087
|
+
emptyMessage: null,
|
|
2088
|
+
errorMessage: null,
|
|
2089
|
+
},
|
|
2090
|
+
resolvedManualResolution: plannedManualResolution,
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
if (firstCompatibleState) {
|
|
2095
|
+
return {
|
|
2096
|
+
bodyState: firstBlockingState
|
|
2097
|
+
? {
|
|
2098
|
+
...firstCompatibleState,
|
|
2099
|
+
blocking: false,
|
|
2100
|
+
updating: true,
|
|
2101
|
+
emptyMessage: null,
|
|
2102
|
+
errorMessage: null,
|
|
2103
|
+
}
|
|
2104
|
+
: firstCompatibleState,
|
|
2105
|
+
resolvedManualResolution: firstCompatibleResolution ?? plannedManualResolution,
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
if (effectiveResolution === "auto" && firstBlockingState && boundsHistory.length > 0) {
|
|
2110
|
+
return {
|
|
2111
|
+
bodyState: {
|
|
2112
|
+
data: boundsHistory,
|
|
2113
|
+
blocking: false,
|
|
2114
|
+
updating: true,
|
|
2115
|
+
emptyMessage: null,
|
|
2116
|
+
errorMessage: null,
|
|
2117
|
+
},
|
|
2118
|
+
resolvedManualResolution: plannedManualResolution,
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
if (effectiveResolution !== "auto" && firstBlockingState && boundsHistoryCompatible) {
|
|
2123
|
+
return {
|
|
2124
|
+
bodyState: {
|
|
2125
|
+
data: boundsHistory,
|
|
2126
|
+
blocking: false,
|
|
2127
|
+
updating: true,
|
|
2128
|
+
emptyMessage: null,
|
|
2129
|
+
errorMessage: null,
|
|
2130
|
+
},
|
|
2131
|
+
resolvedManualResolution: plannedManualResolution,
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
if (firstBlockingState) {
|
|
2136
|
+
return {
|
|
2137
|
+
bodyState: firstBlockingState,
|
|
2138
|
+
resolvedManualResolution: firstBlockingResolution ?? plannedManualResolution,
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
if (renderCandidates[0]?.plan.unsupportedMessage) {
|
|
2143
|
+
return {
|
|
2144
|
+
bodyState: {
|
|
2145
|
+
data: null,
|
|
2146
|
+
blocking: false,
|
|
2147
|
+
updating: false,
|
|
2148
|
+
emptyMessage: null,
|
|
2149
|
+
errorMessage: renderCandidates[0].plan.unsupportedMessage,
|
|
2150
|
+
},
|
|
2151
|
+
resolvedManualResolution: renderCandidates[0].resolution,
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
return {
|
|
2156
|
+
bodyState: lastFailureState ?? {
|
|
2157
|
+
data: null,
|
|
2158
|
+
blocking: false,
|
|
2159
|
+
updating: false,
|
|
2160
|
+
emptyMessage: boundsBodyState.emptyMessage ?? "No price history available.",
|
|
2161
|
+
errorMessage: boundsBodyState.errorMessage,
|
|
2162
|
+
},
|
|
2163
|
+
resolvedManualResolution: plannedManualResolution,
|
|
2164
|
+
};
|
|
2165
|
+
}, [
|
|
2166
|
+
boundsBodyState.emptyMessage,
|
|
2167
|
+
boundsBodyState.errorMessage,
|
|
2168
|
+
boundsBodyState.updating,
|
|
2169
|
+
boundsHistory,
|
|
2170
|
+
boundsHistoryCompatible,
|
|
2171
|
+
candidateDetailEntries,
|
|
2172
|
+
candidateResolutionEntries,
|
|
2173
|
+
compact,
|
|
2174
|
+
effectiveResolution,
|
|
2175
|
+
financials?.priceHistory,
|
|
2176
|
+
historyOverride,
|
|
2177
|
+
plannedDateWindow,
|
|
2178
|
+
plannedManualResolution,
|
|
2179
|
+
renderedAutoView,
|
|
2180
|
+
renderedAutoViewAccepted,
|
|
2181
|
+
renderCandidates,
|
|
2182
|
+
]);
|
|
2183
|
+
const plannedRenderBodyState = resolvedRender.bodyState;
|
|
2184
|
+
const plannedResolvedManualResolution = resolvedRender.resolvedManualResolution;
|
|
2185
|
+
const canCommitPlannedAutoView = effectiveResolution === "auto"
|
|
2186
|
+
&& !!plannedDateWindow?.start
|
|
2187
|
+
&& !!plannedDateWindow.end
|
|
2188
|
+
&& !!plannedResolvedManualResolution
|
|
2189
|
+
&& isSeriesAcceptedForRequest(
|
|
2190
|
+
plannedRenderBodyState.data ?? [],
|
|
2191
|
+
plannedDateWindow,
|
|
2192
|
+
plannedResolvedManualResolution,
|
|
2193
|
+
{
|
|
2194
|
+
requireAutoDensity: true,
|
|
2195
|
+
targetResolution: plannedManualResolution,
|
|
2196
|
+
},
|
|
2197
|
+
);
|
|
2198
|
+
useEffect(() => {
|
|
2199
|
+
if (!canCommitPlannedAutoView || !plannedDateWindow?.start || !plannedDateWindow.end || !plannedResolvedManualResolution || !plannedRenderBodyState.data?.length) {
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
const nextRenderedAutoView: AutoRenderedView = {
|
|
2204
|
+
window: plannedDateWindow,
|
|
2205
|
+
resolution: plannedResolvedManualResolution,
|
|
2206
|
+
data: plannedRenderBodyState.data,
|
|
767
2207
|
};
|
|
768
|
-
|
|
2208
|
+
setRenderedAutoView((current) => (
|
|
2209
|
+
current
|
|
2210
|
+
&& sameDateWindow(current.window, nextRenderedAutoView.window)
|
|
2211
|
+
&& current.resolution === nextRenderedAutoView.resolution
|
|
2212
|
+
&& current.data === nextRenderedAutoView.data
|
|
2213
|
+
? current
|
|
2214
|
+
: nextRenderedAutoView
|
|
2215
|
+
));
|
|
2216
|
+
setPendingAutoWindowOverride((current) => (
|
|
2217
|
+
sameDateWindow(current, plannedDateWindow) ? null : current
|
|
2218
|
+
));
|
|
2219
|
+
}, [
|
|
2220
|
+
canCommitPlannedAutoView,
|
|
2221
|
+
plannedDateWindow,
|
|
2222
|
+
plannedRenderBodyState.data,
|
|
2223
|
+
plannedResolvedManualResolution,
|
|
2224
|
+
]);
|
|
2225
|
+
const shouldRejectPendingAutoView = effectiveResolution === "auto"
|
|
2226
|
+
&& pendingAutoWindowOverride !== null
|
|
2227
|
+
&& !plannedRenderBodyState.blocking
|
|
2228
|
+
&& !plannedRenderBodyState.updating
|
|
2229
|
+
&& !canCommitPlannedAutoView;
|
|
2230
|
+
useEffect(() => {
|
|
2231
|
+
if (!shouldRejectPendingAutoView || !plannedDateWindow?.start || !plannedDateWindow.end) {
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
769
2234
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
2235
|
+
setPendingAutoWindowOverride((current) => (
|
|
2236
|
+
sameDateWindow(current, plannedDateWindow) ? null : current
|
|
2237
|
+
));
|
|
2238
|
+
}, [
|
|
2239
|
+
pendingAutoWindowOverride,
|
|
2240
|
+
plannedDateWindow,
|
|
2241
|
+
plannedRenderBodyState.blocking,
|
|
2242
|
+
plannedRenderBodyState.emptyMessage,
|
|
2243
|
+
plannedRenderBodyState.errorMessage,
|
|
2244
|
+
plannedRenderBodyState.updating,
|
|
2245
|
+
shouldRejectPendingAutoView,
|
|
2246
|
+
]);
|
|
2247
|
+
useEffect(() => {
|
|
2248
|
+
setPendingAutoWindowOverride(null);
|
|
2249
|
+
setRenderedAutoView(null);
|
|
2250
|
+
setLastReadyRenderView(null);
|
|
2251
|
+
}, [instrumentRef?.exchange, instrumentRef?.symbol]);
|
|
2252
|
+
const hasPendingAutoProposal = effectiveResolution === "auto" && pendingAutoWindowOverride !== null;
|
|
2253
|
+
const shouldUseRenderedAutoView = effectiveResolution === "auto"
|
|
2254
|
+
&& !!renderedAutoView
|
|
2255
|
+
&& (
|
|
2256
|
+
plannedRenderBodyState.blocking
|
|
2257
|
+
|| !plannedRenderBodyState.data?.length
|
|
2258
|
+
|| !!plannedRenderBodyState.emptyMessage
|
|
2259
|
+
|| !!plannedRenderBodyState.errorMessage
|
|
2260
|
+
);
|
|
2261
|
+
const isRenderedAutoViewUpdating = hasPendingAutoProposal
|
|
2262
|
+
|| plannedRenderBodyState.blocking
|
|
2263
|
+
|| plannedRenderBodyState.updating;
|
|
2264
|
+
const autoDisplayState = useMemo(() => resolveAutoDisplayState({
|
|
2265
|
+
shouldUseRenderedAutoView,
|
|
2266
|
+
renderedAutoView,
|
|
2267
|
+
isRenderedAutoViewUpdating,
|
|
2268
|
+
plannedRenderBodyState,
|
|
2269
|
+
plannedResolvedManualResolution,
|
|
2270
|
+
plannedDateWindow,
|
|
2271
|
+
}), [
|
|
2272
|
+
isRenderedAutoViewUpdating,
|
|
2273
|
+
plannedDateWindow,
|
|
2274
|
+
plannedRenderBodyState,
|
|
2275
|
+
plannedResolvedManualResolution,
|
|
2276
|
+
renderedAutoView,
|
|
2277
|
+
shouldUseRenderedAutoView,
|
|
2278
|
+
]);
|
|
2279
|
+
const baseRenderBodyState = effectiveResolution === "auto"
|
|
2280
|
+
? autoDisplayState.bodyState
|
|
2281
|
+
: plannedRenderBodyState;
|
|
2282
|
+
const baseResolvedManualResolution = effectiveResolution === "auto"
|
|
2283
|
+
? autoDisplayState.resolution
|
|
2284
|
+
: plannedResolvedManualResolution;
|
|
2285
|
+
const baseDisplayedDateWindow = effectiveResolution === "auto"
|
|
2286
|
+
? autoDisplayState.window
|
|
2287
|
+
: manualVisibleDateWindow;
|
|
2288
|
+
useEffect(() => {
|
|
2289
|
+
if (compact || baseRenderBodyState.blocking || !baseRenderBodyState.data?.length) return;
|
|
785
2290
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
2291
|
+
const nextView: CachedRenderedView = {
|
|
2292
|
+
window: baseDisplayedDateWindow,
|
|
2293
|
+
resolution: baseResolvedManualResolution,
|
|
2294
|
+
data: baseRenderBodyState.data,
|
|
2295
|
+
};
|
|
2296
|
+
setLastReadyRenderView((current) => (
|
|
2297
|
+
current
|
|
2298
|
+
&& current.data === nextView.data
|
|
2299
|
+
&& current.resolution === nextView.resolution
|
|
2300
|
+
&& sameDateWindow(current.window, nextView.window)
|
|
2301
|
+
? current
|
|
2302
|
+
: nextView
|
|
2303
|
+
));
|
|
2304
|
+
}, [
|
|
2305
|
+
baseDisplayedDateWindow,
|
|
2306
|
+
baseRenderBodyState.blocking,
|
|
2307
|
+
baseRenderBodyState.data,
|
|
2308
|
+
baseResolvedManualResolution,
|
|
2309
|
+
compact,
|
|
2310
|
+
]);
|
|
2311
|
+
const shouldUseLastReadyRenderView = !compact && baseRenderBodyState.blocking && !!lastReadyRenderView?.data.length;
|
|
2312
|
+
const renderBodyState = shouldUseLastReadyRenderView
|
|
2313
|
+
? {
|
|
2314
|
+
data: lastReadyRenderView!.data,
|
|
2315
|
+
blocking: false,
|
|
2316
|
+
updating: true,
|
|
2317
|
+
emptyMessage: null,
|
|
2318
|
+
errorMessage: null,
|
|
2319
|
+
}
|
|
2320
|
+
: baseRenderBodyState;
|
|
2321
|
+
const resolvedManualResolution = shouldUseLastReadyRenderView
|
|
2322
|
+
? lastReadyRenderView!.resolution
|
|
2323
|
+
: baseResolvedManualResolution;
|
|
2324
|
+
const displayedDateWindow = shouldUseLastReadyRenderView
|
|
2325
|
+
? lastReadyRenderView!.window
|
|
2326
|
+
: baseDisplayedDateWindow;
|
|
2327
|
+
const renderedResolution: ChartResolution = effectiveResolution === "auto"
|
|
2328
|
+
? "auto"
|
|
2329
|
+
: (resolvedManualResolution ?? effectiveResolution);
|
|
2330
|
+
const selectedResolution: ChartResolution = requestedResolution === "auto"
|
|
2331
|
+
? "auto"
|
|
2332
|
+
: (resolutionChips.includes(requestedResolution) ? requestedResolution : effectiveResolution);
|
|
2333
|
+
const fallbackResolutionLabel = selectedResolution !== "auto" && renderedResolution !== selectedResolution
|
|
2334
|
+
? `showing ${getChartResolutionLabel(renderedResolution)}`
|
|
2335
|
+
: null;
|
|
2336
|
+
const history = compact
|
|
2337
|
+
? fallbackPriceHistory
|
|
2338
|
+
: (renderBodyState.data ?? []);
|
|
2339
|
+
const visibleDateWindow = displayedDateWindow;
|
|
2340
|
+
const navigableDateWindow = effectiveResolution === "auto"
|
|
2341
|
+
? (pendingAutoWindowOverride ?? displayedDateWindow ?? plannedDateWindow)
|
|
2342
|
+
: visibleDateWindow;
|
|
2343
|
+
const navigationOhlcPointCount = !compact && effectiveResolution !== "auto"
|
|
2344
|
+
? manualVisibleDateWindow.dates.length
|
|
2345
|
+
: 0;
|
|
2346
|
+
const resolveOhlcProjectionOptions = useCallback((
|
|
2347
|
+
pointCount: number,
|
|
2348
|
+
sourceIndexOffset: number,
|
|
2349
|
+
) => (
|
|
2350
|
+
resolveStableOhlcProjectionOptions({
|
|
2351
|
+
pointCount,
|
|
2352
|
+
sourceIndexOffset,
|
|
2353
|
+
bucketWidth: measurementChartWidth,
|
|
2354
|
+
navigationPointCount: navigationOhlcPointCount,
|
|
2355
|
+
})
|
|
2356
|
+
), [measurementChartWidth, navigationOhlcPointCount]);
|
|
2357
|
+
const axisWidth = useMemo(() => {
|
|
2358
|
+
const measureAxisWidth = (targetWidth: number) => {
|
|
2359
|
+
const measuredWindow = historyOverride || !displayedDateWindow?.start || !displayedDateWindow.end
|
|
2360
|
+
? { points: history, startIdx: 0, endIdx: history.length }
|
|
2361
|
+
: getVisibleWindowForDateRange(history, displayedDateWindow, 0);
|
|
2362
|
+
const measuredTimeAxisDates = measuredWindow.points.map((point) => point.date);
|
|
2363
|
+
const measuredProjection = projectChartData(
|
|
2364
|
+
measuredWindow.points,
|
|
2365
|
+
targetWidth,
|
|
2366
|
+
viewState.renderMode,
|
|
2367
|
+
!!compact,
|
|
2368
|
+
resolveOhlcProjectionOptions(measuredWindow.points.length, measuredWindow.startIdx),
|
|
2369
|
+
);
|
|
2370
|
+
const measuredResult = renderChart(measuredProjection.points, {
|
|
2371
|
+
width: targetWidth,
|
|
2372
|
+
height: chartHeight,
|
|
2373
|
+
showVolume: showVolume && !compact,
|
|
2374
|
+
volumeHeight,
|
|
2375
|
+
cursorX: null,
|
|
2376
|
+
cursorY: null,
|
|
2377
|
+
mode: measuredProjection.effectiveMode,
|
|
2378
|
+
axisMode,
|
|
2379
|
+
currency: chartCurrency,
|
|
2380
|
+
assetCategory: chartAssetCategory,
|
|
2381
|
+
colors: AXIS_MEASURE_PALETTE,
|
|
2382
|
+
timeAxisDates: measuredTimeAxisDates,
|
|
2383
|
+
});
|
|
2384
|
+
const measuredScene = buildChartScene(measuredProjection.points, {
|
|
2385
|
+
width: targetWidth,
|
|
2386
|
+
height: chartHeight,
|
|
2387
|
+
showVolume: showVolume && !compact,
|
|
2388
|
+
volumeHeight,
|
|
2389
|
+
cursorX: null,
|
|
2390
|
+
cursorY: null,
|
|
2391
|
+
mode: measuredProjection.effectiveMode,
|
|
2392
|
+
axisMode,
|
|
2393
|
+
colors: AXIS_MEASURE_PALETTE,
|
|
2394
|
+
timeAxisDates: measuredTimeAxisDates,
|
|
2395
|
+
});
|
|
2396
|
+
const cursorSamples = measuredScene
|
|
2397
|
+
? [
|
|
2398
|
+
formatCursorAxisValue(
|
|
2399
|
+
measuredScene.min,
|
|
2400
|
+
axisMode,
|
|
2401
|
+
measuredProjection.points[0]?.close ?? 0,
|
|
2402
|
+
chartCurrency,
|
|
2403
|
+
chartAssetCategory,
|
|
2404
|
+
measuredResult.priceRange ?? undefined,
|
|
2405
|
+
),
|
|
2406
|
+
formatCursorAxisValue(
|
|
2407
|
+
measuredScene.max,
|
|
2408
|
+
axisMode,
|
|
2409
|
+
measuredProjection.points[0]?.close ?? 0,
|
|
2410
|
+
chartCurrency,
|
|
2411
|
+
chartAssetCategory,
|
|
2412
|
+
measuredResult.priceRange ?? undefined,
|
|
2413
|
+
),
|
|
2414
|
+
]
|
|
2415
|
+
: [];
|
|
818
2416
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
endDate,
|
|
825
|
-
barSize,
|
|
2417
|
+
return resolveChartAxisWidth(
|
|
2418
|
+
[...measuredResult.axisLabels.map((entry) => entry.label), ...cursorSamples],
|
|
2419
|
+
minimumAxisWidth,
|
|
2420
|
+
Math.max(axisSectionWidthBudget - axisRightPadding, minimumAxisWidth),
|
|
2421
|
+
);
|
|
826
2422
|
};
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
2423
|
+
|
|
2424
|
+
const firstPassWidth = measureAxisWidth(measurementChartWidth);
|
|
2425
|
+
const refinedChartWidth = Math.max(width - firstPassWidth - axisRightPadding - axisGap, minChartWidth);
|
|
2426
|
+
return refinedChartWidth === measurementChartWidth ? firstPassWidth : measureAxisWidth(refinedChartWidth);
|
|
2427
|
+
}, [
|
|
2428
|
+
axisGap,
|
|
2429
|
+
axisMode,
|
|
2430
|
+
axisRightPadding,
|
|
2431
|
+
axisSectionWidthBudget,
|
|
2432
|
+
chartAssetCategory,
|
|
2433
|
+
chartCurrency,
|
|
2434
|
+
chartHeight,
|
|
2435
|
+
compact,
|
|
2436
|
+
displayedDateWindow,
|
|
2437
|
+
history,
|
|
2438
|
+
historyOverride,
|
|
2439
|
+
minChartWidth,
|
|
2440
|
+
minimumAxisWidth,
|
|
2441
|
+
resolveOhlcProjectionOptions,
|
|
2442
|
+
showVolume,
|
|
2443
|
+
viewState.renderMode,
|
|
2444
|
+
volumeHeight,
|
|
2445
|
+
width,
|
|
2446
|
+
]);
|
|
2447
|
+
const axisSectionWidth = axisWidth + axisRightPadding;
|
|
2448
|
+
const chartWidth = Math.max(width - axisSectionWidth - axisGap, minChartWidth);
|
|
2449
|
+
const maxCursorX = chartWidth - 1;
|
|
2450
|
+
const panStep = getKeyboardPanCellCount(chartWidth);
|
|
2451
|
+
const activePreset = compact
|
|
839
2452
|
? null
|
|
840
|
-
|
|
2453
|
+
: effectiveResolution === "auto"
|
|
2454
|
+
? (canonicalAutoWindow && displayedDateWindow && sameDateWindow(displayedDateWindow, canonicalAutoWindow)
|
|
2455
|
+
? viewState.presetRange
|
|
2456
|
+
: null)
|
|
2457
|
+
: resolveVisibleActivePreset(boundsHistoryDates, {
|
|
2458
|
+
presetRange: viewState.presetRange,
|
|
2459
|
+
activePreset: viewState.activePreset,
|
|
2460
|
+
panOffset: viewState.panOffset,
|
|
2461
|
+
zoomLevel: viewState.zoomLevel,
|
|
2462
|
+
resolution: effectiveResolution,
|
|
2463
|
+
});
|
|
841
2464
|
|
|
842
2465
|
useEffect(() => {
|
|
843
|
-
if (compact ||
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
2466
|
+
if (compact || effectiveResolution === "auto" || boundsHistoryDates.length === 0) return;
|
|
2467
|
+
|
|
2468
|
+
const hasPendingCanonicalReset = appliedCanonicalResetRef.current < pendingCanonicalResetRef.current;
|
|
2469
|
+
const shouldReconcileActivePreset = !hasPendingCanonicalReset
|
|
2470
|
+
&& needsCanonicalPresetViewportReset(boundsHistoryDates, viewState);
|
|
2471
|
+
if (hasPendingCanonicalReset || shouldReconcileActivePreset) {
|
|
2472
|
+
if (hasPendingCanonicalReset) {
|
|
2473
|
+
appliedCanonicalResetRef.current = pendingCanonicalResetRef.current;
|
|
2474
|
+
}
|
|
848
2475
|
setViewState((current) => (
|
|
849
|
-
|
|
850
|
-
? current
|
|
851
|
-
:
|
|
852
|
-
...current,
|
|
853
|
-
zoomLevel: canonicalZoom,
|
|
854
|
-
panOffset: 0,
|
|
855
|
-
cursorX: null,
|
|
856
|
-
cursorY: null,
|
|
857
|
-
}
|
|
2476
|
+
hasPendingCanonicalReset
|
|
2477
|
+
? resolvePresetRangeViewport(current, boundsHistoryDates)
|
|
2478
|
+
: resolveCanonicalPresetViewport(current, boundsHistoryDates)
|
|
858
2479
|
));
|
|
859
2480
|
return;
|
|
860
2481
|
}
|
|
@@ -864,24 +2485,62 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
864
2485
|
pendingExpansionRef.current = null;
|
|
865
2486
|
setViewState((current) => {
|
|
866
2487
|
if (pendingExpansion.kind === "zoom-out") {
|
|
867
|
-
const nextVisibleCount = Math.min(
|
|
2488
|
+
const nextVisibleCount = Math.min(boundsHistoryDates.length, Math.max(pendingExpansion.targetVisibleCount, 1));
|
|
868
2489
|
return {
|
|
869
2490
|
...current,
|
|
870
2491
|
...resolveAnchoredChartZoom(
|
|
871
|
-
|
|
2492
|
+
boundsHistoryDates.length,
|
|
872
2493
|
1,
|
|
873
2494
|
0,
|
|
874
|
-
|
|
2495
|
+
boundsHistoryDates.length / nextVisibleCount,
|
|
875
2496
|
pendingExpansion.anchorRatio,
|
|
876
2497
|
),
|
|
877
2498
|
};
|
|
878
2499
|
}
|
|
879
2500
|
return {
|
|
880
2501
|
...current,
|
|
881
|
-
panOffset: clamp(pendingExpansion.targetPanOffset, 0, getMaxPanOffset(
|
|
2502
|
+
panOffset: clamp(pendingExpansion.targetPanOffset, 0, getMaxPanOffset(boundsHistory, current.zoomLevel)),
|
|
882
2503
|
};
|
|
883
2504
|
});
|
|
884
|
-
}, [
|
|
2505
|
+
}, [
|
|
2506
|
+
boundsHistory,
|
|
2507
|
+
boundsHistoryDates,
|
|
2508
|
+
compact,
|
|
2509
|
+
effectiveResolution,
|
|
2510
|
+
viewState.activePreset,
|
|
2511
|
+
viewState.panOffset,
|
|
2512
|
+
viewState.presetRange,
|
|
2513
|
+
viewState.zoomLevel,
|
|
2514
|
+
]);
|
|
2515
|
+
|
|
2516
|
+
useEffect(() => {
|
|
2517
|
+
if (compact || effectiveResolution !== "auto" || !baseDateBounds) return;
|
|
2518
|
+
|
|
2519
|
+
const pendingAutoWindow = pendingAutoWindowRef.current;
|
|
2520
|
+
if (pendingAutoWindow) {
|
|
2521
|
+
const clampedPendingWindow = clampDateWindowToBounds(pendingAutoWindow, baseDateBounds, autoMinimumSpanMs);
|
|
2522
|
+
if (clampedPendingWindow && sameDateWindow(clampedPendingWindow, pendingAutoWindow)) {
|
|
2523
|
+
pendingAutoWindowRef.current = null;
|
|
2524
|
+
const normalizedPendingWindow = canonicalAutoWindow && sameDateWindow(clampedPendingWindow, canonicalAutoWindow)
|
|
2525
|
+
? null
|
|
2526
|
+
: clampedPendingWindow;
|
|
2527
|
+
setPendingAutoWindowOverride((current) => (
|
|
2528
|
+
sameDateWindow(current, normalizedPendingWindow) ? current : normalizedPendingWindow
|
|
2529
|
+
));
|
|
2530
|
+
}
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
if (!pendingAutoWindowOverride) return;
|
|
2535
|
+
const clampedOverride = clampDateWindowToBounds(pendingAutoWindowOverride, baseDateBounds, autoMinimumSpanMs);
|
|
2536
|
+
if (!clampedOverride) return;
|
|
2537
|
+
const normalizedOverride = canonicalAutoWindow && sameDateWindow(clampedOverride, canonicalAutoWindow)
|
|
2538
|
+
? null
|
|
2539
|
+
: clampedOverride;
|
|
2540
|
+
if (!sameDateWindow(pendingAutoWindowOverride, normalizedOverride)) {
|
|
2541
|
+
setPendingAutoWindowOverride(normalizedOverride);
|
|
2542
|
+
}
|
|
2543
|
+
}, [autoMinimumSpanMs, baseDateBounds, canonicalAutoWindow, compact, effectiveResolution, pendingAutoWindowOverride]);
|
|
885
2544
|
|
|
886
2545
|
useEffect(() => {
|
|
887
2546
|
if (interactive) {
|
|
@@ -940,31 +2599,91 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
940
2599
|
: clampTimeRangeToMaxRange(nextCandidate, supportMap.get(effectiveResolution) ?? viewState.bufferRange);
|
|
941
2600
|
if (nextBufferRange === viewState.bufferRange) return false;
|
|
942
2601
|
pendingExpansionRef.current = action;
|
|
943
|
-
setViewState((current) => (
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
2602
|
+
setViewState((current) => applyBufferedPanExpansion(current, nextBufferRange));
|
|
2603
|
+
return true;
|
|
2604
|
+
};
|
|
2605
|
+
|
|
2606
|
+
const requestAutoWindow = (nextWindow: DateWindowRange | null | undefined): boolean => {
|
|
2607
|
+
if (compact || effectiveResolution !== "auto" || !nextWindow?.start || !nextWindow.end || !baseDateBounds?.start || !baseDateBounds.end) {
|
|
2608
|
+
return false;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
if (nextWindow.start.getTime() < baseDateBounds.start.getTime()) {
|
|
2612
|
+
const nextBufferRange = getNextBufferRange(viewState.bufferRange);
|
|
2613
|
+
if (nextBufferRange !== viewState.bufferRange) {
|
|
2614
|
+
pendingAutoWindowRef.current = nextWindow;
|
|
2615
|
+
setViewState((current) => {
|
|
2616
|
+
const nextState = clearAutoViewportState(current);
|
|
2617
|
+
return nextState.bufferRange === nextBufferRange
|
|
2618
|
+
? nextState
|
|
2619
|
+
: { ...nextState, bufferRange: nextBufferRange };
|
|
2620
|
+
});
|
|
2621
|
+
return true;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
const clampedWindow = clampDateWindowToBounds(nextWindow, baseDateBounds, autoMinimumSpanMs);
|
|
2626
|
+
if (!clampedWindow) {
|
|
2627
|
+
return false;
|
|
2628
|
+
}
|
|
2629
|
+
const normalizedWindow = canonicalAutoWindow && sameDateWindow(clampedWindow, canonicalAutoWindow)
|
|
2630
|
+
? null
|
|
2631
|
+
: clampedWindow;
|
|
2632
|
+
if (sameDateWindow(navigableDateWindow, normalizedWindow)) {
|
|
2633
|
+
return false;
|
|
2634
|
+
}
|
|
2635
|
+
pendingAutoWindowRef.current = null;
|
|
2636
|
+
setPendingAutoWindowOverride((current) => (sameDateWindow(current, normalizedWindow) ? current : normalizedWindow));
|
|
2637
|
+
setViewState((current) => clearAutoViewportState(current));
|
|
949
2638
|
return true;
|
|
950
2639
|
};
|
|
951
2640
|
|
|
952
2641
|
const setRange = (range: TimeRange) => {
|
|
953
2642
|
if (!compact && !isRangePresetSupported(range, effectiveResolutionSupport)) return;
|
|
954
2643
|
const supportMaxRange = getSupportMaxRange(effectiveResolutionSupport, getPresetResolution(range));
|
|
2644
|
+
const nextResolution = getPresetResolution(range);
|
|
2645
|
+
setRequestedResolution(nextResolution);
|
|
955
2646
|
if (!compact) {
|
|
956
|
-
persistChartControls(range,
|
|
2647
|
+
persistChartControls(range, nextResolution);
|
|
957
2648
|
}
|
|
958
2649
|
pendingCanonicalResetRef.current += 1;
|
|
2650
|
+
pendingAutoWindowRef.current = null;
|
|
2651
|
+
setPendingAutoWindowOverride(null);
|
|
959
2652
|
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
960
|
-
setViewState((current) =>
|
|
2653
|
+
setViewState((current) => resolveViewportPresetSelection(current, range, supportMaxRange));
|
|
961
2654
|
};
|
|
962
2655
|
|
|
963
2656
|
const setResolution = (resolution: ChartResolution) => {
|
|
964
2657
|
if (compact) return;
|
|
965
2658
|
if (resolution !== "auto" && !availableManualResolutions.includes(resolution)) return;
|
|
966
|
-
|
|
2659
|
+
|
|
2660
|
+
if (resolution === "auto") {
|
|
2661
|
+
const preservedWindow = effectiveResolution === "auto" ? navigableDateWindow : manualVisibleDateWindow;
|
|
2662
|
+
const nextAutoWindow = clampDateWindowToBounds(preservedWindow, baseDateBounds, autoMinimumSpanMs);
|
|
2663
|
+
pendingAutoWindowRef.current = null;
|
|
2664
|
+
setPendingAutoWindowOverride(
|
|
2665
|
+
canonicalAutoWindow && nextAutoWindow && sameDateWindow(nextAutoWindow, canonicalAutoWindow)
|
|
2666
|
+
? null
|
|
2667
|
+
: nextAutoWindow,
|
|
2668
|
+
);
|
|
2669
|
+
if (effectiveResolution !== "auto") {
|
|
2670
|
+
setRenderedAutoView(null);
|
|
2671
|
+
}
|
|
2672
|
+
setRequestedResolution(resolution);
|
|
2673
|
+
persistChartControls(viewState.presetRange, resolution);
|
|
2674
|
+
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
2675
|
+
setViewState((current) => ({
|
|
2676
|
+
...clearAutoViewportState(current),
|
|
2677
|
+
bufferRange: getNextBufferRange(current.presetRange),
|
|
2678
|
+
}));
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
const nextState = resolveViewportResolutionSelection(viewState, resolution, selectionSupportMap, visibleDateWindow);
|
|
967
2683
|
if (!nextState) return;
|
|
2684
|
+
setRequestedResolution(resolution);
|
|
2685
|
+
pendingAutoWindowRef.current = null;
|
|
2686
|
+
setPendingAutoWindowOverride(null);
|
|
968
2687
|
pendingCanonicalResetRef.current += 1;
|
|
969
2688
|
persistChartControls(nextState.presetRange, resolution);
|
|
970
2689
|
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
@@ -976,58 +2695,146 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
976
2695
|
setViewState((current) => ({ ...current, renderMode: mode }));
|
|
977
2696
|
};
|
|
978
2697
|
|
|
979
|
-
const
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
2698
|
+
const focusPaneForMouseInteraction = (event: { stopPropagation?: () => void; preventDefault?: () => void } | null | undefined) => {
|
|
2699
|
+
event?.stopPropagation?.();
|
|
2700
|
+
event?.preventDefault?.();
|
|
2701
|
+
if (!focused) {
|
|
2702
|
+
dispatch({ type: "FOCUS_PANE", paneId });
|
|
2703
|
+
}
|
|
2704
|
+
};
|
|
2705
|
+
|
|
2706
|
+
const chartWindow = useMemo(() => {
|
|
2707
|
+
if (historyOverride || !displayedDateWindow?.start || !displayedDateWindow.end) {
|
|
2708
|
+
return { points: history, startIdx: 0, endIdx: history.length };
|
|
2709
|
+
}
|
|
2710
|
+
return getVisibleWindowForDateRange(history, displayedDateWindow, 0);
|
|
2711
|
+
}, [displayedDateWindow, history, historyOverride]);
|
|
2712
|
+
const historyRenderKey = chartWindow.points.length === 0
|
|
986
2713
|
? "empty"
|
|
987
2714
|
: [
|
|
988
|
-
|
|
989
|
-
new Date(
|
|
990
|
-
new Date(
|
|
991
|
-
|
|
2715
|
+
chartWindow.points.length,
|
|
2716
|
+
new Date(chartWindow.points[0]!.date).getTime(),
|
|
2717
|
+
new Date(chartWindow.points[chartWindow.points.length - 1]!.date).getTime(),
|
|
2718
|
+
chartWindow.points[chartWindow.points.length - 1]!.close,
|
|
992
2719
|
].join(":");
|
|
993
2720
|
|
|
994
2721
|
useEffect(() => {
|
|
995
2722
|
queueMicrotask(() => renderer.requestRender());
|
|
996
2723
|
}, [chartHeight, chartWidth, compact, historyRenderKey, renderer, ticker?.metadata.ticker, viewState.renderMode]);
|
|
997
2724
|
|
|
998
|
-
const chartWindow = useMemo(() => {
|
|
999
|
-
if (historyOverride) {
|
|
1000
|
-
return { points: history, startIdx: 0, endIdx: history.length };
|
|
1001
|
-
}
|
|
1002
|
-
return isDetailView
|
|
1003
|
-
? { points: history, startIdx: 0, endIdx: history.length }
|
|
1004
|
-
: getVisibleWindow(history, viewState, chartWidth);
|
|
1005
|
-
}, [chartWidth, history, historyOverride, isDetailView, viewState.panOffset, viewState.zoomLevel]);
|
|
1006
2725
|
const timeAxisDates = useMemo(
|
|
1007
2726
|
() => chartWindow.points.map((point) => point.date),
|
|
1008
2727
|
[chartWindow.points],
|
|
1009
2728
|
);
|
|
1010
2729
|
|
|
1011
2730
|
const projection = useMemo(() => (
|
|
1012
|
-
projectChartData(
|
|
1013
|
-
|
|
2731
|
+
projectChartData(
|
|
2732
|
+
chartWindow.points,
|
|
2733
|
+
chartWidth,
|
|
2734
|
+
viewState.renderMode,
|
|
2735
|
+
!!compact,
|
|
2736
|
+
resolveOhlcProjectionOptions(chartWindow.points.length, chartWindow.startIdx),
|
|
2737
|
+
)
|
|
2738
|
+
), [
|
|
2739
|
+
chartWindow.points,
|
|
2740
|
+
chartWindow.startIdx,
|
|
2741
|
+
chartWidth,
|
|
2742
|
+
compact,
|
|
2743
|
+
resolveOhlcProjectionOptions,
|
|
2744
|
+
viewState.renderMode,
|
|
2745
|
+
]);
|
|
2746
|
+
|
|
2747
|
+
const sourceIndicatorOverlays = useMemo(() => {
|
|
2748
|
+
if (!hasIndicators || !history.length) return null;
|
|
2749
|
+
return computeIndicatorOverlays(history.map((point) => point.close), indicatorConfig);
|
|
2750
|
+
}, [history, hasIndicators, indicatorConfig]);
|
|
2751
|
+
|
|
2752
|
+
const indicatorSourceKey = useMemo(() => (
|
|
2753
|
+
sourceIndicatorOverlays ? buildIndicatorSourceKey(history, indicatorConfig) : "none"
|
|
2754
|
+
), [history, indicatorConfig, sourceIndicatorOverlays]);
|
|
2755
|
+
const indicatorProjectionKey = useMemo(() => {
|
|
2756
|
+
if (!sourceIndicatorOverlays || !chartWindow.points.length || !projection.points.length) return "none";
|
|
2757
|
+
return buildIndicatorProjectionKey({
|
|
2758
|
+
sourceKey: indicatorSourceKey,
|
|
2759
|
+
sourcePoints: chartWindow.points,
|
|
2760
|
+
sourceIndexOffset: chartWindow.startIdx,
|
|
2761
|
+
projectedPoints: projection.points,
|
|
2762
|
+
mode: projection.effectiveMode,
|
|
2763
|
+
});
|
|
2764
|
+
}, [
|
|
2765
|
+
chartWindow.points,
|
|
2766
|
+
chartWindow.startIdx,
|
|
2767
|
+
indicatorSourceKey,
|
|
2768
|
+
projection.effectiveMode,
|
|
2769
|
+
projection.points,
|
|
2770
|
+
sourceIndicatorOverlays,
|
|
2771
|
+
]);
|
|
2772
|
+
const [deferredIndicators, setDeferredIndicators] = useState<DeferredIndicatorState | null>(null);
|
|
2773
|
+
|
|
2774
|
+
useEffect(() => {
|
|
2775
|
+
if (!sourceIndicatorOverlays || indicatorProjectionKey === "none" || !chartWindow.points.length || !projection.points.length) {
|
|
2776
|
+
setDeferredIndicators((current) => (current === null ? current : null));
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
const timeout = setTimeout(() => {
|
|
2781
|
+
setDeferredIndicators({
|
|
2782
|
+
key: indicatorProjectionKey,
|
|
2783
|
+
overlays: reindexIndicatorOverlaysForProjection(
|
|
2784
|
+
sourceIndicatorOverlays,
|
|
2785
|
+
chartWindow.points,
|
|
2786
|
+
projection.points,
|
|
2787
|
+
chartWindow.startIdx,
|
|
2788
|
+
),
|
|
2789
|
+
});
|
|
2790
|
+
}, INDICATOR_RENDER_DEBOUNCE_MS);
|
|
2791
|
+
|
|
2792
|
+
return () => clearTimeout(timeout);
|
|
2793
|
+
}, [
|
|
2794
|
+
chartWindow.points,
|
|
2795
|
+
chartWindow.startIdx,
|
|
2796
|
+
indicatorProjectionKey,
|
|
2797
|
+
projection.points,
|
|
2798
|
+
sourceIndicatorOverlays,
|
|
2799
|
+
]);
|
|
2800
|
+
|
|
2801
|
+
const indicators = deferredIndicators?.key === indicatorProjectionKey ? deferredIndicators.overlays : null;
|
|
2802
|
+
const indicatorRenderKey = useMemo(() => {
|
|
2803
|
+
return buildIndicatorRenderKey(indicators);
|
|
2804
|
+
}, [indicators]);
|
|
1014
2805
|
|
|
1015
2806
|
useKeyboard((event) => {
|
|
1016
2807
|
if (!focused || compact) return;
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
2808
|
+
const key = resolveChartKeyboardKey(event);
|
|
2809
|
+
|
|
2810
|
+
switch (key) {
|
|
2811
|
+
case "zoom-in":
|
|
2812
|
+
if (effectiveResolution === "auto") {
|
|
2813
|
+
requestAutoWindow(resolveAutoZoomWindow({
|
|
2814
|
+
historyPoints: history,
|
|
2815
|
+
boundsDates: boundsHistoryDates,
|
|
2816
|
+
currentWindow: navigableDateWindow,
|
|
2817
|
+
direction: "in",
|
|
2818
|
+
anchorRatio: RIGHT_EDGE_ANCHOR_RATIO,
|
|
2819
|
+
}));
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
setViewState((current) => applyZoomAroundAnchor(current, current.zoomLevel * 1.5, RIGHT_EDGE_ANCHOR_RATIO, boundsHistory));
|
|
1026
2823
|
return;
|
|
1027
|
-
case "-":
|
|
2824
|
+
case "zoom-out":
|
|
2825
|
+
if (effectiveResolution === "auto") {
|
|
2826
|
+
requestAutoWindow(resolveAutoZoomWindow({
|
|
2827
|
+
historyPoints: history,
|
|
2828
|
+
boundsDates: boundsHistoryDates,
|
|
2829
|
+
currentWindow: navigableDateWindow,
|
|
2830
|
+
direction: "out",
|
|
2831
|
+
anchorRatio: RIGHT_EDGE_ANCHOR_RATIO,
|
|
2832
|
+
}));
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
1028
2835
|
if (viewState.zoomLevel <= 1.001 && expandBufferRange({
|
|
1029
2836
|
kind: "zoom-out",
|
|
1030
|
-
targetVisibleCount: Math.round(
|
|
2837
|
+
targetVisibleCount: Math.round(boundsHistory.length * 1.5),
|
|
1031
2838
|
anchorRatio: RIGHT_EDGE_ANCHOR_RATIO,
|
|
1032
2839
|
})) {
|
|
1033
2840
|
return;
|
|
@@ -1036,14 +2843,27 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1036
2843
|
current,
|
|
1037
2844
|
current.zoomLevel / 1.5,
|
|
1038
2845
|
RIGHT_EDGE_ANCHOR_RATIO,
|
|
1039
|
-
|
|
2846
|
+
boundsHistory,
|
|
1040
2847
|
));
|
|
1041
2848
|
return;
|
|
1042
2849
|
case "0":
|
|
2850
|
+
if (effectiveResolution === "auto") {
|
|
2851
|
+
pendingAutoWindowRef.current = null;
|
|
2852
|
+
setPendingAutoWindowOverride(null);
|
|
2853
|
+
setRenderedAutoView(null);
|
|
2854
|
+
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
2855
|
+
setViewState((current) => clearAutoViewportState(current));
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
1043
2858
|
pendingCanonicalResetRef.current += 1;
|
|
1044
2859
|
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
1045
2860
|
setViewState((current) => {
|
|
1046
|
-
const nextState =
|
|
2861
|
+
const nextState = resolveViewportResolutionSelection(
|
|
2862
|
+
current,
|
|
2863
|
+
effectiveResolution,
|
|
2864
|
+
selectionSupportMap,
|
|
2865
|
+
visibleDateWindow,
|
|
2866
|
+
) ?? current;
|
|
1047
2867
|
return {
|
|
1048
2868
|
...nextState,
|
|
1049
2869
|
panOffset: 0,
|
|
@@ -1053,20 +2873,32 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1053
2873
|
};
|
|
1054
2874
|
});
|
|
1055
2875
|
return;
|
|
1056
|
-
case "v":
|
|
1057
|
-
setShowVolume((value) => !value);
|
|
1058
|
-
return;
|
|
1059
2876
|
case "a":
|
|
1060
|
-
if (
|
|
2877
|
+
if (effectiveResolution === "auto") {
|
|
2878
|
+
requestAutoWindow(shiftDateWindow(navigableDateWindow, panStep / Math.max(chartWidth, 1)));
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
if (viewState.panOffset >= getMaxPanOffset(boundsHistory, viewState.zoomLevel) && expandBufferRange({
|
|
1061
2882
|
kind: "pan-left",
|
|
1062
2883
|
targetPanOffset: viewState.panOffset + panStep,
|
|
1063
2884
|
})) {
|
|
1064
2885
|
return;
|
|
1065
2886
|
}
|
|
1066
|
-
setViewState((current) =>
|
|
2887
|
+
setViewState((current) => {
|
|
2888
|
+
const cleared = clearActivePreset(current);
|
|
2889
|
+
return { ...cleared, panOffset: current.panOffset + panStep };
|
|
2890
|
+
});
|
|
1067
2891
|
return;
|
|
1068
2892
|
case "d":
|
|
1069
|
-
|
|
2893
|
+
if (effectiveResolution === "auto") {
|
|
2894
|
+
requestAutoWindow(shiftDateWindow(navigableDateWindow, -panStep / Math.max(chartWidth, 1)));
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
setViewState((current) => {
|
|
2898
|
+
const cleared = clearActivePreset(current);
|
|
2899
|
+
const nextPanOffset = Math.max(current.panOffset - panStep, 0);
|
|
2900
|
+
return cleared.panOffset === nextPanOffset ? cleared : { ...cleared, panOffset: nextPanOffset };
|
|
2901
|
+
});
|
|
1070
2902
|
return;
|
|
1071
2903
|
case "m":
|
|
1072
2904
|
setViewState((current) => {
|
|
@@ -1078,51 +2910,66 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1078
2910
|
});
|
|
1079
2911
|
return;
|
|
1080
2912
|
case "r": {
|
|
1081
|
-
const currentIndex = resolutionChips.indexOf(
|
|
2913
|
+
const currentIndex = resolutionChips.indexOf(selectedResolution);
|
|
1082
2914
|
const nextResolution = resolutionChips[(currentIndex + 1) % resolutionChips.length] ?? "auto";
|
|
1083
2915
|
setResolution(nextResolution);
|
|
1084
2916
|
return;
|
|
1085
2917
|
}
|
|
1086
2918
|
}
|
|
1087
2919
|
|
|
1088
|
-
if (
|
|
1089
|
-
const index = parseInt(
|
|
2920
|
+
if (key >= "1" && key <= "7") {
|
|
2921
|
+
const index = parseInt(key) - 1;
|
|
1090
2922
|
if (index < TIME_RANGES.length) setRange(TIME_RANGES[index]!);
|
|
1091
2923
|
return;
|
|
1092
2924
|
}
|
|
1093
2925
|
|
|
1094
2926
|
if (!interactive) return;
|
|
1095
2927
|
|
|
1096
|
-
switch (
|
|
2928
|
+
switch (key) {
|
|
1097
2929
|
case "left":
|
|
1098
2930
|
if (event.shift) {
|
|
1099
|
-
if (
|
|
2931
|
+
if (effectiveResolution === "auto") {
|
|
2932
|
+
requestAutoWindow(shiftDateWindow(navigableDateWindow, panStep / Math.max(chartWidth, 1)));
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
if (viewState.panOffset >= getMaxPanOffset(boundsHistory, viewState.zoomLevel) && expandBufferRange({
|
|
1100
2936
|
kind: "pan-left",
|
|
1101
2937
|
targetPanOffset: viewState.panOffset + panStep,
|
|
1102
2938
|
})) {
|
|
1103
2939
|
return;
|
|
1104
2940
|
}
|
|
1105
|
-
setViewState((current) =>
|
|
2941
|
+
setViewState((current) => {
|
|
2942
|
+
const cleared = clearActivePreset(current);
|
|
2943
|
+
return { ...cleared, panOffset: current.panOffset + panStep };
|
|
2944
|
+
});
|
|
1106
2945
|
} else {
|
|
1107
2946
|
cursorMotionKindRef.current = "discrete";
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
: getActivePointIndex(
|
|
1113
|
-
pointCount,
|
|
1114
|
-
chartWidth,
|
|
1115
|
-
current.cursorX ?? maxCursorX,
|
|
1116
|
-
projection.effectiveMode,
|
|
1117
|
-
);
|
|
1118
|
-
const nextCursor = resolveAdjacentSelectionCursorX(
|
|
1119
|
-
current.cursorX,
|
|
1120
|
-
-1,
|
|
2947
|
+
const pointCount = projection.points.length;
|
|
2948
|
+
const currentIndex = pointCount <= 0
|
|
2949
|
+
? -1
|
|
2950
|
+
: getActivePointIndex(
|
|
1121
2951
|
pointCount,
|
|
1122
2952
|
chartWidth,
|
|
2953
|
+
viewState.cursorX ?? maxCursorX,
|
|
1123
2954
|
projection.effectiveMode,
|
|
1124
2955
|
);
|
|
1125
|
-
|
|
2956
|
+
const nextCursor = resolveAdjacentSelectionCursorX(
|
|
2957
|
+
viewState.cursorX,
|
|
2958
|
+
-1,
|
|
2959
|
+
pointCount,
|
|
2960
|
+
chartWidth,
|
|
2961
|
+
projection.effectiveMode,
|
|
2962
|
+
);
|
|
2963
|
+
if (effectiveResolution === "auto" && currentIndex <= 0) {
|
|
2964
|
+
requestAutoWindow(shiftDateWindow(
|
|
2965
|
+
navigableDateWindow,
|
|
2966
|
+
1 / Math.max(pointCount - 1, 1),
|
|
2967
|
+
));
|
|
2968
|
+
commitSelectionCursor({ cursorX: nextCursor, cursorY: null });
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
setViewState((current) => {
|
|
2972
|
+
const maxPan = getMaxPanOffset(boundsHistory, current.zoomLevel);
|
|
1126
2973
|
if (currentIndex <= 0) {
|
|
1127
2974
|
return {
|
|
1128
2975
|
...clearActivePreset(current),
|
|
@@ -1137,26 +2984,42 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1137
2984
|
return;
|
|
1138
2985
|
case "right":
|
|
1139
2986
|
if (event.shift) {
|
|
1140
|
-
|
|
2987
|
+
if (effectiveResolution === "auto") {
|
|
2988
|
+
requestAutoWindow(shiftDateWindow(navigableDateWindow, -panStep / Math.max(chartWidth, 1)));
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
setViewState((current) => {
|
|
2992
|
+
const cleared = clearActivePreset(current);
|
|
2993
|
+
const nextPanOffset = Math.max(current.panOffset - panStep, 0);
|
|
2994
|
+
return cleared.panOffset === nextPanOffset ? cleared : { ...cleared, panOffset: nextPanOffset };
|
|
2995
|
+
});
|
|
1141
2996
|
} else {
|
|
1142
2997
|
cursorMotionKindRef.current = "discrete";
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
: getActivePointIndex(
|
|
1148
|
-
pointCount,
|
|
1149
|
-
chartWidth,
|
|
1150
|
-
current.cursorX ?? 0,
|
|
1151
|
-
projection.effectiveMode,
|
|
1152
|
-
);
|
|
1153
|
-
const nextCursor = resolveAdjacentSelectionCursorX(
|
|
1154
|
-
current.cursorX,
|
|
1155
|
-
1,
|
|
2998
|
+
const pointCount = projection.points.length;
|
|
2999
|
+
const currentIndex = pointCount <= 0
|
|
3000
|
+
? -1
|
|
3001
|
+
: getActivePointIndex(
|
|
1156
3002
|
pointCount,
|
|
1157
3003
|
chartWidth,
|
|
3004
|
+
viewState.cursorX ?? 0,
|
|
1158
3005
|
projection.effectiveMode,
|
|
1159
3006
|
);
|
|
3007
|
+
const nextCursor = resolveAdjacentSelectionCursorX(
|
|
3008
|
+
viewState.cursorX,
|
|
3009
|
+
1,
|
|
3010
|
+
pointCount,
|
|
3011
|
+
chartWidth,
|
|
3012
|
+
projection.effectiveMode,
|
|
3013
|
+
);
|
|
3014
|
+
if (effectiveResolution === "auto" && currentIndex >= pointCount - 1) {
|
|
3015
|
+
requestAutoWindow(shiftDateWindow(
|
|
3016
|
+
navigableDateWindow,
|
|
3017
|
+
-1 / Math.max(pointCount - 1, 1),
|
|
3018
|
+
));
|
|
3019
|
+
commitSelectionCursor({ cursorX: nextCursor, cursorY: null });
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
setViewState((current) => {
|
|
1160
3023
|
if (currentIndex >= pointCount - 1) {
|
|
1161
3024
|
return {
|
|
1162
3025
|
...clearActivePreset(current),
|
|
@@ -1187,7 +3050,6 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1187
3050
|
negative: colors.negative,
|
|
1188
3051
|
}, trend);
|
|
1189
3052
|
}, [chartWindow.points]);
|
|
1190
|
-
const chartCurrency = currencyOverride ?? financials?.quote?.currency ?? ticker?.metadata.currency ?? "USD";
|
|
1191
3053
|
|
|
1192
3054
|
const cursorX = viewState.cursorX !== null ? clamp(viewState.cursorX, 0, chartWidth - 1) : null;
|
|
1193
3055
|
const cursorY = viewState.cursorY !== null ? clamp(viewState.cursorY, 0, chartHeight - 1) : null;
|
|
@@ -1260,7 +3122,8 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1260
3122
|
axisMode,
|
|
1261
3123
|
colors: chartColors,
|
|
1262
3124
|
timeAxisDates,
|
|
1263
|
-
|
|
3125
|
+
indicators,
|
|
3126
|
+
}), [axisMode, chartColors, chartHeight, chartWidth, compact, indicators, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
|
|
1264
3127
|
|
|
1265
3128
|
const rendererState = resolveChartRendererState(preferredRenderer, kittySupport, renderer.resolution);
|
|
1266
3129
|
const effectiveRenderer: ResolvedChartRenderer = rendererState.renderer;
|
|
@@ -1275,9 +3138,11 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1275
3138
|
mode: projection.effectiveMode,
|
|
1276
3139
|
axisMode,
|
|
1277
3140
|
currency: chartCurrency,
|
|
3141
|
+
assetCategory: chartAssetCategory,
|
|
1278
3142
|
colors: chartColors,
|
|
1279
3143
|
timeAxisDates,
|
|
1280
|
-
|
|
3144
|
+
indicators,
|
|
3145
|
+
}), [axisMode, chartAssetCategory, chartColors, chartCurrency, chartHeight, chartWidth, compact, indicators, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
|
|
1281
3146
|
|
|
1282
3147
|
const interactiveResult = useMemo(() => (
|
|
1283
3148
|
effectiveRenderer === "kitty"
|
|
@@ -1292,10 +3157,12 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1292
3157
|
mode: projection.effectiveMode,
|
|
1293
3158
|
axisMode,
|
|
1294
3159
|
currency: chartCurrency,
|
|
3160
|
+
assetCategory: chartAssetCategory,
|
|
1295
3161
|
colors: chartColors,
|
|
1296
3162
|
timeAxisDates,
|
|
3163
|
+
indicators,
|
|
1297
3164
|
})
|
|
1298
|
-
), [axisMode, chartColors, chartCurrency, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, effectiveRenderer, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
|
|
3165
|
+
), [axisMode, chartAssetCategory, chartColors, chartCurrency, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, effectiveRenderer, indicators, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
|
|
1299
3166
|
|
|
1300
3167
|
const result = effectiveRenderer === "kitty" ? staticResult : interactiveResult!;
|
|
1301
3168
|
|
|
@@ -1341,10 +3208,10 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1341
3208
|
if (effectiveRenderer === "kitty") return;
|
|
1342
3209
|
lastNativeBaseBitmapRef.current = null;
|
|
1343
3210
|
lastNativeCrosshairBitmapRef.current = null;
|
|
1344
|
-
nativeSurfaceManager.removeSurface(
|
|
1345
|
-
nativeSurfaceManager.removeSurface(
|
|
3211
|
+
nativeSurfaceManager.removeSurface(nativeBaseSurfaceId);
|
|
3212
|
+
nativeSurfaceManager.removeSurface(nativeCrosshairSurfaceId);
|
|
1346
3213
|
lastNativeGeometryRef.current = null;
|
|
1347
|
-
}, [effectiveRenderer, nativeSurfaceManager]);
|
|
3214
|
+
}, [effectiveRenderer, nativeBaseSurfaceId, nativeCrosshairSurfaceId, nativeSurfaceManager]);
|
|
1348
3215
|
|
|
1349
3216
|
useEffect(() => {
|
|
1350
3217
|
if (effectiveRenderer !== "kitty" || !rendererState.nativeReady || !plotRef.current) return;
|
|
@@ -1375,13 +3242,13 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1375
3242
|
};
|
|
1376
3243
|
syncCachedNativeSurface(
|
|
1377
3244
|
nativeSurfaceManager,
|
|
1378
|
-
|
|
3245
|
+
nativeBaseSurfaceId,
|
|
1379
3246
|
geometry,
|
|
1380
3247
|
lastNativeBaseBitmapRef.current,
|
|
1381
3248
|
);
|
|
1382
3249
|
syncCachedNativeSurface(
|
|
1383
3250
|
nativeSurfaceManager,
|
|
1384
|
-
|
|
3251
|
+
nativeCrosshairSurfaceId,
|
|
1385
3252
|
geometry,
|
|
1386
3253
|
lastNativeCrosshairBitmapRef.current,
|
|
1387
3254
|
);
|
|
@@ -1400,12 +3267,12 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1400
3267
|
renderer.unregisterLifecyclePass(plot);
|
|
1401
3268
|
lastNativeGeometryRef.current = null;
|
|
1402
3269
|
};
|
|
1403
|
-
}, [effectiveRenderer, nativeSurfaceManager, paneId, renderer, rendererState.nativeReady]);
|
|
3270
|
+
}, [effectiveRenderer, nativeBaseSurfaceId, nativeCrosshairSurfaceId, nativeSurfaceManager, paneId, renderer, rendererState.nativeReady]);
|
|
1404
3271
|
|
|
1405
3272
|
useEffect(() => {
|
|
1406
3273
|
if (effectiveRenderer !== "kitty" || !rendererState.nativeReady || !renderer.resolution || !plotRef.current || !nativeBaseScene) {
|
|
1407
3274
|
lastNativeBaseBitmapRef.current = null;
|
|
1408
|
-
nativeSurfaceManager.removeSurface(
|
|
3275
|
+
nativeSurfaceManager.removeSurface(nativeBaseSurfaceId);
|
|
1409
3276
|
return;
|
|
1410
3277
|
}
|
|
1411
3278
|
|
|
@@ -1428,6 +3295,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1428
3295
|
chartColors.candleUp,
|
|
1429
3296
|
chartColors.candleDown,
|
|
1430
3297
|
].join(","),
|
|
3298
|
+
indicatorRenderKey,
|
|
1431
3299
|
);
|
|
1432
3300
|
const cachedBitmap = lastNativeBaseBitmapRef.current?.key === bitmapKey
|
|
1433
3301
|
? lastNativeBaseBitmapRef.current.bitmap
|
|
@@ -1438,12 +3306,12 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1438
3306
|
}
|
|
1439
3307
|
|
|
1440
3308
|
if (!visibleRect) {
|
|
1441
|
-
nativeSurfaceManager.removeSurface(
|
|
3309
|
+
nativeSurfaceManager.removeSurface(nativeBaseSurfaceId);
|
|
1442
3310
|
return;
|
|
1443
3311
|
}
|
|
1444
3312
|
|
|
1445
3313
|
nativeSurfaceManager.upsertSurface({
|
|
1446
|
-
id:
|
|
3314
|
+
id: nativeBaseSurfaceId,
|
|
1447
3315
|
paneId,
|
|
1448
3316
|
rect: plotRect,
|
|
1449
3317
|
visibleRect,
|
|
@@ -1461,7 +3329,9 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1461
3329
|
chartColors.volumeUp,
|
|
1462
3330
|
compact,
|
|
1463
3331
|
effectiveRenderer,
|
|
3332
|
+
indicatorRenderKey,
|
|
1464
3333
|
nativeBaseScene,
|
|
3334
|
+
nativeBaseSurfaceId,
|
|
1465
3335
|
nativeSurfaceManager,
|
|
1466
3336
|
paneId,
|
|
1467
3337
|
projection.effectiveMode,
|
|
@@ -1474,7 +3344,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1474
3344
|
useEffect(() => {
|
|
1475
3345
|
if (effectiveRenderer !== "kitty" || !rendererState.nativeReady || !renderer.resolution || !plotRef.current || !nativeCrosshair) {
|
|
1476
3346
|
lastNativeCrosshairBitmapRef.current = null;
|
|
1477
|
-
nativeSurfaceManager.removeSurface(
|
|
3347
|
+
nativeSurfaceManager.removeSurface(nativeCrosshairSurfaceId);
|
|
1478
3348
|
return;
|
|
1479
3349
|
}
|
|
1480
3350
|
|
|
@@ -1495,7 +3365,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1495
3365
|
|
|
1496
3366
|
if (overlayPixelX === null || overlayPixelY === null) {
|
|
1497
3367
|
lastNativeCrosshairBitmapRef.current = null;
|
|
1498
|
-
nativeSurfaceManager.removeSurface(
|
|
3368
|
+
nativeSurfaceManager.removeSurface(nativeCrosshairSurfaceId);
|
|
1499
3369
|
return;
|
|
1500
3370
|
}
|
|
1501
3371
|
|
|
@@ -1522,12 +3392,12 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1522
3392
|
}
|
|
1523
3393
|
|
|
1524
3394
|
if (!visibleRect) {
|
|
1525
|
-
nativeSurfaceManager.removeSurface(
|
|
3395
|
+
nativeSurfaceManager.removeSurface(nativeCrosshairSurfaceId);
|
|
1526
3396
|
return;
|
|
1527
3397
|
}
|
|
1528
3398
|
|
|
1529
3399
|
nativeSurfaceManager.upsertSurface({
|
|
1530
|
-
id:
|
|
3400
|
+
id: nativeCrosshairSurfaceId,
|
|
1531
3401
|
paneId,
|
|
1532
3402
|
rect: plotRect,
|
|
1533
3403
|
visibleRect,
|
|
@@ -1537,53 +3407,52 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1537
3407
|
}, [
|
|
1538
3408
|
effectiveRenderer,
|
|
1539
3409
|
nativeCrosshair,
|
|
3410
|
+
nativeCrosshairSurfaceId,
|
|
1540
3411
|
nativeSurfaceManager,
|
|
1541
3412
|
paneId,
|
|
1542
3413
|
renderer,
|
|
1543
3414
|
rendererState.nativeReady,
|
|
1544
3415
|
]);
|
|
1545
3416
|
|
|
1546
|
-
const hasHistory =
|
|
1547
|
-
const firstPrice = chartWindow.points[0]?.close ?? 0;
|
|
1548
|
-
const lastPrice = chartWindow.points[chartWindow.points.length - 1]?.close ?? 0;
|
|
1549
|
-
const change = lastPrice - firstPrice;
|
|
1550
|
-
const changePct = firstPrice ? (change / firstPrice) * 100 : 0;
|
|
3417
|
+
const hasHistory = chartWindow.points.length > 0;
|
|
1551
3418
|
const requestedMode = projection.requestedMode;
|
|
1552
3419
|
const showOhlcSummary = projection.effectiveMode === "candles" || projection.effectiveMode === "ohlc";
|
|
1553
3420
|
const hasSelectionCursor = selectionCursorX !== null;
|
|
1554
3421
|
const hasDisplayCursor = displayCursorX !== null && displayCursorY !== null;
|
|
1555
|
-
const displayPrice = !hasHistory
|
|
1556
|
-
? null
|
|
1557
|
-
: hasSelectionCursor
|
|
1558
|
-
? (selectionScene?.priceAtCursor ?? lastPrice)
|
|
1559
|
-
: lastPrice;
|
|
1560
|
-
const displayChange = !hasHistory
|
|
1561
|
-
? null
|
|
1562
|
-
: hasSelectionCursor
|
|
1563
|
-
? (selectionScene?.changeAtCursor ?? change)
|
|
1564
|
-
: change;
|
|
1565
|
-
const displayChangePct = !hasHistory
|
|
1566
|
-
? null
|
|
1567
|
-
: hasSelectionCursor
|
|
1568
|
-
? (selectionScene?.changePctAtCursor ?? changePct)
|
|
1569
|
-
: changePct;
|
|
1570
3422
|
const displayDate = hasSelectionCursor || showOhlcSummary
|
|
1571
3423
|
? (selectionScene?.dateAtCursor ? formatDateShort(selectionScene.dateAtCursor) : null)
|
|
1572
3424
|
: null;
|
|
1573
3425
|
const activePoint = showOhlcSummary ? (selectionScene?.activePoint ?? null) : null;
|
|
3426
|
+
const visiblePriceRange = selectionScene
|
|
3427
|
+
? Math.max(selectionScene.max - selectionScene.min, 0)
|
|
3428
|
+
: (staticResult.priceRange ?? undefined);
|
|
1574
3429
|
const axisLabels = new Map(staticResult.axisLabels.map((entry) => [entry.row, entry.label]));
|
|
1575
3430
|
const cursorAxisLabel = hasDisplayCursor && cursorRow !== null && crosshairPrice !== null
|
|
1576
|
-
?
|
|
3431
|
+
? formatCursorAxisValue(
|
|
3432
|
+
crosshairPrice,
|
|
3433
|
+
axisMode,
|
|
3434
|
+
projection.points[0]?.close ?? 0,
|
|
3435
|
+
chartCurrency,
|
|
3436
|
+
chartAssetCategory,
|
|
3437
|
+
visiblePriceRange,
|
|
3438
|
+
)
|
|
1577
3439
|
: null;
|
|
1578
|
-
const
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
3440
|
+
const isBlockingBody = !compact && renderBodyState.blocking;
|
|
3441
|
+
const bodyMessage = !compact
|
|
3442
|
+
? (renderBodyState.errorMessage ?? renderBodyState.emptyMessage)
|
|
3443
|
+
: null;
|
|
3444
|
+
const isUpdating = !compact && (renderBodyState.updating || boundsBodyState.updating);
|
|
3445
|
+
|
|
3446
|
+
useEffect(() => {
|
|
3447
|
+
queueMicrotask(() => renderer.requestRender());
|
|
3448
|
+
}, [
|
|
3449
|
+
activePreset,
|
|
3450
|
+
bodyMessage,
|
|
3451
|
+
fallbackResolutionLabel,
|
|
3452
|
+
isUpdating,
|
|
3453
|
+
renderer,
|
|
3454
|
+
selectedResolution,
|
|
3455
|
+
]);
|
|
1587
3456
|
|
|
1588
3457
|
const handlePlotMove = (event: ChartMouseEvent) => {
|
|
1589
3458
|
if (!interactive || compact) return;
|
|
@@ -1606,6 +3475,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1606
3475
|
|
|
1607
3476
|
const handlePlotDown = (event: ChartMouseEvent) => {
|
|
1608
3477
|
if (!interactive || compact) return;
|
|
3478
|
+
focusPaneForMouseInteraction(event);
|
|
1609
3479
|
const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
|
|
1610
3480
|
if (!localPointer) return;
|
|
1611
3481
|
const selectionCursor = resolveSelectionCursor(localPointer, projection.points.length, chartWidth, projection.effectiveMode);
|
|
@@ -1623,6 +3493,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1623
3493
|
dragRef.current = {
|
|
1624
3494
|
startGlobalX: getGlobalMouseX(event, renderer),
|
|
1625
3495
|
startPanOffset: viewState.panOffset,
|
|
3496
|
+
startWindow: navigableDateWindow,
|
|
1626
3497
|
};
|
|
1627
3498
|
commitSelectionCursor(selectionCursor);
|
|
1628
3499
|
};
|
|
@@ -1647,24 +3518,48 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1647
3518
|
}
|
|
1648
3519
|
if (!dragRef.current) return;
|
|
1649
3520
|
|
|
1650
|
-
|
|
3521
|
+
if (effectiveResolution === "auto" && dragRef.current.startWindow) {
|
|
3522
|
+
const deltaCells = getGlobalMouseX(event, renderer) - dragRef.current.startGlobalX;
|
|
3523
|
+
requestAutoWindow(shiftDateWindow(
|
|
3524
|
+
dragRef.current.startWindow,
|
|
3525
|
+
-getDragPanWindowRatio(deltaCells, chartWidth),
|
|
3526
|
+
));
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
const visibleCount = getVisiblePointCount(boundsHistory.length, viewState.zoomLevel);
|
|
1651
3531
|
const deltaCells = getGlobalMouseX(event, renderer) - dragRef.current.startGlobalX;
|
|
1652
|
-
const
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
3532
|
+
const nextPan = resolveDragPanOffset(
|
|
3533
|
+
dragRef.current.startPanOffset,
|
|
3534
|
+
deltaCells,
|
|
3535
|
+
chartWidth,
|
|
3536
|
+
visibleCount,
|
|
3537
|
+
boundsHistory.length - visibleCount,
|
|
1657
3538
|
);
|
|
1658
|
-
setViewState((current) =>
|
|
3539
|
+
setViewState((current) => {
|
|
3540
|
+
const cleared = clearActivePreset(current);
|
|
3541
|
+
return cleared.panOffset === nextPan ? cleared : { ...cleared, panOffset: nextPan };
|
|
3542
|
+
});
|
|
1659
3543
|
};
|
|
1660
3544
|
|
|
1661
3545
|
const handlePlotScroll = (event: ChartMouseEvent) => {
|
|
1662
|
-
if (
|
|
3546
|
+
if (compact) return;
|
|
3547
|
+
event.stopPropagation?.();
|
|
3548
|
+
event.preventDefault?.();
|
|
3549
|
+
focusPaneForMouseInteraction(event);
|
|
1663
3550
|
const direction = event.scroll?.direction;
|
|
1664
3551
|
if (!direction) return;
|
|
3552
|
+
const scrollPan = consumeScrollPanMovement(
|
|
3553
|
+
chartWidth,
|
|
3554
|
+
event.scroll?.delta,
|
|
3555
|
+
direction,
|
|
3556
|
+
scrollPanCellRemainderRef.current,
|
|
3557
|
+
);
|
|
3558
|
+
scrollPanCellRemainderRef.current = scrollPan.remainder;
|
|
3559
|
+
const scrollPanCells = scrollPan.cells;
|
|
3560
|
+
const scrollPanRatio = scrollPan.ratio;
|
|
1665
3561
|
|
|
1666
3562
|
const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
|
|
1667
|
-
const anchorRatio = localPointer ? localPointer.cellX / Math.max(chartWidth - 1, 1) : 0.5;
|
|
1668
3563
|
|
|
1669
3564
|
if (localPointer) {
|
|
1670
3565
|
const selectionCursor = resolveSelectionCursor(localPointer, projection.points.length, chartWidth, projection.effectiveMode);
|
|
@@ -1682,72 +3577,35 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1682
3577
|
commitSelectionCursor(selectionCursor);
|
|
1683
3578
|
}
|
|
1684
3579
|
|
|
1685
|
-
if (
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
kind: "pan-left",
|
|
1690
|
-
targetPanOffset,
|
|
1691
|
-
})) {
|
|
1692
|
-
return;
|
|
1693
|
-
}
|
|
1694
|
-
const nextPan = clamp(
|
|
1695
|
-
targetPanOffset,
|
|
1696
|
-
0,
|
|
1697
|
-
getMaxPanOffset(baseHistory, viewState.zoomLevel),
|
|
1698
|
-
);
|
|
1699
|
-
setViewState((current) => ({ ...clearActivePreset(current), panOffset: nextPan }));
|
|
3580
|
+
if (scrollPanCells === 0) return;
|
|
3581
|
+
|
|
3582
|
+
if (effectiveResolution === "auto") {
|
|
3583
|
+
requestAutoWindow(shiftDateWindow(navigableDateWindow, scrollPanRatio));
|
|
1700
3584
|
return;
|
|
1701
3585
|
}
|
|
1702
3586
|
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
kind: "zoom-out",
|
|
1710
|
-
targetVisibleCount: Math.round(baseHistory.length * 1.18),
|
|
1711
|
-
anchorRatio,
|
|
1712
|
-
})) {
|
|
1713
|
-
return;
|
|
1714
|
-
}
|
|
1715
|
-
setViewState((current) => applyZoomAroundAnchor(current, current.zoomLevel / 1.18, anchorRatio, baseHistory));
|
|
1716
|
-
return;
|
|
1717
|
-
case "left":
|
|
1718
|
-
if (viewState.panOffset >= getMaxPanOffset(baseHistory, viewState.zoomLevel) && expandBufferRange({
|
|
1719
|
-
kind: "pan-left",
|
|
1720
|
-
targetPanOffset: viewState.panOffset + Math.max(Math.round(chartWidth * 0.08), 1),
|
|
1721
|
-
})) {
|
|
1722
|
-
return;
|
|
1723
|
-
}
|
|
1724
|
-
setViewState((current) => ({
|
|
1725
|
-
...clearActivePreset(current),
|
|
1726
|
-
panOffset: clamp(
|
|
1727
|
-
current.panOffset + Math.max(Math.round(chartWidth * 0.08), 1),
|
|
1728
|
-
0,
|
|
1729
|
-
getMaxPanOffset(baseHistory, current.zoomLevel),
|
|
1730
|
-
),
|
|
1731
|
-
}));
|
|
1732
|
-
return;
|
|
1733
|
-
case "right":
|
|
1734
|
-
setViewState((current) => ({
|
|
1735
|
-
...clearActivePreset(current),
|
|
1736
|
-
panOffset: clamp(
|
|
1737
|
-
current.panOffset - Math.max(Math.round(chartWidth * 0.08), 1),
|
|
1738
|
-
0,
|
|
1739
|
-
getMaxPanOffset(baseHistory, current.zoomLevel),
|
|
1740
|
-
),
|
|
1741
|
-
}));
|
|
1742
|
-
return;
|
|
3587
|
+
const targetPanOffset = viewState.panOffset + scrollPanCells;
|
|
3588
|
+
if (scrollPanCells > 0 && viewState.panOffset >= getMaxPanOffset(boundsHistory, viewState.zoomLevel) && expandBufferRange({
|
|
3589
|
+
kind: "pan-left",
|
|
3590
|
+
targetPanOffset,
|
|
3591
|
+
})) {
|
|
3592
|
+
return;
|
|
1743
3593
|
}
|
|
3594
|
+
setViewState((current) => ({
|
|
3595
|
+
...clearActivePreset(current),
|
|
3596
|
+
panOffset: clamp(
|
|
3597
|
+
current.panOffset + scrollPanCells,
|
|
3598
|
+
0,
|
|
3599
|
+
getMaxPanOffset(boundsHistory, current.zoomLevel),
|
|
3600
|
+
),
|
|
3601
|
+
}));
|
|
1744
3602
|
};
|
|
1745
3603
|
|
|
1746
3604
|
const plotLines: Array<string | StyledContent> = effectiveRenderer === "kitty"
|
|
1747
3605
|
? blankPlotLines
|
|
1748
3606
|
: result.lines;
|
|
1749
3607
|
const plotContent = plotLines.map((line, index) => (
|
|
1750
|
-
<text key={index} content={line} />
|
|
3608
|
+
<text key={index} content={line as unknown as string} />
|
|
1751
3609
|
));
|
|
1752
3610
|
|
|
1753
3611
|
const plotBox = (
|
|
@@ -1765,7 +3623,6 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1765
3623
|
onMouseOut={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : () => {
|
|
1766
3624
|
dragRef.current = null;
|
|
1767
3625
|
}}
|
|
1768
|
-
onMouseScroll={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotScroll}
|
|
1769
3626
|
>
|
|
1770
3627
|
{isBlockingBody
|
|
1771
3628
|
? (
|
|
@@ -1784,13 +3641,13 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1784
3641
|
);
|
|
1785
3642
|
|
|
1786
3643
|
const axisBox = (
|
|
1787
|
-
<box width={
|
|
3644
|
+
<box width={axisSectionWidth} height={chartHeight} flexDirection="column">
|
|
1788
3645
|
{Array.from({ length: chartHeight }, (_, row) => {
|
|
1789
3646
|
const isCursorRow = cursorAxisLabel !== null && cursorRow === row;
|
|
1790
3647
|
const label = isCursorRow ? cursorAxisLabel : (axisLabels.get(row) ?? null);
|
|
1791
3648
|
return (
|
|
1792
3649
|
<text key={row} fg={isCursorRow ? chartColors.crosshairColor : colors.textDim}>
|
|
1793
|
-
{formatAxisCell(label, axisWidth)}
|
|
3650
|
+
{formatAxisCell(label, axisWidth).padEnd(axisSectionWidth)}
|
|
1794
3651
|
</text>
|
|
1795
3652
|
);
|
|
1796
3653
|
})}
|
|
@@ -1812,27 +3669,45 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1812
3669
|
}
|
|
1813
3670
|
|
|
1814
3671
|
return (
|
|
1815
|
-
<box
|
|
3672
|
+
<box
|
|
3673
|
+
flexDirection="column"
|
|
3674
|
+
flexGrow={1}
|
|
3675
|
+
onMouseScroll={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotScroll}
|
|
3676
|
+
>
|
|
1816
3677
|
<box flexDirection="row" gap={2} height={1}>
|
|
1817
3678
|
<text attributes={TextAttributes.BOLD} fg={colors.textBright}>
|
|
1818
|
-
{ticker?.metadata.ticker ?? ""} - {getChartResolutionLabel(
|
|
1819
|
-
</text>
|
|
1820
|
-
<text fg={colors.textDim}>{visibleLabel}</text>
|
|
1821
|
-
<text fg={displayChange !== null ? priceColor(displayChange) : colors.textDim}>
|
|
1822
|
-
{displayPrice !== null ? formatCurrency(displayPrice, chartCurrency) : "—"}
|
|
1823
|
-
</text>
|
|
1824
|
-
<text fg={displayChange !== null ? priceColor(displayChange) : colors.textDim}>
|
|
1825
|
-
{displayChange !== null && displayChangePct !== null
|
|
1826
|
-
? `${displayChange >= 0 ? "+" : ""}${displayChange.toFixed(2)} (${displayChangePct >= 0 ? "+" : ""}${displayChangePct.toFixed(2)}%)`
|
|
1827
|
-
: "No data"}
|
|
3679
|
+
{ticker?.metadata.ticker ?? ""} - {getChartResolutionLabel(selectedResolution)}
|
|
1828
3680
|
</text>
|
|
3681
|
+
{fallbackResolutionLabel && (
|
|
3682
|
+
<text fg={colors.textDim}>{fallbackResolutionLabel}</text>
|
|
3683
|
+
)}
|
|
1829
3684
|
{displayDate && <text fg={colors.textDim}>{displayDate}</text>}
|
|
1830
3685
|
{showOhlcSummary && activePoint && (
|
|
1831
3686
|
<>
|
|
1832
|
-
<text fg={colors.textDim}>O {
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
3687
|
+
<text fg={colors.textDim}>O {formatMarketPriceWithCurrency(activePoint.open, chartCurrency, {
|
|
3688
|
+
assetCategory: chartAssetCategory,
|
|
3689
|
+
minimumFractionDigits: 2,
|
|
3690
|
+
precisionOffset: 1,
|
|
3691
|
+
priceRange: visiblePriceRange,
|
|
3692
|
+
})}</text>
|
|
3693
|
+
<text fg={colors.textDim}>H {formatMarketPriceWithCurrency(activePoint.high, chartCurrency, {
|
|
3694
|
+
assetCategory: chartAssetCategory,
|
|
3695
|
+
minimumFractionDigits: 2,
|
|
3696
|
+
precisionOffset: 1,
|
|
3697
|
+
priceRange: visiblePriceRange,
|
|
3698
|
+
})}</text>
|
|
3699
|
+
<text fg={colors.textDim}>L {formatMarketPriceWithCurrency(activePoint.low, chartCurrency, {
|
|
3700
|
+
assetCategory: chartAssetCategory,
|
|
3701
|
+
minimumFractionDigits: 2,
|
|
3702
|
+
precisionOffset: 1,
|
|
3703
|
+
priceRange: visiblePriceRange,
|
|
3704
|
+
})}</text>
|
|
3705
|
+
<text fg={colors.textDim}>C {formatMarketPriceWithCurrency(activePoint.close, chartCurrency, {
|
|
3706
|
+
assetCategory: chartAssetCategory,
|
|
3707
|
+
minimumFractionDigits: 2,
|
|
3708
|
+
precisionOffset: 1,
|
|
3709
|
+
priceRange: visiblePriceRange,
|
|
3710
|
+
})}</text>
|
|
1836
3711
|
<text fg={colors.textDim}>V {formatCompact(activePoint.volume)}</text>
|
|
1837
3712
|
</>
|
|
1838
3713
|
)}
|
|
@@ -1845,7 +3720,8 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1845
3720
|
key={range}
|
|
1846
3721
|
fg={activePreset === range ? chartColors.activeRangeColor : (isRangePresetSupported(range, availableManualResolutions) ? chartColors.inactiveRangeColor : colors.textMuted)}
|
|
1847
3722
|
attributes={activePreset === range ? TextAttributes.BOLD : 0}
|
|
1848
|
-
onMouseDown={() => {
|
|
3723
|
+
onMouseDown={(event: any) => {
|
|
3724
|
+
focusPaneForMouseInteraction(event);
|
|
1849
3725
|
if (isRangePresetSupported(range, availableManualResolutions)) setRange(range);
|
|
1850
3726
|
}}
|
|
1851
3727
|
>
|
|
@@ -1860,16 +3736,16 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1860
3736
|
{resolutionChips.map((resolution) => (
|
|
1861
3737
|
<text
|
|
1862
3738
|
key={resolution}
|
|
1863
|
-
fg={
|
|
1864
|
-
attributes={
|
|
1865
|
-
onMouseDown={() =>
|
|
3739
|
+
fg={selectedResolution === resolution ? chartColors.activeRangeColor : chartColors.inactiveRangeColor}
|
|
3740
|
+
attributes={selectedResolution === resolution ? TextAttributes.BOLD : 0}
|
|
3741
|
+
onMouseDown={(event: any) => {
|
|
3742
|
+
focusPaneForMouseInteraction(event);
|
|
3743
|
+
setResolution(resolution);
|
|
3744
|
+
}}
|
|
1866
3745
|
>
|
|
1867
3746
|
{getChartResolutionLabel(resolution)}
|
|
1868
3747
|
</text>
|
|
1869
3748
|
))}
|
|
1870
|
-
{viewState.zoomLevel !== 1 && (
|
|
1871
|
-
<text fg={colors.textDim}>zoom:{viewState.zoomLevel.toFixed(1)}x</text>
|
|
1872
|
-
)}
|
|
1873
3749
|
{isUpdating && (
|
|
1874
3750
|
<text fg={colors.textDim}>updating</text>
|
|
1875
3751
|
)}
|
|
@@ -1882,7 +3758,10 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1882
3758
|
key={mode}
|
|
1883
3759
|
fg={requestedMode === mode ? chartColors.activeRangeColor : chartColors.inactiveRangeColor}
|
|
1884
3760
|
attributes={requestedMode === mode ? TextAttributes.BOLD : 0}
|
|
1885
|
-
onMouseDown={() =>
|
|
3761
|
+
onMouseDown={(event: any) => {
|
|
3762
|
+
focusPaneForMouseInteraction(event);
|
|
3763
|
+
setRenderMode(mode);
|
|
3764
|
+
}}
|
|
1886
3765
|
>
|
|
1887
3766
|
{MODE_CHIPS[mode]}
|
|
1888
3767
|
</text>
|
|
@@ -1907,7 +3786,12 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1907
3786
|
)}
|
|
1908
3787
|
</box>
|
|
1909
3788
|
|
|
1910
|
-
<box
|
|
3789
|
+
<box
|
|
3790
|
+
flexDirection="row"
|
|
3791
|
+
height={chartHeight}
|
|
3792
|
+
gap={axisGap}
|
|
3793
|
+
onMouseScroll={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotScroll}
|
|
3794
|
+
>
|
|
1911
3795
|
{plotBox}
|
|
1912
3796
|
{axisBox}
|
|
1913
3797
|
</box>
|
|
@@ -1916,12 +3800,13 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
|
1916
3800
|
<text fg={colors.textDim}>{selectionScene?.timeLabels ?? staticResult.timeLabels}</text>
|
|
1917
3801
|
</box>
|
|
1918
3802
|
|
|
1919
|
-
<box height={1}>
|
|
1920
|
-
<
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
3803
|
+
<box height={1} flexDirection="row" gap={1}>
|
|
3804
|
+
<ChartControlHint hotkey="m" label="ode" />
|
|
3805
|
+
{footerControls}
|
|
3806
|
+
<ChartControlHint hotkey="r" label="es" />
|
|
3807
|
+
<ChartControlHint hotkey="+/-" label="zoom" />
|
|
3808
|
+
<ChartControlHint hotkey="0" label="reset" />
|
|
3809
|
+
{width >= 72 && <ChartControlHint hotkey="1-7" label="range" />}
|
|
1925
3810
|
</box>
|
|
1926
3811
|
</box>
|
|
1927
3812
|
);
|