pychnosz 1.1.1__cp311-cp311-macosx_10_13_x86_64.whl

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 (131) hide show
  1. pychnosz/.dylibs/libgcc_s.1.1.dylib +0 -0
  2. pychnosz/.dylibs/libgfortran.5.dylib +0 -0
  3. pychnosz/.dylibs/libquadmath.0.dylib +0 -0
  4. pychnosz/__init__.py +129 -0
  5. pychnosz/biomolecules/__init__.py +29 -0
  6. pychnosz/biomolecules/ionize_aa.py +197 -0
  7. pychnosz/biomolecules/proteins.py +595 -0
  8. pychnosz/core/__init__.py +46 -0
  9. pychnosz/core/affinity.py +1256 -0
  10. pychnosz/core/animation.py +593 -0
  11. pychnosz/core/balance.py +334 -0
  12. pychnosz/core/basis.py +716 -0
  13. pychnosz/core/diagram.py +3336 -0
  14. pychnosz/core/equilibrate.py +813 -0
  15. pychnosz/core/equilibrium.py +554 -0
  16. pychnosz/core/info.py +821 -0
  17. pychnosz/core/retrieve.py +364 -0
  18. pychnosz/core/speciation.py +580 -0
  19. pychnosz/core/species.py +599 -0
  20. pychnosz/core/subcrt.py +1700 -0
  21. pychnosz/core/thermo.py +593 -0
  22. pychnosz/core/unicurve.py +1226 -0
  23. pychnosz/data/__init__.py +11 -0
  24. pychnosz/data/add_obigt.py +327 -0
  25. pychnosz/data/extdata/Berman/BDat17_2017.csv +2 -0
  26. pychnosz/data/extdata/Berman/Ber88_1988.csv +68 -0
  27. pychnosz/data/extdata/Berman/Ber90_1990.csv +5 -0
  28. pychnosz/data/extdata/Berman/DS10_2010.csv +6 -0
  29. pychnosz/data/extdata/Berman/FDM+14_2014.csv +2 -0
  30. pychnosz/data/extdata/Berman/Got04_2004.csv +5 -0
  31. pychnosz/data/extdata/Berman/JUN92_1992.csv +3 -0
  32. pychnosz/data/extdata/Berman/SHD91_1991.csv +12 -0
  33. pychnosz/data/extdata/Berman/VGT92_1992.csv +2 -0
  34. pychnosz/data/extdata/Berman/VPT01_2001.csv +3 -0
  35. pychnosz/data/extdata/Berman/VPV05_2005.csv +2 -0
  36. pychnosz/data/extdata/Berman/ZS92_1992.csv +11 -0
  37. pychnosz/data/extdata/Berman/sympy.R +99 -0
  38. pychnosz/data/extdata/Berman/testing/BA96.bib +12 -0
  39. pychnosz/data/extdata/Berman/testing/BA96_Berman.csv +21 -0
  40. pychnosz/data/extdata/Berman/testing/BA96_OBIGT.csv +21 -0
  41. pychnosz/data/extdata/Berman/testing/BA96_refs.csv +6 -0
  42. pychnosz/data/extdata/OBIGT/AD.csv +25 -0
  43. pychnosz/data/extdata/OBIGT/Berman_cr.csv +93 -0
  44. pychnosz/data/extdata/OBIGT/DEW.csv +211 -0
  45. pychnosz/data/extdata/OBIGT/H2O_aq.csv +4 -0
  46. pychnosz/data/extdata/OBIGT/SLOP98.csv +411 -0
  47. pychnosz/data/extdata/OBIGT/SUPCRT92.csv +178 -0
  48. pychnosz/data/extdata/OBIGT/inorganic_aq.csv +729 -0
  49. pychnosz/data/extdata/OBIGT/inorganic_cr.csv +273 -0
  50. pychnosz/data/extdata/OBIGT/inorganic_gas.csv +20 -0
  51. pychnosz/data/extdata/OBIGT/organic_aq.csv +1104 -0
  52. pychnosz/data/extdata/OBIGT/organic_cr.csv +481 -0
  53. pychnosz/data/extdata/OBIGT/organic_gas.csv +268 -0
  54. pychnosz/data/extdata/OBIGT/organic_liq.csv +533 -0
  55. pychnosz/data/extdata/OBIGT/testing/GEMSFIT.csv +43 -0
  56. pychnosz/data/extdata/OBIGT/testing/IGEM.csv +17 -0
  57. pychnosz/data/extdata/OBIGT/testing/Sandia.csv +8 -0
  58. pychnosz/data/extdata/OBIGT/testing/SiO2.csv +4 -0
  59. pychnosz/data/extdata/misc/AD03_Fig1a.csv +69 -0
  60. pychnosz/data/extdata/misc/AD03_Fig1b.csv +43 -0
  61. pychnosz/data/extdata/misc/AD03_Fig1c.csv +89 -0
  62. pychnosz/data/extdata/misc/AD03_Fig1d.csv +30 -0
  63. pychnosz/data/extdata/misc/BZA10.csv +5 -0
  64. pychnosz/data/extdata/misc/HW97_Cp.csv +90 -0
  65. pychnosz/data/extdata/misc/HWM96_V.csv +229 -0
  66. pychnosz/data/extdata/misc/LA19_test.csv +7 -0
  67. pychnosz/data/extdata/misc/Mer75_Table4.csv +42 -0
  68. pychnosz/data/extdata/misc/OBIGT_check.csv +423 -0
  69. pychnosz/data/extdata/misc/PM90.csv +7 -0
  70. pychnosz/data/extdata/misc/RH95.csv +23 -0
  71. pychnosz/data/extdata/misc/RH98_Table15.csv +17 -0
  72. pychnosz/data/extdata/misc/SC10_Rainbow.csv +19 -0
  73. pychnosz/data/extdata/misc/SK95.csv +55 -0
  74. pychnosz/data/extdata/misc/SOJSH.csv +61 -0
  75. pychnosz/data/extdata/misc/SS98_Fig5a.csv +81 -0
  76. pychnosz/data/extdata/misc/SS98_Fig5b.csv +84 -0
  77. pychnosz/data/extdata/misc/TKSS14_Fig2.csv +25 -0
  78. pychnosz/data/extdata/misc/bluered.txt +1000 -0
  79. pychnosz/data/extdata/protein/Cas/Cas_aa.csv +177 -0
  80. pychnosz/data/extdata/protein/Cas/Cas_uniprot.csv +186 -0
  81. pychnosz/data/extdata/protein/Cas/download.R +34 -0
  82. pychnosz/data/extdata/protein/Cas/mkaa.R +34 -0
  83. pychnosz/data/extdata/protein/POLG.csv +12 -0
  84. pychnosz/data/extdata/protein/TBD+05.csv +393 -0
  85. pychnosz/data/extdata/protein/TBD+05_aa.csv +393 -0
  86. pychnosz/data/extdata/protein/rubisco.csv +28 -0
  87. pychnosz/data/extdata/protein/rubisco.fasta +239 -0
  88. pychnosz/data/extdata/protein/rubisco_aa.csv +28 -0
  89. pychnosz/data/extdata/src/H2O92D.f.orig +3457 -0
  90. pychnosz/data/extdata/src/README.txt +5 -0
  91. pychnosz/data/extdata/taxonomy/names.dmp +215 -0
  92. pychnosz/data/extdata/taxonomy/nodes.dmp +63 -0
  93. pychnosz/data/extdata/thermo/Bdot_acirc.csv +60 -0
  94. pychnosz/data/extdata/thermo/buffer.csv +40 -0
  95. pychnosz/data/extdata/thermo/element.csv +135 -0
  96. pychnosz/data/extdata/thermo/groups.csv +6 -0
  97. pychnosz/data/extdata/thermo/opt.csv +2 -0
  98. pychnosz/data/extdata/thermo/protein.csv +506 -0
  99. pychnosz/data/extdata/thermo/refs.csv +343 -0
  100. pychnosz/data/extdata/thermo/stoich.csv.xz +0 -0
  101. pychnosz/data/loader.py +431 -0
  102. pychnosz/data/mod_obigt.py +322 -0
  103. pychnosz/data/obigt.py +471 -0
  104. pychnosz/data/worm.py +228 -0
  105. pychnosz/fortran/__init__.py +16 -0
  106. pychnosz/fortran/h2o92.dylib +0 -0
  107. pychnosz/fortran/h2o92_interface.py +527 -0
  108. pychnosz/geochemistry/__init__.py +21 -0
  109. pychnosz/geochemistry/minerals.py +514 -0
  110. pychnosz/geochemistry/redox.py +500 -0
  111. pychnosz/models/__init__.py +47 -0
  112. pychnosz/models/archer_wang.py +165 -0
  113. pychnosz/models/berman.py +309 -0
  114. pychnosz/models/cgl.py +381 -0
  115. pychnosz/models/dew.py +997 -0
  116. pychnosz/models/hkf.py +523 -0
  117. pychnosz/models/hkf_helpers.py +222 -0
  118. pychnosz/models/iapws95.py +1113 -0
  119. pychnosz/models/supcrt92_fortran.py +238 -0
  120. pychnosz/models/water.py +480 -0
  121. pychnosz/utils/__init__.py +27 -0
  122. pychnosz/utils/expression.py +1074 -0
  123. pychnosz/utils/formula.py +830 -0
  124. pychnosz/utils/formula_ox.py +227 -0
  125. pychnosz/utils/reset.py +33 -0
  126. pychnosz/utils/units.py +259 -0
  127. pychnosz-1.1.1.dist-info/METADATA +197 -0
  128. pychnosz-1.1.1.dist-info/RECORD +131 -0
  129. pychnosz-1.1.1.dist-info/WHEEL +5 -0
  130. pychnosz-1.1.1.dist-info/licenses/LICENSE.txt +19 -0
  131. pychnosz-1.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1074 @@
