Skip to content

Commit 9c0b043

Browse files
committed
Hexbin_mapbox improvements: centered on the lat-axis.
Hexbin_mapbox tests: 1) Aggregation results 2) Check build_dataframe behaviour
1 parent e491d05 commit 9c0b043

File tree

2 files changed

+229
-15
lines changed

2 files changed

+229
-15
lines changed

packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,25 @@ def _compute_hexbin(x, y, x_range, y_range, color, nx, agg_func, min_count):
9797
ymin = y_range.min()
9898
ymax = y_range.max()
9999

100+
# In the x-direction, the hexagons exactly cover the region from
101+
# xmin to xmax. Need some padding to avoid roundoff errors.
102+
padding = 1.0e-9 * (xmax - xmin)
103+
xmin -= padding
104+
xmax += padding
105+
100106
Dx = xmax - xmin
101107
Dy = ymax - ymin
102-
dx = Dx / nx
108+
if Dx == 0 and Dy > 0:
109+
dx = Dy / nx
110+
elif Dx == 0 and Dy == 0:
111+
dx, _ = _project_latlon_to_wgs84(1, 1)
112+
else:
113+
dx = Dx / nx
103114
dy = dx * np.sqrt(3)
104-
ny = np.round(Dy / dy).astype(int)
115+
ny = np.ceil(Dy / dy).astype(int)
116+
117+
# Center the hexagons vertically since we only want regular hexagons
118+
ymin -= (ymin + dy * ny - ymax) / 2
105119

106120
x = (x - xmin) / dx
107121
y = (y - ymin) / dy
@@ -134,7 +148,7 @@ def _compute_hexbin(x, y, x_range, y_range, color, nx, agg_func, min_count):
134148
good_idxs = ~np.isnan(accum)
135149
else:
136150
if min_count is None:
137-
min_count = 0
151+
min_count = 1
138152

139153
# create accumulation arrays
140154
lattice1 = np.empty((nx1, ny1), dtype=object)
@@ -157,14 +171,14 @@ def _compute_hexbin(x, y, x_range, y_range, color, nx, agg_func, min_count):
157171
for i in range(nx1):
158172
for j in range(ny1):
159173
vals = lattice1[i, j]
160-
if len(vals) > min_count:
174+
if len(vals) >= min_count:
161175
lattice1[i, j] = agg_func(vals)
162176
else:
163177
lattice1[i, j] = np.nan
164178
for i in range(nx2):
165179
for j in range(ny2):
166180
vals = lattice2[i, j]
167-
if len(vals) > min_count:
181+
if len(vals) >= min_count:
168182
lattice2[i, j] = agg_func(vals)
169183
else:
170184
lattice2[i, j] = np.nan
@@ -201,15 +215,9 @@ def _compute_hexbin(x, y, x_range, y_range, color, nx, agg_func, min_count):
201215
# Number of hexagons needed
202216
m = len(centers)
203217

204-
# Scale of hexagons
205-
dxh = sorted(list(set(np.diff(sorted(centers[:, 0])))))[1]
206-
dyh = sorted(list(set(np.diff(sorted(centers[:, 1])))))[1]
207-
nx = dxh * 2
208-
ny = 2 / 3 * dyh / (0.5 / np.cos(np.pi / 6))
209-
210218
# Coordinates for all hexagonal patches
211-
hxs = np.array([hx] * m) * nx + np.vstack(centers[:, 0])
212-
hys = np.array([hy] * m) * ny + np.vstack(centers[:, 1])
219+
hxs = np.array([hx] * m) * dx + np.vstack(centers[:, 0])
220+
hys = np.array([hy] * m) * dy / np.sqrt(3) + np.vstack(centers[:, 1])
213221

214222
return hxs, hys, centers, agreggated_value
215223

