Skip to content

layout: polars node/edge frames — 6 layouts bare-crash (AttributeError), 3 silently return pandas; engine='pandas' is not honoured #1966

Description

@lmeyerov

Found by the 2026-08 layouts/viz amplification probe. Repros on master @ 42b4f7f7d, polars 1.42.0 / pandas 2.3.3 / py3.12.

Engine.resolve_input_engine deliberately maps polars → Engine.PANDAS for input-consuming surfaces, and its docstring names layouts as one of them (graphistry/Engine.py:60-73). That policy is fine. The problem is that the layout surface implements it three different ways, and only one of them works.

The matrix

Same graph, polars _nodes and _edges, engine='auto' (the default), one node column of each needed type:

layout result on polars frames
circle_layout(bounding_box=..) silently returns pandas (both _nodes and _edges converted)
tree_layout() silently returns pandas
modularity_weighted_layout() silently returns pandas
layout_igraph('fr') AttributeError: 'Int64' object has no attribute 'name'
group_in_a_box_layout() ValueError: Could not infer engine, please specify
ring_categorical_layout('cat') AttributeError: 'DataFrame' object has no attribute 'reset_index'
ring_continuous_layout('v') AttributeError: 'DataFrame' object has no attribute 'reset_index'
time_ring_layout('t') AttributeError: 'DataFrame' object has no attribute 'reset_index'
mercator_layout() AttributeError: 'Series' object has no attribute 'values'

Repro

import pandas as pd, polars as pl, graphistry

E = pd.DataFrame({'s':[0,1,2], 'd':[1,2,0]})
N = pd.DataFrame({'id':[0,1,2], 'cat':['a','b','c'], 'v':[1.,2.,3.],
                  't': pd.to_datetime(['2020-01-01','2021-01-01','2022-01-01']),
                  'lat':[10.,20.,30.], 'lon':[1.,2.,3.]})
g = (graphistry.edges(pl.from_pandas(E),'s','d').nodes(pl.from_pandas(N),'id')
        .bind(point_latitude='lat', point_longitude='lon'))

g.circle_layout(bounding_box=(0,0,10,10))._nodes   # -> pandas.DataFrame, no warning
g.ring_continuous_layout('v')                      # -> AttributeError

Defect 1 (bare-crash): six layouts crash with an untyped AttributeError / generic ValueError

The three ring layouts do call resolve_input_engine and correctly get Engine.PANDAS — they just never convert the frame, and they call reset_index(drop=True) on it before the resolve even runs:

  • graphistry/layout/ring/continuous.py:183 then :185
  • graphistry/layout/ring/categorical.py:171 then :173
  • graphistry/layout/ring/time.py:336 then :338

mercator.py:73 never consults an engine at all — it does is_not_pandas = not isinstance(g._nodes, pd.DataFrame), which classifies a polars frame as "cuDF" and routes it into the cupy branch, where .values does not exist (:93-94).

gib.py:138-149 sniffs self._edges for pandas-or-cudf only and raises ValueError('Could not infer engine, please specify') for polars — but there is no engine= value that helps, since partition.py:56-57 rejects everything that is not Engine.PANDAS/Engine.CUDF.

Explicit engines do not rescue any of it:

ring_continuous_layout('v', engine='pandas')   -> AttributeError: 'DataFrame' object has no attribute 'reset_index'
ring_categorical_layout('cat', engine='pandas')-> AttributeError: 'DataFrame' object has no attribute 'reset_index'
ring_continuous_layout('v', engine='polars')   -> AttributeError: 'DataFrame' object has no attribute 'reset_index'
circle_layout(bb, engine='polars')             -> ValueError: Only engines pandas/cudf supported, got: Engine.POLARS

engine='pandas' on a polars graph is the exact call a user makes after reading the coercion policy, and it still dies. Only circle_layout honours it.

Expected: either convert the frame the way circle_layout/tree_layout do, or decline with a typed error naming polars (the circle_layout(engine='polars') message Only engines pandas/cudf supported, got: Engine.POLARS is the right shape; the AttributeErrors are not).

Defect 2 (surface-divergence, undocumented): the three that do work silently change the caller's frame type

circle_layout, tree_layout and modularity_weighted_layout return a Plottable whose _nodes and _edges are now pandas.DataFrame. Nothing warns, and the layout docstrings say nothing about it.

docs/source/gfql/engines.rst:48-63 documents this coercion carefully for GFQL ("a graph built from polars.DataFrame is silently coerced to pandas … Result frames match the engine") and tells users to pass engine='polars' to stay native. There is no equivalent statement for layouts, and per Defect 1 there is no engine= value that keeps a layout native. So a documented "pass engine='polars' to stay native" workflow silently loses its frame type the moment a layout is composed in:

out = g.gfql(query, engine='polars')     # documented: out._nodes is polars
out = out.circle_layout(bounding_box=(0,0,10,10))
type(out._nodes)                          # pandas.DataFrame  -- silently

This is the same shape as #1824 (polars-gpu silently serving CPU) but a different axis — frame-type coercion on the layout surface rather than execution placement — so it is filed separately rather than as a comment there.

Suggested resolution (not a fix request, just scoping)

Pick one policy and make all ten entry points obey it:

  • (a) all layouts call df_to_engine(..., Engine.PANDAS) on entry when resolve_input_engine says pandas, and document "layouts return pandas frames"; or
  • (b) all layouts decline polars with a typed error until a native path exists.

Either is defensible. The current split — 3 coerce, 6 bare-crash, and engine='pandas' not honoured — is not.

Category

bare-crash on valid input (6 layouts) + surface-divergence / undocumented silent coercion (3 layouts).

Not exercised

No cuDF arm: cupy in this environment cannot load libnvrtc.so.12 and cugraph is absent, so the GPU column of this matrix is untested. layout_igraph('fr') on cuDF frames was verified working (returns cuDF), so the igraph plugin's polars failure above is polars-specific, not a general non-pandas failure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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