Skip to content

Commit e0da828

Browse files
dabanajonmmease
authored andcommitted
Allow forcing a fixed axis scale ratio to maintain quiver arrow lengths (#1197)
A new scaleratio parameter is added to the create_quiver function to allow forcing a fix scale ratio to the axis of the plot through the scaleratio option in the Layout. A distortion is introduced to the arrows length, angle and shape by having a scale ratio different than 1. In this pull request, this distortion is reversed by multiplying or dividing by scaleratio at key steps of the arrows construction.
1 parent d3d54be commit e0da828

File tree

2 files changed

+96
-16
lines changed

2 files changed

+96
-16
lines changed

Diff for: plotly/figure_factory/_quiver.py

+56-16
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99

1010
def create_quiver(x, y, u, v, scale=.1, arrow_scale=.3,
11-
angle=math.pi / 9, **kwargs):
11+
angle=math.pi / 9, scaleratio = None, **kwargs):
1212
"""
1313
Returns data for a quiver plot.
1414
@@ -21,6 +21,9 @@ def create_quiver(x, y, u, v, scale=.1, arrow_scale=.3,
2121
:param (float in [0,1]) arrow_scale: value multiplied to length of barb
2222
to get length of arrowhead. Default = .3
2323
:param (angle in radians) angle: angle of arrowhead. Default = pi/9
24+
:param (positive float) angle: the ratio between the scale of the y-axis
25+
and the scale of the x-axis (scale_y / scale_x). Default = None, the
26+
scale ratio is not fixed.
2427
:param kwargs: kwargs passed through plotly.graph_objs.Scatter
2528
for more information on valid kwargs call
2629
help(plotly.graph_objs.Scatter)
@@ -80,33 +83,69 @@ def create_quiver(x, y, u, v, scale=.1, arrow_scale=.3,
8083
# Add title to layout
8184
fig['layout'].update(title='Quiver Plot')
8285
86+
# Plot
87+
py.plot(fig, filename='quiver')
88+
```
89+
90+
Example 4: Forcing a fix scale ratio to maintain the arrow length
91+
```
92+
import plotly.plotly as py
93+
from plotly.figure_factory import create_quiver
94+
95+
import numpy as np
96+
97+
# Add data
98+
x,y = np.meshgrid(np.arange(0.5, 3.5, .5), np.arange(0.5, 4.5, .5))
99+
u = x
100+
v = y
101+
angle = np.arctan(v / u)
102+
norm = 0.25
103+
u = norm * np.cos(angle)
104+
v = norm * np.sin(angle)
105+
106+
# Create quiver with a fix scale ratio
107+
fig = create_quiver(x, y, u, v, scale = 1, scaleratio = 0.5)
108+
83109
# Plot
84110
py.plot(fig, filename='quiver')
85111
```
86112
"""
87113
utils.validate_equal_length(x, y, u, v)
88114
utils.validate_positive_scalars(arrow_scale=arrow_scale, scale=scale)
89115

