pychnosz.utils.expression API documentation
Description: Expression utilities for formatted labels and text …

# Module `pychnosz.utils.expression`

Expression utilities for formatted labels and text.

This module provides Python equivalents of the R functions in util.expression.R: - ratlab(): Create formatted text for activity ratios - expr_species(): Format chemical formula for display - syslab(): Create formatted text for thermodynamic systems

Author: CHNOSZ Python port

## Functions

`def add_legend(ax,
labels: list = None,
loc: str = 'best',
frameon: bool = False,
fontsize: float = 9,
**kwargs)`
  Expand source code

      def add_legend(ax, labels: list = None, loc: str = 'best',
                    frameon: bool = False, fontsize: float = 9, **kwargs):
          """
          Add a legend to a diagram with matplotlib or Plotly formatting.

          This is a convenience function that adds a legend with sensible
          defaults matching R CHNOSZ legend styling. Works with both matplotlib
          and Plotly figures.

          Parameters
          ----------
          ax : matplotlib.axes.Axes or plotly.graph_objs.Figure
              Axes/Figure object to add legend to. For interactive diagrams,
              pass the figure from d['fig'] or d['ax'].
          labels : list of str
              Legend labels (can be from describe_property, describe_basis, etc.)
          loc : str, default 'best'
              Legend location. Options: 'best', 'upper left', 'upper right',
              'lower left', 'lower right', 'right', 'center left', 'center right',
              'lower center', 'upper center', 'center'
              For Plotly: 'best' defaults to 'lower right'
          frameon : bool, default False
              Whether to draw a frame around the legend (R bty="n" equivalent)
          fontsize : float, default 9
              Font size for legend text (R cex=0.9 equivalent)
          **kwargs
              Additional arguments passed to matplotlib legend() or Plotly annotation

          Returns
          -------
          matplotlib.legend.Legend or plotly.graph_objs.Figure
              The legend object (matplotlib) or the figure (Plotly)

          Examples
          --------
          >>> from pychnosz.utils.expression import add_legend, describe_property
          >>> # Matplotlib diagram with plot_it=False
          >>> d1 = diagram(a, interactive=False, plot_it=False)
          >>> dprop = describe_property(["T", "P"], [300, 1000])
          >>> add_legend(d1['ax'], dprop, loc='lower right')
          >>> # Display the figure in Jupyter:
          >>> from IPython.display import display
          >>> display(d1['fig'])
          >>> # Or save it:
          >>> d1['fig'].savefig('diagram.png')

          >>> # Plotly diagram
          >>> d1 = diagram(a, interactive=True, plot_it=False)
          >>> dprop = describe_property(["T", "P"], [300, 1000])
          >>> add_legend(d1['fig'], dprop, loc='lower right')
          >>> d1['fig'].show()

          Notes
          -----
          Common R legend locations and their matplotlib equivalents:
          - "bottomright" → "lower right"
          - "topleft" → "upper left"
          - "topright" → "upper right"
          - "bottomleft" → "lower left"

          When using plot_it=False, you need to explicitly display the figure after
          adding legends. In Jupyter notebooks, use display(d['fig']) or d['fig'].show()
          for Plotly diagrams. Outside Jupyter, use plt.show() or save with d['fig'].savefig().
          """
          if labels is None:
              raise ValueError("labels must be provided")

          # Detect if this is a Plotly figure
          is_plotly = _is_plotly_figure(ax)

          if is_plotly:
              return _add_plotly_legend(ax, labels, loc, frameon, fontsize, **kwargs)
          else:
              return _add_matplotlib_legend(ax, labels, loc, frameon, fontsize, **kwargs)

  Add a legend to a diagram with matplotlib or Plotly formatting.

  This is a convenience function that adds a legend with sensible defaults matching R CHNOSZ legend styling. Works with both matplotlib and Plotly figures.

  ## Parameters

  `ax` :`matplotlib.axes.Axes` or `plotly.graph_objs.Figure`
    Axes/Figure object to add legend to. For interactive diagrams, pass the figure from d['fig'] or d['ax'].
  `labels` :`list` of `str`
    Legend labels (can be from describe_property, describe_basis, etc.)
  `loc` :`str`, default `'best'`
    Legend location. Options: 'best', 'upper left', 'upper right', 'lower left', 'lower right', 'right', 'center left', 'center right', 'lower center', 'upper center', 'center' For Plotly: 'best' defaults to 'lower right'
  `frameon` :`bool`, default `False`
    Whether to draw a frame around the legend (R bty="n" equivalent)
  `fontsize` :`float`, default `9`
    Font size for legend text (R cex=0.9 equivalent)
  `**kwargs`
    Additional arguments passed to matplotlib legend() or Plotly annotation

  ## Returns

  `matplotlib.legend.Legend` or `plotly.graph_objs.Figure`
    The legend object (matplotlib) or the figure (Plotly)

  ## Examples

      >>> from pychnosz.utils.expression import add_legend, describe_property
      >>> # Matplotlib diagram with plot_it=False
      >>> d1 = diagram(a, interactive=False, plot_it=False)
      >>> dprop = describe_property(["T", "P"], [300, 1000])
      >>> add_legend(d1['ax'], dprop, loc='lower right')
      >>> # Display the figure in Jupyter:
      >>> from IPython.display import display
      >>> display(d1['fig'])
      >>> # Or save it:
      >>> d1['fig'].savefig('diagram.png')

      >>> # Plotly diagram
      >>> d1 = diagram(a, interactive=True, plot_it=False)
      >>> dprop = describe_property(["T", "P"], [300, 1000])
      >>> add_legend(d1['fig'], dprop, loc='lower right')
      >>> d1['fig'].show()

  ## Notes

  Common R legend locations and their matplotlib equivalents: - "bottomright" → "lower right" - "topleft" → "upper left" - "topright" → "upper right" - "bottomleft" → "lower left"

  When using plot_it=False, you need to explicitly display the figure after adding legends. In Jupyter notebooks, use display(d['fig']) or d['fig'].show() for Plotly diagrams. Outside Jupyter, use plt.show() or save with d['fig'].savefig().

