Skip to content

Plotting

plotting

Plotting subpackage for results analyses.

Classes

HistogramPlotStrategy

Render histogram-like analyses with pyecharts bar charts.

Functions
render
render(data, request)

Render a histogram chart from normalized data.

Source code in src/owi/metadatabase/results/plotting/strategies.py
def render(self, data: pd.DataFrame, request: PlotRequest) -> PlotResponse:
    """Render a histogram chart from normalized data."""
    frame = data.copy()
    frame["bin_label"] = frame.apply(
        lambda row: (
            f"[{row['bin_left']},{row['bin_right']})" if pd.notna(row.get("bin_right")) else str(row["bin_left"])
        ),
        axis=1,
    )
    chart = Bar(init_opts=opts.InitOpts(width="100%", height="420px"))
    labels = list(dict.fromkeys(frame["bin_label"].tolist()))
    chart.add_xaxis(labels)
    for series_name, group in frame.groupby("series_name"):
        values_by_label = dict(group[["bin_label", "value"]].itertuples(index=False, name=None))
        chart.add_yaxis(
            str(series_name),
            [float(values_by_label.get(label, 0.0)) for label in labels],
            category_gap="30%",
        )
    chart.set_series_opts(label_opts=_label_opts(is_show=False))
    chart.set_global_opts(
        title_opts=_title_opts(request.title or request.analysis_name),
        legend_opts=_legend_opts(),
        tooltip_opts=_tooltip_opts(trigger="axis", axis_pointer_type="shadow"),
        xaxis_opts=_xaxis_opts(name="Bin"),
        yaxis_opts=_yaxis_opts(name="Value"),
    )
    _apply_cartesian_layout(chart)
    return _build_plot_response(chart)

TimeSeriesPlotStrategy

Render time-series analyses with pyecharts line charts.

Functions
render
render(data, request)

Render a time-series chart from normalized data.

Source code in src/owi/metadatabase/results/plotting/strategies.py
def render(self, data: pd.DataFrame, request: PlotRequest) -> PlotResponse:
    """Render a time-series chart from normalized data."""
    frame = data.copy()
    chart = Line(init_opts=opts.InitOpts(width="100%", height="420px"))
    x_values = list(dict.fromkeys(frame["x"].astype(str).tolist()))
    chart.add_xaxis(x_values)
    for series_name, group in frame.groupby("series_name"):
        values_by_x = {
            str(x_value): y_value for x_value, y_value in group[["x", "y"]].itertuples(index=False, name=None)
        }
        chart.add_yaxis(
            str(series_name),
            cast(Any, [float(values_by_x.get(value, 0.0)) for value in x_values]),
            is_smooth=False,
            is_symbol_show=True,
            symbol_size=6,
        )
    chart.set_series_opts(label_opts=_label_opts(is_show=False))
    chart.set_global_opts(
        title_opts=_title_opts(request.title or request.analysis_name),
        legend_opts=_legend_opts(),
        tooltip_opts=_tooltip_opts(trigger="axis"),
        xaxis_opts=_xaxis_opts(name="Time / Axis", boundary_gap=False),
        yaxis_opts=_yaxis_opts(name="Value"),
    )
    _apply_cartesian_layout(chart)
    return _build_plot_response(chart)

Functions

build_dropdown_plot_response

build_dropdown_plot_response(
    charts_by_key,
    *,
    dropdown_label,
    default_key=None,
    height="420px",
)

Build an HTML response that switches between chart options via a dropdown.

Source code in src/owi/metadatabase/results/plotting/response.py
def build_dropdown_plot_response(
    charts_by_key: Mapping[str, ChartLike],
    *,
    dropdown_label: str,
    default_key: str | None = None,
    height: str = "420px",
) -> PlotResponse:
    """Build an HTML response that switches between chart options via a dropdown."""
    if not charts_by_key:
        raise ValueError("At least one chart is required to build a dropdown plot.")
    selected_key = default_key or next(iter(charts_by_key))
    chart_id = f"owi_results_chart_{uuid4().hex}"
    select_id = f"owi_results_select_{uuid4().hex}"
    render_function = f"render_{chart_id}"
    load_callback_name = f"renderWhenReady_{chart_id}"
    for _, chart in charts_by_key.items():
        _apply_monospace_theme(chart)
    dependencies = list(
        dict.fromkeys(dependency for chart in charts_by_key.values() for dependency in chart.js_dependencies.items)
    )
    options_map = (
        "{\n" + ",\n".join(f"{json.dumps(key)}: {chart.dump_options()}" for key, chart in charts_by_key.items()) + "\n}"
    )
    key_items = [(key, key) for key in charts_by_key]
    key_dropdown = _custom_dropdown_markup(
        root_id=select_id,
        label=dropdown_label,
        items=key_items,
        selected_value=selected_key,
    )
    html = f"""
<div class="owi-results-dropdown-plot" style="font-family:{MONOSPACE_FONT_FAMILY}; border-radius: 0.5rem; padding: 1rem; border: 1px solid #ddd;">
    <style>
        .owi-results-control {{
            min-width: 160px;
        }}
        .owi-results-dropdown {{
            position: relative;
        }}
        .owi-results-dropdown [data-role="toggle"] {{
            width: 100%;
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
            padding: 4px 8px;
            border: 1px solid #d0d0d0;
            border-radius: 0.25rem;
            background: #ffffff;
            color: inherit;
            font-family: {MONOSPACE_FONT_FAMILY};
            cursor: pointer;
        }}
        .owi-results-dropdown [data-role="menu"] {{
            position: absolute;
            top: calc(100% + 4px);
            left: 0;
            z-index: 20;
            min-width: 100%;
            overflow: hidden;
            border: 1px solid #d0d0d0;
            border-radius: 0.25rem;
            background: #ffffff;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
        }}
        .owi-results-dropdown-item {{
            width: 100%;
            display: block;
            padding: 6px 8px;
            border: 0;
            background: transparent;
            color: inherit;
            font-family: {MONOSPACE_FONT_FAMILY};
            text-align: left;
            cursor: pointer;
        }}
        .owi-results-dropdown-item:hover,
        .owi-results-dropdown-item.is-selected {{
            background: #f2f4f7;
        }}
    </style>
    <div style="display:flex; gap:12px; flex-wrap:wrap; margin-bottom:12px;">
        {key_dropdown}
    </div>
    <div id="{chart_id}" style="width:100%;height:{height};"></div>
    <script>
        (function() {{
            function {load_callback_name}() {{
                var optionsByKey = {options_map};
                var keyItems = {json.dumps([{"value": key, "label": key} for key in charts_by_key])};
                var container = document.getElementById('{chart_id}');
                {_dropdown_script_helpers()}
                var selector = createDropdown('{select_id}', keyItems, {json.dumps(selected_key)}, function(value) {{
                    {render_function}(value);
                }});
                var chart = null;
                function ensureChart() {{
                    if (chart) {{
                        return chart;
                    }}
                    chart = echarts.init(container);
                    return chart;
                }}
                function {render_function}(key) {{
                    var activeChart = ensureChart();
                    activeChart.clear();
                    activeChart.setOption(optionsByKey[key], true);
                    activeChart.resize();
                }}
                window.addEventListener('resize', function() {{ if (chart) {{ chart.resize(); }} }});
                {_initial_render_script(f"{render_function}({json.dumps(selected_key)})")}
            }}
            {_loader_script(dependencies, load_callback_name)}
        }})();
    </script>
</div>
""".strip()
    widget_notebook = _build_widget_dropdown(
        charts_by_key,
        dropdown_label=dropdown_label,
        default_key=selected_key,
        frame_height=_parse_pixel_height(height, default=420) + 90,
    )
    notebook = _select_notebook_dropdown_renderer(
        widget_renderer=widget_notebook,
        html=html,
        frame_height=_parse_pixel_height(height, default=420) + 90,
    )
    return PlotResponse(
        chart=charts_by_key[selected_key],
        notebook=notebook,
        html=html,
        json_options=json.dumps(
            {
                key: json.loads(
                    chart.dump_options_with_quotes()
                    if callable(getattr(chart, "dump_options_with_quotes", None))
                    else chart.dump_options()
                )
                for key, chart in charts_by_key.items()
            }
        ),
    )

build_nested_dropdown_plot_response

build_nested_dropdown_plot_response(
    charts_by_primary_key,
    *,
    primary_label,
    secondary_label,
    default_primary_key=None,
    default_secondary_key=None,
    height="420px",
)

Build an HTML response with dependent primary and secondary dropdowns.

Source code in src/owi/metadatabase/results/plotting/response.py
def build_nested_dropdown_plot_response(
    charts_by_primary_key: Mapping[str, Mapping[str, ChartLike]],
    *,
    primary_label: str,
    secondary_label: str,
    default_primary_key: str | None = None,
    default_secondary_key: str | None = None,
    height: str = "420px",
) -> PlotResponse:
    """Build an HTML response with dependent primary and secondary dropdowns."""
    if not charts_by_primary_key:
        raise ValueError("At least one chart is required to build a dropdown plot.")
    selected_primary_key = default_primary_key or next(iter(charts_by_primary_key))
    secondary_charts = charts_by_primary_key[selected_primary_key]
    if not secondary_charts:
        raise ValueError("Each primary dropdown option must contain at least one chart.")
    selected_secondary_key = default_secondary_key or next(iter(secondary_charts))
    chart_id = f"owi_results_chart_{uuid4().hex}"
    primary_select_id = f"owi_results_primary_select_{uuid4().hex}"
    secondary_select_id = f"owi_results_secondary_select_{uuid4().hex}"
    render_function = f"render_{chart_id}"
    load_callback_name = f"renderWhenReady_{chart_id}"

    for charts_by_secondary_key in charts_by_primary_key.values():
        for chart in charts_by_secondary_key.values():
            _apply_monospace_theme(chart)
    dependencies = list(
        dict.fromkeys(
            dependency
            for charts_by_secondary_key in charts_by_primary_key.values()
            for chart in charts_by_secondary_key.values()
            for dependency in chart.js_dependencies.items
        )
    )

    options_map = (
        "{\n"
        + ",\n".join(
            f"{json.dumps(primary_key)}: {{\n"
            + ",\n".join(
                f"{json.dumps(secondary_key)}: {chart.dump_options()}"
                for secondary_key, chart in charts_by_secondary_key.items()
            )
            + "\n}"
            for primary_key, charts_by_secondary_key in charts_by_primary_key.items()
        )
        + "\n}"
    )
    primary_items = [(primary_key, primary_key) for primary_key in charts_by_primary_key]
    secondary_items_by_primary = {
        primary_key: [(secondary_key, secondary_key) for secondary_key in charts_by_secondary_key]
        for primary_key, charts_by_secondary_key in charts_by_primary_key.items()
    }
    primary_dropdown = _custom_dropdown_markup(
        root_id=primary_select_id,
        label=primary_label,
        items=primary_items,
        selected_value=selected_primary_key,
    )
    secondary_dropdown = _custom_dropdown_markup(
        root_id=secondary_select_id,
        label=secondary_label,
        items=secondary_items_by_primary[selected_primary_key],
        selected_value=selected_secondary_key,
    )
    html = f"""
<div class="owi-results-dropdown-plot" style="font-family:{MONOSPACE_FONT_FAMILY}; border-radius: 0.5rem; padding: 1rem; border: 1px solid #ddd;">
    <style>
        .owi-results-control {{
            min-width: 160px;
        }}
        .owi-results-dropdown {{
            position: relative;
        }}
        .owi-results-dropdown [data-role="toggle"] {{
            width: 100%;
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
            padding: 4px 8px;
            border: 1px solid #d0d0d0;
            border-radius: 0.25rem;
            background: #ffffff;
            color: inherit;
            font-family: {MONOSPACE_FONT_FAMILY};
            cursor: pointer;
        }}
        .owi-results-dropdown [data-role="menu"] {{
            position: absolute;
            top: calc(100% + 4px);
            left: 0;
            z-index: 20;
            min-width: 100%;
            overflow: hidden;
            border: 1px solid #d0d0d0;
            border-radius: 0.25rem;
            background: #ffffff;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
        }}
        .owi-results-dropdown-item {{
            width: 100%;
            display: block;
            padding: 6px 8px;
            border: 0;
            background: transparent;
            color: inherit;
            font-family: {MONOSPACE_FONT_FAMILY};
            text-align: left;
            cursor: pointer;
        }}
        .owi-results-dropdown-item:hover,
        .owi-results-dropdown-item.is-selected {{
            background: #f2f4f7;
        }}
    </style>
    <div style="display:flex; gap:12px; flex-wrap:wrap; margin-bottom:12px;">
        {primary_dropdown}
        {secondary_dropdown}
    </div>
    <div id="{chart_id}" style="width:100%;height:{height};"></div>
    <script>
        (function() {{
            function {load_callback_name}() {{
                var optionsByPrimaryKey = {options_map};
                var primaryItems = {json.dumps([{"value": key, "label": key} for key in charts_by_primary_key])};
                var secondaryItemsByPrimary = {json.dumps({key: [{"value": secondary_key, "label": secondary_key} for secondary_key in charts_by_secondary_key] for key, charts_by_secondary_key in charts_by_primary_key.items()})};
                var container = document.getElementById('{chart_id}');
                {_dropdown_script_helpers()}
                var primarySelector = createDropdown('{primary_select_id}', primaryItems, {json.dumps(selected_primary_key)}, function(primaryKey) {{
                    secondarySelector.setItems(secondaryItemsByPrimary[primaryKey], secondarySelector.getValue());
                    {render_function}(primaryKey, secondarySelector.getValue());
                }});
                var secondarySelector = createDropdown('{secondary_select_id}', secondaryItemsByPrimary[{json.dumps(selected_primary_key)}], {json.dumps(selected_secondary_key)}, function(secondaryKey) {{
                    {render_function}(primarySelector.getValue(), secondaryKey);
                }});
                var chart = null;
                function ensureChart() {{
                    if (chart) {{
                        return chart;
                    }}
                    chart = echarts.init(container);
                    return chart;
                }}
                function resetChart() {{
                    if (chart) {{
                        chart.dispose();
                        chart = null;
                    }}
                }}
                function {render_function}(primaryKey, secondaryKey) {{
                    resetChart();
                    var activeChart = ensureChart();
                    activeChart.clear();
                    activeChart.setOption(optionsByPrimaryKey[primaryKey][secondaryKey], true);
                    activeChart.resize();
                }}
                window.addEventListener('resize', function() {{ if (chart) {{ chart.resize(); }} }});
                {_initial_render_script(f"{render_function}({json.dumps(selected_primary_key)}, {json.dumps(selected_secondary_key)})")}
            }}
            {_loader_script(dependencies, load_callback_name)}
        }})();
    </script>
</div>
""".strip()
    widget_notebook = _build_nested_widget_dropdown(
        charts_by_primary_key,
        primary_label=primary_label,
        secondary_label=secondary_label,
        default_primary_key=selected_primary_key,
        default_secondary_key=selected_secondary_key,
        frame_height=_parse_pixel_height(height, default=420) + 120,
    )
    notebook = _select_notebook_dropdown_renderer(
        widget_renderer=widget_notebook,
        html=html,
        frame_height=_parse_pixel_height(height, default=420) + 120,
    )
    return PlotResponse(
        chart=charts_by_primary_key[selected_primary_key][selected_secondary_key],
        notebook=notebook,
        html=html,
        json_options=json.dumps(
            {
                primary_key: {
                    secondary_key: json.loads(
                        chart.dump_options_with_quotes()
                        if callable(getattr(chart, "dump_options_with_quotes", None))
                        else chart.dump_options()
                    )
                    for secondary_key, chart in charts_by_secondary_key.items()
                }
                for primary_key, charts_by_secondary_key in charts_by_primary_key.items()
            }
        ),
    )

get_plot_strategy

get_plot_strategy(plot_type)

Return the plotting strategy for a given plot type.

Source code in src/owi/metadatabase/results/plotting/strategies.py
def get_plot_strategy(plot_type: str):
    """Return the plotting strategy for a given plot type."""
    return PLOT_STRATEGIES[plot_type]