Implement single-panel indicator management system
Single-panel design with TradingView-inspired UX: - Search bar for filtering indicators by name - Category tabs (Trend, Momentum, Volatility, Volume, Favorites) - Expandable indicators with inline configuration - Favorites system with pinning - Preset system to save/load indicator configurations - Reset to defaults functionality - Real-time configuration changes (apply immediately) - Mobile-friendly responsive design - Touch-optimized for mobile devices - Cleaner single-panel layout replacing two-panel approach Features: ✓ Search functionality (must-have) ✓ Presets high-priority (save, load, delete) ✓ Single expandable panel ✓ Inline configuration (no separate panel) ✓ Categories for organizing indicators ✓ Favorites support ✓ Real-time visual updates ✓ Mobile responsive ✓ Collapse all indicators with one click ○ Drag-to-reorder (not implemented - nice to have) Updated files: - indicators-panel-new.js: Completely new implementation - indicators-new.css: New styles for single panel - index.html: Updated sidebar to use indicator panel - app.js: Updated imports and initialization
This commit is contained in:
822
src/api/dashboard/static/js/ui/indicators-panel-new.js
Normal file
822
src/api/dashboard/static/js/ui/indicators-panel-new.js
Normal file
@ -0,0 +1,822 @@
|
||||
import { getAvailableIndicators, IndicatorRegistry as IR } from '../indicators/index.js';
|
||||
|
||||
// State management
|
||||
let activeIndicators = [];
|
||||
let configuringId = null;
|
||||
let searchQuery = '';
|
||||
let selectedCategory = 'all';
|
||||
let nextInstanceId = 1;
|
||||
|
||||
// Presets storage
|
||||
let userPresets = JSON.parse(localStorage.getItem('indicator_presets') || '{}');
|
||||
|
||||
// Categories
|
||||
const CATEGORIES = [
|
||||
{ id: 'all', name: 'All Indicators', icon: '📊' },
|
||||
{ id: 'trend', name: 'Trend', icon: '📊' },
|
||||
{ id: 'momentum', name: 'Momentum', icon: '📈' },
|
||||
{ id: 'volatility', name: 'Volatility', icon: '📉' },
|
||||
{ id: 'volume', name: 'Volume', icon: '🔀' },
|
||||
{ id: 'favorites', name: 'Favorites', icon: '★' }
|
||||
];
|
||||
|
||||
const CATEGORY_MAP = {
|
||||
sma: 'trend', ema: 'trend', hts: 'trend',
|
||||
rsi: 'momentum', macd: 'momentum', stoch: 'momentum',
|
||||
bb: 'volatility', atr: 'volatility',
|
||||
others: 'volume'
|
||||
};
|
||||
|
||||
const DEFAULT_COLORS = ['#2962ff', '#26a69a', '#ef5350', '#ff9800', '#9c27b0', '#00bcd4', '#ffeb3b', '#e91e63'];
|
||||
const LINE_TYPES = ['solid', 'dotted', 'dashed'];
|
||||
|
||||
// Initialize
|
||||
export function initIndicatorPanel() {
|
||||
renderIndicatorPanel();
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
function getDefaultColor(index) {
|
||||
return DEFAULT_COLORS[index % DEFAULT_COLORS.length];
|
||||
}
|
||||
|
||||
function getIndicatorCategory(indicator) {
|
||||
return CATEGORY_MAP[indicator.type] || 'trend';
|
||||
}
|
||||
|
||||
function getIndicatorLabel(indicator) {
|
||||
const meta = getIndicatorMeta(indicator);
|
||||
if (!meta) return indicator.name;
|
||||
|
||||
const paramParts = meta.inputs.map(input => {
|
||||
const val = indicator.params[input.name];
|
||||
if (val !== undefined && val !== input.default) return val;
|
||||
return null;
|
||||
}).filter(v => v !== null);
|
||||
|
||||
if (paramParts.length > 0) {
|
||||
return `${indicator.name} (${paramParts.join(', ')})`;
|
||||
}
|
||||
return indicator.name;
|
||||
}
|
||||
|
||||
function getIndicatorMeta(indicator) {
|
||||
const IndicatorClass = IR?.[indicator.type];
|
||||
if (!IndicatorClass) return null;
|
||||
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||
return instance.getMetadata();
|
||||
}
|
||||
|
||||
function groupPlotsByColor(plots) {
|
||||
const groups = {};
|
||||
plots.forEach((plot, idx) => {
|
||||
const groupMap = {
|
||||
'fast': 'Fast', 'slow': 'Slow', 'upper': 'Upper', 'lower': 'Lower',
|
||||
'middle': 'Middle', 'basis': 'Middle', 'signal': 'Signal',
|
||||
'histogram': 'Histogram', 'k': '%K', 'd': '%D'
|
||||
};
|
||||
const groupName = Object.entries(groupMap).find(([k, v]) => plot.id.toLowerCase().includes(k))?.[1] || plot.id;
|
||||
if (!groups[groupName]) {
|
||||
groups[groupName] = { name: groupName, indices: [], plots: [] };
|
||||
}
|
||||
groups[groupName].indices.push(idx);
|
||||
groups[groupName].plots.push(plot);
|
||||
});
|
||||
return Object.values(groups);
|
||||
}
|
||||
|
||||
export function getActiveIndicators() {
|
||||
return activeIndicators;
|
||||
}
|
||||
|
||||
export function setActiveIndicators(indicators) {
|
||||
activeIndicators = indicators;
|
||||
renderIndicatorPanel();
|
||||
}
|
||||
|
||||
// Render main panel
|
||||
export function renderIndicatorPanel() {
|
||||
const container = document.getElementById('indicatorPanel');
|
||||
if (!container) return;
|
||||
|
||||
const available = getAvailableIndicators();
|
||||
const catalog = available.filter(ind => {
|
||||
if (searchQuery && !ind.name.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
||||
if (selectedCategory === 'all') return true;
|
||||
if (selectedCategory === 'favorites') return false;
|
||||
const cat = CATEGORY_MAP[ind.type] || 'trend';
|
||||
return cat === selectedCategory;
|
||||
});
|
||||
|
||||
const favoriteIds = new Set(userPresets.favorites || []);
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="indicator-panel">
|
||||
<!-- Search Bar -->
|
||||
<div class="indicator-search">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input
|
||||
type="text"
|
||||
id="indicatorSearch"
|
||||
placeholder="Search indicators..."
|
||||
value="${searchQuery}"
|
||||
autocomplete="off"
|
||||
>
|
||||
${searchQuery ? `<button class="search-clear" onclick="clearSearch()">×</button>` : ''}
|
||||
</div>
|
||||
|
||||
<!-- Categories -->
|
||||
<div class="category-tabs">
|
||||
${CATEGORIES.map(cat => `
|
||||
<button class="category-tab ${selectedCategory === cat.id ? 'active' : ''}" data-category="${cat.id}">
|
||||
${cat.icon} ${cat.name}
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<!-- Favorites (if any) -->
|
||||
${favoriteIds.size > 0 ? `
|
||||
<div class="indicator-section favorites">
|
||||
<div class="section-title">★ Favorites</div>
|
||||
${favoriteIds.map(id => {
|
||||
const ind = available.find(a => {
|
||||
// Find matching indicator by type
|
||||
return a.type === id || (activeIndicators.find(ai => ai.id === id)?.type === '');
|
||||
});
|
||||
if (!ind) return '';
|
||||
return renderIndicatorItem(ind, true);
|
||||
}).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Active Indicators -->
|
||||
${activeIndicators.length > 0 ? `
|
||||
<div class="indicator-section active">
|
||||
<div class="section-title">
|
||||
${activeIndicators.length} Active
|
||||
${activeIndicators.length > 0 ? `<button class="clear-all" onclick="clearAllIndicators()">Clear All</button>` : ''}
|
||||
</div>
|
||||
${activeIndicators.slice().reverse().map(ind => renderActiveIndicator(ind)).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Available Indicators -->
|
||||
${catalog.length > 0 ? `
|
||||
<div class="indicator-section catalog">
|
||||
<div class="section-title">Available Indicators</div>
|
||||
${catalog.map(ind => renderIndicatorItem(ind, false)).join('')}
|
||||
</div>
|
||||
` : `
|
||||
<div class="no-results">
|
||||
No indicators found
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
function renderIndicatorItem(indicator, isFavorite) {
|
||||
const colorDots = '';
|
||||
|
||||
return `
|
||||
<div class="indicator-item ${isFavorite ? 'favorite' : ''}" data-type="${indicator.type}">
|
||||
<div class="indicator-item-main">
|
||||
<span class="indicator-name">${indicator.name}</span>
|
||||
<span class="indicator-desc">${indicator.description || ''}</span>
|
||||
</div>
|
||||
<div class="indicator-actions">
|
||||
<button class="indicator-btn add" title="Add to chart" onclick="window.addIndicator('${indicator.type}')">
|
||||
+
|
||||
</button>
|
||||
${isFavorite ? '' : `
|
||||
<button class="indicator-btn favorite" title="Add to favorites" onclick="window.toggleFavorite('${indicator.type}')">
|
||||
${userPresets.favorites?.includes(indicator.type) ? '★' : '☆'}
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderActiveIndicator(indicator) {
|
||||
const isExpanded = configuringId === indicator.id;
|
||||
const meta = getIndicatorMeta(indicator);
|
||||
const label = getIndicatorLabel(indicator);
|
||||
const isFavorite = userPresets.favorites?.includes(indicator.type) || false;
|
||||
|
||||
return `
|
||||
<div class="indicator-item active ${isExpanded ? 'expanded' : ''}" data-id="${indicator.id}">
|
||||
<div class="indicator-item-main" onclick="window.toggleIndicatorExpand('${indicator.id}')">
|
||||
<div class="drag-handle" title="Drag to reorder">⋮⋮</div>
|
||||
<button class="indicator-btn visible" onclick="event.stopPropagation(); window.toggleIndicatorVisibility('${indicator.id}')" title="${indicator.visible !== false ? 'Hide' : 'Show'}">
|
||||
${indicator.visible !== false ? '👁' : '👁🗨'}
|
||||
</button>
|
||||
<span class="indicator-name">${label}</span>
|
||||
<div class="indicator-presets">
|
||||
${meta.name && renderPresetIndicatorIndicator(meta, indicator)}
|
||||
</div>
|
||||
<button class="indicator-btn favorite" onclick="event.stopPropagation(); window.toggleFavorite('${indicator.type}')" title="Add to favorites">
|
||||
${isFavorite ? '★' : '☆'}
|
||||
</button>
|
||||
<button class="indicator-btn expand ${isExpanded ? 'rotated' : ''}" title="Show settings">
|
||||
${isExpanded ? '▼' : '▶'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${isExpanded ? `
|
||||
<div class="indicator-config">
|
||||
${renderIndicatorConfig(indicator, meta)}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderPresetIndicatorIndicator(meta, indicator) {
|
||||
const hasPresets = getPresetsForIndicator(meta.name);
|
||||
if (!hasPresets || hasPresets.length === 0) return '';
|
||||
|
||||
return `<button class="preset-indicator" title="${hasPresets.length} saved presets" onclick="event.stopPropagation(); window.showPresets('${meta.name}')">💾</button>`;
|
||||
}
|
||||
|
||||
function renderIndicatorConfig(indicator, meta) {
|
||||
const plotGroups = groupPlotsByColor(meta?.plots || []);
|
||||
|
||||
return `
|
||||
<div class="config-sections">
|
||||
<!-- Colors -->
|
||||
<div class="config-section">
|
||||
<div class="section-subtitle">Visual Settings</div>
|
||||
${plotGroups.map(group => {
|
||||
const firstIdx = group.indices[0];
|
||||
const color = indicator.params[`_color_${firstIdx}`] || meta.plots[firstIdx]?.color || getDefaultColor(activeIndicators.indexOf(indicator));
|
||||
return `
|
||||
<div class="config-row">
|
||||
<label>${group.name} Color</label>
|
||||
<div class="color-picker">
|
||||
<input type="color" id="color_${indicator.id}_${firstIdx}" value="${color}" onchange="window.updateIndicatorColor('${indicator.id}', ${firstIdx}, this.value)">
|
||||
<span class="color-preview" style="background: ${color};"></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
|
||||
<div class="config-row">
|
||||
<label>Line Type</label>
|
||||
<select onchange="window.updateIndicatorSetting('${indicator.id}', '_lineType', this.value)">
|
||||
${LINE_TYPES.map(lt => `<option value="${lt}" ${indicator.params._lineType === lt ? 'selected' : ''}>${lt.charAt(0).toUpperCase() + lt.slice(1)}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<label>Line Width</label>
|
||||
<input type="range" min="1" max="5" value="${indicator.params._lineWidth || 2}" onchange="window.updateIndicatorSetting('${indicator.id}', '_lineWidth', parseInt(this.value))">
|
||||
<span class="range-value">${indicator.params._lineWidth || 2}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inputs -->
|
||||
${meta?.inputs && meta.inputs.length > 0 ? `
|
||||
<div class="config-section">
|
||||
<div class="section-subtitle">Parameters</div>
|
||||
${meta.inputs.map(input => `
|
||||
<div class="config-row">
|
||||
<label>${input.label}</label>
|
||||
${input.type === 'select' ?
|
||||
`<select onchange="window.updateIndicatorSetting('${indicator.id}', '${input.name}', this.value)">
|
||||
${input.options.map(o => `<option value="${o}" ${indicator.params[input.name] === o ? 'selected' : ''}>${o}</option>`).join('')}
|
||||
</select>` :
|
||||
`<div class="input-with-preset">
|
||||
<input
|
||||
type="number"
|
||||
value="${indicator.params[input.name]}"
|
||||
${input.min !== undefined ? `min="${input.min}"` : ''}
|
||||
${input.max !== undefined ? `max="${input.max}"` : ''}
|
||||
${input.step !== undefined ? `step="${input.step}"` : ''}
|
||||
onchange="window.updateIndicatorSetting('${indicator.id}', '${input.name}', parseFloat(this.value))"
|
||||
>
|
||||
<button class="presets-btn" onclick="window.showInputPresets('${indicator.id}', '${input.name}')">⋯</button>
|
||||
</div>`
|
||||
}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Presets -->
|
||||
<div class="config-section">
|
||||
<div class="section-subtitle">
|
||||
Presets
|
||||
<button class="preset-action-btn" onclick="window.savePreset('${indicator.id}')" title="Save current settings as preset">+ Save Preset</button>
|
||||
</div>
|
||||
${renderIndicatorPresets(indicator, meta)}
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="config-actions">
|
||||
<button class="btn-secondary" onclick="window.resetIndicator('${indicator.id}')">Reset to Defaults</button>
|
||||
<button class="btn-danger" onclick="window.removeIndicatorById('${indicator.id}')">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderIndicatorPresets(indicator, meta) {
|
||||
const presets = getPresetsForIndicator(meta.name);
|
||||
const instance = new IR[indicator.type]({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||
const metadata = instance.getMetadata();
|
||||
|
||||
return presets.length > 0 ? `
|
||||
<div class="presets-list">
|
||||
${presets.map(preset => {
|
||||
// Match values against current settings
|
||||
const isApplied = metadata.inputs.every(input =>
|
||||
preset.values[input.name] === indicator.params[input.name]
|
||||
);
|
||||
|
||||
return `
|
||||
<div class="preset-item ${isApplied ? 'applied' : ''}" data-preset="${preset.id}">
|
||||
<span class="preset-label" onclick="window.applyPreset('${indicator.id}', '${preset.id}')">${preset.name}</span>
|
||||
<button class="preset-delete" onclick="event.stopPropagation(); window.deletePreset('${preset.id}')" title="Delete preset">×</button>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
` : '<div class="no-presets">No saved presets</div>';
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
function setupEventListeners() {
|
||||
// Search
|
||||
const searchInput = document.getElementById('indicatorSearch');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
searchQuery = e.target.value;
|
||||
renderIndicatorPanel();
|
||||
});
|
||||
}
|
||||
|
||||
// Category tabs
|
||||
document.querySelectorAll('.category-tab').forEach(tab => {
|
||||
tab.addEventListener('click', (e) => {
|
||||
selectedCategory = tab.dataset.category;
|
||||
renderIndicatorPanel();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Actions
|
||||
window.clearSearch = function() {
|
||||
searchQuery = '';
|
||||
renderIndicatorPanel();
|
||||
};
|
||||
|
||||
window.clearAllIndicators = function() {
|
||||
activeIndicators.forEach(ind => {
|
||||
ind.series?.forEach(s => {
|
||||
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
||||
});
|
||||
});
|
||||
activeIndicators = [];
|
||||
configuringId = null;
|
||||
renderIndicatorPanel();
|
||||
drawIndicatorsOnChart();
|
||||
};
|
||||
|
||||
window.addIndicator = function(type) {
|
||||
const IndicatorClass = IR?.[type];
|
||||
if (!IndicatorClass) return;
|
||||
|
||||
const id = `${type}_${nextInstanceId++}`;
|
||||
const instance = new IndicatorClass({ type, params: {}, name: '' });
|
||||
const metadata = instance.getMetadata();
|
||||
|
||||
const params = {
|
||||
_lineType: 'solid',
|
||||
_lineWidth: 2
|
||||
};
|
||||
metadata.plots.forEach((plot, idx) => {
|
||||
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
||||
});
|
||||
metadata.inputs.forEach(input => {
|
||||
params[input.name] = input.default;
|
||||
});
|
||||
|
||||
activeIndicators.push({
|
||||
id,
|
||||
type,
|
||||
name: metadata.name,
|
||||
params,
|
||||
plots: metadata.plots,
|
||||
series: [],
|
||||
visible: true
|
||||
});
|
||||
|
||||
configuringId = id;
|
||||
renderIndicatorPanel();
|
||||
drawIndicatorsOnChart();
|
||||
};
|
||||
|
||||
window.toggleIndicatorExpand = function(id) {
|
||||
configuringId = configuringId === id ? null : id;
|
||||
renderIndicatorPanel();
|
||||
};
|
||||
|
||||
window.toggleIndicatorVisibility = function(id) {
|
||||
const indicator = activeIndicators.find(a => a.id === id);
|
||||
if (!indicator) return;
|
||||
|
||||
indicator.visible = indicator.visible === false ? true : false;
|
||||
|
||||
indicator.series?.forEach(s => {
|
||||
try {
|
||||
s.applyOptions({ visible: indicator.visible });
|
||||
} catch(e) {}
|
||||
});
|
||||
|
||||
renderIndicatorPanel();
|
||||
};
|
||||
|
||||
window.toggleFavorite = function(type) {
|
||||
const favorites = userPresets.favorites || [];
|
||||
const idx = favorites.indexOf(type);
|
||||
|
||||
if (idx >= 0) {
|
||||
favorites.splice(idx, 1);
|
||||
} else {
|
||||
favorites.push(type);
|
||||
}
|
||||
|
||||
userPresets.favorites = favorites;
|
||||
saveUserPresets();
|
||||
renderIndicatorPanel();
|
||||
};
|
||||
|
||||
window.updateIndicatorColor = function(id, index, color) {
|
||||
const indicator = activeIndicators.find(a => a.id === id);
|
||||
if (!indicator) return;
|
||||
|
||||
indicator.params[`_color_${index}`] = color;
|
||||
|
||||
const preview = document.querySelector(`#color_${id}_${index} + .color-preview`);
|
||||
if (preview) {
|
||||
preview.style.background = color;
|
||||
}
|
||||
|
||||
drawIndicatorsOnChart();
|
||||
};
|
||||
|
||||
window.updateIndicatorSetting = function(id, key, value) {
|
||||
const indicator = activeIndicators.find(a => a.id === id);
|
||||
if (!indicator) return;
|
||||
|
||||
indicator.params[key] = value;
|
||||
|
||||
const valueSpan = document.querySelector(`#color_${id}_${index} + .color-preview`);
|
||||
if (valueSpan) {
|
||||
valueSpan.textContent = value;
|
||||
}
|
||||
|
||||
drawIndicatorsOnChart();
|
||||
};
|
||||
|
||||
window.resetIndicator = function(id) {
|
||||
const indicator = activeIndicators.find(a => a.id === id);
|
||||
if (!indicator) return;
|
||||
|
||||
const IndicatorClass = IR?.[indicator.type];
|
||||
if (!IndicatorClass) return;
|
||||
|
||||
const instance = new IndicatorClass({ type: indicator.type, params: {}, name: indicator.name });
|
||||
const metadata = instance.getMetadata();
|
||||
|
||||
metadata.inputs.forEach(input => {
|
||||
indicator.params[input.name] = input.default;
|
||||
});
|
||||
|
||||
renderIndicatorPanel();
|
||||
drawIndicatorsOnChart();
|
||||
};
|
||||
|
||||
window.removeIndicatorById = function(id) {
|
||||
const idx = activeIndicators.findIndex(a => a.id === id);
|
||||
if (idx < 0) return;
|
||||
|
||||
activeIndicators[idx].series?.forEach(s => {
|
||||
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
||||
});
|
||||
|
||||
activeIndicators.splice(idx, 1);
|
||||
|
||||
if (configuringId === id) {
|
||||
configuringId = null;
|
||||
}
|
||||
|
||||
renderIndicatorPanel();
|
||||
drawIndicatorsOnChart();
|
||||
};
|
||||
|
||||
// Presets
|
||||
function getPresetsForIndicator(indicatorName) {
|
||||
const allPresets = Object.values(userPresets).flat().filter(p => typeof p === 'object' && p.name);
|
||||
return allPresets.filter(p => p.indicatorName === indicatorName);
|
||||
}
|
||||
|
||||
window.savePreset = function(id) {
|
||||
const indicator = activeIndicators.find(a => a.id === id);
|
||||
if (!indicator) return;
|
||||
|
||||
const presetName = prompt('Enter preset name:');
|
||||
if (!presetName) return;
|
||||
|
||||
const IndicatorClass = IR?.[indicator.type];
|
||||
if (!IndicatorClass) return;
|
||||
|
||||
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||
const meta = instance.getMetadata();
|
||||
|
||||
const preset = {
|
||||
id: `preset_${Date.now()}`,
|
||||
name: presetName,
|
||||
indicatorName: meta.name,
|
||||
values: {}
|
||||
};
|
||||
|
||||
meta.inputs.forEach(input => {
|
||||
preset.values[input.name] = indicator.params[input.name];
|
||||
});
|
||||
|
||||
if (!userPresets.presets) userPresets.presets = [];
|
||||
userPresets.presets.push(preset);
|
||||
saveUserPresets();
|
||||
renderIndicatorPanel();
|
||||
|
||||
alert(`Preset "${presetName}" saved!`);
|
||||
};
|
||||
|
||||
window.applyPreset = function(id, presetId) {
|
||||
const allPresets = Object.values(userPresets).flat().filter(p => typeof p === 'object' && p.id);
|
||||
const preset = allPresets.find(p => p.id === presetId);
|
||||
if (!preset) return;
|
||||
|
||||
const indicator = activeIndicators.find(a => a.id === id);
|
||||
if (!indicator) return;
|
||||
|
||||
Object.keys(preset.values).forEach(key => {
|
||||
indicator.params[key] = preset.values[key];
|
||||
});
|
||||
|
||||
renderIndicatorPanel();
|
||||
drawIndicatorsOnChart();
|
||||
};
|
||||
|
||||
window.deletePreset = function(presetId) {
|
||||
if (!confirm('Delete this preset?')) return;
|
||||
|
||||
if (userPresets.presets) {
|
||||
userPresets.presets = userPresets.presets.filter(p => p.id !== presetId);
|
||||
saveUserPresets();
|
||||
renderIndicatorPanel();
|
||||
}
|
||||
};
|
||||
|
||||
window.showPresets = function(indicatorName) {
|
||||
const presets = getPresetsForIndicator(indicatorName);
|
||||
if (presets.length === 0) {
|
||||
alert('No saved presets for this indicator');
|
||||
return;
|
||||
}
|
||||
|
||||
const menu = window.open('', '_blank', 'width=400,height=500');
|
||||
menu.document.write(`
|
||||
<html><head><title>Presets - ${indicatorName}</title><style>
|
||||
body { font-family: sans-serif; padding: 20px; background: #1e222d; color: #d1d4dc; }
|
||||
.preset { padding: 10px; margin: 5px; background: #131722; border-radius: 4px; }
|
||||
.preset:hover { background: #2a2e39; cursor: pointer; }
|
||||
</style></head><body>
|
||||
<h3>${indicatorName} Presets</h3>
|
||||
${presets.map(p => `<div class="preset" onclick="opener.applyPresetFromWindow('${p.id}')">${p.name}</div>`).join('')}
|
||||
</body></html>
|
||||
`;
|
||||
};
|
||||
|
||||
window.applyPresetFromWindow = function(presetId) {
|
||||
const indicator = activeIndicators.find(a => a.id === configuringId);
|
||||
if (!indicator) return;
|
||||
applyPreset(indicator.id, presetId);
|
||||
};
|
||||
|
||||
function saveUserPresets() {
|
||||
localStorage.setItem('indicator_presets', JSON.stringify(userPresets));
|
||||
}
|
||||
|
||||
// Chart drawing
|
||||
export function drawIndicatorsOnChart() {
|
||||
if (!window.dashboard || !window.dashboard.chart) return;
|
||||
|
||||
activeIndicators.forEach(ind => {
|
||||
ind.series?.forEach(s => {
|
||||
try { window.dashboard.chart.removeSeries(s); } catch(e) {}
|
||||
});
|
||||
});
|
||||
|
||||
const candles = window.dashboard.allData.get(window.dashboard.currentInterval);
|
||||
if (!candles || candles.length === 0) return;
|
||||
|
||||
const lineStyleMap = {
|
||||
'solid': LightweightCharts.LineStyle.Solid,
|
||||
'dotted': LightweightCharts.LineStyle.Dotted,
|
||||
'dashed': LightweightCharts.LineStyle.Dashed
|
||||
};
|
||||
|
||||
indicatorPanes.clear();
|
||||
nextPaneIndex = 1;
|
||||
|
||||
const overlayIndicators = [];
|
||||
const paneIndicators = [];
|
||||
|
||||
activeIndicators.forEach(ind => {
|
||||
const IndicatorClass = IR?.[ind.type];
|
||||
if (!IndicatorClass) return;
|
||||
|
||||
const instance = new IndicatorClass({ type: ind.type, params: ind.params, name: ind.name });
|
||||
const meta = instance.getMetadata();
|
||||
|
||||
if (meta.displayMode === 'pane') {
|
||||
paneIndicators.push({ indicator: ind, meta, instance });
|
||||
} else {
|
||||
overlayIndicators.push({ indicator: ind, meta, instance });
|
||||
}
|
||||
});
|
||||
|
||||
const totalPanes = 1 + paneIndicators.length;
|
||||
const mainPaneHeight = paneIndicators.length > 0 ? 60 : 100;
|
||||
const paneHeight = paneIndicators.length > 0 ? Math.floor(40 / paneIndicators.length) : 0;
|
||||
|
||||
window.dashboard.chart.panes()[0]?.setHeight(mainPaneHeight);
|
||||
|
||||
overlayIndicators.forEach(({ indicator, meta, instance }) => {
|
||||
if (indicator.visible === false) {
|
||||
indicator.series = [];
|
||||
return;
|
||||
}
|
||||
|
||||
renderIndicatorOnPane(indicator, meta, instance, candles, 0, lineStyleMap);
|
||||
});
|
||||
|
||||
paneIndicators.forEach(({ indicator, meta, instance }, idx) => {
|
||||
if (indicator.visible === false) {
|
||||
indicator.series = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const paneIndex = nextPaneIndex++;
|
||||
indicatorPanes.set(indicator.id, paneIndex);
|
||||
|
||||
renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap);
|
||||
|
||||
const pane = window.dashboard.chart.panes()[paneIndex];
|
||||
if (pane) {
|
||||
pane.setHeight(paneHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy compatibility functions
|
||||
window.renderIndicatorList = renderIndicatorPanel;
|
||||
window.toggleIndicator = addIndicator;
|
||||
window.showIndicatorConfig = function(id) {
|
||||
const ind = activeIndicators.find(a => a.id === id);
|
||||
if (ind) configuringId = id;
|
||||
renderIndicatorPanel();
|
||||
};
|
||||
window.applyIndicatorConfig = function() {
|
||||
// No-op - config is applied immediately
|
||||
};
|
||||
window.showIndicatorConfigByIndex = function(index) {
|
||||
if (index >= 0 && index < activeIndicators.length) {
|
||||
configuringId = activeIndicators[index].id;
|
||||
renderIndicatorPanel();
|
||||
}
|
||||
};
|
||||
|
||||
function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) {
|
||||
const results = instance.calculate(candles);
|
||||
indicator.series = [];
|
||||
|
||||
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
|
||||
const lineWidth = indicator.params._lineWidth || 2;
|
||||
|
||||
const firstNonNull = results?.find(r => r !== null && r !== undefined);
|
||||
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
||||
|
||||
meta.plots.forEach((plot, plotIdx) => {
|
||||
if (isObjectResult) {
|
||||
const hasData = results.some(r => r && r[plot.id] !== undefined && r[plot.id] !== null);
|
||||
if (!hasData) return;
|
||||
}
|
||||
|
||||
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
|
||||
|
||||
const data = [];
|
||||
for (let i = 0; i < candles.length; i++) {
|
||||
let value;
|
||||
if (isObjectResult) {
|
||||
value = results[i]?.[plot.id];
|
||||
} else {
|
||||
value = results[i];
|
||||
}
|
||||
|
||||
if (value !== null && value !== undefined) {
|
||||
data.push({
|
||||
time: candles[i].time,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (data.length === 0) return;
|
||||
|
||||
let series;
|
||||
|
||||
let plotLineStyle = lineStyle;
|
||||
if (plot.style === 'dashed') plotLineStyle = LightweightCharts.LineStyle.Dashed;
|
||||
else if (plot.style === 'dotted') plotLineStyle = LightweightCharts.LineStyle.Dotted;
|
||||
else if (plot.style === 'solid') plotLineStyle = LightweightCharts.LineStyle.Solid;
|
||||
|
||||
if (plot.type === 'histogram') {
|
||||
series = window.dashboard.chart.addSeries(LightweightCharts.HistogramSeries, {
|
||||
color: plotColor,
|
||||
priceFormat: { type: 'price', precision: 4, minMove: 0.0001 },
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: false
|
||||
}, paneIndex);
|
||||
} else if (plot.type === 'baseline') {
|
||||
series = window.dashboard.chart.addSeries(LightweightCharts.BaselineSeries, {
|
||||
baseValue: { type: 'price', price: plot.baseValue || 0 },
|
||||
topLineColor: plot.topLineColor || plotColor,
|
||||
topFillColor1: plot.topFillColor1 || plotColor,
|
||||
topFillColor2: '#00000000',
|
||||
bottomFillColor1: '#00000000',
|
||||
bottomColor: plot.bottomColor || '#00000000',
|
||||
lineWidth: plot.width !== undefined ? plot.width : lineWidth,
|
||||
lineStyle: plotLineStyle,
|
||||
title: plot.title || '',
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: plot.lastValueVisible !== false
|
||||
}, paneIndex);
|
||||
} else {
|
||||
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
|
||||
color: plotColor,
|
||||
lineWidth: plot.width !== undefined ? plot.width : lineWidth,
|
||||
lineStyle: plotLineStyle,
|
||||
title: plot.title || '',
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: plot.lastValueVisible !== false
|
||||
}, paneIndex);
|
||||
}
|
||||
|
||||
series.setData(data);
|
||||
indicator.series.push(series);
|
||||
});
|
||||
}
|
||||
|
||||
let indicatorPanes = new Map();
|
||||
let nextPaneIndex = 1;
|
||||
|
||||
// Legacy support
|
||||
window.renderIndicatorList = function() {
|
||||
renderIndicatorPanel();
|
||||
};
|
||||
|
||||
window.renderIndicatorConfig = function(indicator) {
|
||||
// Called by chart.js, redirect to new panel
|
||||
const meta = getIndicatorMeta(indicator);
|
||||
if (!meta) return;
|
||||
|
||||
const configEl = document.getElementById('configForm');
|
||||
if (configEl) {
|
||||
configEl.innerHTML = renderIndicatorConfig(indicator, meta);
|
||||
}
|
||||
};
|
||||
|
||||
window.applyIndicatorConfig = function() {
|
||||
// No-op - config is applied immediately
|
||||
};
|
||||
|
||||
window.removeIndicator = function() {
|
||||
if (!configuringId) return;
|
||||
removeIndicatorById(configuringId);
|
||||
};
|
||||
|
||||
window.removeIndicatorByIndex = function(index) {
|
||||
if (index < 0 || index >= activeIndicators.length) return;
|
||||
removeIndicatorById(activeIndicators[index].id);
|
||||
};
|
||||
|
||||
window.addIndicator = addIndicator;
|
||||
window.toggleIndicator = addIndicator;
|
||||
window.removeIndicatorById = removeIndicatorById;
|
||||
window.drawIndicatorsOnChart = drawIndicatorsOnChart;
|
||||
Reference in New Issue
Block a user