`def describe_basis(ibasis: list = None,
digits: int = 1,
oneline: bool = False,
molality: bool = False,
use_pH: bool = True) ‑> list`
  Expand source code

      def describe_basis(ibasis: list = None, digits: int = 1,
                        oneline: bool = False, molality: bool = False,
                        use_pH: bool = True) -> list:
          """
          Create formatted text describing basis species activities/fugacities.

          This function generates formatted strings for displaying the chemical
          activities or fugacities of basis species, typically for plot legends.

          Parameters
          ----------
          ibasis : list of int, optional
              Indices of basis species to describe (1-based). If None, describes all.
          digits : int, default 1
              Number of decimal places to display
          oneline : bool, default False
              If True, combine all species on one line (not fully implemented)
          molality : bool, default False
              If True, use molality (m) instead of activity (a)
          use_pH : bool, default True
              If True, display H+ as pH instead of log a_H+

          Returns
          -------
          list of str
              Formatted basis species descriptions

          Examples
          --------
          >>> from pychnosz.core.basis import basis
          >>> basis(["H2O", "H+", "O2"], [-10, -7, -80])
          >>> describe_basis([2, 3])
          ['pH = 7.0', 'log $f_{O_2}$ = -80.0']

          >>> describe_basis()  # All basis species
          ['log $a_{H_2O}$ = -10.0', 'pH = 7.0', 'log $f_{O_2}$ = -80.0']

          Notes
          -----
          This is used to create legend entries showing the basis species
          activities used in thermodynamic calculations.
          """
          from ..core.basis import get_basis

          basis_df = get_basis()
          if basis_df is None:
              raise RuntimeError("Basis species are not defined")

          # Default to all basis species
          if ibasis is None:
              ibasis = list(range(1, len(basis_df) + 1))

          # Convert to 0-based indexing
          ibasis_0 = [i - 1 for i in ibasis]

          descriptions = []

          for i in ibasis_0:
              species_name = basis_df.index[i]
              state = basis_df.iloc[i]['state']
              logact = basis_df.iloc[i]['logact']

              # Check if logact is numeric
              try:
                  logact_val = float(logact)
                  is_numeric = True
              except (ValueError, TypeError):
                  is_numeric = False

              if is_numeric:
                  # Handle H+ specially with pH
                  if species_name == "H+" and use_pH:
                      pH_val = -logact_val
                      val_formatted = format(round(pH_val, digits), f'.{digits}f')
                      descriptions.append(f"pH = {val_formatted}")
                  else:
                      # Format the activity/fugacity
                      val_formatted = format(round(logact_val, digits), f'.{digits}f')

                      # Determine if it's activity or fugacity based on state
                      if state in ['aq', 'liq', 'cr']:
                          a_or_f = "a" if not molality else "m"
                      else:
                          a_or_f = "f"

                      # Format the species name
                      species_formatted = _format_species_latex(species_name)

                      descriptions.append(f"log ${a_or_f}_{{{species_formatted}}}$ = {val_formatted}")
              else:
                  # Non-numeric value (buffer)
                  if species_name == "H+" and use_pH:
                      descriptions.append(f"pH = {logact}")
                  else:
                      # For buffers, just show the buffer name
                      if state in ['aq', 'liq', 'cr']:
                          a_or_f = "a" if not molality else "m"
                      else:
                          a_or_f = "f"

                      species_formatted = _format_species_latex(species_name)
                      descriptions.append(f"${a_or_f}_{{{species_formatted}}}$ = {logact}")

          return descriptions

  Create formatted text describing basis species activities/fugacities.

  This function generates formatted strings for displaying the chemical activities or fugacities of basis species, typically for plot legends.

  ## Parameters

  `ibasis` :`list` of `int`, optional
    Indices of basis species to describe (1-based). If None, describes all.
  `digits` :`int`, default `1`
    Number of decimal places to display
  `oneline` :`bool`, default `False`
    If True, combine all species on one line (not fully implemented)
  `molality` :`bool`, default `False`
    If True, use molality (m) instead of activity (a)
  `use_pH` :`bool`, default `True`
    If True, display H+ as pH instead of log a_H+

  ## Returns

  `list` of `str`
    Formatted basis species descriptions

  ## Examples

      >>> from pychnosz.core.basis import basis
      >>> basis(["H2O", "H+", "O2"], [-10, -7, -80])
      >>> describe_basis([2, 3])
      ['pH = 7.0', 'log $f_{O_2}$ = -80.0']

      >>> describe_basis()  # All basis species
      ['log $a_{H_2O}$ = -10.0', 'pH = 7.0', 'log $f_{O_2}$ = -80.0']

  ## Notes

  This is used to create legend entries showing the basis species activities used in thermodynamic calculations.

`def describe_basis_html(ibasis: list = None,
digits: int = 1,
oneline: bool = False,
molality: bool = False,
use_pH: bool = True) ‑> list`
  Expand source code

      def describe_basis_html(ibasis: list = None, digits: int = 1,
                              oneline: bool = False, molality: bool = False,
                              use_pH: bool = True) -> list:
          """
          Create HTML-formatted text describing basis species (for Plotly).

          This function generates HTML-formatted strings for displaying the chemical
          activities or fugacities of basis species, typically for plot legends in
          interactive diagrams.

          Parameters
          ----------
          ibasis : list of int, optional
              Indices of basis species to describe (1-based). If None, describes all.
          digits : int, default 1
              Number of decimal places to display
          oneline : bool, default False
              If True, combine all species on one line (not fully implemented)
          molality : bool, default False
              If True, use molality (m) instead of activity (a)
          use_pH : bool, default True
              If True, display H+ as pH instead of log a_H+

          Returns
          -------
          list of str
              HTML-formatted basis species descriptions

          Examples
          --------
          >>> from pychnosz.core.basis import basis
          >>> basis(["H2O", "H+", "O2"], [-10, -7, -80])
          >>> describe_basis_html([2, 3])
          ['pH = 7.0', 'log <i>f</i><sub>O<sub>2</sub></sub> = -80.0']

          >>> describe_basis_html([4])  # CO2
          ['log <i>f</i><sub>CO<sub>2</sub></sub> = -1.0']

          Notes
          -----
          Use this instead of describe_basis() when creating legends for
          interactive (Plotly) diagrams.
          """
          if not _HTML_DEPS_AVAILABLE:
              raise ImportError(
                  "describe_basis_html() requires 'WORMutils' package.\n"
                  "Install with: pip install WORMutils"
              )

          from ..core.basis import get_basis

          basis_df = get_basis()
          if basis_df is None:
              raise RuntimeError("Basis species are not defined")

          # Default to all basis species
          if ibasis is None:
              ibasis = list(range(1, len(basis_df) + 1))

          # Convert to 0-based indexing
          ibasis_0 = [i - 1 for i in ibasis]

          descriptions = []

          for i in ibasis_0:
              species_name = basis_df.index[i]
              state = basis_df.iloc[i]['state']
              logact = basis_df.iloc[i]['logact']

              # Check if logact is numeric
              try:
                  logact_val = float(logact)
                  is_numeric = True
              except (ValueError, TypeError):
                  is_numeric = False

              if is_numeric:
                  # Handle H+ specially with pH
                  if species_name == "H+" and use_pH:
                      pH_val = -logact_val
                      val_formatted = format(round(pH_val, digits), f'.{digits}f')
                      descriptions.append(f"pH = {val_formatted}")
                  else:
                      # Format the activity/fugacity
                      val_formatted = format(round(logact_val, digits), f'.{digits}f')

                      # Determine if it's activity or fugacity based on state
                      if state in ['aq', 'liq', 'cr']:
                          a_or_f = "a" if not molality else "m"
                      else:
                          a_or_f = "f"

                      # Format the species name using HTML
                      species_formatted = chemlabel(species_name)

                      descriptions.append(f"log <i>{a_or_f}</i><sub>{species_formatted}</sub> = {val_formatted}")
              else:
                  # Non-numeric value (buffer)
                  if species_name == "H+" and use_pH:
                      descriptions.append(f"pH = {logact}")
                  else:
                      # For buffers, just show the buffer name
                      if state in ['aq', 'liq', 'cr']:
                          a_or_f = "a" if not molality else "m"
                      else:
                          a_or_f = "f"

                      species_formatted = chemlabel(species_name)
                      descriptions.append(f"<i>{a_or_f}</i><sub>{species_formatted}</sub> = {logact}")

          return descriptions

  Create HTML-formatted text describing basis species (for Plotly).

  This function generates HTML-formatted strings for displaying the chemical activities or fugacities of basis species, typically for plot legends in interactive diagrams.

  ## Parameters

  `ibasis` :`list` of `int`, optional
    Indices of basis species to describe (1-based). If None, describes all.
  `digits` :`int`, default `1`
    Number of decimal places to display
  `oneline` :`bool`, default `False`
    If True, combine all species on one line (not fully implemented)
  `molality` :`bool`, default `False`
    If True, use molality (m) instead of activity (a)
  `use_pH` :`bool`, default `True`
    If True, display H+ as pH instead of log a_H+

  ## Returns

  `list` of `str`
    HTML-formatted basis species descriptions

  ## Examples

      >>> from pychnosz.core.basis import basis
      >>> basis(["H2O", "H+", "O2"], [-10, -7, -80])
      >>> describe_basis_html([2, 3])
      ['pH = 7.0', 'log <i>f</i><sub>O<sub>2</sub></sub> = -80.0']

      >>> describe_basis_html([4])  # CO2
      ['log <i>f</i><sub>CO<sub>2</sub></sub> = -1.0']

  ## Notes

  Use this instead of describe_basis() when creating legends for interactive (Plotly) diagrams.

