Customizing a Networkx graph (or Scatter) with Python Plotly - python

I'm using Plotly's Python interface to generate a network. I've managed to create a network with my desired nodes and edges, and to control the size of the nodes.
I am desperately looking for help on how to do the following:
add node labels
add edge labels according to a list of weights
control the edge line width according to a list of weights
All this without using the "hovering" option, as it has to go in a non-interactive paper. I'd greatly appreciate any help! Plotly's output |
In case this fails, the figure itself |
matrix.csv
This is my code (most is copy-pasted from the Plotly tutorial for Networkx):
import pandas as pd
import plotly.plotly as py
from plotly.graph_objs import *
import networkx as nx
matrix = pd.read_csv("matrix.csv", sep = "\t", index_col = 0, header = 0)
G = nx.DiGraph()
# add nodes:
G.add_nodes_from(matrix.columns)
# add edges:
edge_lst = [(i,j, matrix.loc[i,j])
for i in matrix.index
for j in matrix.columns
if matrix.loc[i,j] != 0]
G.add_weighted_edges_from(edge_lst)
# create node trace:
node_trace = Scatter(x = [], y = [], text = [], mode = 'markers',
marker = Marker(
showscale = True,
colorscale = 'YIGnBu',
reversescale = True,
color = [],
size = [],
colorbar = dict(
thickness = 15,
title = 'Node Connections',
xanchor = 'left',
titleside = 'right'),
line = dict(width = 2)))
# set node positions
pos = nx.spring_layout(G)
for node in G.nodes():
G.node[node]['pos']= pos[node]
for node in G.nodes():
x, y = G.node[node]['pos']
node_trace['x'].append(x)
node_trace['y'].append(y)
# create edge trace:
edge_trace = Scatter(x = [], y = [], text = [],
line = Line(width = [], color = '#888'),
mode = 'lines')
for edge in G.edges():
x0, y0 = G.node[edge[0]]['pos']
x1, y1 = G.node[edge[1]]['pos']
edge_trace['x'] += [x0, x1, None]
edge_trace['y'] += [y0, y1, None]
edge_trace['text'] += str(matrix.loc[edge[0], edge[1]])[:5]
# size nodes by degree
deg_dict = {deg[0]:int(deg[1]) for deg in list(G.degree())}
for node, degree in enumerate(deg_dict):
node_trace['marker']['size'].append(deg_dict[degree] + 20)
fig = Figure(data = Data([edge_trace, node_trace]),
layout = Layout(
title = '<br>AA Substitution Rates',
titlefont = dict(size = 16),
showlegend = True,
margin = dict(b = 20, l = 5, r = 5, t = 40),
annotations = [dict(
text = "sub title text",
showarrow = False,
xref = "paper", yref = "paper",
x = 0.005, y = -0.002)],
xaxis = XAxis(showgrid = False,
zeroline = False,
showticklabels = False),
yaxis = YAxis(showgrid = False,
zeroline = False,
showticklabels = False)))
py.plot(fig, filename = 'networkx')

So
1. The solution to this is relative easy, you create a list with the node ids and you set it in the text attribute of the scatter plot. Then you set the mode as "markers+text" and you're done.
2. This is a little bit more tricky. You have to calculate the middle of each line and create a list of dicts including the line's middle position and weight. Then you add set as the layout's annotation.
3. This is too compicated to be done using plotly IMO. As for now I am calculating the position of each node using networkx spring_layout function. If you'd want to set the width of each line based on its weight you would have to modify the position using a function that takes into account all the markers that each line is attached to.
Bonus I give you the option to color each of the graph's components differently.
Here's a (slightly modified) function I made a while ago that does 1 and 2:
import pandas as pd
import plotly.plotly as py
import plotly.graph_objs as go
from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
import networkx as nx
def scatter_plot_2d(G, folderPath, name, savePng = False):
print("Creating scatter plot (2D)...")
Nodes = [comp for comp in nx.connected_components(G)] # Looks for the graph's communities
Edges = G.edges()
edge_weights = nx.get_edge_attributes(G,'weight')
labels = [] # names of the nodes to plot
group = [] # id of the communities
group_cnt = 0
print("Communities | Number of Nodes")
for subgroup in Nodes:
group_cnt += 1
print(" %d | %d" % (group_cnt, len(subgroup)))
for node in subgroup:
labels.append(int(node))
group.append(group_cnt)
labels, group = (list(t) for t in zip(*sorted(zip(labels, group))))
layt = nx.spring_layout(G, dim=2) # Generates the layout of the graph
Xn = [layt[k][0] for k in list(layt.keys())] # x-coordinates of nodes
Yn = [layt[k][1] for k in list(layt.keys())] # y-coordinates
Xe = []
Ye = []
plot_weights = []
for e in Edges:
Xe += [layt[e[0]][0], layt[e[1]][0], None]
Ye += [layt[e[0]][1], layt[e[1]][1], None]
ax = (layt[e[0]][0]+layt[e[1]][0])/2
ay = (layt[e[0]][1]+layt[e[1]][1])/2
plot_weights.append((edge_weights[(e[0], e[1])], ax, ay))
annotations_list =[
dict(
x=plot_weight[1],
y=plot_weight[2],
xref='x',
yref='y',
text=plot_weight[0],
showarrow=True,
arrowhead=7,
ax=plot_weight[1],
ay=plot_weight[2]
)
for plot_weight in plot_weights
]
trace1 = go.Scatter( x=Xe,
y=Ye,
mode='lines',
line=dict(color='rgb(90, 90, 90)', width=1),
hoverinfo='none'
)
trace2 = go.Scatter( x=Xn,
y=Yn,
mode='markers+text',
name='Nodes',
marker=dict(symbol='circle',
size=8,
color=group,
colorscale='Viridis',
line=dict(color='rgb(255,255,255)', width=1)
),
text=labels,
textposition='top center',
hoverinfo='none'
)
xaxis = dict(
backgroundcolor="rgb(200, 200, 230)",
gridcolor="rgb(255, 255, 255)",
showbackground=True,
zerolinecolor="rgb(255, 255, 255)"
)
yaxis = dict(
backgroundcolor="rgb(230, 200,230)",
gridcolor="rgb(255, 255, 255)",
showbackground=True,
zerolinecolor="rgb(255, 255, 255)"
)
layout = go.Layout(
title=name,
width=700,
height=700,
showlegend=False,
plot_bgcolor="rgb(230, 230, 200)",
scene=dict(
xaxis=dict(xaxis),
yaxis=dict(yaxis)
),
margin=dict(
t=100
),
hovermode='closest',
annotations=annotations_list
, )
data = [trace1, trace2]
fig = go.Figure(data=data, layout=layout)
plotDir = folderPath + "/"
print("Plotting..")
if savePng:
plot(fig, filename=plotDir + name + ".html", auto_open=True, image = 'png', image_filename=plotDir + name,
output_type='file', image_width=700, image_height=700, validate=False)
else:
plot(fig, filename=plotDir + name + ".html")

The d3graph library provides the functionalities you want.
pip install d3graph
I downloaded your data and imported it for demonstration:
# Import data
df = pd.read_csv('data.csv', index_col=0)
# Import library
from d3graph import d3graph
# Convert your Pvalues. Note that any edge is set when a value in the matrix is >0. The edge width is however based on this value. A conversion is therefore useful when you work with Pvalues.
df[df.values==0]=1
df = -np.log10(df)
# Increase some distance between edges. Maybe something like this.
df = (np.exp(df)-1)/10
# Make the graph with default settings
d3 = d3graph()
# Make the graph by setting some parameters
d3.graph(df)
# Set edge properties
d3.set_edge_properties(directed=True)
# Set node properties
d3.set_node_properties(color=df.columns.values, size=size, edge_size=10, edge_color='#000000', cmap='Set2')
This will result in an interactive network graph. Two screenshots: one with the default settings and the one with tweaked settings. More examples can be found here.

Related

Is it possibile to optimize Plotly carpet rendering?

I am working on a plotter for Finite Element Method solutions. I decided to use the Plotly library because of the carpet plots. I have my data to plot and this is my result:
Flow over NACA0012
Each element is represented as a Carpet, and for each carpet the solution is shown as a Countourcarpet. Everything is in place, but the rendering is too slow and the interactive interface is therefore nearly useless. Is there a way to enhance the performance of the rendering? I have read about different renderers in Plotly, but the plot just does not open. Is there a a way to speed up the rendering? Surely I will have to manage larger dataset. In this example I am using 740 carpets.
These are the Contourcarpet settings:
fig.add_trace(go.Contourcarpet(
a = a,
b = b,
z = u, # Sution correspondent at (a,b) parametric location
showlegend = showLegendFlag,
name = "Density",
legendgroup = "Density",
autocolorscale = False,
colorscale = "Inferno",
autocontour = False,
carpet = str(e), # The carpet on which to plot the solution is
# referenced as a string number
contours = dict(
start = start1, # Min value
end = end1, # Max value
size = abs(end1-start1) / countour_number, # Plot colour discretization
showlines = False
),
line = dict(
smoothing = 0
),
colorbar = dict(
len = 0.4,
y = 0.25
)
))
And these are the layout settings:
fig.update_layout(
plot_bgcolor="#FFF",
yaxis = dict(
zeroline = False,
range = [-1.800,1.800],
showgrid = False
),
dragmode = "pan",
height = 700,
xaxis = dict(
zeroline = False,
scaleratio = 1,
scaleanchor = 'y',
range = [-3.800,3.800],
showgrid = False
),
title = "Flow over NACA 0012",
hovermode = "closest",
margin = dict(
r = 80,
b = 40,
l = 40,
t = 80
),
width = 900
)
fig.show()

Plotly slider function only updates one argument

I am trying to build a heatmap with annotations and a title. This title and the annotations should update when the slider is moved. I get this to work, but only for one of the two arguments at the same time. The argument that is at index [1] is being updated, but the other one isn't
Below is a snippet of my code and the error happens in the step for loop:
from plotly.offline import init_notebook_mode, iplot
import plotly.graph_objs as go
import numpy as np
# initialize notebook for offline plotting
init_notebook_mode()
# Set initial slider/title index
start_index = 0
# Build all traces with visible=False
timestep = 5
#df2 = np.random.rand(18,365)*70
data = [go.Heatmap(
visible = False,
x = ['P', 'C', 'S'],
y = [11,10,9,8,7,6],
z = df.iloc[:18,[step]].to_numpy().reshape(6,3),
# z = df2[:,step].reshape(6,3),
zmin = 0,
zmax = 70)
for step in np.arange(0, len(df2.transpose())-1, timestep)
]
# Make initial trace visible
data[start_index]['visible'] = True
# Build slider steps
steps = []
for i in range(len(data)):
step = dict(
# Update method allows us to update both trace and layout properties
method = 'update',
args = [
# Make the ith trace visible
{'visible': [t == i for t in range(len(data))]},
{'annotations' : [dict(
x = x,
y = y,
text = str(round(df.iloc[:18,[i]].to_numpy().reshape(6,3)[-y+11,x],1)),
# text = str(df2[:,i].reshape(6,3)[-y+11,x]),
showarrow = False)
for x in range(3) for y in range(6,12)]},
{'title.text': str(df.columns[i*timestep])},]
)
steps.append(step)
# Build sliders
sliders = [go.layout.Slider(
active = start_index,
currentvalue = {"prefix": "Timestep: "},
pad = {"t": 72},
steps = steps
)]
layout = go.Layout(
sliders=sliders,
title={'text': str(df.columns[start_index])},
yaxis = dict(
tickmode = 'array',
tickvals = [11,10,9,8,7,6],
ticktext = ['06','07','08','09','10','11']
),
annotations = steps[start_index]['args'][1]['annotations']
)
fig = go.Figure(
data=data,
layout=layout)
iplot(fig)
I found the problem. Apparently you need to specify 'annotations' and 'title.text in the same dictionary, instead of seperate ones. The code should thus be changed to:
{'annotations' : [dict(
x = x,
y = y,
text = str(round(df.iloc[:18,[i]].to_numpy().reshape(6,3)[-y+11,x],1)),
# text = str(df2[:,i].reshape(6,3)[-y+11,x]),
showarrow = False)
for x in range(3) for y in range(6,12)],
'title.text': str(df.columns[i*timestep])}

