min pivot distance only applies to L1 pivots ,

charts minimize addons,
delete buttons WIP
This commit is contained in:
Dobromir Popov
2025-10-24 13:49:16 +03:00
parent 42bf91b735
commit b970c4ca4d
8 changed files with 532 additions and 169 deletions

View File

@@ -93,34 +93,52 @@ class AnnotationManager {
* Delete old annotation and save updated one
*/
deleteAndSaveAnnotation(oldId, newAnnotation) {
console.log('Updating annotation:', oldId, newAnnotation);
// Delete old
fetch('/api/delete-annotation', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({annotation_id: oldId})
})
.then(() => {
// Save new
return fetch('/api/save-annotation', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(newAnnotation)
});
.then(response => response.json())
.then(deleteData => {
console.log('Delete response:', deleteData);
if (deleteData.success) {
// Save new
return fetch('/api/save-annotation', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(newAnnotation)
});
} else {
throw new Error('Failed to delete old annotation: ' + deleteData.error.message);
}
})
.then(response => response.json())
.then(data => {
console.log('Save response:', data);
if (data.success) {
// Update app state
window.appState.annotations = window.appState.annotations.filter(a => a.annotation_id !== oldId);
window.appState.annotations.push(data.annotation);
// Update UI
window.renderAnnotationsList(window.appState.annotations);
if (typeof window.renderAnnotationsList === 'function') {
window.renderAnnotationsList(window.appState.annotations);
}
// Update chart
this.chartManager.removeAnnotation(oldId);
this.chartManager.addAnnotation(data.annotation);
window.showSuccess('Annotation updated successfully');
} else {
throw new Error('Failed to save updated annotation: ' + data.error.message);
}
})
.catch(error => {
console.error('Update annotation error:', error);
window.showError('Failed to update annotation: ' + error.message);
});
}

View File

