Repository navigation
fix(pyplot): stop minorticks_on() pinning automatic major ticks #494
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
base: main
Are you sure you want to change the base?
Changes from all commits
f4149e6
8e7bff6
bd65812
21ccf0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Keep automatic major ticks responsive to zoom when enabling minor ticks with xy.pyplot.minorticks_on(). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7462,7 +7462,13 @@ def _apply_tickers( | |
| pad = (hi - lo) * 1e-9 | ||
| ticks = ticks[(ticks >= lo - pad) & (ticks <= hi + pad)] | ||
| auto_log = is_log | ||
| props["tick_values"] = list(map(float, _scale_values(ticks, spec))) | ||
| majors_are_auto = ( | ||
| locator is None | ||
| and formatter is None | ||
| and authored_labels is None | ||
| and not is_log | ||
| and "tick_values" not in props | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
|
||
| ) | ||
| if formatter is not None: | ||
| props["tick_labels"] = _formatter_tick_labels( | ||
| formatter, | ||
|
|
@@ -7480,6 +7486,9 @@ def _apply_tickers( | |
| props["tick_labels"] = [f"{value:g}" for value in ticks] | ||
| else: | ||
| props.pop("tick_labels", None) | ||
| # Authoring positions freezes an automatic axis; labels need them anyway. | ||
| if not majors_are_auto or "tick_labels" in props: | ||
| props["tick_values"] = list(map(float, _scale_values(ticks, spec))) | ||
| if len(ticks): | ||
| props["tick_count"] = len(ticks) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the majors are automatic, this line overwrites the f, a = plt.subplots(); a.plot([0, 10], [0, 10]); a.locator_params(nbins=4)
a.minorticks_on()
a._build_chart(600, 400).figure().build_payload()[0]["x_axis"]["tick_count"]
# without minorticks_on(): 4 with minorticks_on(): 6In the default case the count also changes, from 9 to 6. The PR description says Requested change: when Generated by Claude Code |
||
| else: | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -294,3 +294,40 @@ def test_named_annotation_accepts_relative_fontsize() -> None: | |||||||||
| annotation = ax.annotate(text="note", xy=(0, 0), fontsize="small") | ||||||||||
|
|
||||||||||
| assert annotation._entry["kwargs"]["style"]["font_size"] == pytest.approx(8.5) | ||||||||||
|
|
||||||||||
|
|
||||||||||
| def test_minorticks_on_leaves_automatic_major_ticks_unpinned() -> None: | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PR has no news fragment. Generated by Claude Code |
||||||||||
| """Enabling minor ticks must not freeze the majors into authored positions. | ||||||||||
|
|
||||||||||
| Authored `tick_values` are filtered to the visible window by the renderer, | ||||||||||
| so pinning them here makes the tick labels disappear once the view is | ||||||||||
| zoomed past them. | ||||||||||
| """ | ||||||||||
| _fig, ax = plt.subplots() | ||||||||||
| ax.plot([0, 10], [0, 10]) | ||||||||||
| ax.minorticks_on() | ||||||||||
|
|
||||||||||
| spec, _blob = ax._build_chart(600, 400).figure().build_payload() | ||||||||||
| assert spec["x_axis"].get("minor_tick_values") | ||||||||||
| assert spec["x_axis"].get("tick_values") is None | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @pytest.mark.parametrize("scale", ["symlog", "asinh", "logit"]) | ||||||||||
| def test_shared_axis_keeps_nonlinear_labels_paired_with_positions(scale: str) -> None: | ||||||||||
| """A follower axis publishes tick labels, so it must publish positions too. | ||||||||||
|
|
||||||||||
| Scale defaults live on the axes that set the scale, not on the shared ticker | ||||||||||
| source, so a `sharex` follower reaches the label branch with no locator of | ||||||||||
| its own. Labels without positions are rejected when the chart is built. | ||||||||||
| """ | ||||||||||
| xs = np.linspace(-100, 100, 50) | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The logit case uses data spanning [-100, 100], far outside the (0,1) domain that a logit (probability) scale is defined on. In Prompt for AI agents |
||||||||||
| _fig, (leader, follower) = plt.subplots(2, sharex=True) | ||||||||||
| leader.plot(xs, xs) | ||||||||||
| follower.plot(xs, xs) | ||||||||||
| follower.set_xscale(scale, **({"linthresh": 2.0} if scale == "symlog" else {})) | ||||||||||
|
Comment on lines
+323
to
+327
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target test ---'
sed -n '280,345p' tests/pyplot/test_axis_tick_gallery_compat.py
printf '%s\n' '--- scale references ---'
rg -n "logit|set_xscale|linthresh|tick_values" . -g '*.py' -g '!build/**' -g '!dist/**' | head -200Repository: reflex-dev/xy Length of output: 20922 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
p = Path("tests/pyplot/test_axis_tick_gallery_compat.py")
lines = p.read_text().splitlines()
for i in range(295, 338):
print(f"{i+1:4}: {lines[i]}")
PY
printf '%s\n' '--- candidate implementation files ---'
git ls-files | rg '(^|/)(scale|axis|ticker|transforms?)[^/]*\.py$|matplotlib/.*\.py$' | head -100Repository: reflex-dev/xy Length of output: 2076 🏁 Script executed: #!/bin/bash
set -eu
sed -n '295,340p' tests/pyplot/test_axis_tick_gallery_compat.py
rg -n "class .*Logit|def .*logit|LogitScale|set_xscale|tick_values" . -g '*.py' | head -200Repository: reflex-dev/xy Length of output: 21429 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- scale definitions ---'
sed -n '260,325p' python/xy/pyplot/_axes.py
sed -n '6165,6345p' python/xy/pyplot/_axes.py
printf '%s\n' '--- shared-axis tick generation ---'
sed -n '6700,6805p' python/xy/pyplot/_axes.py
printf '%s\n' '--- logit locator ---'
sed -n '390,545p' python/xy/pyplot/_ticker.py
printf '%s\n' '--- logit domain regression ---'
sed -n '350,410p' tests/pyplot/test_silent_drop_regressions.pyRepository: reflex-dev/xy Length of output: 23547 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
for path in ("python/xy/pyplot/_axes.py", "python/xy/pyplot/_ticker.py"):
text = Path(path).read_text().splitlines()
print(f"--- {path} ---")
needles = ("def _scale_values", "def _scale_limits", "LogitLocator", "def _get_tick", "def _axis_props")
for i, line in enumerate(text):
if any(needle in line for needle in needles):
lo, hi = max(0, i-8), min(len(text), i+35)
print(f"[{lo+1}:{hi}]")
for j in range(lo, hi):
print(f"{j+1:5}: {text[j]}")
PYRepository: reflex-dev/xy Length of output: 11368 🏁 Script executed: #!/bin/bash
set -eu
sed -n '280,320p' python/xy/pyplot/_axes.py
sed -n '6180,6335p' python/xy/pyplot/_axes.py
sed -n '6740,6795p' python/xy/pyplot/_axes.py
sed -n '412,455p' python/xy/pyplot/_ticker.py
sed -n '373,405p' tests/pyplot/test_silent_drop_regressions.pyRepository: reflex-dev/xy Length of output: 14200 🏁 Script executed: #!/bin/bash
set -eu
rg -n "def _transform_entry_axis|def get_xlim|def set_xlim|def _build_axis|tick_labels|_inherit_shared_axis_state" python/xy/pyplot/_axes.py
sed -n '320,390p' python/xy/pyplot/_axes.py
sed -n '6480,6655p' python/xy/pyplot/_axes.py
sed -n '7155,7195p' python/xy/pyplot/_axes.py
sed -n '7195,7270p' python/xy/pyplot/_axes.pyRepository: reflex-dev/xy Length of output: 18783 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
import numpy as np
xs = np.linspace(-100, 100, 50)
valid_xs = np.linspace(0.01, 0.99, 50)
def logit_transform(values):
values = np.asarray(values, dtype=float)
with np.errstate(divide="ignore", invalid="ignore"):
result = np.log10(values / (1.0 - values))
return np.where((values > 0.0) & (values < 1.0), result, np.nan)
for name, values in (("current", xs), ("proposed", valid_xs)):
transformed = logit_transform(values)
print(name, "valid_points=", np.isfinite(transformed).sum(),
"masked_points=", np.isnan(transformed).sum(),
"data_min=", values.min(), "data_max=", values.max())
PYRepository: reflex-dev/xy Length of output: 269 🏁 Script executed: #!/bin/bash
set -eu
sed -n '410,450p' python/xy/pyplot/_axes.py
rg -n "def _auto_domain|_auto_domain\(" python/xy/pyplot/_axes.py | head -20
sed -n '3900,3995p' python/xy/pyplot/_axes.pyRepository: reflex-dev/xy Length of output: 7539 Use valid x data for the
🤖 Prompt for AI AgentsSource: MCP tools |
||||||||||
| follower.minorticks_on() | ||||||||||
|
|
||||||||||
| spec, _blob = follower._build_chart(600, 400).figure().build_payload() | ||||||||||
| x_axis = spec["x_axis"] | ||||||||||
|
|
||||||||||
| assert len(x_axis["tick_labels"]) == len(x_axis["tick_values"]) | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This equality is vacuous when both arrays are empty, so the regression can pass without producing nonlinear labels at all. Assert that Prompt for AI agents
Suggested change
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changes behavior that the spec describes, and the spec is not updated.
CLAUDE.mdsaysspec/is the source of truth, and that a change is not complete while its spec is stale. Please update:spec/matplotlib/compat.md(theset_major_locatorrow). It says "minor ticks do not render". Make it say thatminorticks_on()/AutoMinorLocatorkeep the majors automatic, and thatminor_tick_valuesis a fixed list computed for the initial domain, which does not regenerate on zoom. This is the caveat in your PR description.spec/design/renderer-architecture.md(the paragraph aboutminor_tick_values, near line 340). It says pyplot uses the minor tier only for log subdivisions. It also says "An authoredtick_valuesarray is the labelled major tier". Add that an automatic linear axis with minor ticks sends minors and notick_values.Generated by Claude Code