genome-spy-python 0.1.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.
Files changed (64) hide show
  1. genome_spy/__init__.py +199 -0
  2. genome_spy/_chart_authoring.py +231 -0
  3. genome_spy/_conditions.py +72 -0
  4. genome_spy/_embed.py +87 -0
  5. genome_spy/_expressions.py +271 -0
  6. genome_spy/_parameters.py +267 -0
  7. genome_spy/_render.py +207 -0
  8. genome_spy/_utils.py +75 -0
  9. genome_spy/_widget.py +262 -0
  10. genome_spy/api.py +198 -0
  11. genome_spy/arrow.py +155 -0
  12. genome_spy/channels.py +193 -0
  13. genome_spy/chart.py +1240 -0
  14. genome_spy/data.py +56 -0
  15. genome_spy/data_transformers.py +267 -0
  16. genome_spy/datasets/__init__.py +189 -0
  17. genome_spy/datasets/_airway.py +219 -0
  18. genome_spy/datasets/_annotations.py +37 -0
  19. genome_spy/datasets/_gistic.py +43 -0
  20. genome_spy/datasets/_grammar.py +66 -0
  21. genome_spy/datasets/_hapmap.py +180 -0
  22. genome_spy/datasets/_mutation.py +289 -0
  23. genome_spy/datasets/_oncoprint.py +523 -0
  24. genome_spy/datasets/data/airway_metadata.csv +9 -0
  25. genome_spy/datasets/data/airway_scaledcounts.csv +38695 -0
  26. genome_spy/datasets/data/brca.maf.gz +0 -0
  27. genome_spy/datasets/data/hapmap_gwas.csv +14413 -0
  28. genome_spy/datasets/data/mutation_impact_reference.json +27 -0
  29. genome_spy/datasets/data/oncoprint_dataset3.json +266 -0
  30. genome_spy/datasets/data/p53_sequence_comparison.json.gz +0 -0
  31. genome_spy/datasets/data/pik3ca_mutations.json +1 -0
  32. genome_spy/datasets/data/pik3ca_tcga_brca_lollipop.json +38 -0
  33. genome_spy/datasets/data/refseq_gene_bodies.csv.gz +0 -0
  34. genome_spy/datasets/data/tal1_alphagenome_reference.json.gz +0 -0
  35. genome_spy/datasets/data/tcga.tsv +146 -0
  36. genome_spy/datasets/data/tcga_laml.maf.gz +0 -0
  37. genome_spy/datasets/data/tcga_laml_annot.tsv +201 -0
  38. genome_spy/datasets/data/tcga_laml_combined_oncoplot.json.gz +0 -0
  39. genome_spy/datasets/data/tcga_ov_gistic_lesions.tsv.gz +0 -0
  40. genome_spy/datasets/data/tcga_ov_gistic_scores.tsv.gz +0 -0
  41. genome_spy/helpers.py +185 -0
  42. genome_spy/jupyter.py +5 -0
  43. genome_spy/py.typed +0 -0
  44. genome_spy/schema/__init__.py +784 -0
  45. genome_spy/schema/_kwds.py +1394 -0
  46. genome_spy/schema/_typing.py +186 -0
  47. genome_spy/schema/capabilities.json +593 -0
  48. genome_spy/schema/channels.py +8943 -0
  49. genome_spy/schema/composition.py +1064 -0
  50. genome_spy/schema/core.py +51821 -0
  51. genome_spy/schema/ergonomics.py +2056 -0
  52. genome_spy/schema/expressions.py +476 -0
  53. genome_spy/schema/genome-spy-schema.json +33657 -0
  54. genome_spy/schema/lazy.py +326 -0
  55. genome_spy/schema/mixins.py +11684 -0
  56. genome_spy/schemapi.py +264 -0
  57. genome_spy/static/widget.js +345 -0
  58. genome_spy_python-0.1.0.dist-info/METADATA +185 -0
  59. genome_spy_python-0.1.0.dist-info/RECORD +64 -0
  60. genome_spy_python-0.1.0.dist-info/WHEEL +4 -0
  61. genome_spy_python-0.1.0.dist-info/licenses/LICENSE +21 -0
  62. genome_spy_python-0.1.0.dist-info/licenses/LICENSES/ALTAIR-BSD-3-Clause.txt +27 -0
  63. genome_spy_python-0.1.0.dist-info/licenses/LICENSES/GALLERY-DATA-MIT.txt +22 -0
  64. genome_spy_python-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +42 -0
