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

Skip to content

Commit 887971a

Browse files
committed
DEV: add HexBar3DCollection for plotting hexagonal prisms
1 parent b10d2b0 commit 887971a

17 files changed

+627
-280
lines changed

lib/matplotlib/axes/_axes.py

Lines changed: 6 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
_AxesBase, _TransformedBoundsLocator, _process_plot_format)
3737
from matplotlib.axes._secondary_axes import SecondaryAxis
3838
from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer
39+
from matplotlib.hexbin import hexbin
3940

4041
_log = logging.getLogger(__name__)
4142

@@ -5137,114 +5138,11 @@ def reduce_C_function(C: array) -> float
51375138

51385139
x, y, C = cbook.delete_masked_points(x, y, C)
51395140

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))
5146-
# Count the number of data in each hexagon
5147-
x = np.asarray(x, float)
5148-
y = np.asarray(y, float)
5149-
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
5189-
sx = (xmax - xmin) / nx
5190-
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
5214-
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]
5246-
5247-
polygon = [sx, sy / 3] * np.array(
5141+
offsets, accum, (sx, sy) = hexbin(x, y, C, gridsize,
5142+
xscale, yscale, extent,
5143+
reduce_C_function, mincnt)
5144+
5145+
polygon = [sx, sy] * np.array(
52485146
[[.5, -.5], [.5, .5], [0., 1.], [-.5, .5], [-.5, -.5], [0., -1.]])
52495147

52505148
if linewidths is None:

lib/matplotlib/cbook.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,41 @@ 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+
"""
508+
Ensure object size or duplicate if necessary.
509+
510+
Parameters
511+
----------
512+
obj : scalar, str or Sized
513+
514+
Returns
515+
-------
516+
517+
"""
518+
519+
if is_scalar_or_string(obj):
520+
return [obj] * n
521+
522+
size = len(obj)
523+
if size == 0:
524+
if raises:
525+
raise ValueError(f'Cannot duplicate empty {type(obj)}.')
526+
return [obj] * n
527+
528+
if size == 1:
529+
return list(obj) * n
530+
531+
if (size != n) and raises:
532+
raise ValueError(
533+
f'Input object of type {type(obj)} has incorrect size. Expected '
534+
f'either a scalar type object, or a Container with length in {{1, '
535+
f'{n}}}.'
536+
)
537+
538+
return obj
539+
540+
506541
@_api.delete_parameter(
507542
"3.8", "np_load", alternative="open(get_sample_data(..., asfileobj=False))")
508543
def get_sample_data(fname, asfileobj=True, *, np_load=True):
@@ -567,6 +602,23 @@ def flatten(seq, scalarp=is_scalar_or_string):
567602
yield from flatten(item, scalarp)
568603

569604

605+
def pairwise(iterable):
606+
"""
607+
Returns an iterator of paired items, overlapping, from the original
608+
609+
take(4, pairwise(count()))
610+
[(0, 1), (1, 2), (2, 3), (3, 4)]
611+
612+
From more_itertools:
613+
https://more-itertools.readthedocs.io/en/stable/_modules/more_itertools/recipes.html#pairwise
614+
615+
Can be removed on python >3.10 in favour of itertools.pairwise
616+
"""
617+
a, b = itertools.tee(iterable)
618+
next(b, None)
619+
return zip(a, b)
620+
621+
570622
@_api.deprecated("3.8")
571623
class Stack:
572624
"""

lib/matplotlib/hexbin.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""
2+
Function to support histogramming over hexagonal tesselations.
3+
"""
4+
5+
import math
6+
7+
import numpy as np
8+
import matplotlib.transforms as mtransforms
9+
10+
11+
def hexbin(x, y, C=None, gridsize=100,
12+
xscale='linear', yscale='linear', extent=None,
13+
reduce_C_function=np.mean, mincnt=None):
14+
15+
# Set the size of the hexagon grid
16+
if np.iterable(gridsize):
17+
nx, ny = gridsize
18+
else:
19+
nx = gridsize
20+
ny = int(nx / math.sqrt(3))
21+
22+
# Count the number of data in each hexagon
23+
x = np.asarray(x, float)
24+
y = np.asarray(y, float)
25+
26+
# Will be log()'d if necessary, and then rescaled.
27+
tx = x
28+
ty = y
29+
30+
if xscale == 'log':
31+
if np.any(x <= 0.0):
32+
raise ValueError(
33+
"x contains non-positive values, so cannot be log-scaled")
34+
tx = np.log10(tx)
35+
if yscale == 'log':
36+
if np.any(y <= 0.0):
37+
raise ValueError(
38+
"y contains non-positive values, so cannot be log-scaled")
39+
ty = np.log10(ty)
40+
if extent is not None:
41+
xmin, xmax, ymin, ymax = extent
42+
else:
43+
xmin, xmax = (tx.min(), tx.max()) if len(x) else (0, 1)
44+
ymin, ymax = (ty.min(), ty.max()) if len(y) else (0, 1)
45+
46+
# to avoid issues with singular data, expand the min/max pairs
47+
xmin, xmax = mtransforms.nonsingular(xmin, xmax, expander=0.1)
48+
ymin, ymax = mtransforms.nonsingular(ymin, ymax, expander=0.1)
49+
50+
nx1 = nx + 1
51+
ny1 = ny + 1
52+
nx2 = nx
53+
ny2 = ny
54+
n = nx1 * ny1 + nx2 * ny2
55+
56+
# In the x-direction, the hexagons exactly cover the region from
57+
# xmin to xmax. Need some padding to avoid roundoff errors.
58+
padding = 1.e-9 * (xmax - xmin)
59+
xmin -= padding
60+
xmax += padding
61+
sx = (xmax - xmin) / nx
62+
sy = (ymax - ymin) / ny
63+
# Positions in hexagon index coordinates.
64+
ix = (tx - xmin) / sx
65+
iy = (ty - ymin) / sy
66+
ix1 = np.round(ix).astype(int)
67+
iy1 = np.round(iy).astype(int)
68+
ix2 = np.floor(ix).astype(int)
69+
iy2 = np.floor(iy).astype(int)
70+
# flat indices, plus one so that out-of-range points go to position 0.
71+
i1 = np.where((0 <= ix1) & (ix1 < nx1) & (0 <= iy1) & (iy1 < ny1),
72+
ix1 * ny1 + iy1 + 1, 0)
73+
i2 = np.where((0 <= ix2) & (ix2 < nx2) & (0 <= iy2) & (iy2 < ny2),
74+
ix2 * ny2 + iy2 + 1, 0)
75+
76+
d1 = (ix - ix1) ** 2 + 3.0 * (iy - iy1) ** 2
77+
d2 = (ix - ix2 - 0.5) ** 2 + 3.0 * (iy - iy2 - 0.5) ** 2
78+
bdist = (d1 < d2)
79+
80+
if C is None: # [1:] drops out-of-range points.
81+
counts1 = np.bincount(i1[bdist], minlength=1 + nx1 * ny1)[1:]
82+
counts2 = np.bincount(i2[~bdist], minlength=1 + nx2 * ny2)[1:]
83+
accum = np.concatenate([counts1, counts2]).astype(float)
84+
if mincnt is not None:
85+
accum[accum < mincnt] = np.nan
86+
C = np.ones(len(x))
87+
else:
88+
# store the C values in a list per hexagon index
89+
Cs_at_i1 = [[] for _ in range(1 + nx1 * ny1)]
90+
Cs_at_i2 = [[] for _ in range(1 + nx2 * ny2)]
91+
for i in range(len(x)):
92+
if bdist[i]:
93+
Cs_at_i1[i1[i]].append(C[i])
94+
else:
95+
Cs_at_i2[i2[i]].append(C[i])
96+
if mincnt is None:
97+
mincnt = 0
98+
accum = np.array(
99+
[reduce_C_function(acc) if len(acc) > mincnt else np.nan
100+
for Cs_at_i in [Cs_at_i1, Cs_at_i2]
101+
for acc in Cs_at_i[1:]], # [1:] drops out-of-range points.
102+
float)
103+
104+
good_idxs = ~np.isnan(accum)
105+
106+
offsets = np.zeros((n, 2), float)
107+
offsets[:nx1 * ny1, 0] = np.repeat(np.arange(nx1), ny1)
108+
offsets[:nx1 * ny1, 1] = np.tile(np.arange(ny1), nx1)
109+
offsets[nx1 * ny1:, 0] = np.repeat(np.arange(nx2) + 0.5, ny2)
110+
offsets[nx1 * ny1:, 1] = np.tile(np.arange(ny2), nx2) + 0.5
111+
offsets[:, 0] *= sx
112+
offsets[:, 1] *= sy
113+
offsets[:, 0] += xmin
114+
offsets[:, 1] += ymin
115+
# remove accumulation bins with no data
116+
offsets = offsets[good_idxs, :]
117+
accum = accum[good_idxs]
118+
119+
return (*offsets.T, accum), (sx, sy / 3)

0 commit comments

Comments
 (0)