vgplot 0.29.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.
vgplot/plot.py ADDED
@@ -0,0 +1,546 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ from .util import camelize, omit_none
8
+ from .params import _ParamBase
9
+ from .data import DataDef
10
+
11
+
12
+ class FromRef:
13
+ def __init__(self, name: str, **opts: Any):
14
+ self.name = name
15
+ self.opts = {camelize(k): v for k, v in opts.items() if v is not None}
16
+
17
+ def to_dict(self) -> dict[str, Any]:
18
+ return {"from": self.name, **self.opts}
19
+
20
+
21
+ def source(name: str, **opts: Any) -> FromRef:
22
+ return FromRef(name, **opts)
23
+
24
+
25
+ @dataclass
26
+ class Directive:
27
+ key: str
28
+ value: Any
29
+
30
+ def to_kv(self):
31
+ return camelize(self.key), self.value
32
+
33
+
34
+ @dataclass
35
+ class Mark:
36
+ mark: str
37
+ data: Any | None = None
38
+ enc: dict[str, Any] | None = None
39
+
40
+ def to_dict(self, param_names: dict[int, str] | None = None) -> dict[str, Any]:
41
+ payload: dict[str, Any] = {"mark": self.mark}
42
+ enc = dict(self.enc or {})
43
+ _DATA_OPTS = {"filter_by": "filterBy", "optimize": "optimize"}
44
+ data_opts = {out: enc.pop(key) for key, out in _DATA_OPTS.items() if key in enc}
45
+ if self.data is not None:
46
+ if isinstance(self.data, (str, _ParamBase)):
47
+ data_dict: Any = {"from": self.data}
48
+ elif isinstance(self.data, FromRef):
49
+ data_dict = dict(self.data.to_dict())
50
+ elif isinstance(self.data, DataDef):
51
+ data_dict = {"from": self.data}
52
+ else:
53
+ data_dict = self.data
54
+ if data_opts and isinstance(data_dict, dict):
55
+ data_dict = {**data_dict, **data_opts}
56
+ data_opts = {}
57
+ payload["data"] = encode_value(data_dict, param_names)
58
+ for key, val in data_opts.items():
59
+ payload[key] = encode_value(val, param_names)
60
+ for k, v in enc.items():
61
+ payload[camelize(k)] = encode_value(v, param_names)
62
+ return payload
63
+
64
+
65
+ def encode_value(
66
+ v: Any,
67
+ param_names: dict[int, str] | None = None,
68
+ data_names: dict[int, str] | None = None,
69
+ ) -> Any:
70
+ if isinstance(v, DataDef):
71
+ if data_names and id(v) in data_names:
72
+ return data_names[id(v)]
73
+ return v
74
+ if isinstance(v, _ParamBase):
75
+ # Resolve to "$name" ref using the reverse lookup table
76
+ if param_names and id(v) in param_names:
77
+ return f"${param_names[id(v)]}"
78
+ return v
79
+ if isinstance(v, FromRef):
80
+ return v.to_dict()
81
+ if isinstance(v, Mark):
82
+ return v.to_dict(param_names)
83
+ if isinstance(v, list):
84
+ return [encode_value(x, param_names, data_names) for x in v]
85
+ if isinstance(v, dict):
86
+ return {k: encode_value(val, param_names, data_names) for k, val in v.items()}
87
+ return v
88
+
89
+
90
+ def plot(
91
+ *items: Mark | Directive | Mapping[str, Any],
92
+ param_names: dict[int, str] | None = None,
93
+ **kwargs: Any,
94
+ ) -> Any:
95
+ from .spec import View
96
+
97
+ marks: list[dict[str, Any]] = []
98
+ directives: dict[str, Any] = {}
99
+ for item in items:
100
+ if isinstance(item, Mark):
101
+ marks.append(item.to_dict(param_names))
102
+ elif isinstance(item, Directive):
103
+ k, v = item.to_kv()
104
+ directives[k] = encode_value(v, param_names)
105
+ elif isinstance(item, (dict, Mapping)):
106
+ marks.append({k: encode_value(v, param_names) for k, v in item.items()})
107
+ else:
108
+ raise TypeError(f"Unsupported plot item: {item}")
109
+ root: dict[str, Any] = {"plot": marks}
110
+ root.update(directives)
111
+ return View(root, **kwargs)
112
+
113
+
114
+ def directive(key: str, value: Any) -> Directive:
115
+ return Directive(key, value)
116
+
117
+
118
+ def mark(name: str, data: Any = None, **enc: Any) -> Mark:
119
+ return Mark(name, data=data, enc=enc)
120
+
121
+
122
+ # Mark helpers
123
+
124
+
125
+ def margins(
126
+ top: int | None = None,
127
+ right: int | None = None,
128
+ bottom: int | None = None,
129
+ left: int | None = None,
130
+ **kwargs: Any,
131
+ ) -> Directive:
132
+ return Directive(
133
+ "margins",
134
+ omit_none(
135
+ {"top": top, "right": right, "bottom": bottom, "left": left, **kwargs}
136
+ ),
137
+ )
138
+
139
+
140
+ def _encode_component(
141
+ item: Any,
142
+ param_names: dict[int, str] | None,
143
+ data_names: dict[int, str] | None = None,
144
+ ) -> Any:
145
+ from .spec import View
146
+
147
+ if isinstance(item, View):
148
+ item = item._view
149
+ if isinstance(item, dict) and "plot" in item:
150
+ # re-encode an already-built plot dict so param refs resolve
151
+ marks = [
152
+ (
153
+ {k: encode_value(v, param_names, data_names) for k, v in m.items()}
154
+ if isinstance(m, dict)
155
+ else m
156
+ )
157
+ for m in item["plot"]
158
+ ]
159
+ rest = {
160
+ k: encode_value(v, param_names, data_names)
161
+ for k, v in item.items()
162
+ if k != "plot"
163
+ }
164
+ return {"plot": marks, **rest}
165
+ return encode_value(item, param_names, data_names)
166
+
167
+
168
+ # Layout helpers
169
+ def vconcat(
170
+ *items: Any, param_names: dict[int, str] | None = None, **kwargs: Any
171
+ ) -> Any:
172
+ from .spec import View
173
+
174
+ return View(
175
+ {"vconcat": [_encode_component(i, param_names) for i in items]}, **kwargs
176
+ )
177
+
178
+
179
+ def hconcat(
180
+ *items: Any, param_names: dict[int, str] | None = None, **kwargs: Any
181
+ ) -> Any:
182
+ from .spec import View
183
+
184
+ return View(
185
+ {"hconcat": [_encode_component(i, param_names) for i in items]}, **kwargs
186
+ )
187
+
188
+
189
+ def hspace(value: int | str) -> Any:
190
+ from .spec import View
191
+
192
+ return View({"hspace": value})
193
+
194
+
195
+ def vspace(value: int | str) -> Any:
196
+ from .spec import View
197
+
198
+ return View({"vspace": value})
199
+
200
+
201
+ _MISSING = object()
202
+
203
+
204
+ def option(label: Any, value: Any = _MISSING) -> dict[str, Any]:
205
+ return {"label": label, "value": label if value is _MISSING else value}
206
+
207
+
208
+ def brush(**kwargs: Any) -> dict[str, Any]:
209
+ return {camelize(k): v for k, v in kwargs.items() if v is not None}
210
+
211
+
212
+ def sort(**kwargs: Any) -> dict[str, Any]:
213
+ return {k: v for k, v in kwargs.items() if v is not None}
214
+
215
+
216
+ # Interactor helpers
217
+ def _interactor(select: str, **opts: Any) -> dict[str, Any]:
218
+ return omit_none({"select": select, **opts})
219
+
220
+
221
+ def interval_x(
222
+ bind: Any = None,
223
+ field: Any = None,
224
+ pixel_size: Any = None,
225
+ peers: Any = None,
226
+ brush: Any = None,
227
+ ) -> dict[str, Any]:
228
+ return _interactor(
229
+ "intervalX",
230
+ **{
231
+ "as": bind,
232
+ "field": field,
233
+ "pixelSize": pixel_size,
234
+ "peers": peers,
235
+ "brush": brush,
236
+ },
237
+ )
238
+
239
+
240
+ def interval_y(
241
+ bind: Any = None,
242
+ field: Any = None,
243
+ pixel_size: Any = None,
244
+ peers: Any = None,
245
+ brush: Any = None,
246
+ ) -> dict[str, Any]:
247
+ return _interactor(
248
+ "intervalY",
249
+ **{
250
+ "as": bind,
251
+ "field": field,
252
+ "pixelSize": pixel_size,
253
+ "peers": peers,
254
+ "brush": brush,
255
+ },
256
+ )
257
+
258
+
259
+ def interval_xy(
260
+ bind: Any = None,
261
+ xfield: Any = None,
262
+ yfield: Any = None,
263
+ pixel_size: Any = None,
264
+ peers: Any = None,
265
+ brush: Any = None,
266
+ ) -> dict[str, Any]:
267
+ return _interactor(
268
+ "intervalXY",
269
+ **{
270
+ "as": bind,
271
+ "xfield": xfield,
272
+ "yfield": yfield,
273
+ "pixelSize": pixel_size,
274
+ "peers": peers,
275
+ "brush": brush,
276
+ },
277
+ )
278
+
279
+
280
+ def toggle(bind: Any = None, channels: Any = None, peers: Any = None) -> dict[str, Any]:
281
+ return _interactor("toggle", **{"as": bind, "channels": channels, "peers": peers})
282
+
283
+
284
+ def toggle_x(bind: Any = None, peers: Any = None) -> dict[str, Any]:
285
+ return _interactor("toggleX", **{"as": bind, "peers": peers})
286
+
287
+
288
+ def toggle_y(bind: Any = None, peers: Any = None) -> dict[str, Any]:
289
+ return _interactor("toggleY", **{"as": bind, "peers": peers})
290
+
291
+
292
+ def toggle_color(bind: Any = None, peers: Any = None) -> dict[str, Any]:
293
+ return _interactor("toggleColor", **{"as": bind, "peers": peers})
294
+
295
+
296
+ def nearest_x(
297
+ bind: Any = None, field: Any = None, channels: Any = None
298
+ ) -> dict[str, Any]:
299
+ return _interactor("nearestX", **{"as": bind, "field": field, "channels": channels})
300
+
301
+
302
+ def nearest_y(
303
+ bind: Any = None, field: Any = None, channels: Any = None
304
+ ) -> dict[str, Any]:
305
+ return _interactor("nearestY", **{"as": bind, "field": field, "channels": channels})
306
+
307
+
308
+ def region(
309
+ bind: Any = None, channels: Any = None, peers: Any = None, brush: Any = None
310
+ ) -> dict[str, Any]:
311
+ return _interactor(
312
+ "region", **{"as": bind, "channels": channels, "peers": peers, "brush": brush}
313
+ )
314
+
315
+
316
+ def highlight(by: Any = None, channels: Any = None, **kwargs: Any) -> dict[str, Any]:
317
+ return omit_none(
318
+ {
319
+ "select": "highlight",
320
+ "by": by,
321
+ "channels": channels,
322
+ **{camelize(k): v for k, v in kwargs.items()},
323
+ }
324
+ )
325
+
326
+
327
+ def pan(
328
+ x: Any = None, y: Any = None, xfield: Any = None, yfield: Any = None
329
+ ) -> dict[str, Any]:
330
+ return _interactor("pan", x=x, y=y, xfield=xfield, yfield=yfield)
331
+
332
+
333
+ def pan_x(x: Any = None, xfield: Any = None) -> dict[str, Any]:
334
+ return _interactor("panX", x=x, xfield=xfield)
335
+
336
+
337
+ def pan_y(y: Any = None, yfield: Any = None) -> dict[str, Any]:
338
+ return _interactor("panY", y=y, yfield=yfield)
339
+
340
+
341
+ def pan_zoom(
342
+ x: Any = None, y: Any = None, xfield: Any = None, yfield: Any = None
343
+ ) -> dict[str, Any]:
344
+ return _interactor("panZoom", x=x, y=y, xfield=xfield, yfield=yfield)
345
+
346
+
347
+ def pan_zoom_x(x: Any = None, xfield: Any = None) -> dict[str, Any]:
348
+ return _interactor("panZoomX", x=x, xfield=xfield)
349
+
350
+
351
+ def pan_zoom_y(y: Any = None, yfield: Any = None) -> dict[str, Any]:
352
+ return _interactor("panZoomY", y=y, yfield=yfield)
353
+
354
+
355
+ # Legend helpers
356
+ def _legend(
357
+ kind: str,
358
+ plot: Any = None,
359
+ bind: Any = None,
360
+ label: Any = None,
361
+ columns: Any = None,
362
+ **kwargs: Any,
363
+ ) -> dict[str, Any]:
364
+ return omit_none(
365
+ {
366
+ "legend": kind,
367
+ "for": plot,
368
+ "as": bind,
369
+ "label": label,
370
+ "columns": columns,
371
+ **{camelize(k): v for k, v in kwargs.items()},
372
+ }
373
+ )
374
+
375
+
376
+ def color_legend(
377
+ plot: Any = None,
378
+ bind: Any = None,
379
+ label: Any = None,
380
+ columns: Any = None,
381
+ **kwargs: Any,
382
+ ) -> dict[str, Any]:
383
+ return _legend(
384
+ "color", plot=plot, bind=bind, label=label, columns=columns, **kwargs
385
+ )
386
+
387
+
388
+ def opacity_legend(
389
+ plot: Any = None,
390
+ bind: Any = None,
391
+ label: Any = None,
392
+ columns: Any = None,
393
+ **kwargs: Any,
394
+ ) -> dict[str, Any]:
395
+ return _legend(
396
+ "opacity", plot=plot, bind=bind, label=label, columns=columns, **kwargs
397
+ )
398
+
399
+
400
+ def symbol_legend(
401
+ plot: Any = None,
402
+ bind: Any = None,
403
+ label: Any = None,
404
+ columns: Any = None,
405
+ **kwargs: Any,
406
+ ) -> dict[str, Any]:
407
+ return _legend(
408
+ "symbol", plot=plot, bind=bind, label=label, columns=columns, **kwargs
409
+ )
410
+
411
+
412
+ # Input helpers
413
+ _INPUT_ALIASES = {"bind": "as", "source": "from"}
414
+
415
+
416
+ def input(kind: str, **opts: Any) -> Any:
417
+ from .spec import View
418
+
419
+ payload: dict[str, Any] = {"input": kind}
420
+ for k, v in opts.items():
421
+ if v is None:
422
+ continue
423
+ key = _INPUT_ALIASES.get(k, camelize(k))
424
+ if (
425
+ key == "options"
426
+ and isinstance(v, list)
427
+ and any(isinstance(i, dict) for i in v)
428
+ ):
429
+ v = [{"label": i, "value": i} if not isinstance(i, dict) else i for i in v]
430
+ payload[key] = v
431
+ return View(payload)
432
+
433
+
434
+ def slider(
435
+ label: str,
436
+ bind: Any,
437
+ min: Any = None,
438
+ max: Any = None,
439
+ step: Any = None,
440
+ value: Any = None,
441
+ **opts: Any,
442
+ ) -> Any:
443
+ return input(
444
+ "slider",
445
+ label=label,
446
+ bind=bind,
447
+ min=min,
448
+ max=max,
449
+ step=step,
450
+ value=value,
451
+ **opts,
452
+ )
453
+
454
+
455
+ def select(
456
+ label: str,
457
+ bind: Any,
458
+ options: Any,
459
+ multiple: bool = False,
460
+ value: Any = None,
461
+ **opts: Any,
462
+ ) -> Any:
463
+ return input(
464
+ "select",
465
+ label=label,
466
+ bind=bind,
467
+ options=options,
468
+ multiple=multiple,
469
+ value=value,
470
+ **opts,
471
+ )
472
+
473
+
474
+ def checkbox(label: str, bind: Any, value: bool = False, **opts: Any) -> Any:
475
+ return input("checkbox", label=label, bind=bind, value=value, **opts)
476
+
477
+
478
+ def menu(
479
+ label: Any = None,
480
+ bind: Any = None,
481
+ options: Any = None,
482
+ value: Any = None,
483
+ source: Any = None,
484
+ column: Any = None,
485
+ filter_by: Any = None,
486
+ **opts: Any,
487
+ ) -> Any:
488
+ return input(
489
+ "menu",
490
+ label=label,
491
+ bind=bind,
492
+ options=options,
493
+ value=value,
494
+ source=source,
495
+ column=column,
496
+ filter_by=filter_by,
497
+ **opts,
498
+ )
499
+
500
+
501
+ def search(
502
+ label: Any = None,
503
+ bind: Any = None,
504
+ source: Any = None,
505
+ column: Any = None,
506
+ filter_by: Any = None,
507
+ type: Any = None,
508
+ **opts: Any,
509
+ ) -> Any:
510
+ return input(
511
+ "search",
512
+ label=label,
513
+ bind=bind,
514
+ source=source,
515
+ column=column,
516
+ filter_by=filter_by,
517
+ type=type,
518
+ **opts,
519
+ )
520
+
521
+
522
+ def table_input(
523
+ source: Any,
524
+ bind: Any = None,
525
+ columns: Any = None,
526
+ filter_by: Any = None,
527
+ width: Any = None,
528
+ height: Any = None,
529
+ max_width: Any = None,
530
+ row_batch: Any = None,
531
+ align: Any = None,
532
+ **opts: Any,
533
+ ) -> Any:
534
+ return input(
535
+ "table",
536
+ source=source,
537
+ bind=bind,
538
+ columns=columns,
539
+ filter_by=filter_by,
540
+ width=width,
541
+ height=height,
542
+ max_width=max_width,
543
+ row_batch=row_batch,
544
+ align=align,
545
+ **opts,
546
+ )