Python: how to update data selection in bokeh?

I new in using bokeh.
This is what I am doing. From osmnx I get data of schools and hospitals in Haiti.
Without writing all the code I arrive to get the following
data1=dict(
x=list(schools['x'].values),
y=list(schools['y'].values)
)
data2=dict(
x=list(hospitals['x'].values),
y=list(hospitals['y'].values)
)
building = 'Schools'
buildings = {
'Schools': {
'title': 'Schools',
'data': data1,
'color': 'black'
},
'Hospitals': {
'title': 'Hospitals',
'data': data2,
'color': 'red'
}
}
building_select = Select(value=building, title='Building', options=sorted(buildings.keys()))
I would like to change the visualisation between schools and hospitals by selecting it. I define the function that change the data to take and the color.
def returnInfo(building):
dataPoints = buildings[building]['data']
color = buildings[building]['color']
return dataPoints, color
dataPoints, color = returnInfo(building)
I define the function make_plot
def make_plot(dataPoints, title, color):
TOOLS = "pan, wheel_zoom, reset,save"
p = figure(plot_width=800,
tools=TOOLS,
x_axis_location=None,
y_axis_location=None)
# Add points on top (as black points)
buildings = p.circle('x', 'y', size=4, source=data1, color=color)
hover_buildings = HoverTool(renderers = [buildings], point_policy="follow_mouse", tooltips = [("(Long, Lat)", "($x, $y)")])
p.add_tools(hover_buildings)
return p
plot = make_plot(dataPoints, "Data for " + buildings[building]['title'], color)
then I update
def update_plot(attrname, old, new):
building = building_select.value
p.title.text = "Data for " + buildings[building]['title']
src = buildings[building]['data']
dataPoints, color = returnInfo(building)
dataPoints.update
building_select.on_change('value', update_plot)
controls = column(building_select)
curdoc().add_root(row(plot, controls))
but it does not work: i.e. I am not able to change the points from schools to hospitals even if I have the cursor. Where is the error in the update section?
As first solution I suggest to use legend.click_plolicy = 'hide' to toggle visibility of your buildings on the map (Bokeh v1.1.0)
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, show
from bokeh.tile_providers import CARTODBPOSITRON_RETINA
import osmnx as ox
amenities = ['hospital', 'school']
for i, amenity in enumerate(amenities):
buildings = ox.pois_from_address("Port-au-Prince, Haiti", amenities = [amenity], distance = 3500)[['geometry', 'name', 'element_type']]
for item in ['way', 'relation']:
buildings.loc[buildings.element_type == item, 'geometry'] = buildings[buildings.element_type == item]['geometry'].map(lambda x: x.centroid)
buildings.name.fillna('Hospital' if i == 0 else 'School', inplace = True)
amenities[i] = buildings.to_crs(epsg = 3857)
p = figure(title = "Port-au-Prince, Haiti", tools = "pan,wheel_zoom,hover,reset", x_range = (-8057000, -8048500), y_range = (2098000, 2106000),
tooltips = [('Name', '#name'), ("(Long, Lat)", "($x, $y)")], x_axis_location = None, y_axis_location = None, active_scroll = 'wheel_zoom')
p.add_tile(CARTODBPOSITRON_RETINA)
p.grid.grid_line_color = None
for i, b in enumerate(amenities):
source = ColumnDataSource(data = dict(x = b.geometry.x, y = b.geometry.y, name = b.name.values))
p.circle('x', 'y', color = 'red' if i == 0 else 'blue', source = source, legend = 'Hospital' if i == 0 else 'School')
p.legend.click_policy = 'hide'
show(p)
And if you want the Select widget then here is another alternative (Bokeh v1.1.0):
from bokeh.models import ColumnDataSource, Column, Select, CustomJS
from bokeh.plotting import figure, show
from bokeh.tile_providers import CARTODBPOSITRON_RETINA
import osmnx as ox
amenities = ['hospital', 'school']
for i, amenity in enumerate(amenities):
buildings = ox.pois_from_address("Port-au-Prince, Haiti", amenities = [amenity], distance = 3500)[['geometry', 'name', 'element_type']]
for item in ['way', 'relation']:
buildings.loc[buildings.element_type == item, 'geometry'] = buildings[buildings.element_type == item]['geometry'].map(lambda x: x.centroid)
buildings.name.fillna('Hospital' if i == 0 else 'School', inplace = True)
buildings = buildings.to_crs(epsg = 3857)
amenities[i] = dict(x = list(buildings.geometry.x), y = list(buildings.geometry.y), name = list(buildings.name.values), color = (['red'] if i == 0 else ['blue']) * len(buildings.name.values))
source = ColumnDataSource(amenities[0])
p = figure(title = "Hospitals", tools = "pan,wheel_zoom,hover,reset", x_range = (-8057000, -8048500), y_range = (2098000, 2106000),
tooltips = [('Name', '#name'), ("(Long, Lat)", "($x, $y)")], x_axis_location = None, y_axis_location = None, active_scroll = 'wheel_zoom')
p.add_tile(CARTODBPOSITRON_RETINA)
p.circle(x = 'x', y = 'y', color = 'color', source = source)
p.grid.grid_line_color = None
code = ''' source.data = (cb_obj.value == 'Hospitals' ? data[0] : data[1]); p.title.text = cb_obj.value; '''
select = Select(options = ['Hospitals', 'Schools'], callback = CustomJS(args=dict(p = p, source = source, data = amenities), code = code))
show(Column(p, select))
Let me know if you need any explanation on this code.
Below are the changes required to make your code work:
In your make_plot method, since you want to update the title of the plot on selection change, replace
p = figure(plot_width=800,
tools=TOOLS,
x_axis_location=None,
y_axis_location=None)
with
p = figure(plot_width=800,
tools=TOOLS,
title=title,
x_axis_location=None,
y_axis_location=None)
Also, since you want to update the data and color of the buildings, return the buildings too in the method, so that the complete method now looks like:
def make_plot(dataPoints, title, color):
TOOLS = "pan, wheel_zoom, reset,save"
p = figure(plot_width=800,
tools=TOOLS,
title=title,
x_axis_location=None,
y_axis_location=None)
# Add points on top (as black points)
buildings = p.circle('x', 'y', size=4, source=data1, color=color)
hover_buildings = HoverTool(renderers = [buildings], point_policy="follow_mouse", tooltips = [("(Long, Lat)", "($x, $y)")])
p.add_tools(hover_buildings)
return p, buildings
Next, instead of the call to
plot = make_plot(dataPoints, "Data for " + buildings[building]['title'], color)
you need to get the returned buildings also in a variable so that it can be directly updated. So now your call will look like
plot, b = make_plot(dataPoints, "Data for " + buildings[building]['title'], color)
Finally, change your update_plot method, so that it looks like this:
def update_plot(attrname, old, new):
building = building_select.value
plot.title.text = "Data for " + buildings[building]['title']
src = buildings[building]['data']
dataPoints, color = returnInfo(building)
b.data_source.data = dataPoints
b.glyph.fill_color = color
With these changes, it would work as expected. See the results attached.
Sample data used is:
data1=dict(
x=[1,2,3],
y=[2,1,3]
)
data2=dict(
x=[1,2,3],
y=[1,3,2]
)

