Skip to content

Commit 0c69d3b

Browse files
authored
Reformat feature.name and reintroduce averaged data and plot() for CompoundFeatures (#568)
Reformat `feature.name` and reintroduce averaged `data` and `plot()` for `CompoundFeature`s (#568)
2 parents 586c694 + 14e8ce8 commit 0c69d3b

23 files changed

+498
-109
lines changed

e2e/features/activity_timeseries/test_activity_timeseries.py

-19
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,11 @@
44
import sys
55
from siibra.features.tabular.regional_timeseries_activity import RegionalBOLD
66
from siibra.features.feature import CompoundFeature
7-
from e2e.util import check_duplicate
87

98
skip_on_windows = pytest.mark.skipif(sys.platform == "win32", reason="Fails due to memory limitation issues on Windows on Github actions. (Passes on local machines.)")
109

1110
jba_29 = siibra.parcellations["julich 2.9"]
1211

13-
14-
all_bold_instances = [
15-
f
16-
for Cls in siibra.features.feature.Feature._SUBCLASSES[RegionalBOLD]
17-
for f in Cls._get_instances()
18-
]
19-
20-
21-
def test_id_unique():
22-
duplicates = check_duplicate([f.id for f in all_bold_instances])
23-
assert len(duplicates) == 0
24-
25-
26-
def test_feature_unique():
27-
duplicates = check_duplicate([f for f in all_bold_instances])
28-
assert len(duplicates) == 0
29-
30-
3112
bold_cfs = [
3213
*siibra.features.get(jba_29, "bold"),
3314
*siibra.features.get(siibra.parcellations["julich 3"], "bold")

e2e/features/connectivity/test_connectivity.py

-11
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from typing import List
44
from siibra.features.feature import CompoundFeature
55
from siibra.features.connectivity.regional_connectivity import RegionalConnectivity
6-
from e2e.util import check_duplicate
76
from zipfile import ZipFile
87
import os
98

@@ -16,16 +15,6 @@
1615
compound_conns = siibra.features.get(siibra.parcellations['julich 3'], RegionalConnectivity)
1716

1817

19-
def test_id_unique():
20-
duplicates = check_duplicate([f.id for f in all_conn_instances])
21-
assert len(duplicates) == 0
22-
23-
24-
def test_feature_unique():
25-
duplicates = check_duplicate([f for f in all_conn_instances])
26-
assert len(duplicates) == 0
27-
28-
2918
@pytest.mark.parametrize("cf", compound_conns)
3019
def test_connectivity_get_data(cf: CompoundFeature):
3120
assert isinstance(cf, CompoundFeature)

e2e/features/test_get.py

+29-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22
import siibra
3+
from e2e.util import check_duplicate
34

45

56
# We get all registered subclasses of Feature
@@ -11,18 +12,36 @@ def test_get_instances(Cls: siibra.features.Feature):
1112
assert isinstance(instances, list)
1213

1314

15+
@pytest.mark.parametrize(
16+
"Cls", [Cls for Cls in siibra.features.Feature._SUBCLASSES[siibra.features.Feature]]
17+
)
18+
def test_id_unique(Cls: siibra.features.Feature):
19+
instances = Cls._get_instances()
20+
duplicates = check_duplicate([f.id for f in instances])
21+
assert len(duplicates) == 0
22+
23+
24+
@pytest.mark.parametrize(
25+
"Cls", [Cls for Cls in siibra.features.Feature._SUBCLASSES[siibra.features.Feature]]
26+
)
27+
def test_feature_unique(Cls: siibra.features.Feature):
28+
instances = Cls._get_instances()
29+
duplicates = check_duplicate([f for f in instances])
30+
assert len(duplicates) == 0
31+
32+
1433
selected_ids = [
1534
"lq0::EbrainsDataFeature::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::r:Area hOc1 (V1, 17, CalcS) left::https://nexus.humanbrainproject.org/v0/data/minds/core/dataset/v1.0.0/3ff328fa-f48f-474b-bd81-b5ee7ca230b6",
16-
"cf0::BigBrainIntensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::r:Area hOc1 (V1, 17, CalcS) left::nodsid::43ade4d58c0909df996183256e45070d", # CompoundFeature of 1579 BigBrainIntensityProfile features grouped by (Modified silver staining modality) anchored at Area hOc1 (V1, 17, CalcS) left with Set of 1579 points in the Bounding box from (-31.80,-68.85,-12.52) mm to (5.09,-29.20,12.00)mm in BigBrain microscopic template (histology) space
17-
"lq0::BigBrainIntensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::r:Area hOc1 (V1, 17, CalcS) left::0ba613a8aa7eb6a888c88485b8cd715d", # BigBrainIntensityProfile (Modified silver staining) anchored at Area hOc1 (V1, 17, CalcS) left with Point in BigBrain microscopic template (histology) [-5.872700214385986,-55.385398864746094,-1.3151400089263916]
18-
"cf0::CellDensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::r:Area hOc1 (V1, 17, CalcS) left::f2cd6b97-e42d-41af-a01b-5caf2d692e28::43d02182a9133fb030e4071eea539990", # CompoundFeature of 10 CellDensityProfile features grouped by (Segmented cell body density modality) anchored at Area hOc1 (V1, 17, CalcS) with Set of 10 points in the Bounding box from (-3.95,-65.80,-0.44) mm to (20.20,-42.70,9.71)mm in BigBrain microscopic template (histology) space
19-
"f2cd6b97-e42d-41af-a01b-5caf2d692e28--ccc56085205beadcd4e911049e726c43", # CellDensityProfile (Segmented cell body density) anchored at Area hOc1 (V1, 17, CalcS) with Point in BigBrain microscopic template (histology) [20.199174880981445,-64.5999984741211,-0.44111010432243347]
20-
"cf0::ReceptorDensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::r:Area hOc1 (V1, 17, CalcS) left::e715e1f7-2079-45c4-a67f-f76b102acfce::1291b163f73216f756ea258f6ab2efb1", # CompoundFeature of 16 ReceptorDensityProfile features grouped by (Receptor density modality) anchored at Area hOc1 (V1, 17, CalcS)
21-
"e715e1f7-2079-45c4-a67f-f76b102acfce--02200d55e4d91084e3d0014bfb9052f4", # ReceptorDensityProfile (Receptor density) anchored at Area hOc1 (V1, 17, CalcS) for alpha4beta2
22-
"cf0::StreamlineCounts::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::f16e449d-86e1-408b-9487-aa9d72e39901::f09a0f43742ce628203a81029644a2c0", # CompoundFeature of 200 StreamlineCounts features grouped by (StreamlineCounts modality, HCP cohort) anchored at Julich-Brain Cytoarchitectonic Atlas (v2.9)
23-
"f16e449d-86e1-408b-9487-aa9d72e39901--283aaf98c0a6bfd2a272a6ef7ac81dd8", # StreamlineCounts (StreamlineCounts) anchored at Julich-Brain Cytoarchitectonic Atlas (v2.9) with cohort HCP - 025
24-
"3f179784-194d-4795-9d8d-301b524ca00a--713b6b5ddc0136acb757863a8138f85e--9c08356ec0454773885ded630e49b5d3", # FunctionalConnectivity (FunctionalConnectivity) anchored at Julich-Brain Cytoarchitectonic Atlas (v2.9) with cohort 1000BRAINS - 0108_1, Resting state (RestEmpCorrFC) paradigm
25-
"cf0::FunctionalConnectivity::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::3f179784-194d-4795-9d8d-301b524ca00a::0cc40894189c89488637f35554f88da5", # CompoundFeature of 349 FunctionalConnectivity features grouped by (FunctionalConnectivity modality, 1000BRAINS cohort, Resting state (RestEmpCorrFC) paradigm) anchored at Julich-Brain Cytoarchitectonic Atlas (v2.9)
35+
"cf0::BigBrainIntensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::r:Area hOc1 (V1, 17, CalcS)::nodsid::94768ccf7d23b640453fb56b4562c2d2", # 2279 BigBrain Intensity Profile features
36+
"lq0::BigBrainIntensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::r:Area hOc1 (V1, 17, CalcS)::fe933acdb8dea6803c76251e004b9b1f", # BigBrain Intensity Profile: (-3.0491600036621094, -64.58589935302734, 1.1756900548934937)
37+
"cf0::CellDensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::r:Area hOc1 (V1, 17, CalcS)::f2cd6b97-e42d-41af-a01b-5caf2d692e28::599f219267d5bdc3c5c04ddf31f36748", # 10 Cell Density Profile features
38+
"f2cd6b97-e42d-41af-a01b-5caf2d692e28--5fc6ebfcbdf43c1c9fb36263eda160d2", # Cell Density Profile: (19.09100914001465, -64.9000015258789, -0.36307409405708313)
39+
"cf0::ReceptorDensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::r:Area hOc1 (V1, 17, CalcS)::e715e1f7-2079-45c4-a67f-f76b102acfce::48ce018be081dafb160287031fbe08c3", # 16 Receptor Density Profile features
40+
"e715e1f7-2079-45c4-a67f-f76b102acfce--e7b46dcf4fea599385b5653ab78e9784", # Receptor Density Profile: alpha4beta2
41+
"cf0::StreamlineCounts::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::f16e449d-86e1-408b-9487-aa9d72e39901::295693e37131fd55fbcbcac3b35b3f8b", # 200 Streamline Counts features cohort: HCP
42+
"f16e449d-86e1-408b-9487-aa9d72e39901--a45c0c5f53325ac32b59833e7605b18a", # 015 - Streamline Counts cohort: HCP
43+
"3f179784-194d-4795-9d8d-301b524ca00a--e27e3ad4f467fb5c445a504557f340a4--9c08356ec0454773885ded630e49b5d3", # 0108_1 - Functional Connectivity cohort: 1000BRAINS, paradigm: Resting state (RestEmpCorrFC)
44+
"cf0::FunctionalConnectivity::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::f16e449d-86e1-408b-9487-aa9d72e39901::4da1dae86a1fd717e5a3618ab041fd3f", # 200 Functional Connectivity features cohort: HCP, paradigm: Resting state (EmpCorrFC concatenated)
2645
"b08a7dbc-7c75-4ce7-905b-690b2b1e8957--8ff1e1d8bcb26296027b475ec744b83c", # Fiber structures of a human hippocampus based on joint DMRI, 3D-PLI, and TPFM acquisitions (T2 weighted MRI)
2746
]
2847

e2e/features/test_plot.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import pytest
2+
import siibra
3+
4+
selected_ids = [
5+
"cf0::BigBrainIntensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::r:Area hOc1 (V1, 17, CalcS)::nodsid::94768ccf7d23b640453fb56b4562c2d2", # 2279 BigBrain Intensity Profile features
6+
"lq0::BigBrainIntensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::r:Area hOc1 (V1, 17, CalcS)::fe933acdb8dea6803c76251e004b9b1f", # BigBrain Intensity Profile: (-3.0491600036621094, -64.58589935302734, 1.1756900548934937)
7+
"cf0::CellDensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::r:Area hOc1 (V1, 17, CalcS)::f2cd6b97-e42d-41af-a01b-5caf2d692e28::599f219267d5bdc3c5c04ddf31f36748", # 10 Cell Density Profile features
8+
"f2cd6b97-e42d-41af-a01b-5caf2d692e28--5fc6ebfcbdf43c1c9fb36263eda160d2", # Cell Density Profile: (19.09100914001465, -64.9000015258789, -0.36307409405708313)
9+
"cf0::ReceptorDensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::r:Area hOc1 (V1, 17, CalcS)::e715e1f7-2079-45c4-a67f-f76b102acfce::48ce018be081dafb160287031fbe08c3", # 16 Receptor Density Profile features
10+
"e715e1f7-2079-45c4-a67f-f76b102acfce--e7b46dcf4fea599385b5653ab78e9784", # Receptor Density Profile: alpha4beta2
11+
"cf0::StreamlineCounts::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::f16e449d-86e1-408b-9487-aa9d72e39901::295693e37131fd55fbcbcac3b35b3f8b", # 200 Streamline Counts features cohort: HCP
12+
"f16e449d-86e1-408b-9487-aa9d72e39901--a45c0c5f53325ac32b59833e7605b18a", # 015 - Streamline Counts cohort: HCP
13+
"3f179784-194d-4795-9d8d-301b524ca00a--e27e3ad4f467fb5c445a504557f340a4--9c08356ec0454773885ded630e49b5d3", # 0108_1 - Functional Connectivity cohort: 1000BRAINS, paradigm: Resting state (RestEmpCorrFC)
14+
"cf0::FunctionalConnectivity::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290::f16e449d-86e1-408b-9487-aa9d72e39901::4da1dae86a1fd717e5a3618ab041fd3f", # 200 Functional Connectivity features cohort: HCP, paradigm: Resting state (EmpCorrFC concatenated)
15+
"cf0::RegionalBOLD::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-300::1f832869-0361-4064-8e60-c729590e1013::2a993ff2650c1a95e9c331e6a33c4325", # 200 Regional BOLD features cohort: HCP, paradigm: rfMRI_REST1_RL_BOLD
16+
"1f832869-0361-4064-8e60-c729590e1013--dcb293fdce0c34447ab066e8725727c1", # 000 - Regional BOLD cohort: HCP, paradigm: rfMRI_REST1_RL_BOLD
17+
]
18+
19+
20+
@pytest.mark.parametrize("fid", selected_ids)
21+
def test_get_instance(fid):
22+
feat = siibra.features.Feature._get_instance_by_id(fid)
23+
feat.plot()
24+
feat.plot(backend='plotly')

requirements-test.txt

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ pytest-cov
55
coverage
66
requests-mock
77
matplotlib
8+
plotly

siibra/configuration/factory.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ def build_receptor_density_fingerprint(cls, spec):
356356
tsvfile=spec['file'],
357357
anchor=cls.extract_anchor(spec),
358358
datasets=cls.extract_datasets(spec),
359+
id=spec.get("@id", None)
359360
)
360361

361362
@classmethod
@@ -366,6 +367,7 @@ def build_cell_density_fingerprint(cls, spec):
366367
layerfiles=spec['layerfiles'],
367368
anchor=cls.extract_anchor(spec),
368369
datasets=cls.extract_datasets(spec),
370+
id=spec.get("@id", None)
369371
)
370372

371373
@classmethod
@@ -376,6 +378,7 @@ def build_receptor_density_profile(cls, spec):
376378
tsvfile=spec['file'],
377379
anchor=cls.extract_anchor(spec),
378380
datasets=cls.extract_datasets(spec),
381+
id=spec.get("@id", None)
379382
)
380383

