d3graph 2.9.0__tar.gz → 2.9.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {d3graph-2.9.0/d3graph.egg-info → d3graph-2.9.2}/PKG-INFO +1 -1
- {d3graph-2.9.0 → d3graph-2.9.2}/d3graph/__init__.py +1 -1
- {d3graph-2.9.0 → d3graph-2.9.2}/d3graph/d3graph.py +135 -78
- {d3graph-2.9.0 → d3graph-2.9.2}/d3graph/examples.py +15 -4
- {d3graph-2.9.0 → d3graph-2.9.2/d3graph.egg-info}/PKG-INFO +1 -1
- {d3graph-2.9.0 → d3graph-2.9.2}/LICENSE +0 -0
- {d3graph-2.9.0 → d3graph-2.9.2}/MANIFEST.in +0 -0
- {d3graph-2.9.0 → d3graph-2.9.2}/README.md +0 -0
- {d3graph-2.9.0 → d3graph-2.9.2}/d3graph/d3js/d3.v3.js +0 -0
- {d3graph-2.9.0 → d3graph-2.9.2}/d3graph/d3js/d3graphscript.js +0 -0
- {d3graph-2.9.0 → d3graph-2.9.2}/d3graph/d3js/index.html.j2 +0 -0
- {d3graph-2.9.0 → d3graph-2.9.2}/d3graph/d3js/style.css +0 -0
- {d3graph-2.9.0 → d3graph-2.9.2}/d3graph/examples_docs.py +0 -0
- {d3graph-2.9.0 → d3graph-2.9.2}/d3graph.egg-info/SOURCES.txt +0 -0
- {d3graph-2.9.0 → d3graph-2.9.2}/d3graph.egg-info/dependency_links.txt +0 -0
- {d3graph-2.9.0 → d3graph-2.9.2}/d3graph.egg-info/requires.txt +0 -0
- {d3graph-2.9.0 → d3graph-2.9.2}/d3graph.egg-info/top_level.txt +0 -0
- {d3graph-2.9.0 → d3graph-2.9.2}/pyproject.toml +0 -0
- {d3graph-2.9.0 → d3graph-2.9.2}/setup.cfg +0 -0
|
@@ -253,6 +253,7 @@ class d3graph:
|
|
|
253
253
|
edge_style=0,
|
|
254
254
|
edge_color: (str, list) = '#808080',
|
|
255
255
|
edge_opacity: (float, str, list) = 'weight',
|
|
256
|
+
min_weight: float = 1.0,
|
|
256
257
|
scaler: str = 'zscore',
|
|
257
258
|
directed: bool = False,
|
|
258
259
|
marker_start=None,
|
|
@@ -287,6 +288,8 @@ class d3graph:
|
|
|
287
288
|
edge_opacity : (float, str, list), (default: 1.0)
|
|
288
289
|
* 0.8 : Opacity of the edges [0-1] where 0=transparent and 1=fully opaque.
|
|
289
290
|
* 'weight' : Set opacity based on the weight of the edge and the scaler
|
|
291
|
+
min_weight : float
|
|
292
|
+
edges are kept with >= weight.
|
|
290
293
|
scaler : str, (default: 'zscore')
|
|
291
294
|
Scale the edge-width using the following scaler:
|
|
292
295
|
* 'zscore' : Scale values to Z-scores.
|
|
@@ -346,13 +349,14 @@ class d3graph:
|
|
|
346
349
|
self.config['label'] = label
|
|
347
350
|
self.config['label_color'] = label_color
|
|
348
351
|
self.config['label_fontsize'] = label_fontsize
|
|
352
|
+
self.config['min_weight'] = min_weight
|
|
349
353
|
|
|
350
354
|
if not hasattr(self, 'adjmat'):
|
|
351
355
|
logger.error('adjmat is missing. Initialize first with d3 = d3graph(adjmat)')
|
|
352
356
|
return
|
|
353
357
|
|
|
354
358
|
if (not directed) and (marker_end is not None) or (marker_start is not None):
|
|
355
|
-
logger.info('Set directed=True to see the markers
|
|
359
|
+
logger.info('Markers not shown. Set directed=True to see the markers.')
|
|
356
360
|
|
|
357
361
|
# Get node color properties which is required to set the edge color
|
|
358
362
|
node_color_map = None
|
|
@@ -364,9 +368,10 @@ class d3graph:
|
|
|
364
368
|
self.config['edge_color'] = '#808080'
|
|
365
369
|
|
|
366
370
|
# Set the edge properties
|
|
371
|
+
logger.info('Set the edge properties')
|
|
367
372
|
self.edge_properties, self.adjmat = adjmat2dict(
|
|
368
373
|
self.adjmat,
|
|
369
|
-
|
|
374
|
+
min_weight=self.config['min_weight'],
|
|
370
375
|
minmax=self.config['minmax'],
|
|
371
376
|
minmax_distance=self.config['minmax_distance'],
|
|
372
377
|
scaler=self.config['edge_scaler'],
|
|
@@ -385,7 +390,7 @@ class d3graph:
|
|
|
385
390
|
node_color_map=node_color_map,
|
|
386
391
|
)
|
|
387
392
|
|
|
388
|
-
logger.
|
|
393
|
+
# logger.info(f'Number of edges: {len(self.edge_properties.keys())}')
|
|
389
394
|
|
|
390
395
|
def set_node_properties(self,
|
|
391
396
|
label=None,
|
|
@@ -697,6 +702,7 @@ class d3graph:
|
|
|
697
702
|
|
|
698
703
|
def graph(self,
|
|
699
704
|
adjmat,
|
|
705
|
+
min_weight: float = 1.0,
|
|
700
706
|
color: str = 'cluster',
|
|
701
707
|
opacity: str = 'degree',
|
|
702
708
|
size='degree',
|
|
@@ -710,7 +716,9 @@ class d3graph:
|
|
|
710
716
|
Parameters
|
|
711
717
|
----------
|
|
712
718
|
adjmat : pd.DataFrame()
|
|
713
|
-
Adjacency matrix (symmetric
|
|
719
|
+
Adjacency matrix (symmetric and Values > 0 are edges).
|
|
720
|
+
min_weight : float
|
|
721
|
+
edges are kept with >= weight.
|
|
714
722
|
color : list of strings (default: 'cluster')
|
|
715
723
|
Coloring of the nodes.
|
|
716
724
|
* 'cluster' or None : Colours are based on the community distance clusters.
|
|
@@ -764,7 +772,7 @@ class d3graph:
|
|
|
764
772
|
# Checks
|
|
765
773
|
self.adjmat = data_checks(adjmat.copy())
|
|
766
774
|
# Set default edge properties
|
|
767
|
-
self.set_edge_properties(scaler=scaler)
|
|
775
|
+
self.set_edge_properties(scaler=scaler, min_weight=min_weight)
|
|
768
776
|
# Set default node properties
|
|
769
777
|
self.set_node_properties(color=color, opacity=opacity, size=size, scaler=scaler, cmap=cmap)
|
|
770
778
|
|
|
@@ -1059,7 +1067,7 @@ def json_create(G: nx.Graph) -> str:
|
|
|
1059
1067
|
|
|
1060
1068
|
# %% Convert adjacency matrix to vector
|
|
1061
1069
|
def adjmat2dict(adjmat: pd.DataFrame,
|
|
1062
|
-
|
|
1070
|
+
min_weight: float = 1.0,
|
|
1063
1071
|
scaler: str = 'zscore',
|
|
1064
1072
|
marker_start=None,
|
|
1065
1073
|
marker_end='arrow',
|
|
@@ -1083,8 +1091,8 @@ def adjmat2dict(adjmat: pd.DataFrame,
|
|
|
1083
1091
|
----------
|
|
1084
1092
|
adjmat : pd.DataFrame()
|
|
1085
1093
|
Adjacency matrix.
|
|
1086
|
-
|
|
1087
|
-
edges are
|
|
1094
|
+
min_weight : float
|
|
1095
|
+
edges are kept with >= weight.
|
|
1088
1096
|
scaler : str, (default: 'zscore')
|
|
1089
1097
|
Scale the edge-width using the following scaler:
|
|
1090
1098
|
'zscore' : Scale values to Z-scores.
|
|
@@ -1153,21 +1161,23 @@ def adjmat2dict(adjmat: pd.DataFrame,
|
|
|
1153
1161
|
'tooltip': Text that is shown when hovering over the edge.
|
|
1154
1162
|
|
|
1155
1163
|
"""
|
|
1156
|
-
# Convert adjacency matrix into vector
|
|
1157
|
-
df = adjmat.stack().reset_index()
|
|
1158
|
-
# Set columns
|
|
1159
|
-
df.columns = ['source', 'target', 'weight']
|
|
1160
|
-
# Combine source-target values with weights
|
|
1161
|
-
df = create_unique_dataframe(df)
|
|
1162
|
-
# Remove self loops and no-connected edges
|
|
1163
|
-
Iloc = df['source'] != df['target']
|
|
1164
|
-
# Keep only edges with a minimum edge strength
|
|
1165
|
-
if
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
df = df.loc[Iloc, :]
|
|
1170
|
-
df.reset_index(drop=True, inplace=True)
|
|
1164
|
+
# # Convert adjacency matrix into vector
|
|
1165
|
+
# df = adjmat.stack().reset_index()
|
|
1166
|
+
# # Set columns
|
|
1167
|
+
# df.columns = ['source', 'target', 'weight']
|
|
1168
|
+
# # Combine source-target values with weights
|
|
1169
|
+
# df = create_unique_dataframe(df)
|
|
1170
|
+
# # Remove self loops and no-connected edges
|
|
1171
|
+
# Iloc = df['source'] != df['target']
|
|
1172
|
+
# # Keep only edges with a minimum edge strength
|
|
1173
|
+
# if min_weight is not None:
|
|
1174
|
+
# logger.info(f"Keep only edges with weight>{min_weight}")
|
|
1175
|
+
# Iloc2 = df['weight'] > min_weight
|
|
1176
|
+
# Iloc = Iloc & Iloc2
|
|
1177
|
+
# df = df.loc[Iloc, :]
|
|
1178
|
+
# df.reset_index(drop=True, inplace=True)
|
|
1179
|
+
|
|
1180
|
+
df = adjmat2vec(adjmat, min_weight=min_weight)
|
|
1171
1181
|
|
|
1172
1182
|
# Scale the weights for visualization purposes
|
|
1173
1183
|
if minmax_distance is not None:
|
|
@@ -1225,6 +1235,7 @@ def adjmat2dict(adjmat: pd.DataFrame,
|
|
|
1225
1235
|
source_target = list(zip(df['source'], df['target']))
|
|
1226
1236
|
if len(source_target)==0:
|
|
1227
1237
|
raise Exception('There are no links in the input data set that have unique source-target value with weight > 0')
|
|
1238
|
+
|
|
1228
1239
|
# Return
|
|
1229
1240
|
d = {edge: {'weight': df['weight'].iloc[i],
|
|
1230
1241
|
'weight_scaled': df['weight_scaled'].iloc[i],
|
|
@@ -1249,6 +1260,36 @@ def adjmat2dict(adjmat: pd.DataFrame,
|
|
|
1249
1260
|
|
|
1250
1261
|
|
|
1251
1262
|
# %% Create unique dataframe and update weights
|
|
1263
|
+
# def create_unique_dataframe(X, logger=None):
|
|
1264
|
+
# """Combine source-target into adjacency matrix with updated weights.
|
|
1265
|
+
|
|
1266
|
+
# Parameters
|
|
1267
|
+
# ----------
|
|
1268
|
+
# X : DataFrame
|
|
1269
|
+
# Data frame containing the columns [source, target, weight].
|
|
1270
|
+
# logger : Object, optional
|
|
1271
|
+
# Logger object. The default is None.
|
|
1272
|
+
|
|
1273
|
+
# Returns
|
|
1274
|
+
# -------
|
|
1275
|
+
# X : pd.DataFrame
|
|
1276
|
+
# Unique adjacency matrix containing with index as source and columns as target labels. Weights are in the matrix.
|
|
1277
|
+
|
|
1278
|
+
# References
|
|
1279
|
+
# ----------
|
|
1280
|
+
# * This function is similar to that of d3blocks.
|
|
1281
|
+
|
|
1282
|
+
# """
|
|
1283
|
+
# # Check whether labels are unique
|
|
1284
|
+
# if isinstance(X, pd.DataFrame):
|
|
1285
|
+
# Iloc = ismember(X.columns, ['source', 'target', 'weight'])[0]
|
|
1286
|
+
# X = X.loc[:, Iloc]
|
|
1287
|
+
# if 'weight' in X.columns: X['weight'] = X['weight'].astype(float)
|
|
1288
|
+
# # Groupby values and sum the weights
|
|
1289
|
+
# X = X.groupby(by=['source', 'target']).sum()
|
|
1290
|
+
# X.reset_index(drop=False, inplace=True)
|
|
1291
|
+
# return X
|
|
1292
|
+
|
|
1252
1293
|
def create_unique_dataframe(X, logger=None):
|
|
1253
1294
|
"""Combine source-target into adjacency matrix with updated weights.
|
|
1254
1295
|
|
|
@@ -1269,15 +1310,12 @@ def create_unique_dataframe(X, logger=None):
|
|
|
1269
1310
|
* This function is similar to that of d3blocks.
|
|
1270
1311
|
|
|
1271
1312
|
"""
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
X = X.groupby(by=['source', 'target']).sum()
|
|
1279
|
-
X.reset_index(drop=False, inplace=True)
|
|
1280
|
-
return X
|
|
1313
|
+
X = X[['source', 'target', 'weight']]
|
|
1314
|
+
return (
|
|
1315
|
+
X.groupby(['source', 'target'], sort=False, observed=True)
|
|
1316
|
+
.agg({'weight': 'sum'})
|
|
1317
|
+
.reset_index()
|
|
1318
|
+
)
|
|
1281
1319
|
|
|
1282
1320
|
|
|
1283
1321
|
# %% Convert dict with edges to graph (G) (also works with lower versions of networkx)
|
|
@@ -1580,61 +1618,80 @@ def vec2adjmat(source, target, weight=None, symmetric: bool = True, aggfunc='sum
|
|
|
1580
1618
|
|
|
1581
1619
|
|
|
1582
1620
|
# %% Convert adjacency matrix to vector
|
|
1621
|
+
# def adjmat2vec(adjmat, min_weight: float = 1.0) -> pd.DataFrame:
|
|
1622
|
+
# """Convert adjacency matrix into vector with source and target.
|
|
1623
|
+
|
|
1624
|
+
# Parameters
|
|
1625
|
+
# ----------
|
|
1626
|
+
# adjmat : pd.DataFrame()
|
|
1627
|
+
# Adjacency matrix.
|
|
1628
|
+
|
|
1629
|
+
# min_weight : float
|
|
1630
|
+
# edges are returned with a minimum weight.
|
|
1631
|
+
|
|
1632
|
+
# Returns
|
|
1633
|
+
# -------
|
|
1634
|
+
# pd.DataFrame()
|
|
1635
|
+
# nodes that are connected based on source and target
|
|
1636
|
+
|
|
1637
|
+
# Examples
|
|
1638
|
+
# --------
|
|
1639
|
+
# >>> source = ['Cloudy', 'Cloudy', 'Sprinkler', 'Rain']
|
|
1640
|
+
# >>> target = ['Sprinkler', 'Rain', 'Wet_Grass', 'Wet_Grass']
|
|
1641
|
+
# >>> adjmat = vec2adjmat(source, target, weight=[1, 2, 1, 3])
|
|
1642
|
+
# >>> vector = adjmat2vec(adjmat)
|
|
1643
|
+
|
|
1644
|
+
# """
|
|
1645
|
+
# # Convert adjacency matrix into vector
|
|
1646
|
+
# logger.info('Converting adjacency matrix into source-target..')
|
|
1647
|
+
# adjmat = adjmat.stack().reset_index()
|
|
1648
|
+
# # Set columns
|
|
1649
|
+
# adjmat.columns = ['source', 'target', 'weight']
|
|
1650
|
+
# # Remove self loops and no-connected edges
|
|
1651
|
+
# Iloc1 = adjmat['source'] != adjmat['target']
|
|
1652
|
+
# Iloc2 = adjmat['weight'] >= min_weight
|
|
1653
|
+
# Iloc = Iloc1 & Iloc2
|
|
1654
|
+
# # Take only connected nodes
|
|
1655
|
+
# adjmat = adjmat.loc[Iloc, :]
|
|
1656
|
+
# adjmat.reset_index(drop=True, inplace=True)
|
|
1657
|
+
# return adjmat
|
|
1658
|
+
|
|
1659
|
+
|
|
1583
1660
|
def adjmat2vec(adjmat, min_weight: float = 1.0) -> pd.DataFrame:
|
|
1584
|
-
"""
|
|
1661
|
+
"""
|
|
1662
|
+
Fast conversion of adjacency matrix → edge list.
|
|
1663
|
+
Removes self-loops and filters by min_weight.
|
|
1664
|
+
"""
|
|
1665
|
+
weights = adjmat.to_numpy(copy=False)
|
|
1585
1666
|
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
adjmat : pd.DataFrame()
|
|
1589
|
-
Adjacency matrix.
|
|
1667
|
+
# Mask of valid edges (stack() drops NaN, so we mimic that)
|
|
1668
|
+
mask = ~np.isnan(weights)
|
|
1590
1669
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1670
|
+
# Extract coordinates of non-NaN entries
|
|
1671
|
+
row_idx, col_idx = np.nonzero(mask)
|
|
1593
1672
|
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
pd.DataFrame()
|
|
1597
|
-
nodes that are connected based on source and target
|
|
1673
|
+
index = adjmat.index.to_numpy()
|
|
1674
|
+
columns = adjmat.columns.to_numpy()
|
|
1598
1675
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1676
|
+
# Build edge list
|
|
1677
|
+
df = pd.DataFrame({
|
|
1678
|
+
'source': index[row_idx],
|
|
1679
|
+
'target': columns[col_idx],
|
|
1680
|
+
'weight': weights[row_idx, col_idx],
|
|
1681
|
+
})
|
|
1605
1682
|
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
logger.info('Converting adjacency matrix into source-target..')
|
|
1609
|
-
adjmat = adjmat.stack().reset_index()
|
|
1610
|
-
# Set columns
|
|
1611
|
-
adjmat.columns = ['source', 'target', 'weight']
|
|
1612
|
-
# Remove self loops and no-connected edges
|
|
1613
|
-
Iloc1 = adjmat['source'] != adjmat['target']
|
|
1614
|
-
Iloc2 = adjmat['weight'] >= min_weight
|
|
1615
|
-
Iloc = Iloc1 & Iloc2
|
|
1616
|
-
# Take only connected nodes
|
|
1617
|
-
adjmat = adjmat.loc[Iloc, :]
|
|
1618
|
-
adjmat.reset_index(drop=True, inplace=True)
|
|
1619
|
-
return adjmat
|
|
1683
|
+
# Remove self-loops
|
|
1684
|
+
df = df[df['source'] != df['target']]
|
|
1620
1685
|
|
|
1686
|
+
# Apply minimum weight filter
|
|
1687
|
+
if min_weight is not None:
|
|
1688
|
+
df = df[df['weight'] >= min_weight]
|
|
1689
|
+
|
|
1690
|
+
df.reset_index(drop=True, inplace=True)
|
|
1691
|
+
return df
|
|
1621
1692
|
|
|
1622
|
-
# def _check_hex_color(color, n=None, cmap='Set1'):
|
|
1623
|
-
# if isinstance(color, str) and len(color) != 7:
|
|
1624
|
-
# logger.warning('Input parameter [color] has wrong format. Must be like color="#000000" <auto-fixing>')
|
|
1625
|
-
# return get_hex_color(color, cmap=cmap)[0]
|
|
1626
|
-
# if isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and len(color) == 0:
|
|
1627
|
-
# logger.warning('Input parameter [color] has wrong format and length. Must be like: color=["#000000", "...", "#000000"] <auto-fixing>')
|
|
1628
|
-
# return get_hex_color(color, cmap=cmap)[0]
|
|
1629
|
-
# if isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and (not np.all(list(map(lambda x: len(x) == 7, color)))):
|
|
1630
|
-
# logger.warning('[color] contains incorrect hex-colors. Hex must be of length 7: ["#000000", "#000000", etc] <auto-fixing>')
|
|
1631
|
-
# return get_hex_color(color, cmap=cmap)[0]
|
|
1632
|
-
# if (n is not None) and isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and len(color) != n:
|
|
1633
|
-
# logger.warning(f'Input parameter [color] has wrong length. Must be of length: {str(n)} <auto-fixing>')
|
|
1634
|
-
# return get_hex_color(color, cmap=cmap)[0]
|
|
1635
|
-
# # Return original input
|
|
1636
|
-
# return color
|
|
1637
1693
|
|
|
1694
|
+
# %%
|
|
1638
1695
|
def _check_hex_color(color, n=None, cmap='Set1'):
|
|
1639
1696
|
seq_types = (list, np.ndarray, pd.Series, StringArray)
|
|
1640
1697
|
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
# %%
|
|
2
2
|
from d3graph import d3graph, vec2adjmat, import_example
|
|
3
3
|
import pandas as pd
|
|
4
|
+
import time
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
df = pd.read_csv('https://github.com/d3blocks/d3blocks/files/11995798/Df.csv', sep=',', index_col=False)
|
|
6
8
|
del df['Unnamed: 0']
|
|
7
9
|
df = df[0:5000]
|
|
8
10
|
adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
|
|
9
11
|
|
|
10
|
-
# sticky=False — classic spring-back behaviour
|
|
11
12
|
d3 = d3graph()
|
|
13
|
+
|
|
14
|
+
start = time.perf_counter()
|
|
12
15
|
d3.graph(adjmat)
|
|
16
|
+
end = time.perf_counter()
|
|
17
|
+
|
|
13
18
|
d3.show()
|
|
14
19
|
|
|
20
|
+
print(f"Elapsed: {end - start:.6f} seconds")
|
|
21
|
+
|
|
22
|
+
|
|
15
23
|
# %% SET PATH ISSUE https://github.com/erdogant/d3graph/issues/42
|
|
16
24
|
from pathlib import Path
|
|
17
25
|
from d3graph import d3graph, vec2adjmat, import_example
|
|
@@ -123,15 +131,18 @@ adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'
|
|
|
123
131
|
|
|
124
132
|
|
|
125
133
|
# sticky=True (default) — drag to pin, right-click to release
|
|
126
|
-
d3 = d3graph(
|
|
134
|
+
d3 = d3graph(sticky=True)
|
|
135
|
+
d3.graph(adjmat)
|
|
127
136
|
d3.show()
|
|
128
137
|
|
|
129
138
|
# sticky=False — classic spring-back behaviour
|
|
130
|
-
d3 = d3graph(
|
|
139
|
+
d3 = d3graph(sticky=False)
|
|
140
|
+
d3.graph(adjmat)
|
|
131
141
|
d3.show()
|
|
132
142
|
|
|
133
143
|
# Can also be overridden per-render:
|
|
134
|
-
d3.show(
|
|
144
|
+
d3.show(sticky=False)
|
|
145
|
+
d3.graph(adjmat)
|
|
135
146
|
d3.show()
|
|
136
147
|
|
|
137
148
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|