import flask import matplotlib.pyplot as plt import io # input / output import pandas as pd # Notes for matplotlib.pyplot module """ fig, ax = plt.subplots(figsize=(3, 2)) enables us to create a new plot fig.savefig(<file object>, format=<fig format>) enables us to save the figure into a file - default format is png plt.close() closes most recent fig plt.tight_layout() enables us to avoid cropping of the image """ # Notes for io module """ io.BytesIO for fake binary file io.StringIO for fake text file <fileobject>.getvalue() returns the content of the fake file """ # Notes for flask """ flask.request.args enables us process query string @app.route("<URL>", methods=["POST"]) enables us to process HTTP POST requests flask.request.get_data() enables us to access data (byte format) sent via HTTP POST request """ temps = [80, 85, 83, 90] app = flask.Flask("my dashboard") # DYNAMIC @app.route("/") def home(): return """ <html> <body bgcolor="Salmon"> <h3>Example 1: PNG</h3> <img src="plot1.png"> <h3>Example 2: SVG</h3> <img src="plot2.svg"> </body> </html> """ # TODO: add route to plot1.png """ IMPORTANT: file name and extension should match as in html content Steps: 1. generate a plot 2. return the image contents: 2a. v1: write and read from a temporary file 2b. v2: use a fake file (io module) 3. fix the content type: default content type is text/html: Content-Type: text/html 3a. How can we find various content types? Google for "MIME types". 4. IMPORTANT: close the figure. If this is not done, after 20 refreshes, you will start getting the below warning: More than 20 figures have been opened. Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume too much memory. (To control this warning, see the rcParam `figure.max_open_warning`). 5. try to set y_label for the plot. This will not show up. 6. create a line plot with temps Series """ @app.route("/plot1.png") def plot1(): fig, ax = plt.subplots(figsize=(3, 2)) #pd.Series(temps).plot.line(ax=ax) #CDF code s = pd.Series(sorted(temps)) rev = pd.Series((s.index+1)/len(s)*100, index=s.values) rev.plot.line(ax=ax, ylim=0, drawstyle="steps-post") ax.set_xlabel("Temperatures") ax.set_ylabel("Distribution") plt.tight_layout() # v1 - write and read a temporary plot file (cumbersome) # with open("temporary.png", "wb") as f: # fig.savefig(f) # with open("temporary.png", "rb") as f: # return f.read() # v2 - write and read from a fake file f = io.BytesIO() fig.savefig(f) plt.close() return flask.Response(f.getvalue(), headers={"Content-type": "image/png"}) # TODO: add route to plot2.svg """ IMPORTANT: file name and extension should match as in html content Things to change from plot1 function: 1. Change content type 2. Change format for savefig 3. SVG files have text type (unlike PNG) - so we should use io.StringIO """ @app.route("/plot2.svg") def plot2(): fig, ax = plt.subplots(figsize=(3, 2)) #pd.Series(temps).plot.line(ax=ax) pd.Series(temps).plot.hist(ax=ax, bins=100) ax.set_ylabel("Temperatures") plt.tight_layout() f = io.StringIO() fig.savefig(f, format="svg") plt.close() return flask.Response(f.getvalue(), headers={"Content-type": "image/svg+xml"}) # TODO: add route for "/upload" """ Steps: 1. support query string: - with key/parameter as temps and value as "," separated temperature values - add the values into temps list 2. return len(temps) Disadvantages of query string approach: 1. If we have a lot of data, it is difficult to type. What if we are trying to upload a video? 2. Caching: - memory of what we have already seen before; instead of slow web request, show what was already sent previously for the same request - browser cache - cache devices that sit in front of the server - server caching Use POST request instead: 1. Update route to add "methods=["POST"]" 2. Humans don't send POST requests, instead we need to use "curl -X POST <URL> -d <data>" --- curl is a simple command line tool that enables us to send HTTP requests 3. Use flask.request.get_data() - make sure to convert type to str """ @app.route("/upload", methods=["POST"]) def upload(): # v1 - query string # new_temps = flask.request.args["temps"] # new_temps = new_temps.split(",") # for val in new_temps: # temps.append(float(val)) # v2 - POST request new_temps = str(flask.request.get_data(), encoding="utf-8") new_temps = new_temps.split(",") for val in new_temps: temps.append(float(val)) return f"thanks, you now have {len(temps)} records" # TODO: change SVG to histogram - very sensitive to number of "bins" # TODO: change PNG to CDF (Cumulative Distribution Function): """ Idea: sort the data to observe some distribution, then switch x and y axes Steps: 1. sort the data 2. switch x and y axes 3. normalize the y axis from 0 to 100 - make sure to set ylim to 0 4. change line plot "drawstyle" to "steps-post" - avoid extrapolating information between points s = pd.Series(sorted(temps)) rev = pd.Series((s.index+1)/len(s)*100, index=s.values) rev.plot.line(ax=ax, ylim=0, drawstyle="steps-post") """ if __name__ == "__main__": # threaded must be False whenever we use matplotlib app.run(host="0.0.0.0", debug=True, threaded=False) # app.run never returns, so don't define functions # after this (the def lines will never be reached)