Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hotfix contour #24

Merged
merged 24 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/build-docs.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
name: Build docs
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_call:
inputs:
python-version:
required: true
type: string

jobs:
build:
build-docs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
Expand All @@ -16,6 +17,7 @@ jobs:
environment-file: ./environment-dev.yml
init-shell: bash
create-args: --verbose
python=${{ inputs.python-version }}
cache-environment: true
cache-downloads: false

Expand Down
18 changes: 11 additions & 7 deletions .github/workflows/build-ultraplot.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
name: Build and Test
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_call:
inputs:
python-version:
required: true
type: string

jobs:
build:
build-ultraplot:
name: Test Python ${{ inputs.python-version }}
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
Expand All @@ -15,7 +17,9 @@ jobs:
with:
environment-file: ./environment-dev.yml
init-shell: bash
create-args: --verbose
create-args: >-
--verbose
python=${{ inputs.python-version }}
cache-environment: true
cache-downloads: false

Expand All @@ -24,4 +28,4 @@ jobs:
run: |
micromamba activate ultraplot-dev
pip install .
python -m pytest
python -m pytest --mpl
2 changes: 1 addition & 1 deletion .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
branches: [main]

jobs:
build:
linter:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
Expand Down
70 changes: 70 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Matrix Test
on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
get-python-versions:
runs-on: ubuntu-latest
outputs:
python-versions: ${{ steps.set-versions.outputs.python-versions }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"

- name: Install dependencies
run: pip install tomli

- id: set-versions
run: |
# Create a Python script to read and parse versions
cat > get_versions.py << 'EOF'
import tomli
import re
import json

# Read pyproject.toml
with open("pyproject.toml", "rb") as f:
data = tomli.load(f)

# Get Python version requirement
python_req = data["project"]["requires-python"]

# Parse min and max versions
min_version = re.search(r">=(\d+\.\d+)", python_req)
max_version = re.search(r"<(\d+\.\d+)", python_req)

versions = []
if min_version and max_version:
# Convert version strings to tuples
min_v = tuple(map(int, min_version.group(1).split(".")))
max_v = tuple(map(int, max_version.group(1).split(".")))

# Generate version list
current = min_v
while current < max_v:
versions.append(".".join(map(str, current)))
current = (current[0], current[1] + 1)

# Print as JSON array
print(json.dumps(versions))
EOF

# Run the script and capture output
VERSIONS=$(python3 get_versions.py)
echo "Detected versions: ${VERSIONS}" # Debug output
echo "python-versions=${VERSIONS}" >> $GITHUB_OUTPUT

build:
needs: get-python-versions
strategy:
matrix:
python-version: ${{ fromJson(needs.get-python-versions.outputs.python-versions) }}
fail-fast: false
uses: ./.github/workflows/build-ultraplot.yml
with:
python-version: ${{ matrix.python-version }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
ultraplot/tests/baseline
# VIM extras
.session.vim
.vimsession
Expand Down
2 changes: 1 addition & 1 deletion environment-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: ultraplot-dev
channels:
- conda-forge
dependencies:
- python==3.12
- python>=3.8,<3.12
- numpy>=2.2
- matplotlib>=3.9.3
- cartopy
Expand Down
2 changes: 1 addition & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ channels:
- conda-forge
- nodefaults
dependencies:
- python >=3.9
- python >=3.8, <3.12
- pytest
- pytest-mpl
- numpy
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ maintainers = [
]
description = "A succinct matplotlib wrapper for making beautiful, publication-quality graphics."
readme = "README.rst"
requires-python = ">=3.6.0"
requires-python = ">=3.8, <3.12"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the upper bound here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basemap won't install for python>=3.12. Eventhough we don't have a hard requirement for it some of the tests will fail. I contacted the people from basemap and they are working on a fix. I put a crude PR up there that monkey patches basemap for these higher versions, but it isn't (and will likely never be) merged.

I checked the code with this monkey patch for basemap and ultraplot will pass all the tests for these higher versions.

license = {text = "MIT"}
classifiers = [
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Intended Audience :: Science/Research",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
Expand Down
103 changes: 42 additions & 61 deletions ultraplot/axes/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -1806,12 +1806,6 @@ def _fix_patch_edges(obj, edgefix=None, **kwargs):
obj.set_linestyle("-")
obj.set_linewidth(linewidth)
obj.set_edgecolor("face")

import matplotlib.patheffects as pe

obj.set_path_effects(
[pe.Stroke(linewidth=linewidth, linestyle="-", foreground="face")]
)
elif isinstance(obj, mcollections.Collection): # e.g. QuadMesh, PolyCollection
obj.set_linewidth(linewidth)
obj.set_edgecolor("face")
Expand Down Expand Up @@ -2477,54 +2471,43 @@ def _parse_cycle(
Whether to simply return the property cycle or apply it. The cycle is
only applied (and therefore reset) if it differs from the current one.
"""
# Create the property cycler and update it if necessary
# NOTE: Matplotlib Cycler() objects have built-in __eq__ operator
# so really easy to check if the cycler has changed!
if cycle is not None or cycle_kw:
cycle_kw = cycle_kw or {}
if ncycle != 1: # ignore for column-by-column plotting commands
cycle_kw.setdefault("N", ncycle) # if None then filled in Colormap()
if isinstance(cycle, str) and cycle.lower() == "none":
cycle = False
if not cycle:
args = ()
elif cycle is True: # consistency with 'False' ('reactivate' the cycler)
args = (rc["axes.prop_cycle"],)
else:
args = (cycle,)
cycle = constructor.Cycle(*args, **cycle_kw)
with warnings.catch_warnings(): # hide 'elementwise-comparison failed'
warnings.simplefilter("ignore", FutureWarning)
if return_cycle:
pass
elif cycle != self._active_cycle:
self.set_prop_cycle(cycle)

# Manually extract and apply settings to outgoing keyword arguments
# if native matplotlib function does not include desired properties
cycle_kw = cycle_kw or {}
cycle_manually = cycle_manually or {}
parser = self._get_lines # the _process_plot_var_args instance
props = {} # which keys to apply from property cycler
# BREAKING in mpl3.9.1 parse has cycle items and no longer posseses _prop_keys
for prop, key in cycle_manually.items():
if kwargs.get(key, None) is None and any(
prop in item for item in parser._cycler_items
):
props[prop] = key
if props:
for dict_ in parser._cycler_items:
for prop, key in props.items():
value = dict_[prop]
if (
key == "c"
): # special case: scatter() color must be converted to hex
value = pcolors.to_hex(value)
kwargs[key] = value

cycle_kw.setdefault("N", ncycle)

# Match-case for cycle resolution
match cycle:
case None if not cycle_kw:
resolved_cycle = None
case True:
resolved_cycle = constructor.Cycle(rc["axes.prop_cycle"])
case str() if cycle.lower() == "none":
resolved_cycle = None
case str() | int():
resolved_cycle = constructor.Cycle(cycle, **cycle_kw)
case _:
resolved_cycle = None

# Ignore cycle for single-column plotting
resolved_cycle = None if ncycle == 1 else resolved_cycle

# Return or apply cycle
if return_cycle:
return cycle, kwargs # needed for stem() to apply in a context()
else:
return kwargs
return resolved_cycle, kwargs

if resolved_cycle and resolved_cycle != self._active_cycle:
self.set_prop_cycle(resolved_cycle)

# Apply manual cycle properties
if cycle_manually:
current_prop = self._get_lines._cycler_items[self._get_lines._idx]
self._get_lines._idx = (self._get_lines._idx + 1) % len(self._active_cycle)
for prop, key in cycle_manually.items():
if kwargs.get(key) is None and prop in current_prop:
value = current_prop[prop]
kwargs[key] = pcolors.to_rgba(value) if key == "c" else value

return kwargs

def _parse_level_lim(
self,
Expand Down Expand Up @@ -3459,12 +3442,9 @@ def _apply_scatter(self, xs, ys, ss, cc, *, vert=True, **kwargs):
ys, kw = inputs._dist_reduce(ys, **kw)
ss, kw = self._parse_markersize(ss, **kw) # parse 's'

# Move _parse_cycle before _parse_color
kw = self._parse_cycle(xs.shape[1] if xs.ndim > 1 else 1, **kw)

# Only parse color if explicitly provided
infer_rgb = True
if cc is not None:
infer_rgb = True
if not isinstance(cc, str):
test = np.atleast_1d(cc)
if (
Expand All @@ -3482,14 +3462,14 @@ def _apply_scatter(self, xs, ys, ss, cc, *, vert=True, **kwargs):
infer_rgb=infer_rgb,
**kw,
)

# Create the cycler object by manually cycling and sanitzing the inputs
guide_kw = _pop_params(kw, self._update_guide)
objs = []
for _, n, x, y, s, c, kw in self._iter_arg_cols(xs, ys, ss, cc, **kw):
# Don't set 'c' explicitly unless it was provided
kw["s"] = s
if c is not None:
kw["c"] = c
# Cycle s and c as they are in cycle_manually
# Note: they could be None
kw["s"], kw["c"] = s, c
kw = self._parse_cycle(n, cycle_manually=cycle_manually, **kw)
*eb, kw = self._add_error_bars(x, y, vert=vert, default_barstds=True, **kw)
*es, kw = self._add_error_shading(x, y, vert=vert, color_key="c", **kw)
if not vert:
Expand Down Expand Up @@ -4029,6 +4009,7 @@ def _apply_violinplot(
bodies = artists.pop("bodies", ()) # should be no other entries
if bodies:
bodies = cbook.silent_list(type(bodies[0]).__name__, bodies)

for i, body in enumerate(bodies):
body.set_alpha(1.0) # change default to 1.0
if fillcolor[i] is not None:
Expand Down
Loading