gloomberb 0.3.7 → 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 +19 -8
- 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-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 +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 +45 -1
- package/src/plugins/ibkr/gateway-service.ts +141 -7
- 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 +11 -3
- package/src/plugins/prediction-markets/controller.ts +141 -121
- 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 +119 -57
- package/src/plugins/prediction-markets/pane.tsx +74 -153
- 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 +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/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/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,7 +1,9 @@
|
|
|
1
1
|
import { TextAttributes, type BoxRenderable, type CliRenderer } from "@opentui/core";
|
|
2
2
|
import { useKeyboard, useRenderer } from "@opentui/react";
|
|
3
3
|
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { useChartQueries } from "../../market-data/hooks";
|
|
5
|
+
import { buildChartKey } from "../../market-data/selectors";
|
|
6
|
+
import { useAppState, usePaneSettingValue } from "../../state/app-context";
|
|
5
7
|
import { blendHex, colors, getComparisonSeriesColor, priceColor } from "../../theme/colors";
|
|
6
8
|
import type { BrokerContractRef } from "../../types/instrument";
|
|
7
9
|
import type { PricePoint } from "../../types/financials";
|
|
@@ -13,6 +15,35 @@ import {
|
|
|
13
15
|
getVisibleComparisonWindow,
|
|
14
16
|
projectComparisonChartData,
|
|
15
17
|
} from "./comparison-chart-data";
|
|
18
|
+
import { usePersistChartControlSelection } from "./chart-pane-settings";
|
|
19
|
+
import {
|
|
20
|
+
buildVisibleDateWindow,
|
|
21
|
+
clearActivePreset,
|
|
22
|
+
formatVisibleSpanLabel,
|
|
23
|
+
getCanonicalZoomLevel,
|
|
24
|
+
isCanonicalPresetViewport,
|
|
25
|
+
resolveChartBodyState,
|
|
26
|
+
resolvePresetSelection,
|
|
27
|
+
resolveResolutionSelection,
|
|
28
|
+
} from "./chart-controller";
|
|
29
|
+
import {
|
|
30
|
+
buildChartResolutionSupportMap,
|
|
31
|
+
clampTimeRangeToMaxRange,
|
|
32
|
+
DEFAULT_COMPARISON_CHART_RANGE_PRESET,
|
|
33
|
+
DEFAULT_COMPARISON_CHART_RESOLUTION,
|
|
34
|
+
DEFAULT_VISIBLE_MANUAL_CHART_RESOLUTIONS,
|
|
35
|
+
getChartResolutionLabel,
|
|
36
|
+
getNextBufferRange,
|
|
37
|
+
getPresetResolution,
|
|
38
|
+
getSupportMaxRange,
|
|
39
|
+
isRangePresetSupported,
|
|
40
|
+
intersectChartResolutionSupport,
|
|
41
|
+
normalizeChartResolutionSupport,
|
|
42
|
+
sortChartResolutions,
|
|
43
|
+
type ChartResolutionSupport,
|
|
44
|
+
type ManualChartResolution,
|
|
45
|
+
} from "./chart-resolution";
|
|
46
|
+
import { RIGHT_EDGE_ANCHOR_RATIO } from "./chart-viewport";
|
|
16
47
|
import {
|
|
17
48
|
buildComparisonChartScene,
|
|
18
49
|
formatComparisonAxisValue,
|
|
@@ -29,9 +60,11 @@ import {
|
|
|
29
60
|
TIME_RANGES,
|
|
30
61
|
type ChartAxisMode,
|
|
31
62
|
type ChartRendererPreference,
|
|
63
|
+
type ChartResolution,
|
|
32
64
|
type ComparisonChartRenderMode,
|
|
33
65
|
type ComparisonChartViewState,
|
|
34
66
|
type ResolvedChartRenderer,
|
|
67
|
+
type TimeRange,
|
|
35
68
|
} from "./chart-types";
|
|
36
69
|
import {
|
|
37
70
|
computeBitmapSize,
|
|
@@ -84,6 +117,11 @@ interface DragState {
|
|
|
84
117
|
startPanOffset: number;
|
|
85
118
|
}
|
|
86
119
|
|
|
120
|
+
type PendingExpansionAction =
|
|
121
|
+
| { kind: "zoom-out"; targetVisibleCount: number; anchorRatio: number }
|
|
122
|
+
| { kind: "pan-left"; targetPanOffset: number }
|
|
123
|
+
| null;
|
|
124
|
+
|
|
87
125
|
interface ChartMouseEvent {
|
|
88
126
|
x: number;
|
|
89
127
|
y: number;
|
|
@@ -477,6 +515,22 @@ function resolveSelectionCursor(
|
|
|
477
515
|
};
|
|
478
516
|
}
|
|
479
517
|
|
|
518
|
+
function getUniqueSortedSeriesDates(series: Array<{ points: PricePoint[] }>): Date[] {
|
|
519
|
+
const byTimestamp = new Map<number, Date>();
|
|
520
|
+
for (const entry of series) {
|
|
521
|
+
for (const point of entry.points) {
|
|
522
|
+
const date = point.date instanceof Date ? point.date : new Date(point.date);
|
|
523
|
+
const timestamp = date.getTime();
|
|
524
|
+
if (!Number.isNaN(timestamp)) {
|
|
525
|
+
byTimestamp.set(timestamp, date);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return [...byTimestamp.entries()]
|
|
530
|
+
.sort((left, right) => left[0] - right[0])
|
|
531
|
+
.map(([, date]) => date);
|
|
532
|
+
}
|
|
533
|
+
|
|
480
534
|
function ComparisonStockChartView({
|
|
481
535
|
paneId,
|
|
482
536
|
width,
|
|
@@ -491,8 +545,14 @@ function ComparisonStockChartView({
|
|
|
491
545
|
}: ComparisonStockChartViewProps) {
|
|
492
546
|
const renderer = useRenderer();
|
|
493
547
|
const nativeSurfaceManager = useMemo(() => getNativeSurfaceManager(renderer), [renderer]);
|
|
548
|
+
const [storedRangePreset] = usePaneSettingValue("rangePreset", DEFAULT_COMPARISON_CHART_RANGE_PRESET);
|
|
549
|
+
const [storedResolution] = usePaneSettingValue<ChartResolution>("chartResolution", DEFAULT_COMPARISON_CHART_RESOLUTION);
|
|
550
|
+
const persistChartControls = usePersistChartControlSelection("rangePreset");
|
|
494
551
|
const [viewState, setViewState] = useState<ComparisonChartViewState>({
|
|
495
|
-
|
|
552
|
+
presetRange: storedRangePreset,
|
|
553
|
+
bufferRange: storedRangePreset,
|
|
554
|
+
activePreset: storedRangePreset,
|
|
555
|
+
resolution: storedResolution,
|
|
496
556
|
panOffset: 0,
|
|
497
557
|
zoomLevel: 1,
|
|
498
558
|
cursorX: null,
|
|
@@ -500,10 +560,9 @@ function ComparisonStockChartView({
|
|
|
500
560
|
renderMode: getInitialComparisonMode(defaultRenderMode),
|
|
501
561
|
selectedSymbol: symbols[0] ?? null,
|
|
502
562
|
});
|
|
503
|
-
const [
|
|
563
|
+
const [resolutionSupport, setResolutionSupport] = useState<ChartResolutionSupport[] | null>(null);
|
|
504
564
|
const [kittySupport, setKittySupport] = useState<boolean | null>(() => getCachedKittySupport(renderer));
|
|
505
565
|
const [displayCursor, setDisplayCursor] = useState<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
|
|
506
|
-
const fetchIdRef = useRef(0);
|
|
507
566
|
const plotRef = useRef<BoxRenderable | null>(null);
|
|
508
567
|
const nativeBaseSurfaceIdRef = useRef(`comparison-chart-surface:${paneId}:base`);
|
|
509
568
|
const nativeCrosshairSurfaceIdRef = useRef(`comparison-chart-surface:${paneId}:crosshair`);
|
|
@@ -515,11 +574,14 @@ function ComparisonStockChartView({
|
|
|
515
574
|
const targetCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
|
|
516
575
|
const cursorMotionKindRef = useRef<ChartCursorMotionKind>("discrete");
|
|
517
576
|
const animationFrameRef = useRef<number | null>(null);
|
|
577
|
+
const pendingCanonicalResetRef = useRef(1);
|
|
578
|
+
const appliedCanonicalResetRef = useRef(0);
|
|
579
|
+
const pendingExpansionRef = useRef<PendingExpansionAction>(null);
|
|
518
580
|
|
|
519
581
|
const axisWidth = 11;
|
|
520
582
|
const axisGap = 1;
|
|
521
583
|
const headerRows = 1;
|
|
522
|
-
const controlRows =
|
|
584
|
+
const controlRows = 2;
|
|
523
585
|
const timeAxisRows = 1;
|
|
524
586
|
const helpRows = 1;
|
|
525
587
|
const legendColumns = getLegendColumns(width);
|
|
@@ -545,14 +607,13 @@ function ComparisonStockChartView({
|
|
|
545
607
|
));
|
|
546
608
|
};
|
|
547
609
|
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}).join("|"), [symbolSources]);
|
|
610
|
+
const capabilityKey = useMemo(() => symbolSources.map((source) => [
|
|
611
|
+
source.symbol,
|
|
612
|
+
source.exchange,
|
|
613
|
+
source.brokerId ?? "",
|
|
614
|
+
source.brokerInstanceId ?? "",
|
|
615
|
+
source.instrument?.conId ?? "",
|
|
616
|
+
].join(":")).join("|"), [symbolSources]);
|
|
556
617
|
|
|
557
618
|
useEffect(() => {
|
|
558
619
|
if (symbols.includes(viewState.selectedSymbol ?? "")) return;
|
|
@@ -562,6 +623,19 @@ function ComparisonStockChartView({
|
|
|
562
623
|
}));
|
|
563
624
|
}, [symbols, viewState.selectedSymbol]);
|
|
564
625
|
|
|
626
|
+
useEffect(() => {
|
|
627
|
+
pendingCanonicalResetRef.current += 1;
|
|
628
|
+
setViewState((current) => resolvePresetSelection(
|
|
629
|
+
{
|
|
630
|
+
...current,
|
|
631
|
+
resolution: storedResolution,
|
|
632
|
+
},
|
|
633
|
+
storedRangePreset,
|
|
634
|
+
storedResolution === "auto" ? null : getSupportMaxRange(resolutionSupport ?? [], storedResolution),
|
|
635
|
+
));
|
|
636
|
+
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
637
|
+
}, [resolutionSupport, storedRangePreset, storedResolution]);
|
|
638
|
+
|
|
565
639
|
const commitDisplayCursor = (next: DisplayCursorState) => {
|
|
566
640
|
displayCursorRef.current = next;
|
|
567
641
|
setDisplayCursor((current) => (sameDisplayCursorState(current, next) ? current : next));
|
|
@@ -644,33 +718,145 @@ function ComparisonStockChartView({
|
|
|
644
718
|
|
|
645
719
|
useEffect(() => {
|
|
646
720
|
const provider = getSharedDataProvider();
|
|
647
|
-
if (!provider ||
|
|
648
|
-
|
|
721
|
+
if ((!provider?.getChartResolutionSupport && !provider?.getChartResolutionCapabilities) || symbolSources.length === 0) {
|
|
722
|
+
setResolutionSupport(null);
|
|
649
723
|
return;
|
|
650
724
|
}
|
|
651
725
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
726
|
+
let cancelled = false;
|
|
727
|
+
setResolutionSupport(null);
|
|
655
728
|
Promise.all(symbolSources.map(async (source) => {
|
|
656
729
|
try {
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
730
|
+
const support = provider.getChartResolutionSupport
|
|
731
|
+
? await provider.getChartResolutionSupport(
|
|
732
|
+
source.symbol,
|
|
733
|
+
source.exchange,
|
|
734
|
+
{
|
|
735
|
+
brokerId: source.brokerId,
|
|
736
|
+
brokerInstanceId: source.brokerInstanceId,
|
|
737
|
+
instrument: source.instrument ?? null,
|
|
738
|
+
},
|
|
739
|
+
)
|
|
740
|
+
: normalizeChartResolutionSupport(
|
|
741
|
+
(await provider.getChartResolutionCapabilities?.(
|
|
742
|
+
source.symbol,
|
|
743
|
+
source.exchange,
|
|
744
|
+
{
|
|
745
|
+
brokerId: source.brokerId,
|
|
746
|
+
brokerInstanceId: source.brokerInstanceId,
|
|
747
|
+
instrument: source.instrument ?? null,
|
|
748
|
+
},
|
|
749
|
+
) ?? []).map((resolution) => ({ resolution, maxRange: "ALL" })),
|
|
750
|
+
);
|
|
751
|
+
return support;
|
|
663
752
|
} catch {
|
|
664
|
-
return
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
})).then((supportSets) => {
|
|
756
|
+
if (!cancelled) {
|
|
757
|
+
setResolutionSupport(supportSets.some((support) => support === null) ? null : intersectChartResolutionSupport(supportSets));
|
|
665
758
|
}
|
|
666
|
-
})).then((entries) => {
|
|
667
|
-
if (fetchIdRef.current !== id) return;
|
|
668
|
-
setRemoteHistory(Object.fromEntries(entries));
|
|
669
759
|
}).catch(() => {
|
|
670
|
-
if (
|
|
671
|
-
|
|
760
|
+
if (!cancelled) {
|
|
761
|
+
setResolutionSupport(null);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
return () => {
|
|
766
|
+
cancelled = true;
|
|
767
|
+
};
|
|
768
|
+
}, [capabilityKey, symbolSources]);
|
|
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 = viewState.resolution !== "auto"
|
|
776
|
+
&& resolutionSupport !== null
|
|
777
|
+
&& !supportMap.has(viewState.resolution)
|
|
778
|
+
? "auto"
|
|
779
|
+
: viewState.resolution;
|
|
780
|
+
const resolutionChips = useMemo(
|
|
781
|
+
() => sortChartResolutions(["auto", ...availableManualResolutions] as ChartResolution[]),
|
|
782
|
+
[availableManualResolutions],
|
|
783
|
+
);
|
|
784
|
+
const chartRequests = useMemo(() => (
|
|
785
|
+
symbolSources.map((source) => ({
|
|
786
|
+
instrument: {
|
|
787
|
+
symbol: source.symbol,
|
|
788
|
+
exchange: source.exchange,
|
|
789
|
+
brokerId: source.brokerId,
|
|
790
|
+
brokerInstanceId: source.brokerInstanceId,
|
|
791
|
+
instrument: source.instrument,
|
|
792
|
+
},
|
|
793
|
+
bufferRange: viewState.bufferRange,
|
|
794
|
+
granularity: effectiveResolution === "auto" ? "range" as const : "resolution" as const,
|
|
795
|
+
resolution: effectiveResolution === "auto" ? undefined : effectiveResolution,
|
|
796
|
+
}))
|
|
797
|
+
), [effectiveResolution, symbolSources, viewState.bufferRange]);
|
|
798
|
+
const chartEntries = useChartQueries(chartRequests);
|
|
799
|
+
const entryStates = useMemo(() => chartRequests.map((request) => (
|
|
800
|
+
resolveChartBodyState(chartEntries.get(buildChartKey(request)), (value) => Array.isArray(value) && value.length > 0, "No chart data yet.")
|
|
801
|
+
)), [chartEntries, chartRequests]);
|
|
802
|
+
const hasSeriesData = entryStates.some((state) => !!state.data?.length);
|
|
803
|
+
const isBlockingBody = entryStates.some((state) => state.blocking) || (chartRequests.length > 0 && entryStates.length !== chartRequests.length);
|
|
804
|
+
const bodyMessage = hasSeriesData
|
|
805
|
+
? null
|
|
806
|
+
: entryStates.find((state) => state.errorMessage)?.errorMessage
|
|
807
|
+
?? entryStates.find((state) => state.emptyMessage)?.emptyMessage
|
|
808
|
+
?? null;
|
|
809
|
+
const isUpdating = !isBlockingBody && entryStates.some((state) => state.updating);
|
|
810
|
+
const series = useMemo(() => symbolSources.map((source, index) => {
|
|
811
|
+
const request = chartRequests[index];
|
|
812
|
+
const history = request ? (chartEntries.get(buildChartKey(request))?.data ?? []) : [];
|
|
813
|
+
const color = getComparisonSeriesColor(index);
|
|
814
|
+
return {
|
|
815
|
+
symbol: source.symbol,
|
|
816
|
+
color,
|
|
817
|
+
fillColor: blendHex(colors.bg, color, 0.22),
|
|
818
|
+
currency: source.currency,
|
|
819
|
+
points: history,
|
|
820
|
+
};
|
|
821
|
+
}), [chartEntries, chartRequests, colors.bg, symbolSources]);
|
|
822
|
+
const seriesDates = useMemo(() => getUniqueSortedSeriesDates(series), [series]);
|
|
823
|
+
const visibleDateWindow = useMemo(() => buildVisibleDateWindow(seriesDates, viewState.panOffset, viewState.zoomLevel), [seriesDates, viewState.panOffset, viewState.zoomLevel]);
|
|
824
|
+
const activePreset = isCanonicalPresetViewport(seriesDates, {
|
|
825
|
+
activePreset: viewState.activePreset,
|
|
826
|
+
panOffset: viewState.panOffset,
|
|
827
|
+
zoomLevel: viewState.zoomLevel,
|
|
828
|
+
resolution: effectiveResolution,
|
|
829
|
+
}) ? viewState.activePreset : null;
|
|
830
|
+
|
|
831
|
+
useEffect(() => {
|
|
832
|
+
if (seriesDates.length === 0) return;
|
|
833
|
+
if (appliedCanonicalResetRef.current < pendingCanonicalResetRef.current) {
|
|
834
|
+
const canonicalZoom = getCanonicalZoomLevel(seriesDates, viewState.presetRange);
|
|
835
|
+
appliedCanonicalResetRef.current = pendingCanonicalResetRef.current;
|
|
836
|
+
setViewState((current) => (
|
|
837
|
+
current.zoomLevel === canonicalZoom && current.panOffset === 0
|
|
838
|
+
? current
|
|
839
|
+
: { ...current, zoomLevel: canonicalZoom, panOffset: 0, cursorX: null, cursorY: null }
|
|
840
|
+
));
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (!pendingExpansionRef.current) return;
|
|
844
|
+
const pendingExpansion = pendingExpansionRef.current;
|
|
845
|
+
pendingExpansionRef.current = null;
|
|
846
|
+
setViewState((current) => {
|
|
847
|
+
if (pendingExpansion.kind === "zoom-out") {
|
|
848
|
+
const nextVisibleCount = Math.min(seriesDates.length, Math.max(pendingExpansion.targetVisibleCount, 1));
|
|
849
|
+
return {
|
|
850
|
+
...current,
|
|
851
|
+
...resolveAnchoredChartZoom(seriesDates.length, 1, 0, seriesDates.length / nextVisibleCount, pendingExpansion.anchorRatio),
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
...current,
|
|
856
|
+
panOffset: clamp(pendingExpansion.targetPanOffset, 0, getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth)),
|
|
857
|
+
};
|
|
672
858
|
});
|
|
673
|
-
}, [
|
|
859
|
+
}, [chartWidth, series, seriesDates, viewState.presetRange]);
|
|
674
860
|
|
|
675
861
|
useEffect(() => {
|
|
676
862
|
const refreshSupport = () => setKittySupport(getCachedKittySupport(renderer));
|
|
@@ -696,19 +882,40 @@ function ComparisonStockChartView({
|
|
|
696
882
|
};
|
|
697
883
|
}, [preferredRenderer, renderer]);
|
|
698
884
|
|
|
699
|
-
const
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
return
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
885
|
+
const expandBufferRange = (action: PendingExpansionAction): boolean => {
|
|
886
|
+
const nextCandidate = getNextBufferRange(viewState.bufferRange);
|
|
887
|
+
const nextBufferRange = effectiveResolution === "auto"
|
|
888
|
+
? nextCandidate
|
|
889
|
+
: clampTimeRangeToMaxRange(nextCandidate, supportMap.get(effectiveResolution) ?? viewState.bufferRange);
|
|
890
|
+
if (nextBufferRange === viewState.bufferRange) return false;
|
|
891
|
+
pendingExpansionRef.current = action;
|
|
892
|
+
setViewState((current) => ({
|
|
893
|
+
...clearActivePreset(current),
|
|
894
|
+
bufferRange: nextBufferRange,
|
|
895
|
+
cursorX: null,
|
|
896
|
+
cursorY: null,
|
|
897
|
+
}));
|
|
898
|
+
return true;
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
const setRangePreset = (range: TimeRange) => {
|
|
902
|
+
if (!isRangePresetSupported(range, effectiveResolutionSupport)) return;
|
|
903
|
+
const supportMaxRange = getSupportMaxRange(effectiveResolutionSupport, getPresetResolution(range));
|
|
904
|
+
persistChartControls(range, getPresetResolution(range));
|
|
905
|
+
pendingCanonicalResetRef.current += 1;
|
|
906
|
+
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
907
|
+
setViewState((current) => resolvePresetSelection(current, range, supportMaxRange));
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
const setResolution = (resolution: ChartResolution) => {
|
|
911
|
+
if (resolution !== "auto" && !availableManualResolutions.includes(resolution)) return;
|
|
912
|
+
const nextState = resolveResolutionSelection(viewState, resolution, supportMap, visibleDateWindow);
|
|
913
|
+
if (!nextState) return;
|
|
914
|
+
pendingCanonicalResetRef.current += 1;
|
|
915
|
+
persistChartControls(nextState.presetRange, resolution);
|
|
916
|
+
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
917
|
+
setViewState(nextState);
|
|
918
|
+
};
|
|
712
919
|
|
|
713
920
|
const visibleWindow = useMemo(() => (
|
|
714
921
|
getVisibleComparisonWindow(series, viewState, chartWidth)
|
|
@@ -1015,15 +1222,33 @@ function ComparisonStockChartView({
|
|
|
1015
1222
|
|
|
1016
1223
|
switch (event.name) {
|
|
1017
1224
|
case "=":
|
|
1018
|
-
setViewState((current) => applyComparisonZoomAroundAnchor(
|
|
1225
|
+
setViewState((current) => applyComparisonZoomAroundAnchor(
|
|
1226
|
+
clearActivePreset(current),
|
|
1227
|
+
current.zoomLevel * 1.5,
|
|
1228
|
+
RIGHT_EDGE_ANCHOR_RATIO,
|
|
1229
|
+
series,
|
|
1230
|
+
));
|
|
1019
1231
|
return;
|
|
1020
1232
|
case "-":
|
|
1021
|
-
|
|
1233
|
+
if (viewState.zoomLevel <= 1.001 && expandBufferRange({
|
|
1234
|
+
kind: "zoom-out",
|
|
1235
|
+
targetVisibleCount: Math.round(seriesDates.length * 1.5),
|
|
1236
|
+
anchorRatio: RIGHT_EDGE_ANCHOR_RATIO,
|
|
1237
|
+
})) {
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
setViewState((current) => applyComparisonZoomAroundAnchor(
|
|
1241
|
+
clearActivePreset(current),
|
|
1242
|
+
current.zoomLevel / 1.5,
|
|
1243
|
+
RIGHT_EDGE_ANCHOR_RATIO,
|
|
1244
|
+
series,
|
|
1245
|
+
));
|
|
1022
1246
|
return;
|
|
1023
1247
|
case "0":
|
|
1248
|
+
pendingCanonicalResetRef.current += 1;
|
|
1024
1249
|
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
1025
1250
|
setViewState((current) => ({
|
|
1026
|
-
...current,
|
|
1251
|
+
...(resolveResolutionSelection(current, effectiveResolution, supportMap, visibleDateWindow) ?? current),
|
|
1027
1252
|
panOffset: 0,
|
|
1028
1253
|
zoomLevel: 1,
|
|
1029
1254
|
cursorX: null,
|
|
@@ -1031,15 +1256,21 @@ function ComparisonStockChartView({
|
|
|
1031
1256
|
}));
|
|
1032
1257
|
return;
|
|
1033
1258
|
case "a":
|
|
1259
|
+
if (viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.presetRange, viewState.zoomLevel, chartWidth) && expandBufferRange({
|
|
1260
|
+
kind: "pan-left",
|
|
1261
|
+
targetPanOffset: viewState.panOffset + Math.max(Math.floor(chartWidth / 10), 1),
|
|
1262
|
+
})) {
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1034
1265
|
setViewState((current) => ({
|
|
1035
|
-
...current,
|
|
1036
|
-
panOffset: clamp(current.panOffset + Math.max(Math.floor(chartWidth / 10), 1), 0, getMaxComparisonPanOffset(series, current.
|
|
1266
|
+
...clearActivePreset(current),
|
|
1267
|
+
panOffset: clamp(current.panOffset + Math.max(Math.floor(chartWidth / 10), 1), 0, getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth)),
|
|
1037
1268
|
}));
|
|
1038
1269
|
return;
|
|
1039
1270
|
case "d":
|
|
1040
1271
|
setViewState((current) => ({
|
|
1041
|
-
...current,
|
|
1042
|
-
panOffset: clamp(current.panOffset - Math.max(Math.floor(chartWidth / 10), 1), 0, getMaxComparisonPanOffset(series, current.
|
|
1272
|
+
...clearActivePreset(current),
|
|
1273
|
+
panOffset: clamp(current.panOffset - Math.max(Math.floor(chartWidth / 10), 1), 0, getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth)),
|
|
1043
1274
|
}));
|
|
1044
1275
|
return;
|
|
1045
1276
|
case "m":
|
|
@@ -1048,6 +1279,12 @@ function ComparisonStockChartView({
|
|
|
1048
1279
|
renderMode: current.renderMode === "line" ? "area" : "line",
|
|
1049
1280
|
}));
|
|
1050
1281
|
return;
|
|
1282
|
+
case "r": {
|
|
1283
|
+
const currentIndex = resolutionChips.indexOf(effectiveResolution);
|
|
1284
|
+
const nextResolution = resolutionChips[(currentIndex + 1) % resolutionChips.length] ?? "auto";
|
|
1285
|
+
setResolution(nextResolution);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1051
1288
|
case "h":
|
|
1052
1289
|
cursorMotionKindRef.current = "discrete";
|
|
1053
1290
|
setViewState((current) => ({
|
|
@@ -1113,15 +1350,7 @@ function ComparisonStockChartView({
|
|
|
1113
1350
|
if (event.name >= "1" && event.name <= "7") {
|
|
1114
1351
|
const index = parseInt(event.name, 10) - 1;
|
|
1115
1352
|
if (index < TIME_RANGES.length) {
|
|
1116
|
-
|
|
1117
|
-
setViewState((current) => ({
|
|
1118
|
-
...current,
|
|
1119
|
-
timeRange: TIME_RANGES[index]!,
|
|
1120
|
-
panOffset: 0,
|
|
1121
|
-
zoomLevel: 1,
|
|
1122
|
-
cursorX: null,
|
|
1123
|
-
cursorY: null,
|
|
1124
|
-
}));
|
|
1353
|
+
setRangePreset(TIME_RANGES[index]!);
|
|
1125
1354
|
}
|
|
1126
1355
|
}
|
|
1127
1356
|
});
|
|
@@ -1192,7 +1421,7 @@ function ComparisonStockChartView({
|
|
|
1192
1421
|
0,
|
|
1193
1422
|
Math.max(visibleWindow.totalDates - visibleWindow.dates.length, 0),
|
|
1194
1423
|
);
|
|
1195
|
-
setViewState((current) => ({ ...current, panOffset: nextPan }));
|
|
1424
|
+
setViewState((current) => ({ ...clearActivePreset(current), panOffset: nextPan }));
|
|
1196
1425
|
};
|
|
1197
1426
|
|
|
1198
1427
|
const handlePlotScroll = (event: ChartMouseEvent) => {
|
|
@@ -1220,39 +1449,59 @@ function ComparisonStockChartView({
|
|
|
1220
1449
|
|
|
1221
1450
|
if (event.modifiers.shift && (direction === "up" || direction === "down")) {
|
|
1222
1451
|
const shiftDirection = direction === "up" ? 1 : -1;
|
|
1452
|
+
const targetPanOffset = viewState.panOffset + shiftDirection * Math.max(Math.round(chartWidth * 0.08), 1);
|
|
1453
|
+
if (shiftDirection > 0 && viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.presetRange, viewState.zoomLevel, chartWidth) && expandBufferRange({
|
|
1454
|
+
kind: "pan-left",
|
|
1455
|
+
targetPanOffset,
|
|
1456
|
+
})) {
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1223
1459
|
const nextPan = clamp(
|
|
1224
|
-
|
|
1460
|
+
targetPanOffset,
|
|
1225
1461
|
0,
|
|
1226
|
-
getMaxComparisonPanOffset(series, viewState.
|
|
1462
|
+
getMaxComparisonPanOffset(series, viewState.presetRange, viewState.zoomLevel, chartWidth),
|
|
1227
1463
|
);
|
|
1228
|
-
setViewState((current) => ({ ...current, panOffset: nextPan }));
|
|
1464
|
+
setViewState((current) => ({ ...clearActivePreset(current), panOffset: nextPan }));
|
|
1229
1465
|
return;
|
|
1230
1466
|
}
|
|
1231
1467
|
|
|
1232
1468
|
switch (direction) {
|
|
1233
1469
|
case "up":
|
|
1234
|
-
setViewState((current) => applyComparisonZoomAroundAnchor(current, current.zoomLevel * 1.18, anchorRatio, series
|
|
1470
|
+
setViewState((current) => applyComparisonZoomAroundAnchor(clearActivePreset(current), current.zoomLevel * 1.18, anchorRatio, series));
|
|
1235
1471
|
return;
|
|
1236
1472
|
case "down":
|
|
1237
|
-
|
|
1473
|
+
if (viewState.zoomLevel <= 1.001 && expandBufferRange({
|
|
1474
|
+
kind: "zoom-out",
|
|
1475
|
+
targetVisibleCount: Math.round(seriesDates.length * 1.18),
|
|
1476
|
+
anchorRatio,
|
|
1477
|
+
})) {
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
setViewState((current) => applyComparisonZoomAroundAnchor(clearActivePreset(current), current.zoomLevel / 1.18, anchorRatio, series));
|
|
1238
1481
|
return;
|
|
1239
1482
|
case "left":
|
|
1483
|
+
if (viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.presetRange, viewState.zoomLevel, chartWidth) && expandBufferRange({
|
|
1484
|
+
kind: "pan-left",
|
|
1485
|
+
targetPanOffset: viewState.panOffset + Math.max(Math.round(chartWidth * 0.08), 1),
|
|
1486
|
+
})) {
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1240
1489
|
setViewState((current) => ({
|
|
1241
|
-
...current,
|
|
1490
|
+
...clearActivePreset(current),
|
|
1242
1491
|
panOffset: clamp(
|
|
1243
1492
|
current.panOffset + Math.max(Math.round(chartWidth * 0.08), 1),
|
|
1244
1493
|
0,
|
|
1245
|
-
getMaxComparisonPanOffset(series, current.
|
|
1494
|
+
getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth),
|
|
1246
1495
|
),
|
|
1247
1496
|
}));
|
|
1248
1497
|
return;
|
|
1249
1498
|
case "right":
|
|
1250
1499
|
setViewState((current) => ({
|
|
1251
|
-
...current,
|
|
1500
|
+
...clearActivePreset(current),
|
|
1252
1501
|
panOffset: clamp(
|
|
1253
1502
|
current.panOffset - Math.max(Math.round(chartWidth * 0.08), 1),
|
|
1254
1503
|
0,
|
|
1255
|
-
getMaxComparisonPanOffset(series, current.
|
|
1504
|
+
getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth),
|
|
1256
1505
|
),
|
|
1257
1506
|
}));
|
|
1258
1507
|
return;
|
|
@@ -1263,12 +1512,9 @@ function ComparisonStockChartView({
|
|
|
1263
1512
|
return <text fg={colors.textDim}>No comparison tickers configured.</text>;
|
|
1264
1513
|
}
|
|
1265
1514
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
const selectedSeries = result.selectedSeries ?? staticResult.selectedSeries;
|
|
1271
|
-
const selectedPoint = result.selectedPoint ?? staticResult.selectedPoint;
|
|
1515
|
+
const hasChartData = series.some((entry) => entry.points.length > 0);
|
|
1516
|
+
const selectedSeries = hasChartData ? (result.selectedSeries ?? staticResult.selectedSeries) : null;
|
|
1517
|
+
const selectedPoint = hasChartData ? (result.selectedPoint ?? staticResult.selectedPoint) : null;
|
|
1272
1518
|
const selectedRawValue = selectedPoint?.rawValue ?? selectedSeries?.latestRawValue ?? null;
|
|
1273
1519
|
const selectedBaseValue = selectedSeries?.baseValue ?? null;
|
|
1274
1520
|
const selectedChange = selectedRawValue !== null && selectedBaseValue !== null
|
|
@@ -1283,6 +1529,10 @@ function ComparisonStockChartView({
|
|
|
1283
1529
|
const cursorAxisLabel = result.cursorRow !== null && result.crosshairValue !== null
|
|
1284
1530
|
? formatComparisonAxisValue(result.crosshairValue, projection.effectiveAxisMode)
|
|
1285
1531
|
: null;
|
|
1532
|
+
const visibleLabel = formatVisibleSpanLabel({
|
|
1533
|
+
start: visibleWindow.dates[0] ?? null,
|
|
1534
|
+
end: visibleWindow.dates[visibleWindow.dates.length - 1] ?? null,
|
|
1535
|
+
});
|
|
1286
1536
|
|
|
1287
1537
|
const plotLines: Array<string | StyledContent> = effectiveRenderer === "kitty"
|
|
1288
1538
|
? blankPlotLines
|
|
@@ -1298,17 +1548,29 @@ function ComparisonStockChartView({
|
|
|
1298
1548
|
height={chartHeight}
|
|
1299
1549
|
flexDirection="column"
|
|
1300
1550
|
backgroundColor={chartColors.bgColor}
|
|
1301
|
-
onMouseMove={handlePlotMove}
|
|
1302
|
-
onMouseDown={handlePlotDown}
|
|
1303
|
-
onMouseUp={() => { dragRef.current = null; }}
|
|
1304
|
-
onMouseDrag={handlePlotDrag}
|
|
1305
|
-
onMouseDragEnd={() => { dragRef.current = null; }}
|
|
1306
|
-
onMouseOut={() => {
|
|
1551
|
+
onMouseMove={hasChartData && !isBlockingBody && !bodyMessage ? handlePlotMove : undefined}
|
|
1552
|
+
onMouseDown={hasChartData && !isBlockingBody && !bodyMessage ? handlePlotDown : undefined}
|
|
1553
|
+
onMouseUp={hasChartData && !isBlockingBody && !bodyMessage ? () => { dragRef.current = null; } : undefined}
|
|
1554
|
+
onMouseDrag={hasChartData && !isBlockingBody && !bodyMessage ? handlePlotDrag : undefined}
|
|
1555
|
+
onMouseDragEnd={hasChartData && !isBlockingBody && !bodyMessage ? () => { dragRef.current = null; } : undefined}
|
|
1556
|
+
onMouseOut={hasChartData && !isBlockingBody && !bodyMessage ? () => {
|
|
1307
1557
|
dragRef.current = null;
|
|
1308
|
-
}}
|
|
1309
|
-
onMouseScroll={handlePlotScroll}
|
|
1558
|
+
} : undefined}
|
|
1559
|
+
onMouseScroll={hasChartData && !isBlockingBody && !bodyMessage ? handlePlotScroll : undefined}
|
|
1310
1560
|
>
|
|
1311
|
-
{
|
|
1561
|
+
{isBlockingBody
|
|
1562
|
+
? (
|
|
1563
|
+
<box flexGrow={1} alignItems="center" justifyContent="center">
|
|
1564
|
+
<text fg={colors.textDim}>Loading chart...</text>
|
|
1565
|
+
</box>
|
|
1566
|
+
)
|
|
1567
|
+
: bodyMessage
|
|
1568
|
+
? (
|
|
1569
|
+
<box flexGrow={1} alignItems="center" justifyContent="center">
|
|
1570
|
+
<text fg={colors.textDim}>{bodyMessage}</text>
|
|
1571
|
+
</box>
|
|
1572
|
+
)
|
|
1573
|
+
: plotContent}
|
|
1312
1574
|
</box>
|
|
1313
1575
|
);
|
|
1314
1576
|
|
|
@@ -1334,8 +1596,9 @@ function ComparisonStockChartView({
|
|
|
1334
1596
|
<box flexDirection="column" flexGrow={1}>
|
|
1335
1597
|
<box flexDirection="row" gap={2} height={1}>
|
|
1336
1598
|
<text attributes={TextAttributes.BOLD} fg={colors.textBright}>
|
|
1337
|
-
{viewState.selectedSymbol ?? "Compare"} - {
|
|
1599
|
+
{viewState.selectedSymbol ?? "Compare"} - {getChartResolutionLabel(effectiveResolution)}
|
|
1338
1600
|
</text>
|
|
1601
|
+
<text fg={colors.textDim}>{visibleLabel}</text>
|
|
1339
1602
|
<text fg={selectedChange !== null ? priceColor(selectedChange) : colors.textDim}>
|
|
1340
1603
|
{selectedRawValue !== null ? formatCurrency(selectedRawValue, selectedCurrency) : "—"}
|
|
1341
1604
|
</text>
|
|
@@ -1352,25 +1615,37 @@ function ComparisonStockChartView({
|
|
|
1352
1615
|
{TIME_RANGES.map((range, index) => (
|
|
1353
1616
|
<text
|
|
1354
1617
|
key={range}
|
|
1355
|
-
fg={
|
|
1356
|
-
attributes={
|
|
1618
|
+
fg={activePreset === range ? colors.textBright : (isRangePresetSupported(range, availableManualResolutions) ? colors.textDim : colors.textMuted)}
|
|
1619
|
+
attributes={activePreset === range ? TextAttributes.BOLD : 0}
|
|
1357
1620
|
onMouseDown={() => {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
timeRange: range,
|
|
1362
|
-
panOffset: 0,
|
|
1363
|
-
zoomLevel: 1,
|
|
1364
|
-
cursorX: null,
|
|
1365
|
-
cursorY: null,
|
|
1366
|
-
}));
|
|
1621
|
+
if (isRangePresetSupported(range, availableManualResolutions)) {
|
|
1622
|
+
setRangePreset(range);
|
|
1623
|
+
}
|
|
1367
1624
|
}}
|
|
1368
1625
|
>
|
|
1369
1626
|
{`${index + 1}:${range}`}
|
|
1370
1627
|
</text>
|
|
1371
1628
|
))}
|
|
1629
|
+
</box>
|
|
1630
|
+
</box>
|
|
1631
|
+
|
|
1632
|
+
<box flexDirection="row" height={1}>
|
|
1633
|
+
<box flexDirection="row" gap={1}>
|
|
1634
|
+
{resolutionChips.map((resolution) => (
|
|
1635
|
+
<text
|
|
1636
|
+
key={resolution}
|
|
1637
|
+
fg={effectiveResolution === resolution ? colors.textBright : colors.textDim}
|
|
1638
|
+
attributes={effectiveResolution === resolution ? TextAttributes.BOLD : 0}
|
|
1639
|
+
onMouseDown={() => setResolution(resolution)}
|
|
1640
|
+
>
|
|
1641
|
+
{getChartResolutionLabel(resolution)}
|
|
1642
|
+
</text>
|
|
1643
|
+
))}
|
|
1372
1644
|
{viewState.zoomLevel !== 1 && (
|
|
1373
|
-
<text fg={colors.textDim}>
|
|
1645
|
+
<text fg={colors.textDim}>zoom:{viewState.zoomLevel.toFixed(1)}x</text>
|
|
1646
|
+
)}
|
|
1647
|
+
{isUpdating && (
|
|
1648
|
+
<text fg={colors.textDim}>updating</text>
|
|
1374
1649
|
)}
|
|
1375
1650
|
</box>
|
|
1376
1651
|
<box flexGrow={1} />
|
|
@@ -1446,7 +1721,7 @@ function ComparisonStockChartView({
|
|
|
1446
1721
|
|
|
1447
1722
|
<box height={1}>
|
|
1448
1723
|
<text fg={colors.textMuted}>
|
|
1449
|
-
mouse hover inspect drag pan wheel zoom ⇧wheel pan h/l cursor arrows legend Enter open 1-7
|
|
1724
|
+
mouse hover inspect drag pan wheel zoom ⇧wheel pan h/l cursor arrows legend Enter open 1-7 presets r res m mode 0 reset
|
|
1450
1725
|
</text>
|
|
1451
1726
|
</box>
|
|
1452
1727
|
</box>
|