@@ -170,7 +170,7 @@ class ChartManager {
pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} High Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color);
pivotDots.marker.size.push(8);
pivotDots.marker.size.push(this._getPivotMarkerSize(pivot.level));
pivotDots.marker.symbol.push('triangle-down');
// Draw horizontal line ONLY for last pivot of this level
@@ -217,7 +217,7 @@ class ChartManager {
pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} Low Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color);
pivotDots.marker.size.push(8);
pivotDots.marker.size.push(this._getPivotMarkerSize(pivot.level));
pivotDots.marker.symbol.push('triangle-up');
// Draw horizontal line ONLY for last pivot of this level
@@ -285,24 +285,42 @@ class ChartManager {
annotations: []
};
// Add click handler for chart
// Add click handler for chart and annotations
plotElement.on('plotly_click', (eventData) => {
// Check if this is an annotation click by looking at the clicked element
if (eventData.points && eventData.points.length > 0) {
const point = eventData.points[0];
// If it's a shape click (annotation line), try to find the annotation
if (point.data && point.data.name && point.data.name.startsWith('line_')) {
const annotationId = point.data.name.replace('line_', '');
console.log('Line annotation clicked:', annotationId);
this.handleAnnotationClick(annotationId, 'edit');
return; // Don't process as regular chart click
}
}
// Regular chart click for annotation marking
this.handleChartClick(timeframe, eventData);
});
// Add click handler for annotations
plotElement.on('plotly_clickannotation', (eventData) => {
console.log('Annotation clicked:', eventData);
const annotationName = eventData.annotation.name;
if (annotationName) {
const parts = annotationName.split('_');
const action = parts[0]; // 'entry', 'exit', or 'delete'
const annotationId = parts[1];
console.log(`Annotation action: ${action}, ID: ${annotationId}`);
if (action === 'delete') {
this.handleAnnotationClick(annotationId, 'delete');
} else {
this.handleAnnotationClick(annotationId, 'edit');
}
} else {
console.log('No annotation name found in click event');
}
});
@@ -418,7 +436,7 @@ class ChartManager {
pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} High Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color);
pivotDots.marker.size.push(8);
pivotDots.marker.size.push(this._getPivotMarkerSize(pivot.level));
pivotDots.marker.symbol.push('triangle-down');
// Draw horizontal line ONLY for last pivot
@@ -464,7 +482,7 @@ class ChartManager {
pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} Low Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color);
pivotDots.marker.size.push(8);
pivotDots.marker.size.push(this._getPivotMarkerSize(pivot.level));
pivotDots.marker.symbol.push('triangle-up');
// Draw horizontal line ONLY for last pivot
@@ -644,13 +662,32 @@ class ChartManager {
});
});
// Update chart layout with annotations
// Get existing pivot annotations (they have specific names like L1H, L2L)
const existingLayout = chart.element.layout || {};
const existingAnnotations = existingLayout.annotations || [];
const pivotAnnotations = existingAnnotations.filter(ann =>
ann.text && ann.text.match(/^L\d+[HL]$/)
);
// Merge pivot annotations with trade annotations
const allAnnotations = [...pivotAnnotations, ...plotlyAnnotations];
// Get existing pivot shapes
const existingShapes = existingLayout.shapes || [];
const pivotShapes = existingShapes.filter(shape =>
shape.layer === 'below' && shape.line && shape.line.dash === 'dash'
);
// Merge pivot shapes with trade annotation shapes
const allShapes = [...pivotShapes, ...plotlyShapes];
// Update chart layout with merged annotations and shapes
Plotly.relayout(chart.plotId, {
annotations: plotlyAnnotations,
shapes: plotlyShapes
annotations: allAnnotations,
shapes: allShapes
});
console.log(`Updated ${timeframeAnnotations.length} annotations for ${timeframe}`);
console.log(`Updated ${timeframeAnnotations.length} trade annotations for ${timeframe} (preserved ${pivotAnnotations.length} pivot annotations)`);
}
/**
@@ -713,83 +750,217 @@ class ChartManager {
const annotation = this.annotations[annotationId];
if (!annotation) return;
// Show edit dialog
const action = prompt(
'Edit annotation:\n' +
'1 - Move entry point\n' +
'2 - Move exit point\n' +
'3 - Delete annotation\n' +
'Enter choice (1-3):',
'1'
);
// Create a better edit dialog using Bootstrap modal
this.showEditDialog(annotationId, annotation);
}
if (action === '1') {
// Move entry point
window.showSuccess('Click on chart to set new entry point');
/**
* Show edit dialog for annotation
*/
showEditDialog(annotationId, annotation) {
// Create modal HTML
const modalHtml = `
<div class="modal fade" id="editAnnotationModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Annotation</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<strong>Current Annotation:</strong>
<div class="mt-2">
<div class="row">
<div class="col-6">
<small class="text-muted">Entry:</small><br>
<span class="badge bg-success">▲</span>
${new Date(annotation.entry.timestamp).toLocaleString()}<br>
<small>$${annotation.entry.price.toFixed(2)}</small>
</div>
<div class="col-6">
<small class="text-muted">Exit:</small><br>
<span class="badge bg-danger">▼</span>
${new Date(annotation.exit.timestamp).toLocaleString()}<br>
<small>$${annotation.exit.price.toFixed(2)}</small>
</div>
</div>
<div class="mt-2">
<small class="text-muted">P&L:</small>
<span class="badge ${annotation.profit_loss_pct >= 0 ? 'bg-success' : 'bg-danger'}">
${annotation.profit_loss_pct >= 0 ? '+' : ''}${annotation.profit_loss_pct.toFixed(2)}%
</span>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">What would you like to edit?</label>
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-outline-primary" id="edit-entry-btn">
<i class="fas fa-arrow-up"></i> Move Entry
</button>
<button type="button" class="btn btn-outline-primary" id="edit-exit-btn">
<i class="fas fa-arrow-down"></i> Move Exit
</button>
<button type="button" class="btn btn-outline-danger" id="delete-annotation-btn">
<i class="fas fa-trash"></i> Delete
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
`;
// Store annotation for editing
if (window.appState && window.appState.annotationManager) {
window.appState.annotationManager.editingAnnotation = {
annotation_id: annotationId,
original: annotation,
editMode: 'entry'
};
// Remove current annotation from display
this.removeAnnotation(annotationId);
// Show exit marker as reference
const chart = this.charts[annotation.timeframe];
if (chart) {
Plotly.relayout(chart.plotId, {
annotations: [{
x: annotation.exit.timestamp,
y: annotation.exit.price,
text: '▼ (exit)',
showarrow: true,
arrowhead: 2,
ax: 0,
ay: 40,
font: { size: 14, color: '#9ca3af' }
}]
});
}
}
} else if (action === '2') {
// Move exit point
window.showSuccess('Click on chart to set new exit point');
if (window.appState && window.appState.annotationManager) {
window.appState.annotationManager.editingAnnotation = {
annotation_id: annotationId,
original: annotation,
editMode: 'exit'
};
// Remove current annotation from display
this.removeAnnotation(annotationId);
// Show entry marker as reference
const chart = this.charts[annotation.timeframe];
if (chart) {
Plotly.relayout(chart.plotId, {
annotations: [{
x: annotation.entry.timestamp,
y: annotation.entry.price,
text: '▲ (entry)',
showarrow: true,
arrowhead: 2,
ax: 0,
ay: -40,
font: { size: 14, color: '#9ca3af' }
}]
});
}
}
} else if (action === '3') {
// Delete
this.handleAnnotationClick(annotationId, 'delete');
// Remove existing modal if any
const existingModal = document.getElementById('editAnnotationModal');
if (existingModal) {
existingModal.remove();
}
// Add modal to page
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Show modal
const modal = new bootstrap.Modal(document.getElementById('editAnnotationModal'));
modal.show();
// Add event listeners
document.getElementById('edit-entry-btn').addEventListener('click', () => {
modal.hide();
this.startEditMode(annotationId, annotation, 'entry');
});
document.getElementById('edit-exit-btn').addEventListener('click', () => {
modal.hide();
this.startEditMode(annotationId, annotation, 'exit');
});
document.getElementById('delete-annotation-btn').addEventListener('click', () => {
modal.hide();
this.handleAnnotationClick(annotationId, 'delete');
});
// Clean up modal when hidden
document.getElementById('editAnnotationModal').addEventListener('hidden.bs.modal', () => {
document.getElementById('editAnnotationModal').remove();
});
}
/**
* Start edit mode for annotation
*/
startEditMode(annotationId, annotation, editMode) {
const message = editMode === 'entry' ? 'Click on chart to set new entry point' : 'Click on chart to set new exit point';
window.showSuccess(message);
// Store annotation for editing
if (window.appState && window.appState.annotationManager) {
window.appState.annotationManager.editingAnnotation = {
annotation_id: annotationId,
original: annotation,
editMode: editMode
};
// Remove current annotation from display
this.removeAnnotation(annotationId);
// Show reference marker
const chart = this.charts[annotation.timeframe];
if (chart) {
const referencePoint = editMode === 'entry' ? annotation.exit : annotation.entry;
const markerText = editMode === 'entry' ? '▼ (exit)' : '▲ (entry)';
const arrowDirection = editMode === 'entry' ? 40 : -40;
Plotly.relayout(chart.plotId, {
annotations: [{
x: referencePoint.timestamp,
y: referencePoint.price,
text: markerText,
showarrow: true,
arrowhead: 2,
ax: 0,
ay: arrowDirection,
font: { size: 14, color: '#9ca3af' },
bgcolor: 'rgba(31, 41, 55, 0.8)',
borderpad: 4
}]
});
}
}
}
/**
* Clear all annotations from charts
*/
clearAllAnnotations() {
console.log('Clearing all annotations from charts');
// Clear from memory
this.annotations = {};
// Update all charts
Object.keys(this.charts).forEach(timeframe => {
this.updateChartAnnotations(timeframe);
});
console.log('All annotations cleared from charts');
}
/**
* Update chart layout when charts are minimized/maximized
*/
updateChartLayout() {
const chartContainer = document.getElementById('chart-container');
const visibleCharts = document.querySelectorAll('.timeframe-chart:not(.minimized)');
const minimizedCharts = document.querySelectorAll('.timeframe-chart.minimized');
// Remove scroll if all charts are visible or if some are minimized
if (minimizedCharts.length > 0) {
chartContainer.classList.add('no-scroll');
// Calculate available height for visible charts
const containerHeight = chartContainer.clientHeight;
const headerHeight = 50; // Approximate header height
const availableHeight = containerHeight - (visibleCharts.length * headerHeight);
const chartHeight = Math.max(200, availableHeight / visibleCharts.length);
// Update visible chart heights
visibleCharts.forEach(chart => {
const plotElement = chart.querySelector('.chart-plot');
if (plotElement) {
plotElement.style.height = `${chartHeight}px`;
// Trigger Plotly resize
const plotId = plotElement.id;
if (plotId) {
Plotly.Plots.resize(plotId);
}
}
});
} else {
chartContainer.classList.remove('no-scroll');
// Reset to default heights
visibleCharts.forEach(chart => {
const plotElement = chart.querySelector('.chart-plot');
if (plotElement) {
plotElement.style.height = '300px';
// Trigger Plotly resize
const plotId = plotElement.id;
if (plotId) {
Plotly.Plots.resize(plotId);
}
}
});
}
console.log(`Updated chart layout: ${visibleCharts.length} visible, ${minimizedCharts.length} minimized`);
}
/**
@@ -804,6 +975,15 @@ class ChartManager {
return colors[Math.min(level - 1, colors.length - 1)];
}
/**
* Get marker size for pivot level
* L1 = smallest (6px), L5 = largest (14px)
*/
_getPivotMarkerSize(level) {
const sizes = [6, 8, 10, 12, 14]; // L1 to L5
return sizes[Math.min(level - 1, sizes.length - 1)];
}
/**
* Enable crosshair cursor
*/