1
+ """
2
+ Expression utilities for formatted labels and text.
3
+
4
+ This module provides Python equivalents of the R functions in util.expression.R:
5
+ - ratlab(): Create formatted text for activity ratios
6
+ - expr_species(): Format chemical formula for display
7
+ - syslab(): Create formatted text for thermodynamic systems
8
+
9
+ Author: CHNOSZ Python port
10
+ """
11
+
12
+ import re
13
+ from typing import Optional
14
+ from .formula import makeup
15
+
16
+ # Optional imports for ratlab_html
17
+ try:
18
+ from wormutils import chemlabel
19
+ from chemparse import parse_formula
20
+ _HTML_DEPS_AVAILABLE = True
21
+ except ImportError:
22
+ _HTML_DEPS_AVAILABLE = False
23
+
24
+
25
+ def ratlab(top: str = "K+", bottom: str = "H+", molality: bool = False,
26
+ reverse_charge: bool = False) -> str:
27
+ """
28
+ Create formatted text label for activity ratio.
29
+
30
+ This function generates a LaTeX-formatted string suitable for use as
31
+ axis labels in matplotlib plots, showing the ratio of activities of
32
+ two ions raised to appropriate powers based on their charges.
33
+
34
+ Parameters
35
+ ----------
36
+ top : str, default "K+"
37
+ Chemical formula for the numerator ion
38
+ bottom : str, default "H+"
39
+ Chemical formula for the denominator ion
40
+ molality : bool, default False
41
+ If True, use 'm' (molality) instead of 'a' (activity)
42
+ reverse_charge : bool, default False
43
+ If True, reverse charge order in formatting (e.g., "Fe+3" becomes "Fe^{3+}")
44
+ If False, keep original order (e.g., "Fe+3" becomes "Fe^{+3}")
45
+
46
+ Returns
47
+ -------
48
+ str
49
+ LaTeX-formatted string for the activity ratio label
50
+
51
+ Examples
52
+ --------
53
+ >>> ratlab("K+", "H+")
54
+ 'log($a_{K^{+}}$ / $a_{H^{+}}$)'
55
+
56
+ >>> ratlab("Ca+2", "H+")
57
+ 'log($a_{Ca^{+2}}$ / $a_{H^{+}}^{2}$)'
58
+
59
+ >>> ratlab("Ca+2", "H+", reverse_charge=True)
60
+ 'log($a_{Ca^{2+}}$ / $a_{H^{+}}^{2}$)'
61
+
62
+ >>> ratlab("Mg+2", "Ca+2")
63
+ 'log($a_{Mg^{+2}}$ / $a_{Ca^{+2}}$)'
64
+
65
+ Notes
66
+ -----
67
+ The exponents are determined by the charges of the ions to maintain
68
+ charge balance in the ratio. For example, for Ca+2/H+, the H+ term
69
+ is squared because Ca has a +2 charge.
70
+
71
+ The output format is compatible with matplotlib's LaTeX rendering.
72
+ In R CHNOSZ, this uses plotmath expressions; here we use LaTeX strings
73
+ that matplotlib can render.
74
+ """
75
+ # Get the charges of the ions
76
+ makeup_top = makeup(top)
77
+ makeup_bottom = makeup(bottom)
78
+
79
+ Z_top = makeup_top.get('Z', 0)
80
+ Z_bottom = makeup_bottom.get('Z', 0)
81
+
82
+ # The exponents for charge balance
83
+ # If top has charge +2 and bottom has +1, bottom gets exponent 2
84
+ exp_bottom = abs(Z_top)
85
+ exp_top = abs(Z_bottom)
86
+
87
+ # Format exponents (don't show if = 1)
88
+ exp_top_str = "" if exp_top == 1 else f"^{{{int(exp_top)}}}"
89
+ exp_bottom_str = "" if exp_bottom == 1 else f"^{{{int(exp_bottom)}}}"
90
+
91
+ # Format the ion formulas for display
92
+ top_formatted = _format_species_latex(top, reverse_charge=reverse_charge)
93
+ bottom_formatted = _format_species_latex(bottom, reverse_charge=reverse_charge)
94
+
95
+ # Choose activity or molality symbol
96
+ a = "m" if molality else "a"
97
+
98
+ # Build the expression
99
+ # Format: log(a_top^exp / a_bottom^exp)
100
+ numerator = f"${a}_{{{top_formatted}}}{exp_top_str}$"
101
+ denominator = f"${a}_{{{bottom_formatted}}}{exp_bottom_str}$"
102
+
103
+ label = f"log({numerator} / {denominator})"
104
+
105
+ return label
106
+
107
+
108
+ def ratlab_html(top: str = "K+", bottom: str = "H+", molality: bool = False) -> str:
109
+ """
110
+ Create HTML-formatted text label for activity ratio (for Plotly/HTML rendering).
111
+
112
+ This function generates an HTML-formatted string suitable for use with
113
+ Plotly interactive plots, showing the ratio of activities of two ions
114
+ raised to appropriate powers based on their charges.
115
+
116
+ This is a companion function to ratlab() which produces LaTeX format for
117
+ matplotlib. Use ratlab_html() when creating labels for diagram(..., interactive=True).
118
+
119
+ Parameters
120
+ ----------
121
+ top : str, default "K+"
122
+ Chemical formula for the numerator ion
123
+ bottom : str, default "H+"
124
+ Chemical formula for the denominator ion
125
+ molality : bool, default False
126
+ If True, use 'm' (molality) instead of 'a' (activity)
127
+
128
+ Returns
129
+ -------
130
+ str
131
+ HTML-formatted string for the activity ratio label
132
+
133
+ Examples
134
+ --------
135
+ >>> ratlab_html("K+", "H+")
136
+ 'log(a<sub>K<sup>+</sup></sub>/a<sub>H<sup>+</sup></sub>)'
137
+
138
+ >>> ratlab_html("Ca+2", "H+")
139
+ 'log(a<sub>Ca<sup>2+</sup></sub>/a<sup>2</sup><sub>H<sup>+</sup></sub>)'
140
+
141
+ >>> ratlab_html("Mg+2", "Ca+2")
142
+ 'log(a<sub>Mg<sup>2+</sup></sub>/a<sub>Ca<sup>2+</sup></sub>)'
143
+
144
+ Notes
145
+ -----
146
+ The exponents are determined by the charges of the ions to maintain
147
+ charge balance in the ratio. For example, for Ca+2/H+, the H+ term
148
+ is squared because Ca has a +2 charge.
149
+
150
+ The output format uses HTML tags (<sub>, <sup>) compatible with Plotly.
151
+ For matplotlib plots with LaTeX rendering, use ratlab() instead.
152
+
153
+ Requires: WORMutils (for chemlabel) and chemparse (for parse_formula)
154
+
155
+ See Also
156
+ --------
157
+ ratlab : LaTeX version for matplotlib
158
+ """
159
+ if not _HTML_DEPS_AVAILABLE:
160
+ raise ImportError(
161
+ "ratlab_html() requires 'WORMutils' and 'chemparse' packages.\n"
162
+ "Install with: pip install WORMutils chemparse"
163
+ )
164
+
165
+ # Parse the formulas to get charges
166
+ top_formula = parse_formula(top)
167
+ if "+" in top_formula.keys():
168
+ top_charge = top_formula["+"]
169
+ elif "-" in top_formula.keys():
170
+ top_charge = -top_formula["-"]
171
+ else:
172
+ raise ValueError("Cannot create an ion ratio involving one or more neutral species.")
173
+
174
+ bottom_formula = parse_formula(bottom)
175
+ if "+" in bottom_formula.keys():
176
+ bottom_charge = bottom_formula["+"]
177
+ elif "-" in bottom_formula.keys():
178
+ bottom_charge = -bottom_formula["-"]
179
+ else:
180
+ raise ValueError("Cannot create an ion ratio involving one or more neutral species.")
181
+
182
+ # Convert to integers if whole numbers
183
+ if top_charge.is_integer():
184
+ top_charge = int(top_charge)
185
+
186
+ if bottom_charge.is_integer():
187
+ bottom_charge = int(bottom_charge)
188
+
189
+ # The exponents for charge balance
190
+ # If top has charge +2 and bottom has +1, bottom gets exponent 2
191
+ exp_bottom = abs(top_charge)
192
+ exp_top = abs(bottom_charge)
193
+
194
+ # Format exponents as superscripts (don't show if = 1)
195
+ if exp_top != 1:
196
+ top_exp_str = "<sup>" + str(exp_top) + "</sup>"
197
+ else:
198
+ top_exp_str = ""
199
+
200
+ if exp_bottom != 1:
201
+ bottom_exp_str = "<sup>" + str(exp_bottom) + "</sup>"
202
+ else:
203
+ bottom_exp_str = ""
204
+
205
+ # Choose activity or molality symbol
206
+ if molality:
207
+ sym = "m"
208
+ else:
209
+ sym = "a"
210
+
211
+ # Format the chemical formulas with chemlabel
212
+ top_formatted = chemlabel(top)
213
+ bottom_formatted = chemlabel(bottom)
214
+
215
+ # Build the HTML expression
216
+ # Format: log(a_top^exp / a_bottom^exp)
217
+ return f"log({sym}{top_exp_str}<sub>{top_formatted}</sub>/{sym}{bottom_exp_str}<sub>{bottom_formatted}</sub>)"
218
+
219
+
220
+ def _format_species_latex(formula: str, reverse_charge: bool = False) -> str:
221
+ """
222
+ Format a chemical formula for LaTeX rendering.
223
+
224
+ This converts a chemical formula like "Ca+2" to LaTeX format.
225
+
226
+ Parameters
227
+ ----------
228
+ formula : str
229
+ Chemical formula
230
+ reverse_charge : bool, default False
231
+ If True, reverse charge order (e.g., "Fe+3" becomes "Fe^{3+}")
232
+ If False, keep original order (e.g., "Fe+3" becomes "Fe^{+3}")
233
+
234
+ Returns
235
+ -------
236
+ str
237
+ LaTeX-formatted formula
238
+ """
239
+ # Handle charge at the end
240
+ # Look for patterns like +, -, +2, -2, +3, etc.
241
+ charge_match = re.search(r'([+-])(\d*)$', formula)
242
+
243
+ if charge_match:
244
+ sign = charge_match.group(1)
245
+ magnitude = charge_match.group(2)
246
+
247
+ # Get the base formula (without charge)
248
+ base = formula[:charge_match.start()]
249
+
250
+ # Format the charge
251
+ if magnitude == '' or magnitude == '1':
252
+ # Single charge: Ca+ or Ca-
253
+ charge_str = f"^{{{sign}}}"
254
+ else:
255
+ # Multiple charges: Ca+2 can be Ca^{+3} or Ca^{3+}
256
+ if reverse_charge:
257
+ # Reversed: magnitude first, then sign (e.g., Ca^{2+})
258
+ charge_str = f"^{{{magnitude}{sign}}}"
259
+ else:
260
+ # Original order: sign first, then magnitude (e.g., Ca^{+2})
261
+ charge_str = f"^{{{sign}{magnitude}}}"
262
+
263
+ # Add subscripts for numbers in the base formula
264
+ base_formatted = _add_subscripts(base)
265
+
266
+ return f"{base_formatted}{charge_str}"
267
+ else:
268
+ # No charge, just add subscripts
269
+ return _add_subscripts(formula)
270
+
271
+
272
+ def _add_subscripts(formula: str) -> str:
273
+ """
274
+ Add LaTeX subscripts for numbers in a chemical formula.
275
+
276
+ Parameters
277
+ ----------
278
+ formula : str
279
+ Chemical formula without charge
280
+
281
+ Returns
282
+ -------
283
+ str
284
+ Formula with subscripts in LaTeX format
285
+ """
286
+ # Replace numbers following letters with subscripts
287
+ # H2O becomes H_{2}O, CO2 becomes CO_{2}
288
+ result = re.sub(r'([A-Z][a-z]?)(\d+)', r'\1_{\2}', formula)
289
+ return result
290
+
291
+
292
+ def syslab(system: list = None, dash: str = "-") -> str:
293
+ """
294
+ Create formatted text for thermodynamic system.
295
+
296
+ This generates a label showing the components of a thermodynamic system,
297
+ separated by dashes (or other separator).
298
+
299
+ Parameters
300
+ ----------
301
+ system : list of str, optional
302
+ List of component formulas. Default: ["K2O", "Al2O3", "SiO2", "H2O"]
303
+ dash : str, default "-"
304
+ Separator between components
305
+
306
+ Returns
307
+ -------
308
+ str
309
+ LaTeX-formatted string for the system label
310
+
311
+ Examples
312
+ --------
313
+ >>> syslab(["K2O", "Al2O3", "SiO2", "H2O"])
314
+ '$K_{2}O-Al_{2}O_{3}-SiO_{2}-H_{2}O$'
315
+
316
+ >>> syslab(["CaO", "MgO", "SiO2"], dash="–")
317
+ '$CaO–MgO–SiO_{2}$'
318
+ """
319
+ if system is None:
320
+ system = ["K2O", "Al2O3", "SiO2", "H2O"]
321
+
322
+ # Format each component
323
+ formatted_components = []
324
+ for component in system:
325
+ formatted = _add_subscripts(component)
326
+ formatted_components.append(formatted)
327
+
328
+ # Join with separator
329
+ label = dash.join(formatted_components)
330
+
331
+ # Wrap in LaTeX math mode
332
+ return f"${label}$"
333
+
334
+
335
+ def syslab_html(system: list = None, dash: str = "-") -> str:
336
+ """
337
+ Create HTML-formatted text for thermodynamic system (for Plotly).
338
+
339
+ This generates a label showing the components of a thermodynamic system,
340
+ separated by dashes (or other separator), using HTML formatting compatible
341
+ with Plotly instead of LaTeX.
342
+
343
+ Parameters
344
+ ----------
345
+ system : list of str, optional
346
+ List of component formulas. Default: ["K2O", "Al2O3", "SiO2", "H2O"]
347
+ dash : str, default "-"
348
+ Separator between components
349
+
350
+ Returns
351
+ -------
352
+ str
353
+ HTML-formatted string for the system label
354
+
355
+ Examples
356
+ --------
357
+ >>> syslab_html(["K2O", "Al2O3", "SiO2", "H2O"])
358
+ 'K<sub>2</sub>O-Al<sub>2</sub>O<sub>3</sub>-SiO<sub>2</sub>-H<sub>2</sub>O'
359
+
360
+ >>> syslab_html(["CaO", "MgO", "SiO2"], dash="–")
361
+ 'CaO–MgO–SiO<sub>2</sub>'
362
+
363
+ Notes
364
+ -----
365
+ Use this function instead of syslab() when creating titles for interactive
366
+ (Plotly) diagrams. The HTML formatting is compatible with Plotly's rendering.
367
+
368
+ Requires: WORMutils (for chemlabel)
369
+ """
370
+ if not _HTML_DEPS_AVAILABLE:
371
+ raise ImportError(
372
+ "syslab_html() requires 'WORMutils' package.\n"
373
+ "Install with: pip install WORMutils"
374
+ )
375
+
376
+ if system is None:
377
+ system = ["K2O", "Al2O3", "SiO2", "H2O"]
378
+
379
+ # Format each component using HTML via chemlabel
380
+ formatted_components = []
381
+ for component in system:
382
+ formatted = chemlabel(component)
383
+ formatted_components.append(formatted)
384
+
385
+ # Join with separator (no HTML wrapper needed)
386
+ label = dash.join(formatted_components)
387
+
388
+ return label
389
+
390
+
391
+ def expr_species(formula: str, state: Optional[str] = None, use_state: bool = False) -> str:
392
+ """
393
+ Format a chemical species formula for display.
394
+
395
+ This is a simplified version that returns LaTeX-formatted strings
396
+ suitable for matplotlib. The R version returns plotmath expressions.
397
+
398
+ Parameters
399
+ ----------
400
+ formula : str
401
+ Chemical formula
402
+ state : str, optional
403
+ Physical state (aq, cr, gas, liq)
404
+ use_state : bool, default False
405
+ Whether to include state in the formatted output
406
+
407
+ Returns
408
+ -------
409
+ str
410
+ LaTeX-formatted formula string
411
+
412
+ Examples
413
+ --------
414
+ >>> expr_species("H2O")
415
+ '$H_{2}O$'
416
+
417
+ >>> expr_species("Ca+2")
418
+ '$Ca^{2+}$'
419
+
420
+ >>> expr_species("SO4-2")
421
+ '$SO_{4}^{2-}$'
422
+ """
423
+ formatted = _format_species_latex(formula)
424
+
425
+ if use_state and state:
426
+ # Add state subscript
427
+ return f"${formatted}_{{{state}}}$"
428
+ else:
429
+ return f"${formatted}$"
430
+
431
+
432
+ def describe_property(property: list = None, value: list = None,
433
+ digits: int = 0, oneline: bool = False,
434
+ ret_val: bool = False) -> list:
435
+ """
436
+ Create formatted text describing thermodynamic properties and their values.
437
+
438
+ This function generates formatted strings for displaying property-value pairs
439
+ in legends, typically for temperature, pressure, and other conditions.
440
+
441
+ Parameters
442
+ ----------
443
+ property : list of str
444
+ Property names (e.g., ["T", "P"])
445
+ value : list
446
+ Property values (e.g., [300, 1000])
447
+ digits : int, default 0
448
+ Number of decimal places to display
449
+ oneline : bool, default False
450
+ If True, combine all properties on one line (not implemented)
451
+ ret_val : bool, default False
452
+ If True, return only values with units (not property names)
453
+
454
+ Returns
455
+ -------
456
+ list of str
457
+ Formatted property descriptions
458
+
459
+ Examples
460
+ --------
461
+ >>> describe_property(["T", "P"], [300, 1000])
462
+ ['$T$ = 300 °C', '$P$ = 1000 bar']
463
+
464
+ >>> describe_property(["T"], [25], digits=1)
465
+ ['$T$ = 25.0 °C']
466
+
467
+ Notes
468
+ -----
469
+ This is used to create legend entries showing the conditions
470
+ used in thermodynamic calculations.
471
+ """
472
+ if property is None or value is None:
473
+ raise ValueError("property or value is None")
474
+
475
+ descriptions = []
476
+
477
+ for i in range(len(property)):
478
+ prop = property[i]
479
+ val = value[i]
480
+
481
+ # Get property symbol
482
+ if prop == "T":
483
+ prop_str = "$T$"
484
+ if val == "Psat" or val == "NA":
485
+ val_str = "$P_{sat}$"
486
+ else:
487
+ val_formatted = format(round(float(val), digits), f'.{digits}f')
488
+ val_str = f"{val_formatted} °C"
489
+ elif prop == "P":
490
+ prop_str = "$P$"
491
+ if val == "Psat" or val == "NA":
492
+ val_str = "$P_{sat}$"
493
+ else:
494
+ val_formatted = format(round(float(val), digits), f'.{digits}f')
495
+ val_str = f"{val_formatted} bar"
496
+ elif prop == "pH":
497
+ prop_str = "pH"
498
+ val_formatted = format(round(float(val), digits), f'.{digits}f')
499
+ val_str = val_formatted
500
+ elif prop == "Eh":
501
+ prop_str = "Eh"
502
+ val_formatted = format(round(float(val), digits), f'.{digits}f')
503
+ val_str = f"{val_formatted} V"
504
+ elif prop == "IS":
505
+ prop_str = "$IS$"
506
+ val_formatted = format(round(float(val), digits), f'.{digits}f')
507
+ val_str = val_formatted
508
+ else:
509
+ prop_str = f"${prop}$"
510
+ val_formatted = format(round(float(val), digits), f'.{digits}f')
511
+ val_str = val_formatted
512
+
513
+ if ret_val:
514
+ descriptions.append(val_str)
515
+ else:
516
+ descriptions.append(f"{prop_str} = {val_str}")
517
+
518
+ return descriptions
519
+
520
+
521
+ def describe_basis(ibasis: list = None, digits: int = 1,
522
+ oneline: bool = False, molality: bool = False,
523
+ use_pH: bool = True) -> list:
524
+ """
525
+ Create formatted text describing basis species activities/fugacities.
526
+
527
+ This function generates formatted strings for displaying the chemical
528
+ activities or fugacities of basis species, typically for plot legends.
529
+
530
+ Parameters
531
+ ----------
532
+ ibasis : list of int, optional
533
+ Indices of basis species to describe (1-based). If None, describes all.
534
+ digits : int, default 1
535
+ Number of decimal places to display
536
+ oneline : bool, default False
537
+ If True, combine all species on one line (not fully implemented)
538
+ molality : bool, default False
539
+ If True, use molality (m) instead of activity (a)
540
+ use_pH : bool, default True
541
+ If True, display H+ as pH instead of log a_H+
542
+
543
+ Returns
544
+ -------
545
+ list of str
546
+ Formatted basis species descriptions
547
+
548
+ Examples
549
+ --------
550
+ >>> from pychnosz.core.basis import basis
551
+ >>> basis(["H2O", "H+", "O2"], [-10, -7, -80])
552
+ >>> describe_basis([2, 3])
553
+ ['pH = 7.0', 'log $f_{O_2}$ = -80.0']
554
+
555
+ >>> describe_basis() # All basis species
556
+ ['log $a_{H_2O}$ = -10.0', 'pH = 7.0', 'log $f_{O_2}$ = -80.0']
557
+
558
+ Notes
559
+ -----
560
+ This is used to create legend entries showing the basis species
561
+ activities used in thermodynamic calculations.
562
+ """
563
+ from ..core.basis import get_basis
564
+
565
+ basis_df = get_basis()
566
+ if basis_df is None:
567
+ raise RuntimeError("Basis species are not defined")
568
+
569
+ # Default to all basis species
570
+ if ibasis is None:
571
+ ibasis = list(range(1, len(basis_df) + 1))
572
+
573
+ # Convert to 0-based indexing
574
+ ibasis_0 = [i - 1 for i in ibasis]
575
+
576
+ descriptions = []
577
+
578
+ for i in ibasis_0:
579
+ species_name = basis_df.index[i]
580
+ state = basis_df.iloc[i]['state']
581
+ logact = basis_df.iloc[i]['logact']
582
+
583
+ # Check if logact is numeric
584
+ try:
585
+ logact_val = float(logact)
586
+ is_numeric = True
587
+ except (ValueError, TypeError):
588
+ is_numeric = False
589
+
590
+ if is_numeric:
591
+ # Handle H+ specially with pH
592
+ if species_name == "H+" and use_pH:
593
+ pH_val = -logact_val
594
+ val_formatted = format(round(pH_val, digits), f'.{digits}f')
595
+ descriptions.append(f"pH = {val_formatted}")
596
+ else:
597
+ # Format the activity/fugacity
598
+ val_formatted = format(round(logact_val, digits), f'.{digits}f')
599
+
600
+ # Determine if it's activity or fugacity based on state
601
+ if state in ['aq', 'liq', 'cr']:
602
+ a_or_f = "a" if not molality else "m"
603
+ else:
604
+ a_or_f = "f"
605
+
606
+ # Format the species name
607
+ species_formatted = _format_species_latex(species_name)
608
+
609
+ descriptions.append(f"log ${a_or_f}_{{{species_formatted}}}$ = {val_formatted}")
610
+ else:
611
+ # Non-numeric value (buffer)
612
+ if species_name == "H+" and use_pH:
613
+ descriptions.append(f"pH = {logact}")
614
+ else:
615
+ # For buffers, just show the buffer name
616
+ if state in ['aq', 'liq', 'cr']:
617
+ a_or_f = "a" if not molality else "m"
618
+ else:
619
+ a_or_f = "f"
620
+
621
+ species_formatted = _format_species_latex(species_name)
622
+ descriptions.append(f"${a_or_f}_{{{species_formatted}}}$ = {logact}")
623
+
624
+ return descriptions
625
+
626
+
627
+ def describe_property_html(property: list = None, value: list = None,
628
+ digits: int = 0, oneline: bool = False,
629
+ ret_val: bool = False) -> list:
630
+ """
631
+ Create HTML-formatted text describing thermodynamic properties (for Plotly).
632
+
633
+ This function generates HTML-formatted strings for displaying thermodynamic
634
+ properties and their values, typically for plot legends in interactive diagrams.
635
+
636
+ Parameters
637
+ ----------
638
+ property : list of str
639
+ Property names (e.g., ["T", "P"])
640
+ value : list
641
+ Property values
642
+ digits : int, default 0
643
+ Number of decimal places to display
644
+ oneline : bool, default False
645
+ If True, format on one line (not implemented)
646
+ ret_val : bool, default False
647
+ If True, return only values without property names
648
+
649
+ Returns
650
+ -------
651
+ list of str
652
+ HTML-formatted property descriptions
653
+
654
+ Examples
655
+ --------
656
+ >>> describe_property_html(["T", "P"], [300, 1000])
657
+ ['<i>T</i> = 300 °C', '<i>P</i> = 1000 bar']
658
+
659
+ Notes
660
+ -----
661
+ Use this instead of describe_property() when creating legends for
662
+ interactive (Plotly) diagrams.
663
+ """
664
+ if property is None or value is None:
665
+ raise ValueError("property or value is None")
666
+
667
+ descriptions = []
668
+
669
+ for i in range(len(property)):
670
+ prop = property[i]
671
+ val = value[i]
672
+
673
+ # Get property symbol (HTML format)
674
+ if prop == "T":
675
+ prop_str = "<i>T</i>"
676
+ if val == "Psat" or val == "NA":
677
+ val_str = "<i>P</i><sub>sat</sub>"
678
+ else:
679
+ val_formatted = format(round(float(val), digits), f'.{digits}f')
680
+ val_str = f"{val_formatted} °C"
681
+ elif prop == "P":
682
+ prop_str = "<i>P</i>"
683
+ if val == "Psat" or val == "NA":
684
+ val_str = "<i>P</i><sub>sat</sub>"
685
+ else:
686
+ val_formatted = format(round(float(val), digits), f'.{digits}f')
687
+ val_str = f"{val_formatted} bar"
688
+ elif prop == "pH":
689
+ prop_str = "pH"
690
+ val_formatted = format(round(float(val), digits), f'.{digits}f')
691
+ val_str = val_formatted
692
+ elif prop == "Eh":
693
+ prop_str = "Eh"
694
+ val_formatted = format(round(float(val), digits), f'.{digits}f')
695
+ val_str = f"{val_formatted} V"
696
+ elif prop == "IS":
697
+ prop_str = "<i>IS</i>"
698
+ val_formatted = format(round(float(val), digits), f'.{digits}f')
699
+ val_str = val_formatted
700
+ else:
701
+ prop_str = f"<i>{prop}</i>"
702
+ val_formatted = format(round(float(val), digits), f'.{digits}f')
703
+ val_str = val_formatted
704
+
705
+ if ret_val:
706
+ descriptions.append(val_str)
707
+ else:
708
+ descriptions.append(f"{prop_str} = {val_str}")
709
+
710
+ return descriptions
711
+
712
+
713
+ def describe_basis_html(ibasis: list = None, digits: int = 1,
714
+ oneline: bool = False, molality: bool = False,
715
+ use_pH: bool = True) -> list:
716
+ """
717
+ Create HTML-formatted text describing basis species (for Plotly).
718
+
719
+ This function generates HTML-formatted strings for displaying the chemical
720
+ activities or fugacities of basis species, typically for plot legends in
721
+ interactive diagrams.
722
+
723
+ Parameters
724
+ ----------
725
+ ibasis : list of int, optional
726
+ Indices of basis species to describe (1-based). If None, describes all.
727
+ digits : int, default 1
728
+ Number of decimal places to display
729
+ oneline : bool, default False
730
+ If True, combine all species on one line (not fully implemented)
731
+ molality : bool, default False
732
+ If True, use molality (m) instead of activity (a)
733
+ use_pH : bool, default True
734
+ If True, display H+ as pH instead of log a_H+
735
+
736
+ Returns
737
+ -------
738
+ list of str
739
+ HTML-formatted basis species descriptions
740
+
741
+ Examples
742
+ --------
743
+ >>> from pychnosz.core.basis import basis
744
+ >>> basis(["H2O", "H+", "O2"], [-10, -7, -80])
745
+ >>> describe_basis_html([2, 3])
746
+ ['pH = 7.0', 'log <i>f</i><sub>O<sub>2</sub></sub> = -80.0']
747
+
748
+ >>> describe_basis_html([4]) # CO2
749
+ ['log <i>f</i><sub>CO<sub>2</sub></sub> = -1.0']
750
+
751
+ Notes
752
+ -----
753
+ Use this instead of describe_basis() when creating legends for
754
+ interactive (Plotly) diagrams.
755
+ """
756
+ if not _HTML_DEPS_AVAILABLE:
757
+ raise ImportError(
758
+ "describe_basis_html() requires 'WORMutils' package.\n"
759
+ "Install with: pip install WORMutils"
760
+ )
761
+
762
+ from ..core.basis import get_basis
763
+
764
+ basis_df = get_basis()
765
+ if basis_df is None:
766
+ raise RuntimeError("Basis species are not defined")
767
+
768
+ # Default to all basis species
769
+ if ibasis is None:
770
+ ibasis = list(range(1, len(basis_df) + 1))
771
+
772
+ # Convert to 0-based indexing
773
+ ibasis_0 = [i - 1 for i in ibasis]
774
+
775
+ descriptions = []
776
+
777
+ for i in ibasis_0:
778
+ species_name = basis_df.index[i]
779
+ state = basis_df.iloc[i]['state']
780
+ logact = basis_df.iloc[i]['logact']
781
+
782
+ # Check if logact is numeric
783
+ try:
784
+ logact_val = float(logact)
785
+ is_numeric = True
786
+ except (ValueError, TypeError):
787
+ is_numeric = False
788
+
789
+ if is_numeric:
790
+ # Handle H+ specially with pH
791
+ if species_name == "H+" and use_pH:
792
+ pH_val = -logact_val
793
+ val_formatted = format(round(pH_val, digits), f'.{digits}f')
794
+ descriptions.append(f"pH = {val_formatted}")
795
+ else:
796
+ # Format the activity/fugacity
797
+ val_formatted = format(round(logact_val, digits), f'.{digits}f')
798
+
799
+ # Determine if it's activity or fugacity based on state
800
+ if state in ['aq', 'liq', 'cr']:
801
+ a_or_f = "a" if not molality else "m"
802
+ else:
803
+ a_or_f = "f"
804
+
805
+ # Format the species name using HTML
806
+ species_formatted = chemlabel(species_name)
807
+
808
+ descriptions.append(f"log <i>{a_or_f}</i><sub>{species_formatted}</sub> = {val_formatted}")
809
+ else:
810
+ # Non-numeric value (buffer)
811
+ if species_name == "H+" and use_pH:
812
+ descriptions.append(f"pH = {logact}")
813
+ else:
814
+ # For buffers, just show the buffer name
815
+ if state in ['aq', 'liq', 'cr']:
816
+ a_or_f = "a" if not molality else "m"
817
+ else:
818
+ a_or_f = "f"
819
+
820
+ species_formatted = chemlabel(species_name)
821
+ descriptions.append(f"<i>{a_or_f}</i><sub>{species_formatted}</sub> = {logact}")
822
+
823
+ return descriptions
824
+
825
+
826
+ def add_legend(ax, labels: list = None, loc: str = 'best',
827
+ frameon: bool = False, fontsize: float = 9, **kwargs):
828
+ """
829
+ Add a legend to a diagram with matplotlib or Plotly formatting.
830
+
831
+ This is a convenience function that adds a legend with sensible
832
+ defaults matching R CHNOSZ legend styling. Works with both matplotlib
833
+ and Plotly figures.
834
+
835
+ Parameters
836
+ ----------
837
+ ax : matplotlib.axes.Axes or plotly.graph_objs.Figure
838
+ Axes/Figure object to add legend to. For interactive diagrams,
839
+ pass the figure from d['fig'] or d['ax'].
840
+ labels : list of str
841
+ Legend labels (can be from describe_property, describe_basis, etc.)
842
+ loc : str, default 'best'
843
+ Legend location. Options: 'best', 'upper left', 'upper right',
844
+ 'lower left', 'lower right', 'right', 'center left', 'center right',
845
+ 'lower center', 'upper center', 'center'
846
+ For Plotly: 'best' defaults to 'lower right'
847
+ frameon : bool, default False
848
+ Whether to draw a frame around the legend (R bty="n" equivalent)
849
+ fontsize : float, default 9
850
+ Font size for legend text (R cex=0.9 equivalent)
851
+ **kwargs
852
+ Additional arguments passed to matplotlib legend() or Plotly annotation
853
+
854
+ Returns
855
+ -------
856
+ matplotlib.legend.Legend or plotly.graph_objs.Figure
857
+ The legend object (matplotlib) or the figure (Plotly)
858
+
859
+ Examples
860
+ --------
861
+ >>> from pychnosz.utils.expression import add_legend, describe_property
862
+ >>> # Matplotlib diagram with plot_it=False
863
+ >>> d1 = diagram(a, interactive=False, plot_it=False)
864
+ >>> dprop = describe_property(["T", "P"], [300, 1000])
865
+ >>> add_legend(d1['ax'], dprop, loc='lower right')
866
+ >>> # Display the figure in Jupyter:
867
+ >>> from IPython.display import display
868
+ >>> display(d1['fig'])
869
+ >>> # Or save it:
870
+ >>> d1['fig'].savefig('diagram.png')
871
+
872
+ >>> # Plotly diagram
873
+ >>> d1 = diagram(a, interactive=True, plot_it=False)
874
+ >>> dprop = describe_property(["T", "P"], [300, 1000])
875
+ >>> add_legend(d1['fig'], dprop, loc='lower right')
876
+ >>> d1['fig'].show()
877
+
878
+ Notes
879
+ -----
880
+ Common R legend locations and their matplotlib equivalents:
881
+ - "bottomright" → "lower right"
882
+ - "topleft" → "upper left"
883
+ - "topright" → "upper right"
884
+ - "bottomleft" → "lower left"
885
+
886
+ When using plot_it=False, you need to explicitly display the figure after
887
+ adding legends. In Jupyter notebooks, use display(d['fig']) or d['fig'].show()
888
+ for Plotly diagrams. Outside Jupyter, use plt.show() or save with d['fig'].savefig().
889
+ """
890
+ if labels is None:
891
+ raise ValueError("labels must be provided")
892
+
893
+ # Detect if this is a Plotly figure
894
+ is_plotly = _is_plotly_figure(ax)
895
+
896
+ if is_plotly:
897
+ return _add_plotly_legend(ax, labels, loc, frameon, fontsize, **kwargs)
898
+ else:
899
+ return _add_matplotlib_legend(ax, labels, loc, frameon, fontsize, **kwargs)
900
+
901
+
902
+ def _is_plotly_figure(fig):
903
+ """Check if object is a Plotly figure."""
904
+ return hasattr(fig, 'add_annotation') and hasattr(fig, 'update_layout')
905
+
906
+
907
+ def _add_matplotlib_legend(ax, labels, loc, frameon, fontsize, **kwargs):
908
+ """Add legend to matplotlib axes."""
909
+ # Handle R-style location names
910
+ loc_map = {
911
+ 'bottomright': 'lower right',
912
+ 'bottomleft': 'lower left',
913
+ 'topleft': 'upper left',
914
+ 'topright': 'upper right',
915
+ 'bottom': 'lower center',
916
+ 'top': 'upper center',
917
+ 'left': 'center left',
918
+ 'right': 'center right'
919
+ }
920
+
921
+ # Convert R-style location to matplotlib style
922
+ if loc.lower() in loc_map:
923
+ loc = loc_map[loc.lower()]
924
+
925
+ # Create legend with text-only labels (no symbols)
926
+ # This matches R's legend behavior when just providing character vectors
927
+ # We need to create invisible handles for matplotlib to work properly
928
+ from matplotlib.patches import Rectangle
929
+
930
+ # Create invisible (alpha=0) dummy handles for each label
931
+ handles = [Rectangle((0, 0), 1, 1, fc="white", ec="white", alpha=0)
932
+ for _ in labels]
933
+
934
+ # Create legend with invisible handles and no spacing
935
+ legend = ax.legend(handles, labels, loc=loc, frameon=frameon,
936
+ fontsize=fontsize, handlelength=0, handletextpad=0,
937
+ **kwargs)
938
+
939
+ return legend
940
+
941
+
942
+ def _add_plotly_legend(fig, labels, loc, frameon, fontsize, **kwargs):
943
+ """Add legend-style annotation to Plotly figure."""
944
+ # Handle R-style location names and map to Plotly coordinates
945
+ loc_map = {
946
+ 'best': 'lower right',
947
+ 'bottomright': 'lower right',
948
+ 'bottomleft': 'lower left',
949
+ 'topleft': 'upper left',
950
+ 'topright': 'upper right',
951
+ 'bottom': 'lower center',
952
+ 'top': 'upper center',
953
+ 'left': 'center left',
954
+ 'right': 'center right',
955
+ 'lower right': 'lower right',
956
+ 'lower left': 'lower left',
957
+ 'upper left': 'upper left',
958
+ 'upper right': 'upper right',
959
+ 'lower center': 'lower center',
960
+ 'upper center': 'upper center',
961
+ 'center left': 'center left',
962
+ 'center right': 'center right',
963
+ 'center': 'center'
964
+ }
965
+
966
+ # Normalize location
967
+ loc_normalized = loc_map.get(loc.lower(), 'lower right')
968
+
969
+ # Map to Plotly anchor and position coordinates
970
+ # Format: (x, y, xanchor, yanchor)
971
+ plotly_positions = {
972
+ 'upper left': (0.02, 0.98, 'left', 'top'),
973
+ 'upper right': (0.98, 0.98, 'right', 'top'),
974
+ 'lower left': (0.02, 0.02, 'left', 'bottom'),
975
+ 'lower right': (0.98, 0.02, 'right', 'bottom'),
976
+ 'upper center': (0.5, 0.98, 'center', 'top'),
977
+ 'lower center': (0.5, 0.02, 'center', 'bottom'),
978
+ 'center left': (0.02, 0.5, 'left', 'middle'),
979
+ 'center right': (0.98, 0.5, 'right', 'middle'),
980
+ 'center': (0.5, 0.5, 'center', 'middle')
981
+ }
982
+
983
+ x, y, xanchor, yanchor = plotly_positions[loc_normalized]
984
+
985
+ # Build legend text
986
+ legend_text = '<br>'.join(labels)
987
+
988
+ # Add annotation as legend
989
+ fig.add_annotation(
990
+ x=x,
991
+ y=y,
992
+ xref='paper',
993
+ yref='paper',
994
+ text=legend_text,
995
+ showarrow=False,
996
+ xanchor=xanchor,
997
+ yanchor=yanchor,
998
+ font=dict(size=fontsize),
999
+ bgcolor='rgba(255, 255, 255, 0.8)' if not frameon else 'rgba(255, 255, 255, 1)',
1000
+ bordercolor='black' if frameon else 'rgba(0, 0, 0, 0)',
1001
+ borderwidth=1 if frameon else 0,
1002
+ borderpad=4,
1003
+ align='left'
1004
+ )
1005
+
1006
+ return fig
1007
+
1008
+
1009
+ def set_title(ax_or_fig, title: str, fontsize: float = 12, **kwargs):
1010
+ """
1011
+ Set title on a matplotlib axes or Plotly figure.
1012
+
1013
+ This function provides a unified interface for setting titles on both
1014
+ matplotlib and Plotly plots, allowing seamless switching between
1015
+ interactive=True and interactive=False.
1016
+
1017
+ Parameters
1018
+ ----------
1019
+ ax_or_fig : matplotlib.axes.Axes or plotly.graph_objs.Figure
1020
+ Axes or Figure object to set title on
1021
+ title : str
1022
+ The title text
1023
+ fontsize : float, default 12
1024
+ Font size for the title
1025
+ **kwargs
1026
+ Additional arguments passed to matplotlib set_title() or Plotly update_layout()
1027
+
1028
+ Returns
1029
+ -------
1030
+ matplotlib.text.Text or plotly.graph_objs.Figure
1031
+ The title object (matplotlib) or the figure (Plotly)
1032
+
1033
+ Examples
1034
+ --------
1035
+ >>> from pychnosz.utils.expression import set_title, syslab
1036
+ >>> # Matplotlib diagram
1037
+ >>> d1 = diagram(a, interactive=False, plot_it=False)
1038
+ >>> title_text = syslab(["H2O", "CO2", "CaO", "MgO", "SiO2"])
1039
+ >>> set_title(d1['ax'], title_text, fontsize=12)
1040
+ >>> # Display the figure in Jupyter:
1041
+ >>> from IPython.display import display
1042
+ >>> display(d1['fig'])
1043
+
1044
+ >>> # Plotly diagram
1045
+ >>> d1 = diagram(a, interactive=True, plot_it=False)
1046
+ >>> title_text = syslab_html(["H2O", "CO2", "CaO", "MgO", "SiO2"])
1047
+ >>> set_title(d1['ax'], title_text, fontsize=12)
1048
+ >>> d1['fig'].show()
1049
+
1050
+ Notes
1051
+ -----
1052
+ When using plot_it=False, you need to explicitly display the figure after
1053
+ setting the title. In Jupyter notebooks, use display(d['fig']) or d['fig'].show()
1054
+ for Plotly diagrams. Outside Jupyter, use plt.show() or save with d['fig'].savefig().
1055
+ """
1056
+ is_plotly = _is_plotly_figure(ax_or_fig)
1057
+
1058
+ if is_plotly:
1059
+ # Plotly figure
1060
+ title_dict = {'text': title, 'x': 0.5, 'xanchor': 'center'}
1061
+ if fontsize:
1062
+ title_dict['font'] = {'size': fontsize}
1063
+ ax_or_fig.update_layout(title=title_dict, **kwargs)
1064
+ return ax_or_fig
1065
+ else:
1066
+ # Matplotlib axes
1067
+ return ax_or_fig.set_title(title, fontsize=fontsize, **kwargs)
1068
+
1069
+
1070
+ # Export main functions
1071
+ __all__ = ['ratlab', 'ratlab_html', 'expr_species', 'syslab', 'syslab_html',
1072
+ 'describe_property', 'describe_property_html',
1073
+ 'describe_basis', 'describe_basis_html',
1074
+ 'add_legend', 'set_title']