gloomberb 0.3.7 → 0.4.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 +19 -8
- package/package.json +1 -1
- package/src/app.tsx +45 -69
- package/src/cli/base-converter.ts +31 -0
- package/src/cli/commands/search.ts +11 -3
- package/src/cli/commands/ticker.ts +22 -11
- package/src/cli/context.ts +27 -32
- package/src/cli/helpers.ts +1 -4
- package/src/cli/index.ts +140 -123
- package/src/cli/registry.test.ts +226 -0
- package/src/cli/registry.ts +201 -0
- package/src/cli.test.ts +219 -0
- package/src/components/chart/chart-controller.ts +309 -0
- package/src/components/chart/chart-data.ts +5 -6
- package/src/components/chart/chart-pane-settings.ts +27 -0
- package/src/components/chart/chart-resolution.test.ts +56 -0
- package/src/components/chart/chart-resolution.ts +362 -0
- package/src/components/chart/chart-types.ts +12 -4
- package/src/components/chart/chart-viewport.test.ts +25 -0
- package/src/components/chart/chart-viewport.ts +28 -0
- package/src/components/chart/comparison-chart-data.ts +20 -27
- package/src/components/chart/comparison-stock-chart.tsx +374 -99
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
- package/src/components/chart/stock-chart.tsx +455 -106
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
- package/src/components/command-bar/command-bar.test.tsx +650 -5
- package/src/components/command-bar/command-bar.tsx +363 -63
- package/src/components/command-bar/command-registry.ts +8 -0
- package/src/components/command-bar/helpers.test.ts +23 -1
- package/src/components/command-bar/helpers.ts +14 -5
- package/src/components/command-bar/pane-template-display.ts +12 -0
- package/src/components/command-bar/root-shortcuts.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +4 -2
- package/src/components/detail-feed-view.test.tsx +124 -49
- package/src/components/detail-feed-view.tsx +396 -160
- package/src/components/layout/floating-pane.tsx +4 -2
- package/src/components/layout/header.tsx +3 -3
- package/src/components/layout/pane-header.tsx +26 -2
- package/src/components/layout/pane-sizing.ts +6 -0
- package/src/components/layout/pane.tsx +5 -1
- package/src/components/layout/shell.test.tsx +77 -5
- package/src/components/layout/shell.tsx +67 -52
- package/src/components/layout/status-bar.test.tsx +66 -0
- package/src/components/layout/status-bar.tsx +26 -5
- package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
- package/src/components/onboarding/onboarding-wizard.tsx +5 -6
- package/src/components/ticker-list-table.test.tsx +83 -0
- package/src/components/ticker-list-table.tsx +181 -97
- package/src/components/ui/data-table.tsx +50 -5
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/page-stack-view.tsx +60 -0
- package/src/components/use-double-click-activation.ts +31 -0
- package/src/index.tsx +10 -10
- package/src/market-data/coordinator.test.ts +92 -4
- package/src/market-data/coordinator.ts +32 -5
- package/src/market-data/hooks.test.tsx +120 -0
- package/src/market-data/hooks.tsx +53 -16
- package/src/market-data/request-types.ts +4 -2
- package/src/market-data/selectors.ts +24 -58
- package/src/plugins/builtin/chat-controller.test.ts +400 -3
- package/src/plugins/builtin/chat-controller.ts +328 -35
- package/src/plugins/builtin/chat.test.tsx +118 -6
- package/src/plugins/builtin/chat.tsx +180 -116
- package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
- package/src/plugins/builtin/comparison-chart.tsx +21 -1
- package/src/plugins/builtin/layout-manager.tsx +9 -0
- package/src/plugins/builtin/news.tsx +1 -11
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
- package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
- package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
- package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
- package/src/plugins/builtin/sec.tsx +1 -11
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
- package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
- package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
- package/src/plugins/catalog.ts +65 -0
- package/src/plugins/ibkr/broker-adapter.ts +28 -0
- package/src/plugins/ibkr/gateway-service.test.ts +67 -1
- package/src/plugins/ibkr/gateway-service.ts +147 -7
- package/src/plugins/ibkr/index.tsx +7 -1
- package/src/plugins/pane-manager.test.ts +1 -1
- package/src/plugins/plugin-runtime.tsx +27 -7
- package/src/plugins/prediction-markets/cli.ts +28 -0
- package/src/plugins/prediction-markets/controller-data.ts +132 -17
- package/src/plugins/prediction-markets/controller.ts +142 -122
- package/src/plugins/prediction-markets/index.tsx +7 -1
- package/src/plugins/prediction-markets/launch.ts +51 -16
- package/src/plugins/prediction-markets/metrics.ts +9 -5
- package/src/plugins/prediction-markets/pane.test.tsx +197 -57
- package/src/plugins/prediction-markets/pane.tsx +80 -155
- package/src/plugins/prediction-markets/services/fetch.ts +4 -0
- package/src/plugins/prediction-markets/table.tsx +6 -3
- package/src/plugins/prediction-markets/test-helpers.tsx +20 -5
- package/src/plugins/prediction-markets/types.ts +0 -1
- package/src/plugins/registry.ts +23 -0
- package/src/sources/gloomberb-cloud.test.ts +29 -0
- package/src/sources/gloomberb-cloud.ts +89 -1
- package/src/sources/provider-router.test.ts +265 -3
- package/src/sources/provider-router.ts +239 -11
- package/src/sources/yahoo-finance.test.ts +26 -0
- package/src/sources/yahoo-finance.ts +54 -3
- package/src/state/app-context-selector.test.tsx +99 -0
- package/src/state/app-context.test.ts +18 -0
- package/src/state/app-context.tsx +170 -65
- package/src/state/selectors.ts +25 -0
- package/src/theme/colors.ts +8 -8
- package/src/theme/themes.test.ts +2 -0
- package/src/theme/themes.ts +22 -10
- package/src/types/broker.ts +21 -0
- package/src/types/config.ts +17 -1
- package/src/types/data-provider.ts +18 -0
- package/src/types/financials.ts +31 -1
- package/src/types/plugin.ts +70 -0
- package/src/updater.test.ts +2 -0
- package/src/utils/api-client.test.ts +57 -0
- package/src/utils/api-client.ts +47 -12
- package/src/utils/back-navigation.ts +27 -0
- package/src/utils/format.test.ts +14 -0
- package/src/utils/format.ts +3 -3
- package/src/utils/market-status.ts +46 -1
- package/src/utils/quote-freshness.ts +127 -0
- package/src/utils/quote-resolution.test.ts +277 -0
- package/src/utils/quote-resolution.ts +641 -0
- package/src/utils/slugify.ts +3 -0
- package/src/utils/timestamp.ts +25 -0
- package/src/version.ts +1 -1
- package/src/cli/commands/portfolio.ts +0 -210
- package/src/cli/commands/watchlist.ts +0 -210
- package/src/plugins/builtin/data-providers.ts +0 -16
- package/src/sources/exchange-rates.ts +0 -18
- package/src/utils/ascii-chart.ts +0 -172
- package/src/utils/ticker-tokenizer.test.ts +0 -48
|
@@ -1,14 +1,51 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { memo, 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 {
|
|
4
|
+
import { useAppDispatch, useAppSelector, usePaneInstanceId, usePaneSettingValue, usePaneTicker } from "../../state/app-context";
|
|
5
5
|
import { saveConfig } from "../../data/config-store";
|
|
6
|
+
import { getSharedDataProvider } from "../../plugins/registry";
|
|
6
7
|
import { colors, priceColor } from "../../theme/colors";
|
|
7
8
|
import { formatCompact, formatCurrency } from "../../utils/format";
|
|
8
|
-
import { useChartQuery
|
|
9
|
+
import { useChartQuery } from "../../market-data/hooks";
|
|
9
10
|
import { instrumentFromTicker } from "../../market-data/request-types";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
11
|
+
import { usePersistChartControlSelection } from "./chart-pane-settings";
|
|
12
|
+
import { getVisibleWindow, projectChartData, resolveBarSize } from "./chart-data";
|
|
13
|
+
import {
|
|
14
|
+
buildVisibleDateWindow,
|
|
15
|
+
clearActivePreset,
|
|
16
|
+
formatVisibleSpanLabel,
|
|
17
|
+
getCanonicalZoomLevel,
|
|
18
|
+
getPointDates,
|
|
19
|
+
isCanonicalPresetViewport,
|
|
20
|
+
resolveChartBodyState,
|
|
21
|
+
resolvePresetSelection,
|
|
22
|
+
resolvePresetSelectionWithResolution,
|
|
23
|
+
resolveResolutionSelection,
|
|
24
|
+
resolveStoredChartSelection,
|
|
25
|
+
} from "./chart-controller";
|
|
26
|
+
import {
|
|
27
|
+
buildChartResolutionSupportMap,
|
|
28
|
+
clampTimeRangeToMaxRange,
|
|
29
|
+
DEFAULT_TICKER_CHART_RANGE_PRESET,
|
|
30
|
+
DEFAULT_TICKER_CHART_RESOLUTION,
|
|
31
|
+
DEFAULT_VISIBLE_MANUAL_CHART_RESOLUTIONS,
|
|
32
|
+
getBestSupportedResolutionForPreset,
|
|
33
|
+
getChartResolutionLabel,
|
|
34
|
+
getNextFallbackResolution,
|
|
35
|
+
getNextBufferRange,
|
|
36
|
+
getPresetResolution,
|
|
37
|
+
getSupportMaxRange,
|
|
38
|
+
isRangePresetSupported,
|
|
39
|
+
normalizeChartResolutionSupport,
|
|
40
|
+
sortChartResolutions,
|
|
41
|
+
type ChartResolutionSupport,
|
|
42
|
+
type ManualChartResolution,
|
|
43
|
+
} from "./chart-resolution";
|
|
44
|
+
import {
|
|
45
|
+
getVisiblePointCount,
|
|
46
|
+
RIGHT_EDGE_ANCHOR_RATIO,
|
|
47
|
+
resolveAnchoredChartZoom,
|
|
48
|
+
} from "./chart-viewport";
|
|
12
49
|
import {
|
|
13
50
|
buildChartScene,
|
|
14
51
|
formatDateShort,
|
|
@@ -30,6 +67,7 @@ import {
|
|
|
30
67
|
TIME_RANGES,
|
|
31
68
|
type ChartAxisMode,
|
|
32
69
|
type ChartRenderMode,
|
|
70
|
+
type ChartResolution,
|
|
33
71
|
type ChartViewState,
|
|
34
72
|
type ResolvedChartRenderer,
|
|
35
73
|
} from "./chart-types";
|
|
@@ -46,7 +84,8 @@ import { ensureKittySupport, getCachedKittySupport } from "./native/kitty-suppor
|
|
|
46
84
|
import { resolveChartRendererState } from "./native/renderer-selection";
|
|
47
85
|
import { getNativeSurfaceManager } from "./native/surface-manager";
|
|
48
86
|
import { syncCachedNativeSurface } from "./native/surface-sync";
|
|
49
|
-
import type { PricePoint } from "../../types/financials";
|
|
87
|
+
import type { PricePoint, TickerFinancials } from "../../types/financials";
|
|
88
|
+
import type { TickerRecord } from "../../types/ticker";
|
|
50
89
|
|
|
51
90
|
const MODE_CHIPS: Record<ChartRenderMode, string> = {
|
|
52
91
|
area: "A",
|
|
@@ -73,11 +112,22 @@ interface StockChartProps {
|
|
|
73
112
|
currencyOverride?: string | null;
|
|
74
113
|
}
|
|
75
114
|
|
|
115
|
+
interface ResolvedStockChartProps extends StockChartProps {
|
|
116
|
+
symbol: string | null;
|
|
117
|
+
ticker: TickerRecord | null;
|
|
118
|
+
financials: TickerFinancials | null;
|
|
119
|
+
}
|
|
120
|
+
|
|
76
121
|
interface DragState {
|
|
77
122
|
startGlobalX: number;
|
|
78
123
|
startPanOffset: number;
|
|
79
124
|
}
|
|
80
125
|
|
|
126
|
+
type PendingExpansionAction =
|
|
127
|
+
| { kind: "zoom-out"; targetVisibleCount: number; anchorRatio: number }
|
|
128
|
+
| { kind: "pan-left"; targetPanOffset: number }
|
|
129
|
+
| null;
|
|
130
|
+
|
|
81
131
|
interface ChartMouseEvent {
|
|
82
132
|
x: number;
|
|
83
133
|
y: number;
|
|
@@ -400,10 +450,9 @@ function formatAxisCell(label: string | null, width: number): string {
|
|
|
400
450
|
return label.length >= width ? label.slice(0, width) : label.padStart(width);
|
|
401
451
|
}
|
|
402
452
|
|
|
403
|
-
function getMaxPanOffset(history: PricePoint[],
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
return Math.max(filtered.length - visibleCount, 0);
|
|
453
|
+
function getMaxPanOffset(history: PricePoint[], zoomLevel: number): number {
|
|
454
|
+
const visibleCount = getVisiblePointCount(history.length, zoomLevel);
|
|
455
|
+
return Math.max(history.length - visibleCount, 0);
|
|
407
456
|
}
|
|
408
457
|
|
|
409
458
|
function applyZoomAroundAnchor(
|
|
@@ -411,25 +460,20 @@ function applyZoomAroundAnchor(
|
|
|
411
460
|
nextZoomLevel: number,
|
|
412
461
|
anchorRatio: number,
|
|
413
462
|
history: PricePoint[],
|
|
414
|
-
chartWidth: number,
|
|
415
463
|
): ChartViewState {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const nextStart = Math.round(anchorIndex - ratio * Math.max(nextVisibleCount - 1, 0));
|
|
426
|
-
const clampedStart = clamp(nextStart, 0, Math.max(filtered.length - nextVisibleCount, 0));
|
|
427
|
-
const nextPanOffset = filtered.length - nextVisibleCount - clampedStart;
|
|
464
|
+
if (history.length === 0) return view;
|
|
465
|
+
|
|
466
|
+
const nextZoom = resolveAnchoredChartZoom(
|
|
467
|
+
history.length,
|
|
468
|
+
view.zoomLevel,
|
|
469
|
+
view.panOffset,
|
|
470
|
+
nextZoomLevel,
|
|
471
|
+
anchorRatio,
|
|
472
|
+
);
|
|
428
473
|
|
|
429
474
|
return {
|
|
430
|
-
...view,
|
|
431
|
-
|
|
432
|
-
panOffset: clamp(nextPanOffset, 0, Math.max(filtered.length - nextVisibleCount, 0)),
|
|
475
|
+
...clearActivePreset(view),
|
|
476
|
+
...nextZoom,
|
|
433
477
|
};
|
|
434
478
|
}
|
|
435
479
|
|
|
@@ -507,7 +551,12 @@ function resolveSelectionCursor(
|
|
|
507
551
|
};
|
|
508
552
|
}
|
|
509
553
|
|
|
510
|
-
export function StockChart({
|
|
554
|
+
export function StockChart(props: StockChartProps) {
|
|
555
|
+
const { symbol, ticker, financials } = usePaneTicker();
|
|
556
|
+
return <ResolvedStockChart {...props} symbol={symbol} ticker={ticker} financials={financials} />;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export const ResolvedStockChart = memo(function ResolvedStockChart({
|
|
511
560
|
width,
|
|
512
561
|
height,
|
|
513
562
|
focused,
|
|
@@ -516,22 +565,32 @@ export function StockChart({
|
|
|
516
565
|
axisMode = "price",
|
|
517
566
|
historyOverride = null,
|
|
518
567
|
currencyOverride = null,
|
|
519
|
-
|
|
568
|
+
symbol,
|
|
569
|
+
ticker,
|
|
570
|
+
financials,
|
|
571
|
+
}: ResolvedStockChartProps) {
|
|
520
572
|
const renderer = useRenderer();
|
|
521
|
-
const
|
|
573
|
+
const dispatch = useAppDispatch();
|
|
574
|
+
const config = useAppSelector((state) => state.config);
|
|
522
575
|
const paneId = usePaneInstanceId();
|
|
523
|
-
const { ticker, financials } = usePaneTicker();
|
|
524
576
|
const nativeSurfaceManager = useMemo(() => getNativeSurfaceManager(renderer), [renderer]);
|
|
525
|
-
const defaultRenderMode =
|
|
526
|
-
const preferredRenderer =
|
|
577
|
+
const defaultRenderMode = config.chartPreferences.defaultRenderMode;
|
|
578
|
+
const preferredRenderer = config.chartPreferences.renderer;
|
|
579
|
+
const [storedRangePreset] = usePaneSettingValue("chartRangePreset", DEFAULT_TICKER_CHART_RANGE_PRESET);
|
|
580
|
+
const [storedResolution] = usePaneSettingValue<ChartResolution>("chartResolution", DEFAULT_TICKER_CHART_RESOLUTION);
|
|
581
|
+
const persistChartControls = usePersistChartControlSelection("chartRangePreset");
|
|
527
582
|
const [viewState, setViewState] = useState<ChartViewState>({
|
|
528
|
-
|
|
583
|
+
presetRange: compact ? "1Y" : storedRangePreset,
|
|
584
|
+
bufferRange: compact ? "1Y" : storedRangePreset,
|
|
585
|
+
activePreset: compact ? null : storedRangePreset,
|
|
586
|
+
resolution: compact ? "auto" : storedResolution,
|
|
529
587
|
panOffset: 0,
|
|
530
588
|
zoomLevel: 1,
|
|
531
589
|
cursorX: null,
|
|
532
590
|
cursorY: null,
|
|
533
591
|
renderMode: defaultRenderMode,
|
|
534
592
|
});
|
|
593
|
+
const [resolutionSupport, setResolutionSupport] = useState<ChartResolutionSupport[] | null>(null);
|
|
535
594
|
const [showVolume, setShowVolume] = useState(!compact);
|
|
536
595
|
const [kittySupport, setKittySupport] = useState<boolean | null>(() => getCachedKittySupport(renderer));
|
|
537
596
|
const [displayCursor, setDisplayCursor] = useState<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
|
|
@@ -546,6 +605,9 @@ export function StockChart({
|
|
|
546
605
|
const targetCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
|
|
547
606
|
const cursorMotionKindRef = useRef<ChartCursorMotionKind>("discrete");
|
|
548
607
|
const animationFrameRef = useRef<number | null>(null);
|
|
608
|
+
const pendingCanonicalResetRef = useRef(1);
|
|
609
|
+
const appliedCanonicalResetRef = useRef(0);
|
|
610
|
+
const pendingExpansionRef = useRef<PendingExpansionAction>(null);
|
|
549
611
|
|
|
550
612
|
const commitDisplayCursor = (next: DisplayCursorState) => {
|
|
551
613
|
displayCursorRef.current = next;
|
|
@@ -631,16 +693,107 @@ export function StockChart({
|
|
|
631
693
|
() => instrumentFromTicker(ticker, ticker?.metadata.ticker ?? null),
|
|
632
694
|
[ticker],
|
|
633
695
|
);
|
|
696
|
+
const capabilityKey = instrumentRef ? [
|
|
697
|
+
instrumentRef.symbol,
|
|
698
|
+
instrumentRef.exchange ?? "",
|
|
699
|
+
instrumentRef.brokerId ?? "",
|
|
700
|
+
instrumentRef.brokerInstanceId ?? "",
|
|
701
|
+
instrumentRef.instrument?.conId ?? "",
|
|
702
|
+
].join("|") : null;
|
|
703
|
+
|
|
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
|
+
useEffect(() => {
|
|
722
|
+
if (compact || !instrumentRef) {
|
|
723
|
+
setResolutionSupport(null);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const provider = getSharedDataProvider();
|
|
727
|
+
if (!provider?.getChartResolutionSupport && !provider?.getChartResolutionCapabilities) {
|
|
728
|
+
setResolutionSupport(null);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
let cancelled = false;
|
|
733
|
+
setResolutionSupport(null);
|
|
734
|
+
Promise.resolve(provider.getChartResolutionSupport
|
|
735
|
+
? provider.getChartResolutionSupport(
|
|
736
|
+
instrumentRef.symbol,
|
|
737
|
+
instrumentRef.exchange ?? "",
|
|
738
|
+
{
|
|
739
|
+
brokerId: instrumentRef.brokerId,
|
|
740
|
+
brokerInstanceId: instrumentRef.brokerInstanceId,
|
|
741
|
+
instrument: instrumentRef.instrument ?? null,
|
|
742
|
+
},
|
|
743
|
+
)
|
|
744
|
+
: normalizeChartResolutionSupport(
|
|
745
|
+
(provider.getChartResolutionCapabilities?.(
|
|
746
|
+
instrumentRef.symbol,
|
|
747
|
+
instrumentRef.exchange ?? "",
|
|
748
|
+
{
|
|
749
|
+
brokerId: instrumentRef.brokerId,
|
|
750
|
+
brokerInstanceId: instrumentRef.brokerInstanceId,
|
|
751
|
+
instrument: instrumentRef.instrument ?? null,
|
|
752
|
+
},
|
|
753
|
+
) ?? []).map((resolution) => ({ resolution, maxRange: "ALL" })),
|
|
754
|
+
)
|
|
755
|
+
).then((support) => {
|
|
756
|
+
if (!cancelled) {
|
|
757
|
+
setResolutionSupport(support);
|
|
758
|
+
}
|
|
759
|
+
}).catch(() => {
|
|
760
|
+
if (!cancelled) {
|
|
761
|
+
setResolutionSupport(null);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
return () => {
|
|
766
|
+
cancelled = true;
|
|
767
|
+
};
|
|
768
|
+
}, [capabilityKey, compact, instrumentRef]);
|
|
769
|
+
|
|
770
|
+
const supportMap = useMemo(() => buildChartResolutionSupportMap(resolutionSupport ?? []), [resolutionSupport]);
|
|
771
|
+
const effectiveResolutionSupport = useMemo<ChartResolutionSupport[]>(() => (
|
|
772
|
+
resolutionSupport ?? DEFAULT_VISIBLE_MANUAL_CHART_RESOLUTIONS.map((resolution) => ({ resolution, maxRange: "ALL" as const }))
|
|
773
|
+
), [resolutionSupport]);
|
|
774
|
+
const availableManualResolutions = resolutionSupport?.map((entry) => entry.resolution) ?? DEFAULT_VISIBLE_MANUAL_CHART_RESOLUTIONS;
|
|
775
|
+
const effectiveResolution: ChartResolution = !compact
|
|
776
|
+
&& viewState.resolution !== "auto"
|
|
777
|
+
&& resolutionSupport !== null
|
|
778
|
+
&& !supportMap.has(viewState.resolution)
|
|
779
|
+
? "auto"
|
|
780
|
+
: viewState.resolution;
|
|
781
|
+
const resolutionChips = useMemo(
|
|
782
|
+
() => sortChartResolutions(["auto", ...availableManualResolutions] as ChartResolution[]),
|
|
783
|
+
[availableManualResolutions],
|
|
784
|
+
);
|
|
785
|
+
|
|
634
786
|
const baseChartEntry = useChartQuery(
|
|
635
787
|
!compact && instrumentRef
|
|
636
788
|
? {
|
|
637
789
|
instrument: instrumentRef,
|
|
638
|
-
|
|
639
|
-
granularity: "
|
|
790
|
+
bufferRange: viewState.bufferRange,
|
|
791
|
+
granularity: effectiveResolution === "auto" ? "range" : "resolution",
|
|
792
|
+
resolution: effectiveResolution === "auto" ? undefined : effectiveResolution,
|
|
640
793
|
}
|
|
641
794
|
: null,
|
|
642
795
|
);
|
|
643
|
-
const
|
|
796
|
+
const baseBodyState = resolveChartBodyState(baseChartEntry, (value) => Array.isArray(value) && value.length > 0, "No price history available.");
|
|
644
797
|
const axisWidth = compact
|
|
645
798
|
? axisMode === "percent" ? 11 : 8
|
|
646
799
|
: axisMode === "percent" ? 11 : 10;
|
|
@@ -648,12 +801,11 @@ export function StockChart({
|
|
|
648
801
|
const chartWidth = Math.max(width - axisWidth - axisGap, compact ? 12 : 20);
|
|
649
802
|
const maxCursorX = chartWidth - 1;
|
|
650
803
|
const panStep = Math.max(Math.floor(chartWidth / 10), 1);
|
|
651
|
-
const baseHistory =
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
: (financials?.priceHistory ?? []));
|
|
804
|
+
const baseHistory = compact
|
|
805
|
+
? (historyOverride ?? financials?.priceHistory ?? [])
|
|
806
|
+
: (baseBodyState.data ?? []);
|
|
655
807
|
const detailRequest = useMemo(() => {
|
|
656
|
-
if (compact || !instrumentRef || baseHistory.length < 2 || viewState.zoomLevel <= 1) return null;
|
|
808
|
+
if (compact || effectiveResolution !== "auto" || !instrumentRef || baseHistory.length < 2 || viewState.zoomLevel <= 1) return null;
|
|
657
809
|
const window = getVisibleWindow(baseHistory, viewState, chartWidth);
|
|
658
810
|
if (window.points.length < 2) return null;
|
|
659
811
|
|
|
@@ -666,17 +818,70 @@ export function StockChart({
|
|
|
666
818
|
|
|
667
819
|
return {
|
|
668
820
|
instrument: instrumentRef,
|
|
669
|
-
|
|
821
|
+
bufferRange: viewState.bufferRange,
|
|
670
822
|
granularity: "detail" as const,
|
|
671
823
|
startDate,
|
|
672
824
|
endDate,
|
|
673
825
|
barSize,
|
|
674
826
|
};
|
|
675
|
-
}, [baseHistory, chartWidth, compact, instrumentRef, viewState]);
|
|
827
|
+
}, [baseHistory, chartWidth, compact, effectiveResolution, instrumentRef, viewState]);
|
|
676
828
|
const detailChartEntry = useChartQuery(detailRequest);
|
|
677
|
-
const
|
|
829
|
+
const detailBodyState = resolveChartBodyState(detailChartEntry, (value) => Array.isArray(value) && value.length > 0, "No price history available.");
|
|
830
|
+
const history = (effectiveResolution === "auto" && viewState.zoomLevel > 1 && detailBodyState.data) ? detailBodyState.data : baseHistory;
|
|
831
|
+
const baseHistoryDates = useMemo(() => getPointDates(baseHistory), [baseHistory]);
|
|
832
|
+
const visibleDateWindow = useMemo(() => buildVisibleDateWindow(baseHistoryDates, viewState.panOffset, viewState.zoomLevel), [baseHistoryDates, viewState.panOffset, viewState.zoomLevel]);
|
|
833
|
+
const activePreset = compact || !isCanonicalPresetViewport(baseHistoryDates, {
|
|
834
|
+
activePreset: viewState.activePreset,
|
|
835
|
+
panOffset: viewState.panOffset,
|
|
836
|
+
zoomLevel: viewState.zoomLevel,
|
|
837
|
+
resolution: effectiveResolution,
|
|
838
|
+
})
|
|
839
|
+
? null
|
|
840
|
+
: viewState.activePreset;
|
|
678
841
|
|
|
679
|
-
|
|
842
|
+
useEffect(() => {
|
|
843
|
+
if (compact || baseHistoryDates.length === 0) return;
|
|
844
|
+
|
|
845
|
+
if (appliedCanonicalResetRef.current < pendingCanonicalResetRef.current) {
|
|
846
|
+
const canonicalZoom = getCanonicalZoomLevel(baseHistoryDates, viewState.presetRange);
|
|
847
|
+
appliedCanonicalResetRef.current = pendingCanonicalResetRef.current;
|
|
848
|
+
setViewState((current) => (
|
|
849
|
+
current.zoomLevel === canonicalZoom && current.panOffset === 0
|
|
850
|
+
? current
|
|
851
|
+
: {
|
|
852
|
+
...current,
|
|
853
|
+
zoomLevel: canonicalZoom,
|
|
854
|
+
panOffset: 0,
|
|
855
|
+
cursorX: null,
|
|
856
|
+
cursorY: null,
|
|
857
|
+
}
|
|
858
|
+
));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (!pendingExpansionRef.current) return;
|
|
863
|
+
const pendingExpansion = pendingExpansionRef.current;
|
|
864
|
+
pendingExpansionRef.current = null;
|
|
865
|
+
setViewState((current) => {
|
|
866
|
+
if (pendingExpansion.kind === "zoom-out") {
|
|
867
|
+
const nextVisibleCount = Math.min(baseHistoryDates.length, Math.max(pendingExpansion.targetVisibleCount, 1));
|
|
868
|
+
return {
|
|
869
|
+
...current,
|
|
870
|
+
...resolveAnchoredChartZoom(
|
|
871
|
+
baseHistoryDates.length,
|
|
872
|
+
1,
|
|
873
|
+
0,
|
|
874
|
+
baseHistoryDates.length / nextVisibleCount,
|
|
875
|
+
pendingExpansion.anchorRatio,
|
|
876
|
+
),
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
...current,
|
|
881
|
+
panOffset: clamp(pendingExpansion.targetPanOffset, 0, getMaxPanOffset(baseHistory, current.zoomLevel)),
|
|
882
|
+
};
|
|
883
|
+
});
|
|
884
|
+
}, [baseHistory, baseHistoryDates, compact, viewState.presetRange]);
|
|
680
885
|
|
|
681
886
|
useEffect(() => {
|
|
682
887
|
if (interactive) {
|
|
@@ -717,9 +922,9 @@ export function StockChart({
|
|
|
717
922
|
const persistDefaultRenderMode = (nextMode: ChartRenderMode) => {
|
|
718
923
|
if (nextMode === defaultRenderMode) return;
|
|
719
924
|
const nextConfig = {
|
|
720
|
-
...
|
|
925
|
+
...config,
|
|
721
926
|
chartPreferences: {
|
|
722
|
-
...
|
|
927
|
+
...config.chartPreferences,
|
|
723
928
|
defaultRenderMode: nextMode,
|
|
724
929
|
},
|
|
725
930
|
};
|
|
@@ -727,16 +932,43 @@ export function StockChart({
|
|
|
727
932
|
saveConfig(nextConfig).catch(() => {});
|
|
728
933
|
};
|
|
729
934
|
|
|
730
|
-
const
|
|
731
|
-
|
|
935
|
+
const expandBufferRange = (action: PendingExpansionAction): boolean => {
|
|
936
|
+
if (compact) return false;
|
|
937
|
+
const nextCandidate = getNextBufferRange(viewState.bufferRange);
|
|
938
|
+
const nextBufferRange = effectiveResolution === "auto"
|
|
939
|
+
? nextCandidate
|
|
940
|
+
: clampTimeRangeToMaxRange(nextCandidate, supportMap.get(effectiveResolution) ?? viewState.bufferRange);
|
|
941
|
+
if (nextBufferRange === viewState.bufferRange) return false;
|
|
942
|
+
pendingExpansionRef.current = action;
|
|
732
943
|
setViewState((current) => ({
|
|
733
|
-
...current,
|
|
734
|
-
|
|
735
|
-
panOffset: 0,
|
|
736
|
-
zoomLevel: 1,
|
|
944
|
+
...clearActivePreset(current),
|
|
945
|
+
bufferRange: nextBufferRange,
|
|
737
946
|
cursorX: null,
|
|
738
947
|
cursorY: null,
|
|
739
948
|
}));
|
|
949
|
+
return true;
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
const setRange = (range: TimeRange) => {
|
|
953
|
+
if (!compact && !isRangePresetSupported(range, effectiveResolutionSupport)) return;
|
|
954
|
+
const supportMaxRange = getSupportMaxRange(effectiveResolutionSupport, getPresetResolution(range));
|
|
955
|
+
if (!compact) {
|
|
956
|
+
persistChartControls(range, getPresetResolution(range));
|
|
957
|
+
}
|
|
958
|
+
pendingCanonicalResetRef.current += 1;
|
|
959
|
+
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
960
|
+
setViewState((current) => resolvePresetSelection(current, range, supportMaxRange));
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
const setResolution = (resolution: ChartResolution) => {
|
|
964
|
+
if (compact) return;
|
|
965
|
+
if (resolution !== "auto" && !availableManualResolutions.includes(resolution)) return;
|
|
966
|
+
const nextState = resolveResolutionSelection(viewState, resolution, supportMap, visibleDateWindow);
|
|
967
|
+
if (!nextState) return;
|
|
968
|
+
pendingCanonicalResetRef.current += 1;
|
|
969
|
+
persistChartControls(nextState.presetRange, resolution);
|
|
970
|
+
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
971
|
+
setViewState(nextState);
|
|
740
972
|
};
|
|
741
973
|
|
|
742
974
|
const setRenderMode = (mode: ChartRenderMode) => {
|
|
@@ -744,12 +976,12 @@ export function StockChart({
|
|
|
744
976
|
setViewState((current) => ({ ...current, renderMode: mode }));
|
|
745
977
|
};
|
|
746
978
|
|
|
747
|
-
const headerRows = compact ? 0 :
|
|
979
|
+
const headerRows = compact ? 0 : 4;
|
|
748
980
|
const helpRow = compact ? 0 : 1;
|
|
749
981
|
const timeAxisRow = 1;
|
|
750
982
|
const volumeHeight = showVolume && !compact ? 3 : 0;
|
|
751
983
|
const chartHeight = Math.max(height - headerRows - helpRow - timeAxisRow, 4);
|
|
752
|
-
const isDetailView = viewState.zoomLevel > 1 &&
|
|
984
|
+
const isDetailView = effectiveResolution === "auto" && viewState.zoomLevel > 1 && !!detailBodyState.data?.length;
|
|
753
985
|
const historyRenderKey = history.length === 0
|
|
754
986
|
? "empty"
|
|
755
987
|
: [
|
|
@@ -770,7 +1002,7 @@ export function StockChart({
|
|
|
770
1002
|
return isDetailView
|
|
771
1003
|
? { points: history, startIdx: 0, endIdx: history.length }
|
|
772
1004
|
: getVisibleWindow(history, viewState, chartWidth);
|
|
773
|
-
}, [chartWidth, history, historyOverride, isDetailView, viewState.panOffset, viewState.
|
|
1005
|
+
}, [chartWidth, history, historyOverride, isDetailView, viewState.panOffset, viewState.zoomLevel]);
|
|
774
1006
|
const timeAxisDates = useMemo(
|
|
775
1007
|
() => chartWindow.points.map((point) => point.date),
|
|
776
1008
|
[chartWindow.points],
|
|
@@ -785,23 +1017,56 @@ export function StockChart({
|
|
|
785
1017
|
|
|
786
1018
|
switch (event.name) {
|
|
787
1019
|
case "=":
|
|
788
|
-
setViewState((current) => applyZoomAroundAnchor(
|
|
1020
|
+
setViewState((current) => applyZoomAroundAnchor(
|
|
1021
|
+
current,
|
|
1022
|
+
current.zoomLevel * 1.5,
|
|
1023
|
+
RIGHT_EDGE_ANCHOR_RATIO,
|
|
1024
|
+
baseHistory,
|
|
1025
|
+
));
|
|
789
1026
|
return;
|
|
790
1027
|
case "-":
|
|
791
|
-
|
|
1028
|
+
if (viewState.zoomLevel <= 1.001 && expandBufferRange({
|
|
1029
|
+
kind: "zoom-out",
|
|
1030
|
+
targetVisibleCount: Math.round(baseHistory.length * 1.5),
|
|
1031
|
+
anchorRatio: RIGHT_EDGE_ANCHOR_RATIO,
|
|
1032
|
+
})) {
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
setViewState((current) => applyZoomAroundAnchor(
|
|
1036
|
+
current,
|
|
1037
|
+
current.zoomLevel / 1.5,
|
|
1038
|
+
RIGHT_EDGE_ANCHOR_RATIO,
|
|
1039
|
+
baseHistory,
|
|
1040
|
+
));
|
|
792
1041
|
return;
|
|
793
1042
|
case "0":
|
|
1043
|
+
pendingCanonicalResetRef.current += 1;
|
|
794
1044
|
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
795
|
-
setViewState((current) =>
|
|
1045
|
+
setViewState((current) => {
|
|
1046
|
+
const nextState = resolveResolutionSelection(current, effectiveResolution, supportMap, visibleDateWindow) ?? current;
|
|
1047
|
+
return {
|
|
1048
|
+
...nextState,
|
|
1049
|
+
panOffset: 0,
|
|
1050
|
+
zoomLevel: 1,
|
|
1051
|
+
cursorX: null,
|
|
1052
|
+
cursorY: null,
|
|
1053
|
+
};
|
|
1054
|
+
});
|
|
796
1055
|
return;
|
|
797
1056
|
case "v":
|
|
798
1057
|
setShowVolume((value) => !value);
|
|
799
1058
|
return;
|
|
800
1059
|
case "a":
|
|
801
|
-
|
|
1060
|
+
if (viewState.panOffset >= getMaxPanOffset(baseHistory, viewState.zoomLevel) && expandBufferRange({
|
|
1061
|
+
kind: "pan-left",
|
|
1062
|
+
targetPanOffset: viewState.panOffset + panStep,
|
|
1063
|
+
})) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
setViewState((current) => ({ ...clearActivePreset(current), panOffset: current.panOffset + panStep }));
|
|
802
1067
|
return;
|
|
803
1068
|
case "d":
|
|
804
|
-
setViewState((current) => ({ ...current, panOffset: Math.max(current.panOffset - panStep, 0) }));
|
|
1069
|
+
setViewState((current) => ({ ...clearActivePreset(current), panOffset: Math.max(current.panOffset - panStep, 0) }));
|
|
805
1070
|
return;
|
|
806
1071
|
case "m":
|
|
807
1072
|
setViewState((current) => {
|
|
@@ -812,6 +1077,12 @@ export function StockChart({
|
|
|
812
1077
|
return { ...current, renderMode: nextMode };
|
|
813
1078
|
});
|
|
814
1079
|
return;
|
|
1080
|
+
case "r": {
|
|
1081
|
+
const currentIndex = resolutionChips.indexOf(effectiveResolution);
|
|
1082
|
+
const nextResolution = resolutionChips[(currentIndex + 1) % resolutionChips.length] ?? "auto";
|
|
1083
|
+
setResolution(nextResolution);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
815
1086
|
}
|
|
816
1087
|
|
|
817
1088
|
if (event.name >= "1" && event.name <= "7") {
|
|
@@ -825,7 +1096,13 @@ export function StockChart({
|
|
|
825
1096
|
switch (event.name) {
|
|
826
1097
|
case "left":
|
|
827
1098
|
if (event.shift) {
|
|
828
|
-
|
|
1099
|
+
if (viewState.panOffset >= getMaxPanOffset(baseHistory, viewState.zoomLevel) && expandBufferRange({
|
|
1100
|
+
kind: "pan-left",
|
|
1101
|
+
targetPanOffset: viewState.panOffset + panStep,
|
|
1102
|
+
})) {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
setViewState((current) => ({ ...clearActivePreset(current), panOffset: current.panOffset + panStep }));
|
|
829
1106
|
} else {
|
|
830
1107
|
cursorMotionKindRef.current = "discrete";
|
|
831
1108
|
setViewState((current) => {
|
|
@@ -845,10 +1122,10 @@ export function StockChart({
|
|
|
845
1122
|
chartWidth,
|
|
846
1123
|
projection.effectiveMode,
|
|
847
1124
|
);
|
|
848
|
-
const maxPan = getMaxPanOffset(baseHistory, current.
|
|
1125
|
+
const maxPan = getMaxPanOffset(baseHistory, current.zoomLevel);
|
|
849
1126
|
if (currentIndex <= 0) {
|
|
850
1127
|
return {
|
|
851
|
-
...current,
|
|
1128
|
+
...clearActivePreset(current),
|
|
852
1129
|
cursorX: nextCursor,
|
|
853
1130
|
cursorY: null,
|
|
854
1131
|
panOffset: clamp(current.panOffset + 1, 0, maxPan),
|
|
@@ -860,7 +1137,7 @@ export function StockChart({
|
|
|
860
1137
|
return;
|
|
861
1138
|
case "right":
|
|
862
1139
|
if (event.shift) {
|
|
863
|
-
setViewState((current) => ({ ...current, panOffset: Math.max(current.panOffset - panStep, 0) }));
|
|
1140
|
+
setViewState((current) => ({ ...clearActivePreset(current), panOffset: Math.max(current.panOffset - panStep, 0) }));
|
|
864
1141
|
} else {
|
|
865
1142
|
cursorMotionKindRef.current = "discrete";
|
|
866
1143
|
setViewState((current) => {
|
|
@@ -882,7 +1159,7 @@ export function StockChart({
|
|
|
882
1159
|
);
|
|
883
1160
|
if (currentIndex >= pointCount - 1) {
|
|
884
1161
|
return {
|
|
885
|
-
...current,
|
|
1162
|
+
...clearActivePreset(current),
|
|
886
1163
|
cursorX: nextCursor,
|
|
887
1164
|
cursorY: null,
|
|
888
1165
|
panOffset: Math.max(current.panOffset - 1, 0),
|
|
@@ -1266,10 +1543,7 @@ export function StockChart({
|
|
|
1266
1543
|
rendererState.nativeReady,
|
|
1267
1544
|
]);
|
|
1268
1545
|
|
|
1269
|
-
|
|
1270
|
-
return <text fg={colors.textDim}>No price history available.</text>;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1546
|
+
const hasHistory = history.length > 0;
|
|
1273
1547
|
const firstPrice = chartWindow.points[0]?.close ?? 0;
|
|
1274
1548
|
const lastPrice = chartWindow.points[chartWindow.points.length - 1]?.close ?? 0;
|
|
1275
1549
|
const change = lastPrice - firstPrice;
|
|
@@ -1278,9 +1552,21 @@ export function StockChart({
|
|
|
1278
1552
|
const showOhlcSummary = projection.effectiveMode === "candles" || projection.effectiveMode === "ohlc";
|
|
1279
1553
|
const hasSelectionCursor = selectionCursorX !== null;
|
|
1280
1554
|
const hasDisplayCursor = displayCursorX !== null && displayCursorY !== null;
|
|
1281
|
-
const displayPrice =
|
|
1282
|
-
|
|
1283
|
-
|
|
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;
|
|
1284
1570
|
const displayDate = hasSelectionCursor || showOhlcSummary
|
|
1285
1571
|
? (selectionScene?.dateAtCursor ? formatDateShort(selectionScene.dateAtCursor) : null)
|
|
1286
1572
|
: null;
|
|
@@ -1289,6 +1575,15 @@ export function StockChart({
|
|
|
1289
1575
|
const cursorAxisLabel = hasDisplayCursor && cursorRow !== null && crosshairPrice !== null
|
|
1290
1576
|
? formatAxisValue(crosshairPrice, axisMode, projection.points[0]?.close ?? 0, chartCurrency)
|
|
1291
1577
|
: null;
|
|
1578
|
+
const visibleLabel = formatVisibleSpanLabel({
|
|
1579
|
+
start: chartWindow.points[0]?.date ? coerceChartDate(chartWindow.points[0].date as Date | string | number) : null,
|
|
1580
|
+
end: chartWindow.points[chartWindow.points.length - 1]?.date
|
|
1581
|
+
? coerceChartDate(chartWindow.points[chartWindow.points.length - 1]!.date as Date | string | number)
|
|
1582
|
+
: null,
|
|
1583
|
+
});
|
|
1584
|
+
const isBlockingBody = !compact && baseBodyState.blocking;
|
|
1585
|
+
const bodyMessage = !compact ? (baseBodyState.errorMessage ?? baseBodyState.emptyMessage) : null;
|
|
1586
|
+
const isUpdating = !compact && (baseBodyState.updating || (detailChartEntry?.phase === "refreshing" && !!detailBodyState.data?.length));
|
|
1292
1587
|
|
|
1293
1588
|
const handlePlotMove = (event: ChartMouseEvent) => {
|
|
1294
1589
|
if (!interactive || compact) return;
|
|
@@ -1352,16 +1647,15 @@ export function StockChart({
|
|
|
1352
1647
|
}
|
|
1353
1648
|
if (!dragRef.current) return;
|
|
1354
1649
|
|
|
1355
|
-
const
|
|
1356
|
-
const visibleCount = getVisiblePointCount(filtered.length, viewState.zoomLevel);
|
|
1650
|
+
const visibleCount = getVisiblePointCount(baseHistory.length, viewState.zoomLevel);
|
|
1357
1651
|
const deltaCells = getGlobalMouseX(event, renderer) - dragRef.current.startGlobalX;
|
|
1358
1652
|
const pointDelta = Math.round((deltaCells / Math.max(chartWidth, 1)) * visibleCount);
|
|
1359
1653
|
const nextPan = clamp(
|
|
1360
1654
|
dragRef.current.startPanOffset - pointDelta,
|
|
1361
1655
|
0,
|
|
1362
|
-
Math.max(
|
|
1656
|
+
Math.max(baseHistory.length - visibleCount, 0),
|
|
1363
1657
|
);
|
|
1364
|
-
setViewState((current) => ({ ...current, panOffset: nextPan }));
|
|
1658
|
+
setViewState((current) => ({ ...clearActivePreset(current), panOffset: nextPan }));
|
|
1365
1659
|
};
|
|
1366
1660
|
|
|
1367
1661
|
const handlePlotScroll = (event: ChartMouseEvent) => {
|
|
@@ -1390,39 +1684,59 @@ export function StockChart({
|
|
|
1390
1684
|
|
|
1391
1685
|
if (event.modifiers.shift && (direction === "up" || direction === "down")) {
|
|
1392
1686
|
const shiftDirection = direction === "up" ? 1 : -1;
|
|
1687
|
+
const targetPanOffset = viewState.panOffset + shiftDirection * Math.max(Math.round(chartWidth * 0.08), 1);
|
|
1688
|
+
if (shiftDirection > 0 && viewState.panOffset >= getMaxPanOffset(baseHistory, viewState.zoomLevel) && expandBufferRange({
|
|
1689
|
+
kind: "pan-left",
|
|
1690
|
+
targetPanOffset,
|
|
1691
|
+
})) {
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1393
1694
|
const nextPan = clamp(
|
|
1394
|
-
|
|
1695
|
+
targetPanOffset,
|
|
1395
1696
|
0,
|
|
1396
|
-
getMaxPanOffset(baseHistory, viewState.
|
|
1697
|
+
getMaxPanOffset(baseHistory, viewState.zoomLevel),
|
|
1397
1698
|
);
|
|
1398
|
-
setViewState((current) => ({ ...current, panOffset: nextPan }));
|
|
1699
|
+
setViewState((current) => ({ ...clearActivePreset(current), panOffset: nextPan }));
|
|
1399
1700
|
return;
|
|
1400
1701
|
}
|
|
1401
1702
|
|
|
1402
1703
|
switch (direction) {
|
|
1403
1704
|
case "up":
|
|
1404
|
-
setViewState((current) => applyZoomAroundAnchor(current, current.zoomLevel * 1.18, anchorRatio, baseHistory
|
|
1705
|
+
setViewState((current) => applyZoomAroundAnchor(current, current.zoomLevel * 1.18, anchorRatio, baseHistory));
|
|
1405
1706
|
return;
|
|
1406
1707
|
case "down":
|
|
1407
|
-
|
|
1708
|
+
if (viewState.zoomLevel <= 1.001 && expandBufferRange({
|
|
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));
|
|
1408
1716
|
return;
|
|
1409
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
|
+
}
|
|
1410
1724
|
setViewState((current) => ({
|
|
1411
|
-
...current,
|
|
1725
|
+
...clearActivePreset(current),
|
|
1412
1726
|
panOffset: clamp(
|
|
1413
1727
|
current.panOffset + Math.max(Math.round(chartWidth * 0.08), 1),
|
|
1414
1728
|
0,
|
|
1415
|
-
getMaxPanOffset(baseHistory, current.
|
|
1729
|
+
getMaxPanOffset(baseHistory, current.zoomLevel),
|
|
1416
1730
|
),
|
|
1417
1731
|
}));
|
|
1418
1732
|
return;
|
|
1419
1733
|
case "right":
|
|
1420
1734
|
setViewState((current) => ({
|
|
1421
|
-
...current,
|
|
1735
|
+
...clearActivePreset(current),
|
|
1422
1736
|
panOffset: clamp(
|
|
1423
1737
|
current.panOffset - Math.max(Math.round(chartWidth * 0.08), 1),
|
|
1424
1738
|
0,
|
|
1425
|
-
getMaxPanOffset(baseHistory, current.
|
|
1739
|
+
getMaxPanOffset(baseHistory, current.zoomLevel),
|
|
1426
1740
|
),
|
|
1427
1741
|
}));
|
|
1428
1742
|
return;
|
|
@@ -1443,17 +1757,29 @@ export function StockChart({
|
|
|
1443
1757
|
height={chartHeight}
|
|
1444
1758
|
flexDirection="column"
|
|
1445
1759
|
backgroundColor={chartColors.bgColor}
|
|
1446
|
-
onMouseMove={compact ? undefined : handlePlotMove}
|
|
1447
|
-
onMouseDown={compact ? undefined : handlePlotDown}
|
|
1448
|
-
onMouseUp={compact ? undefined : () => { dragRef.current = null; }}
|
|
1449
|
-
onMouseDrag={compact ? undefined : handlePlotDrag}
|
|
1450
|
-
onMouseDragEnd={compact ? undefined : () => { dragRef.current = null; }}
|
|
1451
|
-
onMouseOut={compact ? undefined : () => {
|
|
1760
|
+
onMouseMove={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotMove}
|
|
1761
|
+
onMouseDown={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotDown}
|
|
1762
|
+
onMouseUp={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : () => { dragRef.current = null; }}
|
|
1763
|
+
onMouseDrag={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotDrag}
|
|
1764
|
+
onMouseDragEnd={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : () => { dragRef.current = null; }}
|
|
1765
|
+
onMouseOut={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : () => {
|
|
1452
1766
|
dragRef.current = null;
|
|
1453
1767
|
}}
|
|
1454
|
-
onMouseScroll={compact ? undefined : handlePlotScroll}
|
|
1768
|
+
onMouseScroll={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotScroll}
|
|
1455
1769
|
>
|
|
1456
|
-
{
|
|
1770
|
+
{isBlockingBody
|
|
1771
|
+
? (
|
|
1772
|
+
<box flexGrow={1} alignItems="center" justifyContent="center">
|
|
1773
|
+
<text fg={colors.textDim}>Loading chart...</text>
|
|
1774
|
+
</box>
|
|
1775
|
+
)
|
|
1776
|
+
: bodyMessage
|
|
1777
|
+
? (
|
|
1778
|
+
<box flexGrow={1} alignItems="center" justifyContent="center">
|
|
1779
|
+
<text fg={colors.textDim}>{bodyMessage}</text>
|
|
1780
|
+
</box>
|
|
1781
|
+
)
|
|
1782
|
+
: plotContent}
|
|
1457
1783
|
</box>
|
|
1458
1784
|
);
|
|
1459
1785
|
|
|
@@ -1489,13 +1815,16 @@ export function StockChart({
|
|
|
1489
1815
|
<box flexDirection="column" flexGrow={1}>
|
|
1490
1816
|
<box flexDirection="row" gap={2} height={1}>
|
|
1491
1817
|
<text attributes={TextAttributes.BOLD} fg={colors.textBright}>
|
|
1492
|
-
{ticker?.metadata.ticker ?? ""} - {
|
|
1818
|
+
{ticker?.metadata.ticker ?? ""} - {getChartResolutionLabel(effectiveResolution)}
|
|
1493
1819
|
</text>
|
|
1494
|
-
<text fg={
|
|
1495
|
-
|
|
1820
|
+
<text fg={colors.textDim}>{visibleLabel}</text>
|
|
1821
|
+
<text fg={displayChange !== null ? priceColor(displayChange) : colors.textDim}>
|
|
1822
|
+
{displayPrice !== null ? formatCurrency(displayPrice, chartCurrency) : "—"}
|
|
1496
1823
|
</text>
|
|
1497
|
-
<text fg={priceColor(displayChange)}>
|
|
1498
|
-
{displayChange
|
|
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"}
|
|
1499
1828
|
</text>
|
|
1500
1829
|
{displayDate && <text fg={colors.textDim}>{displayDate}</text>}
|
|
1501
1830
|
{showOhlcSummary && activePoint && (
|
|
@@ -1514,15 +1843,35 @@ export function StockChart({
|
|
|
1514
1843
|
{TIME_RANGES.map((range, index) => (
|
|
1515
1844
|
<text
|
|
1516
1845
|
key={range}
|
|
1517
|
-
fg={
|
|
1518
|
-
attributes={
|
|
1519
|
-
onMouseDown={() =>
|
|
1846
|
+
fg={activePreset === range ? chartColors.activeRangeColor : (isRangePresetSupported(range, availableManualResolutions) ? chartColors.inactiveRangeColor : colors.textMuted)}
|
|
1847
|
+
attributes={activePreset === range ? TextAttributes.BOLD : 0}
|
|
1848
|
+
onMouseDown={() => {
|
|
1849
|
+
if (isRangePresetSupported(range, availableManualResolutions)) setRange(range);
|
|
1850
|
+
}}
|
|
1520
1851
|
>
|
|
1521
1852
|
{`${index + 1}:${range}`}
|
|
1522
1853
|
</text>
|
|
1523
1854
|
))}
|
|
1855
|
+
</box>
|
|
1856
|
+
</box>
|
|
1857
|
+
|
|
1858
|
+
<box flexDirection="row" height={1}>
|
|
1859
|
+
<box flexDirection="row" gap={1}>
|
|
1860
|
+
{resolutionChips.map((resolution) => (
|
|
1861
|
+
<text
|
|
1862
|
+
key={resolution}
|
|
1863
|
+
fg={effectiveResolution === resolution ? chartColors.activeRangeColor : chartColors.inactiveRangeColor}
|
|
1864
|
+
attributes={effectiveResolution === resolution ? TextAttributes.BOLD : 0}
|
|
1865
|
+
onMouseDown={() => setResolution(resolution)}
|
|
1866
|
+
>
|
|
1867
|
+
{getChartResolutionLabel(resolution)}
|
|
1868
|
+
</text>
|
|
1869
|
+
))}
|
|
1524
1870
|
{viewState.zoomLevel !== 1 && (
|
|
1525
|
-
<text fg={colors.textDim}>
|
|
1871
|
+
<text fg={colors.textDim}>zoom:{viewState.zoomLevel.toFixed(1)}x</text>
|
|
1872
|
+
)}
|
|
1873
|
+
{isUpdating && (
|
|
1874
|
+
<text fg={colors.textDim}>updating</text>
|
|
1526
1875
|
)}
|
|
1527
1876
|
</box>
|
|
1528
1877
|
<box flexGrow={1} />
|
|
@@ -1570,10 +1919,10 @@ export function StockChart({
|
|
|
1570
1919
|
<box height={1}>
|
|
1571
1920
|
<text fg={colors.textMuted}>
|
|
1572
1921
|
{interactive
|
|
1573
|
-
? "mouse hover inspect drag pan wheel zoom ⇧wheel pan ←→ cursor ⇧←→ pan m mode 1-7
|
|
1574
|
-
: "Enter crosshair click chart to focus a/d pan +/- zoom m mode 1-7
|
|
1922
|
+
? "mouse hover inspect drag pan wheel zoom ⇧wheel pan ←→ cursor ⇧←→ pan m mode r res 1-7 presets v vol 0 reset Esc exit"
|
|
1923
|
+
: "Enter crosshair click chart to focus a/d pan +/- zoom m mode r res 1-7 presets v volume 0 reset"}
|
|
1575
1924
|
</text>
|
|
1576
1925
|
</box>
|
|
1577
1926
|
</box>
|
|
1578
1927
|
);
|
|
1579
|
-
}
|
|
1928
|
+
});
|