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 { RectangleDrawingPrimitive, RectanglePoint } from '@/plugins/rectangle-drawing';
import type { PerCandlePrediction, PredictionSpan, ModelInfoResponse, PredictionSummary } from '@/types/predictions'; 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 { interface DrawingState {
tool: 'line' | 'rectangle'; tool: 'line' | 'rectangle';
firstPoint: { time: Time; price: number }; firstPoint: { time: Time; price: number };
@ -242,41 +293,41 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
if (isDark) { if (isDark) {
return { return {
layout: { layout: {
background: { color: '#000000' }, background: { color: DARK_BG_COLOR },
textColor: '#00ff41', textColor: DARK_TEXT_COLOR,
}, },
grid: { grid: {
vertLines: { color: '#003311' }, vertLines: { color: DARK_GRID_COLOR },
horzLines: { color: '#003311' }, horzLines: { color: DARK_GRID_COLOR },
}, },
timeScale: { timeScale: {
borderColor: '#003311', borderColor: DARK_BORDER_COLOR,
}, },
rightPriceScale: { rightPriceScale: {
borderColor: '#003311', borderColor: DARK_BORDER_COLOR,
}, },
crosshair: { crosshair: {
color: '#00ff41', color: DARK_CROSSHAIR_COLOR,
}, },
}; };
} else { } else {
return { return {
layout: { layout: {
background: { color: '#ffffff' }, background: { color: LIGHT_BG_COLOR },
textColor: '#1a1a2e', textColor: LIGHT_TEXT_COLOR,
}, },
grid: { grid: {
vertLines: { color: '#d1d5db' }, vertLines: { color: LIGHT_GRID_COLOR },
horzLines: { color: '#d1d5db' }, horzLines: { color: LIGHT_GRID_COLOR },
}, },
timeScale: { timeScale: {
borderColor: '#d1d5db', borderColor: LIGHT_BORDER_COLOR,
}, },
rightPriceScale: { rightPriceScale: {
borderColor: '#d1d5db', borderColor: LIGHT_BORDER_COLOR,
}, },
crosshair: { crosshair: {
color: '#16a34a', color: LIGHT_CROSSHAIR_COLOR,
}, },
}; };
} }
@ -307,18 +358,18 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
}); });
const candlestickSeries = chart.addCandlestickSeries({ const candlestickSeries = chart.addCandlestickSeries({
upColor: '#ffffff', upColor: CANDLESTICK_UP_COLOR,
downColor: '#444444', downColor: CANDLESTICK_DOWN_COLOR,
borderVisible: true, borderVisible: true,
wickUpColor: '#444444', wickUpColor: CANDLESTICK_WICK_UP_COLOR,
wickDownColor: '#444444', wickDownColor: CANDLESTICK_WICK_DOWN_COLOR,
borderUpColor: '#444444', borderUpColor: CANDLESTICK_BORDER_UP_COLOR,
borderDownColor: '#444444', borderDownColor: CANDLESTICK_BORDER_DOWN_COLOR,
}); });
// Add histogram series for prediction overlay (rendered behind candles) // Add histogram series for prediction overlay (rendered behind candles)
const histogramSeries = chart.addHistogramSeries({ const histogramSeries = chart.addHistogramSeries({
color: 'rgba(128, 128, 128, 0.15)', color: PREDICTION_HISTOGRAM_DEFAULT_COLOR,
priceFormat: { type: 'volume' }, priceFormat: { type: 'volume' },
priceScaleId: 'prediction_overlay', priceScaleId: 'prediction_overlay',
}); });
@ -407,7 +458,7 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
: annotationType.color, : annotationType.color,
shape: isUpArrow ? ('arrowUp' as const) : ('arrowDown' as const), shape: isUpArrow ? ('arrowUp' as const) : ('arrowDown' as const),
text: annotationType.display_name, text: annotationType.display_name,
size: isSelected ? 2 : 1, size: isSelected ? MARKER_SIZE_SELECTED : MARKER_SIZE_DEFAULT,
}; };
}) })
.filter((m) => m !== null) .filter((m) => m !== null)
@ -431,18 +482,8 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
// Generate colors for labels since backend no longer provides them // Generate colors for labels since backend no longer provides them
const labelColorMap: Record<string, string> = {}; const labelColorMap: Record<string, string> = {};
if (modelInfo?.labels) { 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) => { 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])); const candleMap = new Map(candles.map((c) => [c.time, c]));
// Detect candle interval from data (fall back to 60s if fewer than 2 candles) // 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 // Build a map from prediction time to its span for disagreement lookup
const predictionTimeToSpan = new Map<number, PredictionSpan>(); const predictionTimeToSpan = new Map<number, PredictionSpan>();
@ -513,18 +554,18 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
const span = predictionTimeToSpan.get(p.time); const span = predictionTimeToSpan.get(p.time);
const disagreementType = span ? disagreementMap.get(span.start_time) : null; const disagreementType = span ? disagreementMap.get(span.start_time) : null;
let baseColor = labelColorMap[p.label] || '#888888'; let baseColor = labelColorMap[p.label] || PREDICTION_DEFAULT_FALLBACK_COLOR;
let alpha = 0.15; let alpha = PREDICTION_HISTOGRAM_ALPHA_DEFAULT;
// Apply disagreement-specific colors and styling // Apply disagreement-specific colors and styling
if (disagreementType === 'missed_by_human') { if (disagreementType === 'missed_by_human') {
// Yellow highlight for predictions missed by humans // Yellow highlight for predictions missed by humans
baseColor = '#eab308'; // yellow baseColor = PREDICTION_MISSED_BY_HUMAN_COLOR;
alpha = 0.25; alpha = PREDICTION_HISTOGRAM_ALPHA_DISAGREEMENT;
} else if (disagreementType === 'label_mismatch') { } else if (disagreementType === 'label_mismatch') {
// Orange for label mismatches // Orange for label mismatches
baseColor = '#f97316'; // orange baseColor = PREDICTION_LABEL_MISMATCH_COLOR;
alpha = 0.25; alpha = PREDICTION_HISTOGRAM_ALPHA_DISAGREEMENT;
} }
return { return {
@ -554,26 +595,26 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
}) })
.map((span) => { .map((span) => {
const disagreementType = disagreementMap.get(span.start_time); 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; let labelText = span.label;
// Apply disagreement-specific styling to markers // Apply disagreement-specific styling to markers
if (disagreementType === 'missed_by_human') { if (disagreementType === 'missed_by_human') {
baseColor = '#eab308'; // yellow baseColor = PREDICTION_MISSED_BY_HUMAN_COLOR;
labelText = `${span.label}`; labelText = `${span.label}`;
} else if (disagreementType === 'label_mismatch') { } else if (disagreementType === 'label_mismatch') {
baseColor = '#f97316'; // orange baseColor = PREDICTION_LABEL_MISMATCH_COLOR;
labelText = `${span.label}`; labelText = `${span.label}`;
} }
const confidencePct = Math.round(span.avg_confidence * 100); const confidencePct = Math.round(span.avg_confidence * CONFIDENCE_PERCENTAGE_MULTIPLIER);
return { return {
time: span.start_time as Time, time: span.start_time as Time,
position: 'belowBar' as const, position: 'belowBar' as const,
color: baseColor, color: baseColor,
shape: 'square' as const, shape: 'square' as const,
text: `${labelText} (${confidencePct}%)`, text: `${labelText} (${confidencePct}%)`,
size: 1, size: MARKER_SIZE_DEFAULT,
}; };
}) })
.sort((a, b) => (a.time as number) - (b.time as number)); .sort((a, b) => (a.time as number) - (b.time as number));
@ -596,7 +637,7 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
Math.pow(clickX - endpointX, 2) + Math.pow(clickY - endpointY, 2) 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) => { const handleClick = async (param: any) => {
@ -682,7 +723,7 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
previewPoint as Point, previewPoint as Point,
{ {
lineColor: selectedColor, lineColor: selectedColor,
width: 2, width: LINE_WIDTH,
showLabels: false, showLabels: false,
isPreview: true, isPreview: true,
} }
@ -865,11 +906,10 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
.map((t) => t.name); .map((t) => t.name);
// Find annotation at this timestamp (within tolerance) // Find annotation at this timestamp (within tolerance)
const tolerance = 60; // 60 seconds tolerance
const annotation = annotationsRef.current.find( const annotation = annotationsRef.current.find(
(a) => (a) =>
markerTypeNames.includes(a.label_type) && markerTypeNames.includes(a.label_type) &&
Math.abs(a.timestamp - timestamp) < tolerance Math.abs(a.timestamp - timestamp) < MARKER_TIME_TOLERANCE_S
); );
if (annotation) { if (annotation) {
@ -982,11 +1022,10 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
.map((t) => t.name); .map((t) => t.name);
// Find annotation at this timestamp (within tolerance) // Find annotation at this timestamp (within tolerance)
const tolerance = 60; // 60 seconds tolerance
const annotation = annotationsRef.current.find( const annotation = annotationsRef.current.find(
(a) => (a) =>
markerTypeNames.includes(a.label_type) && markerTypeNames.includes(a.label_type) &&
Math.abs(a.timestamp - timestamp) < tolerance Math.abs(a.timestamp - timestamp) < MARKER_TIME_TOLERANCE_S
); );
if (annotation) { if (annotation) {
@ -1128,7 +1167,7 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
p2, p2,
{ {
lineColor: color, lineColor: color,
width: 2, width: LINE_WIDTH,
showLabels: false, showLabels: false,
annotationId: String(annotation.id), annotationId: String(annotation.id),
} }