[READ-ONLY] Mirror of https://github.com/jmrplens/PyOctaveBand. [Python3] Octave-Band and Fractional Octave-Band filter. For signal in time domain. jmrplens.github.io/PyOctaveBand/
acoustics audio filter frequency frequency-analysis frequency-domain octave python3 signal time-domain
0

Configure Feed

Select the types of activity you want to include in your feed.

Localize the shared plot helpers for Spanish (#257)

The frequency/band axes, the shifted-reference rating figure, the
band-level bar chart, the facade x-axis, the ISO 717-2 500 Hz annotation
and the time axis in _plot/common.py are reused by every domain's
.plot() renderer. They previously always drew their axis labels and
legend entries in English, so several ES renderers ended up with a
mixed-language figure (an English "Frequency [Hz]" xlabel next to a
Spanish title), and a few had no override at all for it.

I give these shared helpers their own language keyword and small
_STRINGS/_t table, and thread language=language through every call
site across the domain plot modules. I also drop the now-redundant
post-hoc "if language == 'es': ax.set_xlabel(...)" patches that some
renderers used to work around the missing parameter.

English output is unchanged (_t is a no-op for language="en"), verified
against the committed documentation figures.

authored by

José M. Requena Plens and committed by
GitHub
(Jul 20, 2026, 3:26 PM +0200) 438f7868 7298c3d6

+374 -64
+8
CHANGELOG.md
··· 35 35 axis labels, titles and legends in Spanish and switches linear-axis tick 36 36 decimals to a comma; the English output is unchanged. An unsupported code 37 37 raises a clear `ValueError`. 38 + - The frequency/band axes, the shifted-reference rating figure, the 39 + band-level bar chart, the façade x-axis, the ISO 717-2 500 Hz annotation 40 + and the time axis shared by every domain's `.plot()` renderers now 41 + localise their own axis labels and legend entries for `language="es"` 42 + ("Frequency [Hz]" to "Frecuencia [Hz]", "Measured" to "Medido", "Band" to 43 + "Banda", and so on), instead of leaving them in English whenever a domain 44 + renderer relied on the shared helper for that label. English output is 45 + unchanged. 38 46 - IEC 61260-1 filter class compliance can now be exported as a one-page PDF 39 47 fiche. A new `filter_class_compliance(bank, *, num_points=..., edition=...)` 40 48 runs the same verification as `verify_filter_class` and returns a frozen
+217
tests/test_plot_common_i18n.py
··· 1 + # Copyright (c) 2026. Jose M. Requena-Plens 2 + """EN/ES internationalisation of the shared ``_plot/common.py`` renderers. 3 + 4 + These low-level helpers (frequency/band axes, the shifted-reference rating 5 + figure, the band-level bar chart, the facade x-axis, the ISO 717-2 500 Hz 6 + annotation and the time axis) are reused by every ``_plot/<domain>.py`` 7 + module. Each domain module owns its own titles and legends, but the axis 8 + labels and legend entries these *shared* helpers draw themselves must also 9 + localise to Spanish once a domain's ``plot(language="es")`` reaches them. 10 + 11 + English stays byte-identical to the pre-i18n renderers (``_t`` is a no-op for 12 + ``language="en"``), which is verified separately by the committed 13 + documentation figures via ``scripts/check_figures.py``. 14 + """ 15 + 16 + from __future__ import annotations 17 + 18 + import numpy as np 19 + import pytest 20 + 21 + pytest.importorskip("matplotlib") 22 + import matplotlib 23 + 24 + matplotlib.use("Agg") 25 + import matplotlib.pyplot as plt # noqa: E402 26 + 27 + from phonometry._plot.common import ( # noqa: E402 28 + _annotate_impact_500, 29 + _band_axis, 30 + _facade_x_axis, 31 + _freq_axis, 32 + _plot_band_level_bars, 33 + _plot_rating, 34 + _time_axis, 35 + ) 36 + 37 + 38 + def _axes(): 39 + _fig, ax = plt.subplots() 40 + return ax 41 + 42 + 43 + def _legend_texts(ax) -> list[str]: 44 + legend = ax.get_legend() 45 + return [] if legend is None else [t.get_text() for t in legend.get_texts()] 46 + 47 + 48 + def test_freq_axis_spanish_label() -> None: 49 + ax = _axes() 50 + _freq_axis(ax, np.array([125.0, 250.0, 500.0]), language="es") 51 + assert ax.get_xlabel() == "Frecuencia [Hz]" 52 + plt.close("all") 53 + 54 + 55 + def test_freq_axis_english_default_unchanged() -> None: 56 + ax = _axes() 57 + _freq_axis(ax, np.array([125.0, 250.0, 500.0])) 58 + assert ax.get_xlabel() == "Frequency [Hz]" 59 + plt.close("all") 60 + 61 + 62 + def test_band_axis_spanish_default_label() -> None: 63 + ax = _axes() 64 + _band_axis(ax, np.array([125.0, 250.0]), language="es") 65 + assert ax.get_xlabel() == "Frecuencia [Hz]" 66 + plt.close("all") 67 + 68 + 69 + def test_band_axis_spanish_band_xlabel() -> None: 70 + ax = _axes() 71 + _band_axis(ax, ["1", "2", "3"], xlabel="Band", language="es") 72 + assert ax.get_xlabel() == "Banda" 73 + plt.close("all") 74 + 75 + 76 + def test_band_axis_none_xlabel_left_untouched() -> None: 77 + ax = _axes() 78 + ax.set_xlabel("kept") 79 + _band_axis(ax, np.array([125.0, 250.0]), xlabel=None, language="es") 80 + assert ax.get_xlabel() == "kept" 81 + plt.close("all") 82 + 83 + 84 + def test_plot_rating_spanish_labels_and_legend() -> None: 85 + band_centers = np.array([125.0, 250.0, 500.0, 1000.0]) 86 + measured = np.array([30.0, 35.0, 40.0, 45.0]) 87 + reference = np.array([32.0, 34.0, 42.0, 44.0]) 88 + ax = _plot_rating( 89 + band_centers, measured, reference, 90 + impact=False, title="t", ylabel="y", ax=None, language="es", 91 + ) 92 + assert ax.get_xlabel() == "Frecuencia [Hz]" 93 + legend_texts = _legend_texts(ax) 94 + assert "Medido" in legend_texts 95 + assert "Referencia desplazada" in legend_texts 96 + plt.close("all") 97 + 98 + 99 + def test_plot_rating_unfavourable_deviations_localised() -> None: 100 + band_centers = np.array([125.0, 250.0, 500.0]) 101 + measured = np.array([50.0, 55.0, 60.0]) 102 + reference = np.array([40.0, 45.0, 50.0]) 103 + ax = _plot_rating( 104 + band_centers, measured, reference, 105 + impact=True, title="t", ylabel="y", ax=None, language="es", 106 + ) 107 + assert "Desviaciones desfavorables" in _legend_texts(ax) 108 + plt.close("all") 109 + 110 + 111 + def test_plot_rating_english_default_unchanged() -> None: 112 + band_centers = np.array([125.0, 250.0, 500.0]) 113 + measured = np.array([30.0, 35.0, 40.0]) 114 + reference = np.array([32.0, 34.0, 42.0]) 115 + ax = _plot_rating( 116 + band_centers, measured, reference, 117 + impact=False, title="t", ylabel="y", ax=None, 118 + ) 119 + assert ax.get_xlabel() == "Frequency [Hz]" 120 + legend_texts = _legend_texts(ax) 121 + assert "Measured" in legend_texts 122 + assert "Shifted reference" in legend_texts 123 + plt.close("all") 124 + 125 + 126 + def test_plot_band_level_bars_spanish_xlabel_and_comma_total() -> None: 127 + ax = _plot_band_level_bars( 128 + None, np.array([50.0, 55.0, 60.0]), np.array([125.0, 250.0, 500.0]), 129 + 62.3, ylabel="y", title="t", language="es", 130 + ) 131 + assert ax.get_xlabel() == "Frecuencia [Hz]" 132 + assert "total 62,3 dB" in _legend_texts(ax) 133 + plt.close("all") 134 + 135 + 136 + def test_plot_band_level_bars_band_xlabel_when_no_frequencies() -> None: 137 + ax = _plot_band_level_bars( 138 + None, np.array([50.0, 55.0]), None, 53.0, ylabel="y", title="t", 139 + language="es", 140 + ) 141 + assert ax.get_xlabel() == "Banda" 142 + plt.close("all") 143 + 144 + 145 + def test_plot_band_level_bars_english_default_unchanged() -> None: 146 + ax = _plot_band_level_bars( 147 + None, np.array([50.0, 55.0, 60.0]), np.array([125.0, 250.0, 500.0]), 148 + 62.3, ylabel="y", title="t", 149 + ) 150 + assert ax.get_xlabel() == "Frequency [Hz]" 151 + assert "total 62.3 dB" in _legend_texts(ax) 152 + plt.close("all") 153 + 154 + 155 + def test_facade_x_axis_frequency_path_spanish() -> None: 156 + ax = _axes() 157 + _facade_x_axis(ax, np.array([125.0, 250.0]), 2, language="es") 158 + assert ax.get_xlabel() == "Frecuencia [Hz]" 159 + plt.close("all") 160 + 161 + 162 + def test_facade_x_axis_band_index_path_spanish() -> None: 163 + ax = _axes() 164 + _facade_x_axis(ax, None, 3, language="es") 165 + assert ax.get_xlabel() == "Banda" 166 + tick_labels = [t.get_text() for t in ax.get_xticklabels()] 167 + assert tick_labels == ["Banda 1", "Banda 2", "Banda 3"] 168 + plt.close("all") 169 + 170 + 171 + def test_facade_x_axis_band_index_path_english_unchanged() -> None: 172 + ax = _axes() 173 + _facade_x_axis(ax, None, 2) 174 + assert ax.get_xlabel() == "Band" 175 + tick_labels = [t.get_text() for t in ax.get_xticklabels()] 176 + assert tick_labels == ["Band 1", "Band 2"] 177 + plt.close("all") 178 + 179 + 180 + def test_annotate_impact_500_spanish_legend_and_annotation() -> None: 181 + ax = _axes() 182 + band_centers = np.array([125.0, 250.0, 500.0, 1000.0]) 183 + reference = np.array([30.0, 35.0, 40.0, 45.0]) 184 + ax.plot(band_centers, reference) 185 + _annotate_impact_500(ax, band_centers, reference, 35, language="es") 186 + legend_texts = _legend_texts(ax) 187 + assert any("lectura 500 Hz" in t for t in legend_texts) 188 + annotation_texts = [child.get_text() for child in ax.texts] 189 + assert any("regla de octava" in t for t in annotation_texts) 190 + plt.close("all") 191 + 192 + 193 + def test_annotate_impact_500_english_default_unchanged() -> None: 194 + ax = _axes() 195 + band_centers = np.array([125.0, 250.0, 500.0, 1000.0]) 196 + reference = np.array([30.0, 35.0, 40.0, 45.0]) 197 + ax.plot(band_centers, reference) 198 + _annotate_impact_500(ax, band_centers, reference, 35) 199 + legend_texts = _legend_texts(ax) 200 + assert any("500 Hz read" in t for t in legend_texts) 201 + annotation_texts = [child.get_text() for child in ax.texts] 202 + assert any("octave rule" in t for t in annotation_texts) 203 + plt.close("all") 204 + 205 + 206 + def test_time_axis_spanish_labels() -> None: 207 + _, xlabel = _time_axis(100, 1000, language="es") 208 + assert xlabel == "Tiempo [s]" 209 + _, xlabel_no_fs = _time_axis(100, None, language="es") 210 + assert xlabel_no_fs == "Muestra" 211 + 212 + 213 + def test_time_axis_english_default_unchanged() -> None: 214 + _, xlabel = _time_axis(100, 1000) 215 + assert xlabel == "Time [s]" 216 + _, xlabel_no_fs = _time_axis(100, None) 217 + assert xlabel_no_fs == "Sample"
+30
tests/environmental/test_environmental_plot_i18n.py
··· 10 10 import numpy as np 11 11 import pytest 12 12 13 + from phonometry.environmental.ground_barriers import ground_effect 14 + from phonometry.environmental.outdoor_propagation import ( 15 + outdoor_propagation_attenuation, 16 + ) 13 17 from phonometry.environmental.wind_turbine_noise import wind_turbine_tonality 14 18 15 19 ··· 39 43 result = _result() 40 44 with pytest.raises(ValueError, match="Unknown language"): 41 45 result.plot(language="xx") 46 + plt.close("all") 47 + 48 + 49 + def test_plot_outdoor_attenuation_spanish_xaxis_label() -> None: 50 + # plot_outdoor_attenuation draws its per-band terms through the shared 51 + # ``_band_axis`` helper and sets no xlabel of its own, so the Spanish 52 + # label only appears once the language is threaded into that helper. 53 + result = outdoor_propagation_attenuation(100.0, 2.0, 2.0) 54 + ax = result.plot(language="es") 55 + assert ax.get_xlabel() == "Frecuencia [Hz]" 56 + plt.close("all") 57 + ax = result.plot() 58 + assert ax.get_xlabel() == "Frequency [Hz]" 59 + plt.close("all") 60 + 61 + 62 + def test_plot_spherical_ground_spanish_xaxis_label() -> None: 63 + # plot_spherical_ground draws its frequency axis through the shared 64 + # ``_freq_axis`` helper without overriding the xlabel afterwards. 65 + freqs = np.array([250.0, 500.0, 1000.0, 2000.0]) 66 + result = ground_effect(freqs, 2.0, 1.5, 20.0, flow_resistivity=2.0e4) 67 + ax = result.plot(language="es") 68 + assert ax.get_xlabel() == "Frecuencia [Hz]" 69 + plt.close("all") 70 + ax = result.plot() 71 + assert ax.get_xlabel() == "Frequency [Hz]" 42 72 plt.close("all")
+14 -7
src/phonometry/_plot/building.py
··· 287 287 ), 288 288 ylabel=_t("Sound reduction index [dB]", language), 289 289 ax=ax, 290 + language=language, 290 291 **kwargs, 291 292 ) 292 293 localize_axes(ax, language) ··· 331 332 ), 332 333 ylabel=_t("Impact sound pressure level [dB]", language), 333 334 ax=ax, 335 + language=language, 334 336 **kwargs, 335 337 ) 336 - _annotate_impact_500(ax, band_centers, reference, int(result.rating)) 338 + _annotate_impact_500( 339 + ax, band_centers, reference, int(result.rating), language=language 340 + ) 337 341 localize_axes(ax, language) 338 342 return ax 339 343 ··· 361 365 ax = ax if ax is not None else _new_axes() 362 366 dnt = np.asarray(result.d_2m_nt, dtype=np.float64) 363 367 n = dnt.size 364 - x = _facade_x_axis(ax, getattr(result, "frequencies", None), n) 368 + x = _facade_x_axis( 369 + ax, getattr(result, "frequencies", None), n, language=language 370 + ) 365 371 366 372 # D2m,nT first so it is lines[0]; other quantities follow when present. 367 373 curves = [("$D_{2m,nT}$", dnt)] ··· 410 416 ax = ax if ax is not None else _new_axes() 411 417 r_prime = np.asarray(result.r_prime, dtype=np.float64) 412 418 n = r_prime.size 413 - x = _facade_x_axis(ax, result.frequencies, n) 419 + x = _facade_x_axis(ax, result.frequencies, n, language=language) 414 420 415 421 for name, rp in result.element_r.items(): 416 422 ax.plot(x, np.asarray(rp, dtype=np.float64), "--", lw=0.9, alpha=0.6, label=name) ··· 465 471 opts: dict[str, Any] = {"color": "tab:red", "alpha": 0.8, "label": "$L_W$"} 466 472 opts.update(kwargs) 467 473 ax.bar(positions, l_w, **opts) 468 - _band_axis(ax, labels, xlabel=_t(_FREQ_LABEL, language)) 474 + _band_axis(ax, labels, xlabel=_t(_FREQ_LABEL, language), language=language) 469 475 470 476 if result.l_w_dba is not None: 471 477 ax.axhline( ··· 510 516 if result.frequencies is not None: 511 517 freqs = np.asarray(result.frequencies, dtype=np.float64) 512 518 ax.plot(freqs, k_ij, **kwargs) 513 - _freq_axis(ax, freqs) 519 + _freq_axis(ax, freqs, language=language) 514 520 else: 515 521 ax.plot(np.arange(k_ij.size), k_ij, **kwargs) 516 522 ax.set_xlabel(_t("Band index", language)) ··· 548 554 ax, result.power_level, result.frequencies, result.total_level, 549 555 ylabel=_t(r"Structure-borne power level $L_{Ws}$ [dB re 1 pW]", language), 550 556 title=_t("EN 15657 characteristic structure-borne sound power", language), 557 + language=language, 551 558 **kwargs, 552 559 ) 553 560 localize_axes(ax, language) ··· 764 771 kwargs.setdefault("color", _C_PRIMARY) 765 772 kwargs.setdefault("marker", "o") 766 773 ax.plot(freqs, u, **kwargs) 767 - _freq_axis(ax, freqs) 774 + _freq_axis(ax, freqs, language=language) 768 775 ax.set_ylabel(_t("Standard uncertainty u [dB]", language)) 769 776 ax.set_ylim(bottom=0.0) 770 777 quantity = _t("sigma_R95 upper limit", language) if result.upper_limit else "u" ··· 804 811 color=_C_SECONDARY, ms=9, mfc="none", mew=1.6, zorder=5, 805 812 label=_t("limit of measurement (> delta-L)", language), 806 813 ) 807 - _freq_axis(ax, freqs) 814 + _freq_axis(ax, freqs, language=language) 808 815 ax.set_ylabel(_t("Improvement of impact sound insulation delta-L [dB]", language)) 809 816 ax.set_ylim(bottom=0.0) 810 817 title = _t("ISO 16251-1 Floor-Covering Impact Sound Improvement", language)
+77 -22
src/phonometry/_plot/common.py
··· 77 77 _LABEL_TL_DB: Final = "Transmission loss [dB]" 78 78 _LABEL_RANGE_KM: Final = "Range [km]" 79 79 80 + #: Spanish translations of the handful of fixed strings these *shared* 81 + #: renderers set directly (axis labels, generic legend entries). Each 82 + #: ``_plot/<domain>.py`` module carries its own richer ``_STRINGS``/``_t`` for 83 + #: titles and quantity-specific text; ``_t`` here only covers what common.py 84 + #: itself draws, so a domain module that passes an already-localised string 85 + #: through (e.g. an ``xlabel`` built with its own ``_t``) gets it back 86 + #: unchanged — the lookup misses and falls back to the given text. English is 87 + #: always a no-op, so the byte-identical English guarantee holds. 88 + _STRINGS: dict[str, str] = { 89 + "Frequency [Hz]": "Frecuencia [Hz]", 90 + "Band": "Banda", 91 + "Measured": "Medido", 92 + "Shifted reference": "Referencia desplazada", 93 + "Unfavourable deviations": "Desviaciones desfavorables", 94 + "Time [s]": "Tiempo [s]", 95 + "Sample": "Muestra", 96 + } 97 + 98 + 99 + def _t(text: str, language: str = "en") -> str: 100 + """Localise a fixed string; English is returned verbatim (byte-identical).""" 101 + return _STRINGS.get(text, text) if language == "es" else text 102 + 80 103 81 104 def _import_pyplot() -> Any: 82 105 """Import :mod:`matplotlib.pyplot` lazily with an actionable error.""" ··· 102 125 return result 103 126 104 127 105 - def _freq_axis(ax: Axes, freqs: np.ndarray) -> None: 128 + def _freq_axis(ax: Axes, freqs: np.ndarray, *, language: str = "en") -> None: 106 129 """Configure a logarithmic frequency x-axis labelled with band centres.""" 107 130 import matplotlib.ticker as mticker 108 131 ··· 112 135 # Suppress the log-scale minor-tick labels (2x10^2, 3x10^2, ...) that would 113 136 # otherwise collide with off-decade band centres such as 750 or 1500 Hz. 114 137 ax.xaxis.set_minor_formatter(mticker.NullFormatter()) 115 - ax.set_xlabel("Frequency [Hz]") 138 + ax.set_xlabel(_t("Frequency [Hz]", language)) 116 139 117 140 118 141 def _format_freq(f: float) -> str: ··· 194 217 labels_or_freqs: "np.ndarray | Sequence[str] | Sequence[float]", 195 218 *, 196 219 xlabel: str | None = "Frequency [Hz]", 220 + language: str = "en", 197 221 ) -> np.ndarray: 198 222 """Categorical band x-axis: evenly spaced positions labelled with centres. 199 223 ··· 211 235 ax.set_xticks(positions) 212 236 ax.set_xticklabels(labels, rotation=45, ha="right") 213 237 if xlabel is not None: 214 - ax.set_xlabel(xlabel) 238 + ax.set_xlabel(_t(xlabel, language)) 215 239 return positions 216 240 217 241 ··· 352 376 measured_label: str = "Measured", 353 377 ylim: tuple[float, float] | None = None, 354 378 ax: Axes | None, 379 + language: str = "en", 355 380 **kwargs: Any, 356 381 ) -> Axes: 357 382 """Shared renderer for the shifted-reference rating figures. ··· 364 389 """ 365 390 ax = ax if ax is not None else _new_axes() 366 391 kwargs.setdefault("color", _C_PRIMARY) 367 - kwargs.setdefault("label", measured_label) 392 + kwargs.setdefault("label", _t(measured_label, language)) 368 393 ax.plot(band_centers, measured, "o-", **kwargs) 369 394 ax.plot(band_centers, reference, "s--", color=_C_REFERENCE, 370 - label="Shifted reference") 395 + label=_t("Shifted reference", language)) 371 396 unfavourable = _unfavourable_mask(measured, reference, impact) 372 397 ax.fill_between( 373 398 band_centers, ··· 376 401 where=unfavourable.tolist(), 377 402 color=_C_SECONDARY, 378 403 alpha=0.4, 379 - label="Unfavourable deviations", 404 + label=_t("Unfavourable deviations", language), 380 405 interpolate=True, 381 406 ) 382 - _freq_axis(ax, band_centers) 407 + _freq_axis(ax, band_centers, language=language) 383 408 ax.set_ylabel(ylabel) 384 409 if ylim is not None: 385 410 ax.set_ylim(*ylim) ··· 410 435 411 436 412 437 def _annotate_impact_500( 413 - ax: Axes, band_centers: np.ndarray, reference: np.ndarray, rating: int 438 + ax: Axes, 439 + band_centers: np.ndarray, 440 + reference: np.ndarray, 441 + rating: int, 442 + *, 443 + language: str = "en", 414 444 ) -> None: 415 445 """Mark the 500 Hz read value on the reference curve and, when the 416 446 octave-band -5 dB reduction applies (ISO 717-2 Clause 4.3.2), annotate 417 447 the rating as ``read - 5 dB`` so the figure stays normatively truthful. 418 448 """ 449 + from .._i18n import decimal_comma 450 + 419 451 if band_centers.size == 0: 420 452 return 421 453 idx = int(np.argmin(np.abs(band_centers - 500.0))) 422 454 read_value = float(reference[idx]) 455 + read_str = decimal_comma(f"{read_value:.0f}", language) 456 + read_label = ( 457 + f"lectura 500 Hz = {read_str} dB" if language == "es" 458 + else f"500 Hz read = {read_str} dB" 459 + ) 423 460 ax.plot( 424 461 [band_centers[idx]], 425 462 [read_value], ··· 430 467 mfc="none", 431 468 mew=1.6, 432 469 zorder=5, 433 - label=f"500 Hz read = {read_value:.0f} dB", 470 + label=read_label, 434 471 ) 435 472 offset = rating - read_value 436 473 if abs(offset) >= 0.5: # octave-band -5 dB rule (Clause 4.3.2) 474 + rating_str = decimal_comma(str(rating), language) 475 + annotation = ( 476 + f"índice = {rating_str} dB = {read_str} - 5 dB (regla de octava)" 477 + if language == "es" 478 + else f"rating = {rating_str} dB = {read_str} - 5 dB (octave rule)" 479 + ) 437 480 ax.annotate( 438 - f"rating = {rating} dB = {read_value:.0f} - 5 dB (octave rule)", 481 + annotation, 439 482 xy=(band_centers[idx], read_value), 440 483 xytext=(0.0, -32.0), 441 484 textcoords="offset points", ··· 462 505 463 506 464 507 465 - def _facade_x_axis(ax: Axes, freqs: "np.ndarray | None", n: int) -> np.ndarray: 508 + def _facade_x_axis( 509 + ax: Axes, freqs: "np.ndarray | None", n: int, *, language: str = "en" 510 + ) -> np.ndarray: 466 511 """Frequency x-axis when centres are known, else a labelled band index.""" 467 512 if freqs is None: 468 513 x = np.arange(n, dtype=np.float64) 469 514 ax.set_xticks(x) 470 - ax.set_xticklabels([f"Band {i + 1}" for i in range(n)], rotation=45, ha="right") 471 - ax.set_xlabel("Band") 515 + band_word = _t("Band", language) 516 + ax.set_xticklabels( 517 + [f"{band_word} {i + 1}" for i in range(n)], rotation=45, ha="right" 518 + ) 519 + ax.set_xlabel(_t("Band", language)) 472 520 return x 473 521 x = np.asarray(freqs, dtype=np.float64) 474 - _freq_axis(ax, x) 522 + _freq_axis(ax, x, language=language) 475 523 return x 476 524 477 525 ··· 581 629 # --------------------------------------------------------------------------- 582 630 583 631 584 - def _time_axis(n: int, fs: int | None) -> tuple[np.ndarray, str]: 632 + def _time_axis( 633 + n: int, fs: int | None, *, language: str = "en" 634 + ) -> tuple[np.ndarray, str]: 585 635 """Sample times in seconds when ``fs`` is known, else sample index.""" 586 636 if fs: 587 - return np.arange(n) / float(fs), "Time [s]" 588 - return np.arange(n, dtype=np.float64), "Sample" 637 + return np.arange(n) / float(fs), _t("Time [s]", language) 638 + return np.arange(n, dtype=np.float64), _t("Sample", language) 589 639 590 640 591 641 ··· 670 720 *, 671 721 ylabel: str, 672 722 title: str, 723 + language: str = "en", 673 724 **kwargs: Any, 674 725 ) -> Axes: 675 726 """Per-band level bar chart with a band-summed total line (shared helper).""" 727 + from .._i18n import decimal_comma 728 + 676 729 ax = ax if ax is not None else _new_axes() 677 730 lw = np.asarray(levels, dtype=np.float64) 678 731 n = lw.size 679 732 if frequencies is not None: 680 - labels = [f"{f:g}" for f in np.asarray(frequencies)] 681 - ax.set_xlabel("Frequency [Hz]") 733 + labels = [decimal_comma(f"{f:g}", language) for f in np.asarray(frequencies)] 734 + ax.set_xlabel(_t("Frequency [Hz]", language)) 682 735 else: 683 736 labels = [str(i + 1) for i in range(n)] 684 - ax.set_xlabel("Band") 737 + ax.set_xlabel(_t("Band", language)) 685 738 positions = np.arange(n) 686 739 kwargs.setdefault("color", _C_PRIMARY) 687 740 ax.bar(positions, lw, width=0.7, edgecolor=_C_EDGE, linewidth=0.6, **kwargs) 688 741 ax.set_xticks(positions) 689 742 ax.set_xticklabels(labels) 690 - ax.axhline(total_level, color=_C_REFERENCE, ls="--", lw=1.2, 691 - label=f"total {total_level:.1f} dB") 743 + ax.axhline( 744 + total_level, color=_C_REFERENCE, ls="--", lw=1.2, 745 + label=f"total {decimal_comma(f'{total_level:.1f}', language)} dB", 746 + ) 692 747 ax.set_ylabel(ylabel) 693 748 ax.set_title(title) 694 749 ax.legend(loc="best", fontsize="small")
+6 -3
src/phonometry/_plot/emission.py
··· 103 103 if freqs is None: 104 104 positions = _band_axis( 105 105 ax, [f"{_t('Band', language)} {i + 1}" for i in range(n)], 106 - xlabel=_t("Band", language) 106 + xlabel=_t("Band", language), language=language, 107 107 ) 108 108 else: 109 - positions = _band_axis(ax, np.asarray(freqs, dtype=np.float64)) 109 + positions = _band_axis( 110 + ax, np.asarray(freqs, dtype=np.float64), language=language 111 + ) 110 112 111 113 # ``negative_band`` (ISO 9614-2) and ``not_applicable_band`` (ISO 9614-3) 112 114 # both flag bands whose net power is non-positive and therefore unusable. ··· 178 180 kwargs.setdefault("label", _t("Pressure level Lp", language)) 179 181 ax.plot(freqs, lp, "o-", **kwargs) 180 182 ax.plot(freqs, li, "s--", color=_C_REFERENCE, label=_t("Intensity level LI", language)) 181 - _freq_axis(ax, freqs) 183 + _freq_axis(ax, freqs, language=language) 182 184 ax.set_ylabel(_t("Level [dB]", language)) 183 185 ax.grid(True, which="both", alpha=0.3) 184 186 ··· 222 224 ax, result.sound_power_level, result.frequencies, result.total_level, 223 225 ylabel=_t(r"Sound power level $L_W$ [dB re 1 pW]", language), 224 226 title=_t("ISO/TS 7849 sound power from surface vibration", language), 227 + language=language, 225 228 **kwargs, 226 229 ) 227 230 localize_axes(ax, language)
+3 -3
src/phonometry/_plot/environmental.py
··· 271 271 272 272 ax = ax if ax is not None else _new_axes() 273 273 freqs = np.asarray(result.frequencies, dtype=np.float64) 274 - positions = _band_axis(ax, freqs) 274 + positions = _band_axis(ax, freqs, language=language) 275 275 n = freqs.size 276 276 277 277 # Separate positive and negative cumulative baselines so a negative term ··· 334 334 ax.axhline(0.0, color=_C_MUTED, lw=0.8, label=_t("free_field", language)) 335 335 ax.axhline(6.0, color=_C_REFERENCE, ls="--", lw=0.9, 336 336 label=_t("hard_ground", language)) 337 - _freq_axis(ax, freqs) 337 + _freq_axis(ax, freqs, language=language) 338 338 ax.set_ylabel(_t("level_re_ff", language)) 339 339 ax.set_title(_t("spherical_title", language)) 340 340 ax.legend(loc="best", fontsize="small") ··· 368 368 ax.axhline(0.0, color=_C_MUTED, lw=0.8) 369 369 ax.axhline(5.0, color=_C_MUTED, ls=":", lw=0.9, 370 370 label=_t("grazing_limit", language)) 371 - _freq_axis(ax, freqs) 371 + _freq_axis(ax, freqs, language=language) 372 372 ax.set_ylabel(_t("insertion_loss_db", language)) 373 373 ax.set_title(_t("barrier_title", language)) 374 374 ax.legend(loc="best", fontsize="small")
+5 -5
src/phonometry/_plot/hearing.py
··· 140 140 ax.bar(positions, mti, **kwargs) 141 141 banded = mti.size == len(_STI_BAND_CENTERS) 142 142 if banded: 143 - _band_axis(ax, np.asarray(_STI_BAND_CENTERS)) 143 + _band_axis(ax, np.asarray(_STI_BAND_CENTERS), language=language) 144 144 ax.set_xlabel(_t("freq_hz", language)) 145 145 else: 146 146 ax.set_xticks(positions) ··· 227 227 freqs = np.asarray(result.frequencies, dtype=np.float64) 228 228 audibility = np.asarray(result.band_audibility, dtype=np.float64) 229 229 contribution = audibility * np.asarray(result.band_importance, dtype=np.float64) 230 - positions = _band_axis(ax, freqs) 230 + positions = _band_axis(ax, freqs, language=language) 231 231 ax.set_xlabel(_t("freq_hz", language)) 232 232 ax.bar(positions, audibility, color=_C_PRIMARY_LIGHT, 233 233 label=_t("band_audibility_ai", language)) ··· 277 277 ax.plot(freqs, np.asarray(result.threshold, dtype=np.float64), "s--", 278 278 color=_C_REFERENCE, label=_t("fractile", language).format( 279 279 v=decimal_comma(f"{result.fractile:g}", language))) 280 - _freq_axis(ax, freqs) 280 + _freq_axis(ax, freqs, language=language) 281 281 ax.set_xlabel(_t("freq_hz", language)) 282 282 ax.set_ylabel(_t("threshold_dev_18", language)) 283 283 ax.invert_yaxis() # audiogram convention: worse hearing downward ··· 316 316 ax.plot(freqs, np.asarray(result.value, dtype=np.float64), "s--", 317 317 color=_C_REFERENCE, label=_t("fractile", language).format( 318 318 v=decimal_comma(f"{result.fractile:g}", language))) 319 - _freq_axis(ax, freqs) 319 + _freq_axis(ax, freqs, language=language) 320 320 ax.set_xlabel(_t("freq_hz", language)) 321 321 ax.set_ylabel(_t("nipts_db", language)) 322 322 ax.invert_yaxis() # audiogram convention: worse hearing downward ··· 352 352 kwargs.setdefault("color", _C_REFERENCE) 353 353 ax.plot(freqs, np.asarray(result.threshold, dtype=np.float64), "s--", 354 354 label=_t("age_noise_htlan", language), **kwargs) 355 - _freq_axis(ax, freqs) 355 + _freq_axis(ax, freqs, language=language) 356 356 ax.set_xlabel(_t("freq_hz", language)) 357 357 ax.set_ylabel(_t("htl_level", language)) 358 358 ax.invert_yaxis() # audiogram convention: worse hearing downward
+6 -9
src/phonometry/_plot/materials.py
··· 138 138 measured_label=_t("Practical alpha_p", language), 139 139 ylim=(0.0, 1.05), 140 140 ax=ax, 141 + language=language, 141 142 **kwargs, 142 143 ) 143 - if language == "es": 144 - ax.set_xlabel(_t("Frequency [Hz]", language)) 145 144 localize_axes(ax, language) 146 145 return ax 147 146 ··· 165 164 kwargs.setdefault("marker", "o") 166 165 kwargs.setdefault("color", _C_PRIMARY) 167 166 ax.plot(freqs, s, **kwargs) 168 - _freq_axis(ax, freqs) 169 - if language == "es": 170 - ax.set_xlabel(_t("Frequency [Hz]", language)) 167 + _freq_axis(ax, freqs, language=language) 171 168 ax.set_ylabel(_t("Scattering coefficient s", language)) 172 169 # s is normally in [0, 1], but edge effects (Clause 6.3.2) can push it above 173 170 # 1 and those values are kept, not clipped; grow the top so they stay visible. ··· 224 221 ax = ax if ax is not None else _new_axes() 225 222 freqs = np.asarray(result.frequencies, dtype=np.float64) 226 223 alpha = np.asarray(result.absorption, dtype=np.float64) 227 - positions = _band_axis(ax, freqs, xlabel=_t("Frequency [Hz]", language)) 224 + positions = _band_axis( 225 + ax, freqs, xlabel=_t("Frequency [Hz]", language), language=language 226 + ) 228 227 kwargs.setdefault("color", _C_PRIMARY) 229 228 ax.bar(positions, np.nan_to_num(alpha), **kwargs) 230 229 ax.set_ylabel(_t("Absorption coefficient", language)) ··· 390 389 label=f"+/-U (k = {decimal_comma(f'{result.coverage_factor:g}', language)})", 391 390 ) 392 391 ax.plot(freqs, value, **kwargs) 393 - _freq_axis(ax, freqs) 394 - if language == "es": 395 - ax.set_xlabel(_t("Frequency [Hz]", language)) 392 + _freq_axis(ax, freqs, language=language) 396 393 ylabel = _ABSORPTION_QUANTITY_LABELS.get(result.quantity, "Value") 397 394 ax.set_ylabel(_t(ylabel, language)) 398 395 if result.quantity != "equivalent_area":
+7 -14
src/phonometry/_plot/room.py
··· 152 152 _draw_decay_times(ax_times, positions, result, **kwargs) 153 153 ax_times.set_ylabel(_t("Reverberation time [s]", language)) 154 154 ax_times.set_title(_t("ISO 3382 decay times and clarity", language)) 155 - _band_axis(ax_times, labels, xlabel=None) 155 + _band_axis(ax_times, labels, xlabel=None, language=language) 156 156 ax_times.grid(True, axis="y", alpha=0.3) 157 157 ax_times.legend(loc="best", fontsize="small") 158 158 ··· 182 182 ax_clarity, 183 183 labels, 184 184 xlabel=_t("Frequency [Hz]" if use_freq_axis else "Band", language), 185 + language=language, 185 186 ) 186 187 ax_clarity.grid(True, alpha=0.3) 187 188 ax_clarity.legend(loc="best", fontsize="small") ··· 258 259 n = h.shape[-1] 259 260 if n == 0: 260 261 raise ValueError("impulse response is empty; nothing to plot.") 261 - time, xlabel = _time_axis(n, result.fs) 262 + time, xlabel = _time_axis(n, result.fs, language=language) 262 263 peak = float(np.max(np.abs(h))) 263 264 tiny = np.finfo(np.float64).tiny 264 265 norm = peak if peak > 0.0 else 1.0 ··· 344 345 label=(f"{_t('Governing band', language)} " 345 346 f"({_format_freq(result.governing_frequency)})"), 346 347 ) 347 - _freq_axis(ax, OCTAVE_BANDS) 348 - if language == "es": 349 - ax.set_xlabel(_t("Frequency [Hz]", language)) 348 + _freq_axis(ax, OCTAVE_BANDS, language=language) 350 349 ax.set_ylabel(_t("Octave-band SPL [dB]", language)) 351 350 ax.set_title( 352 351 f"ANSI/ASA S12.2 NC-{result.rating:g} " ··· 391 390 kwargs.setdefault("color", _C_PRIMARY) 392 391 kwargs.setdefault("label", _t("Measured", language)) 393 392 ax.plot(freqs[valid], levels[valid], "o-", zorder=3, **kwargs) 394 - _freq_axis(ax, freqs) 395 - if language == "es": 396 - ax.set_xlabel(_t("Frequency [Hz]", language)) 393 + _freq_axis(ax, freqs, language=language) 397 394 ax.set_ylabel(_t("Octave-band SPL [dB]", language)) 398 395 ax.set_title(f"ANSI/ASA S12.2 {result.label}") 399 396 ax.legend(loc=_LEGEND_UPPER_RIGHT, fontsize="small") ··· 420 417 kwargs.setdefault("color", _C_PRIMARY) 421 418 kwargs.setdefault("marker", "o") 422 419 ax.plot(freq, rt, **kwargs) 423 - _freq_axis(ax, freq) 424 - if language == "es": 425 - ax.set_xlabel(_t("Frequency [Hz]", language)) 420 + _freq_axis(ax, freq, language=language) 426 421 ax.set_ylabel(_t("Reverberation time $T$ [s]", language)) 427 422 ax.set_title(_t("EN 12354-6 reverberation time", language)) 428 423 ax.set_ylim(bottom=0.0) ··· 467 462 label=label, 468 463 **kwargs, 469 464 ) 470 - _freq_axis(ax, freq) 471 - if language == "es": 472 - ax.set_xlabel(_t("Frequency [Hz]", language)) 465 + _freq_axis(ax, freq, language=language) 473 466 ax.set_ylabel(_t("Reverberation time $T$ [s]", language)) 474 467 ax.set_title( 475 468 f"{_t('Reverberation-time models — ', language)}"
+1 -1
src/phonometry/_plot/vibration.py
··· 178 178 freqs = np.asarray(result.frequencies, dtype=np.float64) 179 179 raw = np.asarray(result.band_accelerations, dtype=np.float64) 180 180 weighted = np.asarray(result.weighted, dtype=np.float64) 181 - positions = _band_axis(ax, freqs) 181 + positions = _band_axis(ax, freqs, language=language) 182 182 ax.set_xlabel(_t("freq_hz", language)) 183 183 width = 0.4 184 184 # The weighted bars are the primary artist; forward user kwargs there.