@adrienhobbs/candlekit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +15 -3
- package/dist/index.js +126 -84
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
|
|
4
5
|
interface OHLCVBar {
|
|
@@ -18,7 +19,15 @@ interface ChartLine {
|
|
|
18
19
|
lineWidth?: number;
|
|
19
20
|
lineStyle?: 'solid' | 'dashed' | 'dotted';
|
|
20
21
|
title?: string;
|
|
21
|
-
type?: 'entry' | 'stopLoss' | 'takeProfit';
|
|
22
|
+
type?: 'entry' | 'stopLoss' | 'takeProfit' | 'mfe' | 'mae';
|
|
23
|
+
}
|
|
24
|
+
interface ChartTrade {
|
|
25
|
+
id: string;
|
|
26
|
+
entryTime: number;
|
|
27
|
+
exitTime: number;
|
|
28
|
+
entryPrice: number;
|
|
29
|
+
exitPrice: number;
|
|
30
|
+
outcome: 'win' | 'loss';
|
|
22
31
|
}
|
|
23
32
|
interface IndicatorSettings$1 {
|
|
24
33
|
[key: string]: any;
|
|
@@ -154,8 +163,11 @@ interface ChartComponentProps {
|
|
|
154
163
|
onClearAllLines?: () => void;
|
|
155
164
|
enableBarSelection?: boolean;
|
|
156
165
|
onBarClick?: (bar: OHLCVBar | null) => void;
|
|
166
|
+
trades?: ChartTrade[];
|
|
167
|
+
selectedTradeId?: string | null;
|
|
168
|
+
renderTradePopup?: (trade: ChartTrade) => ReactNode;
|
|
157
169
|
}
|
|
158
|
-
declare function ChartComponent({ bars, onLoadMoreData, indicators, lines, onBarUpdate, onNewBar, onDeleteLine, onAddLine, onClearAllLines, enableBarSelection, onBarClick, }: ChartComponentProps): react_jsx_runtime.JSX.Element;
|
|
170
|
+
declare function ChartComponent({ bars, onLoadMoreData, indicators, lines, onBarUpdate, onNewBar, onDeleteLine, onAddLine, onClearAllLines, enableBarSelection, onBarClick, trades, selectedTradeId, renderTradePopup, }: ChartComponentProps): react_jsx_runtime.JSX.Element;
|
|
159
171
|
|
|
160
172
|
interface IndicatorBrowserProps {
|
|
161
173
|
isOpen: boolean;
|
|
@@ -422,4 +434,4 @@ declare function getOldestBar(bars: OHLCVBar[]): OHLCVBar | null;
|
|
|
422
434
|
declare function getNewestBar(bars: OHLCVBar[]): OHLCVBar | null;
|
|
423
435
|
declare function updateCurrentBar(bars: OHLCVBar[], tradePrice: number, tradeVolume: number): OHLCVBar[];
|
|
424
436
|
|
|
425
|
-
export { ADXIndicator, ATRIndicator, AlpacaBarAdapter, type BarDataAdapter, type BarDataAdapterOptions, BollingerBandsIndicator, CCIIndicator, ChartComponent, type ChartLine, ChartSeriesType, DonchianChannelsIndicator, EMAIndicator, ForceIndexIndicator, type HistoricalDataParams, IchimokuIndicator, IndicatorBrowser, type IndicatorCalculation, IndicatorCategory, type IndicatorDefinition, type IndicatorInstance, IndicatorInstanceSchema, type IndicatorMetadata, IndicatorMetadataSchema, type IndicatorOutput, type IndicatorPanel, type IndicatorSettings$1 as IndicatorSettings, IndicatorSettingsForm, IndicatorSettingsSchema, KeltnerChannelsIndicator, type LineStyle, LineStyleSchema, LocalStoragePersistenceAdapter, MACDIndicator, MFIIndicator, MockAdapter, NoOpPersistenceAdapter, OBVIndicator, type OHLCVBar, PSARIndicator, type PersistenceAdapter, ROCIndicator, RSIIndicator, type RealtimeHandlers, type RealtimeSubscription, type RenderConfig, RenderConfigSchema, SMAIndicator, type SettingField, SettingFieldSchema, type SettingFieldType, SettingFieldTypeSchema, SettingsDialog, StochRSIIndicator, StochasticIndicator, SuperTrendIndicator, VWAPIndicator, WMAIndicator, WilliamsRIndicator, appendBar, calculateBollingerBands, calculateEMA, calculateRSI, calculateSMA, calculateStandardDeviation, createPersistenceAdapter, deduplicateBars, displaceArray, getNewestBar, getOldestBar, indicatorCalculator, indicatorRegistry, isValidBar, mergeBars, normalizeTimestamp, padIndicatorArray, prependBars, registerBuiltInIndicators, sortBars, updateBarInArray, updateCurrentBar, useBarsData, useChartAPI, useRealtimeUpdates, validateAndNormalizeBars, validateBar };
|
|
437
|
+
export { ADXIndicator, ATRIndicator, AlpacaBarAdapter, type BarDataAdapter, type BarDataAdapterOptions, BollingerBandsIndicator, CCIIndicator, ChartComponent, type ChartLine, ChartSeriesType, type ChartTrade, DonchianChannelsIndicator, EMAIndicator, ForceIndexIndicator, type HistoricalDataParams, IchimokuIndicator, IndicatorBrowser, type IndicatorCalculation, IndicatorCategory, type IndicatorDefinition, type IndicatorInstance, IndicatorInstanceSchema, type IndicatorMetadata, IndicatorMetadataSchema, type IndicatorOutput, type IndicatorPanel, type IndicatorSettings$1 as IndicatorSettings, IndicatorSettingsForm, IndicatorSettingsSchema, KeltnerChannelsIndicator, type LineStyle, LineStyleSchema, LocalStoragePersistenceAdapter, MACDIndicator, MFIIndicator, MockAdapter, NoOpPersistenceAdapter, OBVIndicator, type OHLCVBar, PSARIndicator, type PersistenceAdapter, ROCIndicator, RSIIndicator, type RealtimeHandlers, type RealtimeSubscription, type RenderConfig, RenderConfigSchema, SMAIndicator, type SettingField, SettingFieldSchema, type SettingFieldType, SettingFieldTypeSchema, SettingsDialog, StochRSIIndicator, StochasticIndicator, SuperTrendIndicator, VWAPIndicator, WMAIndicator, WilliamsRIndicator, appendBar, calculateBollingerBands, calculateEMA, calculateRSI, calculateSMA, calculateStandardDeviation, createPersistenceAdapter, deduplicateBars, displaceArray, getNewestBar, getOldestBar, indicatorCalculator, indicatorRegistry, isValidBar, mergeBars, normalizeTimestamp, padIndicatorArray, prependBars, registerBuiltInIndicators, sortBars, updateBarInArray, updateCurrentBar, useBarsData, useChartAPI, useRealtimeUpdates, validateAndNormalizeBars, validateBar };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useRef, useState, useEffect, useMemo, useCallback } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
import { createChart, ColorType, CandlestickSeries, HistogramSeries, createSeriesMarkers, LineSeries, AreaSeries, LineStyle } from 'lightweight-charts';
|
|
3
4
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
5
|
import { X, Search, Plus } from 'lucide-react';
|
|
@@ -255,6 +256,35 @@ var BandsPrimitive = class {
|
|
|
255
256
|
this._paneViews.forEach((pw) => pw.update());
|
|
256
257
|
}
|
|
257
258
|
};
|
|
259
|
+
|
|
260
|
+
// src/components/trade-markers.ts
|
|
261
|
+
var WIN = "#10b981";
|
|
262
|
+
var LOSS = "#ef4444";
|
|
263
|
+
var SEL = "#3b82f6";
|
|
264
|
+
function buildTradeMarkers(trades, selectedTradeId) {
|
|
265
|
+
const markers = [];
|
|
266
|
+
for (const t of trades) {
|
|
267
|
+
const selected = t.id === selectedTradeId;
|
|
268
|
+
const base = t.outcome === "win" ? WIN : LOSS;
|
|
269
|
+
const color = selected ? SEL : base;
|
|
270
|
+
markers.push({
|
|
271
|
+
time: t.entryTime / 1e3,
|
|
272
|
+
position: "belowBar",
|
|
273
|
+
shape: "arrowUp",
|
|
274
|
+
color,
|
|
275
|
+
text: selected ? `${t.outcome === "win" ? "+" : ""}entry` : ""
|
|
276
|
+
});
|
|
277
|
+
markers.push({
|
|
278
|
+
time: t.exitTime / 1e3,
|
|
279
|
+
position: "aboveBar",
|
|
280
|
+
shape: "arrowDown",
|
|
281
|
+
color,
|
|
282
|
+
text: selected ? "exit" : ""
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
markers.sort((a, b) => a.time - b.time);
|
|
286
|
+
return markers;
|
|
287
|
+
}
|
|
258
288
|
function ChartComponent({
|
|
259
289
|
bars,
|
|
260
290
|
onLoadMoreData,
|
|
@@ -266,7 +296,10 @@ function ChartComponent({
|
|
|
266
296
|
onAddLine,
|
|
267
297
|
onClearAllLines,
|
|
268
298
|
enableBarSelection = true,
|
|
269
|
-
onBarClick
|
|
299
|
+
onBarClick,
|
|
300
|
+
trades = [],
|
|
301
|
+
selectedTradeId = null,
|
|
302
|
+
renderTradePopup
|
|
270
303
|
}) {
|
|
271
304
|
const chartContainerRef = useRef(null);
|
|
272
305
|
const chartRef = useRef(null);
|
|
@@ -283,6 +316,7 @@ function ChartComponent({
|
|
|
283
316
|
const [selectedBar, setSelectedBar] = useState(null);
|
|
284
317
|
const selectedBarRef = useRef(null);
|
|
285
318
|
const [spotlightPosition, setSpotlightPosition] = useState(null);
|
|
319
|
+
const [tradePopupPos, setTradePopupPos] = useState(null);
|
|
286
320
|
const previousBarsRef = useRef([]);
|
|
287
321
|
const previousBarsLengthRef = useRef(0);
|
|
288
322
|
const isLoadingRef = useRef(false);
|
|
@@ -291,6 +325,7 @@ function ChartComponent({
|
|
|
291
325
|
const mouseDownPosRef = useRef(null);
|
|
292
326
|
useEffect(() => {
|
|
293
327
|
if (!chartContainerRef.current) return;
|
|
328
|
+
chartContainerRef.current.style.position = "relative";
|
|
294
329
|
const chart = createChart(chartContainerRef.current, {
|
|
295
330
|
layout: {
|
|
296
331
|
background: { type: ColorType.Solid, color: "#0f172a" },
|
|
@@ -349,11 +384,9 @@ function ChartComponent({
|
|
|
349
384
|
seriesMarkersRef.current = createSeriesMarkers(candlestickSeries, []);
|
|
350
385
|
chart.timeScale().subscribeVisibleLogicalRangeChange((logicalRange) => {
|
|
351
386
|
if (logicalRange && logicalRange.from < 5 && !isLoadingRef.current && onLoadMoreData) {
|
|
352
|
-
console.log("Triggering load more, logicalRange.from:", logicalRange.from);
|
|
353
387
|
isLoadingRef.current = true;
|
|
354
388
|
setIsLoadingMore(true);
|
|
355
389
|
previousBarsLengthRef.current = barsRef.current.length;
|
|
356
|
-
console.log("Set previousBarsLengthRef to:", previousBarsLengthRef.current);
|
|
357
390
|
const sortedBars = [...barsRef.current].sort((a, b) => a.timestamp - b.timestamp);
|
|
358
391
|
const oldestTimestamp = sortedBars[0]?.timestamp;
|
|
359
392
|
if (oldestTimestamp) {
|
|
@@ -367,25 +400,20 @@ function ChartComponent({
|
|
|
367
400
|
}
|
|
368
401
|
};
|
|
369
402
|
const handleContextMenu = (e) => {
|
|
370
|
-
console.log("Context menu event triggered!", e);
|
|
371
403
|
e.preventDefault();
|
|
372
404
|
e.stopPropagation();
|
|
373
405
|
if (!candlestickSeriesRef.current) {
|
|
374
|
-
console.log("No candlestick series yet");
|
|
375
406
|
return;
|
|
376
407
|
}
|
|
377
408
|
const rect = chartContainerRef.current.getBoundingClientRect();
|
|
378
409
|
const y = e.clientY - rect.top;
|
|
379
|
-
console.log("Y position:", y, "Rect:", rect);
|
|
380
410
|
const price = candlestickSeriesRef.current.coordinateToPrice(y);
|
|
381
|
-
console.log("Calculated price:", price);
|
|
382
411
|
if (price !== null) {
|
|
383
412
|
setContextMenu({
|
|
384
413
|
x: e.clientX - rect.left,
|
|
385
414
|
y,
|
|
386
415
|
price
|
|
387
416
|
});
|
|
388
|
-
console.log("Context menu set:", { x: e.clientX - rect.left, y, price });
|
|
389
417
|
}
|
|
390
418
|
};
|
|
391
419
|
const handleMouseDown = (e) => {
|
|
@@ -405,54 +433,34 @@ function ChartComponent({
|
|
|
405
433
|
mouseDownPosRef.current = null;
|
|
406
434
|
};
|
|
407
435
|
const handleClick = (e) => {
|
|
408
|
-
console.log("=== CLICK DEBUG START ===");
|
|
409
|
-
console.log("Click event - closing context menu");
|
|
410
436
|
setContextMenu(null);
|
|
411
437
|
if (isDraggingRef.current) {
|
|
412
|
-
console.log("Ignoring click - user was dragging");
|
|
413
438
|
isDraggingRef.current = false;
|
|
414
439
|
return;
|
|
415
440
|
}
|
|
416
|
-
console.log("enableBarSelection prop:", enableBarSelection);
|
|
417
|
-
console.log("chartRef.current:", !!chartRef.current);
|
|
418
|
-
console.log("candlestickSeriesRef.current:", !!candlestickSeriesRef.current);
|
|
419
|
-
console.log("bars.length:", bars.length);
|
|
420
441
|
if (!enableBarSelection) {
|
|
421
|
-
console.log("Bar selection is DISABLED - exiting");
|
|
422
442
|
return;
|
|
423
443
|
}
|
|
424
444
|
if (!chartRef.current) {
|
|
425
|
-
console.log("Chart ref is NULL - exiting");
|
|
426
445
|
return;
|
|
427
446
|
}
|
|
428
447
|
if (!candlestickSeriesRef.current) {
|
|
429
|
-
console.log("Candlestick series ref is NULL - exiting");
|
|
430
448
|
return;
|
|
431
449
|
}
|
|
432
|
-
console.log("All checks passed - proceeding with bar selection");
|
|
433
450
|
const rect = chartContainerRef.current.getBoundingClientRect();
|
|
434
451
|
const x = e.clientX - rect.left;
|
|
435
|
-
console.log("Click X coordinate:", x);
|
|
436
452
|
const timeScale = chartRef.current.timeScale();
|
|
437
453
|
const coordinate = timeScale.coordinateToTime(x);
|
|
438
|
-
console.log("Coordinate from timeScale:", coordinate);
|
|
439
454
|
if (!coordinate) {
|
|
440
|
-
console.log("No coordinate from timeScale - exiting");
|
|
441
455
|
return;
|
|
442
456
|
}
|
|
443
457
|
const clickedTime = coordinate;
|
|
444
|
-
console.log("Clicked time:", clickedTime);
|
|
445
|
-
console.log("Sample bar timestamps (first 3):", barsRef.current.slice(0, 3).map((b) => b.timestamp / 1e3));
|
|
446
458
|
const clickedBar = barsRef.current.find((bar) => {
|
|
447
459
|
const diff = Math.abs(bar.timestamp / 1e3 - clickedTime);
|
|
448
460
|
return diff < 300;
|
|
449
461
|
});
|
|
450
|
-
console.log("Clicked bar found:", !!clickedBar);
|
|
451
462
|
if (clickedBar) {
|
|
452
|
-
console.log("Clicked bar details:", clickedBar);
|
|
453
|
-
console.log("Current selectedBarRef.current:", selectedBarRef.current);
|
|
454
463
|
if (selectedBarRef.current && selectedBarRef.current.timestamp === clickedBar.timestamp) {
|
|
455
|
-
console.log("Deselecting bar (same bar clicked)");
|
|
456
464
|
selectedBarRef.current = null;
|
|
457
465
|
setSelectedBar(null);
|
|
458
466
|
setSpotlightPosition(null);
|
|
@@ -460,24 +468,18 @@ function ChartComponent({
|
|
|
460
468
|
onBarClick(null);
|
|
461
469
|
}
|
|
462
470
|
} else {
|
|
463
|
-
console.log("Selecting new bar");
|
|
464
471
|
selectedBarRef.current = clickedBar;
|
|
465
472
|
setSelectedBar(clickedBar);
|
|
466
473
|
const barX = timeScale.timeToCoordinate(clickedBar.timestamp / 1e3);
|
|
467
|
-
console.log("Bar X position:", barX);
|
|
468
474
|
if (barX !== null) {
|
|
469
475
|
const barWidth = Math.max(8, rect.width / barsRef.current.length);
|
|
470
|
-
console.log("Bar width:", barWidth);
|
|
471
476
|
setSpotlightPosition({ x: barX - barWidth / 2, width: barWidth });
|
|
472
477
|
}
|
|
473
478
|
if (onBarClick) {
|
|
474
479
|
onBarClick(clickedBar);
|
|
475
480
|
}
|
|
476
481
|
}
|
|
477
|
-
} else {
|
|
478
|
-
console.log("No bar found at clicked position");
|
|
479
482
|
}
|
|
480
|
-
console.log("=== CLICK DEBUG END ===");
|
|
481
483
|
};
|
|
482
484
|
window.addEventListener("resize", handleResize);
|
|
483
485
|
chartContainerRef.current.addEventListener("contextmenu", handleContextMenu, true);
|
|
@@ -509,21 +511,15 @@ function ChartComponent({
|
|
|
509
511
|
}
|
|
510
512
|
return acc;
|
|
511
513
|
}, []);
|
|
512
|
-
console.log("Debug - bars.length:", bars.length, "uniqueBars.length:", uniqueBars.length, "isLoadingRef:", isLoadingRef.current, "previousBarsLength:", previousBarsLengthRef.current);
|
|
513
514
|
const previousBars = previousBarsRef.current;
|
|
514
515
|
const isInitialLoad = previousBars.length === 0;
|
|
515
516
|
const hasNewBars = uniqueBars.length > previousBars.length;
|
|
516
517
|
const lastBarChanged = uniqueBars.length > 0 && previousBars.length > 0 && uniqueBars[uniqueBars.length - 1].timestamp === previousBars[previousBars.length - 1].timestamp;
|
|
517
|
-
console.log("isInitialLoad:", isInitialLoad, "hasNewBars:", hasNewBars, "lastBarChanged:", lastBarChanged);
|
|
518
|
-
console.log("previousBars.length:", previousBars.length, "uniqueBars.length:", uniqueBars.length);
|
|
519
518
|
if (isLoadingRef.current && uniqueBars.length > previousBarsLengthRef.current) {
|
|
520
|
-
console.log("Hiding loading indicator");
|
|
521
519
|
isLoadingRef.current = false;
|
|
522
520
|
setIsLoadingMore(false);
|
|
523
521
|
}
|
|
524
|
-
const firstBarChanged = uniqueBars.length > 0 && previousBars.length > 0 && uniqueBars[0].timestamp !== previousBars[0].timestamp;
|
|
525
522
|
if (isInitialLoad || hasNewBars) {
|
|
526
|
-
console.log("Setting data with", uniqueBars.length, "bars", "firstBarChanged:", firstBarChanged);
|
|
527
523
|
const candleData = uniqueBars.map((bar) => ({
|
|
528
524
|
time: bar.timestamp / 1e3,
|
|
529
525
|
open: bar.open,
|
|
@@ -536,8 +532,6 @@ function ChartComponent({
|
|
|
536
532
|
value: bar.volume,
|
|
537
533
|
color: bar.close >= bar.open ? "#10b98180" : "#ef444480"
|
|
538
534
|
}));
|
|
539
|
-
console.log("candleData sample (first 3):", candleData.slice(0, 3));
|
|
540
|
-
console.log("candleData sample (last 3):", candleData.slice(-3));
|
|
541
535
|
candlestickSeriesRef.current.setData(candleData);
|
|
542
536
|
volumeSeriesRef.current.setData(volumeData);
|
|
543
537
|
} else if (lastBarChanged && uniqueBars.length > 0 && !hasNewBars) {
|
|
@@ -565,7 +559,6 @@ function ChartComponent({
|
|
|
565
559
|
candlestickSeriesRef.current?.removePriceLine(priceLine);
|
|
566
560
|
});
|
|
567
561
|
priceLineRefs.current.clear();
|
|
568
|
-
const newPositions = /* @__PURE__ */ new Map();
|
|
569
562
|
lines.forEach((line) => {
|
|
570
563
|
const priceLine = candlestickSeriesRef.current?.createPriceLine({
|
|
571
564
|
price: line.price,
|
|
@@ -578,28 +571,47 @@ function ChartComponent({
|
|
|
578
571
|
if (priceLine) {
|
|
579
572
|
priceLineRefs.current.set(line.id, priceLine);
|
|
580
573
|
}
|
|
581
|
-
const y = candlestickSeriesRef.current?.priceToCoordinate(line.price);
|
|
582
|
-
if (y !== null && y !== void 0) {
|
|
583
|
-
newPositions.set(line.id, y);
|
|
584
|
-
}
|
|
585
574
|
});
|
|
586
|
-
|
|
575
|
+
requestAnimationFrame(() => {
|
|
576
|
+
if (!candlestickSeriesRef.current || !chartContainerRef.current) return;
|
|
577
|
+
const rect = chartContainerRef.current.getBoundingClientRect();
|
|
578
|
+
const newPositions = /* @__PURE__ */ new Map();
|
|
579
|
+
lines.forEach((line) => {
|
|
580
|
+
const y = candlestickSeriesRef.current?.priceToCoordinate(line.price);
|
|
581
|
+
if (y !== null && y !== void 0) {
|
|
582
|
+
newPositions.set(line.id, {
|
|
583
|
+
top: rect.top + y,
|
|
584
|
+
right: window.innerWidth - rect.right + 68 + estimateLabelTitleWidth(line.title)
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
setLinePositions(newPositions);
|
|
589
|
+
});
|
|
587
590
|
}, [lines]);
|
|
588
591
|
useEffect(() => {
|
|
589
592
|
if (!chartRef.current || !candlestickSeriesRef.current) return;
|
|
590
593
|
const updatePositions = () => {
|
|
594
|
+
if (!candlestickSeriesRef.current || !chartContainerRef.current) return;
|
|
595
|
+
const rect = chartContainerRef.current.getBoundingClientRect();
|
|
591
596
|
const newPositions = /* @__PURE__ */ new Map();
|
|
592
597
|
lines.forEach((line) => {
|
|
593
598
|
const y = candlestickSeriesRef.current?.priceToCoordinate(line.price);
|
|
594
599
|
if (y !== null && y !== void 0) {
|
|
595
|
-
newPositions.set(line.id,
|
|
600
|
+
newPositions.set(line.id, {
|
|
601
|
+
top: rect.top + y,
|
|
602
|
+
right: window.innerWidth - rect.right + 68 + estimateLabelTitleWidth(line.title)
|
|
603
|
+
});
|
|
596
604
|
}
|
|
597
605
|
});
|
|
598
606
|
setLinePositions(newPositions);
|
|
599
607
|
};
|
|
600
608
|
chartRef.current.timeScale().subscribeVisibleLogicalRangeChange(updatePositions);
|
|
609
|
+
window.addEventListener("scroll", updatePositions, { passive: true });
|
|
610
|
+
window.addEventListener("resize", updatePositions);
|
|
601
611
|
return () => {
|
|
602
612
|
chartRef.current?.timeScale().unsubscribeVisibleLogicalRangeChange(updatePositions);
|
|
613
|
+
window.removeEventListener("scroll", updatePositions);
|
|
614
|
+
window.removeEventListener("resize", updatePositions);
|
|
603
615
|
};
|
|
604
616
|
}, [lines]);
|
|
605
617
|
useEffect(() => {
|
|
@@ -719,31 +731,22 @@ function ChartComponent({
|
|
|
719
731
|
});
|
|
720
732
|
}, [bars, indicators]);
|
|
721
733
|
useEffect(() => {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const marker = {
|
|
739
|
-
time: selectedBar.timestamp / 1e3,
|
|
740
|
-
position: "aboveBar",
|
|
741
|
-
color: "#3b82f6",
|
|
742
|
-
shape: "circle",
|
|
743
|
-
text: "Selected"
|
|
744
|
-
};
|
|
745
|
-
seriesMarkersRef.current.setMarkers([marker]);
|
|
746
|
-
}, [selectedBar, enableBarSelection]);
|
|
734
|
+
if (!seriesMarkersRef.current) return;
|
|
735
|
+
const tradeMarkers = buildTradeMarkers(trades, selectedTradeId);
|
|
736
|
+
const selectionMarker = enableBarSelection && selectedBar ? [
|
|
737
|
+
{
|
|
738
|
+
time: selectedBar.timestamp / 1e3,
|
|
739
|
+
position: "aboveBar",
|
|
740
|
+
color: "#3b82f6",
|
|
741
|
+
shape: "circle",
|
|
742
|
+
text: ""
|
|
743
|
+
}
|
|
744
|
+
] : [];
|
|
745
|
+
const all = [...tradeMarkers, ...selectionMarker].sort(
|
|
746
|
+
(a, b) => a.time - b.time
|
|
747
|
+
);
|
|
748
|
+
seriesMarkersRef.current.setMarkers(all);
|
|
749
|
+
}, [trades, selectedTradeId, selectedBar, enableBarSelection]);
|
|
747
750
|
useEffect(() => {
|
|
748
751
|
if (!chartRef.current || !selectedBar || !enableBarSelection) {
|
|
749
752
|
setSpotlightPosition(null);
|
|
@@ -768,6 +771,28 @@ function ChartComponent({
|
|
|
768
771
|
}
|
|
769
772
|
};
|
|
770
773
|
}, [selectedBar, enableBarSelection, bars.length]);
|
|
774
|
+
const selectedTrade = trades.find((t) => t.id === selectedTradeId) ?? null;
|
|
775
|
+
useEffect(() => {
|
|
776
|
+
if (!chartRef.current || !selectedTrade) {
|
|
777
|
+
setTradePopupPos(null);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const updateTradePopupPos = () => {
|
|
781
|
+
if (!chartRef.current || !selectedTrade || !chartContainerRef.current) return;
|
|
782
|
+
const timeScale2 = chartRef.current.timeScale();
|
|
783
|
+
const x = timeScale2.timeToCoordinate(selectedTrade.entryTime / 1e3);
|
|
784
|
+
const width = chartContainerRef.current.getBoundingClientRect().width;
|
|
785
|
+
setTradePopupPos(x !== null && x >= 0 && x <= width ? { x } : null);
|
|
786
|
+
};
|
|
787
|
+
updateTradePopupPos();
|
|
788
|
+
const timeScale = chartRef.current.timeScale();
|
|
789
|
+
timeScale.subscribeVisibleLogicalRangeChange(updateTradePopupPos);
|
|
790
|
+
return () => {
|
|
791
|
+
if (chartRef.current) {
|
|
792
|
+
chartRef.current.timeScale().unsubscribeVisibleLogicalRangeChange(updateTradePopupPos);
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
}, [selectedTrade, selectedTrade?.entryTime]);
|
|
771
796
|
const handleDeleteLine = (lineId) => {
|
|
772
797
|
if (onDeleteLine) {
|
|
773
798
|
onDeleteLine(lineId);
|
|
@@ -802,25 +827,38 @@ function ChartComponent({
|
|
|
802
827
|
}
|
|
803
828
|
}
|
|
804
829
|
),
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
830
|
+
selectedTrade && tradePopupPos && renderTradePopup && /* @__PURE__ */ jsx(
|
|
831
|
+
"div",
|
|
832
|
+
{
|
|
833
|
+
className: "absolute z-20 pointer-events-auto",
|
|
834
|
+
style: { left: `${tradePopupPos.x}px`, top: 8, transform: "translateX(-50%)" },
|
|
835
|
+
children: renderTradePopup(selectedTrade)
|
|
836
|
+
}
|
|
837
|
+
)
|
|
838
|
+
] }),
|
|
839
|
+
chartContainerRef.current && lines.map((line) => {
|
|
840
|
+
const pos = linePositions.get(line.id);
|
|
841
|
+
if (!pos) return null;
|
|
842
|
+
return createPortal(
|
|
843
|
+
/* @__PURE__ */ jsx(
|
|
809
844
|
"button",
|
|
810
845
|
{
|
|
811
846
|
onClick: () => handleDeleteLine(line.id),
|
|
812
|
-
className: "
|
|
847
|
+
className: "bg-red-500/70 hover:bg-red-500 text-white rounded-full w-4 h-4 flex items-center justify-center text-xs font-bold transition-colors shadow-md",
|
|
813
848
|
style: {
|
|
814
|
-
|
|
815
|
-
top: `${
|
|
849
|
+
position: "fixed",
|
|
850
|
+
top: `${pos.top - 8}px`,
|
|
851
|
+
right: `${pos.right}px`,
|
|
852
|
+
zIndex: 9999
|
|
816
853
|
},
|
|
817
854
|
title: "Delete line",
|
|
818
855
|
children: "\xD7"
|
|
819
856
|
},
|
|
820
857
|
line.id
|
|
821
|
-
)
|
|
822
|
-
|
|
823
|
-
|
|
858
|
+
),
|
|
859
|
+
document.body
|
|
860
|
+
);
|
|
861
|
+
}),
|
|
824
862
|
contextMenu && /* @__PURE__ */ jsxs(
|
|
825
863
|
"div",
|
|
826
864
|
{
|
|
@@ -889,6 +927,10 @@ function ChartComponent({
|
|
|
889
927
|
)
|
|
890
928
|
] });
|
|
891
929
|
}
|
|
930
|
+
function estimateLabelTitleWidth(title) {
|
|
931
|
+
if (!title) return 0;
|
|
932
|
+
return title.length * 7 + 18;
|
|
933
|
+
}
|
|
892
934
|
function getLineStyle(style) {
|
|
893
935
|
switch (style) {
|
|
894
936
|
case "dashed":
|