381384
@classmethod
@@ -387,6 +390,7 @@ def build_cell_density_profile(cls, spec):
387390
url=spec['file'],
388391
anchor=cls.extract_anchor(spec),
389392
datasets=cls.extract_datasets(spec),
393+
id=spec.get("@id", None)
390394
)
391395

392396
@classmethod
@@ -399,6 +403,7 @@ def build_section(cls, spec):
399403
"space_spec": vol._space_spec,
400404
"providers": vol._providers.values(),
401405
"datasets": cls.extract_datasets(spec),
406+
"id": spec.get("@id", None)
402407
}
403408
modality = spec.get('modality', "")
404409
if modality == "cell body staining":
@@ -416,6 +421,7 @@ def build_volume_of_interest(cls, spec):
416421
"space_spec": vol._space_spec,
417422
"providers": vol._providers.values(),
418423
"datasets": cls.extract_datasets(spec),
424+
"id": spec.get("@id", None)
419425
}
420426
modality = spec.get('modality', "")
421427
if modality == "cell body staining":
@@ -486,7 +492,8 @@ def build_connectivity_matrix(cls, spec):
486492
"filename": filename,
487493
"subject": fkey if files_indexed_by == "subject" else "average",
488494
"feature": fkey if files_indexed_by == "feature" else None,
489-
"connector": repo_connector or base_url + filename
495+
"connector": repo_connector or base_url + filename,
496+
"id": spec.get("@id", None)
490497
})
491498
conn_by_file.append(conn_cls(**kwargs))
492499
return conn_by_file
@@ -519,7 +526,8 @@ def build_activity_timeseries(cls, spec):
519526
for fkey, filename in files.items():
520527
kwargs.update({
521528
"filename": filename,
522-
"subject": fkey
529+
"subject": fkey,
530+
"id": spec.get("@id", None)
523531
})
524532
timeseries_by_file.append(timeseries_cls(**kwargs))
525533
return timeseries_by_file

