feat: instrument analysis
This commit is contained in:
@@ -38,6 +38,8 @@ interface Props {
|
|||||||
chartType?: 'candles' | 'line'
|
chartType?: 'candles' | 'line'
|
||||||
onDateHover?: (date: string | null) => void
|
onDateHover?: (date: string | null) => void
|
||||||
theoryCurve?: TheoPoint[]
|
theoryCurve?: TheoPoint[]
|
||||||
|
/** Called once the chart is ready with (dateToCoord, canvasPageLeft) — lets external components align with the chart's x-axis */
|
||||||
|
onChartReady?: (dateToCoord: (d: string) => number | null, canvasPageLeft: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { TheoPoint }
|
export type { TheoPoint }
|
||||||
@@ -67,7 +69,7 @@ const CAT_LABELS: Record<string, string> = {
|
|||||||
technical: 'Tech.',
|
technical: 'Tech.',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function InstrumentChart({ priceData, indicators, events = [], height = 420, chartType = 'candles', onDateHover, theoryCurve }: Props) {
|
export default function InstrumentChart({ priceData, indicators, events = [], height = 420, chartType = 'candles', onDateHover, theoryCurve, onChartReady }: Props) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const cleanupRef = useRef<(() => void) | null>(null)
|
const cleanupRef = useRef<(() => void) | null>(null)
|
||||||
const onHoverRef = useRef(onDateHover)
|
const onHoverRef = useRef(onDateHover)
|
||||||
@@ -208,6 +210,12 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
|||||||
|
|
||||||
chart.timeScale().fitContent()
|
chart.timeScale().fitContent()
|
||||||
|
|
||||||
|
// Expose coordinate mapping so siblings can align with the chart x-axis
|
||||||
|
if (onChartReady && containerRef.current) {
|
||||||
|
const canvasLeft = containerRef.current.getBoundingClientRect().left
|
||||||
|
onChartReady((d: string) => chart.timeScale().timeToCoordinate(d as any), canvasLeft)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Star overlay — ★ icons positioned via chart coordinate API ────────
|
// ── Star overlay — ★ icons positioned via chart coordinate API ────────
|
||||||
const candleMap: Record<string, number> = {}
|
const candleMap: Record<string, number> = {}
|
||||||
for (const c of priceData) candleMap[c.time] = c.high
|
for (const c of priceData) candleMap[c.time] = c.high
|
||||||
@@ -318,6 +326,7 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
|||||||
chart.timeScale().unsubscribeVisibleLogicalRangeChange(renderStars)
|
chart.timeScale().unsubscribeVisibleLogicalRangeChange(renderStars)
|
||||||
if (starsOverlay.parentNode) starsOverlay.parentNode.removeChild(starsOverlay)
|
if (starsOverlay.parentNode) starsOverlay.parentNode.removeChild(starsOverlay)
|
||||||
chart.remove()
|
chart.remove()
|
||||||
|
onChartReady?.(null as any, 0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -326,7 +335,7 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
|||||||
cleanupRef.current?.()
|
cleanupRef.current?.()
|
||||||
cleanupRef.current = null
|
cleanupRef.current = null
|
||||||
}
|
}
|
||||||
}, [priceData, indicators, events, height, chartType, theoryCurve])
|
}, [priceData, indicators, events, height, chartType, theoryCurve, onChartReady])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-dark-900/60 rounded-xl border border-slate-700/40 overflow-hidden">
|
<div className="bg-dark-900/60 rounded-xl border border-slate-700/40 overflow-hidden">
|
||||||
|
|||||||
@@ -697,27 +697,37 @@ const FRISE_POPUP_W = 220 // largeur du popup
|
|||||||
|
|
||||||
function CausalFrise({
|
function CausalFrise({
|
||||||
events, templates, priceData, selectedDate, causalInsts,
|
events, templates, priceData, selectedDate, causalInsts,
|
||||||
|
chartDateToX, chartCanvasLeft, chartReady,
|
||||||
}: {
|
}: {
|
||||||
events: SnapshotEvent[]
|
events: SnapshotEvent[]
|
||||||
templates: CausalTemplate[]
|
templates: CausalTemplate[]
|
||||||
priceData: PriceCandle[]
|
priceData: PriceCandle[]
|
||||||
selectedDate: string | null
|
selectedDate: string | null
|
||||||
causalInsts: string[]
|
causalInsts: string[]
|
||||||
|
chartDateToX?: (d: string) => number | null
|
||||||
|
chartCanvasLeft?: number
|
||||||
|
chartReady?: number
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const [contWidth, setContWidth] = useState(400)
|
const [contWidth, setContWidth] = useState(400)
|
||||||
|
const [contLeft, setContLeft] = useState(0)
|
||||||
const [activeChip, setActiveChip] = useState<{
|
const [activeChip, setActiveChip] = useState<{
|
||||||
tmpl: CausalTemplate; ev: SnapshotEvent; chipX: number; chipY: number
|
tmpl: CausalTemplate; ev: SnapshotEvent; chipX: number; chipY: number
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = containerRef.current; if (!el) return
|
const el = containerRef.current; if (!el) return
|
||||||
setContWidth(el.getBoundingClientRect().width)
|
const update = () => {
|
||||||
const obs = new ResizeObserver(e => setContWidth(e[0].contentRect.width))
|
const rect = el.getBoundingClientRect()
|
||||||
|
setContWidth(rect.width)
|
||||||
|
setContLeft(rect.left)
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
const obs = new ResizeObserver(update)
|
||||||
obs.observe(el)
|
obs.observe(el)
|
||||||
return () => obs.disconnect()
|
return () => obs.disconnect()
|
||||||
}, [])
|
}, [chartReady]) // re-measure when chart is ready (layout may have changed)
|
||||||
|
|
||||||
// Close popup on outside click (tiny delay avoids self-close)
|
// Close popup on outside click (tiny delay avoids self-close)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -745,7 +755,20 @@ function CausalFrise({
|
|||||||
const minDate = priceData[0].time
|
const minDate = priceData[0].time
|
||||||
const maxDate = priceData[priceData.length - 1].time
|
const maxDate = priceData[priceData.length - 1].time
|
||||||
const usable = Math.max(contWidth, 1)
|
const usable = Math.max(contWidth, 1)
|
||||||
const tdToX = makeTdToX(priceData, usable)
|
|
||||||
|
// If chart coordinate bridge is available, use it for pixel-perfect alignment.
|
||||||
|
// chart.timeToCoordinate() gives x relative to chart canvas left edge.
|
||||||
|
// We subtract (chartCanvasLeft - contLeft) to convert to frise container coords.
|
||||||
|
// canvasOffset > 0 means chart canvas starts to the right of the frise container.
|
||||||
|
const canvasOffset = (chartCanvasLeft ?? 0) - contLeft
|
||||||
|
const tdToX: (d: string) => number = chartDateToX
|
||||||
|
? (d: string) => {
|
||||||
|
const x = chartDateToX(d)
|
||||||
|
if (x !== null) return x - canvasOffset
|
||||||
|
// Fallback for out-of-range dates: clamp to edges
|
||||||
|
return d <= minDate ? -canvasOffset : usable - canvasOffset
|
||||||
|
}
|
||||||
|
: makeTdToX(priceData, usable)
|
||||||
|
|
||||||
// Build chips (one per event × template)
|
// Build chips (one per event × template)
|
||||||
type Chip = {
|
type Chip = {
|
||||||
@@ -1004,6 +1027,10 @@ export default function InstrumentDashboard() {
|
|||||||
const [theoryCurve, setTheoryCurve] = useState<TheoPoint[] | null>(null)
|
const [theoryCurve, setTheoryCurve] = useState<TheoPoint[] | null>(null)
|
||||||
const [loadingTheory, setLoadingTheory] = useState(false)
|
const [loadingTheory, setLoadingTheory] = useState(false)
|
||||||
const [showTheory, setShowTheory] = useState(false)
|
const [showTheory, setShowTheory] = useState(false)
|
||||||
|
// Chart coordinate bridge — lets CausalFrise align with the chart's x-axis
|
||||||
|
const chartDateToXRef = useRef<((d: string) => number | null) | null>(null)
|
||||||
|
const chartCanvasLeftRef = useRef<number>(0)
|
||||||
|
const [chartReady, setChartReady] = useState(0) // bump to trigger frise re-render
|
||||||
|
|
||||||
const instrumentId = id.toUpperCase()
|
const instrumentId = id.toUpperCase()
|
||||||
|
|
||||||
@@ -1020,6 +1047,8 @@ export default function InstrumentDashboard() {
|
|||||||
setMacroAtDate(null)
|
setMacroAtDate(null)
|
||||||
setTheoryCurve(null)
|
setTheoryCurve(null)
|
||||||
setShowTheory(false)
|
setShowTheory(false)
|
||||||
|
chartDateToXRef.current = null
|
||||||
|
setChartReady(0)
|
||||||
api.get(`/instruments/${instrumentId}/snapshot?period=${period}`)
|
api.get(`/instruments/${instrumentId}/snapshot?period=${period}`)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
setSnapshot(r.data)
|
setSnapshot(r.data)
|
||||||
@@ -1042,6 +1071,12 @@ export default function InstrumentDashboard() {
|
|||||||
if (date) setSelectedDate(date)
|
if (date) setSelectedDate(date)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleChartReady = useCallback((fn: ((d: string) => number | null) | null, left: number) => {
|
||||||
|
chartDateToXRef.current = fn
|
||||||
|
chartCanvasLeftRef.current = left
|
||||||
|
setChartReady(k => k + 1)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const toggleTheory = useCallback(() => {
|
const toggleTheory = useCallback(() => {
|
||||||
if (showTheory) {
|
if (showTheory) {
|
||||||
setShowTheory(false)
|
setShowTheory(false)
|
||||||
@@ -1253,6 +1288,7 @@ export default function InstrumentDashboard() {
|
|||||||
chartType={chartStyle}
|
chartType={chartStyle}
|
||||||
onDateHover={handleDateHover}
|
onDateHover={handleDateHover}
|
||||||
theoryCurve={showTheory && theoryCurve ? theoryCurve : undefined}
|
theoryCurve={showTheory && theoryCurve ? theoryCurve : undefined}
|
||||||
|
onChartReady={handleChartReady}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Date badge */}
|
{/* Date badge */}
|
||||||
@@ -1342,6 +1378,9 @@ export default function InstrumentDashboard() {
|
|||||||
priceData={snapshot.price_data}
|
priceData={snapshot.price_data}
|
||||||
selectedDate={effectiveDate}
|
selectedDate={effectiveDate}
|
||||||
causalInsts={causalInsts}
|
causalInsts={causalInsts}
|
||||||
|
chartDateToX={chartDateToXRef.current ?? undefined}
|
||||||
|
chartCanvasLeft={chartCanvasLeftRef.current}
|
||||||
|
chartReady={chartReady}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user