Search
Pro vizualizaci časových řad se hodí mít k dispozici nástroj na vykreslení grafu. Obrázek lze renderovat buď na straně serveru a odeslat klientovi v binární podobě, nebo s využitím vhodné knihovny (chart.js, D3.js, dygraphs, Google charts) posílat klientovi jen datové řady.
Při renderování na straně serveru je třeba nejprve vytvořit objekt, který reprezentuje graf. To lze jednoduše pomocí třídy Figure z knihovny matplotlib. Tento objekt pak lze ložit metodou savefig do binární podoby vybraného grafického formátu (např. png) a tu následně převést do binárního proudu třídou BytesIO z modulu io.
png
Funkce, která bude vrací binární proud odpovídající grafu funkce y=f(x) může vypadat třeba takto:
y=f(x)
from io import BytesIO from matplotlib.figure import Figure def render(x, y, format="png"): fig = Figure() ax = fig.subplots() ax.plot(x, y) buf = BytesIO() fig.savefig(buf, format=format) return buf
Nejobvyklejším řešením je vytvořit pro binární obrazová data koncový bod API serveru a přistupovat k němu pomocí atributu src tagu img. Ve Flasku bychom by bylo možné relizovat následovně:
src
img
from flask import Flask, Response app = Flask(__name__) @app.route('/') def index(): return '<img src="/img"/>' @app.route('/img') def image(): x = np.arange(0, 4*np.pi, 0.1) y = np.sin(2*x) buf = render(x, y) return Response(buf.getvalue(), mimetype='image/png') if __name__ == '__main__': app.run(debug=True)
Řešení má své výhody i nevýhody. Výhodou je přímočarost a možnost získat obrázek pro uložení na lokální úložiště. Nevýhodou je potřeba dvou TCP/IP spojení při použití protokolu HTTP/1.1 a protože obrázek vystupuje pod stále stejným URL, mohl by být uložen do cache prohlížeče klienta. Tomu lze zabránít např. tím, že za URL dodáme řetězec, který nijak neovlivní stažení obrázku, ale zároveň bude unikátní.
<img id="figure"/> <script> var timestamp = new Date().getTime(); var image = document.getElementById("figure"); image.src = "/img?t=" + timestamp; </script>
Tato technika využívá možnosti přenosu Base64 kódovaných bínárních dat prostřednictvím URL. Řetězec přenášený URL pak vypadá obecně takto:
data:[<mediatype>][;base64],<data>
Pro Base64 kódování binárního proudu pak poslouží modul base64. Klientovi se pak v rámci jedné HTTP odpovědi posílá img tag včetně dat (takže v případě komunikace prostřednictvím HTTP/1.1 není třeba vytvářet další TCP/IP spojení pro přenos binární podoby obrazu).
Příklad přenášeného tagu:
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAHCAYAAAA1WQxeAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4QoQAiIwiYqPWwAAAHNJREFUCNd1zaENQjEAhOGvgAFbAwnMwhoMUFeBYBEEVXQAGIIFGARXjXkC817SEPjdXfLfBR2ptD0WeNQcGUPPDUPNcTcVs85OWGObSjtNfUilwRxDt/SuOa7SpQmjfcbx6+5eczyEVNoGL79ZznD1n+cHk7cb99sXV8cAAAAASUVORK5CYII=">
Kód serveru ve Flasku, který při GET dotazu na kořenový koncový bod vrátí tag img včetně Base64 kódovaného URL může vypadat takto:
import base64 import numpy as np from flask import Flask app = Flask(__name__) @app.route('/') x = np.arange(0, 4*np.pi, 0.1) y = np.sin(2*x) buf = render(x, y) data = base64.b64encode(buf.getbuffer()).decode("ascii") return f"<img src='data:image/png;base64,{data}'/>" if __name__ == '__main__': app.run(debug=True)
Obecně platí, že objem přenesených dat je cca o 30% větší, než u standardně kódovaného obrázku. Přístup má výhodu např. u dynamicky měněných obrázků (URL se mění, takže se nevyužívá lokální cache).
Řešení využívá knihovnu chart.js a SSE.
from flask import Flask, Response, render_template import time import random app = Flask(__name__) class Point(): def __init__(self): self.temp = random.randint(19, 27) self.humi = random.randint(40, 80) def next(self): self.temp = self.temp + 0.5*random.randint(-1, 1) self.humi = self.humi + 0.5*random.randint(-1, 1) self.time = time.strftime('%H:%M:%S') def generate_data(): p = Point() while True: p.next() yield f"data: {p.time},{p.temp},{p.humi}\n\n" time.sleep(1) @app.route('/stats') def stats(): return Response(generate_data(), mimetype='text/event-stream') @app.route('/') def index(): return render_template('index.html') if __name__ == '__main__': app.run(debug=True)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Temperature & Humidity Chart</title> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script> <style> #tempChart { max-width: 90%; margin: 20px auto; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } </style> <script type="text/javascript"> document.addEventListener('DOMContentLoaded', function () { var ctx = document.getElementById('tempChart').getContext('2d'); var tempChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: 'temperature', data: [] },{ label: 'humidity', data: [] }] }, options: { scales: { y: { min: 0, max: 100, position: 'left', title: { display: true, text: 'Temperature (°C)' } }, y1 : { position: 'right', min: 0, max: 100, title: { display: true, text: 'Relative humidity (%)' } } } } }); var eventSource = new EventSource('/stats'); eventSource.onmessage = function (event) { var data = event.data.split(','); console.log(data); var currentTime = data[0]; var temp = parseInt(data[1]); var humi = parseInt(data[2]); if (tempChart.data.labels.length > 10) { tempChart.data.labels.shift(); tempChart.data.datasets[0].data.shift(); tempChart.data.datasets[1].data.shift(); } tempChart.data.labels.push(currentTime); tempChart.data.datasets[0].data.push(temp); tempChart.data.datasets[1].data.push(humi); tempChart.update(); }; }); </script> </head> <body> <h1>Temperature Chart</h1> <canvas id="tempChart" width="800" height="400"></canvas> </body> </html>