goad-toolkit 0.2.13__tar.gz → 0.2.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/memory-2026-08-31.log +33 -0
  2. goad_toolkit-0.2.14/.remember/tmp/last-save-ts +1 -0
  3. goad_toolkit-0.2.14/.remember/tmp/last-summary-failure +1 -0
  4. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/CHANGELOG.md +21 -0
  5. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/PKG-INFO +1 -1
  6. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/docs/03-plot-composition.md +11 -1
  7. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/docs/08-api-reference.md +5 -1
  8. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/pyproject.toml +1 -1
  9. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/src/goad_toolkit/visualizer.py +92 -9
  10. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/tests/test_visualizer.py +55 -0
  11. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/uv.lock +1 -1
  12. goad_toolkit-0.2.13/.remember/tmp/last-save-ts +0 -1
  13. goad_toolkit-0.2.13/.remember/tmp/last-summary-failure +0 -1
  14. goad_toolkit-0.2.13/.remember/tmp/save-session.pid +0 -1
  15. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/settings.local.json +0 -0
  16. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/.claude/settings.local.json +0 -0
  17. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/.gitignore +0 -0
  18. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/.python-version +0 -0
  19. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/CHANGELOG.md +0 -0
  20. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/MCP_SERVER.md +0 -0
  21. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/README.md +0 -0
  22. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/demo/linear.py +0 -0
  23. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/docs/01-goal-oriented-analysis.md +0 -0
  24. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/docs/02-pipelines.md +0 -0
  25. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/docs/03-plot-composition.md +0 -0
  26. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/docs/04-five-families.md +0 -0
  27. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/docs/05-distributions.md +0 -0
  28. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/docs/06-models-and-residuals.md +0 -0
  29. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/docs/07-visual-critique.md +0 -0
  30. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/docs/08-api-reference.md +0 -0
  31. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/docs/09-analysis-method.md +0 -0
  32. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/docs/10-teaching-path.md +0 -0
  33. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/docs/README.md +0 -0
  34. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/goad_mcp.py +0 -0
  35. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/img/distribution_fit.png +0 -0
  36. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/img/goaded.png +0 -0
  37. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/img/linear_results.png +0 -0
  38. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/img/null-distribution.png +0 -0
  39. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/img/residuals.png +0 -0
  40. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/img/zscores.png +0 -0
  41. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/pyproject.toml +0 -0
  42. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/__init__.py +0 -0
  43. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/analytics.py +0 -0
  44. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/cli.py +0 -0
  45. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/config.py +0 -0
  46. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/dataprocessor.py +0 -0
  47. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/datatransforms.py +0 -0
  48. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/distributions.py +0 -0
  49. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/filehandler.py +0 -0
  50. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/models.py +0 -0
  51. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/visualizer.py +0 -0
  52. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/tests/test_cli.py +0 -0
  53. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/tests/test_datatransforms.py +0 -0
  54. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/tests/test_distributions.py +0 -0
  55. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/tests/test_filehandler.py +0 -0
  56. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/tests/test_nulldistribution.py +0 -0
  57. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/tests/test_visualizer.py +0 -0
  58. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.claude/worktrees/quizzical-solomon-b62511/uv.lock +0 -0
  59. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.github/workflows/release.yml +0 -0
  60. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.gitignore +0 -0
  61. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.python-version +0 -0
  62. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/.gitignore +0 -0
  63. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/archive.md +0 -0
  64. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/autonomous/save-170051.log +0 -0
  65. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/autonomous/save-170257.log +0 -0
  66. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/autonomous/save-172752.log +0 -0
  67. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/autonomous/save-172954.log +0 -0
  68. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/autonomous/save-174001.log +0 -0
  69. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/autonomous/save-174204.log +0 -0
  70. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/autonomous/save-174415.log +0 -0
  71. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/autonomous/save-174621.log +0 -0
  72. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/autonomous/save-183124.log +0 -0
  73. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/autonomous/save-183325.log +0 -0
  74. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/autonomous/save-183803.log +0 -0
  75. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/autonomous/save-184713.log +0 -0
  76. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/autonomous/save-184913.log +0 -0
  77. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/hook-errors.log +0 -0
  78. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/logs/logs-2026-08.tar.gz +0 -0
  79. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/now.md +0 -0
  80. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/recent.md +0 -0
  81. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/tmp/capture-alive +0 -0
  82. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/tmp/capture-alive.d/16b4788a-5507-4216-98ec-06bac13a9aba +0 -0
  83. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/tmp/capture-alive.d/309ee39a-6171-47ea-88cf-ee1ad9433f68 +0 -0
  84. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/tmp/capture-alive.d/90574e63-97cd-4c11-87e5-23b656c56fd9 +0 -0
  85. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/tmp/case-divergence +0 -0
  86. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/tmp/last-ndc.ts +0 -0
  87. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/tmp/last-save.json +0 -0
  88. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/tmp/post-tool-ran +0 -0
  89. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/tmp/session-slug +0 -0
  90. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/today-2026-08-10.done.md +0 -0
  91. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/.remember/today-2026-08-10.md +0 -0
  92. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/MCP_SERVER.md +0 -0
  93. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/README.md +0 -0
  94. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/demo/linear.py +0 -0
  95. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/docs/01-goal-oriented-analysis.md +0 -0
  96. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/docs/02-pipelines.md +0 -0
  97. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/docs/04-five-families.md +0 -0
  98. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/docs/05-distributions.md +0 -0
  99. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/docs/06-models-and-residuals.md +0 -0
  100. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/docs/07-visual-critique.md +0 -0
  101. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/docs/09-analysis-method.md +0 -0
  102. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/docs/10-teaching-path.md +0 -0
  103. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/docs/README.md +0 -0
  104. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/goad_mcp.py +0 -0
  105. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/img/distribution_fit.png +0 -0
  106. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/img/goaded.png +0 -0
  107. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/img/linear_results.png +0 -0
  108. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/img/null-distribution.png +0 -0
  109. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/img/residuals.png +0 -0
  110. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/img/zscores.png +0 -0
  111. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/src/goad_toolkit/__init__.py +0 -0
  112. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/src/goad_toolkit/analytics.py +0 -0
  113. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/src/goad_toolkit/cli.py +0 -0
  114. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/src/goad_toolkit/config.py +0 -0
  115. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/src/goad_toolkit/dataprocessor.py +0 -0
  116. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/src/goad_toolkit/datatransforms.py +0 -0
  117. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/src/goad_toolkit/distributions.py +0 -0
  118. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/src/goad_toolkit/filehandler.py +0 -0
  119. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/src/goad_toolkit/models.py +0 -0
  120. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/tests/test_cli.py +0 -0
  121. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/tests/test_datatransforms.py +0 -0
  122. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/tests/test_distributions.py +0 -0
  123. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/tests/test_filehandler.py +0 -0
  124. {goad_toolkit-0.2.13 → goad_toolkit-0.2.14}/tests/test_nulldistribution.py +0 -0
