Skip to content

Generate the public API/docs inventory and wire make check-docs to real docs checks #444

Description

@Alek99

Summary

XY's public surface has outgrown both its API-reference prose and its stability checks. The hard-coded declarative inventory validates only a subset of shipped marks/factories and omits newer Chart methods; the type-surface test duplicates that stale list. Separately, make check-docs is an alias of make check-examples, so the documented local docs gate does not run the docs app or docs quickstart at all.

Evidence

  • Both Make targets invoke verify_local.py --only examples:

    xy/Makefile

    Lines 76 to 80 in 99eda6d

    check-docs:
    $(PYTHON) scripts/verify_local.py --only examples
    check-examples:
    $(PYTHON) scripts/verify_local.py --only examples
  • Real docs CI runs docs-app tests plus verify_docs_quickstart.py, while that script itself executes only first-chart.md:
    - name: Install docs dependencies
    run: uv sync --project docs/app --frozen --group dev
    - name: Run docs tests
    run: uv run --project docs/app --no-sync pytest docs/app/tests -v
    - name: Run public docs quickstart against the checkout
    run: uv run --project docs/app --no-sync python scripts/verify_docs_quickstart.py
    and
    REPO_ROOT = Path(__file__).resolve().parents[1]
    SOURCE_PACKAGE = (REPO_ROOT / "python" / "xy").resolve()
    QUICKSTART_PAGES = (("first-chart page", REPO_ROOT / "docs" / "overview" / "first-chart.md"),)
    PYTHON_FENCE_RE = re.compile(
    r"^~~~python[^\n]*\n(?P<code>.*?)^~~~\s*$",
  • components.__all__ exposes the broader component surface:

    xy/python/xy/components.py

    Lines 64 to 155 in 99eda6d

    __all__ = [
    "CHART_DOM_SLOTS",
    "Animation",
    "Annotation",
    "Axis",
    "Chart",
    "Colorbar",
    "Component",
    "ExportConfig",
    "FacetChart",
    "Interaction",
    "Legend",
    "Mark",
    "Modebar",
    "Spring",
    "Theme",
    "Tooltip",
    "animation",
    "area",
    "area_chart",
    "arrow",
    "bar",
    "bar_chart",
    "box",
    "box_chart",
    "callout",
    "chart",
    "colorbar",
    "column",
    "column_chart",
    "contour",
    "contour_chart",
    "ecdf",
    "ecdf_chart",
    "error_band",
    "error_band_chart",
    "errorbar",
    "errorbar_chart",
    "export_config",
    "facet_chart",
    "heatmap",
    "heatmap_chart",
    "hexbin",
    "hexbin_chart",
    "hist",
    "histogram",
    "histogram_chart",
    "hline",
    "interaction_config",
    "label",
    "legend",
    "line",
    "line_chart",
    "mark",
    "marker",
    "modebar",
    "pie_chart",
    "polar_bar_chart",
    "polar_chart",
    "r_axis",
    "radar_chart",
    "ribbon",
    "sankey",
    "sankey_chart",
    "scatter",
    "scatter_chart",
    "segments",
    "segments_chart",
    "spring",
    "stairs",
    "stairs_chart",
    "stem",
    "stem_chart",
    "step",
    "step_chart",
    "text",
    "theme",
    "theta_axis",
    "threshold",
    "threshold_zone",
    "tooltip",
    "triangle_mesh",
    "triangle_mesh_chart",
    "violin",
    "violin_chart",
    "vline",
    "wind_rose",
    "x_axis",
    "x_band",
    "y_axis",
    "y_band",
    ]
  • check_public_api.py manually lists only a subset and validates only those names/methods:
    DECLARATIVE_MARK_EXPORTS = (
    "scatter",
    "segments",
    "triangle_mesh",
    "line",
    "area",
    "histogram",
    "hist",
    "bar",
    "column",
    "heatmap",
    )
    DECLARATIVE_ANNOTATION_EXPORTS = (
    "arrow",
    "callout",
    "label",
    "marker",
    "threshold",
    "threshold_zone",
    "vline",
    "hline",
    "x_band",
    "y_band",
    "text",
    )
    DECLARATIVE_AXIS_EXPORTS = ("x_axis", "y_axis", "theta_axis", "r_axis")
    DECLARATIVE_CHROME_EXPORTS = (
    "legend",
    "tooltip",
    "colorbar",
    "modebar",
    "theme",
    "interaction_config",
    )
    DECLARATIVE_CHART_EXPORTS = (
    "chart",
    "scatter_chart",
    "polar_chart",
    "radar_chart",
    "polar_bar_chart",
    "pie_chart",
    "wind_rose",
    "segments_chart",
    "triangle_mesh_chart",
    "line_chart",
    "area_chart",
    "histogram_chart",
    "bar_chart",
    "column_chart",
    "heatmap_chart",
    )
    DECLARATIVE_CHART_READOUTS = (
    "figure",
    "widget",
    "show",
    "to_html",
    "html",
    "_repr_html_",
    "to_svg",
    "to_png",
    "memory_report",
    "chrome_components",
    "reflex_components",
    "append",
    "pick",
    "select_range",
    )
    and
    for name in DECLARATIVE_API_EXPORTS:
    if name not in public_names:
    errors.append(f"declarative API export {name!r} is missing from xy.__all__")
    if exports.get(name) != ".components":
    errors.append(
    f"declarative API export {name!r} must map to '.components', "
    f"got {exports.get(name)!r}"
    )
    if name not in component_names:
    errors.append(
    f"declarative API export {name!r} is missing from "
    f"{components_module.__name__}.__all__"
    )
    if not hasattr(components_module, name):
    errors.append(
    f"declarative API export {name!r} is undefined in {components_module.__name__}"
    )
    chart_class = getattr(components_module, "Chart", None)
    if chart_class is None:
    errors.append(f"{components_module.__name__}.Chart is missing")
    return errors
    for method in DECLARATIVE_CHART_READOUTS:
    value = getattr(chart_class, method, None)
    if not callable(value):
    errors.append(f"declarative Chart readout {method!r} must be callable")
  • Shipped Chart has set_view, reset_view, select, clear_selection, view_state, to_image, and write_image:

    xy/python/xy/components.py

    Lines 4100 to 4128 in 99eda6d

    # -- programmatic view state (kernel-connected; view-state.md §5.1) ------
    def set_view(self, ranges: Any = None, *, animate: bool = True, history: bool = True) -> None:
    """Apply a partial per-axis ranges patch (the write-side mirror of the
    ``on_view_change`` payload) through the client's clamped mutation path."""
    self.widget().set_view(ranges, animate=animate, history=history)
    def reset_view(self, axes: Any = None) -> None:
    """Navigate to the home ranges (None = the configured reset_axes)."""
    self.widget().reset_view(axes)
    def select(
    self,
    *,
    range: Any = None,
    polygon: Any = None,
    rows: Any = None,
    history: bool = True,
    ) -> None:
    """Programmatic selection; see `FigureWidget.select`."""
    self.widget().select(range=range, polygon=polygon, rows=rows, history=history)
    def clear_selection(self) -> None:
    """Clear any selection."""
    self.widget().clear_selection()
    def view_state(self) -> dict[str, Any]:
    """Last committed durable view state (kernel-side cache)."""
    return self.widget().view_state()
    and

    xy/python/xy/components.py

    Lines 4246 to 4304 in 99eda6d

    def to_image(
    self,
    format: str = "png",
    *,
    width: Optional[int] = None,
    height: Optional[int] = None,
    scale: Optional[float] = None,
    background: Optional[str] = None,
    engine: export.Engine | str = export.Engine.auto,
    quality: Optional[int] = None,
    optimize: bool = False,
    custom_css: Optional[str] = None,
    sandbox: bool = True,
    gl: str = "software",
    ) -> bytes:
    """Unified static export: PNG/JPEG/WebP/SVG/PDF bytes.
    Omitted width/height/scale/background/quality fall back to the
    chart's `export_config` defaults; explicit arguments override them.
    See `export.to_image` for the full format/engine/background policy."""
    fmt = export._normalize_format(format)
    resolved = export._resolve_image_engine(engine, fmt, custom_css)
    return self.figure().to_image(
    format,
    engine=engine,
    optimize=optimize,
    custom_css=custom_css,
    sandbox=sandbox,
    gl=gl,
    **self._export_defaults(
    fmt,
    width,
    height,
    scale,
    background,
    quality,
    lossy_webp=resolved == "browser",
    ),
    )
    def write_image(
    self,
    path: str | PathLike[str],
    *,
    format: Optional[str] = None,
    width: Optional[int] = None,
    height: Optional[int] = None,
    scale: Optional[float] = None,
    background: Optional[str] = None,
    engine: export.Engine | str = export.Engine.auto,
    quality: Optional[int] = None,
    optimize: bool = False,
    custom_css: Optional[str] = None,
    sandbox: bool = True,
    gl: str = "software",
    ) -> bytes:
    """Atomic file export with extension-inferred format (.png/.jpg/
    .jpeg/.webp/.svg/.pdf/.html). `export_config` defaults apply as in
    `to_image`; explicit arguments override them."""
    . The data-readout reference lists only four older readouts:
    ## Data Readout and Mutation
    ~~~python
    report: dict = chart.memory_report()
    chart.append(trace_id, x, y, color=None, size=None)
    row: dict | None = chart.pick(trace_id, index)
    selection: xy.Selection = chart.select_range(x0, x1, y0, y1, trace_id=None)
    ~~~
    - `memory_report()` describes canonical, derived, and payload allocations.
    - `append()` extends supported scatter or line traces. It mutates chart data,
    not structure; already-exported HTML files remain snapshots.
    - `pick()` translates a shipped vertex index to an exact canonical row when
    possible and returns `None` for an invalid index.
    - `select_range()` performs a Python-side box selection over scatter traces.
  • Selection.rows() ships but is absent from the documented Selection list:

    xy/python/xy/_figure.py

    Lines 72 to 87 in 99eda6d

    def xy(self, trace_id: int = 0) -> tuple[np.ndarray, np.ndarray]:
    """(x, y) f64 arrays for the selected points of a trace (from canonical)."""
    t = interaction._trace(self._figure, trace_id)
    idx = self.per_trace.get(t.id)
    if idx is None:
    return np.empty(0), np.empty(0)
    return t.x.values[idx], t.y.values[idx]
    def rows(self, limit: int | None = None) -> list[dict[str, Any]]:
    """Return deterministic JSON rows based on canonical indices.
    Traces and their indices are ordered ascending. ``limit`` bounds the
    projection without changing the complete selection held by this object.
    """
    rows, _ = interaction.selection_rows(self._figure, self.per_trace, limit)
    return rows
    and
    ## Selection
    `Selection` exposes:
    - `per_trace``{trace_id: numpy_uint32_indices}` in canonical row space.
    - `index` — all selected indices concatenated; use `per_trace` when trace
    identity matters.
    - `xy(trace_id=0)` — canonical f64 x/y arrays for one selected trace.
    - `len(selection)` — total selected rows across traces.
    Clearing selection delivers an empty `Selection`. `Chart.select_range()`
    returns the same type without requiring a browser gesture.

Acceptance criteria

  • Use one machine-readable source of truth for supported public exports, chart factories, and public methods; generate or validate docs/test inventories from it.
  • Explicitly classify experimental/deprecated/private names rather than silently omitting them.
  • Document all supported Chart view/export methods and Selection.rows().
  • Remove duplicate hand-maintained API lists from tests.
  • Make make check-docs run the checkout-backed docs tests/quickstarts it advertises; keep check-examples separate.
  • A fixture that adds a public export/method without classification or reference coverage must fail.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions