Hi, you might remember me from such threads as:
Simple Example - Candlestick Chart From CSV File
And
Example - Blank Annotated Figure, Side-by-side Charts, Scrollable Textbox
Well, after driving myself nuts (for no reason) after posting this thread: Plotly Custom Callbacks – Open Plea For Clarification - I’m Still A Newbie
I’m back with a working and documented example of using Box Select on a bar chart to trigger another function that does processing (Minimally, but you get the idea - you can put anything in there) and puts the output into a Scrollable Textbox.
It has a bunch of stuff in there that I researched and tried to add informative comments on, so give it a look:
Remember to change the path to the file and/or the filename if you alter it on your machine. Most certainly you will have to edit the path, since I doubt anyone is duplicating my folder structure.
Code:
## TallTim's Callback Example -- Based on examples mode here:
## Simple Example - Candlestick Chart From CSV File -- https://community.plotly.com/t/simple-example-candlestick-chart-from-csv-file/76183
## Example - Blnak Annotated FIgure, Side-by-Side Charts, Scrollable Textbox -- https://community.plotly.com/t/example-blank-annotated-figure-side-by-side-charts-scrollable-textbox/77492
##
## Made this to show how to pass selected data from a chart to a function, and return the result to another component
## I hope this helps newbies, so you don't go astray into the thorny thickets at the base of the knowledge mountain.
# Importing some things
import os
import pandas as pd
import math
# Testing plotting chart results
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# Dash imports to make this a served interactive page, not static
from dash import Dash, html, dash_table, callback, Output, Input, dcc
import dash_mantine_components as dmc # This is used in the placeholder HTML for the figure
# Dash bootstrap for using color themes
import dash_bootstrap_components as dbc # Used to load a bootstrap theme
# Figure template for themes
from dash_bootstrap_templates import load_figure_template
load_figure_template('CYBORG') # Load dark theme
# Using bootstrap themes
app = Dash(__name__, external_stylesheets = [dbc.themes.CYBORG]) # Refer to the themes to change this if you wish
# Global Variables --------------------------------------------------------------------------------------------------------------------------
myDataPath = r'C:\Users\Tim\PythonEnvironments\NinjaHeatmap' # Change this to reflect where you put the demo csv file
# Box select x values - initialize as NaN for logic checks later
global temp_x_min
global temp_x_max
temp_x_min = float('NaN')
temp_x_max = float('NaN')
resultStatus = list()
# Functions ---------------------------------------------------------------------------------------------------------------------------------
# Utility function to print out to console
def printResults(myInputList):
tempData = ""
if myInputList is not None:
if len(myInputList) > 0:
for index in myInputList:
tempData += str(index)
return tempData
# This function converts a dataframe column name into a series
def convertColToSeries(myRawDataframe, myColumnName):
tempValList = list()
tempColumn = pd.DataFrame() # Init empty dataframe
tempSeries = []
tempVal = 0
# Convert pandas column into a series
tempColumn = myRawDataframe[myColumnName] # Get column values
tempSeries = pd.Series(tempColumn)
# Get dimensions for debug output
temp_series_dim = tempSeries.shape
tempRows = temp_series_dim[0]
# Debug
print("\n" + myColumnName + " Series - Rows: " + str(tempRows))
# Now do a loop to pack it all into a list
for tempVal in tempSeries:
tempValList.append(tempVal)
return tempValList
# Simple function to display the timestamp values of the box select, passing the list to the callback
def showIndexes(myInputSeries, min_x, max_x): # Data series, min/max x for box selection indexes
resultList = list()
myIndexesToFind = list()
closeData = ""
tempData = ""
outputString = ""
seperator = "\n"
# Take min/max x and retrieve the proper values from myInputSeries list
for idx in range(min_x, (max_x+1), 1): # Range start, stop, step
myIndexesToFind.append(myInputSeries[idx])
# Debug
print(str(df_Input.loc[idx, ['BarTimestamp']]))
# Append bar timestamps and closes to results
tempData = df_Input.loc[idx, ['BarTimestamp']].to_string(index=False) # Supresses index numbers
closeData = df_Input.loc[idx, ['Close']].to_string(index=False)
outputString = tempData + " " + closeData + "<br>" # HTML breaks here because Iframe is HTML output, so no 'newline' '\n'
resultList.append(outputString)
# If result list is empty, print error msg
if len(myIndexesToFind) == 0:
print("\n<Find Seq> No valid indexes to check, aborting...")
return
return resultList
## Main Candlestick Chart Drawing Function --------------------------------------------------------------------------------------------------
def myCandleChart(myInputData, myResults): # Adding results to pass final result list...
# Attempting to use the plotly go objects here... might work?
myFig = make_subplots(rows=2, row_heights=[0.70,0.30], cols=1, shared_xaxes=True, vertical_spacing=0.02) # Heights must add up to 1
# Plotly Chart
myFig.add_trace(
go.Candlestick(
x=myInputData['BarTimestamp'],
open=myInputData['Open'],
high=myInputData['High'],
low=myInputData['Low'],
close=myInputData['Close'],
name="Price"),
row=1, col=1
)
# Add FFI subplot with go.Scatter - works, Line is deprecated
myFig.add_trace(go.Scatter(x=myInputData['BarTimestamp'], y=myInputData['FancyIndicator'], name="Indicator", line=dict(color='rgb(235,140,52)')), row=2, col=1) # Dark orange
# Ref - Named colors list -- https://community.plotly.com/t/plotly-colours-list/11730/3
# Add zeroline to the indicator Subplot
myFig.add_hline(y=0, line_width=1, line_color="white", row=2, col=1)
# Remove default rangeslider
myFig.update_layout(xaxis_rangeslider_visible=False)
# Background color
myFig.update_layout(plot_bgcolor='rgb(0,0,0)')
# Axes line color
myFig.update_xaxes(showline=True, linewidth=1, linecolor='dimgray')
myFig.update_yaxes(showline=True, linewidth=1, linecolor='dimgray')
# Tick value display format - yaxis<num> is for subplots
myFig.update_layout(yaxis={"tickformat" : ','}, yaxis2={"tickformat" : '.2f'}) # Displays thousands,hundreds without scientific 'K' notation
# Grid line color
myFig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='dimgray')
myFig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='dimgray')
# Adjusting font sizes
myFig.update_layout(
font=dict(
family="Microsoft Sans Serif", # Font needs to be installed on system to be accessible
size=12, color='rgb(160,166,184)' # Light Gray
)
)
# Chart title
myFig.update_layout(title="BTCUSD 5min", title_x=0.5, yaxis_title="Price", yaxis2_title="Indicator")
# Legend Background Transparent Color, 'Paper' background color - plot canvas, sets pan enabled on load
myFig.update_layout(legend=dict(bgcolor='rgba(0,0,0,0)'), paper_bgcolor='rgb(12,12,12)', dragmode='pan')
## Ref: https://plotly.com/python/axes/ for range
# Hiding the legend
myFig.update_layout(showlegend=False)
# Adjust initially displayed x-axis range
myFig.update_xaxes(range=[0, 50]) # Entire set is still there, just shows the index numbers in the range
myFig.update_yaxes(range=[25600, 26250], row=1) # This sets the range of price in the candle chart for aesthetics
return myFig
# This is the container that splits up the right panel into a chart/text/input area
def myOutputPanel():
# Adjusting the row heights helped with the border issue... was 0.30 for second one
myFig = make_subplots(rows=2, row_heights=[0.70, 0.10], cols=1, vertical_spacing=0.02) # Heights must add up to 1
# Right pane chart - just a demo placeholder
myFig.add_trace(go.Scatter(x=[], y=[]), row=1, col=1) # This yields an empty chart
# Remove default rangeslider
myFig.update_layout(xaxis_rangeslider_visible=False)
# Background color
myFig.update_layout(plot_bgcolor='rgb(0,0,0)')
# Axes line color
myFig.update_xaxes(showline=True, linewidth=1, linecolor='dimgray')
myFig.update_yaxes(showline=True, linewidth=1, linecolor='dimgray')
# Tick value display format - yaxis<num> is for subplots --- I may have the format wrong for the plot, we'll see...
myFig.update_layout(yaxis={"tickformat" : ','}, yaxis2={"tickformat" : '.2f'}) # Displays thousands,hundreds without scientific 'K' notation
# Grid line color
myFig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='dimgray')
myFig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='dimgray')
# Adjusting font sizes
myFig.update_layout(
font=dict(
family="Microsoft Sans Serif", # Font needs to be installed on system to be accessible
size=12, color='rgb(160,166,184)' # Light Gray
),
margin=dict(l=1, r=70, b=0, t=90, pad=1) # Margin adjustments
)
# Chart title / Lower box title
myFig.update_layout(title="Another Chart", title_x=0.5)
# Legend Background Transparent Color, 'Paper' background color - plot canvas, sets pan enabled on load
myFig.update_layout(legend=dict(bgcolor='rgba(0,0,0,0)'), paper_bgcolor='rgb(12,12,12)')
# Fixed axes for no zoom
myFig.update_xaxes(fixedrange=True)
myFig.update_yaxes(fixedrange=True)
return myFig
# This function styles the result output so it can be sent to the Iframe with proper text color, etc..
def styleTextOutput(myInputText):
tempStr = ""
# Eight zeroes in hex is transparent, also #00FFFFFF
iframe_text_style = "<span style=\"color:#FFFFFF; background:#00000000; font-family:Arial,Helvectica,sans-serif; font-size: 16px;\">"
iframe_close_span = "</span>"
# Concatenates (puts together) string with triple quotes and variables
tempStr = f"""{iframe_text_style}{myInputText}{iframe_close_span}"""
# Debug
#print("\n<Iframe Text Style Func> - Total formatted string is: \n")
#print(tempStr)
return tempStr
# Init Actions ------------------------------------------------------------------------------------------------------------------------------
# Get directory contents
files = os.listdir(myDataPath)
# Read into dataframe
df_Input = pd.read_csv(myDataPath + '/' + "Input_BTCUSD_5_Minute_Demo.csv") # Edit this if you change the filename of the demo csv file
# Get number of dataframe rows and columns
dataDimensions = df_Input.shape
dataRows = dataDimensions[0]
dataCols = dataDimensions[1]
# Debug
print("\nInput dataframe - Rows: " + str(dataRows) + " Columns: " + str(dataCols))
# Returns series from input dataframe, column name
indicator_Val_List = convertColToSeries(df_Input, 'FancyIndicator')
# We're ready and waiting!
print("\nWaiting for box selection... \n")
# Setting up a dynamic graph container
# Only way to get graph to fill full height was to add -- style={'height':'100vh'} to the main layout Div container
# Config settings go inside the dcc.Graph, instead of fig.show() for setting scroll/zoom
# This was way more of a pain in the ass to find a solution to than it should've been for both of the above...
graph_container = html.Div([
dcc.Graph(figure = myCandleChart(df_Input, resultStatus), id='chart-placeholder', style={'height':'100vh','width':'50vw'}, config = dict({'scrollZoom' : True})) # Passing df_Input to chart render function
])
# Output pane on the right side -- search output and scrollable text box console output
output_container = html.Div(
[
dcc.Graph(figure = myOutputPanel(), id='output', style={'height':'70vh','width':'50vw'},
config={'displayModeBar': False} # Removed toolbar and prevent pan/zoom
),
# dcc.Store for retaining output of bar chart index function
dcc.Store(
id='select-result',
storage_type='session' # local - data kept after browser quits, memory - reset on page refresh, session - data cleared on browser quit
),
html.Iframe(id='iframe-output', srcDoc="", style={'width':'90%','height':220,'scrolling':'auto', 'background-color':'#202020'})
],
style={'background-color':'#0C0C0C'} # Sets strip to the right of scrollbox to chart 'paper' color
)
content = html.Div(
[
graph_container
]
)
# This creates a stack that places the windows side-by-side
myStack = html.Div(
[
dbc.Stack(
[
content, output_container
],
direction="horizontal" # The key setting that enables this
)
],
style={'background-color':'#0C0C0C'} # Sets small strip to the right of Iframe to chart 'paper' color
)
page_structure = [
dbc.Row([
myStack
])
]
# Ditched Grid since it isn't a heatmap
app.layout = dbc.Container(
children=page_structure,
fluid=True
)
# Box Select Callback
@callback(
Output('iframe-output', 'srcDoc'), # Pushes results to Iframe srcDoc - which is HTML styled
Input('chart-placeholder', 'selectedData'), # The input that fires when bars are selected
prevent_initial_call=True
)
def display_selected_data(selectedData): # Got a selection? Great, lets do some stuff...
global resultStatus
global mySelectedResult
global myStyledOutput
if selectedData: # Got somthing in here?
temp_points = selectedData['points']
#Debug
#print("\nJSON Data Type Is: " + str(type(selectedData)))
if temp_points:
temp_points_len = len(temp_points)
temp_points_first = temp_points[0] # First point element
temp_points_last = temp_points[(temp_points_len - 1)] # Last point element
temp_x_min = temp_points_first['pointIndex'] # Get first point index
temp_x_max = temp_points_last['pointIndex'] # Get last point index
# Debug
print("\n Points min/max indexes are: " + str(temp_x_min) + ", " + str(temp_x_max))
if temp_points_len > 0: # Have points been selected?
# This uses the selected range of points...
resultStatus = showIndexes(indicator_Val_List, temp_x_min, temp_x_max) # Fire our function to show data at indexes
# Catch exception for 'NoneType' when results returns nothing...
if resultStatus is not None:
if len(resultStatus) > 0:
print("\nFinal results: \n")
print(*resultStatus)
print("\n")
# Making string for Iframe
mySelectedResult = "Final results, Timestamp, Close Price: <br>" + printResults(resultStatus)
# Stuff data into store...
dcc.Store(id='select-result', data=resultStatus) # Store data for reference in Iframe
else:
print("\nFinal results - Nothing selected. \n")
# Making string for Iframe
mySelectedResult = "\nFinal results - Nothing selected. \n"
else:
print("\nFinal results - None Type Error - selection returned nothing... \n")
mySelectedResult = "<br>Final results - None Type Error - selection returned nothing... <br>"
# Branch for selected data IF block...
else: # No points selected on chart?
print("\nSorry, no price bars selected.")
# Making string for Iframe
mySelectedResult = "<br>Sorry, no price bars selected. <br>"
mySelectedResult = styleTextOutput(mySelectedResult) # Apply HTML styling for Iframe
return mySelectedResult # Return HTML styled string to Iframe as specified in the callback Output
# Run the App
if __name__ == '__main__':
app.run_server(debug=True,host='0.0.0.0',port=8060) # Set host argument to allow LAN connections, Port for custom ports
And here’s the csv file I used - you’ll have to save it as the same filename or change the filename in the code for it to work properly:
BarTimestamp,FancyIndicator,Open,High,Low,Close
6/10/2023 8:25:00 PM,-0.50,25789.24,25789.24,25765.28,25779.76
6/10/2023 8:30:00 PM,-0.50,25777.89,25807.54,25761.67,25791.5
6/10/2023 8:35:00 PM,-0.50,25791.15,25827.72,25791.15,25821.8
6/10/2023 8:40:00 PM,-0.50,25822.35,25885.23,25821.76,25847.1
6/10/2023 8:45:00 PM,-0.50,25847.1,25850.69,25833.68,25840.26
6/10/2023 8:50:00 PM,-0.50,25839.71,25851.15,25813.32,25836.01
6/10/2023 8:55:00 PM,-0.50,25834.62,25849.61,25830.74,25841.79
6/10/2023 9:00:00 PM,-0.50,25842.06,25868.25,25838.53,25840.62
6/10/2023 9:05:00 PM,-0.50,25840.6,25846.16,25813.21,25813.22
6/10/2023 9:10:00 PM,-0.50,25813.22,25820.39,25793.97,25806.1
6/10/2023 9:15:00 PM,0.50,25804.85,25809.92,25789.86,25789.9
6/10/2023 9:20:00 PM,0.50,25789.89,25791.37,25768.77,25782.29
6/10/2023 9:25:00 PM,0.50,25782.3,25793.47,25760.65,25778.92
6/10/2023 9:30:00 PM,0.50,25778.92,25786.31,25770.14,25778.27
6/10/2023 9:35:00 PM,0.50,25778.27,25797.74,25777.73,25793.92
6/10/2023 9:40:00 PM,0.50,25794.24,25798.36,25790.62,25795.51
6/10/2023 9:45:00 PM,0.50,25795.5,25805.59,25785.02,25805.59
6/10/2023 9:50:00 PM,0.50,25806.24,25816.44,25784.28,25795.19
6/10/2023 9:55:00 PM,0.50,25795.85,25797.03,25763.07,25771.5
6/10/2023 10:00:00 PM,0.50,25772.25,25787.56,25769.57,25779.99
6/10/2023 10:05:00 PM,-0.50,25779.2,25784.52,25763.24,25769.47
6/10/2023 10:10:00 PM,-0.50,25769.47,25777.18,25751.48,25751.62
6/10/2023 10:15:00 PM,-0.50,25751.62,25764.62,25739.95,25747.35
6/10/2023 10:20:00 PM,-0.50,25747.35,25771.8,25745.35,25754.5
6/10/2023 10:25:00 PM,-0.50,25754.49,25767.23,25738.44,25765.04
6/10/2023 10:30:00 PM,-0.50,25765.04,25768.32,25744.31,25765.99
6/10/2023 10:35:00 PM,-0.50,25765.99,25774.09,25731.69,25746.77
6/10/2023 10:40:00 PM,-0.50,25744.96,25788.51,25744.96,25768.76
6/10/2023 10:45:00 PM,-0.50,25767.99,25777.83,25757.55,25763.91
6/10/2023 10:50:00 PM,-0.50,25763.91,25773.19,25735.07,25742.31
6/10/2023 10:55:00 PM,0.50,25742.3,25758.61,25710.31,25733.57
6/10/2023 11:00:00 PM,0.50,25734.08,25746.73,25716.49,25742.79
6/10/2023 11:05:00 PM,0.50,25742.79,25744.94,25716.4,25729.24
6/10/2023 11:10:00 PM,0.50,25727.33,25733.66,25701.69,25710.49
6/10/2023 11:15:00 PM,0.50,25710.49,25729.47,25708.76,25720.25
6/10/2023 11:20:00 PM,0.50,25718.26,25725.87,25695.58,25700.8
6/10/2023 11:25:00 PM,0.50,25701.26,25710.92,25648,25690.19
6/10/2023 11:30:00 PM,0.50,25690.44,25737.51,25683.44,25702.05
6/10/2023 11:35:00 PM,0.50,25705.86,25717,25680.05,25687.07
6/10/2023 11:40:00 PM,0.50,25687.52,25720.4,25687.52,25712.34
6/10/2023 11:45:00 PM,-0.50,25712.11,25719.96,25698.46,25710.57
6/10/2023 11:50:00 PM,-0.50,25709.28,25767.28,25709.28,25755.48
6/10/2023 11:55:00 PM,-0.50,25754.38,25763.71,25739.31,25745.81
6/11/2023 12:00:00 AM,-0.50,25745.51,25776.13,25739.33,25742.6
6/11/2023 12:05:00 AM,-0.50,25743.63,25761.23,25741.76,25758.57
6/11/2023 12:10:00 AM,-0.50,25758.58,25765.65,25751.61,25757.03
6/11/2023 12:15:00 AM,-0.50,25756.66,25766.91,25746.39,25766.34
6/11/2023 12:20:00 AM,-0.50,25766.34,25767.36,25753.24,25758.45
6/11/2023 12:25:00 AM,-0.50,25758.44,25777.7,25756.04,25777.53
6/11/2023 12:30:00 AM,-0.50,25775.97,25775.97,25757.03,25762.68
6/11/2023 12:35:00 AM,0.50,25761.77,25769.87,25752.15,25768.02
6/11/2023 12:40:00 AM,0.50,25768.02,25819.04,25768.02,25817.54
6/11/2023 12:45:00 AM,0.50,25815.62,25839.14,25775.59,25797.03
6/11/2023 12:50:00 AM,0.50,25797.81,25807.2,25789.87,25801.2
6/11/2023 12:55:00 AM,0.50,25801.07,25827.55,25797.65,25818.71
6/11/2023 1:00:00 AM,0.50,25818.51,25850,25816.73,25834.16
6/11/2023 1:05:00 AM,0.50,25834.17,25841.18,25813.17,25818.91
6/11/2023 1:10:00 AM,0.50,25818.58,25833.9,25805.88,25827.49
6/11/2023 1:15:00 AM,0.50,25826.88,25831.35,25813.78,25816.29
6/11/2023 1:20:00 AM,0.50,25816.99,25830.07,25815.85,25825.28
6/11/2023 1:25:00 AM,-0.50,25826.61,25827.08,25789.1,25800.97
6/11/2023 1:30:00 AM,-0.50,25800.58,25808.22,25781.57,25804.51
6/11/2023 1:35:00 AM,-0.50,25804.5,25807.04,25785.95,25791.55
6/11/2023 1:40:00 AM,-0.50,25791.52,25799.6,25780.78,25785.36
6/11/2023 1:45:00 AM,-0.50,25785.53,25808.02,25784.13,25793.09
6/11/2023 1:50:00 AM,-0.50,25793.4,25802.08,25790.35,25797.41
6/11/2023 1:55:00 AM,-0.50,25797.41,25806.69,25794.3,25802.78
6/11/2023 2:00:00 AM,-0.50,25802.78,25808.44,25797.86,25799.56
6/11/2023 2:05:00 AM,-0.50,25797.25,25816.37,25793.68,25811.55
6/11/2023 2:10:00 AM,-0.50,25811.54,25813.34,25796.12,25799.9
6/11/2023 2:15:00 AM,0.50,25798.85,25803.39,25796.11,25802.4
6/11/2023 2:20:00 AM,0.50,25800.77,25806.74,25786.51,25794.24
6/11/2023 2:25:00 AM,0.50,25793.95,25796.47,25780.16,25782.91
6/11/2023 2:30:00 AM,0.50,25785.2,25800.34,25785.2,25796.69
6/11/2023 2:35:00 AM,0.50,25797.21,25803.06,25786.84,25788
6/11/2023 2:40:00 AM,0.50,25787.99,25788.24,25776.51,25779.54
6/11/2023 2:45:00 AM,0.50,25777.82,25785.71,25760.71,25763.15
6/11/2023 2:50:00 AM,0.50,25763.15,25770.79,25757.06,25767.38
6/11/2023 2:55:00 AM,0.50,25766.73,25772.28,25755.19,25766.11
6/11/2023 3:00:00 AM,0.50,25768.17,25770.19,25750.39,25753.79
6/11/2023 3:05:00 AM,-0.50,25752.22,25764.12,25744.24,25753.59
6/11/2023 3:10:00 AM,-0.50,25751.37,25765.72,25747.74,25765.67
6/11/2023 3:15:00 AM,-0.50,25765.67,25765.67,25753.36,25761.36
6/11/2023 3:20:00 AM,-0.50,25761.36,25783.49,25757.07,25782.18
6/11/2023 3:25:00 AM,-0.50,25782.17,25782.17,25765.89,25767.73
6/11/2023 3:30:00 AM,-0.50,25767.74,25767.74,25746.6,25748.38
6/11/2023 3:35:00 AM,-0.50,25746.92,25750.24,25705.13,25741.17
6/11/2023 3:40:00 AM,-0.50,25740.98,25757.27,25729.62,25752.3
6/11/2023 3:45:00 AM,-0.50,25754.55,25755.83,25726.92,25736.61
6/11/2023 3:50:00 AM,-0.50,25736.61,25746.34,25725.63,25733.15
6/11/2023 3:55:00 AM,0.50,25731.12,25742.77,25731.12,25738.6
6/11/2023 4:00:00 AM,0.50,25738.56,25757.96,25738.56,25753.21
6/11/2023 4:05:00 AM,0.50,25753.21,25755.35,25743.1,25749.68
6/11/2023 4:10:00 AM,0.50,25750.67,25753.65,25735.12,25741.87
6/11/2023 4:15:00 AM,0.50,25742.25,25758.71,25734.88,25737.31
6/11/2023 4:20:00 AM,0.50,25737.54,25752.47,25733.76,25745.01
6/11/2023 4:25:00 AM,0.50,25744.96,25757.94,25744.94,25753.23
6/11/2023 4:30:00 AM,0.50,25753.22,25785.99,25746.65,25774.75
6/11/2023 4:35:00 AM,0.50,25774.4,25779.03,25758.69,25771.34
6/11/2023 4:40:00 AM,0.50,25771.34,25782.54,25761.24,25776.73
And finally, the output of the selection in action, and the corresponding output:
The chart on the right pane doesn’t have anything, but that could be changed easily. Just a placeholder for demo purposes.
I hope this helps someone if you struggled with this stuff like I did. Enjoy!