`def describe_property(property: list = None,
value: list = None,
digits: int = 0,
oneline: bool = False,
ret_val: bool = False) ‑> list`
  Expand source code

      def describe_property(property: list = None, value: list = None,
                           digits: int = 0, oneline: bool = False,
                           ret_val: bool = False) -> list:
          """
          Create formatted text describing thermodynamic properties and their values.

          This function generates formatted strings for displaying property-value pairs
          in legends, typically for temperature, pressure, and other conditions.

          Parameters
          ----------
          property : list of str
              Property names (e.g., ["T", "P"])
          value : list
              Property values (e.g., [300, 1000])
          digits : int, default 0
              Number of decimal places to display
          oneline : bool, default False
              If True, combine all properties on one line (not implemented)
          ret_val : bool, default False
              If True, return only values with units (not property names)

          Returns
          -------
          list of str
              Formatted property descriptions

          Examples
          --------
          >>> describe_property(["T", "P"], [300, 1000])
          ['$T$ = 300 °C', '$P$ = 1000 bar']

          >>> describe_property(["T"], [25], digits=1)
          ['$T$ = 25.0 °C']

          Notes
          -----
          This is used to create legend entries showing the conditions
          used in thermodynamic calculations.
          """
          if property is None or value is None:
              raise ValueError("property or value is None")

          descriptions = []

          for i in range(len(property)):
              prop = property[i]
              val = value[i]

              # Get property symbol
              if prop == "T":
                  prop_str = "$T$"
                  if val == "Psat" or val == "NA":
                      val_str = "$P_{sat}$"
                  else:
                      val_formatted = format(round(float(val), digits), f'.{digits}f')
                      val_str = f"{val_formatted} °C"
              elif prop == "P":
                  prop_str = "$P$"
                  if val == "Psat" or val == "NA":
                      val_str = "$P_{sat}$"
                  else:
                      val_formatted = format(round(float(val), digits), f'.{digits}f')
                      val_str = f"{val_formatted} bar"
              elif prop == "pH":
                  prop_str = "pH"
                  val_formatted = format(round(float(val), digits), f'.{digits}f')
                  val_str = val_formatted
              elif prop == "Eh":
                  prop_str = "Eh"
                  val_formatted = format(round(float(val), digits), f'.{digits}f')
                  val_str = f"{val_formatted} V"
              elif prop == "IS":
                  prop_str = "$IS$"
                  val_formatted = format(round(float(val), digits), f'.{digits}f')
                  val_str = val_formatted
              else:
                  prop_str = f"${prop}$"
                  val_formatted = format(round(float(val), digits), f'.{digits}f')
                  val_str = val_formatted

              if ret_val:
                  descriptions.append(val_str)
              else:
                  descriptions.append(f"{prop_str} = {val_str}")

          return descriptions

  Create formatted text describing thermodynamic properties and their values.

  This function generates formatted strings for displaying property-value pairs in legends, typically for temperature, pressure, and other conditions.

  ## Parameters

  `property` :`list` of `str`
    Property names (e.g., ["T", "P"])
  `value` :`list`
    Property values (e.g., [300, 1000])
  `digits` :`int`, default `0`
    Number of decimal places to display
  `oneline` :`bool`, default `False`
    If True, combine all properties on one line (not implemented)
  `ret_val` :`bool`, default `False`
    If True, return only values with units (not property names)

  ## Returns

  `list` of `str`
    Formatted property descriptions

  ## Examples

      >>> describe_property(["T", "P"], [300, 1000])
      ['$T$ = 300 °C', '$P$ = 1000 bar']

      >>> describe_property(["T"], [25], digits=1)
      ['$T$ = 25.0 °C']

  ## Notes

  This is used to create legend entries showing the conditions used in thermodynamic calculations.

`def describe_property_html(property: list = None,
value: list = None,
digits: int = 0,
oneline: bool = False,
ret_val: bool = False) ‑> list`
  Expand source code

      def describe_property_html(property: list = None, value: list = None,
                                 digits: int = 0, oneline: bool = False,
                                 ret_val: bool = False) -> list:
          """
          Create HTML-formatted text describing thermodynamic properties (for Plotly).

          This function generates HTML-formatted strings for displaying thermodynamic
          properties and their values, typically for plot legends in interactive diagrams.

          Parameters
          ----------
          property : list of str
              Property names (e.g., ["T", "P"])
          value : list
              Property values
          digits : int, default 0
              Number of decimal places to display
          oneline : bool, default False
              If True, format on one line (not implemented)
          ret_val : bool, default False
              If True, return only values without property names

          Returns
          -------
          list of str
              HTML-formatted property descriptions

          Examples
          --------
          >>> describe_property_html(["T", "P"], [300, 1000])
          ['<i>T</i> = 300 °C', '<i>P</i> = 1000 bar']

          Notes
          -----
          Use this instead of describe_property() when creating legends for
          interactive (Plotly) diagrams.
          """
          if property is None or value is None:
              raise ValueError("property or value is None")

          descriptions = []

          for i in range(len(property)):
              prop = property[i]
              val = value[i]

              # Get property symbol (HTML format)
              if prop == "T":
                  prop_str = "<i>T</i>"
                  if val == "Psat" or val == "NA":
                      val_str = "<i>P</i><sub>sat</sub>"
                  else:
                      val_formatted = format(round(float(val), digits), f'.{digits}f')
                      val_str = f"{val_formatted} °C"
              elif prop == "P":
                  prop_str = "<i>P</i>"
                  if val == "Psat" or val == "NA":
                      val_str = "<i>P</i><sub>sat</sub>"
                  else:
                      val_formatted = format(round(float(val), digits), f'.{digits}f')
                      val_str = f"{val_formatted} bar"
              elif prop == "pH":
                  prop_str = "pH"
                  val_formatted = format(round(float(val), digits), f'.{digits}f')
                  val_str = val_formatted
              elif prop == "Eh":
                  prop_str = "Eh"
                  val_formatted = format(round(float(val), digits), f'.{digits}f')
                  val_str = f"{val_formatted} V"
              elif prop == "IS":
                  prop_str = "<i>IS</i>"
                  val_formatted = format(round(float(val), digits), f'.{digits}f')
                  val_str = val_formatted
              else:
                  prop_str = f"<i>{prop}</i>"
                  val_formatted = format(round(float(val), digits), f'.{digits}f')
                  val_str = val_formatted

              if ret_val:
                  descriptions.append(val_str)
              else:
                  descriptions.append(f"{prop_str} = {val_str}")

          return descriptions

  Create HTML-formatted text describing thermodynamic properties (for Plotly).

  This function generates HTML-formatted strings for displaying thermodynamic properties and their values, typically for plot legends in interactive diagrams.

  ## Parameters

  `property` :`list` of `str`
    Property names (e.g., ["T", "P"])
  `value` :`list`
    Property values
  `digits` :`int`, default `0`
    Number of decimal places to display
  `oneline` :`bool`, default `False`
    If True, format on one line (not implemented)
  `ret_val` :`bool`, default `False`
    If True, return only values without property names

  ## Returns

  `list` of `str`
    HTML-formatted property descriptions

  ## Examples

      >>> describe_property_html(["T", "P"], [300, 1000])
      ['<i>T</i> = 300 °C', '<i>P</i> = 1000 bar']

  ## Notes

  Use this instead of describe_property() when creating legends for interactive (Plotly) diagrams.

