Skip to content

Commit 2b8decd

Browse files
authored
CLN: de-duplicate code in _matplotlib.converter (#55155)
* CLN: de-duplicate _matplotlib.converters * REF: avoid changing type of vmin/vmax * CLN: de-duplicate * REF: de-duplicate, annotate * REF: standardize * comment * mypy fixup
1 parent 699a8e4 commit 2b8decd

File tree

3 files changed

+104
-103
lines changed

3 files changed

+104
-103
lines changed

pandas/_libs/tslibs/dtypes.pyx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,6 @@ cdef NPY_DATETIMEUNIT freq_group_code_to_npy_unit(int freq) noexcept nogil:
460460
return NPY_DATETIMEUNIT.NPY_FR_D
461461

462462

463-
# TODO: use in _matplotlib.converter?
464463
cpdef int64_t periods_per_day(
465464
NPY_DATETIMEUNIT reso=NPY_DATETIMEUNIT.NPY_FR_ns
466465
) except? -1:

pandas/plotting/_matplotlib/converter.py

Lines changed: 101 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from typing import (
1212
TYPE_CHECKING,
1313
Any,
14-
Final,
1514
cast,
1615
)
1716

@@ -30,8 +29,14 @@
3029
Timestamp,
3130
to_offset,
3231
)
33-
from pandas._libs.tslibs.dtypes import FreqGroup
34-
from pandas._typing import F
32+
from pandas._libs.tslibs.dtypes import (
33+
FreqGroup,
34+
periods_per_day,
35+
)
36+
from pandas._typing import (
37+
F,
38+
npt,
39+
)
3540

3641
from pandas.core.dtypes.common import (
3742
is_float,
@@ -60,15 +65,6 @@
6065

6166
from pandas._libs.tslibs.offsets import BaseOffset
6267

63-
# constants
64-
HOURS_PER_DAY: Final = 24.0
65-
MIN_PER_HOUR: Final = 60.0
66-
SEC_PER_MIN: Final = 60.0
67-
68-
SEC_PER_HOUR: Final = SEC_PER_MIN * MIN_PER_HOUR
69-
SEC_PER_DAY: Final = SEC_PER_HOUR * HOURS_PER_DAY
70-
71-
MUSEC_PER_DAY: Final = 10**6 * SEC_PER_DAY
7268

7369
_mpl_units = {} # Cache for units overwritten by us
7470

@@ -495,7 +491,7 @@ def _get_default_annual_spacing(nyears) -> tuple[int, int]:
495491
return (min_spacing, maj_spacing)
496492

497493

498-
def period_break(dates: PeriodIndex, period: str) -> np.ndarray:
494+
def _period_break(dates: PeriodIndex, period: str) -> npt.NDArray[np.intp]:
499495
"""
500496
Returns the indices where the given period changes.
501497
@@ -506,12 +502,17 @@ def period_break(dates: PeriodIndex, period: str) -> np.ndarray:
506502
period : str
507503
Name of the period to monitor.
508504
"""
505+
mask = _period_break_mask(dates, period)
506+
return np.nonzero(mask)[0]
507+
508+
509+
def _period_break_mask(dates: PeriodIndex, period: str) -> npt.NDArray[np.bool_]:
509510
current = getattr(dates, period)
510511
previous = getattr(dates - 1 * dates.freq, period)
511-
return np.nonzero(current - previous)[0]
512+
return current != previous
512513

513514

514-
def has_level_label(label_flags: np.ndarray, vmin: float) -> bool:
515+
def has_level_label(label_flags: npt.NDArray[np.intp], vmin: float) -> bool:
515516
"""
516517
Returns true if the ``label_flags`` indicate there is at least one label
517518
for this level.
@@ -527,54 +528,59 @@ def has_level_label(label_flags: np.ndarray, vmin: float) -> bool:
527528
return True
528529

529530

530-
def _daily_finder(vmin, vmax, freq: BaseOffset):
531+
def _get_periods_per_ymd(freq: BaseOffset) -> tuple[int, int, int]:
531532
# error: "BaseOffset" has no attribute "_period_dtype_code"
532533
dtype_code = freq._period_dtype_code # type: ignore[attr-defined]
533534
freq_group = FreqGroup.from_period_dtype_code(dtype_code)
534535

535-
periodsperday = -1
536+
ppd = -1 # placeholder for above-day freqs
536537

537538
if dtype_code >= FreqGroup.FR_HR.value:
538-
if freq_group == FreqGroup.FR_NS:
539-
periodsperday = 24 * 60 * 60 * 1000000000
540-
elif freq_group == FreqGroup.FR_US:
541-
periodsperday = 24 * 60 * 60 * 1000000
542-
elif freq_group == FreqGroup.FR_MS:
543-
periodsperday = 24 * 60 * 60 * 1000
544-
elif freq_group == FreqGroup.FR_SEC:
545-
periodsperday = 24 * 60 * 60
546-
elif freq_group == FreqGroup.FR_MIN:
547-
periodsperday = 24 * 60
548-
elif freq_group == FreqGroup.FR_HR:
549-
periodsperday = 24
550-
else: # pragma: no cover
551-
raise ValueError(f"unexpected frequency: {dtype_code}")
552-
periodsperyear = 365 * periodsperday
553-
periodspermonth = 28 * periodsperday
554-
539+
# error: "BaseOffset" has no attribute "_creso"
540+
ppd = periods_per_day(freq._creso) # type: ignore[attr-defined]
541+
ppm = 28 * ppd
542+
ppy = 365 * ppd
555543
elif freq_group == FreqGroup.FR_BUS:
556-
periodsperyear = 261
557-
periodspermonth = 19
544+
ppm = 19
545+
ppy = 261
558546
elif freq_group == FreqGroup.FR_DAY:
559-
periodsperyear = 365
560-
periodspermonth = 28
547+
ppm = 28
548+
ppy = 365
561549
elif freq_group == FreqGroup.FR_WK:
562-
periodsperyear = 52
563-
periodspermonth = 3
564-
else: # pragma: no cover
565-
raise ValueError("unexpected frequency")
550+
ppm = 3
551+
ppy = 52
552+
elif freq_group == FreqGroup.FR_MTH:
553+
ppm = 1
554+
ppy = 12
555+
elif freq_group == FreqGroup.FR_QTR:
556+
ppm = -1 # placerholder
557+
ppy = 4
558+
elif freq_group == FreqGroup.FR_ANN:
559+
ppm = -1 # placeholder
560+
ppy = 1
561+
else:
562+
raise NotImplementedError(f"Unsupported frequency: {dtype_code}")
563+
564+
return ppd, ppm, ppy
565+
566+
567+
def _daily_finder(vmin, vmax, freq: BaseOffset) -> np.ndarray:
568+
# error: "BaseOffset" has no attribute "_period_dtype_code"
569+
dtype_code = freq._period_dtype_code # type: ignore[attr-defined]
570+
571+
periodsperday, periodspermonth, periodsperyear = _get_periods_per_ymd(freq)
566572

567573
# save this for later usage
568574
vmin_orig = vmin
575+
(vmin, vmax) = (int(vmin), int(vmax))
576+
span = vmax - vmin + 1
569577

570-
(vmin, vmax) = (
571-
Period(ordinal=int(vmin), freq=freq),
572-
Period(ordinal=int(vmax), freq=freq),
578+
dates_ = period_range(
579+
start=Period(ordinal=vmin, freq=freq),
580+
end=Period(ordinal=vmax, freq=freq),
581+
freq=freq,
573582
)
574-
assert isinstance(vmin, Period)
575-
assert isinstance(vmax, Period)
576-
span = vmax.ordinal - vmin.ordinal + 1
577-
dates_ = period_range(start=vmin, end=vmax, freq=freq)
583+
578584
# Initialize the output
579585
info = np.zeros(
580586
span, dtype=[("val", np.int64), ("maj", bool), ("min", bool), ("fmt", "|S20")]
@@ -595,45 +601,38 @@ def first_label(label_flags):
595601

596602
# Case 1. Less than a month
597603
if span <= periodspermonth:
598-
day_start = period_break(dates_, "day")
599-
month_start = period_break(dates_, "month")
604+
day_start = _period_break(dates_, "day")
605+
month_start = _period_break(dates_, "month")
606+
year_start = _period_break(dates_, "year")
600607

601-
def _hour_finder(label_interval, force_year_start) -> None:
602-
_hour = dates_.hour
603-
_prev_hour = (dates_ - 1 * dates_.freq).hour
604-
hour_start = (_hour - _prev_hour) != 0
608+
def _hour_finder(label_interval: int, force_year_start: bool) -> None:
609+
target = dates_.hour
610+
mask = _period_break_mask(dates_, "hour")
605611
info_maj[day_start] = True
606-
info_min[hour_start & (_hour % label_interval == 0)] = True
607-
year_start = period_break(dates_, "year")
608-
info_fmt[hour_start & (_hour % label_interval == 0)] = "%H:%M"
612+
info_min[mask & (target % label_interval == 0)] = True
613+
info_fmt[mask & (target % label_interval == 0)] = "%H:%M"
609614
info_fmt[day_start] = "%H:%M\n%d-%b"
610615
info_fmt[year_start] = "%H:%M\n%d-%b\n%Y"
611616
if force_year_start and not has_level_label(year_start, vmin_orig):
612617
info_fmt[first_label(day_start)] = "%H:%M\n%d-%b\n%Y"
613618

614-
def _minute_finder(label_interval) -> None:
615-
hour_start = period_break(dates_, "hour")
616-
_minute = dates_.minute
617-
_prev_minute = (dates_ - 1 * dates_.freq).minute
618-
minute_start = (_minute - _prev_minute) != 0
619+
def _minute_finder(label_interval: int) -> None:
620+
target = dates_.minute
621+
hour_start = _period_break(dates_, "hour")
622+
mask = _period_break_mask(dates_, "minute")
619623
info_maj[hour_start] = True
620-
info_min[minute_start & (_minute % label_interval == 0)] = True
621-
year_start = period_break(dates_, "year")
622-
info_fmt = info["fmt"]
623-
info_fmt[minute_start & (_minute % label_interval == 0)] = "%H:%M"
624+
info_min[mask & (target % label_interval == 0)] = True
625+
info_fmt[mask & (target % label_interval == 0)] = "%H:%M"
624626
info_fmt[day_start] = "%H:%M\n%d-%b"
625627
info_fmt[year_start] = "%H:%M\n%d-%b\n%Y"
626628

627-
def _second_finder(label_interval) -> None:
628-
minute_start = period_break(dates_, "minute")
629-
_second = dates_.second
630-
_prev_second = (dates_ - 1 * dates_.freq).second
631-
second_start = (_second - _prev_second) != 0
632-
info["maj"][minute_start] = True
633-
info["min"][second_start & (_second % label_interval == 0)] = True
634-
year_start = period_break(dates_, "year")
635-
info_fmt = info["fmt"]
636-
info_fmt[second_start & (_second % label_interval == 0)] = "%H:%M:%S"
629+
def _second_finder(label_interval: int) -> None:
630+
target = dates_.second
631+
minute_start = _period_break(dates_, "minute")
632+
mask = _period_break_mask(dates_, "second")
633+
info_maj[minute_start] = True
634+
info_min[mask & (target % label_interval == 0)] = True
635+
info_fmt[mask & (target % label_interval == 0)] = "%H:%M:%S"
637636
info_fmt[day_start] = "%H:%M:%S\n%d-%b"
638637
info_fmt[year_start] = "%H:%M:%S\n%d-%b\n%Y"
639638

@@ -672,8 +671,6 @@ def _second_finder(label_interval) -> None:
672671
else:
673672
info_maj[month_start] = True
674673
info_min[day_start] = True
675-
year_start = period_break(dates_, "year")
676-
info_fmt = info["fmt"]
677674
info_fmt[day_start] = "%d"
678675
info_fmt[month_start] = "%d\n%b"
679676
info_fmt[year_start] = "%d\n%b\n%Y"
@@ -685,15 +682,15 @@ def _second_finder(label_interval) -> None:
685682

686683
# Case 2. Less than three months
687684
elif span <= periodsperyear // 4:
688-
month_start = period_break(dates_, "month")
685+
month_start = _period_break(dates_, "month")
689686
info_maj[month_start] = True
690687
if dtype_code < FreqGroup.FR_HR.value:
691688
info["min"] = True
692689
else:
693-
day_start = period_break(dates_, "day")
690+
day_start = _period_break(dates_, "day")
694691
info["min"][day_start] = True
695-
week_start = period_break(dates_, "week")
696-
year_start = period_break(dates_, "year")
692+
week_start = _period_break(dates_, "week")
693+
year_start = _period_break(dates_, "year")
697694
info_fmt[week_start] = "%d"
698695
info_fmt[month_start] = "\n\n%b"
699696
info_fmt[year_start] = "\n\n%b\n%Y"
@@ -704,9 +701,9 @@ def _second_finder(label_interval) -> None:
704701
info_fmt[first_label(month_start)] = "\n\n%b\n%Y"
705702
# Case 3. Less than 14 months ...............
706703
elif span <= 1.15 * periodsperyear:
707-
year_start = period_break(dates_, "year")
708-
month_start = period_break(dates_, "month")
709-
week_start = period_break(dates_, "week")
704+
year_start = _period_break(dates_, "year")
705+
month_start = _period_break(dates_, "month")
706+
week_start = _period_break(dates_, "week")
710707
info_maj[month_start] = True
711708
info_min[week_start] = True
712709
info_min[year_start] = False
@@ -717,17 +714,17 @@ def _second_finder(label_interval) -> None:
717714
info_fmt[first_label(month_start)] = "%b\n%Y"
718715
# Case 4. Less than 2.5 years ...............
719716
elif span <= 2.5 * periodsperyear:
720-
year_start = period_break(dates_, "year")
721-
quarter_start = period_break(dates_, "quarter")
722-
month_start = period_break(dates_, "month")
717+
year_start = _period_break(dates_, "year")
718+
quarter_start = _period_break(dates_, "quarter")
719+
month_start = _period_break(dates_, "month")
723720
info_maj[quarter_start] = True
724721
info_min[month_start] = True
725722
info_fmt[quarter_start] = "%b"
726723
info_fmt[year_start] = "%b\n%Y"
727724
# Case 4. Less than 4 years .................
728725
elif span <= 4 * periodsperyear:
729-
year_start = period_break(dates_, "year")
730-
month_start = period_break(dates_, "month")
726+
year_start = _period_break(dates_, "year")
727+
month_start = _period_break(dates_, "month")
731728
info_maj[year_start] = True
732729
info_min[month_start] = True
733730
info_min[year_start] = False
@@ -738,15 +735,15 @@ def _second_finder(label_interval) -> None:
738735
info_fmt[year_start] = "%b\n%Y"
739736
# Case 5. Less than 11 years ................
740737
elif span <= 11 * periodsperyear:
741-
year_start = period_break(dates_, "year")
742-
quarter_start = period_break(dates_, "quarter")
738+
year_start = _period_break(dates_, "year")
739+
quarter_start = _period_break(dates_, "quarter")
743740
info_maj[year_start] = True
744741
info_min[quarter_start] = True
745742
info_min[year_start] = False
746743
info_fmt[year_start] = "%Y"
747744
# Case 6. More than 12 years ................
748745
else:
749-
year_start = period_break(dates_, "year")
746+
year_start = _period_break(dates_, "year")
750747
year_break = dates_[year_start].year
751748
nyears = span / periodsperyear
752749
(min_anndef, maj_anndef) = _get_default_annual_spacing(nyears)
@@ -759,8 +756,8 @@ def _second_finder(label_interval) -> None:
759756
return info
760757

761758

762-
def _monthly_finder(vmin, vmax, freq):
763-
periodsperyear = 12
759+
def _monthly_finder(vmin, vmax, freq: BaseOffset) -> np.ndarray:
760+
_, _, periodsperyear = _get_periods_per_ymd(freq)
764761

765762
vmin_orig = vmin
766763
(vmin, vmax) = (int(vmin), int(vmax))
@@ -795,6 +792,7 @@ def _monthly_finder(vmin, vmax, freq):
795792
quarter_start = (dates_ % 3 == 0).nonzero()
796793
info_maj[year_start] = True
797794
# TODO: Check the following : is it really info['fmt'] ?
795+
# 2023-09-15 this is reached in test_finder_monthly
798796
info["fmt"][quarter_start] = True
799797
info["min"] = True
800798

@@ -829,8 +827,8 @@ def _monthly_finder(vmin, vmax, freq):
829827
return info
830828

831829

832-
def _quarterly_finder(vmin, vmax, freq):
833-
periodsperyear = 4
830+
def _quarterly_finder(vmin, vmax, freq: BaseOffset) -> np.ndarray:
831+
_, _, periodsperyear = _get_periods_per_ymd(freq)
834832
vmin_orig = vmin
835833
(vmin, vmax) = (int(vmin), int(vmax))
836834
span = vmax - vmin + 1
@@ -876,7 +874,8 @@ def _quarterly_finder(vmin, vmax, freq):
876874
return info
877875

878876

879-
def _annual_finder(vmin, vmax, freq):
877+
def _annual_finder(vmin, vmax, freq: BaseOffset) -> np.ndarray:
878+
# Note: small difference here vs other finders in adding 1 to vmax
880879
(vmin, vmax) = (int(vmin), int(vmax + 1))
881880
span = vmax - vmin + 1
882881

@@ -889,8 +888,9 @@ def _annual_finder(vmin, vmax, freq):
889888

890889
(min_anndef, maj_anndef) = _get_default_annual_spacing(span)
891890
major_idx = dates_ % maj_anndef == 0
891+
minor_idx = dates_ % min_anndef == 0
892892
info["maj"][major_idx] = True
893-
info["min"][(dates_ % min_anndef == 0)] = True
893+
info["min"][minor_idx] = True
894894
info["fmt"][major_idx] = "%Y"
895895

896896
return info
@@ -1087,7 +1087,7 @@ def format_timedelta_ticks(x, pos, n_decimals: int) -> str:
10871087
"""
10881088
Convert seconds to 'D days HH:MM:SS.F'
10891089
"""
1090-
s, ns = divmod(x, 10**9)
1090+
s, ns = divmod(x, 10**9) # TODO(non-nano): this looks like it assumes ns
10911091
m, s = divmod(s, 60)
10921092
h, m = divmod(m, 60)
10931093
d, h = divmod(h, 24)

0 commit comments

Comments
 (0)