python .\scripts\poc\test_web_chart_widget.pyContesto
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:
- 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.
- Frontend JavaScript in
QWebEngineView: Il widget utilizza unQWebEngineViewper caricare una pagina HTML dal server locale. Tutta la logica di rendering, interattività e stile del grafico è delegata al codice JavaScript di questa pagina. - 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
QWebChannelpermette 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:
-
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/.
-
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.
- Il widget usa un
-
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 nomebackend.
- Questa è la parte più importante. Viene stabilito un “ponte” (
-
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 ilQWebChannel. Chiama una funzione tipobackend.get_chart_data(). - Il
DataBridgein 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.
- Quando chiami il metodo
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:
- Assicurati di avere la cartella
fire/resources/chart_template/con il filechart.html(e i file JS necessari). - Salva il codice di test qui sopra.
- 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.