Make your first plot
Follow these steps in order: query data, check its columns, configure the chart, then render it.
-
Choose a table and query
In Schema, expand a table with its arrow. A new tab starts with a basic
SELECT *query when a table is available. Edit the SQL in Data source to select the rows and columns you need. -
Run the query
Click Run & show. This checks the query and displays a data preview. The query’s column names then become available in the mapping controls.
-
Configure the chart
Choose a plot type, then select an X column and one or more Y columns in Data mappings. Some plot types show extra required fields; for example, a heatmap asks for a Z column.
-
Render the plot
Click Run in the top bar. If the result is not what you expect, adjust the SQL or mappings and run it again.
-
Save your workspace
Click Export workspace and save the JSON file (plots.json) to the directory from where your ran the visualize command. This way, next time you start the webUI, you will resume from where you left off.
Transform query data with Python
Open Data transform (pandas) to change the SQL result before plotting. The result is provided as the pandas DataFrame df. Edit it in place or assign a new DataFrame to df; the final value of df is used for plotting.
df['new_column'] = df['existing_column'].astype(str).str.lower()
df = df[df['ncpus'] > 1]
Only the columns selected by SQL (including aliases) and columns created by the transform are plotted. Source tables are also available through data for lookups. The pd pandas module, agg helpers, and log(msg) are available in the script.
Use Script paths in the top bar to add directories containing your own Python modules. They are saved in your user config directory and are available to every Python script field; import them normally, for example from my_helpers import clean_flags. Workspace JSON exports include the configured paths and add any missing entries to the global path config.
Customize legends from data
The Layout script runs after the plot data and traces are prepared. It can inspect the query result and change each trace's legend name, order, group, or visibility. The same context is available with either renderer. Only columns selected by SQL or created by the transform are available in df.
Script context
df: the result as a pandas DataFrame after SQL and the pandas transform.df_data: the same data in{'columns': [...], 'rows': [...]}form.traces: the current list of Plotly-style trace dictionaries.layout,config,pd, andlog(msg)are also available.
Plotly: data-driven labels and order
This example assumes one Y column, the trace name is the compiler value, and the query includes compiler and rate. Higher mean rates appear first and the mean is added to each label.
mean_rate = df.groupby('compiler')['rate'].mean().to_dict()
order = sorted(mean_rate, key=mean_rate.get, reverse=True)
rank = {name: i for i, name in enumerate(order)}
for trace in traces:
name = trace.get('name', '')
if name in mean_rate:
trace['name'] = f"{name} — {mean_rate[name]:.1f}"
trace['legendrank'] = rank[name]
trace['legendgroup'] = name
layout.setdefault('legend', {}).update(
title={'text': 'Compiler · mean rate'},
orientation='h',
traceorder='normal',
)
Other useful trace fields are showlegend to hide selected entries and legendgroup to link entries. Set the same legendgroup for traces that should toggle together. Plotly uses the lower legendrank first.
Trace names are generated from the selected group columns and Y columns. If you use multiple Y columns, names often include both the group and Y column; inspect traces or log the names first, then adapt the lookup in the example.
Matplotlib: custom per-panel legend
Define customize_matplotlib in the same script. It runs after the default legend is created; calling ax.legend replaces that axes' legend. In a grid, this hook runs once per panel, and its global df/traces belong to that panel. For independent panel legends, turn off share legend.
mean_rate = df.groupby('compiler')['rate'].mean().to_dict()
def customize_matplotlib(fig, ax, config):
handles, labels = ax.get_legend_handles_labels()
pairs = [(mean_rate[label], handle, label)
for handle, label in zip(handles, labels)
if label in mean_rate]
pairs.sort(reverse=True, key=lambda item: item[0])
ax.legend(
[handle for _, handle, _ in pairs],
[f'{label} — {score:.1f}' for score, _, label in pairs],
title='Compiler · mean rate', ncol=2, frameon=False,
)
Matplotlib: custom shared legend for a grid
Put this hook in Panel 1's Layout script. It runs once after all panels and the default shared legend are drawn. axes is a 2D NumPy array; panel_data contains one {'df_data': ..., 'traces': ...} object per rendered panel.
def customize_matplotlib_legend(fig, axes, config, panel_data):
handles_by_name = {}
for ax in axes.flat:
handles, labels = ax.get_legend_handles_labels()
for handle, label in zip(handles, labels):
if label and not label.startswith('_'):
handles_by_name.setdefault(label, handle)
# Remove the default shared legend before creating the custom one.
fig.legends.clear()
labels = sorted(handles_by_name)
if not labels:
return
fig.legend(
[handles_by_name[label] for label in labels], labels,
title='All panels', loc='upper center', ncol=min(4, len(labels)),
frameon=False,
)
Legend options in the Appearance section still set defaults. A layout script runs after those options, so its edits take precedence. For a grid with a shared legend, use the Panel 1 hook when you need to replace the single figure-level legend.