@adrienhobbs/candlekit 0.1.0 → 0.2.1

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 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,22 @@ 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;
169
+ /**
170
+ * Fixed chart height in px. When omitted, the chart fills its container's
171
+ * height (observed via ResizeObserver) instead of a fixed default — so a
172
+ * flex/percent-sized parent gets a full-height chart.
173
+ */
174
+ height?: number;
175
+ /**
176
+ * When set, recenter the time scale on this trade (by id) so it scrolls into
177
+ * view. Pairs with `selectedTradeId` to drive both selection and focus.
178
+ */
179
+ focusTradeId?: string | null;
157
180
  }
158
- declare function ChartComponent({ bars, onLoadMoreData, indicators, lines, onBarUpdate, onNewBar, onDeleteLine, onAddLine, onClearAllLines, enableBarSelection, onBarClick, }: ChartComponentProps): react_jsx_runtime.JSX.Element;
181
+ declare function ChartComponent({ bars, onLoadMoreData, indicators, lines, onBarUpdate, onNewBar, onDeleteLine, onAddLine, onClearAllLines, enableBarSelection, onBarClick, trades, selectedTradeId, renderTradePopup, height, focusTradeId, }: ChartComponentProps): react_jsx_runtime.JSX.Element;
159
182
 
160
183
  interface IndicatorBrowserProps {
161
184
  isOpen: boolean;
@@ -422,4 +445,4 @@ declare function getOldestBar(bars: OHLCVBar[]): OHLCVBar | null;
422
445
  declare function getNewestBar(bars: OHLCVBar[]): OHLCVBar | null;
423
446
  declare function updateCurrentBar(bars: OHLCVBar[], tradePrice: number, tradeVolume: number): OHLCVBar[];
424
447
 
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 };
448
+ 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,12 @@ function ChartComponent({
266
296
  onAddLine,
267
297
  onClearAllLines,
268
298
  enableBarSelection = true,
269
- onBarClick
299
+ onBarClick,
300
+ trades = [],
301
+ selectedTradeId = null,
302
+ renderTradePopup,
303
+ height,
304
+ focusTradeId = null
270
305
  }) {
271
306
  const chartContainerRef = useRef(null);
272
307
  const chartRef = useRef(null);
@@ -283,14 +318,18 @@ function ChartComponent({
283
318
  const [selectedBar, setSelectedBar] = useState(null);
284
319
  const selectedBarRef = useRef(null);
285
320
  const [spotlightPosition, setSpotlightPosition] = useState(null);
321
+ const [tradePopupPos, setTradePopupPos] = useState(null);
286
322
  const previousBarsRef = useRef([]);
287
323
  const previousBarsLengthRef = useRef(0);
288
324
  const isLoadingRef = useRef(false);
289
325
  const barsRef = useRef(bars);
290
326
  const isDraggingRef = useRef(false);
291
327
  const mouseDownPosRef = useRef(null);
328
+ const heightRef = useRef(height);
329
+ heightRef.current = height;
292
330
  useEffect(() => {
293
331
  if (!chartContainerRef.current) return;
332
+ chartContainerRef.current.style.position = "relative";
294
333
  const chart = createChart(chartContainerRef.current, {
295
334
  layout: {
296
335
  background: { type: ColorType.Solid, color: "#0f172a" },
@@ -306,7 +345,10 @@ function ChartComponent({
306
345
  horzLines: { color: "#1e293b" }
307
346
  },
308
347
  width: chartContainerRef.current.clientWidth,
309
- height: 600,
348
+ // Auto-fill the container's height unless an explicit `height` is given.
349
+ // Fall back to 600 only when the container hasn't been laid out yet (a
350
+ // ResizeObserver below corrects it on first measure).
351
+ height: height ?? (chartContainerRef.current.clientHeight || 600),
310
352
  timeScale: {
311
353
  timeVisible: true,
312
354
  secondsVisible: true,
@@ -349,11 +391,9 @@ function ChartComponent({
349
391
  seriesMarkersRef.current = createSeriesMarkers(candlestickSeries, []);
350
392
  chart.timeScale().subscribeVisibleLogicalRangeChange((logicalRange) => {
351
393
  if (logicalRange && logicalRange.from < 5 && !isLoadingRef.current && onLoadMoreData) {
352
- console.log("Triggering load more, logicalRange.from:", logicalRange.from);
353
394
  isLoadingRef.current = true;
354
395
  setIsLoadingMore(true);
355
396
  previousBarsLengthRef.current = barsRef.current.length;
356
- console.log("Set previousBarsLengthRef to:", previousBarsLengthRef.current);
357
397
  const sortedBars = [...barsRef.current].sort((a, b) => a.timestamp - b.timestamp);
358
398
  const oldestTimestamp = sortedBars[0]?.timestamp;
359
399
  if (oldestTimestamp) {
@@ -363,29 +403,33 @@ function ChartComponent({
363
403
  });
364
404
  const handleResize = () => {
365
405
  if (chartContainerRef.current && chart) {
366
- chart.applyOptions({ width: chartContainerRef.current.clientWidth });
406
+ const nextHeight = heightRef.current ?? chartContainerRef.current.clientHeight;
407
+ chart.applyOptions({
408
+ width: chartContainerRef.current.clientWidth,
409
+ // Only drive height when auto-filling and the container has a real
410
+ // measured height; otherwise leave the current height untouched.
411
+ ...heightRef.current === void 0 && nextHeight > 0 ? { height: nextHeight } : {},
412
+ ...heightRef.current !== void 0 ? { height: heightRef.current } : {}
413
+ });
367
414
  }
368
415
  };
416
+ const resizeObserver = new ResizeObserver(handleResize);
417
+ resizeObserver.observe(chartContainerRef.current);
369
418
  const handleContextMenu = (e) => {
370
- console.log("Context menu event triggered!", e);
371
419
  e.preventDefault();
372
420
  e.stopPropagation();
373
421
  if (!candlestickSeriesRef.current) {
374
- console.log("No candlestick series yet");
375
422
  return;
376
423
  }
377
424
  const rect = chartContainerRef.current.getBoundingClientRect();
378
425
  const y = e.clientY - rect.top;
379
- console.log("Y position:", y, "Rect:", rect);
380
426
  const price = candlestickSeriesRef.current.coordinateToPrice(y);
381
- console.log("Calculated price:", price);
382
427
  if (price !== null) {
383
428
  setContextMenu({
384
429
  x: e.clientX - rect.left,
385
430
  y,
386
431
  price
387
432
  });
388
- console.log("Context menu set:", { x: e.clientX - rect.left, y, price });
389
433
  }
390
434
  };
391
435
  const handleMouseDown = (e) => {
@@ -405,54 +449,34 @@ function ChartComponent({
405
449
  mouseDownPosRef.current = null;
406
450
  };
407
451
  const handleClick = (e) => {
408
- console.log("=== CLICK DEBUG START ===");
409
- console.log("Click event - closing context menu");
410
452
  setContextMenu(null);
411
453
  if (isDraggingRef.current) {
412
- console.log("Ignoring click - user was dragging");
413
454
  isDraggingRef.current = false;
414
455
  return;
415
456
  }
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
457
  if (!enableBarSelection) {
421
- console.log("Bar selection is DISABLED - exiting");
422
458
  return;
423
459
  }
424
460
  if (!chartRef.current) {
425
- console.log("Chart ref is NULL - exiting");
426
461
  return;
427
462
  }
428
463
  if (!candlestickSeriesRef.current) {
429
- console.log("Candlestick series ref is NULL - exiting");
430
464
  return;
431
465
  }
432
- console.log("All checks passed - proceeding with bar selection");
433
466
  const rect = chartContainerRef.current.getBoundingClientRect();
434
467
  const x = e.clientX - rect.left;
435
- console.log("Click X coordinate:", x);
436
468
  const timeScale = chartRef.current.timeScale();
437
469
  const coordinate = timeScale.coordinateToTime(x);
438
- console.log("Coordinate from timeScale:", coordinate);
439
470
  if (!coordinate) {
440
- console.log("No coordinate from timeScale - exiting");
441
471
  return;
442
472
  }
443
473
  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
474
  const clickedBar = barsRef.current.find((bar) => {
447
475
  const diff = Math.abs(bar.timestamp / 1e3 - clickedTime);
448
476
  return diff < 300;
449
477
  });
450
- console.log("Clicked bar found:", !!clickedBar);
451
478
  if (clickedBar) {
452
- console.log("Clicked bar details:", clickedBar);
453
- console.log("Current selectedBarRef.current:", selectedBarRef.current);
454
479
  if (selectedBarRef.current && selectedBarRef.current.timestamp === clickedBar.timestamp) {
455
- console.log("Deselecting bar (same bar clicked)");
456
480
  selectedBarRef.current = null;
457
481
  setSelectedBar(null);
458
482
  setSpotlightPosition(null);
@@ -460,24 +484,18 @@ function ChartComponent({
460
484
  onBarClick(null);
461
485
  }
462
486
  } else {
463
- console.log("Selecting new bar");
464
487
  selectedBarRef.current = clickedBar;
465
488
  setSelectedBar(clickedBar);
466
489
  const barX = timeScale.timeToCoordinate(clickedBar.timestamp / 1e3);
467
- console.log("Bar X position:", barX);
468
490
  if (barX !== null) {
469
491
  const barWidth = Math.max(8, rect.width / barsRef.current.length);
470
- console.log("Bar width:", barWidth);
471
492
  setSpotlightPosition({ x: barX - barWidth / 2, width: barWidth });
472
493
  }
473
494
  if (onBarClick) {
474
495
  onBarClick(clickedBar);
475
496
  }
476
497
  }
477
- } else {
478
- console.log("No bar found at clicked position");
479
498
  }
480
- console.log("=== CLICK DEBUG END ===");
481
499
  };
482
500
  window.addEventListener("resize", handleResize);
483
501
  chartContainerRef.current.addEventListener("contextmenu", handleContextMenu, true);
@@ -488,6 +506,7 @@ function ChartComponent({
488
506
  const container = chartContainerRef.current;
489
507
  return () => {
490
508
  window.removeEventListener("resize", handleResize);
509
+ resizeObserver.disconnect();
491
510
  container.removeEventListener("contextmenu", handleContextMenu, true);
492
511
  container.removeEventListener("mousedown", handleMouseDown, true);
493
512
  container.removeEventListener("mousemove", handleMouseMove, true);
@@ -499,6 +518,31 @@ function ChartComponent({
499
518
  useEffect(() => {
500
519
  barsRef.current = bars;
501
520
  }, [bars]);
521
+ useEffect(() => {
522
+ if (chartRef.current && height !== void 0) {
523
+ chartRef.current.applyOptions({ height });
524
+ }
525
+ }, [height]);
526
+ useEffect(() => {
527
+ if (!chartRef.current || !focusTradeId) return;
528
+ const trade = trades.find((t) => t.id === focusTradeId);
529
+ if (!trade) return;
530
+ const entrySec = trade.entryTime / 1e3;
531
+ const exitSec = trade.exitTime / 1e3;
532
+ const sorted = [...bars].sort((a, b) => a.timestamp - b.timestamp);
533
+ const stepSec = sorted.length > 1 ? (sorted[sorted.length - 1].timestamp - sorted[0].timestamp) / 1e3 / (sorted.length - 1) : 300;
534
+ const pad = Math.max(exitSec - entrySec, stepSec * 12);
535
+ const raf = requestAnimationFrame(() => {
536
+ try {
537
+ chartRef.current?.timeScale().setVisibleRange({
538
+ from: entrySec - pad,
539
+ to: exitSec + pad
540
+ });
541
+ } catch {
542
+ }
543
+ });
544
+ return () => cancelAnimationFrame(raf);
545
+ }, [focusTradeId, trades, bars]);
502
546
  useEffect(() => {
503
547
  if (!candlestickSeriesRef.current || !volumeSeriesRef.current) return;
504
548
  const sortedBars = [...bars].sort((a, b) => a.timestamp - b.timestamp);
@@ -509,21 +553,15 @@ function ChartComponent({
509
553
  }
510
554
  return acc;
511
555
  }, []);
512
- console.log("Debug - bars.length:", bars.length, "uniqueBars.length:", uniqueBars.length, "isLoadingRef:", isLoadingRef.current, "previousBarsLength:", previousBarsLengthRef.current);
513
556
  const previousBars = previousBarsRef.current;
514
557
  const isInitialLoad = previousBars.length === 0;
515
558
  const hasNewBars = uniqueBars.length > previousBars.length;
516
559
  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
560
  if (isLoadingRef.current && uniqueBars.length > previousBarsLengthRef.current) {
520
- console.log("Hiding loading indicator");
521
561
  isLoadingRef.current = false;
522
562
  setIsLoadingMore(false);
523
563
  }
524
- const firstBarChanged = uniqueBars.length > 0 && previousBars.length > 0 && uniqueBars[0].timestamp !== previousBars[0].timestamp;
525
564
  if (isInitialLoad || hasNewBars) {
526
- console.log("Setting data with", uniqueBars.length, "bars", "firstBarChanged:", firstBarChanged);
527
565
  const candleData = uniqueBars.map((bar) => ({
528
566
  time: bar.timestamp / 1e3,
529
567
  open: bar.open,
@@ -536,8 +574,6 @@ function ChartComponent({
536
574
  value: bar.volume,
537
575
  color: bar.close >= bar.open ? "#10b98180" : "#ef444480"
538
576
  }));
539
- console.log("candleData sample (first 3):", candleData.slice(0, 3));
540
- console.log("candleData sample (last 3):", candleData.slice(-3));
541
577
  candlestickSeriesRef.current.setData(candleData);
542
578
  volumeSeriesRef.current.setData(volumeData);
543
579
  } else if (lastBarChanged && uniqueBars.length > 0 && !hasNewBars) {
@@ -565,7 +601,6 @@ function ChartComponent({
565
601
  candlestickSeriesRef.current?.removePriceLine(priceLine);
566
602
  });
567
603
  priceLineRefs.current.clear();
568
- const newPositions = /* @__PURE__ */ new Map();
569
604
  lines.forEach((line) => {
570
605
  const priceLine = candlestickSeriesRef.current?.createPriceLine({
571
606
  price: line.price,
@@ -578,28 +613,47 @@ function ChartComponent({
578
613
  if (priceLine) {
579
614
  priceLineRefs.current.set(line.id, priceLine);
580
615
  }
581
- const y = candlestickSeriesRef.current?.priceToCoordinate(line.price);
582
- if (y !== null && y !== void 0) {
583
- newPositions.set(line.id, y);
584
- }
585
616
  });
586
- setLinePositions(newPositions);
617
+ requestAnimationFrame(() => {
618
+ if (!candlestickSeriesRef.current || !chartContainerRef.current) return;
619
+ const rect = chartContainerRef.current.getBoundingClientRect();
620
+ const newPositions = /* @__PURE__ */ new Map();
621
+ lines.forEach((line) => {
622
+ const y = candlestickSeriesRef.current?.priceToCoordinate(line.price);
623
+ if (y !== null && y !== void 0) {
624
+ newPositions.set(line.id, {
625
+ top: rect.top + y,
626
+ right: window.innerWidth - rect.right + 68 + estimateLabelTitleWidth(line.title)
627
+ });
628
+ }
629
+ });
630
+ setLinePositions(newPositions);
631
+ });
587
632
  }, [lines]);
588
633
  useEffect(() => {
589
634
  if (!chartRef.current || !candlestickSeriesRef.current) return;
590
635
  const updatePositions = () => {
636
+ if (!candlestickSeriesRef.current || !chartContainerRef.current) return;
637
+ const rect = chartContainerRef.current.getBoundingClientRect();
591
638
  const newPositions = /* @__PURE__ */ new Map();
592
639
  lines.forEach((line) => {
593
640
  const y = candlestickSeriesRef.current?.priceToCoordinate(line.price);
594
641
  if (y !== null && y !== void 0) {
595
- newPositions.set(line.id, y);
642
+ newPositions.set(line.id, {
643
+ top: rect.top + y,
644
+ right: window.innerWidth - rect.right + 68 + estimateLabelTitleWidth(line.title)
645
+ });
596
646
  }
597
647
  });
598
648
  setLinePositions(newPositions);
599
649
  };
600
650
  chartRef.current.timeScale().subscribeVisibleLogicalRangeChange(updatePositions);
651
+ window.addEventListener("scroll", updatePositions, { passive: true });
652
+ window.addEventListener("resize", updatePositions);
601
653
  return () => {
602
654
  chartRef.current?.timeScale().unsubscribeVisibleLogicalRangeChange(updatePositions);
655
+ window.removeEventListener("scroll", updatePositions);
656
+ window.removeEventListener("resize", updatePositions);
603
657
  };
604
658
  }, [lines]);
605
659
  useEffect(() => {
@@ -719,31 +773,22 @@ function ChartComponent({
719
773
  });
720
774
  }, [bars, indicators]);
721
775
  useEffect(() => {
722
- console.log("Marker effect running, selectedBar:", selectedBar);
723
- if (!seriesMarkersRef.current) {
724
- console.log("Marker API not initialized yet.");
725
- return;
726
- }
727
- if (!enableBarSelection) {
728
- console.log("Marker effect: clearing markers, selection disabled");
729
- seriesMarkersRef.current.setMarkers([]);
730
- return;
731
- }
732
- if (!selectedBar) {
733
- console.log("Marker effect: clearing markers, no bar selected");
734
- seriesMarkersRef.current.setMarkers([]);
735
- return;
736
- }
737
- console.log("Marker effect: setting marker for bar:", selectedBar.timestamp);
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]);
776
+ if (!seriesMarkersRef.current) return;
777
+ const tradeMarkers = buildTradeMarkers(trades, selectedTradeId);
778
+ const selectionMarker = enableBarSelection && selectedBar ? [
779
+ {
780
+ time: selectedBar.timestamp / 1e3,
781
+ position: "aboveBar",
782
+ color: "#3b82f6",
783
+ shape: "circle",
784
+ text: ""
785
+ }
786
+ ] : [];
787
+ const all = [...tradeMarkers, ...selectionMarker].sort(
788
+ (a, b) => a.time - b.time
789
+ );
790
+ seriesMarkersRef.current.setMarkers(all);
791
+ }, [trades, selectedTradeId, selectedBar, enableBarSelection]);
747
792
  useEffect(() => {
748
793
  if (!chartRef.current || !selectedBar || !enableBarSelection) {
749
794
  setSpotlightPosition(null);
@@ -768,6 +813,28 @@ function ChartComponent({
768
813
  }
769
814
  };
770
815
  }, [selectedBar, enableBarSelection, bars.length]);
816
+ const selectedTrade = trades.find((t) => t.id === selectedTradeId) ?? null;
817
+ useEffect(() => {
818
+ if (!chartRef.current || !selectedTrade) {
819
+ setTradePopupPos(null);
820
+ return;
821
+ }
822
+ const updateTradePopupPos = () => {
823
+ if (!chartRef.current || !selectedTrade || !chartContainerRef.current) return;
824
+ const timeScale2 = chartRef.current.timeScale();
825
+ const x = timeScale2.timeToCoordinate(selectedTrade.entryTime / 1e3);
826
+ const width = chartContainerRef.current.getBoundingClientRect().width;
827
+ setTradePopupPos(x !== null && x >= 0 && x <= width ? { x } : null);
828
+ };
829
+ updateTradePopupPos();
830
+ const timeScale = chartRef.current.timeScale();
831
+ timeScale.subscribeVisibleLogicalRangeChange(updateTradePopupPos);
832
+ return () => {
833
+ if (chartRef.current) {
834
+ chartRef.current.timeScale().unsubscribeVisibleLogicalRangeChange(updateTradePopupPos);
835
+ }
836
+ };
837
+ }, [selectedTrade, selectedTrade?.entryTime]);
771
838
  const handleDeleteLine = (lineId) => {
772
839
  if (onDeleteLine) {
773
840
  onDeleteLine(lineId);
@@ -802,25 +869,38 @@ function ChartComponent({
802
869
  }
803
870
  }
804
871
  ),
805
- lines.map((line) => {
806
- const y = linePositions.get(line.id);
807
- if (y === null || y === void 0) return null;
808
- return /* @__PURE__ */ jsx(
872
+ selectedTrade && tradePopupPos && renderTradePopup && /* @__PURE__ */ jsx(
873
+ "div",
874
+ {
875
+ className: "absolute z-20 pointer-events-auto",
876
+ style: { left: `${tradePopupPos.x}px`, top: 8, transform: "translateX(-50%)" },
877
+ children: renderTradePopup(selectedTrade)
878
+ }
879
+ )
880
+ ] }),
881
+ chartContainerRef.current && lines.map((line) => {
882
+ const pos = linePositions.get(line.id);
883
+ if (!pos) return null;
884
+ return createPortal(
885
+ /* @__PURE__ */ jsx(
809
886
  "button",
810
887
  {
811
888
  onClick: () => handleDeleteLine(line.id),
812
- className: "absolute 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 z-20 shadow-md",
889
+ 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
890
  style: {
814
- right: "68px",
815
- top: `${y - 8}px`
891
+ position: "fixed",
892
+ top: `${pos.top - 8}px`,
893
+ right: `${pos.right}px`,
894
+ zIndex: 9999
816
895
  },
817
896
  title: "Delete line",
818
897
  children: "\xD7"
819
898
  },
820
899
  line.id
821
- );
822
- })
823
- ] }),
900
+ ),
901
+ document.body
902
+ );
903
+ }),
824
904
  contextMenu && /* @__PURE__ */ jsxs(
825
905
  "div",
826
906
  {
@@ -889,6 +969,10 @@ function ChartComponent({
889
969
  )
890
970
  ] });
891
971
  }
972
+ function estimateLabelTitleWidth(title) {
973
+ if (!title) return 0;
974
+ return title.length * 7 + 18;
975
+ }
892
976
  function getLineStyle(style) {
893
977
  switch (style) {
894
978
  case "dashed":