feat: proper time-based X-axis with angled hour labels and bold dates
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-1 Pipeline failed
ci/woodpecker/push/build-3 Pipeline failed
ci/woodpecker/push/build-2 Pipeline failed
ci/woodpecker/push/finalize unknown status
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled

Replaced string-based X-axis with numeric timestamp axis:
- Custom ChartXTick component renders hour marks at -35° angle
- New day boundaries shown in bold (e.g., 'Apr 29')
- Hour marks shown as '9:00 AM', '10:00 AM' etc.
- Tooltip shows full date+time on hover
- Direction timeline uses formatted timestamps
- Bottom margin increased to accommodate angled labels
This commit is contained in:
Celes Renata
2026-04-29 21:26:11 +00:00
parent cb3eb230d6
commit b6e2718007
+44 -7
View File
@@ -602,13 +602,46 @@ interface ChartPoint {
price?: number;
}
function ChartXTick({ x, y, payload }: { x?: number; y?: number; payload?: { value: number } }) {
if (!payload || !x || !y) return null;
const d = new Date(payload.value);
const hour = d.getHours();
const minute = d.getMinutes();
const isNewDay = hour === 0 && minute < 30;
const isHourMark = minute < 15; // Show only on-the-hour ticks
// For date labels (new day boundary)
if (isNewDay) {
const label = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
return (
<g transform={`translate(${x},${y})`}>
<text x={0} y={12} textAnchor="middle" fill="#e2e8f0" fontSize={11} fontWeight="bold">
{label}
</text>
</g>
);
}
// For time labels — show rounded hours at an angle
if (!isHourMark) return null;
const timeStr = d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
return (
<g transform={`translate(${x},${y})`}>
<text x={0} y={8} textAnchor="end" fill="#94a3b8" fontSize={10} transform="rotate(-35)">
{timeStr}
</text>
</g>
);
}
function TrendTooltip({ active, payload, label }: Record<string, unknown>) {
if (!active) return null;
const items = payload as Array<{ name: string; value: number; color: string; dataKey: string }> | undefined;
if (!items?.length) return null;
const ts = typeof label === 'number' ? new Date(label).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : String(label ?? '');
return (
<div className="rounded-lg border border-surface-700 bg-surface-900 px-3 py-2 text-xs shadow-lg">
<div className="mb-1 text-gray-400">{String(label ?? '')}</div>
<div className="mb-1 text-gray-400">{ts}</div>
{items.map((item, i) => (
<div key={i} className="flex justify-between gap-4" style={{ color: item.color }}>
<span>{item.name}:</span>
@@ -709,7 +742,7 @@ function TrendHistoryChart({ trends, latestTrends, ticker, marketPrices, selecte
const trendTs = new Date(t.generated_at).getTime();
const price = findClosestPrice(trendTs);
return {
time: new Date(t.generated_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }),
time: String(trendTs),
timestamp: trendTs,
strength: +(t.trend_strength * 100).toFixed(1),
confidence: +(t.confidence * 100).toFixed(1),
@@ -766,12 +799,16 @@ function TrendHistoryChart({ trends, latestTrends, ticker, marketPrices, selecte
Trend Strength & Confidence {ticker} / {selectedWindow}
</h2>
<ResponsiveContainer width="100%" height={280}>
<LineChart data={chartData} margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
<LineChart data={chartData} margin={{ top: 5, right: 20, bottom: 40, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis
dataKey="time"
tick={{ fill: '#94a3b8', fontSize: 11 }}
dataKey="timestamp"
type="number"
domain={['dataMin', 'dataMax']}
scale="time"
tick={<ChartXTick />}
tickLine={{ stroke: '#475569' }}
interval="preserveStartEnd"
/>
<YAxis
yAxisId="left"
@@ -853,13 +890,13 @@ function TrendHistoryChart({ trends, latestTrends, ticker, marketPrices, selecte
'bg-gray-600';
const height = Math.max(8, pt.strength * 0.5);
return (
<div key={i} className="flex flex-col items-center gap-1" title={`${pt.time}: ${pt.directionLabel} (${pt.strength}%)`}>
<div key={i} className="flex flex-col items-center gap-1" title={`${new Date(pt.timestamp).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}: ${pt.directionLabel} (${pt.strength}%)`}>
<div
className={`w-3 rounded-sm ${color}`}
style={{ height: `${height}px` }}
/>
{i % Math.max(1, Math.floor(chartData.length / 8)) === 0 && (
<span className="text-[9px] text-gray-500 -rotate-45 origin-top-left whitespace-nowrap">{pt.time}</span>
<span className="text-[9px] text-gray-500 -rotate-45 origin-top-left whitespace-nowrap">{new Date(pt.timestamp).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}</span>
)}
</div>
);