From f533c65ae3b7d809dbdf4a10388dacac86084028 Mon Sep 17 00:00:00 2001 From: Fritz Lekschas Date: Sat, 7 Dec 2024 22:01:39 -0500 Subject: [PATCH] chore: ruff everything --- .github/workflows/ci.yml | 2 +- .pre-commit-config.yaml | 10 + jscatter/__init__.py | 4 +- jscatter/_cli.py | 58 +- jscatter/annotations.py | 16 +- jscatter/annotations_traits.py | 9 + jscatter/color_maps.py | 20 +- jscatter/compose.py | 46 +- jscatter/composite_annotations.py | 33 +- jscatter/encodings.py | 65 +- jscatter/jscatter.py | 980 ++++++++++++++++++---------- jscatter/types.py | 11 +- jscatter/utils.py | 35 +- jscatter/widget.py | 309 ++++++--- notebooks/annotations.ipynb | 6 +- notebooks/demo.ipynb | 15 +- notebooks/examples.ipynb | 41 +- notebooks/get-started.ipynb | 19 +- notebooks/linking.ipynb | 66 +- pyproject.toml | 9 +- test-environments/test.ipynb | 1 + tests/test_annotations.py | 24 +- tests/test_composite_annotations.py | 6 +- tests/test_jscatter.py | 166 +++-- tests/test_utils.py | 2 +- uv.lock | 77 +++ 26 files changed, 1308 insertions(+), 722 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ca85720..c8f4b98e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: with: version: "0.4.x" - run: | - uv run ruff check + uv run ruff format --check Test: runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..ed826d40 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.8.2 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/jscatter/__init__.py b/jscatter/__init__.py index 0f2e1e9c..09555026 100644 --- a/jscatter/__init__.py +++ b/jscatter/__init__.py @@ -4,9 +4,9 @@ from importlib_metadata import PackageNotFoundError, version # type: ignore try: - __version__ = version("jupyter-scatter") + __version__ = version('jupyter-scatter') except PackageNotFoundError: - __version__ = "uninstalled" + __version__ = 'uninstalled' from .jscatter import Scatter, plot from .annotations import Line, HLine, VLine, Rect diff --git a/jscatter/_cli.py b/jscatter/_cli.py index 4dee7129..e607dc40 100644 --- a/jscatter/_cli.py +++ b/jscatter/_cli.py @@ -8,44 +8,47 @@ from jscatter import __version__ DEV = False -IS_WINDOWS = sys.platform.startswith("win") +IS_WINDOWS = sys.platform.startswith('win') + def download_demo_notebook() -> Path: notebook = pooch.retrieve( - url=f"https://github.com/flekschas/jupyter-scatter/raw/refs/tags/v{__version__}/notebooks/demo.ipynb", - path=pooch.os_cache("jupyter-scatter"), - fname="demo.ipynb", + url=f'https://github.com/flekschas/jupyter-scatter/raw/refs/tags/v{__version__}/notebooks/demo.ipynb', + path=pooch.os_cache('jupyter-scatter'), + fname='demo.ipynb', known_hash=None, ) return Path(notebook) + def check_uv_available(): - if shutil.which("uv") is None: + if shutil.which('uv') is None: print("Error: 'uv' command not found.", file=sys.stderr) print("Please install 'uv' to run `jscatter demo` entrypoint.", file=sys.stderr) print( - "For more information, visit: https://github.com/astral-sh/uv", + 'For more information, visit: https://github.com/astral-sh/uv', file=sys.stderr, ) sys.exit(1) + def run_notebook(notebook_path: Path): check_uv_available() command = [ - "uv", - "tool", - "run", - "--python", - "3.12", - "--from", - "jupyter-core", - "--with", - "jupyterlab", - "--with", - "." if DEV else f"jupyter-scatter=={__version__}", - "jupyter", - "lab", + 'uv', + 'tool', + 'run', + '--python', + '3.12', + '--from', + 'jupyter-core', + '--with', + 'jupyterlab', + '--with', + '.' if DEV else f'jupyter-scatter=={__version__}', + 'jupyter', + 'lab', str(notebook_path), ] @@ -56,24 +59,25 @@ def run_notebook(notebook_path: Path): completed_process = subprocess.run(command) sys.exit(completed_process.returncode) except subprocess.CalledProcessError as e: - print(f"Error executing {command[0]}: {e}", file=sys.stderr) + print(f'Error executing {command[0]}: {e}', file=sys.stderr) sys.exit(1) else: try: os.execvp(command[0], command) except OSError as e: - print(f"Error executing {command[0]}: {e}", file=sys.stderr) + print(f'Error executing {command[0]}: {e}', file=sys.stderr) sys.exit(1) + def main(): - parser = argparse.ArgumentParser(prog="jupyter-scatter") - subparsers = parser.add_subparsers(dest="command", help="Available commands") - subparsers.add_parser("demo", help=f"Run the demo notebook in JupyterLab") + parser = argparse.ArgumentParser(prog='jupyter-scatter') + subparsers = parser.add_subparsers(dest='command', help='Available commands') + subparsers.add_parser('demo', help=f'Run the demo notebook in JupyterLab') args = parser.parse_args() - if args.command == "demo": + if args.command == 'demo': if DEV: - notebook_path = Path(__file__).parent.parent / "notebooks" / "demo.ipynb" + notebook_path = Path(__file__).parent.parent / 'notebooks' / 'demo.ipynb' else: notebook_path = download_demo_notebook() @@ -82,5 +86,5 @@ def main(): parser.print_help() -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/jscatter/annotations.py b/jscatter/annotations.py index 7a4acfa8..25197f2b 100644 --- a/jscatter/annotations.py +++ b/jscatter/annotations.py @@ -11,8 +11,9 @@ DEFAULT_LINE_COLOR = '#000000' DEFAULT_LINE_WIDTH = 1 + @dataclass -class HLine(): +class HLine: """ A horizontal line annotation. @@ -41,6 +42,7 @@ class HLine(): >>> HLine(0) HLine(y=0, x_start=None, x_end=None, line_color=(0.0, 0.0, 0.0, 1.0), line_width=1) """ + y: float x_start: Optional[float] = DEFAULT_1D_LINE_START x_end: Optional[float] = DEFAULT_1D_LINE_END @@ -50,8 +52,9 @@ class HLine(): def __post_init__(self): self.line_color = to_rgba(self.line_color) + @dataclass -class VLine(): +class VLine: """ A vertical line annotation. @@ -80,6 +83,7 @@ class VLine(): >>> VLine(0) VLine(x=0, y_start=None, y_end=None, line_color=(0.0, 0.0, 0.0, 1.0), line_width=1) """ + x: float y_start: Optional[float] = DEFAULT_1D_LINE_START y_end: Optional[float] = DEFAULT_1D_LINE_END @@ -89,8 +93,9 @@ class VLine(): def __post_init__(self): self.line_color = to_rgba(self.line_color) + @dataclass -class Rect(): +class Rect: """ A rectangle annotation. @@ -122,6 +127,7 @@ class Rect(): >>> Rect(0) Rect(x_start=-1, x_end=1, y_start=-1, y_end=1, line_color=(0.0, 0.0, 0.0, 1.0), line_width=1) """ + x_start: float x_end: float y_start: float @@ -132,8 +138,9 @@ class Rect(): def __post_init__(self): self.line_color = to_rgba(self.line_color) + @dataclass -class Line(): +class Line: """ A line annotation. @@ -156,6 +163,7 @@ class Line(): >>> Line([(-1, -1), (0, 0), (1, 1)]) Line(vertices=[(-1, -1), (0, 0), (1, 1)], line_color=(0.0, 0.0, 0.0, 1.0), line_width=1) """ + vertices: List[Tuple[float]] line_color: Color = DEFAULT_LINE_COLOR line_width: int = DEFAULT_LINE_WIDTH diff --git a/jscatter/annotations_traits.py b/jscatter/annotations_traits.py index 777d8497..11f5b1e0 100644 --- a/jscatter/annotations_traits.py +++ b/jscatter/annotations_traits.py @@ -13,6 +13,7 @@ Rect as ARect, ) + class Line(TraitType): info_text = 'line annotation' @@ -21,6 +22,7 @@ def validate(self, obj: Any, value: Any): return value self.error(obj, value) + class HLine(TraitType): info_text = 'horizontal line annotation' @@ -29,6 +31,7 @@ def validate(self, obj: Any, value: Any): return value self.error(obj, value) + class VLine(TraitType): info_text = 'vertical line annotation' @@ -37,6 +40,7 @@ def validate(self, obj: Any, value: Any): return value self.error(obj, value) + class Rect(TraitType): info_text = 'rectangle annotation' @@ -45,15 +49,18 @@ def validate(self, obj: Any, value: Any): return value self.error(obj, value) + def to_json(value, *args, **kwargs): d = None if value is None else asdict(value) return json.dumps(d, allow_nan=False) + def annotations_to_json(value, *args, **kwargs): if value is None: return None return [to_json(v) for v in value] + def from_json(value, *args, **kwargs): d = json.loads(value) @@ -71,6 +78,7 @@ def from_json(value, *args, **kwargs): return None + def annotations_from_json(value): value = json.loads(value) @@ -79,6 +87,7 @@ def annotations_from_json(value): return [from_json(v) for v in value] + serialization = dict( to_json=annotations_to_json, from_json=annotations_from_json, diff --git a/jscatter/color_maps.py b/jscatter/color_maps.py index 3ecd9bab..9f944ad5 100644 --- a/jscatter/color_maps.py +++ b/jscatter/color_maps.py @@ -1,14 +1,14 @@ from matplotlib.colors import to_rgba okabe_ito = [ - to_rgba('#56B4E9'), # sky blue - to_rgba('#E69F00'), # orange - to_rgba('#009E73'), # blueish green - to_rgba('#F0E442'), # yellow - to_rgba('#0072B2'), # blue - to_rgba('#D55E00'), # vermilion - to_rgba('#CC79A7'), # reddish purple - to_rgba('#000000'), # black + to_rgba('#56B4E9'), # sky blue + to_rgba('#E69F00'), # orange + to_rgba('#009E73'), # blueish green + to_rgba('#F0E442'), # yellow + to_rgba('#0072B2'), # blue + to_rgba('#D55E00'), # vermilion + to_rgba('#CC79A7'), # reddish purple + to_rgba('#000000'), # black ] glasbey_light = [ @@ -267,7 +267,7 @@ to_rgba('#247c2a'), to_rgba('#826723'), to_rgba('#bfbc4d'), - to_rgba('#ddd3a5') + to_rgba('#ddd3a5'), ] glasbey_dark = [ @@ -526,5 +526,5 @@ to_rgba('#a7423b'), to_rgba('#006e70'), to_rgba('#97833d'), - to_rgba('#dbafc8') + to_rgba('#dbafc8'), ] diff --git a/jscatter/compose.py b/jscatter/compose.py index 4e4a6d64..ac3d9bc6 100644 --- a/jscatter/compose.py +++ b/jscatter/compose.py @@ -5,9 +5,10 @@ from .jscatter import Scatter -TITLE_HEIGHT = 28; -AXES_LABEL_SIZE = 16; -AXES_PADDING_Y = 20; +TITLE_HEIGHT = 28 +AXES_LABEL_SIZE = 16 +AXES_PADDING_Y = 20 + def compose( scatters: Union[List[Scatter], List[Tuple[Scatter, str]]], @@ -68,7 +69,9 @@ def compose( rows = 1 if isinstance(match_by, list): - assert len(scatters) == len(match_by), 'The number of scatters and match_bys need to be the same' + assert len(scatters) == len( + match_by + ), 'The number of scatters and match_bys need to be the same' elif match_by != 'index': match_by = [match_by] * len(scatters) @@ -82,10 +85,12 @@ def get_scatter(i: int) -> Scatter: def get_title(i: int) -> str: if has_titles and isinstance(scatters[i], tuple): return scatters[i][1] - return " " + return ' ' if isinstance(match_by, list): - assert all([match_by[i] in get_scatter(i)._data for i, _ in enumerate(scatters)]) + assert all( + [match_by[i] in get_scatter(i)._data for i, _ in enumerate(scatters)] + ) # We need to store the specific handlers created by called # `create_select_handler(index)` and `create_hover_handler(index)` to @@ -127,7 +132,9 @@ def map_single(scatter, matched_id): return matched_id try: - return scatter._data.query(f'{match_by[index]} == @matched_id').index.tolist()[0] + return scatter._data.query( + f'{match_by[index]} == @matched_id' + ).index.tolist()[0] except IndexError: return -1 @@ -135,7 +142,9 @@ def map_multiple(scatter, matched_ids): if match_by == 'index': return matched_ids - return scatter._data.query(f'{match_by[index]} in @matched_ids').index.tolist() + return scatter._data.query( + f'{match_by[index]} in @matched_ids' + ).index.tolist() return map_multiple if multiple else map_single @@ -201,19 +210,13 @@ def hover_handler(change): return hover_handler - has_axes = any([ - get_scatter(i)._axes != False - for i, _ - in enumerate(scatters) - ]) + has_axes = any([get_scatter(i)._axes != False for i, _ in enumerate(scatters)]) y_padding = AXES_PADDING_Y if has_axes else 0 if has_axes: - has_labels = any([ - get_scatter(i)._axes_labels != False - for i, _ - in enumerate(scatters) - ]) + has_labels = any( + [get_scatter(i)._axes_labels != False for i, _ in enumerate(scatters)] + ) y_padding = y_padding + AXES_LABEL_SIZE if has_labels else y_padding y_padding = y_padding + TITLE_HEIGHT if has_titles else y_padding @@ -261,16 +264,17 @@ def get_scatter_widget(i): layout=Layout( grid_template_columns=' '.join(['1fr' for x in range(cols)]), grid_template_rows=' '.join([f'{row_height}px' for x in range(rows)]), - grid_gap='2px' - ) + grid_gap='2px', + ), ) + def link( scatters: Union[List[Scatter], List[Tuple[Scatter, str]]], match_by: Union[str, List[str]] = 'index', rows: Optional[int] = 1, row_height: int = 320, - cols: Optional[int] = None + cols: Optional[int] = None, ): """ A short-hand function for `compose` that composes multiple `Scatter` diff --git a/jscatter/composite_annotations.py b/jscatter/composite_annotations.py index e255aaf5..a664b458 100644 --- a/jscatter/composite_annotations.py +++ b/jscatter/composite_annotations.py @@ -10,9 +10,17 @@ from matplotlib.colors import to_rgba from typing import List, Optional, Union -from .annotations import HLine, VLine, Line, Rect, DEFAULT_LINE_COLOR, DEFAULT_LINE_WIDTH +from .annotations import ( + HLine, + VLine, + Line, + Rect, + DEFAULT_LINE_COLOR, + DEFAULT_LINE_WIDTH, +) from .types import Color + class CompositeAnnotation(metaclass=ABCMeta): @abstractmethod def get_annotations(self, scatter) -> List[Union[HLine, VLine, Line, Rect]]: @@ -46,15 +54,16 @@ class Contour(CompositeAnnotation): See https://en.wikipedia.org/wiki/Contour_line for more information on what a contour line plot is. """ + def __init__( self, by: Optional[str] = None, line_color: Optional[Color] = None, line_width: Optional[int] = None, line_opacity_by_level: Optional[bool] = False, - **kwargs + **kwargs, ): - if sys.version_info < (3,9): + if sys.version_info < (3, 9): raise Exception('The contour line annotation requires at least Python v3.9') self.by = by @@ -64,8 +73,7 @@ def __init__( self.sns_kdeplot_kws = { key: value - for key, value - in kwargs + for key, value in kwargs if key in inspect.getfullargspec(sns.kdeplot).kwonlyargs } @@ -101,17 +109,10 @@ def get_width(): return self.line_width - axes = sns.kdeplot( - data=data, - x=x, - y=y, - hue=hue, - **self.sns_kdeplot_kws - ) + axes = sns.kdeplot(data=data, x=x, y=y, hue=hue, **self.sns_kdeplot_kws) # The `;` is important! Otherwise the plot will be rendered in Jupyter # Notebook/Lab - plt.close(); - + plt.close() lines = [] for k, collection in enumerate(axes.collections): @@ -125,12 +126,12 @@ def get_width(): subpaths = np.vsplit(path.vertices, polygon_end + 1) for subpath in subpaths: - vertices = list(zip(subpath[:,0], subpath[:,1])) + vertices = list(zip(subpath[:, 0], subpath[:, 1])) lines.append( Line( vertices, line_color=get_color(k, l + 1), - line_width=get_width() + line_width=get_width(), ) ) diff --git a/jscatter/encodings.py b/jscatter/encodings.py index ae01f0c7..3589b7bc 100644 --- a/jscatter/encodings.py +++ b/jscatter/encodings.py @@ -4,19 +4,28 @@ from math import floor from typing import List, Tuple, Union, Optional -def create_legend(encoding, norm, categories, labeling=None, linspace_num=5, category_order=None): + +def create_legend( + encoding, norm, categories, labeling=None, linspace_num=5, category_order=None +): variable = labeling.get('variable') if labeling else None values = [] if categories: - assert len(categories) <= len(encoding), 'There need as many or more encodings than categories' - cat_by_idx = { cat_idx: cat for cat, cat_idx in categories.items() } + assert len(categories) <= len( + encoding + ), 'There need as many or more encodings than categories' + cat_by_idx = {cat_idx: cat for cat, cat_idx in categories.items()} idxs = ( - sorted(cat_by_idx.keys()) # category codes + sorted(cat_by_idx.keys()) # category codes if category_order is None else map(categories.get, category_order) ) - values = [(cat_by_idx[idx], encoding[i], None) for i, idx in enumerate(idxs) if idx is not None] + values = [ + (cat_by_idx[idx], encoding[i], None) + for i, idx in enumerate(idxs) + if idx is not None + ] else: values = [ (norm.inverse(s), encoding[floor((len(encoding) - 1) * s)], None) @@ -27,15 +36,11 @@ def create_legend(encoding, norm, categories, labeling=None, linspace_num=5, cat values[0] = (*values[0][0:2], labeling.get('minValue')) values[-1] = (*values[-1][0:2], labeling.get('maxValue')) - return dict( - variable=variable, - values=values, - categorical=categories is not None - ) + return dict(variable=variable, values=values, categorical=categories is not None) -class Component(): - def __init__(self, index, reserved = False): +class Component: + def __init__(self, index, reserved=False): self._index = index self._reserved = reserved self._encoding = None @@ -69,8 +74,8 @@ def clear(self): self.prepared = False -class Components(): - def __init__(self, total = 4, reserved = 2): +class Components: + def __init__(self, total=4, reserved=2): # When using a RGBA float texture to store points, the first two # components (red and green) are reserved for the x and y coordinate self.total = total @@ -82,9 +87,7 @@ def __init__(self, total = 4, reserved = 2): @property def size(self): return reduce( - lambda acc, i: acc + int(self._components[i].used), - self._components, - 0 + lambda acc, i: acc + int(self._components[i].used), self._components, 0 ) @property @@ -106,14 +109,14 @@ def delete(self, encoding): @dataclass -class VisualEncoding(): +class VisualEncoding: channel: str # Visual channel. I.e., color, opacity, or size dimension: str # Data dimension I.e., f'{column_name}_{norm}' legend: Optional[List[Tuple[float, Union[float, int, str]]]] = None -class Encodings(): - def __init__(self, total_components = 4, reserved_components = 2): +class Encodings: + def __init__(self, total_components=4, reserved_components=2): self.data = {} self.visual = {} self.max = total_components - reserved_components @@ -125,7 +128,9 @@ def set(self, channel: str, dimension: str): self.delete(channel) if dimension not in self.data: - assert not self.components.full, f'Only {self.max} data encodings are supported' + assert ( + not self.components.full + ), f'Only {self.max} data encodings are supported' # The first value specifies the component # The second value self.data[dimension] = self.components.add(dimension) @@ -146,9 +151,9 @@ def set_legend( encoding, norm, categories, - labeling = None, - linspace_num = 5, - category_order = None, + labeling=None, + linspace_num=5, + category_order=None, ): if channel in self.visual: self.visual[channel].legend = create_legend( @@ -174,6 +179,12 @@ def is_unique(self, channel): if channel not in self.visual: return False - return sum( - [v.dimension == self.visual[channel].dimension for v in self.visual.values()] - ) == 1 + return ( + sum( + [ + v.dimension == self.visual[channel].dimension + for v in self.visual.values() + ] + ) + == 1 + ) diff --git a/jscatter/jscatter.py b/jscatter/jscatter.py index ae72f0f8..5b3fc3cf 100644 --- a/jscatter/jscatter.py +++ b/jscatter/jscatter.py @@ -5,7 +5,14 @@ import pandas as pd import warnings -from matplotlib.colors import to_rgba, Normalize, LogNorm, PowerNorm, LinearSegmentedColormap, ListedColormap +from matplotlib.colors import ( + to_rgba, + Normalize, + LogNorm, + PowerNorm, + LinearSegmentedColormap, + ListedColormap, +) from typing import Dict, Optional, Union, List, Tuple from .annotations import Line, HLine, VLine, Rect @@ -13,8 +20,37 @@ from .encodings import Encodings from .widget import JupyterScatter, SELECTION_DTYPE from .color_maps import okabe_ito, glasbey_light, glasbey_dark -from .utils import any_not, to_ndc, tolist, uri_validator, to_scale_type, get_scale_type_from_df, get_domain_from_df, create_default_norm, create_labeling, get_histogram_from_df, sanitize_tooltip_properties, zerofy_missing_values -from .types import Auto, Color, Scales, MouseModes, Auto, Reverse, Segment, Size, LegendPosition, VisualProperty, Labeling, TooltipPreviewType, TooltipPreviewImagePosition, TooltipPreviewImageSize, Undefined +from .utils import ( + any_not, + to_ndc, + tolist, + uri_validator, + to_scale_type, + get_scale_type_from_df, + get_domain_from_df, + create_default_norm, + create_labeling, + get_histogram_from_df, + sanitize_tooltip_properties, + zerofy_missing_values, +) +from .types import ( + Auto, + Color, + Scales, + MouseModes, + Auto, + Reverse, + Segment, + Size, + LegendPosition, + VisualProperty, + Labeling, + TooltipPreviewType, + TooltipPreviewImagePosition, + TooltipPreviewImageSize, + Undefined, +) COMPONENT_CONNECT = 4 COMPONENT_CONNECT_ORDER = 5 @@ -40,16 +76,22 @@ 'size', ] + def get_non_visual_properties(properties): return [c for c in properties if c not in visual_properties] + def check_encoding_dtype(series): if not any([check(series.dtype) for check in VALID_ENCODING_TYPES]): - raise ValueError(f'{series.name} is of an unsupported data type: {series.dtype}. Must be one of float*, int*, category, or string.') + raise ValueError( + f'{series.name} is of an unsupported data type: {series.dtype}. Must be one of float*, int*, category, or string.' + ) + def is_categorical_data(data): return pd.CategoricalDtype.is_dtype(data) or pd.api.types.is_string_dtype(data) + def get_categorical_data(data): categorical_data = None @@ -60,6 +102,7 @@ def get_categorical_data(data): return categorical_data + def component_idx_to_name(idx): if idx == 2: return 'valueA' @@ -69,6 +112,7 @@ def component_idx_to_name(idx): return None + def order_map(map, order): ordered_map = map @@ -78,7 +122,8 @@ def order_map(map, order): except TypeError: pass - return ordered_map[::(1 + (-2 * (order == 'reverse')))] + return ordered_map[:: (1 + (-2 * (order == 'reverse')))] + def get_map_order(map, categories): map_keys = list(map.keys()) @@ -90,6 +135,7 @@ def get_map_order(map, categories): return order + def get_scale(scatter: Scatter, channel: str): return to_scale_type( getattr(scatter, f'_{channel}_norm') @@ -97,14 +143,16 @@ def get_scale(scatter: Scatter, channel: str): else None ) + def get_domain(scatter: Scatter, channel: str): if getattr(scatter, f'_{channel}_categories') is None: return ( getattr(scatter, f'_{channel}_norm').vmin, - getattr(scatter, f'_{channel}_norm').vmax + getattr(scatter, f'_{channel}_norm').vmax, ) return getattr(scatter, f'_{channel}_categories') + def get_histogram_bins(bins: Union[int, Dict[str, int]], property: str): if isinstance(bins, dict): return bins.get(property, DEFAULT_HISTOGRAM_BINS) @@ -114,6 +162,7 @@ def get_histogram_bins(bins: Union[int, Dict[str, int]], property: str): return DEFAULT_HISTOGRAM_BINS + def get_histogram_range(ranges, property): if isinstance(ranges, dict): return ranges.get(property) @@ -123,6 +172,7 @@ def get_histogram_range(ranges, property): return None + def normalize_annotation(annotation, x_scale, y_scale): def to_ndc(value, norm): return (norm(value) * 2) - 1 @@ -130,8 +180,12 @@ def to_ndc(value, norm): if isinstance(annotation, HLine): return HLine( y=to_ndc(annotation.y, y_scale), - x_start=None if annotation.x_start is None else to_ndc(annotation.x_start, x_scale), - x_end=None if annotation.x_end is None else to_ndc(annotation.x_end, x_scale), + x_start=None + if annotation.x_start is None + else to_ndc(annotation.x_start, x_scale), + x_end=None + if annotation.x_end is None + else to_ndc(annotation.x_end, x_scale), line_color=annotation.line_color, line_width=annotation.line_width, ) @@ -139,17 +193,21 @@ def to_ndc(value, norm): if isinstance(annotation, VLine): return VLine( x=to_ndc(annotation.x, x_scale), - y_start=None if annotation.y_start is None else to_ndc(annotation.y_start, y_scale), - y_end=None if annotation.y_end is None else to_ndc(annotation.y_end, y_scale), + y_start=None + if annotation.y_start is None + else to_ndc(annotation.y_start, y_scale), + y_end=None + if annotation.y_end is None + else to_ndc(annotation.y_end, y_scale), line_color=annotation.line_color, line_width=annotation.line_width, ) - if isinstance(annotation, Line): return Line( vertices=[ - (to_ndc(v[0], x_scale), to_ndc(v[1], y_scale)) for v in annotation.vertices + (to_ndc(v[0], x_scale), to_ndc(v[1], y_scale)) + for v in annotation.vertices ], line_color=annotation.line_color, line_width=annotation.line_width, @@ -165,14 +223,15 @@ def to_ndc(value, norm): line_width=annotation.line_width, ) + def normalize_annotations(annotations, x_scale, y_scale): if annotations is None: return None return [normalize_annotation(a, x_scale, y_scale) for a in annotations] + def get_annotations( - scatter: Scatter, - annotations: List[Union[Line, VLine, HLine, Rect, Contour]] + scatter: Scatter, annotations: List[Union[Line, VLine, HLine, Rect, Contour]] ): base_annotations = [] @@ -184,13 +243,14 @@ def get_annotations( return base_annotations -class Scatter(): + +class Scatter: def __init__( self, x: Union[str, List[float], np.ndarray], y: Union[str, List[float], np.ndarray], data: Optional[pd.DataFrame] = None, - **kwargs + **kwargs, ): """ Create a scatter instance. @@ -277,9 +337,15 @@ def __init__( self._y_data = None self._y_by = None self._y_histogram = None - self._color = (0, 0, 0, 0.66) if self._background_color_luminance > 0.5 else (1, 1, 1, 0.66) + self._color = ( + (0, 0, 0, 0.66) + if self._background_color_luminance > 0.5 + else (1, 1, 1, 0.66) + ) self._color_selected = (0, 0.55, 1, 1) - self._color_hover = (0, 0, 0, 1) if self._background_color_luminance > 0.5 else (1, 1, 1, 1) + self._color_hover = ( + (0, 0, 0, 1) if self._background_color_luminance > 0.5 else (1, 1, 1, 1) + ) self._color_data = None self._color_by = None self._color_map = None @@ -314,9 +380,15 @@ def __init__( self._connect_by_data = None self._connect_order = None self._connect_order_data = None - self._connection_color = (0, 0, 0, 0.1) if self._background_color_luminance > 0.5 else (1, 1, 1, 0.1) + self._connection_color = ( + (0, 0, 0, 0.1) if self._background_color_luminance > 0.5 else (1, 1, 1, 0.1) + ) self._connection_color_selected = (0, 0.55, 1, 1) - self._connection_color_hover = (0, 0, 0, 0.66) if self._background_color_luminance > 0.5 else (1, 1, 1, 0.66) + self._connection_color_hover = ( + (0, 0, 0, 0.66) + if self._background_color_luminance > 0.5 + else (1, 1, 1, 0.66) + ) self._connection_color_data = None self._connection_color_by = None self._connection_color_map = None @@ -417,8 +489,7 @@ def __init__( kwargs.get('size_labeling', UNDEF), ) self.connect( - kwargs.get('connect_by', UNDEF), - kwargs.get('connect_order', UNDEF) + kwargs.get('connect_by', UNDEF), kwargs.get('connect_order', UNDEF) ) self.connection_color( kwargs.get('connection_color', UNDEF), @@ -452,10 +523,7 @@ def __init__( kwargs.get('lasso_min_dist', UNDEF), kwargs.get('lasso_on_long_press', UNDEF), ) - self.reticle( - kwargs.get('reticle', UNDEF), - kwargs.get('reticle_color', UNDEF) - ) + self.reticle(kwargs.get('reticle', UNDEF), kwargs.get('reticle_color', UNDEF)) self.mouse(kwargs.get('mouse_mode', UNDEF)) self.camera( kwargs.get('camera_target', UNDEF), @@ -515,11 +583,11 @@ def get_point_list(self): if not connect_by: # To avoid having to serialize unused data - view = view[:,:4] + view = view[:, :4] if not connect_order: # To avoid having to serialize unused data - view = view[:,:5] + view = view[:, :5] return view.copy() @@ -530,7 +598,7 @@ def data( reset_scales: Optional[bool] = False, zoom_view: Optional[bool] = False, animate: Optional[bool] = False, - **kwargs + **kwargs, ) -> Union[Scatter, dict]: """ Set or get the referenced Pandas DataFrame @@ -629,7 +697,9 @@ def data( self.size(skip_widget_update=True, **self.size()) self.connect(skip_widget_update=True, **self.connect()) self.connection_color(skip_widget_update=True, **self.connection_color()) - self.connection_opacity(skip_widget_update=True, **self.connection_opacity()) + self.connection_opacity( + skip_widget_update=True, **self.connection_opacity() + ) self.connection_size(skip_widget_update=True, **self.connection_size()) if 'skip_widget_update' not in kwargs: @@ -667,7 +737,7 @@ def data( if any_not([data, use_index], UNDEF): return self - return dict(data = self._data) + return dict(data=self._data) @property def x_data(self): @@ -678,9 +748,11 @@ def x_data(self): def x( self, x: Optional[Union[str, List[float], np.ndarray, Undefined]] = UNDEF, - scale: Optional[Union[Scales, Tuple[float, float], LogNorm, PowerNorm, None, Undefined]] = UNDEF, + scale: Optional[ + Union[Scales, Tuple[float, float], LogNorm, PowerNorm, None, Undefined] + ] = UNDEF, animate: Optional[Union[bool, Undefined]] = UNDEF, - **kwargs + **kwargs, ) -> Union[Scatter, dict]: """ Set or get the x coordinates. @@ -727,7 +799,11 @@ def x( self._x_scale = LogNorm() elif scale == 'pow': self._x_scale = PowerNorm(2) - elif isinstance(scale, LogNorm) or isinstance(scale, PowerNorm) or isinstance(scale, Normalize): + elif ( + isinstance(scale, LogNorm) + or isinstance(scale, PowerNorm) + or isinstance(scale, Normalize) + ): self._x_scale = scale else: try: @@ -743,10 +819,8 @@ def x( self.update_widget( 'annotations', normalize_annotations( - self._annotations, - self._x_scale, - self._y_scale - ) + self._annotations, self._x_scale, self._y_scale + ), ) if x is not UNDEF: @@ -768,8 +842,8 @@ def x( self._x_domain = [self._x_min, self._x_max] self._x_histogram = get_histogram_from_df( self._points[:, 0], - self.get_histogram_bins("x"), - self.get_histogram_range("x") + self.get_histogram_bins('x'), + self.get_histogram_range('x'), ) # Reset scale to new domain @@ -796,10 +870,7 @@ def x( if any_not([x, scale], UNDEF): return self - return dict( - x = self._x, - scale = self._x_scale - ) + return dict(x=self._x, scale=self._x_scale) @property def y_data(self): @@ -810,9 +881,11 @@ def y_data(self): def y( self, y: Optional[Union[str, List[float], np.ndarray, Undefined]] = UNDEF, - scale: Optional[Union[Scales, Tuple[float, float], LogNorm, PowerNorm, None, Undefined]] = UNDEF, + scale: Optional[ + Union[Scales, Tuple[float, float], LogNorm, PowerNorm, None, Undefined] + ] = UNDEF, animate: Optional[Union[bool, Undefined]] = UNDEF, - **kwargs + **kwargs, ) -> Union[Scatter, dict]: """ Set or get the y coordinates. @@ -859,7 +932,11 @@ def y( self._y_scale = LogNorm() elif scale == 'pow': self._y_scale = PowerNorm(2) - elif isinstance(scale, LogNorm) or isinstance(scale, PowerNorm) or isinstance(scale, Normalize): + elif ( + isinstance(scale, LogNorm) + or isinstance(scale, PowerNorm) + or isinstance(scale, Normalize) + ): self._y_scale = scale else: try: @@ -875,10 +952,8 @@ def y( self.update_widget( 'annotations', normalize_annotations( - self._annotations, - self._x_scale, - self._y_scale - ) + self._annotations, self._x_scale, self._y_scale + ), ) if y is not UNDEF: @@ -899,8 +974,8 @@ def y( self._y_domain = [self._y_min, self._y_max] self._y_histogram = get_histogram_from_df( self._points[:, 1], - self.get_histogram_bins("y"), - self.get_histogram_range("y") + self.get_histogram_bins('y'), + self.get_histogram_range('y'), ) # Reset scale to new domain @@ -927,19 +1002,20 @@ def y( if any_not([y, scale], UNDEF): return self - return dict( - y = self._y, - scale = self._y_scale - ) + return dict(y=self._y, scale=self._y_scale) def xy( self, x: Optional[Union[str, List[float], np.ndarray, Undefined]] = UNDEF, y: Optional[Union[str, List[float], np.ndarray, Undefined]] = UNDEF, - x_scale: Optional[Union[Scales, Tuple[float, float], LogNorm, PowerNorm, None, Undefined]] = UNDEF, - y_scale: Optional[Union[Scales, Tuple[float, float], LogNorm, PowerNorm, None, Undefined]] = UNDEF, + x_scale: Optional[ + Union[Scales, Tuple[float, float], LogNorm, PowerNorm, None, Undefined] + ] = UNDEF, + y_scale: Optional[ + Union[Scales, Tuple[float, float], LogNorm, PowerNorm, None, Undefined] + ] = UNDEF, animate: Optional[Union[bool, Undefined]] = UNDEF, - **kwargs + **kwargs, ) -> Union[Scatter, dict]: """ Set the x and y coordinates. @@ -1013,24 +1089,21 @@ def xy( self.update_widget( 'annotations', normalize_annotations( - self._annotations, - self._x_scale, - self._y_scale - ) + self._annotations, self._x_scale, self._y_scale + ), ) return self return dict( - x = self._x, - y = self._y, - x_scale = self._x_scale, - y_scale = self._y_scale, + x=self._x, + y=self._y, + x_scale=self._x_scale, + y_scale=self._y_scale, ) def selection( - self, - point_ids: Optional[Union[List[int], np.ndarray, None, Undefined]] = UNDEF + self, point_ids: Optional[Union[List[int], np.ndarray, None, Undefined]] = UNDEF ) -> Union[Scatter, np.ndarray]: """ Set or get selected points. @@ -1064,10 +1137,16 @@ def selection( try: if self._data is not None and self._data_use_index: row_idxs = self._data.index.get_indexer(point_ids) - self._selected_points_idxs = np.asarray(row_idxs[row_idxs >= 0], dtype=SELECTION_DTYPE) - self._selected_points_ids = self._data.iloc[self._selected_points_idxs].index + self._selected_points_idxs = np.asarray( + row_idxs[row_idxs >= 0], dtype=SELECTION_DTYPE + ) + self._selected_points_ids = self._data.iloc[ + self._selected_points_idxs + ].index else: - self._selected_points_idxs = np.asarray(point_ids, dtype=SELECTION_DTYPE) + self._selected_points_idxs = np.asarray( + point_ids, dtype=SELECTION_DTYPE + ) self._selected_points_ids = self._selected_points_idxs except: if point_ids is None: @@ -1088,8 +1167,7 @@ def selection( return self._selected_points_ids def filter( - self, - point_ids: Optional[Union[List[int], np.ndarray, None, Undefined]] = UNDEF + self, point_ids: Optional[Union[List[int], np.ndarray, None, Undefined]] = UNDEF ) -> Union[Scatter, np.ndarray, None]: """ Set or get filtered points. When filtering down to a set of points, all @@ -1121,12 +1199,15 @@ def filter( if self._data is not None and self._data_use_index: row_idxs = self._data.index.get_indexer(point_ids) self._filtered_points_idxs = np.asarray( - row_idxs[row_idxs >= 0], - dtype=SELECTION_DTYPE + row_idxs[row_idxs >= 0], dtype=SELECTION_DTYPE ) - self._filtered_points_ids = self._data.iloc[self._filtered_points_idxs].index + self._filtered_points_ids = self._data.iloc[ + self._filtered_points_idxs + ].index else: - self._filtered_points_idxs = np.asarray(point_ids, dtype=SELECTION_DTYPE) + self._filtered_points_idxs = np.asarray( + point_ids, dtype=SELECTION_DTYPE + ) self._filtered_points_ids = self._filtered_points_idxs except: if point_ids is None: @@ -1158,11 +1239,23 @@ def color( selected: Optional[Union[Color, Undefined]] = UNDEF, hover: Optional[Union[Color, Auto, Undefined]] = UNDEF, by: Optional[Union[str, List[float], np.ndarray, Undefined]] = UNDEF, - map: Optional[Union[Auto, str, dict, list, LinearSegmentedColormap, ListedColormap, Undefined]] = UNDEF, + map: Optional[ + Union[ + Auto, + str, + dict, + list, + LinearSegmentedColormap, + ListedColormap, + Undefined, + ] + ] = UNDEF, norm: Optional[Union[Tuple[float, float], Normalize, Undefined]] = UNDEF, order: Optional[Union[Reverse, List[int], List[str], Undefined]] = UNDEF, - labeling: Optional[Union[Tuple[str, str], Tuple[str, str, str], Labeling, Undefined]] = UNDEF, - **kwargs + labeling: Optional[ + Union[Tuple[str, str], Tuple[str, str, str], Labeling, Undefined] + ] = UNDEF, + **kwargs, ) -> Union[Scatter, dict]: """ Set or get the color encoding of the points. @@ -1239,7 +1332,11 @@ def color( """ if default is not UNDEF: if default == 'auto': - self._color = (0, 0, 0, 0.66) if self._background_color_luminance > 0.5 else (1, 1, 1, 0.66) + self._color = ( + (0, 0, 0, 0.66) + if self._background_color_luminance > 0.5 + else (1, 1, 1, 0.66) + ) else: try: self._color = to_rgba(default) @@ -1255,7 +1352,11 @@ def color( if hover is not UNDEF: if hover == 'auto': - self._color_hover = (0, 0, 0, 1) if self._background_color_luminance > 0.5 else (1, 1, 1, 1) + self._color_hover = ( + (0, 0, 0, 1) + if self._background_color_luminance > 0.5 + else (1, 1, 1, 1) + ) else: try: self._color_hover = to_rgba(hover) @@ -1301,7 +1402,9 @@ def color( component = self._encodings.data[self._color_data_dimension].component if categorical_data is not None: - self._color_categories = dict(zip(categorical_data, categorical_data.cat.codes)) + self._color_categories = dict( + zip(categorical_data, categorical_data.cat.codes) + ) self._points[:, component] = categorical_data.cat.codes self._color_histogram = get_histogram_from_df(categorical_data) else: @@ -1311,8 +1414,8 @@ def color( ) self._color_histogram = get_histogram_from_df( self._points[:, component], - self.get_histogram_bins("color"), - self.get_histogram_range("color") + self.get_histogram_bins('color'), + self.get_histogram_range('color'), ) if not self._encodings.data[self._color_data_dimension].prepared: @@ -1374,7 +1477,9 @@ def color( self._color_map = okabe_ito if self._color_categories is not None: - assert len(self._color_categories) <= len(self._color_map), 'More categories than colors' + assert len(self._color_categories) <= len( + self._color_map + ), 'More categories than colors' if labeling is not UNDEF: if labeling is None: @@ -1413,14 +1518,14 @@ def color( return self return dict( - default = self._color, - selected = self._color_selected, - hover = self._color_hover, - by = self._color_by, - map = self._color_map, - norm = self._color_norm, - order = self._color_order, - labeling = self._color_labeling, + default=self._color, + selected=self._color_selected, + hover=self._color_hover, + by=self._color_by, + map=self._color_map, + norm=self._color_norm, + order=self._color_order, + labeling=self._color_labeling, ) @property @@ -1434,11 +1539,13 @@ def opacity( default: Optional[Union[float, Undefined]] = UNDEF, unselected: Optional[Union[float, Undefined]] = UNDEF, by: Optional[Union[str, List[float], np.ndarray, Undefined]] = UNDEF, - map: Optional[Union[Auto, dict, List[float], Tuple[float, float, int], Undefined]] = UNDEF, + map: Optional[ + Union[Auto, dict, List[float], Tuple[float, float, int], Undefined] + ] = UNDEF, norm: Optional[Union[Tuple[float, float], Normalize, Undefined]] = UNDEF, order: Optional[Union[Reverse, List[int], List[str], Undefined]] = UNDEF, labeling: Optional[Union[Labeling, Undefined]] = UNDEF, - **kwargs + **kwargs, ): """ Set or get the opacity encoding of the points. @@ -1513,14 +1620,18 @@ def opacity( if default is not UNDEF: try: self._opacity = float(default) - assert self._opacity >= 0 and self._opacity <= 1, 'Opacity must be in [0,1]' + assert ( + self._opacity >= 0 and self._opacity <= 1 + ), 'Opacity must be in [0,1]' except ValueError: pass if unselected is not UNDEF: try: self._opacity_unselected = float(unselected) - assert self._opacity_unselected >= 0 and self._opacity_unselected <= 1, 'Opacity scaling of unselected points must be in [0,1]' + assert ( + self._opacity_unselected >= 0 and self._opacity_unselected <= 1 + ), 'Opacity scaling of unselected points must be in [0,1]' self.update_widget('opacity_unselected', self._opacity_unselected) except ValueError: pass @@ -1565,7 +1676,9 @@ def opacity( if categorical_data is not None: self._points[:, component] = categorical_data.cat.codes - self._opacity_categories = dict(zip(categorical_data, categorical_data.cat.codes)) + self._opacity_categories = dict( + zip(categorical_data, categorical_data.cat.codes) + ) self._opacity_histogram = get_histogram_from_df(categorical_data) else: self._points[:, component] = self._opacity_norm( @@ -1573,8 +1686,8 @@ def opacity( ) self._opacity_histogram = get_histogram_from_df( self._points[:, component], - self.get_histogram_bins("opacity"), - self.get_histogram_range("opacity") + self.get_histogram_bins('opacity'), + self.get_histogram_range('opacity'), ) self._opacity_categories = None @@ -1609,17 +1722,23 @@ def opacity( else: self._opacity_map = np.asarray(map) - if (self._opacity_map is None or map == 'auto') and self._opacity_by is not None: + if ( + self._opacity_map is None or map == 'auto' + ) and self._opacity_by is not None: # The best we can do is provide a linear opacity map if self._opacity_categories is not None: - self._opacity_map = np.linspace(1/len(self._opacity_categories), 1, len(self._opacity_categories)) + self._opacity_map = np.linspace( + 1 / len(self._opacity_categories), 1, len(self._opacity_categories) + ) else: - self._opacity_map = np.linspace(1/256, 1, 256) + self._opacity_map = np.linspace(1 / 256, 1, 256) self._opacity_map = tolist(self._opacity_map) if self._opacity_categories is not None: - assert len(self._opacity_categories) <= len(self._opacity_map), 'More categories than opacities' + assert len(self._opacity_categories) <= len( + self._opacity_map + ), 'More categories than opacities' if labeling is not UNDEF: if labeling is None: @@ -1659,13 +1778,13 @@ def opacity( return self return dict( - default = self._opacity, - unselected = self._opacity_unselected, - by = self._opacity_by, - map = self._opacity_map, - norm = self._opacity_norm, - order = self._opacity_order, - labeling = self._opacity_labeling, + default=self._opacity, + unselected=self._opacity_unselected, + by=self._opacity_by, + map=self._opacity_map, + norm=self._opacity_norm, + order=self._opacity_order, + labeling=self._opacity_labeling, ) @property @@ -1678,11 +1797,13 @@ def size( self, default: Optional[Union[float, Undefined]] = UNDEF, by: Optional[Union[str, List[float], np.ndarray, Undefined]] = UNDEF, - map: Optional[Union[Auto, dict, List[float], Tuple[float, float, int], Undefined]] = UNDEF, + map: Optional[ + Union[Auto, dict, List[float], Tuple[float, float, int], Undefined] + ] = UNDEF, norm: Optional[Union[Tuple[float, float], Normalize, Undefined]] = UNDEF, order: Optional[Union[Reverse, List[int], List[str], Undefined]] = UNDEF, labeling: Optional[Union[Labeling, Undefined]] = UNDEF, - **kwargs + **kwargs, ): """ Set or get the size encoding of the points. @@ -1797,7 +1918,9 @@ def size( if categorical_data is not None: self._points[:, component] = categorical_data.cat.codes - self._size_categories = dict(zip(categorical_data, categorical_data.cat.codes)) + self._size_categories = dict( + zip(categorical_data, categorical_data.cat.codes) + ) self._size_histogram = get_histogram_from_df(categorical_data) else: self._points[:, component] = self._size_norm( @@ -1805,8 +1928,8 @@ def size( ) self._size_histogram = get_histogram_from_df( self._points[:, component], - self.get_histogram_bins("size"), - self.get_histogram_range("size") + self.get_histogram_bins('size'), + self.get_histogram_range('size'), ) self._size_categories = None @@ -1826,7 +1949,9 @@ def size( self._size_order = order elif self._size_categories is not None: # Define order of the sizes instead of changing `points[:, component_idx]` - self._size_order = [self._size_categories[cat] for cat in self._size_order] + self._size_order = [ + self._size_categories[cat] for cat in self._size_order + ] if map is not UNDEF and map != 'auto' and map is not None: self._size_map_order = None @@ -1851,7 +1976,9 @@ def size( self._size_map = tolist(self._size_map) if self._size_categories is not None: - assert len(self._size_categories) <= len(self._size_map), 'More categories than sizes' + assert len(self._size_categories) <= len( + self._size_map + ), 'More categories than sizes' if labeling is not UNDEF: if labeling is None: @@ -1890,12 +2017,12 @@ def size( return self return dict( - default = self._size, - by = self._size_by, - map = self._size_map, - norm = self._size_norm, - order = self._size_order, - labeling = self._size_labeling, + default=self._size, + by=self._size_by, + map=self._size_map, + norm=self._size_norm, + order=self._size_order, + labeling=self._size_labeling, ) @property @@ -1914,7 +2041,7 @@ def connect( self, by: Optional[Union[str, List[int], np.ndarray[int], None, Undefined]] = UNDEF, order: Optional[Union[List[int], np.ndarray[int], None, Undefined]] = UNDEF, - **kwargs + **kwargs, ): """ Set or get the line-connection encoding of points. @@ -1964,9 +2091,7 @@ def connect( self._connect_by_data = None else: self._connect_by_data = pd.Series( - by, - index=self._data_index, - dtype='category' + by, index=self._data_index, dtype='category' ) self._connect_by = 'Custom Connect-By Data' @@ -1989,9 +2114,7 @@ def connect( self._connect_order_data = None else: self._connect_order_data = pd.Series( - order, - index=self._data_index, - dtype='int' + order, index=self._data_index, dtype='int' ) self._connect_order = 'Custom Connect-Order Data' @@ -2012,8 +2135,8 @@ def connect( return self return dict( - by = self._connect_by, - order = self._connect_order, + by=self._connect_by, + order=self._connect_order, ) @property @@ -2028,11 +2151,21 @@ def connection_color( selected: Optional[Union[Color, Undefined]] = UNDEF, hover: Optional[Union[Color, Undefined]] = UNDEF, by: Optional[Union[Segment, str, List[float], np.ndarray, Undefined]] = UNDEF, - map: Optional[Union[Auto, str, dict, list, LinearSegmentedColormap, ListedColormap, Undefined]] = UNDEF, + map: Optional[ + Union[ + Auto, + str, + dict, + list, + LinearSegmentedColormap, + ListedColormap, + Undefined, + ] + ] = UNDEF, norm: Optional[Union[Tuple[float, float], Normalize, Undefined]] = UNDEF, order: Optional[Union[Reverse, List[int], List[str], Undefined]] = UNDEF, labeling: Optional[Union[Labeling, Undefined]] = UNDEF, - **kwargs + **kwargs, ): """ Set or get the color encoding of the point-connecting lines. @@ -2126,14 +2259,18 @@ def connection_color( if selected is not UNDEF: try: self._connection_color_selected = to_rgba(selected) - self.update_widget('connection_color_selected', self._connection_color_selected) + self.update_widget( + 'connection_color_selected', self._connection_color_selected + ) except ValueError: pass if hover is not UNDEF: try: self._connection_color_hover = to_rgba(hover) - self.update_widget('connection_color_hover', self._connection_color_hover) + self.update_widget( + 'connection_color_hover', self._connection_color_hover + ) except ValueError: pass @@ -2175,26 +2312,38 @@ def connection_color( ) self._connection_color_by = 'Custom Connection-Color Data' - self._encodings.set('connection_color', self._connection_color_data_dimension) + self._encodings.set( + 'connection_color', self._connection_color_data_dimension + ) check_encoding_dtype(self.connection_color_data) - component = self._encodings.data[self._connection_color_data_dimension].component + component = self._encodings.data[ + self._connection_color_data_dimension + ].component categorical_data = get_categorical_data(self.connection_color_data) if categorical_data is not None: - self._connection_color_categories = dict(zip(categorical_data, categorical_data.cat.codes)) + self._connection_color_categories = dict( + zip(categorical_data, categorical_data.cat.codes) + ) self._points[:, component] = categorical_data.cat.codes else: self._connection_color_categories = None self._points[:, component] = self._connection_color_norm( - zerofy_missing_values(self.connection_color_data.values, 'Connection color') + zerofy_missing_values( + self.connection_color_data.values, 'Connection color' + ) ) - if not self._encodings.data[self._connection_color_data_dimension].prepared: + if not self._encodings.data[ + self._connection_color_data_dimension + ].prepared: data_updated = True # Make sure we don't prepare the data twice - self._encodings.data[self._connection_color_data_dimension].prepared = True + self._encodings.data[ + self._connection_color_data_dimension + ].prepared = True elif default is not UNDEF: # Presumably the user wants to switch to a static color encoding @@ -2206,7 +2355,9 @@ def connection_color( self._connection_color_order = order elif self._connection_color_categories is not None: # Define order of the colors instead of changing `points[:, component_idx]` - self._connection_color_order = [self._connection_color_categories[cat] for cat in order] + self._connection_color_order = [ + self._connection_color_categories[cat] for cat in order + ] if map is not UNDEF and map != 'auto' and map is not None: if self._connection_color_categories is None: @@ -2226,20 +2377,28 @@ def connection_color( self._connection_color_map = [to_rgba(c) for c in map.colors] elif isinstance(map, str): # Assuming `map` is the name of a Matplotlib ListedColormap - self._connection_color_map = [to_rgba(c) for c in plt.get_cmap(map).colors] + self._connection_color_map = [ + to_rgba(c) for c in plt.get_cmap(map).colors + ] elif isinstance(map, dict): # Assuming `map` is a dictionary of colors self._connection_color_map = [to_rgba(c) for c in map.values()] self._connection_color_map_order = list(map.keys()) - self._connection_color_order = get_map_order(map, self._connection_color_categories) + self._connection_color_order = get_map_order( + map, self._connection_color_categories + ) else: # Assuming `map` is a list of colors self._connection_color_map = [to_rgba(c) for c in map] - if (self._connection_color_map is None or map == 'auto') and self._connection_color_by is not None: + if ( + self._connection_color_map is None or map == 'auto' + ) and self._connection_color_by is not None: # Assign default color maps if self._connection_color_categories is None: - self._connection_color_map = plt.get_cmap('viridis')(range(256)).tolist() + self._connection_color_map = plt.get_cmap('viridis')( + range(256) + ).tolist() elif len(self._connection_color_categories) > 8: if self._background_color_luminance < 0.5: self._connection_color_map = glasbey_light @@ -2249,17 +2408,26 @@ def connection_color( self._connection_color_map = okabe_ito if self._connection_color_categories is not None: - assert len(self._connection_color_categories) <= len(self._connection_color_map), 'More categories than connection colors' + assert len(self._connection_color_categories) <= len( + self._connection_color_map + ), 'More categories than connection colors' if labeling is not UNDEF: if labeling is None: self._connection_color_labeling = None else: - column = self._connection_color_by if isinstance(self._connection_color_by, str) else None + column = ( + self._connection_color_by + if isinstance(self._connection_color_by, str) + else None + ) self._connection_color_labeling = create_labeling(labeling, column) # Update widget and legend encoding - if self._connection_color_by is not None and self._connection_color_map is not None: + if ( + self._connection_color_by is not None + and self._connection_color_map is not None + ): final_connection_color_map = order_map( self._connection_color_map, self._connection_color_order, @@ -2287,14 +2455,14 @@ def connection_color( return self return dict( - default = self._connection_color, - selected = self._connection_color_selected, - hover = self._connection_color_hover, - by = self._connection_color_by, - map = self._connection_color_map, - norm = self._connection_color_norm, - order = self._connection_color_order, - labeling = self._connection_color_labeling, + default=self._connection_color, + selected=self._connection_color_selected, + hover=self._connection_color_hover, + by=self._connection_color_by, + map=self._connection_color_map, + norm=self._connection_color_norm, + order=self._connection_color_order, + labeling=self._connection_color_labeling, ) @property @@ -2307,11 +2475,13 @@ def connection_opacity( self, default: Optional[Union[float, Undefined]] = UNDEF, by: Optional[Union[str, List[float], np.ndarray, Undefined]] = UNDEF, - map: Optional[Union[Auto, dict, List[float], Tuple[float, float, int], Undefined]] = UNDEF, + map: Optional[ + Union[Auto, dict, List[float], Tuple[float, float, int], Undefined] + ] = UNDEF, norm: Optional[Union[Tuple[float, float], Normalize, Undefined]] = UNDEF, order: Optional[Union[Reverse, List[int], List[str], Undefined]] = UNDEF, labeling: Optional[Union[Labeling, Undefined]] = UNDEF, - **kwargs + **kwargs, ): """ Set or get the opacity encoding of the point-connecting lines. @@ -2383,8 +2553,13 @@ def connection_opacity( if default is not UNDEF: try: self._connection_opacity = float(default) - assert self._connection_opacity >= 0 and self._connection_opacity <= 1, 'Connection opacity must be in [0,1]' - self.update_widget('connection_opacity', self._connection_opacity_map or self._connection_opacity) + assert ( + self._connection_opacity >= 0 and self._connection_opacity <= 1 + ), 'Connection opacity must be in [0,1]' + self.update_widget( + 'connection_opacity', + self._connection_opacity_map or self._connection_opacity, + ) except ValueError: pass @@ -2420,26 +2595,38 @@ def connection_opacity( ) self._connection_opacity_by = 'Custom Connection-Opacity Data' - self._encodings.set('connection_opacity', self._connection_opacity_data_dimension) + self._encodings.set( + 'connection_opacity', self._connection_opacity_data_dimension + ) check_encoding_dtype(self.connection_opacity_data) - component = self._encodings.data[self._connection_opacity_data_dimension].component + component = self._encodings.data[ + self._connection_opacity_data_dimension + ].component categorical_data = get_categorical_data(self.connection_opacity_data) if categorical_data is not None: self._points[:, component] = categorical_data.cat.codes - self._connection_opacity_categories = dict(zip(categorical_data, categorical_data.cat.codes)) + self._connection_opacity_categories = dict( + zip(categorical_data, categorical_data.cat.codes) + ) else: self._points[:, component] = self._connection_opacity_norm( - zerofy_missing_values(self.connection_opacity_data.values, 'Connection opacity') + zerofy_missing_values( + self.connection_opacity_data.values, 'Connection opacity' + ) ) self._connection_opacity_categories = None - if not self._encodings.data[self._connection_opacity_data_dimension].prepared: + if not self._encodings.data[ + self._connection_opacity_data_dimension + ].prepared: data_updated = True # Make sure we don't prepare the data twice - self._encodings.data[self._connection_opacity_data_dimension].prepared = True + self._encodings.data[ + self._connection_opacity_data_dimension + ].prepared = True elif default is not UNDEF: # Presumably the user wants to switch to a static opacity encoding @@ -2464,35 +2651,48 @@ def connection_opacity( # Assuming `map` is a dictionary of opacities self._connection_opacity_map = list(map.values()) self._connection_opacity_map_order = list(map.keys()) - self._connection_opacity_order = get_map_order(map, self._connection_opacity_categories) + self._connection_opacity_order = get_map_order( + map, self._connection_opacity_categories + ) else: self._connection_opacity_map = np.asarray(map) - if (self._connection_opacity_map is None or map == 'auto') and self._connection_opacity_by is not None: + if ( + self._connection_opacity_map is None or map == 'auto' + ) and self._connection_opacity_by is not None: # The best we can do is provide a linear opacity map if self._connection_opacity_categories is not None: self._connection_opacity_map = np.linspace( 1 / len(self._connection_opacity_categories), 1, - len(self._connection_opacity_categories) + len(self._connection_opacity_categories), ) else: - self._connection_opacity_map = np.linspace(1/256, 1, 256) + self._connection_opacity_map = np.linspace(1 / 256, 1, 256) self._connection_opacity_map = tolist(self._connection_opacity_map) if self._connection_opacity_categories is not None: - assert len(self._connection_opacity_categories) <= len(self._connection_opacity_map), 'More categories than connection opacities' + assert len(self._connection_opacity_categories) <= len( + self._connection_opacity_map + ), 'More categories than connection opacities' if labeling is not UNDEF: if labeling is None: self._connection_opacity_labeling = None else: - column = self._connection_opacity_by if isinstance(self._connection_opacity_by, str) else None + column = ( + self._connection_opacity_by + if isinstance(self._connection_opacity_by, str) + else None + ) self._connection_opacity_labeling = create_labeling(labeling, column) # Update widget and legend encoding - if self._connection_opacity_by is not None and self._connection_opacity_map is not None: + if ( + self._connection_opacity_by is not None + and self._connection_opacity_map is not None + ): final_opacity_map = order_map( self._connection_opacity_map, self._connection_opacity_order, @@ -2520,12 +2720,12 @@ def connection_opacity( return self return dict( - default = self._connection_opacity, - by = self._connection_opacity_by, - map = self._connection_opacity_map, - norm = self._connection_opacity_norm, - order = self._connection_opacity_order, - labeling = self._connection_opacity_labeling, + default=self._connection_opacity, + by=self._connection_opacity_by, + map=self._connection_opacity_map, + norm=self._connection_opacity_norm, + order=self._connection_opacity_order, + labeling=self._connection_opacity_labeling, ) @property @@ -2538,11 +2738,13 @@ def connection_size( self, default: Optional[Union[float, Undefined]] = UNDEF, by: Optional[Union[str, List[float], np.ndarray, Undefined]] = UNDEF, - map: Optional[Union[Auto, dict, List[float], Tuple[float, float, int], Undefined]] = UNDEF, + map: Optional[ + Union[Auto, dict, List[float], Tuple[float, float, int], Undefined] + ] = UNDEF, norm: Optional[Union[Tuple[float, float], Normalize, Undefined]] = UNDEF, order: Optional[Union[Reverse, List[int], List[str], Undefined]] = UNDEF, labeling: Optional[Union[Labeling, Undefined]] = UNDEF, - **kwargs + **kwargs, ): """ Set or get the size encoding of the point-connecting lines. @@ -2614,7 +2816,9 @@ def connection_size( if default is not UNDEF: try: self._connection_size = int(default) - assert self._connection_size > 0, 'Connection size must be a positive integer' + assert ( + self._connection_size > 0 + ), 'Connection size must be a positive integer' except ValueError: pass @@ -2650,26 +2854,38 @@ def connection_size( ) self._connection_size_by = 'Custom Connection-Size Data' - self._encodings.set('connection_size', self._connection_size_data_dimension) + self._encodings.set( + 'connection_size', self._connection_size_data_dimension + ) check_encoding_dtype(self.connection_size_data) - component = self._encodings.data[self._connection_size_data_dimension].component + component = self._encodings.data[ + self._connection_size_data_dimension + ].component categorical_data = get_categorical_data(self.connection_size_data) if categorical_data is not None: self._points[:, component] = categorical_data.cat.codes - self._connection_size_categories = dict(zip(categorical_data, categorical_data.cat.codes)) + self._connection_size_categories = dict( + zip(categorical_data, categorical_data.cat.codes) + ) else: self._points[:, component] = self._connection_size_norm( - zerofy_missing_values(self.connection_size_data.values, 'Connection size') + zerofy_missing_values( + self.connection_size_data.values, 'Connection size' + ) ) self._connection_size_categories = None - if not self._encodings.data[self._connection_size_data_dimension].prepared: + if not self._encodings.data[ + self._connection_size_data_dimension + ].prepared: data_updated = True # Make sure we don't prepare the data twice - self._encodings.data[self._connection_size_data_dimension].prepared = True + self._encodings.data[ + self._connection_size_data_dimension + ].prepared = True elif default is not UNDEF: # Presumably the user wants to switch to a static size encoding @@ -2681,7 +2897,9 @@ def connection_size( self._connection_size_order = order elif self._connection_size_categories is not None: # Define order of the sizes instead of changing `points[:, component_idx]` - self._connection_size_order = [self._connection_size_categories[cat] for cat in order] + self._connection_size_order = [ + self._connection_size_categories[cat] for cat in order + ] if map is not UNDEF and map != 'auto' and map is not None: self._connection_size_map_order = None @@ -2692,16 +2910,22 @@ def connection_size( # Assuming `map` is a dictionary of sizes self._connection_size_map = list(map.values()) self._connection_size_map_order = list(map.keys()) - self._connection_size_order = get_map_order(map, self._connection_size_categories) + self._connection_size_order = get_map_order( + map, self._connection_size_categories + ) else: self._connection_size_map = np.asarray(map) - if (self._connection_size_map is None or map == 'auto') and self._connection_size_by is not None: + if ( + self._connection_size_map is None or map == 'auto' + ) and self._connection_size_by is not None: # The best we can do is provide a linear size map if self._connection_size_categories is None: self._connection_size_map = np.linspace(1, 10, 19) else: - self._connection_size_map = np.arange(1, len(self._connection_size_categories) + 1) + self._connection_size_map = np.arange( + 1, len(self._connection_size_categories) + 1 + ) self._connection_size_map = tolist(self._connection_size_map) @@ -2709,14 +2933,20 @@ def connection_size( if labeling is None: self._connection_size_labeling = None else: - column = self._connection_size_by if isinstance(self._connection_size_by, str) else None + column = ( + self._connection_size_by + if isinstance(self._connection_size_by, str) + else None + ) self._connection_size_labeling = create_labeling(labeling, column) # Update widget and legend encoding - if self._connection_size_by is not None and self._connection_size_map is not None: + if ( + self._connection_size_by is not None + and self._connection_size_map is not None + ): final_connection_size_map = order_map( - self._connection_size_map, - self._connection_size_order + self._connection_size_map, self._connection_size_order ) self.update_widget('connection_size', final_connection_size_map) self._encodings.set_legend( @@ -2738,25 +2968,27 @@ def connection_size( self.update_widget('points', self.get_point_list()) if self._connection_size_categories is not None: - assert len(self._connection_size_categories) <= len(self._connection_size_map), 'More categories than connection sizes' + assert len(self._connection_size_categories) <= len( + self._connection_size_map + ), 'More categories than connection sizes' if any_not([default, by, map, norm, order, labeling], UNDEF): return self return dict( - default = self._connection_size, - by = self._connection_size_by, - map = self._connection_size_map, - norm = self._connection_size_norm, - order = self._connection_size_order, - labeling = self._connection_size_labeling, + default=self._connection_size, + by=self._connection_size_by, + map=self._connection_size_map, + norm=self._connection_size_norm, + order=self._connection_size_order, + labeling=self._connection_size_labeling, ) def background( self, color: Optional[Union[Color, Undefined]] = UNDEF, image: Optional[Union[str, bytes, Undefined]] = UNDEF, - **kwargs + **kwargs, ): """ Set or get the scatter plot's background. @@ -2834,7 +3066,9 @@ def background( x = im.make_image() h, w, d = x.as_rgba_str() - self._background_image = np.fromstring(d, dtype=np.uint8).reshape(h, w, 4) + self._background_image = np.fromstring(d, dtype=np.uint8).reshape( + h, w, 4 + ) self.update_widget('background_image', self._background_image) except: if image is None: @@ -2846,8 +3080,8 @@ def background( return self return dict( - color = self._background_color, - image = self._background_image, + color=self._background_color, + image=self._background_image, ) def camera( @@ -2933,10 +3167,10 @@ def camera( return self return dict( - target = self._camera_target, - distance = self._camera_distance, - rotation = self._camera_rotation, - view = self._camera_view, + target=self._camera_target, + distance=self._camera_distance, + rotation=self._camera_rotation, + view=self._camera_view, ) def lasso( @@ -2944,8 +3178,8 @@ def lasso( color: Optional[Union[Color, Undefined]] = UNDEF, initiator: Optional[Union[bool, Undefined]] = UNDEF, min_delay: Optional[Union[int, Undefined]] = UNDEF, - min_dist: Optional[Union[float, Undefined]] = UNDEF, - on_long_press: Optional[Union[bool, Undefined]] = UNDEF, + min_dist: Optional[Union[float, Undefined]] = UNDEF, + on_long_press: Optional[Union[bool, Undefined]] = UNDEF, ): """ Set or get the lasso settings. @@ -3050,17 +3284,14 @@ def lasso( return self return dict( - color = self._lasso_color, - initiator = self._lasso_initiator, - min_delay = self._lasso_min_delay, - min_dist = self._lasso_min_dist, - on_long_press = self._lasso_on_long_press, + color=self._lasso_color, + initiator=self._lasso_initiator, + min_delay=self._lasso_min_delay, + min_dist=self._lasso_min_dist, + on_long_press=self._lasso_on_long_press, ) - def width( - self, - width: Optional[Union[Auto, int, Undefined]] = UNDEF - ): + def width(self, width: Optional[Union[Auto, int, Undefined]] = UNDEF): """ Set or get the width of the scatter plot. @@ -3153,7 +3384,7 @@ def get_reticle_color(self): elif self._background_color_luminance < 0.75: return (0, 0, 0, 0.2) - return (0, 0, 0, 0.1) # Defaut + return (0, 0, 0, 0.1) # Defaut def get_axes_color(self): if self._background_color_luminance < 0.5: @@ -3176,14 +3407,13 @@ def get_tooltip_color(self): def get_legend_encoding(self): return { channel: self._encodings.get_legend(channel) - for channel - in self._encodings.visual.keys() + for channel in self._encodings.visual.keys() } def reticle( self, show: Optional[Union[bool, Undefined]] = UNDEF, - color: Optional[Union[Color, Undefined]] = UNDEF + color: Optional[Union[Color, Undefined]] = UNDEF, ): """ Set or get the reticle setting. @@ -3234,14 +3464,11 @@ def reticle( return self return dict( - show = self._reticle, - color = self._reticle_color, + show=self._reticle, + color=self._reticle_color, ) - def mouse( - self, - mode: Optional[Union[MouseModes, Undefined]] = UNDEF - ): + def mouse(self, mode: Optional[Union[MouseModes, Undefined]] = UNDEF): """ Set or get the mouse mode. @@ -3334,9 +3561,7 @@ def axes( if isinstance(labels, bool): self._axes_labels = labels elif isinstance(labels, dict): - self._axes_labels = [ - labels.get('x', ''), labels.get('y', '') - ] + self._axes_labels = [labels.get('x', ''), labels.get('y', '')] else: self._axes_labels = labels @@ -3349,9 +3574,9 @@ def axes( return self return dict( - axes = self._axes, - grid = self._axes_grid, - labels = self._axes_labels, + axes=self._axes, + grid=self._axes_grid, + labels=self._axes_labels, ) def legend( @@ -3398,9 +3623,7 @@ def legend( self._legend = legend self.update_widget('legend', legend) if legend: - self.update_widget( - 'legend_encoding', self.get_legend_encoding() - ) + self.update_widget('legend_encoding', self.get_legend_encoding()) except: pass @@ -3422,9 +3645,9 @@ def legend( return self return dict( - legend = self._legend, - position = self._legend_position, - size = self._legend_size, + legend=self._legend, + position=self._legend_position, + size=self._legend_size, ) def tooltip( @@ -3433,13 +3656,17 @@ def tooltip( properties: Optional[Union[List[VisualProperty], Undefined]] = UNDEF, histograms: Optional[Union[bool, Undefined]] = UNDEF, histograms_bins: Optional[Union[int, Dict[str, int], Undefined]] = UNDEF, - histograms_ranges: Optional[Union[Tuple[float], Dict[str, Tuple[float]], Undefined]] = UNDEF, + histograms_ranges: Optional[ + Union[Tuple[float], Dict[str, Tuple[float]], Undefined] + ] = UNDEF, histograms_size: Optional[Union[Size, Undefined]] = UNDEF, preview: Optional[Union[str, Undefined]] = UNDEF, preview_type: Optional[Union[TooltipPreviewType, Undefined]] = UNDEF, preview_text_lines: Optional[Union[int, Undefined]] = UNDEF, preview_image_background_color: Optional[Union[Auto, Color, Undefined]] = UNDEF, - preview_image_position: Optional[Union[TooltipPreviewImagePosition, str, Undefined]] = UNDEF, + preview_image_position: Optional[ + Union[TooltipPreviewImagePosition, str, Undefined] + ] = UNDEF, preview_image_size: Optional[Union[TooltipPreviewImageSize, Undefined]] = UNDEF, preview_audio_length: Optional[Union[int, Undefined]] = UNDEF, preview_audio_loop: Optional[Union[bool, int, Undefined]] = UNDEF, @@ -3613,8 +3840,12 @@ def tooltip( self.update_widget('tooltip_preview_text_lines', self._preview_text_lines) if preview_image_background_color is not UNDEF: - self._tooltip_preview_image_background_color = preview_image_background_color - self.update_widget('tooltip_preview_image_background_color', preview_image_background_color) + self._tooltip_preview_image_background_color = ( + preview_image_background_color + ) + self.update_widget( + 'tooltip_preview_image_background_color', preview_image_background_color + ) if preview_image_position is not UNDEF: self._tooltip_preview_image_position = preview_image_position @@ -3629,7 +3860,9 @@ def tooltip( self._tooltip_preview_audio_length = max(0, preview_audio_length) except TypeError: self._tooltip_preview_audio_length = None - self.update_widget('tooltip_preview_audio_length', self._tooltip_preview_audio_length) + self.update_widget( + 'tooltip_preview_audio_length', self._tooltip_preview_audio_length + ) if preview_audio_loop is not UNDEF: self._tooltip_preview_audio_loop = preview_audio_loop @@ -3649,27 +3882,17 @@ def tooltip( if properties is not UNDEF: self._tooltip_properties = sanitize_tooltip_properties( - self._data, - visual_properties, - properties + self._data, visual_properties, properties ) self._tooltip_histograms_bins = { - property: get_histogram_bins( - self._tooltip_histograms_bins, - property - ) - for property - in self._tooltip_properties + property: get_histogram_bins(self._tooltip_histograms_bins, property) + for property in self._tooltip_properties } self._tooltip_histograms_ranges = { - property: get_histogram_range( - self._tooltip_histograms_ranges, - property - ) - for property - in self._tooltip_properties + property: get_histogram_range(self._tooltip_histograms_ranges, property) + for property in self._tooltip_properties } if size is not UNDEF: @@ -3687,69 +3910,77 @@ def tooltip( if histograms_bins is not UNDEF: self._tooltip_histograms_bins = { property: get_histogram_bins(histograms_bins, property) - for property - in self._tooltip_properties + for property in self._tooltip_properties } if histograms_ranges is not UNDEF: self._tooltip_histograms_ranges = { property: get_histogram_range(histograms_ranges, property) - for property - in self._tooltip_properties + for property in self._tooltip_properties } if histograms_bins is not UNDEF or histograms_ranges is not UNDEF: # Re-create histograms self._x_histogram = get_histogram_from_df( self._points[:, 0], - self.get_histogram_bins("x"), - self.get_histogram_range("x"), + self.get_histogram_bins('x'), + self.get_histogram_range('x'), ) - self.update_widget('x_histogram_range', self.get_histogram_range("x")) + self.update_widget('x_histogram_range', self.get_histogram_range('x')) self.update_widget('x_histogram', self._x_histogram) self._y_histogram = get_histogram_from_df( self._points[:, 1], - self.get_histogram_bins("y"), - self.get_histogram_range("y") + self.get_histogram_bins('y'), + self.get_histogram_range('y'), ) - self.update_widget('y_histogram_range', self.get_histogram_range("y")) + self.update_widget('y_histogram_range', self.get_histogram_range('y')) self.update_widget('y_histogram', self._y_histogram) if self._color_by is not None and self._color_categories is None: component = self._encodings.data[self._color_by].component self._color_histogram = get_histogram_from_df( self._points[:, component], - self.get_histogram_bins("color"), - self.get_histogram_range("color") + self.get_histogram_bins('color'), + self.get_histogram_range('color'), + ) + self.update_widget( + 'color_histogram_range', self.get_histogram_range('color') ) - self.update_widget('color_histogram_range', self.get_histogram_range("color")) self.update_widget('color_histogram', self._color_histogram) - if self._opacity_by is not None and self._opacity_by != "density" and self._opacity_categories is None: + if ( + self._opacity_by is not None + and self._opacity_by != 'density' + and self._opacity_categories is None + ): component = self._encodings.data[self._opacity_by].component self._opacity_histogram = get_histogram_from_df( self._points[:, component], - self.get_histogram_bins("opacity"), - self.get_histogram_range("opacity") + self.get_histogram_bins('opacity'), + self.get_histogram_range('opacity'), + ) + self.update_widget( + 'opacity_histogram_range', self.get_histogram_range('opacity') ) - self.update_widget('opacity_histogram_range', self.get_histogram_range("opacity")) self.update_widget('opacity_histogram', self._opacity_histogram) if self._size_by is not None and self._size_categories is None: component = self._encodings.data[self._size_by].component self._size_histogram = get_histogram_from_df( self._points[:, component], - self.get_histogram_bins("size"), - self.get_histogram_range("size") + self.get_histogram_bins('size'), + self.get_histogram_range('size'), + ) + self.update_widget( + 'size_histogram_range', self.get_histogram_range('size') ) - self.update_widget('size_histogram_range', self.get_histogram_range("size")) self.update_widget('size_histogram', self._size_histogram) if ( - self._tooltip_properties_non_visual is None or - properties is not UNDEF or - histograms_bins is not UNDEF + self._tooltip_properties_non_visual is None + or properties is not UNDEF + or histograms_bins is not UNDEF ): self._tooltip_properties_non_visual = get_non_visual_properties( self._tooltip_properties @@ -3758,44 +3989,60 @@ def tooltip( self._tooltip_properties_non_visual_info = {} for property in self._tooltip_properties_non_visual: self._tooltip_properties_non_visual_info[property] = dict( - scale = get_scale_type_from_df(self._data[property]), - domain = get_domain_from_df(self._data[property]), - range = self.get_histogram_range(property), - histogram = get_histogram_from_df( + scale=get_scale_type_from_df(self._data[property]), + domain=get_domain_from_df(self._data[property]), + range=self.get_histogram_range(property), + histogram=get_histogram_from_df( self._data[property], self.get_histogram_bins(property), - self.get_histogram_range(property) + self.get_histogram_range(property), ), ) - self.update_widget('tooltip_properties_non_visual_info', self._tooltip_properties_non_visual_info) + self.update_widget( + 'tooltip_properties_non_visual_info', + self._tooltip_properties_non_visual_info, + ) self.update_widget('tooltip_properties', self._tooltip_properties) - if any_not([enable, properties, size, histograms, histograms_bins, histograms_ranges, histograms_size], UNDEF): + if any_not( + [ + enable, + properties, + size, + histograms, + histograms_bins, + histograms_ranges, + histograms_size, + ], + UNDEF, + ): return self return dict( - enable = self._tooltip, - properties = self._tooltip_properties, - histograms = self._tooltip_histograms, - histograms_bins = self._tooltip_histograms_bins, - histograms_ranges = self._tooltip_histograms_ranges, - histograms_size = self._tooltip_histograms_size, - preview = self._tooltip_preview, - preview_type = self._tooltip_preview_type, - preview_text_lines = self._tooltip_preview_text_lines, - preview_image_background_color = self._tooltip_preview_image_background_color, - preview_image_position = self._tooltip_preview_image_position, - preview_image_size = self._tooltip_preview_image_size, - preview_audio_length = self._tooltip_preview_audio_length, - preview_audio_loop = self._tooltip_preview_audio_loop, - preview_audio_controls = self._tooltip_preview_audio_controls, - size = self._tooltip_size, + enable=self._tooltip, + properties=self._tooltip_properties, + histograms=self._tooltip_histograms, + histograms_bins=self._tooltip_histograms_bins, + histograms_ranges=self._tooltip_histograms_ranges, + histograms_size=self._tooltip_histograms_size, + preview=self._tooltip_preview, + preview_type=self._tooltip_preview_type, + preview_text_lines=self._tooltip_preview_text_lines, + preview_image_background_color=self._tooltip_preview_image_background_color, + preview_image_position=self._tooltip_preview_image_position, + preview_image_size=self._tooltip_preview_image_size, + preview_audio_length=self._tooltip_preview_audio_length, + preview_audio_loop=self._tooltip_preview_audio_loop, + preview_audio_controls=self._tooltip_preview_audio_controls, + size=self._tooltip_size, ) def annotations( self, - annotations: Optional[Union[List[Union[Line, HLine, VLine, Rect, Contour]], Undefined]] = UNDEF, + annotations: Optional[ + Union[List[Union[Line, HLine, VLine, Rect, Contour]], Undefined] + ] = UNDEF, ): """ Draw line-based annotatons @@ -3829,19 +4076,13 @@ def annotations( self.update_widget( 'annotations', - normalize_annotations( - self._annotations, - self._x_scale, - self._y_scale - ) + normalize_annotations(self._annotations, self._x_scale, self._y_scale), ) if any_not([annotations], UNDEF): return self - return dict( - annotations = self._annotations - ) + return dict(annotations=self._annotations) def zoom( self, @@ -3923,20 +4164,18 @@ def zoom( return self return dict( - to = self._zoom_to, - animation = self._zoom_animation, - padding = self._zoom_padding, - on_selection = self._zoom_on_selection, - on_filter = self._zoom_on_filter, + to=self._zoom_to, + animation=self._zoom_animation, + padding=self._zoom_padding, + on_selection=self._zoom_on_selection, + on_filter=self._zoom_on_filter, ) - - def options( self, transition_points: Optional[Union[bool, Undefined]] = UNDEF, transition_points_duration: Optional[Union[int, Undefined]] = UNDEF, - regl_scatterplot_options: Optional[Union[dict, Undefined]] = UNDEF + regl_scatterplot_options: Optional[Union[dict, Undefined]] = UNDEF, ): """ Set or get additional options. @@ -3990,7 +4229,9 @@ def options( except: self._transition_points_duration = DEFAULT_TRANSITION_POINTS_DURATION - self.update_widget('transition_points_duration', self._transition_points_duration) + self.update_widget( + 'transition_points_duration', self._transition_points_duration + ) if regl_scatterplot_options is not UNDEF: try: @@ -4001,13 +4242,16 @@ def options( return self - if any_not([transition_points, transition_points_duration, regl_scatterplot_options], UNDEF): + if any_not( + [transition_points, transition_points_duration, regl_scatterplot_options], + UNDEF, + ): return self return dict( transition_points=self._transition_points, transition_points_duration=self._transition_points_duration, - regl_scatterplot_options=self._regl_scatterplot_options + regl_scatterplot_options=self._regl_scatterplot_options, ) def pixels(self): @@ -4039,8 +4283,13 @@ def pixels(self): [0, 0, 0, 0]]], dtype=uint8) """ if self._widget is not None: - assert self._widget.view_data is not None and len(self._widget.view_data) > 0, 'Download pixels first by clicking on the button with the camera icon.' - assert self._widget.view_shape is not None and len(self._widget.view_shape) == 2, 'Download pixels first by clicking on the button with the camera icon.' + assert ( + self._widget.view_data is not None and len(self._widget.view_data) > 0 + ), 'Download pixels first by clicking on the button with the camera icon.' + assert ( + self._widget.view_shape is not None + and len(self._widget.view_shape) == 2 + ), 'Download pixels first by clicking on the button with the camera icon.' self._pixels = np.asarray(self._widget.view_data).astype(np.uint8) self._pixels = self._pixels.reshape([*self._widget.view_shape, 4]) @@ -4075,7 +4324,9 @@ def _size_data_dimension(self): def _connection_color_data_dimension(self): if self._connection_color_by is None or self._connection_color_by == 'segment': return None - return f'{self._connection_color_by}:{to_scale_type(self._connection_color_norm)}' + return ( + f'{self._connection_color_by}:{to_scale_type(self._connection_color_norm)}' + ) @property def _connection_opacity_data_dimension(self): @@ -4124,7 +4375,10 @@ def js_connection_color_by(self): if self._connection_color_by == 'segment': return 'segment' - if self._connection_color == 'inherit' or self._connection_color_by == 'inherit': + if ( + self._connection_color == 'inherit' + or self._connection_color_by == 'inherit' + ): return 'inherit' if self._connection_color_data_dimension is not None: @@ -4160,9 +4414,7 @@ def widget(self): self._widget = JupyterScatter( data=self._data, annotations=normalize_annotations( - self._annotations, - self._x_scale, - self._y_scale + self._annotations, self._x_scale, self._y_scale ), axes=self._axes, axes_color=self.get_axes_color(), @@ -4174,23 +4426,37 @@ def widget(self): camera_rotation=self._camera_rotation, camera_target=self._camera_target, camera_view=self._camera_view, - color=order_map(self._color_map, self._color_order) if self._color_map else self._color, + color=order_map(self._color_map, self._color_order) + if self._color_map + else self._color, color_by=self.js_color_by, color_domain=get_domain(self, 'color'), color_histogram=self._color_histogram, - color_histogram_range=self.get_histogram_range("color"), + color_histogram_range=self.get_histogram_range('color'), color_hover=self._color_hover, color_scale=get_scale(self, 'color'), color_selected=self._color_selected, color_title=self._color_by, connect=bool(self._connect_by), - connection_color=order_map(self._connection_color_map, self._connection_color_order) if self._connection_color_map else self._connection_color, + connection_color=order_map( + self._connection_color_map, self._connection_color_order + ) + if self._connection_color_map + else self._connection_color, connection_color_by=self.js_connection_color_by, connection_color_hover=self._connection_color_hover, connection_color_selected=self._connection_color_selected, - connection_opacity=order_map(self._connection_opacity_map, self._connection_opacity_order) if self._connection_opacity_map else self._connection_opacity, + connection_opacity=order_map( + self._connection_opacity_map, self._connection_opacity_order + ) + if self._connection_opacity_map + else self._connection_opacity, connection_opacity_by=self.js_connection_opacity_by, - connection_size=order_map(self._connection_size_map, self._connection_size_order) if self._connection_size_map else self._connection_size, + connection_size=order_map( + self._connection_size_map, self._connection_size_order + ) + if self._connection_size_map + else self._connection_size, connection_size_by=self.js_connection_size_by, filter=self._filtered_points_idxs, height=self._height, @@ -4205,11 +4471,13 @@ def widget(self): legend_position=self._legend_position, legend_size=self._legend_size, mouse_mode=self._mouse_mode, - opacity=order_map(self._opacity_map, self._opacity_order) if self._opacity_map else self._opacity, + opacity=order_map(self._opacity_map, self._opacity_order) + if self._opacity_map + else self._opacity, opacity_by=self.js_opacity_by, opacity_domain=get_domain(self, 'opacity'), opacity_histogram=self._opacity_histogram, - opacity_histogram_range=self.get_histogram_range("opacity"), + opacity_histogram_range=self.get_histogram_range('opacity'), opacity_scale=get_scale(self, 'opacity'), opacity_title=self._opacity_by, opacity_unselected=self._opacity_unselected, @@ -4217,11 +4485,13 @@ def widget(self): reticle=self._reticle, reticle_color=self.get_reticle_color(), selection=self._selected_points_idxs, - size=order_map(self._size_map, self._size_order) if self._size_map else self._size, + size=order_map(self._size_map, self._size_order) + if self._size_map + else self._size, size_by=self.js_size_by, size_domain=get_domain(self, 'size'), size_histogram=self._size_histogram, - size_histogram_range=self.get_histogram_range("size"), + size_histogram_range=self.get_histogram_range('size'), size_scale=get_scale(self, 'size'), size_title=self._size_by, tooltip_enable=self._tooltip, @@ -4244,13 +4514,13 @@ def widget(self): width=self._width, x_domain=self._x_domain, x_histogram=self._x_histogram, - x_histogram_range=self.get_histogram_range("x"), + x_histogram_range=self.get_histogram_range('x'), x_scale=to_scale_type(self._x_scale), x_scale_domain=self._x_scale_domain, x_title=self._x_by, y_domain=self._y_domain, y_histogram=self._y_histogram, - y_histogram_range=self.get_histogram_range("y"), + y_histogram_range=self.get_histogram_range('y'), y_scale=to_scale_type(self._y_scale), y_scale_domain=self._y_scale_domain, y_title=self._y_by, @@ -4326,7 +4596,7 @@ def plot( x: Union[str, List[float], np.ndarray], y: Union[str, List[float], np.ndarray], data: Optional[pd.DataFrame] = None, - **kwargs + **kwargs, ): """ Create a scatter instance and immediately show it as a widget. diff --git a/jscatter/types.py b/jscatter/types.py index eb3c8592..5967252a 100644 --- a/jscatter/types.py +++ b/jscatter/types.py @@ -15,33 +15,40 @@ Undefined = type( 'Undefined', (object,), - { '__str__': lambda s: 'Undefined', '__repr__': lambda s: 'Undefined' } + {'__str__': lambda s: 'Undefined', '__repr__': lambda s: 'Undefined'}, ) + class Scales(Enum): LINEAR = 'linear' LOG = 'log' POW = 'pow' + class Size(Enum): S = 'small' M = 'medium' L = 'large' + class MouseModes(Enum): PAN_ZOOM = 'panZoom' LASSO = 'lasso' ROTATE = 'rotate' + class Auto(Enum): AUTO = 'auto' + class Reverse(Enum): REVERSE = 'reverse' + class Segment(Enum): SEGMENT = 'segment' + class LegendPosition(Enum): TOP = 'top' TOP_RIGHT = 'top-right' @@ -53,6 +60,7 @@ class LegendPosition(Enum): LEFT = 'left' CENTER = 'center' + class VisualProperty(Enum): X = 'x' Y = 'y' @@ -60,6 +68,7 @@ class VisualProperty(Enum): OPACITY = 'opacity' SIZE = 'size' + class Labeling(TypedDict): variable: NotRequired[str] minValue: NotRequired[str] diff --git a/jscatter/utils.py b/jscatter/utils.py index 4e1020f3..892d4f21 100644 --- a/jscatter/utils.py +++ b/jscatter/utils.py @@ -9,8 +9,9 @@ from .types import Labeling + def to_uint8(x): - return int(max(0, min(x * 255, 255))) + return int(max(0, min(x * 255, 255))) def to_hex(color): @@ -29,15 +30,18 @@ def with_left_label(label_text, widget, label_width: int = 128): return container -def any_not(l, value = None): + +def any_not(l, value=None): return any([x is not value for x in l]) + def tolist(l): try: return l.tolist() except Exception: return l + def uri_validator(x): try: result = urlparse(x) @@ -45,44 +49,51 @@ def uri_validator(x): except Exception: return False + def sorting_to_dict(sorting): out = dict() for order_idx, original_idx in enumerate(sorting): out[original_idx] = order_idx return out + class TimeNormalize(Normalize): is_time = True + def create_default_norm(is_time=False): if is_time: return TimeNormalize() return Normalize() + def to_ndc(X, norm): return (norm(X).data * 2) - 1 -def to_scale_type(norm = None): - if (isinstance(norm, LogNorm)): + +def to_scale_type(norm=None): + if isinstance(norm, LogNorm): return 'log_10' - if (isinstance(norm, PowerNorm)): + if isinstance(norm, PowerNorm): return f'pow_{norm.gamma}' - if (isinstance(norm, TimeNormalize)): + if isinstance(norm, TimeNormalize): return 'time' - if (isinstance(norm, Normalize)): + if isinstance(norm, Normalize): return 'linear' return 'categorical' + def get_scale_type_from_df(data): if pd.CategoricalDtype.is_dtype(data) or pd.api.types.is_string_dtype(data): return 'categorical' return 'linear' + def get_domain_from_df(data): if pd.CategoricalDtype.is_dtype(data) or pd.api.types.is_string_dtype(data): # We need to recreate the categorization in case the data is just a @@ -92,6 +103,7 @@ def get_domain_from_df(data): return [data.min(), data.max()] + def create_labeling(partial_labeling, column: Union[str, None] = None) -> Labeling: labeling: Labeling = {} @@ -119,17 +131,21 @@ def create_labeling(partial_labeling, column: Union[str, None] = None) -> Labeli return labeling + def get_histogram_from_df(data, bins=20, range=None): if pd.CategoricalDtype.is_dtype(data) or pd.api.types.is_string_dtype(data): # We need to recreate the categorization in case the data is just a # filtered view, in which case it might contain "missing" indices - value_counts = data.copy().astype(str).astype('category').cat.codes.value_counts() + value_counts = ( + data.copy().astype(str).astype('category').cat.codes.value_counts() + ) return [y for _, y in sorted(dict(value_counts / value_counts.sum()).items())] hist = histogram(data[~isnan(data)], bins=bins, range=range) return list(hist[0] / hist[0].max()) + def sanitize_tooltip_properties( df, reserved_properties: List[str], @@ -145,11 +161,12 @@ def sanitize_tooltip_properties( return sanitized_properties + def zerofy_missing_values(values, dtype): if isnan(sum(values)): warnings.warn( f'{dtype} data contains missing values. Those missing values will be replaced with zeros.', - UserWarning + UserWarning, ) values[isnan(values)] = 0 return values diff --git a/jscatter/widget.py b/jscatter/widget.py index 579086c0..5051305f 100644 --- a/jscatter/widget.py +++ b/jscatter/widget.py @@ -17,12 +17,13 @@ SELECTION_DTYPE = 'uint32' EVENT_TYPES = { - "TOOLTIP": "tooltip", - "VIEW_DOWNLOAD": "view_download", - "VIEW_RESET": "view_reset", - "VIEW_SAVE": "view_save", + 'TOOLTIP': 'tooltip', + 'VIEW_DOWNLOAD': 'view_download', + 'VIEW_RESET': 'view_reset', + 'VIEW_SAVE': 'view_save', } + def component_idx_to_name(idx): if idx == 2: return 'valueA' @@ -32,33 +33,38 @@ def component_idx_to_name(idx): return None + def sorting_to_dict(sorting): out = dict() for order_idx, original_idx in enumerate(sorting): out[original_idx] = order_idx return out + # Code extracted from maartenbreddels ipyvolume def array_to_binary(ar, obj=None, force_contiguous=True): if ar is None: return None if ar.dtype.kind not in ['u', 'i', 'f']: # ints and floats - raise ValueError("unsupported dtype: %s" % (ar.dtype)) + raise ValueError('unsupported dtype: %s' % (ar.dtype)) if ar.dtype == np.float64: # WebGL does not support float64, case it here ar = ar.astype(np.float32) if ar.dtype == np.int64: # JS does not support int64 ar = ar.astype(np.int32) - if force_contiguous and not ar.flags["C_CONTIGUOUS"]: # make sure it's contiguous + if force_contiguous and not ar.flags['C_CONTIGUOUS']: # make sure it's contiguous ar = np.ascontiguousarray(ar) return {'view': memoryview(ar), 'dtype': str(ar.dtype), 'shape': ar.shape} + def binary_to_array(value, obj=None): return np.frombuffer(value['view'], dtype=value['dtype']).reshape(value['shape']) + ndarray_serialization = dict(to_json=array_to_binary, from_json=binary_to_array) + class JupyterScatter(anywidget.AnyWidget): - _esm = pathlib.Path(__file__).parent / "bundle.js" + _esm = pathlib.Path(__file__).parent / 'bundle.js' # For debugging dom_element_id = Unicode(read_only=True).tag(sync=True) @@ -68,8 +74,12 @@ class JupyterScatter(anywidget.AnyWidget): transition_points = Bool(False).tag(sync=True) transition_points_duration = Int(3000).tag(sync=True) prevent_filter_reset = Bool(False).tag(sync=True) - selection = Array(default_value=None, allow_none=True).tag(sync=True, **ndarray_serialization) - filter = Array(default_value=None, allow_none=True).tag(sync=True, **ndarray_serialization) + selection = Array(default_value=None, allow_none=True).tag( + sync=True, **ndarray_serialization + ) + filter = Array(default_value=None, allow_none=True).tag( + sync=True, **ndarray_serialization + ) hovering = Int(None, allow_none=True).tag(sync=True) # Channel titles @@ -89,9 +99,15 @@ class JupyterScatter(anywidget.AnyWidget): # Domains x_domain = List(minlen=2, maxlen=2).tag(sync=True) y_domain = List(minlen=2, maxlen=2).tag(sync=True) - color_domain = Union([Dict(), List(minlen=2, maxlen=2)], allow_none=True).tag(sync=True) - opacity_domain = Union([Dict(), List(minlen=2, maxlen=2)], allow_none=True).tag(sync=True) - size_domain = Union([Dict(), List(minlen=2, maxlen=2)], allow_none=True).tag(sync=True) + color_domain = Union([Dict(), List(minlen=2, maxlen=2)], allow_none=True).tag( + sync=True + ) + opacity_domain = Union([Dict(), List(minlen=2, maxlen=2)], allow_none=True).tag( + sync=True + ) + size_domain = Union([Dict(), List(minlen=2, maxlen=2)], allow_none=True).tag( + sync=True + ) # Scale domains x_scale_domain = List(minlen=2, maxlen=2).tag(sync=True) @@ -107,9 +123,15 @@ class JupyterScatter(anywidget.AnyWidget): # Histogram ranges x_histogram_range = List(None, allow_none=True, minlen=2, maxlen=2).tag(sync=True) y_histogram_range = List(None, allow_none=True, minlen=2, maxlen=2).tag(sync=True) - color_histogram_range = List(None, allow_none=True, minlen=2, maxlen=2).tag(sync=True) - opacity_histogram_range = List(None, allow_none=True, minlen=2, maxlen=2).tag(sync=True) - size_histogram_range = List(None, allow_none=True, minlen=2, maxlen=2).tag(sync=True) + color_histogram_range = List(None, allow_none=True, minlen=2, maxlen=2).tag( + sync=True + ) + opacity_histogram_range = List(None, allow_none=True, minlen=2, maxlen=2).tag( + sync=True + ) + size_histogram_range = List(None, allow_none=True, minlen=2, maxlen=2).tag( + sync=True + ) # Annotations annotations = List( @@ -125,7 +147,9 @@ class JupyterScatter(anywidget.AnyWidget): camera_view = List(None, allow_none=True).tag(sync=True) # Zoom properties - zoom_to = Array(default_value=None, allow_none=True).tag(sync=True, **ndarray_serialization) + zoom_to = Array(default_value=None, allow_none=True).tag( + sync=True, **ndarray_serialization + ) zoom_to_call_idx = Int(0).tag(sync=True) zoom_animation = Int(1000).tag(sync=True) zoom_padding = Float(0.333).tag(sync=True) @@ -133,7 +157,9 @@ class JupyterScatter(anywidget.AnyWidget): zoom_on_filter = Bool(False).tag(sync=True) # Interaction properties - mouse_mode = Enum(['panZoom', 'lasso', 'rotate'], default_value='panZoom').tag(sync=True) + mouse_mode = Enum(['panZoom', 'lasso', 'rotate'], default_value='panZoom').tag( + sync=True + ) lasso_initiator = Bool().tag(sync=True) lasso_on_long_press = Bool().tag(sync=True) @@ -157,27 +183,23 @@ class JupyterScatter(anywidget.AnyWidget): 'right', 'center', ], - default_value='top-left' - ).tag(sync=True) - legend_size = Enum( - ['small', 'medium', 'large'], default_value='small' - ).tag(sync=True) - legend_color = List( - default_value=[0, 0, 0, 1], minlen=4, maxlen=4 + default_value='top-left', ).tag(sync=True) + legend_size = Enum(['small', 'medium', 'large'], default_value='small').tag( + sync=True + ) + legend_color = List(default_value=[0, 0, 0, 1], minlen=4, maxlen=4).tag(sync=True) legend_encoding = Dict(dict()).tag(sync=True) # Tooltip tooltip_enable = Bool().tag(sync=True) - tooltip_size = Enum( - ['small', 'medium', 'large'], default_value='small' - ).tag(sync=True) - tooltip_color = List( - default_value=[0, 0, 0, 1], minlen=4, maxlen=4 - ).tag(sync=True) - tooltip_properties = List( - default_value=['x', 'y', 'color', 'opacity', 'size'] - ).tag(sync=True) + tooltip_size = Enum(['small', 'medium', 'large'], default_value='small').tag( + sync=True + ) + tooltip_color = List(default_value=[0, 0, 0, 1], minlen=4, maxlen=4).tag(sync=True) + tooltip_properties = List(default_value=['x', 'y', 'color', 'opacity', 'size']).tag( + sync=True + ) tooltip_properties_non_visual_info = Dict(dict()).tag(sync=True) tooltip_histograms = Bool().tag(sync=True) tooltip_histograms_ranges = Dict(dict()).tag(sync=True) @@ -185,36 +207,92 @@ class JupyterScatter(anywidget.AnyWidget): ['small', 'medium', 'large'], default_value='small' ).tag(sync=True) tooltip_preview = Unicode(None, allow_none=True).tag(sync=True) - tooltip_preview_type = Enum( - ['text', 'image', 'audio'], default_value='text' - ).tag(sync=True) + tooltip_preview_type = Enum(['text', 'image', 'audio'], default_value='text').tag( + sync=True + ) tooltip_preview_text_lines = Int(default_value=3, allow_none=True).tag(sync=True) - tooltip_preview_image_background_color = Union([Enum(['auto']), Unicode()], default_value='auto').tag(sync=True) - tooltip_preview_image_position = Union([Enum(['top', 'left', 'right', 'bottom', 'center']), Unicode()], allow_none=True, default_value=None).tag(sync=True) - tooltip_preview_image_size = Enum(['contain', 'cover'], allow_none=True, default_value=None).tag(sync=True) + tooltip_preview_image_background_color = Union( + [Enum(['auto']), Unicode()], default_value='auto' + ).tag(sync=True) + tooltip_preview_image_position = Union( + [Enum(['top', 'left', 'right', 'bottom', 'center']), Unicode()], + allow_none=True, + default_value=None, + ).tag(sync=True) + tooltip_preview_image_size = Enum( + ['contain', 'cover'], allow_none=True, default_value=None + ).tag(sync=True) tooltip_preview_audio_length = Int(None, allow_none=True).tag(sync=True) tooltip_preview_audio_loop = Bool().tag(sync=True) tooltip_preview_audio_controls = Bool().tag(sync=True) # Options - color = Union([Union([Unicode(), List(minlen=4, maxlen=4)]), List(Union([Unicode(), List(minlen=4, maxlen=4)]))]).tag(sync=True) - color_selected = Union([Union([Unicode(), List(minlen=4, maxlen=4)]), List(Union([Unicode(), List(minlen=4, maxlen=4)]))]).tag(sync=True) - color_hover = Union([Union([Unicode(), List(minlen=4, maxlen=4)]), List(Union([Unicode(), List(minlen=4, maxlen=4)]))]).tag(sync=True) - color_by = Enum([None, 'valueA', 'valueB'], allow_none=True, default_value=None).tag(sync=True) + color = Union( + [ + Union([Unicode(), List(minlen=4, maxlen=4)]), + List(Union([Unicode(), List(minlen=4, maxlen=4)])), + ] + ).tag(sync=True) + color_selected = Union( + [ + Union([Unicode(), List(minlen=4, maxlen=4)]), + List(Union([Unicode(), List(minlen=4, maxlen=4)])), + ] + ).tag(sync=True) + color_hover = Union( + [ + Union([Unicode(), List(minlen=4, maxlen=4)]), + List(Union([Unicode(), List(minlen=4, maxlen=4)])), + ] + ).tag(sync=True) + color_by = Enum( + [None, 'valueA', 'valueB'], allow_none=True, default_value=None + ).tag(sync=True) opacity = Union([Float(), List(Float())], allow_none=True).tag(sync=True) opacity_unselected = Float().tag(sync=True) - opacity_by = Enum([None, 'valueA', 'valueB', 'density'], allow_none=True, default_value=None).tag(sync=True) - size = Union([Union([Int(), Float()]), List(Union([Int(), Float()]))]).tag(sync=True) - size_by = Enum([None, 'valueA', 'valueB'], allow_none=True, default_value=None).tag(sync=True) + opacity_by = Enum( + [None, 'valueA', 'valueB', 'density'], allow_none=True, default_value=None + ).tag(sync=True) + size = Union([Union([Int(), Float()]), List(Union([Int(), Float()]))]).tag( + sync=True + ) + size_by = Enum([None, 'valueA', 'valueB'], allow_none=True, default_value=None).tag( + sync=True + ) connect = Bool().tag(sync=True) - connection_color = Union([Union([Unicode(), List(minlen=4, maxlen=4)]), List(Union([Unicode(), List(minlen=4, maxlen=4)]))]).tag(sync=True) - connection_color_selected = Union([Union([Unicode(), List(minlen=4, maxlen=4)]), List(Union([Unicode(), List(minlen=4, maxlen=4)]))]).tag(sync=True) - connection_color_hover = Union([Union([Unicode(), List(minlen=4, maxlen=4)]), List(Union([Unicode(), List(minlen=4, maxlen=4)]))]).tag(sync=True) - connection_color_by = Enum([None, 'valueA', 'valueB', 'segment', 'inherit'], allow_none=True, default_value=None).tag(sync=True) + connection_color = Union( + [ + Union([Unicode(), List(minlen=4, maxlen=4)]), + List(Union([Unicode(), List(minlen=4, maxlen=4)])), + ] + ).tag(sync=True) + connection_color_selected = Union( + [ + Union([Unicode(), List(minlen=4, maxlen=4)]), + List(Union([Unicode(), List(minlen=4, maxlen=4)])), + ] + ).tag(sync=True) + connection_color_hover = Union( + [ + Union([Unicode(), List(minlen=4, maxlen=4)]), + List(Union([Unicode(), List(minlen=4, maxlen=4)])), + ] + ).tag(sync=True) + connection_color_by = Enum( + [None, 'valueA', 'valueB', 'segment', 'inherit'], + allow_none=True, + default_value=None, + ).tag(sync=True) connection_opacity = Union([Float(), List(Float())], allow_none=True).tag(sync=True) - connection_opacity_by = Enum([None, 'valueA', 'valueB', 'segment'], allow_none=True, default_value=None).tag(sync=True) - connection_size = Union([Union([Int(), Float()]), List(Union([Int(), Float()]))]).tag(sync=True) - connection_size_by = Enum([None, 'valueA', 'valueB', 'segment'], allow_none=True, default_value=None).tag(sync=True) + connection_opacity_by = Enum( + [None, 'valueA', 'valueB', 'segment'], allow_none=True, default_value=None + ).tag(sync=True) + connection_size = Union( + [Union([Int(), Float()]), List(Union([Int(), Float()]))] + ).tag(sync=True) + connection_size_by = Enum( + [None, 'valueA', 'valueB', 'segment'], allow_none=True, default_value=None + ).tag(sync=True) width = Union([Unicode(), Int()], default_value='auto').tag(sync=True) height = Int().tag(sync=True) background_color = Union([Unicode(), List(minlen=4, maxlen=4)]).tag(sync=True) @@ -228,8 +306,12 @@ class JupyterScatter(anywidget.AnyWidget): regl_scatterplot_options = Dict(dict()).tag(sync=True) - view_download = Unicode(None, allow_none=True).tag(sync=True) # Used for triggering a download - view_data = Array(default_value=None, allow_none=True, read_only=True).tag(sync=True, **ndarray_serialization) + view_download = Unicode(None, allow_none=True).tag( + sync=True + ) # Used for triggering a download + view_data = Array(default_value=None, allow_none=True, read_only=True).tag( + sync=True, **ndarray_serialization + ) # For synchronyzing view changes across scatter plot instances view_sync = Unicode(None, allow_none=True).tag(sync=True) @@ -243,24 +325,38 @@ def __init__(self, data, *args, **kwargs): self.on_msg(self._handle_custom_msg) def _handle_custom_msg(self, event: dict, buffers): - if event["type"] == EVENT_TYPES["TOOLTIP"] and isinstance(self.data, pd.DataFrame): - data = self.data.iloc[event["index"]] - self.send({ - "type": EVENT_TYPES["TOOLTIP"], - "index": event["index"], - "preview": data[event["preview"]] if event["preview"] is not None else None, - "properties": data[event["properties"]].to_dict() - }) + if event['type'] == EVENT_TYPES['TOOLTIP'] and isinstance( + self.data, pd.DataFrame + ): + data = self.data.iloc[event['index']] + self.send( + { + 'type': EVENT_TYPES['TOOLTIP'], + 'index': event['index'], + 'preview': data[event['preview']] + if event['preview'] is not None + else None, + 'properties': data[event['properties']].to_dict(), + } + ) def show_tooltip(self, point_idx): data = self.data.iloc[point_idx] - self.send({ - "type": TOOLTIP_EVENT_TYPE, - "show": True, - "index": point_idx, - "preview": data[self.tooltip_preview] if self.tooltip_preview is not None else None, - "properties": data[self.tooltip_properties_non_visual_info.keys()].to_dict() if self.tooltip_properties_non_visual_info is not None else {} - }) + self.send( + { + 'type': TOOLTIP_EVENT_TYPE, + 'show': True, + 'index': point_idx, + 'preview': data[self.tooltip_preview] + if self.tooltip_preview is not None + else None, + 'properties': data[ + self.tooltip_properties_non_visual_info.keys() + ].to_dict() + if self.tooltip_properties_non_visual_info is not None + else {}, + } + ) def create_download_view_button(self, icon_only=True, width=36): button = Button( @@ -271,10 +367,12 @@ def create_download_view_button(self, icon_only=True, width=36): ) def click_handler(event): - self.send({ - "type": EVENT_TYPES["VIEW_DOWNLOAD"], - "transparentBackgroundColor": bool(event["alt_key"]), - }) + self.send( + { + 'type': EVENT_TYPES['VIEW_DOWNLOAD'], + 'transparentBackgroundColor': bool(event['alt_key']), + } + ) button.on_click(click_handler) return button @@ -288,31 +386,32 @@ def create_save_view_button(self, icon_only=True, width=36): ) def click_handler(event): - self.send({ - "type": EVENT_TYPES["VIEW_SAVE"], - "transparentBackgroundColor": bool(event["alt_key"]), - }) + self.send( + { + 'type': EVENT_TYPES['VIEW_SAVE'], + 'transparentBackgroundColor': bool(event['alt_key']), + } + ) button.on_click(click_handler) return button def reset_view(self, animation: int = 0, data_extent: bool = False): if data_extent: - self.send({ - "type": EVENT_TYPES["VIEW_RESET"], - "area": { - "x": self.points[:, 0].min(), - "width": self.points[:, 0].max() - self.points[:, 0].min(), - "y": self.points[:, 1].min(), - "height": self.points[:, 1].max() - self.points[:, 1].min(), - }, - "animation": animation - }) + self.send( + { + 'type': EVENT_TYPES['VIEW_RESET'], + 'area': { + 'x': self.points[:, 0].min(), + 'width': self.points[:, 0].max() - self.points[:, 0].min(), + 'y': self.points[:, 1].min(), + 'height': self.points[:, 1].max() - self.points[:, 1].min(), + }, + 'animation': animation, + } + ) else: - self.send({ - "type": EVENT_TYPES["VIEW_RESET"], - "animation": animation - }) + self.send({'type': EVENT_TYPES['VIEW_RESET'], 'animation': animation}) def create_reset_view_button(self, icon_only=True, width=36): button = Button( @@ -323,7 +422,7 @@ def create_reset_view_button(self, icon_only=True, width=36): ) def click_handler(event): - self.reset_view(500, event["alt_key"]) + self.reset_view(500, event['alt_key']) button.on_click(click_handler) return button @@ -338,7 +437,7 @@ def create_mouse_mode_toggle_button( description='', icon=icon, tooltip=tooltip, - button_style = 'primary' if self.mouse_mode == mouse_mode else '', + button_style='primary' if self.mouse_mode == mouse_mode else '', ) button.layout.width = '36px' @@ -388,31 +487,25 @@ def show(self): margin='10px 0', width='100%', height='0', - border='1px solid var(--jp-layout-color2)' - ) + border='1px solid var(--jp-layout-color2)', + ), ), button_view_save, button_view_download, button_view_reset, ], layout=widgets.Layout( - display='flex', - flex_flow='column', - align_items='stretch', - width='40px' - ) + display='flex', flex_flow='column', align_items='stretch', width='40px' + ), ) plots = widgets.VBox( - children=[self], - layout=widgets.Layout( - flex='1', - width='auto' - ) + children=[self], layout=widgets.Layout(flex='1', width='auto') ) return widgets.HBox([buttons, plots]) + class Button(anywidget.AnyWidget): _esm = """ function render({ model, el }) { @@ -496,9 +589,9 @@ def __init__(self, **kwargs): self.on_msg(self._handle_custom_msg) def _handle_custom_msg(self, event: dict, buffers): - if event["type"] == "click" and self._click_handler is not None: + if event['type'] == 'click' and self._click_handler is not None: self._click_handler(event) - if event["type"] == "dblclick" and self._dblclick_handler is not None: + if event['type'] == 'dblclick' and self._dblclick_handler is not None: self._dblclick_handler(event) def on_click(self, callback): diff --git a/notebooks/annotations.ipynb b/notebooks/annotations.ipynb index 3fd53801..6fee79e1 100644 --- a/notebooks/annotations.ipynb +++ b/notebooks/annotations.ipynb @@ -43,8 +43,10 @@ "c4 = jscatter.Rect(x_start=-1.5, x_end=-0.5, y_start=-1.25, y_end=-0.75)\n", "\n", "scatter = jscatter.Scatter(\n", - " x=np.concatenate((x1, x2, x3, x4)), x_scale=(-2, 2),\n", - " y=np.concatenate((y1, y2, y3, y4)), y_scale=(-2, 2),\n", + " x=np.concatenate((x1, x2, x3, x4)),\n", + " x_scale=(-2, 2),\n", + " y=np.concatenate((y1, y2, y3, y4)),\n", + " y_scale=(-2, 2),\n", " annotations=[x0, y0, c1, c2, c3, c4],\n", " width=400,\n", " height=400,\n", diff --git a/notebooks/demo.ipynb b/notebooks/demo.ipynb index 7737acf0..f54246bb 100644 --- a/notebooks/demo.ipynb +++ b/notebooks/demo.ipynb @@ -27,7 +27,7 @@ " data=geonames,\n", " x='Longitude',\n", " y='Latitude',\n", - " color_by=\"Continent\",\n", + " color_by='Continent',\n", " height=360,\n", ")\n", "\n", @@ -137,7 +137,7 @@ " color_by='class',\n", " background_color='black',\n", " axes=False,\n", - " height=480\n", + " height=480,\n", ")" ] }, @@ -158,6 +158,7 @@ "import traitlets\n", "import traittypes\n", "\n", + "\n", "class ImagesWidget(anywidget.AnyWidget):\n", " _esm = \"\"\"\n", " const baseUrl = 'https://paper.jupyter-scatter.dev/fashion-mnist-images/';\n", @@ -218,6 +219,7 @@ "\n", " images = traittypes.Array(default_value=[]).tag(sync=True)\n", "\n", + "\n", "images = ImagesWidget()" ] }, @@ -236,9 +238,11 @@ "source": [ "import ipywidgets\n", "\n", + "\n", "def selection_change_handler(change):\n", " images.images = change['new']\n", "\n", + "\n", "scatter.widget.observe(selection_change_handler, names=['selection'])\n", "\n", "ipywidgets.AppLayout(center=scatter.show(), right_sidebar=images)" @@ -257,6 +261,7 @@ "metadata": {}, "outputs": [], "source": [ + "# fmt: off\n", "scatter.selection([\n", " 1254, 52549, 47543, 11095, 34364, 36959, 11363, 9277, 23068,\n", " 8921, 54801, 46398, 51721, 20057, 50162, 572, 59831, 43542,\n", @@ -267,8 +272,8 @@ " 15293, 9619, 872, 20886, 57006, 42770, 41476, 54424, 34547,\n", " 6570, 5556, 36400, 14179, 16730, 15361, 5192, 58429, 59357,\n", " 2789, 30767, 46375, 45233, 32280, 58065, 20809, 17061, 27960,\n", - " 42692\n", - "])" + "])\n", + "# fmt: on" ] }, { @@ -302,7 +307,7 @@ "cae = jscatter.Scatter(x='caeX', y='caeY', **config)\n", "\n", "jscatter.compose(\n", - " [(pca, \"PCA\"), (tsne, \"t-SNE\"), (umap, \"UMAP\"), (cae, \"CAE\")],\n", + " [(pca, 'PCA'), (tsne, 't-SNE'), (umap, 'UMAP'), (cae, 'CAE')],\n", " sync_selection=True,\n", " sync_hover=True,\n", " rows=2,\n", diff --git a/notebooks/examples.ipynb b/notebooks/examples.ipynb index 0c5d2aae..fe9debde 100644 --- a/notebooks/examples.ipynb +++ b/notebooks/examples.ipynb @@ -193,7 +193,9 @@ } ], "source": [ - "scatter.color(by='group').size(by='value').height(320).selection(df.query('x < 0.5').index).axes(grid=True, labels=['X', 'Y']).legend(True)" + "scatter.color(by='group').size(by='value').height(320).selection(\n", + " df.query('x < 0.5').index\n", + ").axes(grid=True, labels=['X', 'Y']).legend(True)" ] }, { @@ -224,16 +226,18 @@ } ], "source": [ - "scatter.color(map=dict(\n", - " C='red',\n", - " B='blue',\n", - " A='yellow',\n", - " D='pink',\n", - " E='green',\n", - " F='brown',\n", - " G='gray',\n", - " H='#56B4E9'\n", - "))" + "scatter.color(\n", + " map=dict(\n", + " C='red',\n", + " B='blue',\n", + " A='yellow',\n", + " D='pink',\n", + " E='green',\n", + " F='brown',\n", + " G='gray',\n", + " H='#56B4E9',\n", + " )\n", + ")" ] }, { @@ -275,12 +279,12 @@ " color_by='value',\n", " color_order='reverse',\n", " size_by='value',\n", - " size_map=[10,11,12,13,14,15],\n", + " size_map=[10, 11, 12, 13, 14, 15],\n", " opacity_by='value',\n", " connect_by='group',\n", " connection_color_by='value',\n", " connection_color_order='reverse',\n", - " height=600\n", + " height=600,\n", ")" ] }, @@ -303,7 +307,7 @@ "source": [ "def roesslerAttractor(num):\n", " from math import inf\n", - " \n", + "\n", " points = []\n", "\n", " xn = 2.644838333129883\n", @@ -342,10 +346,9 @@ " minY = min(yn1, minY)\n", " maxY = max(yn1, maxY)\n", "\n", - " xn = xn1;\n", - " yn = yn1;\n", - " zn = zn1;\n", - "\n", + " xn = xn1\n", + " yn = yn1\n", + " zn = zn1\n", "\n", " dX = maxX - minX\n", " dY = maxY - minY\n", @@ -384,7 +387,7 @@ ], "source": [ "points = np.asarray(roesslerAttractor(1000000))\n", - "jscatter.plot(points[:,0], points[:,1], height=640)" + "jscatter.plot(points[:, 0], points[:, 1], height=640)" ] }, { diff --git a/notebooks/get-started.ipynb b/notebooks/get-started.ipynb index a6bab6a9..4048857a 100644 --- a/notebooks/get-started.ipynb +++ b/notebooks/get-started.ipynb @@ -116,13 +116,13 @@ ], "source": [ "jscatter.plot(\n", - " data=df,\n", - " x='mass',\n", - " y='speed',\n", - " size=8, # static encoding\n", - " color_by='group', # data-driven encoding\n", - " opacity_by='density', # view-driven encoding\n", - " legend=True,\n", + " data=df,\n", + " x='mass',\n", + " y='speed',\n", + " size=8, # static encoding\n", + " color_by='group', # data-driven encoding\n", + " opacity_by='density', # view-driven encoding\n", + " legend=True,\n", ")" ] }, @@ -287,10 +287,7 @@ } ], "source": [ - "jscatter.link([\n", - " jscatter.Scatter(x=x, y=y),\n", - " jscatter.Scatter(x=x, y=y)\n", - "])" + "jscatter.link([jscatter.Scatter(x=x, y=y), jscatter.Scatter(x=x, y=y)])" ] }, { diff --git a/notebooks/linking.ipynb b/notebooks/linking.ipynb index 117198f9..09784cfa 100644 --- a/notebooks/linking.ipynb +++ b/notebooks/linking.ipynb @@ -109,7 +109,9 @@ "import pyarrow as pa\n", "import requests\n", "\n", - "r = requests.get('https://storage.googleapis.com/flekschas/regl-scatterplot/fashion-mnist-embeddings.arrow')\n", + "r = requests.get(\n", + " 'https://storage.googleapis.com/flekschas/regl-scatterplot/fashion-mnist-embeddings.arrow'\n", + ")\n", "\n", "with pa.ipc.open_file(io.BytesIO(r.content)) as reader:\n", " embeddings = reader.read_pandas()\n", @@ -150,7 +152,18 @@ "config = dict(\n", " background_color='#111111',\n", " color_by='class',\n", - " color_map=['#FFFF00', '#1CE6FF', '#FF34FF', '#FF4A46', '#008941', '#006FA6', '#A30059', '#FFDBE5', '#7A4900', '#0000A6']\n", + " color_map=[\n", + " '#FFFF00',\n", + " '#1CE6FF',\n", + " '#FF34FF',\n", + " '#FF4A46',\n", + " '#008941',\n", + " '#006FA6',\n", + " '#A30059',\n", + " '#FFDBE5',\n", + " '#7A4900',\n", + " '#0000A6',\n", + " ],\n", ")\n", "\n", "scatter_pca = jscatter.Scatter(data=embeddings, x='pcaX', y='pcaY', **config)\n", @@ -158,7 +171,9 @@ "scatter_umap = jscatter.Scatter(data=embeddings, x='umapX', y='umapY', **config)\n", "scatter_cae = jscatter.Scatter(data=embeddings, x='caeX', y='caeY', **config)\n", "\n", - "jscatter.link([scatter_pca, scatter_tsne, scatter_umap, scatter_cae], rows=2, row_height=240)" + "jscatter.link(\n", + " [scatter_pca, scatter_tsne, scatter_umap, scatter_cae], rows=2, row_height=240\n", + ")" ] }, { @@ -197,32 +212,41 @@ "X3, Y3 = np.mgrid[0:8:1, 0:8:1]\n", "\n", "df1 = pd.DataFrame(\n", - " np.concatenate((\n", - " np.expand_dims(X1.flatten(), axis=1),\n", - " np.expand_dims(Y1.flatten(), axis=1),\n", - " np.expand_dims(np.repeat(np.arange(4), 1), axis=1)\n", - " ), axis=1),\n", + " np.concatenate(\n", + " (\n", + " np.expand_dims(X1.flatten(), axis=1),\n", + " np.expand_dims(Y1.flatten(), axis=1),\n", + " np.expand_dims(np.repeat(np.arange(4), 1), axis=1),\n", + " ),\n", + " axis=1,\n", + " ),\n", " columns=['x', 'y', 'group'],\n", ")\n", "df1.group = df1.group.astype('category')\n", "\n", "df2 = pd.DataFrame(\n", - " np.concatenate((\n", - " np.expand_dims(X2.flatten(), axis=1),\n", - " np.expand_dims(Y2.flatten(), axis=1),\n", - " np.expand_dims(np.repeat(np.arange(4), 4), axis=1)\n", - " ), axis=1),\n", - " columns=['x', 'y', 'group']\n", + " np.concatenate(\n", + " (\n", + " np.expand_dims(X2.flatten(), axis=1),\n", + " np.expand_dims(Y2.flatten(), axis=1),\n", + " np.expand_dims(np.repeat(np.arange(4), 4), axis=1),\n", + " ),\n", + " axis=1,\n", + " ),\n", + " columns=['x', 'y', 'group'],\n", ")\n", "df2.group = df2.group.astype('category')\n", "\n", "df3 = pd.DataFrame(\n", - " np.concatenate((\n", - " np.expand_dims(X3.flatten(), axis=1),\n", - " np.expand_dims(Y3.flatten(), axis=1),\n", - " np.expand_dims(np.repeat(np.arange(4), 16), axis=1)\n", - " ), axis=1),\n", - " columns=['x', 'y', 'group']\n", + " np.concatenate(\n", + " (\n", + " np.expand_dims(X3.flatten(), axis=1),\n", + " np.expand_dims(Y3.flatten(), axis=1),\n", + " np.expand_dims(np.repeat(np.arange(4), 16), axis=1),\n", + " ),\n", + " axis=1,\n", + " ),\n", + " columns=['x', 'y', 'group'],\n", ")\n", "df3.group = df3.group.astype('category')" ] @@ -430,7 +454,7 @@ " [jscatter.Scatter(x=x, y=y) for i in range(4)],\n", " sync_selection=True,\n", " sync_hover=True,\n", - " rows=2\n", + " rows=2,\n", ")" ] }, diff --git a/pyproject.toml b/pyproject.toml index 6e829c06..159edbe1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,14 @@ filterwarnings = [ ] [tool.uv] -dev-dependencies = ["anywidget[dev]", "jupyterlab", "pyarrow", "pytest", "ruff"] +dev-dependencies = [ + "anywidget[dev]", + "jupyterlab", + "pre-commit", + "pyarrow", + "pytest", + "ruff", +] [tool.ruff.format] quote-style = "single" diff --git a/test-environments/test.ipynb b/test-environments/test.ipynb index e63cef1e..fcfbd66c 100644 --- a/test-environments/test.ipynb +++ b/test-environments/test.ipynb @@ -24,6 +24,7 @@ "source": [ "import numpy as np\n", "import jscatter\n", + "\n", "jscatter.plot(x=np.random.rand(500), y=np.random.rand(500))" ] }, diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 75f01eab..c6844809 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -5,6 +5,7 @@ from jscatter.annotations import HLine, VLine, Line, Rect from jscatter.jscatter import Scatter + @pytest.fixture def df() -> pd.DataFrame: x1, y1 = np.random.normal(-1, 0.2, 1000), np.random.normal(+1, 0.05, 1000) @@ -12,10 +13,13 @@ def df() -> pd.DataFrame: x3, y3 = np.random.normal(+1, 0.2, 1000), np.random.normal(-1, 0.05, 1000) x4, y4 = np.random.normal(-1, 0.2, 1000), np.random.normal(-1, 0.05, 1000) - return pd.DataFrame({ - 'x': np.concatenate((x1, x2, x3, x4)), - 'y': np.concatenate((y1, y2, y3, y4)), - }) + return pd.DataFrame( + { + 'x': np.concatenate((x1, x2, x3, x4)), + 'y': np.concatenate((y1, y2, y3, y4)), + } + ) + def test_hline(): y0 = HLine(y=0, x_start=-1, x_end=1, line_color='red', line_width=2) @@ -25,6 +29,7 @@ def test_hline(): assert y0.line_color == (1.0, 0.0, 0.0, 1.0) assert y0.line_width == 2 + def test_vline(): x0 = VLine(x=0, y_start=-1, y_end=1, line_color='red', line_width=2) assert x0.x == 0 @@ -33,6 +38,7 @@ def test_vline(): assert x0.line_color == (1.0, 0.0, 0.0, 1.0) assert x0.line_width == 2 + def test_line(): vertices = [(0.0, 0.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)] l = Line(vertices=vertices, line_color='red', line_width=2) @@ -40,6 +46,7 @@ def test_line(): assert l.line_color == (1.0, 0.0, 0.0, 1.0) assert l.line_width == 2 + def test_rect(): r = Rect(x_start=-1, x_end=1, y_start=-1, y_end=1, line_color='red', line_width=2) assert r.x_start == -1 @@ -49,6 +56,7 @@ def test_rect(): assert r.line_color == (1.0, 0.0, 0.0, 1.0) assert r.line_width == 2 + def test_scatter_annotations(df: pd.DataFrame): x0 = VLine(0) y0 = HLine(0) @@ -57,12 +65,14 @@ def test_scatter_annotations(df: pd.DataFrame): c3 = Rect(x_start=+0.5, x_end=+1.5, y_start=-1.25, y_end=-0.75) c4 = Rect(x_start=-1.5, x_end=-0.5, y_start=-1.25, y_end=-0.75) - annotations=[x0, y0, c1, c2, c3, c4] + annotations = [x0, y0, c1, c2, c3, c4] scatter = Scatter( data=df, - x='x', x_scale=(-2, 2), - y='y', y_scale=(-2, 2), + x='x', + x_scale=(-2, 2), + y='y', + y_scale=(-2, 2), annotations=annotations, ) diff --git a/tests/test_composite_annotations.py b/tests/test_composite_annotations.py index 31934518..2c39fb30 100644 --- a/tests/test_composite_annotations.py +++ b/tests/test_composite_annotations.py @@ -1,4 +1,5 @@ import matplotlib + matplotlib.use('Agg') import pytest @@ -9,7 +10,8 @@ from jscatter.composite_annotations import Contour from jscatter.jscatter import Scatter -@pytest.mark.skipif(sys.version_info < (3,9), reason="requires at least Python v3.9") + +@pytest.mark.skipif(sys.version_info < (3, 9), reason='requires at least Python v3.9') def test_contour(): c = Contour() @@ -19,7 +21,7 @@ def test_contour(): assert c.line_opacity_by_level is False scatter = Scatter( - data=sns.load_dataset("geyser"), + data=sns.load_dataset('geyser'), x='waiting', y='duration', color_by='kind', diff --git a/tests/test_jscatter.py b/tests/test_jscatter.py index 8bbbd4a9..e1eb31be 100644 --- a/tests/test_jscatter.py +++ b/tests/test_jscatter.py @@ -92,6 +92,7 @@ def df3() -> pd.DataFrame: return df + def test_component_idx_to_name(): assert component_idx_to_name(2) == 'valueA' assert component_idx_to_name(3) == 'valueB' @@ -127,10 +128,18 @@ def test_scatter_pandas_update(df, df2): x = 'a' y = 'b' scatter = Scatter(data=df, x=x, y=y) - assert np.allclose(np.array([df[x].min(), df[x].max()]), np.array(scatter.widget.x_domain)) - assert np.allclose(np.array([df[y].min(), df[y].max()]), np.array(scatter.widget.y_domain)) - assert np.allclose(np.array([df[x].min(), df[x].max()]), np.array(scatter.widget.x_scale_domain)) - assert np.allclose(np.array([df[y].min(), df[y].max()]), np.array(scatter.widget.y_scale_domain)) + assert np.allclose( + np.array([df[x].min(), df[x].max()]), np.array(scatter.widget.x_domain) + ) + assert np.allclose( + np.array([df[y].min(), df[y].max()]), np.array(scatter.widget.y_domain) + ) + assert np.allclose( + np.array([df[x].min(), df[x].max()]), np.array(scatter.widget.x_scale_domain) + ) + assert np.allclose( + np.array([df[y].min(), df[y].max()]), np.array(scatter.widget.y_scale_domain) + ) prev_x_scale_domain = np.array(scatter.widget.x_scale_domain) prev_y_scale_domain = np.array(scatter.widget.y_scale_domain) @@ -138,8 +147,12 @@ def test_scatter_pandas_update(df, df2): scatter.data(df2) # The data domain updated by the scale domain remain unchanged as the view was not reset - assert np.allclose(np.array([df2[x].min(), df2[x].max()]), np.array(scatter.widget.x_domain)) - assert np.allclose(np.array([df2[y].min(), df2[y].max()]), np.array(scatter.widget.y_domain)) + assert np.allclose( + np.array([df2[x].min(), df2[x].max()]), np.array(scatter.widget.x_domain) + ) + assert np.allclose( + np.array([df2[y].min(), df2[y].max()]), np.array(scatter.widget.y_domain) + ) assert np.allclose(prev_x_scale_domain, np.array(scatter.widget.x_scale_domain)) assert np.allclose(prev_y_scale_domain, np.array(scatter.widget.y_scale_domain)) @@ -147,16 +160,28 @@ def test_scatter_pandas_update(df, df2): scatter.data(df2, reset_scales=True) # Now that we reset the view, both the data and scale domain updated properly - assert np.allclose(np.array([df2[x].min(), df2[x].max()]), np.array(scatter.widget.x_domain)) - assert np.allclose(np.array([df2[y].min(), df2[y].max()]), np.array(scatter.widget.y_domain)) - assert np.allclose(np.array([df2[x].min(), df2[x].max()]), np.array(scatter.widget.x_scale_domain)) - assert np.allclose(np.array([df2[y].min(), df2[y].max()]), np.array(scatter.widget.y_scale_domain)) + assert np.allclose( + np.array([df2[x].min(), df2[x].max()]), np.array(scatter.widget.x_domain) + ) + assert np.allclose( + np.array([df2[y].min(), df2[y].max()]), np.array(scatter.widget.y_domain) + ) + assert np.allclose( + np.array([df2[x].min(), df2[x].max()]), np.array(scatter.widget.x_scale_domain) + ) + assert np.allclose( + np.array([df2[y].min(), df2[y].max()]), np.array(scatter.widget.y_scale_domain) + ) assert df[x].max() != df2[x].max() assert df[y].max() != df2[y].max() - assert np.allclose(to_ndc(df2[x].values, create_default_norm()), scatter.widget.points[:, 0]) - assert np.allclose(to_ndc(df2[y].values, create_default_norm()), scatter.widget.points[:, 1]) + assert np.allclose( + to_ndc(df2[x].values, create_default_norm()), scatter.widget.points[:, 0] + ) + assert np.allclose( + to_ndc(df2[y].values, create_default_norm()), scatter.widget.points[:, 1] + ) def test_xy_scale_shorthands(df): @@ -200,26 +225,26 @@ def test_scatter_point_encoding_updates(df: pd.DataFrame): widget_data = np.asarray(widget.points) assert len(scatter._encodings.data) == 0 - assert np.sum(widget_data[:,2]) == 0 + assert np.sum(widget_data[:, 2]) == 0 scatter.color(by='group') widget_data = np.asarray(widget.points) assert 'color' in scatter._encodings.visual assert 'group:linear' in scatter._encodings.data - assert np.sum(widget_data[:,2]) > 0 - assert np.sum(widget_data[:,3]) == 0 + assert np.sum(widget_data[:, 2]) > 0 + assert np.sum(widget_data[:, 3]) == 0 scatter.opacity(by='c') widget_data = np.asarray(widget.points) assert 'opacity' in scatter._encodings.visual assert 'c:linear' in scatter._encodings.data - assert np.sum(widget_data[:,3]) > 0 + assert np.sum(widget_data[:, 3]) > 0 scatter.size(by='c') widget_data = np.asarray(widget.points) assert 'size' in scatter._encodings.visual assert 'c:linear' in scatter._encodings.data - assert np.sum(widget_data[:,3]) > 0 + assert np.sum(widget_data[:, 3]) > 0 def test_scatter_connection_by(df: pd.DataFrame): @@ -249,90 +274,84 @@ def test_missing_values_handling(): with_nan = np.array([0, 0.25, 0.5, np.nan, 1]) no_nan = np.array([0, 0.25, 0.5, 0.75, 1]) - df = pd.DataFrame({ - 'x': with_nan, - 'y': with_nan, - 'z': with_nan, - 'w': with_nan, - }) + df = pd.DataFrame( + { + 'x': with_nan, + 'y': with_nan, + 'z': with_nan, + 'w': with_nan, + } + ) base_warning = 'data contains missing values. Those missing values will be replaced with zeros.' with pytest.warns(UserWarning, match=f'X {base_warning}'): - scatter = Scatter( - data=pd.DataFrame({ 'x': with_nan, 'y': no_nan }), - x='x', - y='y' - ) + scatter = Scatter(data=pd.DataFrame({'x': with_nan, 'y': no_nan}), x='x', y='y') print(scatter.widget.points) assert np.isfinite(scatter.widget.points).all() assert scatter.widget.points[3, 0] == -1 with pytest.warns(UserWarning, match=f'Y {base_warning}'): - scatter = Scatter( - data=pd.DataFrame({ 'x': no_nan, 'y': with_nan }), - x='x', - y='y' - ) + scatter = Scatter(data=pd.DataFrame({'x': no_nan, 'y': with_nan}), x='x', y='y') assert np.isfinite(scatter.widget.points).all() assert scatter.widget.points[3, 1] == -1 with pytest.warns(UserWarning, match=f'Color {base_warning}'): scatter = Scatter( - data=pd.DataFrame({ 'x': no_nan, 'y': no_nan, 'z': with_nan }), + data=pd.DataFrame({'x': no_nan, 'y': no_nan, 'z': with_nan}), x='x', y='y', - color_by='z' + color_by='z', ) assert np.isfinite(scatter.widget.points).all() assert scatter.widget.points[3, 2] == 0 with pytest.warns(UserWarning, match=f'Opacity {base_warning}'): scatter = Scatter( - data=pd.DataFrame({ 'x': no_nan, 'y': no_nan, 'z': with_nan }), + data=pd.DataFrame({'x': no_nan, 'y': no_nan, 'z': with_nan}), x='x', y='y', - opacity_by='z' + opacity_by='z', ) assert np.isfinite(scatter.widget.points).all() assert scatter.widget.points[3, 2] == 0 with pytest.warns(UserWarning, match=f'Size {base_warning}'): scatter = Scatter( - data=pd.DataFrame({ 'x': no_nan, 'y': no_nan, 'z': with_nan }), + data=pd.DataFrame({'x': no_nan, 'y': no_nan, 'z': with_nan}), x='x', y='y', - size_by='z' + size_by='z', ) assert np.isfinite(scatter.widget.points).all() assert scatter.widget.points[3, 2] == 0 with pytest.warns(UserWarning, match=f'Connection color {base_warning}'): scatter = Scatter( - data=pd.DataFrame({ 'x': no_nan, 'y': no_nan, 'z': with_nan }), + data=pd.DataFrame({'x': no_nan, 'y': no_nan, 'z': with_nan}), x='x', y='y', - connection_color_by='z' + connection_color_by='z', ) assert np.isfinite(scatter.widget.points).all() assert scatter.widget.points[3, 2] == 0 with pytest.warns(UserWarning, match=f'Connection opacity {base_warning}'): scatter = Scatter( - data=pd.DataFrame({ 'x': no_nan, 'y': no_nan, 'z': with_nan }), + data=pd.DataFrame({'x': no_nan, 'y': no_nan, 'z': with_nan}), x='x', y='y', - connection_opacity_by='z' + connection_opacity_by='z', ) assert np.isfinite(scatter.widget.points).all() assert scatter.widget.points[3, 2] == 0 with pytest.warns(UserWarning, match=f'Connection size {base_warning}'): scatter = Scatter( - data=pd.DataFrame({ 'x': no_nan, 'y': no_nan, 'z': with_nan }), + data=pd.DataFrame({'x': no_nan, 'y': no_nan, 'z': with_nan}), x='x', y='y', - connection_size_by='z' + connection_size_by='z', ) assert np.isfinite(scatter.widget.points).all() assert scatter.widget.points[3, 2] == 0 @@ -351,14 +370,18 @@ def test_scatter_axes_labels(df: pd.DataFrame): assert scatter.widget.axes_labels == ['axis 1', 'axis 2'] -def test_scatter_transition_points(df: pd.DataFrame, df2: pd.DataFrame, df3: pd.DataFrame): +def test_scatter_transition_points( + df: pd.DataFrame, df2: pd.DataFrame, df3: pd.DataFrame +): scatter = Scatter(data=df, x='a', y='b') # Default settings assert scatter._transition_points == True assert scatter._transition_points_duration == 3000 - scatter = Scatter(data=df, x='a', y='b', transition_points=False, transition_points_duration=500) + scatter = Scatter( + data=df, x='a', y='b', transition_points=False, transition_points_duration=500 + ) assert scatter._transition_points == False assert scatter._transition_points_duration == 500 @@ -448,66 +471,65 @@ def test_scatter_check_encoding_dtype(df: pd.DataFrame): check_encoding_dtype(scatter.color_data) with pytest.raises(ValueError): - check_encoding_dtype(pd.Series(np.array([1+0j]))) + check_encoding_dtype(pd.Series(np.array([1 + 0j]))) def test_tooltip(df: pd.DataFrame): # Test initializing a scatter plot with tooltip properties scatter = Scatter( - data=df, - x="a", - y="b", - tooltip=True, - tooltip_properties=["a", "b", "c", "group"] + data=df, x='a', y='b', tooltip=True, tooltip_properties=['a', 'b', 'c', 'group'] ) assert scatter.widget.tooltip_enable == True - assert scatter.widget.tooltip_properties == ["a", "b", "c", "group"] + assert scatter.widget.tooltip_properties == ['a', 'b', 'c', 'group'] assert scatter.widget.tooltip_histograms == True - assert scatter.widget.tooltip_histograms_size == "small" + assert scatter.widget.tooltip_histograms_size == 'small' - normalized_x_histogram = np.histogram(df["a"].values, bins=20)[0] + normalized_x_histogram = np.histogram(df['a'].values, bins=20)[0] normalized_x_histogram = normalized_x_histogram / normalized_x_histogram.max() assert np.array_equal(scatter.widget.x_histogram, normalized_x_histogram) - normalized_y_histogram = np.histogram(df["b"].values, bins=20)[0] + normalized_y_histogram = np.histogram(df['b'].values, bins=20)[0] normalized_y_histogram = normalized_y_histogram / normalized_y_histogram.max() assert np.array_equal(scatter.widget.y_histogram, normalized_y_histogram) - assert "c" in scatter.widget.tooltip_properties_non_visual_info - assert "group" in scatter.widget.tooltip_properties_non_visual_info + assert 'c' in scatter.widget.tooltip_properties_non_visual_info + assert 'group' in scatter.widget.tooltip_properties_non_visual_info - normalized_c_histogram = np.histogram(df["c"].values, bins=20)[0] + normalized_c_histogram = np.histogram(df['c'].values, bins=20)[0] normalized_c_histogram = normalized_c_histogram / normalized_c_histogram.max() assert np.array_equal( - scatter.widget.tooltip_properties_non_visual_info["c"]["histogram"], - normalized_c_histogram + scatter.widget.tooltip_properties_non_visual_info['c']['histogram'], + normalized_c_histogram, ) - normalized_group_histogram = df["group"].copy().astype(str).astype('category').cat.codes.value_counts() + normalized_group_histogram = ( + df['group'].copy().astype(str).astype('category').cat.codes.value_counts() + ) normalized_group_histogram = [ - y for _, y in sorted( + y + for _, y in sorted( dict(normalized_group_histogram / normalized_group_histogram.sum()).items() ) ] assert np.array_equal( - scatter.widget.tooltip_properties_non_visual_info["group"]["histogram"], - normalized_group_histogram + scatter.widget.tooltip_properties_non_visual_info['group']['histogram'], + normalized_group_histogram, ) # Test updating tooltip properties - scatter.tooltip(properties=["a", "c"]) - assert scatter.widget.tooltip_properties == ["a", "c"] + scatter.tooltip(properties=['a', 'c']) + assert scatter.widget.tooltip_properties == ['a', 'c'] # Test disabling tooltip scatter.tooltip(False) assert scatter.widget.tooltip_enable == False # Test enabling tooltip without specifying properties - scatter = Scatter(data=df, x="b", y="d") + scatter = Scatter(data=df, x='b', y='d') scatter.tooltip(True) assert scatter.widget.tooltip_enable == True - assert scatter.widget.tooltip_properties == ["x", "y", "color", "opacity", "size"] + assert scatter.widget.tooltip_properties == ['x', 'y', 'color', 'opacity', 'size'] # Test with invalid property - scatter.tooltip(properties=["color", "invalid_column"]) - assert scatter.widget.tooltip_properties == ["color"] + scatter.tooltip(properties=['color', 'invalid_column']) + assert scatter.widget.tooltip_properties == ['color'] diff --git a/tests/test_utils.py b/tests/test_utils.py index f4fe0be0..12e5b26a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -91,7 +91,7 @@ def test_any_not(list_input, value, expected_result): 'uri_input,expected_validity', [ ('http://example.com/foo', True), - ('http://example.com', False), # Must have a path + ('http://example.com', False), # Must have a path ('not a uri', False), ], ) diff --git a/uv.lock b/uv.lock index 22a2c4c4..fabc5119 100644 --- a/uv.lock +++ b/uv.lock @@ -241,6 +241,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, ] +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + [[package]] name = "charset-normalizer" version = "3.4.0" @@ -477,6 +486,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, ] +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + [[package]] name = "exceptiongroup" version = "1.2.2" @@ -504,6 +522,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/ca/086311cdfc017ec964b2436fe0c98c1f4efcb7e4c328956a22456e497655/fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a", size = 23543 }, ] +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + [[package]] name = "fonttools" version = "4.54.1" @@ -598,6 +625,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, ] +[[package]] +name = "identify" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/5f/05f0d167be94585d502b4adf8c7af31f1dc0b1c7e14f9938a88fdbbcf4a7/identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02", size = 99179 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/f5/09644a3ad803fae9eca8efa17e1f2aef380c7f0b02f7ec4e8d446e51d64a/identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd", size = 99049 }, +] + [[package]] name = "idna" version = "3.10" @@ -881,6 +917,7 @@ dependencies = [ dev = [ { name = "anywidget", extra = ["dev"] }, { name = "jupyterlab" }, + { name = "pre-commit" }, { name = "pyarrow" }, { name = "pytest" }, { name = "ruff" }, @@ -906,6 +943,7 @@ requires-dist = [ dev = [ { name = "anywidget", extras = ["dev"] }, { name = "jupyterlab" }, + { name = "pre-commit" }, { name = "pyarrow" }, { name = "pytest" }, { name = "ruff" }, @@ -1331,6 +1369,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + [[package]] name = "notebook-shim" version = "0.2.4" @@ -1612,6 +1659,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-py3-none-any.whl", hash = "sha256:3529a57096f7198778a5ceefd5ac3ef0e4d06a6ddaf9fc2d609b806f25302c47", size = 64574 }, ] +[[package]] +name = "pre-commit" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, +] + [[package]] name = "prometheus-client" version = "0.21.0" @@ -2469,6 +2532,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, ] +[[package]] +name = "virtualenv" +version = "20.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/75/53316a5a8050069228a2f6d11f32046cfa94fbb6cc3f08703f59b873de2e/virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa", size = 7650368 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/f9/0919cf6f1432a8c4baa62511f8f8da8225432d22e83e3476f5be1a1edc6e/virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0", size = 4276702 }, +] + [[package]] name = "watchfiles" version = "0.24.0"