@@ -328,6 +336,7 @@ def create_hexbin_mapbox(
328336
template=None,
329337
width=None,
330338
height=None,
339+
min_count=None,
331340
):
332341
"""
333342
Returns a figure aggregating scattered points into connected hexagons
@@ -348,7 +357,7 @@ def create_hexbin_mapbox(
348357
color=None,
349358
nx=nx_hexagon,
350359
agg_func=agg_func,
351-
min_count=-np.inf,
360+
min_count=min_count,
352361
)
353362

354363
geojson = _hexagons_to_geojson(hexagons_lats, hexagons_lons, hexagons_ids)
@@ -385,7 +394,7 @@ def create_hexbin_mapbox(
385394
color=df[args["color"]].values if args["color"] else None,
386395
nx=nx_hexagon,
387396
agg_func=agg_func,
388-
min_count=None,
397+
min_count=min_count,
389398
)
390399
agg_data_frame_list.append(
391400
pd.DataFrame(
@@ -436,5 +445,11 @@ def create_hexbin_mapbox(
436445
"Numpy array aggregator, it must take as input a 1D array",
437446
"and output a scalar value.",
438447
],
448+
min_count=[
449+
"int",
450+
"Minimum number of points in a hexagon for it to be displayed.",
451+
"If None and color is not set, display all hexagons.",
452+
"If None and color is set, only display hexagons that contain points.",
453+
],
439454
),
440455
)

packages/python/plotly/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4307,3 +4307,202 @@ def test_optional_arguments(self):
43074307
# This test does not work for ilr interpolation
43084308
print(len(fig.data))
43094309
assert len(fig.data) == ncontours + 2 + arg_set["showscale"]
4310+
4311+
4312+
class TestHexbinMapbox(NumpyTestUtilsMixin, TestCaseNoTemplate):
4313+
def test_aggregation(self):
4314+
4315+
lat = [0, 1, 1, 2, 4, 5, 1, 2, 4, 5, 2, 3, 2, 1, 5, 3, 5]
4316+
lon = [1, 2, 3, 3, 0, 4, 5, 0, 5, 3, 1, 5, 4, 0, 1, 2, 5]
4317+
color = np.ones(len(lat))
4318+
4319+
fig1 = ff.create_hexbin_mapbox(lat=lat, lon=lon, nx_hexagon=1)
4320+
4321+
actual_geojson = {
4322+
"type": "FeatureCollection",
4323+
"features": [
4324+
{
4325+
"type": "Feature",
4326+
"id": "-8.726646259971648e-11,-0.031886255679892235",
4327+
"geometry": {
4328+
"type": "Polygon",
4329+
"coordinates": [
4330+
[
4331+
[-5e-09, -4.7083909316316985],
4332+
[2.4999999999999996, -3.268549270944215],
4333+
[2.4999999999999996, -0.38356933397072673],
4334+
[-5e-09, 1.0597430482129082],
4335+
[-2.50000001, -0.38356933397072673],
4336+
[-2.50000001, -3.268549270944215],
4337+
[-5e-09, -4.7083909316316985],
4338+
]
4339+
],
4340+
},
4341+
},
4342+
{
4343+
"type": "Feature",
4344+
"id": "-8.726646259971648e-11,0.1192636916419258",
4345+
"geometry": {
4346+
"type": "Polygon",
4347+
"coordinates": [
4348+
[
4349+
[-5e-09, 3.9434377827164666],
4350+
[2.4999999999999996, 5.381998306154031],
4351+
[2.4999999999999996, 8.248045720432454],
4352+
[-5e-09, 9.673766164509932],
4353+
[-2.50000001, 8.248045720432454],
4354+
[-2.50000001, 5.381998306154031],
4355+
[-5e-09, 3.9434377827164666],
4356+
]
4357+
],
4358+
},
4359+
},
4360+
{
4361+
"type": "Feature",
4362+
"id": "0.08726646268698293,-0.031886255679892235",
4363+
"geometry": {
4364+
"type": "Polygon",
4365+
"coordinates": [
4366+
[
4367+
[5.0000000049999995, -4.7083909316316985],
4368+
[7.500000009999999, -3.268549270944215],
4369+
[7.500000009999999, -0.38356933397072673],
4370+
[5.0000000049999995, 1.0597430482129082],
4371+
[2.5, -0.38356933397072673],
4372+
[2.5, -3.268549270944215],
4373+
[5.0000000049999995, -4.7083909316316985],
4374+
]
4375+
],
4376+
},
4377+
},
4378+
{
4379+
"type": "Feature",
4380+
"id": "0.08726646268698293,0.1192636916419258",
4381+
"geometry": {
4382+
"type": "Polygon",
4383+
"coordinates": [
4384+
[
4385+
[5.0000000049999995, 3.9434377827164666],
4386+
[7.500000009999999, 5.381998306154031],
4387+
[7.500000009999999, 8.248045720432454],
4388+
[5.0000000049999995, 9.673766164509932],
4389+
[2.5, 8.248045720432454],
4390+
[2.5, 5.381998306154031],
4391+
[5.0000000049999995, 3.9434377827164666],
4392+
]
4393+
],
4394+
},
4395+
},
4396+
{
4397+
"type": "Feature",
4398+
"id": "0.04363323129985823,0.04368871798101678",
4399+
"geometry": {
4400+
"type": "Polygon",
4401+
"coordinates": [
4402+
[
4403+
[2.4999999999999996, -0.38356933397072673],
4404+
[5.0000000049999995, 1.0597430482129082],
4405+
[5.0000000049999995, 3.9434377827164666],
4406+
[2.4999999999999996, 5.381998306154031],
4407+
[-5.0000001310894304e-09, 3.9434377827164666],
4408+
[-5.0000001310894304e-09, 1.0597430482129082],
4409+
[2.4999999999999996, -0.38356933397072673],
4410+
]
4411+
],
4412+
},
4413+
},
4414+
],
4415+
}
4416+
4417+
actual_agg = [2.0, 2.0, 1.0, 3.0, 9.0]
4418+
4419+
self.assert_dict_equal(fig1.data[0].geojson, actual_geojson)
4420+
assert np.array_equal(fig1.data[0].z, actual_agg)
4421+
4422+
fig2 = ff.create_hexbin_mapbox(
4423+
lat=lat, lon=lon, nx_hexagon=1, color=color, agg_func=np.mean,
4424+
)
4425+
4426+
assert np.array_equal(fig2.data[0].z, np.ones(5))
4427+
4428+
fig3 = ff.create_hexbin_mapbox(
4429+
lat=np.random.randn(1000), lon=np.random.randn(1000), nx_hexagon=20,
4430+
)
4431+
4432+
assert fig3.data[0].z.sum() == 1000
4433+
4434+
def test_build_dataframe(self):
4435+
np.random.seed(0)
4436+
N = 10000
4437+
nx_hexagon = 20
4438+
n_frames = 3
4439+
4440+
lat = np.random.randn(N)
4441+
lon = np.random.randn(N)
4442+
color = np.ones(N)
4443+
frame = np.random.randint(0, n_frames, N)
4444+
df = pd.DataFrame(
4445+
np.c_[lat, lon, color, frame],
4446+
columns=["Latitude", "Longitude", "Metric", "Frame"],
4447+
)
4448+
4449+
fig1 = ff.create_hexbin_mapbox(lat=lat, lon=lon, nx_hexagon=nx_hexagon)
4450+
fig2 = ff.create_hexbin_mapbox(
4451+
data_frame=df, lat="Latitude", lon="Longitude", nx_hexagon=nx_hexagon
4452+
)
4453+
4454+
assert isinstance(fig1, go.Figure)
4455+
assert len(fig1.data) == 1
4456+
self.assert_dict_equal(
4457+
fig1.to_plotly_json()["data"][0], fig2.to_plotly_json()["data"][0]
4458+
)
4459+
4460+
fig3 = ff.create_hexbin_mapbox(
4461+
lat=lat,
4462+
lon=lon,
4463+
nx_hexagon=nx_hexagon,
4464+
color=color,
4465+
agg_func=np.sum,
4466+
min_count=0,
4467+
)
4468+
fig4 = ff.create_hexbin_mapbox(
4469+
lat=lat, lon=lon, nx_hexagon=nx_hexagon, color=color, agg_func=np.sum,
4470+
)
4471+
fig5 = ff.create_hexbin_mapbox(
4472+
data_frame=df,
4473+
lat="Latitude",
4474+
lon="Longitude",
4475+
nx_hexagon=nx_hexagon,
4476+
color="Metric",
4477+
agg_func=np.sum,
4478+
)
4479+
4480+
self.assert_dict_equal(
4481+
fig1.to_plotly_json()["data"][0], fig3.to_plotly_json()["data"][0]
4482+
)
4483+
self.assert_dict_equal(
4484+
fig4.to_plotly_json()["data"][0], fig5.to_plotly_json()["data"][0]
4485+
)
4486+
4487+
fig6 = ff.create_hexbin_mapbox(
4488+
data_frame=df,
4489+
lat="Latitude",
4490+
lon="Longitude",
4491+
nx_hexagon=nx_hexagon,
4492+
color="Metric",
4493+
agg_func=np.sum,
4494+
animation_frame="Frame",
4495+
)
4496+
4497+
fig7 = ff.create_hexbin_mapbox(
4498+
lat=lat,
4499+
lon=lon,
4500+
nx_hexagon=nx_hexagon,
4501+
color=color,
4502+
agg_func=np.sum,
4503+
animation_frame=frame,
4504+
)
4505+
4506+
assert len(fig6.frames) == n_frames
4507+
assert len(fig7.frames) == n_frames
4508+
assert fig6.data[0].geojson == fig1.data[0].geojson

0 commit comments

Comments
 (0)