I have a Networkx graph like the following image (image source)
I perform edge attacks and observe the change in values at the node of the resulting subgraph.
Example,
If I attack edge (a,2): edge (a, 2) and (2, 1) will be removed. To explain a bit, when edge (a, 2) is attacked the node 2 will have a degree < 2. So the edge that's connected to node 2 is also removed.
The above attack results in a subgraph
Each time an edge is attacked, the value of the terminal node labelled e observed over time changes. Let's say I perform 5 (attack = 5) attacks, I have a time x attack matrix (time=25, attack=5) that stores the time-series data of node e.
I would like to ask for suggestions on how to visualize the effect of these attacks on the value of node e changing over time.
EDIT:
What information do you want to be able to see or identify from your
visualizations?
I want to see the attack on which edge has the maximum effect on the time course value observed at e. We could imagine this to be a transportation network and the values at node reflect the amount of a product that has reached the location/node. From the source node b, the goods are transported to target node e. The observation made is the change in node values after an edge is attacked and no observation of the edge value is available.
Please find the code that is used to attack edges
import networkx as nx
import matplotlib.pyplot as plt
def attack(G):
print(G.edges())
for i, edge in enumerate(G.edges()):
no_attack = [(6, 9), (3, 16)]
if edge not in no_attack:
data = {}
print(f'attacking edge {edge}')
H = G.copy()
# attack an edge
H.remove_edges_from(ebunch=[edge])
n = len(G.nodes)
retain_node_ids = [9, 3]
H.add_edges_from([(u, v) for u in retain_node_ids for v in (n+1, n+2)])
# remove nodes with degree < 2
H = nx.k_core(H, k=2)
H.remove_nodes_from([n + 1, n + 2])
# graph_utils_py.draw_graph3d(H, fig=2, show=True)
# H = nx.convert_node_labels_to_integers(H, first_label=1, ordering='default', label_attribute=None)
# delete connected nodes and edges
diff_nodes = set(G.nodes()).difference(H.nodes())
diff_edges = {e for e in G.edges() for n in diff_nodes if n in e}
print(f"deleting connected nodes {diff_nodes} ...")
print(f"deleting connected edges {diff_edges} ...")
data['diff_nodes'] = list(diff_nodes)
data['diff_edges'] = list(diff_edges)
data['edge'] = edge
if __name__ == '__main__':
n = 20
G = nx.gnm_random_graph(n=20, m=30, seed=1)
# nx.draw(G, with_labels=True)
# plt.show()
retain_node_ids = [11, 4]
G.add_edges_from([(u, v) for u in retain_node_ids for v in (n, n + 1)])
G = nx.k_core(G, k=2)
G.remove_nodes_from([n, n + 1])
# nx.draw(G, with_labels=True)
# plt.show()
G = nx.convert_node_labels_to_integers(G, first_label=1, ordering='default', label_attribute=None)
nx.draw(G, with_labels=True)
plt.show()
attack(G)
EDIT2:
The answer posted below suggests visualizing the edge attacks by varying the opacity and setting different color schemes. Unfortunately, this doesn't help. One has to create a different image for each attack. I am still looking for other suggestions.
EDIT3: Clarifying a bit more on what exactly I want to visualize to keep things simple.
I'm looking for an interactive graph like the following.
One could click the edge that is attacked and the LHS plot will display the observation made at the target node. The dashed lines are the edges that are affected (stored in variable diff_edges in the code) as a result of an attack on a given edge (stored in variable edge).
If there are overlaps in the edges that are affected after attacking a link, we could display it as multiple lines with the corresponding color mappings. An interactive graph will help the user pick and choose the edge attacks to compare the observation at node e. The edges that are attacked can be displayed by varying the opacity/ line style/ color.
EDIT4: The answer posted below helps. But there is a problem when the impacted edges overlap.
Example,
attack(H, (6, 4), color='red')
attack(H, (5, 4), color='yellow')
gives
The colors overlap and it's hard to visualize. If we can draw the impacted edges next to each other, without overlapping, as shown in the image posted above in edit3 that will be good.
You can first remove the attacked edge and see if it makes another neighboring node decommissioned (impacted edge), then after finding the right edges you draw them with a color specific to that attack. Here I drew the main attack in solid style and the impacted one in dashed style.
import matplotlib.pyplot as plt
import networkx as nx
H = nx.gnm_random_graph(n=8, m=9, seed=5) # generate a random graph
H.add_edges_from([('In', 1), (5, 'Out')]) # adding input/output nodes
pos = nx.spring_layout(H, iterations=400) # find good positions for nodes
edges = []
impacted_edges = []
def attack(G, edge, color):
G.remove_edge(*edge) # first remove the edge
# check if another could be also impacted
if G.degree[edge[0]] == 1:
neighbor = [n for n in G.neighbors(edge[0])][0]
impacted_edge = (edge[0], neighbor, color)
elif G.degree[edge[1]] == 1:
neighbor = [n for n in G.neighbors(edge[1])][0]
impacted_edge = (edge[1], neighbor, color)
else:
impacted_edge = None
if impacted_edge:
impacted_edges.append(impacted_edge)
edges.append((edge[0], edge[1], color))
nx.draw_networkx_edges(
H,
edgelist=[edge],
pos=pos,
edge_color=color,
style='solid',
label=f'Attack {edge[0]}-{edge[1]}',
width=4
)
G.add_edge(*edge)
# attack some edges
attack(H, (6, 4), color='red')
attack(H, (3, 6), color='blue')
attack(H, (1, 2), color='green')
attack(H, (5, 4), color='purple')
ax = plt.gca()
for edge in impacted_edges:
ax.annotate('',
xy=pos[edge[0]],
xytext=pos[edge[1]],
zorder=1,
arrowprops=dict(
color=edge[2],
arrowstyle='-',
connectionstyle='arc3,rad=0.2',
lw=4,
linestyle='--'
)
)
H.remove_edges_from([(e[0], e[1]) for e in impacted_edges])
H.remove_edges_from([(e[0], e[1]) for e in edges])
nx.draw(H, pos, node_size=700, with_labels=True, node_color='gray', edge_color='gray')
plt.legend()
plt.show()
I hope you will find what you want in this answer.
Solution
Prior to deleting the node add arrows to the edges pointing towards node e, node and edges to be removed in green, then red, and repeat. Alphas can also be incorporated to represent min-max distances and how they change as the graph is modified.
References
NetworkX directed graph example: https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_directed.html
NetworkX draw_networkx_edges arguments (includes arrow, color and alpha): https://networkx.github.io/documentation/stable/reference/generated/networkx.drawing.nx_pylab.draw_networkx_edges.html
Would a Sankey Chart help?
A sankey diagram is a visualization used to depict a flow from one set of values to another. The snippet below is from Google charts, just as an example of how the graph flow visualization looks.
<html>
<body>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<div id="sankey_multiple" style="width: 900px; height: 300px;"></div>
<script type="text/javascript">
google.charts.load("current", {packages:["sankey"]});
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
var data = new google.visualization.DataTable();
data.addColumn('string', 'From');
data.addColumn('string', 'To');
data.addColumn('number', 'Weight');
data.addRows([
[ 'Brazil', 'Portugal', 5 ],
[ 'Brazil', 'France', 1 ],
[ 'Brazil', 'Spain', 1 ],
[ 'Brazil', 'England', 1 ],
[ 'Canada', 'Portugal', 1 ],
[ 'Canada', 'France', 5 ],
[ 'Canada', 'England', 1 ],
[ 'Mexico', 'Portugal', 1 ],
[ 'Mexico', 'France', 1 ],
[ 'Mexico', 'Spain', 5 ],
[ 'Mexico', 'England', 1 ],
[ 'USA', 'Portugal', 1 ],
[ 'USA', 'France', 1 ],
[ 'USA', 'Spain', 1 ],
[ 'USA', 'England', 5 ],
[ 'Portugal', 'Angola', 2 ],
[ 'Portugal', 'Senegal', 1 ],
[ 'Portugal', 'Morocco', 1 ],
[ 'Portugal', 'South Africa', 3 ],
[ 'France', 'Angola', 1 ],
[ 'France', 'Senegal', 3 ],
[ 'France', 'Mali', 3 ],
[ 'France', 'Morocco', 3 ],
[ 'France', 'South Africa', 1 ],
[ 'Spain', 'Senegal', 1 ],
[ 'Spain', 'Morocco', 3 ],
[ 'Spain', 'South Africa', 1 ],
[ 'England', 'Angola', 1 ],
[ 'England', 'Senegal', 1 ],
[ 'England', 'Morocco', 2 ],
[ 'England', 'South Africa', 7 ],
[ 'South Africa', 'China', 5 ],
[ 'South Africa', 'India', 1 ],
[ 'South Africa', 'Japan', 3 ],
[ 'Angola', 'China', 5 ],
[ 'Angola', 'India', 1 ],
[ 'Angola', 'Japan', 3 ],
[ 'Senegal', 'China', 5 ],
[ 'Senegal', 'India', 1 ],
[ 'Senegal', 'Japan', 3 ],
[ 'Mali', 'China', 5 ],
[ 'Mali', 'India', 1 ],
[ 'Mali', 'Japan', 3 ],
[ 'Morocco', 'China', 5 ],
[ 'Morocco', 'India', 1 ],
[ 'Morocco', 'Japan', 3 ]
]);
// Set chart options
var options = {
width: 600,
};
// Instantiate and draw our chart, passing in some options.
var chart = new google.visualization.Sankey(document.getElementById('sankey_multiple'));
chart.draw(data, options);
}
</script>
</body>
</html>
If you are looking for a python library, check out Sankey diagrams in Plotly
Related
I have the following ground truth designated in the following fashion. It is a list of Name and score. The caveat about this list is that if the score is the same, the order doesn't matter. (Shown below).
Functionally, it should be similar to some sort of distance score, but I am wondering if there is stuff out there that can be adapted directly. -- This way I don't need extensive testing to make sure it's coded correctly.
Note that list of names/scores can be up to 30-50 long! and while unlikely, the score could be all the same...(so you cant just find all permutations...)
Ideally, I am trying to figure out how to incorporate the following idea:
If the score of two elements is similar (ie within 1.5 of each other), allow them to swap, otherwise don't allow them to swap.
# Name and score
list_v1 = [
['NAME1', 0],
['NAME2', 1.25],
['NAME3', 2.5],
['NAME4', 2.5],
['NAME5', 3.5]
]
list_v2 = [
['NAME1', 0],
['NAME2', 1.25],
['NAME4', 2.5], # note that 4 and 3 are swapped
['NAME3', 2.5],
['NAME5', 3.5]
]
def compare_func(list1, list2):
# what goes in here!!
# pass
is_same = compare_func(list_v1, [i[0] for i in list_v2])
assert is_same == True
list_v3 = ['NAME1', 'NAME2', 'NAME3', 'NAME4', 'NAME5']
is_same = compare_func(list_v1, list_v3)
assert is_same == True
list_v3 = ['NAME1', 'NAME2', 'NAME3', 'NAME4', 'NAME5']
is_same = compare_func(list_v2, list_v3)
assert is_same == True
I am visualizing the data stored in pandas dataframe via plotly.
import plotly.graph_objects as go
import numpy as np
import plotly.express as px
# ref: https://plotly.com/python/sliders/
if __name__ == '__main__':
df = px.data.iris()
df = df.iloc[0:10, :]
# print(df)
petal_width = [1.3, 1.4, 1.4]
# Create figure
fig = go.Figure()
# Add traces, one for each slider step
data = [
go.Scatter(
mode="lines+markers",
x=df['sepal_width'],
y=df['sepal_length'],
),
go.Scatter(
mode="lines+markers",
x=df['sepal_width'],
y=df['sepal_length']+2,
),
go.Scatter(
mode="lines+markers",
x=df['sepal_width'],
y=df['sepal_length'] + 3,
)
]
slider_range = min(petal_width), max(petal_width)
low, high = slider_range
# slides = []
# for i in range(3):
# slide = dict(
# method="update",
# args=[{"visible": [False] * len(fig.data)},
# {"title": "Slider switched to step: " + str(i)}], # layout attribute
# )
# slide["args"][0]["visible"][i] = True # Toggle i'th trace to "visible"
# slides.append(slide)
#
# sliders = [
# dict(
# active=10,
# currentvalue={"prefix": "Frequency: "},
# pad={"t": 50},
# steps=slides
# )
# ]
fig = go.Figure(data=data)
#fig.update_layout(
# sliders=slider
#)
# fig.show()
with open("check.html", 'a') as f:
f.write(fig.to_html(full_html=False, include_plotlyjs='cdn'))
Plot:
This figure shows three lines corresponding to the there dataset in data. These threee curves are associated with 3 values stored in petal_width = [1.3, 1.4, 1.4] .
I would like to add a slider like shown in the example (please see the example plot below) presented [here]
(https://plotly.com/python/line-and-scatter/)
I'm not really sure how to add the slider. Basically, I want to add the slider and based on the value selected in the slider the corresponding curves in the plot should appear. e.g. if 1.4 is selected in the slider (petal_width), I want the second and third curves to appear.
Suggestions will be helpful.
steps = []
for i in range(len(fig.data)):
step = dict(
method="update",
args=[{"visible": [False] * len(fig.data)},
{"title": "Slider switched to step: " + str(i)}], # layout attribute
)
visible = []
val = petal_width[i]
for j in range(len(petal_width)):
if petal_width[j] == val:
step["args"][0]["visible"][j] = True # Toggle i'th trace to "visible"
steps.append(step)
sliders = [dict(
active=1,
currentvalue={"prefix": "Frequency: "},
pad={"t": 50},
steps=steps
)]
fig.update_layout(
sliders=sliders
)
You just need to fix the colors, for all 3 categories.
steps output:
[{'method': 'update',
'args': [{'visible': [True, False, False]},
{'title': 'Slider switched to step: 0'}]},
{'method': 'update',
'args': [{'visible': [False, True, True]},
{'title': 'Slider switched to step: 1'}]},
{'method': 'update',
'args': [{'visible': [False, True, True]},
{'title': 'Slider switched to step: 2'}]}]
Output :
Step:0
Step:1 and 2
I have recently started to use Plotly to make 3D plots in python and I wanted to create an animation of what is going on in terms of column vectos of a 3 by 3 matrix when applying Gaussain elimination.
I wrote a function to get the row echelon form and the history of the matrix obtained at each step.
Then I wanted to plot the comuns vectors at each step of the algorithm.
At first I was able to get an animation of the the evolution of the three vectors by adpating this code : https://plotly.com/python/visualizing-mri-volume-slices/
But then I wanted to show on each frame the three row vectors of a given step and the three row vectors from the matrix of the previous step with opacity 0.2.
And when I added that part of the code I got a strange behavior from Plotly. It only showed me the three first vectors which are given to the frame and not all of them.
Here the code I have so far :
import numpy as np
import numpy.linalg as la
import plotly.graph_objects as go
v1 = np.array([5,2,1])
v2 = np.array([2,3,2])
v3 = np.array([3,-1,1])
A = np.transpose(np.vstack([v1,v2,v3]))
# G, H = pivot_Gauss(A)
H = [np.array([[ 5, 2, 3],[ 2, 3, -1],[ 1, 2, 1]]), np.array([[ 1, 0, 0],[ 2, 3, -1],[ 1, 2, 1]]),
np.array([[ 1, 0, 0],[ 0, 3, -1],[ 1, 2, 1]]), np.array([[ 1, 0, 0],[ 0, 3, -1],[ 0, 2, 1]]),
np.array([[1, 0, 0],[0, 1, 0],[0, 2, 1]]), np.array([[1, 0, 0],[0, 1, 0],[0, 0, 1]]),
np.array([[1, 0, 0],[0, 1, 0],[0, 0, 1]]) ]
G = np.array([[1,0,0],[0,1,0],[0,0,1]]) # results obtained using the function pivot_Gauss(A)
nb_frames = len(H)
frames = []
v_norm = 5
colors = ["blue","red","green"]
for k in range(nb_frames): # go.Frame(data,name=str(k))
dat = []
for j in range(np.shape(A)[1]):
v = H[k][:,j]
if la.norm(v) != 0 :
d1 = go.Scatter3d( x=[0,v[0]],y=[0,v[1]],z=[0,v[2]],name="v"+str(k+j+1),hoverinfo='name',
marker=dict(size=0), line=dict(color=colors[j], width=10 ))
dat.append(d1)
d2 = go.Cone(x=[v[0]],y=[v[1]],z=[v[2]],
u=[v[0]/v_norm],v=[v[1]/v_norm],w=[v[2]/v_norm],sizeref=1,
sizemode="scaled",anchor="cm",name="v"+str(k+j+1),hoverinfo='x+y+z+name',
colorscale=[[0, colors[j]], [1,colors[j]]],showscale=False)
dat.append(d2)
if k>0 : # add column vectors of previous Gaussain elimination step (causes some troubles,
#if this if section is commented I get an animation of the three clumn vectors of current step)
vk = H[k-1][:,j]
if la.norm(v) != 0 :
d3 = go.Scatter3d( x=[0,vk[0]],y=[0,vk[1]],z=[0,vk[2]],name="v"+str(k+j+1),hoverinfo='name',
marker=dict(size=0), line=dict(color=colors[j], width=10), opacity = 0.2 )
dat.append(d3)
d4 = go.Cone(x=[vk[0]],y=[vk[1]],z=[vk[2]],
u=[vk[0]/v_norm],v=[vk[1]/v_norm],w=[vk[2]/v_norm],sizeref=1,
sizemode="scaled",anchor="cm",name="v"+str(k+j+1),hoverinfo='x+y+z+name',
colorscale=[[0, colors[j]], [1,colors[j]]],showscale=False,opacity=0.2)
dat.append(d4)
frames.append(go.Frame(data=dat,name=str(k)))
fig = go.Figure(frames=frames)
# Add data to be displayed before animation starts
for j in range(A.shape[1]):
v = A[:,j]
if la.norm(v) != 0 :
fig.add_trace( go.Scatter3d( x=[0,v[0]],y=[0,v[1]],z=[0,v[2]],name="v"+str(k+1),hoverinfo='name',
marker=dict(size=0), line=dict(color=colors[j], width=10 )) )
fig.add_trace( go.Cone(x=[v[0]],y=[v[1]],z=[v[2]],
u=[v[0]/v_norm],v=[v[1]/v_norm],w=[v[2]/v_norm],sizeref=1,
sizemode="scaled",anchor="cm",name="v"+str(k+1),hoverinfo='x+y+z+name',
colorscale=[[0, colors[j]], [1,colors[j]]],showscale=False) )
### This remained almost exactly as the Plotly example
def frame_args(duration):
return {
"frame": {"duration": duration},
"mode": "immediate",
"fromcurrent": True,
"transition": {"duration": duration, "easing": "linear"},
}
sliders = [
{
"pad": {"b": 10, "t": 60},
"len": 0.9,
"x": 0.1,
"y": 0,
"steps": [
{
"args": [[f.name], frame_args(0)],
"label": str(k),
"method": "animate",
}
for k, f in enumerate(fig.frames)
],
}
]
matrix_but = [
{"buttons: [{},{},{},{},{},{}]"}
]
# Layout
fig.update_layout(
title='Pivot de Gauss',
width=600,
height=400,
scene=dict(xaxis=dict(autorange=True),
yaxis=dict(autorange=True),
zaxis=dict(autorange=True),
aspectratio=dict(x=1, y=1, z=1),
),
updatemenus = [
{
"buttons": [
{
"args": [None, frame_args(200)],
"label": "▶", # play symbol
"method": "animate",
},
{
"args": [[None], frame_args(0)],
"label": "◼", # pause symbol
"method": "animate",
},
],
"direction": "left",
"pad": {"r": 10, "t": 70},
"type": "buttons",
"x": 0.1,
"y": 0,
}
],
sliders=sliders
)
fig.show()
You will notice that for each vector I first draw a 3D line and then use cone to get the it arrow_shaped. It might not be the best way to do it, but I do not want to use cone alone as the apsect does not fit what I would like.
I stumbled across a (I think) similar question here : https://community.plotly.com/t/only-one-trace-showing-per-frame-in-animated-plot/25803
But I did not undestand the answer nor the example.
It seems from what I get that only the first six elemetns of the data contained in each frame is taken into account, but I do not understand why and I would like to show everything.
If someone has some insight (and a solution) on the subject, it would be warmly welcomed.
I can clarify things if needed.
Image of the two first column vectors of matrix from current step and first column vector of matrix from previous step
Image of the three column vectors of current matrix when part below if k>0 is commented
It seems from what I get that only the first six elemetns of the data contained in each frame is taken into account, but I do not understand why and I would like to show everything.
There's this paragraph under the heading 'Current Animation Limitations and Caveats':
Animations are designed to work well when each row of input is present across all animation frames, and when categorical values mapped to symbol, color and facet are constant across frames. Animations may be misleading or inconsistent if these constraints are not met.
Though in your first frame you have only three vectors (three lines plus three coneheads) to plot, it violates the above constraint when following frames contain six vectors. To overcome this restriction, we could insert the three vectors in the first frame (and also in the data to be displayed before animation starts) twice, i. e. to the
if k>0 : # add column vectors of previous Gaussain elimination step (causes some troubles,
block add an
else:
dat.append(d1)
dat.append(d2)
block, and in the
if la.norm(v) != 0 :
block duplicate the two fig.add_trace calls.
In pandas >= 1.1.4 / Python 3, I would like to protect a nested element against flattening when using json_normalize().
I cannot figure out such thing in the documentation.
Actual example
Here's a concrete example to figure out the main idea:
res='''
{
"results": [
{
"geometry": {
"type": "Polygon",
"crs": 4326,
"coordinates":
[[
[6.0, 49.0],
[6.0, 40.0],
[7.0, 40.0],
[7.0, 49.0],
[6.0, 49.0]
]]
},
"attribute": "layer.metadata",
"bbox": [6, 40, 7, 49],
"featureName": "Coniferous_Trees",
"layerName": "State_Forests",
"type": "Feature",
"id": "17",
"properties": {
"resolution": "100",
"Year": "2020",
"label": "Coniferous"
}
}
]
}
'''
This is a single JSON record from an API response. Here, there is only one element in the top level list, but there my be more, each following the same structure as the one shown here. I'd like to import this into a DataFrame without columns containing structured element, namely, I want to flatten / normalize them all. Well,... almost all. json_normalize() is a doing an amazing job in doing that:
import pandas as pd
data = json.loads(res)['results']
df = pd.DataFrame(pd.json_normalize(data))
And here are the columns of the DataFrame:
>>> print(df.columns)
Index(['attribute', 'bbox', 'featureName', 'layerName', 'type', 'id',
'geometry.type', 'geometry.crs', 'geometry.coordinates', # <-- the geometry has been flattened
'properties.resolution', 'properties.Year', 'properties.label'],
dtype='object')
Wanted behaviour
But I need to, let's say, "protect" the geometry object in the input JSON response against flattening so that I end up with these columns instead:
# e.g. something like this:
df = pd.DataFrame(pd.json_normalize(data, protect="results.geometry"))
# or this if there isn't two objects with the same name:
df = pd.DataFrame(pd.json_normalize(data, protect="geometry"))
which would lead to:
>>> print(df.columns)
Index(['attribute', 'bbox', 'featureName', 'layerName', 'type', 'id',
'geometry', 'properties.resolution', # <-- the geometry element has been protected!
'properties.Year', 'properties.label'],
dtype='object')
Is there a way of doing that properly?
Consider max_level=0. Per pandas.json_normalize docs:
max_level : int, default None
Max number of levels(depth of dict) to normalize. if None, normalizes all levels.
data = json.loads(response)["results"]
df = pd.DataFrame(pd.json_normalize(data, max_level=0))
print(df.T)
# 0
# geometry {'type': 'Polygon', 'crs': 4326, 'coordinates'...
# attribute layer.metadata
# bbox [6, 40, 7, 49]
# featureName Coniferous_Trees
# layerName State_Forests
# type Feature
# id 17
# properties {'resolution': '100', 'Year': '2020', 'label':...
print(df.columns)
# Index(['geometry', 'attribute', 'bbox', 'featureName', 'layerName', 'type', 'id',
# 'properties'], dtype='object')
And since all nested objects are not normalized, use data wrangling to unwind needed columns like properties:
df = (
df.drop(['properties'], axis="columns")
.join(df["properties"].dropna().apply(pd.Series))
)
print(df.T)
# 0
# geometry {'type': 'Polygon', 'crs': 4326, 'coordinates'...
# attribute layer.metadata
# bbox [6, 40, 7, 49]
# featureName Coniferous_Trees
# layerName State_Forests
# type Feature
# id 17
# resolution 100
# Year 2020
# label Coniferous
print(df.columns)
# Index(['geometry', 'attribute', 'bbox', 'featureName', 'layerName', 'type', 'id',
# 'resolution', 'Year', 'label'], dtype='object')
I have a map where a key holds multiple values
datamap = [ 'Antenna Software':[ 'Salarpuria', 'Cessna', 'Vrindavan Tech', 'Alpha Center' ],
'Ellucian':[ 'Malvern', 'Ellucian House', 'Residency Road'] ]
here i need to alphabetically sort the values
datamap = [ 'Antenna Software':[ 'Alpha Center', 'Cessna', 'Salarpuria', 'Vrindavan Tech' ],
'Ellucian':[ 'Ellucian House', 'Malvern', 'Residency Road' ] ]
how to do it in groovy way?
You should be able to do:
def sortedMap = datamap.sort().collectEntries { k, v ->
[ k, v.sort( false ) ]
}
If you're not bothered about sorting the keys of the map, you can get rid of the initial sort():
def sortedMap = datamap.collectEntries { k, v ->
[ k, v.sort( false ) ]
}
Explanation of sort( false ):
By default, the sort method in Groovy changes the original list, so:
// Given a List
def a = [ 3, 1, 2 ]
// We can sort it
def b = a.sort()
// And the result is sorted
assert b == [ 1, 2, 3 ]
// BUT the original list has changed too!
assert a != [ 3, 1, 2 ] && a == [ 1, 2, 3 ]
So if you pass false to sort, it leaves the original list alone, and just returns the sorted list:
// Given a List
def a = [ 3, 1, 2 ]
// We can sort it (passing false)
def b = a.sort( false )
// And the result is sorted
assert b == [ 1, 2, 3 ]
// AND the original list has remained the same
assert a == [ 3, 1, 2 ]