feat: add annotation types management system

- Created annotation_types table with name, display_name, color, category, icon
- Implemented CRUD API endpoints for annotation types management
- Built annotation types management UI page at /annotation-types
- Updated Toolbox component to dynamically load and display annotation types
- Updated CandleChart component to use dynamic annotation types for markers
- Added seed functionality for default types (break_up, break_down, line)
- Cleaned up duplicate migration files
This commit is contained in:
Marko Djordjevic 2026-02-12 18:16:09 +01:00
parent 50229e2ccf
commit 974d9f5598
13 changed files with 942 additions and 79 deletions

View file

@ -25,6 +25,16 @@ interface Annotation {
created_at: number;
}
type AnnotationType = {
id: number;
name: string;
display_name: string;
color: string;
category: string;
icon: string | null;
is_active: number;
};
interface CandleChartProps {
activeTool: string | null;
onAnnotationChange?: () => void;
@ -44,6 +54,7 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
const seriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
const [candles, setCandles] = useState<Candle[]>([]);
const [annotations, setAnnotations] = useState<Annotation[]>([]);
const [annotationTypes, setAnnotationTypes] = useState<AnnotationType[]>([]);
const [isEmpty, setIsEmpty] = useState(true);
// Fetch candles from API
@ -73,11 +84,25 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
}
};
// Fetch annotation types from API
const fetchAnnotationTypes = async () => {
try {
const response = await fetch('/api/annotation-types');
const data = await response.json();
setAnnotationTypes(data.filter((t: AnnotationType) => t.is_active === 1));
return data;
} catch (error) {
console.error('Failed to fetch annotation types:', error);
return [];
}
};
// Expose refresh method to parent
useImperativeHandle(ref, () => ({
refreshData: async () => {
await fetchCandles();
await fetchAnnotations();
await fetchAnnotationTypes();
},
}));
@ -154,30 +179,43 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
// Update markers from annotations
useEffect(() => {
if (!seriesRef.current) return;
if (!seriesRef.current || annotationTypes.length === 0) return;
const markerAnnotations = annotations.filter(
(a) => a.label_type === 'break_up' || a.label_type === 'break_down'
// Get marker type names
const markerTypeNames = annotationTypes
.filter((t) => t.category === 'marker')
.map((t) => t.name);
const markerAnnotations = annotations.filter((a) =>
markerTypeNames.includes(a.label_type)
);
const markers = markerAnnotations
.map((annotation) => {
const annotationType = annotationTypes.find((t) => t.name === annotation.label_type);
if (!annotationType) return null;
const isSelected = annotation.id === selectedLabelId;
// Determine marker shape and position based on icon
const isUpArrow = annotationType.icon === 'arrowUp';
return {
time: annotation.timestamp as Time,
position: annotation.label_type === 'break_up' ? ('belowBar' as const) : ('aboveBar' as const),
position: isUpArrow ? ('belowBar' as const) : ('aboveBar' as const),
color: isSelected
? (annotation.label_type === 'break_up' ? '#059669' : '#dc2626')
: (annotation.label_type === 'break_up' ? '#10b981' : '#ef4444'),
shape: annotation.label_type === 'break_up' ? ('arrowUp' as const) : ('arrowDown' as const),
text: annotation.label_type === 'break_up' ? 'Break Up' : 'Break Down',
? annotationType.color + 'CC' // Add slight transparency when selected
: annotationType.color,
shape: isUpArrow ? ('arrowUp' as const) : ('arrowDown' as const),
text: annotationType.display_name,
size: isSelected ? 2 : 1,
};
})
.sort((a, b) => (a.time as number) - (b.time as number));
.filter((m) => m !== null)
.sort((a, b) => (a!.time as number) - (b!.time as number));
seriesRef.current.setMarkers(markers);
}, [annotations, selectedLabelId]);
seriesRef.current.setMarkers(markers as any);
}, [annotations, selectedLabelId, annotationTypes]);
// Handle chart clicks for annotation
useEffect(() => {
@ -194,8 +232,12 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
if (time === null || price === null) return;
// For break_up and break_down, snap to nearest candle
if (activeTool === 'break_up' || activeTool === 'break_down') {
// Check if activeTool is a marker type
const markerType = annotationTypes.find(
(t) => t.category === 'marker' && t.name === activeTool
);
if (markerType) {
const timestamp = typeof time === 'string' ? Date.parse(time) / 1000 : (time as number);
// Find nearest candle
@ -228,12 +270,15 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
// For delete tool, find and delete marker at clicked position
if (activeTool === 'delete') {
const timestamp = typeof time === 'string' ? Date.parse(time) / 1000 : (time as number);
const markerTypeNames = annotationTypes
.filter((t) => t.category === 'marker')
.map((t) => t.name);
// Find annotation at this timestamp (within tolerance)
const tolerance = 60; // 60 seconds tolerance
const annotation = annotations.find(
(a) =>
(a.label_type === 'break_up' || a.label_type === 'break_down') &&
markerTypeNames.includes(a.label_type) &&
Math.abs(a.timestamp - timestamp) < tolerance
);
@ -254,14 +299,21 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
}
// Select/deselect label markers by clicking them
if (!activeTool || activeTool === 'break_up' || activeTool === 'break_down') {
const isMarkerTool = annotationTypes.find(
(t) => t.category === 'marker' && t.name === activeTool
);
if (!activeTool || isMarkerTool) {
const timestamp = typeof time === 'string' ? Date.parse(time) / 1000 : (time as number);
const markerTypeNames = annotationTypes
.filter((t) => t.category === 'marker')
.map((t) => t.name);
// Find annotation at this timestamp (within tolerance)
const tolerance = 60; // 60 seconds tolerance
const annotation = annotations.find(
(a) =>
(a.label_type === 'break_up' || a.label_type === 'break_down') &&
markerTypeNames.includes(a.label_type) &&
Math.abs(a.timestamp - timestamp) < tolerance
);
@ -276,12 +328,13 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
return () => {
chartRef.current?.unsubscribeClick(handleClick);
};
}, [activeTool, candles, annotations, onAnnotationChange]);
}, [activeTool, candles, annotations, annotationTypes, onAnnotationChange]);
// Fetch data on mount
useEffect(() => {
fetchCandles();
fetchAnnotations();
fetchAnnotationTypes();
}, []);
if (isEmpty) {