From 5efd65245606c87c66640af4dfb947da0f765f72 Mon Sep 17 00:00:00 2001 From: DiTus Date: Thu, 19 Mar 2026 23:19:38 +0100 Subject: [PATCH] feat: added background fill to first hurst indicator --- js/ui/chart.js | 15 ++-- js/ui/indicators-panel-new.js | 164 +++++++++++++++++++++++++++++----- 2 files changed, 149 insertions(+), 30 deletions(-) diff --git a/js/ui/chart.js b/js/ui/chart.js index db0166e..caf3461 100644 --- a/js/ui/chart.js +++ b/js/ui/chart.js @@ -314,15 +314,14 @@ constructor() { }, }, localization: { - timeFormatter: (timestamp) => { - return TimezoneConfig.formatDate(timestamp * 1000); - }, + timeFormatter: (timestamp) => { + return TimezoneConfig.formatDate(timestamp * 1000); + }, + }, + handleScroll: { + vertTouchDrag: false, }, - handleScroll: { - vertTouchDrag: false, - }, - }); - + }); // Setup price format selector change handler const priceSelect = document.getElementById("priceFormatSelect"); if (priceSelect) { diff --git a/js/ui/indicators-panel-new.js b/js/ui/indicators-panel-new.js index a86b927..830b34f 100644 --- a/js/ui/indicators-panel-new.js +++ b/js/ui/indicators-panel-new.js @@ -817,6 +817,123 @@ function saveUserPresets() { localStorage.setItem('indicator_presets', JSON.stringify(userPresets)); } +// Custom Primitive for filling area between two lines +class SeriesAreaFillPrimitive { + constructor(data, color) { + this._data = data || []; + this._color = color || 'rgba(128, 128, 128, 0.05)'; + this._paneViews = [new SeriesAreaFillPaneView(this)]; + } + + setData(data) { + this._data = data; + this._requestUpdate?.(); + } + + setColor(color) { + this._color = color; + this._requestUpdate?.(); + } + + attached(param) { + this._chart = param.chart; + this._series = param.series; + this._requestUpdate = param.requestUpdate; + this._requestUpdate(); + } + + detached() { + this._chart = undefined; + this._series = undefined; + this._requestUpdate = undefined; + } + + updateAllViews() { + this._requestUpdate?.(); + } + + paneViews() { + return this._paneViews; + } +} + +class SeriesAreaFillPaneView { + constructor(source) { + this._source = source; + } + + renderer() { + return new SeriesAreaFillRenderer(this._source); + } +} + +class SeriesAreaFillRenderer { + constructor(source) { + this._source = source; + } + + draw(target) { + if (!this._source._chart || !this._source._series || this._source._data.length === 0) return; + + target.useBitmapCoordinateSpace((scope) => { + const ctx = scope.context; + const series = this._source._series; + const chart = this._source._chart; + const data = this._source._data; + const color = this._source._color; + const ratio = scope.horizontalPixelRatio; + + ctx.save(); + ctx.beginPath(); + + let started = false; + + // Draw top line (upper) forward + for (let i = 0; i < data.length; i++) { + const point = data[i]; + const timeCoordinate = chart.timeScale().timeToCoordinate(point.time); + if (timeCoordinate === null) continue; + + const upperY = series.priceToCoordinate(point.upper); + if (upperY === null) continue; + + const x = timeCoordinate * ratio; + const y = upperY * ratio; + + if (!started) { + ctx.moveTo(x, y); + started = true; + } else { + ctx.lineTo(x, y); + } + } + + // Draw bottom line (lower) backward + for (let i = data.length - 1; i >= 0; i--) { + const point = data[i]; + const timeCoordinate = chart.timeScale().timeToCoordinate(point.time); + if (timeCoordinate === null) continue; + + const lowerY = series.priceToCoordinate(point.lower); + if (lowerY === null) continue; + + const x = timeCoordinate * ratio; + const y = lowerY * ratio; + + ctx.lineTo(x, y); + } + + if (started) { + ctx.closePath(); + ctx.fillStyle = color; + ctx.fill(); + } + + ctx.restore(); + }); + } +} + function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) { // Recalculate with current TF candles (or use cached if they exist and are the correct length) let results = indicator.cachedResults; @@ -863,8 +980,11 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li } let plotsCreated = 0; - let dataPointsAdded = 0; + // Special logic for Hurst fill + let hurstFillData = []; + const isFirstHurst = indicator.type === 'hurst' && activeIndicators.filter(ind => ind.type === 'hurst')[0].id === indicator.id; + meta.plots.forEach((plot, plotIdx) => { if (isObjectResult) { const hasData = results.some(r => r && r[plot.id] !== undefined && r[plot.id] !== null); @@ -874,20 +994,24 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff'; const data = []; - let firstDataIndex = -1; for (let i = 0; i < candles.length; i++) { let value; if (isObjectResult) { value = results[i]?.[plot.id]; + + // Collect fill data if this is Hurst + if (isFirstHurst && results[i]) { + // Ensure we only add once per index + if (!hurstFillData[i]) hurstFillData[i] = { time: candles[i].time }; + if (plot.id === 'upper') hurstFillData[i].upper = value; + if (plot.id === 'lower') hurstFillData[i].lower = value; + } } else { value = results[i]; } if (value !== null && value !== undefined && typeof value === 'number' && Number.isFinite(value)) { - if (firstDataIndex === -1) { - firstDataIndex = i; - } data.push({ time: candles[i].time, value: value @@ -895,14 +1019,7 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li } } - console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: ${data.length} data points created, first data at index ${firstDataIndex}/${candles.length}`); - - if (data.length === 0) { - console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: No data to render`); - return; - } - - console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: Creating series with ${data.length} data points [${data[0].time} to ${data[data.length - 1].time}]`); + if (data.length === 0) return; let series; let plotLineStyle = lineStyle; @@ -947,23 +1064,18 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li series.setData(data); indicator.series.push(series); plotsCreated++; - console.log(`Created series for ${indicator.id}, plot=${plot.id}, total series now=${indicator.series.length}`); - // Create horizontal band lines for RSI + // Attach RSI bands if (meta.name === 'RSI' && indicator.series.length > 0) { const mainSeries = indicator.series[0]; const overbought = indicator.params.overbought || 70; const oversold = indicator.params.oversold || 30; - // Remove existing price lines first while (indicator.bands && indicator.bands.length > 0) { - try { - indicator.bands.pop(); - } catch(e) {} + try { indicator.bands.pop(); } catch(e) {} } indicator.bands = indicator.bands || []; - // Create overbought band line indicator.bands.push(mainSeries.createPriceLine({ price: overbought, color: '#787B86', @@ -972,8 +1084,6 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li axisLabelVisible: false, title: '' })); - - // Create oversold band line indicator.bands.push(mainSeries.createPriceLine({ price: oversold, color: '#787B86', @@ -984,6 +1094,16 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li })); } }); + + // Attach Hurst Fill Primitive + if (isFirstHurst && hurstFillData.length > 0 && indicator.series.length > 0) { + // Filter out incomplete data points + const validFillData = hurstFillData.filter(d => d && d.time && d.upper !== undefined && d.lower !== undefined); + + // Attach to the first series (usually upper or lower band) + const fillPrimitive = new SeriesAreaFillPrimitive(validFillData, 'rgba(128, 128, 128, 0.05)'); + indicator.series[0].attachPrimitive(fillPrimitive); + } } // Completely redraw indicators (works for both overlay and pane)