quantized-lab 0.10.0__tar.gz → 0.11.0__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 (248) hide show
  1. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/.gitignore +7 -0
  2. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/PKG-INFO +1 -1
  3. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/pyproject.toml +1 -1
  4. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/__init__.py +1 -1
  5. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/app.py +4 -0
  6. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure.py +60 -22
  7. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure_categorical.py +39 -22
  8. quantized_lab-0.11.0/src/quantized/calc/figure_facets.py +359 -0
  9. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure_overrides.py +26 -3
  10. quantized_lab-0.11.0/src/quantized/calc/figure_page.py +449 -0
  11. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure_ticks.py +22 -1
  12. quantized_lab-0.11.0/src/quantized/calc/figure_y2.py +266 -0
  13. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/fitting.py +12 -0
  14. quantized_lab-0.11.0/src/quantized/calc/plotting.py +278 -0
  15. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/semiconductor.py +11 -0
  16. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/datastruct.py +22 -3
  17. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/delimited.py +53 -2
  18. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/fidelity.py +2 -0
  19. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/figure_layers.py +11 -0
  20. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/figure_text.py +36 -0
  21. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/figures_opju.py +2 -0
  22. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/opju_figure_text.py +6 -0
  23. quantized_lab-0.11.0/src/quantized/io/sqlite_query.py +145 -0
  24. quantized_lab-0.11.0/src/quantized/routes/database.py +62 -0
  25. quantized_lab-0.11.0/src/quantized/routes/export_figures.py +467 -0
  26. quantized_lab-0.11.0/src/quantized/routes/export_figures_aux.py +227 -0
  27. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/export_page.py +25 -6
  28. quantized_lab-0.11.0/src/quantized/web/assets/index-CQDio8Fq.css +1 -0
  29. quantized_lab-0.11.0/src/quantized/web/assets/index-jFkYmKQl.js +27 -0
  30. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/web/index.html +2 -2
  31. quantized_lab-0.10.0/src/quantized/calc/figure_facets.py +0 -127
  32. quantized_lab-0.10.0/src/quantized/calc/figure_page.py +0 -270
  33. quantized_lab-0.10.0/src/quantized/calc/plotting.py +0 -147
  34. quantized_lab-0.10.0/src/quantized/routes/export_figures.py +0 -477
  35. quantized_lab-0.10.0/src/quantized/web/assets/index-DkU851uO.css +0 -1
  36. quantized_lab-0.10.0/src/quantized/web/assets/index-N-btoovo.js +0 -27
  37. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/LICENSE +0 -0
  38. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/NOTICE +0 -0
  39. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/README.md +0 -0
  40. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/__main__.py +0 -0
  41. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/api.py +0 -0
  42. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/__init__.py +0 -0
  43. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/_clipfit.py +0 -0
  44. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/_natural_neighbor.py +0 -0
  45. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/aggregate.py +0 -0
  46. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/backgrounds.py +0 -0
  47. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/baseline.py +0 -0
  48. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/batch_fit.py +0 -0
  49. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/constants.py +0 -0
  50. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/corrections.py +0 -0
  51. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/crystallography.py +0 -0
  52. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/diffusion.py +0 -0
  53. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/electrical.py +0 -0
  54. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/electrochemistry.py +0 -0
  55. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/element_data.json +0 -0
  56. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/element_data.py +0 -0
  57. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/errors.py +0 -0
  58. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure_break.py +0 -0
  59. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure_corner.py +0 -0
  60. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure_field.py +0 -0
  61. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure_hitmap.py +0 -0
  62. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure_labels.py +0 -0
  63. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure_map.py +0 -0
  64. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure_scale.py +0 -0
  65. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure_shapes.py +0 -0
  66. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure_statplots.py +0 -0
  67. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure_styles.py +0 -0
  68. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/figure_ternary.py +0 -0
  69. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/fit_autoguess.py +0 -0
  70. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/fit_bootstrap.py +0 -0
  71. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/fit_bumps.py +0 -0
  72. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/fit_constraints.py +0 -0
  73. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/fit_equation.py +0 -0
  74. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/fit_findxy.py +0 -0
  75. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/fit_models.py +0 -0
  76. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/fit_models_special.py +0 -0
  77. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/fit_odr.py +0 -0
  78. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/fit_scan.py +0 -0
  79. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/fit_stats.py +0 -0
  80. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/formula.py +0 -0
  81. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/global_curve_fit.py +0 -0
  82. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/global_fit.py +0 -0
  83. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/interp2d.py +0 -0
  84. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/linecut.py +0 -0
  85. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/magnetic.py +0 -0
  86. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/magnetometry.py +0 -0
  87. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/map.py +0 -0
  88. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/mcmc.py +0 -0
  89. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/optics.py +0 -0
  90. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/pawley.py +0 -0
  91. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/peak_batch.py +0 -0
  92. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/peak_fit.py +0 -0
  93. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/peak_integrate.py +0 -0
  94. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/peak_multifit.py +0 -0
  95. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/peak_track.py +0 -0
  96. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/peaks.py +0 -0
  97. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/peakshapes.py +0 -0
  98. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/processing.py +0 -0
  99. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/qspace.py +0 -0
  100. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/reductions.py +0 -0
  101. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/reductions_fft.py +0 -0
  102. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/refl_sld_presets.json +0 -0
  103. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/reflectivity.py +0 -0
  104. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/registry.py +0 -0
  105. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/relaxation.py +0 -0
  106. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/report.py +0 -0
  107. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/report_emit.py +0 -0
  108. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/resample.py +0 -0
  109. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/rsm.py +0 -0
  110. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/rsm_analyze.py +0 -0
  111. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/sld.py +0 -0
  112. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/sld_formula.py +0 -0
  113. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/spectral.py +0 -0
  114. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/statplots.py +0 -0
  115. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/stats.py +0 -0
  116. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/stats_anova2.py +0 -0
  117. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/stats_anova_ext.py +0 -0
  118. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/stats_dist.py +0 -0
  119. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/stats_glm.py +0 -0
  120. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/stats_multivar.py +0 -0
  121. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/stats_roc.py +0 -0
  122. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/stats_survival.py +0 -0
  123. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/stats_tests.py +0 -0
  124. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/substrates.py +0 -0
  125. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/superconductor.py +0 -0
  126. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/surface_fit.py +0 -0
  127. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/surface_models.py +0 -0
  128. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/thermal.py +0 -0
  129. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/thin_film.py +0 -0
  130. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/unit_convert.py +0 -0
  131. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/units.py +0 -0
  132. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/vacuum.py +0 -0
  133. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/calc/xray.py +0 -0
  134. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/cli.py +0 -0
  135. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/__init__.py +0 -0
  136. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/_hdf5_layout.py +0 -0
  137. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/_jcamp_asdf.py +0 -0
  138. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/_xrdml_scan.py +0 -0
  139. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/base.py +0 -0
  140. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/bruker_brml.py +0 -0
  141. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/bruker_raw.py +0 -0
  142. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/cif.py +0 -0
  143. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/consolidated.py +0 -0
  144. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/excel.py +0 -0
  145. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/hdf5.py +0 -0
  146. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/import_filters.py +0 -0
  147. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/import_preview.py +0 -0
  148. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/jcamp.py +0 -0
  149. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/lakeshore.py +0 -0
  150. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/ncnr.py +0 -0
  151. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/netcdf.py +0 -0
  152. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/opus.py +0 -0
  153. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin.py +0 -0
  154. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_com.py +0 -0
  155. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/__init__.py +0 -0
  156. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/annotation_marks.py +0 -0
  157. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/container.py +0 -0
  158. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/curve_style_color.py +0 -0
  159. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/figure_geometry.py +0 -0
  160. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/figures.py +0 -0
  161. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/graph_preview.py +0 -0
  162. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/notes.py +0 -0
  163. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/opj.py +0 -0
  164. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/opj_curves.py +0 -0
  165. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/opj_shapes.py +0 -0
  166. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/opju.py +0 -0
  167. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/opju_axis_real_form.py +0 -0
  168. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/opju_axis_specimen_form.py +0 -0
  169. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/opju_codec.py +0 -0
  170. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/opju_curves.py +0 -0
  171. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/opju_curves_allcols.py +0 -0
  172. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/opju_figure_curves.py +0 -0
  173. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/opju_reports.py +0 -0
  174. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/origin_richtext.py +0 -0
  175. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/preview.py +0 -0
  176. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/templates.py +0 -0
  177. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/tree.py +0 -0
  178. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/tree_opju.py +0 -0
  179. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/windows.py +0 -0
  180. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/windows_opju.py +0 -0
  181. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/writer.py +0 -0
  182. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/origin_project/writer_blocks.py +0 -0
  183. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/qd.py +0 -0
  184. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/refl1d.py +0 -0
  185. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/registry.py +0 -0
  186. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/report_export.py +0 -0
  187. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/rigaku.py +0 -0
  188. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/sims.py +0 -0
  189. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/spc.py +0 -0
  190. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/xrd_csv.py +0 -0
  191. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/io/xrdml.py +0 -0
  192. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/jobs.py +0 -0
  193. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/plugins/__init__.py +0 -0
  194. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/plugins/contract.py +0 -0
  195. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/plugins/loader.py +0 -0
  196. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/plugins/steps.py +0 -0
  197. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/__init__.py +0 -0
  198. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/_bookcache.py +0 -0
  199. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/_export_common.py +0 -0
  200. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/_payload.py +0 -0
  201. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/_uploadcache.py +0 -0
  202. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/aggregate.py +0 -0
  203. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/baseline.py +0 -0
  204. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/books.py +0 -0
  205. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/calc.py +0 -0
  206. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/corrections.py +0 -0
  207. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/crystallography.py +0 -0
  208. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/diffusion.py +0 -0
  209. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/electrical.py +0 -0
  210. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/electrochemistry.py +0 -0
  211. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/export.py +0 -0
  212. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/export_facets.py +0 -0
  213. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/fitting.py +0 -0
  214. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/fitting_bumps.py +0 -0
  215. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/import_template.py +0 -0
  216. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/import_wizard.py +0 -0
  217. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/jobs_api.py +0 -0
  218. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/magnetic.py +0 -0
  219. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/magnetometry.py +0 -0
  220. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/optics.py +0 -0
  221. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/parsers.py +0 -0
  222. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/peaks.py +0 -0
  223. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/plot.py +0 -0
  224. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/reductions.py +0 -0
  225. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/reference.py +0 -0
  226. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/reflectivity.py +0 -0
  227. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/report_export.py +0 -0
  228. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/rsm.py +0 -0
  229. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/samples.py +0 -0
  230. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/semiconductor.py +0 -0
  231. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/sld.py +0 -0
  232. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/spectral.py +0 -0
  233. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/statplots.py +0 -0
  234. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/stats.py +0 -0
  235. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/stats_design.py +0 -0
  236. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/substrates.py +0 -0
  237. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/superconductor.py +0 -0
  238. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/thermal.py +0 -0
  239. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/thin_film.py +0 -0
  240. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/vacuum.py +0 -0
  241. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/routes/xray.py +0 -0
  242. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/samples/demo_vsm.csv +0 -0
  243. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/server_launch.py +0 -0
  244. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
  245. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
  246. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/src/quantized/web/loading.html +0 -0
  247. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/tools/origin_compare/README.md +0 -0
  248. {quantized_lab-0.10.0 → quantized_lab-0.11.0}/tools/visual/README.md +0 -0
