Fix chart sync, indicators popup z-index, and volume height
- Rewrite chart binding to use single unified sync handler per chart instead of cumulative handlers that conflicted with each other - Use v5 API unsubscribeVisibleTimeRangeChange for cleanup - Add _isSyncing flag to prevent recursive sync loops - Raise indicators popup z-index from 99 to 150 (above formation overlay at 100) to prevent formations from triggering mouseleave - Reduce volume indicator height from 30% to 15% of chart - Remove obsolete v2 scaleMargins call on Volume series - Add better candlestick init logging for debugging Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5874f1cc7a
commit
9a389d20d2
|
|
@ -398,7 +398,7 @@ height: 500px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: white;
|
color: white;
|
||||||
z-index: 99;
|
z-index: 150; /* Above formation overlay (z-index: 100) */
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,12 @@ class Charts {
|
||||||
/* A list of bound charts this is necessary for maintaining a dynamic
|
/* A list of bound charts this is necessary for maintaining a dynamic
|
||||||
number of charts with their position and zoom factors bound.*/
|
number of charts with their position and zoom factors bound.*/
|
||||||
this.bound_charts=[];
|
this.bound_charts=[];
|
||||||
|
// Store unsubscribe functions for cleanup when rebinding
|
||||||
|
this._syncUnsubscribes = [];
|
||||||
// Debounce timestamp to prevent infinite loop in chart synchronization
|
// Debounce timestamp to prevent infinite loop in chart synchronization
|
||||||
this._lastSyncTime = 0;
|
this._lastSyncTime = 0;
|
||||||
|
// Flag to prevent recursive sync
|
||||||
|
this._isSyncing = false;
|
||||||
// Only the main chart is created by default.
|
// Only the main chart is created by default.
|
||||||
this.create_main_chart();
|
this.create_main_chart();
|
||||||
}
|
}
|
||||||
|
|
@ -34,9 +38,11 @@ class Charts {
|
||||||
// Initialize the candlestick series if price_history is available
|
// Initialize the candlestick series if price_history is available
|
||||||
if (this.price_history && this.price_history.length > 0) {
|
if (this.price_history && this.price_history.length > 0) {
|
||||||
this.candleSeries.setData(this.price_history);
|
this.candleSeries.setData(this.price_history);
|
||||||
console.log('Candle series init:', this.price_history);
|
console.log(`Candle series initialized with ${this.price_history.length} candles`);
|
||||||
|
console.log('First candle:', this.price_history[0]);
|
||||||
|
console.log('Last candle:', this.price_history[this.price_history.length - 1]);
|
||||||
} else {
|
} else {
|
||||||
console.error('Price history is not available or is empty.');
|
console.error('Price history is not available or is empty. Received:', this.price_history);
|
||||||
}
|
}
|
||||||
this.bind_charts(this.chart_1);
|
this.bind_charts(this.chart_1);
|
||||||
}
|
}
|
||||||
|
|
@ -158,148 +164,74 @@ class Charts {
|
||||||
|
|
||||||
|
|
||||||
bind_charts(chart){
|
bind_charts(chart){
|
||||||
// keep a list of charts and bind all their position and spacing.
|
// Add chart to list if not already present
|
||||||
// Add (arg1) to bound_charts
|
if (!this.bound_charts.includes(chart)) {
|
||||||
this.add_to_list(chart);
|
|
||||||
// Get the number of objects in bound_charts
|
|
||||||
let bcl = Object.keys(this.bound_charts).length;
|
|
||||||
// if bound_charts has two element in it bind them
|
|
||||||
if (bcl == 2) { this.bind2charts(); }
|
|
||||||
|
|
||||||
// if bound_charts has three elements in it bind them
|
|
||||||
if (bcl == 3) { this.bind3charts(); }
|
|
||||||
|
|
||||||
// if bound_charts has four elements in it bind them
|
|
||||||
if (bcl == 4) { this.bind4charts(); }
|
|
||||||
|
|
||||||
// if bound_charts has five elements in it bind them
|
|
||||||
if (bcl == 5) { this.bind5charts(); }
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
add_to_list(chart){
|
|
||||||
// If the chart isn't already included in the list, add it.
|
|
||||||
if ( !this.bound_charts.includes(chart) ){
|
|
||||||
this.bound_charts.push(chart);
|
this.bound_charts.push(chart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only need to sync when we have 2+ charts
|
||||||
|
if (this.bound_charts.length >= 2) {
|
||||||
|
this._rebindAllCharts();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bind2charts(){
|
/**
|
||||||
//On change in chart 1 change chart 2
|
* Unsubscribe all existing sync handlers and set up fresh ones.
|
||||||
let syncHandler1 = (e) => {
|
* This prevents cumulative handlers from conflicting.
|
||||||
const now = Date.now();
|
*/
|
||||||
if (now - this._lastSyncTime < 50) return;
|
_rebindAllCharts() {
|
||||||
this._lastSyncTime = now;
|
// Unsubscribe all existing handlers using v5 API
|
||||||
// Get the barSpacing(zoom) and position of 1st chart (v5 API: options().barSpacing)
|
for (const {chart, handler} of this._syncUnsubscribes) {
|
||||||
let barSpacing1 = this.bound_charts[0].timeScale().options().barSpacing;
|
try {
|
||||||
let scrollPosition1 = this.bound_charts[0].timeScale().scrollPosition();
|
chart.timeScale().unsubscribeVisibleTimeRangeChange(handler);
|
||||||
// Apply barSpacing(zoom) and position to 2nd chart.
|
} catch (e) {
|
||||||
this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 });
|
// Ignore errors during cleanup
|
||||||
}
|
}
|
||||||
this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncHandler1);
|
}
|
||||||
|
this._syncUnsubscribes = [];
|
||||||
|
|
||||||
//On change in chart 2 change chart 1
|
// Create a single sync handler that works with all current charts
|
||||||
let syncHandler2 = (e) => {
|
const chartCount = this.bound_charts.length;
|
||||||
const now = Date.now();
|
|
||||||
if (now - this._lastSyncTime < 50) return;
|
|
||||||
this._lastSyncTime = now;
|
|
||||||
// Get the barSpacing(zoom) and position of chart 2 (v5 API: options().barSpacing)
|
|
||||||
let barSpacing2 = this.bound_charts[1].timeScale().options().barSpacing;
|
|
||||||
let scrollPosition2 = this.bound_charts[1].timeScale().scrollPosition();
|
|
||||||
// Apply barSpacing(zoom) and position to chart 1
|
|
||||||
this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
|
|
||||||
}
|
|
||||||
this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncHandler2);
|
|
||||||
}
|
|
||||||
bind3charts(){
|
|
||||||
|
|
||||||
//On change to chart 1 change chart 2 and 3
|
for (let sourceIndex = 0; sourceIndex < chartCount; sourceIndex++) {
|
||||||
let syncHandler = (e) => {
|
const sourceChart = this.bound_charts[sourceIndex];
|
||||||
const now = Date.now();
|
|
||||||
if (now - this._lastSyncTime < 50) return;
|
|
||||||
this._lastSyncTime = now;
|
|
||||||
// Get the barSpacing(zoom) and position of chart 1 (v5 API)
|
|
||||||
let barSpacing1 = this.bound_charts[0].timeScale().options().barSpacing;
|
|
||||||
let scrollPosition1 = this.bound_charts[0].timeScale().scrollPosition();
|
|
||||||
// Apply barSpacing(zoom) and position to new chart
|
|
||||||
this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 });
|
|
||||||
this.bound_charts[2].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 });
|
|
||||||
}
|
|
||||||
this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncHandler);
|
|
||||||
|
|
||||||
//On change to chart 2 change chart 1 and 3
|
const syncHandler = () => {
|
||||||
let syncHandler2 = (e) => {
|
// Prevent recursive sync (when we apply options, it triggers another event)
|
||||||
const now = Date.now();
|
if (this._isSyncing) return;
|
||||||
if (now - this._lastSyncTime < 50) return;
|
|
||||||
this._lastSyncTime = now;
|
|
||||||
// Get the barSpacing(zoom) and position of chart 2 (v5 API)
|
|
||||||
let barSpacing2 = this.bound_charts[1].timeScale().options().barSpacing;
|
|
||||||
let scrollPosition2 = this.bound_charts[1].timeScale().scrollPosition();
|
|
||||||
// Apply barSpacing(zoom) and position to chart 1 and 3
|
|
||||||
this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
|
|
||||||
this.bound_charts[2].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
|
|
||||||
}
|
|
||||||
this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncHandler2);
|
|
||||||
|
|
||||||
//On change to chart 3 change chart 1 and 2
|
|
||||||
let syncHandler3 = (e) => {
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - this._lastSyncTime < 50) return;
|
if (now - this._lastSyncTime < 50) return;
|
||||||
this._lastSyncTime = now;
|
|
||||||
// Get the barSpacing(zoom) and position of new chart (v5 API)
|
|
||||||
let barSpacing2 = this.bound_charts[2].timeScale().options().barSpacing;
|
|
||||||
let scrollPosition2 = this.bound_charts[2].timeScale().scrollPosition();
|
|
||||||
// Apply barSpacing(zoom) and position to parent chart
|
|
||||||
this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
|
|
||||||
this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
|
|
||||||
}
|
|
||||||
this.bound_charts[2].timeScale().subscribeVisibleTimeRangeChange(syncHandler3);
|
|
||||||
}
|
|
||||||
|
|
||||||
bind4charts(){
|
this._isSyncing = true;
|
||||||
// Sync all 4 charts together (v5 API: options().barSpacing)
|
|
||||||
let syncFromChart = (sourceIndex) => {
|
|
||||||
return (e) => {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - this._lastSyncTime < 50) return;
|
|
||||||
this._lastSyncTime = now;
|
this._lastSyncTime = now;
|
||||||
let barSpacing = this.bound_charts[sourceIndex].timeScale().options().barSpacing;
|
|
||||||
let scrollPosition = this.bound_charts[sourceIndex].timeScale().scrollPosition();
|
try {
|
||||||
for (let i = 0; i < 4; i++) {
|
const barSpacing = sourceChart.timeScale().options().barSpacing;
|
||||||
|
const scrollPosition = sourceChart.timeScale().scrollPosition();
|
||||||
|
|
||||||
|
// Apply to all other charts
|
||||||
|
for (let i = 0; i < this.bound_charts.length; i++) {
|
||||||
if (i !== sourceIndex) {
|
if (i !== sourceIndex) {
|
||||||
this.bound_charts[i].timeScale().applyOptions({ rightOffset: scrollPosition, barSpacing: barSpacing });
|
this.bound_charts[i].timeScale().applyOptions({
|
||||||
|
rightOffset: scrollPosition,
|
||||||
|
barSpacing: barSpacing
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
// Use setTimeout to reset flag after current event loop
|
||||||
|
setTimeout(() => {
|
||||||
|
this._isSyncing = false;
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(0));
|
|
||||||
this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(1));
|
|
||||||
this.bound_charts[2].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(2));
|
|
||||||
this.bound_charts[3].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(3));
|
|
||||||
}
|
|
||||||
|
|
||||||
bind5charts(){
|
// Subscribe and store chart+handler for later unsubscribe
|
||||||
// Sync all 5 charts together (main + RSI + MACD + %B + Patterns) (v5 API)
|
sourceChart.timeScale().subscribeVisibleTimeRangeChange(syncHandler);
|
||||||
let syncFromChart = (sourceIndex) => {
|
this._syncUnsubscribes.push({chart: sourceChart, handler: syncHandler});
|
||||||
return (e) => {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - this._lastSyncTime < 50) return;
|
|
||||||
this._lastSyncTime = now;
|
|
||||||
let barSpacing = this.bound_charts[sourceIndex].timeScale().options().barSpacing;
|
|
||||||
let scrollPosition = this.bound_charts[sourceIndex].timeScale().scrollPosition();
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
if (i !== sourceIndex) {
|
|
||||||
this.bound_charts[i].timeScale().applyOptions({ rightOffset: scrollPosition, barSpacing: barSpacing });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(0));
|
|
||||||
this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(1));
|
|
||||||
this.bound_charts[2].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(2));
|
|
||||||
this.bound_charts[3].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(3));
|
|
||||||
this.bound_charts[4].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(4));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set trade markers on chart for all trades in backtest results
|
// Set trade markers on chart for all trades in backtest results
|
||||||
setTradeMarkers(trades) {
|
setTradeMarkers(trades) {
|
||||||
|
|
|
||||||
|
|
@ -101,10 +101,10 @@ class Indicator {
|
||||||
priceScaleId: 'volume_ps',
|
priceScaleId: 'volume_ps',
|
||||||
});
|
});
|
||||||
// v5: scaleMargins must be set on the price scale, not series options
|
// v5: scaleMargins must be set on the price scale, not series options
|
||||||
// Volume should only take up bottom 30% of the chart
|
// Volume should only take up bottom 15% of the chart
|
||||||
chart.priceScale('volume_ps').applyOptions({
|
chart.priceScale('volume_ps').applyOptions({
|
||||||
scaleMargins: {
|
scaleMargins: {
|
||||||
top: 0.7,
|
top: 0.85,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -481,7 +481,7 @@ class Volume extends Indicator {
|
||||||
constructor(name, chart) {
|
constructor(name, chart) {
|
||||||
super(name);
|
super(name);
|
||||||
this.addHist(name, chart);
|
this.addHist(name, chart);
|
||||||
this.hist[name].applyOptions({ scaleMargins: { top: 0.95, bottom: 0.0 } });
|
// Note: scaleMargins are set in addHist() on the price scale (v5 API)
|
||||||
this.outputs = ['value'];
|
this.outputs = ['value'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue