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.
hiplot/__init__.py ADDED
@@ -0,0 +1,38 @@
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 . import fetchers
6
+ from .experiment import (
7
+ Datapoint,
8
+ Displays,
9
+ Experiment,
10
+ ExperimentDisplayed,
11
+ ExperimentFetcherDoesntApply,
12
+ ExperimentValidationCircularRef,
13
+ ExperimentValidationError,
14
+ ExperimentValidationMissingParent,
15
+ ValueDef,
16
+ ValueType,
17
+ )
18
+ from .pkginfo import __version__, package_name
19
+ from .server import run_server, run_server_main
20
+
21
+ __all__ = [
22
+ "Datapoint",
23
+ "Displays",
24
+ "Experiment",
25
+ "ExperimentDisplayed",
26
+ "ExperimentFetcherDoesntApply",
27
+ "ExperimentValidationCircularRef",
28
+ "ExperimentValidationError",
29
+ "ExperimentValidationMissingParent",
30
+ "ValueDef",
31
+ "ValueType",
32
+ "__package__",
33
+ "__version__",
34
+ "fetchers",
35
+ "package_name",
36
+ "run_server",
37
+ "run_server_main",
38
+ ]
hiplot/__main__.py ADDED
@@ -0,0 +1,10 @@
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 sys
6
+
7
+ from .server import run_server_main
8
+
9
+ # Only ever executed by `python -m hiplot`, so it needs no __name__ guard.
10
+ sys.exit(run_server_main())
hiplot/compress.py ADDED
@@ -0,0 +1,70 @@
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
+ """Compact encodings of an experiment's datapoints for the frontend.
5
+
6
+ `src/lib/compress.ts` decodes both formats; the two sides change together.
7
+ """
8
+
9
+ import math
10
+ from collections.abc import Iterable, Sequence
11
+ from typing import Any
12
+
13
+ from .experiment import Datapoint
14
+
15
+ RESERVED_COLUMNS = frozenset(("uid", "from_uid"))
16
+
17
+ #: Key of the JSON object standing for a non-finite float, which JSON cannot represent.
18
+ NON_FINITE_KEY = "$f"
19
+
20
+
21
+ def _build_columns_list(datapoints: Iterable[Datapoint]) -> list[str]:
22
+ """Value columns in first-seen order, so the output is deterministic."""
23
+ columns: dict[str, None] = {}
24
+ for dp in datapoints:
25
+ columns.update(dict.fromkeys(dp.values))
26
+ return [column for column in columns if column not in RESERVED_COLUMNS]
27
+
28
+
29
+ def compress(datapoints: list[Datapoint]) -> dict[str, Any]:
30
+ """Row-major encoding: column names once, then one array per row."""
31
+ columns = _build_columns_list(datapoints)
32
+ rows = [[dp.uid, dp.from_uid, *(dp.values.get(c) for c in columns)] for dp in datapoints]
33
+ return {"columns": columns, "rows": rows}
34
+
35
+
36
+ def encode_value(value: Any) -> Any:
37
+ """A JSON-safe value: non-finite floats become {"$f": "inf" | "-inf" | "nan"}."""
38
+ if isinstance(value, float) and not math.isfinite(value):
39
+ return {NON_FINITE_KEY: "nan" if math.isnan(value) else ("inf" if value > 0 else "-inf")}
40
+ return value
41
+
42
+
43
+ def compress_columnar(datapoints: Sequence[Datapoint]) -> dict[str, Any]:
44
+ """Column-major encoding, valid strict JSON.
45
+
46
+ Each column is one array with a value per row, so column names are not
47
+ repeated per row. A datapoint without a value for a column is listed in
48
+ `missing` (its slot holds null), keeping "absent" distinct from None.
49
+ """
50
+ columns = _build_columns_list(datapoints)
51
+ values: dict[str, list[Any]] = {}
52
+ missing: dict[str, list[int]] = {}
53
+ for column in columns:
54
+ column_values: list[Any] = []
55
+ column_missing: list[int] = []
56
+ for index, dp in enumerate(datapoints):
57
+ if column in dp.values:
58
+ column_values.append(encode_value(dp.values[column]))
59
+ else:
60
+ column_values.append(None)
61
+ column_missing.append(index)
62
+ values[column] = column_values
63
+ if column_missing:
64
+ missing[column] = column_missing
65
+ return {
66
+ "uid": [dp.uid for dp in datapoints],
67
+ "from_uid": [dp.from_uid for dp in datapoints],
68
+ "columns": values,
69
+ "missing": missing,
70
+ }
hiplot/csvvalues.py ADDED
@@ -0,0 +1,83 @@
1
+ """Reads the numbers out of CSV cells, which the csv module leaves as text.
2
+
3
+ A column is numeric when every one of its cells spells a number: decimal or
4
+ exponent notation, ``inf``/``infinity``/``nan`` in any case with an optional
5
+ sign, or a missing value: blank, or ``na``/``n/a``/``null``/``none`` in any
6
+ case. At least one cell must be finite. The
7
+ grammar is the one the frontend applies to text cells (``src/lib/d3_scales.ts``),
8
+ so both sides agree on which columns are numeric.
9
+ """
10
+
11
+ import math
12
+ import re
13
+ import typing as tp
14
+ from collections.abc import Iterable
15
+
16
+ _FINITE = re.compile(r"[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?")
17
+ _INTEGER = re.compile(r"[+-]?\d+")
18
+ _INFINITE = re.compile(r"([+-]?)(?:inf|infinity)", re.IGNORECASE)
19
+ _NAN = re.compile(r"[+-]?nan", re.IGNORECASE)
20
+ _MISSING = re.compile(r"na|n/a|null|none", re.IGNORECASE)
21
+
22
+ #: Columns that identify rows and are never read as numbers.
23
+ _IDENTIFIER_COLUMNS = frozenset({"uid", "from_uid"})
24
+
25
+
26
+ def parse_number(cell: str | None) -> int | float | None:
27
+ """The number a CSV cell spells: an int, a float (inf and nan included), or None when missing.
28
+
29
+ Blank cells, the missing-value spellings, and a short row's missing cells
30
+ (None from ``csv.DictReader``) are missing.
31
+
32
+ :raises ValueError: when the cell spells no number.
33
+ """
34
+ text = "" if cell is None else cell.strip()
35
+ if _is_missing_spelling(text):
36
+ return None
37
+ if _INTEGER.fullmatch(text):
38
+ return int(text)
39
+ if _FINITE.fullmatch(text):
40
+ return float(text)
41
+ infinite = _INFINITE.fullmatch(text)
42
+ if infinite is not None:
43
+ return -math.inf if infinite.group(1) == "-" else math.inf
44
+ if _NAN.fullmatch(text):
45
+ return math.nan
46
+ raise ValueError(f"Not a number: {cell!r}")
47
+
48
+
49
+ def _is_missing_spelling(text: str) -> bool:
50
+ return text == "" or _MISSING.fullmatch(text) is not None
51
+
52
+
53
+ def _is_number_spelling(cell: str | None) -> bool:
54
+ text = "" if cell is None else cell.strip()
55
+ return (
56
+ _is_missing_spelling(text)
57
+ or _FINITE.fullmatch(text) is not None
58
+ or _INFINITE.fullmatch(text) is not None
59
+ or _NAN.fullmatch(text) is not None
60
+ )
61
+
62
+
63
+ def _has_finite_number(cells: Iterable[str | None]) -> bool:
64
+ return any(cell is not None and _FINITE.fullmatch(cell.strip()) is not None for cell in cells)
65
+
66
+
67
+ def typed_rows(rows: Iterable[dict[str, str | None]]) -> list[dict[str, tp.Any]]:
68
+ """The rows of a ``csv.DictReader`` with the cells of each numeric column read as numbers.
69
+
70
+ Columns holding any other text keep all of their cells as text, so a column
71
+ never mixes numbers and strings.
72
+ """
73
+ table = list(rows)
74
+ columns = {column for row in table for column in row} - _IDENTIFIER_COLUMNS
75
+ numeric = {
76
+ column
77
+ for column in columns
78
+ if all(_is_number_spelling(row[column]) for row in table if column in row)
79
+ and _has_finite_number(row[column] for row in table if column in row)
80
+ }
81
+ return [
82
+ {column: parse_number(cell) if column in numeric else cell for column, cell in row.items()} for row in table
83
+ ]