python .\scripts\poc\test_web_chart_widget.py

Contesto

Mentre le soluzioni di charting basate su Matplotlib e Plotly sono funzionali, presentano dei limiti intrinseci di performance e interattività quando si lavorano con grandi dataset finanziari. Le librerie di charting JavaScript specializzate (es. Lightweight Charts™) sono ordini di grandezza più performanti, ma la loro integrazione in un’applicazione desktop Qt è una sfida architetturale.

Questo POC è stato creato per validare l’approccio più avanzato e performante possibile: la creazione di un’architettura client-server disaccoppiata all’interno del widget stesso.

Decisione

È stato sviluppato un widget di charting, WebChartWidget, che implementa una mini architettura client-server per renderizzare grafici ad alte prestazioni.

L’architettura si basa su tre pilastri:

  1. Server HTTP Python Locale: All’avvio, il widget lancia un server web in un thread separato. Questo server ha il solo compito di servire i file statici (HTML, CSS, JS) di un template di grafico predefinito, che contiene una libreria di charting JavaScript.
  2. Frontend JavaScript in QWebEngineView: Il widget utilizza un QWebEngineView per caricare una pagina HTML dal server locale. Tutta la logica di rendering, interattività e stile del grafico è delegata al codice JavaScript di questa pagina.
  3. Ponte di Comunicazione QWebChannel: Viene stabilito un “ponte” bidirezionale tra Python (il “backend”) e JavaScript (il “frontend”). Un oggetto Python (DataBridge) viene esposto a JavaScript, permettendo al codice JS di richiedere i dati in formato JSON in modo asincrono.

Il flusso è disaccoppiato: Python si occupa solo di fornire i dati, mentre JavaScript si occupa di tutta la visualizzazione.

Conseguenze

Positive

  • Performance Estreme: Sfruttando librerie JavaScript ottimizzate, questo approccio permette di visualizzare e interagire fluidamente con centinaia di migliaia di punti dati, una performance irraggiungibile con le altre soluzioni.
  • Disaccoppiamento Architetturale Totale: La logica di visualizzazione (frontend) è completamente separata dalla logica dei dati (backend). È possibile cambiare radicalmente l’aspetto del grafico o persino la libreria di charting modificando solo i file JavaScript, senza toccare il codice Python.
  • Interattività Avanzata e Bidirezionale: Il QWebChannel permette una comunicazione complessa. Ad esempio, un’azione dell’utente nel grafico (come disegnare una linea) può inviare un segnale a Python, che può reagire di conseguenza.
  • Efficienza nel Trasferimento Dati: Invece di rigenerare un intero file HTML, vengono scambiati solo i dati JSON necessari, rendendo gli aggiornamenti molto più leggeri.

Negative / Limitazioni

  • Complessità di Implementazione: Questa è di gran lunga l’architettura più complessa da implementare e manutenere. Richiede competenze in Python, Qt, JavaScript, e nella comunicazione asincrona client-server.
  • Setup Iniziale: Richiede la preparazione di un template web separato e la gestione di un server multithread, con tutte le complessità che ne derivano (es. gestione delle porte, chiusura pulita del server).
  • Overhead del Server: Anche se leggero, l’esecuzione di un server HTTP e di un browser integrato consuma più risorse di base rispetto a una soluzione completamente nativa.

Cosa Fa lo Script: web_chart_widget.py

Questo file definisce un widget grafico che implementa un’architettura client-server in miniatura all’interno della tua stessa applicazione.

Invece di generare un file HTML completo e caricarlo (come faceva il PlotlyChartWidget), questo approccio è molto più sofisticato:

  1. Avvia un Server Web Locale (Backend Python):

    • Quando il widget viene creato, avvia un piccolo server HTTP in un thread separato.
    • Questo server non serve l’intero web, ma solo i file statici (HTML, CSS, JS) di un template di grafico che si trovano in fire/resources/chart_template/.
  2. Carica una Pagina Web Statica (Frontend JavaScript):

    • Il widget usa un QWebEngineView (il solito browser integrato) per caricare una singola pagina: http://localhost:8001/chart.html.
    • Questa pagina HTML contiene una libreria di charting JavaScript (probabilmente Lightweight Charts™ by TradingView, a giudicare dal formato dati time, open, high, low, close) e del codice JavaScript che sa come disegnare un grafico.
  3. Crea un Ponte di Comunicazione (QWebChannel):

    • Questa è la parte più importante. Viene stabilito un “ponte” (QWebChannel) tra il mondo Python (backend) e il mondo JavaScript (frontend).
    • Un oggetto Python (DataBridge) viene “esposto” a JavaScript sotto il nome backend.
  4. Flusso di Dati al Momento del Plot:

    • Quando chiami il metodo plot(data) in Python, i dati (il DataFrame di Pandas) non vengono iniettati nell’HTML.
    • Invece, vengono convertiti in un formato JSON e salvati nell’oggetto DataBridge.
    • Poi, il widget ricarica la pagina chart.html.
    • Una volta che la pagina JavaScript è caricata, il suo codice esegue una chiamata all’oggetto backend (che in realtà è il tuo oggetto Python) tramite il QWebChannel. Chiama una funzione tipo backend.get_chart_data().
    • Il DataBridge in Python risponde, inviando i dati in formato JSON attraverso il ponte.
    • Il codice JavaScript riceve i dati e li usa per disegnare il grafico usando la libreria di charting.

