Python Plotter Add-on
Overview
This Add-on empowers users to generate custom figures directly within the UI by calling API endpoints within Jupyter Notebooks. It seamlessly integrates with Plotly, enabling the creation of rich visualizations.
Key Features
Dynamic Figure Generation: Utilize API endpoints within Jupyter Notebooks to generate custom figures on demand.
Enhanced Plotting Integrations: Take advantage of the flexibility offered by Plotly or Matplotlib to create a wide range of visualizations, all easily accessible within Workbench.
Easy Export Options: Effortlessly export your images to SVG format. Furthermore, anything that can be created with HTML can be rendered in Workbench, enabling you to design custom HTML styles and components that extend beyond just images.
Usage
The Add-on uses the signals and condition's present in the details pane and the display range in the display pane. It will pass these parameters to the endpoint of the called POST /plot
Access the Python Plotter functionality within UI.
Choose the desired plot type from the dropdown menu.
(Optional) Customize plot parameters as needed.
The generated SVG image will be displayed or made available for download.
How to Register a New Plot
Registering a new plot is straight forward. Plots are generated from Data Lab Projects. These Projects can be created from the UI or by developing an Add-on with a Data Lab Functions Element. There are only three requirements to get your visualization into workbench.
Project Name: The name of your Data Lab Project must include
.pythonplotter.plotter.
as a substring. The portion of the identifier following.pythonplotter.plotter.
will be displayed in the plot type dropdown. See the example.Notebook Name: The Project must contain a Notebook with the name
api.ipynb
Endpoint: The
api.ipynb
must possess aPOST /plot
endpoint to serve the generated HTML renderable visualization.

Example
An Add-on with the identifier com.test.addon.pythonplotter.plotter.cool_new_plot
would introduce a "Cool New Plot" option within the plot type dropdown. It would be expected to have a POST /plot
endpoint withing the api.ipynb
Notebook.
Reactive Changes
The POST /plot
endpoint is triggered when details pane or display range changes. The information sent through the body
of the REQUEST
. The information sent is :
{
"start": "int",
"end": "int",
"signals": [{}],
"scalars": [{}],
"conditions": [{}],
"metrics": [{}],
"height": "int",
"width": "int",
}
start
- start time, in ms, of the display range
end
- end time, in ms, of the display range
signals
- list of dicts containing the signals on the screen with their properties
scalars
- list of dicts containing the scalars on the screen with their properties
conditons
- list of dicts containing the conditions on the screen with their properties
metrics
- list of dicts containing the metrics on the screen with their properties
height
- height of plot area
width
= width of plot area
and example REQUEST['body']
output looks like this:
{
"start": 1695898858683,
"end": 1698308230500,
"signals": [
{
"id": "F8E053D1-A4D5-4671-9969-1D5D7D4F27DD",
"name": "Temperature",
"dataStatus": "itemDataInitializing",
"lastFetchRequest": "2023-12-28T16:04:33.788Z",
"selected": false,
"color": "#FFC000",
"assets": [
{
"id": "2407642C-0169-4ED0-A25C-321E29DC975B",
"name": "Area A",
"formattedName": "Example » Cooling Tower 1 » Area A",
"pathComponentIds": [
"39F3A721-922E-467D-9DD9-D60364A7EC39",
"2D048792-9E40-4069-85F3-E88930688D78",
"2407642C-0169-4ED0-A25C-321E29DC975B"
]
}
],
"isStringSignal": false,
"autoScale": true,
"axis": "A",
"axisMin": -1,
"axisMax": 1,
"valueUnitOfMeasure": "°F"
}
],
"scalars": [
{
"id": "0EEA59D4-DE3B-6040-9E2E-DAC3DEF3DB3D",
"name": "Scalar",
"dataStatus": "itemDataPresent",
"lastFetchRequest": "2023-12-28T16:22:33.044Z",
"selected": false,
"color": "#E1498E",
"assets": [],
"isStringScalar": false,
"autoScale": true,
"axis": "C",
"axisMin": 30,
"axisMax": 50,
"valueUnitOfMeasure": ""
}
],
"conditions": [
{
"id": "BB9F87CC-9700-4A5B-BB7A-4B4E45382AF8",
"name": "TestCondition",
"dataStatus": "itemDataInitializing",
"lastFetchRequest": "2023-12-28T16:21:26.139Z",
"selected": false,
"color": "#068C45",
"assets": [
{
"id": "AA1E42AE-90BD-4CF7-9449-F8CC81625E8F",
"name": "Area B",
"formattedName": "Example » Cooling Tower 1 » Area B",
"pathComponentIds": [
"39F3A721-922E-467D-9DD9-D60364A7EC39",
"2D048792-9E40-4069-85F3-E88930688D78",
"AA1E42AE-90BD-4CF7-9449-F8CC81625E8F"
]
}
]
}
],
"metrics": [
{
"id": "0EEA59D4-1E26-EA70-AB9C-D6444244A4B6",
"name": "average temp",
"dataStatus": "itemDataPresent",
"lastFetchRequest": "2023-12-28T16:22:13.711Z",
"selected": false,
"color": "#4055A3",
"assets": [
{
"id": "2407642C-0169-4ED0-A25C-321E29DC975B",
"name": "Area A",
"formattedName": "Example » Cooling Tower 1 » Area A",
"pathComponentIds": [
"39F3A721-922E-467D-9DD9-D60364A7EC39",
"2D048792-9E40-4069-85F3-E88930688D78",
"2407642C-0169-4ED0-A25C-321E29DC975B"
]
}
],
"autoScale": true,
"axis": "B",
"axisMin": 50,
"axisMax": 70
}
],
"height": 347,
"width": 1185
}
Building a Custom Chart
A custom graph can be generated using the body
of the REQUEST
. Here is an example that generates the Violin graphs in this Add-on:
# POST /plot
# Import libraries
import os
import logging
import plotly.graph_objects as go
my_log = logging.getLogger(os.environ['SEEQ_DATALAB_FUNCTIONS_LOGGER'])
my_log.info("Violin Plot!")
#Get inforation from REQUEST
# Signals are the only items required for Violin Plots
signals = pd.DataFrame(REQUEST['body']['signals']).sort_values(by='axis') # sorting to maintain order
signals = signals.rename(columns={'id':"ID"})
# Get the Display Range
start = pd.to_datetime(REQUEST['body']['start'], unit='ms')
end = pd.to_datetime(REQUEST['body']['end'], unit='ms')
# Get the size of the display pane
height = REQUEST['body']['height']
width = REQUEST['body']['width']
# Search and Pull the Data
search = spy.search(signals, all_properties=True, quiet=True)
pull = spy.pull(search, start=start, end=end, grid=None, header="ID", quiet=True)
my_log.info(search.to_json())
#Generate the figure
fig = go.Figure()
# Go through all the IDs supplied
# This also shows how the colors can be mapped to the colors selected in the UI
for id in search['ID']:
fig.add_trace(go.Violin(x=list(search['ID'][search['ID'] == id])*len(pull[id]),
y=pull[id],
name=signals[signals['ID']==id]['name'].iloc[0],
line_color=signals[signals['ID']==id]['color'].iloc[0],
box_visible=True,
meanline_visible=True,
opacity=0.6
))
fig.update_layout(xaxis_showticklabels=False, showlegend=False)
# Update the Layout to output the size of the Display Pange
fig.update_layout(
autosize=False,
margin=dict(l=0, r=0, t=0, b=0),
height=height,
width=width
)
#Convert to SVG
svg = fig.to_image(format="svg", scale=1).decode()
svg