@@ -342,3 +342,36 @@ RuntimeError: claude exited 1: Failed to authenticate: OAuth session expired and
342
342
  18:49:15 [haiku] call exited 1; tokens already spent are unknown — the failure carried no usage block, so this run's reported cost is lower than what it actually cost
343
343
  18:49:15 [haiku] ERROR: call-haiku error: claude exited 1: Failed to authenticate: OAuth session expired and could not be refreshed
344
344
  18:49:15 [haiku] failure 1/3 on this span — will retry next run
345
+ 21:09:54 [hook] session-start: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
346
+ 21:09:54 [hook] save-session: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
347
+ 21:09:54 [force] bypassing cooldown + min msgs
348
+ 21:09:54 [hook] run-consolidation: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
349
+ 21:09:54 [extract] session 309ee39a-6171-47ea-88cf-ee1ad9433f68
350
+ 21:09:54 [extract] 204 exchanges (10 human)
351
+ 21:09:54 [consolidation] start
352
+ 21:09:54 [haiku] calling (branch: main)
353
+ 21:09:54 [hook] session-start: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
354
+ 21:09:54 [hook] run-consolidation: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
355
+ 21:09:54 [consolidation] another consolidation holds the lock, skip
356
+ 21:09:56 [haiku] WARNING: this CLI rejected --setting-sources (Failed to authenticate: OAuth session expired and could not be refreshed); retrying WITHOUT hook isolation so saves keep working. The nested call will run with your hooks registered — a hook that blocks it returns its block message as if it were the model's reply (#202).
357
+ 21:09:56 [haiku] WARNING: this CLI rejected --setting-sources (Failed to authenticate: OAuth session expired and could not be refreshed); retrying WITHOUT hook isolation so saves keep working. The nested call will run with your hooks registered — a hook that blocks it returns its block message as if it were the model's reply (#202).
358
+ 21:09:57 [haiku] call exited 1; tokens already spent are unknown — the failure carried no usage block, so this run's reported cost is lower than what it actually cost
359
+ 21:09:57 [haiku] call exited 1; tokens already spent are unknown — the failure carried no usage block, so this run's reported cost is lower than what it actually cost
360
+ 21:09:57 [haiku] ERROR: call-haiku error: claude exited 1: Failed to authenticate: OAuth session expired and could not be refreshed
361
+ 21:09:57 [haiku] failure 1/3 on this span — will retry next run
362
+ 21:09:57 [consolidation] ERROR: pipeline failed — Traceback (most recent call last):
363
+ File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/runpy.py", line 197, in _run_module_as_main
364
+ return _run_code(code, main_globals, None,
365
+ File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/runpy.py", line 87, in _run_code
366
+ exec(code, run_globals)
367
+ File "/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0/pipeline/shell.py", line 683, in <module>
368
+ main()
369
+ File "/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0/pipeline/shell.py", line 670, in main
370
+ cmd_consolidate(
371
+ File "/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0/pipeline/shell.py", line 525, in cmd_consolidate
372
+ result = consolidate(staging_contents, recent, archive,
373
+ File "/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0/pipeline/consolidate.py", line 324, in consolidate
374
+ result = call_haiku(prompt, timeout=180)
375
+ File "/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0/pipeline/haiku.py", line 735, in call_haiku
376
+ raise RuntimeError(
377
+ RuntimeError: claude exited 1: Failed to authenticate: OAuth session expired and could not be refreshed
@@ -0,0 +1 @@
1
+ 1788203394
@@ -0,0 +1 @@
1
+ 309ee39a-6171-47ea-88cf-ee1ad9433f68:747 1
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.14
4
+
5
+ ### Added
6
+
7
+ - `PlotSettings.subplot_xlabels` / `subplot_ylabels`: per-panel axis labels, the
8
+ `subplot_titles` idea extended to labels. On the panels they cover they win over the
9
+ figure-wide `xlabel`/`ylabel`, so a multi-panel figure with panels showing different
10
+ quantities needs no `ax.set_ylabel` calls.
11
+ - `HorizontalLine`: a layer marking one value on the y-axis — the reference line a residual
12
+ plot is read against. Defaults to a quiet grey line rather than `VerticalLine`'s crimson
13
+ marker, because a baseline is context, not a finding.
14
+ - `BarPlot.build` takes `xtick_rotation` for category labels too long to sit flat.
15
+ - `PlotSettings.xtick_rotation`: the same rotation as a figure-wide setting, applied to
16
+ every axis — for date axes whose labels overlap when flat.
17
+
18
+ ### Changed
19
+
20
+ - Multi-panel grids from `create_figure` use constrained layout, which spaces panels,
21
+ labels and the suptitle at draw time — after every layer has landed — so figures no
22
+ longer need a trailing `fig.tight_layout()`.
23
+
3
24
  ## 0.2.1
4
25
 
5
26
  ### Breaking
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: goad-toolkit
3
- Version: 0.2.13
3
+ Version: 0.2.14
4
4
  Summary: An extensible toolkit for Goal Oriented Analysis of Data
5
5
  Project-URL: Github, https://github.com/raoulg/goad_toolkit
6
6
  Author-email: raoul grouls <Raoul.Grouls@han.nl>
@@ -51,6 +51,9 @@ class PlotSettings:
51
51
  grid: bool = False
52
52
  grid_alpha: Optional[float] = None
53
53
  subplot_titles: List[str] = field(default_factory=list)
54
+ subplot_xlabels: List[str] = field(default_factory=list)
55
+ subplot_ylabels: List[str] = field(default_factory=list)
56
+ xtick_rotation: Optional[float] = None
54
57
  ```
55
58
 
56
59
  `xlabel`/`ylabel` left as `None` are not blank — `create_figure` skips setting them at all,
@@ -66,7 +69,11 @@ title.
66
69
 
67
70
  `subplot_titles` is what makes multi-panel figures work: when a plot creates more than one
68
71
  axis, each gets the corresponding entry. `PlotFits` uses this to label each fitted
69
- distribution with its own test statistics.
72
+ distribution with its own test statistics. `subplot_xlabels` and `subplot_ylabels` are the
73
+ same idea for axis labels — panels showing different quantities get their own labels from
74
+ the settings, not from `ax.set_ylabel` calls after the fact. On the panels they cover, the
75
+ lists win over the figure-wide `xlabel`/`ylabel`. `xtick_rotation` rotates the x tick
76
+ labels on every axis, for the date axes and long category names that overlap when flat.
70
77
 
71
78
  ## 3.3 `BasePlot`
72
79
 
@@ -89,6 +96,8 @@ The contract:
89
96
 
90
97
  `create_figure(n_plots=...)` handles the layout: one axis for a single plot, otherwise a grid
91
98
  wrapping at `settings.max_cols` columns — three by default — with the unused axes hidden.
99
+ The grid uses constrained layout, which spaces panels, labels and the suptitle at draw
100
+ time — after every layer has landed — so a figure never needs a `tight_layout()` call.
92
101
  Labels, grid and titles are applied to every visible axis. Set `max_cols=4` for a quartet or
93
102
  `max_cols=2` for a square grid; four panels at the default wrap to 3 + 1, which is rarely
94
103
  what a set of four wants.
@@ -158,6 +167,7 @@ step 3.
158
167
  | `ComparePlot` | two lines with labels | `LinePlot` twice via `plot_on` |
159
168
  | `VerticalDate` | a labelled vertical line at a date | a layer, not a plot |
160
169
  | `VerticalLine` | the same at a number | a layer, not a plot |
170
+ | `HorizontalLine` | a reference level on the y-axis, usually zero | a layer; the line a residual is read against |
161
171
  | `ComparePlotDate` | `ComparePlot` + `VerticalDate` | |
162
172
  | `BarPlot` | `sns.barplot`, one bar per category | see [Five families §4.1](04-five-families.md) |
163
173
  | `GroupedBarPlot` | bars split by `hue` | two categorical variables at once |
@@ -300,6 +300,9 @@ class PlotSettings:
300
300
  grid: bool = False
301
301
  grid_alpha: Optional[float] = None
302
302
  subplot_titles: List[str] = field(default_factory=list)
303
+ subplot_xlabels: List[str] = field(default_factory=list) # per-panel, wins over xlabel
304
+ subplot_ylabels: List[str] = field(default_factory=list) # per-panel, wins over ylabel
305
+ xtick_rotation: Optional[float] = None # rotate x tick labels, every axis
303
306
  max_cols: int = 3 # columns before wrapping
304
307
  highlight: List[str] = field(default_factory=list) # defaults for HighlightCategory
305
308
  base_color: str = "lightgrey"
@@ -332,8 +335,9 @@ class BasePlot(ABC):
332
335
  | `ComparePlot` | `data, x, y1, y2, **kwargs` |
333
336
  | `VerticalDate` | `date: str, label: str` |
334
337
  | `VerticalLine` | `x: float, label=None, color="crimson", linestyle="--", linewidth=2, **kwargs` |
338
+ | `HorizontalLine` | `y: float, label=None, color="grey", linestyle="-", linewidth=1, **kwargs` |
335
339
  | `ComparePlotDate` | `data, x, y1, y2, date, datelabel, **kwargs` |
336
- | `BarPlot` | `data, x, y, order=None, **kwargs` → `sns.barplot` |
340
+ | `BarPlot` | `data, x, y, order=None, xtick_rotation=None, **kwargs` → `sns.barplot` |
337
341
  | `GroupedBarPlot` | `data, x, y, hue, order=None, hue_order=None, **kwargs` |
338
342
  | `HeatmapPlot` | `data, index=None, columns=None, values=None, aggfunc="mean", cmap="rocket_r", annot=True, fmt=".1f", **kwargs` |
339
343
  | `BarbellPlot` | `data, category, start, end, start_label="before", end_label="after", start_color="lightgrey", end_color="crimson", line_color="lightgrey", markersize=80, **kwargs` |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "goad-toolkit"
3
- version = "0.2.13"
3
+ version = "0.2.14"
4
4
  description = "An extensible toolkit for Goal Oriented Analysis of Data"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -30,6 +30,9 @@ class PlotSettings:
30
30
  grid: bool = False # Added grid option
31
31
  grid_alpha: Optional[float] = None # Grid transparency
32
32
  subplot_titles: List[str] = field(default_factory=list) # Titles for subplots
33
+ subplot_xlabels: List[str] = field(default_factory=list) # One xlabel per subplot
34
+ subplot_ylabels: List[str] = field(default_factory=list) # One ylabel per subplot
35
+ xtick_rotation: Optional[float] = None # Rotate x tick labels on every axis
33
36
  max_cols: int = 3 # Columns before create_figure wraps to a new row
34
37
  highlight: List[str] = field(default_factory=list) # Categories to pick out
35
38
  base_color: str = "lightgrey" # Everything that is not highlighted
@@ -76,9 +79,15 @@ class BasePlot(ABC):
76
79
  # Determine grid layout
77
80
  grid_cols = min(self.settings.max_cols, n_plots)
78
81
  grid_rows = int(np.ceil(n_plots / grid_cols))
79
- # Create subplot grid
82
+ # Create subplot grid. Constrained layout keeps panels, labels and the
83
+ # suptitle from colliding, and recomputes at draw time -- after every
84
+ # layer has landed -- so no tight_layout() call is needed afterwards.
80
85
  self.fig, axes = plt.subplots(
81
- grid_rows, grid_cols, figsize=self.settings.figsize, squeeze=False
86
+ grid_rows,
87
+ grid_cols,
88
+ figsize=self.settings.figsize,
89
+ squeeze=False,
90
+ layout="constrained",
82
91
  )
83
92
  axes = axes.flatten()
84
93
  # Hide unused axes
@@ -92,14 +101,23 @@ class BasePlot(ABC):
92
101
  if ax.get_visible():
93
102
  # None means "let the plotting call decide" -- seaborn fills the axis
94
103
  # label in from the column name whenever the label is still empty, so
95
- # only an explicit xlabel/ylabel should pre-empt that.
96
- if self.settings.xlabel is not None:
97
- ax.set_xlabel(self.settings.xlabel)
98
- if self.settings.ylabel is not None:
99
- ax.set_ylabel(self.settings.ylabel)
104
+ # only an explicit xlabel/ylabel should pre-empt that. The per-subplot
105
+ # lists win over the figure-wide label on the panels they cover.
106
+ xlabel = self.settings.xlabel
107
+ if i < len(self.settings.subplot_xlabels):
108
+ xlabel = self.settings.subplot_xlabels[i]
109
+ if xlabel is not None:
110
+ ax.set_xlabel(xlabel)
111
+ ylabel = self.settings.ylabel
112
+ if i < len(self.settings.subplot_ylabels):
113
+ ylabel = self.settings.subplot_ylabels[i]
114
+ if ylabel is not None:
115
+ ax.set_ylabel(ylabel)
100
116
  # Apply grid if requested
101
117
  if self.settings.grid:
102
118
  ax.grid(True, alpha=self.settings.grid_alpha)
119
+ if self.settings.xtick_rotation is not None:
120
+ ax.tick_params(axis="x", rotation=self.settings.xtick_rotation)
103
121
 
104
122
  # Set subplot titles if available
105
123
  if i < len(self.settings.subplot_titles):
@@ -111,7 +129,6 @@ class BasePlot(ABC):
111
129
  self.ax.set_title(self.settings.title)
112
130
  else:
113
131
  self.fig.suptitle(self.settings.title, fontsize=16)
114
- self.fig.tight_layout(rect=(0, 0, 1, 0.96)) # Make room for suptitle
115
132
 
116
133
  if self.settings.legend_title is not None:
117
134
  self.ax.legend(title=self.settings.legend_title)
@@ -176,7 +193,13 @@ class BasePlot(ABC):
176
193
 
177
194
 
178
195
  class LinePlot(BasePlot):
179
- """Plot a line plot using seaborn."""
196
+ """Plot a line plot using seaborn.
197
+
198
+ Seaborn drops rows with missing values before drawing, so a gap in the data
199
+ is connected straight through rather than left open. When absent data must
200
+ look absent — a reindexed series with `NaN` holes — draw with `ax.plot`,
201
+ which breaks the line at every `NaN`.
202
+ """
180
203
 
181
204
  def build(self, data: pd.DataFrame, **kwargs):
182
205
  sns.lineplot(data=data, ax=self.ax, **kwargs)
@@ -266,6 +289,61 @@ class VerticalLine(BasePlot):
266
289
  return self.fig, self.ax
267
290
 
268
291
 
292
+ class HorizontalLine(BasePlot):
293
+ """Mark one value on the y-axis — a baseline, zero more often than not.
294
+
295
+ `VerticalLine` marks a moment; this marks a level. A residual plot without
296
+ a zero line asks the reader to eyeball where "no effect" sits, so the
297
+ defaults here are a quiet reference line rather than a crimson marker.
298
+ """
299
+
300
+ def build(
301
+ self,
302
+ y: float,
303
+ label: Optional[str] = None,
304
+ color: str = "grey",
305
+ linestyle: str = "-",
306
+ linewidth: float = 1,
307
+ **kwargs,
308
+ ):
309
+ """
310
+ Draw a horizontal line at `y`.
311
+
312
+ Parameters:
313
+ -----------
314
+ y : float
315
+ Position on the y-axis.
316
+ label : Optional[str]
317
+ Legend label. The line is drawn without a legend when this is None.
318
+ color : str
319
+ Line colour.
320
+ linestyle : str
321
+ Line style.
322
+ linewidth : float
323
+ Line width.
324
+ **kwargs : Additional keyword arguments passed to `ax.axhline`.
325
+
326
+ Returns:
327
+ --------
328
+ fig, ax : The created figure and axes.
329
+ """
330
+ if self.ax is None:
331
+ raise ValueError("No axes available for plotting")
332
+
333
+ self.ax.axhline(
334
+ y=y,
335
+ color=color,
336
+ linestyle=linestyle,
337
+ linewidth=linewidth,
338
+ label=label,
339
+ **kwargs,
340
+ )
341
+ if label is not None:
342
+ self.ax.legend()
343
+
344
+ return self.fig, self.ax
345
+
346
+
269
347
  class HighlightCategory(BasePlot):
270
348
  """Grey everything, then colour the categories the plot is actually about.
271
349
 
@@ -477,6 +555,7 @@ class BarPlot(BasePlot):
477
555
  x: str,
478
556
  y: str,
479
557
  order: Optional[List[str]] = None,
558
+ xtick_rotation: Optional[float] = None,
480
559
  **kwargs,
481
560
  ):
482
561
  """
@@ -494,6 +573,8 @@ class BarPlot(BasePlot):
494
573
  order : Optional[List[str]]
495
574
  Category order. Seaborn's own order (first appearance) is used
496
575
  when this is None.
576
+ xtick_rotation : Optional[float]
577
+ Rotate the category labels, for names too long to sit flat.
497
578
  **kwargs : Additional keyword arguments passed to `sns.barplot`,
498
579
  including `estimator`, `errorbar`, `color` and `hue`.
499
580
 
@@ -502,6 +583,8 @@ class BarPlot(BasePlot):
502
583
  fig, ax : The created figure and axes.
503
584
  """
504
585
  sns.barplot(data=data, x=x, y=y, order=order, ax=self.ax, **kwargs)
586
+ if xtick_rotation is not None and self.ax is not None:
587
+ self.ax.tick_params(axis="x", rotation=xtick_rotation)
505
588
  return self.fig, self.ax
506
589
 
507
590
 
@@ -25,6 +25,7 @@ from goad_toolkit.visualizer import ( # noqa: E402
25
25
  GroupedBarPlot,
26
26
  HeatmapPlot,
27
27
  HighlightCategory,
28
+ HorizontalLine,
28
29
  LinePlot,
29
30
  NullPlot,
30
31
  PlotSettings,
@@ -235,6 +236,21 @@ def test_vertical_line_without_a_label_has_no_legend():
235
236
  assert ax.get_legend() is None
236
237
 
237
238
 
239
+ def test_horizontal_line_draws_at_the_requested_y():
240
+ fig, ax = HorizontalLine(PlotSettings()).plot(y=0.0, label="baseline")
241
+
242
+ assert fig is not None
243
+ assert ax.lines[0].get_ydata()[0] == pytest.approx(0.0)
244
+ legend_labels = [t.get_text() for t in ax.get_legend().get_texts()]
245
+ assert legend_labels == ["baseline"]
246
+
247
+
248
+ def test_horizontal_line_without_a_label_has_no_legend():
249
+ fig, ax = HorizontalLine(PlotSettings()).plot(y=1.0)
250
+
251
+ assert ax.get_legend() is None
252
+
253
+
238
254
  @pytest.fixture
239
255
  def null_result() -> NullResult:
240
256
  rng = np.random.default_rng(4)
@@ -305,6 +321,36 @@ def test_max_cols_of_two_gives_a_square_grid():
305
321
  assert fig.axes[0].get_subplotspec().get_gridspec().get_geometry() == (2, 2)
306
322
 
307
323
 
324
+ def test_subplot_labels_apply_per_panel():
325
+ settings = PlotSettings(
326
+ subplot_xlabels=["author", "messages"],
327
+ subplot_ylabels=["count", "length"],
328
+ )
329
+ host = LinePlot(settings)
330
+ fig, axes = host.create_figure(n_plots=2)
331
+
332
+ assert [ax.get_xlabel() for ax in axes] == ["author", "messages"]
333
+ assert [ax.get_ylabel() for ax in axes] == ["count", "length"]
334
+
335
+
336
+ def test_xtick_rotation_setting_applies_to_every_axis():
337
+ host = LinePlot(PlotSettings(xtick_rotation=45))
338
+ fig, axes = host.create_figure(n_plots=2)
339
+
340
+ fig.canvas.draw()
341
+ for ax in axes:
342
+ assert all(t.get_rotation() == pytest.approx(45) for t in ax.get_xticklabels())
343
+
344
+
345
+ def test_subplot_labels_win_over_the_figure_wide_label():
346
+ settings = PlotSettings(ylabel="shared", subplot_ylabels=["only the first"])
347
+ host = LinePlot(settings)
348
+ fig, axes = host.create_figure(n_plots=2)
349
+
350
+ assert axes[0].get_ylabel() == "only the first"
351
+ assert axes[1].get_ylabel() == "shared"
352
+
353
+
308
354
  def test_annotate_writes_the_text_on_the_axis():
309
355
  host = LinePlot(PlotSettings())
310
356
  host.create_figure()
@@ -430,6 +476,15 @@ def test_bar_plot_draws_one_bar_per_category(categories):
430
476
  ]
431
477
 
432
478
 
479
+ def test_bar_plot_rotates_its_tick_labels_on_request(categories):
480
+ fig, ax = BarPlot(PlotSettings()).plot(
481
+ data=categories, x="species", y="mass", xtick_rotation=45
482
+ )
483
+
484
+ fig.canvas.draw()
485
+ assert all(t.get_rotation() == pytest.approx(45) for t in ax.get_xticklabels())
486
+
487
+
433
488
  def test_bar_plot_takes_an_explicit_order(categories):
434
489
  order = ["Gentoo", "Adelie", "Chinstrap"]
435
490
 
@@ -383,7 +383,7 @@ wheels = [
383
383
 
384
384
  [[package]]
385
385
  name = "goad-toolkit"
386
- version = "0.2.13"
386
+ version = "0.2.14"
387
387
  source = { editable = "." }
388
388
  dependencies = [
389
389
  { name = "loguru" },
@@ -1 +0,0 @@
1
- 1788194953
@@ -1 +0,0 @@
1
- 309ee39a-6171-47ea-88cf-ee1ad9433f68:745 1
@@ -1 +0,0 @@
1
- 20713
File without changes
File without changes
File without changes