# mlflow-widgets > Anywidget-based notebook widgets for MLflow experiment visualization Anywidget-based notebook widgets for MLflow experiment visualization. Provides MlflowChart (live metric line chart), MlflowRunTable (sortable run table), MlflowParallelCoordinates (parallel coordinates chart), and MlflowRunSelector (DataFrame-based run picker). Works in Jupyter, Marimo, and any notebook supporting anywidget. # Guides # mlflow-widgets Anywidget-based notebook widgets for [MLflow](https://mlflow.org/) experiment visualization. Inspired by [wigglystuff](https://github.com/koaning/wigglystuff)'s [`WandbChart`](https://koaning.github.io/wigglystuff/reference/wandb-chart/), but built for MLflow. ## Overview mlflow-widgets provides interactive, live-updating widgets that work in **Jupyter**, **Marimo**, and any notebook environment that supports [anywidget](https://anywidget.dev/). | Widget | Description | | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | [MlflowChart](https://daviddwlee84.github.io/mlflow-widgets/widgets/chart/index.md) | Live-updating canvas line chart for MLflow metrics | | [MlflowRunTable](https://daviddwlee84.github.io/mlflow-widgets/widgets/table/index.md) | Sortable HTML table of experiment runs with nested run tree | | [MlflowParallelCoordinates](https://daviddwlee84.github.io/mlflow-widgets/widgets/parallel/index.md) | Parallel coordinates chart for hyperparameter comparison | | [MlflowRunSelector](https://daviddwlee84.github.io/mlflow-widgets/widgets/selector/index.md) | Pure Python helper returning pandas DataFrames for run selection | ## Key features - Live-updating canvas-based line chart with multiple x-axis modes (step, wall time, relative time) - Interactive clickable legend with per-series toggle and nested run grouping - Experiment run table with sortable columns and nested run tree view - Parallel coordinates chart with parameter/metric filtering and multiple color modes - Smoothing controls: rolling mean, exponential moving average, gaussian - Hover tooltips with exact values - Auto-polling with configurable interval - Works in Jupyter, Marimo, and any notebook that supports anywidget ## Quick example ``` from mlflow_widgets import MlflowChart chart = MlflowChart( tracking_uri="http://localhost:5000", experiment_id="1", metric_key="loss", ) chart ``` ## For AI assistants This site exposes LLM-friendly endpoints: - [`/llms.txt`](https://daviddwlee84.github.io/mlflow-widgets/llms.txt) -- compact index following the [llmstxt.org](https://llmstxt.org/) spec - [`/llms-full.txt`](https://daviddwlee84.github.io/mlflow-widgets/llms-full.txt) -- all documentation concatenated into a single file Every page is also available as raw Markdown at the same URL path with an `.md` extension (e.g. `/installation/index.md`). ## Links - [PyPI](https://pypi.org/project/mlflow-widgets/) - [GitHub](https://github.com/daviddwlee84/mlflow-widgets) - [Changelog](https://daviddwlee84.github.io/mlflow-widgets/changelog/index.md) # Installation ## From PyPI ``` pip install mlflow-widgets ``` ``` uv add mlflow-widgets ``` ### With MLflow included If you don't already have MLflow installed: ``` pip install mlflow-widgets[mlflow] ``` ``` uv add mlflow-widgets[mlflow] ``` ## From GitHub (latest) Install the latest development version directly from the repository: ``` pip install git+https://github.com/daviddwlee84/mlflow-widgets.git ``` ``` uv add git+https://github.com/daviddwlee84/mlflow-widgets.git # or uv pip install git+https://github.com/daviddwlee84/mlflow-widgets.git ``` ## Requirements - Python >= 3.10 - Dependencies: `anywidget >= 0.9.2`, `traitlets` - Optional: `mlflow >= 2.0` (for data fetching; falls back to REST API via `urllib` if not installed) ## Extras | Extra | Packages | Use case | | -------- | ----------------------------------------- | -------------------------- | | `mlflow` | `mlflow>=2.0`, `python-socks[asyncio]` | Full MLflow client support | | `demo` | `marimo>=0.10.0`, `mlflow>=2.0` | Running example notebooks | | `dev` | `pytest`, `ruff`, `twine`, `taskipy` | Development and testing | | `docs` | `mkdocs-material`, `mkdocstrings[python]` | Building documentation | # Getting Started This guide walks you through setting up MLflow and creating your first widget. ## Prerequisites 1. Install mlflow-widgets with MLflow: ``` pip install mlflow-widgets[mlflow] ``` 1. Start an MLflow tracking server: ``` mlflow server --port 5000 ``` ## Create some runs Log a few training runs so there's data to visualize: ``` import mlflow mlflow.set_tracking_uri("http://localhost:5000") mlflow.set_experiment("my-experiment") for trial in range(3): with mlflow.start_run(run_name=f"trial-{trial}"): mlflow.log_param("lr", 0.01 * (trial + 1)) for step in range(50): mlflow.log_metric("loss", 1.0 / (step + 1 + trial), step=step) ``` ## Visualize with MlflowChart ``` from mlflow_widgets import MlflowChart chart = MlflowChart( tracking_uri="http://localhost:5000", experiment_id="1", metric_key="loss", ) chart ``` The chart will auto-poll every 5 seconds by default. Pass `poll_seconds=None` to disable auto-polling and show a manual refresh button instead. ## Browse runs with MlflowRunTable ``` from mlflow_widgets import MlflowRunTable table = MlflowRunTable( tracking_uri="http://localhost:5000", experiment_id="1", ) table ``` ## Usage with Marimo Wrap any widget with `mo.ui.anywidget()` to use it in a [Marimo](https://marimo.io/) notebook: ``` import marimo as mo from mlflow_widgets import MlflowChart chart = MlflowChart( tracking_uri="http://localhost:5000", experiment_id="1", metric_key="loss", ) widget = mo.ui.anywidget(chart) widget ``` ## Next steps - Learn about each widget in the [Widgets](https://daviddwlee84.github.io/mlflow-widgets/widgets/chart/index.md) section - Browse the [Examples](https://daviddwlee84.github.io/mlflow-widgets/examples/index.md) for complete Marimo notebook demos - See the full [API Reference](https://daviddwlee84.github.io/mlflow-widgets/reference/index.md) for all parameters and methods # Widgets # MlflowChart Live-updating canvas-based line chart for MLflow metrics. Supports multiple runs for side-by-side comparison with smoothing, tooltips, and an interactive legend. ## Basic usage ``` from mlflow_widgets import MlflowChart chart = MlflowChart( tracking_uri="http://localhost:5000", experiment_id="1", metric_key="loss", ) chart ``` ## From specific runs Pass run objects, run ID strings, or dicts with `id` and `label` keys: ``` chart = MlflowChart( tracking_uri="http://localhost:5000", runs=["run_id_1", "run_id_2"], metric_key="loss", ) ``` Or with MLflow Run objects: ``` import mlflow with mlflow.start_run() as run: mlflow.log_metric("loss", 0.5) chart = MlflowChart(runs=[run], metric_key="loss") ``` ## X-axis modes Control what the x-axis represents: | Mode | Description | | ------------ | --------------------------------------- | | `"step"` | MLflow step number (default) | | `"wall"` | Wall clock time | | `"relative"` | Time relative to the first logged point | ``` chart = MlflowChart( experiment_id="1", metric_key="loss", x_axis="relative", ) ``` ## Smoothing Three smoothing algorithms are available: | Kind | Parameter | Description | | --------------- | -------------------------- | ---------------------------- | | `"rolling"` | Window size (integer >= 2) | Rolling mean | | `"exponential"` | Alpha (0 < alpha < 1) | Exponential moving average | | `"gaussian"` | Sigma (> 0) | Gaussian smoothing (default) | ``` chart = MlflowChart( experiment_id="1", metric_key="loss", smoothing_kind="exponential", smoothing_param=0.3, ) ``` Set `show_slider=False` to hide the smoothing slider from the UI. ## Auto-polling By default, the chart polls every 5 seconds. Customize or disable: ``` # Poll every 10 seconds chart = MlflowChart(experiment_id="1", metric_key="loss", poll_seconds=10) # Manual refresh only chart = MlflowChart(experiment_id="1", metric_key="loss", poll_seconds=None) ``` Always clean up timers Call `chart.stop()` or `chart.close()` when done to cancel background polling timers. ## API reference See [`MlflowChart`](https://daviddwlee84.github.io/mlflow-widgets/reference/chart/index.md) for the full API. # MlflowParallelCoordinates Parallel coordinates chart for comparing runs across hyperparameters and metrics. Each vertical axis is a parameter or metric; each polyline is a run. ## Basic usage ``` from mlflow_widgets import MlflowParallelCoordinates chart = MlflowParallelCoordinates( tracking_uri="http://localhost:5000", experiment_id="1", ) chart ``` ## Filtering axes By default, all parameters and metrics are shown. Restrict to specific keys: ``` chart = MlflowParallelCoordinates( experiment_id="1", param_keys=["lr", "batch_size"], metric_keys=["loss", "accuracy"], ) ``` ## Color modes Control how lines are colored using the `color_by` parameter: | Value | Description | | ------------- | ------------------------------ | | `"status"` | Color by run status (default) | | A metric name | Color by metric value gradient | ``` chart = MlflowParallelCoordinates( experiment_id="1", color_by="loss", ) ``` The widget also provides status filter checkboxes in the UI to show/hide runs by status. ## API reference See [`MlflowParallelCoordinates`](https://daviddwlee84.github.io/mlflow-widgets/reference/parallel/index.md) for the full API. # MlflowRunSelector Pure Python helper that fetches MLflow experiment runs and returns them as a pandas DataFrame. Designed for interactive run selection workflows, especially with Marimo's `mo.ui.table`. Note MlflowRunSelector is **not a widget** -- it's a plain Python class. It does not render anything on its own. ## Basic usage ``` from mlflow_widgets import MlflowRunSelector selector = MlflowRunSelector( tracking_uri="http://localhost:5000", experiment_id="1", ) df = selector.to_dataframe() ``` The returned DataFrame includes columns: `run_id`, `run_name`, `status`, `start_time`, `duration_s`, plus `param.` and `metric.` for each parameter and metric. ## Interactive selection with Marimo The primary use case is selecting runs via checkboxes, then passing selected IDs to a chart: ``` import marimo as mo from mlflow_widgets import MlflowRunSelector, MlflowChart selector = MlflowRunSelector( tracking_uri="http://localhost:5000", experiment_id="1", ) table = mo.ui.table(selector.to_dataframe(), selection="multi") ``` In another cell: ``` selected_ids = MlflowRunSelector.get_run_ids(table.value) chart = MlflowChart(runs=selected_ids, metric_key="loss") mo.ui.anywidget(chart) ``` ## Requirements - **pandas** must be installed to use `to_dataframe()`. It will raise `ImportError` with a helpful message if missing. ## API reference See [`MlflowRunSelector`](https://daviddwlee84.github.io/mlflow-widgets/reference/selector/index.md) for the full API. # MlflowRunTable Interactive HTML table showing experiment runs with parameters, metrics, status badges, and duration. Supports nested runs displayed as collapsible tree nodes. ## Basic usage ``` from mlflow_widgets import MlflowRunTable table = MlflowRunTable( tracking_uri="http://localhost:5000", experiment_id="1", ) table ``` ## Features - **Sortable columns** -- click any column header to sort - **Status badges** -- color-coded status indicators (FINISHED, RUNNING, FAILED, KILLED) - **Nested runs** -- parent runs display as collapsible tree nodes with child runs indented beneath - **Auto-discovery** -- automatically shows all parameters and metrics found across runs ## Auto-polling Enable periodic refresh to see new runs as they appear: ``` table = MlflowRunTable( experiment_id="1", poll_seconds=10, ) ``` By default, auto-polling is disabled (`poll_seconds=None`), showing a manual refresh button instead. Always clean up timers Call `table.stop()` or `table.close()` when done to cancel background polling timers. ## API reference See [`MlflowRunTable`](https://daviddwlee84.github.io/mlflow-widgets/reference/table/index.md) for the full API. # Examples # Examples mlflow-widgets ships with several [Marimo](https://marimo.io/) notebook demos in the `examples/` directory. Each file is a self-contained Marimo notebook with inline dependency metadata. ## Running the demos ``` # Install demo dependencies pip install mlflow-widgets[demo] # Start an MLflow server mlflow server --port 5000 & # Run a specific demo MLFLOW_TRACKING_URI=http://localhost:5000 marimo edit examples/demo.py # Run all demos with hot-reload MLFLOW_TRACKING_URI=http://localhost:5000 marimo edit --watch . ``` ## Available demos ### demo.py -- Mock experiments + chart Generates mock training runs with random metrics, then visualizes them with `MlflowChart`. A good starting point to see the chart in action. ### live_tracking.py -- Live tracking Tracks an experiment on-the-fly and shows live chart updates as metrics are logged. Demonstrates the auto-polling feature. ### table_demo.py -- Run table Displays all runs in an experiment using `MlflowRunTable` with sortable columns, status badges, and parameter/metric columns. ### nested_demo.py -- Nested runs Creates parent/child nested runs and displays them in `MlflowRunTable`'s tree view with collapsible groups. ### parallel_demo.py -- Parallel coordinates Shows a parallel coordinates chart with status filtering. Each axis represents a hyperparameter or metric. ### combo_demo.py -- Selector + table + chart End-to-end workflow: `MlflowRunSelector` produces a DataFrame, `mo.ui.table` enables checkbox selection, and `MlflowChart` visualizes the selected runs. Demonstrates how the components compose together. # API Reference # API Reference Full API documentation for all public classes in mlflow-widgets. Generated automatically from source code docstrings. ## Widgets These classes inherit from `anywidget.AnyWidget` and render interactive UI in notebooks: | Class | Description | | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | | [`MlflowChart`](https://daviddwlee84.github.io/mlflow-widgets/reference/chart/index.md) | Live-updating canvas line chart for MLflow metrics | | [`MlflowRunTable`](https://daviddwlee84.github.io/mlflow-widgets/reference/table/index.md) | Sortable HTML table of experiment runs | | [`MlflowParallelCoordinates`](https://daviddwlee84.github.io/mlflow-widgets/reference/parallel/index.md) | Parallel coordinates chart for hyperparameter comparison | ## Helpers | Class | Description | | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | | [`MlflowRunSelector`](https://daviddwlee84.github.io/mlflow-widgets/reference/selector/index.md) | Pure Python helper returning pandas DataFrames for run selection | ## Architecture All widgets follow the same data flow pattern: 1. **Python** fetches data from MLflow (via `MlflowClient` or REST API fallback with `urllib`) 1. Data is passed to **JavaScript** through traitlets (synced traits tagged with `sync=True`) 1. **JavaScript** handles rendering using vanilla DOM and Canvas APIs The `_do_refresh` integer traitlet is used for manual refresh signals (JS increments, Python observes). Auto-polling uses `threading.Timer` in Python. # MlflowChart ## MlflowChart ``` MlflowChart(*, tracking_uri: str | None = None, experiment_id: str | None = None, runs: Sequence[Any] | None = None, metric_key: str = '', poll_seconds: int | None = 5, smoothing_kind: str = 'gaussian', smoothing_param: float | None = None, show_slider: bool = True, x_axis: str = 'step', width: int = 700, height: int = 300, **kwargs: Any) ``` Bases: `AnyWidget` Live line chart that polls MLflow for metric data. Renders a Canvas-based chart that can auto-update while runs are active. Supports multiple runs for side-by-side comparison and optional rolling smoothing (rolling mean, exponential moving average, or gaussian). Examples: ``` import mlflow from mlflow_widgets import MlflowChart with mlflow.start_run() as run: for step in range(100): mlflow.log_metric("loss", 1.0 / (step + 1), step=step) chart = MlflowChart( tracking_uri="http://localhost:5000", runs=[run], metric_key="loss", ) chart ``` Create a MlflowChart widget. Parameters: | Name | Type | Description | Default | | ----------------- | --------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `tracking_uri` | \`str | None\` | MLflow tracking server URI. Defaults to MLFLOW_TRACKING_URI env var or http://localhost:5000. | | `experiment_id` | \`str | None\` | MLflow experiment ID. If provided and runs is not given, all runs in this experiment are charted. | | `runs` | \`Sequence[Any] | None\` | A list of MLflow Run objects, run-ID strings, or dicts with id and label keys. | | `metric_key` | `str` | The metric key to chart (e.g. "loss"). | `''` | | `poll_seconds` | \`int | None\` | Seconds between polling updates, or None to disable auto-polling and show a manual refresh button instead. | | `smoothing_kind` | `str` | "rolling", "exponential", or "gaussian". | `'gaussian'` | | `smoothing_param` | \`float | None\` | Smoothing parameter, or None for no smoothing. | | `show_slider` | `bool` | Whether to show the smoothing slider in the UI. | `True` | | `x_axis` | `str` | X-axis mode: "step", "wall", or "relative". | `'step'` | | `width` | `int` | Chart width in pixels. | `700` | | `height` | `int` | Chart height in pixels. | `300` | Source code in `src/mlflow_widgets/chart.py` ``` def __init__( self, *, tracking_uri: str | None = None, experiment_id: str | None = None, runs: Sequence[Any] | None = None, metric_key: str = "", poll_seconds: int | None = 5, smoothing_kind: str = "gaussian", smoothing_param: float | None = None, show_slider: bool = True, x_axis: str = "step", width: int = 700, height: int = 300, **kwargs: Any, ) -> None: """Create a MlflowChart widget. Args: tracking_uri: MLflow tracking server URI. Defaults to ``MLFLOW_TRACKING_URI`` env var or ``http://localhost:5000``. experiment_id: MLflow experiment ID. If provided and ``runs`` is not given, all runs in this experiment are charted. runs: A list of MLflow Run objects, run-ID strings, or dicts with ``id`` and ``label`` keys. metric_key: The metric key to chart (e.g. ``"loss"``). poll_seconds: Seconds between polling updates, or ``None`` to disable auto-polling and show a manual refresh button instead. smoothing_kind: ``"rolling"``, ``"exponential"``, or ``"gaussian"``. smoothing_param: Smoothing parameter, or ``None`` for no smoothing. show_slider: Whether to show the smoothing slider in the UI. x_axis: X-axis mode: ``"step"``, ``"wall"``, or ``"relative"``. width: Chart width in pixels. height: Chart height in pixels. """ self._tracking_uri = tracking_uri or os.environ.get( "MLFLOW_TRACKING_URI", "http://localhost:5000" ) self._experiment_id = experiment_id self._mlflow = _try_import_mlflow() self._client = None if self._mlflow is not None: self._client = self._mlflow.MlflowClient(self._tracking_uri) self._run_specs: list[dict] = [] if runs is not None: self._run_specs = self._normalize_runs(runs) elif experiment_id is not None: self._run_specs = self._discover_runs(experiment_id) self._poll_timer: threading.Timer | None = None self._poll_lock = threading.Lock() self._stopped = False super().__init__( metric_key=metric_key, poll_seconds=poll_seconds, smoothing_kind=smoothing_kind, smoothing_param=smoothing_param, show_slider=show_slider, x_axis=x_axis, width=width, height=height, **kwargs, ) if self._run_specs and metric_key: self.refresh() if poll_seconds is not None and poll_seconds > 0: self._schedule_poll() ``` ### refresh ``` refresh() -> None ``` Manually refresh metric data from MLflow. Source code in `src/mlflow_widgets/chart.py` ``` def refresh(self) -> None: """Manually refresh metric data from MLflow.""" try: series = self._fetch_series() total_points = sum(len(s["points"]) for s in series) now = datetime.now().strftime("%H:%M:%S") self._series_data = series self._status = f"{total_points} points across {len(series)} runs (updated {now})" still_running = any( r.get("status") in ("RUNNING", None) for r in self._run_specs ) if not still_running and self._poll_timer is not None: self._status += " — all runs finished" self.stop() except Exception as exc: self._status = f"Error: {exc}" ``` ### stop ``` stop() -> None ``` Stop auto-polling. Source code in `src/mlflow_widgets/chart.py` ``` def stop(self) -> None: """Stop auto-polling.""" self._stopped = True with self._poll_lock: if self._poll_timer is not None: self._poll_timer.cancel() self._poll_timer = None ``` ### close ``` close() -> None ``` Source code in `src/mlflow_widgets/chart.py` ``` def close(self) -> None: self.stop() super().close() ``` # MlflowParallelCoordinates ## MlflowParallelCoordinates ``` MlflowParallelCoordinates(*, tracking_uri: str | None = None, experiment_id: str | None = None, param_keys: Sequence[str] | None = None, metric_keys: Sequence[str] | None = None, color_by: str = 'status', width: int = 900, height: int = 400, **kwargs: Any) ``` Bases: `AnyWidget` Parallel coordinates chart for comparing runs across params/metrics. Each vertical axis is a parameter or metric. Each polyline is a run, colored by status. Status filter checkboxes let you show/hide run groups. Examples: ``` from mlflow_widgets import MlflowParallelCoordinates chart = MlflowParallelCoordinates( tracking_uri="http://localhost:5000", experiment_id="1", ) chart ``` Create a MlflowParallelCoordinates widget. Parameters: | Name | Type | Description | Default | | --------------- | --------------- | ----------------------------------------- | ---------------------------------------------------------- | | `tracking_uri` | \`str | None\` | MLflow tracking server URI. | | `experiment_id` | \`str | None\` | Experiment to fetch runs from. | | `param_keys` | \`Sequence[str] | None\` | Which parameters to show as axes (auto-discovers if None). | | `metric_keys` | \`Sequence[str] | None\` | Which metrics to show as axes (auto-discovers if None). | | `color_by` | `str` | Color lines by "status" or a metric name. | `'status'` | | `width` | `int` | Widget width in pixels. | `900` | | `height` | `int` | Widget height in pixels. | `400` | Source code in `src/mlflow_widgets/parallel.py` ``` def __init__( self, *, tracking_uri: str | None = None, experiment_id: str | None = None, param_keys: Sequence[str] | None = None, metric_keys: Sequence[str] | None = None, color_by: str = "status", width: int = 900, height: int = 400, **kwargs: Any, ) -> None: """Create a MlflowParallelCoordinates widget. Args: tracking_uri: MLflow tracking server URI. experiment_id: Experiment to fetch runs from. param_keys: Which parameters to show as axes (auto-discovers if None). metric_keys: Which metrics to show as axes (auto-discovers if None). color_by: Color lines by ``"status"`` or a metric name. width: Widget width in pixels. height: Widget height in pixels. """ self._tracking_uri = tracking_uri or os.environ.get( "MLFLOW_TRACKING_URI", "http://localhost:5000" ) self._experiment_id = experiment_id self._param_keys = param_keys self._metric_keys = metric_keys self._mlflow = _try_import_mlflow() self._client = None if self._mlflow is not None: self._client = self._mlflow.MlflowClient(self._tracking_uri) super().__init__(color_by=color_by, width=width, height=height, **kwargs) if experiment_id: self.refresh() ``` ### refresh ``` refresh() -> None ``` Fetch run data and rebuild axes/run data. Source code in `src/mlflow_widgets/parallel.py` ``` def refresh(self) -> None: """Fetch run data and rebuild axes/run data.""" if not self._experiment_id: self._status = "No experiment_id set" return try: if self._client is not None: rows = _search_runs_detailed_client(self._client, self._experiment_id) else: rows = _search_runs_detailed_rest( self._tracking_uri, self._experiment_id ) axes, runs_data = _build_axes_and_runs( rows, self._param_keys, self._metric_keys ) now = datetime.now().strftime("%H:%M:%S") self._axes_data = axes self._runs_data = runs_data self._status = f"{len(runs_data)} runs, {len(axes)} axes (updated {now})" except Exception as exc: self._status = f"Error: {exc}" ``` ### close ``` close() -> None ``` Source code in `src/mlflow_widgets/parallel.py` ``` def close(self) -> None: super().close() ``` # MlflowRunSelector ## MlflowRunSelector ``` MlflowRunSelector(*, tracking_uri: str | None = None, experiment_id: str | None = None) ``` Fetch MLflow experiment runs and present them as a pandas DataFrame. Designed to work with `marimo.ui.table(df, selection="multi")` so users can select runs via checkboxes, then pass the selected run IDs to `MlflowChart` for visualization. Examples: ``` import marimo as mo from mlflow_widgets import MlflowRunSelector, MlflowChart selector = MlflowRunSelector( tracking_uri="http://localhost:5000", experiment_id="1", ) table = mo.ui.table(selector.to_dataframe(), selection="multi") # In another cell: selected_ids = MlflowRunSelector.get_run_ids(table.value) chart = MlflowChart(runs=selected_ids, metric_key="loss") ``` Create a MlflowRunSelector. Parameters: | Name | Type | Description | Default | | --------------- | ----- | ----------- | --------------------------------------------------------------------------------------------- | | `tracking_uri` | \`str | None\` | MLflow tracking server URI. Defaults to MLFLOW_TRACKING_URI env var or http://localhost:5000. | | `experiment_id` | \`str | None\` | MLflow experiment ID to fetch runs from. | Source code in `src/mlflow_widgets/selector.py` ``` def __init__( self, *, tracking_uri: str | None = None, experiment_id: str | None = None, ) -> None: """Create a MlflowRunSelector. Args: tracking_uri: MLflow tracking server URI. Defaults to ``MLFLOW_TRACKING_URI`` env var or ``http://localhost:5000``. experiment_id: MLflow experiment ID to fetch runs from. """ self._tracking_uri = tracking_uri or os.environ.get( "MLFLOW_TRACKING_URI", "http://localhost:5000" ) self._experiment_id = experiment_id self._mlflow = _try_import_mlflow() self._client = None if self._mlflow is not None: self._client = self._mlflow.MlflowClient(self._tracking_uri) self._raw_rows: list[dict] = [] if experiment_id: self.refresh() ``` ### tracking_uri ``` tracking_uri: str ``` ### experiment_id ``` experiment_id: str | None ``` ### refresh ``` refresh() -> MlflowRunSelector ``` Re-fetch runs from MLflow. Returns self for chaining. Source code in `src/mlflow_widgets/selector.py` ``` def refresh(self) -> MlflowRunSelector: """Re-fetch runs from MLflow. Returns self for chaining.""" if not self._experiment_id: self._raw_rows = [] return self if self._client is not None: self._raw_rows = _search_runs_detailed_client( self._client, self._experiment_id ) else: self._raw_rows = _search_runs_detailed_rest( self._tracking_uri, self._experiment_id ) return self ``` ### to_dataframe ``` to_dataframe() -> Any ``` Return runs as a pandas DataFrame. Columns: run_id, run_name, status, start_time, duration_s, plus `param.` for each parameter and `metric.` for each final metric value. Raises: | Type | Description | | ------------- | --------------------------- | | `ImportError` | If pandas is not installed. | Source code in `src/mlflow_widgets/selector.py` ``` def to_dataframe(self) -> Any: """Return runs as a pandas DataFrame. Columns: run_id, run_name, status, start_time, duration_s, plus ``param.`` for each parameter and ``metric.`` for each final metric value. Raises: ImportError: If pandas is not installed. """ try: import pandas as pd except ImportError as err: raise ImportError( "pandas is required for to_dataframe(). " "Install it with: pip install pandas" ) from err rows = [] for r in self._raw_rows: row: dict[str, Any] = { "run_id": r["run_id"], "run_name": r["run_name"], "status": r["status"], "parent_run_id": r.get("parent_run_id"), "start_time": ( datetime.fromtimestamp(r["start_time"] / 1000.0).strftime( "%Y-%m-%d %H:%M:%S" ) if r["start_time"] else None ), "duration_s": round(r["duration_s"], 1) if r["duration_s"] else None, } for k, v in r.get("params", {}).items(): row[f"param.{k}"] = v for k, v in r.get("metrics", {}).items(): row[f"metric.{k}"] = v rows.append(row) return pd.DataFrame(rows) ``` ### get_run_ids ``` get_run_ids(selection: Any) -> list[str] ``` Extract run_id list from a DataFrame, list of dicts, or similar. Works with `mo.ui.table(...).value` which returns a list of dicts or a DataFrame depending on the input format. Parameters: | Name | Type | Description | Default | | ----------- | ----- | ----------------------------------------------------------------------------------- | ---------- | | `selection` | `Any` | The .value from a mo.ui.table, typically a list of row dicts or a pandas DataFrame. | *required* | Returns: | Type | Description | | ----------- | ----------------------- | | `list[str]` | List of run_id strings. | Source code in `src/mlflow_widgets/selector.py` ``` @staticmethod def get_run_ids( selection: Any, ) -> list[str]: """Extract run_id list from a DataFrame, list of dicts, or similar. Works with ``mo.ui.table(...).value`` which returns a list of dicts or a DataFrame depending on the input format. Args: selection: The ``.value`` from a ``mo.ui.table``, typically a list of row dicts or a pandas DataFrame. Returns: List of run_id strings. """ if hasattr(selection, "to_dict"): # pandas DataFrame if "run_id" in selection.columns: return selection["run_id"].tolist() return [] if isinstance(selection, list): return [ r["run_id"] for r in selection if isinstance(r, dict) and "run_id" in r ] return [] ``` # MlflowRunTable ## MlflowRunTable ``` MlflowRunTable(*, tracking_uri: str | None = None, experiment_id: str | None = None, poll_seconds: int | None = None, width: int = 900, **kwargs: Any) ``` Bases: `AnyWidget` Interactive table showing MLflow experiment runs. Displays run name, status, start time, duration, parameters, and final metric values in a sortable HTML table. Examples: ``` from mlflow_widgets import MlflowRunTable table = MlflowRunTable( tracking_uri="http://localhost:5000", experiment_id="1", ) table ``` Create a MlflowRunTable widget. Parameters: | Name | Type | Description | Default | | --------------- | ----- | ---------------------- | ----------------------------------------------------------------------------------------------- | | `tracking_uri` | \`str | None\` | MLflow tracking server URI. Defaults to MLFLOW_TRACKING_URI env var or http://localhost:5000. | | `experiment_id` | \`str | None\` | MLflow experiment ID to display runs for. | | `poll_seconds` | \`int | None\` | Seconds between auto-refresh, or None to disable auto-polling and show a manual refresh button. | | `width` | `int` | Table width in pixels. | `900` | Source code in `src/mlflow_widgets/table.py` ``` def __init__( self, *, tracking_uri: str | None = None, experiment_id: str | None = None, poll_seconds: int | None = None, width: int = 900, **kwargs: Any, ) -> None: """Create a MlflowRunTable widget. Args: tracking_uri: MLflow tracking server URI. Defaults to ``MLFLOW_TRACKING_URI`` env var or ``http://localhost:5000``. experiment_id: MLflow experiment ID to display runs for. poll_seconds: Seconds between auto-refresh, or ``None`` to disable auto-polling and show a manual refresh button. width: Table width in pixels. """ self._tracking_uri = tracking_uri or os.environ.get( "MLFLOW_TRACKING_URI", "http://localhost:5000" ) self._experiment_id = experiment_id self._mlflow = _try_import_mlflow() self._client = None if self._mlflow is not None: self._client = self._mlflow.MlflowClient(self._tracking_uri) self._poll_timer: threading.Timer | None = None self._poll_lock = threading.Lock() self._stopped = False super().__init__(poll_seconds=poll_seconds, width=width, **kwargs) if experiment_id: self.refresh() if poll_seconds is not None and poll_seconds > 0: self._schedule_poll() ``` ### refresh ``` refresh() -> None ``` Fetch run data from MLflow and update the table. Source code in `src/mlflow_widgets/table.py` ``` def refresh(self) -> None: """Fetch run data from MLflow and update the table.""" if not self._experiment_id: self._status = "No experiment_id set" return try: if self._client is not None: rows = _search_runs_detailed_client(self._client, self._experiment_id) else: rows = _search_runs_detailed_rest(self._tracking_uri, self._experiment_id) all_param_keys: set[str] = set() all_metric_keys: set[str] = set() for row in rows: all_param_keys.update(row["params"].keys()) all_metric_keys.update(row["metrics"].keys()) now = datetime.now().strftime("%H:%M:%S") self._table_data = rows self._param_keys = sorted(all_param_keys) self._metric_keys = sorted(all_metric_keys) self._status = f"{len(rows)} runs (updated {now})" except Exception as exc: self._status = f"Error: {exc}" ``` ### stop ``` stop() -> None ``` Stop auto-polling. Source code in `src/mlflow_widgets/table.py` ``` def stop(self) -> None: """Stop auto-polling.""" self._stopped = True with self._poll_lock: if self._poll_timer is not None: self._poll_timer.cancel() self._poll_timer = None ``` ### close ``` close() -> None ``` Source code in `src/mlflow_widgets/table.py` ``` def close(self) -> None: self.stop() super().close() ``` # Changelog # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/daviddwlee84/mlflow-widgets/compare/v0.1.0...HEAD) ### Added - Documentation site with MkDocs Material + mkdocstrings, deployed to GitHub Pages - LLM-friendly docs output: `/llms.txt`, `/llms-full.txt`, and per-page `.md` files via `mkdocs-llmstxt` - "Copy to LLM" buttons on every docs page (copy as Markdown, open in ChatGPT/Claude) via `mkdocs-copy-to-llm` - `docs` optional dependency group (`mkdocs-material`, `mkdocstrings[python]`, `mkdocs-llmstxt`, `mkdocs-copy-to-llm`) - `docs` and `docs-build` taskipy tasks - GitHub Actions workflow for automatic docs deployment on push to main - Expanded README badge reference and usage notes in `notes/readme-badges.md` - Example index in `examples/README.md` with a Markdown table, per-demo descriptions, and Open in MoLab badges ### Fixed - Incorrect import in `MlflowChart` docstring example (`mlflow_chart` -> `mlflow_widgets`) - README demo list now includes `combo_demo.py` and links to the example index ### Changed - README top badge row now uses dynamic `CI`, `Docs`, `PyPI`, `Python`, and `License` badges ## [0.1.0](https://github.com/daviddwlee84/mlflow-widgets/releases/tag/v0.1.0) - 2026-04-01 First public release on [PyPI](https://pypi.org/project/mlflow-widgets/). ### Added - **MlflowChart** -- live-updating canvas-based line chart for MLflow metrics - Multiple run comparison with color-coded lines - X-axis modes: step, wall time, relative time - Smoothing: rolling mean, exponential moving average, gaussian - Interactive clickable legend with per-series toggle - Hover tooltips with exact values - Auto-polling with configurable interval - **MlflowRunTable** -- sortable HTML table showing experiment runs - Columns: run name, status, start time, duration, params, metrics - Nested run tree view (parent/child collapsible groups) - Status badges with color coding - Auto-polling support - **MlflowParallelCoordinates** -- parallel coordinates chart for hyperparameter comparison - Each axis is a parameter or metric; each polyline is a run - Color modes: by status, per run, by parameter value, by metric gradient - Status filtering checkboxes - Parameter/metric key filtering - **MlflowRunSelector** -- pure Python helper returning pandas DataFrames - Works with `marimo.ui.table` for interactive run selection - `get_run_ids()` static method extracts IDs from DataFrame or list - Hybrid architecture: Python fetches data (MlflowClient or REST fallback), JS renders - Works in Jupyter, Marimo, and any notebook supporting anywidget - `py.typed` marker for type checker support - 6 Marimo demo notebooks in `examples/` - CI with GitHub Actions (Python 3.10-3.13 matrix) - Automated publishing via Trusted Publishing (OIDC)