gloomberb 0.12.0 → 0.13.2
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 +2 -0
- package/package.json +1 -2
- package/src/api-client/auth.ts +2 -1
- package/src/api-client/index.test.ts +34 -0
- package/src/api-client/index.ts +10 -2
- package/src/api-client/request.ts +4 -1
- package/src/api-client/research-activity.ts +61 -0
- package/src/api-client/types.ts +2 -1
- package/src/cli/commands/plugins.ts +4 -3
- package/src/cli/desktop-pane-shot.ts +79 -10
- package/src/cli/pane-functions/capabilities.ts +76 -23
- package/src/cli/pane-functions/cloud-session.test.ts +54 -0
- package/src/cli/pane-functions/cloud-session.ts +38 -0
- package/src/cli/pane-functions/dom.test.ts +41 -0
- package/src/cli/pane-functions/dom.ts +199 -0
- package/src/cli/pane-functions/index.test.ts +13 -2
- package/src/cli/pane-functions/index.ts +9 -5
- package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
- package/src/cli/pane-functions/intraday-shot.ts +334 -0
- package/src/cli/pane-functions/report.test.ts +29 -1
- package/src/cli/pane-functions/report.ts +68 -7
- package/src/cli/pane-functions/screenshot.ts +195 -42
- package/src/cli/restore-plugins.ts +10 -3
- package/src/components/chart/composite/composite-chart.test.tsx +68 -1
- package/src/components/chart/composite/composite-chart.tsx +87 -21
- package/src/components/chart/composite/rasterizer.ts +33 -0
- package/src/components/chart/composite/renderers.test.ts +51 -4
- package/src/components/chart/composite/scene.test.ts +128 -2
- package/src/components/chart/composite/scene.ts +92 -16
- package/src/components/chart/composite/text-renderer.ts +13 -0
- package/src/components/chart/composite/time-scale.ts +30 -10
- package/src/components/chart/composite/types.ts +26 -0
- package/src/components/command-bar/assist/runtime.ts +38 -30
- package/src/components/command-bar/surface/assist.test.tsx +63 -9
- package/src/components/command-bar/surface/test-harness.tsx +23 -4
- package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
- package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
- package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
- package/src/components/onboarding/onboarding-wizard.tsx +49 -14
- package/src/data/config/store/index.test.ts +23 -0
- package/src/data/config/store/node.ts +7 -1
- package/src/data/config/store/normalize.ts +4 -0
- package/src/news/types.ts +1 -0
- package/src/plugins/builtin/cds/client.test.ts +20 -0
- package/src/plugins/builtin/cds/client.ts +13 -3
- package/src/plugins/builtin/cds/headless.test.ts +30 -0
- package/src/plugins/builtin/cds/headless.ts +113 -0
- package/src/plugins/builtin/cds/index.tsx +4 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chat/controller/channels.ts +2 -1
- package/src/plugins/builtin/chat/controller/index.ts +13 -5
- package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
- package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
- package/src/plugins/builtin/chat/controller/state.ts +1 -0
- package/src/plugins/builtin/chat/controller.test.ts +260 -4
- package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
- package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
- package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
- package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
- package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
- package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
- package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
- package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
- package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
- package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
- package/src/plugins/builtin/cloud/plugin.tsx +2 -0
- package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
- package/src/plugins/builtin/congress-trades/client.ts +11 -0
- package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
- package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
- package/src/plugins/builtin/congress-trades/headless.ts +145 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
- package/src/plugins/builtin/credit-conditions/client.ts +1 -1
- package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
- package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
- package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
- package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
- package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
- package/src/plugins/builtin/dividend-yield/model.ts +4 -22
- package/src/plugins/builtin/dividend-yield/view.ts +17 -0
- package/src/plugins/builtin/earnings/headless.test.ts +82 -0
- package/src/plugins/builtin/earnings/headless.ts +162 -0
- package/src/plugins/builtin/earnings/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/data.ts +29 -8
- package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
- package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
- package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
- package/src/plugins/builtin/earnings-calls/model.ts +238 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
- package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
- package/src/plugins/builtin/fear-greed/headless.ts +84 -0
- package/src/plugins/builtin/fear-greed/index.tsx +4 -0
- package/src/plugins/builtin/holders/client.ts +29 -0
- package/src/plugins/builtin/holders/headless.test.ts +58 -0
- package/src/plugins/builtin/holders/headless.ts +162 -0
- package/src/plugins/builtin/holders/index.tsx +14 -8
- package/src/plugins/builtin/holders/pane.tsx +7 -1
- package/src/plugins/builtin/insider/client.ts +50 -0
- package/src/plugins/builtin/insider/headless.test.ts +122 -0
- package/src/plugins/builtin/insider/headless.ts +117 -0
- package/src/plugins/builtin/insider/index.tsx +21 -50
- package/src/plugins/builtin/insider/insider-data.ts +1 -1
- package/src/plugins/builtin/insider/model.ts +72 -0
- package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
- package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
- package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
- package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
- package/src/plugins/builtin/market-movers/client.ts +76 -0
- package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
- package/src/plugins/builtin/market-movers/headless.ts +73 -0
- package/src/plugins/builtin/market-movers/index.tsx +11 -51
- package/src/plugins/builtin/news/headless.test.ts +138 -0
- package/src/plugins/builtin/news/headless.ts +190 -0
- package/src/plugins/builtin/news/index.tsx +13 -9
- package/src/plugins/builtin/news/wire/index.tsx +2 -1
- package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
- package/src/plugins/builtin/research/analyst-headless.ts +172 -0
- package/src/plugins/builtin/research/analyst-model.ts +219 -0
- package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
- package/src/plugins/builtin/research/client.ts +12 -0
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
- package/src/plugins/builtin/research/event-model.ts +245 -0
- package/src/plugins/builtin/research/events-headless.test.ts +32 -0
- package/src/plugins/builtin/research/events-headless.ts +87 -0
- package/src/plugins/builtin/research/headless.test.ts +93 -0
- package/src/plugins/builtin/research/headless.ts +161 -0
- package/src/plugins/builtin/research/index.tsx +32 -18
- package/src/plugins/builtin/research-search/pane.tsx +4 -0
- package/src/plugins/builtin/scanner/headless.test.ts +59 -0
- package/src/plugins/builtin/scanner/headless.ts +125 -0
- package/src/plugins/builtin/scanner/index.ts +4 -0
- package/src/plugins/builtin/sec/client.ts +16 -0
- package/src/plugins/builtin/sec/headless.test.ts +72 -0
- package/src/plugins/builtin/sec/headless.ts +69 -0
- package/src/plugins/builtin/sec/index.tsx +20 -75
- package/src/plugins/builtin/sec/model.ts +89 -0
- package/src/plugins/builtin/sectors/client.ts +54 -0
- package/src/plugins/builtin/sectors/headless.test.ts +28 -0
- package/src/plugins/builtin/sectors/headless.ts +80 -0
- package/src/plugins/builtin/sectors/index.tsx +6 -51
- package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
- package/src/plugins/builtin/short-interest/client.ts +5 -2
- package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
- package/src/plugins/builtin/short-interest/headless.ts +111 -0
- package/src/plugins/builtin/short-interest/index.tsx +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +13 -11
- package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
- package/src/plugins/builtin/thirteenf/headless.ts +286 -0
- package/src/plugins/builtin/thirteenf/index.tsx +4 -0
- package/src/plugins/builtin/thirteenf/model.ts +13 -0
- package/src/plugins/builtin/thirteenf/table.ts +3 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
- package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
- package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
- package/src/plugins/builtin/volatility/client.ts +16 -3
- package/src/plugins/builtin/volatility/headless.test.ts +35 -0
- package/src/plugins/builtin/volatility/headless.ts +75 -0
- package/src/plugins/builtin/volatility/index.tsx +4 -0
- package/src/plugins/builtin/world-indices/client.ts +43 -0
- package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
- package/src/plugins/builtin/world-indices/headless.ts +109 -0
- package/src/plugins/builtin/world-indices/index.tsx +4 -0
- package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
- package/src/plugins/builtin/yield-curve/headless.ts +67 -0
- package/src/plugins/builtin/yield-curve/index.tsx +6 -8
- package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
- package/src/plugins/bundle.test.ts +58 -1
- package/src/plugins/bundle.ts +4 -2
- package/src/plugins/catalog-backend.test.ts +26 -0
- package/src/plugins/catalog-backend.ts +9 -2
- package/src/plugins/catalog-browser.test.ts +0 -13
- package/src/plugins/catalog-browser.ts +4 -0
- package/src/plugins/catalog-manifest.test.ts +15 -0
- package/src/plugins/catalog-manifest.ts +12 -5
- package/src/plugins/catalog-ui.ts +0 -6
- package/src/plugins/host-contract.ts +9 -0
- package/src/plugins/host-modules.ts +12 -0
- package/src/plugins/loader.ts +63 -15
- package/src/plugins/prediction-markets/headless.test.ts +119 -0
- package/src/plugins/prediction-markets/headless.ts +255 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/seed.ts +3 -0
- package/src/public/broker.ts +6 -0
- package/src/remote/schema.test.ts +65 -0
- package/src/remote/schema.ts +322 -39
- package/src/remote/types.ts +49 -2
- package/src/renderers/browser/app-services.ts +3 -0
- package/src/renderers/browser/config-host.ts +24 -8
- package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
- package/src/renderers/browser/deeplink-bridge.ts +15 -3
- package/src/renderers/browser/main.tsx +3 -1
- package/src/renderers/browser/research-entry.test.ts +8 -0
- package/src/renderers/browser/research-entry.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
- package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
- package/src/renderers/electrobun/view/build-assets.ts +0 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
- package/src/sources/gloomberb-cloud/news.ts +1 -0
- package/src/sync/core-contributors.test.ts +111 -0
- package/src/sync/core-contributors.ts +155 -1
- package/src/time-series/resolve.test.ts +118 -1
- package/src/time-series/resolve.ts +14 -16
- package/src/types/config.ts +2 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
- package/src/plugins/broker-sync/native-loader.ts +0 -10
- package/src/plugins/broker-sync/normalize.ts +0 -308
- package/src/plugins/broker-sync/public.ts +0 -122
- package/src/plugins/broker-sync/robinhood-native.ts +0 -367
- package/src/plugins/broker-sync/robinhood.ts +0 -114
- package/src/plugins/broker-sync/simplefin-native.ts +0 -201
- package/src/plugins/broker-sync/simplefin.ts +0 -70
- package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
|
@@ -92,7 +92,11 @@ import {
|
|
|
92
92
|
type ChartToolDrag,
|
|
93
93
|
type ChartToolKind,
|
|
94
94
|
} from "./tools";
|
|
95
|
-
import {
|
|
95
|
+
import {
|
|
96
|
+
COMPOSITE_RIGHT_OFFSET_RATIO,
|
|
97
|
+
compositeRightOffsetRatio,
|
|
98
|
+
unprojectCompositeTimestamp,
|
|
99
|
+
} from "./time-scale";
|
|
96
100
|
import {
|
|
97
101
|
allocateCompositePanelHeights,
|
|
98
102
|
applyCompositeChartCursor,
|
|
@@ -139,12 +143,17 @@ interface PanGesture {
|
|
|
139
143
|
positionsPerCell: number;
|
|
140
144
|
}
|
|
141
145
|
|
|
142
|
-
/**
|
|
146
|
+
/**
|
|
147
|
+
* A drag pans in the frame it started in; see `CompositeNavigationFrame`. The
|
|
148
|
+
* plot covers the viewport plus its reserved right offset, so a cell is worth
|
|
149
|
+
* that much time and the bars keep pace with the pointer.
|
|
150
|
+
*/
|
|
143
151
|
function startPanGesture(
|
|
144
152
|
frame: CompositeNavigationFrame,
|
|
145
153
|
viewport: CompositeViewportRange,
|
|
146
154
|
plotWidth: number,
|
|
147
155
|
globalX: number,
|
|
156
|
+
plotSpanFactor: number,
|
|
148
157
|
): PanGesture {
|
|
149
158
|
const positions = compositeViewportPositions(frame, viewport);
|
|
150
159
|
const span = positions ? Math.max(positions.end - positions.start, Number.EPSILON) : 1;
|
|
@@ -153,7 +162,7 @@ function startPanGesture(
|
|
|
153
162
|
startGlobalX: globalX,
|
|
154
163
|
frame,
|
|
155
164
|
startViewport: viewport,
|
|
156
|
-
positionsPerCell: span / Math.max(plotWidth, 1),
|
|
165
|
+
positionsPerCell: span * plotSpanFactor / Math.max(plotWidth, 1),
|
|
157
166
|
};
|
|
158
167
|
}
|
|
159
168
|
|
|
@@ -740,6 +749,9 @@ function CompositePanelSurface({
|
|
|
740
749
|
>(null);
|
|
741
750
|
const [toolDrag, setToolDrag] = useState<ChartToolDrag | null>(null);
|
|
742
751
|
const plotAspect = (plotWidth * cellWidthPx) / Math.max(panel.height * cellHeightPx, 1);
|
|
752
|
+
// The plot is wider than the viewport by the reserved right offset. Gestures
|
|
753
|
+
// read the pointer in plot space, so they convert through this.
|
|
754
|
+
const plotSpanFactor = 1 + compositeRightOffsetRatio(scene.timeScale);
|
|
743
755
|
const bitmapSize = useStaticChartBitmapSize(plotWidth, panel.height);
|
|
744
756
|
const bitmap = useCompositePanelBitmap({ panel, bitmapSize, colors, isDesktopWeb });
|
|
745
757
|
const columnLayout = useMemo(() => buildCompositeColumnLayout(panel), [panel]);
|
|
@@ -924,20 +936,37 @@ function CompositePanelSurface({
|
|
|
924
936
|
// anchor the drag started from.
|
|
925
937
|
if (!toolDrag || toolDrag.kind === "zoom" || !toolReadout?.startValueLabel) return null;
|
|
926
938
|
return {
|
|
939
|
+
side: null,
|
|
927
940
|
yRatio: toolDrag.startYRatio,
|
|
928
941
|
label: toolReadout.startValueLabel,
|
|
929
942
|
color: toolReadout.direction === "down" ? colors.negative : themeColors.positive,
|
|
930
943
|
};
|
|
931
944
|
}, [colors.negative, toolDrag, toolReadout]);
|
|
945
|
+
const lastPriceMarker = useMemo(() => {
|
|
946
|
+
const marker = panel.lastPrice;
|
|
947
|
+
const domain = marker ? panel.axes[marker.axis] : undefined;
|
|
948
|
+
if (!marker || !domain) return null;
|
|
949
|
+
return {
|
|
950
|
+
side: marker.axis,
|
|
951
|
+
yRatio: marker.yRatio,
|
|
952
|
+
label: formatAxisValue
|
|
953
|
+
? formatAxisValue(marker.value, domain)
|
|
954
|
+
: formatCompositeCursorValue(marker.value, domain),
|
|
955
|
+
color: marker.color,
|
|
956
|
+
};
|
|
957
|
+
}, [formatAxisValue, panel]);
|
|
932
958
|
const buildAxisMarkers = (side: "left" | "right") => {
|
|
933
|
-
if (!
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
959
|
+
if (!panel.axes[side]) return undefined;
|
|
960
|
+
const markers = [lastPriceMarker, axisMarkers].flatMap((marker) => (
|
|
961
|
+
marker && (marker.side === null || marker.side === side) ? [marker] : []
|
|
962
|
+
));
|
|
963
|
+
if (markers.length === 0) return undefined;
|
|
964
|
+
return markers.map((marker) => ({
|
|
965
|
+
row: Math.round(marker.yRatio * Math.max(panel.height - 1, 0)),
|
|
966
|
+
pixelY: marker.yRatio * Math.max(panel.height * cellHeightPx - 1, 0),
|
|
967
|
+
label: marker.label,
|
|
968
|
+
color: marker.color,
|
|
969
|
+
}));
|
|
941
970
|
};
|
|
942
971
|
const leftAxisMarkers = buildAxisMarkers("left");
|
|
943
972
|
const rightAxisMarkers = buildAxisMarkers("right");
|
|
@@ -1013,7 +1042,13 @@ function CompositePanelSurface({
|
|
|
1013
1042
|
return;
|
|
1014
1043
|
}
|
|
1015
1044
|
if (!updateCursor(event)) return;
|
|
1016
|
-
dragRef.current = startPanGesture(
|
|
1045
|
+
dragRef.current = startPanGesture(
|
|
1046
|
+
frame,
|
|
1047
|
+
viewport,
|
|
1048
|
+
plotWidth,
|
|
1049
|
+
getGlobalMouseX(event, renderer),
|
|
1050
|
+
plotSpanFactor,
|
|
1051
|
+
);
|
|
1017
1052
|
}, [
|
|
1018
1053
|
armedTool,
|
|
1019
1054
|
drawings,
|
|
@@ -1022,6 +1057,7 @@ function CompositePanelSurface({
|
|
|
1022
1057
|
onSelectDrawing,
|
|
1023
1058
|
panel,
|
|
1024
1059
|
plotAspect,
|
|
1060
|
+
plotSpanFactor,
|
|
1025
1061
|
plotWidth,
|
|
1026
1062
|
pointerRatios,
|
|
1027
1063
|
renderer,
|
|
@@ -1067,7 +1103,7 @@ function CompositePanelSurface({
|
|
|
1067
1103
|
if (drag.frame !== frame) {
|
|
1068
1104
|
// Data refreshed mid-drag. The new frame maps positions differently, so
|
|
1069
1105
|
// continue from where the plot is now instead of replaying the drag.
|
|
1070
|
-
Object.assign(drag, startPanGesture(frame, viewport, plotWidth, globalX));
|
|
1106
|
+
Object.assign(drag, startPanGesture(frame, viewport, plotWidth, globalX, plotSpanFactor));
|
|
1071
1107
|
}
|
|
1072
1108
|
onPanViewport(
|
|
1073
1109
|
(globalX - drag.startGlobalX) * drag.positionsPerCell,
|
|
@@ -1078,6 +1114,7 @@ function CompositePanelSurface({
|
|
|
1078
1114
|
onEditDrawing,
|
|
1079
1115
|
onPanViewport,
|
|
1080
1116
|
panel,
|
|
1117
|
+
plotSpanFactor,
|
|
1081
1118
|
plotWidth,
|
|
1082
1119
|
pointerRatios,
|
|
1083
1120
|
renderer,
|
|
@@ -1117,8 +1154,8 @@ function CompositePanelSurface({
|
|
|
1117
1154
|
// absorbs every event that arrived since the last one.
|
|
1118
1155
|
const pendingWheelRef = useRef<PendingWheel | null>(null);
|
|
1119
1156
|
const wheelFrameRef = useRef<number | null>(null);
|
|
1120
|
-
const latestWheelStateRef = useRef({ frame, viewport, onPanViewport, onZoomViewport });
|
|
1121
|
-
latestWheelStateRef.current = { frame, viewport, onPanViewport, onZoomViewport };
|
|
1157
|
+
const latestWheelStateRef = useRef({ frame, viewport, onPanViewport, onZoomViewport, plotSpanFactor });
|
|
1158
|
+
latestWheelStateRef.current = { frame, viewport, onPanViewport, onZoomViewport, plotSpanFactor };
|
|
1122
1159
|
const flushWheel = useCallback(() => {
|
|
1123
1160
|
wheelFrameRef.current = null;
|
|
1124
1161
|
const pending = pendingWheelRef.current;
|
|
@@ -1128,7 +1165,7 @@ function CompositePanelSurface({
|
|
|
1128
1165
|
if (pending.panRatio !== 0) {
|
|
1129
1166
|
const positions = compositeViewportPositions(latest.frame, latest.viewport);
|
|
1130
1167
|
const span = positions ? Math.max(positions.end - positions.start, Number.EPSILON) : 0;
|
|
1131
|
-
latest.onPanViewport(pending.panRatio * span);
|
|
1168
|
+
latest.onPanViewport(pending.panRatio * span * latest.plotSpanFactor);
|
|
1132
1169
|
}
|
|
1133
1170
|
if (pending.zoomLog !== 0) {
|
|
1134
1171
|
latest.onZoomViewport(Math.exp(pending.zoomLog), pending.anchorRatio);
|
|
@@ -1152,7 +1189,12 @@ function CompositePanelSurface({
|
|
|
1152
1189
|
const pending = pendingWheelRef.current ?? { panRatio: 0, zoomLog: 0, anchorRatio: 0.5 };
|
|
1153
1190
|
if (event.modifiers.ctrl && pointer && isVerticalWheelDirection(scroll.direction)) {
|
|
1154
1191
|
pending.zoomLog += Math.log(resolveCompositeWheelZoom(scroll));
|
|
1155
|
-
|
|
1192
|
+
// The anchor is a fraction of the viewport, so a pointer over the empty
|
|
1193
|
+
// right offset anchors on the newest observation instead of past it.
|
|
1194
|
+
pending.anchorRatio = Math.min(
|
|
1195
|
+
1,
|
|
1196
|
+
pointer.cellX / Math.max(plotWidth - 1, 1) * plotSpanFactor,
|
|
1197
|
+
);
|
|
1156
1198
|
} else {
|
|
1157
1199
|
pending.panRatio += resolveCompositeWheelPan(scroll, plotWidth * cellWidthPx);
|
|
1158
1200
|
}
|
|
@@ -1164,7 +1206,16 @@ function CompositePanelSurface({
|
|
|
1164
1206
|
if (wheelFrameRef.current === null) {
|
|
1165
1207
|
wheelFrameRef.current = webFrame.requestAnimationFrame(flushWheel);
|
|
1166
1208
|
}
|
|
1167
|
-
}, [
|
|
1209
|
+
}, [
|
|
1210
|
+
cellWidthPx,
|
|
1211
|
+
flushWheel,
|
|
1212
|
+
isDesktopWeb,
|
|
1213
|
+
onActivate,
|
|
1214
|
+
plotSpanFactor,
|
|
1215
|
+
plotWidth,
|
|
1216
|
+
renderer,
|
|
1217
|
+
updateCursor,
|
|
1218
|
+
]);
|
|
1168
1219
|
|
|
1169
1220
|
return (
|
|
1170
1221
|
<Box
|
|
@@ -1611,6 +1662,9 @@ export function CompositeChart({
|
|
|
1611
1662
|
const stableSeries = reuseResolvedSeriesList(seriesIdentityRef.current, series);
|
|
1612
1663
|
seriesIdentityRef.current = stableSeries;
|
|
1613
1664
|
const visibleSeries = useMemo(() => stableSeries.filter((entry) => entry.points.length > 0), [stableSeries]);
|
|
1665
|
+
// Panel layout follows the authored series so a panel keeps its height while
|
|
1666
|
+
// its data loads; only the marks inside it wait for observations.
|
|
1667
|
+
const panelSeries = useMemo(() => stableSeries.filter((entry) => !entry.hidden), [stableSeries]);
|
|
1614
1668
|
const marketTimelineSeries = useMemo(() => {
|
|
1615
1669
|
const supplied = timelineSeries?.filter((entry) => entry.points.length > 0) ?? [];
|
|
1616
1670
|
return supplied.some((entry) => entry.timeBasis?.kind === "market")
|
|
@@ -1749,7 +1803,7 @@ export function CompositeChart({
|
|
|
1749
1803
|
const timeAxisRows = showTimeAxis ? 1 : 0;
|
|
1750
1804
|
const xMarkers = xAxis?.markers ?? NO_X_MARKERS;
|
|
1751
1805
|
const xMarkerRows = xMarkers.some((marker) => marker.label) ? 1 : 0;
|
|
1752
|
-
const panelCount = new Set(
|
|
1806
|
+
const panelCount = new Set(panelSeries.map((entry) => entry.panelId)).size;
|
|
1753
1807
|
const lastTickKey = visibleSeries.map((entry) => {
|
|
1754
1808
|
const last = entry.points.at(-1);
|
|
1755
1809
|
if (!last) return entry.id;
|
|
@@ -1764,16 +1818,28 @@ export function CompositeChart({
|
|
|
1764
1818
|
textDim: colors?.textDim ?? activeThemeColors.textDim,
|
|
1765
1819
|
negative: colors?.negative ?? activeThemeColors.negative,
|
|
1766
1820
|
}), [activeThemeColors, colors]);
|
|
1821
|
+
// A custom x axis means x is not time: a tenor, a fraction, a return. Its
|
|
1822
|
+
// last observation belongs at the right edge, under its own label.
|
|
1823
|
+
const rightOffsetRatio = xAxis ? 0 : COMPOSITE_RIGHT_OFFSET_RATIO;
|
|
1767
1824
|
const projectedScene = useMemo(() => {
|
|
1768
1825
|
// lastTickKey busts this memo when a live tick mutates series identity in place.
|
|
1769
1826
|
void lastTickKey;
|
|
1770
|
-
return buildCompositeChartScene(
|
|
1827
|
+
return buildCompositeChartScene(panelSeries, panels, {
|
|
1771
1828
|
width: 1,
|
|
1772
1829
|
height: Math.max(panelCount, 1),
|
|
1773
1830
|
viewport: effectiveViewport ?? undefined,
|
|
1774
1831
|
timelineSeries: marketTimelineSeries,
|
|
1832
|
+
rightOffsetRatio,
|
|
1775
1833
|
});
|
|
1776
|
-
}, [
|
|
1834
|
+
}, [
|
|
1835
|
+
effectiveViewport,
|
|
1836
|
+
lastTickKey,
|
|
1837
|
+
marketTimelineSeries,
|
|
1838
|
+
panelCount,
|
|
1839
|
+
panelSeries,
|
|
1840
|
+
panels,
|
|
1841
|
+
rightOffsetRatio,
|
|
1842
|
+
]);
|
|
1777
1843
|
// Gutters follow the axes the scene actually built. Reading the series list
|
|
1778
1844
|
// instead drops a gutter the moment its series has no observation in view,
|
|
1779
1845
|
// which is exactly what a zoom does, taking the axis labels with it.
|
|
@@ -29,6 +29,11 @@ interface RenderCompositePanelBitmapOptions {
|
|
|
29
29
|
|
|
30
30
|
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
|
|
31
31
|
|
|
32
|
+
/** Dash geometry of the last price level, in bitmap pixels. */
|
|
33
|
+
const LAST_PRICE_DASH_PIXELS = 6;
|
|
34
|
+
const LAST_PRICE_GAP_PIXELS = 5;
|
|
35
|
+
const LAST_PRICE_OPACITY = 0.9;
|
|
36
|
+
|
|
32
37
|
function pixelPoint(point: CompositeProjectedPoint, width: number, height: number): { x: number; y: number } {
|
|
33
38
|
return {
|
|
34
39
|
x: clamp(point.xRatio * Math.max(width - 1, 0), 0, Math.max(width - 1, 0)),
|
|
@@ -288,6 +293,30 @@ function drawOhlc(
|
|
|
288
293
|
}
|
|
289
294
|
}
|
|
290
295
|
|
|
296
|
+
function drawLastPriceLevel(
|
|
297
|
+
data: Uint8Array,
|
|
298
|
+
width: number,
|
|
299
|
+
height: number,
|
|
300
|
+
yRatio: number,
|
|
301
|
+
color: RgbaColor,
|
|
302
|
+
): void {
|
|
303
|
+
const y = Math.round(clamp(yRatio * Math.max(height - 1, 0), 0, Math.max(height - 1, 0)));
|
|
304
|
+
const period = LAST_PRICE_DASH_PIXELS + LAST_PRICE_GAP_PIXELS;
|
|
305
|
+
for (let x = 0; x < width; x += period) {
|
|
306
|
+
fillRect(
|
|
307
|
+
data,
|
|
308
|
+
width,
|
|
309
|
+
height,
|
|
310
|
+
x,
|
|
311
|
+
y,
|
|
312
|
+
Math.min(x + LAST_PRICE_DASH_PIXELS - 1, width - 1),
|
|
313
|
+
y,
|
|
314
|
+
color,
|
|
315
|
+
LAST_PRICE_OPACITY,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
291
320
|
export function renderCompositePanelBitmap(
|
|
292
321
|
panel: CompositePanelScene,
|
|
293
322
|
options: RenderCompositePanelBitmapOptions,
|
|
@@ -362,5 +391,9 @@ export function renderCompositePanelBitmap(
|
|
|
362
391
|
}
|
|
363
392
|
}
|
|
364
393
|
|
|
394
|
+
if (panel.lastPrice) {
|
|
395
|
+
drawLastPriceLevel(data, width, height, panel.lastPrice.yRatio, parseHex(panel.lastPrice.color));
|
|
396
|
+
}
|
|
397
|
+
|
|
365
398
|
return { width, height, pixels: data };
|
|
366
399
|
}
|
|
@@ -71,8 +71,8 @@ describe("composite chart renderers", () => {
|
|
|
71
71
|
|
|
72
72
|
const rows = renderCompositePanelText(scene.panels[0]!, scene.width, null, null);
|
|
73
73
|
expect(rows[7]!.slice(0, 2)).toBe("██");
|
|
74
|
-
expect(rows[7]!.slice(
|
|
75
|
-
expect(rows[7]!.slice(
|
|
74
|
+
expect(rows[7]!.slice(14, 16)).toBe("██");
|
|
75
|
+
expect(rows[7]!.slice(28, 30)).toBe("██");
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
test("rasterizes same-date columns as separate colored bars", () => {
|
|
@@ -401,7 +401,7 @@ describe("composite chart renderers", () => {
|
|
|
401
401
|
const rows = renderCompositePanelText(scene.panels[0]!, scene.width, null, null);
|
|
402
402
|
const occupied = [...rows[7]!]
|
|
403
403
|
.flatMap((cell, index) => cell === "█" ? [index] : []);
|
|
404
|
-
expect(occupied).toEqual([0,
|
|
404
|
+
expect(occupied).toEqual([0, 14, 28]);
|
|
405
405
|
});
|
|
406
406
|
|
|
407
407
|
test("does not group columns across independent axes or panels", () => {
|
|
@@ -732,12 +732,59 @@ describe("composite chart renderers", () => {
|
|
|
732
732
|
else runs.push([x]);
|
|
733
733
|
}
|
|
734
734
|
|
|
735
|
+
const widths = runs.map((run) => run.length);
|
|
735
736
|
expect(runs).toHaveLength(3);
|
|
736
|
-
expect(
|
|
737
|
+
expect(Math.min(...widths)).toBeGreaterThanOrEqual(13);
|
|
738
|
+
// Fractional centers cost a body at most one pixel of rounding.
|
|
739
|
+
expect(Math.max(...widths) - Math.min(...widths)).toBeLessThanOrEqual(1);
|
|
737
740
|
expect(runs[0]?.[0]).toBe(0);
|
|
741
|
+
// Three bars leave less reserved space than half a body, so the clamp is
|
|
742
|
+
// still what keeps the newest one whole.
|
|
738
743
|
expect(runs.at(-1)?.at(-1)).toBe(bitmap.width - 1);
|
|
739
744
|
});
|
|
740
745
|
|
|
746
|
+
test("leaves the reserved right offset empty after the newest candle", () => {
|
|
747
|
+
const candles = series("price", "candles", [], "left", "#00ff66");
|
|
748
|
+
candles.points = Array.from({ length: 40 }, (_, index) => ({
|
|
749
|
+
...point(`2025-02-${String(index + 1).padStart(2, "0")}`, 10),
|
|
750
|
+
open: 8,
|
|
751
|
+
high: 13,
|
|
752
|
+
low: 7,
|
|
753
|
+
close: 10,
|
|
754
|
+
}));
|
|
755
|
+
const scene = buildCompositeChartScene(
|
|
756
|
+
[candles],
|
|
757
|
+
[{ id: "main" }],
|
|
758
|
+
{ width: 81, height: 9 },
|
|
759
|
+
)!;
|
|
760
|
+
const panel = scene.panels[0]!;
|
|
761
|
+
const bitmap = renderCompositePanelBitmap(panel, {
|
|
762
|
+
pixelWidth: 400,
|
|
763
|
+
pixelHeight: 51,
|
|
764
|
+
colors: {
|
|
765
|
+
background: "#000000",
|
|
766
|
+
grid: "#202020",
|
|
767
|
+
crosshair: "#ffffff",
|
|
768
|
+
text: "#eeeeee",
|
|
769
|
+
textDim: "#999999",
|
|
770
|
+
negative: "#ff0000",
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
const domain = panel.axes.left!;
|
|
774
|
+
const bodyY = Math.round(
|
|
775
|
+
(projectCompositeValue(8, domain)! + projectCompositeValue(10, domain)!) / 2
|
|
776
|
+
* (bitmap.height - 1),
|
|
777
|
+
);
|
|
778
|
+
const painted = Array.from({ length: bitmap.width }, (_, x) => x).filter((x) => {
|
|
779
|
+
const offset = (bodyY * bitmap.width + x) * 4;
|
|
780
|
+
return bitmap.pixels[offset + 1]! > 220;
|
|
781
|
+
});
|
|
782
|
+
const slotWidth = (bitmap.width - 1) / 40;
|
|
783
|
+
|
|
784
|
+
// The newest body ends short of the axis, with whole bar slots of air.
|
|
785
|
+
expect(bitmap.width - 1 - painted.at(-1)!).toBeGreaterThan(slotWidth);
|
|
786
|
+
});
|
|
787
|
+
|
|
741
788
|
test("marks a standalone line observation without extending it through time", () => {
|
|
742
789
|
const scene = buildCompositeChartScene(
|
|
743
790
|
[series("estimate", "line", [42], "left")],
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
unprojectCompositeValue,
|
|
10
10
|
} from "./scene";
|
|
11
11
|
import { buildCompositeColumnLayout } from "./column-layout";
|
|
12
|
+
import { COMPOSITE_RIGHT_OFFSET_RATIO } from "./time-scale";
|
|
13
|
+
|
|
14
|
+
/** Ratio the newest observation lands on once the right offset is reserved. */
|
|
15
|
+
const NEWEST_X_RATIO = 1 / (1 + COMPOSITE_RIGHT_OFFSET_RATIO);
|
|
12
16
|
|
|
13
17
|
function point(date: string, value: number): TimeSeriesPoint {
|
|
14
18
|
const observedAt = new Date(`${date}T00:00:00.000Z`);
|
|
@@ -37,6 +41,49 @@ function series(overrides: Partial<ResolvedSeries> & Pick<ResolvedSeries, "id" |
|
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
describe("composite chart scene", () => {
|
|
44
|
+
test("keeps a panel and its height while its series has no observations in view", () => {
|
|
45
|
+
const price = series({
|
|
46
|
+
id: "price",
|
|
47
|
+
points: [point("2025-01-01", 90), point("2025-01-02", 95), point("2025-01-03", 110)],
|
|
48
|
+
});
|
|
49
|
+
const volume = series({
|
|
50
|
+
id: "volume",
|
|
51
|
+
panelId: "volume",
|
|
52
|
+
style: "columns",
|
|
53
|
+
unit: "shares",
|
|
54
|
+
unitGroup: "volume",
|
|
55
|
+
points: [point("2025-01-01", 1_000), point("2025-01-02", 1_200), point("2025-01-03", 1_400)],
|
|
56
|
+
});
|
|
57
|
+
const panels = [{ id: "main" }, { id: "volume", height: 0.25 }];
|
|
58
|
+
const options = { width: 10, height: 8 };
|
|
59
|
+
|
|
60
|
+
const loaded = buildCompositeChartScene([price, volume], panels, {
|
|
61
|
+
...options,
|
|
62
|
+
viewport: { start: new Date("2025-01-01T00:00:00.000Z"), end: new Date("2025-01-03T00:00:00.000Z") },
|
|
63
|
+
});
|
|
64
|
+
// The same window, with the lower panel's data not yet loaded.
|
|
65
|
+
const loading = buildCompositeChartScene([price, { ...volume, points: [] }], panels, {
|
|
66
|
+
...options,
|
|
67
|
+
viewport: { start: new Date("2025-01-01T00:00:00.000Z"), end: new Date("2025-01-03T00:00:00.000Z") },
|
|
68
|
+
});
|
|
69
|
+
// A window the upper panel covers and the lower panel has no bars in.
|
|
70
|
+
const partial = buildCompositeChartScene([price, {
|
|
71
|
+
...volume,
|
|
72
|
+
points: [point("2025-01-01", 1_000)],
|
|
73
|
+
}], panels, {
|
|
74
|
+
...options,
|
|
75
|
+
viewport: { start: new Date("2025-01-02T00:00:00.000Z"), end: new Date("2025-01-03T00:00:00.000Z") },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const layout = (scene: typeof loaded) => scene?.panels.map((panel) => [panel.id, panel.height]);
|
|
79
|
+
expect(layout(loaded)).toEqual([["main", 6], ["volume", 2]]);
|
|
80
|
+
expect(layout(loading)).toEqual(layout(loaded));
|
|
81
|
+
expect(layout(partial)).toEqual(layout(loaded));
|
|
82
|
+
expect(loading?.panels[1]?.series).toEqual([]);
|
|
83
|
+
// The axis still reads the loaded history, so the gutter does not jump either.
|
|
84
|
+
expect(partial?.panels[1]?.axes.left?.max).toBeGreaterThan(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
40
87
|
test("keeps panels synchronized while preserving independent dual-axis domains", () => {
|
|
41
88
|
const price = series({
|
|
42
89
|
id: "price",
|
|
@@ -235,10 +282,85 @@ describe("composite chart scene", () => {
|
|
|
235
282
|
);
|
|
236
283
|
|
|
237
284
|
expect(scene).not.toBeNull();
|
|
238
|
-
|
|
285
|
+
const stepPoints = scene?.panels[0]?.series[0]?.points ?? [];
|
|
286
|
+
expect(stepPoints).toHaveLength(2);
|
|
287
|
+
expect(stepPoints[0]?.xRatio).toBe(0);
|
|
288
|
+
// The level runs to the viewport end and stops: the reserved right offset
|
|
289
|
+
// never carries a projected value.
|
|
290
|
+
expect(stepPoints[1]?.xRatio).toBeCloseTo(NEWEST_X_RATIO, 10);
|
|
239
291
|
expect(scene?.cursorValues[0]?.value).toBe(90);
|
|
240
292
|
});
|
|
241
293
|
|
|
294
|
+
test("reserves empty space after the newest observation on both time scales", () => {
|
|
295
|
+
const calendar = series({
|
|
296
|
+
id: "calendar",
|
|
297
|
+
points: [point("2025-01-01", 10), point("2025-01-11", 20), point("2025-01-21", 30)],
|
|
298
|
+
});
|
|
299
|
+
const market = series({
|
|
300
|
+
...calendar,
|
|
301
|
+
id: "market",
|
|
302
|
+
timeBasis: { kind: "market", timeZone: "America/New_York" },
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
for (const entry of [calendar, market]) {
|
|
306
|
+
const scene = buildCompositeChartScene([entry], [{ id: "main" }], { width: 101, height: 10 })!;
|
|
307
|
+
const points = scene.panels[0]!.series[0]!.points;
|
|
308
|
+
|
|
309
|
+
expect(points.at(-1)?.xRatio).toBeCloseTo(NEWEST_X_RATIO, 10);
|
|
310
|
+
// The empty tail is the plot's, not the viewport's: navigation still ends
|
|
311
|
+
// on the newest observation.
|
|
312
|
+
expect(scene.endTime).toBe(new Date("2025-01-21T00:00:00.000Z").getTime());
|
|
313
|
+
// A pointer over the reserved space still reads the newest bar.
|
|
314
|
+
expect(resolveCompositeCursorDate(scene, 100)?.toISOString())
|
|
315
|
+
.toBe("2025-01-21T00:00:00.000Z");
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("marks the newest close of the primary price series, skipping studies and volume", () => {
|
|
320
|
+
const timeBasis = { kind: "market", timeZone: "America/New_York" } as const;
|
|
321
|
+
const price = series({
|
|
322
|
+
id: "price",
|
|
323
|
+
unitGroup: "price:USD",
|
|
324
|
+
dataShape: "ohlcv",
|
|
325
|
+
style: "candles",
|
|
326
|
+
timeBasis,
|
|
327
|
+
points: ["2025-01-02", "2025-01-03"].map((date, index) => ({
|
|
328
|
+
...point(date, 100 + index),
|
|
329
|
+
open: 90 + index,
|
|
330
|
+
high: 110 + index,
|
|
331
|
+
low: 80 + index,
|
|
332
|
+
close: 101 + index,
|
|
333
|
+
})),
|
|
334
|
+
});
|
|
335
|
+
const average = series({
|
|
336
|
+
id: "sma",
|
|
337
|
+
unitGroup: "price:USD",
|
|
338
|
+
timeBasis,
|
|
339
|
+
points: [point("2025-01-02", 40), point("2025-01-03", 41)],
|
|
340
|
+
});
|
|
341
|
+
const volume = series({
|
|
342
|
+
id: "volume",
|
|
343
|
+
unitGroup: "volume",
|
|
344
|
+
style: "columns",
|
|
345
|
+
panelId: "volume",
|
|
346
|
+
timeBasis,
|
|
347
|
+
points: [point("2025-01-02", 5_000), point("2025-01-03", 6_000)],
|
|
348
|
+
});
|
|
349
|
+
const scene = buildCompositeChartScene(
|
|
350
|
+
[price, average, volume],
|
|
351
|
+
[{ id: "main" }, { id: "volume" }],
|
|
352
|
+
{ width: 101, height: 12 },
|
|
353
|
+
)!;
|
|
354
|
+
const [main, volumePanel] = scene.panels;
|
|
355
|
+
|
|
356
|
+
expect(main?.lastPrice?.seriesId).toBe("price");
|
|
357
|
+
expect(main?.lastPrice?.value).toBe(102);
|
|
358
|
+
expect(main?.lastPrice?.color).toBe(price.color);
|
|
359
|
+
expect(main?.lastPrice?.yRatio)
|
|
360
|
+
.toBe(projectCompositeValue(102, main!.axes.left!));
|
|
361
|
+
expect(volumePanel?.lastPrice).toBeUndefined();
|
|
362
|
+
});
|
|
363
|
+
|
|
242
364
|
test("uses as-of values across mixed-frequency dates without looking ahead", () => {
|
|
243
365
|
const daily = series({
|
|
244
366
|
id: "daily",
|
|
@@ -430,7 +552,11 @@ describe("composite chart scene", () => {
|
|
|
430
552
|
);
|
|
431
553
|
|
|
432
554
|
expect(scene?.timeScale).toMatchObject({ kind: "market", anchorSeriesId: "price" });
|
|
433
|
-
|
|
555
|
+
const ratios = scene?.panels[0]?.series[0]?.points.map(({ xRatio }) => xRatio) ?? [];
|
|
556
|
+
expect(ratios).toHaveLength(3);
|
|
557
|
+
expect(ratios[0]).toBe(0);
|
|
558
|
+
expect(ratios[1]).toBeCloseTo(0.5 * NEWEST_X_RATIO, 10);
|
|
559
|
+
expect(ratios[2]).toBeCloseTo(NEWEST_X_RATIO, 10);
|
|
434
560
|
});
|
|
435
561
|
|
|
436
562
|
test("retains the authored primary market scale when its plotted series is hidden", () => {
|