Architecture#
This page describes how Gufo’s internal classes interact — the composition relationships, rendering pipeline, and data flow.
Class Diagram#
classDiagram
direction TB
class Chart {
-_data: Any
-_layers: list~Layer~
-_canvas: Canvas
-_theme_override: Theme | str
-_palette: list | None
-_facet_column: str
-_facet_row: str
-_apply_funcs: list
+scatter(x, y, **kw) Chart
+line(x, y, **kw) Chart
+bar(x, y, **kw) Chart
+histogram(x, **kw) Chart
+kdeplot(x, **kw) Chart
+boxplot(x, y, **kw) Chart
+violin(x, y, **kw) Chart
+heatmap(**kw) Chart
+area(x, y, **kw) Chart
+strip(x, y, **kw) Chart
+swarm(x, y, **kw) Chart
+countplot(x, **kw) Chart
+ecdf(x, **kw) Chart
+rug(x, **kw) Chart
+pointplot(x, y, **kw) Chart
+title(text) Chart
+xlabel(text) Chart
+ylabel(text) Chart
+legend(**kw) Chart
+theme(name_or_theme) Chart
+palette(colors) Chart
+facet(column, **kw) Chart
+apply(func) Chart
+size(w, h) Chart
+show()
+save(path)
-_render() tuple
-_render_onto(fig, axes)
-_apply_decorators(fig, axes)
}
class Grid {
-_rows: int
-_cols: int
-_panels: dict~tuple, Chart~
-_title: str
-_theme_override: Theme | str
-_apply_funcs: list
+__setitem__(idx, chart)
+title(text) Grid
+theme(name_or_theme) Grid
+apply(func) Grid
+show()
+save(path)
-_render() tuple
}
class Layer {
<<dataclass>>
+mark_type: str
+x: Any
+y: Any
+encodings: dict
+kwargs: dict
+palette: list | None
}
class Canvas {
-_figsize: tuple
-_figure: Figure
-_axes: Axes
+build() tuple
+from_existing(fig, axes)$ Canvas
}
class DataAdapter {
-_data: Any
-_type: str
+raw_data: Any
+data_type: str
+from_any(data)$ DataAdapter
+resolve(key) ndarray
+subset(mask) DataAdapter
+column_names() list
}
class Theme {
-_name: str
-_rc: dict
+name: str
+merge(overrides) Theme
+rename(name) Theme
+as_context() contextmanager
+apply_global()
}
class Palette {
<<dataclass>>
+categorical: list~str~
+sequential: list~str~
+diverging: list~str~
}
class Regression {
<<dataclass>>
+degree: int
+color: str
+linestyle: str
+linewidth: float
+render(x, y, axes)
}
class KDE {
<<dataclass>>
+bw_method: Any
+color: str
+fill: bool
+n_points: int
+render(x, axes)
}
class Lowess {
<<dataclass>>
+frac: float
+color: str
+linestyle: str
+render(x, y, axes)
}
Chart "1" *-- "1" Canvas : _canvas
Chart "1" *-- "0..*" Layer : _layers
Chart ..> DataAdapter : creates at render time
Chart ..> Theme : resolves via _resolve_theme()
Chart ..> Palette : resolves via resolve_palette()
Grid "1" *-- "0..*" Chart : _panels
Grid ..> Theme : optional override
Layer "0..1" o-- Regression : encodings["fit"]
Layer "0..1" o-- KDE : encodings["kde"]
Layer "0..1" o-- Lowess : encodings["fit"]
Rendering Pipeline#
flowchart TD
A["chart.show() / chart.save()"] --> B["Chart._render()"]
B --> C{Faceted?}
C -- Yes --> D["render_facet()"]
C -- No --> E["_resolve_theme()"]
D --> D1["DataAdapter.from_any(data)"]
D1 --> D2["Split data by facet column"]
D2 --> D3["plt.subplots(rows, cols)"]
D3 --> D4["For each category:<br/>adapter.subset(mask)"]
D4 --> D5["chart._render_onto(fig, axes, sub_adapter)"]
D5 --> D6["Shared legend / colorbar"]
E --> F["theme.as_context()"]
F --> G["Canvas.build() → fig, axes"]
G --> H["Chart._render_onto(fig, axes)"]
H --> I["DataAdapter.from_any(data)"]
I --> J["resolve_palette()"]
J --> K["For each Layer"]
K --> L["render_layer(layer, adapter, axes)"]
L --> M["_REGISTRY lookup by mark_type"]
M --> N["mark.render(layer, adapter, axes)"]
N --> N1["adapter.resolve(x) → numpy"]
N --> N2["adapter.resolve(y) → numpy"]
N --> N3["axes.scatter / .plot / .bar / ..."]
N --> N4{"fit= or kde= overlay?"}
N4 -- Yes --> N5["overlay.render(x, y, axes)"]
K --> O["_apply_decorators(fig, axes)"]
O --> P["title, labels, legend,<br/>limits, scales, ticks,<br/>references, annotations"]
P --> Q["Execute apply(func) callbacks"]
style A fill:#4C72B0,color:#fff
style H fill:#55A868,color:#fff
style N fill:#DD8452,color:#fff
style O fill:#8172B3,color:#fff
Data Flow#
DataAdapter is the single resolution point for all input types. Marks never
receive raw DataFrames — they always go through adapter.resolve(key).
flowchart TD
A["User data"] --> B["DataAdapter.from_any(data)"]
B --> C{"_detect_type()"}
C --> C1["pandas DataFrame"]
C --> C2["polars DataFrame"]
C --> C3["dict"]
C --> C4["None (raw arrays)"]
B --> D["adapter.resolve(key)"]
D --> E{"key type?"}
E -- "str" --> F["Column lookup → numpy array"]
E -- "list of str" --> G["Wide-form → list of arrays"]
E -- "array-like" --> H["np.asarray() passthrough"]
E -- "None" --> I["None"]
F --> J["Mark renderer"]
G --> J
H --> J
Layout Relationships#
flowchart TD
subgraph "Entry Points"
A1["gufo.grid(rows, cols)"] --> G["Grid"]
A2["gufo.pairplot(data, ...)"] --> G
A3["gufo.jointplot(data, x, y)"] --> G
end
G --> |"__setitem__"| P["Chart panels"]
G --> |"_render()"| S["plt.subplots(rows, cols)"]
S --> |"For each panel"| R["chart._render_onto(fig, axes[r,c])"]
subgraph "Faceting (inside Chart)"
F1["chart.facet(column)"] --> F2["render_facet()"]
F2 --> F3["Split data by category"]
F3 --> F4["chart._render_onto(fig, axes, sub_adapter)"]
end
Color Resolution#
flowchart TD
A["enc.get('color')"] --> B{"resolve_color()"}
B -- "None" --> C["Use palette defaults"]
B -- "Column name" --> D["adapter.resolve() → array"]
B -- "Literal color" --> E["Apply to all points"]
D --> F{"Array type?"}
F -- "Categorical" --> G["iter_color_groups()<br/>One color per category"]
F -- "Continuous numeric" --> H["Colormap + colorbar<br/>(vmin, vmax, cmap)"]