Python Plotter Add-on
Applies To: Python Plotter Add-on ≥ v0.3.0
Overview
The Python Plotter Add-on enables users to create rich, custom visualizations directly within Seeq Data Lab and visualized in Workbench. It uses the signals and conditions in the Details Pane and the display range in the Display Pane to dynamically call a POST /plot endpoint inside a Seeq Data Lab project.
Plots can now return results to multiple supported renderers — including Plotly, Highcharts, Bokeh, or direct HTML/SVG — allowing authors to use their preferred visualization libraries while maintaining a consistent experience in Workbench.
Key Features
Dynamic Figure Generation: Create plots using the
POST /plotendpoint that automatically updates when the Details Pane or display range changes.Multi-Renderer Support: Return results via a structured JSON envelope (
chartType:plotly,highcharts,bokeh) or as direct HTML/SVG.Flexible Plotting Libraries: Supports Plotly, Highcharts, Bokeh, and Matplotlib-to-SVG rendering.
Custom HTML Support: Any valid HTML or SVG string returned from
/plotwill render directly in Workbench.Simple Export Options: Visualizations can be saved as images or embedded using browser tools.
Usage
Open the Python Plotter Add-on in Workbench.
Select a Plot Type from the dropdown menu.
(Optional) Adjust configuration parameters for that plot.
The Add-on executes the project’s
POST /plotendpoint and displays the visualization.
The Add-on sends a JSON request body to the endpoint that includes context from the display pane:
{
"start": 1695898858683,
"end": 1698308230500,
"signals": [{}],
"scalars": [{}],
"conditions": [{}],
"metrics": [{}],
"height": 347,
"width": 1185
}
This ensures the visualization reacts dynamically to changes in time range, selections, or layout.
Reactive Changes
The /plot endpoint automatically re-executes whenever:
The display range changes (start or end).
Items in the Details Pane change (signals, conditions, metrics, scalars).
The plot dimensions change (height or width).
This ensures the visualization always stays synchronized with user context.
Response Formats
The Python Plotter Add-on supports two types of responses:
1. JSON Envelope (Recommended)
Return a structured JSON object that instructs the front-end which renderer to use.
{
"chartType": "plotly",
"spec": {}
}
Required Fields
Field | Type | Description |
|---|---|---|
| string | Renderer identifier ( |
| object | Renderer-specific specification ( |
Optional Fields
Field | Type | Description |
|---|---|---|
| object | Renderer-specific configuration. |
| number | Desired pixel dimensions (defaults to 100%). |
2. Direct HTML/SVG (Legacy Compatible)
Return a valid HTML or SVG string directly (no envelope).
This method is ideal for simple static plots or when exporting an image from Plotly or Matplotlib.
Registering a New Plot
To add new plot types to the dropdown, create a Data Lab project with the following structure:
Requirement | Description |
|---|---|
Project Name | Must include |
Notebook Name | Must include a notebook named |
Endpoint | The notebook must have a cell that defines a |

Example Plot Name:
pythonplotter.plotter.cool_new_plot
→ Appears as “Cool New Plot” in the dropdown.
Example Implementations
1. Plotly Violin Plot (Envelope Format)
# POST /plot
import pandas as pd
import plotly.graph_objects as go
from seeq import spy
body = REQUEST['body']
signals = pd.DataFrame(body['signals']).rename(columns={'id': 'ID'})
start = pd.to_datetime(body['start'], unit='ms')
end = pd.to_datetime(body['end'], unit='ms')
search = spy.search(signals, all_properties=True, quiet=True)
pull = spy.pull(search, start=start, end=end, grid=None, header='ID', quiet=True)
fig = go.Figure()
for sig_id in search['ID']:
name = signals.loc[signals['ID'] == sig_id, 'name'].iloc[0]
color = signals.loc[signals['ID'] == sig_id, 'color'].iloc[0]
series = pull[sig_id]
y_vals = series.astype(object).where(series.notna(), None).tolist()
x_vals = [name] * len(y_vals)
fig.add_trace(go.Violin(
x=x_vals,
y=y_vals,
name=name,
line_color=color,
box_visible=True,
meanline_visible=True,
opacity=0.6
))
fig.update_layout(xaxis_showticklabels=True, showlegend=True,
margin=dict(l=0, r=0, t=30, b=0))
envelope = {
"chartType": "plotly",
"spec": fig.to_plotly_json()
}
envelope
2. Highcharts Violin Plot (Polygon Approximation)
# POST /plot
import numpy as np
import pandas as pd
from seeq import spy
body = REQUEST['body']
signals = pd.DataFrame(body['signals']).rename(columns={'id': 'ID'})
start = pd.to_datetime(body['start'], unit='ms')
end = pd.to_datetime(body['end'], unit='ms')
search = spy.search(signals, all_properties=True, quiet=True)
pull = spy.pull(search, start=start, end=end, grid=None, header='ID', quiet=True)
names = []
series_specs = []
for i, sig_id in enumerate(search['ID']):
name = signals.loc[signals['ID'] == sig_id, 'name'].iloc[0]
color = signals.loc[signals['ID'] == sig_id, 'color'].iloc[0]
names.append(name)
s = pull[sig_id]
y_vals = s.astype(object).where(s.notna(), None).tolist()
y_clean = [float(v) for v in y_vals if v is not None]
if len(y_clean) < 2:
series_specs.append({"type": "polygon", "name": name, "data": [], "color": color})
continue
counts, bin_edges = np.histogram(y_clean, bins=30, density=True)
centers = 0.5 * (bin_edges[:-1] + bin_edges[1:])
dens = counts / counts.max() if counts.max() > 0 else counts
y_arr = np.array(y_clean)
stats = {
"count": int(y_arr.size),
"min": float(np.min(y_arr)),
"q1": float(np.percentile(y_arr, 25)),
"median": float(np.percentile(y_arr, 50)),
"q3": float(np.percentile(y_arr, 75)),
"max": float(np.max(y_arr)),
"mean": float(np.mean(y_arr))
}
width = 0.4
x_right = i + dens * width
x_left = i - dens * width
x_poly = np.concatenate([x_right, x_left[::-1]]).tolist()
y_poly = np.concatenate([centers, centers[::-1]]).tolist()
data = [{
"x": float(x),
"y": float(y),
**stats
} for x, y in zip(x_poly, y_poly)]
series_specs.append({
"type": "polygon",
"name": name,
"data": data,
"color": str(color),
"fillOpacity": 0.6,
"tooltip": {
"pointFormat": (
"<b>{series.name}</b><br/>"
"n: {point.count}<br/>"
"min: {point.min:.2f}<br/>"
"q1: {point.q1:.2f}<br/>"
"median: {point.median:.2f}<br/>"
"q3: {point.q3:.2f}<br/>"
"max: {point.max:.2f}<br/>"
"mean: {point.mean:.2f}"
)
}
})
options = {
"chart": {"spacing": [10, 10, 10, 10]},
"title": {"text": None},
"xAxis": {"categories": names, "tickmarkPlacement": "on"},
"yAxis": {"title": {"text": None}},
"legend": {"enabled": True},
"series": series_specs
}
envelope = {
"chartType": "highcharts",
"spec": options,
"width": body.get('width', 700),
"height": body.get('height', 400)
}
envelope
3. Bokeh Violin Plot
# POST /plot
import numpy as np
import pandas as pd
from seeq import spy
from bokeh.plotting import figure
from bokeh.models import Range1d, FixedTicker, HoverTool, ColumnDataSource
from bokeh.embed import json_item
body = REQUEST['body']
signals = pd.DataFrame(body['signals']).rename(columns={'id': 'ID'})
start = pd.to_datetime(body['start'], unit='ms')
end = pd.to_datetime(body['end'], unit='ms')
search = spy.search(signals, all_properties=True, quiet=True)
pull = spy.pull(search, start=start, end=end, grid=None, header='ID', quiet=True)
names = [signals.loc[signals['ID'] == sig_id, 'name'].iloc[0] for sig_id in search['ID']]
p = figure(width=body.get('width', 700), height=body.get('height', 400))
p.x_range = Range1d(-0.6, len(names) - 1 + 0.6)
p.xaxis.ticker = FixedTicker(ticks=list(range(len(names))))
p.xaxis.major_label_overrides = {i: n for i, n in enumerate(names)}
for i, sig_id in enumerate(search['ID']):
color = signals.loc[signals['ID'] == sig_id, 'color'].iloc[0]
s = pull[sig_id]
y_vals = s.astype(object).where(s.notna(), None).tolist()
y_clean = [float(v) for v in y_vals if v is not None]
if len(y_clean) < 2:
continue
counts, bin_edges = np.histogram(y_clean, bins=30, density=True)
centers = 0.5 * (bin_edges[:-1] + bin_edges[1:])
dens = counts / counts.max() if counts.max() > 0 else counts
width = 0.4
x_right = i + dens * width
x_left = i - dens * width
xs = np.concatenate([x_right, x_left[::-1]])
ys = np.concatenate([centers, centers[::-1]])
y_arr = np.array(y_clean)
stats = dict(
count=int(y_arr.size),
min=float(np.min(y_arr)),
q1=float(np.percentile(y_arr, 25)),
median=float(np.percentile(y_arr, 50)),
q3=float(np.percentile(y_arr, 75)),
max=float(np.max(y_arr)),
mean=float(np.mean(y_arr)),
)
src = ColumnDataSource(data=dict(
x=xs.tolist(),
y=ys.tolist(),
**{k: [v]*len(xs) for k, v in stats.items()}
))
r = p.patch(x='x', y='y', source=src,
fill_alpha=0.6, line_alpha=1.0,
line_color=str(color), fill_color=str(color))
p.add_tools(HoverTool(renderers=[r], tooltips=[
("n", "@count"),
("min", "@min{0.00}"),
("q1", "@q1{0.00}"),
("median", "@median{0.00}"),
("q3", "@q3{0.00}"),
("max", "@max{0.00}"),
("mean", "@mean{0.00}"),
]))
envelope = {"chartType": "bokeh", "spec": json_item(p)}
envelope
4. Basic SVG Violin Plot (Legacy Example)
This example follows the original behavior of the Python Plotter Add-on.
It returns an SVG string directly (no envelope).
# POST /plot
import os
import logging
import pandas as pd
import plotly.graph_objects as go
from seeq import spy
my_log = logging.getLogger(os.environ['SEEQ_DATALAB_FUNCTIONS_LOGGER'])
my_log.info("Violin Plot!")
signals = pd.DataFrame(REQUEST['body']['signals']).sort_values(by='axis')
signals = signals.rename(columns={'id': "ID"})
start = pd.to_datetime(REQUEST['body']['start'], unit='ms')
end = pd.to_datetime(REQUEST['body']['end'], unit='ms')
height = REQUEST['body']['height']
width = REQUEST['body']['width']
search = spy.search(signals, all_properties=True, quiet=True)
pull = spy.pull(search, start=start, end=end, grid=None, header="ID", quiet=True)
fig = go.Figure()
for id in search['ID']:
fig.add_trace(go.Violin(
x=[signals[signals['ID'] == id]['name'].iloc[0]] * 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(
autosize=False,
margin=dict(l=0, r=0, t=0, b=0),
height=height,
width=width
)
# Convert to SVG and return directly
svg = fig.to_image(format="svg", scale=1).decode()
svg
Quick Troubleshooting
Symptom | Likely Cause | Resolution |
|---|---|---|
Text instead of chart | Invalid JSON or malformed HTML | Ensure JSON or HTML is valid |
“ndarray is not JSON serializable” | NumPy objects not converted | Use |
Plot not resizing | Missing | Include numeric dimensions |
Kaleido errors | Incompatible version | Downgrade or rely on envelope method |
The most recent release, version 1.0.0, of kaleido https://pypi.org/project/kaleido/#history (which is utilized for generating SVG images in Plotly) is currently incompatible with Data Lab. If you encounter an error associated with this package, it is advisable to revert to an earlier version.
Notes
The envelope format is recommended for modern renderers (Plotly, Highcharts, Bokeh).
Direct SVG/HTML output remains fully supported for legacy compatibility.
Convert all data to native Python types before serializing.
chartTypedetermines renderer; raw HTML bypasses this for inline rendering.