====== Kreslení grafů ======
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 ([[https://www.chartjs.org/|chart.js]], [[https://d3js.org/|D3.js]], [[https://dygraphs.com/|dygraphs]], [[https://developers.google.com/chart|Google charts]]) posílat klientovi jen datové řady.
===== Render na straně serveru =====
Při renderování na straně serveru je třeba nejprve vytvořit objekt, který reprezentuje graf. To lze jednoduše pomocí třídy [[https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.html|Figure]] z knihovny [[https://matplotlib.org/|matplotlib]]. Tento objekt pak lze ložit metodou [[https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.savefig.html|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 [[https://docs.python.org/3/library/io.html#io.BytesIO|BytesIO]] z modulu [[https://docs.python.org/3/library/io.html|io]].
Funkce, která bude vrací binární proud odpovídající grafu funkce ''y=f(x)'' může vypadat třeba takto:
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
==== 1. Posílání binárně kódovaných obrazových dat ====
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ě:
from flask import Flask, Response
app = Flask(__name__)
@app.route('/')
def index():
return '
'
@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í.
==== 2. URL v Base64 kódování ====
Tato technika využívá možnosti přenosu [[https://cs.wikipedia.org/wiki/Base64|Base64]] kódovaných bínárních dat prostřednictvím URL. Řetězec přenášený URL pak vypadá obecně takto:
data:[][;base64],
Pro Base64 kódování binárního proudu pak poslouží modul [[https://docs.python.org/3/library/base64.html|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:
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"
"
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).
===== Render na straně klienta =====
Řešení využívá knihovnu [[https://www.chartjs.org|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)
Temperature & Humidity Chart
Temperature Chart