Skip to main content
Skip table of contents

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 /plot endpoint 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 /plot will render directly in Workbench.

  • Simple Export Options: Visualizations can be saved as images or embedded using browser tools.


Usage

  1. Open the Python Plotter Add-on in Workbench.

  2. Select a Plot Type from the dropdown menu.

  3. (Optional) Adjust configuration parameters for that plot.

  4. The Add-on executes the project’s POST /plot endpoint and displays the visualization.

The Add-on sends a JSON request body to the endpoint that includes context from the display pane:

CODE
{
  "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.

CODE
{
  "chartType": "plotly",
  "spec": {}
}

Required Fields

Field

Type

Description

chartType

string

Renderer identifier (plotly, highcharts, bokeh).

spec

object

Renderer-specific specification (fig.to_plotly_json(), Highcharts options, or Bokeh json_item(...)).

Optional Fields

Field

Type

Description

config

object

Renderer-specific configuration.

width, height

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 .pythonplotter.plotter. in the name. The substring following it appears as the Plot Type label. Any “_” will be treated as blank spaces.

Notebook Name

Must include a notebook named API.ipynb.

Endpoint

The notebook must have a cell that defines a POST /plot endpoint that returns either an envelope or HTML/SVG string.

pythonPlotterRequirements.png

Example Plot Name:

CODE
pythonplotter.plotter.cool_new_plot

→ Appears as “Cool New Plot” in the dropdown.


Example Implementations

1. Plotly Violin Plot (Envelope Format)

PY
# 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)

PY
# 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

PY
# 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).

PY
# 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 .tolist() for arrays

Plot not resizing

Missing width / height

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.

  • chartType determines renderer; raw HTML bypasses this for inline rendering.

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.