gloomberb 0.3.3 → 0.4.0
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 +27 -16
- package/package.json +1 -1
- package/src/app.tsx +29 -65
- 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-renderer.ts +7 -2
- 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/chart.test.ts +26 -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 +484 -112
- 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 +359 -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 +1 -1
- package/src/components/layout/pane-header.tsx +26 -2
- package/src/components/layout/shell.tsx +62 -50
- 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 +174 -97
- package/src/components/ui/data-table.tsx +27 -2
- 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 +370 -3
- package/src/plugins/builtin/chat-controller.ts +310 -35
- package/src/plugins/builtin/chat.test.tsx +118 -6
- package/src/plugins/builtin/chat.tsx +175 -116
- package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
- package/src/plugins/builtin/comparison-chart.tsx +21 -1
- 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 +75 -1
- package/src/plugins/ibkr/gateway-service.ts +179 -9
- package/src/plugins/pane-manager.test.ts +1 -1
- package/src/plugins/plugin-runtime.tsx +27 -7
- package/src/plugins/prediction-markets/chart.test.tsx +139 -0
- package/src/plugins/prediction-markets/chart.tsx +65 -436
- package/src/plugins/prediction-markets/cli.ts +28 -0
- package/src/plugins/prediction-markets/controller-data.ts +17 -5
- package/src/plugins/prediction-markets/controller.ts +141 -121
- package/src/plugins/prediction-markets/detail/outcomes.tsx +2 -6
- package/src/plugins/prediction-markets/detail/pane.tsx +4 -25
- package/src/plugins/prediction-markets/detail/shared.tsx +0 -1
- package/src/plugins/prediction-markets/detail.test.tsx +4 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- 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 +160 -58
- package/src/plugins/prediction-markets/pane.tsx +74 -183
- package/src/plugins/prediction-markets/table.tsx +6 -3
- package/src/plugins/prediction-markets/types.ts +0 -1
- package/src/sources/gloomberb-cloud.test.ts +29 -0
- package/src/sources/gloomberb-cloud.ts +89 -1
- package/src/sources/provider-router.test.ts +308 -3
- package/src/sources/provider-router.ts +247 -17
- 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/utils/api-client.test.ts +57 -0
- package/src/utils/api-client.ts +35 -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/price-history.test.ts +25 -0
- package/src/utils/price-history.ts +29 -5
- 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",
|
|
@@ -69,6 +108,14 @@ interface StockChartProps {
|
|
|
69
108
|
interactive?: boolean;
|
|
70
109
|
compact?: boolean;
|
|
71
110
|
axisMode?: ChartAxisMode;
|
|
111
|
+
historyOverride?: PricePoint[] | null;
|
|
112
|
+
currencyOverride?: string | null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface ResolvedStockChartProps extends StockChartProps {
|
|
116
|
+
symbol: string | null;
|
|
117
|
+
ticker: TickerRecord | null;
|
|
118
|
+
financials: TickerFinancials | null;
|
|
72
119
|
}
|
|
73
120
|
|
|
74
121
|
interface DragState {
|
|
@@ -76,6 +123,11 @@ interface DragState {
|
|
|
76
123
|
startPanOffset: number;
|
|
77
124
|
}
|
|
78
125
|
|
|
126
|
+
type PendingExpansionAction =
|
|
127
|
+
| { kind: "zoom-out"; targetVisibleCount: number; anchorRatio: number }
|
|
128
|
+
| { kind: "pan-left"; targetPanOffset: number }
|
|
129
|
+
| null;
|
|
130
|
+
|
|
79
131
|
interface ChartMouseEvent {
|
|
80
132
|
x: number;
|
|
81
133
|
y: number;
|
|
@@ -398,10 +450,9 @@ function formatAxisCell(label: string | null, width: number): string {
|
|
|
398
450
|
return label.length >= width ? label.slice(0, width) : label.padStart(width);
|
|
399
451
|
}
|
|
400
452
|
|
|
401
|
-
function getMaxPanOffset(history: PricePoint[],
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
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);
|
|
405
456
|
}
|
|
406
457
|
|
|
407
458
|
function applyZoomAroundAnchor(
|
|
@@ -409,25 +460,20 @@ function applyZoomAroundAnchor(
|
|
|
409
460
|
nextZoomLevel: number,
|
|
410
461
|
anchorRatio: number,
|
|
411
462
|
history: PricePoint[],
|
|
412
|
-
chartWidth: number,
|
|
413
463
|
): ChartViewState {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const nextStart = Math.round(anchorIndex - ratio * Math.max(nextVisibleCount - 1, 0));
|
|
424
|
-
const clampedStart = clamp(nextStart, 0, Math.max(filtered.length - nextVisibleCount, 0));
|
|
425
|
-
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
|
+
);
|
|
426
473
|
|
|
427
474
|
return {
|
|
428
|
-
...view,
|
|
429
|
-
|
|
430
|
-
panOffset: clamp(nextPanOffset, 0, Math.max(filtered.length - nextVisibleCount, 0)),
|
|
475
|
+
...clearActivePreset(view),
|
|
476
|
+
...nextZoom,
|
|
431
477
|
};
|
|
432
478
|
}
|
|
433
479
|
|
|
@@ -505,22 +551,46 @@ function resolveSelectionCursor(
|
|
|
505
551
|
};
|
|
506
552
|
}
|
|
507
553
|
|
|
508
|
-
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({
|
|
560
|
+
width,
|
|
561
|
+
height,
|
|
562
|
+
focused,
|
|
563
|
+
interactive,
|
|
564
|
+
compact,
|
|
565
|
+
axisMode = "price",
|
|
566
|
+
historyOverride = null,
|
|
567
|
+
currencyOverride = null,
|
|
568
|
+
symbol,
|
|
569
|
+
ticker,
|
|
570
|
+
financials,
|
|
571
|
+
}: ResolvedStockChartProps) {
|
|
509
572
|
const renderer = useRenderer();
|
|
510
|
-
const
|
|
573
|
+
const dispatch = useAppDispatch();
|
|
574
|
+
const config = useAppSelector((state) => state.config);
|
|
511
575
|
const paneId = usePaneInstanceId();
|
|
512
|
-
const { ticker, financials } = usePaneTicker();
|
|
513
576
|
const nativeSurfaceManager = useMemo(() => getNativeSurfaceManager(renderer), [renderer]);
|
|
514
|
-
const defaultRenderMode =
|
|
515
|
-
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");
|
|
516
582
|
const [viewState, setViewState] = useState<ChartViewState>({
|
|
517
|
-
|
|
583
|
+
presetRange: compact ? "1Y" : storedRangePreset,
|
|
584
|
+
bufferRange: compact ? "1Y" : storedRangePreset,
|
|
585
|
+
activePreset: compact ? null : storedRangePreset,
|
|
586
|
+
resolution: compact ? "auto" : storedResolution,
|
|
518
587
|
panOffset: 0,
|
|
519
588
|
zoomLevel: 1,
|
|
520
589
|
cursorX: null,
|
|
521
590
|
cursorY: null,
|
|
522
591
|
renderMode: defaultRenderMode,
|
|
523
592
|
});
|
|
593
|
+
const [resolutionSupport, setResolutionSupport] = useState<ChartResolutionSupport[] | null>(null);
|
|
524
594
|
const [showVolume, setShowVolume] = useState(!compact);
|
|
525
595
|
const [kittySupport, setKittySupport] = useState<boolean | null>(() => getCachedKittySupport(renderer));
|
|
526
596
|
const [displayCursor, setDisplayCursor] = useState<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
|
|
@@ -535,6 +605,9 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
535
605
|
const targetCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
|
|
536
606
|
const cursorMotionKindRef = useRef<ChartCursorMotionKind>("discrete");
|
|
537
607
|
const animationFrameRef = useRef<number | null>(null);
|
|
608
|
+
const pendingCanonicalResetRef = useRef(1);
|
|
609
|
+
const appliedCanonicalResetRef = useRef(0);
|
|
610
|
+
const pendingExpansionRef = useRef<PendingExpansionAction>(null);
|
|
538
611
|
|
|
539
612
|
const commitDisplayCursor = (next: DisplayCursorState) => {
|
|
540
613
|
displayCursorRef.current = next;
|
|
@@ -620,16 +693,107 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
620
693
|
() => instrumentFromTicker(ticker, ticker?.metadata.ticker ?? null),
|
|
621
694
|
[ticker],
|
|
622
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
|
+
|
|
623
786
|
const baseChartEntry = useChartQuery(
|
|
624
787
|
!compact && instrumentRef
|
|
625
788
|
? {
|
|
626
789
|
instrument: instrumentRef,
|
|
627
|
-
|
|
628
|
-
granularity: "
|
|
790
|
+
bufferRange: viewState.bufferRange,
|
|
791
|
+
granularity: effectiveResolution === "auto" ? "range" : "resolution",
|
|
792
|
+
resolution: effectiveResolution === "auto" ? undefined : effectiveResolution,
|
|
629
793
|
}
|
|
630
794
|
: null,
|
|
631
795
|
);
|
|
632
|
-
const
|
|
796
|
+
const baseBodyState = resolveChartBodyState(baseChartEntry, (value) => Array.isArray(value) && value.length > 0, "No price history available.");
|
|
633
797
|
const axisWidth = compact
|
|
634
798
|
? axisMode === "percent" ? 11 : 8
|
|
635
799
|
: axisMode === "percent" ? 11 : 10;
|
|
@@ -637,11 +801,11 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
637
801
|
const chartWidth = Math.max(width - axisWidth - axisGap, compact ? 12 : 20);
|
|
638
802
|
const maxCursorX = chartWidth - 1;
|
|
639
803
|
const panStep = Math.max(Math.floor(chartWidth / 10), 1);
|
|
640
|
-
const baseHistory =
|
|
641
|
-
?
|
|
642
|
-
: (
|
|
804
|
+
const baseHistory = compact
|
|
805
|
+
? (historyOverride ?? financials?.priceHistory ?? [])
|
|
806
|
+
: (baseBodyState.data ?? []);
|
|
643
807
|
const detailRequest = useMemo(() => {
|
|
644
|
-
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;
|
|
645
809
|
const window = getVisibleWindow(baseHistory, viewState, chartWidth);
|
|
646
810
|
if (window.points.length < 2) return null;
|
|
647
811
|
|
|
@@ -654,17 +818,70 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
654
818
|
|
|
655
819
|
return {
|
|
656
820
|
instrument: instrumentRef,
|
|
657
|
-
|
|
821
|
+
bufferRange: viewState.bufferRange,
|
|
658
822
|
granularity: "detail" as const,
|
|
659
823
|
startDate,
|
|
660
824
|
endDate,
|
|
661
825
|
barSize,
|
|
662
826
|
};
|
|
663
|
-
}, [baseHistory, chartWidth, compact, instrumentRef, viewState]);
|
|
827
|
+
}, [baseHistory, chartWidth, compact, effectiveResolution, instrumentRef, viewState]);
|
|
664
828
|
const detailChartEntry = useChartQuery(detailRequest);
|
|
665
|
-
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;
|
|
666
841
|
|
|
667
|
-
|
|
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]);
|
|
668
885
|
|
|
669
886
|
useEffect(() => {
|
|
670
887
|
if (interactive) {
|
|
@@ -705,9 +922,9 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
705
922
|
const persistDefaultRenderMode = (nextMode: ChartRenderMode) => {
|
|
706
923
|
if (nextMode === defaultRenderMode) return;
|
|
707
924
|
const nextConfig = {
|
|
708
|
-
...
|
|
925
|
+
...config,
|
|
709
926
|
chartPreferences: {
|
|
710
|
-
...
|
|
927
|
+
...config.chartPreferences,
|
|
711
928
|
defaultRenderMode: nextMode,
|
|
712
929
|
},
|
|
713
930
|
};
|
|
@@ -715,16 +932,43 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
715
932
|
saveConfig(nextConfig).catch(() => {});
|
|
716
933
|
};
|
|
717
934
|
|
|
718
|
-
const
|
|
719
|
-
|
|
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;
|
|
720
943
|
setViewState((current) => ({
|
|
721
|
-
...current,
|
|
722
|
-
|
|
723
|
-
panOffset: 0,
|
|
724
|
-
zoomLevel: 1,
|
|
944
|
+
...clearActivePreset(current),
|
|
945
|
+
bufferRange: nextBufferRange,
|
|
725
946
|
cursorX: null,
|
|
726
947
|
cursorY: null,
|
|
727
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);
|
|
728
972
|
};
|
|
729
973
|
|
|
730
974
|
const setRenderMode = (mode: ChartRenderMode) => {
|
|
@@ -732,12 +976,12 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
732
976
|
setViewState((current) => ({ ...current, renderMode: mode }));
|
|
733
977
|
};
|
|
734
978
|
|
|
735
|
-
const headerRows = compact ? 0 :
|
|
979
|
+
const headerRows = compact ? 0 : 4;
|
|
736
980
|
const helpRow = compact ? 0 : 1;
|
|
737
981
|
const timeAxisRow = 1;
|
|
738
982
|
const volumeHeight = showVolume && !compact ? 3 : 0;
|
|
739
983
|
const chartHeight = Math.max(height - headerRows - helpRow - timeAxisRow, 4);
|
|
740
|
-
const isDetailView = viewState.zoomLevel > 1 &&
|
|
984
|
+
const isDetailView = effectiveResolution === "auto" && viewState.zoomLevel > 1 && !!detailBodyState.data?.length;
|
|
741
985
|
const historyRenderKey = history.length === 0
|
|
742
986
|
? "empty"
|
|
743
987
|
: [
|
|
@@ -751,11 +995,18 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
751
995
|
queueMicrotask(() => renderer.requestRender());
|
|
752
996
|
}, [chartHeight, chartWidth, compact, historyRenderKey, renderer, ticker?.metadata.ticker, viewState.renderMode]);
|
|
753
997
|
|
|
754
|
-
const chartWindow = useMemo(() =>
|
|
755
|
-
|
|
998
|
+
const chartWindow = useMemo(() => {
|
|
999
|
+
if (historyOverride) {
|
|
1000
|
+
return { points: history, startIdx: 0, endIdx: history.length };
|
|
1001
|
+
}
|
|
1002
|
+
return isDetailView
|
|
756
1003
|
? { points: history, startIdx: 0, endIdx: history.length }
|
|
757
|
-
: getVisibleWindow(history, viewState, chartWidth)
|
|
758
|
-
|
|
1004
|
+
: getVisibleWindow(history, viewState, chartWidth);
|
|
1005
|
+
}, [chartWidth, history, historyOverride, isDetailView, viewState.panOffset, viewState.zoomLevel]);
|
|
1006
|
+
const timeAxisDates = useMemo(
|
|
1007
|
+
() => chartWindow.points.map((point) => point.date),
|
|
1008
|
+
[chartWindow.points],
|
|
1009
|
+
);
|
|
759
1010
|
|
|
760
1011
|
const projection = useMemo(() => (
|
|
761
1012
|
projectChartData(chartWindow.points, chartWidth, viewState.renderMode, !!compact)
|
|
@@ -766,23 +1017,56 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
766
1017
|
|
|
767
1018
|
switch (event.name) {
|
|
768
1019
|
case "=":
|
|
769
|
-
setViewState((current) => applyZoomAroundAnchor(
|
|
1020
|
+
setViewState((current) => applyZoomAroundAnchor(
|
|
1021
|
+
current,
|
|
1022
|
+
current.zoomLevel * 1.5,
|
|
1023
|
+
RIGHT_EDGE_ANCHOR_RATIO,
|
|
1024
|
+
baseHistory,
|
|
1025
|
+
));
|
|
770
1026
|
return;
|
|
771
1027
|
case "-":
|
|
772
|
-
|
|
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
|
+
));
|
|
773
1041
|
return;
|
|
774
1042
|
case "0":
|
|
1043
|
+
pendingCanonicalResetRef.current += 1;
|
|
775
1044
|
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
776
|
-
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
|
+
});
|
|
777
1055
|
return;
|
|
778
1056
|
case "v":
|
|
779
1057
|
setShowVolume((value) => !value);
|
|
780
1058
|
return;
|
|
781
1059
|
case "a":
|
|
782
|
-
|
|
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 }));
|
|
783
1067
|
return;
|
|
784
1068
|
case "d":
|
|
785
|
-
setViewState((current) => ({ ...current, panOffset: Math.max(current.panOffset - panStep, 0) }));
|
|
1069
|
+
setViewState((current) => ({ ...clearActivePreset(current), panOffset: Math.max(current.panOffset - panStep, 0) }));
|
|
786
1070
|
return;
|
|
787
1071
|
case "m":
|
|
788
1072
|
setViewState((current) => {
|
|
@@ -793,6 +1077,12 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
793
1077
|
return { ...current, renderMode: nextMode };
|
|
794
1078
|
});
|
|
795
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
|
+
}
|
|
796
1086
|
}
|
|
797
1087
|
|
|
798
1088
|
if (event.name >= "1" && event.name <= "7") {
|
|
@@ -806,7 +1096,13 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
806
1096
|
switch (event.name) {
|
|
807
1097
|
case "left":
|
|
808
1098
|
if (event.shift) {
|
|
809
|
-
|
|
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 }));
|
|
810
1106
|
} else {
|
|
811
1107
|
cursorMotionKindRef.current = "discrete";
|
|
812
1108
|
setViewState((current) => {
|
|
@@ -826,10 +1122,10 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
826
1122
|
chartWidth,
|
|
827
1123
|
projection.effectiveMode,
|
|
828
1124
|
);
|
|
829
|
-
const maxPan = getMaxPanOffset(baseHistory, current.
|
|
1125
|
+
const maxPan = getMaxPanOffset(baseHistory, current.zoomLevel);
|
|
830
1126
|
if (currentIndex <= 0) {
|
|
831
1127
|
return {
|
|
832
|
-
...current,
|
|
1128
|
+
...clearActivePreset(current),
|
|
833
1129
|
cursorX: nextCursor,
|
|
834
1130
|
cursorY: null,
|
|
835
1131
|
panOffset: clamp(current.panOffset + 1, 0, maxPan),
|
|
@@ -841,7 +1137,7 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
841
1137
|
return;
|
|
842
1138
|
case "right":
|
|
843
1139
|
if (event.shift) {
|
|
844
|
-
setViewState((current) => ({ ...current, panOffset: Math.max(current.panOffset - panStep, 0) }));
|
|
1140
|
+
setViewState((current) => ({ ...clearActivePreset(current), panOffset: Math.max(current.panOffset - panStep, 0) }));
|
|
845
1141
|
} else {
|
|
846
1142
|
cursorMotionKindRef.current = "discrete";
|
|
847
1143
|
setViewState((current) => {
|
|
@@ -863,7 +1159,7 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
863
1159
|
);
|
|
864
1160
|
if (currentIndex >= pointCount - 1) {
|
|
865
1161
|
return {
|
|
866
|
-
...current,
|
|
1162
|
+
...clearActivePreset(current),
|
|
867
1163
|
cursorX: nextCursor,
|
|
868
1164
|
cursorY: null,
|
|
869
1165
|
panOffset: Math.max(current.panOffset - 1, 0),
|
|
@@ -891,7 +1187,7 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
891
1187
|
negative: colors.negative,
|
|
892
1188
|
}, trend);
|
|
893
1189
|
}, [chartWindow.points]);
|
|
894
|
-
const chartCurrency = financials?.quote?.currency ?? ticker?.metadata.currency ?? "USD";
|
|
1190
|
+
const chartCurrency = currencyOverride ?? financials?.quote?.currency ?? ticker?.metadata.currency ?? "USD";
|
|
895
1191
|
|
|
896
1192
|
const cursorX = viewState.cursorX !== null ? clamp(viewState.cursorX, 0, chartWidth - 1) : null;
|
|
897
1193
|
const cursorY = viewState.cursorY !== null ? clamp(viewState.cursorY, 0, chartHeight - 1) : null;
|
|
@@ -918,7 +1214,8 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
918
1214
|
mode: projection.effectiveMode,
|
|
919
1215
|
axisMode,
|
|
920
1216
|
colors: chartColors,
|
|
921
|
-
|
|
1217
|
+
timeAxisDates,
|
|
1218
|
+
}), [axisMode, chartColors, chartHeight, chartWidth, compact, projection.effectiveMode, projection.points, selectionCursorX, selectionCursorY, showVolume, timeAxisDates, volumeHeight]);
|
|
922
1219
|
const snappedSelectionCursorX = selectionScene
|
|
923
1220
|
? getPointTerminalColumn(selectionScene.activeIdx, projection.points.length, chartWidth, projection.effectiveMode)
|
|
924
1221
|
: null;
|
|
@@ -962,7 +1259,8 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
962
1259
|
mode: projection.effectiveMode,
|
|
963
1260
|
axisMode,
|
|
964
1261
|
colors: chartColors,
|
|
965
|
-
|
|
1262
|
+
timeAxisDates,
|
|
1263
|
+
}), [axisMode, chartColors, chartHeight, chartWidth, compact, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
|
|
966
1264
|
|
|
967
1265
|
const rendererState = resolveChartRendererState(preferredRenderer, kittySupport, renderer.resolution);
|
|
968
1266
|
const effectiveRenderer: ResolvedChartRenderer = rendererState.renderer;
|
|
@@ -978,7 +1276,8 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
978
1276
|
axisMode,
|
|
979
1277
|
currency: chartCurrency,
|
|
980
1278
|
colors: chartColors,
|
|
981
|
-
|
|
1279
|
+
timeAxisDates,
|
|
1280
|
+
}), [axisMode, chartColors, chartCurrency, chartHeight, chartWidth, compact, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
|
|
982
1281
|
|
|
983
1282
|
const interactiveResult = useMemo(() => (
|
|
984
1283
|
effectiveRenderer === "kitty"
|
|
@@ -994,8 +1293,9 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
994
1293
|
axisMode,
|
|
995
1294
|
currency: chartCurrency,
|
|
996
1295
|
colors: chartColors,
|
|
1296
|
+
timeAxisDates,
|
|
997
1297
|
})
|
|
998
|
-
), [axisMode, chartColors, chartCurrency, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, effectiveRenderer, projection.effectiveMode, projection.points, showVolume, volumeHeight]);
|
|
1298
|
+
), [axisMode, chartColors, chartCurrency, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, effectiveRenderer, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
|
|
999
1299
|
|
|
1000
1300
|
const result = effectiveRenderer === "kitty" ? staticResult : interactiveResult!;
|
|
1001
1301
|
|
|
@@ -1243,10 +1543,7 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
1243
1543
|
rendererState.nativeReady,
|
|
1244
1544
|
]);
|
|
1245
1545
|
|
|
1246
|
-
|
|
1247
|
-
return <text fg={colors.textDim}>No price history available.</text>;
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1546
|
+
const hasHistory = history.length > 0;
|
|
1250
1547
|
const firstPrice = chartWindow.points[0]?.close ?? 0;
|
|
1251
1548
|
const lastPrice = chartWindow.points[chartWindow.points.length - 1]?.close ?? 0;
|
|
1252
1549
|
const change = lastPrice - firstPrice;
|
|
@@ -1255,9 +1552,21 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
1255
1552
|
const showOhlcSummary = projection.effectiveMode === "candles" || projection.effectiveMode === "ohlc";
|
|
1256
1553
|
const hasSelectionCursor = selectionCursorX !== null;
|
|
1257
1554
|
const hasDisplayCursor = displayCursorX !== null && displayCursorY !== null;
|
|
1258
|
-
const displayPrice =
|
|
1259
|
-
|
|
1260
|
-
|
|
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;
|
|
1261
1570
|
const displayDate = hasSelectionCursor || showOhlcSummary
|
|
1262
1571
|
? (selectionScene?.dateAtCursor ? formatDateShort(selectionScene.dateAtCursor) : null)
|
|
1263
1572
|
: null;
|
|
@@ -1266,6 +1575,15 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
1266
1575
|
const cursorAxisLabel = hasDisplayCursor && cursorRow !== null && crosshairPrice !== null
|
|
1267
1576
|
? formatAxisValue(crosshairPrice, axisMode, projection.points[0]?.close ?? 0, chartCurrency)
|
|
1268
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));
|
|
1269
1587
|
|
|
1270
1588
|
const handlePlotMove = (event: ChartMouseEvent) => {
|
|
1271
1589
|
if (!interactive || compact) return;
|
|
@@ -1329,16 +1647,15 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
1329
1647
|
}
|
|
1330
1648
|
if (!dragRef.current) return;
|
|
1331
1649
|
|
|
1332
|
-
const
|
|
1333
|
-
const visibleCount = getVisiblePointCount(filtered.length, viewState.zoomLevel);
|
|
1650
|
+
const visibleCount = getVisiblePointCount(baseHistory.length, viewState.zoomLevel);
|
|
1334
1651
|
const deltaCells = getGlobalMouseX(event, renderer) - dragRef.current.startGlobalX;
|
|
1335
1652
|
const pointDelta = Math.round((deltaCells / Math.max(chartWidth, 1)) * visibleCount);
|
|
1336
1653
|
const nextPan = clamp(
|
|
1337
1654
|
dragRef.current.startPanOffset - pointDelta,
|
|
1338
1655
|
0,
|
|
1339
|
-
Math.max(
|
|
1656
|
+
Math.max(baseHistory.length - visibleCount, 0),
|
|
1340
1657
|
);
|
|
1341
|
-
setViewState((current) => ({ ...current, panOffset: nextPan }));
|
|
1658
|
+
setViewState((current) => ({ ...clearActivePreset(current), panOffset: nextPan }));
|
|
1342
1659
|
};
|
|
1343
1660
|
|
|
1344
1661
|
const handlePlotScroll = (event: ChartMouseEvent) => {
|
|
@@ -1367,39 +1684,59 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
1367
1684
|
|
|
1368
1685
|
if (event.modifiers.shift && (direction === "up" || direction === "down")) {
|
|
1369
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
|
+
}
|
|
1370
1694
|
const nextPan = clamp(
|
|
1371
|
-
|
|
1695
|
+
targetPanOffset,
|
|
1372
1696
|
0,
|
|
1373
|
-
getMaxPanOffset(baseHistory, viewState.
|
|
1697
|
+
getMaxPanOffset(baseHistory, viewState.zoomLevel),
|
|
1374
1698
|
);
|
|
1375
|
-
setViewState((current) => ({ ...current, panOffset: nextPan }));
|
|
1699
|
+
setViewState((current) => ({ ...clearActivePreset(current), panOffset: nextPan }));
|
|
1376
1700
|
return;
|
|
1377
1701
|
}
|
|
1378
1702
|
|
|
1379
1703
|
switch (direction) {
|
|
1380
1704
|
case "up":
|
|
1381
|
-
setViewState((current) => applyZoomAroundAnchor(current, current.zoomLevel * 1.18, anchorRatio, baseHistory
|
|
1705
|
+
setViewState((current) => applyZoomAroundAnchor(current, current.zoomLevel * 1.18, anchorRatio, baseHistory));
|
|
1382
1706
|
return;
|
|
1383
1707
|
case "down":
|
|
1384
|
-
|
|
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));
|
|
1385
1716
|
return;
|
|
1386
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
|
+
}
|
|
1387
1724
|
setViewState((current) => ({
|
|
1388
|
-
...current,
|
|
1725
|
+
...clearActivePreset(current),
|
|
1389
1726
|
panOffset: clamp(
|
|
1390
1727
|
current.panOffset + Math.max(Math.round(chartWidth * 0.08), 1),
|
|
1391
1728
|
0,
|
|
1392
|
-
getMaxPanOffset(baseHistory, current.
|
|
1729
|
+
getMaxPanOffset(baseHistory, current.zoomLevel),
|
|
1393
1730
|
),
|
|
1394
1731
|
}));
|
|
1395
1732
|
return;
|
|
1396
1733
|
case "right":
|
|
1397
1734
|
setViewState((current) => ({
|
|
1398
|
-
...current,
|
|
1735
|
+
...clearActivePreset(current),
|
|
1399
1736
|
panOffset: clamp(
|
|
1400
1737
|
current.panOffset - Math.max(Math.round(chartWidth * 0.08), 1),
|
|
1401
1738
|
0,
|
|
1402
|
-
getMaxPanOffset(baseHistory, current.
|
|
1739
|
+
getMaxPanOffset(baseHistory, current.zoomLevel),
|
|
1403
1740
|
),
|
|
1404
1741
|
}));
|
|
1405
1742
|
return;
|
|
@@ -1420,17 +1757,29 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
1420
1757
|
height={chartHeight}
|
|
1421
1758
|
flexDirection="column"
|
|
1422
1759
|
backgroundColor={chartColors.bgColor}
|
|
1423
|
-
onMouseMove={compact ? undefined : handlePlotMove}
|
|
1424
|
-
onMouseDown={compact ? undefined : handlePlotDown}
|
|
1425
|
-
onMouseUp={compact ? undefined : () => { dragRef.current = null; }}
|
|
1426
|
-
onMouseDrag={compact ? undefined : handlePlotDrag}
|
|
1427
|
-
onMouseDragEnd={compact ? undefined : () => { dragRef.current = null; }}
|
|
1428
|
-
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 : () => {
|
|
1429
1766
|
dragRef.current = null;
|
|
1430
1767
|
}}
|
|
1431
|
-
onMouseScroll={compact ? undefined : handlePlotScroll}
|
|
1768
|
+
onMouseScroll={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotScroll}
|
|
1432
1769
|
>
|
|
1433
|
-
{
|
|
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}
|
|
1434
1783
|
</box>
|
|
1435
1784
|
);
|
|
1436
1785
|
|
|
@@ -1466,13 +1815,16 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
1466
1815
|
<box flexDirection="column" flexGrow={1}>
|
|
1467
1816
|
<box flexDirection="row" gap={2} height={1}>
|
|
1468
1817
|
<text attributes={TextAttributes.BOLD} fg={colors.textBright}>
|
|
1469
|
-
{ticker?.metadata.ticker ?? ""} - {
|
|
1818
|
+
{ticker?.metadata.ticker ?? ""} - {getChartResolutionLabel(effectiveResolution)}
|
|
1470
1819
|
</text>
|
|
1471
|
-
<text fg={
|
|
1472
|
-
|
|
1820
|
+
<text fg={colors.textDim}>{visibleLabel}</text>
|
|
1821
|
+
<text fg={displayChange !== null ? priceColor(displayChange) : colors.textDim}>
|
|
1822
|
+
{displayPrice !== null ? formatCurrency(displayPrice, chartCurrency) : "—"}
|
|
1473
1823
|
</text>
|
|
1474
|
-
<text fg={priceColor(displayChange)}>
|
|
1475
|
-
{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"}
|
|
1476
1828
|
</text>
|
|
1477
1829
|
{displayDate && <text fg={colors.textDim}>{displayDate}</text>}
|
|
1478
1830
|
{showOhlcSummary && activePoint && (
|
|
@@ -1491,15 +1843,35 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
1491
1843
|
{TIME_RANGES.map((range, index) => (
|
|
1492
1844
|
<text
|
|
1493
1845
|
key={range}
|
|
1494
|
-
fg={
|
|
1495
|
-
attributes={
|
|
1496
|
-
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
|
+
}}
|
|
1497
1851
|
>
|
|
1498
1852
|
{`${index + 1}:${range}`}
|
|
1499
1853
|
</text>
|
|
1500
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
|
+
))}
|
|
1501
1870
|
{viewState.zoomLevel !== 1 && (
|
|
1502
|
-
<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>
|
|
1503
1875
|
)}
|
|
1504
1876
|
</box>
|
|
1505
1877
|
<box flexGrow={1} />
|
|
@@ -1547,10 +1919,10 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
1547
1919
|
<box height={1}>
|
|
1548
1920
|
<text fg={colors.textMuted}>
|
|
1549
1921
|
{interactive
|
|
1550
|
-
? "mouse hover inspect drag pan wheel zoom ⇧wheel pan ←→ cursor ⇧←→ pan m mode 1-7
|
|
1551
|
-
: "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"}
|
|
1552
1924
|
</text>
|
|
1553
1925
|
</box>
|
|
1554
1926
|
</box>
|
|
1555
1927
|
);
|
|
1556
|
-
}
|
|
1928
|
+
});
|