siibra/features/__init__.py

+19-8
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,18 @@
1414
# limitations under the License.
1515
"""Multimodal data features types and query mechanisms."""
1616

17+
from typing import Union
18+
from functools import partial
19+
1720
from . import (
1821
connectivity,
1922
tabular,
2023
image,
2124
dataset,
2225
)
2326

24-
from typing import Union
27+
from ..commons import logger
28+
2529
from .feature import Feature
2630
from ..retrieval import cache
2731
from ..commons import siibra_tqdm
@@ -49,7 +53,7 @@ def __getattr__(attr: str):
4953

5054

5155
@cache.Warmup.register_warmup_fn()
52-
def _warm_feature_cache_insntaces():
56+
def _warm_feature_cache_instances():
5357
"""Preload preconfigured multimodal data features."""
5458
for ftype in TYPES.values():
5559
_ = ftype._get_instances()
@@ -60,18 +64,25 @@ def _warm_feature_cache_data():
6064
return_callables = []
6165
for ftype in TYPES.values():
6266
instances = ftype._get_instances()
67+
68+
# the instances *must* be cleared, or it will impede the garbage collection, and results in memleak
69+
ftype._clean_instances()
6370
tally = siibra_tqdm(desc=f"Warming data {ftype.__name__}", total=len(instances))
6471
for f in instances:
65-
def get_data():
72+
def get_data(arg):
73+
tally = arg.pop("tally")
74+
feature = arg.pop("feature")
6675
# TODO
6776
# the try catch is as a result of https://github.com/FZJ-INM1-BDA/siibra-python/issues/509
6877
# sometimes f.data can fail
6978
try:
70-
_ = f.data
71-
except Exception:
72-
...
73-
tally.update(1)
74-
return_callables.append(get_data)
79+
_ = feature.data
80+
except Exception as e:
81+
logger.warn(f"Feature {feature.name} warmup failed: {str(e)}")
82+
finally:
83+
tally.update(1)
84+
# append dictionary, so that popping the dictionary will mark the feature to be garbage collected
85+
return_callables.append(partial(get_data, {"feature": f, "tally": tally}))
7586
return return_callables
7687

