Extract magic numbers and colors to named constants in CandleChart.tsx

Replace inline magic numbers (8px, 60s, 2, 1, 0.15, 0.25, 100) and hardcoded
color strings with named module-level constants for better maintainability and
clarity. Organized constants into logical groups:
- Magic number constants (tolerance, intervals, sizes, alphas)
- Dark theme colors
- Light theme colors
- Candlestick colors
- Prediction overlay colors
- Predefined label colors

This improves code readability and makes it easier to adjust UI parameters globally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marko Djordjevic 2026-02-18 15:24:32 +01:00
parent d6d844a003
commit 36182986b6

View file

@ -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<CandleChartHandle, CandleChartProps>(
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<CandleChartHandle, CandleChartProps>(
});
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<CandleChartHandle, CandleChartProps>(
: 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<CandleChartHandle, CandleChartProps>(
// Generate colors for labels since backend no longer provides them
const labelColorMap: Record<string, string> = {};
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<CandleChartHandle, CandleChartProps>(
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<number, PredictionSpan>();
@ -508,25 +549,25 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
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<CandleChartHandle, CandleChartProps>(
})
.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<CandleChartHandle, CandleChartProps>(
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<CandleChartHandle, CandleChartProps>(
previewPoint as Point,
{
lineColor: selectedColor,
width: 2,
width: LINE_WIDTH,
showLabels: false,
isPreview: true,
}
@ -865,11 +906,10 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
.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<CandleChartHandle, CandleChartProps>(
.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<CandleChartHandle, CandleChartProps>(
p2,
{
lineColor: color,
width: 2,
width: LINE_WIDTH,
showLabels: false,
annotationId: String(annotation.id),
}