Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 96f79ee

Browse files
committed
DEV: add HexBar3DCollection for plotting hexagonal prisms
MNT: add stub def for newly added functions FIX: `hexbin` to return grid info FIX: add omitted function, import, remove whitespace FIX: yscaling MNT: move `hexbin` helper to `cbook` FIX: whitespace FIX: whitespace FIX: stub FIX spelling (again!) FIX: whitespace
1 parent d2a2f9e commit 96f79ee

File tree

4 files changed

+261
-140
lines changed

4 files changed

+261
-140
lines changed

lib/matplotlib/axes/_axes.py

Lines changed: 6 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -5137,112 +5137,19 @@ def reduce_C_function(C: array) -> float
51375137

51385138
x, y, C = cbook.delete_masked_points(x, y, C)
51395139

5140-
# Set the size of the hexagon grid
5141-
if np.iterable(gridsize):
5142-
nx, ny = gridsize
5143-
else:
5144-
nx = gridsize
5145-
ny = int(nx / math.sqrt(3))
51465140
# Count the number of data in each hexagon
51475141
x = np.asarray(x, float)
51485142
y = np.asarray(y, float)
51495143

5150-
# Will be log()'d if necessary, and then rescaled.
5151-
tx = x
5152-
ty = y
5153-
5154-
if xscale == 'log':
5155-
if np.any(x <= 0.0):
5156-
raise ValueError(
5157-
"x contains non-positive values, so cannot be log-scaled")
5158-
tx = np.log10(tx)
5159-
if yscale == 'log':
5160-
if np.any(y <= 0.0):
5161-
raise ValueError(
5162-
"y contains non-positive values, so cannot be log-scaled")
5163-
ty = np.log10(ty)
5164-
if extent is not None:
5165-
xmin, xmax, ymin, ymax = extent
5166-
if xmin > xmax:
5167-
raise ValueError("In extent, xmax must be greater than xmin")
5168-
if ymin > ymax:
5169-
raise ValueError("In extent, ymax must be greater than ymin")
5170-
else:
5171-
xmin, xmax = (tx.min(), tx.max()) if len(x) else (0, 1)
5172-
ymin, ymax = (ty.min(), ty.max()) if len(y) else (0, 1)
5173-
5174-
# to avoid issues with singular data, expand the min/max pairs
5175-
xmin, xmax = mtransforms.nonsingular(xmin, xmax, expander=0.1)
5176-
ymin, ymax = mtransforms.nonsingular(ymin, ymax, expander=0.1)
5177-
5178-
nx1 = nx + 1
5179-
ny1 = ny + 1
5180-
nx2 = nx
5181-
ny2 = ny
5182-
n = nx1 * ny1 + nx2 * ny2
5183-
5184-
# In the x-direction, the hexagons exactly cover the region from
5185-
# xmin to xmax. Need some padding to avoid roundoff errors.
5186-
padding = 1.e-9 * (xmax - xmin)
5187-
xmin -= padding
5188-
xmax += padding
5144+
(*offsets, accum), (xmin, xmax), (ymin, ymax), (nx, ny) = cbook.hexbin(
5145+
x, y, C, gridsize, xscale, yscale, extent, reduce_C_function, mincnt
5146+
)
5147+
offsets = np.transpose(offsets)
51895148
sx = (xmax - xmin) / nx
51905149
sy = (ymax - ymin) / ny
5191-
# Positions in hexagon index coordinates.
5192-
ix = (tx - xmin) / sx
5193-
iy = (ty - ymin) / sy
5194-
ix1 = np.round(ix).astype(int)
5195-
iy1 = np.round(iy).astype(int)
5196-
ix2 = np.floor(ix).astype(int)
5197-
iy2 = np.floor(iy).astype(int)
5198-
# flat indices, plus one so that out-of-range points go to position 0.
5199-
i1 = np.where((0 <= ix1) & (ix1 < nx1) & (0 <= iy1) & (iy1 < ny1),
5200-
ix1 * ny1 + iy1 + 1, 0)
5201-
i2 = np.where((0 <= ix2) & (ix2 < nx2) & (0 <= iy2) & (iy2 < ny2),
5202-
ix2 * ny2 + iy2 + 1, 0)
5203-
5204-
d1 = (ix - ix1) ** 2 + 3.0 * (iy - iy1) ** 2
5205-
d2 = (ix - ix2 - 0.5) ** 2 + 3.0 * (iy - iy2 - 0.5) ** 2
5206-
bdist = (d1 < d2)
5207-
5208-
if C is None: # [1:] drops out-of-range points.
5209-
counts1 = np.bincount(i1[bdist], minlength=1 + nx1 * ny1)[1:]
5210-
counts2 = np.bincount(i2[~bdist], minlength=1 + nx2 * ny2)[1:]
5211-
accum = np.concatenate([counts1, counts2]).astype(float)
5212-
if mincnt is not None:
5213-
accum[accum < mincnt] = np.nan
5150+
5151+
if C is None:
52145152
C = np.ones(len(x))
5215-
else:
5216-
# store the C values in a list per hexagon index
5217-
Cs_at_i1 = [[] for _ in range(1 + nx1 * ny1)]
5218-
Cs_at_i2 = [[] for _ in range(1 + nx2 * ny2)]
5219-
for i in range(len(x)):
5220-
if bdist[i]:
5221-
Cs_at_i1[i1[i]].append(C[i])
5222-
else:
5223-
Cs_at_i2[i2[i]].append(C[i])
5224-
if mincnt is None:
5225-
mincnt = 1
5226-
accum = np.array(
5227-
[reduce_C_function(acc) if len(acc) >= mincnt else np.nan
5228-
for Cs_at_i in [Cs_at_i1, Cs_at_i2]
5229-
for acc in Cs_at_i[1:]], # [1:] drops out-of-range points.
5230-
float)
5231-
5232-
good_idxs = ~np.isnan(accum)
5233-
5234-
offsets = np.zeros((n, 2), float)
5235-
offsets[:nx1 * ny1, 0] = np.repeat(np.arange(nx1), ny1)
5236-
offsets[:nx1 * ny1, 1] = np.tile(np.arange(ny1), nx1)
5237-
offsets[nx1 * ny1:, 0] = np.repeat(np.arange(nx2) + 0.5, ny2)
5238-
offsets[nx1 * ny1:, 1] = np.tile(np.arange(ny2), nx2) + 0.5
5239-
offsets[:, 0] *= sx
5240-
offsets[:, 1] *= sy
5241-
offsets[:, 0] += xmin
5242-
offsets[:, 1] += ymin
5243-
# remove accumulation bins with no data
5244-
offsets = offsets[good_idxs, :]
5245-
accum = accum[good_idxs]
52465153

52475154
polygon = [sx, sy / 3] * np.array(
52485155
[[.5, -.5], [.5, .5], [0., 1.], [-.5, .5], [-.5, -.5], [0., -1.]])

lib/matplotlib/cbook.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,31 @@ def is_scalar_or_string(val):
503503
return isinstance(val, str) or not np.iterable(val)
504504

505505

506+
def duplicate_if_scalar(obj, n=2, raises=True):
507+
"""Ensure object size or duplicate into a list if necessary."""
508+
509+
if is_scalar_or_string(obj):
510+
return [obj] * n
511+
512+
size = len(obj)
513+
if size == 0:
514+
if raises:
515+
raise ValueError(f'Cannot duplicate empty {type(obj)}.')
516+
return [obj] * n
517+
518+
if size == 1:
519+
return list(obj) * n
520+
521+
if (size != n) and raises:
522+
raise ValueError(
523+
f'Input object of type {type(obj)} has incorrect size. Expected '
524+
f'either a scalar type object, or a Container with length in {{1, '
525+
f'{n}}}.'
526+
)
527+
528+
return obj
529+
530+
506531
@_api.delete_parameter(
507532
"3.8", "np_load", alternative="open(get_sample_data(..., asfileobj=False))")
508533
def get_sample_data(fname, asfileobj=True, *, np_load=True):
@@ -567,6 +592,23 @@ def flatten(seq, scalarp=is_scalar_or_string):
567592
yield from flatten(item, scalarp)
568593

569594

595+
def pairwise(iterable):
596+
"""
597+
Returns an iterator of paired items, overlapping, from the original
598+
599+
take(4, pairwise(count()))
600+
[(0, 1), (1, 2), (2, 3), (3, 4)]
601+
602+
From more_itertools:
603+
https://more-itertools.readthedocs.io/en/stable/_modules/more_itertools/recipes.html#pairwise
604+
605+
Can be removed on python >3.10 in favour of itertools.pairwise
606+
"""
607+
a, b = itertools.tee(iterable)
608+
next(b, None)
609+
return zip(a, b)
610+
611+
570612
@_api.deprecated("3.8")
571613
class Stack:
572614
"""
@@ -1473,6 +1515,120 @@ def _reshape_2D(X, name):
14731515
return result
14741516

14751517

1518+
def hexbin(x, y, C=None, gridsize=100,
1519+
xscale='linear', yscale='linear', extent=None,
1520+
reduce_C_function=np.mean, mincnt=None):
1521+
1522+
# local import to avoid circular import
1523+
import matplotlib.transforms as mtransforms
1524+
1525+
# Set the size of the hexagon grid
1526+
if np.iterable(gridsize):
1527+
nx, ny = gridsize
1528+
else:
1529+
nx = gridsize
1530+
ny = int(nx / math.sqrt(3))
1531+
1532+
# Will be log()'d if necessary, and then rescaled.
1533+
tx = x
1534+
ty = y
1535+
1536+
if xscale == 'log':
1537+
if np.any(x <= 0.0):
1538+
raise ValueError(
1539+
"x contains non-positive values, so cannot be log-scaled")
1540+
tx = np.log10(tx)
1541+
if yscale == 'log':
1542+
if np.any(y <= 0.0):
1543+
raise ValueError(
1544+
"y contains non-positive values, so cannot be log-scaled")
1545+
ty = np.log10(ty)
1546+
if extent is not None:
1547+
xmin, xmax, ymin, ymax = extent
1548+
if xmin > xmax:
1549+
raise ValueError("In extent, xmax must be greater than xmin")
1550+
if ymin > ymax:
1551+
raise ValueError("In extent, ymax must be greater than ymin")
1552+
else:
1553+
xmin, xmax = (tx.min(), tx.max()) if len(x) else (0, 1)
1554+
ymin, ymax = (ty.min(), ty.max()) if len(y) else (0, 1)
1555+
1556+
# to avoid issues with singular data, expand the min/max pairs
1557+
xmin, xmax = mtransforms.nonsingular(xmin, xmax, expander=0.1)
1558+
ymin, ymax = mtransforms.nonsingular(ymin, ymax, expander=0.1)
1559+
1560+
nx1 = nx + 1
1561+
ny1 = ny + 1
1562+
nx2 = nx
1563+
ny2 = ny
1564+
n = nx1 * ny1 + nx2 * ny2
1565+
1566+
# In the x-direction, the hexagons exactly cover the region from
1567+
# xmin to xmax. Need some padding to avoid roundoff errors.
1568+
padding = 1.e-9 * (xmax - xmin)
1569+
xmin -= padding
1570+
xmax += padding
1571+
sx = (xmax - xmin) / nx
1572+
sy = (ymax - ymin) / ny
1573+
# Positions in hexagon index coordinates.
1574+
ix = (tx - xmin) / sx
1575+
iy = (ty - ymin) / sy
1576+
ix1 = np.round(ix).astype(int)
1577+
iy1 = np.round(iy).astype(int)
1578+
ix2 = np.floor(ix).astype(int)
1579+
iy2 = np.floor(iy).astype(int)
1580+
# flat indices, plus one so that out-of-range points go to position 0.
1581+
i1 = np.where((0 <= ix1) & (ix1 < nx1) & (0 <= iy1) & (iy1 < ny1),
1582+
ix1 * ny1 + iy1 + 1, 0)
1583+
i2 = np.where((0 <= ix2) & (ix2 < nx2) & (0 <= iy2) & (iy2 < ny2),
1584+
ix2 * ny2 + iy2 + 1, 0)
1585+
1586+
d1 = (ix - ix1) ** 2 + 3.0 * (iy - iy1) ** 2
1587+
d2 = (ix - ix2 - 0.5) ** 2 + 3.0 * (iy - iy2 - 0.5) ** 2
1588+
bdist = (d1 < d2)
1589+
1590+
if C is None: # [1:] drops out-of-range points.
1591+
counts1 = np.bincount(i1[bdist], minlength=1 + nx1 * ny1)[1:]
1592+
counts2 = np.bincount(i2[~bdist], minlength=1 + nx2 * ny2)[1:]
1593+
accum = np.concatenate([counts1, counts2]).astype(float)
1594+
if mincnt is not None:
1595+
accum[accum < mincnt] = np.nan
1596+
1597+
else:
1598+
# store the C values in a list per hexagon index
1599+
Cs_at_i1 = [[] for _ in range(1 + nx1 * ny1)]
1600+
Cs_at_i2 = [[] for _ in range(1 + nx2 * ny2)]
1601+
for i in range(len(x)):
1602+
if bdist[i]:
1603+
Cs_at_i1[i1[i]].append(C[i])
1604+
else:
1605+
Cs_at_i2[i2[i]].append(C[i])
1606+
if mincnt is None:
1607+
mincnt = 1
1608+
accum = np.array(
1609+
[reduce_C_function(acc) if len(acc) >= mincnt else np.nan
1610+
for Cs_at_i in [Cs_at_i1, Cs_at_i2]
1611+
for acc in Cs_at_i[1:]], # [1:] drops out-of-range points.
1612+
float)
1613+
1614+
good_idxs = ~np.isnan(accum)
1615+
1616+
offsets = np.zeros((n, 2), float)
1617+
offsets[:nx1 * ny1, 0] = np.repeat(np.arange(nx1), ny1)
1618+
offsets[:nx1 * ny1, 1] = np.tile(np.arange(ny1), nx1)
1619+
offsets[nx1 * ny1:, 0] = np.repeat(np.arange(nx2) + 0.5, ny2)
1620+
offsets[nx1 * ny1:, 1] = np.tile(np.arange(ny2), nx2) + 0.5
1621+
offsets[:, 0] *= sx
1622+
offsets[:, 1] *= sy
1623+
offsets[:, 0] += xmin
1624+
offsets[:, 1] += ymin
1625+
# remove accumulation bins with no data
1626+
offsets = offsets[good_idxs, :]
1627+
accum = accum[good_idxs]
1628+
1629+
return (*offsets.T, accum), (xmin, xmax), (ymin, ymax), (nx, ny)
1630+
1631+
14761632
def violin_stats(X, method, points=100, quantiles=None):
14771633
"""
14781634
Return a list of dictionaries of data which can be used to draw a series

lib/matplotlib/cbook.pyi

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def open_file_cm(
7272
encoding: str | None = ...,
7373
) -> contextlib.AbstractContextManager[IO]: ...
7474
def is_scalar_or_string(val: Any) -> bool: ...
75+
def duplicate_if_scalar(obj: Any, n: int = 2, raises: bool = True) -> list: ...
7576
@overload
7677
def get_sample_data(
7778
fname: str | os.PathLike, asfileobj: Literal[True] = ..., *, np_load: Literal[True]
@@ -91,6 +92,7 @@ def _get_data_path(*args: Path | str) -> Path: ...
9192
def flatten(
9293
seq: Iterable[Any], scalarp: Callable[[Any], bool] = ...
9394
) -> Generator[Any, None, None]: ...
95+
def pairwise(iterable: Iterable[Any]) -> Iterable[Any]: ...
9496

9597
class Stack(Generic[_T]):
9698
def __init__(self, default: _T | None = ...) -> None: ...
@@ -144,6 +146,17 @@ ls_mapper_r: dict[str, str]
144146

145147
def contiguous_regions(mask: ArrayLike) -> list[np.ndarray]: ...
146148
def is_math_text(s: str) -> bool: ...
149+
def hexbin(
150+
x: ArrayLike,
151+
y: ArrayLike,
152+
C: ArrayLike | None = None,
153+
gridsize: int | tuple[int, int] = 100,
154+
xscale: str = 'linear',
155+
yscale: str = 'linear',
156+
extent: ArrayLike | None = None,
157+
reduce_C_function: Callable = np.mean,
158+
mincnt: int | None = None
159+
) -> tuple[tuple]: ...
147160
def violin_stats(
148161
X: ArrayLike, method: Callable, points: int = ..., quantiles: ArrayLike | None = ...
149162
) -> list[dict[str, Any]]: ...

0 commit comments

Comments
 (0)