`def expr_species(formula: str, state: str | None = None, use_state: bool = False) ‑> str`
  Expand source code

      def expr_species(formula: str, state: Optional[str] = None, use_state: bool = False) -> str:
          """
          Format a chemical species formula for display.

          This is a simplified version that returns LaTeX-formatted strings
          suitable for matplotlib. The R version returns plotmath expressions.

          Parameters
          ----------
          formula : str
              Chemical formula
          state : str, optional
              Physical state (aq, cr, gas, liq)
          use_state : bool, default False
              Whether to include state in the formatted output

          Returns
          -------
          str
              LaTeX-formatted formula string

          Examples
          --------
          >>> expr_species("H2O")
          '$H_{2}O$'

          >>> expr_species("Ca+2")
          '$Ca^{2+}$'

          >>> expr_species("SO4-2")
          '$SO_{4}^{2-}$'
          """
          formatted = _format_species_latex(formula)

          if use_state and state:
              # Add state subscript
              return f"${formatted}_{{{state}}}$"
          else:
              return f"${formatted}$"

  Format a chemical species formula for display.

  This is a simplified version that returns LaTeX-formatted strings suitable for matplotlib. The R version returns plotmath expressions.

  ## Parameters

  `formula` :`str`
    Chemical formula
  `state` :`str`, optional
    Physical state (aq, cr, gas, liq)
  `use_state` :`bool`, default `False`
    Whether to include state in the formatted output

  ## Returns

  `str`
    LaTeX-formatted formula string

  ## Examples

      >>> expr_species("H2O")
      '$H_{2}O$'

      >>> expr_species("Ca+2")
      '$Ca^{2+}$'

      >>> expr_species("SO4-2")
      '$SO_{4}^{2-}$'

`def ratlab(top: str = 'K+',
bottom: str = 'H+',
molality: bool = False,
reverse_charge: bool = False) ‑> str`
  Expand source code

      def ratlab(top: str = "K+", bottom: str = "H+", molality: bool = False,
                 reverse_charge: bool = False) -> str:
          """
          Create formatted text label for activity ratio.

          This function generates a LaTeX-formatted string suitable for use as
          axis labels in matplotlib plots, showing the ratio of activities of
          two ions raised to appropriate powers based on their charges.

          Parameters
          ----------
          top : str, default "K+"
              Chemical formula for the numerator ion
          bottom : str, default "H+"
              Chemical formula for the denominator ion
          molality : bool, default False
              If True, use 'm' (molality) instead of 'a' (activity)
          reverse_charge : bool, default False
              If True, reverse charge order in formatting (e.g., "Fe+3" becomes "Fe^{3+}")
              If False, keep original order (e.g., "Fe+3" becomes "Fe^{+3}")

          Returns
          -------
          str
              LaTeX-formatted string for the activity ratio label

          Examples
          --------
          >>> ratlab("K+", "H+")
          'log($a_{K^{+}}$ / $a_{H^{+}}$)'

          >>> ratlab("Ca+2", "H+")
          'log($a_{Ca^{+2}}$ / $a_{H^{+}}^{2}$)'

          >>> ratlab("Ca+2", "H+", reverse_charge=True)
          'log($a_{Ca^{2+}}$ / $a_{H^{+}}^{2}$)'

          >>> ratlab("Mg+2", "Ca+2")
          'log($a_{Mg^{+2}}$ / $a_{Ca^{+2}}$)'

          Notes
          -----
          The exponents are determined by the charges of the ions to maintain
          charge balance in the ratio. For example, for Ca+2/H+, the H+ term
          is squared because Ca has a +2 charge.

          The output format is compatible with matplotlib's LaTeX rendering.
          In R CHNOSZ, this uses plotmath expressions; here we use LaTeX strings
          that matplotlib can render.
          """
          # Get the charges of the ions
          makeup_top = makeup(top)
          makeup_bottom = makeup(bottom)

          Z_top = makeup_top.get('Z', 0)
          Z_bottom = makeup_bottom.get('Z', 0)

          # The exponents for charge balance
          # If top has charge +2 and bottom has +1, bottom gets exponent 2
          exp_bottom = abs(Z_top)
          exp_top = abs(Z_bottom)

          # Format exponents (don't show if = 1)
          exp_top_str = "" if exp_top == 1 else f"^{{{int(exp_top)}}}"
          exp_bottom_str = "" if exp_bottom == 1 else f"^{{{int(exp_bottom)}}}"

          # Format the ion formulas for display
          top_formatted = _format_species_latex(top, reverse_charge=reverse_charge)
          bottom_formatted = _format_species_latex(bottom, reverse_charge=reverse_charge)

          # Choose activity or molality symbol
          a = "m" if molality else "a"

          # Build the expression
          # Format: log(a_top^exp / a_bottom^exp)
          numerator = f"${a}_{{{top_formatted}}}{exp_top_str}$"
          denominator = f"${a}_{{{bottom_formatted}}}{exp_bottom_str}$"

          label = f"log({numerator} / {denominator})"

          return label

  Create formatted text label for activity ratio.

  This function generates a LaTeX-formatted string suitable for use as axis labels in matplotlib plots, showing the ratio of activities of two ions raised to appropriate powers based on their charges.

  ## Parameters

  `top` :`str`, default `"K+"`
    Chemical formula for the numerator ion
  `bottom` :`str`, default `"H+"`
    Chemical formula for the denominator ion
  `molality` :`bool`, default `False`
    If True, use 'm' (molality) instead of 'a' (activity)
  `reverse_charge` :`bool`, default `False`
    If True, reverse charge order in formatting (e.g., "Fe+3" becomes "Fe^{3+}") If False, keep original order (e.g., "Fe+3" becomes "Fe^{+3}")

  ## Returns

  `str`
    LaTeX-formatted string for the activity ratio label

  ## Examples

      >>> ratlab("K+", "H+")
      'log($a_{K^{+}}$ / $a_{H^{+}}$)'

      >>> ratlab("Ca+2", "H+")
      'log($a_{Ca^{+2}}$ / $a_{H^{+}}^{2}$)'

      >>> ratlab("Ca+2", "H+", reverse_charge=True)
      'log($a_{Ca^{2+}}$ / $a_{H^{+}}^{2}$)'

      >>> ratlab("Mg+2", "Ca+2")
      'log($a_{Mg^{+2}}$ / $a_{Ca^{+2}}$)'

  ## Notes

  The exponents are determined by the charges of the ions to maintain charge balance in the ratio. For example, for Ca+2/H+, the H+ term is squared because Ca has a +2 charge.

  The output format is compatible with matplotlib's LaTeX rendering. In R CHNOSZ, this uses plotmath expressions; here we use LaTeX strings that matplotlib can render.

