qeviz
qeviz — Python adapter for the qeviz visualization library.
Converts pandas DataFrames to the qeviz ModelData format and provides a chainable plot object for Jupyter display and standalone HTML export.
Quick start
::
import qeviz
# From DataFrames
p = qeviz.plot(nodes_df, edges_df, points_df, group_col="Condition")
p # auto-display in Jupyter
p.group("FirstGame").points().show() # chain + explicit display
# From a fitted pyENA model
p = qeviz.from_pyena(model, group_col="Condition")
p.edges("FirstGame", also="SecondGame")
p.edges(unit="FirstGame::alice")
# Introspect available groups / units
p.groups() # ["FirstGame", "SecondGame"]
p.units() # ["FirstGame::alice", ...]
# Export to file
p.export_html("output.html")
Chain API
All chain methods return a new QEPlot — the original is unchanged.
plot_obj.group(*names, intervals=True)
Which group means (and CI boxes) to show. Omit names → all groups.
plot_obj.edges(group, *, unit, compare, also, show)
Which edge network to draw. Omit → no network rendered.
plot_obj.points(show=True)
Show or hide individual unit scatter dots.
plot_obj.labels(nodes, means, points)
Label visibility: "on" | "off" | "click" | "auto".
Dependencies
Required : pandas
Optional : scipy (exact t-distribution CIs; falls back to lookup table)
IPython (Jupyter inline display)
pyENA (for from_pyena())
1""" 2qeviz — Python adapter for the qeviz visualization library. 3 4Converts pandas DataFrames to the qeviz ModelData format and provides 5a chainable plot object for Jupyter display and standalone HTML export. 6 7Quick start 8----------- 9:: 10 11 import qeviz 12 13 # From DataFrames 14 p = qeviz.plot(nodes_df, edges_df, points_df, group_col="Condition") 15 p # auto-display in Jupyter 16 p.group("FirstGame").points().show() # chain + explicit display 17 18 # From a fitted pyENA model 19 p = qeviz.from_pyena(model, group_col="Condition") 20 p.edges("FirstGame", also="SecondGame") 21 p.edges(unit="FirstGame::alice") 22 23 # Introspect available groups / units 24 p.groups() # ["FirstGame", "SecondGame"] 25 p.units() # ["FirstGame::alice", ...] 26 27 # Export to file 28 p.export_html("output.html") 29 30Chain API 31--------- 32All chain methods return a *new* ``QEPlot`` — the original is unchanged. 33 34``plot_obj.group(*names, intervals=True)`` 35 Which group means (and CI boxes) to show. Omit names → all groups. 36 37``plot_obj.edges(group, *, unit, compare, also, show)`` 38 Which edge network to draw. Omit → no network rendered. 39 40``plot_obj.points(show=True)`` 41 Show or hide individual unit scatter dots. 42 43``plot_obj.labels(nodes, means, points)`` 44 Label visibility: ``"on" | "off" | "click" | "auto"``. 45 46Dependencies 47------------ 48Required : pandas 49Optional : scipy (exact t-distribution CIs; falls back to lookup table) 50 IPython (Jupyter inline display) 51 pyENA (for :func:`from_pyena`) 52""" 53 54from __future__ import annotations 55 56import copy 57import json 58import math 59import os 60import time 61from typing import Optional, Union 62 63import pandas as pd 64 65 66# ── Internal helpers ─────────────────────────────────────────────────────────── 67 68def _qe_frame(df: pd.DataFrame) -> dict: 69 """Convert a pandas DataFrame to a QEFrame ``{data: [...], types: {...}}``.""" 70 _type_map = { 71 "object": "character", 72 "string": "character", 73 "int64": "integer", 74 "int32": "integer", 75 "int16": "integer", 76 "int8": "integer", 77 "float64": "numeric", 78 "float32": "numeric", 79 "bool": "logical", 80 } 81 types = { 82 col: _type_map.get(str(df[col].dtype), "character") 83 for col in df.columns 84 } 85 data = [] 86 for _, row in df.iterrows(): 87 row_dict: dict = {} 88 for col, val in row.items(): 89 if isinstance(val, float) and (math.isnan(val) or math.isinf(val)): 90 row_dict[col] = None 91 elif hasattr(val, "item"): # numpy scalar → Python native 92 row_dict[col] = val.item() 93 else: 94 row_dict[col] = val 95 data.append(row_dict) 96 return {"data": data, "types": types} 97 98 99def _t_quantile(p: float, df: int) -> float: 100 """Upper t-quantile with scipy if available, lookup-table fallback.""" 101 try: 102 from scipy import stats as _stats # type: ignore 103 return float(_stats.t.ppf(p, df=df)) 104 except ImportError: 105 pass 106 lut = {1: 12.706, 2: 4.303, 3: 3.182, 5: 2.571, 10: 2.228, 107 20: 2.086, 30: 2.042, 60: 2.000, 120: 1.980} 108 for threshold in sorted(lut): 109 if df <= threshold: 110 return lut[threshold] 111 return 1.960 112 113 114def _group_summaries( 115 points_df: pd.DataFrame, 116 group_col: str, 117 dim_cols: list, 118 conf_level: float = 0.95, 119 include_ci: bool = True, 120) -> pd.DataFrame: 121 """Per-group means and optional rectangular 95 % CI bounds.""" 122 rows = [] 123 for g in points_df[group_col].unique(): 124 sub = points_df[points_df[group_col] == g][dim_cols].apply( 125 pd.to_numeric, errors="coerce" 126 ) 127 n = len(sub) 128 means = sub.mean() 129 row: dict = {group_col: str(g)} 130 for d in dim_cols: 131 row[d] = float(means[d]) 132 if include_ci and n >= 2: 133 t_val = _t_quantile((1 + conf_level) / 2, df=n - 1) 134 for d in dim_cols: 135 se = float(sub[d].std(ddof=1)) / math.sqrt(n) 136 row[f"{d}.low"] = row[d] - t_val * se 137 row[f"{d}.high"] = row[d] + t_val * se 138 rows.append(row) 139 return pd.DataFrame(rows) 140 141 142def _bundle_script_tag() -> str: 143 """Return a ``<script>`` tag with the inlined UMD bundle.""" 144 bundle_path = os.path.join( 145 os.path.dirname(os.path.abspath(__file__)), "qeviz.umd.js" 146 ) 147 if os.path.exists(bundle_path): 148 with open(bundle_path, "r", encoding="utf-8") as fh: 149 return f"<script>{fh.read()}</script>" 150 return '<script src="qeviz.umd.js"></script>' 151 152 153# ── QEPlot ───────────────────────────────────────────────────────────────────── 154 155class QEPlot: 156 """ 157 A lazily-rendered qeviz plot. 158 159 Created by :func:`plot` or :func:`from_pyena`. Chain :meth:`group`, 160 :meth:`edges`, :meth:`points`, and :meth:`labels` to customise the view. 161 162 Display options: 163 164 * Evaluate the object as the last expression in a Jupyter cell — the 165 ``_repr_html_`` hook triggers automatic inline display. 166 * Call :meth:`show` explicitly to display in IPython / Jupyter. 167 * Call :meth:`export_html` to write a self-contained ``.html`` file. 168 169 All chain methods return a *new* ``QEPlot`` so the original is not 170 mutated and multiple views can branch from a common base. 171 """ 172 173 def __init__( 174 self, 175 model: dict, 176 title: str = "qeviz", 177 width: Union[int, str] = 700, 178 height: Union[int, str] = 500, 179 ) -> None: 180 self._model = model 181 self._title = title 182 # Normalise width/height to CSS strings for the <qe-graph> attribute. 183 self._width = str(width) if isinstance(width, str) else f"{width}px" 184 self._height = str(height) if isinstance(height, str) else f"{height}px" 185 self._opts: dict = {} 186 187 # ── Model access ─────────────────────────────────────────────────────────── 188 189 @property 190 def model(self) -> dict: 191 """The raw ModelData dict (nodes, edges, points, groups, …).""" 192 return self._model 193 194 # ── Copy helper ──────────────────────────────────────────────────────────── 195 196 def _copy(self) -> "QEPlot": 197 """Return a shallow copy with an independent ``_opts`` dict.""" 198 p = QEPlot.__new__(QEPlot) 199 p._model = self._model # shared — model data is read-only 200 p._title = self._title 201 p._width = self._width 202 p._height = self._height 203 p._opts = dict(self._opts) # independent copy 204 return p 205 206 # ── Introspection ────────────────────────────────────────────────────────── 207 208 def groups(self) -> list: 209 """Return the group names present in the model (from the groups frame).""" 210 frame = self._model.get("groups") 211 if not frame: 212 return [] 213 group_col = self._model.get("group_col") or "group" 214 result = [] 215 for row in frame.get("data", []): 216 val = row.get(group_col) 217 if val is None and row: 218 val = next(iter(row.values())) 219 if val is not None: 220 result.append(str(val)) 221 return result 222 223 def units(self) -> list: 224 """Return the unit IDs present in the model's points frame.""" 225 frame = self._model.get("points") 226 if not frame: 227 return [] 228 id_col = self._model.get("id_col") or "QEUNIT" 229 return [ 230 str(row[id_col]) 231 for row in frame.get("data", []) 232 if id_col in row 233 ] 234 235 # ── Chain methods ────────────────────────────────────────────────────────── 236 237 def group(self, *names: str, intervals: bool = True) -> "QEPlot": 238 """ 239 Select which group means (and optionally CI boxes) to display. 240 241 Pass one or more group names to restrict the view; omit all names to 242 show every group in the model (the default). This is independent of 243 edge rendering — use :meth:`edges` to choose whose network to draw. 244 245 Parameters 246 ---------- 247 *names Group names to display. Omit to show all groups. 248 intervals Draw 95 % CI rectangles around each mean. Default ``True``. 249 """ 250 p = self._copy() 251 available = self.groups() 252 if names: 253 bad = [n for n in names if n not in available] 254 if bad: 255 raise ValueError( 256 f"group(): name(s) not found in model: {bad!r}\n" 257 f"Available: {available!r}" 258 ) 259 p._opts["groups"] = ",".join(names) 260 else: 261 p._opts.pop("groups", None) 262 p._opts["confidence"] = intervals 263 return p 264 265 def edges( 266 self, 267 group: Optional[str] = None, 268 *, 269 unit: Optional[str] = None, 270 compare: Optional[str] = None, 271 also: Optional[str] = None, 272 show: bool = True, 273 ) -> "QEPlot": 274 """ 275 Select which edge network to draw. 276 277 Supply either ``group`` (a group's mean network) or ``unit`` (an 278 individual unit's personal network) as the primary edge source. 279 Omit both to draw no network. Pass ``show=False`` to explicitly 280 suppress any previously set edges. 281 282 Parameters 283 ---------- 284 group Group name whose mean edge network to draw. 285 unit Unit ID for an individual network, e.g. 286 ``"FirstGame::alice"``. Mutually exclusive with ``group``. 287 compare Second group to subtract (``group − compare``). 288 also Second group to overlay alongside ``group``. 289 show ``False`` suppresses all edges regardless of other args. 290 """ 291 if group and unit: 292 raise ValueError("edges(): supply either 'group' or 'unit', not both.") 293 294 p = self._copy() 295 if not show: 296 p._opts["show_edges"] = False 297 for k in ("group", "unit", "compare", "also"): 298 p._opts.pop(k, None) 299 return p 300 301 available_groups = self.groups() 302 available_units = self.units() 303 304 def _check_group(name: str, arg: str) -> None: 305 if name and name not in available_groups: 306 raise ValueError( 307 f"edges(): {arg}={name!r} not found.\n" 308 f"Available groups: {available_groups!r}" 309 ) 310 311 _check_group(group, "group") 312 _check_group(compare, "compare") 313 _check_group(also, "also") 314 315 if unit and unit not in available_units: 316 raise ValueError( 317 f"edges(): unit={unit!r} not found.\n" 318 "Use .units() to see available unit IDs." 319 ) 320 if compare and also: 321 raise ValueError("edges(): supply either 'compare' or 'also', not both.") 322 if unit and (compare or also): 323 raise ValueError( 324 "edges(): 'compare' and 'also' cannot be combined with 'unit'." 325 ) 326 327 p._opts["show_edges"] = True 328 p._opts["group"] = group 329 p._opts["unit"] = unit 330 p._opts["compare"] = compare 331 p._opts["also"] = also 332 return p 333 334 def points(self, show: bool = True) -> "QEPlot": 335 """Show or hide individual unit scatter dots.""" 336 p = self._copy() 337 p._opts["show_points"] = show 338 return p 339 340 def labels( 341 self, 342 nodes: str = "on", 343 means: str = "on", 344 points: str = "auto", 345 ) -> "QEPlot": 346 """ 347 Control label visibility. 348 349 Each argument accepts ``"on"``, ``"off"``, ``"click"``, or ``"auto"``. 350 """ 351 p = self._copy() 352 p._opts["label_nodes"] = nodes 353 p._opts["label_means"] = means 354 p._opts["label_points"] = points 355 return p 356 357 # ── HTML rendering ───────────────────────────────────────────────────────── 358 359 def _build_graph_element(self) -> str: 360 """Build ``<qe-graph …>…</qe-graph>`` with declarative child layer elements.""" 361 opts = self._opts 362 children: list[str] = [] 363 364 # <qe-nodes> — always present; controls node label mode 365 node_label = opts.get("label_nodes", "on") 366 children.append(f' <qe-nodes label="{node_label}"></qe-nodes>') 367 368 # <qe-means> — always present; groups/confidence control what is shown 369 means_parts: list[str] = [] 370 if opts.get("groups"): 371 means_parts.append(f'groups="{opts["groups"]}"') 372 if opts.get("confidence", True): 373 means_parts.append("confidence") 374 means_label = opts.get("label_means", "on") 375 means_parts.append(f'label="{means_label}"') 376 children.append(f' <qe-means {" ".join(means_parts)}></qe-means>') 377 378 # <qe-points> — only when show_points=True 379 if opts.get("show_points", False): 380 pt_label = opts.get("label_points", "auto") 381 children.append(f' <qe-points label="{pt_label}"></qe-points>') 382 383 # <qe-edges> — only when not suppressed and a primary target is set 384 if opts.get("show_edges", True): 385 edge_parts: list[str] = [] 386 if opts.get("group"): 387 edge_parts.append(f'group="{opts["group"]}"') 388 if opts.get("unit"): 389 edge_parts.append(f'unit="{opts["unit"]}"') 390 if opts.get("compare"): 391 edge_parts.append(f'compare="{opts["compare"]}"') 392 if opts.get("also"): 393 edge_parts.append(f'also="{opts["also"]}"') 394 if edge_parts: 395 children.append( 396 f' <qe-edges {" ".join(edge_parts)}></qe-edges>' 397 ) 398 399 children_str = "\n".join(children) 400 return ( 401 f'<qe-graph width="{self._width}" height="{self._height}">\n' 402 f'{children_str}\n' 403 f' </qe-graph>' 404 ) 405 406 def _build_fragment(self) -> str: 407 """HTML fragment for Jupyter inline display (no ``<html>``/``<head>``).""" 408 uid = f"qeviz-{int(time.time() * 1000) % 100_000}" 409 model_json = json.dumps(self._model, default=str) 410 graph_elem = self._build_graph_element() 411 return ( 412 f"{_bundle_script_tag()}\n" 413 f'<qe-visual id="{uid}">\n' 414 f" {graph_elem}\n" 415 f"</qe-visual>\n" 416 f"<script>\n" 417 f" (function(){{\n" 418 f" document.getElementById('{uid}').setModelData({model_json});\n" 419 f" }})();\n" 420 f"</script>" 421 ) 422 423 def _build_html_doc(self) -> str: 424 """Complete self-contained HTML document.""" 425 model_json = json.dumps(self._model, default=str) 426 graph_elem = self._build_graph_element() 427 bundle = _bundle_script_tag() 428 return ( 429 f"<!DOCTYPE html>\n" 430 f'<html lang="en">\n' 431 f"<head>\n" 432 f' <meta charset="UTF-8">\n' 433 f' <meta name="viewport" content="width=device-width, initial-scale=1.0">\n' 434 f" <title>{self._title}</title>\n" 435 f" <style>\n" 436 f" * {{ box-sizing: border-box; margin: 0; padding: 0; }}\n" 437 f" body {{ font-family: system-ui, sans-serif; background: #fff; }}\n" 438 f" </style>\n" 439 f" {bundle}\n" 440 f"</head>\n" 441 f"<body>\n" 442 f' <qe-visual id="vis">\n' 443 f" {graph_elem}\n" 444 f" </qe-visual>\n" 445 f" <script>\n" 446 f" (function(){{\n" 447 f" document.getElementById('vis').setModelData({model_json});\n" 448 f" }})();\n" 449 f" </script>\n" 450 f"</body>\n" 451 f"</html>" 452 ) 453 454 # ── Display ──────────────────────────────────────────────────────────────── 455 456 def show(self) -> None: 457 """ 458 Display the plot inline in a Jupyter / IPython notebook. 459 460 In non-interactive environments (plain Python scripts) this falls back 461 to printing the HTML document to stdout. 462 """ 463 try: 464 from IPython.display import HTML, display # type: ignore 465 display(HTML(self._build_fragment())) 466 except ImportError: 467 print(self._build_html_doc()) 468 469 def _repr_html_(self) -> str: 470 """Jupyter rich-display hook — called automatically when a cell returns a QEPlot.""" 471 return self._build_fragment() 472 473 def export_html(self, path: str) -> str: 474 """ 475 Write a self-contained HTML file. 476 477 The qeviz bundle is inlined — no server or external dependencies 478 required. 479 480 Parameters 481 ---------- 482 path Output file path, e.g. ``"model.html"``. 483 484 Returns 485 ------- 486 str Resolved absolute path of the written file. 487 """ 488 abs_path = os.path.abspath(path) 489 with open(abs_path, "w", encoding="utf-8") as fh: 490 fh.write(self._build_html_doc()) 491 return abs_path 492 493 def __repr__(self) -> str: 494 groups = self.groups() 495 n_units = len(self.units()) 496 opts = {k: v for k, v in self._opts.items() if v is not None} 497 return ( 498 f"<QEPlot groups={groups!r} units={n_units} opts={opts!r}>" 499 ) 500 501 502# ── Public API ───────────────────────────────────────────────────────────────── 503 504def model_data( 505 nodes: pd.DataFrame, 506 edges: pd.DataFrame, 507 points: Optional[pd.DataFrame] = None, 508 *, 509 id_col: str = "QEUNIT", 510 node_id_col: str = "code", 511 x_col: str = "SVD1", 512 y_col: str = "SVD2", 513 group_col: Optional[str] = None, 514 directed: bool = False, 515 conf_level: float = 0.95, 516 include_ci: bool = True, 517) -> dict: 518 """ 519 Build a qeviz ModelData dict from pandas DataFrames. 520 521 This is the low-level constructor — most users will call :func:`plot` 522 directly. Use ``model_data()`` when you need to inspect or transform the 523 serialised dict before rendering. 524 525 Parameters 526 ---------- 527 nodes One row per code node. Must contain ``node_id_col`` plus 528 ``x_col`` and ``y_col`` position columns. 529 edges One row per unit. First column is the unit identifier 530 (``id_col``); remaining columns are dot-separated edge 531 weights (``"A.B"``). 532 points Unit projection positions. Must contain ``id_col``, 533 ``x_col``, ``y_col``, and (optionally) ``group_col``. 534 id_col Unit-identifier column in ``edges`` / ``points``. 535 node_id_col Node-identifier column in ``nodes``. 536 x_col X-position column name. 537 y_col Y-position column name. 538 group_col Grouping column in ``points``. When provided, group means 539 and CI bounds are added as the ``groups`` frame. 540 directed ``True`` for ONA / directed networks (arrowheads). 541 conf_level Confidence level for CI boxes. Default 0.95. 542 include_ci Include CI bounds in the groups frame. Default ``True``. 543 544 Returns 545 ------- 546 dict ModelData dict suitable for :func:`plot` or :func:`export_html`. 547 """ 548 dim_cols = [x_col, y_col] 549 550 result: dict = { 551 "nodes": _qe_frame(nodes), 552 "edges": _qe_frame(edges), 553 "updated": int(time.time() * 1000), 554 "directed": directed, 555 "id_col": id_col, 556 "node_id_col": node_id_col, 557 "x_col": x_col, 558 "y_col": y_col, 559 "group_col": group_col, 560 } 561 562 if points is not None: 563 result["points"] = _qe_frame(points) 564 if group_col and group_col in points.columns: 565 groups_df = _group_summaries( 566 points, group_col, dim_cols, 567 conf_level=conf_level, 568 include_ci=include_ci, 569 ) 570 result["groups"] = _qe_frame(groups_df) 571 572 return result 573 574 575def plot( 576 x: Union[pd.DataFrame, dict], 577 edges: Optional[pd.DataFrame] = None, 578 points: Optional[pd.DataFrame] = None, 579 *, 580 title: str = "qeviz", 581 width: Union[int, str] = 700, 582 height: Union[int, str] = 500, 583 group_col: Optional[str] = None, 584 **model_kwargs, 585) -> QEPlot: 586 """ 587 Create a :class:`QEPlot`. 588 589 Accepts either a pre-built ModelData dict **or** raw DataFrames 590 (``nodes``, ``edges``, ``points``). 591 592 Parameters 593 ---------- 594 x A ModelData dict from :func:`model_data`, **or** a nodes 595 DataFrame (in which case ``edges`` must also be supplied). 596 edges Edge-weights DataFrame (only when ``x`` is a nodes DataFrame). 597 points Unit-positions DataFrame (optional, only with DataFrames). 598 title Window / tab title for exported files. 599 width, height 600 Plot dimensions. Integers are treated as pixels; strings 601 are passed through as CSS (e.g. ``"100%"``). 602 group_col Grouping column (only when building from DataFrames). 603 **model_kwargs 604 Additional keyword arguments forwarded to :func:`model_data` 605 (e.g. ``id_col``, ``x_col``, ``directed``). 606 607 Returns 608 ------- 609 QEPlot 610 """ 611 if isinstance(x, dict): 612 m = x 613 else: 614 if edges is None: 615 raise TypeError("plot(): 'edges' is required when 'x' is a DataFrame.") 616 m = model_data( 617 x, edges, points, 618 group_col=group_col, 619 **model_kwargs, 620 ) 621 return QEPlot(m, title=title, width=width, height=height) 622 623 624def from_pyena( 625 model, 626 group_col: Optional[str] = None, 627 directed: bool = False, 628 id_col: str = "QEUNIT", 629 title: str = "qeviz", 630 width: Union[int, str] = 700, 631 height: Union[int, str] = 500, 632) -> QEPlot: 633 """ 634 Convert a fitted pyENA model to a :class:`QEPlot`. 635 636 Python equivalent of R's ``qe_extract()`` — reads nodes, unit positions, 637 edge weights, and group metadata directly from a fitted ``pyena.ENA`` 638 instance. 639 640 Parameters 641 ---------- 642 model A fitted ``pyena.ENA`` instance (after ``.fit()``). 643 group_col Grouping column name in ``model.accum_.meta`` 644 (e.g. ``"Condition"``). 645 directed ``True`` for ONA / directed networks. 646 id_col Name to give the unit-identifier column. 647 title, width, height 648 Passed to the returned :class:`QEPlot`. 649 650 Returns 651 ------- 652 QEPlot 653 654 Examples 655 -------- 656 :: 657 658 p = qeviz.from_pyena(model, group_col="Condition") 659 p.edges("FirstGame", also="SecondGame").points() # auto-displays in Jupyter 660 p.export_html("output.html") 661 """ 662 # Nodes: code name + (x, y) 663 nodes_df = pd.DataFrame({ 664 "code": model.codes_, 665 "x": model.positions_[:, 0], 666 "y": model.positions_[:, 1], 667 }) 668 669 # Edges: normalised connection weights, "&" → "." separator 670 conn_cols = [c.replace("&", ".") for c in model.connection_names_] 671 edges_df = pd.DataFrame(model.normed_networks_, columns=conn_cols) 672 edges_df.insert(0, id_col, model.units_) 673 674 # Points: projected (x, y) per unit 675 points_df = pd.DataFrame({ 676 id_col: model.units_, 677 "x": model.points_[:, 0], 678 "y": model.points_[:, 1], 679 }) 680 681 # Attach group from unit-level metadata 682 if group_col is not None: 683 meta = model.accum_.meta 684 if group_col not in meta.columns: 685 raise ValueError( 686 f"group_col {group_col!r} not found in model metadata. " 687 f"Available columns: {list(meta.columns)}" 688 ) 689 group_vals = meta.reindex(model.units_)[group_col].tolist() 690 points_df[group_col] = group_vals 691 edges_df[group_col] = group_vals 692 693 m = model_data( 694 nodes = nodes_df, 695 edges = edges_df, 696 points = points_df, 697 id_col = id_col, 698 node_id_col = "code", 699 x_col = "x", 700 y_col = "y", 701 group_col = group_col, 702 directed = directed, 703 ) 704 return QEPlot(m, title=title, width=width, height=height) 705 706 707def export_html( 708 model_or_plot: Union[dict, QEPlot], 709 path: str, 710 options: Optional[dict] = None, 711 width: Union[int, str] = 700, 712 height: Union[int, str] = 500, 713) -> str: 714 """ 715 Write a self-contained HTML file. 716 717 Accepts either a :class:`QEPlot` **or** a raw ModelData dict (legacy). 718 When a dict is supplied ``options`` / ``width`` / ``height`` are forwarded 719 to construct a temporary :class:`QEPlot`. 720 721 Returns the resolved absolute path of the written file. 722 """ 723 if isinstance(model_or_plot, QEPlot): 724 return model_or_plot.export_html(path) 725 726 # Legacy: raw dict path 727 p = QEPlot(model_or_plot, width=width, height=height) 728 if options: 729 # Map old flat options dict onto chain methods for backward compat. 730 group = options.get("group") 731 unit = options.get("unit") 732 compare = options.get("compare") 733 also = options.get("also") 734 if group or unit or compare or also: 735 p = p.edges(group=group, unit=unit, compare=compare, also=also) 736 if options.get("show_points"): 737 p = p.points(True) 738 return p.export_html(path) 739 740 741__all__ = [ 742 "QEPlot", 743 "model_data", 744 "plot", 745 "from_pyena", 746 "export_html", 747]
156class QEPlot: 157 """ 158 A lazily-rendered qeviz plot. 159 160 Created by :func:`plot` or :func:`from_pyena`. Chain :meth:`group`, 161 :meth:`edges`, :meth:`points`, and :meth:`labels` to customise the view. 162 163 Display options: 164 165 * Evaluate the object as the last expression in a Jupyter cell — the 166 ``_repr_html_`` hook triggers automatic inline display. 167 * Call :meth:`show` explicitly to display in IPython / Jupyter. 168 * Call :meth:`export_html` to write a self-contained ``.html`` file. 169 170 All chain methods return a *new* ``QEPlot`` so the original is not 171 mutated and multiple views can branch from a common base. 172 """ 173 174 def __init__( 175 self, 176 model: dict, 177 title: str = "qeviz", 178 width: Union[int, str] = 700, 179 height: Union[int, str] = 500, 180 ) -> None: 181 self._model = model 182 self._title = title 183 # Normalise width/height to CSS strings for the <qe-graph> attribute. 184 self._width = str(width) if isinstance(width, str) else f"{width}px" 185 self._height = str(height) if isinstance(height, str) else f"{height}px" 186 self._opts: dict = {} 187 188 # ── Model access ─────────────────────────────────────────────────────────── 189 190 @property 191 def model(self) -> dict: 192 """The raw ModelData dict (nodes, edges, points, groups, …).""" 193 return self._model 194 195 # ── Copy helper ──────────────────────────────────────────────────────────── 196 197 def _copy(self) -> "QEPlot": 198 """Return a shallow copy with an independent ``_opts`` dict.""" 199 p = QEPlot.__new__(QEPlot) 200 p._model = self._model # shared — model data is read-only 201 p._title = self._title 202 p._width = self._width 203 p._height = self._height 204 p._opts = dict(self._opts) # independent copy 205 return p 206 207 # ── Introspection ────────────────────────────────────────────────────────── 208 209 def groups(self) -> list: 210 """Return the group names present in the model (from the groups frame).""" 211 frame = self._model.get("groups") 212 if not frame: 213 return [] 214 group_col = self._model.get("group_col") or "group" 215 result = [] 216 for row in frame.get("data", []): 217 val = row.get(group_col) 218 if val is None and row: 219 val = next(iter(row.values())) 220 if val is not None: 221 result.append(str(val)) 222 return result 223 224 def units(self) -> list: 225 """Return the unit IDs present in the model's points frame.""" 226 frame = self._model.get("points") 227 if not frame: 228 return [] 229 id_col = self._model.get("id_col") or "QEUNIT" 230 return [ 231 str(row[id_col]) 232 for row in frame.get("data", []) 233 if id_col in row 234 ] 235 236 # ── Chain methods ────────────────────────────────────────────────────────── 237 238 def group(self, *names: str, intervals: bool = True) -> "QEPlot": 239 """ 240 Select which group means (and optionally CI boxes) to display. 241 242 Pass one or more group names to restrict the view; omit all names to 243 show every group in the model (the default). This is independent of 244 edge rendering — use :meth:`edges` to choose whose network to draw. 245 246 Parameters 247 ---------- 248 *names Group names to display. Omit to show all groups. 249 intervals Draw 95 % CI rectangles around each mean. Default ``True``. 250 """ 251 p = self._copy() 252 available = self.groups() 253 if names: 254 bad = [n for n in names if n not in available] 255 if bad: 256 raise ValueError( 257 f"group(): name(s) not found in model: {bad!r}\n" 258 f"Available: {available!r}" 259 ) 260 p._opts["groups"] = ",".join(names) 261 else: 262 p._opts.pop("groups", None) 263 p._opts["confidence"] = intervals 264 return p 265 266 def edges( 267 self, 268 group: Optional[str] = None, 269 *, 270 unit: Optional[str] = None, 271 compare: Optional[str] = None, 272 also: Optional[str] = None, 273 show: bool = True, 274 ) -> "QEPlot": 275 """ 276 Select which edge network to draw. 277 278 Supply either ``group`` (a group's mean network) or ``unit`` (an 279 individual unit's personal network) as the primary edge source. 280 Omit both to draw no network. Pass ``show=False`` to explicitly 281 suppress any previously set edges. 282 283 Parameters 284 ---------- 285 group Group name whose mean edge network to draw. 286 unit Unit ID for an individual network, e.g. 287 ``"FirstGame::alice"``. Mutually exclusive with ``group``. 288 compare Second group to subtract (``group − compare``). 289 also Second group to overlay alongside ``group``. 290 show ``False`` suppresses all edges regardless of other args. 291 """ 292 if group and unit: 293 raise ValueError("edges(): supply either 'group' or 'unit', not both.") 294 295 p = self._copy() 296 if not show: 297 p._opts["show_edges"] = False 298 for k in ("group", "unit", "compare", "also"): 299 p._opts.pop(k, None) 300 return p 301 302 available_groups = self.groups() 303 available_units = self.units() 304 305 def _check_group(name: str, arg: str) -> None: 306 if name and name not in available_groups: 307 raise ValueError( 308 f"edges(): {arg}={name!r} not found.\n" 309 f"Available groups: {available_groups!r}" 310 ) 311 312 _check_group(group, "group") 313 _check_group(compare, "compare") 314 _check_group(also, "also") 315 316 if unit and unit not in available_units: 317 raise ValueError( 318 f"edges(): unit={unit!r} not found.\n" 319 "Use .units() to see available unit IDs." 320 ) 321 if compare and also: 322 raise ValueError("edges(): supply either 'compare' or 'also', not both.") 323 if unit and (compare or also): 324 raise ValueError( 325 "edges(): 'compare' and 'also' cannot be combined with 'unit'." 326 ) 327 328 p._opts["show_edges"] = True 329 p._opts["group"] = group 330 p._opts["unit"] = unit 331 p._opts["compare"] = compare 332 p._opts["also"] = also 333 return p 334 335 def points(self, show: bool = True) -> "QEPlot": 336 """Show or hide individual unit scatter dots.""" 337 p = self._copy() 338 p._opts["show_points"] = show 339 return p 340 341 def labels( 342 self, 343 nodes: str = "on", 344 means: str = "on", 345 points: str = "auto", 346 ) -> "QEPlot": 347 """ 348 Control label visibility. 349 350 Each argument accepts ``"on"``, ``"off"``, ``"click"``, or ``"auto"``. 351 """ 352 p = self._copy() 353 p._opts["label_nodes"] = nodes 354 p._opts["label_means"] = means 355 p._opts["label_points"] = points 356 return p 357 358 # ── HTML rendering ───────────────────────────────────────────────────────── 359 360 def _build_graph_element(self) -> str: 361 """Build ``<qe-graph …>…</qe-graph>`` with declarative child layer elements.""" 362 opts = self._opts 363 children: list[str] = [] 364 365 # <qe-nodes> — always present; controls node label mode 366 node_label = opts.get("label_nodes", "on") 367 children.append(f' <qe-nodes label="{node_label}"></qe-nodes>') 368 369 # <qe-means> — always present; groups/confidence control what is shown 370 means_parts: list[str] = [] 371 if opts.get("groups"): 372 means_parts.append(f'groups="{opts["groups"]}"') 373 if opts.get("confidence", True): 374 means_parts.append("confidence") 375 means_label = opts.get("label_means", "on") 376 means_parts.append(f'label="{means_label}"') 377 children.append(f' <qe-means {" ".join(means_parts)}></qe-means>') 378 379 # <qe-points> — only when show_points=True 380 if opts.get("show_points", False): 381 pt_label = opts.get("label_points", "auto") 382 children.append(f' <qe-points label="{pt_label}"></qe-points>') 383 384 # <qe-edges> — only when not suppressed and a primary target is set 385 if opts.get("show_edges", True): 386 edge_parts: list[str] = [] 387 if opts.get("group"): 388 edge_parts.append(f'group="{opts["group"]}"') 389 if opts.get("unit"): 390 edge_parts.append(f'unit="{opts["unit"]}"') 391 if opts.get("compare"): 392 edge_parts.append(f'compare="{opts["compare"]}"') 393 if opts.get("also"): 394 edge_parts.append(f'also="{opts["also"]}"') 395 if edge_parts: 396 children.append( 397 f' <qe-edges {" ".join(edge_parts)}></qe-edges>' 398 ) 399 400 children_str = "\n".join(children) 401 return ( 402 f'<qe-graph width="{self._width}" height="{self._height}">\n' 403 f'{children_str}\n' 404 f' </qe-graph>' 405 ) 406 407 def _build_fragment(self) -> str: 408 """HTML fragment for Jupyter inline display (no ``<html>``/``<head>``).""" 409 uid = f"qeviz-{int(time.time() * 1000) % 100_000}" 410 model_json = json.dumps(self._model, default=str) 411 graph_elem = self._build_graph_element() 412 return ( 413 f"{_bundle_script_tag()}\n" 414 f'<qe-visual id="{uid}">\n' 415 f" {graph_elem}\n" 416 f"</qe-visual>\n" 417 f"<script>\n" 418 f" (function(){{\n" 419 f" document.getElementById('{uid}').setModelData({model_json});\n" 420 f" }})();\n" 421 f"</script>" 422 ) 423 424 def _build_html_doc(self) -> str: 425 """Complete self-contained HTML document.""" 426 model_json = json.dumps(self._model, default=str) 427 graph_elem = self._build_graph_element() 428 bundle = _bundle_script_tag() 429 return ( 430 f"<!DOCTYPE html>\n" 431 f'<html lang="en">\n' 432 f"<head>\n" 433 f' <meta charset="UTF-8">\n' 434 f' <meta name="viewport" content="width=device-width, initial-scale=1.0">\n' 435 f" <title>{self._title}</title>\n" 436 f" <style>\n" 437 f" * {{ box-sizing: border-box; margin: 0; padding: 0; }}\n" 438 f" body {{ font-family: system-ui, sans-serif; background: #fff; }}\n" 439 f" </style>\n" 440 f" {bundle}\n" 441 f"</head>\n" 442 f"<body>\n" 443 f' <qe-visual id="vis">\n' 444 f" {graph_elem}\n" 445 f" </qe-visual>\n" 446 f" <script>\n" 447 f" (function(){{\n" 448 f" document.getElementById('vis').setModelData({model_json});\n" 449 f" }})();\n" 450 f" </script>\n" 451 f"</body>\n" 452 f"</html>" 453 ) 454 455 # ── Display ──────────────────────────────────────────────────────────────── 456 457 def show(self) -> None: 458 """ 459 Display the plot inline in a Jupyter / IPython notebook. 460 461 In non-interactive environments (plain Python scripts) this falls back 462 to printing the HTML document to stdout. 463 """ 464 try: 465 from IPython.display import HTML, display # type: ignore 466 display(HTML(self._build_fragment())) 467 except ImportError: 468 print(self._build_html_doc()) 469 470 def _repr_html_(self) -> str: 471 """Jupyter rich-display hook — called automatically when a cell returns a QEPlot.""" 472 return self._build_fragment() 473 474 def export_html(self, path: str) -> str: 475 """ 476 Write a self-contained HTML file. 477 478 The qeviz bundle is inlined — no server or external dependencies 479 required. 480 481 Parameters 482 ---------- 483 path Output file path, e.g. ``"model.html"``. 484 485 Returns 486 ------- 487 str Resolved absolute path of the written file. 488 """ 489 abs_path = os.path.abspath(path) 490 with open(abs_path, "w", encoding="utf-8") as fh: 491 fh.write(self._build_html_doc()) 492 return abs_path 493 494 def __repr__(self) -> str: 495 groups = self.groups() 496 n_units = len(self.units()) 497 opts = {k: v for k, v in self._opts.items() if v is not None} 498 return ( 499 f"<QEPlot groups={groups!r} units={n_units} opts={opts!r}>" 500 )
A lazily-rendered qeviz plot.
Created by plot() or from_pyena(). Chain group(),
edges(), points(), and labels() to customise the view.
Display options:
- Evaluate the object as the last expression in a Jupyter cell — the
_repr_html_hook triggers automatic inline display. - Call
show()explicitly to display in IPython / Jupyter. - Call
export_html()to write a self-contained.htmlfile.
All chain methods return a new QEPlot so the original is not
mutated and multiple views can branch from a common base.
174 def __init__( 175 self, 176 model: dict, 177 title: str = "qeviz", 178 width: Union[int, str] = 700, 179 height: Union[int, str] = 500, 180 ) -> None: 181 self._model = model 182 self._title = title 183 # Normalise width/height to CSS strings for the <qe-graph> attribute. 184 self._width = str(width) if isinstance(width, str) else f"{width}px" 185 self._height = str(height) if isinstance(height, str) else f"{height}px" 186 self._opts: dict = {}
190 @property 191 def model(self) -> dict: 192 """The raw ModelData dict (nodes, edges, points, groups, …).""" 193 return self._model
The raw ModelData dict (nodes, edges, points, groups, …).
209 def groups(self) -> list: 210 """Return the group names present in the model (from the groups frame).""" 211 frame = self._model.get("groups") 212 if not frame: 213 return [] 214 group_col = self._model.get("group_col") or "group" 215 result = [] 216 for row in frame.get("data", []): 217 val = row.get(group_col) 218 if val is None and row: 219 val = next(iter(row.values())) 220 if val is not None: 221 result.append(str(val)) 222 return result
Return the group names present in the model (from the groups frame).
224 def units(self) -> list: 225 """Return the unit IDs present in the model's points frame.""" 226 frame = self._model.get("points") 227 if not frame: 228 return [] 229 id_col = self._model.get("id_col") or "QEUNIT" 230 return [ 231 str(row[id_col]) 232 for row in frame.get("data", []) 233 if id_col in row 234 ]
Return the unit IDs present in the model's points frame.
238 def group(self, *names: str, intervals: bool = True) -> "QEPlot": 239 """ 240 Select which group means (and optionally CI boxes) to display. 241 242 Pass one or more group names to restrict the view; omit all names to 243 show every group in the model (the default). This is independent of 244 edge rendering — use :meth:`edges` to choose whose network to draw. 245 246 Parameters 247 ---------- 248 *names Group names to display. Omit to show all groups. 249 intervals Draw 95 % CI rectangles around each mean. Default ``True``. 250 """ 251 p = self._copy() 252 available = self.groups() 253 if names: 254 bad = [n for n in names if n not in available] 255 if bad: 256 raise ValueError( 257 f"group(): name(s) not found in model: {bad!r}\n" 258 f"Available: {available!r}" 259 ) 260 p._opts["groups"] = ",".join(names) 261 else: 262 p._opts.pop("groups", None) 263 p._opts["confidence"] = intervals 264 return p
Select which group means (and optionally CI boxes) to display.
Pass one or more group names to restrict the view; omit all names to
show every group in the model (the default). This is independent of
edge rendering — use edges() to choose whose network to draw.
Parameters
*names Group names to display. Omit to show all groups.
intervals Draw 95 % CI rectangles around each mean. Default True.
266 def edges( 267 self, 268 group: Optional[str] = None, 269 *, 270 unit: Optional[str] = None, 271 compare: Optional[str] = None, 272 also: Optional[str] = None, 273 show: bool = True, 274 ) -> "QEPlot": 275 """ 276 Select which edge network to draw. 277 278 Supply either ``group`` (a group's mean network) or ``unit`` (an 279 individual unit's personal network) as the primary edge source. 280 Omit both to draw no network. Pass ``show=False`` to explicitly 281 suppress any previously set edges. 282 283 Parameters 284 ---------- 285 group Group name whose mean edge network to draw. 286 unit Unit ID for an individual network, e.g. 287 ``"FirstGame::alice"``. Mutually exclusive with ``group``. 288 compare Second group to subtract (``group − compare``). 289 also Second group to overlay alongside ``group``. 290 show ``False`` suppresses all edges regardless of other args. 291 """ 292 if group and unit: 293 raise ValueError("edges(): supply either 'group' or 'unit', not both.") 294 295 p = self._copy() 296 if not show: 297 p._opts["show_edges"] = False 298 for k in ("group", "unit", "compare", "also"): 299 p._opts.pop(k, None) 300 return p 301 302 available_groups = self.groups() 303 available_units = self.units() 304 305 def _check_group(name: str, arg: str) -> None: 306 if name and name not in available_groups: 307 raise ValueError( 308 f"edges(): {arg}={name!r} not found.\n" 309 f"Available groups: {available_groups!r}" 310 ) 311 312 _check_group(group, "group") 313 _check_group(compare, "compare") 314 _check_group(also, "also") 315 316 if unit and unit not in available_units: 317 raise ValueError( 318 f"edges(): unit={unit!r} not found.\n" 319 "Use .units() to see available unit IDs." 320 ) 321 if compare and also: 322 raise ValueError("edges(): supply either 'compare' or 'also', not both.") 323 if unit and (compare or also): 324 raise ValueError( 325 "edges(): 'compare' and 'also' cannot be combined with 'unit'." 326 ) 327 328 p._opts["show_edges"] = True 329 p._opts["group"] = group 330 p._opts["unit"] = unit 331 p._opts["compare"] = compare 332 p._opts["also"] = also 333 return p
Select which edge network to draw.
Supply either group (a group's mean network) or unit (an
individual unit's personal network) as the primary edge source.
Omit both to draw no network. Pass show=False to explicitly
suppress any previously set edges.
Parameters
group Group name whose mean edge network to draw.
unit Unit ID for an individual network, e.g.
"FirstGame::alice". Mutually exclusive with group.
compare Second group to subtract (group − compare).
also Second group to overlay alongside group.
show False suppresses all edges regardless of other args.
335 def points(self, show: bool = True) -> "QEPlot": 336 """Show or hide individual unit scatter dots.""" 337 p = self._copy() 338 p._opts["show_points"] = show 339 return p
Show or hide individual unit scatter dots.
341 def labels( 342 self, 343 nodes: str = "on", 344 means: str = "on", 345 points: str = "auto", 346 ) -> "QEPlot": 347 """ 348 Control label visibility. 349 350 Each argument accepts ``"on"``, ``"off"``, ``"click"``, or ``"auto"``. 351 """ 352 p = self._copy() 353 p._opts["label_nodes"] = nodes 354 p._opts["label_means"] = means 355 p._opts["label_points"] = points 356 return p
Control label visibility.
Each argument accepts "on", "off", "click", or "auto".
457 def show(self) -> None: 458 """ 459 Display the plot inline in a Jupyter / IPython notebook. 460 461 In non-interactive environments (plain Python scripts) this falls back 462 to printing the HTML document to stdout. 463 """ 464 try: 465 from IPython.display import HTML, display # type: ignore 466 display(HTML(self._build_fragment())) 467 except ImportError: 468 print(self._build_html_doc())
Display the plot inline in a Jupyter / IPython notebook.
In non-interactive environments (plain Python scripts) this falls back to printing the HTML document to stdout.
474 def export_html(self, path: str) -> str: 475 """ 476 Write a self-contained HTML file. 477 478 The qeviz bundle is inlined — no server or external dependencies 479 required. 480 481 Parameters 482 ---------- 483 path Output file path, e.g. ``"model.html"``. 484 485 Returns 486 ------- 487 str Resolved absolute path of the written file. 488 """ 489 abs_path = os.path.abspath(path) 490 with open(abs_path, "w", encoding="utf-8") as fh: 491 fh.write(self._build_html_doc()) 492 return abs_path
Write a self-contained HTML file.
The qeviz bundle is inlined — no server or external dependencies required.
Parameters
path Output file path, e.g. "model.html".
Returns
str Resolved absolute path of the written file.
505def model_data( 506 nodes: pd.DataFrame, 507 edges: pd.DataFrame, 508 points: Optional[pd.DataFrame] = None, 509 *, 510 id_col: str = "QEUNIT", 511 node_id_col: str = "code", 512 x_col: str = "SVD1", 513 y_col: str = "SVD2", 514 group_col: Optional[str] = None, 515 directed: bool = False, 516 conf_level: float = 0.95, 517 include_ci: bool = True, 518) -> dict: 519 """ 520 Build a qeviz ModelData dict from pandas DataFrames. 521 522 This is the low-level constructor — most users will call :func:`plot` 523 directly. Use ``model_data()`` when you need to inspect or transform the 524 serialised dict before rendering. 525 526 Parameters 527 ---------- 528 nodes One row per code node. Must contain ``node_id_col`` plus 529 ``x_col`` and ``y_col`` position columns. 530 edges One row per unit. First column is the unit identifier 531 (``id_col``); remaining columns are dot-separated edge 532 weights (``"A.B"``). 533 points Unit projection positions. Must contain ``id_col``, 534 ``x_col``, ``y_col``, and (optionally) ``group_col``. 535 id_col Unit-identifier column in ``edges`` / ``points``. 536 node_id_col Node-identifier column in ``nodes``. 537 x_col X-position column name. 538 y_col Y-position column name. 539 group_col Grouping column in ``points``. When provided, group means 540 and CI bounds are added as the ``groups`` frame. 541 directed ``True`` for ONA / directed networks (arrowheads). 542 conf_level Confidence level for CI boxes. Default 0.95. 543 include_ci Include CI bounds in the groups frame. Default ``True``. 544 545 Returns 546 ------- 547 dict ModelData dict suitable for :func:`plot` or :func:`export_html`. 548 """ 549 dim_cols = [x_col, y_col] 550 551 result: dict = { 552 "nodes": _qe_frame(nodes), 553 "edges": _qe_frame(edges), 554 "updated": int(time.time() * 1000), 555 "directed": directed, 556 "id_col": id_col, 557 "node_id_col": node_id_col, 558 "x_col": x_col, 559 "y_col": y_col, 560 "group_col": group_col, 561 } 562 563 if points is not None: 564 result["points"] = _qe_frame(points) 565 if group_col and group_col in points.columns: 566 groups_df = _group_summaries( 567 points, group_col, dim_cols, 568 conf_level=conf_level, 569 include_ci=include_ci, 570 ) 571 result["groups"] = _qe_frame(groups_df) 572 573 return result
Build a qeviz ModelData dict from pandas DataFrames.
This is the low-level constructor — most users will call plot()
directly. Use model_data() when you need to inspect or transform the
serialised dict before rendering.
Parameters
nodes One row per code node. Must contain node_id_col plus
x_col and y_col position columns.
edges One row per unit. First column is the unit identifier
(id_col); remaining columns are dot-separated edge
weights ("A.B").
points Unit projection positions. Must contain id_col,
x_col, y_col, and (optionally) group_col.
id_col Unit-identifier column in edges / points.
node_id_col Node-identifier column in nodes.
x_col X-position column name.
y_col Y-position column name.
group_col Grouping column in points. When provided, group means
and CI bounds are added as the groups frame.
directed True for ONA / directed networks (arrowheads).
conf_level Confidence level for CI boxes. Default 0.95.
include_ci Include CI bounds in the groups frame. Default True.
Returns
dict ModelData dict suitable for plot() or export_html().
576def plot( 577 x: Union[pd.DataFrame, dict], 578 edges: Optional[pd.DataFrame] = None, 579 points: Optional[pd.DataFrame] = None, 580 *, 581 title: str = "qeviz", 582 width: Union[int, str] = 700, 583 height: Union[int, str] = 500, 584 group_col: Optional[str] = None, 585 **model_kwargs, 586) -> QEPlot: 587 """ 588 Create a :class:`QEPlot`. 589 590 Accepts either a pre-built ModelData dict **or** raw DataFrames 591 (``nodes``, ``edges``, ``points``). 592 593 Parameters 594 ---------- 595 x A ModelData dict from :func:`model_data`, **or** a nodes 596 DataFrame (in which case ``edges`` must also be supplied). 597 edges Edge-weights DataFrame (only when ``x`` is a nodes DataFrame). 598 points Unit-positions DataFrame (optional, only with DataFrames). 599 title Window / tab title for exported files. 600 width, height 601 Plot dimensions. Integers are treated as pixels; strings 602 are passed through as CSS (e.g. ``"100%"``). 603 group_col Grouping column (only when building from DataFrames). 604 **model_kwargs 605 Additional keyword arguments forwarded to :func:`model_data` 606 (e.g. ``id_col``, ``x_col``, ``directed``). 607 608 Returns 609 ------- 610 QEPlot 611 """ 612 if isinstance(x, dict): 613 m = x 614 else: 615 if edges is None: 616 raise TypeError("plot(): 'edges' is required when 'x' is a DataFrame.") 617 m = model_data( 618 x, edges, points, 619 group_col=group_col, 620 **model_kwargs, 621 ) 622 return QEPlot(m, title=title, width=width, height=height)
Create a QEPlot.
Accepts either a pre-built ModelData dict or raw DataFrames
(nodes, edges, points).
Parameters
x A ModelData dict from model_data(), or a nodes
DataFrame (in which case edges must also be supplied).
edges Edge-weights DataFrame (only when x is a nodes DataFrame).
points Unit-positions DataFrame (optional, only with DataFrames).
title Window / tab title for exported files.
width, height
Plot dimensions. Integers are treated as pixels; strings
are passed through as CSS (e.g. "100%").
group_col Grouping column (only when building from DataFrames).
**model_kwargs
Additional keyword arguments forwarded to model_data()
(e.g. id_col, x_col, directed).
Returns
QEPlot
625def from_pyena( 626 model, 627 group_col: Optional[str] = None, 628 directed: bool = False, 629 id_col: str = "QEUNIT", 630 title: str = "qeviz", 631 width: Union[int, str] = 700, 632 height: Union[int, str] = 500, 633) -> QEPlot: 634 """ 635 Convert a fitted pyENA model to a :class:`QEPlot`. 636 637 Python equivalent of R's ``qe_extract()`` — reads nodes, unit positions, 638 edge weights, and group metadata directly from a fitted ``pyena.ENA`` 639 instance. 640 641 Parameters 642 ---------- 643 model A fitted ``pyena.ENA`` instance (after ``.fit()``). 644 group_col Grouping column name in ``model.accum_.meta`` 645 (e.g. ``"Condition"``). 646 directed ``True`` for ONA / directed networks. 647 id_col Name to give the unit-identifier column. 648 title, width, height 649 Passed to the returned :class:`QEPlot`. 650 651 Returns 652 ------- 653 QEPlot 654 655 Examples 656 -------- 657 :: 658 659 p = qeviz.from_pyena(model, group_col="Condition") 660 p.edges("FirstGame", also="SecondGame").points() # auto-displays in Jupyter 661 p.export_html("output.html") 662 """ 663 # Nodes: code name + (x, y) 664 nodes_df = pd.DataFrame({ 665 "code": model.codes_, 666 "x": model.positions_[:, 0], 667 "y": model.positions_[:, 1], 668 }) 669 670 # Edges: normalised connection weights, "&" → "." separator 671 conn_cols = [c.replace("&", ".") for c in model.connection_names_] 672 edges_df = pd.DataFrame(model.normed_networks_, columns=conn_cols) 673 edges_df.insert(0, id_col, model.units_) 674 675 # Points: projected (x, y) per unit 676 points_df = pd.DataFrame({ 677 id_col: model.units_, 678 "x": model.points_[:, 0], 679 "y": model.points_[:, 1], 680 }) 681 682 # Attach group from unit-level metadata 683 if group_col is not None: 684 meta = model.accum_.meta 685 if group_col not in meta.columns: 686 raise ValueError( 687 f"group_col {group_col!r} not found in model metadata. " 688 f"Available columns: {list(meta.columns)}" 689 ) 690 group_vals = meta.reindex(model.units_)[group_col].tolist() 691 points_df[group_col] = group_vals 692 edges_df[group_col] = group_vals 693 694 m = model_data( 695 nodes = nodes_df, 696 edges = edges_df, 697 points = points_df, 698 id_col = id_col, 699 node_id_col = "code", 700 x_col = "x", 701 y_col = "y", 702 group_col = group_col, 703 directed = directed, 704 ) 705 return QEPlot(m, title=title, width=width, height=height)
Convert a fitted pyENA model to a QEPlot.
Python equivalent of R's qe_extract() — reads nodes, unit positions,
edge weights, and group metadata directly from a fitted pyena.ENA
instance.
Parameters
model A fitted pyena.ENA instance (after .fit()).
group_col Grouping column name in model.accum_.meta
(e.g. "Condition").
directed True for ONA / directed networks.
id_col Name to give the unit-identifier column.
title, width, height
Passed to the returned QEPlot.
Returns
QEPlot
Examples
::
p = qeviz.from_pyena(model, group_col="Condition")
p.edges("FirstGame", also="SecondGame").points() # auto-displays in Jupyter
p.export_html("output.html")
708def export_html( 709 model_or_plot: Union[dict, QEPlot], 710 path: str, 711 options: Optional[dict] = None, 712 width: Union[int, str] = 700, 713 height: Union[int, str] = 500, 714) -> str: 715 """ 716 Write a self-contained HTML file. 717 718 Accepts either a :class:`QEPlot` **or** a raw ModelData dict (legacy). 719 When a dict is supplied ``options`` / ``width`` / ``height`` are forwarded 720 to construct a temporary :class:`QEPlot`. 721 722 Returns the resolved absolute path of the written file. 723 """ 724 if isinstance(model_or_plot, QEPlot): 725 return model_or_plot.export_html(path) 726 727 # Legacy: raw dict path 728 p = QEPlot(model_or_plot, width=width, height=height) 729 if options: 730 # Map old flat options dict onto chain methods for backward compat. 731 group = options.get("group") 732 unit = options.get("unit") 733 compare = options.get("compare") 734 also = options.get("also") 735 if group or unit or compare or also: 736 p = p.edges(group=group, unit=unit, compare=compare, also=also) 737 if options.get("show_points"): 738 p = p.points(True) 739 return p.export_html(path)