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:
parent
d6d844a003
commit
36182986b6
1 changed files with 100 additions and 61 deletions
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue