Make your first plot

Follow these steps in order: query data, check its columns, configure the chart, then render it.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

To save the result, choose PNG or SVG in the top bar and click Save image.

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

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.