90-
barb_x, barb_y = _Quiver(x, y, u, v, scale,
91-
arrow_scale, angle).get_barbs()
92-
arrow_x, arrow_y = _Quiver(x, y, u, v, scale,
93-
arrow_scale, angle).get_quiver_arrows()
94-
quiver = graph_objs.Scatter(x=barb_x + arrow_x,
116+
if scaleratio is None:
117+
quiver_obj = _Quiver(x, y, u, v, scale, arrow_scale, angle)
118+
else:
119+
quiver_obj = _Quiver(x, y, u, v, scale, arrow_scale, angle, scaleratio)
120+
121+
barb_x, barb_y = quiver_obj.get_barbs()
122+
arrow_x, arrow_y = quiver_obj.get_quiver_arrows()
123+
124+
quiver_plot = graph_objs.Scatter(x=barb_x + arrow_x,
95125
y=barb_y + arrow_y,
96126
mode='lines', **kwargs)
97127

98-
data = [quiver]
99-
layout = graph_objs.Layout(hovermode='closest')
128+
data = [quiver_plot]
100129

101-
return graph_objs.Figure(data=data, layout=layout)
130+
if scaleratio is None:
131+
layout = graph_objs.Layout(hovermode='closest')
132+
else:
133+
layout = graph_objs.Layout(
134+
hovermode='closest',
135+
yaxis=dict(
136+
scaleratio = scaleratio,
137+
scaleanchor = "x"
138+
)
139+
)
102140

141+
return graph_objs.Figure(data=data, layout=layout)
103142

104143
class _Quiver(object):
105144
"""
106145
Refer to FigureFactory.create_quiver() for docstring
107146
"""
108147
def __init__(self, x, y, u, v,
109-
scale, arrow_scale, angle, **kwargs):
148+
scale, arrow_scale, angle, scaleratio = 1, **kwargs):
110149
try:
111150
x = utils.flatten(x)
112151
except exceptions.PlotlyError:
@@ -132,6 +171,7 @@ def __init__(self, x, y, u, v,
132171
self.u = u
133172
self.v = v
134173
self.scale = scale
174+
self.scaleratio = scaleratio
135175
self.arrow_scale = arrow_scale
136176
self.angle = angle
137177
self.end_x = []
@@ -148,7 +188,7 @@ def scale_uv(self):
148188
endpoints of the arrows so a smaller scale value will
149189
result in less overlap of arrows.
150190
"""
151-
self.u = [i * self.scale for i in self.u]
191+
self.u = [i * self.scale * self.scaleratio for i in self.u]
152192
self.v = [i * self.scale for i in self.v]
153193

154194
def get_barbs(self):
@@ -188,13 +228,13 @@ def get_quiver_arrows(self):
188228
point1, endpoint, point2 y_values separated by a None to create
189229
the barb of the arrow.
190230
"""
191-
dif_x = [i - j for i, j in zip(self.end_x, self.x)]
231+
dif_x = [i - j for i, j in zip(self.end_x, self.x)]
192232
dif_y = [i - j for i, j in zip(self.end_y, self.y)]
193233

194234
# Get barb lengths(default arrow length = 30% barb length)
195235
barb_len = [None] * len(self.x)
196236
for index in range(len(barb_len)):
197-
barb_len[index] = math.hypot(dif_x[index], dif_y[index])
237+
barb_len[index] = math.hypot(dif_x[index] / self.scaleratio, dif_y[index])
198238

199239
# Make arrow lengths
200240
arrow_len = [None] * len(self.x)
@@ -203,7 +243,7 @@ def get_quiver_arrows(self):
203243
# Get barb angles
204244
barb_ang = [None] * len(self.x)
205245
for index in range(len(barb_ang)):
206-
barb_ang[index] = math.atan2(dif_y[index], dif_x[index])
246+
barb_ang[index] = math.atan2(dif_y[index], dif_x[index] / self.scaleratio)
207247

208248
# Set angles to create arrow
209249
ang1 = [i + self.angle for i in barb_ang]
@@ -231,9 +271,9 @@ def get_quiver_arrows(self):
231271

232272
# Set coordinates to create arrow
233273
for index in range(len(self.end_x)):
234-
point1_x = [i - j for i, j in zip(self.end_x, seg1_x)]
274+
point1_x = [i - j * self.scaleratio for i, j in zip(self.end_x, seg1_x)]
235275
point1_y = [i - j for i, j in zip(self.end_y, seg1_y)]
236-
point2_x = [i - j for i, j in zip(self.end_x, seg2_x)]
276+
point2_x = [i - j * self.scaleratio for i, j in zip(self.end_x, seg2_x)]
237277
point2_y = [i - j for i, j in zip(self.end_y, seg2_y)]
238278

239279
# Combine lists to create arrow

Diff for: plotly/tests/test_optional/test_figure_factory/test_figure_factory.py

+40
Original file line numberDiff line numberDiff line change
@@ -2857,3 +2857,43 @@ def test_full_choropleth(self):
28572857
]
28582858

28592859
self.assertEqual(fig['data'][2]['x'][:50], exp_fig_head)
2860+
2861+
class TestQuiver(TestCase):
2862+
2863+
def test_scaleratio_param(self):
2864+
x,y = np.meshgrid(np.arange(0.5, 3.5, .5), np.arange(0.5, 4.5, .5))
2865+
u = x
2866+
v = y
2867+
angle = np.arctan(v / u)
2868+
norm = 0.25
2869+
u = norm * np.cos(angle)
2870+
v = norm * np.sin(angle)
2871+
fig = ff.create_quiver(x, y, u, v, scale = 1, scaleratio = 0.5)
2872+
2873+
exp_fig_head = [(
2874+
0.5,
2875+
0.5883883476483185,
2876+
None,
2877+
1.0,
2878+
1.1118033988749896,
2879+
None,
2880+
1.5,
2881+
1.6185854122563141,
2882+
None,
2883+
2.0),
2884+
(0.5,
2885+
0.6767766952966369,
2886+
None,
2887+
0.5,
2888+
0.6118033988749895,
2889+
None,
2890+
0.5,
2891+
0.5790569415042095,
2892+
None,
2893+
0.5)]
2894+
2895+
fig_head = [fig['data'][0]['x'][:10], fig['data'][0]['y'][:10]]
2896+
2897+
self.assertEqual(fig_head, exp_fig_head)
2898+
2899+

0 commit comments

Comments
 (0)