ui: tighten dashboard copy and fix layout issues
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { TrumpPost, Candle } from '@/types'
|
||||
import { useDashboardStore } from '@/store/dashboard'
|
||||
|
||||
@@ -33,6 +33,16 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
// subscribeVisibleTimeRangeChange callback (when the user pans / zooms).
|
||||
const repositionMacroRef = useRef<(() => void) | null>(null)
|
||||
const fittedRef = useRef(false)
|
||||
// Chart creation is async (dynamic import). If the candles fetch wins the
|
||||
// race (common — SWR serves them instantly from cache), the data effect
|
||||
// runs while seriesRef is still null and bails; nothing re-triggers it
|
||||
// afterwards, so the chart stays EMPTY and the 2s live-tick effect then
|
||||
// paints a single lone candle via series.update(). This state flips when
|
||||
// the chart actually exists so the data effect re-runs.
|
||||
const [chartReady, setChartReady] = useState(false)
|
||||
// The live-tick effect must never run before setData() — updating an empty
|
||||
// series creates a one-candle chart.
|
||||
const dataLoadedRef = useRef(false)
|
||||
const postsRef = useRef(posts)
|
||||
postsRef.current = posts
|
||||
const selectedPostIdRef = useRef(externalSelectedId)
|
||||
@@ -111,7 +121,11 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
|
||||
const chart = createChart(containerRef.current, {
|
||||
width: containerRef.current.clientWidth,
|
||||
height: 360,
|
||||
// Follow the container, don't hardcode: .chart-wrap is 360px on
|
||||
// desktop but 300/240px behind !important media queries. A fixed
|
||||
// 360 overflows those and overflow:hidden cuts off the bottom of
|
||||
// the chart — lowest candles, markers, and the whole time axis.
|
||||
height: containerRef.current.clientHeight || 360,
|
||||
layout: {
|
||||
background: { color: colors.background },
|
||||
textColor: colors.textColor,
|
||||
@@ -145,6 +159,16 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
|
||||
seriesRef.current = series
|
||||
|
||||
// Breathing room above/below the candles. Markers render ~10px outside
|
||||
// the bar's high/low; with the default tight bottom margin a crash
|
||||
// candle at the bottom of the autoscaled range gets its marker dots
|
||||
// clipped by the pane edge.
|
||||
series.priceScale().applyOptions({
|
||||
scaleMargins: { top: 0.12, bottom: 0.16 },
|
||||
})
|
||||
|
||||
setChartReady(true)
|
||||
|
||||
// Re-project macro signal time → pixel X on every visible-range
|
||||
// change (pan / zoom / data update / resize). Without this the line
|
||||
// is set ONCE in the data effect and then stays stuck at a fixed
|
||||
@@ -226,7 +250,10 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
|
||||
ro = new ResizeObserver(() => {
|
||||
if (containerRef.current && !destroyed) {
|
||||
chart.applyOptions({ width: containerRef.current.clientWidth })
|
||||
chart.applyOptions({
|
||||
width: containerRef.current.clientWidth,
|
||||
height: containerRef.current.clientHeight || 360,
|
||||
})
|
||||
// Width change → new pixel coordinates → re-project the macro
|
||||
// marker too, otherwise it'd drift relative to the candles on
|
||||
// window resize / sidebar toggle.
|
||||
@@ -269,6 +296,8 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
ro?.disconnect()
|
||||
themeObserver?.disconnect()
|
||||
fittedRef.current = false
|
||||
dataLoadedRef.current = false
|
||||
setChartReady(false)
|
||||
if (chartRef.current) {
|
||||
// @ts-expect-error lightweight-charts type
|
||||
chartRef.current.remove()
|
||||
@@ -291,7 +320,10 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
useEffect(() => {
|
||||
const series = seriesRef.current
|
||||
const chart = chartRef.current
|
||||
if (!series || !chart || candles.length === 0) return
|
||||
if (!series || !chart || candles.length === 0) {
|
||||
dataLoadedRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
const sorted = [...candles].sort((a, b) => a.time - b.time)
|
||||
|
||||
@@ -303,6 +335,7 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
low: c.low,
|
||||
close: c.close,
|
||||
})))
|
||||
dataLoadedRef.current = true
|
||||
|
||||
const minTime = sorted[0].time
|
||||
const maxTime = sorted[sorted.length - 1].time
|
||||
@@ -313,6 +346,13 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
return t >= minTime && t <= maxTime
|
||||
})
|
||||
|
||||
// Markers show actionable signals only. Hold/filtered posts vastly
|
||||
// outnumber signals and their gray dots pile into unreadable clusters;
|
||||
// they stay browsable in the Recent-signals list instead.
|
||||
const markerPosts = visible.filter(
|
||||
(p) => p.signal === 'buy' || p.signal === 'short' || p.signal === 'sell',
|
||||
)
|
||||
|
||||
const bucketByTf: Record<string, number> = {
|
||||
'5m': 300, '15m': 900, '1h': 3600, '4h': 14400, '1d': 86400, '1w': 604800,
|
||||
}
|
||||
@@ -320,7 +360,7 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
const bucketSecs = bucketByTf[timeframeRef.current.toLowerCase()] ?? candleSpacing
|
||||
|
||||
const bucketMap = new Map<number, typeof visible>()
|
||||
for (const p of visible) {
|
||||
for (const p of markerPosts) {
|
||||
const pt = Math.floor(new Date(p.published_at).getTime() / 1000)
|
||||
const bucket = Math.floor(pt / bucketSecs) * bucketSecs
|
||||
if (!bucketMap.has(bucket)) bucketMap.set(bucket, [])
|
||||
@@ -415,7 +455,7 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
chart.timeScale().fitContent()
|
||||
fittedRef.current = true
|
||||
}
|
||||
}, [candles, posts, externalSelectedId, asset])
|
||||
}, [candles, posts, externalSelectedId, asset, chartReady])
|
||||
|
||||
useEffect(() => { fittedRef.current = false }, [timeframe, asset])
|
||||
|
||||
@@ -426,7 +466,9 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
// the live tick exceeds them.
|
||||
useEffect(() => {
|
||||
const series = seriesRef.current as any
|
||||
if (!series) return
|
||||
// dataLoadedRef: update() on a series that hasn't had setData() yet
|
||||
// creates a chart with a single lone candle.
|
||||
if (!series || !dataLoadedRef.current) return
|
||||
const live = livePrices[asset]
|
||||
if (live == null || !candles.length) return
|
||||
const last = candles[candles.length - 1]
|
||||
@@ -442,7 +484,7 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{ width: '100%', height: 360, borderRadius: 'var(--r-sm)', overflow: 'hidden', cursor: 'crosshair', position: 'relative' }}
|
||||
style={{ width: '100%', height: '100%', borderRadius: 'var(--r-sm)', overflow: 'hidden', cursor: 'crosshair', position: 'relative' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user