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
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:
@@ -602,13 +602,46 @@ interface ChartPoint {
|
|||||||
price?: number;
|
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>) {
|
function TrendTooltip({ active, payload, label }: Record<string, unknown>) {
|
||||||
if (!active) return null;
|
if (!active) return null;
|
||||||
const items = payload as Array<{ name: string; value: number; color: string; dataKey: string }> | undefined;
|
const items = payload as Array<{ name: string; value: number; color: string; dataKey: string }> | undefined;
|
||||||
if (!items?.length) return null;
|
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 (
|
return (
|
||||||
<div className="rounded-lg border border-surface-700 bg-surface-900 px-3 py-2 text-xs shadow-lg">
|
<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) => (
|
{items.map((item, i) => (
|
||||||
<div key={i} className="flex justify-between gap-4" style={{ color: item.color }}>
|
<div key={i} className="flex justify-between gap-4" style={{ color: item.color }}>
|
||||||
<span>{item.name}:</span>
|
<span>{item.name}:</span>
|
||||||
@@ -709,7 +742,7 @@ function TrendHistoryChart({ trends, latestTrends, ticker, marketPrices, selecte
|
|||||||
const trendTs = new Date(t.generated_at).getTime();
|
const trendTs = new Date(t.generated_at).getTime();
|
||||||
const price = findClosestPrice(trendTs);
|
const price = findClosestPrice(trendTs);
|
||||||
return {
|
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,
|
timestamp: trendTs,
|
||||||
strength: +(t.trend_strength * 100).toFixed(1),
|
strength: +(t.trend_strength * 100).toFixed(1),
|
||||||
confidence: +(t.confidence * 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}
|
Trend Strength & Confidence — {ticker} / {selectedWindow}
|
||||||
</h2>
|
</h2>
|
||||||
<ResponsiveContainer width="100%" height={280}>
|
<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" />
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="time"
|
dataKey="timestamp"
|
||||||
tick={{ fill: '#94a3b8', fontSize: 11 }}
|
type="number"
|
||||||
|
domain={['dataMin', 'dataMax']}
|
||||||
|
scale="time"
|
||||||
|
tick={<ChartXTick />}
|
||||||
tickLine={{ stroke: '#475569' }}
|
tickLine={{ stroke: '#475569' }}
|
||||||
|
interval="preserveStartEnd"
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
yAxisId="left"
|
yAxisId="left"
|
||||||
@@ -853,13 +890,13 @@ function TrendHistoryChart({ trends, latestTrends, ticker, marketPrices, selecte
|
|||||||
'bg-gray-600';
|
'bg-gray-600';
|
||||||
const height = Math.max(8, pt.strength * 0.5);
|
const height = Math.max(8, pt.strength * 0.5);
|
||||||
return (
|
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
|
<div
|
||||||
className={`w-3 rounded-sm ${color}`}
|
className={`w-3 rounded-sm ${color}`}
|
||||||
style={{ height: `${height}px` }}
|
style={{ height: `${height}px` }}
|
||||||
/>
|
/>
|
||||||
{i % Math.max(1, Math.floor(chartData.length / 8)) === 0 && (
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user