hiplot-plus 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.
@@ -0,0 +1,459 @@
1
+ # Copyright (c) Facebook, Inc. and its affiliates.
2
+ # This source code is licensed under the MIT license found in the
3
+ # LICENSE file in the root directory of this source tree.
4
+
5
+ import math
6
+ import random
7
+ import time
8
+ import typing as t
9
+ import uuid
10
+
11
+ from . import experiment as hip
12
+
13
+ # Demos from the README. If one of those is modified, please modify the readme as well
14
+
15
+
16
+ def demo_change_column_properties() -> hip.Experiment:
17
+ data = [
18
+ {"param": 1, "loss": 10, "hidden_field": "value1", "c": "red"},
19
+ {"param": 2, "loss": 5, "hidden_field": "value2", "c": "black"},
20
+ ]
21
+ exp = hip.Experiment.from_iterable(data)
22
+ exp.parameters_definition["c"].colors = {"red": "rgb(255, 0, 0)", "black": "rgb(0, 0, 0)"}
23
+ exp.parameters_definition["loss"].type = hip.ValueType.NUMERIC_LOG
24
+ exp.display_data(hip.Displays.PARALLEL_PLOT).update(
25
+ {
26
+ "hide": ["hidden_field"], # This column won't appear in the parallel plot
27
+ "order": ["c"], # Column `c` will be displayed first the in parallel plot
28
+ }
29
+ )
30
+ return exp
31
+
32
+
33
+ def demo_basic_usage() -> hip.Experiment:
34
+ data = [
35
+ {"dropout": 0.1, "lr": 0.001, "loss": 10.0, "optimizer": "SGD"},
36
+ {"dropout": 0.15, "lr": 0.01, "loss": 3.5, "optimizer": "Adam"},
37
+ {"dropout": 0.3, "lr": 0.1, "loss": 4.5, "optimizer": "Adam"},
38
+ ]
39
+ exp = hip.Experiment.from_iterable(data)
40
+ exp.colorby = "loss"
41
+ return exp
42
+
43
+
44
+ def demo_line_xy() -> hip.Experiment:
45
+ # DEMO_LINE_XY_BEGIN
46
+ exp = hip.Experiment()
47
+ exp.display_data(hip.Displays.XY).update(
48
+ {
49
+ "axis_x": "generation",
50
+ "axis_y": "loss",
51
+ }
52
+ )
53
+ history: list[tuple[str, float, float]] = [] # (uid, loss, param) of every datapoint so far
54
+ for i in range(200):
55
+ param = 10 ** random.uniform(-1, 1)
56
+ loss = random.uniform(-5, 5)
57
+ from_uid = None
58
+ if i > 10:
59
+ parent_uid, parent_loss, parent_param = random.choice(history[-10:])
60
+ from_uid = parent_uid # <-- Connect the parent to the child
61
+ loss += parent_loss
62
+ param *= parent_param
63
+ history.append((str(i), loss, param))
64
+ values: dict[str, hip.DisplayableType] = {"generation": i, "param": param, "loss": loss}
65
+ exp.datapoints.append(hip.Datapoint(uid=str(i), from_uid=from_uid, values=values))
66
+ # DEMO_LINE_XY_END
67
+ return exp
68
+
69
+
70
+ def demo_bug_uid() -> hip.Experiment:
71
+ return hip.Experiment.from_iterable([{"a": 1, "b": 2, "uid": 50.0}, {"a": 2, "b": 3, "uid": 49.33}])
72
+
73
+
74
+ def demo(n: int = 100) -> hip.Experiment:
75
+ xp = hip.Experiment()
76
+ xp.display_data(hip.Displays.XY).update(
77
+ {
78
+ "axis_x": "time",
79
+ "axis_y": "exp_metric",
80
+ }
81
+ )
82
+
83
+ # Some fake PBT-ish data
84
+ def fake_params() -> dict[str, hip.DisplayableType]:
85
+ r = random.random()
86
+ p: dict[str, hip.DisplayableType] = {
87
+ "lr": 10 ** random.uniform(-5, 0),
88
+ "seed": random.uniform(0, 10),
89
+ "name": uuid.uuid4().hex[:6],
90
+ "optimizer": random.choice(["sgd", "adam", "adamw"]),
91
+ "r": r,
92
+ "c": random.choice(["red", "green", "black"]),
93
+ }
94
+ if r < 0.1:
95
+ del p["optimizer"]
96
+ if r > 0.3:
97
+ p["optionA"] = random.uniform(1, 5)
98
+ else:
99
+ p["optionB"] = random.uniform(1, 5)
100
+
101
+ if r < 0.2:
102
+ p["pctile"] = -1.0
103
+ elif r < 0.5:
104
+ p["pctile"] = random.uniform(-1.0, 10.0)
105
+ elif r < 0.8:
106
+ p["pctile"] = 10 ** random.uniform(1, 2)
107
+ else:
108
+ p["pctile"] = random.uniform(100, 101)
109
+
110
+ if random.random() > 0.3:
111
+ p["special_values"] = random.uniform(1, 5)
112
+ else:
113
+ p["special_values"] = random.choice([math.inf, -math.inf, math.nan])
114
+ return p
115
+
116
+ def fake_metrics(tm: float) -> dict[str, hip.DisplayableType]:
117
+ return {
118
+ "exp_metric": 10 ** random.uniform(-5, 0),
119
+ "pct_success": random.uniform(10, 90),
120
+ "large_numeric": random.uniform(50000.0, 150000.0),
121
+ "chkpt": uuid.uuid4().hex[:6],
122
+ "time": tm + random.uniform(-0.2, 0.2),
123
+ "force_numericlog": random.uniform(1, 100),
124
+ "timestamp": int(time.time() + (task_idx * 2000)),
125
+ }
126
+
127
+ current_pop: list[dict[str, t.Any]] = [
128
+ {"uid": f"init{i}", "params": fake_params(), "last_ckpt_uid": None} for i in range(10)
129
+ ]
130
+ continue_num = 0
131
+ for task_idx in range(n):
132
+ # All drop checkpoints
133
+ for p in current_pop:
134
+ ckpt_uid = f"{p['uid']}_{uuid.uuid4().hex[:6]}"
135
+ xp.datapoints.append(
136
+ hip.Datapoint(
137
+ uid=ckpt_uid, from_uid=p["last_ckpt_uid"], values={**p["params"], **fake_metrics(task_idx)}
138
+ )
139
+ )
140
+ p["last_ckpt_uid"] = ckpt_uid
141
+
142
+ # Randomly drop some
143
+ current_pop = [p for p in current_pop if random.random() > 0.3]
144
+
145
+ # Respawn as needed
146
+ for _ in range(10 - len(current_pop)):
147
+ continue_num += 1
148
+ parent = random.choice(xp.datapoints[-10:])
149
+ current_pop.append({"uid": f"continue{continue_num}", "params": fake_params(), "last_ckpt_uid": parent.uid})
150
+ xp.parameters_definition["c"].colors = {"red": "rgb(255, 0, 0)", "green": "rgb(0, 255, 0)", "black": "rgb(0, 0, 0)"}
151
+ xp.parameters_definition["force_numericlog"].type = hip.ValueType.NUMERIC_LOG
152
+ xp.parameters_definition["pctile"].type = hip.ValueType.NUMERIC_PERCENTILE
153
+ xp.parameters_definition["timestamp"].type = hip.ValueType.TIMESTAMP
154
+ return xp
155
+
156
+
157
+ def demo_customize() -> hip.Experiment:
158
+ exp = demo()
159
+ # EXPERIMENT_SETTINGS_SNIPPET2_BEGIN
160
+ # Provide configuration for the parallel plot
161
+ exp.display_data(hip.Displays.PARALLEL_PLOT).update(
162
+ {
163
+ # Hide some columns in the parallel plot
164
+ "hide": ["optionB"],
165
+ # Specify the order for others
166
+ "order": ["time"], # Put column time first on the left
167
+ }
168
+ )
169
+
170
+ # Provide configuration for the table with all the rows
171
+ exp.display_data(hip.Displays.TABLE).update(
172
+ {
173
+ # Don't display `uid` and `from_uid` columns to the user
174
+ "hide": ["uid", "from_uid"],
175
+ # In the table, order rows by default
176
+ "order_by": [["pct_success", "desc"]],
177
+ # Specify the order for columns
178
+ "order": ["time"], # Put column time first on the left
179
+ }
180
+ )
181
+
182
+ # Provide configuration for the XY graph
183
+ exp.display_data(hip.Displays.XY).update(
184
+ {
185
+ # Default X axis for the XY plot
186
+ "axis_x": "time",
187
+ # Default Y axis
188
+ "axis_y": "lr",
189
+ # Configure lines
190
+ "lines_thickness": 1.0,
191
+ "lines_opacity": 0.1,
192
+ # Configure dots
193
+ "dots_thickness": 2.0,
194
+ "dots_opacity": 0.3,
195
+ }
196
+ )
197
+ # EXPERIMENT_SETTINGS_SNIPPET2_END
198
+ return exp
199
+
200
+
201
+ def demo_plotxy_large_numeric() -> hip.Experiment:
202
+ exp = demo()
203
+ exp.display_data(hip.Displays.XY).update(
204
+ {
205
+ "axis_x": "time",
206
+ "axis_y": "large_numeric",
207
+ }
208
+ )
209
+ return exp
210
+
211
+
212
+ def demo_force_scale() -> hip.Experiment:
213
+ xp = hip.Experiment()
214
+ for _ in range(100):
215
+ values = [abs(random.gauss(0.0, 1.0)) for _ in range(4)]
216
+ xp.datapoints.append(hip.Datapoint({f"value{i}": v / sum(values) for i, v in enumerate(values)}))
217
+ for i in range(4):
218
+ xp.parameters_definition[f"value{i}"].force_range(0.0, 1.0)
219
+ return xp
220
+
221
+
222
+ def demo_distribution(**kwargs: t.Any) -> hip.Experiment:
223
+ xp = hip.Experiment.from_iterable(
224
+ [
225
+ {
226
+ "cat": random.choice(["a", "b", "c", "d", "e", "f", "g", "h"]),
227
+ "numeric": random.uniform(0.0, 1.0),
228
+ }
229
+ for i in range(1000)
230
+ ]
231
+ )
232
+ xp.display_data(hip.Displays.DISTRIBUTION).update(kwargs)
233
+ return xp
234
+
235
+
236
+ def demo_distribution_colors_deterministic() -> hip.Experiment:
237
+ data = (
238
+ [{"c": "red", "x": i} for i in range(4)]
239
+ + [{"c": "black", "x": i} for i in range(4, 10)]
240
+ + [{"c": "green", "x": i} for i in range(10, 22)]
241
+ )
242
+ xp = hip.Experiment.from_iterable(data)
243
+ xp.parameters_definition["c"].colors = {
244
+ "red": "rgb(255, 0, 0)",
245
+ "green": "rgb(0, 255, 0)",
246
+ "black": "rgb(0, 0, 0)",
247
+ }
248
+ xp.display_data(hip.Displays.DISTRIBUTION).update({"axis": "c"})
249
+ return xp
250
+
251
+
252
+ def demo_bool() -> hip.Experiment:
253
+ return hip.Experiment.from_iterable([{"bool": True}, {"bool": False}])
254
+
255
+
256
+ def demo_color_interpolate() -> hip.Experiment:
257
+ exp = demo()
258
+ exp.parameters_definition["exp_metric"].colormap = "interpolateSinebow"
259
+ return exp
260
+
261
+
262
+ def demo_color_scheme_ylrd() -> hip.Experiment:
263
+ exp = demo()
264
+ exp.parameters_definition["exp_metric"].colormap = "schemeYlOrRd"
265
+ return exp
266
+
267
+
268
+ def demo_color_scheme_accent() -> hip.Experiment:
269
+ exp = demo()
270
+ exp.parameters_definition["exp_metric"].colormap = "schemeAccent"
271
+ return exp
272
+
273
+
274
+ def demo_color_interpolate_inverse() -> hip.Experiment:
275
+ exp = demo_color_interpolate()
276
+ assert exp.parameters_definition["exp_metric"].colormap is not None
277
+ exp.parameters_definition["exp_metric"].colormap += "#inverse"
278
+ return exp
279
+
280
+
281
+ def demo_axis_style() -> hip.Experiment:
282
+ data: list[dict[str, t.Any]] = [
283
+ {
284
+ **{f"param{i}": random.uniform(0, 1) for i in range(6)},
285
+ "loss": random.uniform(0, 100),
286
+ "metric": 10 ** random.uniform(0, 10),
287
+ }
288
+ for _ in range(100)
289
+ ]
290
+ xp = hip.Experiment.from_iterable(data)
291
+ for i in range(6):
292
+ xp.parameters_definition[f"param{i}"].label_css = "badge badge-pill badge-secondary"
293
+ xp.parameters_definition["loss"].label_css = "badge badge-pill badge-primary"
294
+ xp.parameters_definition["metric"].label_css = "badge badge-pill badge-info"
295
+ return xp
296
+
297
+
298
+ def demo_categorical() -> hip.Experiment:
299
+ data: list[dict[str, t.Any]] = [
300
+ {
301
+ "cat_num_05": random.randint(0, 5),
302
+ "cat_num_15": random.randint(0, 10),
303
+ "cat_num_25": random.randint(0, 25),
304
+ "cat_str_05": f"s{random.randint(0, 5)}",
305
+ "cat_str_15": f"s{random.randint(0, 15)}",
306
+ "cat_str_25": f"s{random.randint(0, 25)}",
307
+ }
308
+ for _ in range(100)
309
+ ]
310
+ xp = hip.Experiment.from_iterable(data)
311
+ for param in ["cat_num_05", "cat_num_15", "cat_num_25"]:
312
+ xp.parameters_definition[param].type = hip.ValueType.CATEGORICAL
313
+ xp.colorby = "cat_num_25"
314
+ return xp
315
+
316
+
317
+ def demo_long_names() -> hip.Experiment:
318
+ return hip.Experiment.from_iterable(
319
+ [
320
+ {
321
+ "some very very long name for a field": random.randint(0, 5),
322
+ "this one is also very long": random.randint(0, 10),
323
+ "another.long.one.but.with.dots": random.randint(0, 25),
324
+ }
325
+ for _ in range(100)
326
+ ]
327
+ )
328
+
329
+
330
+ def demo_force_constant_pplot() -> hip.Experiment:
331
+ exp = hip.Experiment.from_iterable([{"uid": 123, "a": 1, "b": 3}, {"uid": 345, "a": 2, "b": 3}])
332
+ exp.parameters_definition["b"].force_range(0, 100)
333
+ return exp
334
+
335
+
336
+ def demo_first_value_nan() -> hip.Experiment:
337
+ return hip.Experiment.from_iterable(
338
+ [
339
+ {},
340
+ {"a": None},
341
+ {"a": 2},
342
+ {"a": 2.1},
343
+ {"a": 2.2},
344
+ {"a": 5.5},
345
+ {"a": math.nan},
346
+ ]
347
+ )
348
+
349
+
350
+ def demo_weighted_rows() -> hip.Experiment:
351
+ experiment = hip.Experiment.from_iterable(
352
+ [
353
+ {"w": 1.0, "a": 1, "b": 1},
354
+ {"w": 2.0, "a": 2, "b": 1},
355
+ {"w": -2.0, "a": 2, "b": 1},
356
+ {"w": math.inf, "a": 2, "b": 2},
357
+ {"w": "not_a_number", "a": 2, "b": 3},
358
+ {"w": None, "a": 3, "b": 3},
359
+ {"a": 4, "b": 3},
360
+ ]
361
+ )
362
+ experiment.weightcolumn = "w"
363
+ return experiment
364
+
365
+
366
+ def demo_3xcols() -> hip.Experiment:
367
+ xp = demo()
368
+ for i in range(2):
369
+ new_xp = demo()
370
+ for dp, new_dp in zip(xp.datapoints, new_xp.datapoints, strict=True):
371
+ dp.values.update({f"{k}{i}": v for k, v in new_dp.values.items()})
372
+ return xp
373
+
374
+
375
+ def demo_col_html() -> hip.Experiment:
376
+ COL1 = "<h1>col1</h1>"
377
+ COL2 = "col_2"
378
+ experiment = hip.Experiment.from_iterable(
379
+ [
380
+ {COL1: 1.0, COL2: 1},
381
+ {COL1: 2.0, COL2: 2},
382
+ {COL1: 3.0, COL2: 3},
383
+ ]
384
+ )
385
+ experiment.parameters_definition[COL2].label_html = "col<sub>2</sub>"
386
+ return experiment
387
+
388
+
389
+ def demo_disable_table() -> hip.Experiment:
390
+ experiment = demo()
391
+ experiment.enabledDisplays.remove(hip.Displays.TABLE)
392
+ return experiment
393
+
394
+
395
+ def demo_big_floats() -> hip.Experiment:
396
+ return hip.Experiment.from_iterable(
397
+ {
398
+ "bigfloat": math.nan if i < 10 else 10 ** random.uniform(15, 32),
399
+ }
400
+ for i in range(100)
401
+ )
402
+
403
+
404
+ def demo_decay() -> hip.Experiment:
405
+ """Simulated exponential decay: remaining = initial_mass * exp(-ln2 / half_life * elapsed).
406
+ Produces a wide numeric range in remaining_grams (spans many orders of magnitude),
407
+ useful for testing log-scale tick labels alongside plain numeric columns.
408
+ """
409
+ rng = random.Random(42)
410
+ data: list[dict[str, t.Any]] = []
411
+ for _ in range(500):
412
+ initial_mass = rng.uniform(50, 500)
413
+ half_life = rng.uniform(1, 30)
414
+ elapsed = rng.uniform(1, 50)
415
+ remaining = initial_mass * math.exp(-math.log(2) / half_life * elapsed)
416
+ data.append(
417
+ {
418
+ "initial_mass": initial_mass,
419
+ "half_life": half_life,
420
+ "elapsed_years": elapsed,
421
+ "remaining_grams": remaining,
422
+ }
423
+ )
424
+ xp = hip.Experiment.from_iterable(data)
425
+ xp.parameters_definition["remaining_grams"].type = hip.ValueType.NUMERIC_LOG
426
+ return xp
427
+
428
+
429
+ README_DEMOS: dict[str, t.Callable[[], hip.Experiment]] = {
430
+ "demo": demo,
431
+ "demo_3xcols": demo_3xcols,
432
+ "demo_big": lambda: demo(1000),
433
+ "demo_change_column_properties": demo_change_column_properties,
434
+ "demo_basic_usage": demo_basic_usage,
435
+ "demo_line_xy": demo_line_xy,
436
+ "demo_bug_uid": demo_bug_uid,
437
+ "demo_force_scale": demo_force_scale,
438
+ "demo_distribution_cat": lambda: demo_distribution(axis="cat"),
439
+ "demo_distribution_num": lambda: demo_distribution(axis="numeric"),
440
+ "demo_distribution_num_100bins": lambda: demo_distribution(axis="numeric", nbins=100),
441
+ "demo_distribution_colors_deterministic": demo_distribution_colors_deterministic,
442
+ "demo_bool": demo_bool,
443
+ "demo_color_interpolate": demo_color_interpolate,
444
+ "demo_color_scheme_ylrd": demo_color_scheme_ylrd,
445
+ "demo_color_scheme_accent": demo_color_scheme_accent,
446
+ "demo_axis_style": demo_axis_style,
447
+ "demo_categorical": demo_categorical,
448
+ "demo_customize": demo_customize,
449
+ "demo_plotxy_large_numeric": demo_plotxy_large_numeric,
450
+ "demo_long_names": demo_long_names,
451
+ "demo_force_constant_pplot": demo_force_constant_pplot,
452
+ "demo_color_interpolate_inverse": demo_color_interpolate_inverse,
453
+ "demo_first_value_nan": demo_first_value_nan,
454
+ "demo_weighted_rows": demo_weighted_rows,
455
+ "demo_col_html": demo_col_html,
456
+ "demo_disable_table": demo_disable_table,
457
+ "demo_big_floats": demo_big_floats,
458
+ "demo_decay": demo_decay,
459
+ }
hiplot/ipython.py ADDED
@@ -0,0 +1,217 @@
1
+ # Copyright (c) Facebook, Inc. and its affiliates.
2
+ # This source code is licensed under the MIT license found in the
3
+ # LICENSE file in the root directory of this source tree.
4
+
5
+ import json
6
+ import typing as t
7
+ import uuid
8
+ from pathlib import Path
9
+
10
+ from . import experiment as exp
11
+ from .render import escapejs, make_experiment_standalone_page
12
+
13
+ # Lazy imports for optional dependencies
14
+ if t.TYPE_CHECKING:
15
+ from ipykernel.comm import Comm
16
+
17
+
18
+ def _get_ipython_display() -> t.Any:
19
+ """Lazy import of IPython.display with helpful error message."""
20
+ try:
21
+ import IPython.display
22
+
23
+ return IPython.display
24
+ except ImportError as e:
25
+ raise ImportError(
26
+ "IPython is required for notebook support. Install it with: pip install hiplot-plus[notebook]"
27
+ ) from e
28
+
29
+
30
+ def _require_ipykernel() -> None:
31
+ """Fail early with a helpful message when ipykernel is missing."""
32
+ try:
33
+ import ipykernel.comm # noqa: F401
34
+ except ImportError as e:
35
+ raise ImportError(
36
+ "ipykernel is required for notebook support. Install it with: pip install hiplot-plus[notebook]"
37
+ ) from e
38
+
39
+
40
+ class GetSelectedFailure(Exception):
41
+ pass
42
+
43
+
44
+ class NotebookJSBundleInjector:
45
+ """
46
+ TODO: Maybe we should do something smart here. Like inject only once?
47
+ But how to be robust to the user clearing the output of the cell where we injected the bundle?
48
+ If he then refreshes the page, HiPlot bundle is no longer injected...
49
+ """
50
+
51
+ @classmethod
52
+ def ensure_injected(cls) -> None:
53
+ IPython_display = _get_ipython_display()
54
+ bundle = Path(__file__).parent / "static" / "built" / "hiplot.bundle.js"
55
+ IPython_display.display(
56
+ IPython_display.Javascript(f"""
57
+ {bundle.read_text("utf-8")}
58
+ // Local variables can't be accessed in other cells, so let's
59
+ // manually create a global variable
60
+ Object.assign(window, {{'hiplot': hiplot}});
61
+ """)
62
+ )
63
+
64
+
65
+ def jupyter_make_full_width(content: str) -> str:
66
+ w_id = f"wrap_html_{uuid.uuid4().hex[:6]}"
67
+ return f"""
68
+ <div id="{w_id}">{content}</div>
69
+ <script type="text/javascript">
70
+ (function() {{
71
+ const elem = document.getElementById({json.dumps(w_id)});
72
+ try {{
73
+ Jupyter.notebook.kernel; // `Jupyter` is not defined on Labs
74
+ }} catch(err) {{
75
+ console.warn('Option "force_full_width" is only supported on Jupyter Notebooks ' +
76
+ '(Labs is not supported as the display area is larger)');
77
+ return;
78
+ }}
79
+ elem.style.width = "100vw";
80
+ const removeElems = elem.parentElement.parentElement.getElementsByClassName("prompt");
81
+ for (var i = 0; i < removeElems.length; ++i) {{
82
+ removeElems[i].remove();
83
+ }}
84
+ elem.parentElement.style.overflowX = "visible";
85
+
86
+ const scale_to_100pct_screen = function() {{
87
+ elem.style.marginLeft = '0px';
88
+ elem.style.marginLeft = (- elem.getBoundingClientRect().x) + 'px';
89
+ }};
90
+ window.addEventListener('resize', function() {{
91
+ scale_to_100pct_screen();
92
+ }});
93
+ scale_to_100pct_screen();
94
+ }})();
95
+ </script>
96
+ """
97
+
98
+
99
+ class IPythonExperimentDisplayed(exp.ExperimentDisplayed):
100
+ def __init__(self, xp: exp.Experiment, comm_name: str) -> None:
101
+ self._exp = xp
102
+ self._num_recv = 0
103
+ self._selected_ids: list[str] = []
104
+ self._last_data_per_type: dict[str, t.Any] = {}
105
+
106
+ _require_ipykernel()
107
+
108
+ def _recv(msg: dict[str, t.Any]) -> None:
109
+ self._num_recv += 1
110
+ msg_data = msg["content"]["data"]
111
+ self._last_data_per_type[msg_data["type"]] = msg_data["data"]
112
+
113
+ def target_func(comm: "Comm", _open_msg: dict[str, t.Any]) -> None:
114
+ """Registers the handler for later messages on the comm the front-end opened."""
115
+ comm.on_msg(_recv)
116
+
117
+ try:
118
+ ip: t.Any = get_ipython() # type: ignore[name-defined]
119
+ except NameError:
120
+ # Not in an ipython environment, for example in tests: there is no front-end to talk to.
121
+ return
122
+ ip.kernel.comm_manager.register_target(comm_name, target_func)
123
+
124
+ no_data_received_error = GetSelectedFailure(
125
+ """No data received from the front-end. Please make sure that:
126
+ 1. You don't call "get_selected" on the same cell
127
+ 2. The interface has loaded
128
+ 3. You are in a Jupyter notebook (Jupyter lab is *not* supported)"""
129
+ )
130
+
131
+ def get_selected(self) -> list[exp.Datapoint]:
132
+ last_selected_uids = self._last_data_per_type.get("selected_uids")
133
+ if last_selected_uids is None:
134
+ raise self.no_data_received_error
135
+ selected_set = set(last_selected_uids)
136
+ datapoints = [i for i in self._exp.datapoints if i.uid in selected_set]
137
+ assert len(datapoints) == len(selected_set)
138
+ return datapoints
139
+
140
+ def get_brush_extents(self) -> dict[str, dict[str, t.Any]]:
141
+ last_msg = self._last_data_per_type.get("brush_extents")
142
+ if last_msg is None:
143
+ raise self.no_data_received_error
144
+ brush_extents: dict[str, dict[str, t.Any]] = last_msg
145
+ return brush_extents
146
+
147
+
148
+ def _should_embed_js_with_html() -> bool:
149
+ from IPython.core.getipython import get_ipython
150
+
151
+ ip = get_ipython()
152
+ if ip is None:
153
+ return True
154
+ return "BentoApp" in ip.config
155
+
156
+
157
+ def display_exp(
158
+ xp: exp.Experiment,
159
+ force_full_width: bool = False,
160
+ store_state_url: str | None = None,
161
+ embed_js_with_html: bool | None = None,
162
+ **kwargs: t.Any,
163
+ ) -> IPythonExperimentDisplayed:
164
+ IPython_display = _get_ipython_display()
165
+
166
+ if embed_js_with_html is None:
167
+ embed_js_with_html = _should_embed_js_with_html()
168
+
169
+ comm_id = f"comm_{uuid.uuid4().hex[:6]}"
170
+ displayed_xp = IPythonExperimentDisplayed(xp, comm_id)
171
+ options: dict[str, t.Any] = {**kwargs, "experiment": xp._payload()}
172
+ if store_state_url is not None:
173
+ options.update({"persistentStateUrlPrefix": store_state_url})
174
+ else:
175
+ options.update({"persistentState": None})
176
+ index_html = make_experiment_standalone_page(options=options)
177
+ # Remove line that references the script bundle - prevents an HTTP error in the notebook
178
+ index_html = index_html.replace('src="static/built/hiplot.bundle.js"', "")
179
+ index_html = index_html.replace(
180
+ "/*ON_LOAD_SCRIPT_INJECT*/",
181
+ f"""/*ON_LOAD_SCRIPT_INJECT*/
182
+ const comm_id = {escapejs(comm_id)};
183
+ try {{
184
+ console.log("Setting up communication channel with Jupyter: ", comm_id);
185
+ const comm = Jupyter.notebook.kernel.comm_manager.new_comm(comm_id, {{'type': 'hello'}});
186
+ var comm_message_id = 0;
187
+ function send_data_change(type, data) {{
188
+ comm.send({{
189
+ 'type': type,
190
+ 'message_id': comm_message_id,
191
+ 'data': data,
192
+ }});
193
+ comm_message_id += 1;
194
+ }};
195
+ Object.assign(options, {{"onChange": {{
196
+ "selected_uids": send_data_change,
197
+ "brush_extents": send_data_change,
198
+ }}}});
199
+ }}
200
+ catch(err) {{
201
+ console.warn('Unable to create Javascript <-> Python communication channel' +
202
+ ' (are you in a Jupyter notebook? Jupyter labs is *not* supported!)');
203
+ }}
204
+ """,
205
+ )
206
+
207
+ if force_full_width:
208
+ index_html = jupyter_make_full_width(index_html)
209
+
210
+ if embed_js_with_html:
211
+ bundle = Path(__file__).parent / "static" / "built" / "hiplot.bundle.js"
212
+ index_html = index_html.replace("/*BUNDLE_FILE*/", bundle.read_text("utf-8"))
213
+ else:
214
+ NotebookJSBundleInjector.ensure_injected()
215
+
216
+ IPython_display.display(IPython_display.HTML(index_html))
217
+ return displayed_xp
hiplot/pkginfo.py ADDED
@@ -0,0 +1,15 @@
1
+ # Copyright (c) Facebook, Inc. and its affiliates.
2
+ # This source code is licensed under the MIT license found in the
3
+ # LICENSE file in the root directory of this source tree.
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+
7
+ package_name = "hiplot-plus"
8
+
9
+ # Dynamic version from installed package metadata
10
+ # This ensures __version__ matches pyproject.toml when installed via pip
11
+ try:
12
+ __version__ = version(package_name)
13
+ except PackageNotFoundError:
14
+ # Package is not installed (e.g., running from source checkout)
15
+ __version__ = "0.0.0.dev0"
hiplot/py.typed ADDED
File without changes