import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go
from ECAgent.Core import Model
# Can be used to customize CSS of Visualizer
external_stylesheets = ['https://rawgit.com/BrandonGower-Winter/ABMECS/master/Assets/VisualizerCustom.css',
'https://rawgit.com/BrandonGower-Winter/ABMECS/master/Assets/VisualizerBase.css']
[docs]class VisualInterface:
"""
Ths is the base class for Visual Interfaces.
VisualInterface's utilize the dash package to create a WebApp to allow individuals to view the results of their
model once a run has been completed or in real-time.
There are a few things to note about the VisualInterface class:
* By calling the VisualInterface.__init__() method, your WebApp will have features setup for you: Namely, play,
stop, restart and step. It'll also include a banner with your System's name as a title on it.
* A frameFreq of 0.0 means that your system is static and will only ever be constructed once.
If you want a dynamic WebApp, you must set the frameFreq to some non-zero positive number. If your frameFreq is 0.0,
the play, stop, restart and step buttons will not be added to your WebApp.
* The server/WebApp will start once you call the VisualInterface.app.run_server().
* The frameFreq property determines how frequently (in milliseconds) the SystemManager.executeSystems() method is
called and how often your your graphs will update.
"""
def __init__(self, name, model: Model, frameFreq: float = 0.0):
self.name = name
self.model = model
self.frameFreq = frameFreq
self.running = False # Is used to determine whether a dynamic model is running or not.
# Create app
self.app = dash.Dash(
self.name, meta_tags=[{"name": "viewport", "content": "width=device-width"}],
external_stylesheets=external_stylesheets
)
# Create parameter lists
self.displays = []
self.parameters = []
self.createBaseLayout()
def isStatic(self) -> bool:
return self.frameFreq == 0.0
def execute(self):
self.render()
def render(self):
pass
[docs] def createBaseLayout(self):
"""Creates the base layout"""
# Create banner
banner = html.Div(
className="app-banner row",
children=[
html.H2(className="h2-title", children=self.name),
html.H2(className="h2-title-mobile", children=self.name),
],
)
# Add parameter header
self.addParameter(createLabel('parameter-heading', 'Parameters:'))
# If framerate > 0, create the play, stop, and restart buttons and Timestep label
if not self.isStatic():
# Add Play/Restart/Step Buttons
banner.children.append(
html.Div(
className='div-play-buttons',
id='dynamic-button',
children=[
html.Button("Play", id='play-stop-button', n_clicks=0),
html.Button('Restart', id='restart-button', n_clicks=0),
html.Button('Step', id='step-button', n_clicks=0),
dcc.Interval(
id='interval-component',
interval=self.frameFreq,
n_intervals=0
)
]
)
)
# Add Timestep label
self.parameters.append(createLabel('timestep-label', 'Timestep: 0'))
# Apply Play/Stop Callback
self.app.callback(
dash.dependencies.Output('play-stop-button', 'children'),
[dash.dependencies.Input('play-stop-button', 'n_clicks')]
)(self.play_button_callback)
# Apply executeSystems() on interval callback and Step button callback
self.app.callback(
dash.dependencies.Output('timestep-label', 'children'),
[dash.dependencies.Input('interval-component', 'n_intervals'),
dash.dependencies.Input('step-button', 'n_clicks')]
)(self.execute_system_on_play_callback)
self.app.layout = html.Div(
children=[
# Error Message
html.Div(id="error-message"),
# Top Banner
banner,
# Body of the App
html.Div(
className="row app-body",
children=[
# User Controls
html.Div(
className="four columns card",
children=html.Div(
className="bg-white user-control",
children=self.parameters)
),
# Graph
html.Div(
className="eight columns card-left",
children=self.displays,
style={'margin-left': 0}
),
dcc.Store(id="error", storage_type="memory"),
],
),
]
)
def addDisplay(self, content, add_break=True):
self.displays.append(content)
if add_break:
self.displays.append(html.Br())
def addParameter(self, content):
self.parameters.append(content)
# #################################### Class Callbacks ###########################################
def play_button_callback(self, n_clicks):
if n_clicks % 2 == 0:
self.running = False
return 'Play'
else:
self.running = True
return 'Stop'
def execute_system_on_play_callback(self, n_intervals, n_clicks):
context = dash.callback_context.triggered[0]['prop_id'].split('.')[0]
if context == 'step-button':
if not self.running:
self.model.systems.executeSystems()
elif self.running:
self.model.systems.executeSystems()
return "Timestep: {}".format(self.model.systems.timestep)
# ############################## Graph and Parameter Functionality ##############################
[docs]def createScatterPlot(title, data: [[[float], [float], dict]], layout_kwargs: dict = {}):
"""Creates a Scatter plot Figure. This function supports multiple traces supplied to the 'data' parameter
Data should be supplied in the following format:
[[xdata_1,ydata_1, fig_layout_1], [xdata_2, ydata_2, fig_layout_2], ..., [xdata_n,ydata_n, fig_layout_n]]
The 'fig_layout' property is optional. If it is supplied, the trace in question will be updated to include all of
the properties specified..
"""
traces = []
for data_packet in data:
scatter = go.Scatter(x=data_packet[0], y=data_packet[1])
traces.append(scatter)
if len(data_packet) > 2:
scatter.update(data_packet[2])
return go.Figure(data=traces, layout=go.Layout(title=title, **layout_kwargs))
[docs]def createScatterGLPlot(title, data: [[[float], [float], dict]], layout_kwargs: dict = {}):
"""Creates a Scatter plot Figure that will be rendered using WebGL.
This function supports multiple traces supplied to the 'data' parameter Data should be supplied in the
following format:
[[xdata_1,ydata_1, fig_layout_1], [xdata_2, ydata_2, fig_layout_2], ..., [xdata_n,ydata_n, fig_layout_n]]
The 'fig_layout' property is optional. If it is supplied, the trace in question will be updated to include all of
the properties specified..
"""
traces = []
for data_packet in data:
scatter = go.Scattergl(x=data_packet[0], y=data_packet[1])
traces.append(scatter)
if len(data_packet) > 2:
scatter.update(data_packet[2])
return go.Figure(data=traces, layout=go.Layout(title=title, **layout_kwargs))
[docs]def createBarGraph(title: str, data: [[[float], [float], dict]], layout_kwargs: dict = {}):
"""Creates a Bar Graph Figure. This function supports multiple traces supplied to the 'data' parameter
Data should be supplied in the following format:
[[xdata_1,ydata_1, fig_layout_1], [xdata_2, ydata_2, fig_layout_2], ..., [xdata_n,ydata_n, fig_layout_n]]
The 'fig_layout' property is optional. If it is supplied, the trace in question will be updated to include all of
the properties specified..
"""
traces = []
for data_packet in data:
bar = go.Bar(x=data_packet[0], y=data_packet[1])
traces.append(bar)
if len(data_packet) > 2:
bar.update(data_packet[2])
return go.Figure(data=traces, layout=go.Layout(title=title, **layout_kwargs))
[docs]def createHeatMap(title: str, data: [[float]], heatmap_kwargs: dict = {}, layout_kwargs: dict = {}):
"""Creates a HeatMap Figure object using Plotly graph objects. The data object determines the dimensions of the
heatmap. The len(data) will be the height. The len(data[i]) will be the width of the heatmap. The Heatmap is
constructed in a bottom-up and left-to-right manner.
Discrete X and Y categories can be specified, this is done by supplying xData and yData with the X and Y category
name respectively. The len(xData) must be equal to the width of your Heatmap, while len(yData) must be equal to the
height of your Heatmap.
A custom color scale can be supplied, ensure that it follows the correct format and that the threshold values are
normalized and that the color scales are in rgb like so 'rgb(r_val, g_val, b_val)'"""
return go.Figure(data=go.Heatmap(
z=data,
**heatmap_kwargs
), layout=go.Layout(title=title, **layout_kwargs))
[docs]def createHeatMapGL(title: str, data: [[float]], heatmap_kwargs: dict = {}, layout_kwargs: dict = {}):
"""Creates a HeatMap Figure object using Plotly graph objects that will be rendered by WebGL.
The data object determines the dimensions of the heatmap. The len(data) will be the height.
The len(data[i]) will be the width of the heatmap.
The Heatmap is constructed in a bottom-up and left-to-right manner.
Discrete X and Y categories can be specified, this is done by supplying xData and yData with the X and Y category
name respectively. The len(xData) must be equal to the width of your Heatmap, while len(yData) must be equal to the
height of your Heatmap.
A custom color scale can be supplied, ensure that it follows the correct format and that the threshold values are
normalized and that the color scales are in rgb like so 'rgb(r_val, g_val, b_val)'"""
return go.Figure(data=go.Heatmapgl(
z=data,
**heatmap_kwargs
), layout=go.Layout(title=title, **layout_kwargs))
[docs]def createContourMap(title: str, data: [[float]], contour_kwargs: dict = {}, layout_kwargs: dict = {}):
"""Creates a Contour Figure object using Plotly graph objects. The data object determines the dimensions of the
Contour plot. The len(data) will be the height. The len(data[i]) will be the width of the contour plot.
The contour plot is constructed in a bottom-up and left-to-right manner.
The contour plot can be customized using the contour_kwargs dict. The dict will be supplied to the contour plot
graph object when it is created. See the plotly api for a list of customizable properties. This can be similarly be
applied to layout_kwargs which can change the layout of contour plot."""
return go.Figure(data=go.Contour(
z=data,
**contour_kwargs
), layout=go.Layout(title=title, **layout_kwargs))
[docs]def createTable(title: str, headers: [str], cells: [[]], header_kwargs: dict = {}, cell_kwargs: dict = {},
layout_kwargs: dict = {}):
"""Creates a Table figure using Plotly graph objects. Table headers and cells need to be supplied separately.
The data format for the headers and cells are as follows:
Headers: [hdr1, hdr2,...,hdrN]
Cells: [column1_data, column2_data,..., columnN_data].
The Table headers and cells are customized separately using the header_kwargs and cell_kwargs parameters. The
layout of the Table can also be customized using the layout_kwargs."""
return go.Figure(data=go.Table(
header=dict(values=headers, **header_kwargs),
cells=dict(values=cells, **cell_kwargs)
), layout=go.Layout(title=title, **layout_kwargs))
[docs]def createPieChart(title: str, labels: [str], values: [float], pie_kwargs: dict = {}, layout_kwargs: dict = {}):
""" Creates a Pie Chart Figure using Plotly graph objects. Chart labels and values need to be supplied separately.
The data format for the labels and values are as follows:
Labels: [lbl1, lbl2,..., lblN]
Values: [val1, val2,..., valN]
The Pie chart can be customized using the pie_kwargs parameter. The layout of the Pie chart can be customized using
the layout_kwargs parameter."""
return go.Figure(data=go.Pie(labels=labels, values=values, **pie_kwargs),
layout=go.Layout(title=title, **layout_kwargs))
def createGraph(graphID: str, figure: go.Figure, classname: str = 'bg-white'):
return html.Div(
className=classname,
children=[
dcc.Graph(id=graphID, figure=figure)
],
style={'height': figure.layout.height}
)
def createLiveGraph(graphID: str, figure: go.Figure, vs: VisualInterface, callback, classname: str = 'bg-white'):
graph = createGraph(graphID, figure, classname)
def update_live_graph_callback(n_intervals, n_clicks, figure):
context = dash.callback_context.triggered[0]['prop_id'].split('.')[0]
if (context == 'step-button' and not vs.running) or vs.running:
return callback(figure)
else:
return figure
# Add Callback
vs.app.callback(
dash.dependencies.Output(graphID, 'figure'),
[dash.dependencies.Input('interval-component', 'n_intervals'),
dash.dependencies.Input('step-button', 'n_clicks'),
dash.dependencies.Input(graphID, 'figure')]
)(update_live_graph_callback)
return graph
def createLabel(label_id, content):
return html.Div(className="padding-top-bot", children=[html.H6(content, id=label_id)])
def createLiveLabel(label_id, initial_content, vs: VisualInterface, callback):
label = createLabel(label_id, initial_content)
def update_live_label_callback(n_intervals, n_clicks, children):
context = dash.callback_context.triggered[0]['prop_id'].split('.')[0]
if (context == 'step-button' and not vs.running) or vs.running:
return callback(children)
else:
return children
# Add Callback
vs.app.callback(
dash.dependencies.Output(label_id, 'children'),
[dash.dependencies.Input('interval-component', 'n_intervals'),
dash.dependencies.Input('step-button', 'n_clicks'),
dash.dependencies.Input(label_id, 'children')]
)(update_live_label_callback)
return label
[docs]def createSlider(slider_id: str, slider_name: str, vs: VisualInterface, set_val, min_val: float = 0.0,
max_val: float = 1.0, step: float = 0.01):
"""This function will add a slider to the parameter window of the visual interface. It will also automatically add
a callback function that will supply your custom function 'set_val' with the value of the slider"""
# Add html
slider = html.Div(
className="padding-top-bot",
children=[
html.H6('{}: [{}]'.format(slider_name, max_val), id=slider_id + '-title'),
dcc.Slider(
id=slider_id,
min=min_val,
max=max_val,
value=max_val,
step=step
)
]
)
# Add callback
def set_slider_val(value):
set_val(value)
return '{}: [{}]'.format(slider_name, value)
vs.app.callback(dash.dependencies.Output(slider_id + '-title', 'children'),
[dash.dependencies.Input(slider_id, 'value')])(set_slider_val)
return slider
[docs]def addRect(fig: go.Figure, x, y, width=1, height=1, **shape_kwargs):
"""Adds a rectangle to Figure 'fig'. x & y refer to the coordinates of the bottom left corner of the rectangle."""
x1 = x + width
y1 = y + height
fig.add_shape(
x0=x,
y0=y,
x1=x1,
y1=y1,
type='rect',
**shape_kwargs
)
[docs]def addCircle(fig: go.Figure, x, y, radius=0.5, **shape_kwargs):
"""Adds a circle to Figure 'fig'. x & y are the coordinates of the center of the circle"""
x0 = x - radius
x1 = x + radius
y0 = y - radius
y1 = y + radius
fig.add_shape(
x0=x0,
x1=x1,
y0=y0,
y1=y1,
type='circle',
**shape_kwargs
)
def createTabs(labels: [str], tabs: []):
return html.Div([
dcc.Tabs(
[
dcc.Tab(label=labels[x], children=tabs[x]) for x in range(len(labels))
]
)])