hiveplotlib 0.26.2__py3-none-any.whl → 0.27.0__py3-none-any.whl
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.
- hiveplotlib/__init__.py +4 -0
- hiveplotlib/axis.py +4 -1
- hiveplotlib/converters.py +18 -15
- hiveplotlib/datasets/international_trade.py +1 -1
- hiveplotlib/datasets/toy_hive_plots.py +304 -6
- hiveplotlib/datasets/toy_p2cps.py +2 -2
- hiveplotlib/edges.py +48 -21
- hiveplotlib/hiveplot.py +286 -75
- hiveplotlib/hiveplot_matrix.py +1274 -0
- hiveplotlib/node.py +26 -13
- hiveplotlib/p2cp.py +20 -17
- hiveplotlib/utils.py +221 -1
- hiveplotlib/viz/base.py +16 -5
- hiveplotlib/viz/bokeh.py +15 -15
- hiveplotlib/viz/datashader.py +106 -49
- hiveplotlib/viz/hiveplot_matrix.py +657 -0
- hiveplotlib/viz/holoviews.py +12 -12
- hiveplotlib/viz/matplotlib.py +8 -7
- hiveplotlib/viz/plotly.py +37 -22
- {hiveplotlib-0.26.2.dist-info → hiveplotlib-0.27.0.dist-info}/METADATA +13 -6
- hiveplotlib-0.27.0.dist-info/RECORD +32 -0
- {hiveplotlib-0.26.2.dist-info → hiveplotlib-0.27.0.dist-info}/WHEEL +1 -1
- hiveplotlib-0.26.2.dist-info/RECORD +0 -30
- {hiveplotlib-0.26.2.dist-info → hiveplotlib-0.27.0.dist-info}/licenses/LICENSE +0 -0
- {hiveplotlib-0.26.2.dist-info → hiveplotlib-0.27.0.dist-info}/top_level.txt +0 -0
hiveplotlib/__init__.py
CHANGED
|
@@ -12,5 +12,9 @@ from hiveplotlib.hiveplot import ( # noqa: F401
|
|
|
12
12
|
HivePlot,
|
|
13
13
|
hive_plot_n_axes,
|
|
14
14
|
)
|
|
15
|
+
from hiveplotlib.hiveplot_matrix import ( # noqa: F401
|
|
16
|
+
HPM_SUPPORTED_VIZ_BACKENDS,
|
|
17
|
+
HivePlotMatrix,
|
|
18
|
+
)
|
|
15
19
|
from hiveplotlib.node import Node, NodeCollection # noqa: F401
|
|
16
20
|
from hiveplotlib.p2cp import P2CP, p2cp_n_axes # noqa: F401
|
hiveplotlib/axis.py
CHANGED
|
@@ -6,6 +6,7 @@ Definition of ``Axis`` instance and helper static methods for generating and wor
|
|
|
6
6
|
|
|
7
7
|
from typing import Hashable, Optional
|
|
8
8
|
|
|
9
|
+
import numpy as np
|
|
9
10
|
import pandas as pd
|
|
10
11
|
|
|
11
12
|
from hiveplotlib.utils import polar2cartesian
|
|
@@ -93,7 +94,9 @@ class Axis:
|
|
|
93
94
|
self.sorting_variable = None
|
|
94
95
|
|
|
95
96
|
# hold all the cartesian coordinates, polar rho, corresponding labels, and node metadata in a pandas dataframe
|
|
96
|
-
self.node_placements = pd.DataFrame(
|
|
97
|
+
self.node_placements = pd.DataFrame(
|
|
98
|
+
columns=np.array(["x", "y", "unique_id", "rho"])
|
|
99
|
+
)
|
|
97
100
|
|
|
98
101
|
# hold the current vmin and vmax used to place nodes on an axis in a ``HivePlot`` instance.
|
|
99
102
|
self.vmin = None
|
hiveplotlib/converters.py
CHANGED
|
@@ -4,22 +4,22 @@
|
|
|
4
4
|
Converters from various data structures to ``hiveplotlib``-ready structures.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import Tuple
|
|
8
|
-
|
|
9
7
|
import pandas as pd
|
|
10
8
|
|
|
11
|
-
from hiveplotlib import Edges,
|
|
12
|
-
from hiveplotlib.node import node_collection_from_node_list
|
|
9
|
+
from hiveplotlib import Edges, NodeCollection
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
try:
|
|
12
|
+
import networkx as nx
|
|
13
|
+
except ImportError as ie: # pragma: no cover
|
|
14
|
+
msg = "`networkx` dependencies not installed, but can be installed by running `pip install hiveplotlib[networkx]`"
|
|
15
|
+
raise ImportError(msg) from ie
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def networkx_to_nodes_edges(
|
|
19
|
-
graph:
|
|
19
|
+
graph: nx.Graph,
|
|
20
20
|
unique_id_name: str = "unique_id",
|
|
21
21
|
check_uniqueness: bool = True,
|
|
22
|
-
) ->
|
|
22
|
+
) -> tuple[NodeCollection, Edges]:
|
|
23
23
|
"""
|
|
24
24
|
Take a ``networkx`` graph and return ``hiveplotlib``-friendly data structures.
|
|
25
25
|
|
|
@@ -31,11 +31,14 @@ def networkx_to_nodes_edges(
|
|
|
31
31
|
:param check_uniqueness: whether or not to check that provided ``Node`` instances have unique IDs.
|
|
32
32
|
:return: ``list`` of ``Node`` instances, ``(n, 2)`` ``np.ndarray`` of edges.
|
|
33
33
|
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
node_data = []
|
|
35
|
+
for node, attributes in graph.nodes(data=True):
|
|
36
|
+
node_data.append({unique_id_name: node, **attributes})
|
|
37
|
+
|
|
38
|
+
node_df = pd.DataFrame(node_data)
|
|
39
|
+
edge_df = nx.to_pandas_edgelist(graph, source="from", target="to")
|
|
40
|
+
return NodeCollection(
|
|
41
|
+
data=node_df,
|
|
42
|
+
unique_id_column=unique_id_name,
|
|
40
43
|
check_uniqueness=check_uniqueness,
|
|
41
|
-
), Edges(data=
|
|
44
|
+
), Edges(data=edge_df)
|
|
@@ -52,7 +52,7 @@ def international_trade_data(
|
|
|
52
52
|
csv_years = [i.split("_")[2] for i in csv_files]
|
|
53
53
|
csv_hs92 = [i.split("_")[-1] for i in csv_files]
|
|
54
54
|
hiveplotlib_supported_values = pd.DataFrame(
|
|
55
|
-
np.c_[csv_years, csv_hs92], columns=["Year", "Trade Code"]
|
|
55
|
+
np.c_[csv_years, csv_hs92], columns=np.array(["Year", "Trade Code"])
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
# check that our implied data and metadata files exist
|
|
@@ -13,6 +13,7 @@ from hiveplotlib import (
|
|
|
13
13
|
NodeCollection,
|
|
14
14
|
hive_plot_n_axes,
|
|
15
15
|
)
|
|
16
|
+
from hiveplotlib.datasets.international_trade import international_trade_data
|
|
16
17
|
from hiveplotlib.edges import Edges
|
|
17
18
|
from hiveplotlib.hiveplot import HivePlot
|
|
18
19
|
from hiveplotlib.node import dataframe_to_node_list
|
|
@@ -52,7 +53,7 @@ def example_base_hive_plot(
|
|
|
52
53
|
rng.uniform(low=10, high=20, size=num_nodes),
|
|
53
54
|
rng.uniform(low=20, high=30, size=num_nodes),
|
|
54
55
|
],
|
|
55
|
-
columns=["low", "med", "high"],
|
|
56
|
+
columns=np.array(["low", "med", "high"]),
|
|
56
57
|
)
|
|
57
58
|
|
|
58
59
|
# make indices a column
|
|
@@ -106,7 +107,7 @@ def example_node_data(num_nodes: int = 100, seed: int = 0) -> pd.DataFrame:
|
|
|
106
107
|
rng.uniform(low=10, high=19.99, size=num_nodes),
|
|
107
108
|
rng.uniform(low=20, high=29.99, size=num_nodes),
|
|
108
109
|
],
|
|
109
|
-
columns=["low", "med", "high"],
|
|
110
|
+
columns=np.array(["low", "med", "high"]),
|
|
110
111
|
)
|
|
111
112
|
# make indices a column
|
|
112
113
|
return data.reset_index(names="unique_id")
|
|
@@ -162,7 +163,9 @@ def example_edge_data(
|
|
|
162
163
|
node_ids = nodes.data[nodes.unique_id_column].to_numpy()
|
|
163
164
|
rng = np.random.default_rng(seed)
|
|
164
165
|
edge_array = rng.choice(node_ids, size=num_edges * 2).reshape(-1, 2)
|
|
165
|
-
return pd.DataFrame(
|
|
166
|
+
return pd.DataFrame(
|
|
167
|
+
edge_array, columns=np.array([from_column_name, to_column_name])
|
|
168
|
+
)
|
|
166
169
|
|
|
167
170
|
|
|
168
171
|
def example_edges(
|
|
@@ -199,6 +202,85 @@ def example_edges(
|
|
|
199
202
|
)
|
|
200
203
|
|
|
201
204
|
|
|
205
|
+
def example_trade_nodes_and_edges() -> Tuple[NodeCollection, Edges]:
|
|
206
|
+
"""
|
|
207
|
+
Load the international trade nodes and edges.
|
|
208
|
+
|
|
209
|
+
Used in the
|
|
210
|
+
:doc:`Hive Plots with More Than 3 Groups </notebooks/hive_plots_more_than_three_groups>`
|
|
211
|
+
notebook.
|
|
212
|
+
|
|
213
|
+
Uses :py:func:`hiveplotlib.datasets.international_trade.international_trade_data` to load 2019 trade data for HS92
|
|
214
|
+
trade group 8112 ("beryllium, chromium, germanium, vanadium, gallium, hafnium, indium, niobium
|
|
215
|
+
(columbium), rhenium and thallium, articles thereof, and waste or scrap") from the
|
|
216
|
+
`Harvard Growth Lab <https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi%3A10.7910%2FDVN%2FT4CHWJ>`_.
|
|
217
|
+
|
|
218
|
+
**Node columns** (152 nodes):
|
|
219
|
+
|
|
220
|
+
- ``"country"``: ISO 3166-1 alpha-3 country code (unique ID).
|
|
221
|
+
- ``"continent"``: continent of the country (Africa, Asia, Europe, North America, Oceania, or South America).
|
|
222
|
+
- ``"export_value"``: total USD exported by that country within trade group 8112 in 2019.
|
|
223
|
+
Countries with no exports are assigned 0.
|
|
224
|
+
|
|
225
|
+
**Edge columns** (1158 edges):
|
|
226
|
+
|
|
227
|
+
Each edge represents non-zero trade from an origin country to a destination country.
|
|
228
|
+
|
|
229
|
+
- ``"origin_country"``: ISO 3166-1 alpha-3 code of the exporting country (from column).
|
|
230
|
+
- ``"destination_country"``: ISO 3166-1 alpha-3 code of the importing country (to column).
|
|
231
|
+
- ``"export_value"``: USD exported from the origin to the destination.
|
|
232
|
+
- ``"origin_continent"``: continent of the origin country.
|
|
233
|
+
- ``"destination_continent"``: continent of the destination country.
|
|
234
|
+
|
|
235
|
+
:return: tuple of ``(NodeCollection, Edges)`` representing the trade network.
|
|
236
|
+
"""
|
|
237
|
+
data, _ = international_trade_data(year=2019, hs92_code=8112)
|
|
238
|
+
|
|
239
|
+
export_totals_per_country = (
|
|
240
|
+
data.loc[:, ["origin_country", "export_value"]]
|
|
241
|
+
.groupby("origin_country")
|
|
242
|
+
.sum()
|
|
243
|
+
.reset_index()
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
node_data = pd.concat(
|
|
247
|
+
[
|
|
248
|
+
data.loc[:, ["origin_country", "origin_continent"]].rename(
|
|
249
|
+
columns={
|
|
250
|
+
"origin_country": "country",
|
|
251
|
+
"origin_continent": "continent",
|
|
252
|
+
}
|
|
253
|
+
),
|
|
254
|
+
data.loc[:, ["destination_country", "destination_continent"]].rename(
|
|
255
|
+
columns={
|
|
256
|
+
"destination_country": "country",
|
|
257
|
+
"destination_continent": "continent",
|
|
258
|
+
}
|
|
259
|
+
),
|
|
260
|
+
]
|
|
261
|
+
).drop_duplicates()
|
|
262
|
+
|
|
263
|
+
node_data = node_data.merge(
|
|
264
|
+
export_totals_per_country,
|
|
265
|
+
how="left",
|
|
266
|
+
left_on="country",
|
|
267
|
+
right_on="origin_country",
|
|
268
|
+
).drop(columns=["origin_country"])
|
|
269
|
+
|
|
270
|
+
# fill in countries with no exports as 0, not NaN
|
|
271
|
+
node_data.export_value = node_data.export_value.fillna(0)
|
|
272
|
+
|
|
273
|
+
nodes = NodeCollection(node_data, unique_id_column="country")
|
|
274
|
+
|
|
275
|
+
edges = Edges(
|
|
276
|
+
data,
|
|
277
|
+
from_column_name="origin_country",
|
|
278
|
+
to_column_name="destination_country",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
return nodes, edges
|
|
282
|
+
|
|
283
|
+
|
|
202
284
|
def example_nodes_and_edges(
|
|
203
285
|
num_nodes: int = 100,
|
|
204
286
|
num_edges: int = 200,
|
|
@@ -238,8 +320,8 @@ def example_hive_plot(
|
|
|
238
320
|
num_nodes: int = 100,
|
|
239
321
|
num_edges: int = 100,
|
|
240
322
|
partition_data_column: Literal["low", "med", "high"] = "low",
|
|
241
|
-
labels: Optional[List[Hashable]] =
|
|
242
|
-
cutoffs:
|
|
323
|
+
labels: Optional[List[Hashable]] = ["A", "B", "C"], # noqa: B006
|
|
324
|
+
cutoffs: Union[List[Union[int, float]], int] = 3,
|
|
243
325
|
partition_variable_name: Optional[Hashable] = None,
|
|
244
326
|
sorting_variables: Union[Hashable, Dict[Hashable, Hashable]] = "low",
|
|
245
327
|
seed: int = 0,
|
|
@@ -310,7 +392,7 @@ def example_hive_plot(
|
|
|
310
392
|
node_collection.data,
|
|
311
393
|
left_on="to",
|
|
312
394
|
right_on=node_unique_id_column,
|
|
313
|
-
suffixes=
|
|
395
|
+
suffixes=(None, "_to"),
|
|
314
396
|
)
|
|
315
397
|
)
|
|
316
398
|
for col in ["low", "med", "high"]:
|
|
@@ -329,3 +411,219 @@ def example_hive_plot(
|
|
|
329
411
|
sorting_variables=sorting_variables,
|
|
330
412
|
**hive_plot_kwargs,
|
|
331
413
|
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def example_hpm_nodes_and_edges(
|
|
417
|
+
num_groups: int = 3,
|
|
418
|
+
nodes_per_group: int = 10,
|
|
419
|
+
edge_tag_counts: Optional[Dict[str, int]] = None,
|
|
420
|
+
edge_structure: Optional[Dict[str, str]] = None,
|
|
421
|
+
seed: int = 42,
|
|
422
|
+
) -> Tuple["NodeCollection", "Edges"]:
|
|
423
|
+
"""
|
|
424
|
+
Generate a shared toy dataset that works well with ``HivePlotMatrix`` examples.
|
|
425
|
+
|
|
426
|
+
Returns a ``NodeCollection`` of ``num_groups * nodes_per_group`` nodes across
|
|
427
|
+
``num_groups`` groups (A, B, C, ...) with structured numeric columns, and a
|
|
428
|
+
multi-tag ``Edges`` object.
|
|
429
|
+
|
|
430
|
+
**Node columns**:
|
|
431
|
+
|
|
432
|
+
- ``"unique_id"``: integers 0 to (num_groups * nodes_per_group - 1).
|
|
433
|
+
- ``"group"``: group label, assigned in consecutive blocks. With ``num_groups=3``,
|
|
434
|
+
nodes 0-9 are ``"A"``, 10-19 are ``"B"``, 20-29 are ``"C"``.
|
|
435
|
+
- ``"value1"``: *correlated with group* - each group draws from a distinct sub-interval
|
|
436
|
+
of [0, 10] in ascending order. Sorting by ``value1`` places groups at distinct,
|
|
437
|
+
separated positions along each axis if setting axes ``vmin`` and ``vmax`` values to the same fixed range.
|
|
438
|
+
- ``"value2"``: *uncorrelated noise* - all nodes draw from Uniform(0, 10). Sorting
|
|
439
|
+
by ``value2`` produces little visible group separation.
|
|
440
|
+
- ``"value3"``: *inversely correlated with group* - the mirror image of ``value1``.
|
|
441
|
+
The first group draws from the highest sub-interval, the last from the lowest.
|
|
442
|
+
|
|
443
|
+
**Default edge tags** (decreasing density):
|
|
444
|
+
|
|
445
|
+
- ``"official"``: 40 edges.
|
|
446
|
+
- ``"social"``: 30 edges.
|
|
447
|
+
- ``"informal"``: 20 edges.
|
|
448
|
+
|
|
449
|
+
:param num_groups: number of groups to generate (A, B, C, ...). Default 3.
|
|
450
|
+
:param nodes_per_group: number of nodes in each group. Default 10.
|
|
451
|
+
:param edge_tag_counts: dictionary mapping edge tag names to edge counts. Defaults to
|
|
452
|
+
``{"official": 40, "social": 30, "informal": 20}``.
|
|
453
|
+
:param edge_structure: dictionary mapping edge tag names to structure types.
|
|
454
|
+
Supported types:
|
|
455
|
+
|
|
456
|
+
- ``"random"``: edges connect any two nodes uniformly at random (default).
|
|
457
|
+
- ``"intragroup"``: edges connect nodes within the same group only.
|
|
458
|
+
- ``"intergroup"``: edges connect nodes in different groups only.
|
|
459
|
+
|
|
460
|
+
Tags not listed in this dictionary default to ``"random"``.
|
|
461
|
+
:param seed: base random seed. Seeds ``seed``, ``seed+1``, ... are used for
|
|
462
|
+
successive edge tags respectively.
|
|
463
|
+
:return: tuple of ``(NodeCollection, Edges)``.
|
|
464
|
+
"""
|
|
465
|
+
_valid_structures = {"random", "intragroup", "intergroup"}
|
|
466
|
+
if edge_tag_counts is None:
|
|
467
|
+
edge_tag_counts = {"official": 40, "social": 30, "informal": 20}
|
|
468
|
+
if edge_structure is None:
|
|
469
|
+
edge_structure = {}
|
|
470
|
+
for tag, structure in edge_structure.items():
|
|
471
|
+
if structure not in _valid_structures:
|
|
472
|
+
msg = (
|
|
473
|
+
f"edge_structure[{tag!r}] = {structure!r} is not valid. "
|
|
474
|
+
f"Must be one of {_valid_structures}."
|
|
475
|
+
)
|
|
476
|
+
raise ValueError(msg)
|
|
477
|
+
num_nodes = num_groups * nodes_per_group
|
|
478
|
+
rng = np.random.default_rng(seed)
|
|
479
|
+
|
|
480
|
+
# Deterministic block group assignment
|
|
481
|
+
group_labels = [chr(ord("A") + i) for i in range(num_groups)]
|
|
482
|
+
groups: list = []
|
|
483
|
+
for label in group_labels:
|
|
484
|
+
groups.extend([label] * nodes_per_group)
|
|
485
|
+
|
|
486
|
+
# Divide [0, 10] into num_groups equal sub-intervals
|
|
487
|
+
breakpoints = np.linspace(0, 10, num_groups + 1)
|
|
488
|
+
|
|
489
|
+
# value1: correlated - group i draws from [breakpoints[i], breakpoints[i+1]]
|
|
490
|
+
value1_parts = []
|
|
491
|
+
for i in range(num_groups):
|
|
492
|
+
lo, hi = breakpoints[i], breakpoints[i + 1]
|
|
493
|
+
value1_parts.append(rng.uniform(lo, hi, size=nodes_per_group))
|
|
494
|
+
value1 = np.concatenate(value1_parts)
|
|
495
|
+
|
|
496
|
+
value2 = rng.uniform(0, 10, size=num_nodes)
|
|
497
|
+
|
|
498
|
+
# value3: inversely correlated - group i draws from the reversed sub-interval
|
|
499
|
+
value3_parts = []
|
|
500
|
+
for i in range(num_groups):
|
|
501
|
+
j = num_groups - 1 - i
|
|
502
|
+
lo, hi = breakpoints[j], breakpoints[j + 1]
|
|
503
|
+
value3_parts.append(rng.uniform(lo, hi, size=nodes_per_group))
|
|
504
|
+
value3 = np.concatenate(value3_parts)
|
|
505
|
+
|
|
506
|
+
data = pd.DataFrame(
|
|
507
|
+
{
|
|
508
|
+
"unique_id": np.arange(num_nodes),
|
|
509
|
+
"group": groups,
|
|
510
|
+
"value1": value1,
|
|
511
|
+
"value2": value2,
|
|
512
|
+
"value3": value3,
|
|
513
|
+
}
|
|
514
|
+
)
|
|
515
|
+
nodes = NodeCollection(data=data, unique_id_column="unique_id")
|
|
516
|
+
node_ids = data["unique_id"].to_numpy()
|
|
517
|
+
groups_array = np.array(groups)
|
|
518
|
+
|
|
519
|
+
def _make_edges(n_edges: int, edge_seed: int, structure: str) -> pd.DataFrame:
|
|
520
|
+
edge_rng = np.random.default_rng(edge_seed)
|
|
521
|
+
if structure == "random":
|
|
522
|
+
return pd.DataFrame(
|
|
523
|
+
edge_rng.choice(node_ids, size=n_edges * 2).reshape(-1, 2),
|
|
524
|
+
columns=np.array(["from", "to"]),
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
from_nodes = edge_rng.choice(node_ids, size=n_edges)
|
|
528
|
+
to_nodes = np.empty(n_edges, dtype=node_ids.dtype)
|
|
529
|
+
for j in range(n_edges):
|
|
530
|
+
src_group = groups_array[from_nodes[j]]
|
|
531
|
+
if structure == "intragroup":
|
|
532
|
+
candidates = node_ids[groups_array == src_group]
|
|
533
|
+
else: # intergroup
|
|
534
|
+
candidates = node_ids[groups_array != src_group]
|
|
535
|
+
to_nodes[j] = edge_rng.choice(candidates)
|
|
536
|
+
return pd.DataFrame(
|
|
537
|
+
{"from": from_nodes, "to": to_nodes},
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
edges = Edges(
|
|
541
|
+
data={
|
|
542
|
+
tag: _make_edges(count, seed + i, edge_structure.get(tag, "random"))
|
|
543
|
+
for i, (tag, count) in enumerate(edge_tag_counts.items())
|
|
544
|
+
}
|
|
545
|
+
)
|
|
546
|
+
return nodes, edges
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def example_minimal_hive_plot() -> HivePlot:
|
|
550
|
+
"""
|
|
551
|
+
Generate a minimal ``HivePlot`` instance with edge color styling.
|
|
552
|
+
|
|
553
|
+
Uses :py:func:`example_hive_plot` with 6 nodes and 6 edges (seed 42) and adds
|
|
554
|
+
a uniform edge color of ``"#006BA4"`` to all axis pairs that have edges.
|
|
555
|
+
|
|
556
|
+
:return: ``HivePlot`` instance.
|
|
557
|
+
"""
|
|
558
|
+
return example_hive_plot(
|
|
559
|
+
num_nodes=6,
|
|
560
|
+
num_edges=6,
|
|
561
|
+
seed=42,
|
|
562
|
+
all_edge_kwargs={"color": "#006BA4"},
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def example_multi_tag_hive_plot() -> HivePlot:
|
|
567
|
+
"""
|
|
568
|
+
Generate a ``HivePlot`` instance with two edge tags and ``repeat_axes=True``.
|
|
569
|
+
|
|
570
|
+
Creates 12 nodes (seed 10) with two independent sets of 15 edges (seeds 10 and 20)
|
|
571
|
+
stored under tags ``"tag_red"`` and ``"tag_blue"``. Edge colors are ``"#FF0000"``
|
|
572
|
+
and ``"#0000FF"`` respectively.
|
|
573
|
+
|
|
574
|
+
:return: ``HivePlot`` instance with two edge tags.
|
|
575
|
+
"""
|
|
576
|
+
nodes = example_node_collection(num_nodes=12, seed=10)
|
|
577
|
+
edge_data_1 = example_edge_data(nodes=nodes, num_edges=15, seed=10)
|
|
578
|
+
edge_data_2 = example_edge_data(nodes=nodes, num_edges=15, seed=20)
|
|
579
|
+
multi_tag_edges = Edges(data={"tag_red": edge_data_1, "tag_blue": edge_data_2})
|
|
580
|
+
partition_variable = nodes.create_partition_variable(
|
|
581
|
+
data_column="low", labels=["A", "B", "C"]
|
|
582
|
+
)
|
|
583
|
+
hp = HivePlot(
|
|
584
|
+
nodes=nodes,
|
|
585
|
+
edges=multi_tag_edges,
|
|
586
|
+
partition_variable=partition_variable,
|
|
587
|
+
sorting_variables="low",
|
|
588
|
+
repeat_axes=True,
|
|
589
|
+
)
|
|
590
|
+
hp.edges.update_edge_viz_kwargs(tag="tag_red", color="#FF0000")
|
|
591
|
+
hp.edges.update_edge_viz_kwargs(tag="tag_blue", color="#0000FF")
|
|
592
|
+
return hp
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def example_full_kwargs_hive_plot() -> HivePlot:
|
|
596
|
+
"""
|
|
597
|
+
Generate a ``HivePlot`` instance with data-dependent edge kwargs, colormaps, and node styling.
|
|
598
|
+
|
|
599
|
+
Uses :py:func:`example_hive_plot` with 9 nodes, 10 edges (seed 99), and
|
|
600
|
+
``repeat_axes=True``. Scaled edge data columns are created for alpha and linewidth,
|
|
601
|
+
and all edge visualization kwargs are set via
|
|
602
|
+
:py:meth:`~hiveplotlib.HivePlot.update_edge_plotting_keyword_arguments` using column
|
|
603
|
+
name references:
|
|
604
|
+
|
|
605
|
+
- **color**: the ``"low"`` edge data column mapped through a ``"cividis"`` colormap.
|
|
606
|
+
- **alpha**: the ``"alpha_scaled"`` column (``low / 10``, yielding values in ~0.3-1.0).
|
|
607
|
+
- **linewidth**: the ``"lw_scaled"`` column (``low / 3``, yielding values in ~1.0-3.3).
|
|
608
|
+
|
|
609
|
+
Node styling is applied via ``update_node_viz_kwargs`` with a ``"viridis"`` colormap mapped to the ``"low"``
|
|
610
|
+
data column.
|
|
611
|
+
|
|
612
|
+
:return: ``HivePlot`` instance with node and edge kwargs.
|
|
613
|
+
"""
|
|
614
|
+
hp = example_hive_plot(num_nodes=9, num_edges=10, seed=99, repeat_axes=True)
|
|
615
|
+
# create scaled edge data columns for alpha (0-1 range) and linewidth
|
|
616
|
+
hp.edges.data["alpha_scaled"] = hp.edges.data["low"] / 10
|
|
617
|
+
hp.edges.data["lw_scaled"] = hp.edges.data["low"] / 3
|
|
618
|
+
# data-dependent edge kwargs: color, alpha, and linewidth all from edge data columns
|
|
619
|
+
hp.update_edge_plotting_keyword_arguments(
|
|
620
|
+
array="low",
|
|
621
|
+
cmap="cividis",
|
|
622
|
+
clim=(0, 10),
|
|
623
|
+
alpha="alpha_scaled",
|
|
624
|
+
linewidth="lw_scaled",
|
|
625
|
+
)
|
|
626
|
+
hp.update_node_viz_kwargs(
|
|
627
|
+
c="low", s=30, alpha=0.8, edgecolor="black", cmap="viridis"
|
|
628
|
+
)
|
|
629
|
+
return hp
|
|
@@ -34,7 +34,7 @@ def four_gaussian_blobs_3d(
|
|
|
34
34
|
corners_to_keep = [0, 1, 2, 4]
|
|
35
35
|
|
|
36
36
|
# name of the features we will create for each set of data generated below
|
|
37
|
-
feature_names = ["X", "Y", "Z", "Label"]
|
|
37
|
+
feature_names = np.array(["X", "Y", "Z", "Label"])
|
|
38
38
|
|
|
39
39
|
# set seed for consistent data
|
|
40
40
|
rng = np.random.default_rng(random_seed)
|
|
@@ -63,7 +63,7 @@ def four_gaussian_blobs_3d(
|
|
|
63
63
|
# make the labels ints
|
|
64
64
|
df.Label = df.Label.astype(int)
|
|
65
65
|
# replace the 4s with 3s so our labels are just range(4)
|
|
66
|
-
df.
|
|
66
|
+
df.loc[df["Label"] == 4, "Label"] = 3
|
|
67
67
|
|
|
68
68
|
return df
|
|
69
69
|
|
hiveplotlib/edges.py
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from copy import deepcopy
|
|
7
|
-
from typing import Hashable, Literal, Optional, Union
|
|
7
|
+
from typing import Hashable, Literal, Optional, Union, cast
|
|
8
8
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
import pandas as pd
|
|
@@ -99,7 +99,9 @@ class Edges:
|
|
|
99
99
|
def __init__(
|
|
100
100
|
self,
|
|
101
101
|
data: Union[
|
|
102
|
-
pd.DataFrame,
|
|
102
|
+
pd.DataFrame,
|
|
103
|
+
np.ndarray,
|
|
104
|
+
dict[Hashable, Union[np.ndarray, pd.DataFrame]],
|
|
103
105
|
],
|
|
104
106
|
from_column_name: Hashable = "from",
|
|
105
107
|
to_column_name: Hashable = "to",
|
|
@@ -114,7 +116,7 @@ class Edges:
|
|
|
114
116
|
|
|
115
117
|
self.edge_viz_kwargs = {} if edge_viz_kwargs is None else edge_viz_kwargs.copy()
|
|
116
118
|
|
|
117
|
-
self._data = self._validate_edge_data(data)
|
|
119
|
+
self._data: dict[Hashable, pd.DataFrame] = self._validate_edge_data(data)
|
|
118
120
|
|
|
119
121
|
# nested dictionary that stores boolean 1d arrays of which edges are relevant to which set of edges plotted
|
|
120
122
|
# edges are plotted in chunks of "from axis", "to axis", and "tag", so this stores 1 boolean array nested in
|
|
@@ -124,7 +126,10 @@ class Edges:
|
|
|
124
126
|
def _validate_edge_data(
|
|
125
127
|
self,
|
|
126
128
|
data: Union[
|
|
127
|
-
pd.DataFrame,
|
|
129
|
+
pd.DataFrame,
|
|
130
|
+
np.ndarray,
|
|
131
|
+
dict[Hashable, Union[np.ndarray, pd.DataFrame]],
|
|
132
|
+
dict[Hashable, pd.DataFrame],
|
|
128
133
|
],
|
|
129
134
|
) -> dict[Hashable, pd.DataFrame]:
|
|
130
135
|
"""
|
|
@@ -134,40 +139,52 @@ class Edges:
|
|
|
134
139
|
of either.
|
|
135
140
|
:return: dictionary of ``pandas.DataFrame`` objects with validated edge data.
|
|
136
141
|
"""
|
|
137
|
-
|
|
142
|
+
# Normalize input to a dictionary without mutating the input parameter to avoid type ambiguity.
|
|
143
|
+
if isinstance(data, dict):
|
|
144
|
+
data_dict = data
|
|
145
|
+
else:
|
|
138
146
|
if isinstance(data, np.ndarray):
|
|
139
147
|
assert data.shape[1] == 2, (
|
|
140
148
|
"Any `numpy.ndarray` input in `data` must be shape (n, 2) "
|
|
141
149
|
f"but found input with {data.shape[1]} columns."
|
|
142
150
|
)
|
|
143
|
-
|
|
144
|
-
data, columns=[self.from_column_name, self.to_column_name]
|
|
151
|
+
df_single = pd.DataFrame(
|
|
152
|
+
data, columns=np.array([self.from_column_name, self.to_column_name])
|
|
145
153
|
)
|
|
146
|
-
|
|
154
|
+
else:
|
|
155
|
+
df_single = cast("pd.DataFrame", data)
|
|
156
|
+
# default to tag 0 for single dataframe provided
|
|
157
|
+
data_dict: dict[Hashable, pd.DataFrame] = {0: df_single}
|
|
147
158
|
if 0 not in self.edge_viz_kwargs:
|
|
148
159
|
self.edge_viz_kwargs[0] = {}
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
|
|
161
|
+
# Build the validated output dictionary of DataFrames.
|
|
162
|
+
out: dict[Hashable, pd.DataFrame] = {}
|
|
163
|
+
for kw, val in data_dict.items():
|
|
164
|
+
assert isinstance(val, (pd.DataFrame, np.ndarray)), (
|
|
151
165
|
f"Provided data for tag {kw} must be a `pandas.DataFrame` or `numpy.ndarray`."
|
|
152
166
|
)
|
|
153
|
-
if isinstance(
|
|
154
|
-
|
|
167
|
+
if isinstance(val, pd.DataFrame):
|
|
168
|
+
df_kw = val
|
|
169
|
+
assert self.from_column_name in df_kw.columns, (
|
|
155
170
|
f"Provided data for tag {kw} must have a column named '{self.from_column_name}'"
|
|
156
171
|
)
|
|
157
|
-
assert self.to_column_name in
|
|
172
|
+
assert self.to_column_name in df_kw.columns, (
|
|
158
173
|
f"Provided data for tag {kw} must have a column named '{self.to_column_name}'"
|
|
159
174
|
)
|
|
160
|
-
|
|
161
|
-
|
|
175
|
+
out[kw] = df_kw
|
|
176
|
+
else:
|
|
177
|
+
assert val.shape[1] == 2, (
|
|
162
178
|
"Any `numpy.ndarray` input in `data` must be shape (n, 2) "
|
|
163
|
-
f"but found input with {
|
|
179
|
+
f"but found input with {val.shape[1]} columns."
|
|
164
180
|
)
|
|
165
|
-
|
|
166
|
-
|
|
181
|
+
out[kw] = pd.DataFrame(
|
|
182
|
+
val,
|
|
183
|
+
columns=np.array([self.from_column_name, self.to_column_name]),
|
|
167
184
|
)
|
|
168
185
|
if kw not in self.edge_viz_kwargs:
|
|
169
186
|
self.edge_viz_kwargs[kw] = {}
|
|
170
|
-
return
|
|
187
|
+
return out
|
|
171
188
|
|
|
172
189
|
@property
|
|
173
190
|
def data(self) -> Union[pd.DataFrame, dict[Hashable, pd.DataFrame]]:
|
|
@@ -182,9 +199,19 @@ class Edges:
|
|
|
182
199
|
return next(iter(self._data.values()))
|
|
183
200
|
return self._data
|
|
184
201
|
|
|
202
|
+
@property
|
|
203
|
+
def tags(self) -> list[Hashable]:
|
|
204
|
+
"""
|
|
205
|
+
Return the list of all edge tags.
|
|
206
|
+
|
|
207
|
+
:return: list of tag keys for this ``Edges`` instance.
|
|
208
|
+
"""
|
|
209
|
+
return list(self._data.keys())
|
|
210
|
+
|
|
185
211
|
def add_edges(
|
|
186
212
|
self,
|
|
187
|
-
data: dict[Hashable, Union[np.ndarray, pd.DataFrame]]
|
|
213
|
+
data: dict[Hashable, Union[np.ndarray, pd.DataFrame]]
|
|
214
|
+
| dict[Hashable, pd.DataFrame],
|
|
188
215
|
) -> None:
|
|
189
216
|
"""
|
|
190
217
|
Add edges to the ``Edges`` instance.
|
|
@@ -217,7 +244,7 @@ class Edges:
|
|
|
217
244
|
:return: copy of the ``Edges`` instance.
|
|
218
245
|
"""
|
|
219
246
|
return Edges(
|
|
220
|
-
data=self._data
|
|
247
|
+
data=deepcopy(self._data), # ty:ignore[invalid-argument-type]
|
|
221
248
|
from_column_name=self.from_column_name,
|
|
222
249
|
to_column_name=self.to_column_name,
|
|
223
250
|
edge_viz_kwargs=deepcopy(self.edge_viz_kwargs),
|