From ef965c9f46eeb1e22ee18d2b32b57c796e3f413e Mon Sep 17 00:00:00 2001 From: Saman Hushi Date: Fri, 12 Jul 2024 10:54:28 +0200 Subject: [PATCH 01/15] feat: add histogram_2d method to TablePlotter --- .../data/tabular/plotting/_table_plotter.py | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/src/safeds/data/tabular/plotting/_table_plotter.py b/src/safeds/data/tabular/plotting/_table_plotter.py index 62f9d1113..8da39f4af 100644 --- a/src/safeds/data/tabular/plotting/_table_plotter.py +++ b/src/safeds/data/tabular/plotting/_table_plotter.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from safeds._utils import _figure_to_image -from safeds._validation import _check_columns_exist +from safeds._validation import _check_bounds, _check_columns_exist, _ClosedBound from safeds._validation._check_columns_are_numeric import _check_columns_are_numeric from safeds.exceptions import ColumnTypeError, NonNumericColumnError @@ -453,6 +453,81 @@ def moving_average_plot(self, x_name: str, y_name: str, window_size: int) -> Ima fig.tight_layout() return _figure_to_image(fig) + + def histogram_2d(self, x_name: str, y_name: str, *, x_max_bin_count: int = 10, y_max_bin_count: int = 10) -> Image: + """ + Create a 2D histogram for two columns in the table. + + Parameters + ---------- + x_name: + The name of the column to be plotted on the x-axis. + y_name: + The name of the column to be plotted on the y-axis. + x_max_bin_count: + The maximum number of bins to use in the histogram for the x-axis. Default is 10. + y_max_bin_count: + The maximum number of bins to use in the histogram for the y-axis. Default is 10. + + Returns + ------- + plot: + The plot as an image. + + Raises + ------ + ColumnNotFoundError + If a column does not exist. + OutOfBoundsError: + If the actual value is outside its expected range (x_max_bin_count, y_max_bin_count). + TypeError + If a column is not numeric. + + Examples + -------- + >>> from safeds.data.tabular.containers import Table + >>> import numpy as np + >>> np.random.seed(1) + >>> x = np.random.randn(5000) + >>> y = 1.2 * x + np.random.randn(5000) / 3 + >>> table = Table( + ... { + ... "a": x, + ... "b": y, + ... } + ... ) + >>> image = table.plot.histogram_2d("a", "b", x_max_bin_count=50, y_max_bin_count=50) + """ + _check_bounds("x_max_bin_count", x_max_bin_count, lower_bound=_ClosedBound(1)) + _check_bounds("y_max_bin_count", y_max_bin_count, lower_bound=_ClosedBound(1)) + _plot_validation(self._table, x_name, [y_name]) + + for name in [x_name, y_name]: + if self._table.get_column(name).missing_value_count() >= 1: + raise ValueError( + f"there are missing values in column '{name}', use transformation to fill missing values " + f"or drop the missing values. For a moving average no missing values are allowed.", + ) + + import matplotlib.pyplot as plt + + fig, ax = plt.subplots() + + ax.hist2d( + x=self._table.get_column(x_name)._series, + y=self._table.get_column(y_name)._series, + bins=(x_max_bin_count, y_max_bin_count), + ) + ax.set_xlabel(x_name) + ax.set_ylabel(y_name) + ax.tick_params( + axis="x", + labelrotation=45, + ) + + fig.tight_layout() + + return _figure_to_image(fig) def _plot_validation(table: Table, x_name: str, y_names: list[str]) -> None: From a813c6c6b8985c5ccc806bd90750dc9e1a073bf5 Mon Sep 17 00:00:00 2001 From: Saman Hushi Date: Fri, 12 Jul 2024 10:59:33 +0200 Subject: [PATCH 02/15] added darkmode for histogram_2d --- src/safeds/data/tabular/plotting/_table_plotter.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/safeds/data/tabular/plotting/_table_plotter.py b/src/safeds/data/tabular/plotting/_table_plotter.py index 8da39f4af..729a1e83c 100644 --- a/src/safeds/data/tabular/plotting/_table_plotter.py +++ b/src/safeds/data/tabular/plotting/_table_plotter.py @@ -9,6 +9,8 @@ from safeds.exceptions import ColumnTypeError, NonNumericColumnError if TYPE_CHECKING: + from typing import Literal + from safeds.data.image.containers import Image from safeds.data.tabular.containers import Table @@ -454,7 +456,7 @@ def moving_average_plot(self, x_name: str, y_name: str, window_size: int) -> Ima return _figure_to_image(fig) - def histogram_2d(self, x_name: str, y_name: str, *, x_max_bin_count: int = 10, y_max_bin_count: int = 10) -> Image: + def histogram_2d(self, x_name: str, y_name: str, *, x_max_bin_count: int = 10, y_max_bin_count: int = 10, theme: Literal["dark", "light"] = "light") -> Image: """ Create a 2D histogram for two columns in the table. @@ -510,7 +512,9 @@ def histogram_2d(self, x_name: str, y_name: str, *, x_max_bin_count: int = 10, y ) import matplotlib.pyplot as plt - + if theme == "dark": + plt.style.use("dark_background") + fig, ax = plt.subplots() ax.hist2d( From 229d221ca896453348a8a4d3187460e4aeb06fb0 Mon Sep 17 00:00:00 2001 From: Saman Hushi Date: Fri, 12 Jul 2024 11:24:22 +0200 Subject: [PATCH 03/15] added test for histogram_2d --- ...test_should_match_snapshot[functional].png | Bin 0 -> 11070 bytes .../test_should_match_snapshot[multiple].png | Bin 0 -> 9435 bytes ...est_should_match_snapshot[overlapping].png | Bin 0 -> 9435 bytes ..._match_snapshot_dark_theme[dark_theme].png | Bin 0 -> 10458 bytes .../plotting/test_plot_histogram_2d.py | 91 ++++++++++++++++++ 5 files changed, 91 insertions(+) create mode 100644 tests/safeds/data/tabular/plotting/__snapshots__/test_plot_histogram_2d/test_should_match_snapshot[functional].png create mode 100644 tests/safeds/data/tabular/plotting/__snapshots__/test_plot_histogram_2d/test_should_match_snapshot[multiple].png create mode 100644 tests/safeds/data/tabular/plotting/__snapshots__/test_plot_histogram_2d/test_should_match_snapshot[overlapping].png create mode 100644 tests/safeds/data/tabular/plotting/__snapshots__/test_plot_histogram_2d/test_should_match_snapshot_dark_theme[dark_theme].png create mode 100644 tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py diff --git a/tests/safeds/data/tabular/plotting/__snapshots__/test_plot_histogram_2d/test_should_match_snapshot[functional].png b/tests/safeds/data/tabular/plotting/__snapshots__/test_plot_histogram_2d/test_should_match_snapshot[functional].png new file mode 100644 index 0000000000000000000000000000000000000000..e9f0cdf59077ed55c4bdde6a320aacf57b097d6a GIT binary patch literal 11070 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 literal 0 HcmV?d00001 diff --git a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py new file mode 100644 index 000000000..6593faf9a --- /dev/null +++ b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py @@ -0,0 +1,91 @@ +import pytest +from safeds.data.tabular.containers import Table +from safeds.exceptions import ColumnNotFoundError, ColumnTypeError, OutOfBoundsError +from syrupy import SnapshotAssertion + + +@pytest.mark.parametrize( + ("table", "x_name", "y_name"), + [ + (Table({"A": [1, 2, 3], "B": [2, 4, 7]}), "A", "B"), + ( + Table( + { + "A": [1, 0.99, 0.99, 2], + "B": [1, 0.99, 1.01, 2], + }, + ), + "A", + "B", + ), + ( + Table( + {"A": [1, 0.99, 0.99, 2], "B": [1, 0.99, 1.01, 2], "C": [2, 2.99, 2.01, 3]}, + ), + "A", + "B", + ), + ], + ids=[ + "functional", + "overlapping", + "multiple", + ], +) +def test_should_match_snapshot( + table: Table, + x_name: str, + y_name: str, + snapshot_png_image: SnapshotAssertion, +) -> None: + histogram_2d = table.plot.histogram_2d(x_name, y_name) + assert histogram_2d == snapshot_png_image + +@pytest.mark.parametrize( + ("table", "col1", "col2"), + [ + (Table({"A": [1, 2, 3], "B": [2, 4, 7]}), "C", "A"), + (Table({"A": [1, 2, 3], "B": [2, 4, 7]}), "B", "C"), + (Table({"A": [1, 2, 3], "B": [2, 4, 7]}), "C", "D"), + (Table(), "C", "D"), + ], + ids=[ + "First argument doesn't exist", + "Second argument doesn't exist", + "Both arguments do not exist", "empty", + ], +) +def test_should_raise_if_column_does_not_exist(table: Table, col1: str, col2: str) -> None: + with pytest.raises(ColumnNotFoundError): + table.plot.histogram_2d(col1, col2) + +@pytest.mark.parametrize( + ("table", "col1", "col2"), + [ + (Table({"A": [1, 2, 3], "B": [2, 4, 7]}), "A", "B"), + ], + ids=["dark_theme"], +) +def test_should_match_snapshot_dark_theme( + table: Table, + x_name: str, + y_name: str, + snapshot_png_image: SnapshotAssertion, +) -> None: + histogram_2d = table.plot.histogram_2d(x_name, y_name, theme="dark") + assert histogram_2d == snapshot_png_image + +def test_should_raise_if_value_not_in_range_x() -> None: + table = Table({"col1": [1, 2, 1], "col2": [1, 2, 4]}) + with pytest.raises(OutOfBoundsError): + table.plot.histogram_2d("col1", "col2", x_max_bin_count=0) + +def test_should_raise_if_value_not_in_range_y() -> None: + table = Table({"col1": [1, 2, 1], "col2": [1, 2, 4]}) + with pytest.raises(OutOfBoundsError): + table.plot.histogram_2d("col1", "col2", y_max_bin_count=0) + +def test_should_raise_if_column_is_not_numeric() -> None: + table = Table({"col1": "a", "col2": "b"}) + with pytest.raises(ColumnTypeError): + table.plot.histogram_2d("col1", "col2") From 2f4e8f2ed2c436c7c6677d44016416003725e3aa Mon Sep 17 00:00:00 2001 From: Saman Hushi Date: Fri, 12 Jul 2024 11:27:03 +0200 Subject: [PATCH 04/15] consistant naming in test --- .../tabular/plotting/test_plot_histogram_2d.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py index 6593faf9a..59396d81f 100644 --- a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py +++ b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py @@ -5,7 +5,7 @@ @pytest.mark.parametrize( - ("table", "x_name", "y_name"), + ("table", "col1", "col2"), [ (Table({"A": [1, 2, 3], "B": [2, 4, 7]}), "A", "B"), ( @@ -34,11 +34,11 @@ ) def test_should_match_snapshot( table: Table, - x_name: str, - y_name: str, + col1: str, + col2: str, snapshot_png_image: SnapshotAssertion, ) -> None: - histogram_2d = table.plot.histogram_2d(x_name, y_name) + histogram_2d = table.plot.histogram_2d(col1, col2) assert histogram_2d == snapshot_png_image @pytest.mark.parametrize( @@ -52,7 +52,8 @@ def test_should_match_snapshot( ids=[ "First argument doesn't exist", "Second argument doesn't exist", - "Both arguments do not exist", "empty", + "Both arguments do not exist", + "empty", ], ) def test_should_raise_if_column_does_not_exist(table: Table, col1: str, col2: str) -> None: @@ -68,11 +69,11 @@ def test_should_raise_if_column_does_not_exist(table: Table, col1: str, col2: st ) def test_should_match_snapshot_dark_theme( table: Table, - x_name: str, - y_name: str, + col1: str, + col2: str, snapshot_png_image: SnapshotAssertion, ) -> None: - histogram_2d = table.plot.histogram_2d(x_name, y_name, theme="dark") + histogram_2d = table.plot.histogram_2d(col1, col2, theme="dark") assert histogram_2d == snapshot_png_image def test_should_raise_if_value_not_in_range_x() -> None: From 69ce282cd435e296997a27a4b380cff62bc68ea1 Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:30:59 +0000 Subject: [PATCH 05/15] style: apply automated linter fixes --- .../data/tabular/plotting/_table_plotter.py | 21 +++++++++++++------ .../plotting/test_plot_histogram_2d.py | 7 ++++++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/safeds/data/tabular/plotting/_table_plotter.py b/src/safeds/data/tabular/plotting/_table_plotter.py index 729a1e83c..5046472a0 100644 --- a/src/safeds/data/tabular/plotting/_table_plotter.py +++ b/src/safeds/data/tabular/plotting/_table_plotter.py @@ -455,8 +455,16 @@ def moving_average_plot(self, x_name: str, y_name: str, window_size: int) -> Ima fig.tight_layout() return _figure_to_image(fig) - - def histogram_2d(self, x_name: str, y_name: str, *, x_max_bin_count: int = 10, y_max_bin_count: int = 10, theme: Literal["dark", "light"] = "light") -> Image: + + def histogram_2d( + self, + x_name: str, + y_name: str, + *, + x_max_bin_count: int = 10, + y_max_bin_count: int = 10, + theme: Literal["dark", "light"] = "light", + ) -> Image: """ Create a 2D histogram for two columns in the table. @@ -510,13 +518,14 @@ def histogram_2d(self, x_name: str, y_name: str, *, x_max_bin_count: int = 10, y f"there are missing values in column '{name}', use transformation to fill missing values " f"or drop the missing values. For a moving average no missing values are allowed.", ) - + import matplotlib.pyplot as plt + if theme == "dark": plt.style.use("dark_background") fig, ax = plt.subplots() - + ax.hist2d( x=self._table.get_column(x_name)._series, y=self._table.get_column(y_name)._series, @@ -526,8 +535,8 @@ def histogram_2d(self, x_name: str, y_name: str, *, x_max_bin_count: int = 10, y ax.set_ylabel(y_name) ax.tick_params( axis="x", - labelrotation=45, - ) + labelrotation=45, + ) fig.tight_layout() diff --git a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py index 59396d81f..a289d8072 100644 --- a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py +++ b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py @@ -41,6 +41,7 @@ def test_should_match_snapshot( histogram_2d = table.plot.histogram_2d(col1, col2) assert histogram_2d == snapshot_png_image + @pytest.mark.parametrize( ("table", "col1", "col2"), [ @@ -54,12 +55,13 @@ def test_should_match_snapshot( "Second argument doesn't exist", "Both arguments do not exist", "empty", - ], + ], ) def test_should_raise_if_column_does_not_exist(table: Table, col1: str, col2: str) -> None: with pytest.raises(ColumnNotFoundError): table.plot.histogram_2d(col1, col2) + @pytest.mark.parametrize( ("table", "col1", "col2"), [ @@ -76,16 +78,19 @@ def test_should_match_snapshot_dark_theme( histogram_2d = table.plot.histogram_2d(col1, col2, theme="dark") assert histogram_2d == snapshot_png_image + def test_should_raise_if_value_not_in_range_x() -> None: table = Table({"col1": [1, 2, 1], "col2": [1, 2, 4]}) with pytest.raises(OutOfBoundsError): table.plot.histogram_2d("col1", "col2", x_max_bin_count=0) + def test_should_raise_if_value_not_in_range_y() -> None: table = Table({"col1": [1, 2, 1], "col2": [1, 2, 4]}) with pytest.raises(OutOfBoundsError): table.plot.histogram_2d("col1", "col2", y_max_bin_count=0) + def test_should_raise_if_column_is_not_numeric() -> None: table = Table({"col1": "a", "col2": "b"}) with pytest.raises(ColumnTypeError): From 5bbb946798c4ba01ec212784b30ab93b453c2978 Mon Sep 17 00:00:00 2001 From: Saman Hushi Date: Fri, 12 Jul 2024 11:40:45 +0200 Subject: [PATCH 06/15] skipping snapchot tests for mac --- tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py index a289d8072..af3cbab09 100644 --- a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py +++ b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py @@ -2,6 +2,7 @@ from safeds.data.tabular.containers import Table from safeds.exceptions import ColumnNotFoundError, ColumnTypeError, OutOfBoundsError from syrupy import SnapshotAssertion +from tests.helpers import os_linux, os_mac, skip_if_os @pytest.mark.parametrize( @@ -38,6 +39,7 @@ def test_should_match_snapshot( col2: str, snapshot_png_image: SnapshotAssertion, ) -> None: + skip_if_os(os_mac) histogram_2d = table.plot.histogram_2d(col1, col2) assert histogram_2d == snapshot_png_image @@ -75,6 +77,7 @@ def test_should_match_snapshot_dark_theme( col2: str, snapshot_png_image: SnapshotAssertion, ) -> None: + skip_if_os(os_mac) histogram_2d = table.plot.histogram_2d(col1, col2, theme="dark") assert histogram_2d == snapshot_png_image From e4c152db63ec129a8ca044add00e470a7ad050b2 Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:42:34 +0000 Subject: [PATCH 07/15] style: apply automated linter fixes --- tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py index af3cbab09..a73c63d39 100644 --- a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py +++ b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py @@ -2,7 +2,8 @@ from safeds.data.tabular.containers import Table from safeds.exceptions import ColumnNotFoundError, ColumnTypeError, OutOfBoundsError from syrupy import SnapshotAssertion -from tests.helpers import os_linux, os_mac, skip_if_os + +from tests.helpers import os_mac, skip_if_os @pytest.mark.parametrize( From dcb02b62dbb5d0fc02e51d2361a835d089d72d47 Mon Sep 17 00:00:00 2001 From: Saman Hushi Date: Fri, 12 Jul 2024 11:48:13 +0200 Subject: [PATCH 08/15] remove linux from snapshot test --- .../safeds/data/tabular/plotting/test_plot_histogram_2d.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py index a73c63d39..a13587ca8 100644 --- a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py +++ b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py @@ -3,7 +3,7 @@ from safeds.exceptions import ColumnNotFoundError, ColumnTypeError, OutOfBoundsError from syrupy import SnapshotAssertion -from tests.helpers import os_mac, skip_if_os +from tests.helpers import os_linux, os_mac, skip_if_os @pytest.mark.parametrize( @@ -40,7 +40,7 @@ def test_should_match_snapshot( col2: str, snapshot_png_image: SnapshotAssertion, ) -> None: - skip_if_os(os_mac) + skip_if_os([os_linux, os_mac]) histogram_2d = table.plot.histogram_2d(col1, col2) assert histogram_2d == snapshot_png_image @@ -78,7 +78,7 @@ def test_should_match_snapshot_dark_theme( col2: str, snapshot_png_image: SnapshotAssertion, ) -> None: - skip_if_os(os_mac) + skip_if_os([os_linux, os_mac]) histogram_2d = table.plot.histogram_2d(col1, col2, theme="dark") assert histogram_2d == snapshot_png_image From 3c24f1454292eab0b45f95840d1ad87e59e932bd Mon Sep 17 00:00:00 2001 From: Saman Hushi Date: Fri, 12 Jul 2024 11:57:44 +0200 Subject: [PATCH 09/15] removed unused code --- src/safeds/data/tabular/plotting/_table_plotter.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/safeds/data/tabular/plotting/_table_plotter.py b/src/safeds/data/tabular/plotting/_table_plotter.py index 5046472a0..91f1f3b32 100644 --- a/src/safeds/data/tabular/plotting/_table_plotter.py +++ b/src/safeds/data/tabular/plotting/_table_plotter.py @@ -512,13 +512,6 @@ def histogram_2d( _check_bounds("y_max_bin_count", y_max_bin_count, lower_bound=_ClosedBound(1)) _plot_validation(self._table, x_name, [y_name]) - for name in [x_name, y_name]: - if self._table.get_column(name).missing_value_count() >= 1: - raise ValueError( - f"there are missing values in column '{name}', use transformation to fill missing values " - f"or drop the missing values. For a moving average no missing values are allowed.", - ) - import matplotlib.pyplot as plt if theme == "dark": From 39666be95743644c683a931332a69875726d1e73 Mon Sep 17 00:00:00 2001 From: Saman Hushi Date: Fri, 12 Jul 2024 13:05:38 +0200 Subject: [PATCH 10/15] added linux back to snapchot tests --- .../safeds/data/tabular/plotting/test_plot_histogram_2d.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py index a13587ca8..0563046ea 100644 --- a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py +++ b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py @@ -3,7 +3,7 @@ from safeds.exceptions import ColumnNotFoundError, ColumnTypeError, OutOfBoundsError from syrupy import SnapshotAssertion -from tests.helpers import os_linux, os_mac, skip_if_os +from tests.helpers import os_mac, skip_if_os @pytest.mark.parametrize( @@ -40,7 +40,7 @@ def test_should_match_snapshot( col2: str, snapshot_png_image: SnapshotAssertion, ) -> None: - skip_if_os([os_linux, os_mac]) + skip_if_os([os_mac]) histogram_2d = table.plot.histogram_2d(col1, col2) assert histogram_2d == snapshot_png_image @@ -78,7 +78,7 @@ def test_should_match_snapshot_dark_theme( col2: str, snapshot_png_image: SnapshotAssertion, ) -> None: - skip_if_os([os_linux, os_mac]) + skip_if_os([os_mac]) histogram_2d = table.plot.histogram_2d(col1, col2, theme="dark") assert histogram_2d == snapshot_png_image From 3c357bdd7fe419dcb53bee5e5f04eac162069889 Mon Sep 17 00:00:00 2001 From: gitgott1 Date: Fri, 12 Jul 2024 13:50:42 +0200 Subject: [PATCH 11/15] changed theme handling --- .../data/tabular/plotting/_table_plotter.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/safeds/data/tabular/plotting/_table_plotter.py b/src/safeds/data/tabular/plotting/_table_plotter.py index 91f1f3b32..4def3f3cb 100644 --- a/src/safeds/data/tabular/plotting/_table_plotter.py +++ b/src/safeds/data/tabular/plotting/_table_plotter.py @@ -515,23 +515,26 @@ def histogram_2d( import matplotlib.pyplot as plt if theme == "dark": - plt.style.use("dark_background") + context = "dark_background" + else: + context = "default" - fig, ax = plt.subplots() + with plt.style.context(context): + fig, ax = plt.subplots() - ax.hist2d( - x=self._table.get_column(x_name)._series, - y=self._table.get_column(y_name)._series, - bins=(x_max_bin_count, y_max_bin_count), - ) - ax.set_xlabel(x_name) - ax.set_ylabel(y_name) - ax.tick_params( - axis="x", - labelrotation=45, - ) + ax.hist2d( + x=self._table.get_column(x_name)._series, + y=self._table.get_column(y_name)._series, + bins=(x_max_bin_count, y_max_bin_count), + ) + ax.set_xlabel(x_name) + ax.set_ylabel(y_name) + ax.tick_params( + axis="x", + labelrotation=45, + ) - fig.tight_layout() + fig.tight_layout() return _figure_to_image(fig) From 7b54140be68529b40853005e7cd94d8ec5f2b128 Mon Sep 17 00:00:00 2001 From: Saman Hushi Date: Fri, 12 Jul 2024 14:16:11 +0200 Subject: [PATCH 12/15] removed numpy in example --- src/safeds/data/tabular/plotting/_table_plotter.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/safeds/data/tabular/plotting/_table_plotter.py b/src/safeds/data/tabular/plotting/_table_plotter.py index 4def3f3cb..400032120 100644 --- a/src/safeds/data/tabular/plotting/_table_plotter.py +++ b/src/safeds/data/tabular/plotting/_table_plotter.py @@ -496,17 +496,13 @@ def histogram_2d( Examples -------- >>> from safeds.data.tabular.containers import Table - >>> import numpy as np - >>> np.random.seed(1) - >>> x = np.random.randn(5000) - >>> y = 1.2 * x + np.random.randn(5000) / 3 >>> table = Table( ... { - ... "a": x, - ... "b": y, + ... "a": [1, 2, 3, 4, 5], + ... "b": [2, 3, 4, 5, 6], ... } ... ) - >>> image = table.plot.histogram_2d("a", "b", x_max_bin_count=50, y_max_bin_count=50) + >>> image = table.plot.histogram_2d("a", "b") """ _check_bounds("x_max_bin_count", x_max_bin_count, lower_bound=_ClosedBound(1)) _check_bounds("y_max_bin_count", y_max_bin_count, lower_bound=_ClosedBound(1)) @@ -536,7 +532,7 @@ def histogram_2d( fig.tight_layout() - return _figure_to_image(fig) + return _figure_to_image(fig) def _plot_validation(table: Table, x_name: str, y_names: list[str]) -> None: From 0df54b6fa6322bfd25d9a01d19840eaa69bbfd8e Mon Sep 17 00:00:00 2001 From: Saman Hushi Date: Fri, 12 Jul 2024 14:19:50 +0200 Subject: [PATCH 13/15] corrected OutOfBoundsError discription --- src/safeds/data/tabular/plotting/_table_plotter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/safeds/data/tabular/plotting/_table_plotter.py b/src/safeds/data/tabular/plotting/_table_plotter.py index 400032120..e66fd88b9 100644 --- a/src/safeds/data/tabular/plotting/_table_plotter.py +++ b/src/safeds/data/tabular/plotting/_table_plotter.py @@ -489,7 +489,7 @@ def histogram_2d( ColumnNotFoundError If a column does not exist. OutOfBoundsError: - If the actual value is outside its expected range (x_max_bin_count, y_max_bin_count). + If x_max_bin_count or y_max_bin_count is less than 1. TypeError If a column is not numeric. From 58efd39191af01e017f113f3aaba143806152adc Mon Sep 17 00:00:00 2001 From: Saman Hushalsadat Date: Fri, 12 Jul 2024 14:57:48 +0200 Subject: [PATCH 14/15] Update tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py Co-authored-by: Lars Reimann --- tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py index 0563046ea..88fef9b5e 100644 --- a/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py +++ b/tests/safeds/data/tabular/plotting/test_plot_histogram_2d.py @@ -96,6 +96,6 @@ def test_should_raise_if_value_not_in_range_y() -> None: def test_should_raise_if_column_is_not_numeric() -> None: - table = Table({"col1": "a", "col2": "b"}) + table = Table({"col1": ["a"], "col2": ["b"]}) with pytest.raises(ColumnTypeError): table.plot.histogram_2d("col1", "col2") From 8e5ec02f523d12a67a29bcf02f49c6e4d5fdede4 Mon Sep 17 00:00:00 2001 From: Saman Hushi Date: Fri, 12 Jul 2024 15:08:51 +0200 Subject: [PATCH 15/15] added theme to the discription --- src/safeds/data/tabular/plotting/_table_plotter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/safeds/data/tabular/plotting/_table_plotter.py b/src/safeds/data/tabular/plotting/_table_plotter.py index e66fd88b9..e7bf679cb 100644 --- a/src/safeds/data/tabular/plotting/_table_plotter.py +++ b/src/safeds/data/tabular/plotting/_table_plotter.py @@ -478,6 +478,8 @@ def histogram_2d( The maximum number of bins to use in the histogram for the x-axis. Default is 10. y_max_bin_count: The maximum number of bins to use in the histogram for the y-axis. Default is 10. + theme: + The color theme of the plot. Default is "light". Returns -------