How to use trame
Trame aims to streamline the creation of visual analytics applications, with support for interactive data manipulation and visualization, through a very compact and simple API. This document describes the core API and concepts behind trame; the document is both a start up guide, but also a quick reference for those more familiar with trame.
Core concepts
Trame allows you to easily share variables between UI components and a Python application. The resulting shared state can be modified by functions and methods invoked from the UI or application. For example, a Python function can be called when a button in the UI is clicked; or the UI can be updated in response to programmatic changes to the state. This document focuses on how you can leverage these capabilities inside your application using trame.
Shared state
Trame mainly focuses on the Python side, therefore the variables that we aim to use across our application will be defined from Python. The following sections illustate the various ways the shared state can be modified.
UI element with a dynamic variable
The example below uses Vuetify to create a slider where the value is controlled by the slider. The variable slider_value
is initialized with the value 8
. So variables in the shared state can be defined and initialized right were we want to use them when defining our UI elements. In this example, the slider will be able to modify that value but also will reflect its content if that value changes on the server side.
from trame.widgets import vuetify
slider = vuetify.VSlider(
label="Example",
min=2,
max=15,
v_model=("slider_value", 8),
)
from trame.widgets import vuetify
slider = vuetify.VSlider(
label="Example",
min=2,
max=15,
v_model=("slider_value", 8),
)
Listening to changes
Imagine that we want to monitor the value of slider_value
within our Python application and execute a method when that variable is updated. The code below illustate how we can annotate a function so it can be called when a value of the shared state is modified.
from trame.app import get_server
server = get_server()
state = server.state
@state.change("slider_value")
def slider_value_change(slider_value, **kwargs):
print(f"Slider is changing slider_value to {slider_value}")
from trame.app import get_server
server = get_server()
state = server.state
@state.change("slider_value")
def slider_value_change(slider_value, **kwargs):
print(f"Slider is changing slider_value to {slider_value}")
So far, we only have the client modifying that variable. But if a function on the server were to change that variable, the function would also be called as we simply react to the fact that the variable has been updated by someone.
Changing the value from Python
Within a Python trame application, it is possible to update the UI in response to read and even write operations to and from the shared state. The code below provides a function that will read the current value and update its content.
from trame.app import get_server
server = get_server()
state = server.state
def random_update():
state.slider_value += 1
if state.slider_value > 15:
state.slider_value = 2
from trame.app import get_server
server = get_server()
state = server.state
def random_update():
state.slider_value += 1
if state.slider_value > 15:
state.slider_value = 2
Forcing state exchange
Sometimes, the variable inside your shared state is an actual object with a nested structure. While the state on the Python side always maintains a reference to that object, when manually editing it's important to flush its contents so that the client reflects changes. In this situation, the dirty()
method can be called with the list of variable names that should be pushed. This is also useful in some async contexts to control when pieces of the state should be synchronized between client and server. The code below provides a typical example.
import asyncio
import time
async def update_time():
while True:
await asyncio.sleep(1)
with state: # needed because of async
state.time = time.time()
import asyncio
import time
async def update_time():
while True:
await asyncio.sleep(1)
with state: # needed because of async
state.time = time.time()
Method calls
When building a client/server application you will need to be able to trigger methods on both sides and trame has some easy ways to do that.
Bind method to a button click
The example below re-uses the function we had defined before but now we bind it to a button.
from trame.app import get_server
from trame.widgets import vuetify
server = get_server()
state, ctrl = server.state, server.controller
change_btn = vuetify.VBtn(
"Change slider_value",
click=ctrl.rnd_update
)
def random_update():
state.slider_value += 1
if state.slider_value > 15:
state.slider_value = 2
# Do the binding later on
ctrl.rnd_update = random_update
from trame.app import get_server
from trame.widgets import vuetify
server = get_server()
state, ctrl = server.state, server.controller
change_btn = vuetify.VBtn(
"Change slider_value",
click=ctrl.rnd_update
)
def random_update():
state.slider_value += 1
if state.slider_value > 15:
state.slider_value = 2
# Do the binding later on
ctrl.rnd_update = random_update
Calling JavaScript methods
Some components in an application may have APIs that you be used and invoked from the Python side. The code below provides an example of such a use case:
from trame.app import get_server
from trame.widgets import vtk
server = get_server()
html_view = vtk.VtkView(
ref="vtkView", # Name to identify the Web component on which we want to call a method
...
)
def reset_camera():
server.js_call(ref="vtkView", method="resetCamera", args=[])
from trame.app import get_server
from trame.widgets import vtk
server = get_server()
html_view = vtk.VtkView(
ref="vtkView", # Name to identify the Web component on which we want to call a method
...
)
def reset_camera():
server.js_call(ref="vtkView", method="resetCamera", args=[])
Layout management
Layouts are meant to define the core UI elements of an application. Think of FullScreen vs Toolbar, Drawer, Footer and so on. Layouts let you drop pieces of your UI into pre-defined locations. The layout organizes the way an application is structured. It could be defined once, or be redifined at runtime.
When creating a layout, you have the opportunity to define the Tab title along with a path to a favicon and a method to call at startup.
HTML Elements
HTML elements in trame (trame.html.*) are just helpers for generating HTML content. But because they exist as Python objects, users can interact with them simply by setting attributes on them in plain Python.
Below are various ways that you can translate what you see on a Vue component into trame syntax.
# Attribute without value (boolean)
# <v-btn outlined />
vuetify.VBtn(outlined=True)
# Bind method to event
# <v-btn @click="do_something">
def do_something():
pass
vuetify.VBtn(click=do_something)
# Dash handling (v-model => v_model)
# <v-slider v-model="slider_value" min="0" max="100" />
vuetify.VSlider(
v_model=("slider_value", 50), # See state section above
min=0,
max=50,
)
# Attribute without value (boolean)
# <v-btn outlined />
vuetify.VBtn(outlined=True)
# Bind method to event
# <v-btn @click="do_something">
def do_something():
pass
vuetify.VBtn(click=do_something)
# Dash handling (v-model => v_model)
# <v-slider v-model="slider_value" min="0" max="100" />
vuetify.VSlider(
v_model=("slider_value", 50), # See state section above
min=0,
max=50,
)
Command line arguments
Since trame is used to create applications, as compared to web services that aim to serve many concurrent users, it may be important to provide information to an application on start up. For example, which ML model to load, or the file/directory that you would like to explore or process. This can be achieved by adding CLI parameters using ArgParse, like in the following example.
from trame.app import get_server
server = get_server()
server.cli.add_argument("-d", "--data", help="Directory to explore", dest="data")
args = server.cli.parse_args()
print(args.data)
from trame.app import get_server
server = get_server()
server.cli.add_argument("-d", "--data", help="Directory to explore", dest="data")
args = server.cli.parse_args()
print(args.data)
Starting an application
The server provides a start()
method which actually starts a trame application. Typically the following section is placed in the main script:
if __name__ == "__main__":
server.start()
if __name__ == "__main__":
server.start()
The full API is listed below
def start(
port=None,
thread=False,
open_browser=True,
show_connection_info=True,
disableLogging=False,
backend="aiohttp",
exec_mode="main",
timeout=None,
):
"""
Start web server for serving your application
Parameters
----------
port : None or Number
Port on which the server should run on. When providing 0, the operating system will pick an available one.
thread : Boolean
If true we won't listen to ctrl-c
open_browser: Boolean
Default True which will open your browser and connect to your local server. Using --server is similar than passing False.
show_connection_info: Boolean
Default True which will print server IP and port information.
disableLogging: Boolean
Default False which will keep the default logging setup otherwise logger will be disabled.
exec_mode: String
Default "main", Options ["main", "task", "coroutine", "desktop"]
"main" will block, while "desktop" will run like main but as a standalone application
timeout=None,
Adjust timeout. 0 will prevent process ripper from running when no client is connected.
def start(
port=None,
thread=False,
open_browser=True,
show_connection_info=True,
disableLogging=False,
backend="aiohttp",
exec_mode="main",
timeout=None,
):
"""
Start web server for serving your application
Parameters
----------
port : None or Number
Port on which the server should run on. When providing 0, the operating system will pick an available one.
thread : Boolean
If true we won't listen to ctrl-c
open_browser: Boolean
Default True which will open your browser and connect to your local server. Using --server is similar than passing False.
show_connection_info: Boolean
Default True which will print server IP and port information.
disableLogging: Boolean
Default False which will keep the default logging setup otherwise logger will be disabled.
exec_mode: String
Default "main", Options ["main", "task", "coroutine", "desktop"]
"main" will block, while "desktop" will run like main but as a standalone application
timeout=None,
Adjust timeout. 0 will prevent process ripper from running when no client is connected.