`def ratlab_html(top: str = 'K+', bottom: str = 'H+', molality: bool = False) ‑> str`
  Expand source code

      def ratlab_html(top: str = "K+", bottom: str = "H+", molality: bool = False) -> str:
          """
          Create HTML-formatted text label for activity ratio (for Plotly/HTML rendering).

          This function generates an HTML-formatted string suitable for use with
          Plotly interactive plots, showing the ratio of activities of two ions
          raised to appropriate powers based on their charges.

          This is a companion function to ratlab() which produces LaTeX format for
          matplotlib. Use ratlab_html() when creating labels for diagram(..., interactive=True).

          Parameters
          ----------
          top : str, default "K+"
              Chemical formula for the numerator ion
          bottom : str, default "H+"
              Chemical formula for the denominator ion
          molality : bool, default False
              If True, use 'm' (molality) instead of 'a' (activity)

          Returns
          -------
          str
              HTML-formatted string for the activity ratio label

          Examples
          --------
          >>> ratlab_html("K+", "H+")
          'log(a<sub>K<sup>+</sup></sub>/a<sub>H<sup>+</sup></sub>)'

          >>> ratlab_html("Ca+2", "H+")
          'log(a<sub>Ca<sup>2+</sup></sub>/a<sup>2</sup><sub>H<sup>+</sup></sub>)'

          >>> ratlab_html("Mg+2", "Ca+2")
          'log(a<sub>Mg<sup>2+</sup></sub>/a<sub>Ca<sup>2+</sup></sub>)'

          Notes
          -----
          The exponents are determined by the charges of the ions to maintain
          charge balance in the ratio. For example, for Ca+2/H+, the H+ term
          is squared because Ca has a +2 charge.

          The output format uses HTML tags (<sub>, <sup>) compatible with Plotly.
          For matplotlib plots with LaTeX rendering, use ratlab() instead.

          Requires: WORMutils (for chemlabel) and chemparse (for parse_formula)

          See Also
          --------
          ratlab : LaTeX version for matplotlib
          """
          if not _HTML_DEPS_AVAILABLE:
              raise ImportError(
                  "ratlab_html() requires 'WORMutils' and 'chemparse' packages.\n"
                  "Install with: pip install WORMutils chemparse"
              )

          # Parse the formulas to get charges
          top_formula = parse_formula(top)
          if "+" in top_formula.keys():
              top_charge = top_formula["+"]
          elif "-" in top_formula.keys():
              top_charge = -top_formula["-"]
          else:
              raise ValueError("Cannot create an ion ratio involving one or more neutral species.")

          bottom_formula = parse_formula(bottom)
          if "+" in bottom_formula.keys():
              bottom_charge = bottom_formula["+"]
          elif "-" in bottom_formula.keys():
              bottom_charge = -bottom_formula["-"]
          else:
              raise ValueError("Cannot create an ion ratio involving one or more neutral species.")

          # Convert to integers if whole numbers
          if top_charge.is_integer():
              top_charge = int(top_charge)

          if bottom_charge.is_integer():
              bottom_charge = int(bottom_charge)

          # The exponents for charge balance
          # If top has charge +2 and bottom has +1, bottom gets exponent 2
          exp_bottom = abs(top_charge)
          exp_top = abs(bottom_charge)

          # Format exponents as superscripts (don't show if = 1)
          if exp_top != 1:
              top_exp_str = "<sup>" + str(exp_top) + "</sup>"
          else:
              top_exp_str = ""

          if exp_bottom != 1:
              bottom_exp_str = "<sup>" + str(exp_bottom) + "</sup>"
          else:
              bottom_exp_str = ""

          # Choose activity or molality symbol
          if molality:
              sym = "m"
          else:
              sym = "a"

          # Format the chemical formulas with chemlabel
          top_formatted = chemlabel(top)
          bottom_formatted = chemlabel(bottom)

          # Build the HTML expression
          # Format: log(a_top^exp / a_bottom^exp)
          return f"log({sym}{top_exp_str}<sub>{top_formatted}</sub>/{sym}{bottom_exp_str}<sub>{bottom_formatted}</sub>)"

  Create HTML-formatted text label for activity ratio (for Plotly/HTML rendering).

  This function generates an HTML-formatted string suitable for use with Plotly interactive plots, showing the ratio of activities of two ions raised to appropriate powers based on their charges.

  This is a companion function to ratlab() which produces LaTeX format for matplotlib. Use ratlab_html() when creating labels for diagram(…, interactive=True).

  ## Parameters

  `top` :`str`, default `"K+"`
    Chemical formula for the numerator ion
  `bottom` :`str`, default `"H+"`
    Chemical formula for the denominator ion
  `molality` :`bool`, default `False`
    If True, use 'm' (molality) instead of 'a' (activity)

  ## Returns

  `str`
    HTML-formatted string for the activity ratio label

  ## Examples

      >>> ratlab_html("K+", "H+")
      'log(a<sub>K<sup>+</sup></sub>/a<sub>H<sup>+</sup></sub>)'

      >>> ratlab_html("Ca+2", "H+")
      'log(a<sub>Ca<sup>2+</sup></sub>/a<sup>2</sup><sub>H<sup>+</sup></sub>)'

      >>> ratlab_html("Mg+2", "Ca+2")
      'log(a<sub>Mg<sup>2+</sup></sub>/a<sub>Ca<sup>2+</sup></sub>)'

  ## Notes

  The exponents are determined by the charges of the ions to maintain charge balance in the ratio. For example, for Ca+2/H+, the H+ term is squared because Ca has a +2 charge.

  The output format uses HTML tags (, ) compatible with Plotly. For matplotlib plots with LaTeX rendering, use ratlab() instead.

  Requires: WORMutils (for chemlabel) and chemparse (for parse_formula)

  ## See Also

  `ratlab()`
    LaTeX version for matplotlib