Plotly Funnel Chart Example Shows Zero Width Shapes

I have copied and pasted the exact code on the plotly example page here in a Jupyter notebook - with the exception that I am running in offline mode so my import statements look like this:
from plotly.offline import init_notebook_mode
import plotly.offline as py
import plotly.figure_factory as ff
from plotly import graph_objs as go
init_notebook_mode()
The resulting plot has funnel shapes with no width.
Is the example code broken? I thought it had something to do with defining the shape and path variables, but printing those values they look reasonable.
Or, seems unlikely, but could offline mode be messing something up?
Complete code from the linked example is below:
values = [13873, 10553, 5443, 3703, 1708]
phases = ['Visit', 'Sign-up', 'Selection', 'Purchase', 'Review']
colors = ['rgb(32,155,160)', 'rgb(253,93,124)', 'rgb(28,119,139)', 'rgb(182,231,235)', 'rgb(35,154,160)']
n_phase = len(phases)
plot_width = 400
section_h = 100
section_d = 10
unit_width = plot_width / max(values)
phase_w = [int(value * unit_width) for value in values]
height = section_h * n_phase + section_d * (n_phase - 1)
shapes = []
label_y = []
for i in range(n_phase):
if (i == n_phase-1):
points = [phase_w[i] / 2, height, phase_w[i] / 2, height - section_h]
else:
points = [phase_w[i] / 2, height, phase_w[i+1] / 2, height - section_h]
path = 'M {0} {1} L {2} {3} L -{2} {3} L -{0} {1} Z'.format(*points)
shape = {
'type': 'path',
'path': path,
'fillcolor': colors[i],
'line': {
'width': 1,
'color': colors[i]
}
}
shapes.append(shape)
# Y-axis location for this section's details (text)
label_y.append(height - (section_h / 2))
height = height - (section_h + section_d)
# For phase names
label_trace = go.Scatter(
x=[-350]*n_phase,
y=label_y,
mode='text',
text=phases,
textfont=dict(
color='rgb(200,200,200)',
size=15
)
)
# For phase values
value_trace = go.Scatter(
x=[350]*n_phase,
y=label_y,
mode='text',
text=values,
textfont=dict(
color='rgb(200,200,200)',
size=15
)
)
data = [label_trace, value_trace]
layout = go.Layout(
title="<b>Funnel Chart</b>",
titlefont=dict(
size=20,
color='rgb(203,203,203)'
),
shapes=shapes,
height=560,
width=800,
showlegend=False,
paper_bgcolor='rgba(44,58,71,1)',
plot_bgcolor='rgba(44,58,71,1)',
xaxis=dict(
showticklabels=False,
zeroline=False,
),
yaxis=dict(
showticklabels=False,
zeroline=False
)
)
fig = go.Figure(data=data, layout=layout)
py.iplot(fig)

