gloomberb 0.7.2 → 0.7.4
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 +79 -132
- package/package.json +1 -1
- package/src/app.tsx +73 -67
- package/src/components/chart/chart-data.ts +7 -2
- package/src/components/chart/chart-pane-settings.ts +7 -4
- package/src/components/chart/chart-renderer.ts +158 -47
- package/src/components/chart/chart-types.ts +20 -2
- package/src/components/chart/chart.test.ts +179 -13
- 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 +129 -129
- package/src/components/chart/market-session.ts +187 -0
- package/src/components/chart/native/chart-rasterizer.ts +108 -19
- package/src/components/chart/price-axis-labels.tsx +25 -9
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +97 -1
- package/src/components/chart/stock-chart.tsx +95 -49
- package/src/components/chart/time-axis-label.tsx +56 -2
- package/src/components/command-bar/command-bar.test.tsx +446 -120
- package/src/components/command-bar/command-bar.tsx +535 -384
- package/src/components/command-bar/command-registry.ts +16 -6
- package/src/components/command-bar/list-model.ts +8 -4
- package/src/components/command-bar/root-shortcuts.ts +4 -5
- package/src/components/command-bar/view-model.test.ts +21 -0
- package/src/components/command-bar/view-model.ts +17 -4
- package/src/components/command-bar/workflow-ops.ts +11 -32
- package/src/components/data-table-view.test.tsx +24 -1
- package/src/components/data-table-view.tsx +3 -2
- package/src/components/feed-data-table-stack-view.tsx +3 -2
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/floating-pane.tsx +9 -2
- package/src/components/layout/pane-footer.tsx +1 -1
- package/src/components/layout/pane-header.tsx +4 -2
- package/src/components/layout/pane-sizing.ts +12 -0
- package/src/components/layout/pane.tsx +24 -2
- package/src/components/layout/shell.tsx +24 -10
- package/src/components/layout/status-bar.test.tsx +4 -1
- package/src/components/layout/status-bar.tsx +4 -24
- package/src/components/markdown-text.tsx +53 -8
- package/src/components/onboarding/onboarding-wizard.tsx +7 -6
- package/src/components/pane-settings-dialog.tsx +9 -6
- package/src/components/pane-template-wizard.tsx +3 -2
- package/src/components/price-selector-dialog.tsx +3 -2
- package/src/components/price-sparkline-view.tsx +463 -0
- package/src/components/price-sparkline.ts +46 -0
- package/src/components/table-view-shared.tsx +21 -3
- package/src/components/ticker-list-table-view.tsx +3 -2
- package/src/components/ticker-list-table.tsx +15 -66
- package/src/components/toggle-list.tsx +3 -3
- package/src/components/ui/choice-dialog.tsx +3 -2
- package/src/components/ui/multi-select-dialog.tsx +3 -2
- package/src/components/ui/native-select.tsx +3 -3
- package/src/core/state/app-state.ts +210 -80
- package/src/core/state/session-persistence.ts +25 -0
- package/src/data/config-store-node.ts +113 -25
- package/src/data/config-store.test.ts +65 -3
- package/src/data/plugin-state-store.ts +39 -13
- package/src/data/sqlite-cache.test.ts +13 -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 +200 -56
- package/src/market-data/hooks.test.tsx +31 -0
- package/src/market-data/hooks.tsx +1 -0
- package/src/news/aggregator.test.ts +41 -0
- package/src/news/aggregator.ts +61 -9
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
- package/src/plugins/builtin/ai/settings.ts +3 -0
- package/src/plugins/builtin/broker-manager/index.tsx +4 -3
- package/src/plugins/builtin/chat-controller.test.ts +398 -17
- package/src/plugins/builtin/chat-controller.ts +263 -48
- package/src/plugins/builtin/chat.test.tsx +312 -6
- package/src/plugins/builtin/chat.tsx +245 -66
- package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
- package/src/plugins/builtin/correlation/colors.test.ts +39 -0
- package/src/plugins/builtin/correlation/colors.ts +87 -0
- package/src/plugins/builtin/correlation/index.test.tsx +151 -0
- package/src/plugins/builtin/correlation/index.tsx +92 -21
- package/src/plugins/builtin/debug.tsx +4 -3
- package/src/plugins/builtin/econ/index.tsx +3 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +25 -13
- package/src/plugins/builtin/layout-manager.tsx +1 -7
- 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 +1 -3
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
- package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
- package/src/plugins/builtin/options.tsx +9 -17
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
- package/src/plugins/builtin/portfolio-list/metrics.ts +13 -2
- package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +139 -23
- package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
- package/src/plugins/builtin/portfolio-list.test.tsx +311 -9
- package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
- package/src/plugins/builtin/sectors/index.tsx +275 -43
- package/src/plugins/builtin/sectors/sector-data.ts +41 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +18 -7
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +445 -63
- package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
- package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
- package/src/plugins/builtin/world-indices/index.tsx +58 -19
- package/src/plugins/ibkr/trading-pane.tsx +3 -2
- package/src/plugins/plugin-runtime.tsx +2 -2
- package/src/plugins/prediction-markets/settings.ts +4 -0
- package/src/plugins/registry.test.ts +55 -0
- package/src/plugins/registry.ts +11 -4
- package/src/plugins/ticker-navigation.test.ts +35 -5
- package/src/plugins/ticker-navigation.ts +16 -22
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
- package/src/renderers/electrobun/bun/application-menu.ts +6 -1
- package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
- package/src/renderers/electrobun/bun/index.ts +52 -13
- package/src/renderers/electrobun/view/app-services.ts +51 -9
- package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
- package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
- package/src/renderers/electrobun/view/main.tsx +0 -16
- package/src/renderers/electrobun/view/styles.css +23 -21
- package/src/renderers/electrobun/view/ui-host.tsx +6 -4
- package/src/renderers/opentui/host.tsx +2 -1
- package/src/renderers/opentui/start.tsx +1 -1
- package/src/renderers/opentui/test-utils.tsx +6 -5
- 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 +29 -13
- package/src/state/app-context.test.ts +113 -0
- package/src/state/app-context.tsx +8 -4
- 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/theme/colors.ts +13 -14
- package/src/types/config.ts +9 -4
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +1 -0
- package/src/types/plugin.ts +8 -1
- package/src/ui/context-menu.tsx +4 -4
- package/src/utils/api-client.test.ts +248 -0
- package/src/utils/api-client.ts +178 -26
- package/src/utils/exchanges.test.ts +16 -0
- package/src/utils/exchanges.ts +33 -0
- package/src/utils/fuzzy-search.ts +12 -1
- package/src/utils/keyboard.ts +31 -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.test.ts +2 -2
- package/src/utils/ticker-search.ts +48 -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
|
|
|
@@ -183,6 +184,8 @@ describe("resolveRenderMode", () => {
|
|
|
183
184
|
expect(resolveRenderMode("candles", 28)).toMatchObject({ effectiveMode: "ohlc", fallbackMode: "ohlc" });
|
|
184
185
|
expect(resolveRenderMode("candles", 39)).toMatchObject({ effectiveMode: "ohlc", fallbackMode: "ohlc" });
|
|
185
186
|
expect(resolveRenderMode("candles", 40)).toMatchObject({ effectiveMode: "candles", fallbackMode: null });
|
|
187
|
+
expect(resolveRenderMode("hlc", 27)).toMatchObject({ effectiveMode: "line", fallbackMode: "line" });
|
|
188
|
+
expect(resolveRenderMode("hlc", 28)).toMatchObject({ effectiveMode: "hlc", fallbackMode: null });
|
|
186
189
|
});
|
|
187
190
|
|
|
188
191
|
test("forces compact charts back to area mode", () => {
|
|
@@ -192,7 +195,7 @@ describe("resolveRenderMode", () => {
|
|
|
192
195
|
expect(projection.fallbackMode).toBeNull();
|
|
193
196
|
});
|
|
194
197
|
|
|
195
|
-
test("uses fewer projected buckets for candles and ohlc to preserve spacing", () => {
|
|
198
|
+
test("uses fewer projected buckets for candles and ohlc-style bars to preserve spacing", () => {
|
|
196
199
|
const denseSeries = Array.from({ length: 100 }, (_, i) => ({
|
|
197
200
|
date: new Date(2024, 0, i + 1),
|
|
198
201
|
open: 100 + i,
|
|
@@ -204,6 +207,7 @@ describe("resolveRenderMode", () => {
|
|
|
204
207
|
|
|
205
208
|
expect(projectChartData(denseSeries, 80, "candles", false).points).toHaveLength(34);
|
|
206
209
|
expect(projectChartData(denseSeries, 80, "ohlc", false).points).toHaveLength(34);
|
|
210
|
+
expect(projectChartData(denseSeries, 80, "hlc", false).points).toHaveLength(34);
|
|
207
211
|
expect(projectChartData(denseSeries, 80, "line", false).points).toHaveLength(80);
|
|
208
212
|
});
|
|
209
213
|
|
|
@@ -277,9 +281,19 @@ describe("renderChart", () => {
|
|
|
277
281
|
expect(result.axisLabels.every((entry) => entry.label.includes("."))).toBe(true);
|
|
278
282
|
});
|
|
279
283
|
|
|
280
|
-
test("
|
|
284
|
+
test("keeps indicator overlays out of the chart price range", () => {
|
|
281
285
|
const projection = projectChartData(chartFixture, 12, "line", false);
|
|
282
|
-
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, {
|
|
283
297
|
width: 12,
|
|
284
298
|
height: 6,
|
|
285
299
|
showVolume: false,
|
|
@@ -297,6 +311,63 @@ describe("renderChart", () => {
|
|
|
297
311
|
{ index: 1, value: 18 },
|
|
298
312
|
],
|
|
299
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
|
+
}],
|
|
300
371
|
emaLines: [],
|
|
301
372
|
bollinger: null,
|
|
302
373
|
rsi: null,
|
|
@@ -304,8 +375,9 @@ describe("renderChart", () => {
|
|
|
304
375
|
},
|
|
305
376
|
});
|
|
306
377
|
|
|
307
|
-
|
|
308
|
-
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);
|
|
309
381
|
});
|
|
310
382
|
|
|
311
383
|
test("normalizes fractional web dimensions before drawing volume bars", () => {
|
|
@@ -431,6 +503,31 @@ describe("renderChart", () => {
|
|
|
431
503
|
expect(segments.find((segment) => segment.highlighted)?.text).toBe("12:30");
|
|
432
504
|
});
|
|
433
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
|
+
|
|
434
531
|
test("positions desktop cursor x-axis overlay from exact pixels", () => {
|
|
435
532
|
const dates = Array.from({ length: 13 }, (_, index) => new Date(2026, 0, 5, 9, 30 + index * 30));
|
|
436
533
|
const width = 72;
|
|
@@ -468,7 +565,7 @@ describe("renderChart", () => {
|
|
|
468
565
|
cellHeightPx,
|
|
469
566
|
});
|
|
470
567
|
|
|
471
|
-
expect(overlay.labelText).toBe("$214.03
|
|
568
|
+
expect(overlay.labelText).toBe("$214.03");
|
|
472
569
|
expect(overlay.topPercent).toBeCloseTo((cursorPixelY / (height * cellHeightPx - 1)) * 100, 5);
|
|
473
570
|
});
|
|
474
571
|
|
|
@@ -496,6 +593,59 @@ describe("renderChart", () => {
|
|
|
496
593
|
expect(buildTimeAxis(yearlyDates, 48)).toBe("2020 2021 2022 2023 2024 2025");
|
|
497
594
|
});
|
|
498
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
|
+
|
|
499
649
|
test("uses source time-axis dates instead of projected extrema dates when provided", () => {
|
|
500
650
|
const sourceHistory = Array.from({ length: 96 }, (_, index) => ({
|
|
501
651
|
date: new Date(Date.UTC(2026, 2, 25, index * 3)),
|
|
@@ -528,6 +678,7 @@ describe("renderChart", () => {
|
|
|
528
678
|
{ mode: "line", width: 12, cursorX: 4 },
|
|
529
679
|
{ mode: "candles", width: 40, cursorX: 13 },
|
|
530
680
|
{ mode: "ohlc", width: 28, cursorX: 9 },
|
|
681
|
+
{ mode: "hlc", width: 28, cursorX: 9 },
|
|
531
682
|
];
|
|
532
683
|
|
|
533
684
|
for (const testCase of cases) {
|
|
@@ -553,6 +704,7 @@ describe("renderChart", () => {
|
|
|
553
704
|
{ mode: "area", width: 12, height: 6, cursorX: 5, cursorY: 4 },
|
|
554
705
|
{ mode: "candles", width: 40, height: 6, cursorX: 13, cursorY: 3 },
|
|
555
706
|
{ mode: "ohlc", width: 28, height: 6, cursorX: 9, cursorY: 1 },
|
|
707
|
+
{ mode: "hlc", width: 28, height: 6, cursorX: 9, cursorY: 1 },
|
|
556
708
|
];
|
|
557
709
|
|
|
558
710
|
for (const testCase of cases) {
|
|
@@ -638,12 +790,26 @@ describe("renderChart", () => {
|
|
|
638
790
|
width: 28,
|
|
639
791
|
height: 6,
|
|
640
792
|
lines: [
|
|
641
|
-
"⠁ ⠁ ⠁
|
|
642
|
-
"
|
|
643
|
-
"
|
|
644
|
-
"
|
|
645
|
-
"
|
|
646
|
-
"⡀ ⡀ ⡀ ⡀ ⡀ ⡀
|
|
793
|
+
"⠁ ⠁ ⠁ ⢸⣿ ⠁ ⠁ ⣤⡄ ⠁ ⠁ ⠁",
|
|
794
|
+
" ⢰⣶ ⢸⣿⠒⠂ ⠐⠒⣿⡇ ",
|
|
795
|
+
"⠁⢸⣿⠒⠂ ⠁ ⠒⢺⣿ ⠁ ⠁ ⣿⡇ ⠁ ⠁⣶⡆⠁",
|
|
796
|
+
"⠤⢼⣿⡀ ⡀ ⠸⠿ ⡀ ⡀ ⣿⡇ ⡀ ⡀⣿⡏⠉",
|
|
797
|
+
" ⠸⠿ ⣿⡧⠤ ⠠⠤⣿⡇ ",
|
|
798
|
+
"⡀ ⡀ ⡀ ⡀ ⡀ ⡀ ⣿⡇ ⡀ ⡀⠛⠃⡀",
|
|
799
|
+
],
|
|
800
|
+
timeLabels: "Jan 2 3 4 Jan 5",
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
mode: "hlc",
|
|
804
|
+
width: 28,
|
|
805
|
+
height: 6,
|
|
806
|
+
lines: [
|
|
807
|
+
"⠁ ⠁ ⠁ ⢸⣿ ⠁ ⠁ ⣤⡄ ⠁ ⠁ ⠁",
|
|
808
|
+
" ⢰⣶ ⢸⣿⠒⠂ ⣿⡇ ",
|
|
809
|
+
"⠁⢸⣿⠒⠂ ⠁ ⢸⣿ ⠁ ⠁ ⣿⡇ ⠁ ⠁⣶⡆⠁",
|
|
810
|
+
"⡀⢸⣿⡀ ⡀ ⠸⠿ ⡀ ⡀ ⣿⡇ ⡀ ⡀⣿⡏⠉",
|
|
811
|
+
" ⠸⠿ ⣿⡧⠤ ⣿⡇ ",
|
|
812
|
+
"⡀ ⡀ ⡀ ⡀ ⡀ ⡀ ⣿⡇ ⡀ ⡀⠛⠃⡀",
|
|
647
813
|
],
|
|
648
814
|
timeLabels: "Jan 2 3 4 Jan 5",
|
|
649
815
|
},
|
|
@@ -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);
|