gloomberb 0.7.3 → 0.7.5
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/package.json +1 -1
- package/src/app.tsx +59 -30
- package/src/components/chart/chart-renderer.ts +109 -32
- package/src/components/chart/chart-types.ts +18 -0
- package/src/components/chart/chart.test.ts +152 -5
- package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
- package/src/components/chart/comparison-chart-renderer.ts +50 -11
- package/src/components/chart/comparison-stock-chart.tsx +45 -8
- package/src/components/chart/market-session.ts +187 -0
- package/src/components/chart/native/chart-rasterizer.ts +65 -2
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +96 -0
- package/src/components/chart/stock-chart.tsx +36 -8
- package/src/components/chart/time-axis-label.tsx +56 -2
- package/src/components/command-bar/command-bar.test.tsx +1 -1
- package/src/components/command-bar/command-bar.tsx +26 -2
- package/src/components/command-bar/list-model.ts +8 -4
- package/src/components/command-bar/view-model.test.ts +20 -0
- package/src/components/command-bar/view-model.ts +15 -3
- package/src/components/layout/shell.test.tsx +26 -0
- package/src/components/layout/shell.tsx +69 -21
- package/src/components/ticker-list-table.tsx +4 -15
- package/src/core/state/session-persistence.ts +25 -0
- package/src/market-data/coordinator-subscriptions.test.ts +102 -18
- package/src/market-data/coordinator.test.ts +122 -2
- package/src/market-data/coordinator.ts +192 -55
- package/src/market-data/hooks.test.tsx +31 -0
- package/src/market-data/hooks.tsx +1 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
- package/src/plugins/builtin/chat-controller.ts +8 -7
- package/src/plugins/builtin/chat.test.tsx +55 -0
- package/src/plugins/builtin/chat.tsx +20 -6
- package/src/plugins/builtin/holders/index.test.ts +30 -1
- package/src/plugins/builtin/holders/index.tsx +7 -3
- package/src/plugins/builtin/market-movers/index.tsx +72 -40
- package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
- package/src/plugins/builtin/news-wire/index.tsx +0 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
- package/src/plugins/builtin/portfolio-list/pane.tsx +26 -9
- package/src/plugins/builtin/portfolio-list.test.tsx +29 -7
- package/src/plugins/builtin/sectors/index.tsx +30 -9
- package/src/plugins/builtin/ticker-detail/pane.tsx +18 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -1
- package/src/plugins/builtin/world-indices/index.tsx +58 -19
- package/src/plugins/pane-manager.test.ts +31 -0
- package/src/plugins/pane-manager.ts +32 -6
- package/src/renderers/electrobun/bun/index.ts +83 -3
- package/src/renderers/electrobun/shared/protocol.ts +8 -1
- package/src/renderers/electrobun/view/backend-rpc.ts +5 -0
- package/src/renderers/electrobun/view/fatal-screen.tsx +118 -0
- package/src/renderers/electrobun/view/main.tsx +38 -24
- package/src/renderers/electrobun/view/styles.css +48 -4
- package/src/sources/gloomberb-cloud.test.ts +55 -2
- package/src/sources/gloomberb-cloud.ts +180 -29
- package/src/sources/provider-router.test.ts +567 -10
- package/src/sources/provider-router.ts +323 -93
- package/src/sources/yahoo-finance.test.ts +37 -0
- package/src/sources/yahoo-finance.ts +48 -2
- package/src/state/app-bootstrap.test.ts +50 -23
- package/src/state/app-bootstrap.ts +26 -10
- package/src/state/session-persistence.test.ts +5 -3
- package/src/state/use-inline-tickers.ts +3 -0
- package/src/state/use-quote-streaming.ts +11 -0
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +1 -0
- package/src/utils/api-client.test.ts +120 -0
- package/src/utils/api-client.ts +66 -19
- package/src/utils/exchanges.test.ts +16 -0
- package/src/utils/exchanges.ts +33 -0
- package/src/utils/options.ts +1 -1
- package/src/utils/price-history.test.ts +28 -1
- package/src/utils/price-history.ts +16 -0
- package/src/utils/quote-freshness.test.ts +150 -0
- package/src/utils/quote-freshness.ts +42 -1
- package/src/utils/quote-resolution.ts +2 -0
- package/src/utils/quote-time.test.ts +34 -0
- package/src/utils/quote-time.ts +18 -5
- package/src/utils/ticker-search.ts +72 -1
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/digest-store.ts +0 -44
|
@@ -7,8 +7,9 @@ import {
|
|
|
7
7
|
} from "./chart-data";
|
|
8
8
|
import { stepCursorTowards } from "./cursor-motion";
|
|
9
9
|
import { buildChartScene, buildCursorTimeAxisSegments, buildTimeAxis, formatAxisValue, formatCursorAxisValue, renderChart, resolveChartAxisWidth, resolveChartPalette } from "./chart-renderer";
|
|
10
|
-
import { buildCursorTimeAxisOverlay } from "./time-axis-label";
|
|
10
|
+
import { buildCursorTimeAxisOverlay, resolveCursorDateFromAxis } from "./time-axis-label";
|
|
11
11
|
import { buildCursorPriceAxisOverlay } from "./price-axis-labels";
|
|
12
|
+
import { resolveChartMarketSession, resolveExtendedHoursBackgroundSpans } from "./market-session";
|
|
12
13
|
import type { PricePoint } from "../../types/financials";
|
|
13
14
|
import type { ChartRenderMode } from "./chart-types";
|
|
14
15
|
|
|
@@ -280,9 +281,19 @@ describe("renderChart", () => {
|
|
|
280
281
|
expect(result.axisLabels.every((entry) => entry.label.includes("."))).toBe(true);
|
|
281
282
|
});
|
|
282
283
|
|
|
283
|
-
test("
|
|
284
|
+
test("keeps indicator overlays out of the chart price range", () => {
|
|
284
285
|
const projection = projectChartData(chartFixture, 12, "line", false);
|
|
285
|
-
const
|
|
286
|
+
const baseScene = buildChartScene(projection.points, {
|
|
287
|
+
width: 12,
|
|
288
|
+
height: 6,
|
|
289
|
+
showVolume: false,
|
|
290
|
+
volumeHeight: 0,
|
|
291
|
+
cursorX: null,
|
|
292
|
+
cursorY: null,
|
|
293
|
+
mode: projection.effectiveMode,
|
|
294
|
+
colors: palette,
|
|
295
|
+
});
|
|
296
|
+
const sceneWithIndicators = buildChartScene(projection.points, {
|
|
286
297
|
width: 12,
|
|
287
298
|
height: 6,
|
|
288
299
|
showVolume: false,
|
|
@@ -300,6 +311,63 @@ describe("renderChart", () => {
|
|
|
300
311
|
{ index: 1, value: 18 },
|
|
301
312
|
],
|
|
302
313
|
}],
|
|
314
|
+
emaLines: [{
|
|
315
|
+
period: 2,
|
|
316
|
+
color: "#00ffff",
|
|
317
|
+
points: [
|
|
318
|
+
{ index: 0, value: 4 },
|
|
319
|
+
{ index: 1, value: 20 },
|
|
320
|
+
],
|
|
321
|
+
}],
|
|
322
|
+
bollinger: {
|
|
323
|
+
color: "#ffaa00",
|
|
324
|
+
upper: [
|
|
325
|
+
{ index: 0, value: 22 },
|
|
326
|
+
{ index: 1, value: 23 },
|
|
327
|
+
],
|
|
328
|
+
middle: [
|
|
329
|
+
{ index: 0, value: 14 },
|
|
330
|
+
{ index: 1, value: 15 },
|
|
331
|
+
],
|
|
332
|
+
lower: [
|
|
333
|
+
{ index: 0, value: 3 },
|
|
334
|
+
{ index: 1, value: 4 },
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
rsi: null,
|
|
338
|
+
macd: null,
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(sceneWithIndicators?.min).toBe(baseScene?.min);
|
|
343
|
+
expect(sceneWithIndicators?.max).toBe(baseScene?.max);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("draws indicator overlays below price pixels", () => {
|
|
347
|
+
const points: PricePoint[] = [
|
|
348
|
+
{ date: new Date("2024-01-02"), close: 10, volume: 100 },
|
|
349
|
+
{ date: new Date("2024-01-03"), close: 11, volume: 120 },
|
|
350
|
+
{ date: new Date("2024-01-04"), close: 12, volume: 140 },
|
|
351
|
+
];
|
|
352
|
+
const result = renderChart(points, {
|
|
353
|
+
width: 12,
|
|
354
|
+
height: 6,
|
|
355
|
+
showVolume: false,
|
|
356
|
+
volumeHeight: 0,
|
|
357
|
+
cursorX: null,
|
|
358
|
+
cursorY: null,
|
|
359
|
+
mode: "line",
|
|
360
|
+
colors: palette,
|
|
361
|
+
indicators: {
|
|
362
|
+
smaLines: [{
|
|
363
|
+
period: 2,
|
|
364
|
+
color: "#ff00ff",
|
|
365
|
+
points: [
|
|
366
|
+
{ index: 0, value: 10 },
|
|
367
|
+
{ index: 1, value: 11 },
|
|
368
|
+
{ index: 2, value: 12 },
|
|
369
|
+
],
|
|
370
|
+
}],
|
|
303
371
|
emaLines: [],
|
|
304
372
|
bollinger: null,
|
|
305
373
|
rsi: null,
|
|
@@ -307,8 +375,9 @@ describe("renderChart", () => {
|
|
|
307
375
|
},
|
|
308
376
|
});
|
|
309
377
|
|
|
310
|
-
|
|
311
|
-
expect(
|
|
378
|
+
const pixels = result.pixelBuffer?.pixels.flat().filter(Boolean) ?? [];
|
|
379
|
+
expect(pixels.some((pixel) => pixel?.color === palette.lineColor)).toBe(true);
|
|
380
|
+
expect(pixels.some((pixel) => pixel?.color === "#ff00ff")).toBe(false);
|
|
312
381
|
});
|
|
313
382
|
|
|
314
383
|
test("normalizes fractional web dimensions before drawing volume bars", () => {
|
|
@@ -434,6 +503,31 @@ describe("renderChart", () => {
|
|
|
434
503
|
expect(segments.find((segment) => segment.highlighted)?.text).toBe("12:30");
|
|
435
504
|
});
|
|
436
505
|
|
|
506
|
+
test("keeps day precision for cursor labels when the axis coarsens to months", () => {
|
|
507
|
+
const dates = Array.from({ length: 12 }, (_, index) => new Date(2025, index, 1));
|
|
508
|
+
const width = 48;
|
|
509
|
+
const segments = buildCursorTimeAxisSegments({
|
|
510
|
+
timeLabels: buildTimeAxis(dates, width),
|
|
511
|
+
width,
|
|
512
|
+
cursorColumn: 27,
|
|
513
|
+
cursorDate: new Date(2025, 8, 15),
|
|
514
|
+
dates,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
expect(segments.find((segment) => segment.highlighted)?.text).toBe("Sep 15 2025");
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("resolves cursor dates from the full source axis between coarse month ticks", () => {
|
|
521
|
+
const dates = Array.from({ length: 32 }, (_, index) => new Date(2025, 2, index + 1));
|
|
522
|
+
const resolved = resolveCursorDateFromAxis({
|
|
523
|
+
dates,
|
|
524
|
+
width: 32,
|
|
525
|
+
cursorColumn: 15,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
expect(resolved?.toISOString()).toBe(dates[15]!.toISOString());
|
|
529
|
+
});
|
|
530
|
+
|
|
437
531
|
test("positions desktop cursor x-axis overlay from exact pixels", () => {
|
|
438
532
|
const dates = Array.from({ length: 13 }, (_, index) => new Date(2026, 0, 5, 9, 30 + index * 30));
|
|
439
533
|
const width = 72;
|
|
@@ -499,6 +593,59 @@ describe("renderChart", () => {
|
|
|
499
593
|
expect(buildTimeAxis(yearlyDates, 48)).toBe("2020 2021 2022 2023 2024 2025");
|
|
500
594
|
});
|
|
501
595
|
|
|
596
|
+
test("marks US premarket and postmarket bars for chart backgrounds", () => {
|
|
597
|
+
const session = resolveChartMarketSession([{ exchange: "NASDAQ", currency: "USD", assetCategory: "STK" }]);
|
|
598
|
+
const dates = [
|
|
599
|
+
new Date("2026-05-14T11:00:00Z"),
|
|
600
|
+
new Date("2026-05-14T14:00:00Z"),
|
|
601
|
+
new Date("2026-05-14T20:30:00Z"),
|
|
602
|
+
];
|
|
603
|
+
|
|
604
|
+
expect(resolveExtendedHoursBackgroundSpans(dates, session)).toEqual([
|
|
605
|
+
{ kind: "pre", startIndex: 0, endIndex: 0 },
|
|
606
|
+
{ kind: "post", startIndex: 2, endIndex: 2 },
|
|
607
|
+
]);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("omits extended-hours backgrounds for wide intraday windows", () => {
|
|
611
|
+
const session = resolveChartMarketSession([{ exchange: "NASDAQ", currency: "USD", assetCategory: "STK" }]);
|
|
612
|
+
const dates = Array.from({ length: 12 }).flatMap((_, dayIndex) => {
|
|
613
|
+
const day = 4 + dayIndex;
|
|
614
|
+
return [
|
|
615
|
+
new Date(Date.UTC(2026, 4, day, 11, 0)),
|
|
616
|
+
new Date(Date.UTC(2026, 4, day, 14, 0)),
|
|
617
|
+
new Date(Date.UTC(2026, 4, day, 20, 30)),
|
|
618
|
+
];
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
expect(resolveExtendedHoursBackgroundSpans(dates, session)).toEqual([]);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test("paints extended-hours chart cells as background colors", () => {
|
|
625
|
+
const session = resolveChartMarketSession([{ exchange: "NASDAQ", currency: "USD", assetCategory: "STK" }]);
|
|
626
|
+
const points: PricePoint[] = [
|
|
627
|
+
{ date: new Date("2026-05-14T11:00:00Z"), close: 10, volume: 100 },
|
|
628
|
+
{ date: new Date("2026-05-14T14:00:00Z"), close: 12, volume: 120 },
|
|
629
|
+
{ date: new Date("2026-05-14T20:30:00Z"), close: 11, volume: 110 },
|
|
630
|
+
];
|
|
631
|
+
|
|
632
|
+
const result = renderChart(points, {
|
|
633
|
+
width: 12,
|
|
634
|
+
height: 4,
|
|
635
|
+
showVolume: false,
|
|
636
|
+
volumeHeight: 0,
|
|
637
|
+
cursorX: null,
|
|
638
|
+
cursorY: null,
|
|
639
|
+
mode: "line",
|
|
640
|
+
colors: palette,
|
|
641
|
+
marketSession: session,
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const backgrounds = result.lines.flatMap((line) => line.chunks.map((chunk) => chunk.bg));
|
|
645
|
+
expect(backgrounds).toContain(palette.preMarketBgColor);
|
|
646
|
+
expect(backgrounds).toContain(palette.postMarketBgColor);
|
|
647
|
+
});
|
|
648
|
+
|
|
502
649
|
test("uses source time-axis dates instead of projected extrema dates when provided", () => {
|
|
503
650
|
const sourceHistory = Array.from({ length: 96 }, (_, index) => ({
|
|
504
651
|
date: new Date(Date.UTC(2026, 2, 25, index * 3)),
|
|
@@ -8,6 +8,14 @@ import {
|
|
|
8
8
|
} from "./comparison-chart-renderer";
|
|
9
9
|
import type { ComparisonChartSeries } from "./chart-types";
|
|
10
10
|
|
|
11
|
+
const comparisonColors = {
|
|
12
|
+
bgColor: "#000000",
|
|
13
|
+
gridColor: "#333333",
|
|
14
|
+
crosshairColor: "#ffffff",
|
|
15
|
+
preMarketBgColor: "#001b44",
|
|
16
|
+
postMarketBgColor: "#442300",
|
|
17
|
+
};
|
|
18
|
+
|
|
11
19
|
function makeSeries(symbol: string, color: string, closes: number[]): ComparisonChartSeries {
|
|
12
20
|
return {
|
|
13
21
|
symbol,
|
|
@@ -51,11 +59,7 @@ describe("renderComparisonChart", () => {
|
|
|
51
59
|
cursorX: null,
|
|
52
60
|
cursorY: null,
|
|
53
61
|
selectedSymbol: "AAPL",
|
|
54
|
-
colors:
|
|
55
|
-
bgColor: "#000000",
|
|
56
|
-
gridColor: "#333333",
|
|
57
|
-
crosshairColor: "#ffffff",
|
|
58
|
-
},
|
|
62
|
+
colors: comparisonColors,
|
|
59
63
|
});
|
|
60
64
|
|
|
61
65
|
expect(result.axisFractionDigits).toBeGreaterThanOrEqual(1);
|
|
@@ -78,11 +82,7 @@ describe("renderComparisonChart", () => {
|
|
|
78
82
|
cursorX: null,
|
|
79
83
|
cursorY: null,
|
|
80
84
|
selectedSymbol: "AAPL",
|
|
81
|
-
colors:
|
|
82
|
-
bgColor: "#000000",
|
|
83
|
-
gridColor: "#333333",
|
|
84
|
-
crosshairColor: "#ffffff",
|
|
85
|
-
},
|
|
85
|
+
colors: comparisonColors,
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
expect(result.axisFractionDigits).toBe(1);
|
|
@@ -105,11 +105,7 @@ describe("renderComparisonChart", () => {
|
|
|
105
105
|
cursorX: 5,
|
|
106
106
|
cursorY: null,
|
|
107
107
|
selectedSymbol: "MSFT",
|
|
108
|
-
colors:
|
|
109
|
-
bgColor: "#000000",
|
|
110
|
-
gridColor: "#333333",
|
|
111
|
-
crosshairColor: "#ffffff",
|
|
112
|
-
},
|
|
108
|
+
colors: comparisonColors,
|
|
113
109
|
});
|
|
114
110
|
|
|
115
111
|
expect(result.lines).toHaveLength(6);
|
|
@@ -134,11 +130,7 @@ describe("renderComparisonChart", () => {
|
|
|
134
130
|
cursorX: null,
|
|
135
131
|
cursorY: null,
|
|
136
132
|
selectedSymbol: "AAPL",
|
|
137
|
-
colors:
|
|
138
|
-
bgColor: "#000000",
|
|
139
|
-
gridColor: "#333333",
|
|
140
|
-
crosshairColor: "#ffffff",
|
|
141
|
-
},
|
|
133
|
+
colors: comparisonColors,
|
|
142
134
|
});
|
|
143
135
|
|
|
144
136
|
expect(result.lines.length).toBeGreaterThan(0);
|
|
@@ -160,11 +152,7 @@ describe("renderComparisonChart", () => {
|
|
|
160
152
|
cursorX: null,
|
|
161
153
|
cursorY: null,
|
|
162
154
|
selectedSymbol: "AAPL",
|
|
163
|
-
colors:
|
|
164
|
-
bgColor: "#000000",
|
|
165
|
-
gridColor: "#333333",
|
|
166
|
-
crosshairColor: "#ffffff",
|
|
167
|
-
},
|
|
155
|
+
colors: comparisonColors,
|
|
168
156
|
});
|
|
169
157
|
|
|
170
158
|
const chunks = result.lines.flatMap((line) => line.chunks);
|
|
@@ -188,11 +176,7 @@ describe("renderComparisonChart", () => {
|
|
|
188
176
|
cursorX: 9,
|
|
189
177
|
cursorY: null,
|
|
190
178
|
selectedSymbol: "NVDA",
|
|
191
|
-
colors:
|
|
192
|
-
bgColor: "#000000",
|
|
193
|
-
gridColor: "#333333",
|
|
194
|
-
crosshairColor: "#ffffff",
|
|
195
|
-
},
|
|
179
|
+
colors: comparisonColors,
|
|
196
180
|
});
|
|
197
181
|
|
|
198
182
|
expect(scene).not.toBeNull();
|
|
@@ -15,10 +15,12 @@ import {
|
|
|
15
15
|
resolveAxisFractionDigits,
|
|
16
16
|
type StyledContent,
|
|
17
17
|
} from "./chart-renderer";
|
|
18
|
-
import type { ChartAxisMode, ComparisonChartRenderMode } from "./chart-types";
|
|
18
|
+
import type { ChartAxisMode, ChartColors, ChartMarketSession, ChartSessionBackgroundSpan, ComparisonChartRenderMode } from "./chart-types";
|
|
19
|
+
import { resolveExtendedHoursBackgroundSpans } from "./market-session";
|
|
19
20
|
|
|
20
21
|
const LAYER_FILL = 1;
|
|
21
22
|
const LAYER_DATA = 2;
|
|
23
|
+
type ComparisonChartColors = Pick<ChartColors, "bgColor" | "gridColor" | "crosshairColor" | "preMarketBgColor" | "postMarketBgColor">;
|
|
22
24
|
|
|
23
25
|
export interface ComparisonChartScene {
|
|
24
26
|
dates: Date[];
|
|
@@ -29,11 +31,8 @@ export interface ComparisonChartScene {
|
|
|
29
31
|
mode: ComparisonChartRenderMode;
|
|
30
32
|
axisMode: ChartAxisMode;
|
|
31
33
|
selectedSymbol: string | null;
|
|
32
|
-
colors:
|
|
33
|
-
|
|
34
|
-
gridColor: string;
|
|
35
|
-
crosshairColor: string;
|
|
36
|
-
};
|
|
34
|
+
colors: ComparisonChartColors;
|
|
35
|
+
sessionBackgroundSpans: ChartSessionBackgroundSpan[];
|
|
37
36
|
min: number;
|
|
38
37
|
max: number;
|
|
39
38
|
activeIdx: number;
|
|
@@ -55,11 +54,8 @@ export interface RenderComparisonChartOptions {
|
|
|
55
54
|
cursorX: number | null;
|
|
56
55
|
cursorY: number | null;
|
|
57
56
|
selectedSymbol: string | null;
|
|
58
|
-
colors:
|
|
59
|
-
|
|
60
|
-
gridColor: string;
|
|
61
|
-
crosshairColor: string;
|
|
62
|
-
};
|
|
57
|
+
colors: ComparisonChartColors;
|
|
58
|
+
marketSession?: ChartMarketSession | null;
|
|
63
59
|
}
|
|
64
60
|
|
|
65
61
|
export interface RenderComparisonChartResult {
|
|
@@ -90,6 +86,16 @@ function getComparisonDotX(index: number, count: number, width: number): number
|
|
|
90
86
|
return Math.round((index / (count - 1)) * Math.max(width - 1, 0));
|
|
91
87
|
}
|
|
92
88
|
|
|
89
|
+
function getComparisonPointBand(index: number, pointCount: number, width: number): { left: number; right: number } {
|
|
90
|
+
const currentX = getComparisonDotX(index, pointCount, width);
|
|
91
|
+
const previousX = index > 0 ? getComparisonDotX(index - 1, pointCount, width) : currentX;
|
|
92
|
+
const nextX = index < pointCount - 1 ? getComparisonDotX(index + 1, pointCount, width) : currentX;
|
|
93
|
+
return {
|
|
94
|
+
left: index === 0 ? 0 : Math.floor((previousX + currentX) / 2) + 1,
|
|
95
|
+
right: index === pointCount - 1 ? Math.max(width - 1, 0) : Math.floor((currentX + nextX) / 2),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
function getScaledY(value: number, min: number, max: number, chartTop: number, chartBottom: number): number {
|
|
94
100
|
const range = max - min || 1;
|
|
95
101
|
const chartHeight = chartBottom - chartTop;
|
|
@@ -223,6 +229,29 @@ function drawComparisonAreaSeries(
|
|
|
223
229
|
}
|
|
224
230
|
}
|
|
225
231
|
|
|
232
|
+
function drawSessionBackgrounds(
|
|
233
|
+
buf: ReturnType<typeof createPixelBuffer>,
|
|
234
|
+
spans: readonly ChartSessionBackgroundSpan[],
|
|
235
|
+
pointCount: number,
|
|
236
|
+
yTop: number,
|
|
237
|
+
yBottom: number,
|
|
238
|
+
colors: ComparisonChartColors,
|
|
239
|
+
) {
|
|
240
|
+
if (spans.length === 0 || pointCount === 0) return;
|
|
241
|
+
for (const span of spans) {
|
|
242
|
+
const startIndex = Math.max(Math.min(span.startIndex, pointCount - 1), 0);
|
|
243
|
+
const endIndex = Math.max(Math.min(span.endIndex, pointCount - 1), startIndex);
|
|
244
|
+
const startBand = getComparisonPointBand(startIndex, pointCount, buf.width);
|
|
245
|
+
const endBand = getComparisonPointBand(endIndex, pointCount, buf.width);
|
|
246
|
+
const color = span.kind === "pre" ? colors.preMarketBgColor : colors.postMarketBgColor;
|
|
247
|
+
for (let y = Math.max(yTop, 0); y <= Math.min(yBottom, Math.max(buf.height - 1, 0)); y += 1) {
|
|
248
|
+
for (let x = Math.max(startBand.left, 0); x <= Math.min(endBand.right, Math.max(buf.width - 1, 0)); x += 1) {
|
|
249
|
+
buf.backgrounds[y]![x] = color;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
226
255
|
function getActiveIndex(pointCount: number, width: number, cursorX: number | null): number {
|
|
227
256
|
if (pointCount <= 0) return 0;
|
|
228
257
|
if (cursorX === null || cursorX < 0 || cursorX >= width) {
|
|
@@ -294,6 +323,7 @@ export function buildComparisonChartScene(
|
|
|
294
323
|
const crosshairValue = cursorY === null
|
|
295
324
|
? null
|
|
296
325
|
: max - (cursorY / Math.max(chartRows - 1, 1)) * range;
|
|
326
|
+
const sessionBackgroundSpans = resolveExtendedHoursBackgroundSpans(projection.dates, opts.marketSession);
|
|
297
327
|
|
|
298
328
|
return {
|
|
299
329
|
dates: projection.dates,
|
|
@@ -305,6 +335,7 @@ export function buildComparisonChartScene(
|
|
|
305
335
|
axisMode: projection.effectiveAxisMode,
|
|
306
336
|
selectedSymbol: selectedSeries?.symbol ?? null,
|
|
307
337
|
colors: opts.colors,
|
|
338
|
+
sessionBackgroundSpans,
|
|
308
339
|
min,
|
|
309
340
|
max,
|
|
310
341
|
activeIdx,
|
|
@@ -354,6 +385,14 @@ export function renderComparisonChart(
|
|
|
354
385
|
getComparisonAxisFractionDigitFloor(priceRange),
|
|
355
386
|
)
|
|
356
387
|
: null;
|
|
388
|
+
drawSessionBackgrounds(
|
|
389
|
+
buf,
|
|
390
|
+
scene.sessionBackgroundSpans,
|
|
391
|
+
scene.dates.length,
|
|
392
|
+
0,
|
|
393
|
+
chartDotBottom,
|
|
394
|
+
scene.colors,
|
|
395
|
+
);
|
|
357
396
|
drawGridLines(buf, gridLines.map((line) => line.y), scene.colors.gridColor);
|
|
358
397
|
|
|
359
398
|
const selectedSeries = getSelectedSeries(scene.series, scene.selectedSymbol);
|
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
import { resolveChartAxisWidth, type StyledContent } from "./chart-renderer";
|
|
99
99
|
import { TimeAxisLabel } from "./time-axis-label";
|
|
100
100
|
import { PriceAxisLabels } from "./price-axis-labels";
|
|
101
|
+
import { getChartMarketSessionKey, resolveChartMarketSession } from "./market-session";
|
|
101
102
|
|
|
102
103
|
const MODE_CHIPS: Record<ComparisonChartRenderMode, string> = {
|
|
103
104
|
area: "A",
|
|
@@ -392,6 +393,7 @@ function buildComparisonNativeBitmapKey(
|
|
|
392
393
|
pixelWidth: number,
|
|
393
394
|
pixelHeight: number,
|
|
394
395
|
paletteKey: string,
|
|
396
|
+
marketSessionKey: string,
|
|
395
397
|
): string {
|
|
396
398
|
const fingerprint = projection.series
|
|
397
399
|
.map((series) => [
|
|
@@ -412,6 +414,7 @@ function buildComparisonNativeBitmapKey(
|
|
|
412
414
|
pixelWidth,
|
|
413
415
|
pixelHeight,
|
|
414
416
|
paletteKey,
|
|
417
|
+
marketSessionKey,
|
|
415
418
|
fingerprint,
|
|
416
419
|
].join("::");
|
|
417
420
|
}
|
|
@@ -598,7 +601,16 @@ function ComparisonStockChartView({
|
|
|
598
601
|
bgColor: colors.bg,
|
|
599
602
|
gridColor: blendHex(colors.bg, colors.border, 0.55),
|
|
600
603
|
crosshairColor: colors.borderFocused,
|
|
604
|
+
preMarketBgColor: blendHex(colors.bg, "#1d4ed8", 0.18),
|
|
605
|
+
postMarketBgColor: blendHex(colors.bg, "#b45309", 0.18),
|
|
601
606
|
}), [colors.bg, colors.border, colors.borderFocused]);
|
|
607
|
+
const marketSession = useMemo(() => resolveChartMarketSession(symbolSources.map((source) => ({
|
|
608
|
+
exchange: source.exchange,
|
|
609
|
+
primaryExchange: source.instrument?.primaryExchange,
|
|
610
|
+
currency: source.currency,
|
|
611
|
+
assetCategory: source.instrument?.secType,
|
|
612
|
+
}))), [symbolSources]);
|
|
613
|
+
const marketSessionKey = useMemo(() => getChartMarketSessionKey(marketSession), [marketSession]);
|
|
602
614
|
|
|
603
615
|
const setSelectedSymbol = (symbol: string) => {
|
|
604
616
|
setViewState((current) => (
|
|
@@ -1088,7 +1100,8 @@ function ComparisonStockChartView({
|
|
|
1088
1100
|
cursorY: null,
|
|
1089
1101
|
selectedSymbol: viewState.selectedSymbol,
|
|
1090
1102
|
colors: chartColors,
|
|
1091
|
-
|
|
1103
|
+
marketSession,
|
|
1104
|
+
}), [chartColors, chartHeight, chartWidth, marketSession, projection, viewState.selectedSymbol]);
|
|
1092
1105
|
const displayScene = useMemo(() => buildComparisonChartScene(projection, {
|
|
1093
1106
|
width: chartWidth,
|
|
1094
1107
|
height: chartHeight,
|
|
@@ -1096,7 +1109,8 @@ function ComparisonStockChartView({
|
|
|
1096
1109
|
cursorY: displayCursorY,
|
|
1097
1110
|
selectedSymbol: viewState.selectedSymbol,
|
|
1098
1111
|
colors: chartColors,
|
|
1099
|
-
|
|
1112
|
+
marketSession,
|
|
1113
|
+
}), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, marketSession, projection, viewState.selectedSymbol]);
|
|
1100
1114
|
|
|
1101
1115
|
const staticResult = useMemo(() => renderComparisonChart(projection, {
|
|
1102
1116
|
width: chartWidth,
|
|
@@ -1105,7 +1119,8 @@ function ComparisonStockChartView({
|
|
|
1105
1119
|
cursorY: null,
|
|
1106
1120
|
selectedSymbol: viewState.selectedSymbol,
|
|
1107
1121
|
colors: chartColors,
|
|
1108
|
-
|
|
1122
|
+
marketSession,
|
|
1123
|
+
}), [chartColors, chartHeight, chartWidth, marketSession, projection, viewState.selectedSymbol]);
|
|
1109
1124
|
|
|
1110
1125
|
const interactiveResult = useMemo(() => (
|
|
1111
1126
|
effectiveRenderer === "kitty" || useCanvasChart
|
|
@@ -1117,8 +1132,9 @@ function ComparisonStockChartView({
|
|
|
1117
1132
|
cursorY: displayCursorY,
|
|
1118
1133
|
selectedSymbol: viewState.selectedSymbol,
|
|
1119
1134
|
colors: chartColors,
|
|
1135
|
+
marketSession,
|
|
1120
1136
|
})
|
|
1121
|
-
), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, effectiveRenderer, projection, useCanvasChart, viewState.selectedSymbol]);
|
|
1137
|
+
), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, effectiveRenderer, marketSession, projection, useCanvasChart, viewState.selectedSymbol]);
|
|
1122
1138
|
|
|
1123
1139
|
const result = effectiveRenderer === "kitty" || useCanvasChart ? staticResult : interactiveResult!;
|
|
1124
1140
|
const timeAxisLabel = result.timeLabels || staticResult.timeLabels;
|
|
@@ -1282,7 +1298,14 @@ function ComparisonStockChartView({
|
|
|
1282
1298
|
viewState.selectedSymbol,
|
|
1283
1299
|
bitmapSize.pixelWidth,
|
|
1284
1300
|
bitmapSize.pixelHeight,
|
|
1285
|
-
[
|
|
1301
|
+
[
|
|
1302
|
+
chartColors.bgColor,
|
|
1303
|
+
chartColors.gridColor,
|
|
1304
|
+
chartColors.crosshairColor,
|
|
1305
|
+
chartColors.preMarketBgColor,
|
|
1306
|
+
chartColors.postMarketBgColor,
|
|
1307
|
+
].join(","),
|
|
1308
|
+
marketSessionKey,
|
|
1286
1309
|
);
|
|
1287
1310
|
const cachedBitmap = lastNativeBaseBitmapRef.current?.key === bitmapKey
|
|
1288
1311
|
? lastNativeBaseBitmapRef.current.bitmap
|
|
@@ -1312,7 +1335,10 @@ function ComparisonStockChartView({
|
|
|
1312
1335
|
chartColors.bgColor,
|
|
1313
1336
|
chartColors.crosshairColor,
|
|
1314
1337
|
chartColors.gridColor,
|
|
1338
|
+
chartColors.postMarketBgColor,
|
|
1339
|
+
chartColors.preMarketBgColor,
|
|
1315
1340
|
effectiveRenderer,
|
|
1341
|
+
marketSessionKey,
|
|
1316
1342
|
nativeSurfaceManager,
|
|
1317
1343
|
paneId,
|
|
1318
1344
|
projection,
|
|
@@ -1685,9 +1711,10 @@ function ComparisonStockChartView({
|
|
|
1685
1711
|
cursorY: null,
|
|
1686
1712
|
selectedSymbol: viewState.selectedSymbol,
|
|
1687
1713
|
colors: chartColors,
|
|
1714
|
+
marketSession,
|
|
1688
1715
|
})
|
|
1689
1716
|
: null
|
|
1690
|
-
), [canvasBitmapSize?.pixelWidth, canvasProjection, chartColors, chartHeight, chartWidth, viewState.selectedSymbol]);
|
|
1717
|
+
), [canvasBitmapSize?.pixelWidth, canvasProjection, chartColors, chartHeight, chartWidth, marketSession, viewState.selectedSymbol]);
|
|
1691
1718
|
|
|
1692
1719
|
const canvasBaseBitmapKey = useMemo(() => {
|
|
1693
1720
|
if (!canvasBitmapSize || !canvasProjection || !hasChartData || isBlockingBody || bodyMessage) return null;
|
|
@@ -1697,7 +1724,14 @@ function ComparisonStockChartView({
|
|
|
1697
1724
|
viewState.selectedSymbol,
|
|
1698
1725
|
canvasBitmapSize.pixelWidth,
|
|
1699
1726
|
canvasBitmapSize.pixelHeight,
|
|
1700
|
-
[
|
|
1727
|
+
[
|
|
1728
|
+
chartColors.bgColor,
|
|
1729
|
+
chartColors.gridColor,
|
|
1730
|
+
chartColors.crosshairColor,
|
|
1731
|
+
chartColors.preMarketBgColor,
|
|
1732
|
+
chartColors.postMarketBgColor,
|
|
1733
|
+
].join(","),
|
|
1734
|
+
marketSessionKey,
|
|
1701
1735
|
);
|
|
1702
1736
|
}, [
|
|
1703
1737
|
bodyMessage,
|
|
@@ -1706,8 +1740,11 @@ function ComparisonStockChartView({
|
|
|
1706
1740
|
chartColors.bgColor,
|
|
1707
1741
|
chartColors.crosshairColor,
|
|
1708
1742
|
chartColors.gridColor,
|
|
1743
|
+
chartColors.postMarketBgColor,
|
|
1744
|
+
chartColors.preMarketBgColor,
|
|
1709
1745
|
hasChartData,
|
|
1710
1746
|
isBlockingBody,
|
|
1747
|
+
marketSessionKey,
|
|
1711
1748
|
symbols.length,
|
|
1712
1749
|
viewState.selectedSymbol,
|
|
1713
1750
|
]);
|
|
@@ -1922,7 +1959,7 @@ function ComparisonStockChartView({
|
|
|
1922
1959
|
cursorColumn={cursorTimeAxisColumn}
|
|
1923
1960
|
cursorPixelX={hasDisplayCursor ? displayCursor.pixelX : null}
|
|
1924
1961
|
cursorDate={cursorTimeAxisDate}
|
|
1925
|
-
dates={
|
|
1962
|
+
dates={visibleWindow.dates}
|
|
1926
1963
|
cursorColor={chartColors.crosshairColor}
|
|
1927
1964
|
/>
|
|
1928
1965
|
</Box>
|