Dahsboard app for viewing data ============================== A simple dashboard was created using the Dash library in python in order to view the collected data. The dashboard consists of 3 pages displaying different information General structure ------------------------------ The app is broken down into 4 pieces. A landing page when the app is accessed and pages to view recent data, historical data and images. Each page is defined in its own file in a folder called pages. To include the page, it has to be added by the built in register page function, which takes in a page name and what number in the sequence it is. .. code-block:: python dash.register_page(__name__, name="Page_name", order=0) The page file is defined as normal by defining the page layout. .. code-block:: python :linenos: layout = html.Div( children=[ html.H1( id='header_text', children='Text to display'), html.Div( children=[ dcc.Graph( id='graph name', figure=createmainSubplot(df) ), dcc.Interval( id='update_interval', interval=4000, n_intervals=0 ) ] ) ] ) The main program initializes the app. Here a div container is created that holds all the previously defined pages as other div containers. This is where the buttons to access other pages are created. Finally, it displays the currently selected page. .. code-block:: python :linenos: app = Dash(__name__, use_pages=True) app.layout = html.Div( children=[ html.H1( children=[ "MASXXX DataView" ], style={ 'textAlign':'center' } ), #Create links to other pages html.Div( children=[ html.Div( children=[ dcc.Link(html.Button(page['name'], style={'width':'100px'}), href=page["relative_path"], style={'padding':'5px'}) for page in dash.page_registry.values() if page['name'] != 'Home' ], ) ] ), #Show the contents of the current page dash.page_container, ] ) Landing page ------------ The landing page is the first page shown when the app is accessed. Here, the user is told a little about what the pages contain and are presented with buttons to access each individual page. .. code-block:: python :linenos: dash.register_page(__name__, name="Home", path='/') layout = html.Div( children=[ html.H1( children='Wecome to the MASXXX data view app', style={'textAlign':'center'} ), html.Div( children="In the top left corner you find buttons that take you to individual pages to view specific data.", style={'textAlign':'center'} ), html.Img(src='assets/3.jpg', style={'width':'50%'}) ] ) Recent data page -------------------- The recent data page displays all 8 measurements taken the past 90 minutes. .. code-block:: python :linenos: dash.register_page(__name__, name="Recent Data", order=0) layout = html.Div( children=[ html.H1( id='header_text', children='Recent Data'), html.Div( children=[ dcc.Graph( id='recent_data_subplots', figure=createmainSubplot(df) ), dcc.Interval( id='update_interval', interval=4000, n_intervals=0 ) ] ) ] ) This page has a timed callback that fetches new data evert 5 minutes, then refreshes the graphs. .. code-block:: python :linenos: @callback( Output(component_id='recent_data_subplots', component_property='figure'), Input(component_id='update_interval', component_property='n_intervals') ) def updateRecentData(n_intervals): #Reload dataframe df = pd.read_csv('server/data.csv') dfRows = df.shape[0] if dfRows > 400: df = df[dfRows - 400 : dfRows] return createmainSubplot(df) The graphs are drawn with a custom function that uses the Plotly library in order to create a custom subplot. The new subplot figure is returned to the callback function, which returns it to the graph element, replacing the previous graph. .. code-block:: python :linenos: def createmainSubplot(df): mainSubplotFigure = make_subplots(rows=4, cols=2, subplot_titles=['Soil', 'Humidity', 'Temperature Inside', 'Temperature Outside', 'eCO2', 'TVOC', 'NA', 'PH']) mainSubplotFigure.add_trace(go.Scatter(x=df['time'], y=df['Soil']), row=1, col=1) mainSubplotFigure.add_trace(go.Scatter(x=df['time'], y=df['Humidity']), row=1, col=2) mainSubplotFigure.add_trace(go.Scatter(x=df['time'], y=df['Temperature_in']), row=2, col=1) mainSubplotFigure.add_trace(go.Scatter(x=df['time'], y=df['Temperature_out']), row=2, col=2) mainSubplotFigure.add_trace(go.Scatter(x=df['time'], y=df['eCO2']), row=3, col=1) mainSubplotFigure.add_trace(go.Scatter(x=df['time'], y=df['TVOC']), row=3, col=2) mainSubplotFigure.add_trace(go.Scatter(x=df['time'], y=df['TVOC']), row=4, col=1) mainSubplotFigure.add_trace(go.Scatter(x=df['time'], y=df['PH']), row=4, col=2) mainSubplotFigure.update_yaxes(title_text="", row=1, col=1) #Soil 1 plot mainSubplotFigure.update_yaxes(title_text="%", row=1, col=2) #Humidity plot mainSubplotFigure.update_yaxes(title_text="°C", row=2, col=1) #Temp inside plot mainSubplotFigure.update_yaxes(title_text="°C", row=2, col=2) #Temp outside mainSubplotFigure.update_yaxes(title_text="PPM", row=3, col=1) #CO2 mainSubplotFigure.update_yaxes(title_text="PH", row=3, col=2) #TVOC mainSubplotFigure.update_yaxes(title_text="PPM", row=4, col=1) #CO2 mainSubplotFigure.update_yaxes(title_text="", row=4, col=2) #TVOC mainSubplotFigure.update_layout(showlegend=False) mainSubplotFigure.update_layout(height=600) return mainSubplotFigure Historical data page -------------------- This page shows data over a longer time frame than the recent data page. The data to view is selected with a dropdown menu, and the appropriate data is loaded in and presented. .. code-block:: python :linenos: df = pd.read_csv('server/data.csv') dash.register_page(__name__, name="Historical Data", order=1) layout = html.Div( children=[ html.H1('Historical Data'), html.Div( "This is the page for viewing selected historical data" ), html.Div( children=[ dcc.Dropdown( id='selection_dropdown', options=df.columns[:-1], value="Humidity" ) ] ), html.Div( id='graph_container', children=[ dcc.Graph( id="historical_graph", figure=px.line(x = df['time'], y = df['Humidity']) ) ] ) ] ) This page also uses a callback function. Based on what data category is selected from the dropdown, the appropriate column is selected from the dataframe and displayed. If the selection is left empty, humudidy is displayed by default. .. code-block:: python :linenos: @callback( Output(component_id='historical_graph', component_property='figure'), Input(component_id='selection_dropdown', component_property='value') ) def updateGraph(selectedColumn): if selectedColumn is None: return px.line(x = df['time'], y = df['Humidity']) return px.line(x = df['time'], y = df[selectedColumn]) Pictures page ------------- The final page is for displaying pictures of the plants as they grow. One picture should be taken per day, and a slider is used to select the day. By sliding the slider back and forth, a timelapse of the plants growing will show. .. code-block:: python :linenos: dash.register_page(__name__, name="Pictures", order=3) layout = html.Div( children=[ html.H1('Pictures'), html.H3('I propose a slider to select a date, and view the picture taken that day'), dcc.Slider( id='date_slider', min=0, max=110, value=0, step=1, updatemode='drag' ), html.Div( children=[ html.Img( id='image', src='assets/plant_pictures/0.png' ), ], style={ 'display':'inline-block', 'width':'100%' } ), ] ) The callback retrieves the current number on the slider and loads in the corresponding image. .. code-block:: python :linenos: @callback( Output(component_id='image', component_property='src'), Input(component_id='date_slider', component_property='value') ) def updateImage(imgNumber): return f"assets/plant_pictures/{imgNumber}.png"