In sintesi, stai creando un’architettura disaccoppiata:

  • Python è responsabile solo di fornire i dati grezzi.
  • JavaScript è responsabile di tutta la logica di visualizzazione e interattività.

Vantaggi di Questo Approccio:

  • Performance Estreme: Librerie come Lightweight Charts sono ottimizzate per renderizzare centinaia di migliaia di punti dati in modo fluido, con zoom e pan istantanei. Le performance sono ordini di grandezza superiori a quelle di Matplotlib o Plotly per grandi dataset.
  • Disaccoppiamento Totale: Puoi cambiare completamente la libreria di charting JavaScript o l’aspetto del grafico modificando solo i file in resources/chart_template/ senza toccare una riga di codice Python.
  • Interattività Avanzata: Puoi implementare logiche molto complesse in JavaScript (es. disegnare linee di trend, aggiungere indicatori custom) e farle comunicare con Python tramite il QWebChannel (es. inviare a Python le coordinate di una linea disegnata dall’utente).
  • Leggerezza: Invece di rigenerare un intero file HTML da 2MB ad ogni aggiornamento (come fa Plotly), qui si inviano solo i dati JSON necessari, che sono molto più piccoli.

Svantaggi:

  • Complessità Architetturale: È di gran lunga l’approccio più complesso da implementare. Richiede conoscenze di Python, Qt, JavaScript e dell’architettura client-server.
  • Setup Iniziale: Richiede la creazione di un template HTML/JS separato e la gestione di un server locale.

Come Eseguirlo

Dato che questo widget è molto più complesso, anche il codice di test è leggermente più articolato. Assicurati di avere una cartella fire/resources/chart_template/ con dentro un file chart.html e il codice della libreria di charting.

Crea un file test_web_chart_widget.py nella cartella scripts/poc/ e incollaci questo codice.

# test_web_chart_widget.py
import sys
import pandas as pd
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtCore import QTimer
 
# Aggiungi la root del progetto al path per trovare il modulo 'fire'
import os
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
if project_root not in sys.path:
    sys.path.insert(0, project_root)
 
# Ora importa il tuo widget
from fire.ui_components.charts.web_chart_widget import WebChartWidget
 
def create_sample_data(num_points=200):
    """Crea un DataFrame di esempio con dati più realistici."""
    start_date = pd.to_datetime("2023-01-01")
    dates = pd.date_range(start=start_date, periods=num_points, freq='D')
    
    # Simula un andamento di prezzo
    close = 100 + pd.Series(range(num_points)).cumsum() * 0.1 + pd.Series(range(num_points)).apply(lambda x: 5 * pd.np.sin(x/10))
    open_ = close.shift(1).fillna(100)
    high = pd.concat([open_, close], axis=1).max(axis=1) + pd.np.random.uniform(0, 2, size=num_points)
    low = pd.concat([open_, close], axis=1).min(axis=1) - pd.np.random.uniform(0, 2, size=num_points)
    
    data = {
        'Open': open_, 'High': high, 'Low': low, 'Close': close
    }
    df = pd.DataFrame(data, index=dates)
    return df
 
if __name__ == "__main__":
    app = QApplication(sys.argv)
    
    window = QMainWindow()
    window.setWindowTitle("Test Web Chart Widget (Lightweight Charts)")
    window.resize(1200, 800)
    
    # Crea un'istanza del tuo widget
    chart_widget = WebChartWidget()
    
    window.setCentralWidget(chart_widget)
    window.show()
    
    # Fornisci i dati per disegnarli
    sample_data = create_sample_data(500)
    
    # Usiamo un QTimer per assicurarci che il server sia partito prima di plottare
    # Questo è un dettaglio importante per un'architettura asincrona come questa
    QTimer.singleShot(1000, lambda: chart_widget.plot(data=sample_data))
    
    sys.exit(app.exec())

Istruzioni per l’esecuzione:

  1. Assicurati di avere la cartella fire/resources/chart_template/ con il file chart.html (e i file JS necessari).
  2. Salva il codice di test qui sopra.
  3. Esegui il file di test:
    python .\scripts\poc\test_web_chart_widget.py

Dovresti vedere apparire una finestra, e dopo un secondo (il tempo del QTimer), verrà caricato un grafico molto performante e interattivo.