From 68e2672790f7b6bf9fd525777f0191dc4a4c08f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 22:59:16 +0000 Subject: [PATCH 1/5] Initial plan From 4b607aaac19b0451d70865c97960a79a65fd0d1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 23:06:43 +0000 Subject: [PATCH 2/5] Initial analysis of edgecolor issue - found root cause in _from_data function Co-authored-by: cvanelteren <19485143+cvanelteren@users.noreply.github.com> --- debug_scatter.py | 73 +++++++++++++++++++++++++++++++++++++++++++ test_ec_processing.py | 57 +++++++++++++++++++++++++++++++++ test_edge_issue.py | 42 +++++++++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 debug_scatter.py create mode 100644 test_ec_processing.py create mode 100644 test_edge_issue.py diff --git a/debug_scatter.py b/debug_scatter.py new file mode 100644 index 000000000..2ea90bfeb --- /dev/null +++ b/debug_scatter.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Debug script to understand the edgecolor issue.""" + +import pandas as pd +import numpy as np +import ultraplot as pplt +import matplotlib.pyplot as plt + + +def debug_scatter_call(): + """Debug what happens during scatter calls with different data sizes.""" + + # Test data + d = pd.DataFrame(dict(x=[1, 2], y=[1, 2], sizes=[300, 300])) + + # Create figure + fig, axes = pplt.subplots(ncols=1, figsize=(4, 4)) + ax = axes[0] # Get the actual axis object + + # Try to inspect the arguments being passed to matplotlib's scatter + print("=== Testing single row with alpha ===") + + # Monkey patch to see what's happening + original_call_native = ax._call_native + + def debug_call_native(method_name, *args, **kwargs): + if method_name == 'scatter': + print(f"scatter called with args: {args}") + print(f"scatter called with kwargs keys: {list(kwargs.keys())}") + if 'edgecolors' in kwargs: + print(f"edgecolors: {kwargs['edgecolors']}") + if 'alpha' in kwargs: + print(f"alpha: {kwargs['alpha']}") + if 'c' in kwargs: + print(f"c (color): {kwargs['c']}") + if 's' in kwargs: + print(f"s (size): {kwargs['s']}") + result = original_call_native(method_name, *args, **kwargs) + return result + + ax._call_native = debug_call_native + + # Test single row with alpha + try: + result = ax.scatter("x", "y", s="sizes", data=d.loc[[1]], fc="red8", ec="none", alpha=1) + print("Single row with alpha: SUCCESS") + print(f"Result type: {type(result)}") + + # Check if the scatter plot has the right edge color + if hasattr(result, 'get_edgecolors'): + edge_colors = result.get_edgecolors() + print(f"Actual edge colors: {edge_colors}") + + # Let's also test the opposite case + print("\n=== Testing multiple rows with alpha ===") + ax.clear() + result2 = ax.scatter("x", "y", s="sizes", data=d, fc="red8", ec="none", alpha=1) + print("Multiple rows with alpha: SUCCESS") + + if hasattr(result2, 'get_edgecolors'): + edge_colors2 = result2.get_edgecolors() + print(f"Actual edge colors: {edge_colors2}") + + except Exception as e: + print(f"ERROR - {e}") + import traceback + traceback.print_exc() + + plt.close() + + +if __name__ == "__main__": + debug_scatter_call() \ No newline at end of file diff --git a/test_ec_processing.py b/test_ec_processing.py new file mode 100644 index 000000000..78c81e2df --- /dev/null +++ b/test_ec_processing.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Test to see what happens to ec parameter during preprocessing.""" + +import pandas as pd +import numpy as np +import ultraplot as pplt + + +def test_ec_processing(): + """Test what happens to the ec parameter.""" + + # Test data + d1 = pd.DataFrame(dict(x=[1, 2], y=[1, 2], sizes=[300, 300])) # 2 rows + d2 = pd.DataFrame(dict(x=[2], y=[2], sizes=[300])) # 1 row + + # Create figure + fig, axes = pplt.subplots(ncols=2, figsize=(8, 4)) + + # Patch the _from_data function to see what it does + from ultraplot.internals import inputs + original_from_data = inputs._from_data + + def debug_from_data(data, *args): + print(f"_from_data called with data: {type(data)}, len: {len(data) if hasattr(data, '__len__') else 'N/A'}") + print(f"_from_data args: {args}") + result = original_from_data(data, *args) + if result is not None: + print(f"_from_data result: {result}") + return result + + inputs._from_data = debug_from_data + + # Test with multiple rows + print("=== Multiple rows case ===") + try: + axes[0].scatter("x", "y", s="sizes", data=d1, fc="red8", ec="none", alpha=1) + print("Multiple rows: SUCCESS") + except Exception as e: + print(f"Multiple rows: ERROR - {e}") + + # Test with single row + print("\n=== Single row case ===") + try: + axes[1].scatter("x", "y", s="sizes", data=d2, fc="red8", ec="none", alpha=1) + print("Single row: SUCCESS") + except Exception as e: + print(f"Single row: ERROR - {e}") + + # Restore original function + inputs._from_data = original_from_data + + import matplotlib.pyplot as plt + plt.close() + + +if __name__ == "__main__": + test_ec_processing() \ No newline at end of file diff --git a/test_edge_issue.py b/test_edge_issue.py new file mode 100644 index 000000000..49a134924 --- /dev/null +++ b/test_edge_issue.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Test script to reproduce the edgecolor issue with scatter plots.""" + +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import ultraplot as pplt + + +def test_edgecolor_issue(): + """Test to reproduce the edgecolor issue described in the GitHub issue.""" + # Create test data + d = pd.DataFrame(dict(x=[1, 2], y=[1, 2], sizes=[300, 300])) + + # Create subplots + fig, ax = pplt.subplots(ncols=3, figsize=(12, 4)) + + # Test case 1: Multiple rows with alpha - should work correctly + ax[0].scatter("x", "y", s="sizes", data=d, fc="red8", ec="none", alpha=1) + + # Test case 2: Single row with alpha - BUG: edgecolor ignored + ax[1].scatter("x", "y", s="sizes", data=d.loc[[1]], fc="red8", ec="none", alpha=1) + + # Test case 3: Single row without alpha - should work correctly + ax[2].scatter("x", "y", s="sizes", data=d.loc[[1]], fc="red8", ec="none") + + # Format axes + ax.format( + xlim=(0.8, 2.2), + ylim=(0.9, 2.1), + suptitle="ax[0]: Right ec with len(d)>1 and alpha" + + "\nax[1]: Wrong ec with len(d)=1 and alpha" + + "\nax[2]: Right ec with len(d)=1 and no alpha", + suptitle_kw={"ha": "left"}, + ) + + plt.show() + return fig, ax + + +if __name__ == "__main__": + fig, ax = test_edgecolor_issue() \ No newline at end of file From 81eb708c1568d1518ef8c8588182d96be6ed8aea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 23:10:52 +0000 Subject: [PATCH 3/5] Fix edgecolor issue for scatter plots with single-row DataFrame data Co-authored-by: cvanelteren <19485143+cvanelteren@users.noreply.github.com> --- debug_scatter.py | 73 --------------------------------- test_ec_processing.py | 57 ------------------------- test_edge_issue.py | 42 ------------------- ultraplot/internals/inputs.py | 8 ++++ ultraplot/tests/test_1dplots.py | 31 ++++++++++++++ 5 files changed, 39 insertions(+), 172 deletions(-) delete mode 100644 debug_scatter.py delete mode 100644 test_ec_processing.py delete mode 100644 test_edge_issue.py diff --git a/debug_scatter.py b/debug_scatter.py deleted file mode 100644 index 2ea90bfeb..000000000 --- a/debug_scatter.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -"""Debug script to understand the edgecolor issue.""" - -import pandas as pd -import numpy as np -import ultraplot as pplt -import matplotlib.pyplot as plt - - -def debug_scatter_call(): - """Debug what happens during scatter calls with different data sizes.""" - - # Test data - d = pd.DataFrame(dict(x=[1, 2], y=[1, 2], sizes=[300, 300])) - - # Create figure - fig, axes = pplt.subplots(ncols=1, figsize=(4, 4)) - ax = axes[0] # Get the actual axis object - - # Try to inspect the arguments being passed to matplotlib's scatter - print("=== Testing single row with alpha ===") - - # Monkey patch to see what's happening - original_call_native = ax._call_native - - def debug_call_native(method_name, *args, **kwargs): - if method_name == 'scatter': - print(f"scatter called with args: {args}") - print(f"scatter called with kwargs keys: {list(kwargs.keys())}") - if 'edgecolors' in kwargs: - print(f"edgecolors: {kwargs['edgecolors']}") - if 'alpha' in kwargs: - print(f"alpha: {kwargs['alpha']}") - if 'c' in kwargs: - print(f"c (color): {kwargs['c']}") - if 's' in kwargs: - print(f"s (size): {kwargs['s']}") - result = original_call_native(method_name, *args, **kwargs) - return result - - ax._call_native = debug_call_native - - # Test single row with alpha - try: - result = ax.scatter("x", "y", s="sizes", data=d.loc[[1]], fc="red8", ec="none", alpha=1) - print("Single row with alpha: SUCCESS") - print(f"Result type: {type(result)}") - - # Check if the scatter plot has the right edge color - if hasattr(result, 'get_edgecolors'): - edge_colors = result.get_edgecolors() - print(f"Actual edge colors: {edge_colors}") - - # Let's also test the opposite case - print("\n=== Testing multiple rows with alpha ===") - ax.clear() - result2 = ax.scatter("x", "y", s="sizes", data=d, fc="red8", ec="none", alpha=1) - print("Multiple rows with alpha: SUCCESS") - - if hasattr(result2, 'get_edgecolors'): - edge_colors2 = result2.get_edgecolors() - print(f"Actual edge colors: {edge_colors2}") - - except Exception as e: - print(f"ERROR - {e}") - import traceback - traceback.print_exc() - - plt.close() - - -if __name__ == "__main__": - debug_scatter_call() \ No newline at end of file diff --git a/test_ec_processing.py b/test_ec_processing.py deleted file mode 100644 index 78c81e2df..000000000 --- a/test_ec_processing.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 -"""Test to see what happens to ec parameter during preprocessing.""" - -import pandas as pd -import numpy as np -import ultraplot as pplt - - -def test_ec_processing(): - """Test what happens to the ec parameter.""" - - # Test data - d1 = pd.DataFrame(dict(x=[1, 2], y=[1, 2], sizes=[300, 300])) # 2 rows - d2 = pd.DataFrame(dict(x=[2], y=[2], sizes=[300])) # 1 row - - # Create figure - fig, axes = pplt.subplots(ncols=2, figsize=(8, 4)) - - # Patch the _from_data function to see what it does - from ultraplot.internals import inputs - original_from_data = inputs._from_data - - def debug_from_data(data, *args): - print(f"_from_data called with data: {type(data)}, len: {len(data) if hasattr(data, '__len__') else 'N/A'}") - print(f"_from_data args: {args}") - result = original_from_data(data, *args) - if result is not None: - print(f"_from_data result: {result}") - return result - - inputs._from_data = debug_from_data - - # Test with multiple rows - print("=== Multiple rows case ===") - try: - axes[0].scatter("x", "y", s="sizes", data=d1, fc="red8", ec="none", alpha=1) - print("Multiple rows: SUCCESS") - except Exception as e: - print(f"Multiple rows: ERROR - {e}") - - # Test with single row - print("\n=== Single row case ===") - try: - axes[1].scatter("x", "y", s="sizes", data=d2, fc="red8", ec="none", alpha=1) - print("Single row: SUCCESS") - except Exception as e: - print(f"Single row: ERROR - {e}") - - # Restore original function - inputs._from_data = original_from_data - - import matplotlib.pyplot as plt - plt.close() - - -if __name__ == "__main__": - test_ec_processing() \ No newline at end of file diff --git a/test_edge_issue.py b/test_edge_issue.py deleted file mode 100644 index 49a134924..000000000 --- a/test_edge_issue.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -"""Test script to reproduce the edgecolor issue with scatter plots.""" - -import pandas as pd -import numpy as np -import matplotlib.pyplot as plt -import ultraplot as pplt - - -def test_edgecolor_issue(): - """Test to reproduce the edgecolor issue described in the GitHub issue.""" - # Create test data - d = pd.DataFrame(dict(x=[1, 2], y=[1, 2], sizes=[300, 300])) - - # Create subplots - fig, ax = pplt.subplots(ncols=3, figsize=(12, 4)) - - # Test case 1: Multiple rows with alpha - should work correctly - ax[0].scatter("x", "y", s="sizes", data=d, fc="red8", ec="none", alpha=1) - - # Test case 2: Single row with alpha - BUG: edgecolor ignored - ax[1].scatter("x", "y", s="sizes", data=d.loc[[1]], fc="red8", ec="none", alpha=1) - - # Test case 3: Single row without alpha - should work correctly - ax[2].scatter("x", "y", s="sizes", data=d.loc[[1]], fc="red8", ec="none") - - # Format axes - ax.format( - xlim=(0.8, 2.2), - ylim=(0.9, 2.1), - suptitle="ax[0]: Right ec with len(d)>1 and alpha" - + "\nax[1]: Wrong ec with len(d)=1 and alpha" - + "\nax[2]: Right ec with len(d)=1 and no alpha", - suptitle_kw={"ha": "left"}, - ) - - plt.show() - return fig, ax - - -if __name__ == "__main__": - fig, ax = test_edgecolor_issue() \ No newline at end of file diff --git a/ultraplot/internals/inputs.py b/ultraplot/internals/inputs.py index 25bcbf6ec..922b559ea 100644 --- a/ultraplot/internals/inputs.py +++ b/ultraplot/internals/inputs.py @@ -247,6 +247,7 @@ def _from_data(data, *args): if data is None: return args = list(args) + found_in_data = False for i, arg in enumerate(args): if isinstance(arg, str): try: @@ -255,6 +256,13 @@ def _from_data(data, *args): pass else: args[i] = array + found_in_data = True + + # If only one argument and it wasn't found in data, return the original scalar + # This prevents scalar values like "none" from becoming ["none"] + if len(args) == 1 and not found_in_data: + return args[0] + return args diff --git a/ultraplot/tests/test_1dplots.py b/ultraplot/tests/test_1dplots.py index 84debe9cc..40e28b971 100644 --- a/ultraplot/tests/test_1dplots.py +++ b/ultraplot/tests/test_1dplots.py @@ -295,6 +295,37 @@ def test_scatter_args(rng): return fig +def test_scatter_edgecolor_single_row(): + """ + Test that edgecolor is properly handled for single-row DataFrame input. + This is a regression test for issue #324. + """ + import pandas as pd + + # Create test data + df_multi = pd.DataFrame({'x': [1, 2], 'y': [1, 2], 'sizes': [300, 300]}) + df_single = pd.DataFrame({'x': [2], 'y': [2], 'sizes': [300]}) + + fig, axs = uplt.subplots(ncols=3, share=0) + + # Test multiple rows with alpha + result1 = axs[0].scatter('x', 'y', s='sizes', data=df_multi, fc='red8', ec='none', alpha=1) + + # Test single row with alpha (the problematic case) + result2 = axs[1].scatter('x', 'y', s='sizes', data=df_single, fc='red8', ec='none', alpha=1) + + # Test single row without alpha + result3 = axs[2].scatter('x', 'y', s='sizes', data=df_single, fc='red8', ec='none') + + # Verify that edgecolors are correctly set to no edges for all cases + # An empty array means no edges (which is what 'none' should produce) + assert len(result1.get_edgecolors()) == 0, "Multiple rows should have no edges" + assert len(result2.get_edgecolors()) == 0, "Single row with alpha should have no edges" + assert len(result3.get_edgecolors()) == 0, "Single row without alpha should have no edges" + + return fig + + @pytest.mark.mpl_image_compare def test_scatter_inbounds(): """ From 3056fa42d21fe3ee18b3d23ba98e097746b23775 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 24 Aug 2025 07:26:52 +0000 Subject: [PATCH 4/5] Add pytest.mark.mpl_image_compare decorator to test_scatter_edgecolor_single_row Co-authored-by: cvanelteren <19485143+cvanelteren@users.noreply.github.com> --- ultraplot/tests/test_1dplots.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ultraplot/tests/test_1dplots.py b/ultraplot/tests/test_1dplots.py index 40e28b971..7bacd4033 100644 --- a/ultraplot/tests/test_1dplots.py +++ b/ultraplot/tests/test_1dplots.py @@ -295,6 +295,7 @@ def test_scatter_args(rng): return fig +@pytest.mark.mpl_image_compare def test_scatter_edgecolor_single_row(): """ Test that edgecolor is properly handled for single-row DataFrame input. From 5edfe47c6148956b58519b057052d1d959fd1e42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 24 Aug 2025 07:30:48 +0000 Subject: [PATCH 5/5] Format code with black Co-authored-by: cvanelteren <19485143+cvanelteren@users.noreply.github.com> --- ultraplot/internals/inputs.py | 4 ++-- ultraplot/tests/test_1dplots.py | 36 ++++++++++++++++++++------------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/ultraplot/internals/inputs.py b/ultraplot/internals/inputs.py index 922b559ea..ef3fe3876 100644 --- a/ultraplot/internals/inputs.py +++ b/ultraplot/internals/inputs.py @@ -257,12 +257,12 @@ def _from_data(data, *args): else: args[i] = array found_in_data = True - + # If only one argument and it wasn't found in data, return the original scalar # This prevents scalar values like "none" from becoming ["none"] if len(args) == 1 and not found_in_data: return args[0] - + return args diff --git a/ultraplot/tests/test_1dplots.py b/ultraplot/tests/test_1dplots.py index 7bacd4033..eee2178bb 100644 --- a/ultraplot/tests/test_1dplots.py +++ b/ultraplot/tests/test_1dplots.py @@ -302,28 +302,36 @@ def test_scatter_edgecolor_single_row(): This is a regression test for issue #324. """ import pandas as pd - + # Create test data - df_multi = pd.DataFrame({'x': [1, 2], 'y': [1, 2], 'sizes': [300, 300]}) - df_single = pd.DataFrame({'x': [2], 'y': [2], 'sizes': [300]}) - + df_multi = pd.DataFrame({"x": [1, 2], "y": [1, 2], "sizes": [300, 300]}) + df_single = pd.DataFrame({"x": [2], "y": [2], "sizes": [300]}) + fig, axs = uplt.subplots(ncols=3, share=0) - + # Test multiple rows with alpha - result1 = axs[0].scatter('x', 'y', s='sizes', data=df_multi, fc='red8', ec='none', alpha=1) - + result1 = axs[0].scatter( + "x", "y", s="sizes", data=df_multi, fc="red8", ec="none", alpha=1 + ) + # Test single row with alpha (the problematic case) - result2 = axs[1].scatter('x', 'y', s='sizes', data=df_single, fc='red8', ec='none', alpha=1) - + result2 = axs[1].scatter( + "x", "y", s="sizes", data=df_single, fc="red8", ec="none", alpha=1 + ) + # Test single row without alpha - result3 = axs[2].scatter('x', 'y', s='sizes', data=df_single, fc='red8', ec='none') - + result3 = axs[2].scatter("x", "y", s="sizes", data=df_single, fc="red8", ec="none") + # Verify that edgecolors are correctly set to no edges for all cases # An empty array means no edges (which is what 'none' should produce) assert len(result1.get_edgecolors()) == 0, "Multiple rows should have no edges" - assert len(result2.get_edgecolors()) == 0, "Single row with alpha should have no edges" - assert len(result3.get_edgecolors()) == 0, "Single row without alpha should have no edges" - + assert ( + len(result2.get_edgecolors()) == 0 + ), "Single row with alpha should have no edges" + assert ( + len(result3.get_edgecolors()) == 0 + ), "Single row without alpha should have no edges" + return fig