genome_spy/__init__.py ADDED
@@ -0,0 +1,199 @@
1
+ """Public package interface for genome_spy."""
2
+
3
+ from genome_spy.arrow import to_arrow_ipc
4
+ from genome_spy.data_transformers import data_transformers
5
+ from genome_spy.api import (
6
+ Angle,
7
+ AxisGenomeData,
8
+ BrushConfig,
9
+ Color,
10
+ condition,
11
+ Direction,
12
+ ConcatChart,
13
+ Data,
14
+ DataFormat,
15
+ DynamicOpacity,
16
+ Dx,
17
+ Dy,
18
+ ExprRef,
19
+ Expression,
20
+ FacetIndex,
21
+ Fill,
22
+ FillOpacity,
23
+ GenomeAxis,
24
+ HandledTooltip,
25
+ HConcatChart,
26
+ ImportedView,
27
+ JupyterChart,
28
+ Key,
29
+ LayerChart,
30
+ Legend,
31
+ Locus,
32
+ MultiscaleChart,
33
+ Opacity,
34
+ Parameter,
35
+ Paddings,
36
+ Parse,
37
+ RulerMarkConfig,
38
+ Sample,
39
+ Scale,
40
+ Search,
41
+ SelectionDomainRef,
42
+ SemanticScore,
43
+ Shape,
44
+ Size,
45
+ SizeDef,
46
+ Step,
47
+ Stroke,
48
+ StrokeOpacity,
49
+ StrokeWidth,
50
+ Text,
51
+ Tooltip,
52
+ TopLevelSpec,
53
+ Title,
54
+ UniqueId,
55
+ VConcatChart,
56
+ X,
57
+ X2,
58
+ XOffset,
59
+ Y,
60
+ Y2,
61
+ YOffset,
62
+ Chart,
63
+ axes,
64
+ config,
65
+ compare,
66
+ concat,
67
+ data_format,
68
+ datum,
69
+ dynamic_opacity,
70
+ expr,
71
+ hconcat,
72
+ import_view,
73
+ layer,
74
+ lazy,
75
+ locus,
76
+ multiscale,
77
+ parse,
78
+ scales,
79
+ step,
80
+ title,
81
+ value,
82
+ view,
83
+ view_config,
84
+ vconcat,
85
+ when,
86
+ )
87
+
88
+ # BEGIN GENERATED INTERACTION IMPORTS
89
+ from genome_spy.helpers import (
90
+ binding,
91
+ binding_checkbox,
92
+ binding_radio,
93
+ binding_range,
94
+ binding_select,
95
+ param,
96
+ ruler,
97
+ selection_interval,
98
+ selection_point,
99
+ )
100
+ # END GENERATED INTERACTION IMPORTS
101
+
102
+ __all__ = [
103
+ "data_transformers",
104
+ "__version__",
105
+ "Angle",
106
+ "AxisGenomeData",
107
+ "axes",
108
+ "BrushConfig",
109
+ "Chart",
110
+ "Color",
111
+ "condition",
112
+ "Direction",
113
+ "compare",
114
+ "ConcatChart",
115
+ "Data",
116
+ "DataFormat",
117
+ "DynamicOpacity",
118
+ "Dx",
119
+ "Dy",
120
+ "ExprRef",
121
+ "Expression",
122
+ "FacetIndex",
123
+ "Fill",
124
+ "FillOpacity",
125
+ "GenomeAxis",
126
+ "HandledTooltip",
127
+ "HConcatChart",
128
+ "ImportedView",
129
+ "JupyterChart",
130
+ "Key",
131
+ "LayerChart",
132
+ "Legend",
133
+ "Locus",
134
+ "MultiscaleChart",
135
+ "Opacity",
136
+ "Parameter",
137
+ "Paddings",
138
+ "Parse",
139
+ "Sample",
140
+ "Scale",
141
+ "Search",
142
+ "SelectionDomainRef",
143
+ "SemanticScore",
144
+ "Shape",
145
+ "Size",
146
+ "SizeDef",
147
+ "Step",
148
+ "Stroke",
149
+ "StrokeOpacity",
150
+ "StrokeWidth",
151
+ "Text",
152
+ "Tooltip",
153
+ "TopLevelSpec",
154
+ "to_arrow_ipc",
155
+ "Title",
156
+ "UniqueId",
157
+ "VConcatChart",
158
+ "X",
159
+ "X2",
160
+ "XOffset",
161
+ "Y",
162
+ "Y2",
163
+ "YOffset",
164
+ "concat",
165
+ "config",
166
+ "data_format",
167
+ "datum",
168
+ "dynamic_opacity",
169
+ "expr",
170
+ "hconcat",
171
+ "import_view",
172
+ "layer",
173
+ "lazy",
174
+ "locus",
175
+ "multiscale",
176
+ "parse",
177
+ "RulerMarkConfig",
178
+ "scales",
179
+ "step",
180
+ "title",
181
+ "value",
182
+ "view",
183
+ "view_config",
184
+ "vconcat",
185
+ "when",
186
+ # BEGIN GENERATED INTERACTION EXPORTS
187
+ "binding",
188
+ "binding_checkbox",
189
+ "binding_radio",
190
+ "binding_range",
191
+ "binding_select",
192
+ "param",
193
+ "ruler",
194
+ "selection_interval",
195
+ "selection_point",
196
+ # END GENERATED INTERACTION EXPORTS
197
+ ]
198
+
199
+ __version__ = "0.1.0"
@@ -0,0 +1,231 @@
1
+ """Authoring-edge normalization helpers for the handwritten chart API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Sequence
7
+ from datetime import date, datetime
8
+ from typing import Any, cast
9
+
10
+ from genome_spy._utils import is_mapping
11
+ from genome_spy.arrow import _is_pandas_frame, _is_polars_frame, _is_pyarrow_table
12
+ from genome_spy.channels import Channel, channel
13
+ from genome_spy.schemapi import (
14
+ SchemaBase,
15
+ Undefined,
16
+ normalize_mapping_value,
17
+ normalize_schema_value,
18
+ )
19
+
20
+
21
+ def normalize_data(data: Any) -> Any:
22
+ """Normalize Python-side data inputs into schema-compatible values."""
23
+ if data is None:
24
+ return None
25
+ if isinstance(data, SchemaBase):
26
+ return json_safe(data.to_dict(validate=False))
27
+ if is_mapping(data):
28
+ return cast(
29
+ dict[str, Any], json_safe(normalize_schema_value(data, validate=False))
30
+ )
31
+ records = records_from_data(data)
32
+ if records is not None:
33
+ return records_data(records)
34
+ raise TypeError(f"Unsupported data value: {type(data)!r}")
35
+
36
+
37
+ def json_safe(value: Any) -> Any:
38
+ """Convert authoring-edge values into JSON-safe primitives."""
39
+ if value is None:
40
+ return None
41
+ if isinstance(value, float) and not math.isfinite(value):
42
+ return None
43
+ if isinstance(value, datetime | date):
44
+ return value.isoformat()
45
+ if not isinstance(value, str | bytes) and hasattr(value, "item"):
46
+ try:
47
+ item = value.item()
48
+ except (AttributeError, TypeError, ValueError):
49
+ item = value
50
+ if item is not value:
51
+ return json_safe(item)
52
+ if isinstance(value, list):
53
+ return [json_safe(item) for item in value]
54
+ if is_mapping(value):
55
+ return {key: json_safe(item) for key, item in value.items()}
56
+ return value
57
+
58
+
59
+ def records_from_data(data: Any) -> list[dict[str, Any]] | None:
60
+ """Extract record-like rows from common Python table inputs."""
61
+ if isinstance(data, list):
62
+ return data
63
+ if _is_pyarrow_table(data):
64
+ records = data.to_pylist()
65
+ return records if isinstance(records, list) else None
66
+ if hasattr(data, "to_dicts"):
67
+ records = data.to_dicts()
68
+ if isinstance(records, list):
69
+ return records
70
+ return None
71
+ if is_mapping(data):
72
+ values = data.get("values")
73
+ if isinstance(values, list):
74
+ return values
75
+ return None
76
+ if hasattr(data, "to_dict"):
77
+ try:
78
+ records = data.to_dict(orient="records")
79
+ except TypeError:
80
+ return None
81
+ if isinstance(records, list):
82
+ return records
83
+ return None
84
+
85
+
86
+ def records_data(records: list[dict[str, Any]]) -> dict[str, Any]:
87
+ """Wrap record rows as inline schema data."""
88
+ return {"values": json_safe(records)}
89
+
90
+
91
+ def infer_field_type(field: str, data: Any) -> str | None:
92
+ """Infer a GenomeSpy encoding type from up to the first 100 records."""
93
+ table_type = infer_table_field_type(field, data)
94
+ if table_type is not None:
95
+ return table_type
96
+ if _is_polars_frame(data) or _is_pandas_frame(data) or _is_pyarrow_table(data):
97
+ return None
98
+
99
+ records = records_from_data(data)
100
+ if not records:
101
+ return None
102
+
103
+ for row in records[:100]:
104
+ if not is_mapping(row) or field not in row:
105
+ continue
106
+ inferred_type = infer_value_type(row[field])
107
+ if inferred_type is not None:
108
+ return inferred_type
109
+ return None
110
+
111
+
112
+ def infer_table_field_type(field: str, data: Any) -> str | None:
113
+ """Infer a field type from supported table dtype metadata without rows."""
114
+ if _is_polars_frame(data):
115
+ dtype = getattr(data, "schema", {}).get(field)
116
+ if dtype is None:
117
+ return None
118
+ is_numeric = getattr(dtype, "is_numeric", None)
119
+ return "quantitative" if callable(is_numeric) and is_numeric() else "nominal"
120
+
121
+ if _is_pandas_frame(data):
122
+ dtypes = getattr(data, "dtypes", None)
123
+ if dtypes is None:
124
+ return None
125
+ try:
126
+ dtype = dtypes[field]
127
+ except (KeyError, TypeError):
128
+ return None
129
+ return "quantitative" if getattr(dtype, "kind", "") in "iufc" else "nominal"
130
+
131
+ if _is_pyarrow_table(data):
132
+ schema = getattr(data, "schema", None)
133
+ if schema is None:
134
+ return None
135
+ try:
136
+ arrow_type = schema.field(field).type
137
+ except (KeyError, TypeError, AttributeError):
138
+ return None
139
+ return (
140
+ "quantitative"
141
+ if str(arrow_type).startswith(("int", "uint", "float", "double", "decimal"))
142
+ else "nominal"
143
+ )
144
+
145
+ return None
146
+
147
+
148
+ def infer_value_type(value: Any) -> str | None:
149
+ """Infer a GenomeSpy encoding type for one Python value."""
150
+ if value is None:
151
+ return None
152
+ if isinstance(value, bool):
153
+ return "nominal"
154
+ if isinstance(value, int | float):
155
+ return "quantitative"
156
+ return "nominal"
157
+
158
+
159
+ def normalize_channel(
160
+ name: str,
161
+ value: Channel
162
+ | SchemaBase
163
+ | str
164
+ | dict[str, Any]
165
+ | Sequence[Channel | SchemaBase | str | dict[str, Any]]
166
+ | None,
167
+ *,
168
+ data: Any = None,
169
+ ) -> dict[str, Any] | list[dict[str, Any]] | None:
170
+ """Normalize one chart encoding channel definition."""
171
+ if value is None:
172
+ return None
173
+ if isinstance(value, Sequence) and not isinstance(value, str | bytes):
174
+ return [channel(item, encoding_name=name).to_dict() for item in value]
175
+ definition = channel(value).to_dict()
176
+ return normalized_channel_definition(name, definition, data=data)
177
+
178
+
179
+ def normalized_channel_definition(
180
+ name: str,
181
+ definition: dict[str, Any],
182
+ *,
183
+ data: Any = None,
184
+ ) -> dict[str, Any]:
185
+ """Normalize one mapping-form channel definition."""
186
+ normalized = dict(definition)
187
+ if name in {"x2", "y2", "key"}:
188
+ normalized.pop("type", None)
189
+ elif "type" not in normalized and isinstance(normalized.get("field"), str):
190
+ inferred_type = infer_field_type(normalized["field"], data)
191
+ if inferred_type is not None:
192
+ normalized["type"] = inferred_type
193
+ return normalized
194
+
195
+
196
+ def merge_encoding_definitions(
197
+ current_encoding: Any,
198
+ updates: dict[
199
+ str,
200
+ Channel
201
+ | SchemaBase
202
+ | str
203
+ | dict[str, Any]
204
+ | Sequence[Channel | SchemaBase | str | dict[str, Any]]
205
+ | None,
206
+ ],
207
+ *,
208
+ data: Any,
209
+ ) -> dict[str, Any]:
210
+ """Merge and normalize chart encoding updates."""
211
+ merged = {} if current_encoding is Undefined else dict(current_encoding)
212
+ for name, value in updates.items():
213
+ merged[name] = normalize_channel(name, value, data=data)
214
+ return merged
215
+
216
+
217
+ def normalize_transform(transform: SchemaBase | dict[str, Any]) -> dict[str, Any]:
218
+ """Normalize one transform definition."""
219
+ try:
220
+ return normalize_mapping_value(transform, key="transform", validate=False)
221
+ except TypeError as error:
222
+ raise TypeError(f"Unsupported transform value: {type(transform)!r}") from error
223
+
224
+
225
+ def normalize_transform_kwarg(
226
+ value: SchemaBase | dict[str, Any],
227
+ *,
228
+ key: str,
229
+ ) -> dict[str, Any]:
230
+ """Normalize one nested transform keyword value."""
231
+ return normalize_mapping_value(value, key=key, validate=False)
@@ -0,0 +1,72 @@
1
+ """Altair-style conditional encoding authoring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TypeAlias
6
+
7
+ from genome_spy._parameters import Parameter
8
+ from genome_spy.channels import Channel, channel
9
+ from genome_spy.schemapi import SchemaBase
10
+
11
+ ConditionValue: TypeAlias = Channel | SchemaBase | str | dict[str, object]
12
+
13
+
14
+ def _branch_definition(value: ConditionValue) -> dict[str, object]:
15
+ return channel(value).to_dict()
16
+
17
+
18
+ class _Then(Channel):
19
+ """A conditional channel awaiting an optional fallback branch."""
20
+
21
+ def otherwise(self, value: ConditionValue) -> Channel:
22
+ """Return the conditional channel with its fallback branch."""
23
+ return Channel({**_branch_definition(value), **self.to_dict()})
24
+
25
+
26
+ class _When:
27
+ """A validated parameter predicate awaiting its true branch."""
28
+
29
+ def __init__(self, predicate: Parameter) -> None:
30
+ self._predicate = predicate
31
+
32
+ def then(self, value: ConditionValue) -> _Then:
33
+ """Return a conditional channel using ``value`` when selected."""
34
+ condition = {
35
+ "param": self._predicate.name,
36
+ "empty": self._predicate.empty,
37
+ **_branch_definition(value),
38
+ }
39
+ return _Then({"condition": condition})
40
+
41
+
42
+ def when(predicate: Parameter) -> _When:
43
+ """Start an Altair-style selection condition.
44
+
45
+ GenomeSpy 0.86 supports selection parameters as conditional predicates.
46
+ Expression predicates will become available only if the upstream schema
47
+ adds that grammar.
48
+
49
+ Args:
50
+ predicate: A point or interval selection parameter.
51
+
52
+ Returns:
53
+ A builder whose ``then()`` method defines the selected branch.
54
+
55
+ Raises:
56
+ TypeError: If ``predicate`` is not a selection parameter.
57
+
58
+ Example:
59
+ >>> import genome_spy as gs
60
+ >>> brush = gs.selection_interval(encodings=["x"])
61
+ >>> condition = gs.when(brush).then(gs.value("red")).otherwise(
62
+ ... gs.value("gray")
63
+ ... )
64
+ >>> condition.to_dict()["condition"]["param"] == brush.name
65
+ True
66
+ """
67
+ if not isinstance(predicate, Parameter) or not predicate.is_selection:
68
+ raise TypeError("when() currently requires a selection parameter.")
69
+ return _When(predicate)
70
+
71
+
72
+ __all__ = ["when"]
genome_spy/_embed.py ADDED
@@ -0,0 +1,87 @@
1
+ """Shared rendering configuration for GenomeSpy browser embeds."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Literal, TypeAlias
7
+
8
+ from genome_spy.schema import SCHEMA_VERSION
9
+ from genome_spy.schemapi import Undefined, UndefinedType
10
+
11
+ ControlName: TypeAlias = Literal["svg", "png", "inspector", "full-window"]
12
+ Controls: TypeAlias = bool | ControlName | Sequence[ControlName]
13
+
14
+ _CONTROL_DEFINITIONS: dict[ControlName, tuple[str, str]] = {
15
+ "svg": ("core", "svgButton"),
16
+ "png": ("core", "pngButton"),
17
+ "inspector": ("inspector", "inspectorButton"),
18
+ "full-window": ("core", "fullWindowButton"),
19
+ }
20
+ DEFAULT_CONTROLS: tuple[ControlName, ...] = ("svg", "png", "inspector")
21
+ SUPPORTED_CONTROLS: tuple[ControlName, ...] = tuple(_CONTROL_DEFINITIONS)
22
+
23
+ _CORE_PACKAGE_URL = (
24
+ f"https://cdn.jsdelivr.net/npm/@genome-spy/core@{SCHEMA_VERSION}/dist"
25
+ )
26
+ DEFAULT_EMBED_URL = f"{_CORE_PACKAGE_URL}/bundle/index.es.js"
27
+ DEFAULT_CONTROLS_MODULE_URL = f"{_CORE_PACKAGE_URL}/src/controls.js"
28
+ DEFAULT_INSPECTOR_MODULE_URL = (
29
+ "https://cdn.jsdelivr.net/npm/"
30
+ f"@genome-spy/inspector@{SCHEMA_VERSION}/dist/index.es.js"
31
+ )
32
+
33
+
34
+ def normalize_controls(
35
+ controls: Controls | UndefinedType = Undefined,
36
+ ) -> tuple[ControlName, ...]:
37
+ """Return validated control names in display order."""
38
+ if controls is Undefined or controls is True:
39
+ return DEFAULT_CONTROLS
40
+ if controls is False:
41
+ return ()
42
+
43
+ values: Sequence[str]
44
+ if isinstance(controls, str):
45
+ values = (controls,)
46
+ elif isinstance(controls, Sequence):
47
+ values = controls
48
+ else:
49
+ raise TypeError(
50
+ "controls must be a boolean, a control name, or a sequence of "
51
+ "control names."
52
+ )
53
+
54
+ normalized: list[ControlName] = []
55
+ seen: set[str] = set()
56
+ for value in values:
57
+ if not isinstance(value, str):
58
+ raise TypeError("Every control name must be a string.")
59
+ if value not in SUPPORTED_CONTROLS:
60
+ expected = ", ".join(repr(name) for name in SUPPORTED_CONTROLS)
61
+ raise ValueError(
62
+ f"Unknown GenomeSpy control {value!r}. Expected one of: {expected}."
63
+ )
64
+ if value in seen:
65
+ raise ValueError(f"GenomeSpy control {value!r} was specified twice.")
66
+ seen.add(value)
67
+ normalized.append(value)
68
+ return tuple(normalized)
69
+
70
+
71
+ def control_definitions() -> dict[str, dict[str, str]]:
72
+ """Return browser module and export metadata for supported controls."""
73
+ return {
74
+ name: {"module": module, "export": export}
75
+ for name, (module, export) in _CONTROL_DEFINITIONS.items()
76
+ }
77
+
78
+
79
+ __all__ = [
80
+ "ControlName",
81
+ "Controls",
82
+ "DEFAULT_CONTROLS",
83
+ "DEFAULT_CONTROLS_MODULE_URL",
84
+ "DEFAULT_EMBED_URL",
85
+ "DEFAULT_INSPECTOR_MODULE_URL",
86
+ "SUPPORTED_CONTROLS",
87
+ ]