@@ -16,6 +16,13 @@ node_modules/
16
16
  frontend/dist/
17
17
  *.tsbuildinfo
18
18
 
19
+ # ── Playwright E2E (GUI_INTERACTION_PLAN #15) ──────────────────────────
20
+ frontend/test-results/
21
+ frontend/playwright-report/
22
+ frontend/e2e/test-results/
23
+ frontend/e2e/playwright-report/
24
+ frontend/blob-report/
25
+
19
26
  # ── Tauri / desktop ───────────────────────────────────────────────────
20
27
  src-tauri/target/
21
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantized-lab
3
- Version: 0.10.0
3
+ Version: 0.11.0
4
4
  Summary: Plot, analyze, and publish lab data - magnetometry, XRD, reflectometry - with OriginPro file interop, golden-verified analysis, and reproducible pipelines.
5
5
  Project-URL: Homepage, https://github.com/pquarterman17/quantized
6
6
  Project-URL: Documentation, https://github.com/pquarterman17/quantized/wiki
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quantized-lab"
3
- version = "0.10.0"
3
+ version = "0.11.0"
4
4
  description = "Plot, analyze, and publish lab data - magnetometry, XRD, reflectometry - with OriginPro file interop, golden-verified analysis, and reproducible pipelines."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -7,5 +7,5 @@ Layered, enforced architecture (see CLAUDE.md):
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- __version__ = "0.10.0"
10
+ __version__ = "0.11.0"
11
11
  __all__ = ["__version__"]
@@ -25,12 +25,14 @@ from quantized.routes import (
25
25
  calc,
26
26
  corrections,
27
27
  crystallography,
28
+ database,
28
29
  diffusion,
29
30
  electrical,
30
31
  electrochemistry,
31
32
  export,
32
33
  export_facets,
33
34
  export_figures,
35
+ export_figures_aux,
34
36
  export_page,
35
37
  fitting,
36
38
  fitting_bumps,
@@ -146,6 +148,7 @@ def create_app() -> FastAPI:
146
148
  application.include_router(import_template.router)
147
149
  application.include_router(plot.router)
148
150
  application.include_router(corrections.router)
151
+ application.include_router(database.router)
149
152
  application.include_router(fitting.router)
150
153
  application.include_router(fitting_bumps.router)
151
154
  application.include_router(jobs_api.router)
@@ -156,6 +159,7 @@ def create_app() -> FastAPI:
156
159
  application.include_router(reference.router)
157
160
  application.include_router(export.router)
158
161
  application.include_router(export_figures.router)
162
+ application.include_router(export_figures_aux.router)
159
163
  application.include_router(export_facets.router)
160
164
  application.include_router(export_page.router)
161
165
  application.include_router(report_export.router)
@@ -32,7 +32,7 @@ from quantized.calc.figure_ticks import apply_tick_formats, apply_tick_steps #
32
32
  __all__ = ["draw_series_axes", "render_figure", "style_rc"]
33
33
 
34
34
  _FORMATS = ("pdf", "svg", "png", "tiff")
35
- _LINESTYLE = {"solid": "-", "dashed": "--", "dotted": ":"}
35
+ _LINESTYLE = {"solid": "-", "dashed": "--", "dotted": ":", "none": "none"}
36
36
  # Fixed fill translucency for MAIN #13 (fill under/between curves) — matches
37
37
  # the screen side's `uplotFill.ts` FILL_ALPHA_PCT (25%) so an exported figure
38
38
  # reads the same as its on-screen counterpart.
@@ -278,6 +278,11 @@ def _render_impl(
278
278
  y_fmt: Mapping[str, Any] | None = None,
279
279
  x_step: float | None = None,
280
280
  y_step: float | None = None,
281
+ y2_mask: Sequence[bool] | None = None,
282
+ y2_label: str = "",
283
+ y2_scale: str | None = None,
284
+ y2_fmt: Mapping[str, Any] | None = None,
285
+ y2_step: float | None = None,
281
286
  ) -> bytes | dict[str, Any]:
282
287
  """Render ``series`` (each ``(label, y)``) against ``x`` to image bytes.
283
288
 
@@ -303,7 +308,17 @@ def _render_impl(
303
308
  (MAIN #12) select linear/log/reciprocal; ``x_log``/``y_log`` are the
304
309
  back-compat boolean fallback (see :func:`draw_series_axes`'s doc).
305
310
  ``x_fmt``/``y_fmt`` (MAIN #24) are the tick-label number format -- see
306
- :func:`draw_series_axes`'s doc.
311
+ :func:`draw_series_axes`'s doc. ``y2_mask`` (parallel to ``series``,
312
+ ``True`` marks a channel drawn on a secondary/right Y axis) dispatches to
313
+ :func:`quantized.calc.figure_y2.render_with_secondary_axis` -- a real
314
+ ``Axes.twinx()`` -- instead of ``draw_series_axes``'s single axes;
315
+ ``None``/all-``False`` (the default) is today's single-axis behaviour,
316
+ byte-identical. ``y2_label``/``y2_scale``/``y2_fmt``/``y2_step`` mirror
317
+ their primary-axis counterparts but apply only to the secondary axis;
318
+ a fixed secondary range rides ``overrides["y2_lim"]`` (see
319
+ ``figure_y2.render_with_secondary_axis``'s doc). Not compatible with
320
+ ``x_breaks`` (raises ``ValueError``) -- a broken figure has several
321
+ axes already and gains no coherent secondary-axis meaning.
307
322
  """
308
323
  if fmt not in _FORMATS:
309
324
  raise ValueError(f"fmt must be one of {_FORMATS}")
@@ -315,11 +330,16 @@ def _render_impl(
315
330
  title = safe_mathtext_label(title)
316
331
  x_label = safe_mathtext_label(x_label)
317
332
  y_label = safe_mathtext_label(y_label)
333
+ y2_label = safe_mathtext_label(y2_label)
318
334
  series = [(safe_mathtext_label(label), y) for label, y in series]
319
335
  st = figure_style(style)
320
336
  ov = dict(overrides or {})
321
337
  _validate_overrides(ov)
322
338
  resolved_dpi = int(dpi) if dpi is not None else int(st.dpi)
339
+ y2_mask_list = list(y2_mask) if y2_mask is not None else [False] * len(series)
340
+ if len(y2_mask_list) != len(series):
341
+ raise ValueError("y2_mask must have the same length as series")
342
+ has_y2 = any(y2_mask_list)
323
343
 
324
344
  # rc_context scopes typography to this render (see style_rc). (matplotlib's
325
345
  # RcParams Literal-key type is impractical with the dynamic font.<generic>
@@ -329,6 +349,8 @@ def _render_impl(
329
349
 
330
350
  xv: NDArray[np.float64] = np.asarray(x, dtype=float)
331
351
  x_breaks = ov.get("x_breaks")
352
+ if has_y2 and x_breaks:
353
+ raise ValueError("y2_keys is not supported together with x_breaks")
332
354
  with matplotlib.rc_context(rc): # type: ignore[arg-type]
333
355
  # Manual axis breaks (gap #21): a distinct, twinned-panel rendering
334
356
  # path — not compatible with the hit-map collector (collect_map is
@@ -365,26 +387,42 @@ def _render_impl(
365
387
  )
366
388
  fig, ax = plt.subplots(figsize=figsize)
367
389
  try:
368
- artists = draw_series_axes(
369
- fig,
370
- ax,
371
- xv,
372
- series,
373
- st=st,
374
- ov=ov,
375
- x_log=x_log,
376
- y_log=y_log,
377
- x_scale=x_scale,
378
- y_scale=y_scale,
379
- title=title,
380
- x_label=x_label,
381
- y_label=y_label,
382
- series_styles=series_styles,
383
- x_fmt=x_fmt,
384
- y_fmt=y_fmt,
385
- x_step=x_step,
386
- y_step=y_step,
387
- )
390
+ if has_y2:
391
+ # Lazy import: mirrors figure_break's own lazy import above —
392
+ # keeps this module's own top-level import list light, and
393
+ # the twinx orchestration out of the 500-line ceiling here.
394
+ from quantized.calc.figure_y2 import render_with_secondary_axis
395
+
396
+ artists = render_with_secondary_axis(
397
+ fig, ax, xv, series, series_styles, y2_mask_list,
398
+ st=st, ov=ov, x_log=x_log, y_log=y_log,
399
+ x_scale=x_scale, y_scale=y_scale,
400
+ title=title, x_label=x_label, y_label=y_label,
401
+ x_fmt=x_fmt, y_fmt=y_fmt, x_step=x_step, y_step=y_step,
402
+ y2_label=y2_label, y2_scale=y2_scale,
403
+ y2_fmt=y2_fmt, y2_step=y2_step,
404
+ )
405
+ else:
406
+ artists = draw_series_axes(
407
+ fig,
408
+ ax,
409
+ xv,
410
+ series,
411
+ st=st,
412
+ ov=ov,
413
+ x_log=x_log,
414
+ y_log=y_log,
415
+ x_scale=x_scale,
416
+ y_scale=y_scale,
417
+ title=title,
418
+ x_label=x_label,
419
+ y_label=y_label,
420
+ series_styles=series_styles,
421
+ x_fmt=x_fmt,
422
+ y_fmt=y_fmt,
423
+ x_step=x_step,
424
+ y_step=y_step,
425
+ )
388
426
  if not ov.get("margins"):
389
427
  fig.tight_layout()
390
428
  if collect_map:
@@ -28,6 +28,44 @@ __all__ = ["render_categorical_figure"]
28
28
  _FORMATS = ("pdf", "svg", "png", "tiff")
29
29
 
30
30
 
31
+ def _draw_categorical_bars(
32
+ ax: Any,
33
+ groups: list[str],
34
+ series: list[str],
35
+ vals: Any,
36
+ errs: Any | None,
37
+ stacked: bool,
38
+ ) -> None:
39
+ """Draw one grouped/stacked bar panel into `ax` — shared by the flat
40
+ single-panel path below and `figure_facets.render_categorical_facets_figure`
41
+ (GUI_INTERACTION #12 slice 4b), so a faceted panel matches the flat
42
+ export exactly. Caller applies title/axis-labels/spines/legend/grid —
43
+ this function only draws the bars + category ticks + zero baseline."""
44
+ n_groups, n_series = len(groups), len(series)
45
+ x = np.arange(n_groups, dtype=float)
46
+ if stacked:
47
+ bottom = np.zeros(n_groups)
48
+ for si in range(n_series):
49
+ yerr = errs[:, si] if errs is not None and si == n_series - 1 else None
50
+ ax.bar(
51
+ x, vals[:, si], 0.68, bottom=bottom, yerr=yerr, capsize=3,
52
+ label=series[si],
53
+ )
54
+ bottom = bottom + np.nan_to_num(vals[:, si])
55
+ else:
56
+ width = 0.8 / n_series
57
+ for si in range(n_series):
58
+ offset = (si - (n_series - 1) / 2) * width
59
+ yerr = errs[:, si] if errs is not None else None
60
+ ax.bar(
61
+ x + offset, vals[:, si], width * 0.85, yerr=yerr, capsize=3,
62
+ label=series[si],
63
+ )
64
+ ax.set_xticks(x)
65
+ ax.set_xticklabels(groups)
66
+ ax.axhline(0, color="0.3", linewidth=0.8) # baseline, visible for mixed-sign data
67
+
68
+
31
69
  def _to_matrix(
32
70
  values: list[list[float]], n_groups: int, n_series: int, name: str
33
71
  ) -> np.ndarray:
@@ -111,28 +149,7 @@ def render_categorical_figure(
111
149
  with matplotlib.rc_context(rc): # type: ignore[arg-type]
112
150
  fig, ax = plt.subplots(figsize=figsize)
113
151
  try:
114
- x = np.arange(n_groups, dtype=float)
115
- if stacked:
116
- bottom = np.zeros(n_groups)
117
- for si in range(n_series):
118
- yerr = errs[:, si] if errs is not None and si == n_series - 1 else None
119
- ax.bar(
120
- x, vals[:, si], 0.68, bottom=bottom, yerr=yerr, capsize=3,
121
- label=series[si],
122
- )
123
- bottom = bottom + np.nan_to_num(vals[:, si])
124
- else:
125
- width = 0.8 / n_series
126
- for si in range(n_series):
127
- offset = (si - (n_series - 1) / 2) * width
128
- yerr = errs[:, si] if errs is not None else None
129
- ax.bar(
130
- x + offset, vals[:, si], width * 0.85, yerr=yerr, capsize=3,
131
- label=series[si],
132
- )
133
- ax.set_xticks(x)
134
- ax.set_xticklabels(groups)
135
- ax.axhline(0, color="0.3", linewidth=0.8) # baseline, visible for mixed-sign data
152
+ _draw_categorical_bars(ax, groups, series, vals, errs, stacked)
136
153
  if title:
137
154
  ax.set_title(title)
138
155
  if x_label:
@@ -0,0 +1,359 @@
1
+ """Publication rendering for faceted (small-multiples) plots.
2
+
3
+ ORIGIN_GAP_PLAN #21 (export half, faceting) + GUI_INTERACTION #12 slice 4b
4
+ (stat-stage faceted export). Pure layer: N independently pre-split panels
5
+ sharing scales/labels -> image bytes out. ``render_facets_figure`` mirrors
6
+ the interactive xy facet splitter (frontend ``lib/facet.facetPayloads``):
7
+ each panel is already the (label, x, series) data for one categorical level.
8
+ ``render_stat_facets_figure``/``render_categorical_facets_figure`` mirror
9
+ the interactive StatStage's OWN facet grid (``useStatStage``'s
10
+ ``drawFacets``) for box/violin and bar mode respectively — reusing
11
+ ``figure_statplots``/``figure_categorical``'s per-panel draw functions so a
12
+ single facet renders byte-identically to that module's flat single-panel
13
+ export. All three share this module's grid-layout helpers
14
+ (``_grid_shape``/``_new_grid_figure``): the SAME ``ceil(sqrt(n))``-column,
15
+ auto-wrapping-row layout the screen's CSS grid uses
16
+ (``Math.ceil(Math.sqrt(n)))`` columns — see ``StatStage.tsx``/
17
+ ``GraphPreview.tsx``), so an exported faceted figure tiles identically to
18
+ what's on screen.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from io import BytesIO
24
+ from typing import Any
25
+
26
+ import matplotlib
27
+
28
+ matplotlib.use("Agg") # headless
29
+
30
+ import matplotlib.pyplot as plt # noqa: E402
31
+ import numpy as np # noqa: E402
32
+
33
+ from quantized.calc.figure import _plot_kwargs # noqa: E402
34
+ from quantized.calc.figure_labels import safe_mathtext_label # noqa: E402
35
+ from quantized.calc.figure_styles import figure_style # noqa: E402
36
+
37
+ __all__ = [
38
+ "render_facets_figure",
39
+ "render_stat_facets_figure",
40
+ "render_categorical_facets_figure",
41
+ ]
42
+
43
+ _FORMATS = ("pdf", "svg", "png", "tiff")
44
+
45
+
46
+ def _grid_shape(n: int) -> tuple[int, int]:
47
+ """(rows, cols) for `n` panels — the SAME `ceil(sqrt(n))`-column layout
48
+ `render_facets_figure` below already uses, and the screen's own CSS grid
49
+ formula (`Math.ceil(Math.sqrt(n))` columns, auto-wrapping rows —
50
+ `StatStage.tsx`/`GraphPreview.tsx`), so every faceted export tiles
51
+ identically to what's on screen."""
52
+ cols = int(np.ceil(np.sqrt(n)))
53
+ rows = int(np.ceil(n / cols))
54
+ return rows, cols
55
+
56
+
57
+ def _new_grid_figure(n: int, figsize: tuple[float, float]) -> tuple[Any, list[Any]]:
58
+ """A fresh `_grid_shape(n)` subplot grid; returns the figure and its axes
59
+ flattened + trimmed to exactly `n` (unused trailing cells past `n` are
60
+ hidden, matching `render_facets_figure`'s own convention below)."""
61
+ rows, cols = _grid_shape(n)
62
+ fig, axes_grid = plt.subplots(rows, cols, figsize=figsize, squeeze=False)
63
+ flat = [ax for row in axes_grid for ax in row]
64
+ for j in range(n, len(flat)):
65
+ flat[j].set_visible(False)
66
+ return fig, flat[:n]
67
+
68
+
69
+ def render_facets_figure(
70
+ panels: list[dict[str, Any]],
71
+ *,
72
+ x_log: bool = False,
73
+ y_log: bool = False,
74
+ title: str = "",
75
+ x_label: str = "",
76
+ y_label: str = "",
77
+ fmt: str = "pdf",
78
+ style: str = "default",
79
+ width_in: float | None = None,
80
+ height_in: float | None = None,
81
+ dpi: int = 200,
82
+ ) -> bytes:
83
+ """Render one small-multiples panel per facet level.
84
+
85
+ Each ``panels[i]`` is ``{"label": str, "x": [...], "series": [{"label":
86
+ str, "y": [...]}]}`` — a pre-split slice of data for one categorical
87
+ level (see the frontend's ``lib/facet.facetPayloads``). Panels tile into
88
+ as-square-as-possible rows/columns and share x/y scales (``sharex`` /
89
+ ``sharey``) so magnitudes stay comparable across levels; each panel
90
+ carries its own facet-level title, and unused trailing grid cells (when
91
+ the panel count isn't a perfect rows*cols rectangle) are hidden.
92
+ """
93
+ if fmt not in _FORMATS:
94
+ raise ValueError(f"fmt must be one of {_FORMATS}")
95
+ if not panels:
96
+ raise ValueError("panels must be non-empty")
97
+ # Rich-text labels (GOTO #5): de-math INVALID $...$ so savefig never raises.
98
+ title = safe_mathtext_label(title)
99
+ x_label = safe_mathtext_label(x_label)
100
+ y_label = safe_mathtext_label(y_label)
101
+
102
+ st = figure_style(style)
103
+ n = len(panels)
104
+ cols = int(np.ceil(np.sqrt(n)))
105
+ rows = int(np.ceil(n / cols))
106
+ figsize = (
107
+ width_in or st.fig_width_in * cols * 0.8,
108
+ height_in or st.fig_height_in * rows * 0.8,
109
+ )
110
+ fallback = "DejaVu Serif" if st.font_generic == "serif" else "DejaVu Sans"
111
+ rc: dict[str, Any] = {
112
+ "font.family": st.font_generic,
113
+ f"font.{st.font_generic}": [st.font_name, fallback],
114
+ "font.size": st.font_size,
115
+ "axes.labelsize": st.font_size,
116
+ "axes.titlesize": st.font_size,
117
+ }
118
+
119
+ with matplotlib.rc_context(rc): # type: ignore[arg-type]
120
+ fig, axes_grid = plt.subplots(
121
+ rows, cols, figsize=figsize, sharex=True, sharey=True, squeeze=False,
122
+ )
123
+ try:
124
+ flat = [ax for row in axes_grid for ax in row]
125
+ for i, panel in enumerate(panels):
126
+ ax = flat[i]
127
+ x = np.asarray(panel.get("x", []), dtype=float)
128
+ series = panel.get("series", [])
129
+ for si, s in enumerate(series):
130
+ kw = _plot_kwargs(st.line_width, st.marker_size, None)
131
+ ax.plot(
132
+ x, np.asarray(s.get("y", []), dtype=float),
133
+ label=safe_mathtext_label(str(s.get("label", f"s{si}"))), **kw,
134
+ )
135
+ ax.set_title(
136
+ safe_mathtext_label(str(panel.get("label", ""))), fontsize=st.font_size
137
+ )
138
+ if x_log:
139
+ ax.set_xscale("log")
140
+ if y_log:
141
+ ax.set_yscale("log")
142
+ if not st.box_on:
143
+ ax.spines["top"].set_visible(False)
144
+ ax.spines["right"].set_visible(False)
145
+ if st.grid_alpha > 0:
146
+ ax.grid(True, alpha=st.grid_alpha)
147
+ if len(series) > 1:
148
+ ax.legend(fontsize=max(6.0, st.legend_font_size - 2), frameon=st.legend_box)
149
+ for j in range(n, len(flat)):
150
+ flat[j].set_visible(False)
151
+
152
+ if title:
153
+ fig.suptitle(title)
154
+ if x_label:
155
+ fig.supxlabel(x_label)
156
+ if y_label:
157
+ fig.supylabel(y_label)
158
+ fig.tight_layout()
159
+ buf = BytesIO()
160
+ fig.savefig(buf, format=fmt, dpi=dpi)
161
+ return buf.getvalue()
162
+ finally:
163
+ plt.close(fig)
164
+
165
+
166
+ def render_stat_facets_figure(
167
+ panels: list[dict[str, Any]],
168
+ *,
169
+ default_kind: str,
170
+ dist: str = "norm",
171
+ bins: str | int = "fd",
172
+ fit: str | None = None,
173
+ title: str = "",
174
+ x_label: str = "",
175
+ y_label: str = "",
176
+ fmt: str = "pdf",
177
+ style: str = "default",
178
+ width_in: float | None = None,
179
+ height_in: float | None = None,
180
+ dpi: int | None = None,
181
+ ) -> bytes:
182
+ """Faceted box/violin export (GUI_INTERACTION #12 slice 4b, StatStage's
183
+ "facet by" grid). Each ``panels[i]`` is ``{"label": str, "kind": "box" |
184
+ "violin" (optional), "data": [[...], ...], "labels": [...] | None}``.
185
+
186
+ ``kind`` is per-facet MODE FIDELITY: the interactive StatStage computes
187
+ each facet slice independently (``useStatStage.computeFacetGroupDraws``)
188
+ and a violin slice whose own ``/api/statplots/violin`` call failed
189
+ degrades to a box plot for JUST that slice (never fabricating a KDE) —
190
+ omitting ``kind`` falls back to ``default_kind`` (today's degrade-free
191
+ case, every panel the same requested kind), while an explicit per-panel
192
+ ``kind`` reproduces a mixed grid exactly as the screen showed it.
193
+
194
+ Reuses ``figure_statplots._draw_statplot`` for each panel so a single
195
+ facet renders byte-identically to that module's flat single-panel path.
196
+ """
197
+ from quantized.calc.figure_statplots import _GROUPED, _draw_statplot
198
+
199
+ if fmt not in _FORMATS:
200
+ raise ValueError(f"fmt must be one of {_FORMATS}")
201
+ if not panels:
202
+ raise ValueError("panels must be non-empty")
203
+ title = safe_mathtext_label(title)
204
+ x_label = safe_mathtext_label(x_label)
205
+ y_label = safe_mathtext_label(y_label)
206
+
207
+ st = figure_style(style)
208
+ resolved_dpi = int(dpi) if dpi is not None else int(st.dpi)
209
+ n = len(panels)
210
+ rows, cols = _grid_shape(n)
211
+ figsize = (
212
+ width_in or st.fig_width_in * cols * 0.8,
213
+ height_in or st.fig_height_in * rows * 0.8,
214
+ )
215
+ fallback = "DejaVu Serif" if st.font_generic == "serif" else "DejaVu Sans"
216
+ rc: dict[str, Any] = {
217
+ "font.family": st.font_generic,
218
+ f"font.{st.font_generic}": [st.font_name, fallback],
219
+ "font.size": st.font_size,
220
+ "axes.labelsize": st.font_size,
221
+ "axes.titlesize": st.font_size,
222
+ "xtick.top": st.box_on,
223
+ "ytick.right": st.box_on,
224
+ }
225
+
226
+ prepared: list[tuple[str, str, Any, list[str] | None]] = []
227
+ for p in panels:
228
+ label = safe_mathtext_label(str(p.get("label", "")))
229
+ kind = p.get("kind") or default_kind
230
+ if kind not in _GROUPED:
231
+ raise ValueError(f"facet kind must be one of {_GROUPED}")
232
+ data = p.get("data")
233
+ if not isinstance(data, list) or not data:
234
+ raise ValueError(f"facet {label!r} needs a non-empty list of groups")
235
+ flabels = p.get("labels")
236
+ flabels = [safe_mathtext_label(str(g)) for g in flabels] if flabels else flabels
237
+ prepared.append((label, kind, data, flabels))
238
+
239
+ with matplotlib.rc_context(rc): # type: ignore[arg-type]
240
+ fig, axes = _new_grid_figure(n, figsize)
241
+ try:
242
+ for ax, (label, kind, data, flabels) in zip(axes, prepared, strict=True):
243
+ _draw_statplot(ax, kind, data, flabels, dist, bins, fit, st)
244
+ ax.set_title(label, fontsize=st.font_size)
245
+ if not st.box_on:
246
+ ax.spines["top"].set_visible(False)
247
+ ax.spines["right"].set_visible(False)
248
+ if title:
249
+ fig.suptitle(title)
250
+ if x_label:
251
+ fig.supxlabel(x_label)
252
+ if y_label:
253
+ fig.supylabel(y_label)
254
+ fig.tight_layout()
255
+ buf = BytesIO()
256
+ fig.savefig(buf, format=fmt, dpi=resolved_dpi)
257
+ return buf.getvalue()
258
+ finally:
259
+ plt.close(fig)
260
+
261
+
262
+ def render_categorical_facets_figure(
263
+ panels: list[dict[str, Any]],
264
+ *,
265
+ stacked: bool = False,
266
+ title: str = "",
267
+ x_label: str = "",
268
+ y_label: str = "",
269
+ fmt: str = "pdf",
270
+ style: str = "default",
271
+ width_in: float | None = None,
272
+ height_in: float | None = None,
273
+ dpi: int = 200,
274
+ ) -> bytes:
275
+ """Faceted grouped/stacked bar export (GUI_INTERACTION #12 slice 4b,
276
+ StatStage's bar-mode "facet by" grid). Each ``panels[i]`` is
277
+ ``{"label": str, "groups": [...], "series": [...], "values": [[...]],
278
+ "errors": [[...]] | None}`` — a SELF-CONTAINED category x series matrix
279
+ (a facet's category set can differ slice to slice when a grouped-by
280
+ column level is absent from that slice, so panels never share one
281
+ ``groups`` list). Reuses ``figure_categorical._draw_categorical_bars``
282
+ so a single panel matches that module's flat single-panel export.
283
+
284
+ ``series`` is assumed consistent across panels (the same plotted
285
+ channels every slice draws), so ONE legend on the first panel documents
286
+ the whole figure rather than repeating it in every small cell.
287
+ """
288
+ from quantized.calc.figure_categorical import (
289
+ _draw_categorical_bars,
290
+ _to_error_matrix,
291
+ _to_matrix,
292
+ )
293
+
294
+ if fmt not in _FORMATS:
295
+ raise ValueError(f"fmt must be one of {_FORMATS}")
296
+ if not panels:
297
+ raise ValueError("panels must be non-empty")
298
+ title = safe_mathtext_label(title)
299
+ x_label = safe_mathtext_label(x_label)
300
+ y_label = safe_mathtext_label(y_label)
301
+
302
+ st = figure_style(style)
303
+ n = len(panels)
304
+ rows, cols = _grid_shape(n)
305
+ figsize = (
306
+ width_in or st.fig_width_in * cols * 0.8,
307
+ height_in or st.fig_height_in * rows * 0.8,
308
+ )
309
+ fallback = "DejaVu Serif" if st.font_generic == "serif" else "DejaVu Sans"
310
+ rc: dict[str, Any] = {
311
+ "font.family": st.font_generic,
312
+ f"font.{st.font_generic}": [st.font_name, fallback],
313
+ "font.size": st.font_size,
314
+ "axes.labelsize": st.font_size,
315
+ "axes.titlesize": st.font_size,
316
+ }
317
+
318
+ prepared = []
319
+ for p in panels:
320
+ label = safe_mathtext_label(str(p.get("label", "")))
321
+ groups = [safe_mathtext_label(str(g)) for g in p.get("groups", [])]
322
+ series = [safe_mathtext_label(str(s)) for s in p.get("series", [])]
323
+ if not groups:
324
+ raise ValueError(f"facet {label!r} needs a non-empty groups list")
325
+ if not series:
326
+ raise ValueError(f"facet {label!r} needs a non-empty series list")
327
+ vals = _to_matrix(p.get("values", []), len(groups), len(series), f"facet {label!r} values")
328
+ errs = _to_error_matrix(p.get("errors"), len(groups), len(series))
329
+ prepared.append((label, groups, series, vals, errs))
330
+
331
+ with matplotlib.rc_context(rc): # type: ignore[arg-type]
332
+ fig, axes = _new_grid_figure(n, figsize)
333
+ try:
334
+ for ax, (label, groups, series, vals, errs) in zip(axes, prepared, strict=True):
335
+ _draw_categorical_bars(ax, groups, series, vals, errs, stacked)
336
+ ax.set_title(label, fontsize=st.font_size)
337
+ if not st.box_on:
338
+ ax.spines["top"].set_visible(False)
339
+ ax.spines["right"].set_visible(False)
340
+ if st.grid_alpha > 0:
341
+ ax.grid(True, alpha=st.grid_alpha, axis="y")
342
+ first_series = prepared[0][2]
343
+ if len(first_series) > 1:
344
+ axes[0].legend(
345
+ frameon=st.legend_box, fontsize=max(6.0, st.legend_font_size - 2),
346
+ loc=st.legend_location,
347
+ )
348
+ if title:
349
+ fig.suptitle(title)
350
+ if x_label:
351
+ fig.supxlabel(x_label)
352
+ if y_label:
353
+ fig.supylabel(y_label)
354
+ fig.tight_layout()
355
+ buf = BytesIO()
356
+ fig.savefig(buf, format=fmt, dpi=dpi)
357
+ return buf.getvalue()
358
+ finally:
359
+ plt.close(fig)