diff --git a/src/components/CandleChart.tsx b/src/components/CandleChart.tsx index 791f3d9..3f8a07f 100644 --- a/src/components/CandleChart.tsx +++ b/src/components/CandleChart.tsx @@ -8,6 +8,57 @@ import { TrendLine } from '@/plugins/trend-line'; import { RectangleDrawingPrimitive, RectanglePoint } from '@/plugins/rectangle-drawing'; import type { PerCandlePrediction, PredictionSpan, ModelInfoResponse, PredictionSummary } from '@/types/predictions'; +// === Magic Number Constants === +const ENDPOINT_CLICK_TOLERANCE_PX = 8; +const DEFAULT_CANDLE_INTERVAL_S = 60; +const MARKER_TIME_TOLERANCE_S = 60; +const LINE_WIDTH = 2; +const MARKER_SIZE_DEFAULT = 1; +const MARKER_SIZE_SELECTED = 2; +const PREDICTION_HISTOGRAM_ALPHA_DEFAULT = 0.15; +const PREDICTION_HISTOGRAM_ALPHA_DISAGREEMENT = 0.25; +const CONFIDENCE_PERCENTAGE_MULTIPLIER = 100; + +// === Color Constants - Dark Theme === +const DARK_BG_COLOR = '#000000'; +const DARK_TEXT_COLOR = '#00ff41'; +const DARK_GRID_COLOR = '#003311'; +const DARK_BORDER_COLOR = '#003311'; +const DARK_CROSSHAIR_COLOR = '#00ff41'; + +// === Color Constants - Light Theme === +const LIGHT_BG_COLOR = '#ffffff'; +const LIGHT_TEXT_COLOR = '#1a1a2e'; +const LIGHT_GRID_COLOR = '#d1d5db'; +const LIGHT_BORDER_COLOR = '#d1d5db'; +const LIGHT_CROSSHAIR_COLOR = '#16a34a'; + +// === Color Constants - Candlestick === +const CANDLESTICK_UP_COLOR = '#ffffff'; +const CANDLESTICK_DOWN_COLOR = '#444444'; +const CANDLESTICK_BORDER_UP_COLOR = '#444444'; +const CANDLESTICK_BORDER_DOWN_COLOR = '#444444'; +const CANDLESTICK_WICK_UP_COLOR = '#444444'; +const CANDLESTICK_WICK_DOWN_COLOR = '#444444'; + +// === Color Constants - Prediction === +const PREDICTION_HISTOGRAM_DEFAULT_COLOR = 'rgba(128, 128, 128, 0.15)'; +const PREDICTION_DEFAULT_FALLBACK_COLOR = '#888888'; +const PREDICTION_MISSED_BY_HUMAN_COLOR = '#eab308'; +const PREDICTION_LABEL_MISMATCH_COLOR = '#f97316'; + +// === Predefined Label Colors === +const PREDEFINED_LABEL_COLORS = [ + '#3b82f6', // blue + '#ef4444', // red + '#10b981', // green + '#f59e0b', // amber + '#8b5cf6', // violet + '#ec4899', // pink + '#06b6d4', // cyan + '#f97316', // orange +]; + interface DrawingState { tool: 'line' | 'rectangle'; firstPoint: { time: Time; price: number }; @@ -242,41 +293,41 @@ const CandleChart = forwardRef( if (isDark) { return { layout: { - background: { color: '#000000' }, - textColor: '#00ff41', + background: { color: DARK_BG_COLOR }, + textColor: DARK_TEXT_COLOR, }, grid: { - vertLines: { color: '#003311' }, - horzLines: { color: '#003311' }, + vertLines: { color: DARK_GRID_COLOR }, + horzLines: { color: DARK_GRID_COLOR }, }, timeScale: { - borderColor: '#003311', + borderColor: DARK_BORDER_COLOR, }, rightPriceScale: { - borderColor: '#003311', + borderColor: DARK_BORDER_COLOR, }, crosshair: { - color: '#00ff41', + color: DARK_CROSSHAIR_COLOR, }, }; } else { return { layout: { - background: { color: '#ffffff' }, - textColor: '#1a1a2e', + background: { color: LIGHT_BG_COLOR }, + textColor: LIGHT_TEXT_COLOR, }, grid: { - vertLines: { color: '#d1d5db' }, - horzLines: { color: '#d1d5db' }, + vertLines: { color: LIGHT_GRID_COLOR }, + horzLines: { color: LIGHT_GRID_COLOR }, }, timeScale: { - borderColor: '#d1d5db', + borderColor: LIGHT_BORDER_COLOR, }, rightPriceScale: { - borderColor: '#d1d5db', + borderColor: LIGHT_BORDER_COLOR, }, crosshair: { - color: '#16a34a', + color: LIGHT_CROSSHAIR_COLOR, }, }; } @@ -307,18 +358,18 @@ const CandleChart = forwardRef( }); const candlestickSeries = chart.addCandlestickSeries({ - upColor: '#ffffff', - downColor: '#444444', + upColor: CANDLESTICK_UP_COLOR, + downColor: CANDLESTICK_DOWN_COLOR, borderVisible: true, - wickUpColor: '#444444', - wickDownColor: '#444444', - borderUpColor: '#444444', - borderDownColor: '#444444', + wickUpColor: CANDLESTICK_WICK_UP_COLOR, + wickDownColor: CANDLESTICK_WICK_DOWN_COLOR, + borderUpColor: CANDLESTICK_BORDER_UP_COLOR, + borderDownColor: CANDLESTICK_BORDER_DOWN_COLOR, }); // Add histogram series for prediction overlay (rendered behind candles) const histogramSeries = chart.addHistogramSeries({ - color: 'rgba(128, 128, 128, 0.15)', + color: PREDICTION_HISTOGRAM_DEFAULT_COLOR, priceFormat: { type: 'volume' }, priceScaleId: 'prediction_overlay', }); @@ -407,7 +458,7 @@ const CandleChart = forwardRef( : annotationType.color, shape: isUpArrow ? ('arrowUp' as const) : ('arrowDown' as const), text: annotationType.display_name, - size: isSelected ? 2 : 1, + size: isSelected ? MARKER_SIZE_SELECTED : MARKER_SIZE_DEFAULT, }; }) .filter((m) => m !== null) @@ -431,18 +482,8 @@ const CandleChart = forwardRef( // Generate colors for labels since backend no longer provides them const labelColorMap: Record = {}; if (modelInfo?.labels) { - const predefinedColors = [ - '#3b82f6', // blue - '#ef4444', // red - '#10b981', // green - '#f59e0b', // amber - '#8b5cf6', // violet - '#ec4899', // pink - '#06b6d4', // cyan - '#f97316', // orange - ]; modelInfo.labels.forEach((label, index) => { - labelColorMap[label] = predefinedColors[index % predefinedColors.length]; + labelColorMap[label] = PREDEFINED_LABEL_COLORS[index % PREDEFINED_LABEL_COLORS.length]; }); } @@ -473,7 +514,7 @@ const CandleChart = forwardRef( const candleMap = new Map(candles.map((c) => [c.time, c])); // Detect candle interval from data (fall back to 60s if fewer than 2 candles) - const candleInterval = candles.length >= 2 ? candles[1].time - candles[0].time : 60; + const candleInterval = candles.length >= 2 ? candles[1].time - candles[0].time : DEFAULT_CANDLE_INTERVAL_S; // Build a map from prediction time to its span for disagreement lookup const predictionTimeToSpan = new Map(); @@ -508,25 +549,25 @@ const CandleChart = forwardRef( const candle = candleMap.get(p.time); // Use candle high as the histogram value so it overlays correctly const value = candle ? candle.high : 0; - + // Check if this prediction is part of a disagreement const span = predictionTimeToSpan.get(p.time); const disagreementType = span ? disagreementMap.get(span.start_time) : null; - - let baseColor = labelColorMap[p.label] || '#888888'; - let alpha = 0.15; - + + let baseColor = labelColorMap[p.label] || PREDICTION_DEFAULT_FALLBACK_COLOR; + let alpha = PREDICTION_HISTOGRAM_ALPHA_DEFAULT; + // Apply disagreement-specific colors and styling if (disagreementType === 'missed_by_human') { // Yellow highlight for predictions missed by humans - baseColor = '#eab308'; // yellow - alpha = 0.25; + baseColor = PREDICTION_MISSED_BY_HUMAN_COLOR; + alpha = PREDICTION_HISTOGRAM_ALPHA_DISAGREEMENT; } else if (disagreementType === 'label_mismatch') { // Orange for label mismatches - baseColor = '#f97316'; // orange - alpha = 0.25; + baseColor = PREDICTION_LABEL_MISMATCH_COLOR; + alpha = PREDICTION_HISTOGRAM_ALPHA_DISAGREEMENT; } - + return { time: p.time as Time, value, @@ -554,26 +595,26 @@ const CandleChart = forwardRef( }) .map((span) => { const disagreementType = disagreementMap.get(span.start_time); - let baseColor = labelColorMap[span.label] || '#888888'; + let baseColor = labelColorMap[span.label] || PREDICTION_DEFAULT_FALLBACK_COLOR; let labelText = span.label; - + // Apply disagreement-specific styling to markers if (disagreementType === 'missed_by_human') { - baseColor = '#eab308'; // yellow + baseColor = PREDICTION_MISSED_BY_HUMAN_COLOR; labelText = `⚠ ${span.label}`; } else if (disagreementType === 'label_mismatch') { - baseColor = '#f97316'; // orange + baseColor = PREDICTION_LABEL_MISMATCH_COLOR; labelText = `⚠ ${span.label}`; } - - const confidencePct = Math.round(span.avg_confidence * 100); + + const confidencePct = Math.round(span.avg_confidence * CONFIDENCE_PERCENTAGE_MULTIPLIER); return { time: span.start_time as Time, position: 'belowBar' as const, color: baseColor, shape: 'square' as const, text: `${labelText} (${confidencePct}%)`, - size: 1, + size: MARKER_SIZE_DEFAULT, }; }) .sort((a, b) => (a.time as number) - (b.time as number)); @@ -589,14 +630,14 @@ const CandleChart = forwardRef( const isNearEndpoint = (clickX: number, clickY: number, endpointTime: Time, endpointPrice: number): boolean => { const endpointX = chartRef.current!.timeScale().timeToCoordinate(endpointTime); const endpointY = seriesRef.current!.priceToCoordinate(endpointPrice); - + if (endpointX === null || endpointY === null) return false; - + const distance = Math.sqrt( Math.pow(clickX - endpointX, 2) + Math.pow(clickY - endpointY, 2) ); - - return distance <= 8; // 8 pixel tolerance for endpoint handles + + return distance <= ENDPOINT_CLICK_TOLERANCE_PX; }; const handleClick = async (param: any) => { @@ -682,7 +723,7 @@ const CandleChart = forwardRef( previewPoint as Point, { lineColor: selectedColor, - width: 2, + width: LINE_WIDTH, showLabels: false, isPreview: true, } @@ -865,11 +906,10 @@ const CandleChart = forwardRef( .map((t) => t.name); // Find annotation at this timestamp (within tolerance) - const tolerance = 60; // 60 seconds tolerance const annotation = annotationsRef.current.find( (a) => markerTypeNames.includes(a.label_type) && - Math.abs(a.timestamp - timestamp) < tolerance + Math.abs(a.timestamp - timestamp) < MARKER_TIME_TOLERANCE_S ); if (annotation) { @@ -982,11 +1022,10 @@ const CandleChart = forwardRef( .map((t) => t.name); // Find annotation at this timestamp (within tolerance) - const tolerance = 60; // 60 seconds tolerance const annotation = annotationsRef.current.find( (a) => markerTypeNames.includes(a.label_type) && - Math.abs(a.timestamp - timestamp) < tolerance + Math.abs(a.timestamp - timestamp) < MARKER_TIME_TOLERANCE_S ); if (annotation) { @@ -1128,7 +1167,7 @@ const CandleChart = forwardRef( p2, { lineColor: color, - width: 2, + width: LINE_WIDTH, showLabels: false, annotationId: String(annotation.id), }