7788

siibra/features/connectivity/functional_connectivity.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def id(self):
4141

4242
@property
4343
def name(self):
44-
return f"{super().name}, {self.paradigm} paradigm"
44+
return super().name + f", paradigm: {self.paradigm}"
4545

4646

4747
class AnatomoFunctionalConnectivity(

siibra/features/connectivity/regional_connectivity.py

+45-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from .. import anchor as _anchor
2121

22-
from ...commons import logger, QUIET
22+
from ...commons import logger, QUIET, siibra_tqdm
2323
from ...core import region as _region
2424
from ...locations import pointset
2525
from ...retrieval.repositories import RepositoryConnector
@@ -57,7 +57,8 @@ def __init__(
5757
description: str = "",
5858
datasets: list = [],
5959
subject: str = "average",
60-
feature: str = None
60+
feature: str = None,
61+
id: str = None
6162
):
6263
"""
6364
Construct a parcellation-averaged connectivity matrix.
@@ -91,6 +92,7 @@ def __init__(
9192
description=description,
9293
anchor=anchor,
9394
datasets=datasets,
95+
id=id
9496
)
9597
self.cohort = cohort.upper()
9698
if isinstance(connector, str) and connector:
@@ -116,7 +118,7 @@ def feature(self):
116118

117119
@property
118120
def name(self):
119-
return f"{super().name} with cohort {self.cohort} - {self.feature or self.subject}"
121+
return f"{self.feature or self.subject} - " + super().name + f" cohort: {self.cohort}"
120122

121123
@property
122124
def data(self) -> pd.DataFrame:
@@ -132,6 +134,45 @@ def data(self) -> pd.DataFrame:
132134
self._load_matrix()
133135
return self._matrix.copy()
134136

137+
@classmethod
138+
def _merge_elements(
139+
cls,
140+
elements: List["RegionalConnectivity"],
141+
description: str,
142+
modality: str,
143+
anchor: _anchor.AnatomicalAnchor,
144+
):
145+
assert len({f.cohort for f in elements}) == 1
146+
merged = cls(
147+
cohort=elements[0].cohort,
148+
regions=elements[0].regions,
149+
connector=elements[0]._connector,
150+
decode_func=elements[0]._decode_func,
151+
filename="",
152+
subject="average",
153+
feature="average",
154+
description=description,
155+
modality=modality,
156+
anchor=anchor,
157+
**{"paradigm": "averaged (by siibra)"} if getattr(elements[0], "paradigm", None) else {}
158+
)
159+
if isinstance(elements[0]._connector, HttpRequest):
160+
getter = lambda elm: elm._connector.get()
161+
else:
162+
getter = lambda elm: elm._connector.get(elm._filename, decode_func=elm._decode_func)
163+
all_arrays = [
164+
getter(elm)
165+
for elm in siibra_tqdm(
166+
elements,
167+
total=len(elements),
168+
desc=f"Averaging {len(elements)} connectivity matrices"
169+
)
170+
]
171+
merged._matrix = elements[0]._arraylike_to_dataframe(
172+
np.stack(all_arrays).mean(0)
173+
)
174+
return merged
175+
135176
def _plot_matrix(
136177
self, regions: List[str] = None,
137178
logscale: bool = False, *args, backend="nilearn", **kwargs
@@ -179,6 +220,7 @@ def _plot_matrix(
179220
**kwargs
180221
)
181222
elif backend == "plotly":
223+
kwargs["title"] = kwargs["title"].replace("\n", "<br>")
182224
from plotly.express import imshow
183225
return imshow(matrix, *args, x=regions, y=regions, **kwargs)
184226
else:

0 commit comments

Comments
 (0)