├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── RELEASE.md ├── codegen ├── References.md ├── __init__.py ├── datatypes.py ├── download_plotly_schema.py ├── resources │ └── plot-schema.json ├── utils.py └── validators.py ├── examples └── overviews │ ├── Bar PCT dashboard.ipynb │ ├── DataShaderExample.ipynb │ ├── Overview.ipynb │ ├── Scatter GL.ipynb │ └── exports │ └── README.md ├── ipyplotly ├── __init__.py ├── _version.py ├── animation.py ├── basedatatypes.py ├── basevalidators.py ├── basewidget.py ├── callbacks.py └── serializers.py ├── js ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── Figure.js │ ├── embed.js │ ├── extension.js │ ├── index.js │ └── jupyterlab-plugin.js └── webpack.config.js ├── recipe ├── build.bat ├── build.sh ├── meta.yaml ├── post-link.bat ├── post-link.sh ├── pre-unlink.bat ├── pre-unlink.sh └── run_test.py ├── setup.cfg ├── setup.py └── test ├── codegen └── test_codegen.py ├── datatypes └── properties │ ├── conftest.py │ ├── test_compound_property.py │ └── test_simple_properties.py ├── resources └── 1x1-black.png ├── utils.py └── validators ├── test_angle_validator.py ├── test_any_validator.py ├── test_basetraces_validator.py ├── test_boolean_validator.py ├── test_color_validator.py ├── test_colorscale_validator.py ├── test_compound_validator.py ├── test_compoundarray_validator.py ├── test_dataarray_validator.py ├── test_enumerated_validator.py ├── test_flaglist_validator.py ├── test_imageuri_validator.py ├── test_infoarray_validator.py ├── test_integer_validator.py ├── test_number_validator.py ├── test_string_validator.py ├── test_subplotid_validator.py └── test_validators_common.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | .ipynb_checkpoints/ 3 | dist/ 4 | build/ 5 | *.py[cod] 6 | js/node_modules/ 7 | 8 | # Compiled javascript 9 | ipyplotly/static/ 10 | 11 | # OS X 12 | .DS_Store 13 | 14 | # PyCharm project 15 | .idea/ 16 | 17 | # Generated code 18 | ipyplotly/validators 19 | ipyplotly/datatypes 20 | 21 | # General scratch directories 22 | **/scratch 23 | 24 | # Examples 25 | /examples/overviews/exports/ 26 | !/examples/overviews/exports/README.md 27 | 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Johns Hopkins Applied Physics Laboratory 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude codegen * 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ipyplotly 2 | ========= 3 | 4 | Experiments towards a Pythonic [Plotly](https://plot.ly/) API 5 | and [ipywidget](http://ipywidgets.readthedocs.io/en/latest/index.html) for use in the Jupyter Notebook. 6 | 7 | Features 8 | -------- 9 | - Plots may be displayed in the notebook, and then updated in-place using property assignment syntax. 10 | - The entire plotting API is now discoverable using tab completion and documented with descriptive docstrings. 11 | - Property validation is performed in the Python library and informative error messages are raised on validation 12 | failures. 13 | - Arbitrary Python callbacks may be executed upon zoom, pan, click, hover, and data selection events. 14 | - Multiple views of the same plot may be displayed across different notebook output cells. 15 | - Static PNG and SVG images may be exported programmatically with no external dependencies or network connection 16 | required. 17 | - Plot transitions may be animated with custom duration and easing properties. 18 | - Numpy arrays are transferred between the Python and JavaScript libraries using the binary serialization protocol 19 | introduced in ipywidgets 7.0. 20 | - Plots may be combined with built-in 21 | [ipywidgets](http://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html) 22 | to create rich dashboard layouts in the notebook 23 | 24 | 25 | Development Installation 26 | ------------------------ 27 | 28 | For a development installation (requires npm), 29 | 30 | $ git clone https://github.com/jmmease/ipyplotly.git 31 | $ cd ipyplotly 32 | $ pip install -e . 33 | $ pip install yapf 34 | $ python setup.py codegen 35 | $ jupyter nbextension enable --py widgetsnbextension 36 | $ jupyter nbextension install --py --symlink --sys-prefix ipyplotly 37 | $ jupyter nbextension enable --py --sys-prefix ipyplotly 38 | 39 | Python Version Requirements 40 | --------------------------- 41 | - Usage requires Python >= 3.5 42 | - Code generation requires Python >= 3.6 43 | 44 | Future 45 | ------ 46 | This project was a successful experiment to test the feasibility of creating a 47 | Plotly ipywidget library. This approach has been embraced by the official 48 | [plotly.py](https://github.com/plotly/plotly.py) project and will be integrated into 49 | a new major version of plotly.py in the not-too-distant future. 50 | 51 | See [plotly/plotly.py#942](https://github.com/plotly/plotly.py/pull/942) for current status. 52 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | - To release a new version of ipyplotly on PyPI: 2 | 3 | Update _version.py (set release version, remove 'dev') 4 | git add and git commit 5 | python setup.py sdist upload 6 | python setup.py bdist_wheel upload 7 | git tag -a X.X.X -m 'comment' 8 | Update _version.py (add 'dev' and increment minor) 9 | git add and git commit 10 | git push 11 | git push --tags 12 | 13 | - To release a new version of ipyplotly on NPM: 14 | 15 | # nuke the `dist` and `node_modules` 16 | git clean -fdx 17 | npm install 18 | npm publish 19 | -------------------------------------------------------------------------------- /codegen/References.md: -------------------------------------------------------------------------------- 1 | Documentation of json schema 2 | 3 | https://api.plot.ly/v2/plot-schema?sha1 -------------------------------------------------------------------------------- /codegen/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os.path as opath 3 | import os 4 | import shutil 5 | 6 | import time 7 | 8 | from codegen.datatypes import build_datatypes_py, write_datatypes_py, append_figure_class 9 | from codegen.utils import TraceNode, PlotlyNode, LayoutNode, FrameNode 10 | from codegen.validators import write_validator_py, append_traces_validator_py 11 | 12 | 13 | def perform_codegen(): 14 | outdir = 'ipyplotly/' 15 | # outdir = 'codegen/output' 16 | # Load plotly schema 17 | # ------------------ 18 | with open('codegen/resources/plot-schema.json', 'r') as f: 19 | plotly_schema = json.load(f) 20 | 21 | # Compute property paths 22 | # ---------------------- 23 | base_traces_node = TraceNode(plotly_schema) 24 | compound_trace_nodes = PlotlyNode.get_all_compound_datatype_nodes(plotly_schema, TraceNode) 25 | compound_layout_nodes = PlotlyNode.get_all_compound_datatype_nodes(plotly_schema, LayoutNode) 26 | compound_frame_nodes = PlotlyNode.get_all_compound_datatype_nodes(plotly_schema, FrameNode) 27 | 28 | extra_layout_nodes = PlotlyNode.get_all_trace_layout_nodes(plotly_schema) 29 | 30 | # Write out validators 31 | # -------------------- 32 | validators_pkgdir = opath.join(outdir, 'validators') 33 | if opath.exists(validators_pkgdir): 34 | shutil.rmtree(validators_pkgdir) 35 | 36 | # ### Layout ### 37 | for node in compound_layout_nodes: 38 | write_validator_py(outdir, node, extra_layout_nodes) 39 | 40 | # ### Trace ### 41 | for node in compound_trace_nodes: 42 | write_validator_py(outdir, node) 43 | 44 | # Write out datatypes 45 | # ------------------- 46 | datatypes_pkgdir = opath.join(outdir, 'datatypes') 47 | if opath.exists(datatypes_pkgdir): 48 | shutil.rmtree(datatypes_pkgdir) 49 | 50 | # ### Layout ### 51 | for node in compound_layout_nodes: 52 | write_datatypes_py(outdir, node, extra_layout_nodes) 53 | 54 | # ### Trace ### 55 | for node in compound_trace_nodes: 56 | write_datatypes_py(outdir, node) 57 | 58 | # Append traces validator class 59 | # ----------------------------- 60 | append_traces_validator_py(validators_pkgdir, base_traces_node) 61 | 62 | # Add Frames 63 | # ---------- 64 | # ### Validator ### 65 | for node in compound_frame_nodes: 66 | write_validator_py(outdir, node) 67 | 68 | # ### Datatypes ### 69 | for node in compound_frame_nodes: 70 | write_datatypes_py(outdir, node) 71 | 72 | # Append figure class to datatypes 73 | # -------------------------------- 74 | append_figure_class(datatypes_pkgdir, base_traces_node) 75 | 76 | 77 | if __name__ == '__main__': 78 | perform_codegen() 79 | -------------------------------------------------------------------------------- /codegen/datatypes.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | import os 3 | import os.path as opath 4 | import textwrap 5 | import importlib 6 | from typing import List, Dict 7 | 8 | from codegen.utils import TraceNode, format_source, PlotlyNode 9 | 10 | 11 | def get_typing_type(plotly_type, array_ok=False): 12 | if plotly_type in ('data_array', 'info_array', 'colorlist'): 13 | pytype = 'List' 14 | elif plotly_type in ('string', 'color', 'colorscale', 'subplotid'): 15 | pytype = 'str' 16 | elif plotly_type in ('enumerated', 'flaglist', 'any'): 17 | pytype = 'Any' 18 | elif plotly_type in ('number', 'angle'): 19 | pytype = 'Number' 20 | elif plotly_type == 'integer': 21 | pytype = 'int' 22 | elif plotly_type == 'boolean': 23 | pytype = 'bool' 24 | else: 25 | raise ValueError('Unknown plotly type: %s' % plotly_type) 26 | 27 | if array_ok: 28 | return f'Union[{pytype}, List[{pytype}]]' 29 | else: 30 | return pytype 31 | 32 | 33 | def build_datatypes_py(parent_node: PlotlyNode, 34 | extra_nodes: Dict[str, 'PlotlyNode'] = {}): 35 | 36 | compound_nodes = parent_node.child_compound_datatypes 37 | if not compound_nodes: 38 | return None 39 | 40 | buffer = StringIO() 41 | 42 | # Imports 43 | # ------- 44 | buffer.write('from typing import *\n') 45 | buffer.write('from numbers import Number\n') 46 | buffer.write(f'from ipyplotly.basedatatypes import {parent_node.base_datatype_class}\n') 47 | 48 | # ### Validators ### 49 | validators_csv = ', '.join([f'{n.plotly_name} as v_{n.plotly_name}' for n in compound_nodes]) 50 | buffer.write(f'from ipyplotly.validators{parent_node.pkg_str} import ({validators_csv})\n') 51 | 52 | # ### Datatypes ### 53 | datatypes_csv = ', '.join([f'{n.plotly_name} as d_{n.plotly_name}' for n in compound_nodes if n.child_compound_datatypes]) 54 | if datatypes_csv: 55 | buffer.write(f'from ipyplotly.datatypes{parent_node.pkg_str} import ({datatypes_csv})\n') 56 | 57 | # Compound datatypes loop 58 | # ----------------------- 59 | for compound_node in compound_nodes: 60 | 61 | # grab literals 62 | literal_nodes = [n for n in compound_node.child_literals if n.plotly_name in ['type']] 63 | 64 | # ### Class definition ### 65 | buffer.write(f""" 66 | 67 | class {compound_node.name_class}({parent_node.base_datatype_class}):\n""") 68 | 69 | # ### Property definitions ### 70 | child_datatype_nodes = compound_node.child_datatypes 71 | extra_subtype_nodes = [node for node_name, node in 72 | extra_nodes.items() if 73 | node_name.startswith(compound_node.dir_str)] 74 | 75 | subtype_nodes = child_datatype_nodes + extra_subtype_nodes 76 | for subtype_node in subtype_nodes: 77 | if subtype_node.is_array_element: 78 | prop_type = f'Tuple[d_{compound_node.plotly_name}.{subtype_node.name_class}]' 79 | elif subtype_node.is_compound: 80 | prop_type = f'd_{compound_node.plotly_name}.{subtype_node.name_class}' 81 | else: 82 | prop_type = get_typing_type(subtype_node.datatype) 83 | 84 | 85 | # #### Get property description #### 86 | raw_description = subtype_node.description 87 | property_description = '\n'.join(textwrap.wrap(raw_description, 88 | subsequent_indent=' ' * 8, 89 | width=80 - 8)) 90 | 91 | # #### Get validator description #### 92 | validator = subtype_node.validator_instance 93 | validator_description = reindent_validator_description(validator, 4) 94 | 95 | # #### Combine to form property docstring #### 96 | if property_description.strip(): 97 | property_docstring = f"""{property_description} 98 | 99 | {validator_description}""" 100 | else: 101 | property_docstring = validator_description 102 | 103 | # #### Write property ### 104 | buffer.write(f"""\ 105 | 106 | # {subtype_node.name_property} 107 | # {'-' * len(subtype_node.name_property)} 108 | @property 109 | def {subtype_node.name_property}(self) -> {prop_type}: 110 | \"\"\" 111 | {property_docstring} 112 | \"\"\" 113 | return self['{subtype_node.name_property}']""") 114 | 115 | # #### Set property ### 116 | buffer.write(f""" 117 | 118 | @{subtype_node.name_property}.setter 119 | def {subtype_node.name_property}(self, val): 120 | self['{subtype_node.name_property}'] = val\n""") 121 | 122 | # ### Literals ### 123 | for literal_node in literal_nodes: 124 | buffer.write(f"""\ 125 | 126 | # {literal_node.name_property} 127 | # {'-' * len(literal_node.name_property)} 128 | @property 129 | def {literal_node.name_property}(self) -> {prop_type}: 130 | return self._props['{literal_node.name_property}']\n""") 131 | 132 | # ### Self properties description ### 133 | buffer.write(f""" 134 | 135 | # property parent name 136 | # -------------------- 137 | @property 138 | def _parent_path(self) -> str: 139 | return '{compound_node.parent_dir_str}' 140 | 141 | # Self properties description 142 | # --------------------------- 143 | @property 144 | def _prop_descriptions(self) -> str: 145 | return \"\"\"\\""") 146 | 147 | buffer.write(compound_node.get_constructor_params_docstring( 148 | indent=8, 149 | extra_nodes=extra_subtype_nodes)) 150 | 151 | buffer.write(f""" 152 | \"\"\"""") 153 | 154 | # ### Constructor ### 155 | buffer.write(f""" 156 | def __init__(self""") 157 | 158 | add_constructor_params(buffer, subtype_nodes) 159 | add_docstring(buffer, compound_node, extra_subtype_nodes) 160 | 161 | buffer.write(f""" 162 | super().__init__('{compound_node.name_property}', **kwargs) 163 | 164 | # Initialize validators 165 | # ---------------------""") 166 | for subtype_node in subtype_nodes: 167 | 168 | buffer.write(f""" 169 | self._validators['{subtype_node.name_property}'] = v_{compound_node.plotly_name}.{subtype_node.name_validator}()""") 170 | 171 | buffer.write(f""" 172 | 173 | # Populate data dict with properties 174 | # ----------------------------------""") 175 | for subtype_node in subtype_nodes: 176 | buffer.write(f""" 177 | self.{subtype_node.name_property} = {subtype_node.name_property}""") 178 | 179 | # ### Literals ### 180 | literal_nodes = [n for n in compound_node.child_literals if n.plotly_name in ['type']] 181 | if literal_nodes: 182 | buffer.write(f""" 183 | 184 | # Read-only literals 185 | # ------------------""") 186 | for literal_node in literal_nodes: 187 | buffer.write(f""" 188 | self._props['{literal_node.name_property}'] = '{literal_node.node_data}'""") 189 | 190 | return buffer.getvalue() 191 | 192 | 193 | def reindent_validator_description(validator, extra_indent): 194 | # Remove leading indent and add extra spaces to subsequent indent 195 | return ('\n' + ' ' * extra_indent).join(validator.description().strip().split('\n')) 196 | 197 | 198 | def add_constructor_params(buffer, subtype_nodes, colon=True): 199 | for i, subtype_node in enumerate(subtype_nodes): 200 | dflt = None 201 | buffer.write(f""", 202 | {subtype_node.name_property}={repr(dflt)}""") 203 | 204 | buffer.write(""", 205 | **kwargs""") 206 | buffer.write(f""" 207 | ){':' if colon else ''}""") 208 | 209 | 210 | def add_docstring(buffer, compound_node, extra_subtype_nodes=[]): 211 | # ### Docstring ### 212 | buffer.write(f""" 213 | \"\"\" 214 | Construct a new {compound_node.name_pascal_case} object 215 | 216 | Parameters 217 | ----------""") 218 | buffer.write(compound_node.get_constructor_params_docstring( 219 | indent=8, 220 | extra_nodes=extra_subtype_nodes )) 221 | 222 | # #### close docstring #### 223 | buffer.write(f""" 224 | 225 | Returns 226 | ------- 227 | {compound_node.name_pascal_case} 228 | \"\"\"""") 229 | 230 | 231 | def write_datatypes_py(outdir, node: PlotlyNode, 232 | extra_nodes: Dict[str, 'PlotlyNode']={}): 233 | 234 | # Generate source code 235 | # -------------------- 236 | datatype_source = build_datatypes_py(node, extra_nodes) 237 | if datatype_source: 238 | try: 239 | formatted_source = format_source(datatype_source) 240 | except Exception as e: 241 | print(datatype_source) 242 | raise e 243 | 244 | # Write file 245 | # ---------- 246 | filedir = opath.join(outdir, 'datatypes', *node.dir_path) 247 | os.makedirs(filedir, exist_ok=True) 248 | filepath = opath.join(filedir, '__init__.py') 249 | 250 | mode = 'at' if os.path.exists(filepath) else 'wt' 251 | with open(filepath, mode) as f: 252 | if mode == 'at': 253 | f.write("\n\n") 254 | f.write(formatted_source) 255 | f.flush() 256 | os.fsync(f.fileno()) 257 | 258 | 259 | def build_figure_py(trace_node, base_package, base_classname, fig_classname): 260 | buffer = StringIO() 261 | trace_nodes = trace_node.child_compound_datatypes 262 | 263 | # Imports 264 | # ------- 265 | buffer.write(f'from ipyplotly.{base_package} import {base_classname}\n') 266 | 267 | trace_types_csv = ', '.join([n.name_pascal_case for n in trace_nodes]) 268 | buffer.write(f'from ipyplotly.datatypes.trace import ({trace_types_csv})\n') 269 | 270 | buffer.write(f""" 271 | 272 | class {fig_classname}({base_classname}):\n""") 273 | 274 | # Reload validators and datatypes modules since we're appending 275 | # Classes to them as we go 276 | validators_module = importlib.import_module('ipyplotly.validators') 277 | importlib.reload(validators_module) 278 | datatypes_module = importlib.import_module('ipyplotly.datatypes') 279 | importlib.reload(datatypes_module) 280 | 281 | # Build constructor description strings 282 | data_validator = validators_module.DataValidator() 283 | data_description = reindent_validator_description(data_validator, 8) 284 | 285 | layout_validator = validators_module.LayoutValidator() 286 | layout_description = reindent_validator_description(layout_validator, 8) 287 | 288 | frames_validator = validators_module.FramesValidator() 289 | frames_description = reindent_validator_description(frames_validator, 8) 290 | 291 | buffer.write(f""" 292 | def __init__(self, data=None, layout=None, frames=None): 293 | \"\"\" 294 | Create a new {fig_classname} instance 295 | 296 | Parameters 297 | ---------- 298 | data 299 | {data_description} 300 | layout 301 | {layout_description} 302 | frames 303 | {frames_description} 304 | \"\"\" 305 | super().__init__(data, layout, frames) 306 | """) 307 | 308 | # add_trace methods 309 | for trace_node in trace_nodes: 310 | 311 | # Function signature 312 | # ------------------ 313 | buffer.write(f""" 314 | def add_{trace_node.plotly_name}(self""") 315 | 316 | add_constructor_params(buffer, trace_node.child_datatypes) 317 | add_docstring(buffer, trace_node) 318 | 319 | # Function body 320 | # ------------- 321 | buffer.write(f""" 322 | new_trace = {trace_node.name_pascal_case}( 323 | """) 324 | 325 | for i, subtype_node in enumerate(trace_node.child_datatypes): 326 | is_last = i == len(trace_node.child_datatypes) - 1 327 | buffer.write(f""" 328 | {subtype_node.name_property}={subtype_node.name_property}{'' if is_last else ','}""") 329 | 330 | buffer.write(f""", 331 | **kwargs)""") 332 | 333 | buffer.write(f""" 334 | return self.add_traces(new_trace)[0]""") 335 | 336 | buffer.write('\n') 337 | return buffer.getvalue() 338 | 339 | 340 | def append_figure_class(outdir, trace_node): 341 | 342 | if trace_node.node_path: 343 | raise ValueError('Expected root trace node. Received node with path "%s"' % trace_node.dir_str) 344 | 345 | base_figures = [('basewidget', 'BaseFigureWidget', 'FigureWidget'), 346 | ('basedatatypes', 'BaseFigure', 'Figure')] 347 | 348 | for base_package, base_classname, fig_classname in base_figures: 349 | figure_source = build_figure_py(trace_node, base_package, base_classname, fig_classname) 350 | formatted_source = format_source(figure_source) 351 | 352 | # Append to file 353 | # -------------- 354 | filepath = opath.join(outdir, '__init__.py') 355 | 356 | with open(filepath, 'a') as f: 357 | f.write('\n\n') 358 | f.write(formatted_source) 359 | f.flush() 360 | os.fsync(f.fileno()) 361 | -------------------------------------------------------------------------------- /codegen/download_plotly_schema.py: -------------------------------------------------------------------------------- 1 | import urllib.request 2 | import json 3 | 4 | if __name__ == '__main__': 5 | with urllib.request.urlopen('https://api.plot.ly/v2/plot-schema?sha1') as response: 6 | with open('resources/plotly-schema-v2.json', 'w') as f: 7 | f.write(json.dumps(json.load(response), indent=4)) 8 | -------------------------------------------------------------------------------- /codegen/utils.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import inspect 3 | import textwrap 4 | from typing import List, Dict 5 | 6 | from io import StringIO 7 | from yapf.yapflib.yapf_api import FormatCode 8 | 9 | from ipyplotly.basevalidators import BaseValidator, CompoundValidator, CompoundArrayValidator 10 | 11 | 12 | def format_source(validator_source): 13 | formatted_source, _ = FormatCode(validator_source, 14 | style_config={'based_on_style': 'google', 15 | 'DEDENT_CLOSING_BRACKETS': True, 16 | 'COLUMN_LIMIT': 119}) 17 | return formatted_source 18 | 19 | 20 | custom_validator_datatypes = { 21 | 'layout.image.source': 'ipyplotly.basevalidators.ImageUriValidator', 22 | 'frame.data': 'ipyplotly.validators.DataValidator', 23 | 'frame.layout': 'ipyplotly.validators.LayoutValidator' 24 | } 25 | 26 | class PlotlyNode: 27 | 28 | # Constructor 29 | # ----------- 30 | def __init__(self, plotly_schema, node_path=(), parent=None): 31 | self.plotly_schema = plotly_schema 32 | if isinstance(node_path, str): 33 | node_path = (node_path,) 34 | self.node_path = node_path 35 | 36 | # Compute children 37 | if isinstance(self.node_data, dict): 38 | self._children = [self.__class__(self.plotly_schema, 39 | node_path=self.node_path + (c,), 40 | parent=self) 41 | for c in self.node_data if c and c[0] != '_'] 42 | else: 43 | self._children = [] 44 | 45 | # Parent 46 | self._parent = parent 47 | 48 | def __repr__(self): 49 | return self.dir_str 50 | 51 | # Abstract methods 52 | # ---------------- 53 | @property 54 | def node_data(self) -> dict: 55 | raise NotImplementedError() 56 | 57 | @property 58 | def description(self) -> str: 59 | raise NotImplementedError() 60 | 61 | @property 62 | def base_datatype_class(self): 63 | raise NotImplementedError 64 | 65 | # Names 66 | # ----- 67 | @property 68 | def base_name(self): 69 | raise NotImplementedError() 70 | 71 | @property 72 | def plotly_name(self) -> str: 73 | if len(self.node_path) == 0: 74 | return self.base_name 75 | else: 76 | return self.node_path[-1] 77 | 78 | @property 79 | def name_pascal_case(self) -> str: 80 | return self.plotly_name.title().replace('_', '') 81 | 82 | @property 83 | def name_undercase(self) -> str: 84 | if not self.plotly_name: 85 | # Empty plotly_name 86 | return self.plotly_name 87 | 88 | # Lowercase leading char 89 | # ---------------------- 90 | name1 = self.plotly_name[0].lower() + self.plotly_name[1:] 91 | 92 | # Replace capital chars by underscore-lower 93 | # ----------------------------------------- 94 | name2 = ''.join([('' if not c.isupper() else '_') + c.lower() for c in name1]) 95 | 96 | return name2 97 | 98 | @property 99 | def name_property(self) -> str: 100 | return self.plotly_name + ('s' if self.is_array_element else '') 101 | 102 | @property 103 | def name_validator(self) -> str: 104 | return self.name_pascal_case + ('s' if self.is_array_element else '') + 'Validator' 105 | 106 | @property 107 | def name_base_validator(self) -> str: 108 | if self.dir_str in custom_validator_datatypes: 109 | validator_base = f"{custom_validator_datatypes[self.dir_str]}" 110 | else: 111 | validator_base = f"ipyplotly.basevalidators.{self.datatype_pascal_case}Validator" 112 | 113 | return validator_base 114 | 115 | def get_constructor_params_docstring(self, indent=12, extra_nodes=[]): 116 | assert self.is_compound 117 | 118 | buffer = StringIO() 119 | 120 | subtype_nodes = self.child_datatypes + extra_nodes 121 | for subtype_node in subtype_nodes: 122 | raw_description = subtype_node.description 123 | subtype_description = '\n'.join(textwrap.wrap(raw_description, 124 | subsequent_indent=' ' * (indent + 4), 125 | width=80 - (indent + 4))) 126 | 127 | buffer.write('\n' + ' ' * indent + subtype_node.name_property) 128 | buffer.write('\n' + ' ' * (indent + 4) + subtype_description) 129 | 130 | return buffer.getvalue() 131 | 132 | @property 133 | def validator_instance(self) -> BaseValidator: 134 | 135 | module_parts = self.name_base_validator.split('.') 136 | module_path = '.'.join(module_parts[:-1]) 137 | cls_name = module_parts[-1] 138 | 139 | validators_module = importlib.import_module(module_path) 140 | 141 | validator_class_list = [cls 142 | for _, cls in inspect.getmembers(validators_module, inspect.isclass) 143 | if cls.__name__ == cls_name] 144 | if not validator_class_list: 145 | raise ValueError(f"Unknown base validator '{self.name_base_validator}'") 146 | 147 | validator_class = validator_class_list[0] 148 | 149 | args = dict(plotly_name=self.name_property, parent_name=self.parent_dir_str) 150 | 151 | if validator_class == CompoundValidator: 152 | data_class_str = f"" 153 | extra_args = {'data_class': data_class_str, 'data_docs': self.get_constructor_params_docstring()} 154 | elif validator_class == CompoundArrayValidator: 155 | element_class_str = f"" 156 | extra_args = {'element_class': element_class_str, 'element_docs': self.get_constructor_params_docstring()} 157 | else: 158 | extra_args = {n.name_undercase: n.node_data for n in self.simple_attrs} 159 | 160 | # Add extra properties 161 | if self.datatype == 'color': 162 | # Check for colorscale sibling 163 | colorscale_node_list = [node for node in self.parent.child_datatypes 164 | if node.datatype == 'colorscale'] 165 | if colorscale_node_list: 166 | colorscale_path = colorscale_node_list[0].dir_str 167 | extra_args['colorscale_path'] = repr(colorscale_path) 168 | 169 | return validator_class(**args, **extra_args) 170 | 171 | @property 172 | def name_class(self) -> str: 173 | return self.name_pascal_case 174 | 175 | # Datatypes 176 | # --------- 177 | @property 178 | def datatype(self) -> str: 179 | if self.is_array_element: 180 | return 'compound_array' 181 | elif self.is_compound: 182 | return 'compound' 183 | elif self.is_simple: 184 | return self.node_data.get('valType') 185 | else: 186 | return 'literal' 187 | 188 | @property 189 | def datatype_pascal_case(self) -> str: 190 | return self.datatype.title().replace('_', '') 191 | 192 | @property 193 | def is_compound(self) -> bool: 194 | return isinstance(self.node_data, dict) and not self.is_simple and self.plotly_name != 'impliedEdits' 195 | 196 | @property 197 | def is_literal(self) -> bool: 198 | return isinstance(self.node_data, str) 199 | 200 | @property 201 | def is_simple(self) -> bool: 202 | return isinstance(self.node_data, dict) and 'valType' in self.node_data 203 | 204 | @property 205 | def is_array(self) -> bool: 206 | return isinstance(self.node_data, dict) and \ 207 | self.node_data.get('role', '') == 'object' and \ 208 | 'items' in self.node_data 209 | 210 | @property 211 | def is_array_element(self): 212 | if self.parent and self.parent.parent: 213 | return self.parent.parent.is_array 214 | else: 215 | return False 216 | 217 | @property 218 | def is_datatype(self) -> bool: 219 | return self.is_simple or self.is_compound 220 | 221 | # Node path 222 | # --------- 223 | def tidy_dir_path(self, p): 224 | return p 225 | 226 | @property 227 | def dir_path(self) -> List[str]: 228 | res = [self.base_name] if self.base_name else [] 229 | for i, p in enumerate(self.node_path): 230 | if p == 'items' or \ 231 | (i < len(self.node_path) - 1 and self.node_path[i+1] == 'items'): 232 | # e.g. [parcoords, dimensions, items, dimension] -> [parcoords, dimension] 233 | pass 234 | else: 235 | res.append(self.tidy_dir_path(p)) 236 | return res 237 | 238 | # Node path strings 239 | # ----------------- 240 | @property 241 | def dir_str(self) -> str: 242 | return '.'.join(self.dir_path) 243 | 244 | @property 245 | def parent_dir_str(self) -> str: 246 | return '.'.join(self.dir_path[:-1]) 247 | 248 | @property 249 | def pkg_str(self) -> str: 250 | path_str = '' 251 | for p in self.dir_path: 252 | path_str += '.' + p 253 | return path_str 254 | 255 | # Children 256 | # -------- 257 | @property 258 | def children(self) -> List['PlotlyNode']: 259 | return self._children 260 | 261 | @property 262 | def simple_attrs(self) -> List['PlotlyNode']: 263 | if not self.is_simple: 264 | raise ValueError(f"Cannot get simple attributes of the simple object '{self.dir_str}'") 265 | 266 | return [n for n in self.children if n.plotly_name not in ['valType', 'description', 'role']] 267 | 268 | @property 269 | def parent(self) -> 'PlotlyNode': 270 | return self._parent 271 | 272 | @property 273 | def child_datatypes(self) -> List['PlotlyNode']: 274 | """ 275 | Returns 276 | ------- 277 | children: list of TraceNode 278 | """ 279 | # if self.is_array: 280 | # items_child = [c for c in self.children if c.plotly_name == 'items'][0] 281 | # return items_child.children 282 | # else: 283 | nodes = [] 284 | for n in self.children: 285 | if n.is_array: 286 | nodes.append(n.children[0].children[0]) 287 | elif n.is_datatype: 288 | nodes.append(n) 289 | 290 | return nodes 291 | 292 | @property 293 | def child_compound_datatypes(self) -> List['PlotlyNode']: 294 | return [n for n in self.child_datatypes if n.is_compound] 295 | 296 | @property 297 | def child_simple_datatypes(self) -> List['PlotlyNode']: 298 | return [n for n in self.child_datatypes if n.is_simple] 299 | 300 | @property 301 | def child_literals(self) -> List['PlotlyNode']: 302 | return [n for n in self.children if n.is_literal] 303 | 304 | # Static helpers 305 | # -------------- 306 | @staticmethod 307 | def get_all_compound_datatype_nodes(plotly_schema, node_class) -> List['PlotlyNode']: 308 | nodes = [] 309 | nodes_to_process = [node_class(plotly_schema)] 310 | 311 | while nodes_to_process: 312 | node = nodes_to_process.pop() 313 | 314 | if not node.is_array: 315 | nodes.append(node) 316 | 317 | nodes_to_process.extend(node.child_compound_datatypes) 318 | 319 | return nodes 320 | 321 | @staticmethod 322 | def get_all_trace_layout_nodes(plotly_schema) -> Dict[str, 'LayoutNode']: 323 | trace_names = plotly_schema['traces'].keys() 324 | 325 | datatype_nodes = {} 326 | nodes_to_process = [TraceLayoutNode(plotly_schema, trace_name) 327 | for trace_name in trace_names] 328 | 329 | while nodes_to_process: 330 | parent_node = nodes_to_process.pop() 331 | for node in parent_node.child_simple_datatypes: 332 | datatype_nodes[node.dir_str] = node 333 | 334 | return datatype_nodes 335 | 336 | 337 | class TraceNode(PlotlyNode): 338 | 339 | # Constructor 340 | # ----------- 341 | def __init__(self, plotly_schema, node_path=(), parent=None): 342 | super().__init__(plotly_schema, node_path, parent) 343 | 344 | @property 345 | def base_datatype_class(self): 346 | if len(self.node_path) == 0: 347 | return 'BaseTraceType' 348 | else: 349 | return 'BaseTraceHierarchyType' 350 | 351 | @property 352 | def base_name(self): 353 | return 'trace' 354 | 355 | # Raw data 356 | # -------- 357 | @property 358 | def node_data(self) -> dict: 359 | if not self.node_path: 360 | node_data = self.plotly_schema['traces'] 361 | else: 362 | node_data = self.plotly_schema['traces'][self.node_path[0]]['attributes'] 363 | for prop_name in self.node_path[1:]: 364 | node_data = node_data[prop_name] 365 | 366 | return node_data 367 | 368 | # Description 369 | # ----------- 370 | @property 371 | def description(self) -> str: 372 | if len(self.node_path) == 0: 373 | desc = "" 374 | elif len(self.node_path) == 1: 375 | desc = self.plotly_schema['traces'][self.node_path[0]]['meta'].get('description', '') 376 | else: 377 | desc = self.node_data.get('description', '') 378 | 379 | if isinstance(desc, list): 380 | desc = ''.join(desc) 381 | 382 | return desc 383 | 384 | 385 | class LayoutNode(PlotlyNode): 386 | 387 | # Constructor 388 | # ----------- 389 | def __init__(self, plotly_schema, node_path=(), parent=None): 390 | super().__init__(plotly_schema, node_path, parent) 391 | 392 | @property 393 | def base_datatype_class(self): 394 | if len(self.node_path) == 0: 395 | return 'BaseLayoutType' 396 | else: 397 | return 'BaseLayoutHierarchyType' 398 | 399 | @property 400 | def base_name(self): 401 | return '' 402 | 403 | @property 404 | def plotly_name(self) -> str: 405 | if len(self.node_path) == 0: 406 | return self.base_name 407 | elif len(self.node_path) == 1: 408 | return 'layout' # override 'layoutAttributes' 409 | else: 410 | return self.node_path[-1] 411 | 412 | def tidy_dir_path(self, p): 413 | return 'layout' if p == 'layoutAttributes' else p 414 | 415 | # Description 416 | # ----------- 417 | @property 418 | def description(self) -> str: 419 | desc = self.node_data.get('description', '') 420 | if isinstance(desc, list): 421 | desc = ''.join(desc) 422 | return desc 423 | 424 | # Raw data 425 | # -------- 426 | @property 427 | def node_data(self) -> dict: 428 | node_data = self.plotly_schema['layout'] 429 | for prop_name in self.node_path: 430 | node_data = node_data[prop_name] 431 | 432 | return node_data 433 | 434 | 435 | class TraceLayoutNode(LayoutNode): 436 | 437 | # Constructor 438 | # ----------- 439 | def __init__(self, plotly_schema, trace_name=None, node_path=(), parent=None): 440 | 441 | # Handle trace name 442 | assert parent is not None or trace_name is not None 443 | if parent is not None: 444 | trace_name = parent.trace_name 445 | 446 | self.trace_name = trace_name 447 | super().__init__(plotly_schema, node_path, parent) 448 | 449 | @property 450 | def base_name(self): 451 | return 'layout' 452 | 453 | @property 454 | def plotly_name(self) -> str: 455 | if len(self.node_path) == 0: 456 | return self.base_name 457 | else: 458 | return self.node_path[-1] 459 | 460 | # Raw data 461 | # -------- 462 | @property 463 | def node_data(self) -> dict: 464 | try: 465 | node_data = (self.plotly_schema['traces'] 466 | [self.trace_name]['layoutAttributes']) 467 | 468 | for prop_name in self.node_path: 469 | node_data = node_data[prop_name] 470 | 471 | except KeyError: 472 | node_data = [] 473 | 474 | return node_data 475 | 476 | 477 | class FrameNode(PlotlyNode): 478 | 479 | # Constructor 480 | # ----------- 481 | def __init__(self, plotly_schema, node_path=(), parent=None): 482 | super().__init__(plotly_schema, node_path, parent) 483 | 484 | @property 485 | def base_datatype_class(self): 486 | return 'BaseFrameHierarchyType' 487 | 488 | @property 489 | def base_name(self): 490 | return '' 491 | 492 | @property 493 | def plotly_name(self) -> str: 494 | if len(self.node_path) < 2: 495 | return self.base_name 496 | elif len(self.node_path) == 2: 497 | return 'frame' # override 'frames_entry' 498 | else: 499 | return self.node_path[-1] 500 | 501 | def tidy_dir_path(self, p): 502 | return 'frame' if p == 'frames_entry' else p 503 | 504 | # Description 505 | # ----------- 506 | @property 507 | def description(self) -> str: 508 | desc = self.node_data.get('description', '') 509 | if isinstance(desc, list): 510 | desc = ''.join(desc) 511 | return desc 512 | 513 | # Raw data 514 | # -------- 515 | @property 516 | def node_data(self) -> dict: 517 | node_data = self.plotly_schema['frames'] 518 | for prop_name in self.node_path: 519 | node_data = node_data[prop_name] 520 | 521 | return node_data 522 | -------------------------------------------------------------------------------- /codegen/validators.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path as opath 3 | import shutil 4 | from io import StringIO 5 | from typing import Dict 6 | 7 | from codegen.utils import format_source, PlotlyNode, TraceNode 8 | 9 | def build_validators_py(parent_node: PlotlyNode, 10 | extra_nodes: Dict[str, 'PlotlyNode'] = {}): 11 | 12 | extra_subtype_nodes = [node for node_name, node in 13 | extra_nodes.items() if 14 | parent_node.dir_str and node_name.startswith(parent_node.dir_str)] 15 | 16 | datatype_nodes = parent_node.child_datatypes + extra_subtype_nodes 17 | 18 | if not datatype_nodes: 19 | return None 20 | 21 | buffer = StringIO() 22 | 23 | # Imports 24 | # ------- 25 | # Compute needed imports 26 | import_strs = set() 27 | for datatype_node in datatype_nodes: 28 | module_str = '.'.join(datatype_node.name_base_validator.split('.')[:-1]) 29 | import_strs.add(module_str) 30 | 31 | for import_str in import_strs: 32 | buffer.write(f'import {import_str}\n') 33 | 34 | # Check for colorscale node 35 | # ------------------------- 36 | colorscale_node_list = [node for node in datatype_nodes if node.datatype == 'colorscale'] 37 | if colorscale_node_list: 38 | colorscale_path = colorscale_node_list[0].dir_str 39 | else: 40 | colorscale_path = None 41 | 42 | # Compound datatypes loop 43 | # ----------------------- 44 | for datatype_node in datatype_nodes: 45 | 46 | parent_dir_str = datatype_node.parent_dir_str if datatype_node.parent_dir_str else 'figure' 47 | buffer.write(f""" 48 | 49 | class {datatype_node.name_validator}({datatype_node.name_base_validator}): 50 | def __init__(self, plotly_name='{datatype_node.name_property}', parent_name='{parent_dir_str}'):""") 51 | 52 | # Add import 53 | if datatype_node.is_compound: 54 | buffer.write(f""" 55 | from ipyplotly.datatypes{parent_node.pkg_str} import {datatype_node.name_pascal_case}""") 56 | 57 | buffer.write(f""" 58 | super().__init__(plotly_name=plotly_name, 59 | parent_name=parent_name""") 60 | 61 | if datatype_node.is_array_element: 62 | buffer.write(f""", 63 | element_class={datatype_node.name_class}, 64 | element_docs=\"\"\"{datatype_node.get_constructor_params_docstring()}\"\"\"""") 65 | elif datatype_node.is_compound: 66 | buffer.write(f""", 67 | data_class={datatype_node.name_class}, 68 | data_docs=\"\"\"{datatype_node.get_constructor_params_docstring()}\"\"\"""") 69 | else: 70 | assert datatype_node.is_simple 71 | 72 | # Exclude general properties 73 | excluded_props = ['valType', 'description', 'role', 'dflt'] 74 | if datatype_node.datatype == 'subplotid': 75 | # Default is required for subplotid validator 76 | excluded_props.remove('dflt') 77 | 78 | attr_nodes = [n for n in datatype_node.simple_attrs 79 | if n.plotly_name not in excluded_props] 80 | 81 | attr_dict = {node.name_undercase: repr(node.node_data) for node in attr_nodes} 82 | 83 | # Add special properties 84 | if datatype_node.datatype == 'color' and colorscale_path: 85 | attr_dict['colorscale_path'] = repr(colorscale_path) 86 | 87 | for attr_name, attr_val in attr_dict.items(): 88 | buffer.write(f""", 89 | {attr_name}={attr_val}""") 90 | 91 | buffer.write(')') 92 | 93 | return buffer.getvalue() 94 | 95 | 96 | def write_validator_py(outdir, 97 | node: PlotlyNode, 98 | extra_nodes: Dict[str, 'PlotlyNode'] = {}): 99 | 100 | # Generate source code 101 | # -------------------- 102 | validator_source = build_validators_py(node, extra_nodes) 103 | if validator_source: 104 | try: 105 | formatted_source = format_source(validator_source) 106 | except Exception as e: 107 | print(validator_source) 108 | raise e 109 | 110 | # Write file 111 | # ---------- 112 | filedir = opath.join(outdir, 'validators', *node.dir_path) 113 | os.makedirs(filedir, exist_ok=True) 114 | filepath = opath.join(filedir, '__init__.py') 115 | 116 | mode = 'at' if os.path.exists(filepath) else 'wt' 117 | with open(filepath, mode) as f: 118 | if mode == 'at': 119 | f.write("\n\n") 120 | f.write(formatted_source) 121 | f.flush() 122 | os.fsync(f.fileno()) 123 | 124 | 125 | def build_traces_validator_py(base_node: TraceNode): 126 | tracetype_nodes = base_node.child_compound_datatypes 127 | buffer = StringIO() 128 | 129 | import_csv = ', '.join([tracetype_node.name_class for tracetype_node in tracetype_nodes]) 130 | 131 | buffer.write(f""" 132 | class DataValidator(ipyplotly.basevalidators.BaseDataValidator): 133 | 134 | def __init__(self, plotly_name='data', parent_name='figure'): 135 | from ipyplotly.datatypes.trace import ({import_csv}) 136 | super().__init__(class_map={{ 137 | """) 138 | 139 | for i, tracetype_node in enumerate(tracetype_nodes): 140 | sfx = ',' if i < len(tracetype_nodes) else '' 141 | 142 | buffer.write(f""" 143 | '{tracetype_node.name_property}': {tracetype_node.name_class}{sfx}""") 144 | 145 | buffer.write(""" 146 | }, 147 | plotly_name=plotly_name, 148 | parent_name=parent_name)""") 149 | 150 | return buffer.getvalue() 151 | 152 | 153 | def append_traces_validator_py(outdir, base_node: TraceNode): 154 | 155 | if base_node.node_path: 156 | raise ValueError('Expected root trace node. Received node with path "%s"' % base_node.dir_str) 157 | 158 | source = build_traces_validator_py(base_node) 159 | formatted_source = format_source(source) 160 | 161 | # Append to file 162 | # -------------- 163 | filepath = opath.join(outdir, '__init__.py') 164 | 165 | with open(filepath, 'a') as f: 166 | f.write('\n\n') 167 | f.write(formatted_source) 168 | f.flush() 169 | os.fsync(f.fileno()) 170 | -------------------------------------------------------------------------------- /examples/overviews/Bar PCT dashboard.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Notebook Dashboard\n", 8 | "Example of a fairly complex dashboard. Initially inspired by a Dash tutorial example" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "### Imports" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": null, 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "# ipyplotly\n", 25 | "from ipyplotly.datatypes import FigureWidget\n", 26 | "from ipyplotly.callbacks import Points, InputState\n", 27 | "\n", 28 | "# pandas\n", 29 | "import pandas as pd\n", 30 | "from pandas.api.types import is_numeric_dtype\n", 31 | "\n", 32 | "# numpy\n", 33 | "import numpy as np\n", 34 | "\n", 35 | "# ipywidgets\n", 36 | "from ipywidgets import Dropdown, HBox, VBox" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/mtcars.csv')" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "numeric_cols = [col for col in df.columns if is_numeric_dtype(df[col])]\n", 55 | "numeric_cols" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": null, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "f = FigureWidget()\n", 65 | "f" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": null, 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "bar = f.add_bar(y=df.manufacturer.values, orientation='h')" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "f.layout.margin.l = 120\n", 84 | "bar.marker.showscale = True\n", 85 | "bar.marker.colorscale = 'viridis'\n", 86 | "\n", 87 | "f.layout.width = 1100\n", 88 | "f.layout.height = 800\n", 89 | "bar.marker.line.width = 1\n", 90 | "bar.marker.line.color = 'darkgray'" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": null, 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "trace, points, state = bar, Points(), InputState()\n", 100 | "\n", 101 | "# Bar click callback\n", 102 | "def update_click(trace, points, state):\n", 103 | " new_clr = np.zeros(df['mpg'].size)\n", 104 | " new_clr[points.point_inds] = 1\n", 105 | " \n", 106 | " bar_line_sizes = np.ones(df['mpg'].size)\n", 107 | " bar_line_sizes[points.point_inds] = 3\n", 108 | " \n", 109 | " # Update pct line color\n", 110 | " par.line.color = new_clr\n", 111 | " \n", 112 | " # Update bar line color and width\n", 113 | " with f.batch_update():\n", 114 | " bar.marker.line.width = bar_line_sizes \n", 115 | " bar.marker.line.color = new_clr\n", 116 | "\n", 117 | "bar.on_click(update_click)\n", 118 | "bar.on_selected(update_click)" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "metadata": {}, 125 | "outputs": [], 126 | "source": [ 127 | "f2 = FigureWidget(layout={'width': 1100})\n", 128 | "f2" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": null, 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "par = f2.add_parcoords(dimensions=[{\n", 138 | " 'values': df[col].values, \n", 139 | " 'label': col,\n", 140 | " 'range': [np.floor(df[col].min()), np.ceil(df[col].max())]} for col in numeric_cols])" 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "metadata": {}, 147 | "outputs": [], 148 | "source": [ 149 | "# Set up selection colormap\n", 150 | "par.line.colorscale = [[0, 'darkgray'], [1, 'red']]\n", 151 | "par.line.cmin = 0\n", 152 | "par.line.cmax = 1\n", 153 | "par.line.color = np.zeros(df['mpg'].size)\n", 154 | "\n", 155 | "bar.marker.line.colorscale = par.line.colorscale\n", 156 | "bar.marker.line.cmin = 0\n", 157 | "bar.marker.line.cmax = 1" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "# Widgets\n", 167 | "dd = Dropdown(options=df.columns, description='X', value='mpg')\n", 168 | "clr_dd = Dropdown(options=numeric_cols, description='Color')\n", 169 | "\n", 170 | "def update_col(val):\n", 171 | " col = dd.value\n", 172 | " clr = clr_dd.value\n", 173 | " with f.batch_update():\n", 174 | " bar.x = df[col].values\n", 175 | " bar.marker.color = df[clr].values\n", 176 | " bar.marker.colorbar.title = clr\n", 177 | " f.layout.xaxis.title = col\n", 178 | "\n", 179 | "dd.observe(update_col, 'value')\n", 180 | "clr_dd.observe(update_col, 'value')\n", 181 | "\n", 182 | "update_col(None)" 183 | ] 184 | }, 185 | { 186 | "cell_type": "markdown", 187 | "metadata": {}, 188 | "source": [ 189 | "## Display Dashboard\n", 190 | " - Dropdowns control barchart x-axis feature and coloring feature\n", 191 | " - Click or select bars to highlight in barchart and parallel coordinate diagram" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": null, 197 | "metadata": { 198 | "scrolled": false 199 | }, 200 | "outputs": [], 201 | "source": [ 202 | "VBox([f, HBox([dd, clr_dd]), f2])" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": null, 208 | "metadata": {}, 209 | "outputs": [], 210 | "source": [ 211 | "# Adjust barchart height\n", 212 | "f.layout.height = 650" 213 | ] 214 | } 215 | ], 216 | "metadata": { 217 | "kernelspec": { 218 | "display_name": "Python [default]", 219 | "language": "python", 220 | "name": "python3" 221 | }, 222 | "language_info": { 223 | "codemirror_mode": { 224 | "name": "ipython", 225 | "version": 3 226 | }, 227 | "file_extension": ".py", 228 | "mimetype": "text/x-python", 229 | "name": "python", 230 | "nbconvert_exporter": "python", 231 | "pygments_lexer": "ipython3", 232 | "version": "3.6.2" 233 | } 234 | }, 235 | "nbformat": 4, 236 | "nbformat_minor": 2 237 | } 238 | -------------------------------------------------------------------------------- /examples/overviews/DataShaderExample.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Overview\n", 8 | "This notebook demonstrates how to use DataShader to display large datasets inside an ipyplotly Figure. Change callbacks are used to recompute the datashader image whenever the axis range or figure size changes" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "## Install Datashader" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "`$ conda install datashader -y`" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "## Imports" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "# ipyplotly\n", 39 | "from ipyplotly.datatypes import FigureWidget\n", 40 | "\n", 41 | "# core\n", 42 | "import io\n", 43 | "import base64 \n", 44 | "import time\n", 45 | "\n", 46 | "# pandas\n", 47 | "import pandas as pd\n", 48 | "\n", 49 | "# numpy\n", 50 | "import numpy as np\n", 51 | "\n", 52 | "# scikit learn\n", 53 | "from sklearn import datasets\n", 54 | "\n", 55 | "# datashader\n", 56 | "import datashader as ds\n", 57 | "import datashader.transfer_functions as tf\n", 58 | "from datashader.colors import inferno" 59 | ] 60 | }, 61 | { 62 | "cell_type": "markdown", 63 | "metadata": {}, 64 | "source": [ 65 | "## Generate dataset\n", 66 | "We will create a large dataset by duplicating the Iris dataset many times with random noise" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": null, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "num_copies = 7000 # 1,050,000 rows\n", 76 | "\n", 77 | "iris_data = datasets.load_iris()\n", 78 | "feature_names = [name.replace(' (cm)', '').replace(' ', '_') for name in iris_data.feature_names]\n", 79 | "iris_df_orig = pd.DataFrame(iris_data.data, columns=feature_names)\n", 80 | "target_orig = iris_data.target + 1\n", 81 | "\n", 82 | "# frame of features\n", 83 | "iris_df = pd.concat(\n", 84 | " np.random.normal(scale=0.2, size=iris_df_orig.shape) + iris_df_orig for i in range(num_copies)\n", 85 | ").reset_index(drop=True)\n", 86 | "\n", 87 | "# array of targets\n", 88 | "target = [t for i in range(num_copies) for t in target_orig]\n", 89 | "\n", 90 | "# dataframe that includes target as categorical\n", 91 | "iris_target_df = pd.concat([iris_df, pd.Series(target, name='target', dtype='category')], axis=1)\n", 92 | "\n", 93 | "iris_df.describe()" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "metadata": {}, 99 | "source": [ 100 | "## Define DataShader image generation function\n", 101 | "Define a function that inputs an x/y ranges and the plot width/height and generates a DataShader image of the dataset. The image will be returned as a PIL image object" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": null, 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "def gen_ds_image(x_range, y_range, plot_width, plot_height):\n", 111 | " if x_range is None or y_range is None or plot_width is None or plot_height is None:\n", 112 | " return None\n", 113 | " \n", 114 | " cvs = ds.Canvas(x_range=x_range, y_range=y_range, plot_height=plot_height, plot_width=plot_width)\n", 115 | " agg_scatter = cvs.points(iris_target_df, \n", 116 | " 'sepal_length', 'sepal_width', \n", 117 | " ds.count_cat('target'))\n", 118 | " img = tf.shade(agg_scatter)\n", 119 | " img = tf.dynspread(img, threshold=0.95, max_px=5, shape='circle')\n", 120 | " \n", 121 | " return img.to_pil()" 122 | ] 123 | }, 124 | { 125 | "cell_type": "markdown", 126 | "metadata": { 127 | "collapsed": true 128 | }, 129 | "source": [ 130 | "## Define initial ranges and plot size" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "metadata": {}, 137 | "outputs": [], 138 | "source": [ 139 | "x_range=[3, 10]\n", 140 | "y_range=[0, 6]\n", 141 | "plot_height=500\n", 142 | "plot_width=700" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": null, 148 | "metadata": {}, 149 | "outputs": [], 150 | "source": [ 151 | "# Test image generation function and display the PIL image\n", 152 | "initial_img = gen_ds_image(x_range, y_range, plot_width, plot_height)" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": null, 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "initial_img" 162 | ] 163 | }, 164 | { 165 | "cell_type": "markdown", 166 | "metadata": {}, 167 | "source": [ 168 | "# Create ipyplotly with background image" 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": null, 174 | "metadata": { 175 | "scrolled": false 176 | }, 177 | "outputs": [], 178 | "source": [ 179 | "f = FigureWidget(data=[{'x': x_range, \n", 180 | " 'y': y_range, \n", 181 | " 'mode': 'markers',\n", 182 | " 'marker': {'opacity': 0}}], # invisible trace to init axes and to support autoresize\n", 183 | " layout={'width': plot_width, 'height': plot_height})\n", 184 | "f" 185 | ] 186 | }, 187 | { 188 | "cell_type": "code", 189 | "execution_count": null, 190 | "metadata": {}, 191 | "outputs": [], 192 | "source": [ 193 | "# Set background image\n", 194 | "f.layout.images = [dict(\n", 195 | " source = initial_img, # ipyplotly performs auto conversion of PIL image to png data URI\n", 196 | " xref = \"x\",\n", 197 | " yref = \"y\",\n", 198 | " x = x_range[0],\n", 199 | " y = y_range[1],\n", 200 | " sizex = x_range[1] - x_range[0],\n", 201 | " sizey = y_range[1] - y_range[0],\n", 202 | " sizing = \"stretch\",\n", 203 | " layer = \"below\")]" 204 | ] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "metadata": {}, 209 | "source": [ 210 | "## Install change callback to update image on zoom/resize" 211 | ] 212 | }, 213 | { 214 | "cell_type": "code", 215 | "execution_count": null, 216 | "metadata": {}, 217 | "outputs": [], 218 | "source": [ 219 | "def update_ds_image(layout, x_range, y_range, plot_width, plot_height):\n", 220 | " img = f.layout.images[0]\n", 221 | " \n", 222 | " # Update with batch_update so all updates happen simultaneously\n", 223 | " with f.batch_update():\n", 224 | " img.x = x_range[0]\n", 225 | " img.y = y_range[1]\n", 226 | " img.sizex = x_range[1] - x_range[0]\n", 227 | " img.sizey = y_range[1] - y_range[0]\n", 228 | " img.source = gen_ds_image(x_range, y_range, plot_width, plot_height)\n", 229 | "\n", 230 | "# Install callback to run exactly once if one or more of the following properties changes\n", 231 | "# - xaxis range\n", 232 | "# - yaxis range\n", 233 | "# - figure width\n", 234 | "# - figure height\n", 235 | "f.layout.on_change(update_ds_image, ('xaxis', 'range'), ('yaxis', 'range'), 'width', 'height')" 236 | ] 237 | }, 238 | { 239 | "cell_type": "markdown", 240 | "metadata": {}, 241 | "source": [ 242 | "## Image updates on drag zoom" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": null, 248 | "metadata": {}, 249 | "outputs": [], 250 | "source": [ 251 | "f.layout.dragmode = 'zoom'\n", 252 | "f" 253 | ] 254 | }, 255 | { 256 | "cell_type": "markdown", 257 | "metadata": {}, 258 | "source": [ 259 | "## Image updates on change axis range" 260 | ] 261 | }, 262 | { 263 | "cell_type": "code", 264 | "execution_count": null, 265 | "metadata": {}, 266 | "outputs": [], 267 | "source": [ 268 | "f.layout.xaxis.range = [3.5, 9]" 269 | ] 270 | }, 271 | { 272 | "cell_type": "markdown", 273 | "metadata": {}, 274 | "source": [ 275 | "## Image updates on change figure dimensions" 276 | ] 277 | }, 278 | { 279 | "cell_type": "code", 280 | "execution_count": null, 281 | "metadata": {}, 282 | "outputs": [], 283 | "source": [ 284 | "f" 285 | ] 286 | }, 287 | { 288 | "cell_type": "code", 289 | "execution_count": null, 290 | "metadata": {}, 291 | "outputs": [], 292 | "source": [ 293 | "with f.batch_update():\n", 294 | " f.layout.width = 1000\n", 295 | " f.layout.height = 500 " 296 | ] 297 | }, 298 | { 299 | "cell_type": "markdown", 300 | "metadata": {}, 301 | "source": [ 302 | "## Export figure to stand-alone html" 303 | ] 304 | }, 305 | { 306 | "cell_type": "code", 307 | "execution_count": null, 308 | "metadata": {}, 309 | "outputs": [], 310 | "source": [ 311 | "f.save_html(filename='exports/background.html')" 312 | ] 313 | }, 314 | { 315 | "cell_type": "markdown", 316 | "metadata": {}, 317 | "source": [ 318 | "## Export figure to static image" 319 | ] 320 | }, 321 | { 322 | "cell_type": "code", 323 | "execution_count": null, 324 | "metadata": { 325 | "scrolled": false 326 | }, 327 | "outputs": [], 328 | "source": [ 329 | "f.save_image('exports/datashader.png')\n", 330 | "f.save_image('exports/datashader.svg')\n", 331 | "f.save_image('exports/datashader.pdf')" 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": null, 337 | "metadata": {}, 338 | "outputs": [], 339 | "source": [] 340 | } 341 | ], 342 | "metadata": { 343 | "kernelspec": { 344 | "display_name": "Python [default]", 345 | "language": "python", 346 | "name": "python3" 347 | }, 348 | "language_info": { 349 | "codemirror_mode": { 350 | "name": "ipython", 351 | "version": 3 352 | }, 353 | "file_extension": ".py", 354 | "mimetype": "text/x-python", 355 | "name": "python", 356 | "nbconvert_exporter": "python", 357 | "pygments_lexer": "ipython3", 358 | "version": "3.6.2" 359 | } 360 | }, 361 | "nbformat": 4, 362 | "nbformat_minor": 2 363 | } 364 | -------------------------------------------------------------------------------- /examples/overviews/Overview.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Overview\n", 8 | "\n", 9 | "This notebook introduces the ipyplotly visualization library and demonstrates some of its features.\n", 10 | "\n", 11 | "## What is ipyplotly?\n", 12 | "ipyplotly wraps the excellent Plotly.js JavaScript plotting library for interactive use as an ipywidget inside the Jupyter Notebook.\n", 13 | "\n", 14 | "## Features\n", 15 | "\n", 16 | " - Traces can be added and updated interactively by simply assigning to properties\n", 17 | " - The full Traces and Layout API is generated from the plotly schema to provide a great experience for interactive use in the notebook\n", 18 | " - Data validation covering the full API with clear, informative error messages\n", 19 | " - Jupyter friendly docstrings on constructor params and properties\n", 20 | " - Support for setting array properties as numpy arrays. When numpy arrays are used, ipywidgets binary serialization protocol is used to avoid converting these to JSON strings.\n", 21 | " - Context manager API for animation\n", 22 | " - Export figures to standalone html\n", 23 | " - Programmatic export of figures to static SVG, PNG, or PDF images" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "# Imports" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "# ipyplotly\n", 40 | "from ipyplotly.datatypes import FigureWidget\n", 41 | "from ipyplotly.callbacks import Points, InputState\n", 42 | "\n", 43 | "# pandas\n", 44 | "import pandas as pd\n", 45 | "\n", 46 | "# numpy\n", 47 | "import numpy as np\n", 48 | "\n", 49 | "# scikit learn\n", 50 | "from sklearn import datasets\n", 51 | "\n", 52 | "# ipywidgets\n", 53 | "from ipywidgets import HBox, VBox, Button\n", 54 | "\n", 55 | "# functools\n", 56 | "from functools import partial" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": null, 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "# Load iris dataset\n", 66 | "iris_data = datasets.load_iris()\n", 67 | "feature_names = [name.replace(' (cm)', '').replace(' ', '_') for name in iris_data.feature_names]\n", 68 | "iris_df = pd.DataFrame(iris_data.data, columns=feature_names)\n", 69 | "iris_class = iris_data.target + 1\n", 70 | "iris_df.head()" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "# Create and display an empty ipyplotly Figure\n", 80 | "f1 = FigureWidget()\n", 81 | "f1" 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "metadata": {}, 87 | "source": [ 88 | "# Tab completion \n", 89 | "Entering ``f1.add_`` displays add methods for all of the supported trace types" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": null, 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "# f1.add_scatter" 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "metadata": {}, 104 | "source": [ 105 | "Entering ``f1.add_scatter()`` displays the names of all of the top-level properties for the scatter trace type\n", 106 | "\n", 107 | "Entering ``f1.add_scatter()`` displays the signature pop-up. Expanding this pop-up reveals the method doc string which contains the descriptions of all of the top level properties" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": null, 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [ 116 | "# f1.add_scatter(" 117 | ] 118 | }, 119 | { 120 | "cell_type": "markdown", 121 | "metadata": {}, 122 | "source": [ 123 | "# Add scatter trace" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": null, 129 | "metadata": {}, 130 | "outputs": [], 131 | "source": [ 132 | "scatt1 = f1.add_scatter(x=iris_df.sepal_length.values, y=iris_df.petal_width.values)" 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": null, 138 | "metadata": {}, 139 | "outputs": [], 140 | "source": [ 141 | "f1" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": null, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "scatt1.mode?" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": null, 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "# That's not what we wanted, change the mode to 'markers'\n", 160 | "scatt1.mode = 'markers'" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": null, 166 | "metadata": {}, 167 | "outputs": [], 168 | "source": [ 169 | "# Set size to 8\n", 170 | "scatt1.marker.size = 8" 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": null, 176 | "metadata": {}, 177 | "outputs": [], 178 | "source": [ 179 | "# Color markers by iris class\n", 180 | "scatt1.marker.color = iris_class" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": null, 186 | "metadata": {}, 187 | "outputs": [], 188 | "source": [ 189 | "# Change colorscale\n", 190 | "scatt1.marker.cmin = 0.5\n", 191 | "scatt1.marker.cmax = 3.5\n", 192 | "scatt1.marker.colorscale = [[0, 'red'], [0.33, 'red'], \n", 193 | " [0.33, 'green'], [0.67, 'green'], \n", 194 | " [0.67, 'blue'], [1.0, 'blue']]\n", 195 | "\n", 196 | "scatt1.marker.showscale = True" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": null, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "# Fix up colorscale ticks\n", 206 | "scatt1.marker.colorbar.ticks = 'outside'\n", 207 | "scatt1.marker.colorbar.tickvals = [1, 2, 3]\n", 208 | "scatt1.marker.colorbar.ticktext = iris_data.target_names.tolist()" 209 | ] 210 | }, 211 | { 212 | "cell_type": "code", 213 | "execution_count": null, 214 | "metadata": {}, 215 | "outputs": [], 216 | "source": [ 217 | "# Set colorscale title\n", 218 | "scatt1.marker.colorbar.title = 'Species'\n", 219 | "scatt1.marker.colorbar.titlefont.size = 16\n", 220 | "scatt1.marker.colorbar.titlefont.family = 'Rockwell'" 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": null, 226 | "metadata": {}, 227 | "outputs": [], 228 | "source": [ 229 | "# Add axis labels\n", 230 | "f1.layout.xaxis.title = 'sepal_length'\n", 231 | "f1.layout.yaxis.title = 'petal_width'" 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": null, 237 | "metadata": {}, 238 | "outputs": [], 239 | "source": [ 240 | "f1" 241 | ] 242 | }, 243 | { 244 | "cell_type": "code", 245 | "execution_count": null, 246 | "metadata": {}, 247 | "outputs": [], 248 | "source": [ 249 | "# Hover info\n", 250 | "scatt1.text = iris_data.target_names[iris_data.target]\n", 251 | "scatt1.hoverinfo = 'text+x+y'\n", 252 | "f1.layout.hovermode = 'closest'" 253 | ] 254 | }, 255 | { 256 | "cell_type": "code", 257 | "execution_count": null, 258 | "metadata": {}, 259 | "outputs": [], 260 | "source": [ 261 | "f1" 262 | ] 263 | }, 264 | { 265 | "cell_type": "markdown", 266 | "metadata": {}, 267 | "source": [ 268 | "## Animate marker size change" 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": null, 274 | "metadata": {}, 275 | "outputs": [], 276 | "source": [ 277 | "# Set marker size based on petal_length\n", 278 | "with f1.batch_animate(duration=1000):\n", 279 | " scatt1.marker.size = np.sqrt(iris_df.petal_length.values * 50)\n", 280 | " " 281 | ] 282 | }, 283 | { 284 | "cell_type": "code", 285 | "execution_count": null, 286 | "metadata": {}, 287 | "outputs": [], 288 | "source": [ 289 | "# Restore constant marker size\n", 290 | "with f1.batch_animate(duration=1000):\n", 291 | " scatt1.marker.size = 8" 292 | ] 293 | }, 294 | { 295 | "cell_type": "markdown", 296 | "metadata": {}, 297 | "source": [ 298 | "## Set drag mode property callback\n", 299 | "Make points more transparent when `dragmode` is `select` or `lasso`" 300 | ] 301 | }, 302 | { 303 | "cell_type": "code", 304 | "execution_count": null, 305 | "metadata": {}, 306 | "outputs": [], 307 | "source": [ 308 | "def set_opacity(marker, layout, dragmode):\n", 309 | " if dragmode in ['select', 'lasso']:\n", 310 | " marker.opacity = 0.5\n", 311 | " else:\n", 312 | " marker.opacity = 1.0" 313 | ] 314 | }, 315 | { 316 | "cell_type": "code", 317 | "execution_count": null, 318 | "metadata": {}, 319 | "outputs": [], 320 | "source": [ 321 | "f1.layout.on_change(partial(set_opacity, scatt1.marker), 'dragmode')" 322 | ] 323 | }, 324 | { 325 | "cell_type": "markdown", 326 | "metadata": {}, 327 | "source": [ 328 | "## Configure colorscale for brushing" 329 | ] 330 | }, 331 | { 332 | "cell_type": "code", 333 | "execution_count": null, 334 | "metadata": {}, 335 | "outputs": [], 336 | "source": [ 337 | "scatt1.marker.colorbar = None\n", 338 | "scatt1.marker.colorscale = [[0, 'lightgray'], [0.5, 'lightgray'], [0.5, 'red'], [1, 'red']]\n", 339 | "scatt1.marker.cmin = -0.5\n", 340 | "scatt1.marker.cmax = 1.5\n", 341 | "scatt1.marker.colorbar.ticks = 'outside'\n", 342 | "scatt1.marker.colorbar.tickvals = [0, 1]\n", 343 | "scatt1.marker.colorbar.ticktext = ['unselected', 'selected']" 344 | ] 345 | }, 346 | { 347 | "cell_type": "code", 348 | "execution_count": null, 349 | "metadata": {}, 350 | "outputs": [], 351 | "source": [ 352 | "# Reset colors to zeros (unselected)\n", 353 | "scatt1.marker.color = np.zeros(iris_class.size)\n", 354 | "selected = np.zeros(iris_class.size)" 355 | ] 356 | }, 357 | { 358 | "cell_type": "markdown", 359 | "metadata": {}, 360 | "source": [ 361 | "### Configure brushing callback" 362 | ] 363 | }, 364 | { 365 | "cell_type": "code", 366 | "execution_count": null, 367 | "metadata": {}, 368 | "outputs": [], 369 | "source": [ 370 | "# Completion helpers\n", 371 | "trace, points, state = scatt1, Points(), InputState()" 372 | ] 373 | }, 374 | { 375 | "cell_type": "code", 376 | "execution_count": null, 377 | "metadata": {}, 378 | "outputs": [], 379 | "source": [ 380 | "def brush(trace, points, state):\n", 381 | " inds = np.array(points.point_inds)\n", 382 | " if inds.size:\n", 383 | " selected[inds] = 1\n", 384 | " trace.marker.color = selected" 385 | ] 386 | }, 387 | { 388 | "cell_type": "code", 389 | "execution_count": null, 390 | "metadata": {}, 391 | "outputs": [], 392 | "source": [ 393 | "scatt1.on_selected(brush)" 394 | ] 395 | }, 396 | { 397 | "cell_type": "markdown", 398 | "metadata": {}, 399 | "source": [ 400 | "Now box or lasso select points on the figure and see them turn red" 401 | ] 402 | }, 403 | { 404 | "cell_type": "code", 405 | "execution_count": null, 406 | "metadata": {}, 407 | "outputs": [], 408 | "source": [ 409 | "# Reset brush\n", 410 | "selected = np.zeros(iris_class.size)\n", 411 | "scatt1.marker.color = selected" 412 | ] 413 | }, 414 | { 415 | "cell_type": "markdown", 416 | "metadata": {}, 417 | "source": [ 418 | "## Create second plot with different features" 419 | ] 420 | }, 421 | { 422 | "cell_type": "code", 423 | "execution_count": null, 424 | "metadata": { 425 | "scrolled": false 426 | }, 427 | "outputs": [], 428 | "source": [ 429 | "f2 = FigureWidget(data=[{'type': 'scatter',\n", 430 | " 'x': iris_df.petal_length.values, \n", 431 | " 'y': iris_df.sepal_width.values,\n", 432 | " 'mode': 'markers'}])\n", 433 | "f2" 434 | ] 435 | }, 436 | { 437 | "cell_type": "code", 438 | "execution_count": null, 439 | "metadata": {}, 440 | "outputs": [], 441 | "source": [ 442 | "# Set axis titles\n", 443 | "f2.layout.xaxis.title = 'petal_length'\n", 444 | "f2.layout.yaxis.title = 'sepal_width'" 445 | ] 446 | }, 447 | { 448 | "cell_type": "code", 449 | "execution_count": null, 450 | "metadata": {}, 451 | "outputs": [], 452 | "source": [ 453 | "# Grab trace reference\n", 454 | "scatt2 = f2.data[0]" 455 | ] 456 | }, 457 | { 458 | "cell_type": "code", 459 | "execution_count": null, 460 | "metadata": { 461 | "scrolled": false 462 | }, 463 | "outputs": [], 464 | "source": [ 465 | "# Set marker styles / colorbars to match between figures\n", 466 | "scatt2.marker = scatt1.marker" 467 | ] 468 | }, 469 | { 470 | "cell_type": "code", 471 | "execution_count": null, 472 | "metadata": {}, 473 | "outputs": [], 474 | "source": [ 475 | "# Configure brush on both plots to update both plots\n", 476 | "def brush(trace, points, state):\n", 477 | " inds = np.array(points.point_inds)\n", 478 | " if inds.size:\n", 479 | " selected = scatt1.marker.color.copy()\n", 480 | " selected[inds] = 1\n", 481 | " scatt1.marker.color = selected\n", 482 | " scatt2.marker.color = selected \n", 483 | " \n", 484 | "scatt1.on_selected(brush)\n", 485 | "scatt2.on_selected(brush)" 486 | ] 487 | }, 488 | { 489 | "cell_type": "code", 490 | "execution_count": null, 491 | "metadata": {}, 492 | "outputs": [], 493 | "source": [ 494 | "f2.layout.on_change(partial(set_opacity, scatt2.marker), 'dragmode')" 495 | ] 496 | }, 497 | { 498 | "cell_type": "code", 499 | "execution_count": null, 500 | "metadata": {}, 501 | "outputs": [], 502 | "source": [ 503 | "# Reset brush\n", 504 | "def reset_brush(btn):\n", 505 | " selected = np.zeros(iris_class.size)\n", 506 | " scatt1.marker.color = selected\n", 507 | " scatt2.marker.color = selected" 508 | ] 509 | }, 510 | { 511 | "cell_type": "code", 512 | "execution_count": null, 513 | "metadata": {}, 514 | "outputs": [], 515 | "source": [ 516 | "# Create reset button\n", 517 | "button = Button(description=\"clear\")\n", 518 | "button.on_click(reset_brush)" 519 | ] 520 | }, 521 | { 522 | "cell_type": "code", 523 | "execution_count": null, 524 | "metadata": {}, 525 | "outputs": [], 526 | "source": [ 527 | "# Hide colorbar for figure 1\n", 528 | "scatt1.marker.showscale = False" 529 | ] 530 | }, 531 | { 532 | "cell_type": "code", 533 | "execution_count": null, 534 | "metadata": {}, 535 | "outputs": [], 536 | "source": [ 537 | "# Set dragmode to lasso for both plots\n", 538 | "f1.layout.dragmode = 'lasso'\n", 539 | "f2.layout.dragmode = 'lasso'" 540 | ] 541 | }, 542 | { 543 | "cell_type": "code", 544 | "execution_count": null, 545 | "metadata": {}, 546 | "outputs": [], 547 | "source": [ 548 | "# Display two figures and the reset button\n", 549 | "f1.layout.width = 600\n", 550 | "f2.layout.width = 600" 551 | ] 552 | }, 553 | { 554 | "cell_type": "code", 555 | "execution_count": null, 556 | "metadata": {}, 557 | "outputs": [], 558 | "source": [ 559 | "VBox([HBox([f1, f2]), button])" 560 | ] 561 | }, 562 | { 563 | "cell_type": "code", 564 | "execution_count": null, 565 | "metadata": {}, 566 | "outputs": [], 567 | "source": [ 568 | "# Save figure 2 to a png image in the exports directory\n", 569 | "#f2.save_image('exports/f2.png')" 570 | ] 571 | }, 572 | { 573 | "cell_type": "code", 574 | "execution_count": null, 575 | "metadata": {}, 576 | "outputs": [], 577 | "source": [ 578 | "# Save figure 1 to a pdf in the exports directory\n", 579 | "#f1.save_image('exports/f1.pdf')" 580 | ] 581 | }, 582 | { 583 | "cell_type": "code", 584 | "execution_count": null, 585 | "metadata": {}, 586 | "outputs": [], 587 | "source": [] 588 | } 589 | ], 590 | "metadata": { 591 | "kernelspec": { 592 | "display_name": "Python [default]", 593 | "language": "python", 594 | "name": "python3" 595 | }, 596 | "language_info": { 597 | "codemirror_mode": { 598 | "name": "ipython", 599 | "version": 3 600 | }, 601 | "file_extension": ".py", 602 | "mimetype": "text/x-python", 603 | "name": "python", 604 | "nbconvert_exporter": "python", 605 | "pygments_lexer": "ipython3", 606 | "version": "3.6.2" 607 | } 608 | }, 609 | "nbformat": 4, 610 | "nbformat_minor": 2 611 | } 612 | -------------------------------------------------------------------------------- /examples/overviews/Scatter GL.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## ScatterGL Example\n", 8 | "Data is transfered to JS side using ipywidgets binary protocol without JSON serialization" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 1, 14 | "metadata": {}, 15 | "outputs": [], 16 | "source": [ 17 | "# ipyplotly\n", 18 | "from ipyplotly.datatypes import FigureWidget\n", 19 | "\n", 20 | "# ipywidgets\n", 21 | "from IPython.display import display\n", 22 | "\n", 23 | "# numpy\n", 24 | "import numpy as np\n", 25 | "\n", 26 | "# core\n", 27 | "import datetime\n", 28 | "import time" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 5, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "# One million points\n", 38 | "N = 1000000" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 6, 44 | "metadata": {}, 45 | "outputs": [ 46 | { 47 | "data": { 48 | "application/vnd.jupyter.widget-view+json": { 49 | "model_id": "5d253676c95b4f53a1ff518301d250f5", 50 | "version_major": 2, 51 | "version_minor": 0 52 | }, 53 | "text/html": [ 54 | "