Custom arrow style for matplotlib, pyplot.annotate

I am using matplotlib.pyplot.annotate to draw an arrow on my plot, like so:
import matplotlib.pyplot as plt
plt.annotate("",(x,ybottom),(x,ytop),arrowprops=dict(arrowstyle="->"))
I want to use an arrow style that has a flat line at one end and an arrow at the other, so combining the styles "|-|" and "->" to make something we might call "|->", but I can't figure out how to define my own style.
I thought I might try something like
import matplotlib.patches as patches
myarrow = patches.ArrowStyle("Fancy", head_length=0.4,head_width=0.2)
(which should just be the same as "->" for now; I can tweak the style later) but then how do I tell plt.annotate to use myarrow as the style? There is no arrowstyle property for plt.annotate, and arrowprops=dict(arrowstyle=myarrow) doesn't work either.
I've also tried defining it in the arrowprops dictionary, such as
plt.annotate("",(x,ybottom),(x,ytop),arrowprops=dict(head_length=0.4,head_width=0.2))
but that gives me errors about no attribute 'set_head_width'.
So, how can I define my own style for pyplot.annotate to use?
In the last example in your code you could have used headwidth, frac and width to customize the arrow, the result is arrow0 shown below. For highly customized arrows you can used arbitrary polygons. Below you can see the code I used to produce the figures.
To add more polygons you have to edit polygons dictionary, and the new polygons must have the first and the last point at the origin (0,0), the rescaling and repositioning are done automatically. The figure below illustrates how the polygons are defined.
There is still an issue with shrinking that disconnects the line with the polygons. The '|-|>' arrow that you requested can be easily created using this customization.
The code follows:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib import transforms
import numpy as np
from numpy import cos, sin
plt.close()
plt.plot([1,2],[0,4], 'w')
ax = plt.gcf().axes[0]
def patchesAB(styleA, styleB, orig, target,
widthA, lengthA, widthB, lengthB,
kwargsA, kwargsB, shrinkA=0., shrinkB=0.):
'''
Select 'styleA' and 'styleB' from the dictionary 'polygons'
widthA, lengthA, widthB, lenghtB, shrinkA, shrinkB are defined in points
kwargsA and kwargsB are dictionaries
'''
polygons = {\
'|':np.array([[0,0],[0,1],[0.1,1],[0.1,-1],[0,-1],[0,0]], dtype=float),
'arrow1':np.array([[0,0],[0,1],[-1,2],[3,0],[-1,-2],[0,-1],[0,0]], dtype=float),
'arrow2':np.array([[0,0],[-1,1],[0,2],[3,0],[0,-2],[-1,-1],[0,0]], dtype=float),
}
xyA = polygons.get( styleA )
xyB = polygons.get( styleB )
#
fig = plt.gcf()
ax = fig.axes[0]
trans = ax.transData
pixPunit = trans.transform([(1,0),(0,1)])-ax.transData.transform((0,0))
unitPpix = pixPunit
unitPpix[0,0] = 1/unitPpix[0,0]
unitPpix[1,1] = 1/unitPpix[1,1]
#
orig = np.array(orig)
target = np.array(target)
vec = target-orig
angle = np.arctan2( vec[1], vec[0] )
#
lengthA *= unitPpix[0,0]
lengthB *= unitPpix[0,0]
widthA *= unitPpix[1,1]
widthB *= unitPpix[1,1]
orig += (unitPpix[1,1]*sin(angle)+unitPpix[0,0]*cos(angle))*vec*shrinkA
target -= (unitPpix[1,1]*sin(angle)+unitPpix[0,0]*cos(angle))*vec*shrinkB
#TODO improve shrinking... another attempt:
#orig += unitPpix.dot(vec) * shrinkA
#target -= unitPpix.dot(vec) * shrinkB
# polA
if xyA != None:
a = transforms.Affine2D()
tA = a.rotate_around( orig[0], orig[1], angle+np.pi ) + trans
xyA = np.float_(xyA)
xyA[:,0] *= lengthA/(xyA[:,0].max()-xyA[:,0].min())
xyA[:,1] *= widthA/(xyA[:,1].max()-xyA[:,1].min())
xyA += orig
polA = patches.Polygon( xyA, **kwargsA )
polA.set_transform( tA )
else:
polA = None
# polB
if xyB != None:
a = transforms.Affine2D()
tB = a.rotate_around( target[0], target[1], angle ) + trans
xyB = np.float_(xyB)
xyB[:,0] *= lengthB/(xyB[:,0].max()-xyB[:,0].min())
xyB[:,1] *= widthB/(xyB[:,1].max()-xyB[:,1].min())
xyB += target
polB = patches.Polygon( xyB, **kwargsB )
polB.set_transform( tB )
else:
polB = None
return polA, polB
# ARROW 0
plt.annotate('arrow0',xy=(2,1.5),xycoords='data',
xytext=(1.1,1), textcoords='data',
arrowprops=dict(frac=0.1,headwidth=10., width=2.))
#
kwargsA = dict( lw=1., ec='k', fc='gray' )
kwargsB = dict( lw=1., ec='k', fc='b' )
# ARROW 1
orig = (1.1,2.)
target = (2.,2.5)
shrinkA = 0.
shrinkB = 0.
polA, polB = patchesAB( '|', 'arrow1', orig, target, 20.,1.,60.,60.,
kwargsA, kwargsB, shrinkA, shrinkB )
ax.add_patch(polA)
ax.add_patch(polB)
ax.annotate('arrow1', xy=target, xycoords='data',
xytext=orig, textcoords='data',
arrowprops=dict(arrowstyle='-', patchA=polA, patchB=polB,
lw=1., shrinkA=shrinkA, shrinkB=shrinkB, relpos=(0.,0.),
mutation_scale=1.))
# ARROW 2
orig = (1.1,3.)
target = (2.,3.5)
polA, polB = patchesAB( '|', 'arrow2', orig, target, 20.,1.,60.,60.,
kwargsA, kwargsB, shrinkA, shrinkB )
ax.add_patch(polA)
ax.add_patch(polB)
ax.annotate('arrow2', xy=target, xycoords='data',
xytext=orig, textcoords='data',
arrowprops=dict(arrowstyle='-', patchA=polA, patchB=polB,
lw=1., shrinkA=shrinkA, shrinkB=shrinkB, relpos=(0.,0.),
mutation_scale=1.))
plt.autoscale()
plt.xlim(1.,2.2)
plt.ylim(0.5,4)
plt.show()

Categories

Resources