`def set_title(ax_or_fig, title: str, fontsize: float = 12, **kwargs)`
  Expand source code

      def set_title(ax_or_fig, title: str, fontsize: float = 12, **kwargs):
          """
          Set title on a matplotlib axes or Plotly figure.

          This function provides a unified interface for setting titles on both
          matplotlib and Plotly plots, allowing seamless switching between
          interactive=True and interactive=False.

          Parameters
          ----------
          ax_or_fig : matplotlib.axes.Axes or plotly.graph_objs.Figure
              Axes or Figure object to set title on
          title : str
              The title text
          fontsize : float, default 12
              Font size for the title
          **kwargs
              Additional arguments passed to matplotlib set_title() or Plotly update_layout()

          Returns
          -------
          matplotlib.text.Text or plotly.graph_objs.Figure
              The title object (matplotlib) or the figure (Plotly)

          Examples
          --------
          >>> from pychnosz.utils.expression import set_title, syslab
          >>> # Matplotlib diagram
          >>> d1 = diagram(a, interactive=False, plot_it=False)
          >>> title_text = syslab(["H2O", "CO2", "CaO", "MgO", "SiO2"])
          >>> set_title(d1['ax'], title_text, fontsize=12)
          >>> # Display the figure in Jupyter:
          >>> from IPython.display import display
          >>> display(d1['fig'])

          >>> # Plotly diagram
          >>> d1 = diagram(a, interactive=True, plot_it=False)
          >>> title_text = syslab_html(["H2O", "CO2", "CaO", "MgO", "SiO2"])
          >>> set_title(d1['ax'], title_text, fontsize=12)
          >>> d1['fig'].show()

          Notes
          -----
          When using plot_it=False, you need to explicitly display the figure after
          setting the title. In Jupyter notebooks, use display(d['fig']) or d['fig'].show()
          for Plotly diagrams. Outside Jupyter, use plt.show() or save with d['fig'].savefig().
          """
          is_plotly = _is_plotly_figure(ax_or_fig)

          if is_plotly:
              # Plotly figure
              title_dict = {'text': title, 'x': 0.5, 'xanchor': 'center'}
              if fontsize:
                  title_dict['font'] = {'size': fontsize}
              ax_or_fig.update_layout(title=title_dict, **kwargs)
              return ax_or_fig
          else:
              # Matplotlib axes
              return ax_or_fig.set_title(title, fontsize=fontsize, **kwargs)

  Set title on a matplotlib axes or Plotly figure.

  This function provides a unified interface for setting titles on both matplotlib and Plotly plots, allowing seamless switching between interactive=True and interactive=False.

  ## Parameters

  `ax_or_fig` :`matplotlib.axes.Axes` or `plotly.graph_objs.Figure`
    Axes or Figure object to set title on
  `title` :`str`
    The title text
  `fontsize` :`float`, default `12`
    Font size for the title
  `**kwargs`
    Additional arguments passed to matplotlib set_title() or Plotly update_layout()

  ## Returns

  `matplotlib.text.Text` or `plotly.graph_objs.Figure`
    The title object (matplotlib) or the figure (Plotly)

  ## Examples

      >>> from pychnosz.utils.expression import set_title, syslab
      >>> # Matplotlib diagram
      >>> d1 = diagram(a, interactive=False, plot_it=False)
      >>> title_text = syslab(["H2O", "CO2", "CaO", "MgO", "SiO2"])
      >>> set_title(d1['ax'], title_text, fontsize=12)
      >>> # Display the figure in Jupyter:
      >>> from IPython.display import display
      >>> display(d1['fig'])

      >>> # Plotly diagram
      >>> d1 = diagram(a, interactive=True, plot_it=False)
      >>> title_text = syslab_html(["H2O", "CO2", "CaO", "MgO", "SiO2"])
      >>> set_title(d1['ax'], title_text, fontsize=12)
      >>> d1['fig'].show()

  ## Notes

  When using plot_it=False, you need to explicitly display the figure after setting the title. In Jupyter notebooks, use display(d['fig']) or d['fig'].show() for Plotly diagrams. Outside Jupyter, use plt.show() or save with d['fig'].savefig().