Failed to display Jupyter Widget of type FigureWidget.

\n", 55 | "

\n", 56 | " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", 57 | " that the widgets JavaScript is still loading. If this message persists, it\n", 58 | " likely means that the widgets JavaScript library is either not installed or\n", 59 | " not enabled. See the Jupyter\n", 60 | " Widgets Documentation for setup instructions.\n", 61 | "

\n", 62 | "

\n", 63 | " If you're reading this message in another frontend (for example, a static\n", 64 | " rendering on GitHub or NBViewer),\n", 65 | " it may mean that your frontend doesn't currently support widgets.\n", 66 | "

\n" 67 | ], 68 | "text/plain": [ 69 | "FigureWidget()" 70 | ] 71 | }, 72 | "metadata": {}, 73 | "output_type": "display_data" 74 | } 75 | ], 76 | "source": [ 77 | "f = FigureWidget()\n", 78 | "f" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 7, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "# Adding 1 million points takes ~5 seconds\n", 88 | "scatt1 = f.add_scattergl(x = np.random.randn(N), \n", 89 | " y = np.random.randn(N),\n", 90 | " mode = 'markers',\n", 91 | " marker={'opacity': 0.8, 'line': {'width': 1}})" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": null, 97 | "metadata": {}, 98 | "outputs": [], 99 | "source": [] 100 | } 101 | ], 102 | "metadata": { 103 | "kernelspec": { 104 | "display_name": "Python [default]", 105 | "language": "python", 106 | "name": "python3" 107 | }, 108 | "language_info": { 109 | "codemirror_mode": { 110 | "name": "ipython", 111 | "version": 3 112 | }, 113 | "file_extension": ".py", 114 | "mimetype": "text/x-python", 115 | "name": "python", 116 | "nbconvert_exporter": "python", 117 | "pygments_lexer": "ipython3", 118 | "version": "3.6.2" 119 | } 120 | }, 121 | "nbformat": 4, 122 | "nbformat_minor": 2 123 | } 124 | -------------------------------------------------------------------------------- /examples/overviews/exports/README.md: -------------------------------------------------------------------------------- 1 | This is a directory to save example images 2 | -------------------------------------------------------------------------------- /ipyplotly/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import version_info, __version__ 2 | 3 | 4 | def _jupyter_nbextension_paths(): 5 | return [{ 6 | 'section': 'notebook', 7 | 'src': 'static', 8 | 'dest': 'ipyplotly', 9 | 'require': 'ipyplotly/extension' 10 | }] 11 | 12 | __frontend_version__ = '^0.1' 13 | -------------------------------------------------------------------------------- /ipyplotly/_version.py: -------------------------------------------------------------------------------- 1 | version_info = (0, 1, 0, 'alpha', 2) 2 | 3 | _specifier_ = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc', 'final': ''} 4 | 5 | __version__ = '%s.%s.%s%s'%(version_info[0], version_info[1], version_info[2], 6 | '' if version_info[3]=='final' else _specifier_[version_info[3]]+str(version_info[4])) 7 | -------------------------------------------------------------------------------- /ipyplotly/animation.py: -------------------------------------------------------------------------------- 1 | from ipyplotly.basevalidators import EnumeratedValidator, NumberValidator 2 | 3 | 4 | class EasingValidator(EnumeratedValidator): 5 | 6 | def __init__(self, plotly_name='easing'): 7 | super().__init__(plotly_name=plotly_name, 8 | parent_name='batch_animate', 9 | values=[ 10 | "linear", 11 | "quad", 12 | "cubic", 13 | "sin", 14 | "exp", 15 | "circle", 16 | "elastic", 17 | "back", 18 | "bounce", 19 | "linear-in", 20 | "quad-in", 21 | "cubic-in", 22 | "sin-in", 23 | "exp-in", 24 | "circle-in", 25 | "elastic-in", 26 | "back-in", 27 | "bounce-in", 28 | "linear-out", 29 | "quad-out", 30 | "cubic-out", 31 | "sin-out", 32 | "exp-out", 33 | "circle-out", 34 | "elastic-out", 35 | "back-out", 36 | "bounce-out", 37 | "linear-in-out", 38 | "quad-in-out", 39 | "cubic-in-out", 40 | "sin-in-out", 41 | "exp-in-out", 42 | "circle-in-out", 43 | "elastic-in-out", 44 | "back-in-out", 45 | "bounce-in-out" 46 | ]) 47 | 48 | 49 | class DurationValidator(NumberValidator): 50 | 51 | def __init__(self, plotly_name='duration'): 52 | super().__init__(plotly_name=plotly_name, parent_name='batch_animate', min=0) 53 | -------------------------------------------------------------------------------- /ipyplotly/basewidget.py: -------------------------------------------------------------------------------- 1 | import ipywidgets as widgets 2 | from traitlets import List, Unicode, Dict, observe, Integer, Undefined 3 | from ipyplotly.basedatatypes import BaseFigure 4 | from ipyplotly.callbacks import BoxSelector, LassoSelector, InputState, Points 5 | from ipyplotly.serializers import custom_serializers 6 | 7 | 8 | @widgets.register 9 | class BaseFigureWidget(BaseFigure, widgets.DOMWidget): 10 | 11 | # Widget Traits 12 | # ------------- 13 | _view_name = Unicode('FigureView').tag(sync=True) 14 | _view_module = Unicode('ipyplotly').tag(sync=True) 15 | _model_name = Unicode('FigureModel').tag(sync=True) 16 | _model_module = Unicode('ipyplotly').tag(sync=True) 17 | 18 | # Data properties for front end 19 | # Note: These are only automatically synced on full assignment, not on mutation 20 | _layout = Dict().tag(sync=True, **custom_serializers) 21 | _data = List().tag(sync=True, **custom_serializers) 22 | 23 | # Python -> JS message properties 24 | _py2js_addTraces = List(trait=Dict(), 25 | allow_none=True).tag(sync=True, **custom_serializers) 26 | 27 | _py2js_restyle = List(allow_none=True).tag(sync=True, **custom_serializers) 28 | _py2js_relayout = Dict(allow_none=True).tag(sync=True, **custom_serializers) 29 | _py2js_update = List(allow_none=True).tag(sync=True, **custom_serializers) 30 | _py2js_animate = List(allow_none=True).tag(sync=True, **custom_serializers) 31 | 32 | _py2js_deleteTraces = Dict(allow_none=True).tag(sync=True, **custom_serializers) 33 | _py2js_moveTraces = List(allow_none=True).tag(sync=True, **custom_serializers) 34 | 35 | _py2js_removeLayoutProps = List(allow_none=True).tag(sync=True, **custom_serializers) 36 | _py2js_removeStyleProps = List(allow_none=True).tag(sync=True, **custom_serializers) 37 | _py2js_requestSvg = Unicode(allow_none=True).tag(sync=True) 38 | 39 | # JS -> Python message properties 40 | _js2py_styleDelta = List(allow_none=True).tag(sync=True, **custom_serializers) 41 | _js2py_layoutDelta = Dict(allow_none=True).tag(sync=True, **custom_serializers) 42 | _js2py_restyle = List(allow_none=True).tag(sync=True, **custom_serializers) 43 | _js2py_relayout = Dict(allow_none=True).tag(sync=True, **custom_serializers) 44 | _js2py_update = Dict(allow_none=True).tag(sync=True, **custom_serializers) 45 | 46 | # For plotly_select/hover/unhover/click 47 | _js2py_pointsCallback = Dict(allow_none=True).tag(sync=True, **custom_serializers) 48 | 49 | # Message tracking 50 | _last_relayout_msg_id = Integer(0).tag(sync=True) 51 | _last_restyle_msg_id = Integer(0).tag(sync=True) 52 | 53 | # Constructor 54 | # ----------- 55 | def __init__(self, data=None, layout=None, frames=None): 56 | # TODO: error if frames is not None 57 | # Validate Frames 58 | # --------------- 59 | if frames: 60 | BaseFigureWidget._display_frames_error() 61 | 62 | self._frame_objs = None 63 | 64 | # Call superclass constructors 65 | # ---------------------------- 66 | # Note: We rename layout to layout_plotly because ipywidget also accepts a layout parameter 67 | # We map a layout_ipywidget property to the layout property of the ipywidget 68 | super().__init__(data=data, layout_plotly=layout) 69 | 70 | # Messages 71 | # -------- 72 | self.on_msg(self._handler_messages) 73 | 74 | # ### Trait methods ### 75 | @observe('_js2py_styleDelta') 76 | def handler_plotly_styleDelta(self, change): 77 | deltas = change['new'] 78 | self._js2py_styleDelta = None 79 | 80 | if not deltas: 81 | return 82 | 83 | msg_id = deltas[0].get('_restyle_msg_id', None) 84 | # print(f'styleDelta: {msg_id} == {self._last_restyle_msg_id}') 85 | if msg_id == self._last_restyle_msg_id: 86 | for delta in deltas: 87 | trace_uid = delta['uid'] 88 | 89 | # Remove message id 90 | # pprint(delta) 91 | # print('Processing styleDelta') 92 | 93 | trace_uids = [trace.uid for trace in self.data] 94 | trace_index = trace_uids.index(trace_uid) 95 | uid_trace = self.data[trace_index] 96 | delta_transform = BaseFigure.transform_data(uid_trace._prop_defaults, delta) 97 | 98 | removed_props = self._remove_overlapping_props(uid_trace._props, uid_trace._prop_defaults) 99 | 100 | if removed_props: 101 | # print(f'Removed_props: {removed_props}') 102 | self._py2js_removeStyleProps = [removed_props, trace_index] 103 | self._py2js_removeStyleProps = None 104 | 105 | # print(delta_transform) 106 | self._dispatch_change_callbacks_restyle(delta_transform, [trace_index]) 107 | 108 | self._restyle_in_process = False 109 | while self._waiting_restyle_callbacks: 110 | # Call callbacks 111 | self._waiting_restyle_callbacks.pop()() 112 | 113 | @observe('_js2py_restyle') 114 | def handler_js2py_restyle(self, change): 115 | restyle_msg = change['new'] 116 | self._js2py_restyle = None 117 | 118 | if not restyle_msg: 119 | return 120 | 121 | self.restyle(*restyle_msg) 122 | 123 | @observe('_js2py_update') 124 | def handler_js2py_update(self, change): 125 | update_msg = change['new'] 126 | self._js2py_update = None 127 | 128 | if not update_msg: 129 | return 130 | 131 | # print('Update (JS->Py):') 132 | # pprint(update_msg) 133 | 134 | style = update_msg['data'][0] 135 | trace_indexes = update_msg['data'][1] 136 | layout = update_msg['layout'] 137 | 138 | self.update(style=style, layout=layout, trace_indexes=trace_indexes) 139 | 140 | @observe('_js2py_layoutDelta') 141 | def handler_plotly_layoutDelta(self, change): 142 | delta = change['new'] 143 | self._js2py_layoutDelta = None 144 | 145 | if not delta: 146 | return 147 | 148 | msg_id = delta.get('_relayout_msg_id') 149 | # print(f'layoutDelta: {msg_id} == {self._last_relayout_msg_id}') 150 | if msg_id == self._last_relayout_msg_id: 151 | 152 | # print('Processing layoutDelta') 153 | # print('layoutDelta: {deltas}'.format(deltas=delta)) 154 | delta_transform = self.transform_data(self._layout_defaults, delta) 155 | # print(f'delta_transform: {delta_transform}') 156 | 157 | # No relayout messages in process. Handle removing overlapping properties 158 | removed_props = self._remove_overlapping_props(self._layout, self._layout_defaults) 159 | if removed_props: 160 | # print(f'Removed_props: {removed_props}') 161 | self._py2js_removeLayoutProps = removed_props 162 | self._py2js_removeLayoutProps = None 163 | 164 | self._dispatch_change_callbacks_relayout(delta_transform) 165 | self._relayout_in_process = False 166 | while self._waiting_relayout_callbacks: 167 | # Call callbacks 168 | self._waiting_relayout_callbacks.pop()() 169 | 170 | @observe('_js2py_relayout') 171 | def handler_js2py_relayout(self, change): 172 | relayout_data = change['new'] 173 | # print('Relayout (JS->Py):') 174 | # pprint(relayout_data) 175 | 176 | self._js2py_relayout = None 177 | 178 | if not relayout_data: 179 | return 180 | 181 | if 'lastInputTime' in relayout_data: 182 | # Remove 'lastInputTime'. Seems to be an internal plotly property that is introduced for some plot types 183 | relayout_data.pop('lastInputTime') 184 | 185 | self.relayout(relayout_data) 186 | 187 | @observe('_js2py_pointsCallback') 188 | def handler_plotly_pointsCallback(self, change): 189 | callback_data = change['new'] 190 | self._js2py_pointsCallback = None 191 | 192 | if not callback_data: 193 | return 194 | 195 | # Get event type 196 | # -------------- 197 | event_type = callback_data['event_type'] 198 | 199 | # Build Selector Object 200 | # --------------------- 201 | if callback_data.get('selector', None): 202 | selector_data = callback_data['selector'] 203 | selector_type = selector_data['type'] 204 | if selector_type == 'box': 205 | selector = BoxSelector(**selector_data) 206 | elif selector_type == 'lasso': 207 | selector = LassoSelector(**selector_data) 208 | else: 209 | raise ValueError('Unsupported selector type: %s' % selector_type) 210 | else: 211 | selector = None 212 | 213 | # Build State Object 214 | # ------------------ 215 | if callback_data.get('state', None): 216 | state_data = callback_data['state'] 217 | state = InputState(**state_data) 218 | else: 219 | state = None 220 | 221 | # Build Trace Points Dictionary 222 | # ----------------------------- 223 | points_data = callback_data['points'] 224 | trace_points = {trace_ind: {'point_inds': [], 225 | 'xs': [], 226 | 'ys': [], 227 | 'trace_name': self._data_objs[trace_ind].plotly_name, 228 | 'trace_index': trace_ind} 229 | for trace_ind in range(len(self._data_objs))} 230 | 231 | for x, y, point_ind, trace_ind in zip(points_data['xs'], 232 | points_data['ys'], 233 | points_data['pointNumbers'], 234 | points_data['curveNumbers']): 235 | 236 | trace_dict = trace_points[trace_ind] 237 | trace_dict['xs'].append(x) 238 | trace_dict['ys'].append(y) 239 | trace_dict['point_inds'].append(point_ind) 240 | 241 | # Dispatch callbacks 242 | # ------------------ 243 | for trace_ind, trace_points_data in trace_points.items(): 244 | points = Points(**trace_points_data) 245 | trace = self.data[trace_ind] # type: BaseTraceType 246 | 247 | if event_type == 'plotly_click': 248 | trace._dispatch_on_click(points, state) 249 | elif event_type == 'plotly_hover': 250 | trace._dispatch_on_hover(points, state) 251 | elif event_type == 'plotly_unhover': 252 | trace._dispatch_on_unhover(points, state) 253 | elif event_type == 'plotly_selected': 254 | trace._dispatch_on_selected(points, selector) 255 | 256 | # Custom Messages 257 | # --------------- 258 | def _handler_messages(self, widget, content, buffers): 259 | """Handle a msg from the front-end. 260 | """ 261 | if content.get('event', '') == 'svg': 262 | req_id = content['req_id'] 263 | svg_uri = content['svg_uri'] 264 | self._do_save_image(req_id, svg_uri) 265 | 266 | # Validate No Frames 267 | # ------------------ 268 | @property 269 | def frames(self): 270 | return self._frame_objs 271 | 272 | @frames.setter 273 | def frames(self, new_frames): 274 | if new_frames: 275 | BaseFigureWidget._display_frames_error() 276 | 277 | @staticmethod 278 | def _display_frames_error(): 279 | msg = ("Frames are not supported by the datatypes.FigureWidget class.\n" 280 | "Note: Frames are supported by the datatypes.Figure class") 281 | 282 | raise ValueError(msg) 283 | -------------------------------------------------------------------------------- /ipyplotly/callbacks.py: -------------------------------------------------------------------------------- 1 | import typing as typ 2 | 3 | 4 | class InputState: 5 | def __init__(self, ctrl=None, alt=None, shift=None, meta=None, button=None, buttons=None, **_): 6 | self._ctrl = ctrl 7 | self._alt = alt 8 | self._meta = meta 9 | self._shift = shift 10 | self._button = button 11 | self._buttons = buttons 12 | 13 | def __repr__(self): 14 | return """\ 15 | InputState(ctrl={ctrl}, 16 | alt={alt}, 17 | shift={shift}, 18 | meta={meta}, 19 | button={button}, 20 | buttons={buttons})""" 21 | 22 | @property 23 | def alt(self) -> bool: 24 | """ 25 | Whether alt key pressed 26 | 27 | Returns 28 | ------- 29 | bool 30 | """ 31 | return self._alt 32 | 33 | @property 34 | def ctrl(self) -> bool: 35 | """ 36 | Whether ctrl key pressed 37 | 38 | Returns 39 | ------- 40 | bool 41 | """ 42 | return self._ctrl 43 | 44 | @property 45 | def shift(self) -> bool: 46 | """ 47 | Whether shift key pressed 48 | 49 | Returns 50 | ------- 51 | bool 52 | """ 53 | return self._shift 54 | 55 | @property 56 | def meta(self) -> bool: 57 | """ 58 | Whether meta key pressed 59 | 60 | Returns 61 | ------- 62 | bool 63 | """ 64 | return self._meta 65 | 66 | @property 67 | def button(self) -> int: 68 | """ 69 | Integer code for the button that was pressed on the mouse to trigger the event 70 | 71 | - 0: Main button pressed, usually the left button or the un-initialized state 72 | - 1: Auxiliary button pressed, usually the wheel button or the middle button (if present) 73 | - 2: Secondary button pressed, usually the right button 74 | - 3: Fourth button, typically the Browser Back button 75 | - 4: Fifth button, typically the Browser Forward button 76 | 77 | Returns 78 | ------- 79 | int 80 | """ 81 | return self._button 82 | 83 | @property 84 | def buttons(self) -> int: 85 | """ 86 | Integer code for which combination of buttons are pressed on the mouse when the event is triggered. 87 | 88 | - 0: No button or un-initialized 89 | - 1: Primary button (usually left) 90 | - 2: Secondary button (usually right) 91 | - 4: Auxilary button (usually middle or mouse wheel button) 92 | - 8: 4th button (typically the "Browser Back" button) 93 | - 16: 5th button (typically the "Browser Forward" button) 94 | 95 | Combinations of buttons are represented as the decimal form of the bitmask of the values above. 96 | 97 | For example, pressing both the primary (1) and auxilary (4) buttons will result in a code of 5 98 | 99 | Returns 100 | ------- 101 | int 102 | """ 103 | return self._buttons 104 | 105 | 106 | class Points: 107 | 108 | def __init__(self, point_inds=None, xs=None, ys=None, trace_name=None, trace_index=None): 109 | self._point_inds = point_inds 110 | self._xs = xs 111 | self._ys = ys 112 | self._trace_name = trace_name 113 | self._trace_index = trace_index 114 | 115 | @property 116 | def point_inds(self) -> typ.List[int]: 117 | return self._point_inds 118 | 119 | @property 120 | def xs(self) -> typ.List: 121 | return self._xs 122 | 123 | @property 124 | def ys(self) -> typ.List: 125 | return self._ys 126 | 127 | @property 128 | def trace_name(self) -> str: 129 | return self._trace_name 130 | 131 | @property 132 | def trace_index(self) -> int: 133 | return self._trace_index 134 | 135 | 136 | class BoxSelector: 137 | def __init__(self, xrange=None, yrange=None, **_): 138 | self._type = 'box' 139 | self._xrange = xrange 140 | self._yrange = yrange 141 | 142 | @property 143 | def type(self) -> str: 144 | return self._type 145 | 146 | @property 147 | def xrange(self) -> typ.Tuple[float, float]: 148 | return self._xrange 149 | 150 | @property 151 | def yrange(self) -> typ.Tuple[float, float]: 152 | return self._yrange 153 | 154 | 155 | class LassoSelector: 156 | def __init__(self, xs=None, ys=None, **_): 157 | self._type = 'lasso' 158 | self._xs = xs 159 | self._ys = ys 160 | 161 | @property 162 | def type(self) -> str: 163 | return self._type 164 | 165 | @property 166 | def xs(self) -> typ.List[float]: 167 | return self._xs 168 | 169 | @property 170 | def ys(self) -> typ.List[float]: 171 | return self._ys 172 | -------------------------------------------------------------------------------- /ipyplotly/serializers.py: -------------------------------------------------------------------------------- 1 | 2 | # Create sentinal Undefined object 3 | from traitlets import Undefined 4 | import numpy as np 5 | 6 | def _py_to_js(v, widget_manager): 7 | # print('_py_to_js') 8 | # print(v) 9 | if isinstance(v, dict): 10 | return {k: _py_to_js(v, widget_manager) for k, v in v.items()} 11 | elif isinstance(v, (list, tuple)): 12 | return [_py_to_js(v, widget_manager) for v in v] 13 | elif isinstance(v, np.ndarray): 14 | if v.ndim == 1 and v.dtype.kind in ['u', 'i', 'f']: # (un)signed integer or float 15 | return {'buffer': memoryview(v), 'dtype': str(v.dtype), 'shape': v.shape} 16 | else: 17 | return v.tolist() 18 | else: 19 | if v is Undefined: 20 | return '_undefined_' 21 | else: 22 | return v 23 | 24 | 25 | def _js_to_py(v, widget_manager): 26 | # print('_js_to_py') 27 | # print(v) 28 | if isinstance(v, dict): 29 | return {k: _js_to_py(v, widget_manager) for k, v in v.items()} 30 | elif isinstance(v, (list, tuple)): 31 | return [_js_to_py(v, widget_manager) for v in v] 32 | elif isinstance(v, str) and v == '_undefined_': 33 | return Undefined 34 | else: 35 | return v 36 | 37 | 38 | custom_serializers = { 39 | 'from_json': _js_to_py, 40 | 'to_json': _py_to_js 41 | } 42 | -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | pythonic plotly API for use in Jupyter 2 | 3 | Package Install 4 | --------------- 5 | 6 | **Prerequisites** 7 | - [node](http://nodejs.org/) 8 | 9 | ```bash 10 | npm install --save ipyplotly 11 | ``` 12 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ipyplotly", 3 | "version": "0.1.0", 4 | "description": "pythonic plotly API for use in Jupyter", 5 | "author": "Jon Mease", 6 | "main": "src/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/jmmease/ipyplotly.git" 10 | }, 11 | "keywords": [ 12 | "jupyter", 13 | "widgets", 14 | "ipython", 15 | "ipywidgets" 16 | ], 17 | "files": [ 18 | "src/**/*.js", 19 | "dist/*.js" 20 | ], 21 | "scripts": { 22 | "clean": "rimraf dist/ && rimraf ../ipyplotly/static", 23 | "build": "webpack", 24 | "prepublish": "npm run clean && npm run build", 25 | "test": "echo \"Error: no test specified\" && exit 1" 26 | }, 27 | "devDependencies": { 28 | "json-loader": "^0.5.4", 29 | "webpack": "^3.5.5", 30 | "rimraf": "^2.6.1", 31 | "ify-loader": "^1.1.0" 32 | }, 33 | "dependencies": { 34 | "plotly.js": "^1.33.1", 35 | "@jupyter-widgets/base": "^1.0.4", 36 | "lodash": "^4.0" 37 | }, 38 | "jupyterlab": { 39 | "extension": "src/jupyterlab-plugin" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /js/src/embed.js: -------------------------------------------------------------------------------- 1 | // Entry point for the unpkg bundle containing custom model definitions. 2 | // 3 | // It differs from the notebook bundle in that it does not need to define a 4 | // dynamic baseURL for the static assets and may load some css that would 5 | // already be loaded by the notebook otherwise. 6 | 7 | // Export widget models and views, and the npm package version number. 8 | module.exports = require('./Figure.js'); 9 | module.exports['version'] = require('../package.json').version; 10 | -------------------------------------------------------------------------------- /js/src/extension.js: -------------------------------------------------------------------------------- 1 | // This file contains the javascript that is run when the notebook is loaded. 2 | // It contains some requirejs configuration and the `load_ipython_extension` 3 | // which is required for any notebook extension. 4 | 5 | // Configure requirejs 6 | if (window.require) { 7 | window.require.config({ 8 | map: { 9 | "*" : { 10 | "ipyplotly": "nbextensions/ipyplotly/index", 11 | } 12 | } 13 | }); 14 | } 15 | 16 | // Export the required load_ipython_extention 17 | module.exports = { 18 | load_ipython_extension: function() {} 19 | }; 20 | -------------------------------------------------------------------------------- /js/src/index.js: -------------------------------------------------------------------------------- 1 | // Entry point for the notebook bundle containing custom model definitions. 2 | // 3 | // Setup notebook base URL 4 | // 5 | // Some static assets may be required by the custom widget javascript. The base 6 | // url for the notebook is not known at build time and is therefore computed 7 | // dynamically. 8 | __webpack_public_path__ = document.querySelector('body').getAttribute('data-base-url') + 'nbextensions/ipyplotly/'; 9 | 10 | // Export widget models and views, and the npm package version number. 11 | module.exports = require('./Figure.js'); 12 | module.exports['version'] = require('../package.json').version; 13 | -------------------------------------------------------------------------------- /js/src/jupyterlab-plugin.js: -------------------------------------------------------------------------------- 1 | var ipyplotly = require('./index'); 2 | var base = require('@jupyter-widgets/base'); 3 | 4 | /** 5 | * The widget manager provider. 6 | */ 7 | module.exports = { 8 | id: 'ipyplotly', 9 | requires: [base.IJupyterWidgetRegistry], 10 | activate: function(app, widgets) { 11 | widgets.registerWidget({ 12 | name: 'ipyplotly', 13 | version: ipyplotly.version, 14 | exports: ipyplotly 15 | }); 16 | }, 17 | autoStart: true 18 | }; 19 | -------------------------------------------------------------------------------- /js/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var version = require('./package.json').version; 3 | 4 | // Custom webpack loaders are generally the same for all webpack bundles, hence 5 | // stored in a separate local variable. 6 | var rules = [ 7 | { test: /\.css$/, use: ['style-loader', 'css-loader']}, 8 | { test: /\.json$/, use: 'json-loader' }, 9 | { test: /\.js$/, use: 'ify-loader' } 10 | ]; 11 | 12 | 13 | module.exports = [ 14 | {// Notebook extension 15 | // 16 | // This bundle only contains the part of the JavaScript that is run on 17 | // load of the notebook. This section generally only performs 18 | // some configuration for requirejs, and provides the legacy 19 | // "load_ipython_extension" function which is required for any notebook 20 | // extension. 21 | // 22 | entry: './src/extension.js', 23 | output: { 24 | filename: 'extension.js', 25 | path: path.resolve(__dirname, '..', 'ipyplotly', 'static'), 26 | libraryTarget: 'amd' 27 | } 28 | }, 29 | {// Bundle for the notebook containing the custom widget views and models 30 | // 31 | // This bundle contains the implementation for the custom widget views and 32 | // custom widget. 33 | // It must be an amd module 34 | // 35 | entry: './src/index.js', 36 | output: { 37 | filename: 'index.js', 38 | path: path.resolve(__dirname, '..', 'ipyplotly', 'static'), 39 | libraryTarget: 'amd' 40 | }, 41 | devtool: 'source-map', 42 | node: { 43 | fs: 'empty' 44 | }, 45 | module: { 46 | rules: rules 47 | }, 48 | externals: ['@jupyter-widgets/base'] 49 | }, 50 | {// Embeddable ipyplotly bundle 51 | // 52 | // This bundle is generally almost identical to the notebook bundle 53 | // containing the custom widget views and models. 54 | // 55 | // The only difference is in the configuration of the webpack public path 56 | // for the static assets. 57 | // 58 | // It will be automatically distributed by unpkg to work with the static 59 | // widget embedder. 60 | // 61 | // The target bundle is always `dist/index.js`, which is the path required 62 | // by the custom widget embedder. 63 | // 64 | entry: './src/embed.js', 65 | output: { 66 | filename: 'index.js', 67 | path: path.resolve(__dirname, 'dist'), 68 | libraryTarget: 'amd', 69 | publicPath: 'https://unpkg.com/ipyplotly@' + version + '/dist/' 70 | }, 71 | devtool: 'source-map', 72 | node: { 73 | fs: 'empty' 74 | }, 75 | module: { 76 | rules: rules 77 | }, 78 | externals: ['@jupyter-widgets/base'] 79 | } 80 | ]; 81 | -------------------------------------------------------------------------------- /recipe/build.bat: -------------------------------------------------------------------------------- 1 | "%PYTHON%" setup.py install --single-version-externally-managed --record=record.txt 2 | if errorlevel 1 exit 1 3 | -------------------------------------------------------------------------------- /recipe/build.sh: -------------------------------------------------------------------------------- 1 | $PYTHON setup.py install --single-version-externally-managed --record=record.txt # Python command to install the script. 2 | -------------------------------------------------------------------------------- /recipe/meta.yaml: -------------------------------------------------------------------------------- 1 | {% set name = "ipyplotly" %} 2 | {% set version = "0.1.0a2" %} 3 | 4 | package: 5 | name: {{ name }} 6 | version: {{ version }} 7 | 8 | source: 9 | path: ../ 10 | 11 | requirements: 12 | build: 13 | - python 14 | - setuptools 15 | 16 | run: 17 | - python >=3.5 18 | - ipywidgets >=7.0 19 | - numpy 20 | - pandas >=0.20 21 | - pillow >=4.2 22 | - cairosvg >=2.0.0rc6 23 | - plotly >=2.1 24 | 25 | build: 26 | noarch: python 27 | 28 | test: 29 | requires: 30 | - pytest 31 | imports: 32 | - ipyplotly 33 | source_files: 34 | - test/ 35 | -------------------------------------------------------------------------------- /recipe/post-link.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | "%PREFIX%\Scripts\jupyter-nbextension.exe" enable widgetsnbextension --py --sys-prefix > NUL 2>&1 && if errorlevel 1 exit 1 4 | "%PREFIX%\Scripts\jupyter-nbextension.exe" enable ipyplotly --py --sys-prefix > NUL 2>&1 && if errorlevel 1 exit 1 5 | -------------------------------------------------------------------------------- /recipe/post-link.sh: -------------------------------------------------------------------------------- 1 | "${PREFIX}/bin/jupyter-nbextension" enable --py widgetsnbextension --sys-prefix 2 | "${PREFIX}/bin/jupyter-nbextension" enable --py ipyplotly --sys-prefix 3 | -------------------------------------------------------------------------------- /recipe/pre-unlink.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | "%PREFIX%\Scripts\jupyter-nbextension.exe" disable ipyplotly --py --sys-prefix > NUL 2>&1 && if errorlevel 1 exit 1 4 | -------------------------------------------------------------------------------- /recipe/pre-unlink.sh: -------------------------------------------------------------------------------- 1 | "${PREFIX}/bin/jupyter-nbextension" disable ipyplotly --py --sys-prefix > /dev/null 2>&1 2 | -------------------------------------------------------------------------------- /recipe/run_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | pytest.main(['test/']) 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from setuptools import setup, find_packages, Command 3 | from setuptools.command.sdist import sdist 4 | from setuptools.command.build_py import build_py 5 | from setuptools.command.egg_info import egg_info 6 | from subprocess import check_call 7 | import os 8 | import sys 9 | import platform 10 | 11 | here = os.path.dirname(os.path.abspath(__file__)) 12 | node_root = os.path.join(here, 'js') 13 | is_repo = os.path.exists(os.path.join(here, '.git')) 14 | 15 | npm_path = os.pathsep.join([ 16 | os.path.join(node_root, 'node_modules', '.bin'), 17 | os.environ.get('PATH', os.defpath), 18 | ]) 19 | 20 | from distutils import log 21 | log.set_verbosity(log.DEBUG) 22 | log.info('setup.py entered') 23 | log.info('$PATH=%s' % os.environ['PATH']) 24 | 25 | LONG_DESCRIPTION = 'A pythonic plotly API and ipywidget for use in Jupyter' 26 | 27 | def js_prerelease(command, strict=False): 28 | """decorator for building minified js/css prior to another command""" 29 | class DecoratedCommand(command): 30 | def run(self): 31 | jsdeps = self.distribution.get_command_obj('jsdeps') 32 | if not is_repo and all(os.path.exists(t) for t in jsdeps.targets): 33 | # sdist, nothing to do 34 | command.run(self) 35 | return 36 | 37 | try: 38 | self.distribution.run_command('jsdeps') 39 | except Exception as e: 40 | missing = [t for t in jsdeps.targets if not os.path.exists(t)] 41 | if strict or missing: 42 | log.warn('rebuilding js and css failed') 43 | if missing: 44 | log.error('missing files: %s' % missing) 45 | raise e 46 | else: 47 | log.warn('rebuilding js and css failed (not a problem)') 48 | log.warn(str(e)) 49 | command.run(self) 50 | update_package_data(self.distribution) 51 | return DecoratedCommand 52 | 53 | def update_package_data(distribution): 54 | """update package_data to catch changes during setup""" 55 | build_py = distribution.get_command_obj('build_py') 56 | # distribution.package_data = find_package_data() 57 | # re-init build_py options which load package_data 58 | build_py.finalize_options() 59 | 60 | 61 | class NPM(Command): 62 | description = 'install package.json dependencies using npm' 63 | 64 | user_options = [] 65 | 66 | node_modules = os.path.join(node_root, 'node_modules') 67 | 68 | targets = [ 69 | os.path.join(here, 'ipyplotly', 'static', 'extension.js'), 70 | os.path.join(here, 'ipyplotly', 'static', 'index.js') 71 | ] 72 | 73 | def initialize_options(self): 74 | pass 75 | 76 | def finalize_options(self): 77 | pass 78 | 79 | def get_npm_name(self): 80 | npmName = 'npm'; 81 | if platform.system() == 'Windows': 82 | npmName = 'npm.cmd'; 83 | 84 | return npmName; 85 | 86 | def has_npm(self): 87 | npmName = self.get_npm_name(); 88 | try: 89 | check_call([npmName, '--version']) 90 | return True 91 | except: 92 | return False 93 | 94 | def should_run_npm_install(self): 95 | package_json = os.path.join(node_root, 'package.json') 96 | node_modules_exists = os.path.exists(self.node_modules) 97 | return self.has_npm() 98 | 99 | def run(self): 100 | has_npm = self.has_npm() 101 | if not has_npm: 102 | log.error("`npm` unavailable. If you're running this command using sudo, make sure `npm` is available to sudo") 103 | 104 | env = os.environ.copy() 105 | env['PATH'] = npm_path 106 | 107 | if self.should_run_npm_install(): 108 | log.info("Installing build dependencies with npm. This may take a while...") 109 | npmName = self.get_npm_name(); 110 | check_call([npmName, 'install'], cwd=node_root, stdout=sys.stdout, stderr=sys.stderr) 111 | os.utime(self.node_modules, None) 112 | 113 | for t in self.targets: 114 | if not os.path.exists(t): 115 | msg = 'Missing file: %s' % t 116 | if not has_npm: 117 | msg += '\nnpm is required to build a development version of widgetsnbextension' 118 | raise ValueError(msg) 119 | 120 | # update package data in case this created new files 121 | update_package_data(self.distribution) 122 | 123 | 124 | class CodegenCommand(Command): 125 | description = 'Generate class hierarchy from Plotly JSON schema' 126 | user_options = [] 127 | 128 | def initialize_options(self): 129 | pass 130 | 131 | def finalize_options(self): 132 | pass 133 | 134 | def run(self): 135 | from codegen import perform_codegen 136 | perform_codegen() 137 | 138 | 139 | version_ns = {} 140 | with open(os.path.join(here, 'ipyplotly', '_version.py')) as f: 141 | exec(f.read(), {}, version_ns) 142 | 143 | setup_args = { 144 | 'name': 'ipyplotly', 145 | 'version': version_ns['__version__'], 146 | 'description': 'pythonic plotly API for use in Jupyter', 147 | 'long_description': LONG_DESCRIPTION, 148 | 'include_package_data': True, 149 | 'data_files': [ 150 | ('share/jupyter/nbextensions/ipyplotly', [ 151 | 'ipyplotly/static/extension.js', 152 | 'ipyplotly/static/index.js', 153 | 'ipyplotly/static/index.js.map', 154 | ]), 155 | ], 156 | 'python_requires': '>=3.5', 157 | 'install_requires': [ 158 | 'ipywidgets>=7.0', 159 | 'numpy>=1.13', 160 | 'pandas>=0.20', 161 | 'plotly>=2.1' 162 | ], 163 | 'packages': find_packages(exclude=('codegen',)), 164 | 'zip_safe': False, 165 | 'cmdclass': { 166 | 'build_py': js_prerelease(build_py), 167 | 'egg_info': js_prerelease(egg_info), 168 | 'sdist': js_prerelease(sdist, strict=True), 169 | 'jsdeps': NPM, 170 | 'codegen': CodegenCommand, 171 | }, 172 | 173 | 'author': 'Jon Mease', 174 | 'author_email': 'jon.mease@gmail.com', 175 | 'keywords': [ 176 | 'ipython', 177 | 'jupyter', 178 | 'widgets', 179 | ], 180 | 'classifiers': [ 181 | 'Development Status :: 4 - Beta', 182 | 'Framework :: IPython', 183 | 'Intended Audience :: Developers', 184 | 'Intended Audience :: Science/Research', 185 | 'Topic :: Multimedia :: Graphics', 186 | 'Programming Language :: Python :: 3.5', 187 | ], 188 | } 189 | 190 | setup(**setup_args) 191 | -------------------------------------------------------------------------------- /test/codegen/test_codegen.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | import pytest 4 | import inspect 5 | 6 | 7 | # Datatypes modules 8 | # ----------------- 9 | datatypes_root = 'ipyplotly/datatypes' 10 | datatype_modules = [dirpath.replace('/', '.') 11 | for dirpath, _, _ in os.walk(datatypes_root) 12 | if not dirpath.endswith('__pycache__')] 13 | 14 | 15 | @pytest.fixture(params=datatype_modules) 16 | def datatypes_module(request): 17 | return request.param 18 | 19 | 20 | # Validate datatype modules 21 | # ------------------------- 22 | def test_import_datatypes(datatypes_module): 23 | importlib.import_module(datatypes_module) 24 | 25 | 26 | def test_construct_datatypes(datatypes_module): 27 | module = importlib.import_module(datatypes_module) 28 | for name, obj in inspect.getmembers(module, inspect.isclass): 29 | if obj.__module__ == datatypes_module: 30 | datatype_class = obj 31 | 32 | # Call datatype constructor with not arguments 33 | datatype_class() 34 | 35 | 36 | # Validate validator modules 37 | # -------------------------- 38 | validators_root = 'ipyplotly/validators' 39 | validator_modules = [dirpath.replace('/', '.') 40 | for dirpath, _, _ in os.walk(validators_root) 41 | if not dirpath.endswith('__pycache__')] 42 | 43 | 44 | @pytest.fixture(params=validator_modules) 45 | def validators_module(request): 46 | return request.param 47 | 48 | 49 | def test_import_validators(validators_module): 50 | importlib.import_module(validators_module) 51 | 52 | 53 | def test_construct_validators(validators_module): 54 | module = importlib.import_module(validators_module) 55 | for name, obj in inspect.getmembers(module, inspect.isclass): 56 | if obj.__module__ == validators_module: 57 | validator_class = obj 58 | 59 | # Call datatype constructor with not arguments 60 | validator_class() 61 | -------------------------------------------------------------------------------- /test/datatypes/properties/conftest.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | import pytest 3 | 4 | 5 | @pytest.fixture(scope="module") 6 | def parent(): 7 | parent_obj = mock.Mock() 8 | parent_props = {'plotly_obj': {}} 9 | parent_prop_defaults = {'plotly_obj': {}} 10 | parent_obj._get_child_props.return_value = parent_props['plotly_obj'] 11 | parent_obj._get_child_prop_defaults.return_value = parent_prop_defaults['plotly_obj'] 12 | parent_obj._in_batch_mode = False 13 | return parent_obj 14 | -------------------------------------------------------------------------------- /test/datatypes/properties/test_compound_property.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | import pytest 3 | from ipyplotly.basedatatypes import BasePlotlyType 4 | from ipyplotly.basevalidators import CompoundValidator 5 | 6 | 7 | # Fixtures 8 | # -------- 9 | @pytest.fixture() 10 | def plotly_obj(): 11 | 12 | # ### Setup plotly obj (make fixture eventually) ### 13 | plotly_obj = BasePlotlyType('plotly_obj') 14 | 15 | # Add validator 16 | validator = mock.Mock(spec=CompoundValidator, 17 | wraps=CompoundValidator('prop1', 'plotly_obj', data_class=mock.Mock, data_docs='')) 18 | plotly_obj._validators['prop1'] = validator 19 | 20 | # Mock out _send_update 21 | plotly_obj._send_update = mock.Mock() 22 | 23 | return plotly_obj 24 | 25 | 26 | # Validation 27 | # ---------- 28 | def test_set_invalid_property(plotly_obj): 29 | with pytest.raises(KeyError) as failure: 30 | plotly_obj['bogus'] = 'Hello' 31 | 32 | 33 | def test_get_invalid_property(plotly_obj): 34 | with pytest.raises(KeyError) as failure: 35 | p = plotly_obj['bogus'] 36 | 37 | 38 | # Orphan 39 | # ------ 40 | @pytest.mark.xfail 41 | def test_set_get_compound_property(plotly_obj): 42 | # Setup value 43 | # ----------- 44 | v = mock.Mock() 45 | d = {'a': 23} 46 | type(v)._data = mock.PropertyMock(return_value=d) 47 | 48 | # Perform set_prop 49 | # ---------------- 50 | plotly_obj['prop1'] = v 51 | 52 | # Mutate d 53 | # -------- 54 | # Just to make sure we copy data on assignment 55 | d['a'] = 1 56 | 57 | # Object Assertions 58 | # ----------------- 59 | # ### test get object is a copy ### 60 | assert plotly_obj['prop1'] is not v 61 | 62 | # ### _send_update sent ### 63 | plotly_obj._send_update.assert_called_once_with('prop1', {'a': 23}) 64 | 65 | # ### _orphan_data configured properly ### 66 | assert plotly_obj._orphan_data == {'prop1': {'a': 23}} 67 | 68 | # ### _data is mapped to _orphan_data 69 | assert plotly_obj._props is plotly_obj._orphan_data 70 | 71 | # ### validator called properly ### 72 | plotly_obj._validators['prop1'].validate_coerce.assert_called_once_with(v) 73 | 74 | # Value Assertions 75 | # ---------------- 76 | # ### Parent set to plotly_obj 77 | assert v._parent is plotly_obj 78 | 79 | # ### Orphan data cleared ### 80 | v._orphan_data.clear.assert_called_once() 81 | 82 | 83 | # With parent 84 | # ----------- 85 | @pytest.mark.xfail 86 | def test_set_get_property_with_parent(plotly_obj, parent): 87 | 88 | # Setup value 89 | # ----------- 90 | v = mock.Mock() 91 | d = {'a': 23} 92 | type(v)._data = mock.PropertyMock(return_value=d) 93 | 94 | # Setup parent 95 | # ------------ 96 | plotly_obj._parent = parent 97 | 98 | # Perform set_prop 99 | # ---------------- 100 | plotly_obj['prop1'] = v 101 | 102 | # Parent Assertions 103 | # ----------------- 104 | parent._get_child_props.assert_called_with(plotly_obj) 105 | 106 | # Object Assertions 107 | # ----------------- 108 | # ### test get object is a copy ### 109 | assert plotly_obj['prop1'] is not v 110 | 111 | # ### _send_update sent ### 112 | plotly_obj._send_update.assert_called_once_with('prop1', d) 113 | 114 | # ### orphan data cleared ### 115 | assert plotly_obj._orphan_data == {} 116 | 117 | # ### _data bound to parent dict ### 118 | assert parent._get_child_props(plotly_obj) is plotly_obj._props 119 | 120 | # ### validator called properly ### 121 | plotly_obj._validators['prop1'].validate_coerce.assert_called_once_with(v) 122 | 123 | # Value Assertions 124 | # ---------------- 125 | # ### Parent set to plotly_obj 126 | assert v._parent is plotly_obj 127 | 128 | # ### Orphan data cleared ### 129 | v._orphan_data.clear.assert_called_once() 130 | -------------------------------------------------------------------------------- /test/datatypes/properties/test_simple_properties.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | import pytest 3 | from ipyplotly.basedatatypes import BasePlotlyType 4 | from ipyplotly.basevalidators import StringValidator 5 | 6 | # Fixtures 7 | # -------- 8 | @pytest.fixture() 9 | def plotly_obj(): 10 | # ### Setup plotly obj (make fixture eventually) ### 11 | plotly_obj = BasePlotlyType('plotly_obj') 12 | 13 | # Add validator 14 | validator = mock.Mock(spec=StringValidator, 15 | wraps=StringValidator('prop1', 'plotly_obj')) 16 | plotly_obj._validators['prop1'] = validator 17 | 18 | # Mock out _send_update 19 | plotly_obj._send_update = mock.Mock() 20 | 21 | return plotly_obj 22 | 23 | # Validation 24 | # ---------- 25 | def test_set_invalid_property(plotly_obj): 26 | with pytest.raises(KeyError) as failure: 27 | plotly_obj['bogus'] = 'Hello' 28 | 29 | 30 | def test_get_invalid_property(plotly_obj): 31 | with pytest.raises(KeyError) as failure: 32 | p = plotly_obj['bogus'] 33 | 34 | 35 | # Orphan 36 | # ------ 37 | def test_set_get_property_orphan(plotly_obj): 38 | # Perform set_prop 39 | # ---------------- 40 | plotly_obj['prop1'] = 'Hello' 41 | 42 | # Assertions 43 | # ---------- 44 | # ### test get ### 45 | assert plotly_obj['prop1'] == 'Hello' 46 | 47 | # ### _send_update sent ### 48 | plotly_obj._send_update.assert_called_once_with('prop1', 'Hello') 49 | 50 | # ### _orphan_data configured properly ### 51 | assert plotly_obj._orphan_props == {'prop1': 'Hello'} 52 | 53 | # ### _props is mapped to _orphan_props 54 | assert plotly_obj._props is plotly_obj._orphan_props 55 | 56 | # ### validator called properly ### 57 | plotly_obj._validators['prop1'].validate_coerce.assert_called_once_with('Hello') 58 | 59 | 60 | # With parent 61 | # ----------- 62 | def test_set_get_property_with_parent(plotly_obj, parent): 63 | 64 | # Setup parent 65 | # ------------ 66 | plotly_obj._parent = parent 67 | 68 | # Perform set_prop 69 | # ---------------- 70 | plotly_obj['prop1'] = 'Hello' 71 | 72 | # Parent Assertions 73 | # ----------------- 74 | parent._get_child_props.assert_called_with(plotly_obj) 75 | 76 | # Child Assertions 77 | # ---------------- 78 | # ### test get ### 79 | assert plotly_obj['prop1'] == 'Hello' 80 | 81 | # ### _props bound to parent dict ### 82 | assert parent._get_child_props(plotly_obj) is plotly_obj._props 83 | 84 | # ### _send_update sent ### 85 | plotly_obj._send_update.assert_called_once_with('prop1', 'Hello') 86 | 87 | # ### Orphan data cleared ### 88 | assert plotly_obj._orphan_props == {} 89 | 90 | # ### validator called properly ### 91 | plotly_obj._validators['prop1'].validate_coerce.assert_called_once_with('Hello') 92 | -------------------------------------------------------------------------------- /test/resources/1x1-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonmmease/ipyplotly/498b6ad362c5ffdb5106f8ab3566af68eb7076d9/test/resources/1x1-black.png -------------------------------------------------------------------------------- /test/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # def vals_equal(v1, v2): 3 | # if isinstance(v1, np.ndarray) or isinstance(v2, np.ndarray): 4 | # return np.array_equal(v1, v2) 5 | # else: 6 | # return v1 == v2 -------------------------------------------------------------------------------- /test/validators/test_angle_validator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ipyplotly.basevalidators import AngleValidator 3 | import numpy as np 4 | 5 | 6 | # Fixtures 7 | # -------- 8 | @pytest.fixture() 9 | def validator(): 10 | return AngleValidator('prop', 'parent') 11 | 12 | 13 | # Tests 14 | # ----- 15 | # ### Test acceptance ### 16 | @pytest.mark.parametrize('val', [0] + list(np.linspace(-180, 179.99))) 17 | def test_acceptance(val, validator): 18 | assert validator.validate_coerce(val) == val 19 | 20 | 21 | # ### Test coercion above 180 ### 22 | @pytest.mark.parametrize('val,expected', [ 23 | (180, -180), 24 | (181, -179), 25 | (-180.25, 179.75), 26 | (540, -180), 27 | (-541, 179) 28 | ]) 29 | def test_coercion(val, expected, validator): 30 | assert validator.validate_coerce(val) == expected 31 | 32 | 33 | # ### Test rejection ### 34 | @pytest.mark.parametrize('val', 35 | ['hello', (), [], [1, 2, 3], set(), '34']) 36 | def test_rejection(val, validator: AngleValidator): 37 | with pytest.raises(ValueError) as validation_failure: 38 | validator.validate_coerce(val) 39 | 40 | assert 'Invalid value' in str(validation_failure.value) 41 | -------------------------------------------------------------------------------- /test/validators/test_any_validator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ipyplotly.basevalidators import AnyValidator 3 | import numpy as np 4 | 5 | 6 | # Fixtures 7 | # -------- 8 | @pytest.fixture() 9 | def validator(): 10 | return AnyValidator('prop', 'parent') 11 | 12 | 13 | @pytest.fixture() 14 | def validator_aok(): 15 | return AnyValidator('prop', 'parent', array_ok=True) 16 | 17 | # Tests 18 | # ----- 19 | # ### Acceptance ### 20 | @pytest.mark.parametrize('val', [ 21 | set(), 'Hello', 123, np.inf, np.nan, {} 22 | ]) 23 | def test_acceptance(val, validator: AnyValidator): 24 | assert validator.validate_coerce(val) is val 25 | 26 | 27 | # ### Acceptance of arrays ### 28 | @pytest.mark.parametrize('val', [ 29 | [], np.array([]), ['Hello', 'World'], [np.pi, np.e, {}] 30 | ]) 31 | def test_acceptance_array(val, validator_aok: AnyValidator): 32 | coerce_val = validator_aok.validate_coerce(val) 33 | assert isinstance(coerce_val, np.ndarray) 34 | assert coerce_val.dtype == 'object' 35 | assert np.array_equal(coerce_val, val) 36 | -------------------------------------------------------------------------------- /test/validators/test_basetraces_validator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ipyplotly.basevalidators import BaseTracesValidator 3 | 4 | 5 | # Build test classes 6 | # ------------------ 7 | class Scatter: 8 | def __init__(self, a=None, b=None, c=None, uid=None): 9 | self.type = 'scatter' 10 | self.a = a 11 | self.b = b 12 | self.c = c 13 | self.uid = uid 14 | 15 | 16 | class Bar: 17 | def __init__(self, a=None, b=None, c=None, uid=None): 18 | self.type = 'bar' 19 | self.a = a 20 | self.b = b 21 | self.c = c 22 | self.uid = uid 23 | 24 | 25 | class Box: 26 | def __init__(self, a=None, b=None, c=None, uid=None): 27 | self.type = 'bar' 28 | self.a = a 29 | self.b = b 30 | self.c = c 31 | self.uid = uid 32 | 33 | 34 | # Fixtures 35 | # -------- 36 | @pytest.fixture() 37 | def validator(): 38 | return BaseTracesValidator(class_map={'scatter': Scatter, 'bar': Bar, 'box': Box}) 39 | 40 | 41 | # Tests 42 | # ----- 43 | def test_acceptance(validator: BaseTracesValidator): 44 | val = [Scatter(a=1, c=[3]), Box(b='two')] 45 | res = validator.validate_coerce(val) 46 | 47 | assert isinstance(res, tuple) 48 | assert isinstance(res[0], Scatter) 49 | assert res[0].a == 1 50 | assert res[0].b is None 51 | assert res[0].c == [3] 52 | assert res[0].uid is not None 53 | 54 | assert isinstance(res[1], Box) 55 | assert res[1].a is None 56 | assert res[1].b == 'two' 57 | assert res[1].c is None 58 | assert res[1].uid is not None 59 | 60 | # Make sure UIDs are actually unique 61 | assert res[0].uid != res[1].uid 62 | 63 | 64 | def test_acceptance_dict(validator: BaseTracesValidator): 65 | val = (dict(type='scatter', a=1, c=[3]), dict(type='box', b='two')) 66 | res = validator.validate_coerce(val) 67 | 68 | assert isinstance(res, tuple) 69 | assert isinstance(res[0], Scatter) 70 | assert res[0].a == 1 71 | assert res[0].b is None 72 | assert res[0].c == [3] 73 | assert res[0].uid is not None 74 | 75 | assert isinstance(res[1], Box) 76 | assert res[1].a is None 77 | assert res[1].b == 'two' 78 | assert res[1].c is None 79 | assert res[1].uid is not None 80 | 81 | # Make sure UIDs are actually unique 82 | assert res[0].uid != res[1].uid 83 | 84 | 85 | def test_default_is_scatter(validator: BaseTracesValidator): 86 | val = [dict(a=1, c=[3])] 87 | res = validator.validate_coerce(val) 88 | 89 | assert isinstance(res, tuple) 90 | assert isinstance(res[0], Scatter) 91 | assert res[0].a == 1 92 | assert res[0].b is None 93 | assert res[0].c == [3] 94 | assert res[0].uid is not None 95 | 96 | 97 | def test_uid_preserved(validator: BaseTracesValidator): 98 | uid1 = 'qwerty' 99 | uid2 = 'asdf' 100 | val = (dict(type='scatter', a=1, c=[3], uid=uid1), dict(type='box', b='two', uid=uid2)) 101 | res = validator.validate_coerce(val) 102 | 103 | assert isinstance(res, tuple) 104 | assert isinstance(res[0], Scatter) 105 | assert res[0].a == 1 106 | assert res[0].b is None 107 | assert res[0].c == [3] 108 | assert res[0].uid == uid1 109 | 110 | assert isinstance(res[1], Box) 111 | assert res[1].a is None 112 | assert res[1].b == 'two' 113 | assert res[1].c is None 114 | assert res[1].uid == uid2 115 | 116 | 117 | def test_rejection_type(validator: BaseTracesValidator): 118 | val = 37 119 | 120 | with pytest.raises(ValueError) as validation_failure: 121 | validator.validate_coerce(val) 122 | 123 | assert "Invalid value" in str(validation_failure.value) 124 | 125 | 126 | def test_rejection_element_type(validator: BaseTracesValidator): 127 | val = [42] 128 | 129 | with pytest.raises(ValueError) as validation_failure: 130 | validator.validate_coerce(val) 131 | 132 | assert "Invalid element(s)" in str(validation_failure.value) 133 | 134 | 135 | def test_rejection_element_attr(validator: BaseTracesValidator): 136 | val = [dict(type='scatter', bogus=99)] 137 | 138 | with pytest.raises(TypeError) as validation_failure: 139 | validator.validate_coerce(val) 140 | 141 | assert "got an unexpected keyword argument 'bogus'" in str(validation_failure.value) 142 | 143 | 144 | def test_rejection_element_tracetype(validator: BaseTracesValidator): 145 | val = [dict(type='bogus', a=4)] 146 | 147 | with pytest.raises(ValueError) as validation_failure: 148 | validator.validate_coerce(val) 149 | 150 | assert "Invalid element(s)" in str(validation_failure.value) 151 | -------------------------------------------------------------------------------- /test/validators/test_boolean_validator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ipyplotly.basevalidators import BooleanValidator 3 | import numpy as np 4 | 5 | 6 | # Boolean Validator 7 | # ================= 8 | # ### Fixtures ### 9 | @pytest.fixture(params=[True, False]) 10 | def validator(request): 11 | return BooleanValidator('prop', 'parent', dflt=request.param) 12 | 13 | 14 | # ### Acceptance ### 15 | @pytest.mark.parametrize('val', [True, False]) 16 | def test_acceptance(val, validator): 17 | assert val == validator.validate_coerce(val) 18 | 19 | 20 | # ### Rejection ### 21 | @pytest.mark.parametrize('val', 22 | [1.0, 0.0, 'True', 'False', [], 0, np.nan]) 23 | def test_rejection(val, validator): 24 | with pytest.raises(ValueError) as validation_failure: 25 | validator.validate_coerce(val) 26 | 27 | assert 'Invalid value' in str(validation_failure.value) 28 | -------------------------------------------------------------------------------- /test/validators/test_color_validator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ipyplotly.basevalidators import ColorValidator 3 | import numpy as np 4 | 5 | 6 | # Fixtures 7 | # -------- 8 | @pytest.fixture() 9 | def validator(): 10 | return ColorValidator('prop', 'parent') 11 | 12 | 13 | @pytest.fixture() 14 | def validator_colorscale(): 15 | return ColorValidator('prop', 'parent', colorscale_path='parent.colorscale') 16 | 17 | 18 | @pytest.fixture() 19 | def validator_aok(): 20 | return ColorValidator('prop', 'parent', array_ok=True) 21 | 22 | 23 | @pytest.fixture() 24 | def validator_aok_colorscale(): 25 | return ColorValidator('prop', 'parent', array_ok=True, colorscale_path='parent.colorscale') 26 | 27 | 28 | # Array not ok, numbers not ok 29 | # ---------------------------- 30 | @pytest.mark.parametrize('val', 31 | ['red', 'BLUE', 'rgb(255, 0, 0)', 'hsl(0, 100%, 50%)', 'hsla(0, 100%, 50%, 100%)', 32 | 'hsv(0, 100%, 100%)', 'hsva(0, 100%, 100%, 50%)']) 33 | def test_acceptance(val, validator: ColorValidator): 34 | if isinstance(val, str): 35 | assert validator.validate_coerce(val) == str.replace(val.lower(), ' ', '') 36 | else: 37 | assert validator.validate_coerce(val) == val 38 | 39 | 40 | # ### Rejection by type ### 41 | @pytest.mark.parametrize('val', 42 | [set(), 23, 0.5, {}, ['red'], [12]]) 43 | def test_rejection(val, validator: ColorValidator): 44 | with pytest.raises(ValueError) as validation_failure: 45 | validator.validate_coerce(val) 46 | 47 | assert 'Invalid value' in str(validation_failure.value) 48 | 49 | 50 | # ### Rejection by value ### 51 | @pytest.mark.parametrize('val', 52 | ['redd', 'rgbbb(255, 0, 0)', 'hsl(0, 1%0000%, 50%)']) 53 | def test_rejection(val, validator: ColorValidator): 54 | with pytest.raises(ValueError) as validation_failure: 55 | validator.validate_coerce(val) 56 | 57 | assert 'Invalid value' in str(validation_failure.value) 58 | 59 | 60 | # Array not ok, numbers ok 61 | # ------------------------ 62 | # ### Acceptance ### 63 | @pytest.mark.parametrize('val', 64 | ['red', 'BLUE', 23, 15, 'rgb(255, 0, 0)', 'hsl(0, 100%, 50%)', 'hsla(0, 100%, 50%, 100%)', 65 | 'hsv(0, 100%, 100%)', 'hsva(0, 100%, 100%, 50%)']) 66 | def test_acceptance_colorscale(val, validator_colorscale: ColorValidator): 67 | if isinstance(val, str): 68 | assert validator_colorscale.validate_coerce(val) == str.replace(val.lower(), ' ', '') 69 | else: 70 | assert validator_colorscale.validate_coerce(val) == val 71 | 72 | 73 | # ### Rejection by type ### 74 | @pytest.mark.parametrize('val', 75 | [set(), {}, ['red'], [12]]) 76 | def test_rejection_colorscale(val, validator_colorscale: ColorValidator): 77 | with pytest.raises(ValueError) as validation_failure: 78 | validator_colorscale.validate_coerce(val) 79 | 80 | assert 'Invalid value' in str(validation_failure.value) 81 | 82 | 83 | # ### Rejection by value ### 84 | @pytest.mark.parametrize('val', 85 | ['redd', 'rgbbb(255, 0, 0)', 'hsl(0, 1%0000%, 50%)']) 86 | def test_rejection_colorscale(val, validator_colorscale: ColorValidator): 87 | with pytest.raises(ValueError) as validation_failure: 88 | validator_colorscale.validate_coerce(val) 89 | 90 | assert 'Invalid value' in str(validation_failure.value) 91 | 92 | 93 | # Array ok, numbers not ok 94 | # ------------------------ 95 | # ### Acceptance ### 96 | @pytest.mark.parametrize('val', 97 | ['blue', 98 | ['red', 'rgb(255, 0, 0)'], 99 | ['hsl(0, 100%, 50%)', 'hsla(0, 100%, 50%, 100%)', 'hsv(0, 100%, 100%)'], 100 | ['hsva(0, 100%, 100%, 50%)']]) 101 | def test_acceptance_aok(val, validator_aok: ColorValidator): 102 | coerce_val = validator_aok.validate_coerce(val) 103 | if isinstance(val, (list, np.ndarray)): 104 | expected = np.array( 105 | [str.replace(v.lower(), ' ', '') if isinstance(v, str) else v for v in val], 106 | dtype=coerce_val.dtype) 107 | assert np.array_equal(coerce_val, expected) 108 | else: 109 | expected = str.replace(val.lower(), ' ', '') if isinstance(val, str) else val 110 | assert coerce_val == expected 111 | 112 | 113 | # ### Rejection ### 114 | @pytest.mark.parametrize('val', 115 | [[23], [0, 1, 2], 116 | ['redd', 'rgb(255, 0, 0)'], 117 | ['hsl(0, 100%, 50_00%)', 'hsla(0, 100%, 50%, 100%)', 'hsv(0, 100%, 100%)'], 118 | ['hsva(0, 1%00%, 100%, 50%)']]) 119 | def test_rejection_aok(val, validator_aok: ColorValidator): 120 | with pytest.raises(ValueError) as validation_failure: 121 | validator_aok.validate_coerce(val) 122 | 123 | assert 'Invalid element(s)' in str(validation_failure.value) 124 | 125 | 126 | # Array ok, numbers ok 127 | # -------------------- 128 | # ### Acceptance ### 129 | @pytest.mark.parametrize('val', 130 | ['blue', 23, [0, 1, 2], 131 | ['red', 0.5, 'rgb(255, 0, 0)'], 132 | ['hsl(0, 100%, 50%)', 'hsla(0, 100%, 50%, 100%)', 'hsv(0, 100%, 100%)'], 133 | ['hsva(0, 100%, 100%, 50%)']]) 134 | def test_acceptance_aok_colorscale(val, validator_aok_colorscale: ColorValidator): 135 | coerce_val = validator_aok_colorscale.validate_coerce(val) 136 | if isinstance(val, (list, np.ndarray)): 137 | expected = np.array( 138 | [str.replace(v.lower(), ' ', '') if isinstance(v, str) else v for v in val], 139 | dtype=coerce_val.dtype) 140 | assert np.array_equal(coerce_val, expected) 141 | else: 142 | expected = str.replace(val.lower(), ' ', '') if isinstance(val, str) else val 143 | assert coerce_val == expected 144 | 145 | 146 | # ### Rejection ### 147 | @pytest.mark.parametrize('val', 148 | [['redd', 0.5, 'rgb(255, 0, 0)'], 149 | ['hsl(0, 100%, 50_00%)', 'hsla(0, 100%, 50%, 100%)', 'hsv(0, 100%, 100%)'], 150 | ['hsva(0, 1%00%, 100%, 50%)']]) 151 | def test_rejection_aok_colorscale(val, validator_aok_colorscale: ColorValidator): 152 | with pytest.raises(ValueError) as validation_failure: 153 | validator_aok_colorscale.validate_coerce(val) 154 | 155 | assert 'Invalid element(s)' in str(validation_failure.value) 156 | 157 | 158 | # Description 159 | # ----------- 160 | # Test dynamic description logic 161 | def test_description(validator: ColorValidator): 162 | desc = validator.description() 163 | assert 'A number that will be interpreted as a color' not in desc 164 | assert 'A list or array of any of the above' not in desc 165 | 166 | 167 | def test_description_aok(validator_aok: ColorValidator): 168 | desc = validator_aok.description() 169 | assert 'A number that will be interpreted as a color' not in desc 170 | assert 'A list or array of any of the above' in desc 171 | 172 | 173 | def test_description_aok(validator_colorscale: ColorValidator): 174 | desc = validator_colorscale.description() 175 | assert 'A number that will be interpreted as a color' in desc 176 | assert 'A list or array of any of the above' not in desc 177 | 178 | 179 | def test_description_aok_colorscale(validator_aok_colorscale: ColorValidator): 180 | desc = validator_aok_colorscale.description() 181 | assert 'A number that will be interpreted as a color' in desc 182 | assert 'A list or array of any of the above' in desc 183 | -------------------------------------------------------------------------------- /test/validators/test_colorscale_validator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ipyplotly.basevalidators import ColorscaleValidator 3 | import numpy as np 4 | 5 | 6 | # Fixtures 7 | # -------- 8 | @pytest.fixture() 9 | def validator(): 10 | return ColorscaleValidator('prop', 'parent') 11 | 12 | 13 | @pytest.fixture(params=['Greys', 'YlGnBu', 'Greens', 'YlOrRd', 'Bluered', 'RdBu', 'Reds', 'Blues', 14 | 'Picnic', 'Rainbow', 'Portland', 'Jet', 'Hot', 'Blackbody', 'Earth', 'Electric', 'Viridis']) 15 | def named_colorscale(request): 16 | return request.param 17 | 18 | 19 | # Tests 20 | # ----- 21 | # ### Acceptance by name ### 22 | def test_acceptance_named(named_colorscale, validator: ColorscaleValidator): 23 | assert validator.validate_coerce(named_colorscale) == named_colorscale 24 | 25 | 26 | # ### Acceptance as array ### 27 | @pytest.mark.parametrize('val', [ 28 | ((0, 'red'),), 29 | ((0.1, 'rgb(255,0,0)'), (0.3, 'green')), 30 | ((0, 'purple'), (0.2, 'yellow'), (1.0, 'rgba(255,0,0,100)')), 31 | ]) 32 | def test_acceptance_array(val, validator: ColorscaleValidator): 33 | assert validator.validate_coerce(val) == val 34 | 35 | 36 | # ### Coercion of scale names ### 37 | def test_coercion_named(named_colorscale, validator: ColorscaleValidator): 38 | # As is 39 | assert validator.validate_coerce(named_colorscale) == named_colorscale 40 | 41 | # Uppercase 42 | assert validator.validate_coerce(named_colorscale.upper()) == named_colorscale 43 | 44 | # Lowercase 45 | assert validator.validate_coerce(named_colorscale.lower()) == named_colorscale 46 | 47 | 48 | # ### Coercion as array ### 49 | @pytest.mark.parametrize('val', [ 50 | ([0, 'red'],), 51 | [(0.1, 'rgb(255, 0, 0)'), (0.3, 'GREEN')], 52 | (np.array([0, 'Purple'], dtype='object'), (0.2, 'yellow'), (1.0, 'RGBA(255,0,0,100)')), 53 | ]) 54 | def test_acceptance_array(val, validator: ColorscaleValidator): 55 | # Compute expected (tuple of tuples where color is lowercase with no spaces) 56 | expected = tuple([tuple([e[0], str.replace(e[1].lower(), ' ', '')]) for e in val]) 57 | assert validator.validate_coerce(val) == expected 58 | 59 | 60 | # ### Rejection by type ### 61 | @pytest.mark.parametrize('val', [ 62 | 23, set(), {}, np.pi 63 | ]) 64 | def test_rejection_type(val, validator: ColorscaleValidator): 65 | with pytest.raises(ValueError) as validation_failure: 66 | validator.validate_coerce(val) 67 | 68 | assert 'Invalid value' in str(validation_failure.value) 69 | 70 | 71 | # ### Rejection by string value ### 72 | @pytest.mark.parametrize('val', [ 73 | 'Invalid', '' 74 | ]) 75 | def test_rejection_str_value(val, validator: ColorscaleValidator): 76 | with pytest.raises(ValueError) as validation_failure: 77 | validator.validate_coerce(val) 78 | 79 | assert 'Invalid value' in str(validation_failure.value) 80 | 81 | 82 | # ### Rejection by array ### 83 | @pytest.mark.parametrize('val', [ 84 | [0, 'red'], # Elements must be tuples 85 | [[0.1, 'rgb(255,0,0)', None], (0.3, 'green')], # length 3 element 86 | ([1.1, 'purple'], [0.2, 'yellow']), # Number > 1 87 | ([0.1, 'purple'], [-0.2, 'yellow']), # Number < 0 88 | ([0.1, 'purple'], [0.2, 123]), # Color not a string 89 | ([0.1, 'purple'], [0.2, 'yellowww']), # Invalid color string 90 | ]) 91 | def test_rejection_array(val, validator: ColorscaleValidator): 92 | with pytest.raises(ValueError) as validation_failure: 93 | validator.validate_coerce(val) 94 | 95 | assert 'Invalid value' in str(validation_failure.value) 96 | -------------------------------------------------------------------------------- /test/validators/test_compound_validator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ipyplotly.basevalidators import CompoundValidator 3 | 4 | 5 | # Build test class 6 | # ---------------- 7 | class CompoundType: 8 | def __init__(self, a=None, b=None, c=None): 9 | self.a = a 10 | self.b = b 11 | self.c = c 12 | self._props = {'a': a, 'b': b, 'c': c} 13 | 14 | 15 | # Fixtures 16 | # -------- 17 | @pytest.fixture() 18 | def validator(): 19 | return CompoundValidator('prop', 'parent', data_class=CompoundType, data_docs='') 20 | 21 | 22 | # Tests 23 | # ----- 24 | def test_acceptance(validator: CompoundValidator): 25 | val = CompoundType(a=1, c=[3]) 26 | res = validator.validate_coerce(val) 27 | 28 | assert isinstance(res, CompoundType) 29 | assert res.a == 1 30 | assert res.b is None 31 | assert res.c == [3] 32 | 33 | 34 | def test_acceptance_none(validator: CompoundValidator): 35 | val = None 36 | res = validator.validate_coerce(val) 37 | 38 | assert isinstance(res, CompoundType) 39 | assert res.a is None 40 | assert res.b is None 41 | assert res.c is None 42 | 43 | 44 | def test_acceptance_dict(validator: CompoundValidator): 45 | val = dict(a=1, b='two') 46 | res = validator.validate_coerce(val) 47 | 48 | assert isinstance(res, CompoundType) 49 | assert res.a == 1 50 | assert res.b == 'two' 51 | assert res.c is None 52 | 53 | 54 | def test_rejection_type(validator: CompoundValidator): 55 | val = 37 56 | 57 | with pytest.raises(ValueError) as validation_failure: 58 | validator.validate_coerce(val) 59 | 60 | assert "Invalid value" in str(validation_failure.value) 61 | 62 | 63 | def test_rejection_value(validator: CompoundValidator): 64 | val = dict(a=1, b='two', bogus=99) 65 | 66 | with pytest.raises(TypeError) as validation_failure: 67 | validator.validate_coerce(val) 68 | 69 | assert "got an unexpected keyword argument 'bogus'" in str(validation_failure.value) 70 | -------------------------------------------------------------------------------- /test/validators/test_compoundarray_validator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ipyplotly.basevalidators import CompoundArrayValidator 3 | 4 | 5 | # Build test class 6 | # ---------------- 7 | class CompoundType: 8 | def __init__(self, a=None, b=None, c=None): 9 | self.a = a 10 | self.b = b 11 | self.c = c 12 | 13 | 14 | # Fixtures 15 | # -------- 16 | @pytest.fixture() 17 | def validator(): 18 | return CompoundArrayValidator('prop', 'parent', element_class=CompoundType, element_docs='') 19 | 20 | 21 | # Tests 22 | # ----- 23 | def test_acceptance(validator: CompoundArrayValidator): 24 | val = [CompoundType(a=1, c=[3]), CompoundType(b='two')] 25 | res = validator.validate_coerce(val) 26 | 27 | assert isinstance(res, tuple) 28 | assert isinstance(res[0], CompoundType) 29 | assert res[0].a == 1 30 | assert res[0].b is None 31 | assert res[0].c == [3] 32 | 33 | assert isinstance(res[1], CompoundType) 34 | assert res[1].a is None 35 | assert res[1].b == 'two' 36 | assert res[1].c is None 37 | 38 | 39 | def test_acceptance_empty(validator: CompoundArrayValidator): 40 | val = [{}] 41 | res = validator.validate_coerce(val) 42 | 43 | assert isinstance(res, tuple) 44 | assert isinstance(res[0], CompoundType) 45 | assert res[0].a is None 46 | assert res[0].b is None 47 | assert res[0].c is None 48 | 49 | 50 | def test_acceptance_dict(validator: CompoundArrayValidator): 51 | val = [dict(a=1, c=[3]), dict(b='two')] 52 | res = validator.validate_coerce(val) 53 | 54 | assert isinstance(res, tuple) 55 | assert isinstance(res[0], CompoundType) 56 | assert res[0].a == 1 57 | assert res[0].b is None 58 | assert res[0].c == [3] 59 | 60 | assert isinstance(res[1], CompoundType) 61 | assert res[1].a is None 62 | assert res[1].b == 'two' 63 | assert res[1].c is None 64 | 65 | 66 | def test_rejection_type(validator: CompoundArrayValidator): 67 | val = 37 68 | 69 | with pytest.raises(ValueError) as validation_failure: 70 | validator.validate_coerce(val) 71 | 72 | assert "Invalid value" in str(validation_failure.value) 73 | 74 | 75 | def test_rejection_element(validator: CompoundArrayValidator): 76 | val = [{'a': 23}, 37] 77 | 78 | with pytest.raises(ValueError) as validation_failure: 79 | validator.validate_coerce(val) 80 | 81 | assert "Invalid element(s)" in str(validation_failure.value) 82 | 83 | 84 | def test_rejection_value(validator: CompoundArrayValidator): 85 | val = [dict(a=1, b='two', bogus=99)] 86 | 87 | with pytest.raises(TypeError) as validation_failure: 88 | validator.validate_coerce(val) 89 | 90 | assert "got an unexpected keyword argument 'bogus'" in str(validation_failure.value) 91 | -------------------------------------------------------------------------------- /test/validators/test_dataarray_validator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ipyplotly.basevalidators import DataArrayValidator 3 | import numpy as np 4 | 5 | 6 | # Fixtures 7 | # -------- 8 | @pytest.fixture() 9 | def validator(): 10 | return DataArrayValidator('prop', 'parent') 11 | 12 | 13 | # Tests 14 | # ----- 15 | # ### Acceptance ### 16 | @pytest.mark.parametrize('val', [ 17 | [], [1], np.array([2, 3, 4]), [''], (), ('Hello, ', 'world!') 18 | ]) 19 | def test_validator_acceptance(val, validator: DataArrayValidator): 20 | coerce_val = validator.validate_coerce(val) 21 | assert isinstance(coerce_val, np.ndarray) 22 | assert np.array_equal(coerce_val, val) 23 | 24 | 25 | # ### Rejection ### 26 | @pytest.mark.parametrize('val', [ 27 | 'Hello', 23, set(), {}, 28 | ]) 29 | def test_rejection(val, validator: DataArrayValidator): 30 | with pytest.raises(ValueError) as validation_failure: 31 | validator.validate_coerce(val) 32 | 33 | assert 'Invalid value' in str(validation_failure.value) 34 | -------------------------------------------------------------------------------- /test/validators/test_enumerated_validator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from ipyplotly.basevalidators import EnumeratedValidator 4 | 5 | 6 | # Fixtures 7 | # -------- 8 | @pytest.fixture() 9 | def validator(): 10 | values = ['first', 'second', 'third', 4] 11 | return EnumeratedValidator('prop', 'parent', values, array_ok=False) 12 | 13 | 14 | @pytest.fixture() 15 | def validator_re(): 16 | values = ['foo', '/bar(\d)+/', 'baz'] 17 | return EnumeratedValidator('prop', 'parent', values, array_ok=False) 18 | 19 | 20 | @pytest.fixture() 21 | def validator_aok(): 22 | values = ['first', 'second', 'third', 4] 23 | return EnumeratedValidator('prop', 'parent', values, array_ok=True) 24 | 25 | 26 | @pytest.fixture() 27 | def validator_aok_re(): 28 | values = ['foo', '/bar(\d)+/', 'baz'] 29 | return EnumeratedValidator('prop', 'parent', values, array_ok=True) 30 | 31 | 32 | # Array not ok 33 | # ------------ 34 | # ### Acceptance ### 35 | @pytest.mark.parametrize('val', 36 | ['first', 'second', 'third', 4]) 37 | def test_acceptance(val, validator): 38 | # Values should be accepted and returned unchanged 39 | assert validator.validate_coerce(val) == val 40 | 41 | 42 | # ### Value Rejection ### 43 | @pytest.mark.parametrize('val', 44 | [True, 0, 1, 23, np.inf, set(), 45 | ['first', 'second'], [True], ['third', 4], [4]]) 46 | def test_rejection_by_value(val, validator): 47 | with pytest.raises(ValueError) as validation_failure: 48 | validator.validate_coerce(val) 49 | 50 | assert 'Invalid value' in str(validation_failure.value) 51 | 52 | 53 | # Array not ok, regular expression 54 | # -------------------------------- 55 | @pytest.mark.parametrize('val', 56 | ['foo', 'bar0', 'bar1', 'bar234']) 57 | def test_acceptance(val, validator_re): 58 | # Values should be accepted and returned unchanged 59 | assert validator_re.validate_coerce(val) == val 60 | 61 | 62 | # ### Value Rejection ### 63 | @pytest.mark.parametrize('val', 64 | [12, set(), 'bar', 'BAR0', 'FOO']) 65 | def test_rejection_by_value(val, validator_re): 66 | with pytest.raises(ValueError) as validation_failure: 67 | validator_re.validate_coerce(val) 68 | 69 | assert 'Invalid value' in str(validation_failure.value) 70 | 71 | 72 | # Array ok 73 | # -------- 74 | # ### Acceptance ### 75 | @pytest.mark.parametrize('val', 76 | ['first', 'second', 'third', 4, 77 | [], ['first', 4], [4], ['third', 'first'], 78 | ['first', 'second', 'third', 4]]) 79 | def test_acceptance_aok(val, validator_aok): 80 | # Values should be accepted and returned unchanged 81 | coerce_val = validator_aok.validate_coerce(val) 82 | if isinstance(val, (list, np.ndarray)): 83 | assert np.array_equal(coerce_val, np.array(val, dtype=coerce_val.dtype)) 84 | else: 85 | assert coerce_val == val 86 | 87 | 88 | # ### Rejection by value ### 89 | @pytest.mark.parametrize('val', 90 | [True, 0, 1, 23, np.inf, set()]) 91 | def test_rejection_by_value_aok(val, validator_aok): 92 | with pytest.raises(ValueError) as validation_failure: 93 | validator_aok.validate_coerce(val) 94 | 95 | assert 'Invalid value' in str(validation_failure.value) 96 | 97 | 98 | # ### Reject by elements ### 99 | @pytest.mark.parametrize('val', 100 | [[True], [0], [1, 23], [np.inf, set()], 101 | ['ffirstt', 'second', 'third']]) 102 | def test_rejection_by_element_aok(val, validator_aok): 103 | with pytest.raises(ValueError) as validation_failure: 104 | validator_aok.validate_coerce(val) 105 | 106 | assert 'Invalid element(s)' in str(validation_failure.value) 107 | 108 | 109 | # Array ok, regular expression 110 | # ---------------------------- 111 | # ### Acceptance ### 112 | @pytest.mark.parametrize('val', 113 | ['foo', 'bar12', 'bar21', 114 | [], ['bar12'], ['foo', 'bar012', 'baz']]) 115 | def test_acceptance_aok(val, validator_aok_re): 116 | # Values should be accepted and returned unchanged 117 | coerce_val = validator_aok_re.validate_coerce(val) 118 | if isinstance(val, (list, np.ndarray)): 119 | assert np.array_equal(coerce_val, np.array(val, dtype=coerce_val.dtype)) 120 | else: 121 | assert coerce_val == val 122 | 123 | 124 | # ### Reject by elements ### 125 | @pytest.mark.parametrize('val', 126 | [['bar', 'bar0'], ['foo', 123]]) 127 | def test_rejection_by_element_aok(val, validator_aok_re): 128 | with pytest.raises(ValueError) as validation_failure: 129 | validator_aok_re.validate_coerce(val) 130 | 131 | assert 'Invalid element(s)' in str(validation_failure.value) 132 | -------------------------------------------------------------------------------- /test/validators/test_flaglist_validator.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import pytest 3 | from ipyplotly.basevalidators import FlaglistValidator 4 | import numpy as np 5 | 6 | 7 | # Fixtures 8 | # -------- 9 | @pytest.fixture(params=[None, ['none', 'all']]) 10 | def validator(request): 11 | # Validator with or without extras 12 | return FlaglistValidator('prop', 'parent', flags=['lines', 'markers', 'text'], extras=request.param) 13 | 14 | 15 | @pytest.fixture() 16 | def validator_extra(): 17 | return FlaglistValidator('prop', 'parent', 18 | flags=['lines', 'markers', 'text'], 19 | extras=['none', 'all']) 20 | 21 | 22 | @pytest.fixture() 23 | def validator_extra_aok(): 24 | return FlaglistValidator('prop', 'parent', 25 | flags=['lines', 'markers', 'text'], 26 | extras=['none', 'all'], 27 | array_ok=True) 28 | 29 | 30 | @pytest.fixture(params= 31 | ["+".join(p) 32 | for i in range(1, 4) 33 | for p in itertools.permutations(['lines', 'markers', 'text'], i)]) 34 | def flaglist(request): 35 | return request.param 36 | 37 | 38 | @pytest.fixture(params=['none', 'all']) 39 | def extra(request): 40 | return request.param 41 | 42 | 43 | # Array not ok (with or without extras) 44 | # ------------------------------------- 45 | # ### Acceptance ### 46 | def test_acceptance(flaglist, validator: FlaglistValidator): 47 | assert validator.validate_coerce(flaglist) == flaglist 48 | 49 | 50 | # ### Coercion ### 51 | @pytest.mark.parametrize('in_val,coerce_val', 52 | [(' lines ', 'lines'), # Strip outer whitespace 53 | (' lines + markers ', 'lines+markers'), # Remove inner whitespace around '+' 54 | ('lines ,markers', 'lines+markers'), # Accept comma separated 55 | ]) 56 | def test_coercion(in_val, coerce_val, validator): 57 | assert validator.validate_coerce(in_val) == coerce_val 58 | 59 | 60 | # ### Rejection by type ### 61 | @pytest.mark.parametrize('val', 62 | [21, (), ['lines'], set(), {}]) 63 | def test_rejection_type(val, validator: FlaglistValidator): 64 | with pytest.raises(ValueError) as validation_failure: 65 | validator.validate_coerce(val) 66 | 67 | assert 'Invalid value' in str(validation_failure.value) 68 | 69 | 70 | # ### Rejection by value ### 71 | @pytest.mark.parametrize('val', 72 | ['', 'line', 'markers+line', 'lin es', 'lin es+markers']) 73 | def test_rejection_val(val, validator: FlaglistValidator): 74 | with pytest.raises(ValueError) as validation_failure: 75 | validator.validate_coerce(val) 76 | 77 | assert 'Invalid value' in str(validation_failure.value) 78 | 79 | 80 | # Array not ok (with extras) 81 | # -------------------------- 82 | # ### Acceptance ### 83 | # Note: Acceptance of flaglists without extras already tested above 84 | def test_acceptance_extra(extra, validator_extra: FlaglistValidator): 85 | assert validator_extra.validate_coerce(extra) == extra 86 | 87 | 88 | # ### Coercion ### 89 | @pytest.mark.parametrize('in_val,coerce_val', 90 | [(' none ', 'none'), 91 | ('all ', 'all'), 92 | ]) 93 | def test_coercion(in_val, coerce_val, validator_extra): 94 | assert validator_extra.validate_coerce(in_val) == coerce_val 95 | 96 | 97 | # ### Rejection by value ### 98 | # Note: Rejection by type already handled above 99 | @pytest.mark.parametrize('val', 100 | ['al l', # Don't remove inner whitespace 101 | 'lines+all', # Extras cannot be combined with flags 102 | 'none+markers', 103 | 'markers+lines+text+none']) 104 | def test_rejection_val(val, validator_extra: FlaglistValidator): 105 | with pytest.raises(ValueError) as validation_failure: 106 | validator_extra.validate_coerce(val) 107 | 108 | assert 'Invalid value' in str(validation_failure.value) 109 | 110 | 111 | # Array OK (with extras) 112 | # ---------------------- 113 | # ### Acceptance (scalars) ### 114 | def test_acceptance_aok_scalar_flaglist(flaglist, validator_extra_aok: FlaglistValidator): 115 | assert validator_extra_aok.validate_coerce(flaglist) == flaglist 116 | 117 | 118 | def test_acceptance_aok_scalar_extra(extra, validator_extra_aok: FlaglistValidator): 119 | assert validator_extra_aok.validate_coerce(extra) == extra 120 | 121 | 122 | # ### Acceptance (lists) ### 123 | def test_acceptance_aok_scalarlist_flaglist(flaglist, validator_extra_aok: FlaglistValidator): 124 | assert np.array_equal(validator_extra_aok.validate_coerce([flaglist]), 125 | np.array([flaglist], dtype='unicode')) 126 | 127 | 128 | @pytest.mark.parametrize('val', [ 129 | ['all', 'markers', 'text+markers'], 130 | ['lines', 'lines+markers', 'markers+lines+text'], 131 | ['all', 'all', 'lines+text', 'none'] 132 | ]) 133 | def test_acceptance_aok_list_flaglist(val, validator_extra_aok: FlaglistValidator): 134 | assert np.array_equal(validator_extra_aok.validate_coerce(val), 135 | np.array(val, dtype='unicode')) 136 | 137 | 138 | # ### Coercion ### 139 | @pytest.mark.parametrize('in_val,coerce_val', 140 | [([' lines ', ' lines + markers ', 'lines ,markers'], 141 | np.array(['lines', 'lines+markers', 'lines+markers'], dtype='unicode') 142 | ), 143 | (np.array(['text +lines']), 144 | np.array(['text+lines'], dtype='unicode') 145 | ) 146 | ]) 147 | def test_coercion_aok(in_val, coerce_val, validator_extra_aok): 148 | assert np.array_equal(validator_extra_aok.validate_coerce(in_val), coerce_val) 149 | 150 | 151 | # ### Rejection by type ### 152 | @pytest.mark.parametrize('val', 153 | [21, set(), {}]) 154 | def test_rejection_aok_type(val, validator_extra_aok: FlaglistValidator): 155 | with pytest.raises(ValueError) as validation_failure: 156 | validator_extra_aok.validate_coerce(val) 157 | 158 | assert 'Invalid value' in str(validation_failure.value) 159 | 160 | 161 | # ### Rejection by element type ### 162 | @pytest.mark.parametrize('val', 163 | [[21, 'markers'], 164 | ['lines', ()], 165 | ['none', set()], 166 | ['lines+text', {}, 'markers']]) 167 | def test_rejection_aok_element_type(val, validator_extra_aok: FlaglistValidator): 168 | with pytest.raises(ValueError) as validation_failure: 169 | validator_extra_aok.validate_coerce(val) 170 | 171 | assert 'Invalid element(s)' in str(validation_failure.value) 172 | 173 | 174 | # ### Rejection by element values ### 175 | @pytest.mark.parametrize('val', [ 176 | ['all+markers', 'text+markers'], # extra plus flag 177 | ['line', 'lines+markers', 'markers+lines+text'], # Invalid flag 178 | ['all', '', 'lines+text', 'none'] # Empty string 179 | ]) 180 | def test_rejection_aok_element_val(val, validator_extra_aok: FlaglistValidator): 181 | with pytest.raises(ValueError) as validation_failure: 182 | validator_extra_aok.validate_coerce(val) 183 | 184 | assert 'Invalid element(s)' in str(validation_failure.value) 185 | -------------------------------------------------------------------------------- /test/validators/test_imageuri_validator.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | import pytest 4 | from ipyplotly.basevalidators import ImageUriValidator 5 | import numpy as np 6 | from PIL import Image 7 | 8 | 9 | # Fixtures 10 | # -------- 11 | @pytest.fixture() 12 | def validator(): 13 | return ImageUriValidator('prop', 'parent') 14 | 15 | 16 | # Tests 17 | # ----- 18 | # ### Acceptance ### 19 | @pytest.mark.parametrize('val', [ 20 | 'http://somewhere.com/images/image12.png', 21 | 'data:image/png;base64,iVBORw0KGgoAAAANSU', 22 | ]) 23 | def test_validator_acceptance(val, validator: ImageUriValidator): 24 | assert validator.validate_coerce(val) == val 25 | 26 | 27 | # ### Coercion from PIL Image ### 28 | def test_validator_coercion_PIL(validator: ImageUriValidator): 29 | # Single pixel black png (http://png-pixel.com/) 30 | 31 | img_path = 'test/resources/1x1-black.png' 32 | with open(img_path, 'rb') as f: 33 | hex_bytes = base64.b64encode(f.read()).decode('ascii') 34 | expected_uri = 'data:image/png;base64,' + hex_bytes 35 | 36 | img = Image.open(img_path) 37 | coerce_val = validator.validate_coerce(img) 38 | assert coerce_val == expected_uri 39 | 40 | 41 | # ### Rejection ### 42 | @pytest.mark.parametrize('val', [ 43 | 23, set(), [] 44 | ]) 45 | def test_rejection_by_type(val, validator): 46 | with pytest.raises(ValueError) as validation_failure: 47 | validator.validate_coerce(val) 48 | 49 | assert 'Invalid value' in str(validation_failure.value) 50 | -------------------------------------------------------------------------------- /test/validators/test_infoarray_validator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ipyplotly.basevalidators import InfoArrayValidator, type_str 3 | import numpy as np 4 | 5 | 6 | # Fixtures 7 | # -------- 8 | @pytest.fixture() 9 | def validator_any2(): 10 | return InfoArrayValidator('prop', 'parent', items=[{'valType': 'any'}, {'valType': 'any'}]) 11 | 12 | 13 | @pytest.fixture() 14 | def validator_number3(): 15 | return InfoArrayValidator('prop', 'parent', items=[ 16 | {'valType': 'number', 'min': 0, 'max': 1}, 17 | {'valType': 'number', 'min': 0, 'max': 1}, 18 | {'valType': 'number', 'min': 0, 'max': 1}]) 19 | 20 | 21 | @pytest.fixture() 22 | def validator_number3_free(): 23 | return InfoArrayValidator('prop', 'parent', items=[ 24 | {'valType': 'number', 'min': 0, 'max': 1}, 25 | {'valType': 'number', 'min': 0, 'max': 1}, 26 | {'valType': 'number', 'min': 0, 'max': 1}], free_length=True) 27 | 28 | 29 | # Any2 Tests 30 | # ---------- 31 | 32 | # ### Acceptance ### 33 | @pytest.mark.parametrize('val', [ 34 | [1, 'A'], ('hello', 'world!'), [{}, []], [-1, 1] 35 | ]) 36 | def test_validator_acceptance_any2(val, validator_any2: InfoArrayValidator): 37 | coerce_val = validator_any2.validate_coerce(val) 38 | assert np.array_equal(coerce_val, tuple(val)) 39 | 40 | 41 | # ### Rejection by type ### 42 | @pytest.mark.parametrize('val', [ 43 | 'Not a list', 123, set(), {} 44 | ]) 45 | def test_validator_rejection_any2_type(val, validator_any2: InfoArrayValidator): 46 | with pytest.raises(ValueError) as validation_failure: 47 | validator_any2.validate_coerce(val) 48 | 49 | assert 'must be a list or tuple.' in str(validation_failure.value) 50 | 51 | 52 | # ### Rejection by length ### 53 | @pytest.mark.parametrize('val', [ 54 | [0, 1, 'A'], ('hello', 'world', '!'), [None, {}, []], [-1, 1, 9] 55 | ]) 56 | def test_validator_rejection_any2_length(val, validator_any2: InfoArrayValidator): 57 | with pytest.raises(ValueError) as validation_failure: 58 | validator_any2.validate_coerce(val) 59 | 60 | assert 'Invalid value' in str(validation_failure.value) 61 | 62 | 63 | # Number3 Tests 64 | # ------------- 65 | # ### Acceptance ### 66 | @pytest.mark.parametrize('val', [ 67 | [1, 0, 0.5], (0.1, 0.4, 0.99), [1, 1, 0] 68 | ]) 69 | def test_validator_acceptance_number3(val, validator_number3: InfoArrayValidator): 70 | coerce_val = validator_number3.validate_coerce(val) 71 | assert np.array_equal(coerce_val, tuple(val)) 72 | 73 | 74 | # ### Rejection by length ### 75 | @pytest.mark.parametrize('val', [ 76 | [1, 0], (0.1, 0.4, 0.99, 0.4), [1] 77 | ]) 78 | def test_validator_rejection_number3_length(val, validator_number3: InfoArrayValidator): 79 | with pytest.raises(ValueError) as validation_failure: 80 | validator_number3.validate_coerce(val) 81 | 82 | assert 'must be a list or tuple of length 3.' in str(validation_failure.value) 83 | 84 | 85 | # ### Rejection by element type ### 86 | @pytest.mark.parametrize('val,first_invalid_ind', [ 87 | ([1, 0, '0.5'], 2), 88 | ((0.1, set(), 0.99), 1), 89 | ([[], '2', {}], 0) 90 | ]) 91 | def test_validator_rejection_number3_length(val, first_invalid_ind, validator_number3: InfoArrayValidator): 92 | with pytest.raises(ValueError) as validation_failure: 93 | validator_number3.validate_coerce(val) 94 | 95 | assert 'The prop[%d] property of parent must be a number.' % first_invalid_ind in str(validation_failure.value) 96 | 97 | 98 | # ### Rejection by element value ### 99 | # Elements must be in [0, 1] 100 | @pytest.mark.parametrize('val,first_invalid_ind', [ 101 | ([1, 0, 1.5], 2), 102 | ((0.1, -0.4, 0.99), 1), 103 | ([-1, 1, 0], 0) 104 | ]) 105 | def test_validator_rejection_number3_length(val, first_invalid_ind, validator_number3: InfoArrayValidator): 106 | with pytest.raises(ValueError) as validation_failure: 107 | validator_number3.validate_coerce(val) 108 | 109 | assert ('The prop[%d] property of parent must be in the range [0, 1]' % first_invalid_ind 110 | in str(validation_failure.value)) 111 | 112 | 113 | # Number3 Tests (free_length=True) 114 | # -------------------------------- 115 | # ### Acceptance ### 116 | @pytest.mark.parametrize('val', [ 117 | [1, 0, 0.5], (0.1, 0.99), [0], [] 118 | ]) 119 | def test_validator_acceptance_number3_free(val, validator_number3_free: InfoArrayValidator): 120 | coerce_val = validator_number3_free.validate_coerce(val) 121 | assert np.array_equal(coerce_val, tuple(val)) 122 | 123 | 124 | # ### Rejection by type ### 125 | @pytest.mark.parametrize('val', [ 126 | 'Not a list', 123, set(), {} 127 | ]) 128 | def test_validator_rejection_any2_type(val, validator_number3_free: InfoArrayValidator): 129 | with pytest.raises(ValueError) as validation_failure: 130 | validator_number3_free.validate_coerce(val) 131 | 132 | assert 'Invalid value' in str(validation_failure.value) 133 | 134 | 135 | # ### Rejection by length ### 136 | @pytest.mark.parametrize('val', [ 137 | (0.1, 0.4, 0.99, 0.4), [1, 0, 0, 0, 0, 0, 0] 138 | ]) 139 | def test_validator_rejection_number3_free_length(val, validator_number3_free: InfoArrayValidator): 140 | with pytest.raises(ValueError) as validation_failure: 141 | validator_number3_free.validate_coerce(val) 142 | 143 | assert 'Invalid value' in str(validation_failure.value) 144 | 145 | 146 | # ### Rejection by element type ### 147 | @pytest.mark.parametrize('val,first_invalid_ind', [ 148 | ([1, 0, '0.5'], 2), 149 | ((0.1, set()), 1), 150 | ([[]], 0) 151 | ]) 152 | def test_validator_rejection_number3_length(val, first_invalid_ind, validator_number3_free: InfoArrayValidator): 153 | with pytest.raises(ValueError) as validation_failure: 154 | validator_number3_free.validate_coerce(val) 155 | 156 | assert ("Invalid value of type {typ} received for the 'prop[{first_invalid_ind}]' property of parent" 157 | .format(typ= type_str(val[first_invalid_ind]), 158 | first_invalid_ind=first_invalid_ind)) in str(validation_failure.value) 159 | -------------------------------------------------------------------------------- /test/validators/test_integer_validator.py: -------------------------------------------------------------------------------- 1 | # Array not ok 2 | # ------------ 3 | import pytest 4 | from pytest import approx 5 | from ipyplotly.basevalidators import IntegerValidator 6 | import numpy as np 7 | 8 | 9 | # ### Fixtures ### 10 | @pytest.fixture() 11 | def validator(): 12 | return IntegerValidator('prop', 'parent') 13 | 14 | 15 | @pytest.fixture 16 | def validator_min_max(): 17 | return IntegerValidator('prop', 'parent', min=-1, max=2) 18 | 19 | 20 | @pytest.fixture 21 | def validator_min(): 22 | return IntegerValidator('prop', 'parent', min=-1) 23 | 24 | 25 | @pytest.fixture 26 | def validator_max(): 27 | return IntegerValidator('prop', 'parent', max=2) 28 | 29 | 30 | @pytest.fixture 31 | def validator_aok(request): 32 | return IntegerValidator('prop', 'parent', min=-2, max=10, array_ok=True) 33 | 34 | 35 | # ### Acceptance ### 36 | @pytest.mark.parametrize('val', 37 | [1, -19, 0, -1234]) 38 | def test_acceptance(val, validator: IntegerValidator): 39 | assert validator.validate_coerce(val) == val 40 | 41 | 42 | # ### Coercion ### 43 | @pytest.mark.parametrize('val,expected', 44 | [(1.0, 1), (-19.1, -19), (0.001, 0), (1234.9, 1234)]) 45 | def test_coercion(val, expected, validator: IntegerValidator): 46 | assert validator.validate_coerce(val) == expected 47 | 48 | 49 | # ### Rejection by value ### 50 | @pytest.mark.parametrize('val', 51 | ['hello', (), [], [1, 2, 3], set(), '34', np.nan, np.inf, -np.inf]) 52 | def test_rejection_by_value(val, validator: IntegerValidator): 53 | with pytest.raises(ValueError) as validation_failure: 54 | validator.validate_coerce(val) 55 | 56 | assert 'Invalid value' in str(validation_failure.value) 57 | 58 | 59 | # ### With min/max ### 60 | # min == -1 and max == 2 61 | @pytest.mark.parametrize('val', 62 | [0, 1, -1, 2]) 63 | def test_acceptance_min_max(val, validator_min_max: IntegerValidator): 64 | assert validator_min_max.validate_coerce(val) == approx(val) 65 | 66 | 67 | @pytest.mark.parametrize('val', 68 | [-1.01, -10, 2.1, 3, np.iinfo(np.int).max, np.iinfo(np.int).min]) 69 | def test_rejection_min_max(val, validator_min_max: IntegerValidator): 70 | with pytest.raises(ValueError) as validation_failure: 71 | validator_min_max.validate_coerce(val) 72 | 73 | assert 'in the interval [-1, 2]' in str(validation_failure.value) 74 | 75 | 76 | # ### With min only ### 77 | # min == -1 78 | @pytest.mark.parametrize('val', 79 | [-1, 0, 1, 23, 99999]) 80 | def test_acceptance_min(val, validator_min: IntegerValidator): 81 | assert validator_min.validate_coerce(val) == approx(val) 82 | 83 | 84 | @pytest.mark.parametrize('val', 85 | [-2, -123, np.iinfo(np.int).min]) 86 | def test_rejection_min(val, validator_min: IntegerValidator): 87 | with pytest.raises(ValueError) as validation_failure: 88 | validator_min.validate_coerce(val) 89 | 90 | assert 'in the interval [-1, 2147483647]' in str(validation_failure.value) 91 | 92 | 93 | # ### With max only ### 94 | # max == 2 95 | @pytest.mark.parametrize('val', 96 | [1, 2, -10, -999999, np.iinfo(np.int32).min]) 97 | def test_acceptance_max(val, validator_max: IntegerValidator): 98 | assert validator_max.validate_coerce(val) == approx(val) 99 | 100 | 101 | @pytest.mark.parametrize('val', 102 | [3, 10, np.iinfo(np.int32).max]) 103 | def test_rejection_max(val, validator_max: IntegerValidator): 104 | with pytest.raises(ValueError) as validation_failure: 105 | validator_max.validate_coerce(val) 106 | 107 | assert 'in the interval [-2147483648, 2]' in str(validation_failure.value) 108 | 109 | 110 | # Array ok 111 | # -------- 112 | # min=-2 and max=10 113 | # ### Acceptance ### 114 | @pytest.mark.parametrize('val', 115 | [-2, 1, 0, 1, 10]) 116 | def test_acceptance_aok_scalars(val, validator_aok: IntegerValidator): 117 | assert validator_aok.validate_coerce(val) == val 118 | 119 | 120 | @pytest.mark.parametrize('val', 121 | [[1, 0], [1], [-2, 1, 8], np.array([3, 2, -1, 5])]) 122 | def test_acceptance_aok_list(val, validator_aok: IntegerValidator): 123 | assert np.array_equal(validator_aok.validate_coerce(val), val) 124 | 125 | 126 | # ### Coerce ### 127 | # Coerced to general consistent numeric type 128 | @pytest.mark.parametrize('val,expected', 129 | [([1.0, 0], [1, 0]), 130 | (np.array([1.1, -1]), [1, -1]), 131 | ([-1.9, 0, 5.1], [-1, 0, 5]), 132 | (np.array([1, 0], dtype=np.int64), [1, 0])]) 133 | def test_coercion_aok_list(val, expected, validator_aok: IntegerValidator): 134 | v = validator_aok.validate_coerce(val) 135 | assert v.dtype == np.int32 136 | assert np.array_equal(v, np.array(expected, dtype=np.int32)) 137 | 138 | 139 | # ### Rejection ### 140 | # 141 | @pytest.mark.parametrize('val', 142 | [['a', 4], [[], 3, 4]]) 143 | def test_integer_validator_rejection_aok(val, validator_aok: IntegerValidator): 144 | with pytest.raises(ValueError) as validation_failure: 145 | validator_aok.validate_coerce(val) 146 | 147 | assert 'Invalid value' in str(validation_failure.value) 148 | 149 | 150 | # ### Rejection by element ### 151 | @pytest.mark.parametrize('val', 152 | [[-1, 11], [1.5, -3], [0, np.iinfo(np.int32).max], [0, np.iinfo(np.int32).min]]) 153 | def test_rejection_aok_min_max(val, validator_aok: IntegerValidator): 154 | with pytest.raises(ValueError) as validation_failure: 155 | validator_aok.validate_coerce(val) 156 | 157 | assert 'in the interval [-2, 10]' in str(validation_failure.value) 158 | -------------------------------------------------------------------------------- /test/validators/test_number_validator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pytest import approx 3 | 4 | from ipyplotly.basevalidators import NumberValidator 5 | import numpy as np 6 | 7 | 8 | # Fixtures 9 | # -------- 10 | @pytest.fixture 11 | def validator(request): 12 | return NumberValidator('prop', 'parent') 13 | 14 | 15 | @pytest.fixture 16 | def validator_min_max(request): 17 | return NumberValidator('prop', 'parent', min=-1.0, max=2.0) 18 | 19 | 20 | @pytest.fixture 21 | def validator_min(request): 22 | return NumberValidator('prop', 'parent', min=-1.0) 23 | 24 | 25 | @pytest.fixture 26 | def validator_max(request): 27 | return NumberValidator('prop', 'parent', max=2.0) 28 | 29 | 30 | @pytest.fixture 31 | def validator_aok(): 32 | return NumberValidator('prop', 'parent', min=-1, max=1.5, array_ok=True) 33 | 34 | 35 | # Array not ok 36 | # ------------ 37 | # ### Acceptance ### 38 | @pytest.mark.parametrize('val', 39 | [1.0, 0.0, 1, -1234.5678, 54321, np.pi, np.nan, np.inf, -np.inf]) 40 | def test_acceptance(val, validator: NumberValidator): 41 | assert validator.validate_coerce(val) == approx(val, nan_ok=True) 42 | 43 | 44 | # ### Rejection by value ### 45 | @pytest.mark.parametrize('val', 46 | ['hello', (), [], [1, 2, 3], set(), '34']) 47 | def test_rejection_by_value(val, validator: NumberValidator): 48 | with pytest.raises(ValueError) as validation_failure: 49 | validator.validate_coerce(val) 50 | 51 | assert 'Invalid value' in str(validation_failure.value) 52 | 53 | 54 | # ### With min/max ### 55 | @pytest.mark.parametrize('val', 56 | [0, 0.0, -0.5, 1, 1.0, 2, 2.0, np.pi/2.0]) 57 | def test_acceptance_min_max(val, validator_min_max: NumberValidator): 58 | assert validator_min_max.validate_coerce(val) == approx(val) 59 | 60 | 61 | @pytest.mark.parametrize('val', 62 | [-1.01, -10, 2.1, 234, -np.inf, np.nan, np.inf]) 63 | def test_rejection_min_max(val, validator_min_max: NumberValidator): 64 | with pytest.raises(ValueError) as validation_failure: 65 | validator_min_max.validate_coerce(val) 66 | 67 | assert 'in the interval [-1.0, 2.0]' in str(validation_failure.value) 68 | 69 | 70 | # ### With min only ### 71 | @pytest.mark.parametrize('val', 72 | [0, 0.0, -0.5, 99999, np.inf]) 73 | def test_acceptance_min(val, validator_min: NumberValidator): 74 | assert validator_min.validate_coerce(val) == approx(val) 75 | 76 | 77 | @pytest.mark.parametrize('val', 78 | [-1.01, -np.inf, np.nan]) 79 | def test_rejection_min(val, validator_min: NumberValidator): 80 | with pytest.raises(ValueError) as validation_failure: 81 | validator_min.validate_coerce(val) 82 | 83 | assert 'in the interval [-1.0, inf]' in str(validation_failure.value) 84 | 85 | 86 | # ### With max only ### 87 | @pytest.mark.parametrize('val', 88 | [0, 0.0, -np.inf, -123456, np.pi/2]) 89 | def test_acceptance_max(val, validator_max: NumberValidator): 90 | assert validator_max.validate_coerce(val) == approx(val) 91 | 92 | 93 | @pytest.mark.parametrize('val', 94 | [2.01, np.inf, np.nan]) 95 | def test_rejection_max(val, validator_max: NumberValidator): 96 | with pytest.raises(ValueError) as validation_failure: 97 | validator_max.validate_coerce(val) 98 | 99 | assert 'in the interval [-inf, 2.0]' in str(validation_failure.value) 100 | 101 | 102 | # Array ok 103 | # -------- 104 | # ### Acceptance ### 105 | @pytest.mark.parametrize('val', 106 | [1.0, 0.0, 1, 0.4]) 107 | def test_acceptance_aok_scalars(val, validator_aok: NumberValidator): 108 | assert validator_aok.validate_coerce(val) == val 109 | 110 | 111 | @pytest.mark.parametrize('val', 112 | [[1.0, 0.0], [1], [-0.1234, .41, -1.0]]) 113 | def test_acceptance_aok_list(val, validator_aok: NumberValidator): 114 | assert np.array_equal(validator_aok.validate_coerce(val), np.array(val, dtype='float')) 115 | 116 | 117 | # ### Coerce ### 118 | # Coerced to general consistent numeric type 119 | @pytest.mark.parametrize('val,expected', 120 | [([1.0, 0], np.array([1.0, 0.0])), 121 | (np.array([1, -1]), np.array([1.0, -1.0])), 122 | ([-0.1234, 0, -1], np.array([-0.1234, 0.0, -1.0]))]) 123 | def test_coercion_aok_list(val, expected, validator_aok: NumberValidator): 124 | v = validator_aok.validate_coerce(val) 125 | assert isinstance(v, np.ndarray) 126 | assert v.dtype == 'float' 127 | assert np.array_equal(v, expected) 128 | 129 | 130 | # ### Rejection ### 131 | # 132 | @pytest.mark.parametrize('val', 133 | [['a', 4]]) 134 | def test_rejection_aok(val, validator_aok: NumberValidator): 135 | with pytest.raises(ValueError) as validation_failure: 136 | validator_aok.validate_coerce(val) 137 | 138 | assert 'Invalid value' in str(validation_failure.value) 139 | 140 | 141 | # ### Rejection by element ### 142 | @pytest.mark.parametrize('val', 143 | [[-1.6, 0.0], [1, 1.5, 2], [-0.1234, .41, np.nan], 144 | [0, np.inf], [0, -np.inf]]) 145 | def test_rejection_aok_min_max(val, validator_aok: NumberValidator): 146 | with pytest.raises(ValueError) as validation_failure: 147 | validator_aok.validate_coerce(val) 148 | 149 | assert 'Invalid element(s)' in str(validation_failure.value) 150 | assert 'in the interval [-1, 1.5]' in str(validation_failure.value) 151 | -------------------------------------------------------------------------------- /test/validators/test_string_validator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ipyplotly.basevalidators import StringValidator 3 | import numpy as np 4 | 5 | 6 | # Fixtures 7 | # -------- 8 | @pytest.fixture() 9 | def validator(): 10 | return StringValidator('prop', 'parent') 11 | 12 | 13 | @pytest.fixture() 14 | def validator_values(): 15 | return StringValidator('prop', 'parent', values=['foo', 'BAR', '']) 16 | 17 | 18 | @pytest.fixture() 19 | def validator_no_blanks(): 20 | return StringValidator('prop', 'parent', no_blank=True) 21 | 22 | 23 | @pytest.fixture 24 | def validator_aok(): 25 | return StringValidator('prop', 'parent', array_ok=True) 26 | 27 | 28 | @pytest.fixture 29 | def validator_aok_values(): 30 | return StringValidator('prop', 'parent', values=['foo', 'BAR', '', 'baz'], array_ok=True) 31 | 32 | 33 | @pytest.fixture() 34 | def validator_no_blanks_aok(): 35 | return StringValidator('prop', 'parent', no_blank=True, array_ok=True) 36 | 37 | 38 | # Array not ok 39 | # ------------ 40 | # ### Acceptance ### 41 | @pytest.mark.parametrize('val', 42 | ['bar', 'HELLO!!!', 'world!@#$%^&*()', '']) 43 | def test_acceptance(val, validator: StringValidator): 44 | assert validator.validate_coerce(val) == val 45 | 46 | 47 | # ### Rejection by value ### 48 | @pytest.mark.parametrize('val', 49 | [(), [], [1, 2, 3], set(), np.nan, np.pi]) 50 | def test_rejection(val, validator: StringValidator): 51 | with pytest.raises(ValueError) as validation_failure: 52 | validator.validate_coerce(val) 53 | 54 | assert 'Invalid value' in str(validation_failure.value) 55 | 56 | 57 | # Valid values 58 | # ------------ 59 | @pytest.mark.parametrize('val', 60 | ['foo', 'BAR', '']) 61 | def test_acceptance_values(val, validator_values: StringValidator): 62 | assert validator_values.validate_coerce(val) == val 63 | 64 | 65 | @pytest.mark.parametrize('val', 66 | ['FOO', 'bar', 'other', '1234']) 67 | def test_rejection_values(val, validator_values: StringValidator): 68 | with pytest.raises(ValueError) as validation_failure: 69 | validator_values.validate_coerce(val) 70 | 71 | assert 'Invalid value'.format(val=val) in str(validation_failure.value) 72 | assert "['foo', 'BAR', '']" in str(validation_failure.value) 73 | 74 | 75 | # ### No blanks ### 76 | @pytest.mark.parametrize('val', 77 | ['bar', 'HELLO!!!', 'world!@#$%^&*()']) 78 | def test_acceptance_no_blanks(val, validator_no_blanks: StringValidator): 79 | assert validator_no_blanks.validate_coerce(val) == val 80 | 81 | 82 | @pytest.mark.parametrize('val', 83 | ['']) 84 | def test_rejection_no_blanks(val, validator_no_blanks: StringValidator): 85 | with pytest.raises(ValueError) as validation_failure: 86 | validator_no_blanks.validate_coerce(val) 87 | 88 | assert 'A non-empty string' in str(validation_failure.value) 89 | 90 | 91 | # Array ok 92 | # -------- 93 | # ### Acceptance ### 94 | @pytest.mark.parametrize('val', 95 | ['foo', 'BAR', '', 'baz']) 96 | def test_acceptance_aok_scalars(val, validator_aok: StringValidator): 97 | assert validator_aok.validate_coerce(val) == val 98 | 99 | 100 | @pytest.mark.parametrize('val', 101 | ['foo', ['foo'], np.array(['BAR', ''], dtype='object'), ['baz', 'baz', 'baz']]) 102 | def test_acceptance_aok_list(val, validator_aok: StringValidator): 103 | coerce_val = validator_aok.validate_coerce(val) 104 | if isinstance(val, (list, np.ndarray)): 105 | assert np.array_equal(coerce_val, np.array(val, dtype=coerce_val.dtype)) 106 | else: 107 | assert coerce_val == val 108 | 109 | 110 | # ### Rejection by type ### 111 | @pytest.mark.parametrize('val', 112 | [['foo', ()], ['foo', 3, 4], [3, 2, 1]]) 113 | def test_rejection_aok(val, validator_aok: StringValidator): 114 | with pytest.raises(ValueError) as validation_failure: 115 | validator_aok.validate_coerce(val) 116 | 117 | assert 'Invalid element(s)' in str(validation_failure.value) 118 | 119 | 120 | # ### Rejection by value ### 121 | @pytest.mark.parametrize('val', 122 | [['foo', 'bar'], ['3', '4'], ['BAR', 'BAR', 'hello!']]) 123 | def test_rejection_aok_values(val, validator_aok_values: StringValidator): 124 | with pytest.raises(ValueError) as validation_failure: 125 | validator_aok_values.validate_coerce(val) 126 | 127 | assert 'Invalid element(s)' in str(validation_failure.value) 128 | 129 | 130 | # ### No blanks ### 131 | @pytest.mark.parametrize('val', 132 | ['123', ['bar', 'HELLO!!!'], ['world!@#$%^&*()']]) 133 | def test_acceptance_no_blanks_aok(val, validator_no_blanks_aok: StringValidator): 134 | coerce_val = validator_no_blanks_aok.validate_coerce(val) 135 | if isinstance(val, (list, np.ndarray)): 136 | assert np.array_equal(coerce_val, np.array(val, dtype=coerce_val.dtype)) 137 | else: 138 | assert coerce_val == val 139 | 140 | 141 | @pytest.mark.parametrize('val', 142 | ['', ['foo', 'bar', ''], ['']]) 143 | def test_rejection_no_blanks_aok(val, validator_no_blanks_aok: StringValidator): 144 | with pytest.raises(ValueError) as validation_failure: 145 | validator_no_blanks_aok.validate_coerce(val) 146 | 147 | assert 'A non-empty string' in str(validation_failure.value) 148 | 149 | -------------------------------------------------------------------------------- /test/validators/test_subplotid_validator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ipyplotly.basevalidators import SubplotidValidator 3 | import numpy as np 4 | 5 | 6 | # Fixtures 7 | # -------- 8 | @pytest.fixture() 9 | def validator(): 10 | return SubplotidValidator('prop', 'parent', dflt='geo') 11 | 12 | 13 | # Tests 14 | # ----- 15 | # ### Acceptance ### 16 | @pytest.mark.parametrize('val', ['geo'] + ['geo%d' % i for i in range(2, 10)]) 17 | def test_acceptance(val, validator: SubplotidValidator): 18 | assert validator.validate_coerce(val) == val 19 | 20 | 21 | # ### Rejection by type ### 22 | @pytest.mark.parametrize('val', [ 23 | 23, [], {}, set(), np.inf, np.nan 24 | ]) 25 | def test_rejection_type(val, validator: SubplotidValidator): 26 | with pytest.raises(ValueError) as validation_failure: 27 | validator.validate_coerce(val) 28 | 29 | assert 'Invalid value' in str(validation_failure.value) 30 | 31 | 32 | # ### Rejection by value ### 33 | @pytest.mark.parametrize('val', [ 34 | '', # Cannot be empty 35 | 'bogus', # Must begin with 'geo' 36 | 'geo0', 'geo1' # If followed by a number the number must be > 1 37 | ]) 38 | def test_rejection_value(val, validator: SubplotidValidator): 39 | with pytest.raises(ValueError) as validation_failure: 40 | validator.validate_coerce(val) 41 | 42 | assert "Invalid value" in str(validation_failure.value) 43 | -------------------------------------------------------------------------------- /test/validators/test_validators_common.py: -------------------------------------------------------------------------------- 1 | 2 | # # ### Accept None ### 3 | # def test_accept_none(validator: NumberValidator): 4 | # assert validator.validate_coerce(None) is None 5 | 6 | 7 | # Test numpy arrays readonly 8 | --------------------------------------------------------------------------------