`def syslab(system: list = None, dash: str = '-') ‑> str`
  Expand source code

      def syslab(system: list = None, dash: str = "-") -> str:
          """
          Create formatted text for thermodynamic system.

          This generates a label showing the components of a thermodynamic system,
          separated by dashes (or other separator).

          Parameters
          ----------
          system : list of str, optional
              List of component formulas. Default: ["K2O", "Al2O3", "SiO2", "H2O"]
          dash : str, default "-"
              Separator between components

          Returns
          -------
          str
              LaTeX-formatted string for the system label

          Examples
          --------
          >>> syslab(["K2O", "Al2O3", "SiO2", "H2O"])
          '$K_{2}O-Al_{2}O_{3}-SiO_{2}-H_{2}O$'

          >>> syslab(["CaO", "MgO", "SiO2"], dash="–")
          '$CaO–MgO–SiO_{2}$'
          """
          if system is None:
              system = ["K2O", "Al2O3", "SiO2", "H2O"]

          # Format each component
          formatted_components = []
          for component in system:
              formatted = _add_subscripts(component)
              formatted_components.append(formatted)

          # Join with separator
          label = dash.join(formatted_components)

          # Wrap in LaTeX math mode
          return f"${label}$"

  Create formatted text for thermodynamic system.

  This generates a label showing the components of a thermodynamic system, separated by dashes (or other separator).

  ## Parameters

  `system` :`list` of `str`, optional
    List of component formulas. Default: ["K2O", "Al2O3", "SiO2", "H2O"]
  `dash` :`str`, default `"-"`
    Separator between components

  ## Returns

  `str`
    LaTeX-formatted string for the system label

  ## Examples

      >>> syslab(["K2O", "Al2O3", "SiO2", "H2O"])
      '$K_{2}O-Al_{2}O_{3}-SiO_{2}-H_{2}O$'

      >>> syslab(["CaO", "MgO", "SiO2"], dash="–")
      '$CaO–MgO–SiO_{2}$'

`def syslab_html(system: list = None, dash: str = '-') ‑> str`
  Expand source code

      def syslab_html(system: list = None, dash: str = "-") -> str:
          """
          Create HTML-formatted text for thermodynamic system (for Plotly).

          This generates a label showing the components of a thermodynamic system,
          separated by dashes (or other separator), using HTML formatting compatible
          with Plotly instead of LaTeX.

          Parameters
          ----------
          system : list of str, optional
              List of component formulas. Default: ["K2O", "Al2O3", "SiO2", "H2O"]
          dash : str, default "-"
              Separator between components

          Returns
          -------
          str
              HTML-formatted string for the system label

          Examples
          --------
          >>> syslab_html(["K2O", "Al2O3", "SiO2", "H2O"])
          'K<sub>2</sub>O-Al<sub>2</sub>O<sub>3</sub>-SiO<sub>2</sub>-H<sub>2</sub>O'

          >>> syslab_html(["CaO", "MgO", "SiO2"], dash="–")
          'CaO–MgO–SiO<sub>2</sub>'

          Notes
          -----
          Use this function instead of syslab() when creating titles for interactive
          (Plotly) diagrams. The HTML formatting is compatible with Plotly's rendering.

          Requires: WORMutils (for chemlabel)
          """
          if not _HTML_DEPS_AVAILABLE:
              raise ImportError(
                  "syslab_html() requires 'WORMutils' package.\n"
                  "Install with: pip install WORMutils"
              )

          if system is None:
              system = ["K2O", "Al2O3", "SiO2", "H2O"]

          # Format each component using HTML via chemlabel
          formatted_components = []
          for component in system:
              formatted = chemlabel(component)
              formatted_components.append(formatted)

          # Join with separator (no HTML wrapper needed)
          label = dash.join(formatted_components)

          return label

  Create HTML-formatted text for thermodynamic system (for Plotly).

  This generates a label showing the components of a thermodynamic system, separated by dashes (or other separator), using HTML formatting compatible with Plotly instead of LaTeX.

  ## Parameters

  `system` :`list` of `str`, optional
    List of component formulas. Default: ["K2O", "Al2O3", "SiO2", "H2O"]
  `dash` :`str`, default `"-"`
    Separator between components

  ## Returns

  `str`
    HTML-formatted string for the system label

  ## Examples

      >>> syslab_html(["K2O", "Al2O3", "SiO2", "H2O"])
      'K<sub>2</sub>O-Al<sub>2</sub>O<sub>3</sub>-SiO<sub>2</sub>-H<sub>2</sub>O'

      >>> syslab_html(["CaO", "MgO", "SiO2"], dash="–")
      'CaO–MgO–SiO<sub>2</sub>'

  ## Notes

  Use this function instead of syslab() when creating titles for interactive (Plotly) diagrams. The HTML formatting is compatible with Plotly's rendering.

  Requires: WORMutils (for chemlabel)